Merge remote-tracking branch 'origin/v0.8.0' into datamodel-v1.0

This commit is contained in:
Thomas Krijnen
2026-04-18 20:15:28 +02:00
1655 changed files with 105534 additions and 46934 deletions
+68 -10
View File
@@ -25,9 +25,19 @@ WEBAPP_BUILD_DIR := $(WEBAPP_DIR)/dist
PYODIDE_DIR := $(WEBAPP_DIR)/public/pyodide
PYODIDE_VERSION := 0.28.0
PYODIDE_URL := https://github.com/pyodide/pyodide/releases/download/$(PYODIDE_VERSION)/pyodide-$(PYODIDE_VERSION).tar.bz2
WORKER_BIN_DIR := $(WEBAPP_DIR)/public/worker/bin
IFCOPENSHELL_WASM_WHEEL := ifcopenshell-0.8.5+a51b2c5-cp313-cp313-pyodide_2025_0_wasm32.whl
IFCOPENSHELL_WASM_WHEEL_URL := https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-0.8.5%2Ba51b2c5-cp313-cp313-pyodide_2025_0_wasm32.whl
IFCOPENSHELL_WASM_WHEEL_PATH := $(WORKER_BIN_DIR)/$(IFCOPENSHELL_WASM_WHEEL)
WEBAPP_GENERATED_DIR := $(WEBAPP_DIR)/public/worker/generated
WEBAPP_IFCTESTER_MANIFEST := $(WEBAPP_GENERATED_DIR)/ifctester.json
WEBAPP_IFCTESTER_BUILD_DIR := build-webapp-wheel
PACKAGE_WEBAPP_DIR := $(PACKAGE_NAME)/webapp
PACKAGE_WEBAPP_WWW_DIR := $(PACKAGE_WEBAPP_DIR)/www
.PHONY: webapp-dev
webapp-dev:
webapp-dev: pyodide-download ifcopenshell-wasm-download webapp-stage-ifctester-wheel
cd $(WEBAPP_DIR) && npm install
cd $(WEBAPP_DIR) && npm run dev
.PHONY: pyodide-download
@@ -62,8 +72,43 @@ pyodide-download:
echo "Pyodide $(PYODIDE_VERSION) prepared in $(PYODIDE_DIR)"; \
fi
.PHONY: ifcopenshell-wasm-download
ifcopenshell-wasm-download:
@if [ -f "$(IFCOPENSHELL_WASM_WHEEL_PATH)" ]; then \
echo "IfcOpenShell wasm wheel already exists at $(IFCOPENSHELL_WASM_WHEEL_PATH), skipping download"; \
else \
echo "Downloading IfcOpenShell wasm wheel..."; \
mkdir -p $(WORKER_BIN_DIR); \
rm -f $(WORKER_BIN_DIR)/ifcopenshell-*.whl; \
curl -fL -o "$(IFCOPENSHELL_WASM_WHEEL_PATH)" "$(IFCOPENSHELL_WASM_WHEEL_URL)"; \
echo "IfcOpenShell wasm wheel prepared at $(IFCOPENSHELL_WASM_WHEEL_PATH)"; \
fi
.PHONY: webapp-stage-ifctester-wheel
webapp-stage-ifctester-wheel:
rm -rf $(WEBAPP_GENERATED_DIR)
rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR)
mkdir -p $(WEBAPP_IFCTESTER_BUILD_DIR)
cp -r $(PACKAGE_NAME) $(WEBAPP_IFCTESTER_BUILD_DIR)/
rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR)/$(PACKAGE_NAME)/webapp
cp pyproject.toml $(WEBAPP_IFCTESTER_BUILD_DIR)/
cp README.md $(WEBAPP_IFCTESTER_BUILD_DIR)/
ifeq ($(IS_STABLE), TRUE)
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/$(PACKAGE_NAME)/__init__.py
else
$(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/$(PACKAGE_NAME)/__init__.py
endif
cd $(WEBAPP_IFCTESTER_BUILD_DIR) && $(PYTHON) -m venv env --system-site-packages && . env/$(VENV_ACTIVATE) && python -m pip install build && python -m build --wheel --no-isolation
mkdir -p $(WEBAPP_GENERATED_DIR)
wheel=$$(basename $(WEBAPP_IFCTESTER_BUILD_DIR)/dist/$(PACKAGE_NAME)-*.whl); \
cp "$(WEBAPP_IFCTESTER_BUILD_DIR)/dist/$$wheel" "$(WEBAPP_GENERATED_DIR)/$$wheel"; \
printf '{\n "wheel_url": "/worker/generated/%s"\n}\n' "$$wheel" > "$(WEBAPP_IFCTESTER_MANIFEST)"
rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR)
.PHONY: webapp-build
webapp-build: pyodide-download
webapp-build: pyodide-download ifcopenshell-wasm-download webapp-stage-ifctester-wheel
cd $(WEBAPP_DIR) && npm install
cd $(WEBAPP_DIR) && npm run build
@@ -76,23 +121,36 @@ clean:
rm -rf $(WEBAPP_BUILD_DIR)
rm -rf $(WEBAPP_DIR)/node_modules
rm -rf $(PYODIDE_DIR)
rm -f $(WORKER_BIN_DIR)/ifcopenshell-*.whl
rm -rf $(WEBAPP_GENERATED_DIR)
rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR)
rm -rf $(PACKAGE_NAME)/webapp
rm -rf dist
.PHONY: dist
dist: webapp-prepare
.PHONY: python-dist
python-dist:
rm -rf dist
# For some reason OS is not initalized when we call common.mk dist, which matters on Windows.
# So we pass it explicitly.
$(MAKE) -f ../common.mk dist PACKAGE_NAME=$(PACKAGE_NAME) OS=$(OS)
$(MAKE) -f ../common.mk dist PACKAGE_NAME=$(PACKAGE_NAME) OS=$(OS) IS_STABLE=$(IS_STABLE)
.PHONY: dist
dist: webapp-prepare
$(MAKE) python-dist OS=$(OS) IS_STABLE=$(IS_STABLE)
.PHONY: webapp-prepare
webapp-prepare: webapp-build
rm -rf $(PACKAGE_NAME)/webapp/www/*
mkdir -p $(PACKAGE_NAME)/webapp/www
cp -r $(WEBAPP_BUILD_DIR)/* $(PACKAGE_NAME)/webapp/www/
cp $(WEBAPP_DIR)/__init__.py $(PACKAGE_NAME)/webapp/__init__.py
cp $(WEBAPP_DIR)/serve.py $(PACKAGE_NAME)/webapp/serve.py
rm -rf $(PACKAGE_WEBAPP_WWW_DIR)
mkdir -p $(PACKAGE_WEBAPP_WWW_DIR)
cp -r $(WEBAPP_BUILD_DIR)/* $(PACKAGE_WEBAPP_WWW_DIR)/
cp $(WEBAPP_DIR)/__init__.py $(PACKAGE_WEBAPP_DIR)/__init__.py
cp $(WEBAPP_DIR)/serve.py $(PACKAGE_WEBAPP_DIR)/serve.py
.PHONY: test
test:
pytest -p no:pytest-blender test
.PHONY: build-ids-docs
build-ids-docs:
mkdir -p test/build
cd test && python ids_doc_generator.py
+1
View File
@@ -19,3 +19,4 @@
from .ids import open
__version__ = version = "0.0.0"
__all__ = ["open"]
+5 -4
View File
@@ -18,13 +18,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
import time
import argparse
import ifcopenshell
from . import ids
from . import reporter
import time
from pathlib import Path
import ifcopenshell
from . import ids, reporter
parser = argparse.ArgumentParser(description="Uses an IDS to audit an IFC")
parser.add_argument("ids", type=str, help="Path to an IDS")
parser.add_argument("ifc", type=str, help="Path to an IFC", nargs="?")
+8 -6
View File
@@ -17,15 +17,17 @@
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import re
import builtins
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.classification
import re
from functools import lru_cache
from xmlschema.validators import identities
from typing import Union, Optional, Any, Literal, TYPE_CHECKING, TypedDict
from logging import Logger
from typing import TYPE_CHECKING, Any, Literal, Optional, TypedDict, Union
import ifcopenshell.util.classification
import ifcopenshell.util.element
import ifcopenshell.util.unit
from xmlschema.validators import identities
if TYPE_CHECKING:
from .ids import Specification
+19 -24
View File
@@ -17,29 +17,32 @@
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os
import datetime
import ifcopenshell
from xmlschema.validators.exceptions import XMLSchemaValidationError
from xmlschema import XMLSchema
from xmlschema import etree_tostring
import os
from typing import Any, Optional, Union
from xml.etree import ElementTree as ET
import ifcopenshell
from xmlschema import XMLSchema, etree_tostring
from xmlschema.validators.exceptions import XMLSchemaValidationError
from .facet import (
Facet,
Entity,
Attribute,
Cardinality,
Classification,
Property,
PartOf,
Entity,
Facet,
FacetFailure,
Material,
PartOf,
Property,
Restriction,
get_pset,
get_psets,
Cardinality,
FacetFailure,
)
from typing import Optional, Union, Any
__all__ = ["Attribute", "Classification", "Entity", "Material", "PartOf", "Property", "Restriction"]
cwd = os.path.dirname(os.path.realpath(__file__))
schema = None
@@ -298,23 +301,15 @@ class Specification:
if not is_applicable:
continue
self.applicable_entities.append(element)
for facet in self.requirements:
result = facet(element)
is_pass = bool(result)
if self.maxOccurs != 0: # This is a required or optional specification
if is_pass:
if self.maxOccurs != 0: # Requirements are skipped for prohibited applicability
for facet in self.requirements:
result = facet(element)
if bool(result):
self.passed_entities.add(element)
facet.passed_entities.add(element)
else:
self.failed_entities.add(element)
facet.failures.append(FacetFailure(element=element, reason=str(result)))
else: # This is a prohibited specification
if is_pass:
self.failed_entities.add(element)
facet.failures.append(FacetFailure(element=element, reason=str(result)))
else:
self.passed_entities.add(element)
facet.passed_entities.add(element)
self.status = True
for facet in self.requirements:
+41 -8
View File
@@ -17,18 +17,21 @@
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import datetime
import math
import os
import re
import sys
import math
import datetime
from typing import Literal, Optional, TypedDict, Union
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.unit
from .ids import Specification, Ids
from .facet import Facet, FacetFailure
from typing import TypedDict, Union, Literal, Optional
from .ids import Ids, Specification
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -78,10 +81,12 @@ class ResultsSpecification(TypedDict):
description: str
instructions: str
status: bool
is_skipped: bool
is_ifc_version: bool
total_applicable: int
total_applicable_pass: int
total_applicable_fail: int
applicable_entities: list[ResultsEntity]
percent_applicable_pass: ResultsPercent
total_checks: int
total_checks_pass: int
@@ -364,16 +369,24 @@ class Json(Reporter):
cardinality = "optional"
elif specification.minOccurs == 0 and specification.maxOccurs == 0:
cardinality = "prohibited"
elif specification.minOccurs >= 1:
# Any minimum occurrence >= 1 means the specification is required
cardinality = "required"
else:
# minOccurs == 0 with any other maxOccurs value means optional
cardinality = "optional"
return ResultsSpecification(
name=specification.name,
description=specification.description,
instructions=specification.instructions,
status=specification.status,
is_skipped=cardinality == "optional" and total_checks == 0,
is_ifc_version=specification.is_ifc_version,
total_applicable=total_applicable,
total_applicable_pass=total_applicable_pass,
total_applicable_fail=total_applicable - total_applicable_pass,
applicable_entities=self.report_applicable_entities(specification),
percent_applicable_pass=percent_applicable_pass,
total_checks=total_checks,
total_checks_pass=total_checks_pass,
@@ -384,6 +397,24 @@ class Json(Reporter):
requirements=requirements,
)
def report_applicable_entities(self, specification: Specification) -> list[ResultsEntity]:
return [
ResultsEntity(
{
"element": e,
"element_type": ifcopenshell.util.element.get_type(e),
"class": e.is_a(),
"predefined_type": ifcopenshell.util.element.get_predefined_type(e),
"name": getattr(e, "Name", None),
"description": getattr(e, "Description", None),
"id": e.id(),
"global_id": getattr(e, "GlobalId", None),
"tag": getattr(e, "Tag", None),
}
)
for e in specification.applicable_entities
]
def report_passed_entities(self, requirement: Facet) -> list[ResultsEntity]:
return [
ResultsEntity(
@@ -445,11 +476,13 @@ class Html(Json):
def report(self) -> None:
super().report()
for spec in self.results["specifications"]:
if spec["cardinality"] == "optional" and spec["total_checks"] == 0:
spec["is_skipped"] = True
spec["is_prohibited"] = spec["cardinality"] == "prohibited"
spec["cardinality"] = spec["cardinality"].capitalize()
spec["has_requirements"] = bool(spec["requirements"])
total_applicable_entities = len(spec["applicable_entities"])
spec["applicable_entities"] = self.limit_entities(spec["applicable_entities"])
spec["has_omitted_applicable"] = total_applicable_entities > self.entity_limit
spec["total_omitted_applicable"] = total_applicable_entities - self.entity_limit
for requirement in spec["requirements"]:
total_passed_entities = len(requirement["passed_entities"])
total_failed_entities = len(requirement["failed_entities"])
@@ -555,7 +588,7 @@ class Ods(Json):
def to_file(self, filepath: str) -> None:
from odf.opendocument import OpenDocumentSpreadsheet
from odf.style import Style, TableCellProperties
from odf.table import Table, TableRow, TableCell
from odf.table import Table, TableCell, TableRow
from odf.text import P
self.doc = OpenDocumentSpreadsheet()
@@ -696,7 +729,7 @@ class OdsSummary(Json):
def to_file(self, filepath: str) -> None:
from odf.opendocument import OpenDocumentSpreadsheet
from odf.style import Style, TableCellProperties
from odf.table import Table, TableRow, TableCell
from odf.table import Table, TableCell, TableRow
from odf.text import P
self.doc = OpenDocumentSpreadsheet()
+39 -1
View File
@@ -152,7 +152,6 @@
<p>
<strong>Requirements</strong>
</p>
{{/has_requirements}}
<ol>
{{#requirements}}
<li class="{{^total_checks}}skipped{{/total_checks}}{{#total_checks}}{{#status}}pass{{/status}}{{^status}}fail{{/status}}{{/total_checks}}">
@@ -252,6 +251,45 @@
</li>
{{/requirements}}
</ol>
{{/has_requirements}}
{{#is_prohibited}}
{{#total_applicable}}
<table class="fail">
<thead>
<tr>
<th>Class</th>
<th>PredefinedType</th>
<th>Name</th>
<th>Description</th>
<th>GlobalId</th>
<th>Tag</th>
</tr>
</thead>
<tbody>
{{#applicable_entities}}
<tr>
<td>{{class}}</td>
<td>{{predefined_type}}</td>
<td>{{name}}</td>
<td>{{description}}</td>
<td>{{global_id}}</td>
<td>{{tag}}</td>
</tr>
{{#extra_of_type}}
<tr>
<td colspan="7">... {{extra_of_type}} more of the same element type ({{type_name}} with Tag {{type_tag}} and GlobalId {{type_global_id}}) not shown ...</td>
</tr>
{{/extra_of_type}}
{{/applicable_entities}}
{{#has_omitted_applicable}}
<tr>
<td colspan="7"> ... {{total_omitted_applicable}} more failing elements not shown out of {{total_applicable}} total ...</td>
</tr>
{{/has_omitted_applicable}}
</tbody>
</table>
{{/total_applicable}}
{{/is_prohibited}}
</div>
</section>
{{/specifications}}
+6
View File
@@ -32,3 +32,9 @@ exclude = ["test*"]
[tool.setuptools.package-data]
"*" = ["*.*"]
[tool.ruff]
extend = "../../pyproject.toml"
lint.select = [
"F401", # unused imports
]
+31 -13
View File
@@ -16,13 +16,24 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
"""
Documentation generator for IfcTester IDS facets and test cases.
This is not a test file. It lives in the test/ directory because it reuses
test cases from test_facet.py and test_ids.py to generate example IFC files,
IDS files, and Markdown documentation into test/build/.
Run via: make build-ids-docs (from src/ifctester/)
"""
import functools
import os
import re
import uuid
import pytest
import functools
from pathlib import Path
from xml.dom.minidom import parseString
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.aggregate
import ifcopenshell.api.context
import ifcopenshell.api.geometry
@@ -32,15 +43,16 @@ import ifcopenshell.api.spatial
import ifcopenshell.api.type
import ifcopenshell.api.unit
import ifcopenshell.guid
import ifctester
import numpy as np
import pytest
import test_facet
import test_ids
import numpy as np
from pathlib import Path
from xml.dom.minidom import parseString
from ifctester import ids
from ifcopenshell import validate
import ifctester
import ifctester.facet
from ifctester import ids
outdir = "build"
@@ -116,6 +128,8 @@ class FacetDocGenerator:
{"name": name, "ids": xml_text, "ifc": ifc_text, "basename": basename, "result": result, "id": inst.id()}
)
ifctester.facet.get_pset.cache_clear()
ifctester.facet.get_psets.cache_clear()
assert bool(facet(inst)) is expected
def set_facet(self, facet):
@@ -139,7 +153,7 @@ class IdsDocGenerator:
all_applicable.update(spec.applicable_entities)
for requirement in spec.requirements:
if requirement.status is False:
all_failures.update(requirement.failed_entities)
all_failures.update(f["element"] for f in requirement.failures)
assert set(all_applicable) == set(applicable_entities)
assert set(all_failures) == set(failed_entities)
@@ -150,7 +164,11 @@ class IdsDocGenerator:
l = validate.json_logger()
validate.validate(ifc, l)
for issue in l.statements:
raise Exception("About to emit invalid example data:", issue)
# test_parsing_entities_with_no_attributes uses nameless IfcMaterial; fix for doc generation.
if issue["instance"].is_a("IfcMaterial") and issue.get("attribute") == "IfcMaterial.Name":
issue["instance"].Name = "Unnamed"
else:
raise Exception("About to emit invalid example data:", issue)
lines = ifc.to_string().split("\n")[7:-3]
ifc_text = ""
@@ -292,12 +310,12 @@ spec = ifctester.ids.Specification(
)
specs.specifications.append(spec)
spec.applicability.append(ifctester.ids.Entity(name="IFCWALLTYPE"))
restriction = ifctester.ids.Restriction(options={"pattern": "(-|[0-9]{2,3})\/(-|[0-9]{2,3})\/(-|[0-9]{2,3})"})
restriction = ifctester.ids.Restriction(options={"pattern": r"(-|[0-9]{2,3})/(-|[0-9]{2,3})/(-|[0-9]{2,3})"})
spec.requirements.append(
ifctester.ids.Property(
propertySet="Pset_WallCommon",
name="FireRating",
datatype="IfcLabel",
baseName="FireRating",
dataType="IfcLabel",
value=restriction,
instructions="Fire rating is specified using the Fire Resistance Level as defined in the Australian National Construction Code (NCC) 2019. Valid examples include -/-/-, -/120/120, and 60/60/60",
)
+12 -3
View File
@@ -18,8 +18,8 @@
import uuid
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.aggregate
import ifcopenshell.api.classification
import ifcopenshell.api.group
@@ -33,9 +33,18 @@ import ifcopenshell.api.type
import ifcopenshell.api.unit
import ifcopenshell.guid
import ifcopenshell.util.pset
import ifctester.ids
import ifctester.facet
from ifctester.facet import Entity, Attribute, Classification, Property, PartOf, Material, Restriction
import ifctester.ids
from ifctester.facet import (
Attribute,
Classification,
Entity,
Material,
PartOf,
Property,
Restriction,
)
def set_facet(facet):
+5 -4
View File
@@ -17,13 +17,14 @@
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
import os
from typing import Optional
import ifcopenshell
import ifcopenshell.api.material
import pytest
import xmlschema
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.material
from ifctester import ids
from typing import Optional
def run(
+3 -1
View File
@@ -11,6 +11,8 @@ node_modules
dist
dist-ssr
*.local
public/worker/bin/ifcopenshell-*.whl
public/worker/generated
# Editor directories and files
.vscode/*
@@ -24,4 +26,4 @@ dist-ssr
*.sw?
.claude
experiment/*
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>
<body>
<div id="root"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</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"]
}
+482 -159
View File
@@ -16,6 +16,7 @@
"svelte-spa-router": "^4.0.1"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@internationalized/date": "^3.8.1",
"@lucide/svelte": "^0.515.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
@@ -24,19 +25,22 @@
"clsx": "^2.1.1",
"mode-watcher": "^1.1.0",
"sass-embedded": "^1.89.0",
"svelte": "^5.28.1",
"svelte": "^5.53.6",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.3.2",
"vite": "^6.4.1"
"typescript": "^5.8.3",
"vite": "^6.4.2"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -46,6 +50,170 @@
"node": ">=6.0.0"
}
},
"node_modules/@biomejs/biome": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
"integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
"dev": true,
"hasInstallScript": true,
"license": "MIT OR Apache-2.0",
"bin": {
"biome": "bin/biome"
},
"engines": {
"node": ">=14.21.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "1.9.4",
"@biomejs/cli-darwin-x64": "1.9.4",
"@biomejs/cli-linux-arm64": "1.9.4",
"@biomejs/cli-linux-arm64-musl": "1.9.4",
"@biomejs/cli-linux-x64": "1.9.4",
"@biomejs/cli-linux-x64-musl": "1.9.4",
"@biomejs/cli-win32-arm64": "1.9.4",
"@biomejs/cli-win32-x64": "1.9.4"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
"integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
"integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
"integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
"integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
"integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
"integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
"integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
"integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.5.1.tgz",
@@ -543,6 +711,16 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -588,9 +766,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz",
"integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
@@ -602,9 +780,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz",
"integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
@@ -616,9 +794,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz",
"integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
@@ -630,9 +808,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz",
"integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
@@ -644,9 +822,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz",
"integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
@@ -658,9 +836,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz",
"integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
@@ -672,9 +850,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz",
"integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
@@ -686,9 +864,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz",
"integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
@@ -700,9 +878,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz",
"integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
@@ -714,9 +892,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz",
"integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
@@ -727,10 +905,10 @@
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz",
"integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==",
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
@@ -741,10 +919,38 @@
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz",
"integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==",
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
@@ -756,9 +962,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz",
"integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
@@ -770,9 +976,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz",
"integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
@@ -784,9 +990,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz",
"integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
@@ -798,9 +1004,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz",
"integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
@@ -812,9 +1018,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz",
"integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
@@ -825,10 +1031,38 @@
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz",
"integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
@@ -840,9 +1074,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz",
"integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
@@ -853,10 +1087,24 @@
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz",
"integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
@@ -1270,9 +1518,15 @@
}
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/acorn": {
@@ -1288,9 +1542,9 @@
}
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -1381,6 +1635,22 @@
"dev": true,
"license": "MIT/X11"
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -1411,7 +1681,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -1445,6 +1714,12 @@
"node": ">=8"
}
},
"node_modules/devalue": {
"version": "5.6.4",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
"license": "MIT"
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
@@ -1546,9 +1821,9 @@
"license": "MIT"
},
"node_modules/esrap": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz",
"integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -1639,9 +1914,9 @@
"license": "BSD-3-Clause"
},
"node_modules/immutable": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
"integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"dev": true,
"license": "MIT"
},
@@ -1955,9 +2230,9 @@
}
},
"node_modules/minizlib": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1967,22 +2242,6 @@
"node": ">= 18"
}
},
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mode-watcher": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz",
@@ -2050,6 +2309,16 @@
"svelte": "^5.7.0"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2083,9 +2352,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2124,6 +2393,20 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/regexparam": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz",
@@ -2134,13 +2417,13 @@
}
},
"node_modules/rollup": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz",
"integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.7"
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -2150,26 +2433,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.41.1",
"@rollup/rollup-android-arm64": "4.41.1",
"@rollup/rollup-darwin-arm64": "4.41.1",
"@rollup/rollup-darwin-x64": "4.41.1",
"@rollup/rollup-freebsd-arm64": "4.41.1",
"@rollup/rollup-freebsd-x64": "4.41.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.41.1",
"@rollup/rollup-linux-arm-musleabihf": "4.41.1",
"@rollup/rollup-linux-arm64-gnu": "4.41.1",
"@rollup/rollup-linux-arm64-musl": "4.41.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.41.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.41.1",
"@rollup/rollup-linux-riscv64-gnu": "4.41.1",
"@rollup/rollup-linux-riscv64-musl": "4.41.1",
"@rollup/rollup-linux-s390x-gnu": "4.41.1",
"@rollup/rollup-linux-x64-gnu": "4.41.1",
"@rollup/rollup-linux-x64-musl": "4.41.1",
"@rollup/rollup-win32-arm64-msvc": "4.41.1",
"@rollup/rollup-win32-ia32-msvc": "4.41.1",
"@rollup/rollup-win32-x64-msvc": "4.41.1",
"@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},
@@ -2200,6 +2488,19 @@
"tslib": "^2.1.0"
}
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"mri": "^1.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/sass-embedded": {
"version": "1.89.0",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.0.tgz",
@@ -2618,35 +2919,18 @@
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
"debug": "~4.4.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2693,21 +2977,23 @@
}
},
"node_modules/svelte": {
"version": "5.33.10",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.10.tgz",
"integrity": "sha512-/yArPQIBoQS2p86LKnvJywOXkVHeEXnFgrDPSxkEfIAEkykopYuy2bF6UUqHG4IbZlJD6OurLxJT8Kn7kTk9WA==",
"version": "5.53.6",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.6.tgz",
"integrity": "sha512-lP5DGF3oDDI9fhHcSpaBiJEkFLuS16h92DhM1L5K1lFm0WjOmUh1i2sNkBBk8rkxJRpob0dBE75jRfUzGZUOGA==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "^5.3.1",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.3",
"esm-env": "^1.2.1",
"esrap": "^1.4.6",
"esrap": "^2.2.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
@@ -2717,6 +3003,30 @@
"node": ">=18"
}
},
"node_modules/svelte-check": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz",
"integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"chokidar": "^4.0.1",
"fdir": "^6.2.0",
"picocolors": "^1.0.0",
"sade": "^1.7.4"
},
"bin": {
"svelte-check": "bin/svelte-check"
},
"engines": {
"node": ">= 18.0.0"
},
"peerDependencies": {
"svelte": "^4.0.0 || ^5.0.0-next.0",
"typescript": ">=5.0.0"
}
},
"node_modules/svelte-sonner": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.5.tgz",
@@ -2867,17 +3177,16 @@
}
},
"node_modules/tar": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"version": "7.5.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"minizlib": "^3.1.0",
"yallist": "^5.0.0"
},
"engines": {
@@ -2918,6 +3227,20 @@
"url": "https://github.com/sponsors/Wombosvideo"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -2941,9 +3264,9 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
+8 -2
View File
@@ -6,11 +6,15 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"check": "tsc -p tsconfig.json --noEmit && svelte-check && biome lint .",
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
"preview": "vite preview",
"deploy": "npm run build && npx wrangler pages deploy dist"
},
"devDependencies": {
"@internationalized/date": "^3.8.1",
"@biomejs/biome": "^1.9.4",
"@lucide/svelte": "^0.515.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.0.0",
@@ -18,13 +22,15 @@
"clsx": "^2.1.1",
"mode-watcher": "^1.1.0",
"sass-embedded": "^1.89.0",
"svelte": "^5.28.1",
"svelte": "^5.53.6",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.8.3",
"tw-animate-css": "^1.3.2",
"vite": "^6.4.1"
"vite": "^6.4.2"
},
"dependencies": {
"eventemitter3": "^5.0.1",
+1 -3
View File
@@ -1,10 +1,8 @@
import ifcopenshell
import ifcopenshell.util
import ifcopenshell.util.attribute
import ifcopenshell.util.pset
import ifcopenshell.util.schema
import xml.etree.ElementTree as ET
from xmlschema.validators.exceptions import XMLSchemaValidationError
from ifctester.ids import Ids, IdsXmlValidationError, get_schema
# https://github.com/buildingSMART/IDS/blob/9914d568c7ac037acd97e58a0d16e9f93c3e3416/Schema/ids.xsd#L232
+3 -6
View File
@@ -16,20 +16,17 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import argparse
import mimetypes
import os
import sys
bonsai_lib_path = os.environ.get("BONSAI_LIB_PATH")
print(os.environ)
print(bonsai_lib_path)
bonsai_version = os.environ.get("BONSAI_VERSION")
if bonsai_lib_path:
sys.path.insert(0, bonsai_lib_path)
from flask import Flask, send_from_directory, send_file
from flask import Flask, send_file, send_from_directory
# Browsers expecting this specific MIME type for .mjs files,
# otherwise they fail to load.
+1 -1
View File
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import Router from 'svelte-spa-router';
import routes from './routes';
</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 Dialog from "$lib/components/ui/dialog";
import * as IDS from "$src/modules/api/ids.svelte.js";
import * as API from "$src/modules/api/api.svelte.js";
import { error, success, info } from "$src/modules/utils/toast.svelte.js";
import * as IDS from "$src/modules/api/ids.svelte";
import * as API from "$src/modules/api/api.svelte";
import { error, success } from "$src/modules/utils/toast.svelte";
let { isOpen = false } = $props();
let { isOpen = false } : { isOpen?: boolean } = $props();
function openForum() {
window.open('https://community.osarch.org', '_blank');
@@ -23,8 +23,9 @@
try {
await IDS.openDocument();
} catch (err) {
if (err.message !== 'File selection cancelled') {
error('Error opening file: ' + err.message);
const message = err instanceof Error ? err.message : String(err);
if (message !== 'File selection cancelled') {
error(`Error opening file: ${message}`);
console.error(err);
}
}
@@ -50,7 +51,7 @@
success('Audit completed successfully');
} catch (err) {
console.error("Audit failed: ", err);
error(`Audit failed: check console for details`);
error("Audit failed: check console for details");
}
}
</script>
@@ -142,4 +143,4 @@
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
</Dialog.Root>
@@ -1,6 +1,5 @@
<script>
import { onMount } from "svelte";
import { Module } from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import { Module } from "$src/modules/api/ids.svelte";
</script>
<div class="app-ribbon">
@@ -20,4 +19,4 @@
<span>Error</span>
</div>
{/if}
</div>
</div>
@@ -1,14 +1,16 @@
<script>
<script lang="ts">
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 * as IDS from "$src/modules/api/ids.svelte.js";
import { error, success } from "$src/modules/utils/toast.svelte.js";
import { IFCModels, openIfc, unloadIfc, runAudit } from "$src/modules/api/api.svelte";
import * as IDS from "$src/modules/api/ids.svelte";
import { error, success } from "$src/modules/utils/toast.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 type { AuditReport } from "$src/types/report";
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);
const handleLoadModel = async () => {
@@ -16,15 +18,17 @@
await openIfc();
success('IFC model loaded successfully');
} 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 {
await unloadIfc(modelId);
} 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');
} catch (err) {
console.error("Audit failed: ", err);
error(`Audit failed: check console for details`);
error("Audit failed: check console for details");
} finally {
isAuditing = false;
}
};
const handleViewAuditReport = (auditId) => {
const auditReport = IFCModels.audits.find(audit => audit.id === auditId);
const handleViewAuditReport = (auditId: string) => {
const auditReport = IFCModels.audits.find(audit => audit.id === auditId) as AuditReport | undefined;
if (!auditReport) return;
// 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'];
let size = bytes;
let unitIndex = 0;
@@ -91,7 +95,7 @@
<div class="buttons">
<Tooltip.Provider>
{#if isMinimized}
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Root disableHoverableContent>
<Tooltip.Trigger>
<button class="tb-btn expand-btn" onclick={() => isMinimized = false} aria-label="Expand Toolbar">
<ChevronRightIcon size={24} />
@@ -102,7 +106,7 @@
</Tooltip.Content>
</Tooltip.Root>
{/if}
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Root disableHoverableContent>
<Tooltip.Trigger>
<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">
@@ -114,7 +118,7 @@
<p>Home</p>
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Root disableHoverableContent>
<Tooltip.Trigger>
<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">
@@ -692,4 +696,4 @@
opacity: 0.5;
}
}
</style>
</style>
@@ -1,7 +1,9 @@
<script>
<script lang="ts">
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>
<DropdownMenu.Root>
@@ -63,4 +65,4 @@
Part Of
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</DropdownMenu.Root>
@@ -1,13 +1,22 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
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;
}
function closeDocument(docId) {
function closeDocument(docId: string) {
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>
<div class="ids-tabs">
@@ -15,10 +24,13 @@
<div
class="ids-tab"
class:active={IDS.Module.activeDocument === docId}
role="button"
tabindex="0"
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
class="tab-close"
onclick={(e) => { e.stopPropagation(); closeDocument(docId); }}
@@ -31,4 +43,4 @@
</div>
{/each}
<div class="filler-tab"></div>
</div>
</div>
+3 -4
View File
@@ -1,8 +1,7 @@
{
"wasm": {
"wheel_url": "/worker/bin/ifcopenshell-0.8.3+bb329af-cp313-cp313-emscripten_4_0_9_wasm32.whl",
"wheel_url": "/worker/bin/ifcopenshell-0.8.5+a51b2c5-cp313-cp313-pyodide_2025_0_wasm32.whl",
"odfpy_url": "/worker/bin/odfpy-1.4.2-py2.py3-none-any.whl",
"api_py_url": "/worker/api.py",
"pyodide_url": "https://cdn.jsdelivr.net/pyodide/v0.28.0/full/pyodide.js"
"api_py_url": "/worker/api.py"
}
}
}
+2 -1
View File
@@ -576,7 +576,8 @@ html, body {
display: flex;
flex-direction: column;
label {
label,
.form-label {
margin-bottom: 4px;
font-size: 14px;
font-weight: 500;
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
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>
<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 XIcon from "@lucide/svelte/icons/x";
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
import * as Dialog from "./index";
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 {
ref = $bindable(null),
@@ -11,7 +20,7 @@
children,
showCloseButton = true,
...restProps
} = $props();
} : Props = $props();
</script>
<Dialog.Portal {...portalProps}>
@@ -35,4 +44,4 @@
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content>
</Dialog.Portal>
</Dialog.Portal>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
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 {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DialogPrimitive.Description
@@ -14,4 +19,4 @@
data-slot="dialog-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>
/>
@@ -1,11 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -15,4 +23,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,12 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -16,4 +23,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
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 {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DialogPrimitive.Overlay
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
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 {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DialogPrimitive.Title
@@ -14,4 +19,4 @@
data-slot="dialog-title"
class={cn("text-lg leading-none", className)}
{...restProps}
/>
/>
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
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>
<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 CheckIcon from "@lucide/svelte/icons/check";
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 {
ref = $bindable(null),
checked = $bindable(false),
@@ -10,7 +18,7 @@
class: className,
children: childrenProp,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
@@ -34,4 +42,4 @@
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>
</DropdownMenuPrimitive.CheckboxItem>
@@ -1,14 +1,21 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
sideOffset?: number;
portalProps?: Record<string, unknown>;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
@@ -22,4 +29,4 @@
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Portal>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
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 {
ref = $bindable(null),
class: className,
inset,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
@@ -15,4 +20,4 @@
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
{...restProps}
/>
/>
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
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>
<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>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
variant?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
inset,
variant = "default",
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.Item
@@ -21,4 +28,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,12 +1,20 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
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 {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -17,4 +25,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,11 +1,16 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
value?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
value = $bindable(),
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.RadioGroup
@@ -13,4 +18,4 @@
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>
/>
@@ -1,14 +1,22 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
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 {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.RadioItem
@@ -28,4 +36,4 @@
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>
</DropdownMenuPrimitive.RadioItem>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
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 {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.Separator
@@ -14,4 +19,4 @@
data-slot="dropdown-menu-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
/>
/>
@@ -1,12 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<span
@@ -16,4 +23,4 @@
{...restProps}
>
{@render children?.()}
</span>
</span>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
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 {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.SubContent
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,7 +1,15 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
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 {
ref = $bindable(null),
@@ -9,7 +17,7 @@
inset,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
@@ -24,4 +32,4 @@
>
{@render children?.()}
<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";
let { ref = $bindable(null), ...restProps } = $props();
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</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 CheckIcon from "@lucide/svelte/icons/check";
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 {
ref = $bindable(null),
class: className,
@@ -10,7 +18,7 @@
indeterminate = $bindable(false),
children: childrenProp,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.CheckboxItem
@@ -34,4 +42,4 @@
</span>
{@render childrenProp?.()}
{/snippet}
</MenubarPrimitive.CheckboxItem>
</MenubarPrimitive.CheckboxItem>
@@ -1,6 +1,19 @@
<script>
<script lang="ts">
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 {
ref = $bindable(null),
@@ -11,7 +24,7 @@
side = "bottom",
portalProps,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Portal {...portalProps}>
@@ -28,4 +41,4 @@
)}
{...restProps}
/>
</MenubarPrimitive.Portal>
</MenubarPrimitive.Portal>
@@ -1,12 +1,17 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import { Menubar as MenubarPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
inset?: boolean;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
inset,
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.GroupHeading
@@ -15,4 +20,4 @@
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
{...restProps}
/>
/>
@@ -1,10 +1,14 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let {
ref = $bindable(null),
...restProps
} = $props();
} : Props = $props();
</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 { 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 {
ref = $bindable(null),
@@ -8,7 +15,7 @@
inset = undefined,
variant = "default",
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Item
@@ -21,4 +28,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,12 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
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 {
ref = $bindable(null),
inset,
children,
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -17,4 +24,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,14 +1,22 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
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 {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.RadioItem
@@ -28,4 +36,4 @@
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</MenubarPrimitive.RadioItem>
</MenubarPrimitive.RadioItem>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
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 {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Separator
@@ -14,4 +19,4 @@
data-slot="menubar-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
/>
/>
@@ -1,12 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<span
@@ -16,4 +23,4 @@
{...restProps}
>
{@render children?.()}
</span>
</span>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
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 {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.SubContent
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,7 +1,15 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
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 {
ref = $bindable(null),
@@ -9,7 +17,7 @@
inset = undefined,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.SubTrigger
@@ -24,4 +32,4 @@
>
{@render children?.()}
<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 { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Trigger
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
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 {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Root
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,8 +1,8 @@
<script>
<script lang="ts">
import { Toaster as Sonner } from "svelte-sonner";
import { mode } from "mode-watcher";
let { ...restProps } = $props();
let { ...restProps } : Record<string, unknown> = $props();
</script>
<Sonner
@@ -10,4 +10,4 @@
class="toaster group"
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
{...restProps}
/>
/>
@@ -1,6 +1,18 @@
<script>
<script lang="ts">
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 {
ref = $bindable(null),
@@ -10,7 +22,7 @@
children,
arrowClasses,
...restProps
} = $props();
} : Props = $props();
</script>
<TooltipPrimitive.Portal>
@@ -42,4 +54,4 @@
{/snippet}
</TooltipPrimitive.Arrow>
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
</TooltipPrimitive.Portal>
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
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>
<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 * as IDS from "$src/modules/api/ids.svelte.js";
import * as IDS from "$src/modules/api/ids.svelte";
import hyperid from "hyperid";
import type { AuditReport, AuditReportData } from "$src/types/report";
import type { IdsDocument } from "$src/types/ids";
export let Autocompletions = $state({
type AutocompletionState = {
entityClasses: string[];
materialCategories: string[];
classificationSystems: Record<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: [],
materialCategories: [],
classificationSystems: {},
@@ -10,13 +33,13 @@ export let Autocompletions = $state({
isLoaded: false
});
export let IFCModels = $state({
export const IFCModels: IfcModelState = $state({
models: [],
isLoading: false,
audits: []
});
const id = hyperid();
const id: () => string = hyperid();
// Preload autocompletions on initialization
wasm.init().then(async () => {
@@ -30,26 +53,30 @@ export async function preloadAutocompletions() {
// Entity classes
const entitySets = await Promise.all(
schemas.map(schema => wasm.getAllEntityClasses(schema))
);
const allEntities = new Set();
entitySets.forEach(entities => {
entities.forEach(entity => allEntities.add(entity.toUpperCase()));
});
) as string[][];
const allEntities = new Set<string>();
for (const entities of entitySets) {
for (const entity of entities) {
allEntities.add(entity.toUpperCase());
}
}
// Data types
const dataTypeSets = await Promise.all(
schemas.map(schema => wasm.getAllDataTypes(schema))
);
const allDataTypes = new Set();
dataTypeSets.forEach(dataTypes => {
Object.keys(dataTypes).forEach(dataType => allDataTypes.add(dataType));
});
) as Record<string, string>[];
const allDataTypes = new Set<string>();
for (const dataTypes of dataTypeSets) {
for (const dataType of Object.keys(dataTypes as Record<string, string>)) {
allDataTypes.add(dataType);
}
}
// Material categories and Classification systems
const [materialCategories, classificationSystems] = await Promise.all([
wasm.getMaterialCategories(),
wasm.getStandardClassificationSystems()
]);
]) as [string[], AutocompletionState["classificationSystems"]];
// Cache autocompletions
Autocompletions.entityClasses = Array.from(allEntities).sort();
@@ -64,15 +91,15 @@ export async function preloadAutocompletions() {
}
}
export async function getPredefinedTypes(schema, entity) {
export async function getPredefinedTypes(schema: string, entity: string) {
return await wasm.getPredefinedTypes(schema, entity);
}
export async function getEntityAttributes(schema, entity) {
export async function getEntityAttributes(schema: string, entity: string) {
return await wasm.getEntityAttributes(schema, entity);
}
export async function getApplicablePsets(schema, entity, predefinedType = '') {
export async function getApplicablePsets(schema: string, entity: string, predefinedType = '') {
return await wasm.getApplicablePsets(schema, entity, predefinedType);
}
@@ -92,7 +119,7 @@ export function getDataTypes() {
return Autocompletions.dataTypes;
}
export async function loadIfc(file) {
export async function loadIfc(file: File): Promise<IfcModel> {
try {
IFCModels.isLoading = true;
@@ -100,10 +127,10 @@ export async function loadIfc(file) {
const uint8Array = new Uint8Array(arrayBuffer);
// Load IFC model
const ifcId = await wasm.loadIfc(Array.from(uint8Array));
const ifcId = await wasm.loadIfc(Array.from(uint8Array)) as string;
// Add to models list
const model = {
const model: IfcModel = {
id: ifcId,
fileName: file.name,
fileSize: file.size,
@@ -121,7 +148,7 @@ export async function loadIfc(file) {
}
}
export async function unloadIfc(modelId) {
export async function unloadIfc(modelId: string) {
try {
// Unload model
await wasm.unloadIfc(modelId);
@@ -136,9 +163,9 @@ export async function unloadIfc(modelId) {
}
}
export async function auditIfc(modelId, idsData) {
export async function auditIfc(modelId: string, idsData: string | Uint8Array | ArrayBuffer) {
try {
let idsBytes;
let idsBytes: Uint8Array;
if (typeof idsData === 'string') {
idsBytes = new TextEncoder().encode(idsData);
} else if (idsData instanceof ArrayBuffer) {
@@ -148,7 +175,7 @@ export async function auditIfc(modelId, idsData) {
}
// Run audit
const auditResult = await wasm.auditIfc(modelId, idsBytes);
const auditResult = await wasm.auditIfc(modelId, idsBytes) as { json: AuditReportData; html: string };
console.log(`Audit completed for model ${modelId}`);
return auditResult;
@@ -163,13 +190,14 @@ export function getLoadedModels() {
}
export async function openIfc() {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.ifc';
fileInput.onchange = async (event) => {
const file = event.target.files[0];
const target = event.target as HTMLInputElement | null;
const file = target?.files?.[0];
if (!file) {
reject(new Error('No file selected'));
return;
@@ -194,15 +222,20 @@ export async function openIfc() {
});
}
export function getIfcById(modelId) {
export function getIfcById(modelId: string) {
return IFCModels.models.find(model => model.id === modelId);
}
export function createAuditReport(modelId, document, auditData, htmlReport = null) {
export function createAuditReport(
modelId: string,
document: string,
auditData: AuditReportData,
htmlReport: string | null = null
): AuditReport | undefined {
const model = getIfcById(modelId);
if (!model) return;
const auditReport = {
const auditReport: AuditReport = {
id: id(),
modelId: modelId,
modelName: model.fileName,
@@ -216,19 +249,19 @@ export function createAuditReport(modelId, document, auditData, htmlReport = nul
return auditReport;
}
export function getAuditReportsForIfc(modelId) {
export function getAuditReportsForIfc(modelId: string) {
return IFCModels.audits.filter(audit => audit.modelId === modelId);
}
export function getAuditReportById(auditId) {
export function getAuditReportById(auditId: string) {
return IFCModels.audits.find(audit => audit.id === auditId);
}
export function clearIdsAuditReports(document) {
export function clearIdsAuditReports(document: string) {
IFCModels.audits = IFCModels.audits.filter(audit => audit.document !== document);
}
export async function downloadAuditReport(auditId) {
export async function downloadAuditReport(auditId: string) {
const audit = getAuditReportById(auditId);
if (!audit || !audit.htmlReport) {
throw new Error('HTML report not available for this audit');
@@ -237,7 +270,7 @@ export async function downloadAuditReport(auditId) {
// Get IDS document title for filename
let filename = 'report.html';
if (audit.document && IDS.Module.documents[audit.document]) {
const doc = IDS.Module.documents[audit.document];
const doc = IDS.Module.documents[audit.document] as IdsDocument;
const title = doc.info?.title || 'untitled';
filename = `report_${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.html`;
}
@@ -270,9 +303,12 @@ export async function runAudit() {
// Get the active IDS document XML
const idsXml = await IDS.exportActiveDocument();
if (!idsXml) {
throw new Error('Failed to export IDS document');
}
// Run audit on all loaded models
let firstAuditReport = null;
let firstAuditReport: AuditReport | undefined;
for (const model of IFCModels.models) {
const result = await auditIfc(model.id, idsXml);
@@ -280,7 +316,10 @@ export async function runAudit() {
const jsonData = result.json || null;
const htmlReport = result.html || null;
const auditReport = createAuditReport(model.id, IDS.Module.activeDocument, jsonData, htmlReport);
if (!jsonData) {
continue;
}
const auditReport = createAuditReport(model.id, IDS.Module.activeDocument as string, jsonData, htmlReport);
// Store the first audit report to open in viewer
if (!firstAuditReport) {
@@ -1,12 +1,37 @@
import { io } from 'socket.io-client';
import { IFCModels } from './api.svelte.js';
import * as IDS from './ids.svelte.js';
import { error, success } from '../utils/toast.svelte.js';
import type { Socket } from 'socket.io-client';
import { IFCModels } from './api.svelte';
import * as IDS from './ids.svelte';
import { error, success } from '../utils/toast.svelte';
import hyperid from 'hyperid';
import { onMount } from 'svelte';
import type { AuditReport, AuditReportData } from "$src/types/report";
// Bonsai connection state
export let Bonsai = $state({
type BonsaiState = {
enabled: boolean;
port: string | null;
socket: Socket | null;
connected: boolean;
auditing: boolean;
};
type PendingAudit = {
resolve: (value: string | null) => void;
reject: (reason?: unknown) => void;
};
type AuditResultPayload = {
id?: string;
json_report?: string;
html_report?: string;
};
type AuditErrorPayload = {
id?: string;
error?: string;
};
export const Bonsai: BonsaiState = $state({
enabled: false,
port: null,
socket: null,
@@ -14,8 +39,8 @@ export let Bonsai = $state({
auditing: false
});
const id = hyperid();
const pendingAudits = new Map();
const id: () => string = hyperid();
const pendingAudits = new Map<string, PendingAudit>();
// Check for Bonsai server port in URL parameters
const urlParams = new URLSearchParams(window.location.search);
@@ -29,8 +54,11 @@ if (serverPort) {
/**
* Connect to Bonsai server
*/
export const connect = () => new Promise((resolve, reject) => {
if (!Bonsai.port) return;
export const connect = () => new Promise<void>((resolve, reject) => {
if (!Bonsai.port) {
resolve();
return;
}
try {
Bonsai.socket = io(`ws://127.0.0.1:${Bonsai.port}/ifctester`, {
@@ -49,7 +77,7 @@ export const connect = () => new Promise((resolve, reject) => {
Bonsai.connected = false;
});
Bonsai.socket.on('connect_error', (err) => {
Bonsai.socket.on('connect_error', (err: Error) => {
Bonsai.connected = false;
error(`Failed to connect to Bonsai: ${err.message}`);
reject(err);
@@ -59,7 +87,8 @@ export const connect = () => new Promise((resolve, reject) => {
Bonsai.socket.on('error', handleAuditError);
} catch (err) {
error(`Failed to connect to Bonsai: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to connect to Bonsai: ${message}`);
reject(err);
}
});
@@ -93,14 +122,21 @@ export const runAudit = async () => {
// Convert IDS document to XML string
const idsXml = await IDS.exportActiveDocument();
if (!idsXml) {
throw new Error('Failed to export IDS document');
}
const requestId = id();
const socket = Bonsai.socket;
if (!socket) {
throw new Error('Bonsai socket not connected');
}
return new Promise((resolve, reject) => {
return new Promise<string | null>((resolve, reject) => {
// Store request with resolve/reject functions
pendingAudits.set(requestId, { resolve, reject });
Bonsai.socket.emit('audit_ids', {
socket.emit('audit_ids', {
id: requestId,
ids: idsXml
});
@@ -108,7 +144,8 @@ export const runAudit = async () => {
} catch (err) {
Bonsai.auditing = false;
error(`Failed to run Bonsai audit: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to run Bonsai audit: ${message}`);
return null;
}
};
@@ -117,7 +154,7 @@ export const runAudit = async () => {
* Handles audit results from Bonsai server
* @param {Object} data - Audit result data
*/
const handleAuditResult = (data) => {
const handleAuditResult = (data: AuditResultPayload) => {
if (!data.id || !data.json_report) return;
const pendingAudit = pendingAudits.get(data.id);
@@ -130,13 +167,14 @@ const handleAuditResult = (data) => {
const { resolve } = pendingAudit;
try {
const reportData = JSON.parse(data.json_report);
const reportData = JSON.parse(data.json_report) as AuditReportData;
const auditReport = {
const auditReport: AuditReport = {
id: data.id,
modelId: `bonsai:${data.id}`,
date: new Date().toISOString(),
modelName: 'Bonsai IFC Model',
document: IDS.Module.activeDocument,
document: IDS.Module.activeDocument ?? "",
data: reportData,
htmlReport: data.html_report
};
@@ -152,7 +190,8 @@ const handleAuditResult = (data) => {
} catch (err) {
Bonsai.auditing = false;
error(`Failed to process audit result: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to process audit result: ${message}`);
resolve(null);
}
};
@@ -161,7 +200,7 @@ const handleAuditResult = (data) => {
* Handles audit errors from Bonsai server
* @param {Object} data - Error data
*/
const handleAuditError = (data) => {
const handleAuditError = (data: AuditErrorPayload) => {
if (!data.id) return;
const pendingAudit = pendingAudits.get(data.id);
@@ -174,7 +213,6 @@ const handleAuditError = (data) => {
const { resolve } = pendingAudit;
Bonsai.auditing = false;
error(`Audit failed (Bonsai): ${data.error}`);
error(`Audit failed (Bonsai): ${data.error ?? "Unknown error"}`);
resolve(null);
};
@@ -1,10 +1,18 @@
import wasm from "$src/modules/wasm";
import { clearIdsAuditReports } from "./api.svelte.js";
import { clearIdsAuditReports } from "./api.svelte";
import hyperid from "hyperid";
import {tick} from "svelte";
import type { DocumentState, Facet, FacetValue, IdsDocument, IdsCardinality, Restriction, Specification } from "$src/types/ids";
export let Module = $state({
documents: [],
type ModuleState = {
documents: Record<string, IdsDocument>;
activeDocument: string | null;
status: "loading" | "ready" | "error";
states: Record<string, DocumentState>;
};
export const Module: ModuleState = $state({
documents: {},
activeDocument: null,
status: "loading",
states: {}
@@ -17,14 +25,15 @@ wasm.init().then(() => {
Module.status = "error";
});
const id = hyperid()
const id: () => string = hyperid();
export function setDocumentState(docId, updates) {
export function setDocumentState(docId: string, updates: Partial<DocumentState>) {
if (!Module.states[docId]) {
Module.states[docId] = {
activeTab: 'info',
viewMode: 'editor',
activeSpecification: null
activeSpecification: null,
auditReport: null
};
}
Object.assign(Module.states[docId], updates);
@@ -32,7 +41,7 @@ export function setDocumentState(docId, updates) {
export async function createDocument() {
const docId = id();
const doc = await wasm.createIDS();
const doc = await wasm.createIDS() as IdsDocument;
Module.documents[docId] = doc;
@@ -43,14 +52,14 @@ export async function createDocument() {
Module.activeDocument = docId;
}
export async function deleteDocument(id) {
export async function deleteDocument(id: string) {
// Clear any audit reports generated using this IDS document
clearIdsAuditReports(id);
delete Module.documents[id];
delete Module.states[id];
if (Module.activeDocument == id) {
if (Module.activeDocument === id) {
// If there are other documents, set the first one as active
if (Object.keys(Module.documents).length > 0) {
Module.activeDocument = Object.keys(Module.documents)[0];
@@ -62,19 +71,19 @@ export async function deleteDocument(id) {
// Normalize (remove xs: prefix) from JSON dict returned from Python
// We need this because the backend exports with xs: prefix, yet expects a dict without prefixes.
function normalizeIdsDict(obj) {
function normalizeIdsDict(obj: unknown): unknown {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(normalizeIdsDict);
}
const result = {};
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (key === 'xs:restriction' && Array.isArray(value) && value.length > 0) {
// Convert xs:restriction array to restriction object
const restriction = value[0];
const newRestriction = {};
const restriction = value[0] as Record<string, unknown>;
const newRestriction: Record<string, unknown> = {};
for (const [restrictionKey, restrictionValue] of Object.entries(restriction)) {
if (restrictionKey.startsWith('xs:')) {
@@ -86,7 +95,7 @@ function normalizeIdsDict(obj) {
}
}
result['restriction'] = newRestriction;
result.restriction = newRestriction;
} else {
result[key] = normalizeIdsDict(value);
}
@@ -96,13 +105,16 @@ function normalizeIdsDict(obj) {
}
export async function openDocument() {
return new Promise((resolve, reject) => {
const fileInput = document.createElement('input');
return new Promise<void>((resolve, reject) => {
const fileInput = document.createElement('input') as HTMLInputElement & {
oncancel?: ((this: HTMLInputElement, ev: Event) => void) | null;
};
fileInput.type = 'file';
fileInput.accept = '.ids,.xml';
fileInput.onchange = async (event) => {
const file = event.target.files[0];
const target = event.target as HTMLInputElement | null;
const file = target?.files?.[0];
if (!file) {
reject(new Error('No file selected'));
return;
@@ -112,8 +124,8 @@ export async function openDocument() {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const fileContent = e.target.result;
const doc = normalizeIdsDict(await wasm.openIDS(fileContent, false));
const fileContent = (e.target as FileReader).result;
const doc = normalizeIdsDict(await wasm.openIDS(String(fileContent), false)) as IdsDocument;
const docId = id();
// Add document to list and set as active
@@ -145,16 +157,16 @@ export async function openDocument() {
});
}
export async function exportActiveDocument() {
export async function exportActiveDocument(): Promise<string | null> {
if (!Module.activeDocument) return null;
const doc = $state.snapshot(Module.documents[Module.activeDocument]);
const xmlString = await wasm.exportIDS(doc);
const xmlString = await wasm.exportIDS(doc as Record<string, unknown>) as string;
return xmlString;
}
export async function exportDocument(docId) {
export async function exportDocument(docId: string) {
const doc = $state.snapshot(Module.documents[docId]);
// Validate
@@ -162,37 +174,38 @@ export async function exportDocument(docId) {
throw new Error("Please create at least one specification before exporting the document.");
}
const xmlString = await wasm.exportIDS(doc);
const xmlString = await wasm.exportIDS(doc as Record<string, unknown>) as string;
// Create and download file
const blob = new Blob([xmlString], { type: 'application/xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${Module.documents[docId].info.title.replace(/[^a-zA-Z0-9]/g, '_')}.ids`;
const title = Module.documents[docId].info.title || "untitled";
a.download = `${title.replace(/[^a-zA-Z0-9]/g, '_')}.ids`;
a.click();
URL.revokeObjectURL(url);
}
export async function createSpecification(docId) {
const spec = await wasm.createSpecification();
export async function createSpecification(docId: string) {
const spec = await wasm.createSpecification() as Specification;
// Add specification to document
Module.documents[docId].specifications.specification.push(spec);
// Set as active specification
if (Module.activeDocument == docId) {
if (Module.activeDocument === docId) {
const state = Module.states[docId];
state.activeSpecification = Module.documents[docId].specifications.specification.length - 1;
}
}
export async function deleteSpecification(docId, specId) {
export async function deleteSpecification(docId: string, specId: number) {
Module.documents[docId].specifications.specification.splice(specId, 1);
if (Module.activeDocument == docId) {
if (Module.activeDocument === docId) {
const state = Module.states[docId];
if (state.activeSpecification == specId) {
if (state.activeSpecification === specId) {
// We need to wait for the next tick here because of Svelte's internal shenanigans
await tick();
setDocumentState(docId, { activeSpecification: null });
@@ -209,124 +222,148 @@ export async function deleteSpecification(docId, specId) {
* clause: "applicability", "requirements"
* facet: "entity", "attribute", "classification", "partOf", "property", "material"
*/
export async function createFacet(docId, specId, clause, facet) {
let facetObj;
if (facet == "entity") {
facetObj = await wasm.createEntityFacet(clause, {});
} else if (facet == "attribute") {
facetObj = await wasm.createAttributeFacet(clause, {});
} else if (facet == "classification") {
facetObj = await wasm.createClassificationFacet(clause, {});
} else if (facet == "partOf") {
facetObj = await wasm.createPartOfFacet(clause, {});
} else if (facet == "property") {
facetObj = await wasm.createPropertyFacet(clause, {});
} else if (facet == "material") {
facetObj = await wasm.createMaterialFacet(clause, {});
export async function createFacet(
docId: string,
specId: number,
clause: "applicability" | "requirements",
facet: "entity" | "attribute" | "classification" | "partOf" | "property" | "material"
) {
let facetObj: Facet | undefined;
if (facet === "entity") {
facetObj = await wasm.createEntityFacet(clause, {}) as Facet;
} else if (facet === "attribute") {
facetObj = await wasm.createAttributeFacet(clause, {}) as Facet;
} else if (facet === "classification") {
facetObj = await wasm.createClassificationFacet(clause, {}) as Facet;
} else if (facet === "partOf") {
facetObj = await wasm.createPartOfFacet(clause, {}) as Facet;
} else if (facet === "property") {
facetObj = await wasm.createPropertyFacet(clause, {}) as Facet;
} else if (facet === "material") {
facetObj = await wasm.createMaterialFacet(clause, {}) as Facet;
}
if (!(facet in Module.documents[docId].specifications.specification[specId][clause])) {
Module.documents[docId].specifications.specification[specId][clause][facet] = [];
if (!facetObj) return;
const spec = Module.documents[docId].specifications.specification[specId];
const clauseKey = clause as "applicability" | "requirements";
if (!spec[clauseKey]) spec[clauseKey] = {};
if (!(facet in (spec[clauseKey] as Record<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) {
delete Module.documents[docId].specifications.specification[specId][clause][facet][facetId];
export async function deleteFacet(
docId: string,
specId: number,
clause: "applicability" | "requirements",
facet: "entity" | "attribute" | "classification" | "partOf" | "property" | "material",
facetId: number
) {
const spec = Module.documents[docId].specifications.specification[specId];
const list = (spec[clause] as Record<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';
const minOccurs = spec.applicability["@minOccurs"];
const maxOccurs = spec.applicability["@maxOccurs"];
if (minOccurs === 1 && maxOccurs === "unbounded") return 'required';
if (minOccurs === 0 && maxOccurs === "unbounded") return 'optional';
if (minOccurs === 0 && maxOccurs === 0) return 'prohibited';
const minOccurs = spec.applicability["@minOccurs"] as number | undefined;
const maxOccurs = spec.applicability["@maxOccurs"] as number | "unbounded" | undefined;
if (minOccurs !== 0) return 'required';
if (minOccurs === 0 && maxOccurs !== 0) return 'optional';
if (maxOccurs === 0) return 'prohibited';
return 'required';
};
// Converts facet to human-readable description
export function stringifyFacet(clauseType, facet, facetType, spec) {
export function stringifyFacet(
clauseType: "applicability" | "requirements",
facet: Facet,
facetType: string,
spec?: Specification | null
) {
if (!facet) return "";
const usage = getSpecUsage(spec);
const descriptions = [];
const descriptions: string[] = [];
// Entity facet
if (facetType === "entity") {
if (clauseType === "applicability") {
descriptions.push(`All data where IFC class ${stringifyValue(facet.name)}`);
descriptions.push(`All data where IFC class ${stringifyValue(facet.name as FacetValue)}`);
} else {
descriptions.push(`Shall be data where IFC class ${stringifyValue(facet.name)}`);
descriptions.push(`Shall be data where IFC class ${stringifyValue(facet.name as FacetValue)}`);
}
if (facet.predefinedType) {
descriptions.push(`and type ${stringifyValue(facet.predefinedType)}`);
descriptions.push(`and type ${stringifyValue(facet.predefinedType as FacetValue)}`);
}
}
// Attribute facet
else if (facetType === "attribute") {
if (clauseType === "applicability") {
descriptions.push(`All data where attribute ${stringifyValue(facet.name)}`);
descriptions.push(`All data where attribute ${stringifyValue(facet.name as FacetValue)}`);
} else {
descriptions.push(`Shall be data where attribute ${stringifyValue(facet.name)}`);
descriptions.push(`Shall be data where attribute ${stringifyValue(facet.name as FacetValue)}`);
}
descriptions.push(`and value ${stringifyValue(facet.value)}`);
descriptions.push(`and value ${stringifyValue(facet.value as FacetValue)}`);
}
// Property facet
else if (facetType === "property") {
if (clauseType === "applicability") {
descriptions.push(`Elements where property ${stringifyValue(facet.baseName)}`);
descriptions.push(`Elements where property ${stringifyValue(facet.baseName as FacetValue)}`);
} else {
descriptions.push(`Shall be elements where property ${stringifyValue(facet.baseName)}`);
descriptions.push(`Shall be elements where property ${stringifyValue(facet.baseName as FacetValue)}`);
}
if (facet.value) {
descriptions.push(`and value ${stringifyValue(facet.value)}`);
descriptions.push(`and value ${stringifyValue(facet.value as FacetValue)}`);
}
descriptions.push(`and dataset ${stringifyValue(facet.propertySet)}`);
descriptions.push(`and dataset ${stringifyValue(facet.propertySet as FacetValue)}`);
}
// Classification facet
else if (facetType === "classification") {
if (clauseType === "applicability") {
descriptions.push(`All data where classification system ${stringifyValue(facet.system)}`);
descriptions.push(`All data where classification system ${stringifyValue(facet.system as FacetValue)}`);
} else {
descriptions.push(`Shall be data where classification system ${stringifyValue(facet.system)}`);
descriptions.push(`Shall be data where classification system ${stringifyValue(facet.system as FacetValue)}`);
}
if (facet.value) {
descriptions.push(`and classification ${stringifyValue(facet.value)}`);
descriptions.push(`and classification ${stringifyValue(facet.value as FacetValue)}`);
}
}
// Material facet
else if (facetType === "material") {
if (clauseType === "applicability") {
descriptions.push(`All data where material ${stringifyValue(facet.value)}`);
descriptions.push(`All data where material ${stringifyValue(facet.value as FacetValue)}`);
} else {
descriptions.push(`Shall be data where material ${stringifyValue(facet.value)}`);
descriptions.push(`Shall be data where material ${stringifyValue(facet.value as FacetValue)}`);
}
}
// PartOf facet
else if (facetType === "partOf") {
if (clauseType === "applicability") {
descriptions.push(`An element with an **${facet['@relation']}** relationship`);
descriptions.push(`An element with an **${String(facet['@relation'] ?? "")}** relationship`);
if (facet.name) {
descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name)}`);
descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name as FacetValue)}`);
}
} else {
descriptions.push(`An element shall have an **${facet['@relation']}** relationship`);
descriptions.push(`An element shall have an **${String(facet['@relation'] ?? "")}** relationship`);
if (facet.name) {
descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name)}`);
descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name as FacetValue)}`);
}
if (facet.predefinedType) {
descriptions.push(`and predefined type ${stringifyValue(facet.predefinedType)}`);
descriptions.push(`and predefined type ${stringifyValue(facet.predefinedType as FacetValue)}`);
}
}
}
@@ -336,20 +373,22 @@ export function stringifyFacet(clauseType, facet, facetType, spec) {
// Post-process for prohibited and optional requirements
let isProhibited = false;
if (usage == "prohibited") isProhibited = !isProhibited;
if (clauseType == "requirements" && "@cardinality" in facet && facet["@cardinality"] == "prohibited") isProhibited = !isProhibited;
if (usage === "prohibited") isProhibited = !isProhibited;
if (clauseType === "requirements" && "@cardinality" in facet && facet["@cardinality"] === "prohibited") {
isProhibited = !isProhibited;
}
if (isProhibited)
combined = combined.replace("Shall", "Shall not").replace("shall", "shall not");
if (clauseType == "requirements" && "@cardinality" in facet && facet["@cardinality"] == "optional")
if (clauseType === "requirements" && "@cardinality" in facet && facet["@cardinality"] === "optional")
combined = combined.replace("Shall", "May").replace("shall", "may");
return renderFacetString(combined);
}
// Converts value objects to human-readable strings
function stringifyValue(value) {
function stringifyValue(value?: FacetValue) {
if (!value) return "is provided";
if (value.simpleValue) return `is **${value.simpleValue}**`;
if (value.restriction) return stringifyRestriction(value.restriction);
@@ -357,7 +396,7 @@ function stringifyValue(value) {
}
// Converts restriction objects to human-readable strings
function stringifyRestriction(restriction) {
function stringifyRestriction(restriction: Restriction) {
if (!restriction) return "";
// Handle enumeration
@@ -394,7 +433,7 @@ function stringifyRestriction(restriction) {
if (restriction.maxExclusive && restriction.maxExclusive.length > 0) {
parts.push(`**< ${restriction.maxExclusive[0]['@value'] || ''}**`);
}
return parts.length > 0 ? "is in range " + parts.join(", ") : "has range restriction";
return parts.length > 0 ? `is in range ${parts.join(", ")}` : "has range restriction";
}
// Handle length range restrictions
@@ -406,18 +445,18 @@ function stringifyRestriction(restriction) {
if (restriction.maxLength && restriction.maxLength.length > 0) {
parts.push(`**max length ${restriction.maxLength[0]['@value'] || ''}**`);
}
return parts.length > 0 ? "has " + parts.join(", ") : "has length range restriction";
return parts.length > 0 ? `has ${parts.join(", ")}` : "has length range restriction";
}
return "has complex restriction";
}
function renderFacetString(text) {
function renderFacetString(text: string): string {
// Convert **text** to <strong>text</strong>
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
const withStrong = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// 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
* @param {string} message - The error message to display
*/
export function error(message) {
export function error(message: string): void {
toast.error(message);
}
@@ -12,7 +12,7 @@ export function error(message) {
* Show a success toast notification
* @param {string} message - The success message to display
*/
export function success(message) {
export function success(message: string): void {
toast.success(message);
}
@@ -20,7 +20,7 @@ export function success(message) {
* Show an info toast notification
* @param {string} message - The info message to display
*/
export function info(message) {
export function info(message: string): void {
toast.info(message);
}
@@ -28,7 +28,7 @@ export function info(message) {
* Show a warning toast notification
* @param {string} message - The warning message to display
*/
export function warning(message) {
export function warning(message: string): void {
toast.warning(message);
}
@@ -37,7 +37,7 @@ export function warning(message) {
* @param {string} message - The loading message to display
* @returns {string} - Toast ID for dismissing later
*/
export function loading(message) {
export function loading(message: string): string | number {
return toast.loading(message);
}
@@ -45,7 +45,7 @@ export function loading(message) {
* Dismiss a specific toast
* @param {string} toastId - The toast ID to dismiss
*/
export function dismiss(toastId) {
export function dismiss(toastId: string | number): void {
toast.dismiss(toastId);
}
@@ -57,10 +57,16 @@ export function dismiss(toastId) {
* @param {string} messages.success - Success message
* @param {string} messages.error - Error message
*/
export function promise(promiseToTrack, messages) {
type PromiseToastMessages = {
loading: string;
success: string;
error: string;
};
export function promise<T>(promiseToTrack: Promise<T>, messages: PromiseToastMessages) {
return toast.promise(promiseToTrack, {
loading: messages.loading,
success: messages.success,
error: messages.error,
});
}
}
@@ -5,6 +5,7 @@
import hyperid from "hyperid";
import EventEmitter from "eventemitter3";
import type { WorkerResponse } from "$src/types/wasm";
// Message types
export const MessageType = {
@@ -25,52 +26,57 @@ export const MessageType = {
// WASM module disposed
DISPOSED: 'disposed'
} as const;
type PendingMessage = {
resolve: (value?: unknown) => void;
reject: (reason?: unknown) => void;
};
type WasmReadyState = boolean | Promise<boolean>;
class WASMModule extends EventEmitter {
id = hyperid();
ready = false;
worker = null;
pendingMessages = new Map();
ready: WasmReadyState = false;
worker: Worker | null = null;
pendingMessages = new Map<string, PendingMessage>();
async init() {
if (this.ready === true) return;
else if (this.ready instanceof Promise) return this.ready;
if (this.ready instanceof Promise) return this.ready;
this.worker = new Worker(new URL('./worker/worker.js', import.meta.url), {type: 'module'});
this.worker = new Worker(new URL('./worker/worker.ts', import.meta.url), {type: 'module'});
this.worker.onmessage = (event) => {
this.worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
this._handleWorkerMessage(event.data);
};
this.worker.onerror = (error) => {
this.worker.onerror = (error: ErrorEvent) => {
console.error('[WASM] Web worker error:', error);
this._rejectPendingMessages(error);
};
this.ready = new Promise(async (resolve, reject) => {
try {
await this._sendMessage(MessageType.INIT);
resolve(true);
} catch (error) {
this.ready = this._sendMessage(MessageType.INIT)
.then(() => true)
.catch((error) => {
console.error('[WASM] Failed to initialize:', error);
this.ready = false;
reject(error);
}
});
throw error;
});
return this.ready;
}
async _sendMessage(type, payload = {}) {
if (!this.worker) throw new Error('Worker not initialized');
async _sendMessage(type: string, payload: Record<string, unknown> = {}): Promise<unknown> {
const worker = this.worker;
if (!worker) throw new Error('Worker not initialized');
const id = this.id();
return new Promise((resolve, reject) => {
this.pendingMessages.set(id, { resolve, reject });
this.worker.postMessage({
worker.postMessage({
type,
payload,
id
@@ -78,7 +84,7 @@ class WASMModule extends EventEmitter {
});
}
_handleWorkerMessage({ type, payload, id }) {
_handleWorkerMessage({ type, payload, id }: WorkerResponse) {
const pendingMessage = this.pendingMessages.get(id);
if (!pendingMessage) {
@@ -97,23 +103,28 @@ class WASMModule extends EventEmitter {
case MessageType.API_RESPONSE:
resolve(payload);
break;
case MessageType.ERROR:
reject(new Error(payload.message));
case MessageType.ERROR: {
const message =
payload && typeof payload === "object" && "message" in payload
? String(payload.message)
: "Unknown worker error";
reject(new Error(message));
break;
}
default:
console.warn('[WASM] Unknown message type:', type);
reject(new Error(`Unknown message type: ${type}`));
}
}
_rejectPendingMessages(error) {
_rejectPendingMessages(error: unknown) {
for (const { reject } of this.pendingMessages.values()) {
reject(error);
}
this.pendingMessages.clear();
}
async _apiCall(method, ...args) {
async _apiCall(method: string, ...args: unknown[]) {
if (!this.ready) await this.init();
const result = await this._sendMessage(MessageType.API_CALL, { method, args });
@@ -123,35 +134,35 @@ class WASMModule extends EventEmitter {
/**
* Get all entity classes in a given IFC schema
*/
async getAllEntityClasses(schema) {
async getAllEntityClasses(schema: string) {
return this._apiCall('getAllEntityClasses', schema);
}
/**
* Get all data types in a given IFC schema
*/
async getAllDataTypes(schema) {
async getAllDataTypes(schema: string) {
return this._apiCall('getAllDataTypes', schema);
}
/**
* Get predefined types for a given IFC entity
*/
async getPredefinedTypes(schema, entity) {
async getPredefinedTypes(schema: string, entity: string) {
return this._apiCall('getPredefinedTypes', schema, entity);
}
/**
* Get all attributes for a given IFC entity
*/
async getEntityAttributes(schema, entity) {
async getEntityAttributes(schema: string, entity: string) {
return this._apiCall('getEntityAttributes', schema, entity);
}
/**
* Get applicable property sets for a given IFC entity
*/
async getApplicablePsets(schema, entity, predefinedType = '') {
async getApplicablePsets(schema: string, entity: string, predefinedType = '') {
return this._apiCall('getApplicablePsets', schema, entity, predefinedType);
}
@@ -172,21 +183,21 @@ class WASMModule extends EventEmitter {
/**
* Load an IFC file. Returns a unique ID for the loaded file.
*/
async loadIfc(ifcData) {
async loadIfc(ifcData: number[] | Uint8Array | ArrayBuffer) {
return this._apiCall('loadIfc', ifcData);
}
/**
* Unload an IFC file
*/
async unloadIfc(ifcId) {
async unloadIfc(ifcId: string) {
return this._apiCall('unloadIfc', ifcId);
}
/**
* Audit a loaded IFC file against IDS specifications
*/
async auditIfc(ifcId, idsData) {
async auditIfc(ifcId: string, idsData: ArrayBuffer | Uint8Array | number[]) {
const idsBytes = idsData instanceof ArrayBuffer ? new Uint8Array(idsData) : idsData;
return this._apiCall('auditIfc', ifcId, Array.from(idsBytes));
@@ -204,70 +215,70 @@ class WASMModule extends EventEmitter {
/**
* Open an existing IDS from XML string
*/
async openIDS(idsXml, validate = false) {
async openIDS(idsXml: string, validate = false) {
return this._apiCall('openIDS', idsXml, validate);
}
/**
* Create a specification
*/
async createSpecification(options = {}) {
async createSpecification(options: Record<string, unknown> = {}) {
return this._apiCall('createSpecification', options);
}
/**
* Create an entity facet
*/
async createEntityFacet(clause, options = {}) {
async createEntityFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createEntityFacet', clause, options);
}
/**
* Create an attribute facet
*/
async createAttributeFacet(clause, options = {}) {
async createAttributeFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createAttributeFacet', clause, options);
}
/**
* Create a property facet
*/
async createPropertyFacet(clause, options = {}) {
async createPropertyFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createPropertyFacet', clause, options);
}
/**
* Create a material facet
*/
async createMaterialFacet(clause, options = {}) {
async createMaterialFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createMaterialFacet', clause, options);
}
/**
* Create a classification facet
*/
async createClassificationFacet(clause, options = {}) {
async createClassificationFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createClassificationFacet', clause, options);
}
/**
* Create a part-of facet
*/
async createPartOfFacet(clause, options = {}) {
async createPartOfFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createPartOfFacet', clause, options);
}
/**
* Validate an IDS object
*/
async validateIDS(idsObj) {
async validateIDS(idsObj: Record<string, unknown>) {
return await this._apiCall('validateIDS', idsObj);
}
/**
* Export IDS instance to XML string
*/
async exportIDS(idsObj) {
async exportIDS(idsObj: Record<string, unknown>) {
return this._apiCall('exportIDS', idsObj);
}
@@ -319,4 +330,4 @@ export const {
dispose
} = wasm;
export default wasm;
export default wasm;
@@ -1,12 +1,13 @@
import config from '../../../config.json';
import hyperid from 'hyperid';
import config from '../../../config.json';
import type { AuditReportData } from "$src/types/report";
let pyodide = null;
let id = hyperid();
let pyodide: any = null;
const id = hyperid();
let LoadedIFC = new Map();
const LoadedIFC = new Map<string, unknown>();
export async function init(pdide) {
export async function init(pdide: any) {
pyodide = pdide;
// Load Python API bindings
@@ -18,7 +19,7 @@ export async function init(pdide) {
`);
}
export async function getPredefinedTypes(schema, entity) {
export async function getPredefinedTypes(schema: string, entity: string) {
const result = await pyodide.runPythonAsync(`
from api import get_predefined_types_for_entity
predef_types = get_predefined_types_for_entity("${schema}", "${entity}")
@@ -27,7 +28,7 @@ export async function getPredefinedTypes(schema, entity) {
return result.toJs({ dict_converter: Object.fromEntries });
}
export async function getAllEntityClasses(schema) {
export async function getAllEntityClasses(schema: string) {
const result = await pyodide.runPythonAsync(`
from api import get_all_entity_classes
entities = get_all_entity_classes("${schema}")
@@ -36,7 +37,7 @@ export async function getAllEntityClasses(schema) {
return result.toJs({ dict_converter: Object.fromEntries });
}
export async function getAllDataTypes(schema) {
export async function getAllDataTypes(schema: string) {
const result = await pyodide.runPythonAsync(`
from api import get_all_data_types
data_types = get_all_data_types("${schema}")
@@ -45,7 +46,7 @@ export async function getAllDataTypes(schema) {
return result.toJs({ dict_converter: Object.fromEntries });
}
export async function getEntityAttributes(schema, entity) {
export async function getEntityAttributes(schema: string, entity: string) {
const result = await pyodide.runPythonAsync(`
from api import get_entity_attributes
attrs = get_entity_attributes("${schema}", "${entity}")
@@ -54,7 +55,7 @@ export async function getEntityAttributes(schema, entity) {
return result.toJs({ dict_converter: Object.fromEntries });
}
export async function getApplicablePsets(schema, entity, predefinedType = '') {
export async function getApplicablePsets(schema: string, entity: string, predefinedType = '') {
const result = await pyodide.runPythonAsync(`
from api import get_applicable_psets
psets = get_applicable_psets("${schema}", "${entity}", "${predefinedType}")
@@ -81,7 +82,7 @@ export async function getStandardClassificationSystems() {
return result.toJs({ dict_converter: Object.fromEntries });
}
export async function loadIfc(ifcData) {
export async function loadIfc(ifcData: number[] | Uint8Array | ArrayBuffer) {
const ifc_id = id();
const path = `/tmp/${encodeURIComponent(ifc_id)}.ifc`;
@@ -97,14 +98,14 @@ export async function loadIfc(ifcData) {
return ifc_id;
}
export async function unloadIfc(ifcId) {
export async function unloadIfc(ifcId: string) {
const path = `/tmp/${encodeURIComponent(ifcId)}.ifc`;
pyodide.FS.unlink(path);
LoadedIFC.delete(ifcId);
}
export async function auditIfc(ifcId, idsData) {
export async function auditIfc(ifcId: string, idsData: number[] | Uint8Array | ArrayBuffer) {
const reporter = pyodide.pyimport("ifctester.reporter");
const api = pyodide.pyimport("api");
@@ -116,16 +117,16 @@ export async function auditIfc(ifcId, idsData) {
specs.validate(ifc);
// Create report in both HTML and JSON formats
let jsonReporter = reporter.Json(specs);
const jsonReporter = reporter.Json(specs);
jsonReporter.report();
const jsonReport = jsonReporter.to_string();
let htmlReporter = reporter.Html(specs);
const htmlReporter = reporter.Html(specs);
htmlReporter.report();
const htmlReport = htmlReporter.to_string();
return {
json: JSON.parse(jsonReport),
json: JSON.parse(jsonReport) as AuditReportData,
html: htmlReport
};
}
@@ -142,4 +143,4 @@ export const API = {
"loadIfc": loadIfc,
"unloadIfc": unloadIfc,
"auditIfc": auditIfc
};
};
@@ -2,13 +2,19 @@
* IDS module
*/
let pyodide = null;
let pyodide: any = null;
// IDS Python classes
let Ids, Specification;
let Entity, Attribute, Property, Material, Classification, PartOf;
let Ids: any;
let Specification: any;
let Entity: any;
let Attribute: any;
let Property: any;
let Material: any;
let Classification: any;
let PartOf: any;
export async function init(pdide) {
export async function init(pdide: any) {
pyodide = pdide;
await pyodide.loadPackagesFromImports(`
@@ -29,24 +35,24 @@ export async function init(pdide) {
PartOf = pyodide.pyimport("ifctester.facet").PartOf;
}
function _idsToInstance(idsObj) {
function _idsToInstance(idsObj: Record<string, unknown>) {
const ids_raw = Ids();
return ids_raw.parse(pyodide.toPy(idsObj))
}
export function createIDS() {
export function createIDS(): Record<string, unknown> {
const ids_raw = Ids()
return ids_raw.asdict().toJs({dict_converter: Object.fromEntries});
}
export function openIDS(ids_xml, validate = false) {
export function openIDS(ids_xml: string, validate = false): Record<string, unknown> {
const ids_from_xml_string = pyodide.pyimport("api").ids_from_xml_string;
const ids_raw = ids_from_xml_string(ids_xml, validate);
return ids_raw.asdict().toJs({dict_converter: Object.fromEntries});
}
export function validateIDS(idsObj) {
export function validateIDS(idsObj: Record<string, unknown>): boolean {
const ids_raw = _idsToInstance(idsObj)
const tempFilename = `temp_${Date.now()}.xml`;
const isValid = ids_raw.to_xml(tempFilename); // to_xml validates the XML as well, as far as I understand
@@ -60,12 +66,26 @@ export function validateIDS(idsObj) {
return isValid;
}
export function exportIDS(idsObj) {
export function exportIDS(idsObj: Record<string, unknown>): string {
const ids_raw = _idsToInstance(idsObj)
return ids_raw.to_string();
}
export function createSpecification({name = "Unnamed", ifcVersion = ["IFC2X3", "IFC4"], identifier = null, description = null, instructions = null, usage = "required"}) {
export function createSpecification({
name = "Unnamed",
ifcVersion = ["IFC2X3", "IFC4"],
identifier = null,
description = null,
instructions = null,
usage = "required"
}: {
name?: string;
ifcVersion?: string[];
identifier?: string | null;
description?: string | null;
instructions?: string | null;
usage?: string;
} = {}): Record<string, unknown> {
const spec = Specification.callKwargs({
name: name,
ifcVersion: ifcVersion,
@@ -79,7 +99,14 @@ export function createSpecification({name = "Unnamed", ifcVersion = ["IFC2X3", "
}
// @instructions
export function createEntityFacet(clause, {name = "IFCWALL", predefinedType = null, instructions = null}) {
export function createEntityFacet(
clause: string,
{name = "IFCWALL", predefinedType = null, instructions = null}: {
name?: string;
predefinedType?: string | null;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const entity = Entity.callKwargs({
name: name,
predefinedType: predefinedType,
@@ -89,7 +116,15 @@ export function createEntityFacet(clause, {name = "IFCWALL", predefinedType = nu
}
// @cardinality, @instructions
export function createAttributeFacet(clause, {name = "Name", value = null, cardinality = "required", instructions = null}) {
export function createAttributeFacet(
clause: string,
{name = "Name", value = null, cardinality = "required", instructions = null}: {
name?: string;
value?: string | null;
cardinality?: string;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const attribute = Attribute.callKwargs({
name: name,
value: value,
@@ -100,7 +135,16 @@ export function createAttributeFacet(clause, {name = "Name", value = null, cardi
}
// @uri, @cardinality, @instructions
export function createClassificationFacet(clause, {value = null, system = null, uri = null, cardinality = "required", instructions = null}) {
export function createClassificationFacet(
clause: string,
{value = null, system = null, uri = null, cardinality = "required", instructions = null}: {
value?: string | null;
system?: string | null;
uri?: string | null;
cardinality?: string;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const classification = Classification.callKwargs({
value: value,
system: system,
@@ -112,7 +156,16 @@ export function createClassificationFacet(clause, {value = null, system = null,
}
// @relation, @cardinality, @instructions
export function createPartOfFacet(clause, {name = "IFCWALL", predefinedType = null, relation = null, cardinality = "required", instructions = null}) {
export function createPartOfFacet(
clause: string,
{name = "IFCWALL", predefinedType = null, relation = null, cardinality = "required", instructions = null}: {
name?: string;
predefinedType?: string | null;
relation?: string | null;
cardinality?: string;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const part_of = PartOf.callKwargs({
name: name,
predefinedType: predefinedType,
@@ -124,7 +177,26 @@ export function createPartOfFacet(clause, {name = "IFCWALL", predefinedType = nu
}
// @dataType, @uri, @cardinality, @instructions
export function createPropertyFacet(clause, {propertySet = "Property_Set", baseName = "propertyName", value = null, dataType = null, uri = null, cardinality = "required", instructions = null}) {
export function createPropertyFacet(
clause: string,
{
propertySet = "Property_Set",
baseName = "propertyName",
value = null,
dataType = null,
uri = null,
cardinality = "required",
instructions = null
}: {
propertySet?: string;
baseName?: string;
value?: string | null;
dataType?: string | null;
uri?: string | null;
cardinality?: string;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const property = Property.callKwargs({
propertySet: propertySet,
baseName: baseName,
@@ -138,7 +210,15 @@ export function createPropertyFacet(clause, {propertySet = "Property_Set", baseN
}
// @uri, @cardinality, @instructions
export function createMaterialFacet(clause, {value = null, uri = null, cardinality = "required", instructions = null}) {
export function createMaterialFacet(
clause: string,
{value = null, uri = null, cardinality = "required", instructions = null}: {
value?: string | null;
uri?: string | null;
cardinality?: string;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const material = Material.callKwargs({
value: value,
uri: uri,
@@ -149,7 +229,7 @@ export function createMaterialFacet(clause, {value = null, uri = null, cardinali
}
// Helper function to convert date to ISO format string
export function formatDate(date) {
export function formatDate(date?: string | number | Date | null): string | null {
if (!date) return null;
const d = new Date(date);
return d.toISOString().split('T')[0];
@@ -168,4 +248,4 @@ export const API = {
"createPartOfFacet": createPartOfFacet,
"createPropertyFacet": createPropertyFacet,
"createMaterialFacet": createMaterialFacet,
};
};
@@ -4,13 +4,14 @@
import { MessageType } from '../index';
import config from '../../../config.json';
import * as IDS from './ids.js';
import * as IDS from './ids';
import * as API from './api';
import type { ApiCallPayload, WorkerRequest } from "$src/types/wasm";
let pyodide = null;
let pyodide: any = null;
let ready = false;
self.addEventListener('message', async (event) => {
self.addEventListener('message', async (event: MessageEvent<WorkerRequest>) => {
console.log("[worker] Received message:", event.data);
const { type, payload, id } = event.data;
@@ -25,27 +26,33 @@ self.addEventListener('message', async (event) => {
});
break;
case MessageType.API_CALL:
case MessageType.API_CALL: {
if (!ready) {
throw new Error('[worker] Pyodide not initialized');
}
const result = await handleApiCall(payload);
if (!payload) {
throw new Error('[worker] Missing payload for API call');
}
const result = await handleApiCall(payload as ApiCallPayload);
self.postMessage({
type: MessageType.API_RESPONSE,
payload: result,
id
});
break;
}
default:
throw new Error(`[worker] Unknown message type: ${type}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
self.postMessage({
type: MessageType.ERROR,
payload: {
message: error.message,
stack: error.stack
message,
stack
},
id
});
@@ -76,7 +83,13 @@ async function initEnvironment() {
await pyodide.loadPackage("shapely");
// Install IfcTester
await micropip.install('ifctester');
const ifctesterManifest = await fetch('/worker/generated/ifctester.json').then((response) => {
if (!response.ok) {
throw new Error(`[worker] Failed to load IfcTester wheel manifest: ${response.status} ${response.statusText}`);
}
return response.json() as Promise<{ wheel_url: string }>;
});
await micropip.install(ifctesterManifest.wheel_url);
// Initialize IDS and API
await API.init(pyodide);
@@ -93,17 +106,17 @@ async function cleanupEnvironment() {
console.log("[worker] Closed environment");
}
async function handleApiCall({ method, args = [] }) {
async function handleApiCall({ method, args = [] }: ApiCallPayload) {
if (method === 'internal.cleanup') {
await cleanupEnvironment();
return true;
}
if (method in API.API) {
return await API.API[method](...args);
} else if (method in IDS.API) {
return await IDS.API[method](...args);
} else {
throw new Error(`[worker] Unknown API method: ${method}`);
return await (API.API as Record<string, (...params: unknown[]) => unknown>)[method](...args);
}
}
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>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import FacetEditor from './FacetEditor.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 documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null);
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null);
let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : 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) {
if (!activeSpecification) return;
async function addFacet(facetType: FacetType) {
if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.createFacet(
IDS.Module.activeDocument,
documentState.activeSpecification,
documentState.activeSpecification ?? 0,
"applicability",
facetType
);
}
async function removeFacet(facetType, facetIndex) {
if (!activeSpecification) return;
async function removeFacet(facetType: FacetType, facetIndex: number) {
if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.deleteFacet(
IDS.Module.activeDocument,
documentState.activeSpecification,
documentState.activeSpecification ?? 0,
"applicability",
facetType,
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>
<div class="restrictions-panel">
@@ -40,22 +59,19 @@
<CreateFacetDropdown {addFacet} />
</div>
<div class="restrictions-list">
{#if activeSpecification?.applicability}
{#each Object.entries(activeSpecification.applicability) as [facetType, facets]}
{#if facetType !== "@minOccurs" && facetType !== "@maxOccurs"}
{#each facets as facet, index}
<FacetEditor
bind:facet={facets[index]}
{facetType}
specification={activeSpecification}
activeTab="applicability"
{removeFacet}
{index}
key={`${activeDocument}-${documentState?.activeSpecification}-applicability-${facetType}-${index}`}
/>
{/each}
{/if}
{#if activeSpecification && applicabilityEntries.length > 0}
{#each applicabilityEntries as [facetType, facets]}
{#each facets as facet, index}
<FacetEditor
bind:facet={facets[index]}
{facetType}
specification={activeSpecification}
activeTab="applicability"
{removeFacet}
{index}
/>
{/each}
{/each}
{/if}
</div>
</div>
</div>
@@ -1,20 +1,40 @@
<script>
<script lang="ts">
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"
* @ in name --> simple string value
* 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) => {
return facet[prop] ?? "";
let {
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) => {
facet[prop] = value;
const setSpecialProp = (prop: string, value: string) => {
(facet as Record<string, unknown>)[prop] = value;
};
let baseId = $derived(`facet-${facetType}-${index}`);
</script>
<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="predefinedType" label="Predefined Type" placeholder="e.g., SOLIDWALL" autocomplete="predefinedType" />
<div class="form-group">
<label>Relation</label>
<select class="form-input" bind:value={() => getSpecialProp("@relation"), (v) => setSpecialProp("@relation", v)}>
<label for={`${baseId}-relation`}>Relation</label>
<select class="form-input" id={`${baseId}-relation`} bind:value={() => getSpecialProp("@relation"), (v) => setSpecialProp("@relation", v)}>
<option value="">Select relation...</option>
<option value="IFCRELAGGREGATES">IFCRELAGGREGATES</option>
<option value="IFCRELASSIGNSTOGROUP">IFCRELASSIGNSTOGROUP</option>
@@ -62,8 +82,8 @@
{#if activeTab === 'requirements'}
{#if facetType !== 'entity'}
<div class="form-group">
<label>Cardinality</label>
<select class="form-input" bind:value={() => getSpecialProp("@cardinality"), (v) => setSpecialProp("@cardinality", v)}>
<label for={`${baseId}-cardinality`}>Cardinality</label>
<select class="form-input" id={`${baseId}-cardinality`} bind:value={() => getSpecialProp("@cardinality"), (v) => setSpecialProp("@cardinality", v)}>
<option value="required">Required</option>
<option value="optional">Optional</option>
<option value="prohibited">Prohibited</option>
@@ -71,9 +91,9 @@
</div>
{/if}
<div class="form-group full-width">
<label>Instructions</label>
<textarea class="form-input" bind:value={() => getSpecialProp("@instructions"), (v) => setSpecialProp("@instructions", v)} placeholder="Optional instructions for IFC authors" rows="2"></textarea>
<label for={`${baseId}-instructions`}>Instructions</label>
<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>
{/if}
</div>
</div>
</div>
@@ -1,13 +1,17 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
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] ?? "";
};
const setProp = (prop, value) => {
const setProp = (prop: keyof IdsInfo, value: string) => {
if (!activeDocument) return;
activeDocument.info[prop] = value;
};
</script>
@@ -18,36 +22,36 @@
</div>
<div class="form-grid">
<div class="form-group">
<label>Title</label>
<input class="form-input" type="text" bind:value={() => getProp("title"), (v) => setProp("title", v)} placeholder="Enter IDS title">
<label for="ids-title">Title</label>
<input class="form-input" id="ids-title" type="text" bind:value={() => getProp("title"), (v) => setProp("title", v)} placeholder="Enter IDS title">
</div>
<div class="form-group">
<label>Author Email</label>
<input class="form-input" type="email" bind:value={() => getProp("author"), (v) => setProp("author", v)} placeholder="Enter author">
<label for="ids-author">Author Email</label>
<input class="form-input" id="ids-author" type="email" bind:value={() => getProp("author"), (v) => setProp("author", v)} placeholder="Enter author">
</div>
<div class="form-group">
<label>Version</label>
<input class="form-input" type="text" bind:value={() => getProp("version"), (v) => setProp("version", v)} placeholder="Enter version">
<label for="ids-version">Version</label>
<input class="form-input" id="ids-version" type="text" bind:value={() => getProp("version"), (v) => setProp("version", v)} placeholder="Enter version">
</div>
<div class="form-group">
<label>Date</label>
<input class="form-input" type="date" bind:value={() => getProp("date"), (v) => setProp("date", v)}>
<label for="ids-date">Date</label>
<input class="form-input" id="ids-date" type="date" bind:value={() => getProp("date"), (v) => setProp("date", v)}>
</div>
<div class="form-group full-width">
<label>Description</label>
<textarea class="form-input" bind:value={() => getProp("description"), (v) => setProp("description", v)} placeholder="Enter description" rows="3"></textarea>
<label for="ids-description">Description</label>
<textarea class="form-input" id="ids-description" bind:value={() => getProp("description"), (v) => setProp("description", v)} placeholder="Enter description" rows="3"></textarea>
</div>
<div class="form-group">
<label>Purpose</label>
<input class="form-input" type="text" bind:value={() => getProp("purpose"), (v) => setProp("purpose", v)} placeholder="Enter purpose">
<label for="ids-purpose">Purpose</label>
<input class="form-input" id="ids-purpose" type="text" bind:value={() => getProp("purpose"), (v) => setProp("purpose", v)} placeholder="Enter purpose">
</div>
<div class="form-group">
<label>Milestone</label>
<input class="form-input" type="text" bind:value={() => getProp("milestone"), (v) => setProp("milestone", v)} placeholder="Enter milestone">
<label for="ids-milestone">Milestone</label>
<input class="form-input" id="ids-milestone" type="text" bind:value={() => getProp("milestone"), (v) => setProp("milestone", v)} placeholder="Enter milestone">
</div>
<div class="form-group full-width">
<label>Copyright</label>
<input class="form-input" type="text" bind:value={() => getProp("copyright"), (v) => setProp("copyright", v)} placeholder="Enter copyright">
<label for="ids-copyright">Copyright</label>
<input class="form-input" id="ids-copyright" type="text" bind:value={() => getProp("copyright"), (v) => setProp("copyright", v)} placeholder="Enter copyright">
</div>
</div>
</div>
@@ -1,18 +1,28 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
import { getAuditReportById, downloadAuditReport } from "$src/modules/api/api.svelte.js";
import { error, success } from "$src/modules/utils/toast.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import { getAuditReportById, downloadAuditReport } from "$src/modules/api/api.svelte";
import { error, success } from "$src/modules/utils/toast.svelte";
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 documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null);
let auditReport = $derived(documentState?.auditReport ? getAuditReportById(documentState.auditReport) : null);
let expandedSpecs = $state(new Set());
let expandedRequirements = $state(new Set());
let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
);
let documentState = $derived(
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);
type SpecificationStatus = boolean | 'skipped' | null;
// Open Editor mode and jump to a specific specification
function editSpecification(index) {
function editSpecification(index: number) {
if (IDS.Module.activeDocument) {
IDS.setDocumentState(IDS.Module.activeDocument, {
viewMode: 'editor',
@@ -23,13 +33,13 @@
}
}
function toggleSpecification(index) {
function toggleSpecification(index: number) {
if (expandedSpecs.has(index)) {
expandedSpecs.delete(index);
} else {
expandedSpecs.add(index);
}
expandedSpecs = new Set(expandedSpecs);
expandedSpecs = new Set<number>(expandedSpecs);
}
function toggleAllSpecifications() {
@@ -37,7 +47,7 @@
if (allExpanded) {
// Collapse all
expandedSpecs = new Set();
expandedSpecs = new Set<number>();
allExpanded = false;
} else {
// Expand all
@@ -47,43 +57,39 @@
}
}
function getSpecificationStatus(specIndex, auditData) {
function getSpecificationStatus(specIndex: number, auditData: AuditReportData): SpecificationStatus {
const spec = auditData.specifications[specIndex];
if (!spec) return null;
// If no applicable elements and no checks, and it passed, it's actually skipped
if (spec.total_applicable === 0 && spec.total_checks === 0 && spec.status === true) {
return 'skipped';
}
return spec.status;
return spec.is_skipped ? 'skipped' : spec.status;
}
function getSpecificationStats(specIndex, auditData) {
function getSpecificationStats(specIndex: number, auditData: AuditReportData) {
const spec = auditData.specifications[specIndex];
if (!spec) return null;
return {
requirements: `${spec.total_requirements || 0}`,
requirementsPassed: `${spec.total_requirements_pass || 0}`,
checksTotal: `${spec.total_checks || 0}`,
checksPassed: `${spec.total_checks_pass || 0}`,
applicableTotal: `${spec.total_applicable || 0}`,
applicablePassed: `${spec.total_applicable_pass || 0}`
requirements: spec.total_requirements || 0,
requirementsPassed: spec.total_requirements_pass || 0,
checksTotal: spec.total_checks || 0,
checksPassed: spec.total_checks_pass || 0,
applicableTotal: spec.total_applicable || 0,
applicablePassed: spec.total_applicable_pass || 0
};
}
function getSpecificationReason(specIndex, auditData) {
function getSpecificationReason(specIndex: number, auditData: AuditReportData) {
const spec = auditData.specifications[specIndex];
if (!spec) return null;
const status = getSpecificationStatus(specIndex, auditData);
if (status === 'skipped') {
return "Skipped because no applicable entities were found and the cardinality is OPTIONAL or PROHIBITED";
return "Skipped because no applicable entities were found and the cardinality is OPTIONAL";
}
if (status === false) { // Failed
if (spec.total_applicable === 0) {
if (spec.cardinality === 'prohibited') {
return `Failed because ${spec.total_applicable} prohibited entities were found`;
} else if (spec.total_applicable === 0) {
return "Failed because no applicable entities were found but the cardinality is REQUIRED";
} else {
const failedChecks = spec.total_checks - spec.total_checks_pass;
@@ -94,6 +100,10 @@
return null; // No reason needed for passed specifications
}
function getDocumentSpecificationUsage(spec: Specification) {
return IDS.getSpecUsage(spec);
}
async function handleDownloadReport() {
if (!auditReport) return;
@@ -101,29 +111,62 @@
await downloadAuditReport(auditReport.id);
success('Audit report downloaded successfully');
} 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];
if (!spec || !spec.requirements || !spec.requirements[reqIndex]) return null;
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}`;
if (expandedRequirements.has(key)) {
expandedRequirements.delete(key);
} else {
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}`);
}
const handleActivation = (event: KeyboardEvent, action: () => void) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
action();
}
};
</script>
<div class="ids-viewer">
@@ -211,8 +254,17 @@
</button>
</div>
{#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="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-row">
<h2>{spec["@name"] || `Specification ${index + 1}`}</h2>
@@ -240,19 +292,30 @@
{#if "@description" in spec}
<p class="spec-description">{spec["@description"]}</p>
{/if}
<div class="spec-stats">
{#if usage === 'required'}
<span class="stat-item">Required</span>
{/if}
{#if usage === 'optional'}
<span class="stat-item">Optional</span>
{/if}
{#if usage === 'prohibited'}
<span class="stat-item">Prohibited</span>
{/if}
{#if auditReport}
{@const stats = getSpecificationStats(index, auditReport.data)}
{@const status = getSpecificationStatus(index, auditReport.data)}
{#if stats && usage !== 'prohibited' && status !== 'skipped'}
<span class="stat-item">Checks: {stats.checksPassed}/{stats.checksTotal}</span>
<span class="stat-item">Requirements: {stats.requirementsPassed}/{stats.requirements}</span>
{/if}
{/if}
</div>
{#if auditReport}
{@const reason = getSpecificationReason(index, auditReport.data)}
{#if reason}
<p class="spec-reason">{reason}</p>
{/if}
{@const stats = getSpecificationStats(index, auditReport.data)}
{@const status = getSpecificationStatus(index, auditReport.data)}
{#if stats && status !== 'skipped'}
<div class="spec-stats">
<span class="stat-item">Checks: {stats.checksPassed}/{stats.checksTotal}</span>
<span class="stat-item">Requirements: {stats.requirementsPassed}/{stats.requirements}</span>
</div>
{/if}
{/if}
</div>
<div class="spec-actions">
@@ -292,39 +355,135 @@
{/if}
{/each}
</div>
{#if auditReport}
{@const status = getSpecificationStatus(index, auditReport.data)}
{#if status === false && usage === 'prohibited'}
{@const specReport = auditReport.data.specifications[index]}
{@const applicableEntities = specReport.applicable_entities ?? []}
<div class="entity-tables">
{#if applicableEntities.length > 0}
<div class="entity-table-section fail">
<h4>Failed Elements ({applicableEntities.length})</h4>
<div class="entity-table-container">
<Tooltip.Provider>
<table class="entity-table">
<thead>
<tr>
<th>Class</th>
<th>PredefinedType</th>
<th>Name</th>
<th>Description</th>
<th>Warning</th>
<th>GlobalId</th>
<th>Tag</th>
</tr>
</thead>
<tbody>
{#each applicableEntities.slice(0, 10) as entity}
<tr>
<td>{entity.class}</td>
<td>{entity.predefined_type || '-'}</td>
<td>
<Tooltip.Root>
<Tooltip.Trigger>
<div class="truncated-text">{entity.name || '-'}</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{entity.name || '-'}</p>
</Tooltip.Content>
</Tooltip.Root>
</td>
<td>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<div class="truncated-text">{entity.description || '-'}</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{entity.description || '-'}</p>
</Tooltip.Content>
</Tooltip.Root>
</td>
<td>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<div class="truncated-text">{entity.reason || '-'}</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{entity.reason || '-'}</p>
</Tooltip.Content>
</Tooltip.Root>
</td>
<td>
<Tooltip.Root>
<Tooltip.Trigger>
<div class="truncated-text">{entity.global_id || '-'}</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{entity.global_id || '-'}</p>
</Tooltip.Content>
</Tooltip.Root>
</td>
<td>
<Tooltip.Root>
<Tooltip.Trigger>
<div class="truncated-text">{entity.tag || '-'}</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{entity.tag || '-'}</p>
</Tooltip.Content>
</Tooltip.Root>
</td>
</tr>
{/each}
{#if applicableEntities.length > 10}
<tr class="more-row">
<td colspan="7">... {applicableEntities.length - 10} more failing elements not shown ...</td>
</tr>
{/if}
</tbody>
</table>
</Tooltip.Provider>
</div>
</div>
{/if}
</div>
{/if}
{/if}
</div>
<!-- Requirements Section -->
{#if requirementGroups.length > 0}
<div class="facet-section">
<h3>Requirements</h3>
<div class="facets-list">
{#each Object.entries(spec.requirements || {}) as [facetType, facets]}
{#if Array.isArray(facets) && facets.length > 0}
<div class="facet-group">
{#each facets as facet, facetIndex}
{@const reqAuditData = auditReport ? getRequirementStatus(index, facetIndex, 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') : ''}">
<button class="facet-header" onclick={() => {if (auditReport && reqAuditData && specStatus !== 'skipped') toggleRequirementDetails(index, facetIndex)}}>
<span class="facet-bullet"></span>
<span class="facet-text">{@html IDS.stringifyFacet("requirements", facet, facetType, spec)}</span>
{#if auditReport && reqAuditData && specStatus !== 'skipped'}
{#if reqAuditData.total_applicable > 0}
<div class="audit-details-toggle">
{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, facetIndex)}>
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</div>
{:else}
<span class="audit-status-badge">
{reqAuditData.status ? 'PASS' : 'FAIL'}
</span>
{/if}
{#each requirementGroups as group}
<div class="facet-group">
{#each group.items as item}
{@const reqAuditData = auditReport ? getRequirementStatus(index, item.reqIndex, 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') : ''}">
<button class="facet-header" onclick={() => {if (auditReport && reqAuditData && specStatus !== 'skipped') toggleRequirementDetails(index, item.reqIndex)}}>
<span class="facet-bullet"></span>
<span class="facet-text">{@html IDS.stringifyFacet("requirements", item.facet, group.facetType, spec)}</span>
{#if auditReport && reqAuditData && specStatus !== 'skipped'}
{#if reqAuditData.total_applicable > 0}
<div class="audit-details-toggle">
{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)}>
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</div>
{:else}
<span class="audit-status-badge">
{reqAuditData.status ? 'PASS' : 'FAIL'}
</span>
{/if}
</button>
{#if isRequirementDetailsExpanded(index, facetIndex)}
{/if}
</button>
{#if reqAuditData && isRequirementDetailsExpanded(index, item.reqIndex)}
<div class="facet-expansion">
<div class="entity-tables">
{#if reqAuditData.passed_entities && reqAuditData.passed_entities.length > 0}
@@ -489,14 +648,14 @@
{/if}
</div>
</div>
{/if}
</div>
{/each}
</div>
{/if}
{/if}
</div>
{/each}
</div>
{/each}
</div>
</div>
{/if}
</div>
{/if}
</div>
@@ -1128,4 +1287,4 @@
white-space: nowrap;
cursor: pointer;
}
</style>
</style>
@@ -1,37 +1,53 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import FacetEditor from './FacetEditor.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 documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null);
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null);
let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : 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) {
if (!activeSpecification) return;
async function addFacet(facetType: FacetType) {
if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.createFacet(
IDS.Module.activeDocument,
documentState.activeSpecification,
documentState.activeSpecification ?? 0,
"requirements",
facetType
);
}
async function removeFacet(facetType, facetIndex) {
if (!activeSpecification) return;
async function removeFacet(facetType: FacetType, facetIndex: number) {
if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.deleteFacet(
IDS.Module.activeDocument,
documentState.activeSpecification,
documentState.activeSpecification ?? 0,
"requirements",
facetType,
facetIndex
);
}
let requirementEntries = $derived(
activeSpecification?.requirements
? (Object.entries(activeSpecification.requirements).filter(([, facets]) =>
Array.isArray(facets)
) as Array<[FacetType, Facet[]]>)
: []
);
</script>
<div class="restrictions-panel">
@@ -40,8 +56,8 @@
<CreateFacetDropdown {addFacet} />
</div>
<div class="restrictions-list">
{#if activeSpecification?.requirements}
{#each Object.entries(activeSpecification.requirements) as [facetType, facets]}
{#if activeSpecification && requirementEntries.length > 0}
{#each requirementEntries as [facetType, facets]}
{#each facets as facet, index}
<FacetEditor
bind:facet={facets[index]}
@@ -50,10 +66,9 @@
activeTab="requirements"
{removeFacet}
{index}
key={`${activeDocument}-${documentState?.activeSpecification}-requirements-${facetType}-${index}`}
/>
{/each}
{/each}
{/if}
</div>
</div>
</div>
@@ -1,34 +1,86 @@
<script>
<script lang="ts">
import Svelecte from 'svelecte';
import { getEntityClasses, getMaterialCategories, getClassificationSystems, getDataTypes, getPredefinedTypes, getEntityAttributes, getApplicablePsets } from '$src/modules/api/api.svelte.js';
import * as IDS from '$src/modules/api/ids.svelte.js';
import {
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();
const isEntityNameField = autocomplete === 'entityName';
const isMaterialField = autocomplete === 'material';
const isClassificationSystemField = autocomplete === 'classificationSystem';
const isPredefinedTypeField = autocomplete === 'predefinedType';
const isAttributeNameField = autocomplete === 'attributeName';
const isPropertySetField = autocomplete === 'propertySet';
const isDataTypeField = autocomplete === 'dataType';
type AutocompleteType =
| 'entityName'
| 'material'
| 'classificationSystem'
| 'predefinedType'
| 'attributeName'
| 'propertySet'
| '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
let predefinedTypeOptions = $state([]);
let predefinedTypeOptions: string[] = $state([]);
// Attribute Names autocompletions
let attributeNameOptions = $state([]);
let attributeNameOptions: string[] = $state([]);
// Property Sets autocompletions
let propertySetOptions = $state([]);
let propertySetOptions: string[] = $state([]);
// Get the active specification's IFC schemas
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'];
// TODO: Fix: Filter out IFC4X3 because it's buggy
return versions.filter(version => version !== 'IFC4X3_ADD2');
@@ -36,7 +88,7 @@
// Get the entity name from facet
const getEntityName = () => {
const nameField = facet?.name;
const nameField = (facet as Record<string, unknown>)?.name as FacetValue | undefined;
if (!nameField) return '';
if (nameField.simpleValue) return nameField.simpleValue;
return '';
@@ -44,18 +96,13 @@
// Get all entity names from Entity facets in Applicability
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 [];
const entityFacets = activeSpecification.applicability.entity;
const entityNames = [];
const entityFacets = activeSpecification.applicability.entity as Facet[];
const entityNames: string[] = [];
entityFacets.forEach(entityFacet => {
const nameField = entityFacet.name;
const nameField = (entityFacet as Record<string, unknown>).name as FacetValue | undefined;
if (!nameField) return;
if (nameField.simpleValue) {
@@ -74,19 +121,14 @@
// Get entity facets with their predefined types
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 [];
return activeSpecification.applicability.entity.map(entityFacet => {
const nameField = entityFacet.name;
const predefinedTypeField = entityFacet.predefinedType;
return (activeSpecification.applicability.entity as Facet[]).map(entityFacet => {
const nameField = (entityFacet as Record<string, unknown>).name as FacetValue | undefined;
const predefinedTypeField = (entityFacet as Record<string, unknown>).predefinedType as FacetValue | undefined;
// Extract entity name
let entityNames = [];
let entityNames: string[] = [];
if (nameField?.simpleValue) {
entityNames = [nameField.simpleValue];
} else if (nameField?.restriction?.enumeration) {
@@ -99,9 +141,12 @@
let predefinedType = '';
if (predefinedTypeField?.simpleValue) {
predefinedType = predefinedTypeField.simpleValue;
} else if (predefinedTypeField?.restriction?.enumeration?.length > 0) {
// For enumeration predefined types, use the first one or empty string
predefinedType = predefinedTypeField.restriction.enumeration[0]['@value'] || '';
} else {
const enumValues = predefinedTypeField?.restriction?.enumeration;
if (enumValues && enumValues.length > 0) {
// For enumeration predefined types, use the first one or empty string
predefinedType = enumValues[0]['@value'] || '';
}
}
return { entityNames, predefinedType };
@@ -109,7 +154,8 @@
};
// Contextual autocompletions
$effect(async () => {
$effect(() => {
void (async () => {
// Predefined Types
if (isPredefinedTypeField) {
@@ -122,10 +168,10 @@
const typePromises = ifcVersions.map(schema =>
getPredefinedTypes(schema, entityName.toUpperCase())
);
const typeSets = await Promise.all(typePromises);
const typeSets = await Promise.all(typePromises) as string[][];
// Deduplicate predefined types across all schemas
const allTypes = new Set();
const allTypes = new Set<string>();
typeSets.forEach(types => {
if (types && Array.isArray(types)) {
types.forEach(type => allTypes.add(type));
@@ -150,19 +196,19 @@
if (entityNames.length > 0 && ifcVersions.length > 0) {
try {
// Fetch attributes for all entity names across all selected schemas
const attributePromises = [];
const attributePromises: Array<Promise<{ name: string }[]>> = [];
entityNames.forEach(entityName => {
ifcVersions.forEach(schema => {
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
const allAttributes = new Set();
const allAttributes = new Set<string>();
attributeSets.forEach(attributes => {
if (attributes && Array.isArray(attributes)) {
attributes.forEach(attr => {
@@ -191,21 +237,21 @@
if (entityFacets.length > 0 && ifcVersions.length > 0) {
try {
// Fetch applicable property sets for all entity facets across all selected schemas
const psetPromises = [];
const psetPromises: Array<Promise<string[]>> = [];
entityFacets.forEach(facet => {
facet.entityNames.forEach(entityName => {
ifcVersions.forEach(schema => {
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
const allPsets = new Set();
const allPsets = new Set<string>();
psetSets.forEach(psets => {
if (psets && Array.isArray(psets)) {
psets.forEach(pset => allPsets.add(pset));
@@ -221,10 +267,11 @@
propertySetOptions = [];
}
}
})();
});
// Get autocomplete options based on field type
const getAutocompleteOptions = () => {
const getAutocompleteOptions = (): string[] => {
if (isEntityNameField) return getEntityClasses();
if (isMaterialField) return getMaterialCategories();
if (isClassificationSystemField) return Object.keys(getClassificationSystems());
@@ -236,11 +283,11 @@
};
const getRestrictionType = () => {
const fieldValue = facet[fieldName];
const fieldValue = getFacetValue();
if (!fieldValue) return 'Simple';
if (fieldValue.simpleValue !== undefined) return 'Simple';
if (fieldValue['restriction']) {
const restriction = fieldValue['restriction'];
const restriction = fieldValue['restriction'] as Restriction;
if (restriction['enumeration']) return 'Enumeration';
if (restriction['pattern']) return 'Pattern';
if (restriction['minInclusive'] || restriction['maxInclusive'] ||
@@ -251,39 +298,40 @@
return 'Simple';
};
const getSimpleValue = () => {
const fieldValue = facet[fieldName];
const getSimpleValue = (): string => {
const fieldValue = getFieldValue();
// 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;
return null;
return '';
};
const getEnumerationValues = () => {
const fieldValue = facet[fieldName];
const fieldValue = getFacetValue();
if (!fieldValue?.['restriction']) return [''];
const restriction = fieldValue['restriction'];
const enumValues = restriction['enumeration'];
const restriction = fieldValue['restriction'] as Restriction;
const enumValues = restriction['enumeration'] as RestrictionValue[] | undefined;
if (!enumValues) return [''];
return enumValues.map(item => item['@value'] || '');
};
const getPatternValue = () => {
const fieldValue = facet[fieldName];
const fieldValue = getFacetValue();
if (!fieldValue?.['restriction']) return '';
const restriction = fieldValue['restriction'];
const pattern = restriction['pattern'];
const restriction = fieldValue['restriction'] as Restriction;
const pattern = restriction['pattern'] as RestrictionValue[] | undefined;
if (!pattern || !pattern.length) return '';
return pattern[0]['@value'] || '';
};
const getRangeValues = () => {
const fieldValue = facet[fieldName];
const fieldValue = getFacetValue();
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';
@@ -307,18 +355,18 @@
};
const getLengthValue = () => {
const fieldValue = facet[fieldName];
const fieldValue = getFacetValue();
if (!fieldValue?.['restriction']) return '';
const restriction = fieldValue['restriction'];
const length = restriction['length'];
const restriction = fieldValue['restriction'] as Restriction;
const length = restriction['length'] as RestrictionValue[] | undefined;
if (!length || !length.length) return '';
return length[0]['@value'] || '';
};
const getLengthRangeValues = () => {
const fieldValue = facet[fieldName];
const fieldValue = getFacetValue();
if (!fieldValue?.['restriction']) return { min: '', max: '' };
const restriction = fieldValue['restriction'];
const restriction = fieldValue['restriction'] as Restriction;
let min = '', max = '';
@@ -332,77 +380,85 @@
return { min, max };
};
const setSimpleValue = (value) => {
const setSimpleValue = (value: string) => {
// For special properties (eg. @dataType), we set the value directly
if (isSpecialProp) {
facet[fieldName] = value;
setFieldValue(value);
return;
}
if (!facet[fieldName]) facet[fieldName] = {};
facet[fieldName] = { simpleValue: value };
setFieldValue({ simpleValue: value });
};
const setEnumerationValues = (values) => {
if (!facet[fieldName]) facet[fieldName] = {};
const setEnumerationValues = (values: string[]) => {
const enumItems = values.filter(v => v && typeof v === 'string' && v.trim() !== '').map(v => ({ '@value': v }));
facet[fieldName] = {
setFieldValue({
'restriction': {
'@base': 'xs:string',
'enumeration': enumItems
}
};
});
};
const setPatternValue = (value) => {
if (!facet[fieldName]) facet[fieldName] = {};
facet[fieldName] = {
const setPatternValue = (value: string) => {
setFieldValue({
'restriction': {
'@base': 'xs:string',
'pattern': [{ '@value': value }]
}
};
});
};
const setRangeValues = (min, max, minType, maxType) => {
if (!facet[fieldName]) facet[fieldName] = {};
const restriction = { '@base': 'xs:string' };
const setRangeValues = (min: string, max: string, minType: string, maxType: string) => {
const restriction: Restriction = { '@base': 'xs:string' };
if (min !== '') {
const minKey = minType === 'Inclusive' ? 'minInclusive' : 'minExclusive';
restriction[minKey] = [{ '@value': min }];
(restriction as Record<string, RestrictionValue[]>)[minKey] = [{ '@value': min }];
}
if (max !== '') {
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) => {
if (!facet[fieldName]) facet[fieldName] = {};
facet[fieldName] = {
const setLengthValue = (value: string) => {
setFieldValue({
'restriction': {
'@base': 'xs:string',
'length': [{ '@value': value }]
}
};
});
};
const setLengthRangeValues = (min, max) => {
if (!facet[fieldName]) facet[fieldName] = {};
const restriction = { '@base': 'xs:string' };
const setLengthRangeValues = (min: string, max: string) => {
const restriction: Restriction = { '@base': 'xs:string' };
if (min !== '') restriction['minLength'] = [{ '@value': min }];
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());
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;
switch (newType) {
@@ -435,23 +491,27 @@
enumerationValues = [...enumerationValues, ''];
};
const removeEnumerationValue = (index) => {
const removeEnumerationValue = (index: number) => {
enumerationValues = enumerationValues.filter((_, i) => i !== index);
setEnumerationValues(enumerationValues);
};
const updateEnumerationValue = (index, value) => {
const updateEnumerationValue = (index: number, value: string) => {
enumerationValues[index] = value;
setEnumerationValues(enumerationValues);
};
</script>
<div class="form-group">
<label>{label}</label>
<div class="restriction-controls">
<span class="form-label" id={`${baseId}-label`}>{label}</span>
<div class="restriction-controls" role="group" aria-labelledby={`${baseId}-label`}>
{#if !isSpecialProp}
<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="Enumeration">Enumeration</option>
<option value="Pattern">Pattern</option>
@@ -477,7 +537,7 @@
placeholder={placeholder}
/>
{: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}
{:else if restrictionType === 'Enumeration'}
@@ -497,7 +557,14 @@
placeholder={placeholder}
/>
{: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}
<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">
@@ -515,22 +582,43 @@
</div>
{: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'}
{@const range = getRangeValues()}
<div class="range-controls">
<div class="range-group">
<label>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">
<select class="form-input" bind:value={() => getRangeValues().minType, (v) => { const range = getRangeValues(); setRangeValues(range.min, range.max, v, range.maxType); }}>
<label for={`${baseId}-range-min`}>Min</label>
<input
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="Exclusive">Exclusive</option>
</select>
</div>
<div class="range-group">
<label>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">
<select class="form-input" bind:value={() => getRangeValues().maxType, (v) => { const range = getRangeValues(); setRangeValues(range.min, range.max, range.minType, v); }}>
<label for={`${baseId}-range-max`}>Max</label>
<input
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="Exclusive">Exclusive</option>
</select>
@@ -538,17 +626,18 @@
</div>
{: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'}
{@const lengthRange = getLengthRangeValues()}
<div class="length-range-controls">
<div class="length-group">
<label>Min Length</label>
<input class="form-input" type="number" bind:value={() => getLengthRangeValues().min, (v) => { const range = getLengthRangeValues(); setLengthRangeValues(v, range.max); }} placeholder="0">
<label for={`${baseId}-length-min`}>Min Length</label>
<input class="form-input" id={`${baseId}-length-min`} type="number" bind:value={() => lengthRange.min, (v) => setLengthRangeValues(v, lengthRange.max)} placeholder="0">
</div>
<div class="length-group">
<label>Max Length</label>
<input class="form-input" type="number" bind:value={() => getLengthRangeValues().max, (v) => { const range = getLengthRangeValues(); setLengthRangeValues(range.min, v); }} placeholder="0">
<label for={`${baseId}-length-max`}>Max Length</label>
<input class="form-input" id={`${baseId}-length-max`} type="number" bind:value={() => lengthRange.max, (v) => setLengthRangeValues(lengthRange.min, v)} placeholder="0">
</div>
</div>
{/if}
@@ -656,4 +745,4 @@
color: #666;
margin: 0;
}
</style>
</style>
@@ -1,33 +1,44 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
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 documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null);
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null);
let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : 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) => {
return activeSpecification?.[prop] ?? "";
const getProp = (prop: string) => {
const value = (activeSpecification as Record<string, unknown>)?.[prop];
return typeof value === "string" ? value : "";
};
const setProp = (prop, value) => {
activeSpecification[prop] = value;
const setProp = (prop: string, value: string) => {
if (!activeSpecification) return;
(activeSpecification as Record<string, unknown>)[prop] = value;
};
const addIfcVersion = (e, version) => {
const addIfcVersion = (e: Event, version: string) => {
if (activeSpecification) {
if (!("@ifcVersion" in activeSpecification)) activeSpecification["@ifcVersion"] = [];
if (e.target.checked) {
if (!activeSpecification["@ifcVersion"].includes(version)) {
activeSpecification["@ifcVersion"] = [...activeSpecification["@ifcVersion"], version];
const target = e.target as HTMLInputElement | null;
if (target?.checked) {
if (!activeSpecification["@ifcVersion"]?.includes(version)) {
activeSpecification["@ifcVersion"] = [...(activeSpecification["@ifcVersion"] ?? []), version];
}
} else {
} else if (activeSpecification["@ifcVersion"]) {
activeSpecification["@ifcVersion"] = activeSpecification["@ifcVersion"].filter(v => v !== version);
}
}
};
const setUsage = (usage) => {
const setUsage = (usage: IdsCardinality) => {
if (!activeSpecification) return;
if (!activeSpecification.applicability) activeSpecification.applicability = {};
@@ -60,14 +71,19 @@
</div>
<div class="form-group">
<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="optional">Optional</option>
<option value="prohibited">Prohibited</option>
</select>
</div>
<div class="form-group full-width">
<label>IFC Version</label>
<span class="form-label">IFC Version</span>
<div class="radio-group">
<label class="radio-label">
<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>
</div>
</div>
</div>
</div>
@@ -1,5 +1,5 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import AppHeader from "$src/components/AppHeader.svelte";
import AppRibbon from "$src/components/AppRibbon.svelte";
import AppToolbar from "$src/components/AppToolbar.svelte";
@@ -11,14 +11,26 @@
import IdsViewer from "./IdsViewer.svelte";
import SplashScreen from "$src/components/SplashScreen.svelte";
import { Toaster } from "$lib/components/ui/sonner";
import { error, success } from "$src/modules/utils/toast.svelte.js";
import {onMount} from "svelte";
import { error, success } from "$src/modules/utils/toast.svelte";
import type { DocumentState, IdsDocument, Specification } from "$src/types/ids";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null);
let documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null);
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null);
let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : 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() {
if (!IDS.Module.activeDocument) return;
@@ -28,11 +40,11 @@
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;
try {
const sourceDoc = IDS.Module.documents[sourceDocId];
const sourceDoc = IDS.Module.documents[sourceDocId] as IdsDocument;
const sourceSpec = sourceDoc.specifications?.specification?.[specIndex];
if (!sourceSpec) {
@@ -41,13 +53,13 @@
}
// 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
IDS.Module.documents[IDS.Module.activeDocument].specifications.specification.push(specCopy);
// 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;
IDS.setDocumentState(IDS.Module.activeDocument, {
activeSpecification: newSpecIndex,
@@ -57,17 +69,18 @@
success(`Specification "${sourceSpec['@name'] || 'Unnamed'}" imported successfully`);
} catch (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) {
IDS.setDocumentState(IDS.Module.activeDocument, { activeSpecification: index });
}
}
async function deleteSpecification(specIndex) {
async function deleteSpecification(specIndex: number) {
if (!IDS.Module.activeDocument) return;
await IDS.deleteSpecification(IDS.Module.activeDocument, specIndex);
}
@@ -83,9 +96,24 @@
success('IDS document exported successfully');
} catch (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>
{#if IDS.Module.status != "ready"}
@@ -130,8 +158,7 @@
Import from IDS
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent class="w-64 max-h-64 overflow-y-auto">
{#each Object.entries(IDS.Module.documents) as [docId, doc]}
{#if docId !== IDS.Module.activeDocument && doc.specifications?.specification?.length > 0}
{#each importableDocuments as [docId, doc], docIndex}
<DropdownMenu.Label class="font-medium text-xs text-muted-foreground px-2 py-1 truncate">
{doc.info?.title || 'Untitled Document'}
</DropdownMenu.Label>
@@ -146,12 +173,11 @@
</span>
</DropdownMenu.Item>
{/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 />
{/if}
{/if}
{/each}
{#if Object.entries(IDS.Module.documents).filter(([docId, doc]) => docId !== IDS.Module.activeDocument && doc.specifications?.specification?.length > 0).length === 0}
{#if importableDocuments.length === 0}
<DropdownMenu.Item disabled>
<span class="text-sm">No specifications available to import</span>
</DropdownMenu.Item>
@@ -162,16 +188,30 @@
</DropdownMenu.Root>
</div>
<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-name">IDS Information</span>
</div>
{#if activeDocument?.specifications?.specification}
{#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-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">
<path d="M18 6L6 18M6 6l12 12"></path>
</svg>
@@ -192,10 +232,10 @@
{:else}
<!-- Editor/Viewer 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
</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
</button>
</div>
@@ -209,18 +249,18 @@
<div class="spec-header">
<h2>{activeSpecification ? activeSpecification["@name"] || "Specification" : "Specification"}</h2>
<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 === 'applicability'} onclick={() => IDS.setDocumentState(IDS.Module.activeDocument, { 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 === 'info'} onclick={() => updateActiveDocumentState({ activeTab: 'info' })}>Info</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={() => updateActiveDocumentState({ activeTab: 'requirements' })}>Requirements</button>
</div>
</div>
{#if documentState?.activeTab === 'info'}
<SpecificationEditor />
{:else if documentState?.activeTab === 'applicability'}
<ApplicabilityPanel activeTab />
<ApplicabilityPanel />
{:else if documentState?.activeTab === 'requirements'}
<RequirementsPanel activeTab />
<RequirementsPanel />
{/if}
</div>
{/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 { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import path from "path";
import path from "node:path";
export default defineConfig({
plugins: [tailwindcss(), svelte()],
@@ -11,4 +11,4 @@ export default defineConfig({
$src: path.resolve("./src"),
},
},
});
});