@ifc-lite/viewer 1.19.0 → 1.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +15 -14
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +8 -0
  4. package/dist/assets/basketViewActivator-CA2CTcVo.js +71 -0
  5. package/dist/assets/{bcf-DOG9_WPX.js → bcf-4K724hw0.js} +18 -18
  6. package/dist/assets/{exporters-BraHBeoi.js → exporters-xbXqEDlO.js} +53 -46
  7. package/dist/assets/ids-2WdONLlu.js +2033 -0
  8. package/dist/assets/index-BXeEKqJG.css +1 -0
  9. package/dist/assets/{index-BOi3BuUI.js → index-D8Epw-e7.js} +48072 -30928
  10. package/dist/assets/{native-bridge-CpBeOPQa.js → native-bridge-DKmx1z95.js} +2 -2
  11. package/dist/assets/{sandbox-Baez7n-t.js → sandbox-tccwm5Bo.js} +547 -529
  12. package/dist/assets/{server-client-BB6cMAXE.js → server-client-LoWPK1N2.js} +1 -1
  13. package/dist/assets/three-CDRZThFA.js +4057 -0
  14. package/dist/assets/{wasm-bridge-CAYCUHbE.js → wasm-bridge-BsJGgPMs.js} +1 -1
  15. package/dist/index.html +8 -7
  16. package/dist/samples/building-architecture.ifc +453 -0
  17. package/dist/samples/hello-wall.ifc +1054 -0
  18. package/dist/samples/infra-bridge.ifc +962 -0
  19. package/package.json +7 -2
  20. package/public/samples/building-architecture.ifc +453 -0
  21. package/public/samples/hello-wall.ifc +1054 -0
  22. package/public/samples/infra-bridge.ifc +962 -0
  23. package/src/App.tsx +37 -3
  24. package/src/components/mcp/HeroScene.tsx +876 -0
  25. package/src/components/mcp/McpLanding.tsx +1318 -0
  26. package/src/components/mcp/McpPlayground.tsx +524 -0
  27. package/src/components/mcp/PlaygroundChat.tsx +1097 -0
  28. package/src/components/mcp/PlaygroundViewer.tsx +815 -0
  29. package/src/components/mcp/README.md +171 -0
  30. package/src/components/mcp/data.ts +659 -0
  31. package/src/components/mcp/playground-dispatcher.ts +1649 -0
  32. package/src/components/mcp/playground-files.ts +107 -0
  33. package/src/components/mcp/playground-uploads.ts +122 -0
  34. package/src/components/mcp/types.ts +65 -0
  35. package/src/components/mcp/use-mcp-page.ts +109 -0
  36. package/src/components/viewer/MainToolbar.tsx +19 -0
  37. package/src/components/viewer/ViewportContainer.tsx +35 -4
  38. package/src/generated/mcp-catalog.json +82 -0
  39. package/vite.config.ts +6 -0
  40. package/dist/assets/basketViewActivator-RZy5c3Td.js +0 -1
  41. package/dist/assets/ids-DQ5jY0E8.js +0 -1
  42. package/dist/assets/index-0XpVr_S5.css +0 -1
@@ -0,0 +1,107 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * playground-files.ts — virtual file store for the playground.
7
+ *
8
+ * Tools that "write a file" (bcf_export, model_save, export_ifc / csv /
9
+ * json) DON'T trigger a browser download — that would be a surprising
10
+ * privacy issue and against the user's explicit "never auto-download"
11
+ * rule. Instead they push the artifact into this store, which a
12
+ * Downloads panel in the playground sidebar lists with a per-row
13
+ * "Download" button. The actual `Blob` → `<a download>` click only
14
+ * happens when the user presses that button.
15
+ */
16
+ import { useEffect, useState } from 'react';
17
+
18
+ export interface PlaygroundFile {
19
+ /** Stable id used by tools to refer back to a written artifact. */
20
+ id: string;
21
+ /** Suggested filename used when the user clicks Download. */
22
+ filename: string;
23
+ /** MIME type for the download Blob. */
24
+ mimeType: string;
25
+ /** Bytes — read once, cheap. */
26
+ size: number;
27
+ /** The data. */
28
+ blob: Blob;
29
+ /** ms since epoch. */
30
+ createdAt: number;
31
+ /** Tool that produced it (`bcf_export`, `model_save`, …). */
32
+ source: string;
33
+ /** Free-form line shown under the filename in the UI. */
34
+ description?: string;
35
+ }
36
+
37
+ class FileStore {
38
+ private files: PlaygroundFile[] = [];
39
+ private listeners = new Set<() => void>();
40
+ private nextId = 1;
41
+
42
+ add(input: Omit<PlaygroundFile, 'id' | 'createdAt'>): PlaygroundFile {
43
+ const file: PlaygroundFile = {
44
+ ...input,
45
+ id: `pg-file-${this.nextId++}`,
46
+ createdAt: Date.now(),
47
+ };
48
+ this.files = [file, ...this.files];
49
+ this.notify();
50
+ return file;
51
+ }
52
+
53
+ list(): PlaygroundFile[] {
54
+ return this.files;
55
+ }
56
+
57
+ remove(id: string): void {
58
+ this.files = this.files.filter((f) => f.id !== id);
59
+ this.notify();
60
+ }
61
+
62
+ clear(): void {
63
+ this.files = [];
64
+ this.notify();
65
+ }
66
+
67
+ /** User-triggered. Synthesises an <a download> click — never called by
68
+ * tool code, only by the explicit Download button. */
69
+ download(id: string): void {
70
+ const file = this.files.find((f) => f.id === id);
71
+ if (!file) return;
72
+ const url = URL.createObjectURL(file.blob);
73
+ const a = document.createElement('a');
74
+ a.href = url;
75
+ a.download = file.filename;
76
+ a.style.display = 'none';
77
+ document.body.appendChild(a);
78
+ a.click();
79
+ a.remove();
80
+ // Revoke after a tick so the browser actually fetched the blob.
81
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
82
+ }
83
+
84
+ subscribe(listener: () => void): () => void {
85
+ this.listeners.add(listener);
86
+ return () => this.listeners.delete(listener);
87
+ }
88
+
89
+ private notify(): void {
90
+ for (const l of this.listeners) l();
91
+ }
92
+ }
93
+
94
+ export const playgroundFiles = new FileStore();
95
+
96
+ /** React hook for components that want to render the file list reactively. */
97
+ export function usePlaygroundFiles(): PlaygroundFile[] {
98
+ const [files, setFiles] = useState<PlaygroundFile[]>(() => playgroundFiles.list());
99
+ useEffect(() => playgroundFiles.subscribe(() => setFiles(playgroundFiles.list())), []);
100
+ return files;
101
+ }
102
+
103
+ export function formatBytes(bytes: number): string {
104
+ if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
105
+ if (bytes >= 1024) return (bytes / 1024).toFixed(0) + ' KB';
106
+ return bytes + ' B';
107
+ }
@@ -0,0 +1,122 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * playground-uploads.ts — virtual file system for INPUTS the user attaches
7
+ * to a chat turn (IDS specs, side IFC files for diff, BCF imports later).
8
+ *
9
+ * Mirrors the playground-files.ts shape but for the other direction: the
10
+ * user drops a `.ids` (or any text file) onto the chat textarea, we cache
11
+ * it here, and tools that take a `*_path` argument (ids_validate,
12
+ * ids_explain) resolve the path through this store BEFORE asking the agent
13
+ * to inline the XML. That lifts the "the playground can't read disk" wart
14
+ * and makes the chat experience continuous.
15
+ */
16
+
17
+ import { useEffect, useState } from 'react';
18
+
19
+ export interface UploadedFile {
20
+ /** The original filename, used as the lookup key (paths are normalised
21
+ * to the basename so the agent can pass `./foo.ids` or just `foo.ids`). */
22
+ name: string;
23
+ /** MIME type as the browser saw it. May be empty for `.ids`. */
24
+ mimeType: string;
25
+ /** Bytes — for sizing the chip and bounding what we accept. */
26
+ size: number;
27
+ /** Text content if the file is text/* — this is the path we use for
28
+ * ids_validate. Binaries store an empty string here and put bytes in
29
+ * `bytes` (future use; v1 only handles text). */
30
+ text: string;
31
+ /** Wall-clock when the user attached it. */
32
+ uploadedAt: number;
33
+ }
34
+
35
+ class UploadStore {
36
+ private uploads: UploadedFile[] = [];
37
+ private listeners = new Set<() => void>();
38
+
39
+ /** Read the file as text and stash it. Returns the entry.
40
+ *
41
+ * We only decode known-text formats — `.bcfzip` and other binaries
42
+ * are zip archives whose .text() decode would chew through tens of
43
+ * megabytes on the main thread for no user benefit (the playground
44
+ * has no read path for binary attachments yet). */
45
+ async add(file: File): Promise<UploadedFile> {
46
+ const name = file.name.split(/[\\/]/).pop() ?? file.name;
47
+ const mimeType = file.type || guessMimeType(name);
48
+ const text = isTextLike(name, mimeType) ? await file.text() : '';
49
+ const entry: UploadedFile = {
50
+ name,
51
+ mimeType,
52
+ size: file.size,
53
+ text,
54
+ uploadedAt: Date.now(),
55
+ };
56
+ // De-dup by basename — re-attaching with the same name overwrites.
57
+ this.uploads = [entry, ...this.uploads.filter((u) => u.name !== name)];
58
+ this.notify();
59
+ return entry;
60
+ }
61
+
62
+ /** Resolve a path-ish string to an upload. Tolerates absolute paths,
63
+ * ./relative, and bare filenames. */
64
+ resolve(pathOrName: string): UploadedFile | null {
65
+ if (!pathOrName) return null;
66
+ const base = pathOrName.split(/[\\/]/).pop() ?? pathOrName;
67
+ return this.uploads.find((u) => u.name === base) ?? null;
68
+ }
69
+
70
+ list(): UploadedFile[] {
71
+ return this.uploads;
72
+ }
73
+
74
+ remove(name: string): void {
75
+ this.uploads = this.uploads.filter((u) => u.name !== name);
76
+ this.notify();
77
+ }
78
+
79
+ clear(): void {
80
+ this.uploads = [];
81
+ this.notify();
82
+ }
83
+
84
+ subscribe(listener: () => void): () => void {
85
+ this.listeners.add(listener);
86
+ return () => this.listeners.delete(listener);
87
+ }
88
+
89
+ private notify(): void {
90
+ for (const l of this.listeners) l();
91
+ }
92
+ }
93
+
94
+ export const playgroundUploads = new UploadStore();
95
+
96
+ export function usePlaygroundUploads(): UploadedFile[] {
97
+ const [uploads, setUploads] = useState<UploadedFile[]>(() => playgroundUploads.list());
98
+ useEffect(() => playgroundUploads.subscribe(() => setUploads(playgroundUploads.list())), []);
99
+ return uploads;
100
+ }
101
+
102
+ /** Whether we should decode this attachment as text. STEP files (.ifc) and
103
+ * IDS specs are text under the hood; .bcfzip / arbitrary octet-stream
104
+ * attachments are zip archives we only ever surface as references. */
105
+ function isTextLike(name: string, mimeType: string): boolean {
106
+ if (mimeType.startsWith('text/')) return true;
107
+ if (mimeType === 'application/json' || mimeType === 'application/xml' || mimeType === 'application/xhtml+xml') return true;
108
+ const ext = name.toLowerCase().split('.').pop();
109
+ return ext === 'ifc' || ext === 'ids' || ext === 'csv' || ext === 'tsv' || ext === 'xml' || ext === 'json' || ext === 'txt' || ext === 'md';
110
+ }
111
+
112
+ function guessMimeType(name: string): string {
113
+ const ext = name.toLowerCase().split('.').pop() ?? '';
114
+ switch (ext) {
115
+ case 'ids': return 'application/xml';
116
+ case 'xml': return 'application/xml';
117
+ case 'json': return 'application/json';
118
+ case 'csv': return 'text/csv';
119
+ case 'txt': return 'text/plain';
120
+ default: return 'application/octet-stream';
121
+ }
122
+ }
@@ -0,0 +1,65 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Shared types for the /mcp landing page variants.
7
+ *
8
+ * The catalog shape mirrors what `node packages/mcp/dist/cli.js --dump-tools`
9
+ * is expected to emit. Until that script lands, the landing pages can fall
10
+ * back to `MOCK_CATALOG` from ./data.ts so we can iterate on visuals.
11
+ */
12
+
13
+ export type ToolScope = 'read' | 'mutate' | 'export';
14
+
15
+ export type ToolCategory =
16
+ | 'Discovery'
17
+ | 'Query'
18
+ | 'Geometry'
19
+ | 'Validation'
20
+ | 'Mutation'
21
+ | 'BCF'
22
+ | 'bSDD'
23
+ | 'Diff'
24
+ | 'Export'
25
+ | 'Viewer';
26
+
27
+ export interface CatalogTool {
28
+ name: string;
29
+ description: string;
30
+ scope: ToolScope;
31
+ category: ToolCategory;
32
+ inputSchema: unknown;
33
+ }
34
+
35
+ export interface McpCatalog {
36
+ generatedAt?: string;
37
+ version?: string;
38
+ tools: CatalogTool[];
39
+ }
40
+
41
+ export type McpClientId = 'claude-desktop' | 'cursor' | 'windsurf' | 'vscode' | 'goose';
42
+
43
+ export interface McpClient {
44
+ id: McpClientId;
45
+ name: string;
46
+ /** Short blurb shown under the button title. */
47
+ blurb: string;
48
+ /** Path to a logo image inside /public, or null if we render a glyph. */
49
+ logo?: string;
50
+ /** Optional deep-link URL scheme prefix (cursor://, windsurf://, vscode:). */
51
+ deepLinkPrefix?: string;
52
+ /** Where the user pastes the JSON snippet. */
53
+ configHint: string;
54
+ }
55
+
56
+ export interface McpRecipe {
57
+ id: string;
58
+ title: string;
59
+ /** The actual prompt the user copies. */
60
+ prompt: string;
61
+ /** Comma-separated tool names this recipe likely fans out to. */
62
+ uses: string[];
63
+ /** Visual category for grouping/coloring. */
64
+ family: 'audit' | 'visualize' | 'validate' | 'author' | 'compare' | 'discover';
65
+ }
@@ -0,0 +1,109 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Tiny utilities shared by every landing-page variant:
7
+ *
8
+ * • useFonts(href) — injects a Google Fonts <link> only while a /mcp page
9
+ * is mounted, so the global app stays unbothered.
10
+ * • useCopyToClipboard() — returns [copy, isJustCopied] for the install
11
+ * snippets and recipe cards.
12
+ * • useDocumentMeta(title, themeColor)
13
+ * — keeps <title> / theme-color in sync per variant.
14
+ *
15
+ * None of these touch React Suspense / global state — they’re plain
16
+ * useEffect plumbing so the variants can be lifted out of the chooser
17
+ * without surprises.
18
+ */
19
+
20
+ import { useEffect, useState } from 'react';
21
+
22
+ /** Inject a stylesheet <link> while this hook is mounted. Idempotent. */
23
+ export function useFonts(...hrefs: string[]): void {
24
+ useEffect(() => {
25
+ const tags: HTMLLinkElement[] = [];
26
+ for (const href of hrefs) {
27
+ const existing = document.head.querySelector(`link[data-mcp-font="${href}"]`);
28
+ if (existing) {
29
+ // Already injected by another variant — refcount via a data attribute.
30
+ const refs = Number(existing.getAttribute('data-refs') ?? '1') + 1;
31
+ existing.setAttribute('data-refs', String(refs));
32
+ tags.push(existing as HTMLLinkElement);
33
+ continue;
34
+ }
35
+ const link = document.createElement('link');
36
+ link.rel = 'stylesheet';
37
+ link.href = href;
38
+ link.setAttribute('data-mcp-font', href);
39
+ link.setAttribute('data-refs', '1');
40
+ document.head.appendChild(link);
41
+ tags.push(link);
42
+ }
43
+ return () => {
44
+ for (const tag of tags) {
45
+ const refs = Number(tag.getAttribute('data-refs') ?? '1') - 1;
46
+ if (refs <= 0) tag.parentNode?.removeChild(tag);
47
+ else tag.setAttribute('data-refs', String(refs));
48
+ }
49
+ };
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, [hrefs.join('|')]);
52
+ }
53
+
54
+ /** Browser-safe clipboard with a 1.4s "just copied" indicator. */
55
+ export function useCopyToClipboard(): {
56
+ copy: (text: string, key?: string) => Promise<void>;
57
+ copiedKey: string | null;
58
+ } {
59
+ const [copiedKey, setCopiedKey] = useState<string | null>(null);
60
+ return {
61
+ copiedKey,
62
+ copy: async (text: string, key?: string) => {
63
+ try {
64
+ await navigator.clipboard.writeText(text);
65
+ setCopiedKey(key ?? text);
66
+ setTimeout(() => setCopiedKey((curr) => (curr === (key ?? text) ? null : curr)), 1400);
67
+ } catch {
68
+ // Older browsers — fall through silently. The install dialogs always
69
+ // show the snippet anyway so the user can manually select+copy.
70
+ }
71
+ },
72
+ };
73
+ }
74
+
75
+ /** Set <title> + theme-color while mounted; restore on unmount. */
76
+ export function useDocumentMeta(title: string, themeColor?: string): void {
77
+ useEffect(() => {
78
+ const prevTitle = document.title;
79
+ document.title = title;
80
+ let prevTheme: string | null = null;
81
+ let prevThemeExisted = false;
82
+ let themeMeta: HTMLMetaElement | null = null;
83
+ if (themeColor) {
84
+ themeMeta = document.querySelector('meta[name="theme-color"]');
85
+ if (themeMeta) {
86
+ // Track existence separately so a meta tag without `content`
87
+ // doesn't permanently keep our temporary color after unmount.
88
+ prevThemeExisted = themeMeta.hasAttribute('content');
89
+ prevTheme = themeMeta.getAttribute('content');
90
+ themeMeta.setAttribute('content', themeColor);
91
+ }
92
+ }
93
+ return () => {
94
+ document.title = prevTitle;
95
+ if (themeMeta) {
96
+ if (prevThemeExisted && prevTheme !== null) themeMeta.setAttribute('content', prevTheme);
97
+ else themeMeta.removeAttribute('content');
98
+ }
99
+ };
100
+ }, [title, themeColor]);
101
+ }
102
+
103
+ /** Smooth-scroll to an in-page anchor; updates the URL hash. */
104
+ export function scrollToAnchor(id: string): void {
105
+ const el = document.getElementById(id);
106
+ if (!el) return;
107
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
108
+ history.replaceState(null, '', `#${id}`);
109
+ }
@@ -24,6 +24,7 @@ import {
24
24
  ArrowRight,
25
25
  Box,
26
26
  HelpCircle,
27
+ Sparkles,
27
28
  Loader2,
28
29
  Camera,
29
30
  Info,
@@ -1333,6 +1334,24 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1333
1334
 
1334
1335
  {/* Right Side Actions */}
1335
1336
  <div className="flex items-center gap-2 ml-2 pl-2 border-l border-zinc-200 dark:border-zinc-700/60">
1337
+ {/* /mcp cross-link — lives in the meta cluster (Settings / Theme /
1338
+ Help) so it shares space with shell-level navigation rather
1339
+ than competing with the modeling tools to its left. */}
1340
+ <Tooltip>
1341
+ <TooltipTrigger asChild>
1342
+ <Button
1343
+ variant="ghost"
1344
+ size="icon"
1345
+ className="rounded-full"
1346
+ onClick={() => navigateToPath('/mcp')}
1347
+ aria-label="Open ifc-lite MCP"
1348
+ >
1349
+ <Sparkles className="!h-[20px] !w-[20px]" />
1350
+ </Button>
1351
+ </TooltipTrigger>
1352
+ <TooltipContent>Drive ifc-lite from any LLM (MCP)</TooltipContent>
1353
+ </Tooltip>
1354
+
1336
1355
  {desktopShell ? (
1337
1356
  <Tooltip>
1338
1357
  <TooltipTrigger asChild>
@@ -22,7 +22,7 @@ import { cacheFileBlobs, formatFileSize, getCachedFile, getRecentFiles, recordRe
22
22
  import { isTauri } from '@/lib/platform';
23
23
  import { toast } from '@/components/ui/toast';
24
24
  import { describeUnsupportedFormat } from '@/hooks/ingest/pointCloudIngest';
25
- import { Upload, MousePointer, Layers, Info, Command, AlertTriangle, ChevronDown, ExternalLink, Plus, Clock3 } from 'lucide-react';
25
+ import { Upload, MousePointer, Layers, Info, Command, AlertTriangle, ChevronDown, ExternalLink, Plus, Clock3, Sparkles, ArrowUpRight } from 'lucide-react';
26
26
  import type { MeshData, CoordinateInfo, GeometryResult, PointCloudAsset } from '@ifc-lite/geometry';
27
27
  import { type IfcDataStore } from '@ifc-lite/parser';
28
28
  import { getEffectiveGeoreference } from '@/lib/geo/effective-georef';
@@ -735,10 +735,18 @@ export function ViewportContainer() {
735
735
  IFClite
736
736
  </h2>
737
737
  <p className="text-zinc-500 dark:text-[#565f89] font-mono text-sm text-center mb-8 border-b border-zinc-200 dark:border-[#3b4261] pb-4 w-full">
738
- High-performance web viewer demo
738
+ IFC toolkit for the open web
739
739
  </p>
740
740
 
741
- {/* Action */}
741
+ {/*
742
+ Two-track action area: a primary "open file" track and a
743
+ secondary "drive with LLM" track sit in mirrored slots — same
744
+ width, same vertical rhythm, each followed by its own caption
745
+ line. Reads as one balanced composition instead of a primary
746
+ CTA + a tacked-on link, while keeping the file-open path
747
+ visually dominant via the filled-on-hover treatment.
748
+ */}
749
+ {/* Track 1 — open / drag */}
742
750
  <button
743
751
  onClick={async () => {
744
752
  if (!webgpu.supported) {
@@ -774,10 +782,33 @@ export function ViewportContainer() {
774
782
  <span>{webgpu.checking ? 'Checking WebGPU...' : webgpu.supported ? 'Open .ifc file' : 'WebGPU Required'}</span>
775
783
  </button>
776
784
 
777
- <p className="mt-3 text-xs font-mono text-zinc-400 dark:text-[#565f89]">
785
+ <p className="mt-2.5 text-[11px] font-mono text-center text-zinc-400 dark:text-[#565f89]">
778
786
  {webgpu.supported ? 'or drag & drop anywhere' : 'file upload disabled'}
779
787
  </p>
780
788
 
789
+ {/* Subtle "or" rule — anchors the symmetry between the two tracks */}
790
+ <div className="mt-5 mb-5 w-full flex items-center gap-3 text-[10px] font-mono uppercase tracking-[0.22em] text-zinc-400 dark:text-[#565f89]">
791
+ <span className="h-px flex-1 bg-zinc-200 dark:bg-[#3b4261]" />
792
+ <span>or</span>
793
+ <span className="h-px flex-1 bg-zinc-200 dark:bg-[#3b4261]" />
794
+ </div>
795
+
796
+ {/* Track 2 — agent / MCP. Compact inline pill, self-centred so
797
+ it reads as a meta-link sibling to the primary file-open
798
+ CTA, not a competing full-width button. */}
799
+ <a
800
+ href="/mcp"
801
+ className="group inline-flex self-center items-center gap-1.5 px-3 py-1.5 font-mono text-[11px] border border-dashed border-zinc-300 dark:border-[#3b4261] text-zinc-500 dark:text-[#7a82a5] hover:border-primary hover:text-primary transition-all cursor-pointer"
802
+ >
803
+ <Sparkles className="h-3 w-3 transition-transform group-hover:-translate-y-0.5" />
804
+ <span>Drive with any LLM</span>
805
+ <ArrowUpRight className="h-2.5 w-2.5 opacity-60 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
806
+ </a>
807
+
808
+ <p className="mt-1.5 text-[10px] font-mono text-center text-zinc-400 dark:text-[#565f89]">
809
+ via MCP · install or try the playground
810
+ </p>
811
+
781
812
  {recentFiles.length > 0 && (
782
813
  <div className="mt-6 w-full border-t border-zinc-200 dark:border-[#3b4261] pt-4">
783
814
  <div className="mb-3 flex items-center gap-2 text-xs font-mono uppercase tracking-[0.2em] text-zinc-400 dark:text-[#565f89]">
@@ -0,0 +1,82 @@
1
+ {
2
+ "generatedAt": "2026-05-03T09:30:00Z",
3
+ "version": "0.1.0",
4
+ "tools": [
5
+ { "name": "model_info", "category": "Discovery", "scope": "read", "description": "Schema, entity counts, units, georeferencing — the at-a-glance summary of a loaded IFC.", "inputSchema": { "type": "object", "properties": { "model_id": { "type": "string" } } } },
6
+ { "name": "model_list", "category": "Discovery", "scope": "read", "description": "List every model loaded in the current MCP session.", "inputSchema": { "type": "object" } },
7
+ { "name": "model_load", "category": "Discovery", "scope": "mutate", "description": "Load an additional .ifc from disk into the federated session.", "inputSchema": { "type": "object", "required": ["file_path"], "properties": { "file_path": { "type": "string" }, "model_id": { "type": "string" } } } },
8
+ { "name": "model_unload", "category": "Discovery", "scope": "mutate", "description": "Drop a model from the registry; frees memory.", "inputSchema": { "type": "object", "required": ["model_id"], "properties": { "model_id": { "type": "string" } } } },
9
+ { "name": "schema_describe", "category": "Discovery", "scope": "read", "description": "Attributes, parent class, and inheritance for any IfcType — useful before mutating.", "inputSchema": { "type": "object", "required": ["type"], "properties": { "type": { "type": "string" }, "include_inherited": { "type": "boolean" } } } },
10
+
11
+ { "name": "query_entities", "category": "Query", "scope": "read", "description": "Type + property filters with pagination. Returns expressId, GlobalId, name, type for each match.", "inputSchema": { "type": "object", "properties": { "type": { "type": "string" }, "limit": { "type": "integer" } } } },
12
+ { "name": "count_entities", "category": "Query", "scope": "read", "description": "Group counts by type, storey, or material — histogram form, not the full set.", "inputSchema": { "type": "object", "properties": { "group_by": { "type": "string", "enum": ["type", "storey", "material"] } } } },
13
+ { "name": "get_entity", "category": "Query", "scope": "read", "description": "Full attributes + property sets for one entity by GlobalId or expressId.", "inputSchema": { "type": "object", "properties": { "global_id": { "type": "string" }, "express_id": { "type": "integer" } } } },
14
+ { "name": "get_entities_bulk", "category": "Query", "scope": "read", "description": "Batched get_entity for up to 200 ids at once.", "inputSchema": { "type": "object", "required": ["global_ids"], "properties": { "global_ids": { "type": "array", "items": { "type": "string" } } } } },
15
+ { "name": "spatial_hierarchy", "category": "Query", "scope": "read", "description": "Project → site → building → storey → space tree, with element counts at each node.", "inputSchema": { "type": "object" } },
16
+ { "name": "containment_chain", "category": "Query", "scope": "read", "description": "Walk up the spatial chain for one entity (storey + parent + grandparent…).", "inputSchema": { "type": "object", "required": ["global_id"] } },
17
+ { "name": "relationships", "category": "Query", "scope": "read", "description": "Voids, fills, groups, connections — every IfcRel touching this entity.", "inputSchema": { "type": "object", "required": ["global_id"] } },
18
+ { "name": "properties_unique", "category": "Query", "scope": "read", "description": "Unique values + counts for one property across a type set (perfect for filter UIs).", "inputSchema": { "type": "object", "required": ["type", "pset", "property"] } },
19
+ { "name": "materials_list", "category": "Query", "scope": "read", "description": "Distinct materials across the model with usage counts.", "inputSchema": { "type": "object" } },
20
+ { "name": "classifications_list","category": "Query", "scope": "read", "description": "Distinct classification references (system + identification) and how often each is used.", "inputSchema": { "type": "object" } },
21
+ { "name": "georeferencing", "category": "Query", "scope": "read", "description": "MapConversion, projected CRS, project north, true north — the geo handshake.", "inputSchema": { "type": "object" } },
22
+ { "name": "units", "category": "Query", "scope": "read", "description": "Length unit scale + the unit assignments declared in the file.", "inputSchema": { "type": "object" } },
23
+
24
+ { "name": "geometry_bbox", "category": "Geometry", "scope": "read", "description": "Per-entity axis-aligned bounding box (read from quantity sets when available).", "inputSchema": { "type": "object", "required": ["global_id"] } },
25
+ { "name": "geometry_volume", "category": "Geometry", "scope": "read", "description": "Net/gross volume in m³ for a single entity or a type aggregate.", "inputSchema": { "type": "object", "required": ["global_id"] } },
26
+ { "name": "geometry_area", "category": "Geometry", "scope": "read", "description": "Surface area for an entity (front/side/footprint depending on what the IFC carries).", "inputSchema": { "type": "object", "required": ["global_id"] } },
27
+
28
+ { "name": "model_audit", "category": "Validation", "scope": "read", "description": "Out-of-the-box health score + a list of issues (missing GlobalIds, broken refs, orphan entities).", "inputSchema": { "type": "object" } },
29
+ { "name": "ids_validate", "category": "Validation", "scope": "read", "description": "Run a buildingSMART IDS spec against the loaded model. Per-spec pass/fail with offending entities.", "inputSchema": { "type": "object", "required": ["ids_path"], "properties": { "ids_path": { "type": "string" } } } },
30
+ { "name": "ids_explain", "category": "Validation", "scope": "read", "description": "Parse + summarize an IDS file in plain language — what each spec asks for, in what order.", "inputSchema": { "type": "object", "required": ["ids_path"] } },
31
+
32
+ { "name": "entity_set_property", "category": "Mutation", "scope": "mutate", "description": "Queue a Pset.property write on one entity. Persist later via export_ifc / model_save.", "inputSchema": { "type": "object", "required": ["pset", "name"] } },
33
+ { "name": "entity_delete_property", "category": "Mutation","scope": "mutate", "description": "Queue a property removal from a Pset. Reversible via mutation_undo.", "inputSchema": { "type": "object", "required": ["pset", "name"] } },
34
+ { "name": "entity_set_attribute","category": "Mutation", "scope": "mutate", "description": "Set Name, Description, ObjectType, or Tag on an entity.", "inputSchema": { "type": "object", "required": ["attribute", "value"] } },
35
+ { "name": "entity_create", "category": "Mutation", "scope": "mutate", "description": "Create a new IFC entity with raw STEP attributes and get back its expressId.", "inputSchema": { "type": "object", "required": ["type"] } },
36
+ { "name": "entity_delete", "category": "Mutation", "scope": "mutate", "description": "Delete an entity by expressId/GlobalId. Caller is responsible for cascading rels.", "inputSchema": { "type": "object" } },
37
+ { "name": "mutation_batch", "category": "Mutation", "scope": "mutate", "description": "Apply N mutation ops in order, returning per-step results.", "inputSchema": { "type": "object", "required": ["operations"] } },
38
+ { "name": "mutation_diff", "category": "Mutation", "scope": "read", "description": "Inspect every queued mutation vs the original parsed state.", "inputSchema": { "type": "object" } },
39
+ { "name": "mutation_undo", "category": "Mutation", "scope": "mutate", "description": "Pop the last N pending mutations off the queue.", "inputSchema": { "type": "object", "properties": { "n": { "type": "integer" } } } },
40
+ { "name": "model_save", "category": "Mutation", "scope": "mutate", "description": "Write the current model (with pending mutations) back to .ifc.", "inputSchema": { "type": "object", "required": ["file_path"] } },
41
+
42
+ { "name": "bcf_topic_list", "category": "BCF", "scope": "read", "description": "List BCF topics in this session, optionally filtered by status.", "inputSchema": { "type": "object" } },
43
+ { "name": "bcf_topic_create", "category": "BCF", "scope": "mutate", "description": "Create a topic with title/description/priority and get the GUID for follow-ups.", "inputSchema": { "type": "object", "required": ["title"] } },
44
+ { "name": "bcf_topic_update", "category": "BCF", "scope": "mutate", "description": "Update topic fields or append a comment.", "inputSchema": { "type": "object", "required": ["guid"] } },
45
+ { "name": "bcf_topic_close", "category": "BCF", "scope": "mutate", "description": "Mark a topic resolved (status=Closed).", "inputSchema": { "type": "object", "required": ["guid"] } },
46
+ { "name": "bcf_viewpoint_create","category": "BCF", "scope": "mutate", "description": "Attach a selection-based viewpoint (or full viewer state) to a topic.", "inputSchema": { "type": "object", "required": ["guid"] } },
47
+ { "name": "bcf_export", "category": "BCF", "scope": "export", "description": "Export the in-memory BCF project as a .bcfzip file.", "inputSchema": { "type": "object", "required": ["file_path"] } },
48
+
49
+ { "name": "bsdd_search", "category": "bSDD", "scope": "read", "description": "Full-text search the buildingSMART Data Dictionary for classes by keyword.", "inputSchema": { "type": "object", "required": ["query"] } },
50
+ { "name": "bsdd_class", "category": "bSDD", "scope": "read", "description": "Full bSDD class info for an IFC entity name (definition, parent, properties).", "inputSchema": { "type": "object", "required": ["ifc_type"] } },
51
+ { "name": "bsdd_property_sets", "category": "bSDD", "scope": "read", "description": "Pset_* groups for an IFC type (Pset_WallCommon for IfcWall, etc.).", "inputSchema": { "type": "object", "required": ["ifc_type"] } },
52
+ { "name": "bsdd_match", "category": "bSDD", "scope": "read", "description": "Suggest matching bSDD classes for an entity in the loaded model.", "inputSchema": { "type": "object" } },
53
+
54
+ { "name": "model_diff", "category": "Diff", "scope": "read", "description": "Compare two loaded models. Reports added/removed entities by GlobalId and per-type count deltas.", "inputSchema": { "type": "object", "required": ["a", "b"] } },
55
+ { "name": "quantity_diff", "category": "Diff", "scope": "read", "description": "Per-entity-type quantity comparison between two models, optionally grouped by storey.", "inputSchema": { "type": "object", "required": ["a", "b"] } },
56
+
57
+ { "name": "export_ifc", "category": "Export", "scope": "export", "description": "Write the model (with pending mutations) to .ifc/.ifczip on disk.", "inputSchema": { "type": "object", "required": ["file_path"] } },
58
+ { "name": "export_csv", "category": "Export", "scope": "export", "description": "Tabular property/quantity export. Columns may be Pset_X.Property paths.", "inputSchema": { "type": "object" } },
59
+ { "name": "export_json", "category": "Export", "scope": "export", "description": "Structured JSON dump of attributes/properties/quantities for a type set.", "inputSchema": { "type": "object" } },
60
+ { "name": "export_glb", "category": "Export", "scope": "export", "description": "(v0.2) Geometry export to glTF binary — needs the WASM mesh pipeline.", "inputSchema": { "type": "object" } },
61
+ { "name": "export_ifcx", "category": "Export", "scope": "export", "description": "(v0.2) Export to the new IFCx interchange format.", "inputSchema": { "type": "object" } },
62
+ { "name": "export_pdf_report", "category": "Export", "scope": "export", "description": "(v0.5) Branded PDF audit report with charts.", "inputSchema": { "type": "object" } },
63
+
64
+ { "name": "viewer_ask", "category": "Viewer", "scope": "read", "description": "Suggest wording the agent can use to ask the user for permission to open the viewer.", "inputSchema": { "type": "object" } },
65
+ { "name": "viewer_open", "category": "Viewer", "scope": "read", "description": "Boot the in-process WebGL viewer and return its URL for the user to open.", "inputSchema": { "type": "object" } },
66
+ { "name": "viewer_close", "category": "Viewer", "scope": "read", "description": "Stop the viewer + clear its selection state.", "inputSchema": { "type": "object" } },
67
+ { "name": "viewer_status", "category": "Viewer", "scope": "read", "description": "Report whether the viewer is open, on what port, and the current selection.", "inputSchema": { "type": "object" } },
68
+ { "name": "viewer_colorize", "category": "Viewer", "scope": "read", "description": "Paint a set of entities with a color (hex / rgb / named).", "inputSchema": { "type": "object", "required": ["color"] } },
69
+ { "name": "viewer_isolate", "category": "Viewer", "scope": "read", "description": "Hide everything except the picked set.", "inputSchema": { "type": "object" } },
70
+ { "name": "viewer_hide", "category": "Viewer", "scope": "read", "description": "Hide the picked set; everything else stays.", "inputSchema": { "type": "object" } },
71
+ { "name": "viewer_show", "category": "Viewer", "scope": "read", "description": "Show the picked set (un-hide).", "inputSchema": { "type": "object" } },
72
+ { "name": "viewer_reset", "category": "Viewer", "scope": "read", "description": "Reset visibility + colors to the model defaults.", "inputSchema": { "type": "object" } },
73
+ { "name": "viewer_fly_to", "category": "Viewer", "scope": "read", "description": "Frame the camera on a set of entities or a bbox.", "inputSchema": { "type": "object" } },
74
+ { "name": "viewer_set_section", "category": "Viewer", "scope": "read", "description": "Apply an axis-aligned section plane.", "inputSchema": { "type": "object", "required": ["axis", "position"] } },
75
+ { "name": "viewer_clear_section","category": "Viewer", "scope": "read", "description": "Remove the active section plane.", "inputSchema": { "type": "object" } },
76
+ { "name": "viewer_color_by_storey","category": "Viewer", "scope": "read", "description": "Apply a per-storey overlay (built-in viewer preset).", "inputSchema": { "type": "object" } },
77
+ { "name": "viewer_color_by_property","category": "Viewer", "scope": "read", "description": "Color a type set by property value, returns a legend the agent can describe.", "inputSchema": { "type": "object", "required": ["type", "pset", "property"] } },
78
+ { "name": "viewer_get_selection","category": "Viewer", "scope": "read", "description": "Return the current selection — type, expressId, GlobalId, name, attributes, materials.", "inputSchema": { "type": "object" } },
79
+ { "name": "viewer_describe_selection","category": "Viewer","scope": "read", "description": "Kitchen-sink: every section (attributes, properties, quantities, classifications, materials) for the picked entity.", "inputSchema": { "type": "object" } },
80
+ { "name": "viewer_wait_for_selection","category": "Viewer","scope": "read", "description": "Block until the user picks something in the viewer (or timeout).", "inputSchema": { "type": "object" } }
81
+ ]
82
+ }
package/vite.config.ts CHANGED
@@ -313,6 +313,12 @@ export default defineConfig({
313
313
  if (id.includes('/node_modules/apache-arrow/')) return 'arrow';
314
314
  if (id.includes('/node_modules/parquet-wasm/')) return 'parquet';
315
315
  if (id.includes('/node_modules/cesium/')) return 'cesium';
316
+ // three.js + addons — only the /mcp landing imports them, keep
317
+ // the main viewer / pages off the hook.
318
+ if (
319
+ id.includes('/node_modules/three/') ||
320
+ id.includes('/node_modules/.pnpm/three@')
321
+ ) return 'three';
316
322
  return undefined;
317
323
  },
318
324
  },
@@ -1 +0,0 @@
1
- import{u as o}from"./index-BOi3BuUI.js";import"./cesium-DUOzBlqv.js";import"./arrow-CZ5kQ26f.js";import"./exporters-BraHBeoi.js";import"./bcf-DOG9_WPX.js";import"./zip-DBEtpeu6.js";import"./sandbox-Baez7n-t.js";import"./lens-CSASnhAL.js";import"./drawing-2d-DoxKMqbO.js";import"./server-client-BB6cMAXE.js";import"./ids-DQ5jY0E8.js";const n=700;function v(s){const e=o.getState(),i=e.basketViews.find(t=>t.id===s);if(i){if(e.setDrawing2D(null),e.setDrawing2DPanelVisible(!1),e.updateDrawing2DDisplayOptions({show3DOverlay:!1}),e.clearEntitySelection(),e.restoreBasketEntities(i.entityRefs,s),i.viewpoint){const t=i.transitionMs??n;e.cameraCallbacks.applyViewpoint?.(i.viewpoint,!0,t)}if(i.section){const t=i.section;o.setState({sectionPlane:{...t.plane},drawing2DPanelVisible:!1}),t.plane.enabled?(e.activeTool!=="section"&&e.setSuppressNextSection2DPanelAutoOpen(!0),e.setActiveTool("section")):e.activeTool==="section"&&e.setActiveTool("select")}else{const t=o.getState().sectionPlane;o.setState({sectionPlane:{...t,enabled:!1}}),e.activeTool==="section"&&e.setActiveTool("select")}}}export{v as activateBasketViewFromStore};