@ifc-lite/viewer 1.8.0 → 1.10.0
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.
- package/CHANGELOG.md +77 -0
- package/dist/assets/{Arrow.dom-CwcRxist.js → Arrow.dom-Bw5JMdDs.js} +1 -1
- package/dist/assets/browser-DdRf3aWl.js +694 -0
- package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
- package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
- package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
- package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
- package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
- package/dist/assets/esbuild-COv63sf-.js +1 -0
- package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
- package/dist/assets/ffi-DlhRHxHv.js +1 -0
- package/dist/assets/ifc-lite_bg-C1-gLAHo.wasm +0 -0
- package/dist/assets/index-1ff6P0kc.js +100011 -0
- package/dist/assets/index-Bz7vHRxl.js +216 -0
- package/dist/assets/index-mvbV6NHd.css +1 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-5LbrYh3R.js → native-bridge-C5hD5vae.js} +1 -1
- package/dist/assets/{wasm-bridge-CgpLtj1h.js → wasm-bridge-CaNKXFGM.js} +1 -1
- package/dist/index.html +12 -3
- package/index.html +10 -1
- package/package.json +30 -21
- package/src/App.tsx +6 -1
- package/src/components/ui/dialog.tsx +8 -6
- package/src/components/viewer/CodeEditor.tsx +309 -0
- package/src/components/viewer/CommandPalette.tsx +597 -0
- package/src/components/viewer/MainToolbar.tsx +31 -3
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -2
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +3 -1
- package/src/components/viewer/useAnimationLoop.ts +4 -1
- package/src/components/viewer/useGeometryStreaming.ts +13 -1
- package/src/components/viewer/useRenderUpdates.ts +6 -1
- package/src/hooks/useKeyboardShortcuts.ts +1 -0
- package/src/hooks/useLens.ts +2 -1
- package/src/hooks/useSandbox.ts +113 -0
- package/src/hooks/useViewerSelectors.ts +22 -0
- package/src/index.css +6 -0
- package/src/lib/recent-files.ts +122 -0
- package/src/lib/scripts/persistence.ts +132 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
- package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
- package/src/lib/scripts/templates/envelope-check.ts +164 -0
- package/src/lib/scripts/templates/federation-compare.ts +189 -0
- package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
- package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
- package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
- package/src/lib/scripts/templates/reset-view.ts +6 -0
- package/src/lib/scripts/templates/space-validation.ts +189 -0
- package/src/lib/scripts/templates/tsconfig.json +13 -0
- package/src/lib/scripts/templates.ts +86 -0
- package/src/sdk/BimProvider.tsx +50 -0
- package/src/sdk/adapters/export-adapter.ts +283 -0
- package/src/sdk/adapters/lens-adapter.ts +44 -0
- package/src/sdk/adapters/model-adapter.ts +32 -0
- package/src/sdk/adapters/model-compat.ts +80 -0
- package/src/sdk/adapters/mutate-adapter.ts +45 -0
- package/src/sdk/adapters/query-adapter.ts +241 -0
- package/src/sdk/adapters/selection-adapter.ts +29 -0
- package/src/sdk/adapters/spatial-adapter.ts +37 -0
- package/src/sdk/adapters/types.ts +11 -0
- package/src/sdk/adapters/viewer-adapter.ts +103 -0
- package/src/sdk/adapters/visibility-adapter.ts +61 -0
- package/src/sdk/local-backend.ts +144 -0
- package/src/sdk/useBimHost.ts +69 -0
- package/src/store/constants.ts +30 -2
- package/src/store/index.ts +24 -1
- package/src/store/slices/pinboardSlice.ts +37 -41
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +43 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/index-7WoQ-qVC.css +0 -1
- package/dist/assets/index-BSANf7-H.js +0 -78795
package/package.json
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifc-lite/viewer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "IFC-Lite viewer application",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
+
"@codemirror/autocomplete": "^6.20.0",
|
|
8
|
+
"@codemirror/commands": "^6.10.2",
|
|
9
|
+
"@codemirror/lang-javascript": "^6.2.4",
|
|
10
|
+
"@codemirror/language": "^6.12.1",
|
|
11
|
+
"@codemirror/search": "^6.6.0",
|
|
12
|
+
"@codemirror/state": "^6.5.4",
|
|
13
|
+
"@codemirror/view": "^6.39.14",
|
|
7
14
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
|
8
15
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
9
16
|
"@radix-ui/react-context-menu": "^2.2.16",
|
|
10
17
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
11
18
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
12
19
|
"@radix-ui/react-label": "^2.1.4",
|
|
13
|
-
"@radix-ui/react-select": "^2.1.6",
|
|
14
|
-
"@radix-ui/react-switch": "^1.1.6",
|
|
15
20
|
"@radix-ui/react-progress": "^1.1.8",
|
|
16
21
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
22
|
+
"@radix-ui/react-select": "^2.1.6",
|
|
17
23
|
"@radix-ui/react-separator": "^1.1.8",
|
|
18
24
|
"@radix-ui/react-slot": "^1.2.4",
|
|
25
|
+
"@radix-ui/react-switch": "^1.1.6",
|
|
19
26
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
20
27
|
"@radix-ui/react-toggle": "^1.1.10",
|
|
21
28
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
@@ -33,23 +40,24 @@
|
|
|
33
40
|
"tailwind-merge": "^3.4.0",
|
|
34
41
|
"tailwindcss": "^4.1.18",
|
|
35
42
|
"zustand": "^4.4.0",
|
|
36
|
-
"@ifc-lite/bcf": "^1.
|
|
37
|
-
"@ifc-lite/cache": "^1.
|
|
38
|
-
"@ifc-lite/data": "^1.
|
|
39
|
-
"@ifc-lite/
|
|
40
|
-
"@ifc-lite/
|
|
41
|
-
"@ifc-lite/
|
|
42
|
-
"@ifc-lite/
|
|
43
|
-
"@ifc-lite/
|
|
44
|
-
"@ifc-lite/
|
|
45
|
-
"@ifc-lite/
|
|
46
|
-
"@ifc-lite/mutations": "^1.
|
|
47
|
-
"@ifc-lite/parser": "^1.
|
|
48
|
-
"@ifc-lite/query": "^1.
|
|
49
|
-
"@ifc-lite/renderer": "^1.
|
|
50
|
-
"@ifc-lite/
|
|
51
|
-
"@ifc-lite/
|
|
52
|
-
"@ifc-lite/
|
|
43
|
+
"@ifc-lite/bcf": "^1.10.0",
|
|
44
|
+
"@ifc-lite/cache": "^1.10.0",
|
|
45
|
+
"@ifc-lite/data": "^1.10.0",
|
|
46
|
+
"@ifc-lite/drawing-2d": "^1.10.0",
|
|
47
|
+
"@ifc-lite/encoding": "^1.10.0",
|
|
48
|
+
"@ifc-lite/export": "^1.10.0",
|
|
49
|
+
"@ifc-lite/geometry": "^1.10.0",
|
|
50
|
+
"@ifc-lite/ids": "^1.10.0",
|
|
51
|
+
"@ifc-lite/lens": "^1.10.0",
|
|
52
|
+
"@ifc-lite/lists": "^1.10.0",
|
|
53
|
+
"@ifc-lite/mutations": "^1.10.0",
|
|
54
|
+
"@ifc-lite/parser": "^1.10.0",
|
|
55
|
+
"@ifc-lite/query": "^1.10.0",
|
|
56
|
+
"@ifc-lite/renderer": "^1.10.0",
|
|
57
|
+
"@ifc-lite/sandbox": "^1.10.0",
|
|
58
|
+
"@ifc-lite/server-client": "^1.10.0",
|
|
59
|
+
"@ifc-lite/spatial": "^1.10.0",
|
|
60
|
+
"@ifc-lite/wasm": "^1.10.0"
|
|
53
61
|
},
|
|
54
62
|
"devDependencies": {
|
|
55
63
|
"@tailwindcss/postcss": "^4.1.18",
|
|
@@ -67,6 +75,7 @@
|
|
|
67
75
|
"dev": "vite",
|
|
68
76
|
"build": "NODE_OPTIONS='--max-old-space-size=4096' bash -c '(tsc || true) && vite build'",
|
|
69
77
|
"preview": "vite preview",
|
|
70
|
-
"test": "bash -c 'shopt -s globstar && tsx --test src/**/*.test.ts'"
|
|
78
|
+
"test": "bash -c 'shopt -s globstar && tsx --test src/**/*.test.ts'",
|
|
79
|
+
"check:templates": "tsc -p src/lib/scripts/templates/tsconfig.json --noEmit"
|
|
71
80
|
}
|
|
72
81
|
}
|
package/src/App.tsx
CHANGED
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ViewerLayout } from './components/viewer/ViewerLayout';
|
|
10
|
+
import { BimProvider } from './sdk/BimProvider';
|
|
10
11
|
|
|
11
12
|
export function App() {
|
|
12
|
-
return
|
|
13
|
+
return (
|
|
14
|
+
<BimProvider>
|
|
15
|
+
<ViewerLayout />
|
|
16
|
+
</BimProvider>
|
|
17
|
+
);
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export default App;
|
|
@@ -29,8 +29,8 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
|
29
29
|
|
|
30
30
|
const DialogContent = React.forwardRef<
|
|
31
31
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
32
|
-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
33
|
-
>(({ className, children, ...props }, ref) => (
|
|
32
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { hideCloseButton?: boolean }
|
|
33
|
+
>(({ className, children, hideCloseButton, ...props }, ref) => (
|
|
34
34
|
<DialogPortal>
|
|
35
35
|
<DialogOverlay />
|
|
36
36
|
<DialogPrimitive.Content
|
|
@@ -42,10 +42,12 @@ const DialogContent = React.forwardRef<
|
|
|
42
42
|
{...props}
|
|
43
43
|
>
|
|
44
44
|
{children}
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
{!hideCloseButton && (
|
|
46
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
47
|
+
<X className="h-4 w-4" />
|
|
48
|
+
<span className="sr-only">Close</span>
|
|
49
|
+
</DialogPrimitive.Close>
|
|
50
|
+
)}
|
|
49
51
|
</DialogPrimitive.Content>
|
|
50
52
|
</DialogPortal>
|
|
51
53
|
));
|
|
@@ -0,0 +1,309 @@
|
|
|
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
|
+
* CodeEditor — CodeMirror 6 wrapper for the script panel.
|
|
7
|
+
*
|
|
8
|
+
* Provides JavaScript/TypeScript editing with autocomplete for the bim.* API
|
|
9
|
+
* surface. Completions are auto-generated from the bridge-schema so they
|
|
10
|
+
* stay in sync with the sandbox API automatically.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useRef, useEffect } from 'react';
|
|
14
|
+
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection } from '@codemirror/view';
|
|
15
|
+
import { EditorState, Compartment } from '@codemirror/state';
|
|
16
|
+
import { javascript } from '@codemirror/lang-javascript';
|
|
17
|
+
import { autocompletion, type CompletionContext, type CompletionResult, type Completion } from '@codemirror/autocomplete';
|
|
18
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
|
19
|
+
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching, indentOnInput } from '@codemirror/language';
|
|
20
|
+
import { highlightSelectionMatches } from '@codemirror/search';
|
|
21
|
+
import { NAMESPACE_SCHEMAS } from '@ifc-lite/sandbox/schema';
|
|
22
|
+
|
|
23
|
+
/** Shared structural styles (mode-agnostic) */
|
|
24
|
+
const baseTheme = EditorView.theme({
|
|
25
|
+
'&': {
|
|
26
|
+
backgroundColor: 'transparent',
|
|
27
|
+
fontSize: '13px',
|
|
28
|
+
height: '100%',
|
|
29
|
+
},
|
|
30
|
+
'.cm-scroller': {
|
|
31
|
+
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
32
|
+
lineHeight: '1.6',
|
|
33
|
+
overflow: 'auto',
|
|
34
|
+
},
|
|
35
|
+
'.cm-content': {
|
|
36
|
+
padding: '8px 0',
|
|
37
|
+
},
|
|
38
|
+
'.cm-gutters': {
|
|
39
|
+
backgroundColor: 'transparent',
|
|
40
|
+
border: 'none',
|
|
41
|
+
paddingRight: '4px',
|
|
42
|
+
},
|
|
43
|
+
'.cm-activeLineGutter': {
|
|
44
|
+
backgroundColor: 'transparent',
|
|
45
|
+
},
|
|
46
|
+
'.cm-completionIcon': {
|
|
47
|
+
display: 'none',
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** Dark mode colors */
|
|
52
|
+
const darkTheme = EditorView.theme({
|
|
53
|
+
'&': { color: '#e4e4e7' },
|
|
54
|
+
'.cm-content': { caretColor: '#e4e4e7' },
|
|
55
|
+
'.cm-gutters': { color: '#71717a' },
|
|
56
|
+
'.cm-activeLineGutter': { color: '#e4e4e7' },
|
|
57
|
+
'.cm-activeLine': { backgroundColor: 'rgba(255,255,255,0.04)' },
|
|
58
|
+
'.cm-selectionMatch': { backgroundColor: 'rgba(255,255,255,0.1)' },
|
|
59
|
+
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground': {
|
|
60
|
+
backgroundColor: 'rgba(99,102,241,0.3)',
|
|
61
|
+
},
|
|
62
|
+
'.cm-cursor': { borderLeftColor: '#e4e4e7' },
|
|
63
|
+
'.cm-tooltip': {
|
|
64
|
+
backgroundColor: '#1c1c22',
|
|
65
|
+
color: '#e4e4e7',
|
|
66
|
+
border: '1px solid #27272a',
|
|
67
|
+
borderRadius: '6px',
|
|
68
|
+
},
|
|
69
|
+
'.cm-tooltip-autocomplete': {
|
|
70
|
+
'& > ul > li[aria-selected]': { backgroundColor: 'rgba(99,102,241,0.2)' },
|
|
71
|
+
},
|
|
72
|
+
}, { dark: true });
|
|
73
|
+
|
|
74
|
+
/** Light mode colors */
|
|
75
|
+
const lightTheme = EditorView.theme({
|
|
76
|
+
'&': { color: '#18181b' },
|
|
77
|
+
'.cm-content': { caretColor: '#18181b' },
|
|
78
|
+
'.cm-gutters': { color: '#a1a1aa' },
|
|
79
|
+
'.cm-activeLineGutter': { color: '#52525b' },
|
|
80
|
+
'.cm-activeLine': { backgroundColor: 'rgba(0,0,0,0.03)' },
|
|
81
|
+
'.cm-selectionMatch': { backgroundColor: 'rgba(99,102,241,0.15)' },
|
|
82
|
+
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground': {
|
|
83
|
+
backgroundColor: 'rgba(99,102,241,0.2)',
|
|
84
|
+
},
|
|
85
|
+
'.cm-cursor': { borderLeftColor: '#18181b' },
|
|
86
|
+
'.cm-tooltip': {
|
|
87
|
+
backgroundColor: '#ffffff',
|
|
88
|
+
color: '#18181b',
|
|
89
|
+
border: '1px solid #e4e4e7',
|
|
90
|
+
borderRadius: '6px',
|
|
91
|
+
},
|
|
92
|
+
'.cm-tooltip-autocomplete': {
|
|
93
|
+
'& > ul > li[aria-selected]': { backgroundColor: 'rgba(99,102,241,0.12)' },
|
|
94
|
+
},
|
|
95
|
+
}, { dark: false });
|
|
96
|
+
|
|
97
|
+
/** Compartment for swapping light/dark theme at runtime */
|
|
98
|
+
const themeCompartment = new Compartment();
|
|
99
|
+
|
|
100
|
+
function isDarkMode(): boolean {
|
|
101
|
+
return document.documentElement.classList.contains('dark');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getColorTheme() {
|
|
105
|
+
return isDarkMode() ? darkTheme : lightTheme;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Completion Generation (auto-generated from bridge-schema)
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
/** Namespace → method completions derived from NAMESPACE_SCHEMAS */
|
|
113
|
+
interface NamespaceCompletions {
|
|
114
|
+
namespace: string;
|
|
115
|
+
detail: string;
|
|
116
|
+
methods: Completion[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build completions for bim.* SDK methods from the bridge schema.
|
|
121
|
+
* Lazily initialized once, then cached for all CodeEditor instances.
|
|
122
|
+
*/
|
|
123
|
+
let cachedCompletionMap: Map<string, NamespaceCompletions> | null = null;
|
|
124
|
+
|
|
125
|
+
function getCompletionMap(): Map<string, NamespaceCompletions> {
|
|
126
|
+
if (cachedCompletionMap) return cachedCompletionMap;
|
|
127
|
+
|
|
128
|
+
const map = new Map<string, NamespaceCompletions>();
|
|
129
|
+
|
|
130
|
+
for (const ns of NAMESPACE_SCHEMAS) {
|
|
131
|
+
const methods: Completion[] = ns.methods.map(m => ({
|
|
132
|
+
label: m.args.length === 0
|
|
133
|
+
? `bim.${ns.name}.${m.name}()` // no-arg methods get ()
|
|
134
|
+
: `bim.${ns.name}.${m.name}(`, // methods with args get (
|
|
135
|
+
type: 'function',
|
|
136
|
+
detail: m.doc,
|
|
137
|
+
}));
|
|
138
|
+
map.set(ns.name, { namespace: ns.name, detail: ns.doc, methods });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
cachedCompletionMap = map;
|
|
142
|
+
return map;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** bim.* API completions — generated from node registry */
|
|
146
|
+
function bimCompletions(context: CompletionContext): CompletionResult | null {
|
|
147
|
+
const word = context.matchBefore(/[\w.]*$/);
|
|
148
|
+
if (!word || (word.from === word.to && !context.explicit)) return null;
|
|
149
|
+
|
|
150
|
+
const text = word.text;
|
|
151
|
+
const completionMap = getCompletionMap();
|
|
152
|
+
|
|
153
|
+
// Top-level bim completions
|
|
154
|
+
if (text === 'bim' || text === 'bim.') {
|
|
155
|
+
return {
|
|
156
|
+
from: word.from,
|
|
157
|
+
options: Array.from(completionMap.values()).map(ns => ({
|
|
158
|
+
label: `bim.${ns.namespace}`,
|
|
159
|
+
type: 'variable',
|
|
160
|
+
detail: ns.detail,
|
|
161
|
+
})),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Namespace method completions
|
|
166
|
+
for (const [ns, data] of completionMap) {
|
|
167
|
+
if (text.startsWith(`bim.${ns}`)) {
|
|
168
|
+
return {
|
|
169
|
+
from: word.from,
|
|
170
|
+
options: data.methods,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Entity data field completions — IFC attribute names (both PascalCase and camelCase work)
|
|
176
|
+
if (text.endsWith('.Name') || text.endsWith('.Type') || text.endsWith('.GlobalId')
|
|
177
|
+
|| text.endsWith('.name') || text.endsWith('.type') || text.endsWith('.ref')) {
|
|
178
|
+
return {
|
|
179
|
+
from: word.from + text.lastIndexOf('.') + 1,
|
|
180
|
+
options: [
|
|
181
|
+
{ label: 'Name', type: 'property', detail: 'IFC Name attribute (IfcLabel)' },
|
|
182
|
+
{ label: 'Type', type: 'property', detail: 'IFC entity type (e.g. IfcWall)' },
|
|
183
|
+
{ label: 'GlobalId', type: 'property', detail: 'IFC GlobalId (IfcGloballyUniqueId)' },
|
|
184
|
+
{ label: 'Description', type: 'property', detail: 'IFC Description attribute (IfcText)' },
|
|
185
|
+
{ label: 'ObjectType', type: 'property', detail: 'IFC ObjectType attribute (IfcLabel)' },
|
|
186
|
+
{ label: 'ref', type: 'property', detail: 'Entity reference { modelId, expressId }' },
|
|
187
|
+
{ label: 'name', type: 'property', detail: 'IFC Name (camelCase alias)' },
|
|
188
|
+
{ label: 'type', type: 'property', detail: 'IFC type (camelCase alias)' },
|
|
189
|
+
{ label: 'globalId', type: 'property', detail: 'IFC GlobalId (camelCase alias)' },
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// Component
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
interface CodeEditorProps {
|
|
202
|
+
value: string;
|
|
203
|
+
onChange: (value: string) => void;
|
|
204
|
+
onRun?: () => void;
|
|
205
|
+
onSave?: () => void;
|
|
206
|
+
className?: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function CodeEditor({ value, onChange, onRun, onSave, className }: CodeEditorProps) {
|
|
210
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
211
|
+
const viewRef = useRef<EditorView | null>(null);
|
|
212
|
+
const onChangeRef = useRef(onChange);
|
|
213
|
+
const onRunRef = useRef(onRun);
|
|
214
|
+
const onSaveRef = useRef(onSave);
|
|
215
|
+
|
|
216
|
+
// Keep callback refs up to date without recreating the editor
|
|
217
|
+
onChangeRef.current = onChange;
|
|
218
|
+
onRunRef.current = onRun;
|
|
219
|
+
onSaveRef.current = onSave;
|
|
220
|
+
|
|
221
|
+
// Create editor once
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (!containerRef.current) return;
|
|
224
|
+
|
|
225
|
+
const runKeymap = keymap.of([
|
|
226
|
+
{
|
|
227
|
+
key: 'Ctrl-Enter',
|
|
228
|
+
mac: 'Cmd-Enter',
|
|
229
|
+
run: () => { onRunRef.current?.(); return true; },
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
key: 'Ctrl-s',
|
|
233
|
+
mac: 'Cmd-s',
|
|
234
|
+
run: () => { onSaveRef.current?.(); return true; },
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
const updateListener = EditorView.updateListener.of((update) => {
|
|
239
|
+
if (update.docChanged) {
|
|
240
|
+
onChangeRef.current(update.state.doc.toString());
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const state = EditorState.create({
|
|
245
|
+
doc: value,
|
|
246
|
+
extensions: [
|
|
247
|
+
lineNumbers(),
|
|
248
|
+
highlightActiveLineGutter(),
|
|
249
|
+
highlightActiveLine(),
|
|
250
|
+
drawSelection(),
|
|
251
|
+
bracketMatching(),
|
|
252
|
+
indentOnInput(),
|
|
253
|
+
history(),
|
|
254
|
+
highlightSelectionMatches(),
|
|
255
|
+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
256
|
+
javascript({ typescript: true }),
|
|
257
|
+
autocompletion({
|
|
258
|
+
override: [bimCompletions],
|
|
259
|
+
activateOnTyping: true,
|
|
260
|
+
maxRenderedOptions: 15,
|
|
261
|
+
}),
|
|
262
|
+
runKeymap,
|
|
263
|
+
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
|
|
264
|
+
updateListener,
|
|
265
|
+
baseTheme,
|
|
266
|
+
themeCompartment.of(getColorTheme()),
|
|
267
|
+
EditorView.lineWrapping,
|
|
268
|
+
],
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const view = new EditorView({
|
|
272
|
+
state,
|
|
273
|
+
parent: containerRef.current,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
viewRef.current = view;
|
|
277
|
+
|
|
278
|
+
// Watch for light/dark mode changes on <html> class
|
|
279
|
+
const observer = new MutationObserver(() => {
|
|
280
|
+
view.dispatch({ effects: themeCompartment.reconfigure(getColorTheme()) });
|
|
281
|
+
});
|
|
282
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
283
|
+
|
|
284
|
+
return () => {
|
|
285
|
+
observer.disconnect();
|
|
286
|
+
view.destroy();
|
|
287
|
+
viewRef.current = null;
|
|
288
|
+
};
|
|
289
|
+
// Only create once — value is set via initial doc
|
|
290
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
291
|
+
}, []);
|
|
292
|
+
|
|
293
|
+
// Sync external value changes into the editor (e.g., loading a different script)
|
|
294
|
+
const lastExternalValue = useRef(value);
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
const view = viewRef.current;
|
|
297
|
+
if (!view) return;
|
|
298
|
+
|
|
299
|
+
// Only update if value changed externally (not from our own onChange)
|
|
300
|
+
if (value !== lastExternalValue.current && value !== view.state.doc.toString()) {
|
|
301
|
+
view.dispatch({
|
|
302
|
+
changes: { from: 0, to: view.state.doc.length, insert: value },
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
lastExternalValue.current = value;
|
|
306
|
+
}, [value]);
|
|
307
|
+
|
|
308
|
+
return <div ref={containerRef} className={className} />;
|
|
309
|
+
}
|