mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
update webapp + bonsai integration
This commit is contained in:
committed by
Dion Moult
parent
1672a7da32
commit
a5d9fe551a
@@ -20,7 +20,7 @@ PACKAGE_NAME:=ifctester
|
||||
include ../common.mk
|
||||
|
||||
NODE_ENV ?= production
|
||||
WEBAPP_DIR := webapp-next
|
||||
WEBAPP_DIR := webapp
|
||||
WEBAPP_BUILD_DIR := $(WEBAPP_DIR)/dist
|
||||
|
||||
.PHONY: webapp-dev
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.claude
|
||||
experiment/*
|
||||
@@ -0,0 +1,2 @@
|
||||
# IfcTester (Next)
|
||||
The "next" version of IDS authoring and auditing on the web.
|
||||
@@ -0,0 +1,3 @@
|
||||
from .serve import app
|
||||
|
||||
__all__ = ['app']
|
||||
@@ -1,83 +0,0 @@
|
||||
# IfcTester - IDS based model auditing
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcTester.
|
||||
#
|
||||
# IfcTester is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcTester is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# 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 time
|
||||
import ifctester
|
||||
import ifctester.reporter
|
||||
import ifcopenshell
|
||||
import ifcopenshell.guid
|
||||
from flask import Flask, request, send_from_directory
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
class Ifc:
|
||||
ifc = None
|
||||
filepath = None
|
||||
|
||||
@classmethod
|
||||
def get(cls, filepath=None):
|
||||
if filepath is None or filepath == cls.filepath:
|
||||
return cls.ifc
|
||||
cls.filepath = filepath
|
||||
cls.ifc = ifcopenshell.open(filepath)
|
||||
return cls.ifc
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
with open("www/index.html") as template:
|
||||
return template.read()
|
||||
|
||||
|
||||
@app.route("/<path:asset>.<string:ext>")
|
||||
def get_asset(asset, ext):
|
||||
if ext in ("js", "css"):
|
||||
return send_from_directory("www", asset + "." + ext)
|
||||
|
||||
|
||||
@app.route("/audit", methods=["POST"])
|
||||
def audit():
|
||||
filename = ifcopenshell.guid.new()
|
||||
ids_filepath = os.path.join("uploads", filename + ".ids")
|
||||
ifc_filepath = os.path.join("uploads", filename + ".ifc")
|
||||
os.makedirs("uploads", exist_ok=True)
|
||||
request.files.get("ids").save(ids_filepath)
|
||||
request.files.get("ifc").save(ifc_filepath)
|
||||
|
||||
start = time.time()
|
||||
specs = ifctester.open(ids_filepath)
|
||||
ifc = Ifc.get(ifc_filepath)
|
||||
print("Finished loading:", time.time() - start)
|
||||
start = time.time()
|
||||
specs.validate(ifc)
|
||||
print("Finished validating:", time.time() - start)
|
||||
start = time.time()
|
||||
|
||||
os.remove(ids_filepath)
|
||||
os.remove(ifc_filepath)
|
||||
|
||||
engine = ifctester.reporter.Json(specs)
|
||||
engine.report()
|
||||
return engine.to_string()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=False)
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"tailwind": {
|
||||
"css": "src/css/app.scss",
|
||||
"baseColor": "gray"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": false,
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>IFC Tester</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
Generated
+3023
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "ifctester-next",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"deploy": "npm run build && npx wrangler pages deploy dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internationalized/date": "^3.8.1",
|
||||
"@lucide/svelte": "^0.515.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"bits-ui": "^2.8.10",
|
||||
"clsx": "^2.1.1",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"sass-embedded": "^1.89.0",
|
||||
"svelte": "^5.28.1",
|
||||
"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.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"hyperid": "^3.3.0",
|
||||
"lucide-svelte": "^0.542.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"svelecte": "^5.2.0",
|
||||
"svelte-spa-router": "^4.0.1"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,63 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="783.031" viewBox="0 0 800 783.031" xmlns:xlink="http://www.w3.org/1999/xlink" role="img" artist="Katerina Limpitsouni" source="https://undraw.co/">
|
||||
<g id="Group_178" data-name="Group 178" transform="translate(-656 -242)">
|
||||
<g id="Group_177" data-name="Group 177" transform="translate(656 242)">
|
||||
<path id="Path_2918-215" data-name="Path 2918" d="M914.2,382.533q-.65-3.406-1.369-6.789c-1.077-5.056-2.259-10.1-3.535-15.1-.632-2.528-1.311-5.056-2-7.573v-.012A392.052,392.052,0,0,0,545.043,66.735h-.035q-7.532-.3-15.1-.293c-1.381,0-2.762.012-4.131.035A391.992,391.992,0,0,0,152.944,351.586q-1.089,3.827-2.072,7.678c-1.311,5.045-2.5,10.113-3.617,15.216v.012c-.492,2.282-.96,4.553-1.416,6.835a395.6,395.6,0,0,0-7.444,76.629c0,9.82.363,19.652,1.089,29.3.105,1.381.21,2.762.327,4.132.281,3.453.632,7.046,1.042,10.7a391.609,391.609,0,0,0,756.588,91.084q4.056-11.008,7.421-22.239a385.285,385.285,0,0,0,12.407-55.76q.965-6.531,1.7-13.086c.41-3.652.761-7.245,1.042-10.686.936-10.967,1.416-22.215,1.416-33.44a395.4,395.4,0,0,0-7.221-75.424Z" transform="translate(-121.423 -66.442)" fill="#fff"/>
|
||||
<path id="Path_2919-216" data-name="Path 2919" d="M506.048,137.05a1.028,1.028,0,1,1,0-2.057h68.076A32.535,32.535,0,0,1,605.6,110.127a45.245,45.245,0,0,1,82.18-9.652c1.012-.079,2-.119,2.958-.119a38.41,38.41,0,0,1,38.2,35.592,1.028,1.028,0,0,1-.954,1.1l-.073,0a1.028,1.028,0,0,1-1.025-.956Z" transform="translate(-5.811 -62.72)" fill="#e6e6e6"/>
|
||||
<path id="Path_2920-217" data-name="Path 2920" d="M705.856,133.47H537.193a1.028,1.028,0,1,1,0-2.057H705.856a1.028,1.028,0,1,1,0,2.057Z" transform="translate(4.01 -45.954)" fill="#e6e6e6"/>
|
||||
<rect id="Rectangle_663" data-name="Rectangle 663" width="46.814" height="108.524" transform="translate(88.233 423.738)" fill="#090814"/>
|
||||
<rect id="Rectangle_664" data-name="Rectangle 664" width="46.814" height="108.524" transform="translate(681.923 423.738)" fill="#090814"/>
|
||||
<path id="Path_2921-218" data-name="Path 2921" d="M889.646,466.426C834.857,615.354,691.711,721.583,523.764,721.583S212.671,615.354,157.882,466.426Z" transform="translate(-115.278 59.69)" fill="#1ca595"/>
|
||||
<circle id="Ellipse_470" data-name="Ellipse 470" cx="134.601" cy="134.601" r="134.601" transform="translate(0 62.619)" fill="#e6e6e6"/>
|
||||
<path id="Path_2922-219" data-name="Path 2922" d="M914.2,382.533q-.65-3.406-1.369-6.789c-1.077-5.056-2.259-10.1-3.535-15.1-.632-2.528-1.311-5.056-2-7.573v-.012A392.052,392.052,0,0,0,545.043,66.735h-.035q-7.532-.3-15.1-.293c-1.381,0-2.762.012-4.131.035A391.992,391.992,0,0,0,152.944,351.586q-1.089,3.827-2.072,7.678c-1.311,5.045-2.5,10.113-3.617,15.216v.012c-.492,2.282-.96,4.553-1.416,6.835a395.6,395.6,0,0,0-7.444,76.629c0,9.82.363,19.652,1.089,29.3.105,1.381.21,2.762.327,4.132.281,3.453.632,7.046,1.042,10.7a391.609,391.609,0,0,0,756.588,91.084q4.056-11.008,7.421-22.239a385.285,385.285,0,0,0,12.407-55.76q.965-6.531,1.7-13.086c.41-3.652.761-7.245,1.042-10.686.936-10.967,1.416-22.215,1.416-33.44a395.4,395.4,0,0,0-7.221-75.424ZM916.507,491.1c-.269,3.406-.62,6.976-1.03,10.581q-.737,6.514-1.673,12.992a386.684,386.684,0,0,1-12.3,55.268q-3.354,11.113-7.362,22C838.252,743.885,691.875,845.96,529.909,845.96S221.567,743.885,165.678,591.949a385.547,385.547,0,0,1-21.337-90.265c-.41-3.6-.761-7.175-1.042-10.593-.117-1.358-.222-2.715-.327-4.085-.7-9.562-1.065-19.324-1.065-29.05a387.49,387.49,0,0,1,8.4-80.5c.492-2.353,1.018-4.693,1.545-7.034v-.012q1.861-7.936,4.015-15.754c.2-.726.4-1.451.609-2.165q.28-1.036.6-2.072c.351-1.264.726-2.516,1.1-3.757C205.754,188.028,352.317,71.779,525.8,69.977c1.369-.012,2.739-.023,4.108-.023q7.515,0,14.982.281c169.27,6.461,311.117,121.89,357.221,277.981q1.159,3.933,2.247,7.912c1.4,5.15,2.7,10.358,3.9,15.59q.79,3.476,1.51,6.987a387.08,387.08,0,0,1,8.146,79.251C917.912,469.076,917.444,480.23,916.507,491.1Z" transform="translate(-121.423 -66.442)" fill="#090814"/>
|
||||
<rect id="Rectangle_665" data-name="Rectangle 665" width="36.175" height="68.093" transform="translate(93.554 360.965)" fill="#090814"/>
|
||||
<rect id="Rectangle_666" data-name="Rectangle 666" width="8.512" height="153.21" transform="translate(98.874 214.138)" fill="#090814"/>
|
||||
<rect id="Rectangle_667" data-name="Rectangle 667" width="8.512" height="153.21" transform="translate(115.897 214.138)" fill="#090814"/>
|
||||
<rect id="Rectangle_668" data-name="Rectangle 668" width="19.151" height="10.64" transform="translate(103.131 220.522)" fill="#090814"/>
|
||||
<rect id="Rectangle_669" data-name="Rectangle 669" width="19.151" height="10.64" transform="translate(103.131 263.08)" fill="#090814"/>
|
||||
<rect id="Rectangle_670" data-name="Rectangle 670" width="19.151" height="10.64" transform="translate(103.131 309.894)" fill="#090814"/>
|
||||
<rect id="Rectangle_671" data-name="Rectangle 671" width="4.256" height="10.64" transform="translate(101.001 207.754)" fill="#090814"/>
|
||||
<rect id="Rectangle_672" data-name="Rectangle 672" width="4.256" height="10.64" transform="translate(118.026 207.754)" fill="#090814"/>
|
||||
<rect id="Rectangle_673" data-name="Rectangle 673" width="36.175" height="68.093" transform="translate(687.243 360.965)" fill="#090814"/>
|
||||
<rect id="Rectangle_674" data-name="Rectangle 674" width="8.512" height="153.21" transform="translate(692.564 214.138)" fill="#090814"/>
|
||||
<rect id="Rectangle_675" data-name="Rectangle 675" width="8.512" height="153.21" transform="translate(709.586 214.138)" fill="#090814"/>
|
||||
<rect id="Rectangle_676" data-name="Rectangle 676" width="19.151" height="10.64" transform="translate(696.818 220.522)" fill="#090814"/>
|
||||
<rect id="Rectangle_677" data-name="Rectangle 677" width="19.151" height="10.64" transform="translate(696.818 263.08)" fill="#090814"/>
|
||||
<rect id="Rectangle_678" data-name="Rectangle 678" width="19.151" height="10.64" transform="translate(696.818 309.894)" fill="#090814"/>
|
||||
<rect id="Rectangle_679" data-name="Rectangle 679" width="4.256" height="10.64" transform="translate(694.691 207.754)" fill="#090814"/>
|
||||
<rect id="Rectangle_680" data-name="Rectangle 680" width="4.256" height="10.64" transform="translate(711.713 207.754)" fill="#090814"/>
|
||||
<path id="Path_2923-220" data-name="Path 2923" d="M917.505,389.4q-.439,5.355-1.041,10.639H141.839q-.6-5.285-1.041-10.639Z" transform="translate(-120.666 35.401)" fill="#090814"/>
|
||||
<path id="Path_2924-221" data-name="Path 2924" d="M515.482,424.693c-102.026,0-207.255-66.265-312.844-197.029l3.311-2.675C312.155,356.519,417.736,422.315,519.7,420.4c99.111-1.811,197.892-67.535,293.6-195.348l3.407,2.552C720.175,356.521,620.274,422.818,519.782,424.655Q517.633,424.694,515.482,424.693Z" transform="translate(-101.165 -16.445)" fill="#090814"/>
|
||||
<path id="Path_2925-222" data-name="Path 2925" d="M511.075,410.395c-96.371,0-195.758-61.448-295.485-182.712l3.287-2.7C319.169,346.93,418.815,407.834,515.151,406.1c93.523-1.716,186.738-62.634,277.056-181.061l3.384,2.581C704.445,347.134,610.117,408.616,515.229,410.357,513.846,410.382,512.459,410.395,511.075,410.395Z" transform="translate(-97.081 -16.448)" fill="#090814"/>
|
||||
<path id="Path_2926-223" data-name="Path 2926" d="M752.237,332.535c-1.475-1.264-2.961-2.54-4.436-3.827q-47.772-41.592-95.789-101.045l3.312-2.669q45.594,56.48,90.967,96.726,2.282,2.037,4.577,4.026Q751.587,329.135,752.237,332.535Z" transform="translate(40.542 -16.444)" fill="#090814"/>
|
||||
<path id="Path_2927-224" data-name="Path 2927" d="M743.25,310.653q-2.476-2.212-4.939-4.518a785.148,785.148,0,0,1-73.352-78.455l3.289-2.7a793.373,793.373,0,0,0,67.816,73.247c1.732,1.65,3.453,3.265,5.185,4.846v.012C741.939,305.6,742.618,308.125,743.25,310.653Z" transform="translate(44.625 -16.449)" fill="#090814"/>
|
||||
<rect id="Rectangle_681" data-name="Rectangle 681" width="4.256" height="184.538" transform="translate(146.992 243.456)" fill="#090814"/>
|
||||
<rect id="Rectangle_682" data-name="Rectangle 682" width="4.256" height="144.025" transform="translate(186.583 283.969)" fill="#090814"/>
|
||||
<rect id="Rectangle_683" data-name="Rectangle 683" width="4.256" height="100.05" transform="translate(240.515 329.008)" fill="#090814"/>
|
||||
<rect id="Rectangle_684" data-name="Rectangle 684" width="4.256" height="67.029" transform="translate(292.142 360.965)" fill="#090814"/>
|
||||
<rect id="Rectangle_685" data-name="Rectangle 685" width="4.256" height="51.089" transform="translate(334.981 379.033)" fill="#090814"/>
|
||||
<rect id="Rectangle_686" data-name="Rectangle 686" width="4.256" height="184.538" transform="translate(736.898 243.456)" fill="#090814"/>
|
||||
<rect id="Rectangle_687" data-name="Rectangle 687" width="4.256" height="144.025" transform="translate(776.489 283.969)" fill="#090814"/>
|
||||
<path id="Path_2928-225" data-name="Path 2928" d="M242.886,227.663q-22.191,27.476-44.348,51.172-2.142,2.265-4.249,4.506-17.7,18.592-35.336,34.762-2.141,1.966-4.26,3.874c-2.06,1.849-4.109,3.675-6.168,5.478-.48.421-.948.843-1.428,1.252-1.006.878-2.025,1.756-3.043,2.622.457-2.282.925-4.553,1.416-6.835v-.012c1.054-.9,2.1-1.826,3.137-2.762.491-.433.972-.866,1.463-1.3v-.012q2.317-2.054,4.623-4.167,2.141-1.931,4.26-3.921,17.645-16.345,35.336-35.2c1.416-1.51,2.832-3.02,4.249-4.553q20.506-22.139,41.036-47.579Z" transform="translate(-119.639 -16.444)" fill="#090814"/>
|
||||
<path id="Path_2929-226" data-name="Path 2929" d="M224.65,227.68Q210.762,244.57,196.9,259.9c-1.276,1.4-2.54,2.8-3.815,4.178q-17.856,19.47-35.687,36.342-1.967,1.861-3.909,3.675c-.2.2-.41.386-.609.562-.527.5-1.053.983-1.58,1.475-1.136,1.065-2.271,2.107-3.418,3.137q.983-3.845,2.072-7.678,1.808-1.65,3.593-3.371c.55-.515,1.089-1.03,1.639-1.557q18.4-17.469,36.834-37.735,14.66-16.117,29.343-33.955Z" transform="translate(-118.432 -16.449)" fill="#090814"/>
|
||||
<rect id="Rectangle_688" data-name="Rectangle 688" width="4.256" height="184.538" transform="translate(74.648 243.456)" fill="#090814"/>
|
||||
<rect id="Rectangle_689" data-name="Rectangle 689" width="4.256" height="144.025" transform="translate(35.057 283.969)" fill="#090814"/>
|
||||
<rect id="Rectangle_690" data-name="Rectangle 690" width="4.256" height="188.793" transform="translate(672.109 239.2)" fill="#090814"/>
|
||||
<rect id="Rectangle_691" data-name="Rectangle 691" width="4.256" height="144.025" transform="translate(632.515 283.969)" fill="#090814"/>
|
||||
<rect id="Rectangle_692" data-name="Rectangle 692" width="4.256" height="100.05" transform="translate(578.586 329.008)" fill="#090814"/>
|
||||
<rect id="Rectangle_693" data-name="Rectangle 693" width="4.256" height="67.029" transform="translate(526.959 360.965)" fill="#090814"/>
|
||||
<rect id="Rectangle_694" data-name="Rectangle 694" width="4.256" height="51.089" transform="translate(484.118 379.033)" fill="#090814"/>
|
||||
<rect id="Rectangle_695" data-name="Rectangle 695" width="4.256" height="38.53" transform="translate(401.616 391.592)" fill="#090814"/>
|
||||
<path id="Path_2930-227" data-name="Path 2930" d="M438.4,547.825H340.018a5.725,5.725,0,1,1,0-11.449H438.4a5.725,5.725,0,0,1,0,11.449Z" transform="translate(-59.648 81.748)" fill="#fff"/>
|
||||
<path id="Path_2931-228" data-name="Path 2931" d="M558.407,521.032H460.025a5.725,5.725,0,1,1,0-11.449h98.381a5.724,5.724,0,1,1,0,11.449Z" transform="translate(-21.805 73.299)" fill="#fff"/>
|
||||
<path id="Path_2932-229" data-name="Path 2932" d="M509.8,585.3H411.415a5.725,5.725,0,0,1,0-11.449H509.8a5.725,5.725,0,0,1,0,11.449Z" transform="translate(-37.134 93.564)" fill="#fff"/>
|
||||
<path id="Path_2933-230" data-name="Path 2933" d="M362.048,212.05a1.028,1.028,0,1,1,0-2.057h68.076A32.535,32.535,0,0,1,461.6,185.127a45.245,45.245,0,0,1,82.18-9.652c1.012-.079,2-.119,2.958-.119a38.41,38.41,0,0,1,38.2,35.592,1.028,1.028,0,0,1-.954,1.1l-.073,0a1.028,1.028,0,0,1-1.025-.956Z" transform="translate(-51.22 -39.069)" fill="#e6e6e6"/>
|
||||
<path id="Path_2934-231" data-name="Path 2934" d="M438.029,219.807H381.465a1.028,1.028,0,1,1,0-2.057h56.564a1.028,1.028,0,1,1,0,2.057Z" transform="translate(-45.097 -18.728)" fill="#e6e6e6"/>
|
||||
<path id="Path_2935-232" data-name="Path 2935" d="M451.665,113.7l8.553-6.841c-6.644-.733-9.374,2.891-10.492,5.759-5.191-2.155-10.842.669-10.842.669L456,119.5A12.949,12.949,0,0,0,451.665,113.7Z" transform="translate(-26.666 -53.727)" fill="#090814"/>
|
||||
<path id="Path_2936-233" data-name="Path 2936" d="M474.8,248.956l8.553-6.841c-6.644-.733-9.374,2.891-10.492,5.759-5.191-2.155-10.842.669-10.842.669l17.113,6.213a12.949,12.949,0,0,0-4.332-5.8Z" transform="translate(-19.371 -11.075)" fill="#090814"/>
|
||||
<path id="Path_2937-234" data-name="Path 2937" d="M626.963,198.235l8.553-6.841c-6.644-.733-9.374,2.891-10.492,5.759-5.191-2.155-10.842.669-10.842.669l17.113,6.213A12.95,12.95,0,0,0,626.963,198.235Z" transform="translate(28.613 -27.069)" fill="#090814"/>
|
||||
<rect id="Rectangle_696" data-name="Rectangle 696" width="83.102" height="14.045" transform="translate(67.887 512.07)" fill="#090814"/>
|
||||
<rect id="Rectangle_697" data-name="Rectangle 697" width="83.102" height="14.045" transform="translate(663.644 512.07)" fill="#090814"/>
|
||||
<rect id="Rectangle_698" data-name="Rectangle 698" width="778.085" height="3.511" transform="translate(19.376 414.524)" fill="#090814"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32mm" height="32mm" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:serif="http://www.serif.com/" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><linearGradient id="d" x2="1" gradientTransform="matrix(388.04 -111.5 84.017 279.39 391.79 1018.9)" gradientUnits="userSpaceOnUse"><stop stop-color="#3bb549" offset="0"/><stop stop-color="#f5ee30" offset="1"/></linearGradient><linearGradient id="c" x2=".95791" y2=".0001803" gradientTransform="matrix(122.94 -266.68 378.61 181.95 228.98 392.44)" gradientUnits="userSpaceOnUse"><stop stop-color="#f5ee30" offset="0"/><stop stop-color="#3bb549" offset="1"/></linearGradient><linearGradient id="b" x2="1" gradientTransform="matrix(208.13 0 0 420.73 148.9 534.09)" gradientUnits="userSpaceOnUse"><stop stop-color="#f1592a" offset="0"/><stop stop-color="#f5e732" offset="1"/></linearGradient><linearGradient id="a" x2="1" gradientTransform="matrix(401.83 285.46 -279.32 410.66 391.79 578.6)" gradientUnits="userSpaceOnUse"><stop stop-color="#f5e732" offset="0"/><stop stop-color="#f1592a" offset="1"/></linearGradient></defs><g transform="translate(-1082.3 -67.993)"><g transform="matrix(.02877 0 0 .027964 1085.1 67.175)" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" stroke-width="8.4072" serif:id="Logo IfcOpenShell"><path d="m744.02 832.33c18.051 11.403 55.982 31.518 63.628 94.935l-0.12525 23.628c-8.342 104.33-183.41 185.58-415.74 186.93v-215.59c141.61-0.7966 269.74-30.595 352.23-89.901z" fill="url(#d)" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" stroke-width="8.1335"/><path d="m181.03 442.16c-12.017-10.232-30.084-43.15-32.067-93.421l-0.0617-16.621c1.4066-109.27 91.01-193.98 208.13-195.34v214.31c-47.868 0.55282-122.9 15.206-176 91.069z" fill="url(#c)" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" stroke-width="8.3203"/><path d="m148.9 334.48c1.4117 109.69 91.548 194.06 208.13 195.34v214.63c-116.74-1.3026-206.72-85.776-208.13-195.34z" fill="url(#b)" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" stroke-width="8.3203"/><path d="m391.79 744.46v-214.64c235.46 1.3846 413.03 88.962 415.86 200.34v214.64c-2.8649-112.42-183.57-199-415.86-200.34z" fill="url(#a)" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" stroke-width="8.3164"/><rect transform="matrix(4.3448 0 0 4.47 -94.827 29.251)" x="-8.2421e-7" y="-1.0518e-16" width="256" height="256" fill="none"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,181 @@
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util
|
||||
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
|
||||
ifc_schemas = ["IFC2X3", "IFC4", "IFC4X3_ADD2"]
|
||||
|
||||
def get_predefined_types_for_entity(schema_name, entity_name):
|
||||
"""Get a list of predefined types for a given entity."""
|
||||
|
||||
schema = ifcopenshell.schema_by_name(schema_name)
|
||||
|
||||
try:
|
||||
entity = schema.declaration_by_name(entity_name)
|
||||
except:
|
||||
return []
|
||||
|
||||
if not entity or not entity.as_entity():
|
||||
print(f"Entity {entity_name} not found")
|
||||
return []
|
||||
|
||||
entity = entity.as_entity()
|
||||
predefined_type_attr = None
|
||||
|
||||
# Check all attributes for "PredefinedType"
|
||||
for attr in entity.all_attributes():
|
||||
if attr.name() == "PredefinedType":
|
||||
predefined_type_attr = attr
|
||||
break
|
||||
|
||||
if not predefined_type_attr:
|
||||
return []
|
||||
|
||||
param_type = predefined_type_attr.type_of_attribute()
|
||||
|
||||
if param_type.as_named_type():
|
||||
enum_decl = param_type.as_named_type().declared_type()
|
||||
if enum_decl.as_enumeration_type():
|
||||
return enum_decl.as_enumeration_type().enumeration_items()
|
||||
|
||||
return []
|
||||
|
||||
def get_all_entity_classes(schema_name):
|
||||
"""Get all IFC entity classes in the given schema."""
|
||||
|
||||
schema = ifcopenshell.schema_by_name(schema_name)
|
||||
entities = []
|
||||
|
||||
for entity in schema.entities():
|
||||
entities.append(entity.name())
|
||||
|
||||
# Sort alphabetically
|
||||
entities.sort()
|
||||
return entities
|
||||
|
||||
def get_all_data_types(schema_name):
|
||||
"""Get all data types in the given schema."""
|
||||
|
||||
schema = ifcopenshell.schema_by_name(schema_name)
|
||||
return {d.name(): ifcopenshell.util.attribute.get_primitive_type(d) for d in schema.declarations() if d.as_type_declaration()}
|
||||
|
||||
def get_entity_attributes(schema_name, entity_name):
|
||||
"""Get all attributes for a given entity."""
|
||||
|
||||
schema = ifcopenshell.schema_by_name(schema_name)
|
||||
|
||||
try:
|
||||
entity = schema.declaration_by_name(entity_name)
|
||||
except:
|
||||
return []
|
||||
|
||||
if not entity or not entity.as_entity():
|
||||
print(f"Entity {entity_name} not found")
|
||||
return []
|
||||
|
||||
entity = entity.as_entity()
|
||||
attributes = []
|
||||
for attr in entity.all_attributes():
|
||||
attributes.append({
|
||||
"name": attr.name(),
|
||||
# "type": attr.type_of_attribute() # TODO Types of attribute
|
||||
})
|
||||
|
||||
return attributes
|
||||
|
||||
def get_applicable_psets(schema_name, entity_name, predefined_type = ""):
|
||||
"""Get all applicable property and quantity sets for a given entity."""
|
||||
|
||||
pset_qto = ifcopenshell.util.pset.PsetQto(schema_name)
|
||||
pset_names = pset_qto.get_applicable_names(entity_name, predefined_type)
|
||||
|
||||
return pset_names
|
||||
|
||||
def get_all_psets(schema_name):
|
||||
"""Get all property sets and quantity sets defined in an IFC schema"""
|
||||
|
||||
pset_qto = ifcopenshell.util.pset.PsetQto(schema_name)
|
||||
result = {}
|
||||
|
||||
for template_file in pset_qto.templates:
|
||||
for pset_template in template_file.by_type("IfcPropertySetTemplate"):
|
||||
pset_name = pset_template.Name
|
||||
properties = []
|
||||
|
||||
# Get property templates for this pset
|
||||
if pset_template.HasPropertyTemplates:
|
||||
for prop_template in pset_template.HasPropertyTemplates:
|
||||
prop_info = {
|
||||
"name": prop_template.Name,
|
||||
# "description": prop_template.Description
|
||||
}
|
||||
|
||||
# Extract type information
|
||||
if prop_template.is_a("IfcSimplePropertyTemplate"):
|
||||
if prop_template.PrimaryMeasureType:
|
||||
prop_info["type"] = prop_template.PrimaryMeasureType
|
||||
else:
|
||||
prop_info["type"] = str(prop_template.TemplateType)
|
||||
elif prop_template.is_a("IfcComplexPropertyTemplate"):
|
||||
prop_info["type"] = None # Complex properties are not supported
|
||||
else:
|
||||
prop_info["type"] = None
|
||||
|
||||
properties.append(prop_info)
|
||||
result[pset_name] = properties
|
||||
|
||||
return result
|
||||
|
||||
def get_material_categories():
|
||||
return ['concrete', 'steel', 'aluminium', 'block', 'brick', 'stone', 'wood', 'glass', 'gypsum', 'plastic', 'earth']
|
||||
|
||||
def get_standard_classification_systems():
|
||||
return {
|
||||
'BB/SfB (3/4 cijfers)': {'source': 'Regie der Gebouwen', 'tokens': ['.']},
|
||||
'BIMTypeCode': {'source': 'BIMStockholm', 'tokens': None},
|
||||
'Common Arrangement of Work Sections (CAWS)': {'source': 'NBS', 'tokens': ['/']},
|
||||
'CBI Classification - Level 2': {'source': 'Masterspec', 'tokens': None},
|
||||
'CBI Classification - Level 4': {'source': 'Masterspec', 'tokens': None},
|
||||
'Rumsfunktionskoder CC001 - 001': {'source': 'BIMAlliance', 'tokens': ['-']},
|
||||
'CCS': {'source': 'Molio', 'tokens': None},
|
||||
'CCTB': {'source': 'CCT-Bâtiments', 'tokens': ['.']},
|
||||
'Funktionskoder Regionservice CD001 - 001': {'source': 'BIMAlliance', 'tokens': None},
|
||||
'Rumsfunktion Blekinge CD002 - 001': {'source': 'BIMAlliance', 'tokens': None},
|
||||
'EcoQuaestor Codetabel': {'source': 'EcoQuaestor', 'tokens': ['.', '-']},
|
||||
'GuBIMclass CA': {'source': 'GuBIMClass', 'tokens': ['.']},
|
||||
'GuBIMclass ES': {'source': 'GuBIMClass', 'tokens': ['.']},
|
||||
'MasterFormat': {'source': 'CSI', 'tokens': [' ', '.']},
|
||||
'NATSPEC Worksections': {'source': 'NATSPEC', 'tokens': None},
|
||||
'NBS Create': {'source': 'NBS', 'tokens': ['_', '/']},
|
||||
'NL/SfB (4 cijfers)': {'source': 'BIMLoket', 'tokens': ['.']},
|
||||
'NS 3451 - Bygningsdelstabell': {'source': 'Standard Norge', 'tokens': None},
|
||||
'OmniClass': {'source': 'OmniClass', 'tokens': ['-', ' ']},
|
||||
'ÖNORM 6241-2': {'source': 'freeBIM 2', 'tokens': None},
|
||||
'RICS NRM1': {'source': 'RICS', 'tokens': ['.']},
|
||||
'RICS NRM3': {'source': 'RICS', 'tokens': ['.']},
|
||||
'SFG20': {'source': 'SFG20', 'tokens': ['-']},
|
||||
'SINAPI': {'source': 'Caixa', 'tokens': ['/']},
|
||||
'STABU-Element': {'source': 'STABU', 'tokens': ['.']},
|
||||
'TALO 2000 Building Component Classification': {'source': 'Rakennustieto', 'tokens': ['.']},
|
||||
'TALO 2000 Hankenimikkeistö': {'source': 'Rakennustieto', 'tokens': ['.']},
|
||||
'Uniclass': {'source': 'RIBA Enterprises Ltd', 'tokens': ['_']},
|
||||
'Uniclass 2015': {'source': 'RIBA Enterprises Ltd', 'tokens': ['_']},
|
||||
'UniFormat': {'source': 'UniFormat', 'tokens': ['.']},
|
||||
'Uniformat': {'source': 'UniFormat', 'tokens': ['.']},
|
||||
'VMSW': {'source': 'VMSW', 'tokens': ['.']}
|
||||
}
|
||||
|
||||
def ids_from_xml_string(xml: str, validate: bool = False) -> Ids:
|
||||
try:
|
||||
decode = get_schema().decode(
|
||||
xml, strip_namespaces=True, namespaces={
|
||||
"": "http://standards.buildingsmart.org/IDS"
|
||||
}
|
||||
)
|
||||
except XMLSchemaValidationError as e:
|
||||
raise IdsXmlValidationError(e, "Provided XML appears to be invalid. See details above.")
|
||||
return Ids().parse(decode)
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,69 @@
|
||||
# IfcTester - IDS based model auditing
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcTester.
|
||||
#
|
||||
# IfcTester is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcTester is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# 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 argparse
|
||||
from flask import Flask, send_from_directory, send_file
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def get_static_folder():
|
||||
base_dir = os.path.dirname(__file__)
|
||||
dist_dir = os.path.join(base_dir, 'dist')
|
||||
www_dir = os.path.join(base_dir, 'www')
|
||||
|
||||
if os.path.exists(dist_dir) and os.path.isdir(dist_dir):
|
||||
return dist_dir
|
||||
elif os.path.exists(www_dir) and os.path.isdir(www_dir):
|
||||
return www_dir
|
||||
else:
|
||||
return dist_dir
|
||||
|
||||
STATIC_FOLDER = get_static_folder()
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return send_file(os.path.join(STATIC_FOLDER, 'index.html'))
|
||||
|
||||
@app.route('/<path:filename>')
|
||||
def static_files(filename):
|
||||
return send_from_directory(STATIC_FOLDER, filename)
|
||||
|
||||
@app.route('/assets/<path:filename>')
|
||||
def assets(filename):
|
||||
return send_from_directory(os.path.join(STATIC_FOLDER, 'assets'), filename)
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return send_file(os.path.join(STATIC_FOLDER, 'index.html'))
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Start IfcTester webapp')
|
||||
parser.add_argument('--host', default='127.0.0.1', help='Host to bind to (default: 127.0.0.1)')
|
||||
parser.add_argument('--port', type=int, default=5000, help='Port to bind to (default: 5000)')
|
||||
parser.add_argument('--debug', action='store_true', help='Enable debug mode')
|
||||
parser.add_argument('--dist-dir', default=STATIC_FOLDER, help='Directory containing built files')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
STATIC_FOLDER = args.dist_dir
|
||||
|
||||
print(f"Serving IfcTester webapp from: {STATIC_FOLDER}")
|
||||
print(f"Server running at: http://{args.host}:{args.port}")
|
||||
|
||||
app.run(host=args.host, port=args.port, debug=args.debug)
|
||||
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
import Router from 'svelte-spa-router';
|
||||
import routes from './routes';
|
||||
</script>
|
||||
|
||||
<Router {routes} />
|
||||
@@ -0,0 +1,145 @@
|
||||
<script>
|
||||
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";
|
||||
|
||||
let { isOpen = false } = $props();
|
||||
|
||||
function openForum() {
|
||||
window.open('https://community.osarch.org', '_blank');
|
||||
}
|
||||
|
||||
function openAbout() {
|
||||
isOpen = true;
|
||||
}
|
||||
|
||||
async function newIDSFile() {
|
||||
await IDS.createDocument();
|
||||
}
|
||||
|
||||
async function openIDSFile() {
|
||||
try {
|
||||
await IDS.openDocument();
|
||||
} catch (err) {
|
||||
if (err.message !== 'File selection cancelled') {
|
||||
error('Error opening file: ' + err.message);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveIDSFile() {
|
||||
if (!IDS.Module.activeDocument) {
|
||||
error('No document to save!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await IDS.exportDocument(IDS.Module.activeDocument);
|
||||
success('Document saved successfully');
|
||||
} catch (err) {
|
||||
console.error("Error saving file: ", err);
|
||||
error('Error saving file: check console for details');
|
||||
}
|
||||
}
|
||||
|
||||
async function runAudit() {
|
||||
try {
|
||||
await API.runAudit();
|
||||
success('Audit completed successfully');
|
||||
} catch (err) {
|
||||
console.error("Audit failed: ", err);
|
||||
error(`Audit failed: check console for details`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="app-header">
|
||||
<div class="logo"></div>
|
||||
<div class="menu">
|
||||
<Menubar.Root>
|
||||
<Menubar.Menu>
|
||||
<Menubar.Trigger>File</Menubar.Trigger>
|
||||
<Menubar.Content>
|
||||
<Menubar.Item onclick={newIDSFile}>
|
||||
New IDS file
|
||||
<Menubar.Shortcut>⌘N</Menubar.Shortcut>
|
||||
</Menubar.Item>
|
||||
<Menubar.Item onclick={openIDSFile}>
|
||||
Open IDS file
|
||||
<Menubar.Shortcut>⌘O</Menubar.Shortcut>
|
||||
</Menubar.Item>
|
||||
<Menubar.Item onclick={saveIDSFile}>
|
||||
Save IDS file
|
||||
<Menubar.Shortcut>⌘S</Menubar.Shortcut>
|
||||
</Menubar.Item>
|
||||
</Menubar.Content>
|
||||
</Menubar.Menu>
|
||||
<Menubar.Menu>
|
||||
<Menubar.Trigger>IFC</Menubar.Trigger>
|
||||
<Menubar.Content>
|
||||
<Menubar.Item onclick={API.openIfc}>Open IFC model</Menubar.Item>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item onclick={runAudit}>Run Audit</Menubar.Item>
|
||||
</Menubar.Content>
|
||||
</Menubar.Menu>
|
||||
<Menubar.Menu>
|
||||
<Menubar.Trigger>Help</Menubar.Trigger>
|
||||
<Menubar.Content>
|
||||
<Menubar.Item onclick={openForum}>OSArch Forum</Menubar.Item>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item onclick={openAbout}>About</Menubar.Item>
|
||||
</Menubar.Content>
|
||||
</Menubar.Menu>
|
||||
</Menubar.Root>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- About Dialog -->
|
||||
<Dialog.Root bind:open={isOpen}>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>About IfcTester</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div class="py-4 space-y-4">
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
IfcTester (Next). Designed and developed by
|
||||
<a href="https://github.com/theseyan" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline">Sayan J. Das</a>
|
||||
as their <a href="https://summerofcode.withgoogle.com/programs/2025/projects/888lO1F8" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline">Google Summer of Code 2025 project</a> under the mentorship of
|
||||
<a href="https://github.com/moult" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline">Dion Moult</a>.
|
||||
</p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm">
|
||||
<span class="font-medium">Source Code:</span>
|
||||
<br>
|
||||
<a href="https://github.com/theseyan/ifctester-next" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline text-xs">
|
||||
github.com/theseyan/ifctester-next
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="text-sm">
|
||||
<span class="font-medium">Support & Bug Reports:</span>
|
||||
<br>
|
||||
<a href="https://matrix.to/#/@sayanjdas:matrix.org" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline text-xs">
|
||||
@sayanjdas:matrix.org
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="text-sm">
|
||||
<span class="font-medium">License:</span>
|
||||
<br>
|
||||
<a href="https://github.com/IfcOpenShell/IfcOpenShell/blob/master/COPYING.LESSER" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline text-xs">
|
||||
LGPL License
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close asChild>
|
||||
<button class="btn">Close</button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { Module } from "$src/modules/api/ids.svelte.js";
|
||||
</script>
|
||||
|
||||
<div class="app-ribbon">
|
||||
{#if Module.status === 'loading'}
|
||||
<div class="status-indicator">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading IfcOpenShell...</span>
|
||||
</div>
|
||||
{:else if Module.status === 'ready'}
|
||||
<div class="status-indicator ready">
|
||||
<div class="status-dot"></div>
|
||||
<span>Ready</span>
|
||||
</div>
|
||||
{:else if Module.status === 'error'}
|
||||
<div class="status-indicator error">
|
||||
<div class="error-dot"></div>
|
||||
<span>Error</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,695 @@
|
||||
<script>
|
||||
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 { ChevronRightIcon, LinkIcon, XIcon } from "@lucide/svelte";
|
||||
import { Bonsai, connect, disconnect, runAudit as runBonsaiAudit } from "$src/modules/api/bonsai.svelte.js";
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let isAuditing = $state(false);
|
||||
let activeTab = $state('home');
|
||||
let isMinimized = $state(false);
|
||||
|
||||
const handleLoadModel = async () => {
|
||||
try {
|
||||
await openIfc();
|
||||
success('IFC model loaded successfully');
|
||||
} catch (err) {
|
||||
error(`Failed to load IFC model: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnloadModel = async (modelId) => {
|
||||
try {
|
||||
await unloadIfc(modelId);
|
||||
} catch (err) {
|
||||
error(`Failed to unload model: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunAudit = async () => {
|
||||
try {
|
||||
isAuditing = true;
|
||||
await runAudit();
|
||||
success('Audit completed successfully');
|
||||
} catch (err) {
|
||||
console.error("Audit failed: ", err);
|
||||
error(`Audit failed: check console for details`);
|
||||
} finally {
|
||||
isAuditing = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewAuditReport = (auditId) => {
|
||||
const auditReport = IFCModels.audits.find(audit => audit.id === auditId);
|
||||
if (!auditReport) return;
|
||||
|
||||
// Switch to the IDS document that was used for this audit
|
||||
if (auditReport.document && IDS.Module.documents[auditReport.document]) {
|
||||
// Set the correct document as active
|
||||
IDS.Module.activeDocument = auditReport.document;
|
||||
|
||||
// Set the document state to show the audit report
|
||||
IDS.setDocumentState(auditReport.document, {
|
||||
viewMode: 'viewer',
|
||||
auditReport: auditId
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes) => {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
const handleBonsaiAudit = async () => {
|
||||
const auditId = await runBonsaiAudit();
|
||||
if (auditId) {
|
||||
// Auto-open the report viewer
|
||||
handleViewAuditReport(auditId);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (Bonsai.enabled) {
|
||||
activeTab = 'bonsai';
|
||||
connect();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="buttons">
|
||||
<Tooltip.Provider>
|
||||
{#if isMinimized}
|
||||
<Tooltip.Root disableHoverableContent="true">
|
||||
<Tooltip.Trigger>
|
||||
<button class="tb-btn expand-btn" onclick={() => isMinimized = false} aria-label="Expand Toolbar">
|
||||
<ChevronRightIcon size={24} />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="right">
|
||||
<p>Expand Toolbar</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
<Tooltip.Root disableHoverableContent="true">
|
||||
<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">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m4 12 8-8 8 8M6 10.5V19a1 1 0 0 0 1 1h3v-3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3h3a1 1 0 0 0 1-1v-8.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="right">
|
||||
<p>Home</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root disableHoverableContent="true">
|
||||
<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">
|
||||
<defs><linearGradient id="a" x1="319.66" x2="414.22" y1="725.95" y2="631.1" gradientTransform="matrix(.34384 0 0 .34384 1065.6 -23.668)" gradientUnits="userSpaceOnUse"><stop stop-color="currentColor" offset="0" /><stop stop-color="currentColor" offset="1" /></linearGradient></defs>
|
||||
<g transform="translate(-1274.3 -68)"><g transform="matrix(.89066 0 0 .89066 230.47 -102.44)" clip-rule="evenodd"><path d="m1177.3 192.49c-1.0334-2e-5 -1.8713 0.83759-1.8715 1.871v29.941c0 1.0336 0.8379 1.8716 1.8715 1.8715h19.461c0.4963-1.1e-4 0.9723-0.19737 1.3231-0.54839l7.8162-7.8161c0.7306-0.73081 0.7306-1.9154 0-2.6462l-5.0282-5.0282-2.3818 2.3818 3.9696 3.9696-6.3191 6.3191h-17.344v-26.946l17.321-0.0212 6.3429 6.3581-12.703 12.703-5.5574-5.5574 5.5574-5.5574 4.7635 4.7635 2.3818-2.3818-5.8217-5.8221c-0.7286-0.72842-1.9168-0.72974-2.6467 0l-7.6763 7.6763c-0.706 0.70636-0.733 1.8426-0.061 2.5817l7.7091 7.7091c0.7309 0.80387 1.9905 0.81846 2.7398 0.0317l14.796-14.864c0.7006-0.73553 0.6866-1.8956-0.032-2.614l-7.8253-7.8253c-0.351-0.35081-0.8269-0.54787-1.3231-0.54785z" fill="currentColor" fill-rule="evenodd"/><rect transform="matrix(.14035 0 0 .14035 1172 191.36)" x="-8.2421e-7" y="-1.0518e-16" width="256" height="256" fill="none"/></g></g>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="right">
|
||||
<p>Bonsai Integration</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
</div>
|
||||
<div class="content scrollbar" style:display={isMinimized ? 'none' : 'block'}>
|
||||
<div class="content-header">
|
||||
{#if isMinimized}
|
||||
<button onclick={() => isMinimized = false} class="open-btn" aria-label="Open Toolbar">
|
||||
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 10 12 14 8 10m8 0V4h-8v6"/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<h1>{activeTab === 'home' ? 'IFC Models' : 'Bonsai Integration'}</h1>
|
||||
<button onclick={() => isMinimized = true} aria-label="Minimize Toolbar">
|
||||
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.99994 10 7 11.9999l1.99994 2M12 5v14M5 4h14c.5523 0 1 .44772 1 1v14c0 .5523-.4477 1-1 1H5c-.55228 0-1-.4477-1-1V5c0-.55228.44772-1 1-1Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
{#if activeTab === 'home'}
|
||||
<div class="section">
|
||||
<button class="load-btn" onclick={handleLoadModel} disabled={IFCModels.isLoading}>
|
||||
{#if IFCModels.isLoading}
|
||||
<svg class="spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12a9 9 0 11-6.219-8.56"/>
|
||||
</svg>
|
||||
Loading...
|
||||
{:else}
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/>
|
||||
<polyline points="14,2 14,8 20,8"/>
|
||||
</svg>
|
||||
Load IFC Model
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- IFC Models -->
|
||||
{#if IFCModels.models.length > 0}
|
||||
<div class="section">
|
||||
<h3>Active Models</h3>
|
||||
<div class="models-list">
|
||||
{#each IFCModels.models as model}
|
||||
<div class="model-item">
|
||||
<div class="model-info">
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<div class="model-name">{model.fileName}</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{model.fileName}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<div class="model-meta">
|
||||
<span class="model-size">{formatFileSize(model.fileSize)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="unload-btn" onclick={() => handleUnloadModel(model.id)} title="Unload model" aria-label="Unload model">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<button class="audit-btn" onclick={handleRunAudit} disabled={isAuditing || !IDS.Module.activeDocument}>
|
||||
{#if isAuditing}
|
||||
<svg class="spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12a9 9 0 11-6.219-8.56"/>
|
||||
</svg>
|
||||
Running Audit...
|
||||
{:else}
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12,6 12,12 16,14"/>
|
||||
</svg>
|
||||
Run Audit
|
||||
{/if}
|
||||
</button>
|
||||
{#if !IDS.Module.activeDocument}
|
||||
<p class="help-text">Create or open an IDS document to enable auditing</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/>
|
||||
<polyline points="14,2 14,8 20,8"/>
|
||||
</svg>
|
||||
<p>No IFC models loaded</p>
|
||||
<p class="help-text">Load an IFC model to begin auditing</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Audit Reports -->
|
||||
{#if IFCModels.audits.length > 0}
|
||||
<div class="section">
|
||||
<h3>Audit Reports</h3>
|
||||
<div class="audit-reports">
|
||||
{#each IFCModels.audits as audit}
|
||||
<button class="audit-report-item" onclick={() => handleViewAuditReport(audit.id)} aria-label="View audit report for {audit.modelName}">
|
||||
<div class="report-info">
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<div class="report-title">{audit.modelName}</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{audit.modelName}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<div class="report-meta">
|
||||
<span class="report-date">{new Date(audit.date).toLocaleString()}</span>
|
||||
<span class="report-status {audit.data.status ? 'pass' : 'fail'}">
|
||||
{audit.data.status ? 'PASS' : 'FAIL'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="report-summary">
|
||||
{audit.data.total_checks_pass}/{audit.data.total_checks} checks passed
|
||||
</div>
|
||||
<div class="report-progress">
|
||||
<div class="progress-bar-small">
|
||||
<div class="progress-fill-small" style="width: {audit.data.percent_checks_pass}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg class="view-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 18l6-6-6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if activeTab === 'bonsai'}
|
||||
{#if !Bonsai.enabled}
|
||||
<div class="empty-state">
|
||||
<svg style="height: 48px;" 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">
|
||||
<defs><linearGradient id="bonsai-grad" x1="319.66" x2="414.22" y1="725.95" y2="631.1" gradientTransform="matrix(.34384 0 0 .34384 1065.6 -23.668)" gradientUnits="userSpaceOnUse"><stop stop-color="currentColor" offset="0" /><stop stop-color="currentColor" offset="1" /></linearGradient></defs>
|
||||
<g transform="translate(-1274.3 -68)"><g transform="matrix(.89066 0 0 .89066 230.47 -102.44)" clip-rule="evenodd"><path d="m1177.3 192.49c-1.0334-2e-5 -1.8713 0.83759-1.8715 1.871v29.941c0 1.0336 0.8379 1.8716 1.8715 1.8715h19.461c0.4963-1.1e-4 0.9723-0.19737 1.3231-0.54839l7.8162-7.8161c0.7306-0.73081 0.7306-1.9154 0-2.6462l-5.0282-5.0282-2.3818 2.3818 3.9696 3.9696-6.3191 6.3191h-17.344v-26.946l17.321-0.0212 6.3429 6.3581-12.703 12.703-5.5574-5.5574 5.5574-5.5574 4.7635 4.7635 2.3818-2.3818-5.8217-5.8221c-0.7286-0.72842-1.9168-0.72974-2.6467 0l-7.6763 7.6763c-0.706 0.70636-0.733 1.8426-0.061 2.5817l7.7091 7.7091c0.7309 0.80387 1.9905 0.81846 2.7398 0.0317l14.796-14.864c0.7006-0.73553 0.6866-1.8956-0.032-2.614l-7.8253-7.8253c-0.351-0.35081-0.8269-0.54787-1.3231-0.54785z" fill="currentColor" fill-rule="evenodd"/><rect transform="matrix(.14035 0 0 .14035 1172 191.36)" x="-8.2421e-7" y="-1.0518e-16" width="256" height="256" fill="none"/></g></g>
|
||||
</svg>
|
||||
<p>Bonsai Integration disabled</p>
|
||||
<p class="help-text">Please run the app from within Bonsai</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="section">
|
||||
<h3>Connection Status</h3>
|
||||
<div class="connection-status {Bonsai.connected ? 'connected' : 'disconnected'}">
|
||||
<div class="status-indicator"></div>
|
||||
<span class="status-text">
|
||||
{Bonsai.connected ? 'Connected' : 'Not Connected'}
|
||||
</span>
|
||||
{#if Bonsai.port}
|
||||
<span class="status-details">Port: {Bonsai.port}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button class="load-btn" onclick={Bonsai.connected ? disconnect : connect}>
|
||||
{#if Bonsai.connected}
|
||||
<XIcon size={18} />
|
||||
{:else}
|
||||
<LinkIcon size={18} />
|
||||
{/if}
|
||||
{Bonsai.connected ? 'Disconnect' : 'Connect'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if Bonsai.connected}
|
||||
<div class="section">
|
||||
<button class="audit-btn" onclick={handleBonsaiAudit} disabled={Bonsai.auditing || !IDS.Module.activeDocument}>
|
||||
{#if Bonsai.auditing}
|
||||
<svg class="spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12a9 9 0 11-6.219-8.56"/>
|
||||
</svg>
|
||||
Running Bonsai Audit...
|
||||
{:else}
|
||||
<svg style="height: 18px; width: 18px;" 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">
|
||||
<defs><linearGradient id="bonsai-audit-grad" x1="319.66" x2="414.22" y1="725.95" y2="631.1" gradientTransform="matrix(.34384 0 0 .34384 1065.6 -23.668)" gradientUnits="userSpaceOnUse"><stop stop-color="currentColor" offset="0" /><stop stop-color="currentColor" offset="1" /></linearGradient></defs>
|
||||
<g transform="translate(-1274.3 -68)"><g transform="matrix(.89066 0 0 .89066 230.47 -102.44)" clip-rule="evenodd"><path d="m1177.3 192.49c-1.0334-2e-5 -1.8713 0.83759-1.8715 1.871v29.941c0 1.0336 0.8379 1.8716 1.8715 1.8715h19.461c0.4963-1.1e-4 0.9723-0.19737 1.3231-0.54839l7.8162-7.8161c0.7306-0.73081 0.7306-1.9154 0-2.6462l-5.0282-5.0282-2.3818 2.3818 3.9696 3.9696-6.3191 6.3191h-17.344v-26.946l17.321-0.0212 6.3429 6.3581-12.703 12.703-5.5574-5.5574 5.5574-5.5574 4.7635 4.7635 2.3818-2.3818-5.8217-5.8221c-0.7286-0.72842-1.9168-0.72974-2.6467 0l-7.6763 7.6763c-0.706 0.70636-0.733 1.8426-0.061 2.5817l7.7091 7.7091c0.7309 0.80387 1.9905 0.81846 2.7398 0.0317l14.796-14.864c0.7006-0.73553 0.6866-1.8956-0.032-2.614l-7.8253-7.8253c-0.351-0.35081-0.8269-0.54787-1.3231-0.54785z" fill="currentColor" fill-rule="evenodd"/><rect transform="matrix(.14035 0 0 .14035 1172 191.36)" x="-8.2421e-7" y="-1.0518e-16" width="256" height="256" fill="none"/></g></g>
|
||||
</svg>
|
||||
Run Bonsai Audit
|
||||
{/if}
|
||||
</button>
|
||||
{#if !IDS.Module.activeDocument}
|
||||
<p class="help-text">Create or open an IDS document to enable auditing</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.content-body {
|
||||
padding-top: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #acacac;
|
||||
}
|
||||
|
||||
.load-btn, .audit-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #ffffff12;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.load-btn:hover:not(:disabled), .audit-btn:hover:not(:disabled) {
|
||||
background: #ffffff1a;
|
||||
}
|
||||
|
||||
.load-btn:disabled, .audit-btn:disabled {
|
||||
background: #ffffff0a;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.audit-btn {
|
||||
background: #12613d;
|
||||
}
|
||||
|
||||
.audit-btn:hover:not(:disabled) {
|
||||
background: #197148;
|
||||
}
|
||||
|
||||
.audit-btn:disabled {
|
||||
background: #ffffff24;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.models-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.model-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e5e7eb24;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.model-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.model-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #ffffffd9;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.model-meta {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.unload-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
background: none;
|
||||
color: #6b7280;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.unload-btn:hover {
|
||||
color: #ff7171;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.empty-state p:first-of-type {
|
||||
font-weight: 500;
|
||||
color: #8d8d8d;
|
||||
}
|
||||
|
||||
.audit-reports {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.audit-report-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e5e7eb24;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: none;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.audit-report-item:hover {
|
||||
background: #ffffff0a;
|
||||
border-color: #e5e7eb40;
|
||||
}
|
||||
|
||||
.report-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.report-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #ffffffd9;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.report-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.report-date {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.report-status {
|
||||
padding: 2px 6px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.report-status.pass {
|
||||
border: 1px solid #30dea422;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.report-status.fail {
|
||||
border: 1px solid #ff989863;
|
||||
color: #ff8282;
|
||||
}
|
||||
|
||||
.report-summary {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.view-icon {
|
||||
color: #6b7280;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.report-progress {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-bar-small {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: #ffffff12;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill-small {
|
||||
height: 100%;
|
||||
background: #ffffff4f;
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Minimization and Tab Styles */
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.open-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: none;
|
||||
color: #6b7280;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.open-btn:hover {
|
||||
color: #ffffffd9;
|
||||
background: #ffffff0a;
|
||||
}
|
||||
|
||||
.tb-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: none;
|
||||
color: #6b7280;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tb-btn:hover {
|
||||
background: #ffffff12;
|
||||
color: #ffffffd9;
|
||||
}
|
||||
|
||||
.tb-btn.active {
|
||||
background: #ffffff1a;
|
||||
color: #ffffffd9;
|
||||
}
|
||||
|
||||
.tb-btn.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: #12a05e;
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
/* Bonsai Integration Styles */
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e5e7eb24;
|
||||
border-radius: 0.375rem;
|
||||
background: #ffffff06;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.connection-status.connected .status-indicator {
|
||||
background: #10b981;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.connection-status.disconnected .status-indicator {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #ffffffd9;
|
||||
}
|
||||
|
||||
.status-details {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script>
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
|
||||
let { addFacet } = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger class="btn">
|
||||
Create Facet
|
||||
<svg class="ml-2 h-4 w-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6,9 12,15 18,9"></polyline>
|
||||
</svg>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="w-56">
|
||||
<DropdownMenu.Item onclick={() => addFacet('entity')}>
|
||||
<svg class="mr-2 h-4 w-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="9" cy="9" r="2"></circle>
|
||||
<path d="M21 15.5c-3-3.5-10-3.5-13 0"></path>
|
||||
</svg>
|
||||
Entity
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => addFacet('attribute')}>
|
||||
<svg class="mr-2 h-4 w-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14,2 14,8 20,8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<polyline points="10,9 9,9 8,9"></polyline>
|
||||
</svg>
|
||||
Attribute
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => addFacet('property')}>
|
||||
<svg class="mr-2 h-4 w-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M8 2v4"></path>
|
||||
<path d="M16 2v4"></path>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2"></rect>
|
||||
<path d="M3 10h18"></path>
|
||||
</svg>
|
||||
Property
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => addFacet('material')}>
|
||||
<svg class="mr-2 h-4 w-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
|
||||
<path d="M2 17l10 5 10-5"></path>
|
||||
<path d="M2 12l10 5 10-5"></path>
|
||||
</svg>
|
||||
Material
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => addFacet('classification')}>
|
||||
<svg class="mr-2 h-4 w-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
|
||||
</svg>
|
||||
Classification
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => addFacet('partOf')}>
|
||||
<svg class="mr-2 h-4 w-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M12 1v6m0 6v6"></path>
|
||||
<path d="M5.636 5.636l4.242 4.242m4.242 4.242l4.242 4.242"></path>
|
||||
<path d="M18.364 5.636l-4.242 4.242m-4.242 4.242L5.636 18.364"></path>
|
||||
</svg>
|
||||
Part Of
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import * as IDS from "$src/modules/api/ids.svelte.js";
|
||||
|
||||
function switchDocument(docId) {
|
||||
IDS.Module.activeDocument = docId;
|
||||
}
|
||||
|
||||
function closeDocument(docId) {
|
||||
IDS.deleteDocument(docId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ids-tabs">
|
||||
{#each Object.entries(IDS.Module.documents) as [docId, doc]}
|
||||
<div
|
||||
class="ids-tab"
|
||||
class:active={IDS.Module.activeDocument === docId}
|
||||
onclick={() => switchDocument(docId)}
|
||||
aria-label={doc.info.title || "Untitled"}
|
||||
>
|
||||
<span class="tab-title">{doc.info.title || "Untitled"}</span>
|
||||
<button
|
||||
class="tab-close"
|
||||
onclick={(e) => { e.stopPropagation(); closeDocument(docId); }}
|
||||
aria-label="Close document"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="filler-tab"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,100 @@
|
||||
<div class="splash-screen">
|
||||
<div class="splash-content">
|
||||
<div class="logo-container">
|
||||
<div class="logo"></div>
|
||||
</div>
|
||||
<div class="loading-text">
|
||||
<h1>IfcTester</h1>
|
||||
<p>Initializing environment...</p>
|
||||
</div>
|
||||
<div class="spinner-container">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.splash-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
background: #00000059;
|
||||
}
|
||||
|
||||
.splash-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
text-align: center;
|
||||
background: #2b2d2b;
|
||||
padding: 50px 50px;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0px 1px 5px #00000040;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
background: #ffffff08;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ffffff1a;
|
||||
}
|
||||
|
||||
.logo {
|
||||
background: url(/logo.svg);
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.loading-text h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.loading-text p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: #b0b0b0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #ffffff20;
|
||||
border-top: 3px solid #ffffff80;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"wasm": {
|
||||
"wheel_url": "/worker/bin/ifcopenshell-0.8.3+bb329af-cp313-cp313-emscripten_4_0_9_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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,845 @@
|
||||
@import "./font.css";
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(0.27 0.00 0);
|
||||
--foreground: oklch(0.985 0.002 247.839);
|
||||
--card: oklch(0.21 0.034 264.665);
|
||||
--card-foreground: oklch(0.985 0.002 247.839);
|
||||
--popover: oklch(0.27 0.00 0);
|
||||
--popover-foreground: oklch(0.97 0.00 0);
|
||||
--primary: oklch(0.928 0.006 264.531);
|
||||
--primary-foreground: oklch(0.21 0.034 264.665);
|
||||
--secondary: oklch(0.37 0.00 0);
|
||||
--secondary-foreground: oklch(0.985 0.002 247.839);
|
||||
--muted: oklch(0.37 0.00 0);
|
||||
--muted-foreground: oklch(0.707 0.022 261.325);
|
||||
--accent: oklch(0.37 0.00 0);
|
||||
--accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.034 264.665);
|
||||
--sidebar-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-accent: oklch(0.37 0.00 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||
|
||||
--sv-bg: #2e2e2e;
|
||||
--sv-border: none;
|
||||
--sv-selection-gap: 10px 14px;
|
||||
--sv-min-height: 46px;
|
||||
--sv-dropdown-bg: #373737;
|
||||
--sv-dropdown-shadow: 0 6px 12px #0000002d;
|
||||
--sv-item-wrap-padding: 5px 14px;
|
||||
|
||||
--sv-disabled-bg: #eee;
|
||||
--sv-border-radius: 4px;
|
||||
--sv-control-bg: var(--sv-bg);
|
||||
--sv-item-selected-bg: #626262;
|
||||
--sv-item-btn-color: #ccc;
|
||||
--sv-item-btn-color-hover: #ccc;
|
||||
--sv-item-btn-bg: #626262;
|
||||
--sv-item-btn-bg-hover: #bc6063;
|
||||
--sv-icon-color: #bbb;
|
||||
--sv-icon-color-hover: #ccc;
|
||||
--sv-icon-bg: transparent;
|
||||
--sv-icon-size: 20px;
|
||||
--sv-separator-bg: #626262;
|
||||
--sv-btn-border: 0;
|
||||
--sv-placeholder-color: #ccccd6;
|
||||
--sv-dropdown-offset: 1px;
|
||||
--sv-dropdown-width: auto;
|
||||
--sv-dropdown-height: 320px;
|
||||
--sv-dropdown-active-bg: #ffffff0f;
|
||||
--sv-dropdown-selected-bg: #ffffff0f;
|
||||
--sv-create-kbd-border: 1px solid #626262;
|
||||
--sv-create-kbd-bg: #626262;
|
||||
--sv-create-disabled-bg: #fcbaba;
|
||||
--sv-loader-border: 2px solid #626262;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.app-ribbon {
|
||||
height: 30px;
|
||||
border-top: 1px solid #ffffff1c;
|
||||
padding: 0px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.app-ribbon .status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.app-ribbon .status-indicator.ready {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.app-ribbon .status-indicator.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.app-ribbon .spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #555;
|
||||
border-top: 2px solid #e0e0e0;
|
||||
border-radius: 50%;
|
||||
animation: app-ribbon-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.app-ribbon .status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.app-ribbon .error-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
@keyframes app-ribbon-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
font-family: "DM Sans", "Open Sans", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #111111;
|
||||
color: white;
|
||||
}
|
||||
|
||||
*, *::after, *::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
|
||||
.app-header {
|
||||
height: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
padding: 0px 15px;
|
||||
gap: 15px;
|
||||
|
||||
.logo {
|
||||
background: url(/logo.svg);
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.main-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
background-color: #191919;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
|
||||
.buttons {
|
||||
border-right: 1px solid #ffffff1c;
|
||||
padding: 10px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
gap: 15px;
|
||||
|
||||
.tb-btn {
|
||||
padding: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
transition-duration: 100ms;
|
||||
color: #ffffff69;
|
||||
}
|
||||
}
|
||||
.tb-btn.active {
|
||||
background-color: #ffffff0f;
|
||||
border-radius: 7px;
|
||||
|
||||
svg {
|
||||
color: #ffffffab;
|
||||
}
|
||||
}
|
||||
.tb-btn:hover svg {
|
||||
color: #ffffffab;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
/* display: none; */
|
||||
border-right: 1px solid #ffffff1c;
|
||||
width: 280px;
|
||||
padding: 15px 15px;
|
||||
overflow-y: auto;
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.9rem;
|
||||
color: #ffffffa6;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: auto;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition-duration: 100ms;
|
||||
|
||||
svg {
|
||||
color: #ffffffa6;
|
||||
width: 1.3rem;
|
||||
}
|
||||
}
|
||||
button:hover {
|
||||
background-color: #ffffff2b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 14px;
|
||||
background-color: #177148;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 200ms;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: 0px 1px 3px #00000070;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btn.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.btn:hover:not(:disabled) {
|
||||
background-color: #135c3a;
|
||||
}
|
||||
.btn:disabled {
|
||||
background-color: #115838;
|
||||
color: #ffffff91;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ids-tabs {
|
||||
display: flex;
|
||||
background: #111111;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
|
||||
.ids-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #e0e0e0;
|
||||
transition: all 0.2s;
|
||||
border-radius: 0;
|
||||
border-right: 1px solid #ffffff1c;
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
max-width: 170px;
|
||||
transition: all 200ms;
|
||||
|
||||
.tab-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #a0a0a0;
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
background: #ffffff1f;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
.ids-tab:hover {
|
||||
background-color: #ffffff08;
|
||||
}
|
||||
.ids-tab:hover > .tab-close {
|
||||
opacity: 1;
|
||||
}
|
||||
.ids-tab.active {
|
||||
background: #ffffff0f;
|
||||
border-bottom: 1px solid #239460;
|
||||
|
||||
.tab-close {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.filler-tab {
|
||||
flex: 1;
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ids-builder {
|
||||
display: flex;
|
||||
height: calc(100% - 45px);
|
||||
color: #e0e0e0;
|
||||
|
||||
.ids-sidebar {
|
||||
width: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.9rem;
|
||||
color: #ffffffa6;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.cta-btn {
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 3px 7px 3px 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #a0a0a0;
|
||||
transition: 200ms all;
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 1px solid #ffffff1f;
|
||||
}
|
||||
|
||||
.cta-btn svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.cta-btn:hover {
|
||||
background: #ffffff17;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.specifications-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.spec-item {
|
||||
padding: 9px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
color: #e0e0e0;
|
||||
border-radius: 10px;
|
||||
|
||||
.spec-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.spec-name {
|
||||
flex: 1;
|
||||
font-size: 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.btn-delete:hover {
|
||||
background: #404040;
|
||||
}
|
||||
}
|
||||
.spec-item:hover {
|
||||
background: #ffffff12;
|
||||
}
|
||||
.spec-item.active {
|
||||
background: #ffffff12;
|
||||
}
|
||||
.spec-item:hover .btn-delete {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.main-panel {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
|
||||
.no-document {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: #a0a0a0;
|
||||
gap: 16px;
|
||||
|
||||
.no-document-icon {
|
||||
height: 125px;
|
||||
width: 200px;
|
||||
background: url(/images/no_document.svg);
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.ids-md-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.specification-editor {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.ids-info h2, .specification-editor h2 {
|
||||
margin: 0 0 0px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: #e0e0e0;
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
background: #ffffff12;
|
||||
color: #e0e0e0;
|
||||
width: 100%;
|
||||
}
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #008242;
|
||||
box-shadow: 0 0 0 0.2rem #00824254;
|
||||
}
|
||||
.form-input::placeholder {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.form-ac-input {
|
||||
cursor: text;
|
||||
|
||||
.sv-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button .shortcut {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.sv-control input::placeholder {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.sv-control .is-single {
|
||||
max-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.form-group.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-label input[type="radio"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.spec-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.spec-tabs {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ffffff1c;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
font-weight: 500;
|
||||
color: #b0b0b0;
|
||||
font-size: 14px;
|
||||
transition: all 200ms;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.tab-btn:hover {
|
||||
background: transparent;
|
||||
color: white;
|
||||
}
|
||||
.tab-btn.active {
|
||||
background: #ffffff21;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.restrictions-panel {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.restrictions-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.btn {
|
||||
background-color: transparent;
|
||||
border: 1px solid #ffffff36;
|
||||
}
|
||||
}
|
||||
|
||||
.restrictions-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #b1b1b1;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.restriction-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 6px 12px;
|
||||
background: #3d3d3d87;
|
||||
border: 1px solid #55555596;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-small:hover {
|
||||
background: #555555;
|
||||
}
|
||||
|
||||
.restrictions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.restriction-item {
|
||||
background: #ffffff05;
|
||||
border: 1px solid #5555556e;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.restriction-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
gap: 12px;
|
||||
|
||||
.restriction-type {
|
||||
background: #ffffff1a;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.restriction-name {
|
||||
flex: 1;
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
color: #b1b1b1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
strong {
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
code {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
padding: 5px;
|
||||
border-radius: 50px;
|
||||
border: 1px solid #ffffff26;
|
||||
cursor: pointer;
|
||||
transition: background-color 200ms;
|
||||
}
|
||||
.btn-delete:hover {
|
||||
background: #ffffff12;
|
||||
}
|
||||
}
|
||||
|
||||
.restriction-form {
|
||||
display: grid;
|
||||
/* grid-template-columns: 1fr 1fr; */
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.restriction-form .form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.restriction-form .form-group.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
.main-panel.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.view-mode-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
width: fit-content;
|
||||
border: 1px solid #ffffff1c;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
padding: 5px 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #b0b0b0;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: #10623d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Custom Scrollbars */
|
||||
.scrollbar {
|
||||
scrollbar-color: #ffffff1c transparent;
|
||||
}
|
||||
.scrollbar::-webkit-scrollbar {
|
||||
width: 0.55em;
|
||||
background-color: transparent;
|
||||
}
|
||||
.scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #ffffff1c;
|
||||
border-radius: 50px;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
@font-face {
|
||||
font-family: 'DM Sans';
|
||||
src: url('/fonts/dmsans/DMSans-VariableFont_opsz,wght.ttf') format('truetype');
|
||||
font-weight: 100 1000;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DM Sans';
|
||||
src: url('/fonts/dmsans/DMSans-Italic-VariableFont_opsz,wght.ttf') format('truetype');
|
||||
font-weight: 100 1000;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
|
||||
@@ -0,0 +1,38 @@
|
||||
<script>
|
||||
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";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Portal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute right-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import { cn } from "$lib/utils.js";
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-header"
|
||||
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn("text-lg leading-none", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
import Header from "./dialog-header.svelte";
|
||||
import Overlay from "./dialog-overlay.svelte";
|
||||
import Content from "./dialog-content.svelte";
|
||||
import Description from "./dialog-description.svelte";
|
||||
import Trigger from "./dialog-trigger.svelte";
|
||||
import Close from "./dialog-close.svelte";
|
||||
|
||||
const Root = DialogPrimitive.Root;
|
||||
const Portal = DialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose,
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
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";
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:ref
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
{#if indeterminate}
|
||||
<MinusIcon class="size-4" />
|
||||
{:else}
|
||||
<CheckIcon class={cn("size-4", !checked && "text-transparent")} />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Portal {...portalProps}>
|
||||
<DropdownMenuPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-content"
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-dropdown-menu-content-available-height) origin-(--bits-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md outline-none",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-group-heading"
|
||||
data-inset={inset}
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
|
||||
@@ -0,0 +1,24 @@
|
||||
<script>
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
class={cn(
|
||||
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { cn } from "$lib/utils.js";
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...restProps}
|
||||
/>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<script>
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import CircleIcon from "@lucide/svelte/icons/circle";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
{#if checked}
|
||||
<CircleIcon class="size-2 fill-current" />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.({ checked })}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-separator"
|
||||
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<script>
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronRightIcon class="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
|
||||
@@ -0,0 +1,49 @@
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||
import Content from "./dropdown-menu-content.svelte";
|
||||
import Group from "./dropdown-menu-group.svelte";
|
||||
import Item from "./dropdown-menu-item.svelte";
|
||||
import Label from "./dropdown-menu-label.svelte";
|
||||
import RadioGroup from "./dropdown-menu-radio-group.svelte";
|
||||
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||
import Separator from "./dropdown-menu-separator.svelte";
|
||||
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||
import Trigger from "./dropdown-menu-trigger.svelte";
|
||||
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||
import GroupHeading from "./dropdown-menu-group-heading.svelte";
|
||||
const Sub = DropdownMenuPrimitive.Sub;
|
||||
const Root = DropdownMenuPrimitive.Root;
|
||||
|
||||
export {
|
||||
CheckboxItem,
|
||||
Content,
|
||||
Root as DropdownMenu,
|
||||
CheckboxItem as DropdownMenuCheckboxItem,
|
||||
Content as DropdownMenuContent,
|
||||
Group as DropdownMenuGroup,
|
||||
Item as DropdownMenuItem,
|
||||
Label as DropdownMenuLabel,
|
||||
RadioGroup as DropdownMenuRadioGroup,
|
||||
RadioItem as DropdownMenuRadioItem,
|
||||
Separator as DropdownMenuSeparator,
|
||||
Shortcut as DropdownMenuShortcut,
|
||||
Sub as DropdownMenuSub,
|
||||
SubContent as DropdownMenuSubContent,
|
||||
SubTrigger as DropdownMenuSubTrigger,
|
||||
Trigger as DropdownMenuTrigger,
|
||||
GroupHeading as DropdownMenuGroupHeading,
|
||||
Group,
|
||||
GroupHeading,
|
||||
Item,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioItem,
|
||||
Root,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Sub,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
Trigger,
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||
import Root from "./menubar.svelte";
|
||||
import CheckboxItem from "./menubar-checkbox-item.svelte";
|
||||
import Content from "./menubar-content.svelte";
|
||||
import Item from "./menubar-item.svelte";
|
||||
import Group from "./menubar-group.svelte";
|
||||
import RadioItem from "./menubar-radio-item.svelte";
|
||||
import Separator from "./menubar-separator.svelte";
|
||||
import Shortcut from "./menubar-shortcut.svelte";
|
||||
import SubContent from "./menubar-sub-content.svelte";
|
||||
import SubTrigger from "./menubar-sub-trigger.svelte";
|
||||
import Trigger from "./menubar-trigger.svelte";
|
||||
import Label from "./menubar-label.svelte";
|
||||
import GroupHeading from "./menubar-group-heading.svelte";
|
||||
|
||||
const Menu = MenubarPrimitive.Menu;
|
||||
const Sub = MenubarPrimitive.Sub;
|
||||
const RadioGroup = MenubarPrimitive.RadioGroup;
|
||||
|
||||
export {
|
||||
Root,
|
||||
CheckboxItem,
|
||||
Content,
|
||||
Item,
|
||||
RadioItem,
|
||||
Separator,
|
||||
Shortcut,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
Trigger,
|
||||
Menu,
|
||||
Group,
|
||||
Sub,
|
||||
RadioGroup,
|
||||
Label,
|
||||
GroupHeading,
|
||||
//
|
||||
Root as Menubar,
|
||||
CheckboxItem as MenubarCheckboxItem,
|
||||
Content as MenubarContent,
|
||||
Item as MenubarItem,
|
||||
RadioItem as MenubarRadioItem,
|
||||
Separator as MenubarSeparator,
|
||||
Shortcut as MenubarShortcut,
|
||||
SubContent as MenubarSubContent,
|
||||
SubTrigger as MenubarSubTrigger,
|
||||
Trigger as MenubarTrigger,
|
||||
Menu as MenubarMenu,
|
||||
Group as MenubarGroup,
|
||||
Sub as MenubarSub,
|
||||
RadioGroup as MenubarRadioGroup,
|
||||
Label as MenubarLabel,
|
||||
GroupHeading as MenubarGroupHeading,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
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";
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
bind:ref
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
data-slot="menubar-checkbox-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground rounded-xs outline-hidden relative flex cursor-default select-none items-center gap-2 py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
{#if indeterminate}
|
||||
<MinusIcon class="size-4" />
|
||||
{:else}
|
||||
<CheckIcon class={cn("size-4", !checked && "text-transparent")} />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
{/snippet}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script>
|
||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 8,
|
||||
alignOffset = -4,
|
||||
align = "start",
|
||||
side = "bottom",
|
||||
portalProps,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<MenubarPrimitive.Portal {...portalProps}>
|
||||
<MenubarPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="menubar-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
{alignOffset}
|
||||
{side}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-menubar-content-transform-origin) z-50 min-w-[12rem] overflow-hidden rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</MenubarPrimitive.Portal>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
inset,
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<MenubarPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="menubar-group-heading"
|
||||
data-inset={inset}
|
||||
class={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script>
|
||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<MenubarPrimitive.Group bind:ref data-slot="menubar-group" {...restProps} />
|
||||
@@ -0,0 +1,24 @@
|
||||
<script>
|
||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset = undefined,
|
||||
variant = "default",
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<MenubarPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="menubar-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-base data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { cn } from "$lib/utils.js";
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
inset,
|
||||
children,
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="menubar-label"
|
||||
data-inset={inset}
|
||||
class={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script>
|
||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||
import CircleIcon from "@lucide/svelte/icons/circle";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<MenubarPrimitive.RadioItem
|
||||
bind:ref
|
||||
data-slot="menubar-radio-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground rounded-xs outline-hidden relative flex cursor-default select-none items-center gap-2 py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
{#if checked}
|
||||
<CircleIcon class="size-2 fill-current" />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.({ checked })}
|
||||
{/snippet}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<MenubarPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="menubar-separator"
|
||||
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="menubar-shortcut"
|
||||
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<MenubarPrimitive.SubContent
|
||||
bind:ref
|
||||
data-slot="menubar-sub-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-menubar-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script>
|
||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset = undefined,
|
||||
children,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<MenubarPrimitive.SubTrigger
|
||||
bind:ref
|
||||
data-slot="menubar-sub-trigger"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronRightIcon class="ml-auto size-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<MenubarPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="menubar-trigger"
|
||||
class={cn(
|
||||
"data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden flex select-none items-center rounded-sm px-2 py-1 text-sm font-medium",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<MenubarPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="menubar"
|
||||
class={cn(
|
||||
"flex h-9 items-center gap-1 rounded-md p-1",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Toaster } from "./sonner.svelte";
|
||||
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
import { Toaster as Sonner } from "svelte-sonner";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
let { ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<Sonner
|
||||
theme={mode.current}
|
||||
class="toaster group"
|
||||
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||
import Trigger from "./tooltip-trigger.svelte";
|
||||
import Content from "./tooltip-content.svelte";
|
||||
|
||||
const Root = TooltipPrimitive.Root;
|
||||
const Provider = TooltipPrimitive.Provider;
|
||||
const Portal = TooltipPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Trigger,
|
||||
Content,
|
||||
Provider,
|
||||
Portal,
|
||||
//
|
||||
Root as Tooltip,
|
||||
Content as TooltipContent,
|
||||
Trigger as TooltipTrigger,
|
||||
Provider as TooltipProvider,
|
||||
Portal as TooltipPortal,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<script>
|
||||
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 0,
|
||||
side = "top",
|
||||
children,
|
||||
arrowClasses,
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="tooltip-content"
|
||||
{sideOffset}
|
||||
{side}
|
||||
class={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-tooltip-content-transform-origin) z-50 w-fit text-balance rounded-md px-3 py-1.5 text-xs",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<TooltipPrimitive.Arrow>
|
||||
{#snippet child({ props })}
|
||||
<div
|
||||
class={cn(
|
||||
"bg-primary z-50 size-2.5 rotate-45 rounded-[2px]",
|
||||
"data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]",
|
||||
"data-[side=bottom]:-translate-y-[calc(-50%_+_1px)] data-[side=bottom]:translate-x-1/2",
|
||||
"data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2",
|
||||
"data-[side=left]:translate-y-[calc(50%_-_3px)]",
|
||||
arrowClasses
|
||||
)}
|
||||
{...props}
|
||||
></div>
|
||||
{/snippet}
|
||||
</TooltipPrimitive.Arrow>
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} />
|
||||
@@ -0,0 +1,8 @@
|
||||
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
|
||||
@@ -0,0 +1,9 @@
|
||||
import { mount } from 'svelte';
|
||||
import './css/app.css';
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('root'),
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,300 @@
|
||||
import wasm from "$src/modules/wasm";
|
||||
import * as IDS from "$src/modules/api/ids.svelte.js";
|
||||
import hyperid from "hyperid";
|
||||
|
||||
export let Autocompletions = $state({
|
||||
entityClasses: [],
|
||||
materialCategories: [],
|
||||
classificationSystems: {},
|
||||
dataTypes: [],
|
||||
isLoaded: false
|
||||
});
|
||||
|
||||
export let IFCModels = $state({
|
||||
models: [],
|
||||
isLoading: false,
|
||||
audits: []
|
||||
});
|
||||
|
||||
const id = hyperid();
|
||||
|
||||
// Preload autocompletions on initialization
|
||||
wasm.init().then(async () => {
|
||||
await preloadAutocompletions();
|
||||
});
|
||||
|
||||
export async function preloadAutocompletions() {
|
||||
try {
|
||||
const schemas = ["IFC2X3", "IFC4"]; // TODO: IFC4X3 is excluded for now because of an error
|
||||
|
||||
// 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));
|
||||
});
|
||||
|
||||
// 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));
|
||||
});
|
||||
|
||||
// Material categories and Classification systems
|
||||
const [materialCategories, classificationSystems] = await Promise.all([
|
||||
wasm.getMaterialCategories(),
|
||||
wasm.getStandardClassificationSystems()
|
||||
]);
|
||||
|
||||
// Cache autocompletions
|
||||
Autocompletions.entityClasses = Array.from(allEntities).sort();
|
||||
Autocompletions.materialCategories = materialCategories;
|
||||
Autocompletions.classificationSystems = classificationSystems;
|
||||
Autocompletions.dataTypes = Array.from(allDataTypes).sort();
|
||||
Autocompletions.isLoaded = true;
|
||||
|
||||
console.log('Autocompletions preloaded');
|
||||
} catch (error) {
|
||||
console.error('Failed to preload autocompletions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPredefinedTypes(schema, entity) {
|
||||
return await wasm.getPredefinedTypes(schema, entity);
|
||||
}
|
||||
|
||||
export async function getEntityAttributes(schema, entity) {
|
||||
return await wasm.getEntityAttributes(schema, entity);
|
||||
}
|
||||
|
||||
export async function getApplicablePsets(schema, entity, predefinedType = '') {
|
||||
return await wasm.getApplicablePsets(schema, entity, predefinedType);
|
||||
}
|
||||
|
||||
export function getEntityClasses() {
|
||||
return Autocompletions.entityClasses;
|
||||
}
|
||||
|
||||
export function getMaterialCategories() {
|
||||
return Autocompletions.materialCategories;
|
||||
}
|
||||
|
||||
export function getClassificationSystems() {
|
||||
return Autocompletions.classificationSystems;
|
||||
}
|
||||
|
||||
export function getDataTypes() {
|
||||
return Autocompletions.dataTypes;
|
||||
}
|
||||
|
||||
export async function loadIfc(file) {
|
||||
try {
|
||||
IFCModels.isLoading = true;
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
|
||||
// Load IFC model
|
||||
const ifcId = await wasm.loadIfc(Array.from(uint8Array));
|
||||
|
||||
// Add to models list
|
||||
const model = {
|
||||
id: ifcId,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
loadedAt: new Date()
|
||||
};
|
||||
IFCModels.models = [...IFCModels.models, model];
|
||||
|
||||
console.log(`IFC model "${file.name}" loaded with ID: ${ifcId}`);
|
||||
return model;
|
||||
} catch (error) {
|
||||
console.error('Failed to load IFC model:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
IFCModels.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function unloadIfc(modelId) {
|
||||
try {
|
||||
// Unload model
|
||||
await wasm.unloadIfc(modelId);
|
||||
|
||||
// Remove from models list
|
||||
IFCModels.models = IFCModels.models.filter(model => model.id !== modelId);
|
||||
|
||||
console.log(`IFC model with ID ${modelId} unloaded`);
|
||||
} catch (error) {
|
||||
console.error('Failed to unload IFC model:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function auditIfc(modelId, idsData) {
|
||||
try {
|
||||
let idsBytes;
|
||||
if (typeof idsData === 'string') {
|
||||
idsBytes = new TextEncoder().encode(idsData);
|
||||
} else if (idsData instanceof ArrayBuffer) {
|
||||
idsBytes = new Uint8Array(idsData);
|
||||
} else {
|
||||
idsBytes = idsData;
|
||||
}
|
||||
|
||||
// Run audit
|
||||
const auditResult = await wasm.auditIfc(modelId, idsBytes);
|
||||
|
||||
console.log(`Audit completed for model ${modelId}`);
|
||||
return auditResult;
|
||||
} catch (error) {
|
||||
console.error('Failed to audit IFC model:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLoadedModels() {
|
||||
return IFCModels.models;
|
||||
}
|
||||
|
||||
export async function openIfc() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = '.ifc';
|
||||
|
||||
fileInput.onchange = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) {
|
||||
reject(new Error('No file selected'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's an IFC file
|
||||
if (!file.name.toLowerCase().endsWith('.ifc')) {
|
||||
reject(new Error('Please select a valid IFC file (.ifc)'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await loadIfc(file);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.onerror = () => reject(new Error('Failed to open file picker'));
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
export function getIfcById(modelId) {
|
||||
return IFCModels.models.find(model => model.id === modelId);
|
||||
}
|
||||
|
||||
export function createAuditReport(modelId, document, auditData, htmlReport = null) {
|
||||
const model = getIfcById(modelId);
|
||||
if (!model) return;
|
||||
|
||||
const auditReport = {
|
||||
id: id(),
|
||||
modelId: modelId,
|
||||
modelName: model.fileName,
|
||||
document: document,
|
||||
date: new Date().toISOString(),
|
||||
data: auditData,
|
||||
htmlReport: htmlReport
|
||||
};
|
||||
|
||||
IFCModels.audits.unshift(auditReport);
|
||||
return auditReport;
|
||||
}
|
||||
|
||||
export function getAuditReportsForIfc(modelId) {
|
||||
return IFCModels.audits.filter(audit => audit.modelId === modelId);
|
||||
}
|
||||
|
||||
export function getAuditReportById(auditId) {
|
||||
return IFCModels.audits.find(audit => audit.id === auditId);
|
||||
}
|
||||
|
||||
export function clearIdsAuditReports(document) {
|
||||
IFCModels.audits = IFCModels.audits.filter(audit => audit.document !== document);
|
||||
}
|
||||
|
||||
export async function downloadAuditReport(auditId) {
|
||||
const audit = getAuditReportById(auditId);
|
||||
if (!audit || !audit.htmlReport) {
|
||||
throw new Error('HTML report not available for this audit');
|
||||
}
|
||||
|
||||
// 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 title = doc.info?.title || 'untitled';
|
||||
filename = `report_${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.html`;
|
||||
}
|
||||
|
||||
const blob = new Blob([audit.htmlReport], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.style.display = 'none';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export async function runAudit() {
|
||||
if (IFCModels.models.length === 0) {
|
||||
throw new Error('Please load an IFC model first');
|
||||
}
|
||||
|
||||
if (!IDS.Module.activeDocument) {
|
||||
throw new Error('Please create or open an IDS document first');
|
||||
}
|
||||
|
||||
// Clear previous audit reports
|
||||
IFCModels.audits = [];
|
||||
|
||||
// Get the active IDS document XML
|
||||
const idsXml = await IDS.exportActiveDocument();
|
||||
|
||||
// Run audit on all loaded models
|
||||
let firstAuditReport = null;
|
||||
for (const model of IFCModels.models) {
|
||||
const result = await auditIfc(model.id, idsXml);
|
||||
|
||||
// Extract JSON and HTML reports from the result
|
||||
const jsonData = result.json || null;
|
||||
const htmlReport = result.html || null;
|
||||
|
||||
const auditReport = createAuditReport(model.id, IDS.Module.activeDocument, jsonData, htmlReport);
|
||||
|
||||
// Store the first audit report to open in viewer
|
||||
if (!firstAuditReport) {
|
||||
firstAuditReport = auditReport;
|
||||
}
|
||||
}
|
||||
|
||||
// Switch to viewer mode and set the first audit report as active
|
||||
if (firstAuditReport && IDS.Module.activeDocument) {
|
||||
IDS.setDocumentState(IDS.Module.activeDocument, {
|
||||
viewMode: 'viewer',
|
||||
auditReport: firstAuditReport.id
|
||||
});
|
||||
}
|
||||
|
||||
return firstAuditReport;
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
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 hyperid from 'hyperid';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// Bonsai connection state
|
||||
export let Bonsai = $state({
|
||||
enabled: false,
|
||||
port: null,
|
||||
socket: null,
|
||||
connected: false,
|
||||
auditing: false
|
||||
});
|
||||
|
||||
const id = hyperid();
|
||||
const pendingAudits = new Map();
|
||||
|
||||
// Check for Bonsai server port in URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const serverPort = urlParams.get('bonsai_server');
|
||||
|
||||
if (serverPort) {
|
||||
Bonsai.enabled = true;
|
||||
Bonsai.port = serverPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Bonsai server
|
||||
*/
|
||||
export const connect = () => new Promise((resolve, reject) => {
|
||||
if (!Bonsai.port) return;
|
||||
|
||||
try {
|
||||
Bonsai.socket = io(`ws://127.0.0.1:${Bonsai.port}/ifctester`, {
|
||||
transports: ['websocket'],
|
||||
reconnection: false,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
Bonsai.socket.on('connect', () => {
|
||||
Bonsai.connected = true;
|
||||
success('Connected to Bonsai');
|
||||
resolve();
|
||||
});
|
||||
|
||||
Bonsai.socket.on('disconnect', () => {
|
||||
Bonsai.connected = false;
|
||||
});
|
||||
|
||||
Bonsai.socket.on('connect_error', (err) => {
|
||||
Bonsai.connected = false;
|
||||
error(`Failed to connect to Bonsai: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
Bonsai.socket.on('audit_result', handleAuditResult);
|
||||
Bonsai.socket.on('error', handleAuditError);
|
||||
|
||||
} catch (err) {
|
||||
error(`Failed to connect to Bonsai: ${err.message}`);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Disconnect from Bonsai server
|
||||
*/
|
||||
export const disconnect = () => {
|
||||
if (Bonsai.socket) {
|
||||
Bonsai.socket.disconnect();
|
||||
Bonsai.socket = null;
|
||||
Bonsai.connected = false;
|
||||
success('Disconnected from Bonsai');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Run audit using current IDS document against Bonsai's IFC model
|
||||
* @returns {Promise<string|null>} Returns audit ID when completed, null if failed
|
||||
*/
|
||||
export const runAudit = async () => {
|
||||
if (!Bonsai.socket || !Bonsai.connected || !IDS.Module.activeDocument) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Bonsai.auditing = true;
|
||||
|
||||
const activeDoc = IDS.Module.documents[IDS.Module.activeDocument];
|
||||
if (!activeDoc) throw new Error('No active IDS document');
|
||||
|
||||
// Convert IDS document to XML string
|
||||
const idsXml = await IDS.exportActiveDocument();
|
||||
|
||||
const requestId = id();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Store request with resolve/reject functions
|
||||
pendingAudits.set(requestId, { resolve, reject });
|
||||
|
||||
Bonsai.socket.emit('audit_ids', {
|
||||
id: requestId,
|
||||
ids: idsXml
|
||||
});
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Bonsai.auditing = false;
|
||||
error(`Failed to run Bonsai audit: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles audit results from Bonsai server
|
||||
* @param {Object} data - Audit result data
|
||||
*/
|
||||
const handleAuditResult = (data) => {
|
||||
if (!data.id || !data.json_report) return;
|
||||
|
||||
const pendingAudit = pendingAudits.get(data.id);
|
||||
if (!pendingAudit) {
|
||||
console.warn('[Bonsai] Received response for unknown audit ID:', data.id);
|
||||
return;
|
||||
}
|
||||
|
||||
pendingAudits.delete(data.id);
|
||||
const { resolve } = pendingAudit;
|
||||
|
||||
try {
|
||||
const reportData = JSON.parse(data.json_report);
|
||||
|
||||
const auditReport = {
|
||||
id: data.id,
|
||||
date: new Date().toISOString(),
|
||||
modelName: 'Bonsai IFC Model',
|
||||
document: IDS.Module.activeDocument,
|
||||
data: reportData,
|
||||
htmlReport: data.html_report
|
||||
};
|
||||
|
||||
// Store audit report
|
||||
IFCModels.audits.unshift(auditReport);
|
||||
|
||||
Bonsai.auditing = false;
|
||||
success('Audit completed (Bonsai)');
|
||||
|
||||
// Resolve promise with audit ID
|
||||
resolve(data.id);
|
||||
|
||||
} catch (err) {
|
||||
Bonsai.auditing = false;
|
||||
error(`Failed to process audit result: ${err.message}`);
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles audit errors from Bonsai server
|
||||
* @param {Object} data - Error data
|
||||
*/
|
||||
const handleAuditError = (data) => {
|
||||
if (!data.id) return;
|
||||
|
||||
const pendingAudit = pendingAudits.get(data.id);
|
||||
if (!pendingAudit) {
|
||||
console.warn('[Bonsai] Received error for unknown audit ID:', data.id);
|
||||
return;
|
||||
}
|
||||
|
||||
pendingAudits.delete(data.id);
|
||||
const { resolve } = pendingAudit;
|
||||
|
||||
Bonsai.auditing = false;
|
||||
error(`Audit failed (Bonsai): ${data.error}`);
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
import wasm from "$src/modules/wasm";
|
||||
import { clearIdsAuditReports } from "./api.svelte.js";
|
||||
import hyperid from "hyperid";
|
||||
import {tick} from "svelte";
|
||||
|
||||
export let Module = $state({
|
||||
documents: [],
|
||||
activeDocument: null,
|
||||
status: "loading",
|
||||
states: {}
|
||||
});
|
||||
|
||||
// Initialize module
|
||||
wasm.init().then(() => {
|
||||
Module.status = "ready";
|
||||
}).catch((error) => {
|
||||
Module.status = "error";
|
||||
});
|
||||
|
||||
const id = hyperid()
|
||||
|
||||
export function setDocumentState(docId, updates) {
|
||||
if (!Module.states[docId]) {
|
||||
Module.states[docId] = {
|
||||
activeTab: 'info',
|
||||
viewMode: 'editor',
|
||||
activeSpecification: null
|
||||
};
|
||||
}
|
||||
Object.assign(Module.states[docId], updates);
|
||||
}
|
||||
|
||||
export async function createDocument() {
|
||||
const docId = id();
|
||||
const doc = await wasm.createIDS();
|
||||
|
||||
Module.documents[docId] = doc;
|
||||
|
||||
// Initialize document state
|
||||
setDocumentState(docId, {});
|
||||
|
||||
// Set as active document
|
||||
Module.activeDocument = docId;
|
||||
}
|
||||
|
||||
export async function deleteDocument(id) {
|
||||
// Clear any audit reports generated using this IDS document
|
||||
clearIdsAuditReports(id);
|
||||
|
||||
delete Module.documents[id];
|
||||
delete Module.states[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];
|
||||
} else {
|
||||
Module.activeDocument = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (typeof obj !== 'object' || obj === null) return obj;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(normalizeIdsDict);
|
||||
}
|
||||
|
||||
const result = {};
|
||||
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 = {};
|
||||
|
||||
for (const [restrictionKey, restrictionValue] of Object.entries(restriction)) {
|
||||
if (restrictionKey.startsWith('xs:')) {
|
||||
// Remove xs: prefix from keys
|
||||
const newKey = restrictionKey.replace('xs:', '');
|
||||
newRestriction[newKey] = restrictionValue;
|
||||
} else {
|
||||
newRestriction[restrictionKey] = restrictionValue;
|
||||
}
|
||||
}
|
||||
|
||||
result['restriction'] = newRestriction;
|
||||
} else {
|
||||
result[key] = normalizeIdsDict(value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function openDocument() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = '.ids,.xml';
|
||||
|
||||
fileInput.onchange = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) {
|
||||
reject(new Error('No file selected'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const fileContent = e.target.result;
|
||||
const doc = normalizeIdsDict(await wasm.openIDS(fileContent, false));
|
||||
const docId = id();
|
||||
|
||||
// Add document to list and set as active
|
||||
Module.documents[docId] = doc;
|
||||
|
||||
// Initialize document state and switch to viewer mode
|
||||
setDocumentState(docId, { viewMode: 'viewer' });
|
||||
|
||||
Module.activeDocument = docId;
|
||||
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read IDS file'));
|
||||
reader.readAsText(file);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.oncancel = () => {
|
||||
reject(new Error('File selection cancelled'));
|
||||
};
|
||||
|
||||
// Trigger the file dialog
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
export async function exportActiveDocument() {
|
||||
if (!Module.activeDocument) return null;
|
||||
|
||||
const doc = $state.snapshot(Module.documents[Module.activeDocument]);
|
||||
const xmlString = await wasm.exportIDS(doc);
|
||||
|
||||
return xmlString;
|
||||
}
|
||||
|
||||
export async function exportDocument(docId) {
|
||||
const doc = $state.snapshot(Module.documents[docId]);
|
||||
|
||||
// Validate
|
||||
if (doc.specifications.specification.length < 1) {
|
||||
throw new Error("Please create at least one specification before exporting the document.");
|
||||
}
|
||||
|
||||
const xmlString = await wasm.exportIDS(doc);
|
||||
|
||||
// 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`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export async function createSpecification(docId) {
|
||||
const spec = await wasm.createSpecification();
|
||||
|
||||
// Add specification to document
|
||||
Module.documents[docId].specifications.specification.push(spec);
|
||||
|
||||
// Set as active specification
|
||||
if (Module.activeDocument == docId) {
|
||||
const state = Module.states[docId];
|
||||
state.activeSpecification = Module.documents[docId].specifications.specification.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSpecification(docId, specId) {
|
||||
Module.documents[docId].specifications.specification.splice(specId, 1);
|
||||
|
||||
if (Module.activeDocument == docId) {
|
||||
const state = Module.states[docId];
|
||||
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 });
|
||||
|
||||
// If there are other specifications, set the first one as active
|
||||
if (Module.documents[docId].specifications.specification.length > 0) {
|
||||
setDocumentState(docId, { activeSpecification: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, {});
|
||||
}
|
||||
|
||||
if (!(facet in Module.documents[docId].specifications.specification[specId][clause])) {
|
||||
Module.documents[docId].specifications.specification[specId][clause][facet] = [];
|
||||
}
|
||||
|
||||
Module.documents[docId].specifications.specification[specId][clause][facet].push(facetObj);
|
||||
}
|
||||
|
||||
export async function deleteFacet(docId, specId, clause, facet, facetId) {
|
||||
delete Module.documents[docId].specifications.specification[specId][clause][facet][facetId];
|
||||
}
|
||||
|
||||
export function getSpecUsage(spec) {
|
||||
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';
|
||||
return 'required';
|
||||
};
|
||||
|
||||
// Converts facet to human-readable description
|
||||
export function stringifyFacet(clauseType, facet, facetType, spec) {
|
||||
if (!facet) return "";
|
||||
|
||||
const usage = getSpecUsage(spec);
|
||||
const descriptions = [];
|
||||
|
||||
// Entity facet
|
||||
if (facetType === "entity") {
|
||||
if (clauseType === "applicability") {
|
||||
descriptions.push(`All data where IFC class ${stringifyValue(facet.name)}`);
|
||||
} else {
|
||||
descriptions.push(`Shall be data where IFC class ${stringifyValue(facet.name)}`);
|
||||
}
|
||||
|
||||
if (facet.predefinedType) {
|
||||
descriptions.push(`and type ${stringifyValue(facet.predefinedType)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Attribute facet
|
||||
else if (facetType === "attribute") {
|
||||
if (clauseType === "applicability") {
|
||||
descriptions.push(`All data where attribute ${stringifyValue(facet.name)}`);
|
||||
} else {
|
||||
descriptions.push(`Shall be data where attribute ${stringifyValue(facet.name)}`);
|
||||
}
|
||||
descriptions.push(`and value ${stringifyValue(facet.value)}`);
|
||||
}
|
||||
|
||||
// Property facet
|
||||
else if (facetType === "property") {
|
||||
if (clauseType === "applicability") {
|
||||
descriptions.push(`Elements where property ${stringifyValue(facet.baseName)}`);
|
||||
} else {
|
||||
descriptions.push(`Shall be elements where property ${stringifyValue(facet.baseName)}`);
|
||||
}
|
||||
if (facet.value) {
|
||||
descriptions.push(`and value ${stringifyValue(facet.value)}`);
|
||||
}
|
||||
descriptions.push(`and dataset ${stringifyValue(facet.propertySet)}`);
|
||||
}
|
||||
|
||||
// Classification facet
|
||||
else if (facetType === "classification") {
|
||||
if (clauseType === "applicability") {
|
||||
descriptions.push(`All data where classification system ${stringifyValue(facet.system)}`);
|
||||
} else {
|
||||
descriptions.push(`Shall be data where classification system ${stringifyValue(facet.system)}`);
|
||||
}
|
||||
if (facet.value) {
|
||||
descriptions.push(`and classification ${stringifyValue(facet.value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Material facet
|
||||
else if (facetType === "material") {
|
||||
if (clauseType === "applicability") {
|
||||
descriptions.push(`All data where material ${stringifyValue(facet.value)}`);
|
||||
} else {
|
||||
descriptions.push(`Shall be data where material ${stringifyValue(facet.value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// PartOf facet
|
||||
else if (facetType === "partOf") {
|
||||
if (clauseType === "applicability") {
|
||||
descriptions.push(`An element with an **${facet['@relation']}** relationship`);
|
||||
|
||||
if (facet.name) {
|
||||
descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name)}`);
|
||||
}
|
||||
} else {
|
||||
descriptions.push(`An element shall have an **${facet['@relation']}** relationship`);
|
||||
|
||||
if (facet.name) {
|
||||
descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name)}`);
|
||||
}
|
||||
if (facet.predefinedType) {
|
||||
descriptions.push(`and predefined type ${stringifyValue(facet.predefinedType)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let combined = descriptions.join(" ");
|
||||
|
||||
// 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 (isProhibited)
|
||||
combined = combined.replace("Shall", "Shall not").replace("shall", "shall not");
|
||||
|
||||
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) {
|
||||
if (!value) return "is provided";
|
||||
if (value.simpleValue) return `is **${value.simpleValue}**`;
|
||||
if (value.restriction) return stringifyRestriction(value.restriction);
|
||||
return "";
|
||||
}
|
||||
|
||||
// Converts restriction objects to human-readable strings
|
||||
function stringifyRestriction(restriction) {
|
||||
if (!restriction) return "";
|
||||
|
||||
// Handle enumeration
|
||||
if (restriction.enumeration && Array.isArray(restriction.enumeration)) {
|
||||
const values = restriction.enumeration.map(item => `**${item['@value']}**` || '').filter(v => v);
|
||||
return values.length > 0 ? `is one of ${values.join(", ")}` : "has enumeration restriction";
|
||||
}
|
||||
|
||||
// Handle pattern
|
||||
if (restriction.pattern && Array.isArray(restriction.pattern) && restriction.pattern.length > 0) {
|
||||
const pattern = `\`${restriction.pattern[0]['@value']}\`` || '';
|
||||
return pattern ? `matches pattern ${pattern}` : "has pattern restriction";
|
||||
}
|
||||
|
||||
// Handle length restrictions
|
||||
if (restriction.length && Array.isArray(restriction.length) && restriction.length.length > 0) {
|
||||
const length = `**${restriction.length[0]['@value']}**` || '';
|
||||
return length ? `has length ${length}` : "has length restriction";
|
||||
}
|
||||
|
||||
// Handle range restrictions
|
||||
if (restriction.minInclusive || restriction.maxInclusive ||
|
||||
restriction.minExclusive || restriction.maxExclusive) {
|
||||
const parts = [];
|
||||
if (restriction.minInclusive && restriction.minInclusive.length > 0) {
|
||||
parts.push(`**≥ ${restriction.minInclusive[0]['@value'] || ''}**`);
|
||||
}
|
||||
if (restriction.maxInclusive && restriction.maxInclusive.length > 0) {
|
||||
parts.push(`**≤ ${restriction.maxInclusive[0]['@value'] || ''}**`);
|
||||
}
|
||||
if (restriction.minExclusive && restriction.minExclusive.length > 0) {
|
||||
parts.push(`**> ${restriction.minExclusive[0]['@value'] || ''}**`);
|
||||
}
|
||||
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";
|
||||
}
|
||||
|
||||
// Handle length range restrictions
|
||||
if (restriction.minLength || restriction.maxLength) {
|
||||
const parts = [];
|
||||
if (restriction.minLength && restriction.minLength.length > 0) {
|
||||
parts.push(`**min length ${restriction.minLength[0]['@value'] || ''}**`);
|
||||
}
|
||||
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 "has complex restriction";
|
||||
}
|
||||
|
||||
function renderFacetString(text) {
|
||||
// Convert **text** to <strong>text</strong>
|
||||
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
|
||||
// Convert `text` to <code>text</code>
|
||||
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
return text;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
/**
|
||||
* Show an error toast notification
|
||||
* @param {string} message - The error message to display
|
||||
*/
|
||||
export function error(message) {
|
||||
toast.error(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a success toast notification
|
||||
* @param {string} message - The success message to display
|
||||
*/
|
||||
export function success(message) {
|
||||
toast.success(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an info toast notification
|
||||
* @param {string} message - The info message to display
|
||||
*/
|
||||
export function info(message) {
|
||||
toast.info(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a warning toast notification
|
||||
* @param {string} message - The warning message to display
|
||||
*/
|
||||
export function warning(message) {
|
||||
toast.warning(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a loading toast notification
|
||||
* @param {string} message - The loading message to display
|
||||
* @returns {string} - Toast ID for dismissing later
|
||||
*/
|
||||
export function loading(message) {
|
||||
return toast.loading(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss a specific toast
|
||||
* @param {string} toastId - The toast ID to dismiss
|
||||
*/
|
||||
export function dismiss(toastId) {
|
||||
toast.dismiss(toastId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a promise-based toast that updates based on promise state
|
||||
* @param {Promise} promise - The promise to track
|
||||
* @param {Object} messages - Messages for different states
|
||||
* @param {string} messages.loading - Loading message
|
||||
* @param {string} messages.success - Success message
|
||||
* @param {string} messages.error - Error message
|
||||
*/
|
||||
export function promise(promiseToTrack, messages) {
|
||||
return toast.promise(promiseToTrack, {
|
||||
loading: messages.loading,
|
||||
success: messages.success,
|
||||
error: messages.error,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* WASM module
|
||||
* Exposes an API that abstracts the underlying WASM thread
|
||||
*/
|
||||
|
||||
import hyperid from "hyperid";
|
||||
import EventEmitter from "eventemitter3";
|
||||
|
||||
// Message types
|
||||
export const MessageType = {
|
||||
// Initialize the WASM module
|
||||
INIT: 'init',
|
||||
|
||||
// API call
|
||||
API_CALL: 'api_call',
|
||||
|
||||
// Ready to serve API calls
|
||||
READY: 'ready',
|
||||
|
||||
// API response
|
||||
API_RESPONSE: 'api_response',
|
||||
|
||||
// Error
|
||||
ERROR: 'error',
|
||||
|
||||
// WASM module disposed
|
||||
DISPOSED: 'disposed'
|
||||
};
|
||||
|
||||
class WASMModule extends EventEmitter {
|
||||
id = hyperid();
|
||||
ready = false;
|
||||
worker = null;
|
||||
pendingMessages = new Map();
|
||||
|
||||
async init() {
|
||||
if (this.ready === true) return;
|
||||
else if (this.ready instanceof Promise) return this.ready;
|
||||
|
||||
this.worker = new Worker(new URL('./worker/worker.js', import.meta.url), {type: 'module'});
|
||||
|
||||
this.worker.onmessage = (event) => {
|
||||
this._handleWorkerMessage(event.data);
|
||||
};
|
||||
|
||||
this.worker.onerror = (error) => {
|
||||
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) {
|
||||
console.error('[WASM] Failed to initialize:', error);
|
||||
this.ready = false;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return this.ready;
|
||||
}
|
||||
|
||||
async _sendMessage(type, payload = {}) {
|
||||
if (!this.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({
|
||||
type,
|
||||
payload,
|
||||
id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_handleWorkerMessage({ type, payload, id }) {
|
||||
const pendingMessage = this.pendingMessages.get(id);
|
||||
|
||||
if (!pendingMessage) {
|
||||
console.warn('[WASM] Received response for unknown message ID:', id);
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingMessages.delete(id);
|
||||
const { resolve, reject } = pendingMessage;
|
||||
|
||||
switch (type) {
|
||||
case MessageType.READY:
|
||||
this.emit(MessageType.READY);
|
||||
resolve();
|
||||
break;
|
||||
case MessageType.API_RESPONSE:
|
||||
resolve(payload);
|
||||
break;
|
||||
case MessageType.ERROR:
|
||||
reject(new Error(payload.message));
|
||||
break;
|
||||
default:
|
||||
console.warn('[WASM] Unknown message type:', type);
|
||||
reject(new Error(`Unknown message type: ${type}`));
|
||||
}
|
||||
}
|
||||
|
||||
_rejectPendingMessages(error) {
|
||||
for (const { reject } of this.pendingMessages.values()) {
|
||||
reject(error);
|
||||
}
|
||||
this.pendingMessages.clear();
|
||||
}
|
||||
|
||||
async _apiCall(method, ...args) {
|
||||
if (!this.ready) await this.init();
|
||||
|
||||
const result = await this._sendMessage(MessageType.API_CALL, { method, args });
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entity classes in a given IFC schema
|
||||
*/
|
||||
async getAllEntityClasses(schema) {
|
||||
return this._apiCall('getAllEntityClasses', schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all data types in a given IFC schema
|
||||
*/
|
||||
async getAllDataTypes(schema) {
|
||||
return this._apiCall('getAllDataTypes', schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get predefined types for a given IFC entity
|
||||
*/
|
||||
async getPredefinedTypes(schema, entity) {
|
||||
return this._apiCall('getPredefinedTypes', schema, entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attributes for a given IFC entity
|
||||
*/
|
||||
async getEntityAttributes(schema, entity) {
|
||||
return this._apiCall('getEntityAttributes', schema, entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get applicable property sets for a given IFC entity
|
||||
*/
|
||||
async getApplicablePsets(schema, entity, predefinedType = '') {
|
||||
return this._apiCall('getApplicablePsets', schema, entity, predefinedType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get standard material categories
|
||||
*/
|
||||
async getMaterialCategories() {
|
||||
return this._apiCall('getMaterialCategories');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get standard classification systems
|
||||
*/
|
||||
async getStandardClassificationSystems() {
|
||||
return this._apiCall('getStandardClassificationSystems');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an IFC file. Returns a unique ID for the loaded file.
|
||||
*/
|
||||
async loadIfc(ifcData) {
|
||||
return this._apiCall('loadIfc', ifcData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload an IFC file
|
||||
*/
|
||||
async unloadIfc(ifcId) {
|
||||
return this._apiCall('unloadIfc', ifcId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit a loaded IFC file against IDS specifications
|
||||
*/
|
||||
async auditIfc(ifcId, idsData) {
|
||||
const idsBytes = idsData instanceof ArrayBuffer ? new Uint8Array(idsData) : idsData;
|
||||
|
||||
return this._apiCall('auditIfc', ifcId, Array.from(idsBytes));
|
||||
}
|
||||
|
||||
// IDS API Methods
|
||||
|
||||
/**
|
||||
* Create a new IDS instance
|
||||
*/
|
||||
async createIDS() {
|
||||
return this._apiCall('createIDS');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an existing IDS from XML string
|
||||
*/
|
||||
async openIDS(idsXml, validate = false) {
|
||||
return this._apiCall('openIDS', idsXml, validate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a specification
|
||||
*/
|
||||
async createSpecification(options = {}) {
|
||||
return this._apiCall('createSpecification', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an entity facet
|
||||
*/
|
||||
async createEntityFacet(clause, options = {}) {
|
||||
return this._apiCall('createEntityFacet', clause, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an attribute facet
|
||||
*/
|
||||
async createAttributeFacet(clause, options = {}) {
|
||||
return this._apiCall('createAttributeFacet', clause, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a property facet
|
||||
*/
|
||||
async createPropertyFacet(clause, options = {}) {
|
||||
return this._apiCall('createPropertyFacet', clause, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a material facet
|
||||
*/
|
||||
async createMaterialFacet(clause, options = {}) {
|
||||
return this._apiCall('createMaterialFacet', clause, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a classification facet
|
||||
*/
|
||||
async createClassificationFacet(clause, options = {}) {
|
||||
return this._apiCall('createClassificationFacet', clause, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a part-of facet
|
||||
*/
|
||||
async createPartOfFacet(clause, options = {}) {
|
||||
return this._apiCall('createPartOfFacet', clause, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an IDS object
|
||||
*/
|
||||
async validateIDS(idsObj) {
|
||||
return await this._apiCall('validateIDS', idsObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export IDS instance to XML string
|
||||
*/
|
||||
async exportIDS(idsObj) {
|
||||
return this._apiCall('exportIDS', idsObj);
|
||||
}
|
||||
|
||||
async _cleanupWorker() {
|
||||
return this._apiCall('internal.cleanup', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
async dispose() {
|
||||
if (this.worker) {
|
||||
await this._cleanupWorker();
|
||||
this.worker.terminate();
|
||||
this.worker = null;
|
||||
}
|
||||
this.ready = false;
|
||||
this._rejectPendingMessages(new Error('WASM module disposed'));
|
||||
this.emit(MessageType.DISPOSED);
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
const wasm = new WASMModule();
|
||||
|
||||
export const {
|
||||
init,
|
||||
getAllEntityClasses,
|
||||
getAllDataTypes,
|
||||
getPredefinedTypes,
|
||||
getEntityAttributes,
|
||||
getApplicablePsets,
|
||||
getMaterialCategories,
|
||||
getStandardClassificationSystems,
|
||||
loadIfc,
|
||||
unloadIfc,
|
||||
auditIfc,
|
||||
createIDS,
|
||||
openIDS,
|
||||
createSpecification,
|
||||
createEntityFacet,
|
||||
createAttributeFacet,
|
||||
createPropertyFacet,
|
||||
createMaterialFacet,
|
||||
createClassificationFacet,
|
||||
createPartOfFacet,
|
||||
validateIDS,
|
||||
exportIDS,
|
||||
dispose
|
||||
} = wasm;
|
||||
|
||||
export default wasm;
|
||||
@@ -0,0 +1,145 @@
|
||||
import config from '../../../config.json';
|
||||
import hyperid from 'hyperid';
|
||||
|
||||
let pyodide = null;
|
||||
let id = hyperid();
|
||||
|
||||
let LoadedIFC = new Map();
|
||||
|
||||
export async function init(pdide) {
|
||||
pyodide = pdide;
|
||||
|
||||
// Load Python API bindings
|
||||
await pyodide.runPythonAsync(`
|
||||
from pyodide.http import pyfetch
|
||||
response = await pyfetch("${config.wasm.api_py_url}")
|
||||
with open("api.py", "wb") as f:
|
||||
f.write(await response.bytes())
|
||||
`);
|
||||
}
|
||||
|
||||
export async function getPredefinedTypes(schema, entity) {
|
||||
const result = await pyodide.runPythonAsync(`
|
||||
from api import get_predefined_types_for_entity
|
||||
predef_types = get_predefined_types_for_entity("${schema}", "${entity}")
|
||||
predef_types
|
||||
`);
|
||||
return result.toJs({ dict_converter: Object.fromEntries });
|
||||
}
|
||||
|
||||
export async function getAllEntityClasses(schema) {
|
||||
const result = await pyodide.runPythonAsync(`
|
||||
from api import get_all_entity_classes
|
||||
entities = get_all_entity_classes("${schema}")
|
||||
entities
|
||||
`);
|
||||
return result.toJs({ dict_converter: Object.fromEntries });
|
||||
}
|
||||
|
||||
export async function getAllDataTypes(schema) {
|
||||
const result = await pyodide.runPythonAsync(`
|
||||
from api import get_all_data_types
|
||||
data_types = get_all_data_types("${schema}")
|
||||
data_types
|
||||
`);
|
||||
return result.toJs({ dict_converter: Object.fromEntries });
|
||||
}
|
||||
|
||||
export async function getEntityAttributes(schema, entity) {
|
||||
const result = await pyodide.runPythonAsync(`
|
||||
from api import get_entity_attributes
|
||||
attrs = get_entity_attributes("${schema}", "${entity}")
|
||||
attrs
|
||||
`);
|
||||
return result.toJs({ dict_converter: Object.fromEntries });
|
||||
}
|
||||
|
||||
export async function getApplicablePsets(schema, entity, predefinedType = '') {
|
||||
const result = await pyodide.runPythonAsync(`
|
||||
from api import get_applicable_psets
|
||||
psets = get_applicable_psets("${schema}", "${entity}", "${predefinedType}")
|
||||
psets
|
||||
`);
|
||||
return result.toJs({ dict_converter: Object.fromEntries });
|
||||
}
|
||||
|
||||
export async function getMaterialCategories() {
|
||||
const result = await pyodide.runPythonAsync(`
|
||||
from api import get_material_categories
|
||||
materials = get_material_categories()
|
||||
materials
|
||||
`);
|
||||
return result.toJs({ dict_converter: Object.fromEntries });
|
||||
}
|
||||
|
||||
export async function getStandardClassificationSystems() {
|
||||
const result = await pyodide.runPythonAsync(`
|
||||
from api import get_standard_classification_systems
|
||||
systems = get_standard_classification_systems()
|
||||
systems
|
||||
`);
|
||||
return result.toJs({ dict_converter: Object.fromEntries });
|
||||
}
|
||||
|
||||
export async function loadIfc(ifcData) {
|
||||
const ifc_id = id();
|
||||
const path = `/tmp/${encodeURIComponent(ifc_id)}.ifc`;
|
||||
|
||||
pyodide.FS.writeFile(path, new Uint8Array(ifcData));
|
||||
const ifc = await pyodide.runPythonAsync(`
|
||||
import ifcopenshell
|
||||
|
||||
ifc = ifcopenshell.open("${path}")
|
||||
ifc
|
||||
`);
|
||||
|
||||
LoadedIFC.set(ifc_id, ifc);
|
||||
return ifc_id;
|
||||
}
|
||||
|
||||
export async function unloadIfc(ifcId) {
|
||||
const path = `/tmp/${encodeURIComponent(ifcId)}.ifc`;
|
||||
|
||||
pyodide.FS.unlink(path);
|
||||
LoadedIFC.delete(ifcId);
|
||||
}
|
||||
|
||||
export async function auditIfc(ifcId, idsData) {
|
||||
const reporter = pyodide.pyimport("ifctester.reporter");
|
||||
const api = pyodide.pyimport("api");
|
||||
|
||||
const idsString = new TextDecoder().decode(new Uint8Array(idsData));
|
||||
const specs = api.ids_from_xml_string(idsString, true);
|
||||
const ifc = LoadedIFC.get(ifcId);
|
||||
|
||||
// Run audit
|
||||
specs.validate(ifc);
|
||||
|
||||
// Create report in both HTML and JSON formats
|
||||
let jsonReporter = reporter.Json(specs);
|
||||
jsonReporter.report();
|
||||
const jsonReport = jsonReporter.to_string();
|
||||
|
||||
let htmlReporter = reporter.Html(specs);
|
||||
htmlReporter.report();
|
||||
const htmlReport = htmlReporter.to_string();
|
||||
|
||||
return {
|
||||
json: JSON.parse(jsonReport),
|
||||
html: htmlReport
|
||||
};
|
||||
}
|
||||
|
||||
// Expose interface
|
||||
export const API = {
|
||||
"getPredefinedTypes": getPredefinedTypes,
|
||||
"getAllEntityClasses": getAllEntityClasses,
|
||||
"getAllDataTypes": getAllDataTypes,
|
||||
"getEntityAttributes": getEntityAttributes,
|
||||
"getApplicablePsets": getApplicablePsets,
|
||||
"getMaterialCategories": getMaterialCategories,
|
||||
"getStandardClassificationSystems": getStandardClassificationSystems,
|
||||
"loadIfc": loadIfc,
|
||||
"unloadIfc": unloadIfc,
|
||||
"auditIfc": auditIfc
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* IDS module
|
||||
*/
|
||||
|
||||
let pyodide = null;
|
||||
|
||||
// IDS Python classes
|
||||
let Ids, Specification;
|
||||
let Entity, Attribute, Property, Material, Classification, PartOf;
|
||||
|
||||
export async function init(pdide) {
|
||||
pyodide = pdide;
|
||||
|
||||
await pyodide.loadPackagesFromImports(`
|
||||
import ifctester.ids
|
||||
import ifctester.facet
|
||||
`);
|
||||
|
||||
// Import the core IDS classes
|
||||
Ids = pyodide.pyimport("ifctester.ids").Ids;
|
||||
Specification = pyodide.pyimport("ifctester.ids").Specification;
|
||||
|
||||
// Import facet classes
|
||||
Entity = pyodide.pyimport("ifctester.facet").Entity;
|
||||
Attribute = pyodide.pyimport("ifctester.facet").Attribute;
|
||||
Property = pyodide.pyimport("ifctester.facet").Property;
|
||||
Material = pyodide.pyimport("ifctester.facet").Material;
|
||||
Classification = pyodide.pyimport("ifctester.facet").Classification;
|
||||
PartOf = pyodide.pyimport("ifctester.facet").PartOf;
|
||||
}
|
||||
|
||||
function _idsToInstance(idsObj) {
|
||||
const ids_raw = Ids();
|
||||
return ids_raw.parse(pyodide.toPy(idsObj))
|
||||
}
|
||||
|
||||
export function createIDS() {
|
||||
const ids_raw = Ids()
|
||||
return ids_raw.asdict().toJs({dict_converter: Object.fromEntries});
|
||||
}
|
||||
|
||||
export function openIDS(ids_xml, validate = false) {
|
||||
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) {
|
||||
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
|
||||
|
||||
pyodide.runPython(`
|
||||
import os
|
||||
if os.path.exists("${tempFilename}"):
|
||||
os.remove("${tempFilename}")
|
||||
`);
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
export function exportIDS(idsObj) {
|
||||
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"}) {
|
||||
const spec = Specification.callKwargs({
|
||||
name: name,
|
||||
ifcVersion: ifcVersion,
|
||||
identifier: identifier,
|
||||
description: description,
|
||||
instructions: instructions
|
||||
});
|
||||
spec.set_usage(usage);
|
||||
|
||||
return spec.asdict().toJs({dict_converter: Object.fromEntries});
|
||||
}
|
||||
|
||||
// @instructions
|
||||
export function createEntityFacet(clause, {name = "IFCWALL", predefinedType = null, instructions = null}) {
|
||||
const entity = Entity.callKwargs({
|
||||
name: name,
|
||||
predefinedType: predefinedType,
|
||||
instructions: instructions
|
||||
});
|
||||
return entity.asdict(clause).toJs({dict_converter: Object.fromEntries});
|
||||
}
|
||||
|
||||
// @cardinality, @instructions
|
||||
export function createAttributeFacet(clause, {name = "Name", value = null, cardinality = "required", instructions = null}) {
|
||||
const attribute = Attribute.callKwargs({
|
||||
name: name,
|
||||
value: value,
|
||||
cardinality: cardinality,
|
||||
instructions: instructions
|
||||
});
|
||||
return attribute.asdict(clause).toJs({dict_converter: Object.fromEntries});
|
||||
}
|
||||
|
||||
// @uri, @cardinality, @instructions
|
||||
export function createClassificationFacet(clause, {value = null, system = null, uri = null, cardinality = "required", instructions = null}) {
|
||||
const classification = Classification.callKwargs({
|
||||
value: value,
|
||||
system: system,
|
||||
uri: uri,
|
||||
cardinality: cardinality,
|
||||
instructions: instructions
|
||||
});
|
||||
return classification.asdict(clause).toJs({dict_converter: Object.fromEntries});
|
||||
}
|
||||
|
||||
// @relation, @cardinality, @instructions
|
||||
export function createPartOfFacet(clause, {name = "IFCWALL", predefinedType = null, relation = null, cardinality = "required", instructions = null}) {
|
||||
const part_of = PartOf.callKwargs({
|
||||
name: name,
|
||||
predefinedType: predefinedType,
|
||||
relation: relation,
|
||||
cardinality: cardinality,
|
||||
instructions: instructions
|
||||
});
|
||||
return part_of.asdict(clause).toJs({dict_converter: Object.fromEntries});
|
||||
}
|
||||
|
||||
// @dataType, @uri, @cardinality, @instructions
|
||||
export function createPropertyFacet(clause, {propertySet = "Property_Set", baseName = "propertyName", value = null, dataType = null, uri = null, cardinality = "required", instructions = null}) {
|
||||
const property = Property.callKwargs({
|
||||
propertySet: propertySet,
|
||||
baseName: baseName,
|
||||
value: value,
|
||||
dataType: dataType,
|
||||
uri: uri,
|
||||
cardinality: cardinality,
|
||||
instructions: instructions
|
||||
});
|
||||
return property.asdict(clause).toJs({dict_converter: Object.fromEntries});
|
||||
}
|
||||
|
||||
// @uri, @cardinality, @instructions
|
||||
export function createMaterialFacet(clause, {value = null, uri = null, cardinality = "required", instructions = null}) {
|
||||
const material = Material.callKwargs({
|
||||
value: value,
|
||||
uri: uri,
|
||||
cardinality: cardinality,
|
||||
instructions: instructions
|
||||
});
|
||||
return material.asdict(clause).toJs({dict_converter: Object.fromEntries});
|
||||
}
|
||||
|
||||
// Helper function to convert date to ISO format string
|
||||
export function formatDate(date) {
|
||||
if (!date) return null;
|
||||
const d = new Date(date);
|
||||
return d.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
// Expose interface
|
||||
export const API = {
|
||||
"createIDS": createIDS,
|
||||
"openIDS": openIDS,
|
||||
"validateIDS": validateIDS,
|
||||
"exportIDS": exportIDS,
|
||||
"createSpecification": createSpecification,
|
||||
"createEntityFacet": createEntityFacet,
|
||||
"createAttributeFacet": createAttributeFacet,
|
||||
"createClassificationFacet": createClassificationFacet,
|
||||
"createPartOfFacet": createPartOfFacet,
|
||||
"createPropertyFacet": createPropertyFacet,
|
||||
"createMaterialFacet": createMaterialFacet,
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* WASM worker
|
||||
*/
|
||||
|
||||
import { MessageType } from '../index';
|
||||
import config from '../../../config.json';
|
||||
import * as IDS from './ids.js';
|
||||
import * as API from './api';
|
||||
|
||||
let pyodide = null;
|
||||
let ready = false;
|
||||
|
||||
self.addEventListener('message', async (event) => {
|
||||
console.log("[worker] Received message:", event.data);
|
||||
const { type, payload, id } = event.data;
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case MessageType.INIT:
|
||||
await initEnvironment();
|
||||
self.postMessage({
|
||||
type: MessageType.READY,
|
||||
payload: { success: true },
|
||||
id
|
||||
});
|
||||
break;
|
||||
|
||||
case MessageType.API_CALL:
|
||||
if (!ready) {
|
||||
throw new Error('[worker] Pyodide not initialized');
|
||||
}
|
||||
const result = await handleApiCall(payload);
|
||||
self.postMessage({
|
||||
type: MessageType.API_RESPONSE,
|
||||
payload: result,
|
||||
id
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`[worker] Unknown message type: ${type}`);
|
||||
}
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
type: MessageType.ERROR,
|
||||
payload: {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
},
|
||||
id
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function initEnvironment() {
|
||||
if (ready) return;
|
||||
|
||||
// Load Pyodide
|
||||
const scriptUrl = new URL('/pyodide/pyodide.mjs', import.meta.url);
|
||||
const { loadPyodide } = await import(scriptUrl.href);
|
||||
pyodide = await loadPyodide({
|
||||
convertNullToNone: true
|
||||
});
|
||||
|
||||
// Load required packages
|
||||
await pyodide.loadPackage('micropip');
|
||||
await pyodide.loadPackage('numpy');
|
||||
|
||||
const micropip = pyodide.pyimport('micropip');
|
||||
|
||||
// Install IfcOpenShell wheel
|
||||
await micropip.install(config.wasm.wheel_url);
|
||||
|
||||
// Install IfcTester dependencies
|
||||
await micropip.install(config.wasm.odfpy_url);
|
||||
await pyodide.loadPackage("shapely");
|
||||
|
||||
// Install IfcTester
|
||||
await micropip.install('ifctester');
|
||||
|
||||
// Initialize IDS and API
|
||||
await API.init(pyodide);
|
||||
await IDS.init(pyodide);
|
||||
|
||||
console.log("[worker] Environment initialized");
|
||||
|
||||
ready = true;
|
||||
}
|
||||
|
||||
async function cleanupEnvironment() {
|
||||
ready = false;
|
||||
pyodide = null;
|
||||
console.log("[worker] Closed environment");
|
||||
}
|
||||
|
||||
async function handleApiCall({ method, args = [] }) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<script>
|
||||
import * as IDS from "$src/modules/api/ids.svelte.js";
|
||||
import FacetEditor from './FacetEditor.svelte';
|
||||
import CreateFacetDropdown from "$src/components/CreateFacetDropdown.svelte";
|
||||
|
||||
let { activeTab } = $props();
|
||||
|
||||
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);
|
||||
|
||||
async function addFacet (facetType) {
|
||||
if (!activeSpecification) return;
|
||||
|
||||
await IDS.createFacet(
|
||||
IDS.Module.activeDocument,
|
||||
documentState.activeSpecification,
|
||||
"applicability",
|
||||
facetType
|
||||
);
|
||||
}
|
||||
|
||||
async function removeFacet(facetType, facetIndex) {
|
||||
if (!activeSpecification) return;
|
||||
|
||||
await IDS.deleteFacet(
|
||||
IDS.Module.activeDocument,
|
||||
documentState.activeSpecification,
|
||||
"applicability",
|
||||
facetType,
|
||||
facetIndex
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="restrictions-panel">
|
||||
<div class="restrictions-header">
|
||||
<h3>Applicability</h3>
|
||||
<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}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script>
|
||||
import RestrictionEditor from './RestrictionEditor.svelte';
|
||||
import {stringifyFacet} from "$src/modules/api/ids.svelte.js";
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
const getSpecialProp = (prop) => {
|
||||
return facet[prop] ?? "";
|
||||
};
|
||||
const setSpecialProp = (prop, value) => {
|
||||
facet[prop] = value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="restriction-item">
|
||||
<div class="restriction-header">
|
||||
<span class="restriction-type">{facetType.toUpperCase()}</span>
|
||||
<span class="restriction-name">{@html stringifyFacet(activeTab, facet, facetType, specification)}</span>
|
||||
<button class="btn-delete" onclick={() => removeFacet(facetType, index)} aria-label="Delete Restriction">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
<div class="restriction-form">
|
||||
{#if facetType === 'entity'}
|
||||
<RestrictionEditor bind:facet={facet} fieldName="name" label="Entity Name" placeholder="e.g., IfcWall" autocomplete="entityName" />
|
||||
<RestrictionEditor bind:facet={facet} fieldName="predefinedType" label="Predefined Type" placeholder="e.g., SOLIDWALL" autocomplete="predefinedType" />
|
||||
{:else if facetType === 'attribute'}
|
||||
<RestrictionEditor bind:facet={facet} fieldName="name" label="Attribute Name" placeholder="e.g., Name" autocomplete="attributeName" />
|
||||
<RestrictionEditor bind:facet={facet} fieldName="value" label="Value" placeholder="Optional value" />
|
||||
{:else if facetType === 'property'}
|
||||
<RestrictionEditor bind:facet={facet} fieldName="propertySet" label="Property Set" placeholder="e.g., Pset_WallCommon" autocomplete="propertySet" />
|
||||
<RestrictionEditor bind:facet={facet} fieldName="baseName" label="Base Name" placeholder="e.g., FireRating" />
|
||||
<RestrictionEditor bind:facet={facet} fieldName="value" label="Value" placeholder="Optional value" />
|
||||
<RestrictionEditor bind:facet={facet} fieldName="@dataType" label="Data Type" placeholder="Optional data type" autocomplete="dataType" isSpecialProp={true} />
|
||||
{:else if facetType === 'material'}
|
||||
<RestrictionEditor bind:facet={facet} fieldName="value" label="Material Value" placeholder="e.g., Concrete" autocomplete="material" />
|
||||
{:else if facetType === 'classification'}
|
||||
<RestrictionEditor bind:facet={facet} fieldName="system" label="System" placeholder="e.g., Uniclass 2015" autocomplete="classificationSystem" />
|
||||
<RestrictionEditor bind:facet={facet} fieldName="value" label="Value" placeholder="e.g., EF_25_10_25" />
|
||||
{:else if facetType === 'partOf'}
|
||||
<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)}>
|
||||
<option value="">Select relation...</option>
|
||||
<option value="IFCRELAGGREGATES">IFCRELAGGREGATES</option>
|
||||
<option value="IFCRELASSIGNSTOGROUP">IFCRELASSIGNSTOGROUP</option>
|
||||
<option value="IFCRELCONTAINEDINSPATIALSTRUCTURE">IFCRELCONTAINEDINSPATIALSTRUCTURE</option>
|
||||
<option value="IFCRELNESTS">IFCRELNESTS</option>
|
||||
<option value="IFCRELVOIDSELEMENT IFCRELFILLSELEMENT">IFCRELVOIDSELEMENT IFCRELFILLSELEMENT</option>
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
{#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)}>
|
||||
<option value="required">Required</option>
|
||||
<option value="optional">Optional</option>
|
||||
<option value="prohibited">Prohibited</option>
|
||||
</select>
|
||||
</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>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script>
|
||||
import * as IDS from "$src/modules/api/ids.svelte.js";
|
||||
|
||||
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null);
|
||||
|
||||
const getProp = (prop) => {
|
||||
return activeDocument?.info[prop] ?? "";
|
||||
};
|
||||
|
||||
const setProp = (prop, value) => {
|
||||
activeDocument.info[prop] = value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="ids-info">
|
||||
<div class="ids-md-header">
|
||||
<h2>IDS Information</h2>
|
||||
</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">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Author</label>
|
||||
<input class="form-input" 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">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Date</label>
|
||||
<input class="form-input" 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>
|
||||
</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">
|
||||
</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">
|
||||
</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">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user