+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/components/AppRibbon.svelte b/src/ifctester/webapp/src/components/AppRibbon.svelte
new file mode 100644
index 0000000000..4f9643637e
--- /dev/null
+++ b/src/ifctester/webapp/src/components/AppRibbon.svelte
@@ -0,0 +1,23 @@
+
+
+
+ {#if Module.status === 'loading'}
+
+
+ Loading IfcOpenShell...
+
+ {:else if Module.status === 'ready'}
+
+
+ Ready
+
+ {:else if Module.status === 'error'}
+
+
+ Error
+
+ {/if}
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/components/AppToolbar.svelte b/src/ifctester/webapp/src/components/AppToolbar.svelte
new file mode 100644
index 0000000000..3552b13969
--- /dev/null
+++ b/src/ifctester/webapp/src/components/AppToolbar.svelte
@@ -0,0 +1,695 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/components/SplashScreen.svelte b/src/ifctester/webapp/src/components/SplashScreen.svelte
new file mode 100644
index 0000000000..476eb9333b
--- /dev/null
+++ b/src/ifctester/webapp/src/components/SplashScreen.svelte
@@ -0,0 +1,100 @@
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-header.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-header.svelte
new file mode 100644
index 0000000000..791dcde5d7
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-header.svelte
@@ -0,0 +1,19 @@
+
+
+
+ {@render children?.()}
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-overlay.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-overlay.svelte
new file mode 100644
index 0000000000..adb8a38e94
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-overlay.svelte
@@ -0,0 +1,20 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-title.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-title.svelte
new file mode 100644
index 0000000000..2d36371d01
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-title.svelte
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-trigger.svelte
new file mode 100644
index 0000000000..7a38ff9e6a
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-trigger.svelte
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/index.js b/src/ifctester/webapp/src/lib/components/ui/dialog/index.js
new file mode 100644
index 0000000000..036ba9420a
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dialog/index.js
@@ -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,
+};
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
new file mode 100644
index 0000000000..fc034efd31
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
@@ -0,0 +1,37 @@
+
+
+
+ {#snippet children({ checked, indeterminate })}
+
+ {#if indeterminate}
+
+ {:else}
+
+ {/if}
+
+ {@render childrenProp?.()}
+ {/snippet}
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
new file mode 100644
index 0000000000..d13c1ad08d
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
@@ -0,0 +1,25 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
new file mode 100644
index 0000000000..89454b26df
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
@@ -0,0 +1,18 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte
new file mode 100644
index 0000000000..4f3421fbe8
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
new file mode 100644
index 0000000000..a06f2099dd
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
@@ -0,0 +1,24 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
new file mode 100644
index 0000000000..e0aceee7d3
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte
new file mode 100644
index 0000000000..009a4dd02d
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte
@@ -0,0 +1,16 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
new file mode 100644
index 0000000000..218db7f78a
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
@@ -0,0 +1,31 @@
+
+
+
+ {#snippet children({ checked })}
+
+ {#if checked}
+
+ {/if}
+
+ {@render childrenProp?.({ checked })}
+ {/snippet}
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
new file mode 100644
index 0000000000..4d02884d17
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
new file mode 100644
index 0000000000..20f2210c5a
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
@@ -0,0 +1,19 @@
+
+
+
+ {@render children?.()}
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
new file mode 100644
index 0000000000..0f3c698fea
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
@@ -0,0 +1,20 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
new file mode 100644
index 0000000000..e4e236e111
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
@@ -0,0 +1,27 @@
+
+
+
+ {@render children?.()}
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte
new file mode 100644
index 0000000000..720d196b77
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/index.js b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/index.js
new file mode 100644
index 0000000000..5d3f473faf
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/index.js
@@ -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,
+};
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/index.js b/src/ifctester/webapp/src/lib/components/ui/menubar/index.js
new file mode 100644
index 0000000000..95c2fa4abf
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/index.js
@@ -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,
+};
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-checkbox-item.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-checkbox-item.svelte
new file mode 100644
index 0000000000..0d6e298b0f
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-checkbox-item.svelte
@@ -0,0 +1,37 @@
+
+
+
+ {#snippet children({ checked, indeterminate })}
+
+ {#if indeterminate}
+
+ {:else}
+
+ {/if}
+
+ {@render childrenProp?.()}
+ {/snippet}
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-content.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-content.svelte
new file mode 100644
index 0000000000..2706fc76e2
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-content.svelte
@@ -0,0 +1,31 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group-heading.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group-heading.svelte
new file mode 100644
index 0000000000..c64dd91534
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group-heading.svelte
@@ -0,0 +1,18 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group.svelte
new file mode 100644
index 0000000000..8acc8b9549
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group.svelte
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-item.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-item.svelte
new file mode 100644
index 0000000000..f4fee4711e
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-item.svelte
@@ -0,0 +1,24 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-label.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-label.svelte
new file mode 100644
index 0000000000..129be16dd5
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-label.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-radio-item.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-radio-item.svelte
new file mode 100644
index 0000000000..c7c122881f
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-radio-item.svelte
@@ -0,0 +1,31 @@
+
+
+
+ {#snippet children({ checked })}
+
+ {#if checked}
+
+ {/if}
+
+ {@render childrenProp?.({ checked })}
+ {/snippet}
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-separator.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-separator.svelte
new file mode 100644
index 0000000000..d32bbab673
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-separator.svelte
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-shortcut.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-shortcut.svelte
new file mode 100644
index 0000000000..7df7a0d118
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-shortcut.svelte
@@ -0,0 +1,19 @@
+
+
+
+ {@render children?.()}
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-content.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-content.svelte
new file mode 100644
index 0000000000..5a79de2c58
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-content.svelte
@@ -0,0 +1,20 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-trigger.svelte
new file mode 100644
index 0000000000..f9fa0461ee
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-trigger.svelte
@@ -0,0 +1,27 @@
+
+
+
+ {@render children?.()}
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-trigger.svelte
new file mode 100644
index 0000000000..c39921a2a1
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-trigger.svelte
@@ -0,0 +1,20 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar.svelte
new file mode 100644
index 0000000000..cddff1b908
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar.svelte
@@ -0,0 +1,20 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/sonner/index.js b/src/ifctester/webapp/src/lib/components/ui/sonner/index.js
new file mode 100644
index 0000000000..1f016b4ece
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/sonner/index.js
@@ -0,0 +1 @@
+export { default as Toaster } from "./sonner.svelte";
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/sonner/sonner.svelte b/src/ifctester/webapp/src/lib/components/ui/sonner/sonner.svelte
new file mode 100644
index 0000000000..91981de7e6
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/sonner/sonner.svelte
@@ -0,0 +1,13 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/tooltip/index.js b/src/ifctester/webapp/src/lib/components/ui/tooltip/index.js
new file mode 100644
index 0000000000..0810f5df1f
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/tooltip/index.js
@@ -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,
+};
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-content.svelte
new file mode 100644
index 0000000000..3c25c65b9a
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-content.svelte
@@ -0,0 +1,45 @@
+
+
+
+
+ {@render children?.()}
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-trigger.svelte
new file mode 100644
index 0000000000..a2885f2cca
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-trigger.svelte
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/lib/utils.js b/src/ifctester/webapp/src/lib/utils.js
new file mode 100644
index 0000000000..f79e2db4c9
--- /dev/null
+++ b/src/ifctester/webapp/src/lib/utils.js
@@ -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
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/main.js b/src/ifctester/webapp/src/main.js
new file mode 100644
index 0000000000..14fb0c3be8
--- /dev/null
+++ b/src/ifctester/webapp/src/main.js
@@ -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;
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/modules/api/api.svelte.js b/src/ifctester/webapp/src/modules/api/api.svelte.js
new file mode 100644
index 0000000000..6b050d5772
--- /dev/null
+++ b/src/ifctester/webapp/src/modules/api/api.svelte.js
@@ -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;
+}
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/modules/api/bonsai.svelte.js b/src/ifctester/webapp/src/modules/api/bonsai.svelte.js
new file mode 100644
index 0000000000..bcc4b8965d
--- /dev/null
+++ b/src/ifctester/webapp/src/modules/api/bonsai.svelte.js
@@ -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} 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);
+};
+
diff --git a/src/ifctester/webapp/src/modules/api/ids.svelte.js b/src/ifctester/webapp/src/modules/api/ids.svelte.js
new file mode 100644
index 0000000000..3d30948d3d
--- /dev/null
+++ b/src/ifctester/webapp/src/modules/api/ids.svelte.js
@@ -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 text
+ text = text.replace(/\*\*([^*]+)\*\*/g, '$1');
+
+ // Convert `text` to text
+ text = text.replace(/`([^`]+)`/g, '$1');
+
+ return text;
+}
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/modules/utils/toast.svelte.js b/src/ifctester/webapp/src/modules/utils/toast.svelte.js
new file mode 100644
index 0000000000..eaf9b373f0
--- /dev/null
+++ b/src/ifctester/webapp/src/modules/utils/toast.svelte.js
@@ -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,
+ });
+}
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/modules/wasm/index.js b/src/ifctester/webapp/src/modules/wasm/index.js
new file mode 100644
index 0000000000..cecb655f73
--- /dev/null
+++ b/src/ifctester/webapp/src/modules/wasm/index.js
@@ -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;
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/modules/wasm/worker/api.js b/src/ifctester/webapp/src/modules/wasm/worker/api.js
new file mode 100644
index 0000000000..d08c17efe2
--- /dev/null
+++ b/src/ifctester/webapp/src/modules/wasm/worker/api.js
@@ -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
+};
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/modules/wasm/worker/ids.js b/src/ifctester/webapp/src/modules/wasm/worker/ids.js
new file mode 100644
index 0000000000..8afc023cec
--- /dev/null
+++ b/src/ifctester/webapp/src/modules/wasm/worker/ids.js
@@ -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,
+};
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/modules/wasm/worker/worker.js b/src/ifctester/webapp/src/modules/wasm/worker/worker.js
new file mode 100644
index 0000000000..6bf47e46fe
--- /dev/null
+++ b/src/ifctester/webapp/src/modules/wasm/worker/worker.js
@@ -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}`);
+ }
+}
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/pages/Home/ApplicabilityPanel.svelte b/src/ifctester/webapp/src/pages/Home/ApplicabilityPanel.svelte
new file mode 100644
index 0000000000..2bf687ecc0
--- /dev/null
+++ b/src/ifctester/webapp/src/pages/Home/ApplicabilityPanel.svelte
@@ -0,0 +1,61 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/pages/Home/FacetEditor.svelte b/src/ifctester/webapp/src/pages/Home/FacetEditor.svelte
new file mode 100644
index 0000000000..4d807625b3
--- /dev/null
+++ b/src/ifctester/webapp/src/pages/Home/FacetEditor.svelte
@@ -0,0 +1,79 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/pages/Home/IdsMetadataEditor.svelte b/src/ifctester/webapp/src/pages/Home/IdsMetadataEditor.svelte
new file mode 100644
index 0000000000..2e9f94dd04
--- /dev/null
+++ b/src/ifctester/webapp/src/pages/Home/IdsMetadataEditor.svelte
@@ -0,0 +1,53 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/pages/Home/IdsViewer.svelte b/src/ifctester/webapp/src/pages/Home/IdsViewer.svelte
new file mode 100644
index 0000000000..e9645e0984
--- /dev/null
+++ b/src/ifctester/webapp/src/pages/Home/IdsViewer.svelte
@@ -0,0 +1,1131 @@
+
+
+
... {reqAuditData.failed_entities.length - 10} more failing elements not shown ...
+
+ {/if}
+
+
+
+
+
+ {/if}
+
+
+ {/if}
+
+ {/each}
+
+ {/if}
+ {/each}
+
+
+
+ {/if}
+
+ {/each}
+ {:else}
+
+
No specifications defined in this IDS document.
+
+ {/if}
+
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/pages/Home/RequirementsPanel.svelte b/src/ifctester/webapp/src/pages/Home/RequirementsPanel.svelte
new file mode 100644
index 0000000000..189d9c7066
--- /dev/null
+++ b/src/ifctester/webapp/src/pages/Home/RequirementsPanel.svelte
@@ -0,0 +1,59 @@
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/pages/Home/RestrictionEditor.svelte b/src/ifctester/webapp/src/pages/Home/RestrictionEditor.svelte
new file mode 100644
index 0000000000..3bc5042065
--- /dev/null
+++ b/src/ifctester/webapp/src/pages/Home/RestrictionEditor.svelte
@@ -0,0 +1,659 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ifctester/webapp/src/pages/Home/SpecificationEditor.svelte b/src/ifctester/webapp/src/pages/Home/SpecificationEditor.svelte
new file mode 100644
index 0000000000..be524ea411
--- /dev/null
+++ b/src/ifctester/webapp/src/pages/Home/SpecificationEditor.svelte
@@ -0,0 +1,95 @@
+
+
+