Merge pull request #7808 from theseyan/ifctester-improvements-rebased

IfcTester webapp improvements
This commit is contained in:
Sayan J. Das
2026-03-23 15:48:34 +05:30
committed by GitHub
89 changed files with 2090 additions and 830 deletions
+1
View File
@@ -14,6 +14,7 @@
/src/ifcmax/out/ /src/ifcmax/out/
/src/ifcwrap/out/ /src/ifcwrap/out/
/src/qtviewer/out/ /src/qtviewer/out/
/src/ifctester/webapp/public/pyodide/
/win/BuildDepsCache*.txt /win/BuildDepsCache*.txt
+1
View File
@@ -28,6 +28,7 @@ dist:
mkdir -p dist mkdir -p dist
cp -r $(PACKAGE_NAME) build/ cp -r $(PACKAGE_NAME) build/
cp pyproject.toml build/ cp pyproject.toml build/
if [ -f README.md ]; then cp README.md build/; fi
ifeq ($(IS_STABLE), TRUE) ifeq ($(IS_STABLE), TRUE)
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml $(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
ifdef IS_MODULE ifdef IS_MODULE
+63 -10
View File
@@ -25,9 +25,19 @@ WEBAPP_BUILD_DIR := $(WEBAPP_DIR)/dist
PYODIDE_DIR := $(WEBAPP_DIR)/public/pyodide PYODIDE_DIR := $(WEBAPP_DIR)/public/pyodide
PYODIDE_VERSION := 0.28.0 PYODIDE_VERSION := 0.28.0
PYODIDE_URL := https://github.com/pyodide/pyodide/releases/download/$(PYODIDE_VERSION)/pyodide-$(PYODIDE_VERSION).tar.bz2 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 .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 cd $(WEBAPP_DIR) && npm run dev
.PHONY: pyodide-download .PHONY: pyodide-download
@@ -62,8 +72,43 @@ pyodide-download:
echo "Pyodide $(PYODIDE_VERSION) prepared in $(PYODIDE_DIR)"; \ echo "Pyodide $(PYODIDE_VERSION) prepared in $(PYODIDE_DIR)"; \
fi 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 .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 install
cd $(WEBAPP_DIR) && npm run build cd $(WEBAPP_DIR) && npm run build
@@ -76,22 +121,30 @@ clean:
rm -rf $(WEBAPP_BUILD_DIR) rm -rf $(WEBAPP_BUILD_DIR)
rm -rf $(WEBAPP_DIR)/node_modules rm -rf $(WEBAPP_DIR)/node_modules
rm -rf $(PYODIDE_DIR) 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 $(PACKAGE_NAME)/webapp
rm -rf dist rm -rf dist
.PHONY: dist .PHONY: python-dist
dist: webapp-prepare python-dist:
rm -rf dist
# For some reason OS is not initalized when we call common.mk dist, which matters on Windows. # For some reason OS is not initalized when we call common.mk dist, which matters on Windows.
# So we pass it explicitly. # 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 .PHONY: webapp-prepare
webapp-prepare: webapp-build webapp-prepare: webapp-build
rm -rf $(PACKAGE_NAME)/webapp/www/* rm -rf $(PACKAGE_WEBAPP_WWW_DIR)
mkdir -p $(PACKAGE_NAME)/webapp/www mkdir -p $(PACKAGE_WEBAPP_WWW_DIR)
cp -r $(WEBAPP_BUILD_DIR)/* $(PACKAGE_NAME)/webapp/www/ cp -r $(WEBAPP_BUILD_DIR)/* $(PACKAGE_WEBAPP_WWW_DIR)/
cp $(WEBAPP_DIR)/__init__.py $(PACKAGE_NAME)/webapp/__init__.py cp $(WEBAPP_DIR)/__init__.py $(PACKAGE_WEBAPP_DIR)/__init__.py
cp $(WEBAPP_DIR)/serve.py $(PACKAGE_NAME)/webapp/serve.py cp $(WEBAPP_DIR)/serve.py $(PACKAGE_WEBAPP_DIR)/serve.py
.PHONY: test .PHONY: test
test: test:
+3 -1
View File
@@ -11,6 +11,8 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
public/worker/bin/ifcopenshell-*.whl
public/worker/generated
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
@@ -24,4 +26,4 @@ dist-ssr
*.sw? *.sw?
.claude .claude
experiment/* experiment/*
+31
View File
@@ -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"
}
}
}
}
]
}
+1 -1
View File
@@ -9,6 +9,6 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
-39
View File
@@ -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"]
}
+258
View File
@@ -16,6 +16,7 @@
"svelte-spa-router": "^4.0.1" "svelte-spa-router": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4",
"@internationalized/date": "^3.8.1", "@internationalized/date": "^3.8.1",
"@lucide/svelte": "^0.515.0", "@lucide/svelte": "^0.515.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
@@ -25,11 +26,13 @@
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
"sass-embedded": "^1.89.0", "sass-embedded": "^1.89.0",
"svelte": "^5.53.6", "svelte": "^5.53.6",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.5", "svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.3.0", "tailwind-merge": "^3.3.0",
"tailwind-variants": "^1.0.0", "tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"tw-animate-css": "^1.3.2", "tw-animate-css": "^1.3.2",
"typescript": "^5.8.3",
"vite": "^6.4.1" "vite": "^6.4.1"
} }
}, },
@@ -47,6 +50,170 @@
"node": ">=6.0.0" "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": { "node_modules/@bufbuild/protobuf": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.5.1.tgz", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.5.1.tgz",
@@ -1468,6 +1635,22 @@
"dev": true, "dev": true,
"license": "MIT/X11" "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": { "node_modules/chownr": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -2126,6 +2309,16 @@
"svelte": "^5.7.0" "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": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2200,6 +2393,20 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/regexparam": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz", "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz",
@@ -2281,6 +2488,19 @@
"tslib": "^2.1.0" "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": { "node_modules/sass-embedded": {
"version": "1.89.0", "version": "1.89.0",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.0.tgz", "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.0.tgz",
@@ -2783,6 +3003,30 @@
"node": ">=18" "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": { "node_modules/svelte-sonner": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.5.tgz", "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.5.tgz",
@@ -2983,6 +3227,20 @@
"url": "https://github.com/sponsors/Wombosvideo" "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": { "node_modules/uuid": {
"version": "8.3.2", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+6
View File
@@ -6,11 +6,15 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"check": "tsc -p tsconfig.json --noEmit && svelte-check && biome lint .",
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
"preview": "vite preview", "preview": "vite preview",
"deploy": "npm run build && npx wrangler pages deploy dist" "deploy": "npm run build && npx wrangler pages deploy dist"
}, },
"devDependencies": { "devDependencies": {
"@internationalized/date": "^3.8.1", "@internationalized/date": "^3.8.1",
"@biomejs/biome": "^1.9.4",
"@lucide/svelte": "^0.515.0", "@lucide/svelte": "^0.515.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
@@ -19,10 +23,12 @@
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
"sass-embedded": "^1.89.0", "sass-embedded": "^1.89.0",
"svelte": "^5.53.6", "svelte": "^5.53.6",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.5", "svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.3.0", "tailwind-merge": "^3.3.0",
"tailwind-variants": "^1.0.0", "tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"typescript": "^5.8.3",
"tw-animate-css": "^1.3.2", "tw-animate-css": "^1.3.2",
"vite": "^6.4.1" "vite": "^6.4.1"
}, },
-3
View File
@@ -22,9 +22,6 @@ import os
import sys import sys
bonsai_lib_path = os.environ.get("BONSAI_LIB_PATH") 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: if bonsai_lib_path:
sys.path.insert(0, bonsai_lib_path) sys.path.insert(0, bonsai_lib_path)
+1 -1
View File
@@ -1,4 +1,4 @@
<script> <script lang="ts">
import Router from 'svelte-spa-router'; import Router from 'svelte-spa-router';
import routes from './routes'; import routes from './routes';
</script> </script>
+4
View File
@@ -0,0 +1,4 @@
declare module "*.svelte" {
import type { SvelteComponent } from "svelte";
export default class Component extends SvelteComponent {}
}
@@ -1,11 +1,11 @@
<script> <script lang="ts">
import * as Menubar from "$lib/components/ui/menubar"; import * as Menubar from "$lib/components/ui/menubar";
import * as Dialog from "$lib/components/ui/dialog"; import * as Dialog from "$lib/components/ui/dialog";
import * as IDS from "$src/modules/api/ids.svelte.js"; import * as IDS from "$src/modules/api/ids.svelte";
import * as API from "$src/modules/api/api.svelte.js"; import * as API from "$src/modules/api/api.svelte";
import { error, success, info } from "$src/modules/utils/toast.svelte.js"; import { error, success } from "$src/modules/utils/toast.svelte";
let { isOpen = false } = $props(); let { isOpen = false } : { isOpen?: boolean } = $props();
function openForum() { function openForum() {
window.open('https://community.osarch.org', '_blank'); window.open('https://community.osarch.org', '_blank');
@@ -23,8 +23,9 @@
try { try {
await IDS.openDocument(); await IDS.openDocument();
} catch (err) { } catch (err) {
if (err.message !== 'File selection cancelled') { const message = err instanceof Error ? err.message : String(err);
error('Error opening file: ' + err.message); if (message !== 'File selection cancelled') {
error(`Error opening file: ${message}`);
console.error(err); console.error(err);
} }
} }
@@ -50,7 +51,7 @@
success('Audit completed successfully'); success('Audit completed successfully');
} catch (err) { } catch (err) {
console.error("Audit failed: ", err); console.error("Audit failed: ", err);
error(`Audit failed: check console for details`); error("Audit failed: check console for details");
} }
} }
</script> </script>
@@ -142,4 +143,4 @@
</Dialog.Close> </Dialog.Close>
</Dialog.Footer> </Dialog.Footer>
</Dialog.Content> </Dialog.Content>
</Dialog.Root> </Dialog.Root>
@@ -1,6 +1,5 @@
<script> <script lang="ts">
import { onMount } from "svelte"; import { Module } from "$src/modules/api/ids.svelte";
import { Module } from "$src/modules/api/ids.svelte.js";
</script> </script>
<div class="app-ribbon"> <div class="app-ribbon">
@@ -20,4 +19,4 @@
<span>Error</span> <span>Error</span>
</div> </div>
{/if} {/if}
</div> </div>
@@ -1,14 +1,16 @@
<script> <script lang="ts">
import * as Tooltip from "$lib/components/ui/tooltip"; import * as Tooltip from "$lib/components/ui/tooltip";
import { IFCModels, loadIfc, unloadIfc, auditIfc, openIfc, createAuditReport, clearIdsAuditReports, runAudit } from "$src/modules/api/api.svelte.js"; import { IFCModels, openIfc, unloadIfc, runAudit } from "$src/modules/api/api.svelte";
import * as IDS from "$src/modules/api/ids.svelte.js"; import * as IDS from "$src/modules/api/ids.svelte";
import { error, success } from "$src/modules/utils/toast.svelte.js"; import { error, success } from "$src/modules/utils/toast.svelte";
import { ChevronRightIcon, LinkIcon, XIcon } from "@lucide/svelte"; import { ChevronRightIcon, LinkIcon, XIcon } from "@lucide/svelte";
import { Bonsai, connect, disconnect, runAudit as runBonsaiAudit } from "$src/modules/api/bonsai.svelte.js"; import { Bonsai, connect, disconnect, runAudit as runBonsaiAudit } from "$src/modules/api/bonsai.svelte";
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { AuditReport } from "$src/types/report";
let isAuditing = $state(false); let isAuditing = $state(false);
let activeTab = $state('home'); let activeTab = $state<"home" | "bonsai">('home');
// biome-ignore lint/style/useConst: Svelte state uses assignment for updates.
let isMinimized = $state(false); let isMinimized = $state(false);
const handleLoadModel = async () => { const handleLoadModel = async () => {
@@ -16,15 +18,17 @@
await openIfc(); await openIfc();
success('IFC model loaded successfully'); success('IFC model loaded successfully');
} catch (err) { } catch (err) {
error(`Failed to load IFC model: ${err.message}`); const message = err instanceof Error ? err.message : String(err);
error(`Failed to load IFC model: ${message}`);
} }
}; };
const handleUnloadModel = async (modelId) => { const handleUnloadModel = async (modelId: string) => {
try { try {
await unloadIfc(modelId); await unloadIfc(modelId);
} catch (err) { } catch (err) {
error(`Failed to unload model: ${err.message}`); const message = err instanceof Error ? err.message : String(err);
error(`Failed to unload model: ${message}`);
} }
}; };
@@ -35,14 +39,14 @@
success('Audit completed successfully'); success('Audit completed successfully');
} catch (err) { } catch (err) {
console.error("Audit failed: ", err); console.error("Audit failed: ", err);
error(`Audit failed: check console for details`); error("Audit failed: check console for details");
} finally { } finally {
isAuditing = false; isAuditing = false;
} }
}; };
const handleViewAuditReport = (auditId) => { const handleViewAuditReport = (auditId: string) => {
const auditReport = IFCModels.audits.find(audit => audit.id === auditId); const auditReport = IFCModels.audits.find(audit => audit.id === auditId) as AuditReport | undefined;
if (!auditReport) return; if (!auditReport) return;
// Switch to the IDS document that was used for this audit // Switch to the IDS document that was used for this audit
@@ -58,7 +62,7 @@
} }
}; };
const formatFileSize = (bytes) => { const formatFileSize = (bytes: number) => {
const units = ['B', 'KB', 'MB', 'GB']; const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes; let size = bytes;
let unitIndex = 0; let unitIndex = 0;
@@ -91,7 +95,7 @@
<div class="buttons"> <div class="buttons">
<Tooltip.Provider> <Tooltip.Provider>
{#if isMinimized} {#if isMinimized}
<Tooltip.Root disableHoverableContent="true"> <Tooltip.Root disableHoverableContent>
<Tooltip.Trigger> <Tooltip.Trigger>
<button class="tb-btn expand-btn" onclick={() => isMinimized = false} aria-label="Expand Toolbar"> <button class="tb-btn expand-btn" onclick={() => isMinimized = false} aria-label="Expand Toolbar">
<ChevronRightIcon size={24} /> <ChevronRightIcon size={24} />
@@ -102,7 +106,7 @@
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
{/if} {/if}
<Tooltip.Root disableHoverableContent="true"> <Tooltip.Root disableHoverableContent>
<Tooltip.Trigger> <Tooltip.Trigger>
<button class="tb-btn {activeTab === 'home' ? 'active' : ''}" onclick={() => activeTab = 'home'} aria-label="Home"> <button class="tb-btn {activeTab === 'home' ? 'active' : ''}" onclick={() => activeTab = 'home'} aria-label="Home">
<svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
@@ -114,7 +118,7 @@
<p>Home</p> <p>Home</p>
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
<Tooltip.Root disableHoverableContent="true"> <Tooltip.Root disableHoverableContent>
<Tooltip.Trigger> <Tooltip.Trigger>
<button class="tb-btn {activeTab === 'bonsai' ? 'active' : ''}" onclick={() => activeTab = 'bonsai'} aria-label="Bonsai Integration"> <button class="tb-btn {activeTab === 'bonsai' ? 'active' : ''}" onclick={() => activeTab = 'bonsai'} aria-label="Bonsai Integration">
<svg style="height: 20px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32mm" height="32mm" version="1.1" viewBox="0 0 32 32" xml:space="preserve"> <svg style="height: 20px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32mm" height="32mm" version="1.1" viewBox="0 0 32 32" xml:space="preserve">
@@ -692,4 +696,4 @@
opacity: 0.5; opacity: 0.5;
} }
} }
</style> </style>
@@ -1,7 +1,9 @@
<script> <script lang="ts">
import * as DropdownMenu from "$lib/components/ui/dropdown-menu"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
let { addFacet } = $props(); type FacetType = "entity" | "attribute" | "classification" | "partOf" | "property" | "material";
const { addFacet } : { addFacet: (facetType: FacetType) => void } = $props();
</script> </script>
<DropdownMenu.Root> <DropdownMenu.Root>
@@ -63,4 +65,4 @@
Part Of Part Of
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
@@ -1,13 +1,22 @@
<script> <script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte.js"; import * as IDS from "$src/modules/api/ids.svelte";
import type { IdsDocument } from "$src/types/ids";
function switchDocument(docId) { function switchDocument(docId: string) {
IDS.Module.activeDocument = docId; IDS.Module.activeDocument = docId;
} }
function closeDocument(docId) { function closeDocument(docId: string) {
IDS.deleteDocument(docId); IDS.deleteDocument(docId);
} }
const handleActivation = (event: KeyboardEvent, action: () => void) => {
if (event.currentTarget !== event.target) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
action();
}
};
</script> </script>
<div class="ids-tabs"> <div class="ids-tabs">
@@ -15,10 +24,13 @@
<div <div
class="ids-tab" class="ids-tab"
class:active={IDS.Module.activeDocument === docId} class:active={IDS.Module.activeDocument === docId}
role="button"
tabindex="0"
onclick={() => switchDocument(docId)} onclick={() => switchDocument(docId)}
aria-label={doc.info.title || "Untitled"} onkeydown={(event) => handleActivation(event, () => switchDocument(docId))}
aria-label={(doc as IdsDocument).info.title || "Untitled"}
> >
<span class="tab-title">{doc.info.title || "Untitled"}</span> <span class="tab-title">{(doc as IdsDocument).info.title || "Untitled"}</span>
<button <button
class="tab-close" class="tab-close"
onclick={(e) => { e.stopPropagation(); closeDocument(docId); }} onclick={(e) => { e.stopPropagation(); closeDocument(docId); }}
@@ -31,4 +43,4 @@
</div> </div>
{/each} {/each}
<div class="filler-tab"></div> <div class="filler-tab"></div>
</div> </div>
+3 -4
View File
@@ -1,8 +1,7 @@
{ {
"wasm": { "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", "odfpy_url": "/worker/bin/odfpy-1.4.2-py2.py3-none-any.whl",
"api_py_url": "/worker/api.py", "api_py_url": "/worker/api.py"
"pyodide_url": "https://cdn.jsdelivr.net/pyodide/v0.28.0/full/pyodide.js"
} }
} }
+2 -1
View File
@@ -576,7 +576,8 @@ html, body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
label { label,
.form-label {
margin-bottom: 4px; margin-bottom: 4px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
@@ -1,7 +1,11 @@
<script> <script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props(); type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script> </script>
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} /> <DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
@@ -1,8 +1,17 @@
<script> <script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from "bits-ui";
import XIcon from "@lucide/svelte/icons/x"; import XIcon from "@lucide/svelte/icons/x";
import * as Dialog from "./index.js"; import * as Dialog from "./index";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
portalProps?: Record<string, unknown>;
children?: Snippet;
showCloseButton?: boolean;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -11,7 +20,7 @@
children, children,
showCloseButton = true, showCloseButton = true,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<Dialog.Portal {...portalProps}> <Dialog.Portal {...portalProps}>
@@ -35,4 +44,4 @@
</DialogPrimitive.Close> </DialogPrimitive.Close>
{/if} {/if}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</Dialog.Portal> </Dialog.Portal>
@@ -1,12 +1,17 @@
<script> <script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DialogPrimitive.Description <DialogPrimitive.Description
@@ -14,4 +19,4 @@
data-slot="dialog-description" data-slot="dialog-description"
class={cn("text-muted-foreground text-sm", className)} class={cn("text-muted-foreground text-sm", className)}
{...restProps} {...restProps}
/> />
@@ -1,11 +1,19 @@
<script> <script lang="ts">
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<div <div
@@ -15,4 +23,4 @@
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
</div> </div>
@@ -1,12 +1,19 @@
<script> <script lang="ts">
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<div <div
@@ -16,4 +23,4 @@
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
</div> </div>
@@ -1,12 +1,17 @@
<script> <script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
@@ -17,4 +22,4 @@
className className
)} )}
{...restProps} {...restProps}
/> />
@@ -1,12 +1,17 @@
<script> <script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DialogPrimitive.Title <DialogPrimitive.Title
@@ -14,4 +19,4 @@
data-slot="dialog-title" data-slot="dialog-title"
class={cn("text-lg leading-none", className)} class={cn("text-lg leading-none", className)}
{...restProps} {...restProps}
/> />
@@ -1,7 +1,11 @@
<script> <script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props(); type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script> </script>
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} /> <DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
@@ -1,8 +1,16 @@
<script> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check"; import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus"; import MinusIcon from "@lucide/svelte/icons/minus";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
checked?: boolean;
indeterminate?: boolean;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
checked = $bindable(false), checked = $bindable(false),
@@ -10,7 +18,7 @@
class: className, class: className,
children: childrenProp, children: childrenProp,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
@@ -34,4 +42,4 @@
</span> </span>
{@render childrenProp?.()} {@render childrenProp?.()}
{/snippet} {/snippet}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
@@ -1,14 +1,21 @@
<script> <script lang="ts">
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
sideOffset?: number;
portalProps?: Record<string, unknown>;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
sideOffset = 4, sideOffset = 4,
portalProps, portalProps,
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DropdownMenuPrimitive.Portal {...portalProps}> <DropdownMenuPrimitive.Portal {...portalProps}>
@@ -22,4 +29,4 @@
)} )}
{...restProps} {...restProps}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
@@ -1,12 +1,17 @@
<script> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
inset, inset,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DropdownMenuPrimitive.GroupHeading <DropdownMenuPrimitive.GroupHeading
@@ -15,4 +20,4 @@
data-inset={inset} data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)} class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
{...restProps} {...restProps}
/> />
@@ -1,7 +1,11 @@
<script> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props(); type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script> </script>
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} /> <DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
@@ -1,14 +1,21 @@
<script> <script lang="ts">
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
variant?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
inset, inset,
variant = "default", variant = "default",
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
@@ -21,4 +28,4 @@
className className
)} )}
{...restProps} {...restProps}
/> />
@@ -1,12 +1,20 @@
<script> <script lang="ts">
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
inset?: boolean;
children?: Snippet;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
inset, inset,
children, children,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<div <div
@@ -17,4 +25,4 @@
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
</div> </div>
@@ -1,11 +1,16 @@
<script> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
value?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
value = $bindable(), value = $bindable(),
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DropdownMenuPrimitive.RadioGroup <DropdownMenuPrimitive.RadioGroup
@@ -13,4 +18,4 @@
bind:value bind:value
data-slot="dropdown-menu-radio-group" data-slot="dropdown-menu-radio-group"
{...restProps} {...restProps}
/> />
@@ -1,14 +1,22 @@
<script> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle"; import CircleIcon from "@lucide/svelte/icons/circle";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
value: string;
children?: Snippet<[ { checked: boolean } ]>;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children: childrenProp, children: childrenProp,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
@@ -28,4 +36,4 @@
</span> </span>
{@render childrenProp?.({ checked })} {@render childrenProp?.({ checked })}
{/snippet} {/snippet}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
@@ -1,12 +1,17 @@
<script> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
@@ -14,4 +19,4 @@
data-slot="dropdown-menu-separator" data-slot="dropdown-menu-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)} class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps} {...restProps}
/> />
@@ -1,12 +1,19 @@
<script> <script lang="ts">
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<span <span
@@ -16,4 +23,4 @@
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
</span> </span>
@@ -1,12 +1,17 @@
<script> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
@@ -17,4 +22,4 @@
className className
)} )}
{...restProps} {...restProps}
/> />
@@ -1,7 +1,15 @@
<script> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right"; import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
children?: Snippet;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -9,7 +17,7 @@
inset, inset,
children, children,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
@@ -24,4 +32,4 @@
> >
{@render children?.()} {@render children?.()}
<ChevronRightIcon class="ml-auto size-4" /> <ChevronRightIcon class="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
@@ -1,7 +1,11 @@
<script> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props(); type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script> </script>
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} /> <DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
@@ -1,8 +1,16 @@
<script> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check"; import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus"; import MinusIcon from "@lucide/svelte/icons/minus";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
checked?: boolean;
indeterminate?: boolean;
children?: Snippet;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
@@ -10,7 +18,7 @@
indeterminate = $bindable(false), indeterminate = $bindable(false),
children: childrenProp, children: childrenProp,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<MenubarPrimitive.CheckboxItem <MenubarPrimitive.CheckboxItem
@@ -34,4 +42,4 @@
</span> </span>
{@render childrenProp?.()} {@render childrenProp?.()}
{/snippet} {/snippet}
</MenubarPrimitive.CheckboxItem> </MenubarPrimitive.CheckboxItem>
@@ -1,6 +1,19 @@
<script> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Align = "start" | "center" | "end";
type Side = "top" | "bottom" | "left" | "right";
type Props = {
ref?: HTMLElement | null;
class?: string;
sideOffset?: number;
alignOffset?: number;
align?: Align;
side?: Side;
portalProps?: Record<string, unknown>;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -11,7 +24,7 @@
side = "bottom", side = "bottom",
portalProps, portalProps,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<MenubarPrimitive.Portal {...portalProps}> <MenubarPrimitive.Portal {...portalProps}>
@@ -28,4 +41,4 @@
)} )}
{...restProps} {...restProps}
/> />
</MenubarPrimitive.Portal> </MenubarPrimitive.Portal>
@@ -1,12 +1,17 @@
<script> <script lang="ts">
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
inset?: boolean;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
inset, inset,
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<MenubarPrimitive.GroupHeading <MenubarPrimitive.GroupHeading
@@ -15,4 +20,4 @@
data-inset={inset} data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)} class={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
{...restProps} {...restProps}
/> />
@@ -1,10 +1,14 @@
<script> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<MenubarPrimitive.Group bind:ref data-slot="menubar-group" {...restProps} /> <MenubarPrimitive.Group bind:ref data-slot="menubar-group" {...restProps} />
@@ -1,6 +1,13 @@
<script> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
variant?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -8,7 +15,7 @@
inset = undefined, inset = undefined,
variant = "default", variant = "default",
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<MenubarPrimitive.Item <MenubarPrimitive.Item
@@ -21,4 +28,4 @@
className className
)} )}
{...restProps} {...restProps}
/> />
@@ -1,12 +1,19 @@
<script> <script lang="ts">
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
inset?: boolean;
children?: Snippet;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
inset, inset,
children, children,
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<div <div
@@ -17,4 +24,4 @@
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
</div> </div>
@@ -1,14 +1,22 @@
<script> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle"; import CircleIcon from "@lucide/svelte/icons/circle";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
value: string;
children?: Snippet<[ { checked: boolean } ]>;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children: childrenProp, children: childrenProp,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<MenubarPrimitive.RadioItem <MenubarPrimitive.RadioItem
@@ -28,4 +36,4 @@
</span> </span>
{@render childrenProp?.({ checked })} {@render childrenProp?.({ checked })}
{/snippet} {/snippet}
</MenubarPrimitive.RadioItem> </MenubarPrimitive.RadioItem>
@@ -1,12 +1,17 @@
<script> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<MenubarPrimitive.Separator <MenubarPrimitive.Separator
@@ -14,4 +19,4 @@
data-slot="menubar-separator" data-slot="menubar-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)} class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps} {...restProps}
/> />
@@ -1,12 +1,19 @@
<script> <script lang="ts">
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<span <span
@@ -16,4 +23,4 @@
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
</span> </span>
@@ -1,12 +1,17 @@
<script> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<MenubarPrimitive.SubContent <MenubarPrimitive.SubContent
@@ -17,4 +22,4 @@
className className
)} )}
{...restProps} {...restProps}
/> />
@@ -1,7 +1,15 @@
<script> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right"; import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
children?: Snippet;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -9,7 +17,7 @@
inset = undefined, inset = undefined,
children, children,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<MenubarPrimitive.SubTrigger <MenubarPrimitive.SubTrigger
@@ -24,4 +32,4 @@
> >
{@render children?.()} {@render children?.()}
<ChevronRightIcon class="ml-auto size-4" /> <ChevronRightIcon class="ml-auto size-4" />
</MenubarPrimitive.SubTrigger> </MenubarPrimitive.SubTrigger>
@@ -1,12 +1,17 @@
<script> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<MenubarPrimitive.Trigger <MenubarPrimitive.Trigger
@@ -17,4 +22,4 @@
className className
)} )}
{...restProps} {...restProps}
/> />
@@ -1,12 +1,17 @@
<script> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<MenubarPrimitive.Root <MenubarPrimitive.Root
@@ -17,4 +22,4 @@
className className
)} )}
{...restProps} {...restProps}
/> />
@@ -1,8 +1,8 @@
<script> <script lang="ts">
import { Toaster as Sonner } from "svelte-sonner"; import { Toaster as Sonner } from "svelte-sonner";
import { mode } from "mode-watcher"; import { mode } from "mode-watcher";
let { ...restProps } = $props(); let { ...restProps } : Record<string, unknown> = $props();
</script> </script>
<Sonner <Sonner
@@ -10,4 +10,4 @@
class="toaster group" class="toaster group"
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);" style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
{...restProps} {...restProps}
/> />
@@ -1,6 +1,18 @@
<script> <script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui"; import { Tooltip as TooltipPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Side = "top" | "bottom" | "left" | "right";
type Props = {
ref?: HTMLElement | null;
class?: string;
sideOffset?: number;
side?: Side;
children?: Snippet;
arrowClasses?: string;
} & Record<string, unknown>;
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -10,7 +22,7 @@
children, children,
arrowClasses, arrowClasses,
...restProps ...restProps
} = $props(); } : Props = $props();
</script> </script>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
@@ -42,4 +54,4 @@
{/snippet} {/snippet}
</TooltipPrimitive.Arrow> </TooltipPrimitive.Arrow>
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -1,7 +1,11 @@
<script> <script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui"; import { Tooltip as TooltipPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props(); type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script> </script>
<TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} /> <TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} />
-8
View File
@@ -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
+7
View File
@@ -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));
}
-9
View File
@@ -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;
+14
View File
@@ -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;
@@ -1,8 +1,31 @@
import wasm from "$src/modules/wasm"; 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 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<string, { source: string; tokens: string[] | null }>;
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: [], entityClasses: [],
materialCategories: [], materialCategories: [],
classificationSystems: {}, classificationSystems: {},
@@ -10,13 +33,13 @@ export let Autocompletions = $state({
isLoaded: false isLoaded: false
}); });
export let IFCModels = $state({ export const IFCModels: IfcModelState = $state({
models: [], models: [],
isLoading: false, isLoading: false,
audits: [] audits: []
}); });
const id = hyperid(); const id: () => string = hyperid();
// Preload autocompletions on initialization // Preload autocompletions on initialization
wasm.init().then(async () => { wasm.init().then(async () => {
@@ -30,26 +53,30 @@ export async function preloadAutocompletions() {
// Entity classes // Entity classes
const entitySets = await Promise.all( const entitySets = await Promise.all(
schemas.map(schema => wasm.getAllEntityClasses(schema)) schemas.map(schema => wasm.getAllEntityClasses(schema))
); ) as string[][];
const allEntities = new Set(); const allEntities = new Set<string>();
entitySets.forEach(entities => { for (const entities of entitySets) {
entities.forEach(entity => allEntities.add(entity.toUpperCase())); for (const entity of entities) {
}); allEntities.add(entity.toUpperCase());
}
}
// Data types // Data types
const dataTypeSets = await Promise.all( const dataTypeSets = await Promise.all(
schemas.map(schema => wasm.getAllDataTypes(schema)) schemas.map(schema => wasm.getAllDataTypes(schema))
); ) as Record<string, string>[];
const allDataTypes = new Set(); const allDataTypes = new Set<string>();
dataTypeSets.forEach(dataTypes => { for (const dataTypes of dataTypeSets) {
Object.keys(dataTypes).forEach(dataType => allDataTypes.add(dataType)); for (const dataType of Object.keys(dataTypes as Record<string, string>)) {
}); allDataTypes.add(dataType);
}
}
// Material categories and Classification systems // Material categories and Classification systems
const [materialCategories, classificationSystems] = await Promise.all([ const [materialCategories, classificationSystems] = await Promise.all([
wasm.getMaterialCategories(), wasm.getMaterialCategories(),
wasm.getStandardClassificationSystems() wasm.getStandardClassificationSystems()
]); ]) as [string[], AutocompletionState["classificationSystems"]];
// Cache autocompletions // Cache autocompletions
Autocompletions.entityClasses = Array.from(allEntities).sort(); 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); 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); 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); return await wasm.getApplicablePsets(schema, entity, predefinedType);
} }
@@ -92,7 +119,7 @@ export function getDataTypes() {
return Autocompletions.dataTypes; return Autocompletions.dataTypes;
} }
export async function loadIfc(file) { export async function loadIfc(file: File): Promise<IfcModel> {
try { try {
IFCModels.isLoading = true; IFCModels.isLoading = true;
@@ -100,10 +127,10 @@ export async function loadIfc(file) {
const uint8Array = new Uint8Array(arrayBuffer); const uint8Array = new Uint8Array(arrayBuffer);
// Load IFC model // 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 // Add to models list
const model = { const model: IfcModel = {
id: ifcId, id: ifcId,
fileName: file.name, fileName: file.name,
fileSize: file.size, fileSize: file.size,
@@ -121,7 +148,7 @@ export async function loadIfc(file) {
} }
} }
export async function unloadIfc(modelId) { export async function unloadIfc(modelId: string) {
try { try {
// Unload model // Unload model
await wasm.unloadIfc(modelId); 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 { try {
let idsBytes; let idsBytes: Uint8Array;
if (typeof idsData === 'string') { if (typeof idsData === 'string') {
idsBytes = new TextEncoder().encode(idsData); idsBytes = new TextEncoder().encode(idsData);
} else if (idsData instanceof ArrayBuffer) { } else if (idsData instanceof ArrayBuffer) {
@@ -148,7 +175,7 @@ export async function auditIfc(modelId, idsData) {
} }
// Run audit // 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}`); console.log(`Audit completed for model ${modelId}`);
return auditResult; return auditResult;
@@ -163,13 +190,14 @@ export function getLoadedModels() {
} }
export async function openIfc() { export async function openIfc() {
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const fileInput = document.createElement('input'); const fileInput = document.createElement('input');
fileInput.type = 'file'; fileInput.type = 'file';
fileInput.accept = '.ifc'; fileInput.accept = '.ifc';
fileInput.onchange = async (event) => { fileInput.onchange = async (event) => {
const file = event.target.files[0]; const target = event.target as HTMLInputElement | null;
const file = target?.files?.[0];
if (!file) { if (!file) {
reject(new Error('No file selected')); reject(new Error('No file selected'));
return; 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); 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); const model = getIfcById(modelId);
if (!model) return; if (!model) return;
const auditReport = { const auditReport: AuditReport = {
id: id(), id: id(),
modelId: modelId, modelId: modelId,
modelName: model.fileName, modelName: model.fileName,
@@ -216,19 +249,19 @@ export function createAuditReport(modelId, document, auditData, htmlReport = nul
return auditReport; return auditReport;
} }
export function getAuditReportsForIfc(modelId) { export function getAuditReportsForIfc(modelId: string) {
return IFCModels.audits.filter(audit => audit.modelId === modelId); 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); 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); IFCModels.audits = IFCModels.audits.filter(audit => audit.document !== document);
} }
export async function downloadAuditReport(auditId) { export async function downloadAuditReport(auditId: string) {
const audit = getAuditReportById(auditId); const audit = getAuditReportById(auditId);
if (!audit || !audit.htmlReport) { if (!audit || !audit.htmlReport) {
throw new Error('HTML report not available for this audit'); 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 // Get IDS document title for filename
let filename = 'report.html'; let filename = 'report.html';
if (audit.document && IDS.Module.documents[audit.document]) { 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'; const title = doc.info?.title || 'untitled';
filename = `report_${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.html`; filename = `report_${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.html`;
} }
@@ -270,9 +303,12 @@ export async function runAudit() {
// Get the active IDS document XML // Get the active IDS document XML
const idsXml = await IDS.exportActiveDocument(); const idsXml = await IDS.exportActiveDocument();
if (!idsXml) {
throw new Error('Failed to export IDS document');
}
// Run audit on all loaded models // Run audit on all loaded models
let firstAuditReport = null; let firstAuditReport: AuditReport | undefined;
for (const model of IFCModels.models) { for (const model of IFCModels.models) {
const result = await auditIfc(model.id, idsXml); const result = await auditIfc(model.id, idsXml);
@@ -280,7 +316,10 @@ export async function runAudit() {
const jsonData = result.json || null; const jsonData = result.json || null;
const htmlReport = result.html || 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 // Store the first audit report to open in viewer
if (!firstAuditReport) { if (!firstAuditReport) {
@@ -1,12 +1,37 @@
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
import { IFCModels } from './api.svelte.js'; import type { Socket } from 'socket.io-client';
import * as IDS from './ids.svelte.js'; import { IFCModels } from './api.svelte';
import { error, success } from '../utils/toast.svelte.js'; import * as IDS from './ids.svelte';
import { error, success } from '../utils/toast.svelte';
import hyperid from 'hyperid'; import hyperid from 'hyperid';
import { onMount } from 'svelte'; import type { AuditReport, AuditReportData } from "$src/types/report";
// Bonsai connection state // 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, enabled: false,
port: null, port: null,
socket: null, socket: null,
@@ -14,8 +39,8 @@ export let Bonsai = $state({
auditing: false auditing: false
}); });
const id = hyperid(); const id: () => string = hyperid();
const pendingAudits = new Map(); const pendingAudits = new Map<string, PendingAudit>();
// Check for Bonsai server port in URL parameters // Check for Bonsai server port in URL parameters
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@@ -29,8 +54,11 @@ if (serverPort) {
/** /**
* Connect to Bonsai server * Connect to Bonsai server
*/ */
export const connect = () => new Promise((resolve, reject) => { export const connect = () => new Promise<void>((resolve, reject) => {
if (!Bonsai.port) return; if (!Bonsai.port) {
resolve();
return;
}
try { try {
Bonsai.socket = io(`ws://127.0.0.1:${Bonsai.port}/ifctester`, { 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.connected = false;
}); });
Bonsai.socket.on('connect_error', (err) => { Bonsai.socket.on('connect_error', (err: Error) => {
Bonsai.connected = false; Bonsai.connected = false;
error(`Failed to connect to Bonsai: ${err.message}`); error(`Failed to connect to Bonsai: ${err.message}`);
reject(err); reject(err);
@@ -59,7 +87,8 @@ export const connect = () => new Promise((resolve, reject) => {
Bonsai.socket.on('error', handleAuditError); Bonsai.socket.on('error', handleAuditError);
} catch (err) { } 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); reject(err);
} }
}); });
@@ -93,14 +122,21 @@ export const runAudit = async () => {
// Convert IDS document to XML string // Convert IDS document to XML string
const idsXml = await IDS.exportActiveDocument(); const idsXml = await IDS.exportActiveDocument();
if (!idsXml) {
throw new Error('Failed to export IDS document');
}
const requestId = id(); const requestId = id();
const socket = Bonsai.socket;
if (!socket) {
throw new Error('Bonsai socket not connected');
}
return new Promise((resolve, reject) => { return new Promise<string | null>((resolve, reject) => {
// Store request with resolve/reject functions // Store request with resolve/reject functions
pendingAudits.set(requestId, { resolve, reject }); pendingAudits.set(requestId, { resolve, reject });
Bonsai.socket.emit('audit_ids', { socket.emit('audit_ids', {
id: requestId, id: requestId,
ids: idsXml ids: idsXml
}); });
@@ -108,7 +144,8 @@ export const runAudit = async () => {
} catch (err) { } catch (err) {
Bonsai.auditing = false; 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; return null;
} }
}; };
@@ -117,7 +154,7 @@ export const runAudit = async () => {
* Handles audit results from Bonsai server * Handles audit results from Bonsai server
* @param {Object} data - Audit result data * @param {Object} data - Audit result data
*/ */
const handleAuditResult = (data) => { const handleAuditResult = (data: AuditResultPayload) => {
if (!data.id || !data.json_report) return; if (!data.id || !data.json_report) return;
const pendingAudit = pendingAudits.get(data.id); const pendingAudit = pendingAudits.get(data.id);
@@ -130,13 +167,14 @@ const handleAuditResult = (data) => {
const { resolve } = pendingAudit; const { resolve } = pendingAudit;
try { 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, id: data.id,
modelId: `bonsai:${data.id}`,
date: new Date().toISOString(), date: new Date().toISOString(),
modelName: 'Bonsai IFC Model', modelName: 'Bonsai IFC Model',
document: IDS.Module.activeDocument, document: IDS.Module.activeDocument ?? "",
data: reportData, data: reportData,
htmlReport: data.html_report htmlReport: data.html_report
}; };
@@ -152,7 +190,8 @@ const handleAuditResult = (data) => {
} catch (err) { } catch (err) {
Bonsai.auditing = false; 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); resolve(null);
} }
}; };
@@ -161,7 +200,7 @@ const handleAuditResult = (data) => {
* Handles audit errors from Bonsai server * Handles audit errors from Bonsai server
* @param {Object} data - Error data * @param {Object} data - Error data
*/ */
const handleAuditError = (data) => { const handleAuditError = (data: AuditErrorPayload) => {
if (!data.id) return; if (!data.id) return;
const pendingAudit = pendingAudits.get(data.id); const pendingAudit = pendingAudits.get(data.id);
@@ -174,7 +213,6 @@ const handleAuditError = (data) => {
const { resolve } = pendingAudit; const { resolve } = pendingAudit;
Bonsai.auditing = false; Bonsai.auditing = false;
error(`Audit failed (Bonsai): ${data.error}`); error(`Audit failed (Bonsai): ${data.error ?? "Unknown error"}`);
resolve(null); resolve(null);
}; };
@@ -1,10 +1,18 @@
import wasm from "$src/modules/wasm"; import wasm from "$src/modules/wasm";
import { clearIdsAuditReports } from "./api.svelte.js"; import { clearIdsAuditReports } from "./api.svelte";
import hyperid from "hyperid"; import hyperid from "hyperid";
import {tick} from "svelte"; import {tick} from "svelte";
import type { DocumentState, Facet, FacetValue, IdsDocument, IdsCardinality, Restriction, Specification } from "$src/types/ids";
export let Module = $state({ type ModuleState = {
documents: [], documents: Record<string, IdsDocument>;
activeDocument: string | null;
status: "loading" | "ready" | "error";
states: Record<string, DocumentState>;
};
export const Module: ModuleState = $state({
documents: {},
activeDocument: null, activeDocument: null,
status: "loading", status: "loading",
states: {} states: {}
@@ -17,14 +25,15 @@ wasm.init().then(() => {
Module.status = "error"; Module.status = "error";
}); });
const id = hyperid() const id: () => string = hyperid();
export function setDocumentState(docId, updates) { export function setDocumentState(docId: string, updates: Partial<DocumentState>) {
if (!Module.states[docId]) { if (!Module.states[docId]) {
Module.states[docId] = { Module.states[docId] = {
activeTab: 'info', activeTab: 'info',
viewMode: 'editor', viewMode: 'editor',
activeSpecification: null activeSpecification: null,
auditReport: null
}; };
} }
Object.assign(Module.states[docId], updates); Object.assign(Module.states[docId], updates);
@@ -32,7 +41,7 @@ export function setDocumentState(docId, updates) {
export async function createDocument() { export async function createDocument() {
const docId = id(); const docId = id();
const doc = await wasm.createIDS(); const doc = await wasm.createIDS() as IdsDocument;
Module.documents[docId] = doc; Module.documents[docId] = doc;
@@ -43,14 +52,14 @@ export async function createDocument() {
Module.activeDocument = docId; Module.activeDocument = docId;
} }
export async function deleteDocument(id) { export async function deleteDocument(id: string) {
// Clear any audit reports generated using this IDS document // Clear any audit reports generated using this IDS document
clearIdsAuditReports(id); clearIdsAuditReports(id);
delete Module.documents[id]; delete Module.documents[id];
delete Module.states[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 there are other documents, set the first one as active
if (Object.keys(Module.documents).length > 0) { if (Object.keys(Module.documents).length > 0) {
Module.activeDocument = Object.keys(Module.documents)[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 // 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. // 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 (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
return obj.map(normalizeIdsDict); return obj.map(normalizeIdsDict);
} }
const result = {}; const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
if (key === 'xs:restriction' && Array.isArray(value) && value.length > 0) { if (key === 'xs:restriction' && Array.isArray(value) && value.length > 0) {
// Convert xs:restriction array to restriction object // Convert xs:restriction array to restriction object
const restriction = value[0]; const restriction = value[0] as Record<string, unknown>;
const newRestriction = {}; const newRestriction: Record<string, unknown> = {};
for (const [restrictionKey, restrictionValue] of Object.entries(restriction)) { for (const [restrictionKey, restrictionValue] of Object.entries(restriction)) {
if (restrictionKey.startsWith('xs:')) { if (restrictionKey.startsWith('xs:')) {
@@ -86,7 +95,7 @@ function normalizeIdsDict(obj) {
} }
} }
result['restriction'] = newRestriction; result.restriction = newRestriction;
} else { } else {
result[key] = normalizeIdsDict(value); result[key] = normalizeIdsDict(value);
} }
@@ -96,13 +105,16 @@ function normalizeIdsDict(obj) {
} }
export async function openDocument() { export async function openDocument() {
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const fileInput = document.createElement('input'); const fileInput = document.createElement('input') as HTMLInputElement & {
oncancel?: ((this: HTMLInputElement, ev: Event) => void) | null;
};
fileInput.type = 'file'; fileInput.type = 'file';
fileInput.accept = '.ids,.xml'; fileInput.accept = '.ids,.xml';
fileInput.onchange = async (event) => { fileInput.onchange = async (event) => {
const file = event.target.files[0]; const target = event.target as HTMLInputElement | null;
const file = target?.files?.[0];
if (!file) { if (!file) {
reject(new Error('No file selected')); reject(new Error('No file selected'));
return; return;
@@ -112,8 +124,8 @@ export async function openDocument() {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async (e) => { reader.onload = async (e) => {
try { try {
const fileContent = e.target.result; const fileContent = (e.target as FileReader).result;
const doc = normalizeIdsDict(await wasm.openIDS(fileContent, false)); const doc = normalizeIdsDict(await wasm.openIDS(String(fileContent), false)) as IdsDocument;
const docId = id(); const docId = id();
// Add document to list and set as active // 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<string | null> {
if (!Module.activeDocument) return null; if (!Module.activeDocument) return null;
const doc = $state.snapshot(Module.documents[Module.activeDocument]); const doc = $state.snapshot(Module.documents[Module.activeDocument]);
const xmlString = await wasm.exportIDS(doc); const xmlString = await wasm.exportIDS(doc as Record<string, unknown>) as string;
return xmlString; return xmlString;
} }
export async function exportDocument(docId) { export async function exportDocument(docId: string) {
const doc = $state.snapshot(Module.documents[docId]); const doc = $state.snapshot(Module.documents[docId]);
// Validate // Validate
@@ -162,37 +174,38 @@ export async function exportDocument(docId) {
throw new Error("Please create at least one specification before exporting the document."); 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<string, unknown>) as string;
// Create and download file // Create and download file
const blob = new Blob([xmlString], { type: 'application/xml' }); const blob = new Blob([xmlString], { type: 'application/xml' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; 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(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
export async function createSpecification(docId) { export async function createSpecification(docId: string) {
const spec = await wasm.createSpecification(); const spec = await wasm.createSpecification() as Specification;
// Add specification to document // Add specification to document
Module.documents[docId].specifications.specification.push(spec); Module.documents[docId].specifications.specification.push(spec);
// Set as active specification // Set as active specification
if (Module.activeDocument == docId) { if (Module.activeDocument === docId) {
const state = Module.states[docId]; const state = Module.states[docId];
state.activeSpecification = Module.documents[docId].specifications.specification.length - 1; 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); Module.documents[docId].specifications.specification.splice(specId, 1);
if (Module.activeDocument == docId) { if (Module.activeDocument === docId) {
const state = Module.states[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 // We need to wait for the next tick here because of Svelte's internal shenanigans
await tick(); await tick();
setDocumentState(docId, { activeSpecification: null }); setDocumentState(docId, { activeSpecification: null });
@@ -209,124 +222,148 @@ export async function deleteSpecification(docId, specId) {
* clause: "applicability", "requirements" * clause: "applicability", "requirements"
* facet: "entity", "attribute", "classification", "partOf", "property", "material" * facet: "entity", "attribute", "classification", "partOf", "property", "material"
*/ */
export async function createFacet(docId, specId, clause, facet) { export async function createFacet(
let facetObj; docId: string,
if (facet == "entity") { specId: number,
facetObj = await wasm.createEntityFacet(clause, {}); clause: "applicability" | "requirements",
} else if (facet == "attribute") { facet: "entity" | "attribute" | "classification" | "partOf" | "property" | "material"
facetObj = await wasm.createAttributeFacet(clause, {}); ) {
} else if (facet == "classification") { let facetObj: Facet | undefined;
facetObj = await wasm.createClassificationFacet(clause, {}); if (facet === "entity") {
} else if (facet == "partOf") { facetObj = await wasm.createEntityFacet(clause, {}) as Facet;
facetObj = await wasm.createPartOfFacet(clause, {}); } else if (facet === "attribute") {
} else if (facet == "property") { facetObj = await wasm.createAttributeFacet(clause, {}) as Facet;
facetObj = await wasm.createPropertyFacet(clause, {}); } else if (facet === "classification") {
} else if (facet == "material") { facetObj = await wasm.createClassificationFacet(clause, {}) as Facet;
facetObj = await wasm.createMaterialFacet(clause, {}); } 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])) { if (!facetObj) return;
Module.documents[docId].specifications.specification[specId][clause][facet] = [];
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<string, unknown>))) {
(spec[clauseKey] as Record<string, unknown>)[facet] = [];
} }
Module.documents[docId].specifications.specification[specId][clause][facet].push(facetObj); ((spec[clauseKey] as Record<string, unknown>)[facet] as Facet[]).push(facetObj);
} }
export async function deleteFacet(docId, specId, clause, facet, facetId) { export async function deleteFacet(
delete Module.documents[docId].specifications.specification[specId][clause][facet][facetId]; 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<string, unknown> | 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'; if (!spec?.applicability) return 'required';
const minOccurs = spec.applicability["@minOccurs"]; const minOccurs = spec.applicability["@minOccurs"] as number | undefined;
const maxOccurs = spec.applicability["@maxOccurs"]; const maxOccurs = spec.applicability["@maxOccurs"] as number | "unbounded" | undefined;
if (minOccurs === 1 && maxOccurs === "unbounded") return 'required'; if (minOccurs !== 0) return 'required';
if (minOccurs === 0 && maxOccurs === "unbounded") return 'optional'; if (minOccurs === 0 && maxOccurs !== 0) return 'optional';
if (minOccurs === 0 && maxOccurs === 0) return 'prohibited'; if (maxOccurs === 0) return 'prohibited';
return 'required'; return 'required';
}; };
// Converts facet to human-readable description // 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 ""; if (!facet) return "";
const usage = getSpecUsage(spec); const usage = getSpecUsage(spec);
const descriptions = []; const descriptions: string[] = [];
// Entity facet // Entity facet
if (facetType === "entity") { if (facetType === "entity") {
if (clauseType === "applicability") { 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 { } 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) { if (facet.predefinedType) {
descriptions.push(`and type ${stringifyValue(facet.predefinedType)}`); descriptions.push(`and type ${stringifyValue(facet.predefinedType as FacetValue)}`);
} }
} }
// Attribute facet // Attribute facet
else if (facetType === "attribute") { else if (facetType === "attribute") {
if (clauseType === "applicability") { if (clauseType === "applicability") {
descriptions.push(`All data where attribute ${stringifyValue(facet.name)}`); descriptions.push(`All data where attribute ${stringifyValue(facet.name as FacetValue)}`);
} else { } 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 // Property facet
else if (facetType === "property") { else if (facetType === "property") {
if (clauseType === "applicability") { if (clauseType === "applicability") {
descriptions.push(`Elements where property ${stringifyValue(facet.baseName)}`); descriptions.push(`Elements where property ${stringifyValue(facet.baseName as FacetValue)}`);
} else { } 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) { 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 // Classification facet
else if (facetType === "classification") { else if (facetType === "classification") {
if (clauseType === "applicability") { 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 { } 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) { if (facet.value) {
descriptions.push(`and classification ${stringifyValue(facet.value)}`); descriptions.push(`and classification ${stringifyValue(facet.value as FacetValue)}`);
} }
} }
// Material facet // Material facet
else if (facetType === "material") { else if (facetType === "material") {
if (clauseType === "applicability") { if (clauseType === "applicability") {
descriptions.push(`All data where material ${stringifyValue(facet.value)}`); descriptions.push(`All data where material ${stringifyValue(facet.value as FacetValue)}`);
} else { } 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 // PartOf facet
else if (facetType === "partOf") { else if (facetType === "partOf") {
if (clauseType === "applicability") { 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) { 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 { } 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) { 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) { 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 // Post-process for prohibited and optional requirements
let isProhibited = false; let isProhibited = false;
if (usage == "prohibited") isProhibited = !isProhibited; if (usage === "prohibited") isProhibited = !isProhibited;
if (clauseType == "requirements" && "@cardinality" in facet && facet["@cardinality"] == "prohibited") isProhibited = !isProhibited; if (clauseType === "requirements" && "@cardinality" in facet && facet["@cardinality"] === "prohibited") {
isProhibited = !isProhibited;
}
if (isProhibited) if (isProhibited)
combined = combined.replace("Shall", "Shall not").replace("shall", "shall not"); 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"); combined = combined.replace("Shall", "May").replace("shall", "may");
return renderFacetString(combined); return renderFacetString(combined);
} }
// Converts value objects to human-readable strings // Converts value objects to human-readable strings
function stringifyValue(value) { function stringifyValue(value?: FacetValue) {
if (!value) return "is provided"; if (!value) return "is provided";
if (value.simpleValue) return `is **${value.simpleValue}**`; if (value.simpleValue) return `is **${value.simpleValue}**`;
if (value.restriction) return stringifyRestriction(value.restriction); if (value.restriction) return stringifyRestriction(value.restriction);
@@ -357,7 +396,7 @@ function stringifyValue(value) {
} }
// Converts restriction objects to human-readable strings // Converts restriction objects to human-readable strings
function stringifyRestriction(restriction) { function stringifyRestriction(restriction: Restriction) {
if (!restriction) return ""; if (!restriction) return "";
// Handle enumeration // Handle enumeration
@@ -394,7 +433,7 @@ function stringifyRestriction(restriction) {
if (restriction.maxExclusive && restriction.maxExclusive.length > 0) { if (restriction.maxExclusive && restriction.maxExclusive.length > 0) {
parts.push(`**< ${restriction.maxExclusive[0]['@value'] || ''}**`); 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 // Handle length range restrictions
@@ -406,18 +445,18 @@ function stringifyRestriction(restriction) {
if (restriction.maxLength && restriction.maxLength.length > 0) { if (restriction.maxLength && restriction.maxLength.length > 0) {
parts.push(`**max length ${restriction.maxLength[0]['@value'] || ''}**`); 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"; return "has complex restriction";
} }
function renderFacetString(text) { function renderFacetString(text: string): string {
// Convert **text** to <strong>text</strong> // Convert **text** to <strong>text</strong>
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); const withStrong = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// Convert `text` to <code>text</code> // Convert `text` to <code>text</code>
text = text.replace(/`([^`]+)`/g, '<code>$1</code>'); const withCode = withStrong.replace(/`([^`]+)`/g, '<code>$1</code>');
return text; return withCode;
} }
@@ -4,7 +4,7 @@ import { toast } from "svelte-sonner";
* Show an error toast notification * Show an error toast notification
* @param {string} message - The error message to display * @param {string} message - The error message to display
*/ */
export function error(message) { export function error(message: string): void {
toast.error(message); toast.error(message);
} }
@@ -12,7 +12,7 @@ export function error(message) {
* Show a success toast notification * Show a success toast notification
* @param {string} message - The success message to display * @param {string} message - The success message to display
*/ */
export function success(message) { export function success(message: string): void {
toast.success(message); toast.success(message);
} }
@@ -20,7 +20,7 @@ export function success(message) {
* Show an info toast notification * Show an info toast notification
* @param {string} message - The info message to display * @param {string} message - The info message to display
*/ */
export function info(message) { export function info(message: string): void {
toast.info(message); toast.info(message);
} }
@@ -28,7 +28,7 @@ export function info(message) {
* Show a warning toast notification * Show a warning toast notification
* @param {string} message - The warning message to display * @param {string} message - The warning message to display
*/ */
export function warning(message) { export function warning(message: string): void {
toast.warning(message); toast.warning(message);
} }
@@ -37,7 +37,7 @@ export function warning(message) {
* @param {string} message - The loading message to display * @param {string} message - The loading message to display
* @returns {string} - Toast ID for dismissing later * @returns {string} - Toast ID for dismissing later
*/ */
export function loading(message) { export function loading(message: string): string | number {
return toast.loading(message); return toast.loading(message);
} }
@@ -45,7 +45,7 @@ export function loading(message) {
* Dismiss a specific toast * Dismiss a specific toast
* @param {string} toastId - The toast ID to dismiss * @param {string} toastId - The toast ID to dismiss
*/ */
export function dismiss(toastId) { export function dismiss(toastId: string | number): void {
toast.dismiss(toastId); toast.dismiss(toastId);
} }
@@ -57,10 +57,16 @@ export function dismiss(toastId) {
* @param {string} messages.success - Success message * @param {string} messages.success - Success message
* @param {string} messages.error - Error message * @param {string} messages.error - Error message
*/ */
export function promise(promiseToTrack, messages) { type PromiseToastMessages = {
loading: string;
success: string;
error: string;
};
export function promise<T>(promiseToTrack: Promise<T>, messages: PromiseToastMessages) {
return toast.promise(promiseToTrack, { return toast.promise(promiseToTrack, {
loading: messages.loading, loading: messages.loading,
success: messages.success, success: messages.success,
error: messages.error, error: messages.error,
}); });
} }
@@ -5,6 +5,7 @@
import hyperid from "hyperid"; import hyperid from "hyperid";
import EventEmitter from "eventemitter3"; import EventEmitter from "eventemitter3";
import type { WorkerResponse } from "$src/types/wasm";
// Message types // Message types
export const MessageType = { export const MessageType = {
@@ -25,52 +26,57 @@ export const MessageType = {
// WASM module disposed // WASM module disposed
DISPOSED: 'disposed' DISPOSED: 'disposed'
} as const;
type PendingMessage = {
resolve: (value?: unknown) => void;
reject: (reason?: unknown) => void;
}; };
type WasmReadyState = boolean | Promise<boolean>;
class WASMModule extends EventEmitter { class WASMModule extends EventEmitter {
id = hyperid(); id = hyperid();
ready = false; ready: WasmReadyState = false;
worker = null; worker: Worker | null = null;
pendingMessages = new Map(); pendingMessages = new Map<string, PendingMessage>();
async init() { async init() {
if (this.ready === true) return; 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<WorkerResponse>) => {
this._handleWorkerMessage(event.data); this._handleWorkerMessage(event.data);
}; };
this.worker.onerror = (error) => { this.worker.onerror = (error: ErrorEvent) => {
console.error('[WASM] Web worker error:', error); console.error('[WASM] Web worker error:', error);
this._rejectPendingMessages(error); this._rejectPendingMessages(error);
}; };
this.ready = new Promise(async (resolve, reject) => { this.ready = this._sendMessage(MessageType.INIT)
try { .then(() => true)
await this._sendMessage(MessageType.INIT); .catch((error) => {
resolve(true);
} catch (error) {
console.error('[WASM] Failed to initialize:', error); console.error('[WASM] Failed to initialize:', error);
this.ready = false; this.ready = false;
reject(error); throw error;
} });
});
return this.ready; return this.ready;
} }
async _sendMessage(type, payload = {}) { async _sendMessage(type: string, payload: Record<string, unknown> = {}): Promise<unknown> {
if (!this.worker) throw new Error('Worker not initialized'); const worker = this.worker;
if (!worker) throw new Error('Worker not initialized');
const id = this.id(); const id = this.id();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.pendingMessages.set(id, { resolve, reject }); this.pendingMessages.set(id, { resolve, reject });
this.worker.postMessage({ worker.postMessage({
type, type,
payload, payload,
id id
@@ -78,7 +84,7 @@ class WASMModule extends EventEmitter {
}); });
} }
_handleWorkerMessage({ type, payload, id }) { _handleWorkerMessage({ type, payload, id }: WorkerResponse) {
const pendingMessage = this.pendingMessages.get(id); const pendingMessage = this.pendingMessages.get(id);
if (!pendingMessage) { if (!pendingMessage) {
@@ -97,23 +103,28 @@ class WASMModule extends EventEmitter {
case MessageType.API_RESPONSE: case MessageType.API_RESPONSE:
resolve(payload); resolve(payload);
break; break;
case MessageType.ERROR: case MessageType.ERROR: {
reject(new Error(payload.message)); const message =
payload && typeof payload === "object" && "message" in payload
? String(payload.message)
: "Unknown worker error";
reject(new Error(message));
break; break;
}
default: default:
console.warn('[WASM] Unknown message type:', type); console.warn('[WASM] Unknown message type:', type);
reject(new Error(`Unknown message type: ${type}`)); reject(new Error(`Unknown message type: ${type}`));
} }
} }
_rejectPendingMessages(error) { _rejectPendingMessages(error: unknown) {
for (const { reject } of this.pendingMessages.values()) { for (const { reject } of this.pendingMessages.values()) {
reject(error); reject(error);
} }
this.pendingMessages.clear(); this.pendingMessages.clear();
} }
async _apiCall(method, ...args) { async _apiCall(method: string, ...args: unknown[]) {
if (!this.ready) await this.init(); if (!this.ready) await this.init();
const result = await this._sendMessage(MessageType.API_CALL, { method, args }); 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 * Get all entity classes in a given IFC schema
*/ */
async getAllEntityClasses(schema) { async getAllEntityClasses(schema: string) {
return this._apiCall('getAllEntityClasses', schema); return this._apiCall('getAllEntityClasses', schema);
} }
/** /**
* Get all data types in a given IFC schema * Get all data types in a given IFC schema
*/ */
async getAllDataTypes(schema) { async getAllDataTypes(schema: string) {
return this._apiCall('getAllDataTypes', schema); return this._apiCall('getAllDataTypes', schema);
} }
/** /**
* Get predefined types for a given IFC entity * Get predefined types for a given IFC entity
*/ */
async getPredefinedTypes(schema, entity) { async getPredefinedTypes(schema: string, entity: string) {
return this._apiCall('getPredefinedTypes', schema, entity); return this._apiCall('getPredefinedTypes', schema, entity);
} }
/** /**
* Get all attributes for a given IFC 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); return this._apiCall('getEntityAttributes', schema, entity);
} }
/** /**
* Get applicable property sets for a given IFC 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); 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. * 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); return this._apiCall('loadIfc', ifcData);
} }
/** /**
* Unload an IFC file * Unload an IFC file
*/ */
async unloadIfc(ifcId) { async unloadIfc(ifcId: string) {
return this._apiCall('unloadIfc', ifcId); return this._apiCall('unloadIfc', ifcId);
} }
/** /**
* Audit a loaded IFC file against IDS specifications * 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; const idsBytes = idsData instanceof ArrayBuffer ? new Uint8Array(idsData) : idsData;
return this._apiCall('auditIfc', ifcId, Array.from(idsBytes)); return this._apiCall('auditIfc', ifcId, Array.from(idsBytes));
@@ -204,70 +215,70 @@ class WASMModule extends EventEmitter {
/** /**
* Open an existing IDS from XML string * Open an existing IDS from XML string
*/ */
async openIDS(idsXml, validate = false) { async openIDS(idsXml: string, validate = false) {
return this._apiCall('openIDS', idsXml, validate); return this._apiCall('openIDS', idsXml, validate);
} }
/** /**
* Create a specification * Create a specification
*/ */
async createSpecification(options = {}) { async createSpecification(options: Record<string, unknown> = {}) {
return this._apiCall('createSpecification', options); return this._apiCall('createSpecification', options);
} }
/** /**
* Create an entity facet * Create an entity facet
*/ */
async createEntityFacet(clause, options = {}) { async createEntityFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createEntityFacet', clause, options); return this._apiCall('createEntityFacet', clause, options);
} }
/** /**
* Create an attribute facet * Create an attribute facet
*/ */
async createAttributeFacet(clause, options = {}) { async createAttributeFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createAttributeFacet', clause, options); return this._apiCall('createAttributeFacet', clause, options);
} }
/** /**
* Create a property facet * Create a property facet
*/ */
async createPropertyFacet(clause, options = {}) { async createPropertyFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createPropertyFacet', clause, options); return this._apiCall('createPropertyFacet', clause, options);
} }
/** /**
* Create a material facet * Create a material facet
*/ */
async createMaterialFacet(clause, options = {}) { async createMaterialFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createMaterialFacet', clause, options); return this._apiCall('createMaterialFacet', clause, options);
} }
/** /**
* Create a classification facet * Create a classification facet
*/ */
async createClassificationFacet(clause, options = {}) { async createClassificationFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createClassificationFacet', clause, options); return this._apiCall('createClassificationFacet', clause, options);
} }
/** /**
* Create a part-of facet * Create a part-of facet
*/ */
async createPartOfFacet(clause, options = {}) { async createPartOfFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createPartOfFacet', clause, options); return this._apiCall('createPartOfFacet', clause, options);
} }
/** /**
* Validate an IDS object * Validate an IDS object
*/ */
async validateIDS(idsObj) { async validateIDS(idsObj: Record<string, unknown>) {
return await this._apiCall('validateIDS', idsObj); return await this._apiCall('validateIDS', idsObj);
} }
/** /**
* Export IDS instance to XML string * Export IDS instance to XML string
*/ */
async exportIDS(idsObj) { async exportIDS(idsObj: Record<string, unknown>) {
return this._apiCall('exportIDS', idsObj); return this._apiCall('exportIDS', idsObj);
} }
@@ -319,4 +330,4 @@ export const {
dispose dispose
} = wasm; } = wasm;
export default wasm; export default wasm;
@@ -1,12 +1,13 @@
import config from '../../../config.json';
import hyperid from 'hyperid'; import hyperid from 'hyperid';
import config from '../../../config.json';
import type { AuditReportData } from "$src/types/report";
let pyodide = null; let pyodide: any = null;
let id = hyperid(); const id = hyperid();
let LoadedIFC = new Map(); const LoadedIFC = new Map<string, unknown>();
export async function init(pdide) { export async function init(pdide: any) {
pyodide = pdide; pyodide = pdide;
// Load Python API bindings // 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(` const result = await pyodide.runPythonAsync(`
from api import get_predefined_types_for_entity from api import get_predefined_types_for_entity
predef_types = get_predefined_types_for_entity("${schema}", "${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 }); return result.toJs({ dict_converter: Object.fromEntries });
} }
export async function getAllEntityClasses(schema) { export async function getAllEntityClasses(schema: string) {
const result = await pyodide.runPythonAsync(` const result = await pyodide.runPythonAsync(`
from api import get_all_entity_classes from api import get_all_entity_classes
entities = get_all_entity_classes("${schema}") entities = get_all_entity_classes("${schema}")
@@ -36,7 +37,7 @@ export async function getAllEntityClasses(schema) {
return result.toJs({ dict_converter: Object.fromEntries }); return result.toJs({ dict_converter: Object.fromEntries });
} }
export async function getAllDataTypes(schema) { export async function getAllDataTypes(schema: string) {
const result = await pyodide.runPythonAsync(` const result = await pyodide.runPythonAsync(`
from api import get_all_data_types from api import get_all_data_types
data_types = get_all_data_types("${schema}") data_types = get_all_data_types("${schema}")
@@ -45,7 +46,7 @@ export async function getAllDataTypes(schema) {
return result.toJs({ dict_converter: Object.fromEntries }); 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(` const result = await pyodide.runPythonAsync(`
from api import get_entity_attributes from api import get_entity_attributes
attrs = get_entity_attributes("${schema}", "${entity}") attrs = get_entity_attributes("${schema}", "${entity}")
@@ -54,7 +55,7 @@ export async function getEntityAttributes(schema, entity) {
return result.toJs({ dict_converter: Object.fromEntries }); 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(` const result = await pyodide.runPythonAsync(`
from api import get_applicable_psets from api import get_applicable_psets
psets = get_applicable_psets("${schema}", "${entity}", "${predefinedType}") psets = get_applicable_psets("${schema}", "${entity}", "${predefinedType}")
@@ -81,7 +82,7 @@ export async function getStandardClassificationSystems() {
return result.toJs({ dict_converter: Object.fromEntries }); 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 ifc_id = id();
const path = `/tmp/${encodeURIComponent(ifc_id)}.ifc`; const path = `/tmp/${encodeURIComponent(ifc_id)}.ifc`;
@@ -97,14 +98,14 @@ export async function loadIfc(ifcData) {
return ifc_id; return ifc_id;
} }
export async function unloadIfc(ifcId) { export async function unloadIfc(ifcId: string) {
const path = `/tmp/${encodeURIComponent(ifcId)}.ifc`; const path = `/tmp/${encodeURIComponent(ifcId)}.ifc`;
pyodide.FS.unlink(path); pyodide.FS.unlink(path);
LoadedIFC.delete(ifcId); 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 reporter = pyodide.pyimport("ifctester.reporter");
const api = pyodide.pyimport("api"); const api = pyodide.pyimport("api");
@@ -116,16 +117,16 @@ export async function auditIfc(ifcId, idsData) {
specs.validate(ifc); specs.validate(ifc);
// Create report in both HTML and JSON formats // Create report in both HTML and JSON formats
let jsonReporter = reporter.Json(specs); const jsonReporter = reporter.Json(specs);
jsonReporter.report(); jsonReporter.report();
const jsonReport = jsonReporter.to_string(); const jsonReport = jsonReporter.to_string();
let htmlReporter = reporter.Html(specs); const htmlReporter = reporter.Html(specs);
htmlReporter.report(); htmlReporter.report();
const htmlReport = htmlReporter.to_string(); const htmlReport = htmlReporter.to_string();
return { return {
json: JSON.parse(jsonReport), json: JSON.parse(jsonReport) as AuditReportData,
html: htmlReport html: htmlReport
}; };
} }
@@ -142,4 +143,4 @@ export const API = {
"loadIfc": loadIfc, "loadIfc": loadIfc,
"unloadIfc": unloadIfc, "unloadIfc": unloadIfc,
"auditIfc": auditIfc "auditIfc": auditIfc
}; };
@@ -2,13 +2,19 @@
* IDS module * IDS module
*/ */
let pyodide = null; let pyodide: any = null;
// IDS Python classes // IDS Python classes
let Ids, Specification; let Ids: any;
let Entity, Attribute, Property, Material, Classification, PartOf; 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; pyodide = pdide;
await pyodide.loadPackagesFromImports(` await pyodide.loadPackagesFromImports(`
@@ -29,24 +35,24 @@ export async function init(pdide) {
PartOf = pyodide.pyimport("ifctester.facet").PartOf; PartOf = pyodide.pyimport("ifctester.facet").PartOf;
} }
function _idsToInstance(idsObj) { function _idsToInstance(idsObj: Record<string, unknown>) {
const ids_raw = Ids(); const ids_raw = Ids();
return ids_raw.parse(pyodide.toPy(idsObj)) return ids_raw.parse(pyodide.toPy(idsObj))
} }
export function createIDS() { export function createIDS(): Record<string, unknown> {
const ids_raw = Ids() const ids_raw = Ids()
return ids_raw.asdict().toJs({dict_converter: Object.fromEntries}); 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<string, unknown> {
const ids_from_xml_string = pyodide.pyimport("api").ids_from_xml_string; const ids_from_xml_string = pyodide.pyimport("api").ids_from_xml_string;
const ids_raw = ids_from_xml_string(ids_xml, validate); const ids_raw = ids_from_xml_string(ids_xml, validate);
return ids_raw.asdict().toJs({dict_converter: Object.fromEntries}); return ids_raw.asdict().toJs({dict_converter: Object.fromEntries});
} }
export function validateIDS(idsObj) { export function validateIDS(idsObj: Record<string, unknown>): boolean {
const ids_raw = _idsToInstance(idsObj) const ids_raw = _idsToInstance(idsObj)
const tempFilename = `temp_${Date.now()}.xml`; 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 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; return isValid;
} }
export function exportIDS(idsObj) { export function exportIDS(idsObj: Record<string, unknown>): string {
const ids_raw = _idsToInstance(idsObj) const ids_raw = _idsToInstance(idsObj)
return ids_raw.to_string(); 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<string, unknown> {
const spec = Specification.callKwargs({ const spec = Specification.callKwargs({
name: name, name: name,
ifcVersion: ifcVersion, ifcVersion: ifcVersion,
@@ -79,7 +99,14 @@ export function createSpecification({name = "Unnamed", ifcVersion = ["IFC2X3", "
} }
// @instructions // @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<string, unknown> {
const entity = Entity.callKwargs({ const entity = Entity.callKwargs({
name: name, name: name,
predefinedType: predefinedType, predefinedType: predefinedType,
@@ -89,7 +116,15 @@ export function createEntityFacet(clause, {name = "IFCWALL", predefinedType = nu
} }
// @cardinality, @instructions // @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<string, unknown> {
const attribute = Attribute.callKwargs({ const attribute = Attribute.callKwargs({
name: name, name: name,
value: value, value: value,
@@ -100,7 +135,16 @@ export function createAttributeFacet(clause, {name = "Name", value = null, cardi
} }
// @uri, @cardinality, @instructions // @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<string, unknown> {
const classification = Classification.callKwargs({ const classification = Classification.callKwargs({
value: value, value: value,
system: system, system: system,
@@ -112,7 +156,16 @@ export function createClassificationFacet(clause, {value = null, system = null,
} }
// @relation, @cardinality, @instructions // @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<string, unknown> {
const part_of = PartOf.callKwargs({ const part_of = PartOf.callKwargs({
name: name, name: name,
predefinedType: predefinedType, predefinedType: predefinedType,
@@ -124,7 +177,26 @@ export function createPartOfFacet(clause, {name = "IFCWALL", predefinedType = nu
} }
// @dataType, @uri, @cardinality, @instructions // @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<string, unknown> {
const property = Property.callKwargs({ const property = Property.callKwargs({
propertySet: propertySet, propertySet: propertySet,
baseName: baseName, baseName: baseName,
@@ -138,7 +210,15 @@ export function createPropertyFacet(clause, {propertySet = "Property_Set", baseN
} }
// @uri, @cardinality, @instructions // @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<string, unknown> {
const material = Material.callKwargs({ const material = Material.callKwargs({
value: value, value: value,
uri: uri, uri: uri,
@@ -149,7 +229,7 @@ export function createMaterialFacet(clause, {value = null, uri = null, cardinali
} }
// Helper function to convert date to ISO format string // 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; if (!date) return null;
const d = new Date(date); const d = new Date(date);
return d.toISOString().split('T')[0]; return d.toISOString().split('T')[0];
@@ -168,4 +248,4 @@ export const API = {
"createPartOfFacet": createPartOfFacet, "createPartOfFacet": createPartOfFacet,
"createPropertyFacet": createPropertyFacet, "createPropertyFacet": createPropertyFacet,
"createMaterialFacet": createMaterialFacet, "createMaterialFacet": createMaterialFacet,
}; };
@@ -4,13 +4,14 @@
import { MessageType } from '../index'; import { MessageType } from '../index';
import config from '../../../config.json'; import config from '../../../config.json';
import * as IDS from './ids.js'; import * as IDS from './ids';
import * as API from './api'; import * as API from './api';
import type { ApiCallPayload, WorkerRequest } from "$src/types/wasm";
let pyodide = null; let pyodide: any = null;
let ready = false; let ready = false;
self.addEventListener('message', async (event) => { self.addEventListener('message', async (event: MessageEvent<WorkerRequest>) => {
console.log("[worker] Received message:", event.data); console.log("[worker] Received message:", event.data);
const { type, payload, id } = event.data; const { type, payload, id } = event.data;
@@ -25,27 +26,33 @@ self.addEventListener('message', async (event) => {
}); });
break; break;
case MessageType.API_CALL: case MessageType.API_CALL: {
if (!ready) { if (!ready) {
throw new Error('[worker] Pyodide not initialized'); 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({ self.postMessage({
type: MessageType.API_RESPONSE, type: MessageType.API_RESPONSE,
payload: result, payload: result,
id id
}); });
break; break;
}
default: default:
throw new Error(`[worker] Unknown message type: ${type}`); throw new Error(`[worker] Unknown message type: ${type}`);
} }
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
self.postMessage({ self.postMessage({
type: MessageType.ERROR, type: MessageType.ERROR,
payload: { payload: {
message: error.message, message,
stack: error.stack stack
}, },
id id
}); });
@@ -76,7 +83,13 @@ async function initEnvironment() {
await pyodide.loadPackage("shapely"); await pyodide.loadPackage("shapely");
// Install IfcTester // 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 // Initialize IDS and API
await API.init(pyodide); await API.init(pyodide);
@@ -93,17 +106,17 @@ async function cleanupEnvironment() {
console.log("[worker] Closed environment"); console.log("[worker] Closed environment");
} }
async function handleApiCall({ method, args = [] }) { async function handleApiCall({ method, args = [] }: ApiCallPayload) {
if (method === 'internal.cleanup') { if (method === 'internal.cleanup') {
await cleanupEnvironment(); await cleanupEnvironment();
return true; return true;
} }
if (method in API.API) { if (method in API.API) {
return await API.API[method](...args); return await (API.API as Record<string, (...params: unknown[]) => unknown>)[method](...args);
} else if (method in IDS.API) {
return await IDS.API[method](...args);
} else {
throw new Error(`[worker] Unknown API method: ${method}`);
} }
} if (method in IDS.API) {
return await (IDS.API as Record<string, (...params: unknown[]) => unknown>)[method](...args);
}
throw new Error(`[worker] Unknown API method: ${method}`);
}
@@ -1,37 +1,56 @@
<script> <script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte.js"; import * as IDS from "$src/modules/api/ids.svelte";
import FacetEditor from './FacetEditor.svelte'; import FacetEditor from './FacetEditor.svelte';
import CreateFacetDropdown from "$src/components/CreateFacetDropdown.svelte"; import CreateFacetDropdown from "$src/components/CreateFacetDropdown.svelte";
import type { DocumentState, Facet, IdsDocument, Specification } from "$src/types/ids";
let { activeTab } = $props(); type FacetType = "entity" | "attribute" | "classification" | "partOf" | "property" | "material";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null); let activeDocument = $derived(
let documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null); IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ? );
activeDocument.specifications.specification[documentState.activeSpecification] : null); let documentState = $derived(
IDS.Module.activeDocument ? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState) : null
);
let activeSpecification = $derived(
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null
);
async function addFacet (facetType) { async function addFacet(facetType: FacetType) {
if (!activeSpecification) return; if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.createFacet( await IDS.createFacet(
IDS.Module.activeDocument, IDS.Module.activeDocument,
documentState.activeSpecification, documentState.activeSpecification ?? 0,
"applicability", "applicability",
facetType facetType
); );
} }
async function removeFacet(facetType, facetIndex) { async function removeFacet(facetType: FacetType, facetIndex: number) {
if (!activeSpecification) return; if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.deleteFacet( await IDS.deleteFacet(
IDS.Module.activeDocument, IDS.Module.activeDocument,
documentState.activeSpecification, documentState.activeSpecification ?? 0,
"applicability", "applicability",
facetType, facetType,
facetIndex facetIndex
); );
} }
let applicabilityEntries = $derived(
activeSpecification?.applicability
? (Object.entries(activeSpecification.applicability).filter(
([facetType, facets]) =>
facetType !== "@minOccurs" &&
facetType !== "@maxOccurs" &&
Array.isArray(facets)
) as Array<[FacetType, Facet[]]>)
: []
);
</script> </script>
<div class="restrictions-panel"> <div class="restrictions-panel">
@@ -40,22 +59,19 @@
<CreateFacetDropdown {addFacet} /> <CreateFacetDropdown {addFacet} />
</div> </div>
<div class="restrictions-list"> <div class="restrictions-list">
{#if activeSpecification?.applicability} {#if activeSpecification && applicabilityEntries.length > 0}
{#each Object.entries(activeSpecification.applicability) as [facetType, facets]} {#each applicabilityEntries as [facetType, facets]}
{#if facetType !== "@minOccurs" && facetType !== "@maxOccurs"} {#each facets as facet, index}
{#each facets as facet, index} <FacetEditor
<FacetEditor bind:facet={facets[index]}
bind:facet={facets[index]} {facetType}
{facetType} specification={activeSpecification}
specification={activeSpecification} activeTab="applicability"
activeTab="applicability" {removeFacet}
{removeFacet} {index}
{index} />
key={`${activeDocument}-${documentState?.activeSpecification}-applicability-${facetType}-${index}`} {/each}
/>
{/each}
{/if}
{/each} {/each}
{/if} {/if}
</div> </div>
</div> </div>
@@ -1,20 +1,40 @@
<script> <script lang="ts">
import RestrictionEditor from './RestrictionEditor.svelte'; import RestrictionEditor from './RestrictionEditor.svelte';
import {stringifyFacet} from "$src/modules/api/ids.svelte.js"; import {stringifyFacet} from "$src/modules/api/ids.svelte";
import type { Facet, Specification } from "$src/types/ids";
/** /**
* Applicability facets wont have: "@uri", "@instructions", "@cardinality" * Applicability facets wont have: "@uri", "@instructions", "@cardinality"
* @ in name --> simple string value * @ in name --> simple string value
* else --> can be simpleValue, Restriction or list of Restrictions * else --> can be simpleValue, Restriction or list of Restrictions
*/ */
let { facet = $bindable(), facetType, activeTab, removeFacet, index, specification } = $props(); type FacetType = "entity" | "attribute" | "classification" | "partOf" | "property" | "material";
const getSpecialProp = (prop) => { let {
return facet[prop] ?? ""; facet = $bindable<Facet>({}),
facetType,
activeTab,
removeFacet,
index,
specification
}: {
facet: Facet;
facetType: FacetType;
activeTab: "applicability" | "requirements";
removeFacet: (facetType: FacetType, facetIndex: number) => void | Promise<void>;
index: number;
specification: Specification;
} = $props();
const getSpecialProp = (prop: string) => {
const value = (facet as Record<string, unknown>)[prop];
return typeof value === "string" ? value : "";
}; };
const setSpecialProp = (prop, value) => { const setSpecialProp = (prop: string, value: string) => {
facet[prop] = value; (facet as Record<string, unknown>)[prop] = value;
}; };
let baseId = $derived(`facet-${facetType}-${index}`);
</script> </script>
<div class="restriction-item"> <div class="restriction-item">
@@ -48,8 +68,8 @@
<RestrictionEditor bind:facet={facet} fieldName="name" label="Entity Name" placeholder="e.g., IfcSpace" autocomplete="entityName" /> <RestrictionEditor bind:facet={facet} fieldName="name" label="Entity Name" placeholder="e.g., IfcSpace" autocomplete="entityName" />
<RestrictionEditor bind:facet={facet} fieldName="predefinedType" label="Predefined Type" placeholder="e.g., SOLIDWALL" autocomplete="predefinedType" /> <RestrictionEditor bind:facet={facet} fieldName="predefinedType" label="Predefined Type" placeholder="e.g., SOLIDWALL" autocomplete="predefinedType" />
<div class="form-group"> <div class="form-group">
<label>Relation</label> <label for={`${baseId}-relation`}>Relation</label>
<select class="form-input" bind:value={() => getSpecialProp("@relation"), (v) => setSpecialProp("@relation", v)}> <select class="form-input" id={`${baseId}-relation`} bind:value={() => getSpecialProp("@relation"), (v) => setSpecialProp("@relation", v)}>
<option value="">Select relation...</option> <option value="">Select relation...</option>
<option value="IFCRELAGGREGATES">IFCRELAGGREGATES</option> <option value="IFCRELAGGREGATES">IFCRELAGGREGATES</option>
<option value="IFCRELASSIGNSTOGROUP">IFCRELASSIGNSTOGROUP</option> <option value="IFCRELASSIGNSTOGROUP">IFCRELASSIGNSTOGROUP</option>
@@ -62,8 +82,8 @@
{#if activeTab === 'requirements'} {#if activeTab === 'requirements'}
{#if facetType !== 'entity'} {#if facetType !== 'entity'}
<div class="form-group"> <div class="form-group">
<label>Cardinality</label> <label for={`${baseId}-cardinality`}>Cardinality</label>
<select class="form-input" bind:value={() => getSpecialProp("@cardinality"), (v) => setSpecialProp("@cardinality", v)}> <select class="form-input" id={`${baseId}-cardinality`} bind:value={() => getSpecialProp("@cardinality"), (v) => setSpecialProp("@cardinality", v)}>
<option value="required">Required</option> <option value="required">Required</option>
<option value="optional">Optional</option> <option value="optional">Optional</option>
<option value="prohibited">Prohibited</option> <option value="prohibited">Prohibited</option>
@@ -71,9 +91,9 @@
</div> </div>
{/if} {/if}
<div class="form-group full-width"> <div class="form-group full-width">
<label>Instructions</label> <label for={`${baseId}-instructions`}>Instructions</label>
<textarea class="form-input" bind:value={() => getSpecialProp("@instructions"), (v) => setSpecialProp("@instructions", v)} placeholder="Optional instructions for IFC authors" rows="2"></textarea> <textarea class="form-input" id={`${baseId}-instructions`} bind:value={() => getSpecialProp("@instructions"), (v) => setSpecialProp("@instructions", v)} placeholder="Optional instructions for IFC authors" rows="2"></textarea>
</div> </div>
{/if} {/if}
</div> </div>
</div> </div>
@@ -1,13 +1,17 @@
<script> <script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte.js"; import * as IDS from "$src/modules/api/ids.svelte";
import type { IdsDocument, IdsInfo } from "$src/types/ids";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null); let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
);
const getProp = (prop) => { const getProp = (prop: keyof IdsInfo) => {
return activeDocument?.info[prop] ?? ""; return activeDocument?.info[prop] ?? "";
}; };
const setProp = (prop, value) => { const setProp = (prop: keyof IdsInfo, value: string) => {
if (!activeDocument) return;
activeDocument.info[prop] = value; activeDocument.info[prop] = value;
}; };
</script> </script>
@@ -18,36 +22,36 @@
</div> </div>
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label>Title</label> <label for="ids-title">Title</label>
<input class="form-input" type="text" bind:value={() => getProp("title"), (v) => setProp("title", v)} placeholder="Enter IDS title"> <input class="form-input" id="ids-title" type="text" bind:value={() => getProp("title"), (v) => setProp("title", v)} placeholder="Enter IDS title">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Author Email</label> <label for="ids-author">Author Email</label>
<input class="form-input" type="email" bind:value={() => getProp("author"), (v) => setProp("author", v)} placeholder="Enter author"> <input class="form-input" id="ids-author" type="email" bind:value={() => getProp("author"), (v) => setProp("author", v)} placeholder="Enter author">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Version</label> <label for="ids-version">Version</label>
<input class="form-input" type="text" bind:value={() => getProp("version"), (v) => setProp("version", v)} placeholder="Enter version"> <input class="form-input" id="ids-version" type="text" bind:value={() => getProp("version"), (v) => setProp("version", v)} placeholder="Enter version">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Date</label> <label for="ids-date">Date</label>
<input class="form-input" type="date" bind:value={() => getProp("date"), (v) => setProp("date", v)}> <input class="form-input" id="ids-date" type="date" bind:value={() => getProp("date"), (v) => setProp("date", v)}>
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label>Description</label> <label for="ids-description">Description</label>
<textarea class="form-input" bind:value={() => getProp("description"), (v) => setProp("description", v)} placeholder="Enter description" rows="3"></textarea> <textarea class="form-input" id="ids-description" bind:value={() => getProp("description"), (v) => setProp("description", v)} placeholder="Enter description" rows="3"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Purpose</label> <label for="ids-purpose">Purpose</label>
<input class="form-input" type="text" bind:value={() => getProp("purpose"), (v) => setProp("purpose", v)} placeholder="Enter purpose"> <input class="form-input" id="ids-purpose" type="text" bind:value={() => getProp("purpose"), (v) => setProp("purpose", v)} placeholder="Enter purpose">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Milestone</label> <label for="ids-milestone">Milestone</label>
<input class="form-input" type="text" bind:value={() => getProp("milestone"), (v) => setProp("milestone", v)} placeholder="Enter milestone"> <input class="form-input" id="ids-milestone" type="text" bind:value={() => getProp("milestone"), (v) => setProp("milestone", v)} placeholder="Enter milestone">
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label>Copyright</label> <label for="ids-copyright">Copyright</label>
<input class="form-input" type="text" bind:value={() => getProp("copyright"), (v) => setProp("copyright", v)} placeholder="Enter copyright"> <input class="form-input" id="ids-copyright" type="text" bind:value={() => getProp("copyright"), (v) => setProp("copyright", v)} placeholder="Enter copyright">
</div> </div>
</div> </div>
</div> </div>
@@ -1,18 +1,28 @@
<script> <script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte.js"; import * as IDS from "$src/modules/api/ids.svelte";
import { getAuditReportById, downloadAuditReport } from "$src/modules/api/api.svelte.js"; import { getAuditReportById, downloadAuditReport } from "$src/modules/api/api.svelte";
import { error, success } from "$src/modules/utils/toast.svelte.js"; import { error, success } from "$src/modules/utils/toast.svelte";
import * as Tooltip from "$src/lib/components/ui/tooltip"; import * as Tooltip from "$src/lib/components/ui/tooltip";
import type { AuditReport, AuditReportData } from "$src/types/report";
import type { DocumentState, Facet, IdsDocument, Specification } from "$src/types/ids";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null); let activeDocument = $derived(
let documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null); IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
let auditReport = $derived(documentState?.auditReport ? getAuditReportById(documentState.auditReport) : null); );
let expandedSpecs = $state(new Set()); let documentState = $derived(
let expandedRequirements = $state(new Set()); IDS.Module.activeDocument ? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState) : null
);
let auditReport = $derived(
documentState?.auditReport ? (getAuditReportById(documentState.auditReport) as AuditReport | undefined) : null
);
let expandedSpecs = $state(new Set<number>());
let expandedRequirements = $state(new Set<string>());
let allExpanded = $state(false); let allExpanded = $state(false);
type SpecificationStatus = boolean | 'skipped' | null;
// Open Editor mode and jump to a specific specification // Open Editor mode and jump to a specific specification
function editSpecification(index) { function editSpecification(index: number) {
if (IDS.Module.activeDocument) { if (IDS.Module.activeDocument) {
IDS.setDocumentState(IDS.Module.activeDocument, { IDS.setDocumentState(IDS.Module.activeDocument, {
viewMode: 'editor', viewMode: 'editor',
@@ -23,13 +33,13 @@
} }
} }
function toggleSpecification(index) { function toggleSpecification(index: number) {
if (expandedSpecs.has(index)) { if (expandedSpecs.has(index)) {
expandedSpecs.delete(index); expandedSpecs.delete(index);
} else { } else {
expandedSpecs.add(index); expandedSpecs.add(index);
} }
expandedSpecs = new Set(expandedSpecs); expandedSpecs = new Set<number>(expandedSpecs);
} }
function toggleAllSpecifications() { function toggleAllSpecifications() {
@@ -37,7 +47,7 @@
if (allExpanded) { if (allExpanded) {
// Collapse all // Collapse all
expandedSpecs = new Set(); expandedSpecs = new Set<number>();
allExpanded = false; allExpanded = false;
} else { } else {
// Expand all // Expand all
@@ -47,26 +57,26 @@
} }
} }
function getSpecificationStatus(specIndex, auditData) { function getSpecificationStatus(specIndex: number, auditData: AuditReportData): SpecificationStatus {
const spec = auditData.specifications[specIndex]; const spec = auditData.specifications[specIndex];
if (!spec) return null; if (!spec) return null;
return spec.is_skipped ? 'skipped' : spec.status; return spec.is_skipped ? 'skipped' : spec.status;
} }
function getSpecificationStats(specIndex, auditData) { function getSpecificationStats(specIndex: number, auditData: AuditReportData) {
const spec = auditData.specifications[specIndex]; const spec = auditData.specifications[specIndex];
if (!spec) return null; if (!spec) return null;
return { return {
requirements: `${spec.total_requirements || 0}`, requirements: spec.total_requirements || 0,
requirementsPassed: `${spec.total_requirements_pass || 0}`, requirementsPassed: spec.total_requirements_pass || 0,
checksTotal: `${spec.total_checks || 0}`, checksTotal: spec.total_checks || 0,
checksPassed: `${spec.total_checks_pass || 0}`, checksPassed: spec.total_checks_pass || 0,
applicableTotal: `${spec.total_applicable || 0}`, applicableTotal: spec.total_applicable || 0,
applicablePassed: `${spec.total_applicable_pass || 0}` applicablePassed: spec.total_applicable_pass || 0
}; };
} }
function getSpecificationReason(specIndex, auditData) { function getSpecificationReason(specIndex: number, auditData: AuditReportData) {
const spec = auditData.specifications[specIndex]; const spec = auditData.specifications[specIndex];
if (!spec) return null; if (!spec) return null;
@@ -90,6 +100,10 @@
return null; // No reason needed for passed specifications return null; // No reason needed for passed specifications
} }
function getDocumentSpecificationUsage(spec: Specification) {
return IDS.getSpecUsage(spec);
}
async function handleDownloadReport() { async function handleDownloadReport() {
if (!auditReport) return; if (!auditReport) return;
@@ -97,29 +111,62 @@
await downloadAuditReport(auditReport.id); await downloadAuditReport(auditReport.id);
success('Audit report downloaded successfully'); success('Audit report downloaded successfully');
} catch (err) { } catch (err) {
error(`Failed to download report: ${err.message}`); const message = err instanceof Error ? err.message : String(err);
error(`Failed to download report: ${message}`);
} }
} }
function getRequirementStatus(specIndex, reqIndex, auditData) { function getRequirementStatus(specIndex: number, reqIndex: number, auditData: AuditReportData) {
const spec = auditData.specifications[specIndex]; const spec = auditData.specifications[specIndex];
if (!spec || !spec.requirements || !spec.requirements[reqIndex]) return null; if (!spec || !spec.requirements || !spec.requirements[reqIndex]) return null;
return spec.requirements[reqIndex]; return spec.requirements[reqIndex];
} }
function toggleRequirementDetails(specIndex, reqIndex) { type RequirementGroup = {
facetType: string;
items: { facet: Facet; reqIndex: number }[];
};
function getRequirementGroups(spec: Specification | undefined | null): RequirementGroup[] {
if (!spec?.requirements) return [];
const groups: RequirementGroup[] = [];
let reqIndex = 0;
for (const [facetType, facets] of Object.entries(spec.requirements)) {
if (!Array.isArray(facets) || facets.length === 0) continue;
groups.push({
facetType,
items: facets.map((facet) => ({
facet,
reqIndex: reqIndex++
}))
});
}
return groups;
}
function toggleRequirementDetails(specIndex: number, reqIndex: number) {
const key = `${specIndex}-${reqIndex}`; const key = `${specIndex}-${reqIndex}`;
if (expandedRequirements.has(key)) { if (expandedRequirements.has(key)) {
expandedRequirements.delete(key); expandedRequirements.delete(key);
} else { } else {
expandedRequirements.add(key); expandedRequirements.add(key);
} }
expandedRequirements = new Set(expandedRequirements); expandedRequirements = new Set<string>(expandedRequirements);
} }
function isRequirementDetailsExpanded(specIndex, reqIndex) { function isRequirementDetailsExpanded(specIndex: number, reqIndex: number) {
return expandedRequirements.has(`${specIndex}-${reqIndex}`); return expandedRequirements.has(`${specIndex}-${reqIndex}`);
} }
const handleActivation = (event: KeyboardEvent, action: () => void) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
action();
}
};
</script> </script>
<div class="ids-viewer"> <div class="ids-viewer">
@@ -207,8 +254,17 @@
</button> </button>
</div> </div>
{#each activeDocument.specifications.specification as spec, index} {#each activeDocument.specifications.specification as spec, index}
{@const usage = getDocumentSpecificationUsage(spec)}
{@const requirementGroups = getRequirementGroups(spec)}
<div class="specification-card {auditReport ? 'with-audit' : ''} {auditReport && getSpecificationStatus(index, auditReport.data) !== null ? (getSpecificationStatus(index, auditReport.data) === 'skipped' ? 'spec-skipped' : (getSpecificationStatus(index, auditReport.data) ? 'spec-pass' : 'spec-fail')) : ''}"> <div class="specification-card {auditReport ? 'with-audit' : ''} {auditReport && getSpecificationStatus(index, auditReport.data) !== null ? (getSpecificationStatus(index, auditReport.data) === 'skipped' ? 'spec-skipped' : (getSpecificationStatus(index, auditReport.data) ? 'spec-pass' : 'spec-fail')) : ''}">
<div class="spec-card-header" onclick={() => toggleSpecification(index)}> <div
class="spec-card-header"
role="button"
tabindex="0"
aria-expanded={expandedSpecs.has(index)}
onclick={() => toggleSpecification(index)}
onkeydown={(event) => handleActivation(event, () => toggleSpecification(index))}
>
<div class="spec-title-section"> <div class="spec-title-section">
<div class="spec-title-row"> <div class="spec-title-row">
<h2>{spec["@name"] || `Specification ${index + 1}`}</h2> <h2>{spec["@name"] || `Specification ${index + 1}`}</h2>
@@ -237,19 +293,19 @@
<p class="spec-description">{spec["@description"]}</p> <p class="spec-description">{spec["@description"]}</p>
{/if} {/if}
<div class="spec-stats"> <div class="spec-stats">
{#if spec.applicability["@minOccurs"] === 1 && spec.applicability["@maxOccurs"] === 'unbounded'} {#if usage === 'required'}
<span class="stat-item">Required</span> <span class="stat-item">Required</span>
{/if} {/if}
{#if spec.applicability["@minOccurs"] === 0 && spec.applicability["@maxOccurs"] === 'unbounded'} {#if usage === 'optional'}
<span class="stat-item">Optional</span> <span class="stat-item">Optional</span>
{/if} {/if}
{#if spec.applicability["@minOccurs"] === 0 && spec.applicability["@maxOccurs"] === 0} {#if usage === 'prohibited'}
<span class="stat-item">Prohibited</span> <span class="stat-item">Prohibited</span>
{/if} {/if}
{#if auditReport} {#if auditReport}
{@const stats = getSpecificationStats(index, auditReport.data)} {@const stats = getSpecificationStats(index, auditReport.data)}
{@const status = getSpecificationStatus(index, auditReport.data)} {@const status = getSpecificationStatus(index, auditReport.data)}
{#if stats && spec.applicability["@maxOccurs"] !== 0 && status !== 'skipped'} {#if stats && usage !== 'prohibited' && status !== 'skipped'}
<span class="stat-item">Checks: {stats.checksPassed}/{stats.checksTotal}</span> <span class="stat-item">Checks: {stats.checksPassed}/{stats.checksTotal}</span>
<span class="stat-item">Requirements: {stats.requirementsPassed}/{stats.requirements}</span> <span class="stat-item">Requirements: {stats.requirementsPassed}/{stats.requirements}</span>
{/if} {/if}
@@ -302,12 +358,13 @@
{#if auditReport} {#if auditReport}
{@const status = getSpecificationStatus(index, auditReport.data)} {@const status = getSpecificationStatus(index, auditReport.data)}
{#if ! status && spec.applicability["@maxOccurs"] == 0} {#if status === false && usage === 'prohibited'}
{@const specReport = auditReport.data.specifications[index]} {@const specReport = auditReport.data.specifications[index]}
{@const applicableEntities = specReport.applicable_entities ?? []}
<div class="entity-tables"> <div class="entity-tables">
{#if specReport.applicable_entities && specReport.applicable_entities.length > 0} {#if applicableEntities.length > 0}
<div class="entity-table-section fail"> <div class="entity-table-section fail">
<h4>Failed Elements ({specReport.applicable_entities.length})</h4> <h4>Failed Elements ({applicableEntities.length})</h4>
<div class="entity-table-container"> <div class="entity-table-container">
<Tooltip.Provider> <Tooltip.Provider>
<table class="entity-table"> <table class="entity-table">
@@ -323,7 +380,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each specReport.applicable_entities.slice(0, 10) as entity} {#each applicableEntities.slice(0, 10) as entity}
<tr> <tr>
<td>{entity.class}</td> <td>{entity.class}</td>
<td>{entity.predefined_type || '-'}</td> <td>{entity.predefined_type || '-'}</td>
@@ -379,9 +436,9 @@
</td> </td>
</tr> </tr>
{/each} {/each}
{#if specReport.applicable_entities.length > 10} {#if applicableEntities.length > 10}
<tr class="more-row"> <tr class="more-row">
<td colspan="7">... {specReport.applicable_entities.length - 10} more failing elements not shown ...</td> <td colspan="7">... {applicableEntities.length - 10} more failing elements not shown ...</td>
</tr> </tr>
{/if} {/if}
</tbody> </tbody>
@@ -397,37 +454,36 @@
</div> </div>
<!-- Requirements Section --> <!-- Requirements Section -->
{#if Array.isArray(spec.requirements) && spec.requirements.length > 0} {#if requirementGroups.length > 0}
<div class="facet-section"> <div class="facet-section">
<h3>Requirements</h3> <h3>Requirements</h3>
<div class="facets-list"> <div class="facets-list">
{#each Object.entries(spec.requirements || {}) as [facetType, facets]} {#each requirementGroups as group}
{#if Array.isArray(facets) && facets.length > 0} <div class="facet-group">
<div class="facet-group"> {#each group.items as item}
{#each facets as facet, facetIndex} {@const reqAuditData = auditReport ? getRequirementStatus(index, item.reqIndex, auditReport.data) : null}
{@const reqAuditData = auditReport ? getRequirementStatus(index, facetIndex, auditReport.data) : null} {@const specStatus = auditReport ? getSpecificationStatus(index, auditReport.data) : null}
{@const specStatus = auditReport ? getSpecificationStatus(index, auditReport.data) : null} <div class="facet-item {auditReport && reqAuditData && specStatus !== 'skipped' ? (reqAuditData.status ? 'audit-pass' : 'audit-fail') : ''}">
<div class="facet-item {auditReport && reqAuditData && specStatus !== 'skipped' ? (reqAuditData.status ? 'audit-pass' : 'audit-fail') : ''}"> <button class="facet-header" onclick={() => {if (auditReport && reqAuditData && specStatus !== 'skipped') toggleRequirementDetails(index, item.reqIndex)}}>
<button class="facet-header" onclick={() => {if (auditReport && reqAuditData && specStatus !== 'skipped') toggleRequirementDetails(index, facetIndex)}}> <span class="facet-bullet"></span>
<span class="facet-bullet"></span> <span class="facet-text">{@html IDS.stringifyFacet("requirements", item.facet, group.facetType, spec)}</span>
<span class="facet-text">{@html IDS.stringifyFacet("requirements", facet, facetType, spec)}</span> {#if auditReport && reqAuditData && specStatus !== 'skipped'}
{#if auditReport && reqAuditData && specStatus !== 'skipped'} {#if reqAuditData.total_applicable > 0}
{#if reqAuditData.total_applicable > 0} <div class="audit-details-toggle">
<div class="audit-details-toggle"> {reqAuditData.status ? 'PASS' : 'FAIL'} ({reqAuditData.total_pass}/{reqAuditData.total_applicable})
{reqAuditData.status ? 'PASS' : 'FAIL'} ({reqAuditData.total_pass}/{reqAuditData.total_applicable}) <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class:rotated={isRequirementDetailsExpanded(index, item.reqIndex)}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class:rotated={isRequirementDetailsExpanded(index, facetIndex)}> <polyline points="6,9 12,15 18,9"></polyline>
<polyline points="6,9 12,15 18,9"></polyline> </svg>
</svg> </div>
</div> {:else}
{:else} <span class="audit-status-badge">
<span class="audit-status-badge"> {reqAuditData.status ? 'PASS' : 'FAIL'}
{reqAuditData.status ? 'PASS' : 'FAIL'} </span>
</span>
{/if}
{/if} {/if}
</button> {/if}
{#if isRequirementDetailsExpanded(index, facetIndex)} </button>
{#if reqAuditData && isRequirementDetailsExpanded(index, item.reqIndex)}
<div class="facet-expansion"> <div class="facet-expansion">
<div class="entity-tables"> <div class="entity-tables">
{#if reqAuditData.passed_entities && reqAuditData.passed_entities.length > 0} {#if reqAuditData.passed_entities && reqAuditData.passed_entities.length > 0}
@@ -592,11 +648,10 @@
{/if} {/if}
</div> </div>
</div> </div>
{/if} {/if}
</div> </div>
{/each} {/each}
</div> </div>
{/if}
{/each} {/each}
</div> </div>
</div> </div>
@@ -1,37 +1,53 @@
<script> <script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte.js"; import * as IDS from "$src/modules/api/ids.svelte";
import FacetEditor from './FacetEditor.svelte'; import FacetEditor from './FacetEditor.svelte';
import CreateFacetDropdown from "$src/components/CreateFacetDropdown.svelte"; import CreateFacetDropdown from "$src/components/CreateFacetDropdown.svelte";
import type { DocumentState, Facet, IdsDocument, Specification } from "$src/types/ids";
let { activeTab } = $props(); type FacetType = "entity" | "attribute" | "classification" | "partOf" | "property" | "material";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null); let activeDocument = $derived(
let documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null); IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ? );
activeDocument.specifications.specification[documentState.activeSpecification] : null); let documentState = $derived(
IDS.Module.activeDocument ? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState) : null
);
let activeSpecification = $derived(
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null
);
async function addFacet (facetType) { async function addFacet(facetType: FacetType) {
if (!activeSpecification) return; if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.createFacet( await IDS.createFacet(
IDS.Module.activeDocument, IDS.Module.activeDocument,
documentState.activeSpecification, documentState.activeSpecification ?? 0,
"requirements", "requirements",
facetType facetType
); );
} }
async function removeFacet(facetType, facetIndex) { async function removeFacet(facetType: FacetType, facetIndex: number) {
if (!activeSpecification) return; if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.deleteFacet( await IDS.deleteFacet(
IDS.Module.activeDocument, IDS.Module.activeDocument,
documentState.activeSpecification, documentState.activeSpecification ?? 0,
"requirements", "requirements",
facetType, facetType,
facetIndex facetIndex
); );
} }
let requirementEntries = $derived(
activeSpecification?.requirements
? (Object.entries(activeSpecification.requirements).filter(([, facets]) =>
Array.isArray(facets)
) as Array<[FacetType, Facet[]]>)
: []
);
</script> </script>
<div class="restrictions-panel"> <div class="restrictions-panel">
@@ -40,8 +56,8 @@
<CreateFacetDropdown {addFacet} /> <CreateFacetDropdown {addFacet} />
</div> </div>
<div class="restrictions-list"> <div class="restrictions-list">
{#if activeSpecification?.requirements} {#if activeSpecification && requirementEntries.length > 0}
{#each Object.entries(activeSpecification.requirements) as [facetType, facets]} {#each requirementEntries as [facetType, facets]}
{#each facets as facet, index} {#each facets as facet, index}
<FacetEditor <FacetEditor
bind:facet={facets[index]} bind:facet={facets[index]}
@@ -50,10 +66,9 @@
activeTab="requirements" activeTab="requirements"
{removeFacet} {removeFacet}
{index} {index}
key={`${activeDocument}-${documentState?.activeSpecification}-requirements-${facetType}-${index}`}
/> />
{/each} {/each}
{/each} {/each}
{/if} {/if}
</div> </div>
</div> </div>
@@ -1,34 +1,86 @@
<script> <script lang="ts">
import Svelecte from 'svelecte'; import Svelecte from 'svelecte';
import { getEntityClasses, getMaterialCategories, getClassificationSystems, getDataTypes, getPredefinedTypes, getEntityAttributes, getApplicablePsets } from '$src/modules/api/api.svelte.js'; import {
import * as IDS from '$src/modules/api/ids.svelte.js'; getApplicablePsets,
getClassificationSystems,
getDataTypes,
getEntityAttributes,
getEntityClasses,
getMaterialCategories,
getPredefinedTypes
} from '$src/modules/api/api.svelte';
import * as IDS from '$src/modules/api/ids.svelte';
import type { DocumentState, Facet, FacetValue, IdsDocument, Restriction, RestrictionValue, Specification } from '$src/types/ids';
let { facet = $bindable(), fieldName, label, placeholder, autocomplete = null, isSpecialProp = false } = $props(); type AutocompleteType =
| 'entityName'
const isEntityNameField = autocomplete === 'entityName'; | 'material'
const isMaterialField = autocomplete === 'material'; | 'classificationSystem'
const isClassificationSystemField = autocomplete === 'classificationSystem'; | 'predefinedType'
const isPredefinedTypeField = autocomplete === 'predefinedType'; | 'attributeName'
const isAttributeNameField = autocomplete === 'attributeName'; | 'propertySet'
const isPropertySetField = autocomplete === 'propertySet'; | 'dataType'
const isDataTypeField = autocomplete === 'dataType'; | null;
let {
facet = $bindable<Facet>({}),
fieldName,
label,
placeholder,
autocomplete = null,
isSpecialProp = false
}: {
facet: Facet;
fieldName: string;
label: string;
placeholder: string;
autocomplete?: AutocompleteType;
isSpecialProp?: boolean;
} = $props();
let isEntityNameField = $derived(autocomplete === 'entityName');
let isMaterialField = $derived(autocomplete === 'material');
let isClassificationSystemField = $derived(autocomplete === 'classificationSystem');
let isPredefinedTypeField = $derived(autocomplete === 'predefinedType');
let isAttributeNameField = $derived(autocomplete === 'attributeName');
let isPropertySetField = $derived(autocomplete === 'propertySet');
let isDataTypeField = $derived(autocomplete === 'dataType');
const uniqueId = Math.random().toString(36).slice(2, 8);
let baseId = $derived(`restriction-${fieldName}-${uniqueId}`);
const activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
);
const documentState = $derived(
IDS.Module.activeDocument ? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState) : null
);
const activeSpecification = $derived(
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null
);
const getFieldValue = () => (facet as Record<string, unknown>)[fieldName] as FacetValue | string | undefined;
const setFieldValue = (value: FacetValue | string) => {
(facet as Record<string, unknown>)[fieldName] = value;
};
const getFacetValue = () => {
const value = getFieldValue();
return typeof value === 'object' && value !== null ? (value as FacetValue) : undefined;
};
// Predefined Types autocompletions // Predefined Types autocompletions
let predefinedTypeOptions = $state([]); let predefinedTypeOptions: string[] = $state([]);
// Attribute Names autocompletions // Attribute Names autocompletions
let attributeNameOptions = $state([]); let attributeNameOptions: string[] = $state([]);
// Property Sets autocompletions // Property Sets autocompletions
let propertySetOptions = $state([]); let propertySetOptions: string[] = $state([]);
// Get the active specification's IFC schemas // Get the active specification's IFC schemas
const getIfcVersions = () => { const getIfcVersions = () => {
const activeDocument = IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null;
const documentState = IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null;
const activeSpecification = activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null;
const versions = activeSpecification?.["@ifcVersion"] || ['IFC4']; const versions = activeSpecification?.["@ifcVersion"] || ['IFC4'];
// TODO: Fix: Filter out IFC4X3 because it's buggy // TODO: Fix: Filter out IFC4X3 because it's buggy
return versions.filter(version => version !== 'IFC4X3_ADD2'); return versions.filter(version => version !== 'IFC4X3_ADD2');
@@ -36,7 +88,7 @@
// Get the entity name from facet // Get the entity name from facet
const getEntityName = () => { const getEntityName = () => {
const nameField = facet?.name; const nameField = (facet as Record<string, unknown>)?.name as FacetValue | undefined;
if (!nameField) return ''; if (!nameField) return '';
if (nameField.simpleValue) return nameField.simpleValue; if (nameField.simpleValue) return nameField.simpleValue;
return ''; return '';
@@ -44,18 +96,13 @@
// Get all entity names from Entity facets in Applicability // Get all entity names from Entity facets in Applicability
const getApplicabilityEntityNames = () => { const getApplicabilityEntityNames = () => {
const activeDocument = IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null;
const documentState = IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null;
const activeSpecification = activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null;
if (!activeSpecification?.applicability?.entity) return []; if (!activeSpecification?.applicability?.entity) return [];
const entityFacets = activeSpecification.applicability.entity; const entityFacets = activeSpecification.applicability.entity as Facet[];
const entityNames = []; const entityNames: string[] = [];
entityFacets.forEach(entityFacet => { entityFacets.forEach(entityFacet => {
const nameField = entityFacet.name; const nameField = (entityFacet as Record<string, unknown>).name as FacetValue | undefined;
if (!nameField) return; if (!nameField) return;
if (nameField.simpleValue) { if (nameField.simpleValue) {
@@ -74,19 +121,14 @@
// Get entity facets with their predefined types // Get entity facets with their predefined types
const getApplicabilityEntityFacets = () => { const getApplicabilityEntityFacets = () => {
const activeDocument = IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null;
const documentState = IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null;
const activeSpecification = activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null;
if (!activeSpecification?.applicability?.entity) return []; if (!activeSpecification?.applicability?.entity) return [];
return activeSpecification.applicability.entity.map(entityFacet => { return (activeSpecification.applicability.entity as Facet[]).map(entityFacet => {
const nameField = entityFacet.name; const nameField = (entityFacet as Record<string, unknown>).name as FacetValue | undefined;
const predefinedTypeField = entityFacet.predefinedType; const predefinedTypeField = (entityFacet as Record<string, unknown>).predefinedType as FacetValue | undefined;
// Extract entity name // Extract entity name
let entityNames = []; let entityNames: string[] = [];
if (nameField?.simpleValue) { if (nameField?.simpleValue) {
entityNames = [nameField.simpleValue]; entityNames = [nameField.simpleValue];
} else if (nameField?.restriction?.enumeration) { } else if (nameField?.restriction?.enumeration) {
@@ -99,9 +141,12 @@
let predefinedType = ''; let predefinedType = '';
if (predefinedTypeField?.simpleValue) { if (predefinedTypeField?.simpleValue) {
predefinedType = predefinedTypeField.simpleValue; predefinedType = predefinedTypeField.simpleValue;
} else if (predefinedTypeField?.restriction?.enumeration?.length > 0) { } else {
// For enumeration predefined types, use the first one or empty string const enumValues = predefinedTypeField?.restriction?.enumeration;
predefinedType = predefinedTypeField.restriction.enumeration[0]['@value'] || ''; if (enumValues && enumValues.length > 0) {
// For enumeration predefined types, use the first one or empty string
predefinedType = enumValues[0]['@value'] || '';
}
} }
return { entityNames, predefinedType }; return { entityNames, predefinedType };
@@ -109,7 +154,8 @@
}; };
// Contextual autocompletions // Contextual autocompletions
$effect(async () => { $effect(() => {
void (async () => {
// Predefined Types // Predefined Types
if (isPredefinedTypeField) { if (isPredefinedTypeField) {
@@ -122,10 +168,10 @@
const typePromises = ifcVersions.map(schema => const typePromises = ifcVersions.map(schema =>
getPredefinedTypes(schema, entityName.toUpperCase()) getPredefinedTypes(schema, entityName.toUpperCase())
); );
const typeSets = await Promise.all(typePromises); const typeSets = await Promise.all(typePromises) as string[][];
// Deduplicate predefined types across all schemas // Deduplicate predefined types across all schemas
const allTypes = new Set(); const allTypes = new Set<string>();
typeSets.forEach(types => { typeSets.forEach(types => {
if (types && Array.isArray(types)) { if (types && Array.isArray(types)) {
types.forEach(type => allTypes.add(type)); types.forEach(type => allTypes.add(type));
@@ -150,19 +196,19 @@
if (entityNames.length > 0 && ifcVersions.length > 0) { if (entityNames.length > 0 && ifcVersions.length > 0) {
try { try {
// Fetch attributes for all entity names across all selected schemas // Fetch attributes for all entity names across all selected schemas
const attributePromises = []; const attributePromises: Array<Promise<{ name: string }[]>> = [];
entityNames.forEach(entityName => { entityNames.forEach(entityName => {
ifcVersions.forEach(schema => { ifcVersions.forEach(schema => {
attributePromises.push( attributePromises.push(
getEntityAttributes(schema, entityName.toUpperCase()) getEntityAttributes(schema, entityName.toUpperCase()) as Promise<{ name: string }[]>
); );
}); });
}); });
const attributeSets = await Promise.all(attributePromises); const attributeSets = await Promise.all(attributePromises) as { name: string }[][];
// Deduplicate attribute names across all entities and schemas // Deduplicate attribute names across all entities and schemas
const allAttributes = new Set(); const allAttributes = new Set<string>();
attributeSets.forEach(attributes => { attributeSets.forEach(attributes => {
if (attributes && Array.isArray(attributes)) { if (attributes && Array.isArray(attributes)) {
attributes.forEach(attr => { attributes.forEach(attr => {
@@ -191,21 +237,21 @@
if (entityFacets.length > 0 && ifcVersions.length > 0) { if (entityFacets.length > 0 && ifcVersions.length > 0) {
try { try {
// Fetch applicable property sets for all entity facets across all selected schemas // Fetch applicable property sets for all entity facets across all selected schemas
const psetPromises = []; const psetPromises: Array<Promise<string[]>> = [];
entityFacets.forEach(facet => { entityFacets.forEach(facet => {
facet.entityNames.forEach(entityName => { facet.entityNames.forEach(entityName => {
ifcVersions.forEach(schema => { ifcVersions.forEach(schema => {
psetPromises.push( psetPromises.push(
getApplicablePsets(schema, entityName.toUpperCase(), facet.predefinedType) getApplicablePsets(schema, entityName.toUpperCase(), facet.predefinedType) as Promise<string[]>
); );
}); });
}); });
}); });
const psetSets = await Promise.all(psetPromises); const psetSets = await Promise.all(psetPromises) as string[][];
// Deduplicate property set names // Deduplicate property set names
const allPsets = new Set(); const allPsets = new Set<string>();
psetSets.forEach(psets => { psetSets.forEach(psets => {
if (psets && Array.isArray(psets)) { if (psets && Array.isArray(psets)) {
psets.forEach(pset => allPsets.add(pset)); psets.forEach(pset => allPsets.add(pset));
@@ -221,10 +267,11 @@
propertySetOptions = []; propertySetOptions = [];
} }
} }
})();
}); });
// Get autocomplete options based on field type // Get autocomplete options based on field type
const getAutocompleteOptions = () => { const getAutocompleteOptions = (): string[] => {
if (isEntityNameField) return getEntityClasses(); if (isEntityNameField) return getEntityClasses();
if (isMaterialField) return getMaterialCategories(); if (isMaterialField) return getMaterialCategories();
if (isClassificationSystemField) return Object.keys(getClassificationSystems()); if (isClassificationSystemField) return Object.keys(getClassificationSystems());
@@ -236,11 +283,11 @@
}; };
const getRestrictionType = () => { const getRestrictionType = () => {
const fieldValue = facet[fieldName]; const fieldValue = getFacetValue();
if (!fieldValue) return 'Simple'; if (!fieldValue) return 'Simple';
if (fieldValue.simpleValue !== undefined) return 'Simple'; if (fieldValue.simpleValue !== undefined) return 'Simple';
if (fieldValue['restriction']) { if (fieldValue['restriction']) {
const restriction = fieldValue['restriction']; const restriction = fieldValue['restriction'] as Restriction;
if (restriction['enumeration']) return 'Enumeration'; if (restriction['enumeration']) return 'Enumeration';
if (restriction['pattern']) return 'Pattern'; if (restriction['pattern']) return 'Pattern';
if (restriction['minInclusive'] || restriction['maxInclusive'] || if (restriction['minInclusive'] || restriction['maxInclusive'] ||
@@ -251,39 +298,40 @@
return 'Simple'; return 'Simple';
}; };
const getSimpleValue = () => { const getSimpleValue = (): string => {
const fieldValue = facet[fieldName]; const fieldValue = getFieldValue();
// For special properties (eg. @dataType), we return the value directly // For special properties (eg. @dataType), we return the value directly
if (isSpecialProp) return fieldValue || null; if (typeof fieldValue === 'string') return fieldValue;
if (isSpecialProp) return '';
if (!fieldValue) return null; if (!fieldValue) return '';
if (fieldValue.simpleValue !== undefined) return fieldValue.simpleValue; if (fieldValue.simpleValue !== undefined) return fieldValue.simpleValue;
return null; return '';
}; };
const getEnumerationValues = () => { const getEnumerationValues = () => {
const fieldValue = facet[fieldName]; const fieldValue = getFacetValue();
if (!fieldValue?.['restriction']) return ['']; if (!fieldValue?.['restriction']) return [''];
const restriction = fieldValue['restriction']; const restriction = fieldValue['restriction'] as Restriction;
const enumValues = restriction['enumeration']; const enumValues = restriction['enumeration'] as RestrictionValue[] | undefined;
if (!enumValues) return ['']; if (!enumValues) return [''];
return enumValues.map(item => item['@value'] || ''); return enumValues.map(item => item['@value'] || '');
}; };
const getPatternValue = () => { const getPatternValue = () => {
const fieldValue = facet[fieldName]; const fieldValue = getFacetValue();
if (!fieldValue?.['restriction']) return ''; if (!fieldValue?.['restriction']) return '';
const restriction = fieldValue['restriction']; const restriction = fieldValue['restriction'] as Restriction;
const pattern = restriction['pattern']; const pattern = restriction['pattern'] as RestrictionValue[] | undefined;
if (!pattern || !pattern.length) return ''; if (!pattern || !pattern.length) return '';
return pattern[0]['@value'] || ''; return pattern[0]['@value'] || '';
}; };
const getRangeValues = () => { const getRangeValues = () => {
const fieldValue = facet[fieldName]; const fieldValue = getFacetValue();
if (!fieldValue?.['restriction']) return { min: '', max: '', minType: 'Inclusive', maxType: 'Inclusive' }; if (!fieldValue?.['restriction']) return { min: '', max: '', minType: 'Inclusive', maxType: 'Inclusive' };
const restriction = fieldValue['restriction']; const restriction = fieldValue['restriction'] as Restriction;
let min = '', max = '', minType = 'Inclusive', maxType = 'Inclusive'; let min = '', max = '', minType = 'Inclusive', maxType = 'Inclusive';
@@ -307,18 +355,18 @@
}; };
const getLengthValue = () => { const getLengthValue = () => {
const fieldValue = facet[fieldName]; const fieldValue = getFacetValue();
if (!fieldValue?.['restriction']) return ''; if (!fieldValue?.['restriction']) return '';
const restriction = fieldValue['restriction']; const restriction = fieldValue['restriction'] as Restriction;
const length = restriction['length']; const length = restriction['length'] as RestrictionValue[] | undefined;
if (!length || !length.length) return ''; if (!length || !length.length) return '';
return length[0]['@value'] || ''; return length[0]['@value'] || '';
}; };
const getLengthRangeValues = () => { const getLengthRangeValues = () => {
const fieldValue = facet[fieldName]; const fieldValue = getFacetValue();
if (!fieldValue?.['restriction']) return { min: '', max: '' }; if (!fieldValue?.['restriction']) return { min: '', max: '' };
const restriction = fieldValue['restriction']; const restriction = fieldValue['restriction'] as Restriction;
let min = '', max = ''; let min = '', max = '';
@@ -332,77 +380,85 @@
return { min, max }; return { min, max };
}; };
const setSimpleValue = (value) => { const setSimpleValue = (value: string) => {
// For special properties (eg. @dataType), we set the value directly // For special properties (eg. @dataType), we set the value directly
if (isSpecialProp) { if (isSpecialProp) {
facet[fieldName] = value; setFieldValue(value);
return; return;
} }
if (!facet[fieldName]) facet[fieldName] = {}; setFieldValue({ simpleValue: value });
facet[fieldName] = { simpleValue: value };
}; };
const setEnumerationValues = (values) => { const setEnumerationValues = (values: string[]) => {
if (!facet[fieldName]) facet[fieldName] = {};
const enumItems = values.filter(v => v && typeof v === 'string' && v.trim() !== '').map(v => ({ '@value': v })); const enumItems = values.filter(v => v && typeof v === 'string' && v.trim() !== '').map(v => ({ '@value': v }));
facet[fieldName] = { setFieldValue({
'restriction': { 'restriction': {
'@base': 'xs:string', '@base': 'xs:string',
'enumeration': enumItems 'enumeration': enumItems
} }
}; });
}; };
const setPatternValue = (value) => { const setPatternValue = (value: string) => {
if (!facet[fieldName]) facet[fieldName] = {}; setFieldValue({
facet[fieldName] = {
'restriction': { 'restriction': {
'@base': 'xs:string', '@base': 'xs:string',
'pattern': [{ '@value': value }] 'pattern': [{ '@value': value }]
} }
}; });
}; };
const setRangeValues = (min, max, minType, maxType) => { const setRangeValues = (min: string, max: string, minType: string, maxType: string) => {
if (!facet[fieldName]) facet[fieldName] = {}; const restriction: Restriction = { '@base': 'xs:string' };
const restriction = { '@base': 'xs:string' };
if (min !== '') { if (min !== '') {
const minKey = minType === 'Inclusive' ? 'minInclusive' : 'minExclusive'; const minKey = minType === 'Inclusive' ? 'minInclusive' : 'minExclusive';
restriction[minKey] = [{ '@value': min }]; (restriction as Record<string, RestrictionValue[]>)[minKey] = [{ '@value': min }];
} }
if (max !== '') { if (max !== '') {
const maxKey = maxType === 'Inclusive' ? 'maxInclusive' : 'maxExclusive'; const maxKey = maxType === 'Inclusive' ? 'maxInclusive' : 'maxExclusive';
restriction[maxKey] = [{ '@value': max }]; (restriction as Record<string, RestrictionValue[]>)[maxKey] = [{ '@value': max }];
} }
facet[fieldName] = { 'restriction': restriction }; setFieldValue({ 'restriction': restriction });
}; };
const setLengthValue = (value) => { const setLengthValue = (value: string) => {
if (!facet[fieldName]) facet[fieldName] = {}; setFieldValue({
facet[fieldName] = {
'restriction': { 'restriction': {
'@base': 'xs:string', '@base': 'xs:string',
'length': [{ '@value': value }] 'length': [{ '@value': value }]
} }
}; });
}; };
const setLengthRangeValues = (min, max) => { const setLengthRangeValues = (min: string, max: string) => {
if (!facet[fieldName]) facet[fieldName] = {}; const restriction: Restriction = { '@base': 'xs:string' };
const restriction = { '@base': 'xs:string' };
if (min !== '') restriction['minLength'] = [{ '@value': min }]; if (min !== '') restriction['minLength'] = [{ '@value': min }];
if (max !== '') restriction['maxLength'] = [{ '@value': max }]; if (max !== '') restriction['maxLength'] = [{ '@value': max }];
facet[fieldName] = { 'restriction': restriction }; setFieldValue({ 'restriction': restriction });
}; };
let restrictionType = $derived(getRestrictionType()); let restrictionType = $state(getRestrictionType());
let hasUserSelectedType = $state(false);
let lastFacetRef = $state(facet);
let enumerationValues = $derived(getEnumerationValues()); let enumerationValues = $derived(getEnumerationValues());
const handleTypeChange = (newType) => { $effect(() => {
if (facet !== lastFacetRef) {
lastFacetRef = facet;
hasUserSelectedType = false;
}
const detected = getRestrictionType();
if (!hasUserSelectedType || detected !== 'Simple' || restrictionType === 'Simple') {
restrictionType = detected;
}
});
const handleTypeChange = (newType: string) => {
hasUserSelectedType = true;
restrictionType = newType; restrictionType = newType;
switch (newType) { switch (newType) {
@@ -435,23 +491,27 @@
enumerationValues = [...enumerationValues, '']; enumerationValues = [...enumerationValues, ''];
}; };
const removeEnumerationValue = (index) => { const removeEnumerationValue = (index: number) => {
enumerationValues = enumerationValues.filter((_, i) => i !== index); enumerationValues = enumerationValues.filter((_, i) => i !== index);
setEnumerationValues(enumerationValues); setEnumerationValues(enumerationValues);
}; };
const updateEnumerationValue = (index, value) => { const updateEnumerationValue = (index: number, value: string) => {
enumerationValues[index] = value; enumerationValues[index] = value;
setEnumerationValues(enumerationValues); setEnumerationValues(enumerationValues);
}; };
</script> </script>
<div class="form-group"> <div class="form-group">
<label>{label}</label> <span class="form-label" id={`${baseId}-label`}>{label}</span>
<div class="restriction-controls"> <div class="restriction-controls" role="group" aria-labelledby={`${baseId}-label`}>
{#if !isSpecialProp} {#if !isSpecialProp}
<div class="restriction-type-selector"> <div class="restriction-type-selector">
<select class="form-input" bind:value={restrictionType} onchange={(e) => handleTypeChange(e.target.value)}> <select
class="form-input"
bind:value={restrictionType}
onchange={(e) => handleTypeChange((e.target as HTMLSelectElement).value)}
>
<option value="Simple">Simple</option> <option value="Simple">Simple</option>
<option value="Enumeration">Enumeration</option> <option value="Enumeration">Enumeration</option>
<option value="Pattern">Pattern</option> <option value="Pattern">Pattern</option>
@@ -477,7 +537,7 @@
placeholder={placeholder} placeholder={placeholder}
/> />
{:else} {:else}
<input class="form-input" type="text" bind:value={() => getSimpleValue(), (v) => setSimpleValue(v)} {placeholder}> <input class="form-input" type="text" bind:value={() => getSimpleValue(), (v) => setSimpleValue(v)} {placeholder} aria-label={label}>
{/if} {/if}
{:else if restrictionType === 'Enumeration'} {:else if restrictionType === 'Enumeration'}
@@ -497,7 +557,14 @@
placeholder={placeholder} placeholder={placeholder}
/> />
{:else} {:else}
<input class="form-input" type="text" value={value} oninput={(e) => updateEnumerationValue(index, e.target.value)} {placeholder}> <input
class="form-input"
type="text"
value={value}
oninput={(e) => updateEnumerationValue(index, (e.target as HTMLInputElement).value)}
{placeholder}
aria-label={`${label} option ${index + 1}`}
>
{/if} {/if}
<button class="btn-delete" onclick={() => removeEnumerationValue(index)} type="button" aria-label="Remove enumeration value"> <button class="btn-delete" onclick={() => removeEnumerationValue(index)} type="button" aria-label="Remove enumeration value">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -515,22 +582,43 @@
</div> </div>
{:else if restrictionType === 'Pattern'} {:else if restrictionType === 'Pattern'}
<input class="form-input" type="text" bind:value={() => getPatternValue(), (v) => setPatternValue(v)} placeholder="Enter regex pattern (e.g., DT[0-9]{2})"> <input class="form-input" type="text" bind:value={() => getPatternValue(), (v) => setPatternValue(v)} placeholder="Enter regex pattern (e.g., DT[0-9]{2})" aria-label={`${label} pattern`}>
{:else if restrictionType === 'Range'} {:else if restrictionType === 'Range'}
{@const range = getRangeValues()}
<div class="range-controls"> <div class="range-controls">
<div class="range-group"> <div class="range-group">
<label>Min</label> <label for={`${baseId}-range-min`}>Min</label>
<input class="form-input" type="text" bind:value={() => getRangeValues().min, (v) => { const range = getRangeValues(); setRangeValues(v, range.max, range.minType, range.maxType); }} placeholder="0"> <input
<select class="form-input" bind:value={() => getRangeValues().minType, (v) => { const range = getRangeValues(); setRangeValues(range.min, range.max, v, range.maxType); }}> class="form-input"
type="text"
id={`${baseId}-range-min`}
bind:value={() => range.min, (v) => setRangeValues(v, range.max, range.minType, range.maxType)}
placeholder="0"
>
<select
class="form-input"
aria-label="Min bound type"
bind:value={() => range.minType, (v) => setRangeValues(range.min, range.max, v, range.maxType)}
>
<option value="Inclusive">Inclusive</option> <option value="Inclusive">Inclusive</option>
<option value="Exclusive">Exclusive</option> <option value="Exclusive">Exclusive</option>
</select> </select>
</div> </div>
<div class="range-group"> <div class="range-group">
<label>Max</label> <label for={`${baseId}-range-max`}>Max</label>
<input class="form-input" type="text" bind:value={() => getRangeValues().max, (v) => { const range = getRangeValues(); setRangeValues(range.min, v, range.minType, range.maxType); }} placeholder="0"> <input
<select class="form-input" bind:value={() => getRangeValues().maxType, (v) => { const range = getRangeValues(); setRangeValues(range.min, range.max, range.minType, v); }}> class="form-input"
type="text"
id={`${baseId}-range-max`}
bind:value={() => range.max, (v) => setRangeValues(range.min, v, range.minType, range.maxType)}
placeholder="0"
>
<select
class="form-input"
aria-label="Max bound type"
bind:value={() => range.maxType, (v) => setRangeValues(range.min, range.max, range.minType, v)}
>
<option value="Inclusive">Inclusive</option> <option value="Inclusive">Inclusive</option>
<option value="Exclusive">Exclusive</option> <option value="Exclusive">Exclusive</option>
</select> </select>
@@ -538,17 +626,18 @@
</div> </div>
{:else if restrictionType === 'Length'} {:else if restrictionType === 'Length'}
<input class="form-input" type="number" bind:value={() => getLengthValue(), (v) => setLengthValue(v)} placeholder="Enter exact length"> <input class="form-input" type="number" bind:value={() => getLengthValue(), (v) => setLengthValue(v)} placeholder="Enter exact length" aria-label={`${label} length`}>
{:else if restrictionType === 'Length Range'} {:else if restrictionType === 'Length Range'}
{@const lengthRange = getLengthRangeValues()}
<div class="length-range-controls"> <div class="length-range-controls">
<div class="length-group"> <div class="length-group">
<label>Min Length</label> <label for={`${baseId}-length-min`}>Min Length</label>
<input class="form-input" type="number" bind:value={() => getLengthRangeValues().min, (v) => { const range = getLengthRangeValues(); setLengthRangeValues(v, range.max); }} placeholder="0"> <input class="form-input" id={`${baseId}-length-min`} type="number" bind:value={() => lengthRange.min, (v) => setLengthRangeValues(v, lengthRange.max)} placeholder="0">
</div> </div>
<div class="length-group"> <div class="length-group">
<label>Max Length</label> <label for={`${baseId}-length-max`}>Max Length</label>
<input class="form-input" type="number" bind:value={() => getLengthRangeValues().max, (v) => { const range = getLengthRangeValues(); setLengthRangeValues(range.min, v); }} placeholder="0"> <input class="form-input" id={`${baseId}-length-max`} type="number" bind:value={() => lengthRange.max, (v) => setLengthRangeValues(lengthRange.min, v)} placeholder="0">
</div> </div>
</div> </div>
{/if} {/if}
@@ -656,4 +745,4 @@
color: #666; color: #666;
margin: 0; margin: 0;
} }
</style> </style>
@@ -1,33 +1,44 @@
<script> <script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte.js"; import * as IDS from "$src/modules/api/ids.svelte";
import type { DocumentState, IdsCardinality, IdsDocument, Specification } from "$src/types/ids";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null); let activeDocument = $derived(
let documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null); IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ? );
activeDocument.specifications.specification[documentState.activeSpecification] : null); let documentState = $derived(
IDS.Module.activeDocument ? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState) : null
);
let activeSpecification = $derived(
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null
);
const getProp = (prop) => { const getProp = (prop: string) => {
return activeSpecification?.[prop] ?? ""; const value = (activeSpecification as Record<string, unknown>)?.[prop];
return typeof value === "string" ? value : "";
}; };
const setProp = (prop, value) => { const setProp = (prop: string, value: string) => {
activeSpecification[prop] = value; if (!activeSpecification) return;
(activeSpecification as Record<string, unknown>)[prop] = value;
}; };
const addIfcVersion = (e, version) => { const addIfcVersion = (e: Event, version: string) => {
if (activeSpecification) { if (activeSpecification) {
if (!("@ifcVersion" in activeSpecification)) activeSpecification["@ifcVersion"] = []; if (!("@ifcVersion" in activeSpecification)) activeSpecification["@ifcVersion"] = [];
if (e.target.checked) { const target = e.target as HTMLInputElement | null;
if (!activeSpecification["@ifcVersion"].includes(version)) { if (target?.checked) {
activeSpecification["@ifcVersion"] = [...activeSpecification["@ifcVersion"], version]; if (!activeSpecification["@ifcVersion"]?.includes(version)) {
activeSpecification["@ifcVersion"] = [...(activeSpecification["@ifcVersion"] ?? []), version];
} }
} else { } else if (activeSpecification["@ifcVersion"]) {
activeSpecification["@ifcVersion"] = activeSpecification["@ifcVersion"].filter(v => v !== version); activeSpecification["@ifcVersion"] = activeSpecification["@ifcVersion"].filter(v => v !== version);
} }
} }
}; };
const setUsage = (usage) => { const setUsage = (usage: IdsCardinality) => {
if (!activeSpecification) return; if (!activeSpecification) return;
if (!activeSpecification.applicability) activeSpecification.applicability = {}; if (!activeSpecification.applicability) activeSpecification.applicability = {};
@@ -60,14 +71,19 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="spec-cardinality">Usage</label> <label for="spec-cardinality">Usage</label>
<select class="form-input" id="spec-cardinality" value={IDS.getSpecUsage(activeSpecification)} onchange={(e) => setUsage(e.target.value)}> <select
class="form-input"
id="spec-cardinality"
value={IDS.getSpecUsage(activeSpecification)}
onchange={(e) => setUsage((e.target as HTMLSelectElement).value as IdsCardinality)}
>
<option value="required">Required</option> <option value="required">Required</option>
<option value="optional">Optional</option> <option value="optional">Optional</option>
<option value="prohibited">Prohibited</option> <option value="prohibited">Prohibited</option>
</select> </select>
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label>IFC Version</label> <span class="form-label">IFC Version</span>
<div class="radio-group"> <div class="radio-group">
<label class="radio-label"> <label class="radio-label">
<input type="checkbox" value="IFC2X3" checked={activeSpecification?.["@ifcVersion"]?.includes('IFC2X3')} onchange={(e) => addIfcVersion(e, 'IFC2X3')}> <input type="checkbox" value="IFC2X3" checked={activeSpecification?.["@ifcVersion"]?.includes('IFC2X3')} onchange={(e) => addIfcVersion(e, 'IFC2X3')}>
@@ -92,4 +108,4 @@
<textarea class="form-input" id="spec-instructions" bind:value={() => getProp("@instructions"), (v) => setProp("@instructions", v)} placeholder="Enter instructions" rows="3"></textarea> <textarea class="form-input" id="spec-instructions" bind:value={() => getProp("@instructions"), (v) => setProp("@instructions", v)} placeholder="Enter instructions" rows="3"></textarea>
</div> </div>
</div> </div>
</div> </div>
@@ -1,5 +1,5 @@
<script> <script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte.js"; import * as IDS from "$src/modules/api/ids.svelte";
import AppHeader from "$src/components/AppHeader.svelte"; import AppHeader from "$src/components/AppHeader.svelte";
import AppRibbon from "$src/components/AppRibbon.svelte"; import AppRibbon from "$src/components/AppRibbon.svelte";
import AppToolbar from "$src/components/AppToolbar.svelte"; import AppToolbar from "$src/components/AppToolbar.svelte";
@@ -11,14 +11,26 @@
import IdsViewer from "./IdsViewer.svelte"; import IdsViewer from "./IdsViewer.svelte";
import SplashScreen from "$src/components/SplashScreen.svelte"; import SplashScreen from "$src/components/SplashScreen.svelte";
import { Toaster } from "$lib/components/ui/sonner"; import { Toaster } from "$lib/components/ui/sonner";
import { error, success } from "$src/modules/utils/toast.svelte.js"; import { error, success } from "$src/modules/utils/toast.svelte";
import {onMount} from "svelte"; import type { DocumentState, IdsDocument, Specification } from "$src/types/ids";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null); let activeDocument = $derived(
let documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null); IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ? );
activeDocument.specifications.specification[documentState.activeSpecification] : null); let documentState = $derived(
IDS.Module.activeDocument ? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState) : null
);
let activeSpecification = $derived(
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null
);
let importableDocuments = $derived(
Object.entries(IDS.Module.documents).filter(
([docId, doc]) => docId !== IDS.Module.activeDocument && doc.specifications?.specification?.length > 0
) as [string, IdsDocument][]
);
async function addNewSpecification() { async function addNewSpecification() {
if (!IDS.Module.activeDocument) return; if (!IDS.Module.activeDocument) return;
@@ -28,11 +40,11 @@
IDS.setDocumentState(IDS.Module.activeDocument, { activeTab: 'info' }); IDS.setDocumentState(IDS.Module.activeDocument, { activeTab: 'info' });
} }
async function importSpecification(sourceDocId, specIndex) { async function importSpecification(sourceDocId: string, specIndex: number) {
if (!IDS.Module.activeDocument || !IDS.Module.documents[sourceDocId]) return; if (!IDS.Module.activeDocument || !IDS.Module.documents[sourceDocId]) return;
try { try {
const sourceDoc = IDS.Module.documents[sourceDocId]; const sourceDoc = IDS.Module.documents[sourceDocId] as IdsDocument;
const sourceSpec = sourceDoc.specifications?.specification?.[specIndex]; const sourceSpec = sourceDoc.specifications?.specification?.[specIndex];
if (!sourceSpec) { if (!sourceSpec) {
@@ -41,13 +53,13 @@
} }
// Create a deep copy of the specification // Create a deep copy of the specification
const specCopy = JSON.parse(JSON.stringify(sourceSpec)); const specCopy = JSON.parse(JSON.stringify(sourceSpec)) as Specification;
// Add the copied specification to the current document // Add the copied specification to the current document
IDS.Module.documents[IDS.Module.activeDocument].specifications.specification.push(specCopy); IDS.Module.documents[IDS.Module.activeDocument].specifications.specification.push(specCopy);
// Switch to the newly created specification and info tab // Switch to the newly created specification and info tab
const currentDoc = IDS.Module.documents[IDS.Module.activeDocument]; const currentDoc = IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument;
const newSpecIndex = (currentDoc.specifications?.specification?.length || 1) - 1; const newSpecIndex = (currentDoc.specifications?.specification?.length || 1) - 1;
IDS.setDocumentState(IDS.Module.activeDocument, { IDS.setDocumentState(IDS.Module.activeDocument, {
activeSpecification: newSpecIndex, activeSpecification: newSpecIndex,
@@ -57,17 +69,18 @@
success(`Specification "${sourceSpec['@name'] || 'Unnamed'}" imported successfully`); success(`Specification "${sourceSpec['@name'] || 'Unnamed'}" imported successfully`);
} catch (err) { } catch (err) {
console.error('Error importing specification:', err); console.error('Error importing specification:', err);
error(`Failed to import specification: ${err.message}`); const message = err instanceof Error ? err.message : String(err);
error(`Failed to import specification: ${message}`);
} }
} }
function selectSpecification(index) { function selectSpecification(index: number) {
if (IDS.Module.activeDocument) { if (IDS.Module.activeDocument) {
IDS.setDocumentState(IDS.Module.activeDocument, { activeSpecification: index }); IDS.setDocumentState(IDS.Module.activeDocument, { activeSpecification: index });
} }
} }
async function deleteSpecification(specIndex) { async function deleteSpecification(specIndex: number) {
if (!IDS.Module.activeDocument) return; if (!IDS.Module.activeDocument) return;
await IDS.deleteSpecification(IDS.Module.activeDocument, specIndex); await IDS.deleteSpecification(IDS.Module.activeDocument, specIndex);
} }
@@ -83,9 +96,24 @@
success('IDS document exported successfully'); success('IDS document exported successfully');
} catch (err) { } catch (err) {
console.error('Error exporting IDS:', err); console.error('Error exporting IDS:', err);
error('Error exporting IDS: ' + err.message); const message = err instanceof Error ? err.message : String(err);
error('Error exporting IDS: ' + message);
} }
} }
const updateActiveDocumentState = (updates: Partial<DocumentState>) => {
if (IDS.Module.activeDocument) {
IDS.setDocumentState(IDS.Module.activeDocument, updates);
}
};
const handleActivation = (event: KeyboardEvent, action: () => void) => {
if (event.currentTarget !== event.target) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
action();
}
};
</script> </script>
{#if IDS.Module.status != "ready"} {#if IDS.Module.status != "ready"}
@@ -130,8 +158,7 @@
Import from IDS Import from IDS
</DropdownMenu.SubTrigger> </DropdownMenu.SubTrigger>
<DropdownMenu.SubContent class="w-64 max-h-64 overflow-y-auto"> <DropdownMenu.SubContent class="w-64 max-h-64 overflow-y-auto">
{#each Object.entries(IDS.Module.documents) as [docId, doc]} {#each importableDocuments as [docId, doc], docIndex}
{#if docId !== IDS.Module.activeDocument && doc.specifications?.specification?.length > 0}
<DropdownMenu.Label class="font-medium text-xs text-muted-foreground px-2 py-1 truncate"> <DropdownMenu.Label class="font-medium text-xs text-muted-foreground px-2 py-1 truncate">
{doc.info?.title || 'Untitled Document'} {doc.info?.title || 'Untitled Document'}
</DropdownMenu.Label> </DropdownMenu.Label>
@@ -146,12 +173,11 @@
</span> </span>
</DropdownMenu.Item> </DropdownMenu.Item>
{/each} {/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}
<DropdownMenu.Separator /> <DropdownMenu.Separator />
{/if} {/if}
{/if}
{/each} {/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}
<DropdownMenu.Item disabled> <DropdownMenu.Item disabled>
<span class="text-sm">No specifications available to import</span> <span class="text-sm">No specifications available to import</span>
</DropdownMenu.Item> </DropdownMenu.Item>
@@ -162,16 +188,30 @@
</DropdownMenu.Root> </DropdownMenu.Root>
</div> </div>
<div class="specifications-list scrollbar"> <div class="specifications-list scrollbar">
<div class="spec-item" class:active={documentState?.activeSpecification === null} onclick={() => { if (IDS.Module.activeDocument) IDS.setDocumentState(IDS.Module.activeDocument, { activeSpecification: null }); }}> <div
class="spec-item"
class:active={documentState?.activeSpecification === null}
role="button"
tabindex="0"
onclick={() => { 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 }); })}
>
<span class="spec-icon"></span> <span class="spec-icon"></span>
<span class="spec-name">IDS Information</span> <span class="spec-name">IDS Information</span>
</div> </div>
{#if activeDocument?.specifications?.specification} {#if activeDocument?.specifications?.specification}
{#each activeDocument.specifications.specification as spec, index} {#each activeDocument.specifications.specification as spec, index}
<div class="spec-item" class:active={documentState?.activeSpecification === index} onclick={() => selectSpecification(index)}> <div
class="spec-item"
class:active={documentState?.activeSpecification === index}
role="button"
tabindex="0"
onclick={() => selectSpecification(index)}
onkeydown={(event) => handleActivation(event, () => selectSpecification(index))}
>
<span class="spec-icon">📄</span> <span class="spec-icon">📄</span>
<span class="spec-name">{spec["@name"] || "Specification " + (index + 1)}</span> <span class="spec-name">{spec["@name"] || "Specification " + (index + 1)}</span>
<button class="btn-delete" onclick={() => deleteSpecification(index)}> <button class="btn-delete" onclick={() => deleteSpecification(index)} aria-label="Delete specification">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"></path> <path d="M18 6L6 18M6 6l12 12"></path>
</svg> </svg>
@@ -192,10 +232,10 @@
{:else} {:else}
<!-- Editor/Viewer Toggle --> <!-- Editor/Viewer Toggle -->
<div class="view-mode-toggle"> <div class="view-mode-toggle">
<button class="toggle-btn" class:active={documentState?.viewMode === 'editor'} onclick={() => IDS.setDocumentState(IDS.Module.activeDocument, { viewMode: 'editor', auditReport: null })}> <button class="toggle-btn" class:active={documentState?.viewMode === 'editor'} onclick={() => updateActiveDocumentState({ viewMode: 'editor', auditReport: null })}>
Editor Editor
</button> </button>
<button class="toggle-btn" class:active={documentState?.viewMode === 'viewer'} onclick={() => IDS.setDocumentState(IDS.Module.activeDocument, { viewMode: 'viewer', auditReport: null })}> <button class="toggle-btn" class:active={documentState?.viewMode === 'viewer'} onclick={() => updateActiveDocumentState({ viewMode: 'viewer', auditReport: null })}>
Viewer Viewer
</button> </button>
</div> </div>
@@ -209,18 +249,18 @@
<div class="spec-header"> <div class="spec-header">
<h2>{activeSpecification ? activeSpecification["@name"] || "Specification" : "Specification"}</h2> <h2>{activeSpecification ? activeSpecification["@name"] || "Specification" : "Specification"}</h2>
<div class="spec-tabs"> <div class="spec-tabs">
<button class="btn tab-btn" class:active={documentState?.activeTab === 'info'} onclick={() => IDS.setDocumentState(IDS.Module.activeDocument, { activeTab: 'info' })}>Info</button> <button class="btn tab-btn" class:active={documentState?.activeTab === 'info'} onclick={() => updateActiveDocumentState({ activeTab: 'info' })}>Info</button>
<button class="btn tab-btn" class:active={documentState?.activeTab === 'applicability'} onclick={() => IDS.setDocumentState(IDS.Module.activeDocument, { activeTab: 'applicability' })}>Applicability</button> <button class="btn tab-btn" class:active={documentState?.activeTab === 'applicability'} onclick={() => updateActiveDocumentState({ activeTab: 'applicability' })}>Applicability</button>
<button class="btn tab-btn" class:active={documentState?.activeTab === 'requirements'} onclick={() => IDS.setDocumentState(IDS.Module.activeDocument, { activeTab: 'requirements' })}>Requirements</button> <button class="btn tab-btn" class:active={documentState?.activeTab === 'requirements'} onclick={() => updateActiveDocumentState({ activeTab: 'requirements' })}>Requirements</button>
</div> </div>
</div> </div>
{#if documentState?.activeTab === 'info'} {#if documentState?.activeTab === 'info'}
<SpecificationEditor /> <SpecificationEditor />
{:else if documentState?.activeTab === 'applicability'} {:else if documentState?.activeTab === 'applicability'}
<ApplicabilityPanel activeTab /> <ApplicabilityPanel />
{:else if documentState?.activeTab === 'requirements'} {:else if documentState?.activeTab === 'requirements'}
<RequirementsPanel activeTab /> <RequirementsPanel />
{/if} {/if}
</div> </div>
{/if} {/if}
+70
View File
@@ -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<string, FacetValue | string | number | boolean | null | undefined>;
export type FacetClause = Record<string, Facet[] | number | "unbounded" | undefined>;
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;
};
+95
View File
@@ -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<string, unknown>;
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;
};
+24
View File
@@ -0,0 +1,24 @@
export type WorkerMessageType =
| "init"
| "api_call"
| "ready"
| "api_response"
| "error"
| "disposed";
export type WorkerRequest = {
type: WorkerMessageType;
payload?: Record<string, unknown>;
id: string;
};
export type WorkerResponse = {
type: WorkerMessageType;
payload?: Record<string, unknown>;
id: string;
};
export type ApiCallPayload = {
method: string;
args?: unknown[];
};
+22
View File
@@ -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"]
}
+2 -2
View File
@@ -1,7 +1,7 @@
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte'; import { svelte } from '@sveltejs/vite-plugin-svelte';
import path from "path"; import path from "node:path";
export default defineConfig({ export default defineConfig({
plugins: [tailwindcss(), svelte()], plugins: [tailwindcss(), svelte()],
@@ -11,4 +11,4 @@ export default defineConfig({
$src: path.resolve("./src"), $src: path.resolve("./src"),
}, },
}, },
}); });