BIM OOTB β Plugin SDK¶
1. Overview¶
A plugin is one JS file + one JSON manifest. No build step, no SDK install, no API key. The browser IDE lets you write, test, and publish plugins without leaving the browser.
IDE: deploy/sandbox/ide.html
Test harness: node plugin_test.js plugins/my_plugin/
Fixture DB: Duplex (1119 elements, 2.8MB)
2. Install / Remove / Toggle¶
Install: drop folder into plugins/ β add line to manifest.json β refresh
Remove: delete folder + manifest line β refresh (data auto-wiped)
Disable: set "enabled": false in manifest β refresh (folder stays, not loaded)
Plugin settings panel in viewer:
Plugins [+ Install]
ββββββββββββββββββββββββββββββββββββββββββ
β π± Carbon Calculator [ON ] [Γ] β
β π Progress Tracker [OFF] [Γ] β
β π₯ Clash Detector [ON ] [Γ] β
ββββββββββββββββββββββββββββββββββββββββββ
- Toggle = flip
enabledin manifest. No code change. - Delete = remove manifest entry + folder. Plugin's IndexedDB keys (prefixed
store_{id}_*) auto-wiped. - No residue in core code, DOM, or event listeners.
plugin_loader.jshandles full lifecycle.
3. Plugin Structure¶
plugins/my_plugin/
manifest.json β declares name, icon, type, entry point
plugin.js β function setupMyPlugin(A) { ... }
manifest.json¶
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0",
"author": "you",
"icon": "π",
"concern": "QS",
"type": "tool",
"toolbar_button": true,
"requires": ["db"],
"entry": "plugin.js",
"description": "One line β what it does"
}
Fields: id (unique slug), type (tool | template | view | connector), concern (QS | Factory | Owner | Inspector | Green | General), requires (db | libDb | scene β what APP objects the plugin uses).
plugin.js¶
function setupMyPlugin(A) {
// A = the APP object with full API access
// Register buttons, hooks, panels here
}
Convention: function name = setup + PascalCase of id. The plugin loader calls it automatically.
3. Tutorial β Your First Plugin (60 Seconds)¶
Step 1: Open the IDE¶
Open ide.html. Duplex building loads in the preview panel. Code editor has a starter template.
Step 2: Paint all walls blue¶
function setupMyPlugin(A) {
A.interact.toolbarButton('π§±', () => {
const walls = A.query.byClass('IfcWall');
A.display.colorBy(el =>
el.ifc_class === 'IfcWall' ? 0x4488ff : null
);
A.display.toast(walls.length + ' walls');
});
}
Step 3: Click Run¶
Log panel:
Β§PLUGIN_LOAD setupMyPlugin
Β§TOOLBAR added π§± button
Β§READY click π§± to activate
Step 4: Click the π§± button¶
Log panel:
Β§QUERY byClass('IfcWall') β 38 rows 0.2ms
Β§DISPLAY colorBy applied to 1119 elements
Β§DISPLAY toast "38 walls"
Β§PLUGIN_PASS all API calls valid
Preview: 38 walls are blue. Everything else unchanged.
Step 5: Add a table¶
Add after the toast line:
const byStorey = A.query.totals('storey', 'count', { where: "ifc_class='IfcWall'" });
A.display.table(byStorey, 'Walls per Storey');
Click Run, click π§±. A sortable table panel appears.
Step 6: Save and Package¶
- Save β writes
plugins/my_plugin/plugin.js+ auto-generatesmanifest.json - Package β downloads
my_plugin.zip - Publish β uploads to OCI plugins/ bucket, appears on marketplace
4. Advanced Example β Progress Tracker with Site Photos¶
Popular demand: colour elements by construction progress, log photos per element, export weekly report.
function setupProgressTracker(A) {
// Persistent storage β survives page reload
const STATUS = { NOT_STARTED: 0, IN_PROGRESS: 1, COMPLETE: 2, DEFECT: 3 };
const COLORS = { 0: 0xcc4444, 1: 0xffaa00, 2: 0x44cc44, 3: 0xff00ff };
const LABELS = { 0: 'Not Started', 1: 'In Progress', 2: 'Complete', 3: 'Defect' };
// --- Toolbar button: toggle progress view ---
A.interact.toolbarButton('π', () => {
A.display.colorBy(el => {
const s = A.store.get('progress_' + el.guid);
return s != null ? COLORS[s] : null;
});
// Summary by status
const all = A.query.totals('discipline', 'count');
const summary = Object.entries(LABELS).map(([k, label]) => {
const guids = A.store.keys('progress_').filter(key => A.store.get(key) == k);
return { Status: label, Count: guids.length };
});
A.display.table(summary, 'Progress Summary');
});
// --- Click element: set status + take photo ---
A.interact.onPick(el => {
A.interact.prompt([
{ name: 'status', type: 'select', label: 'Status',
options: Object.entries(LABELS).map(([k, v]) => ({ key: k, display: v })) },
{ name: 'photo', type: 'camera', label: 'Site Photo (optional)' },
{ name: 'notes', type: 'text', label: 'Notes' }
]).then(r => {
A.store.set('progress_' + el.guid, parseInt(r.status));
if (r.photo) A.store.set('photo_' + el.guid, r.photo);
if (r.notes) A.store.set('notes_' + el.guid, r.notes);
A.display.toast(el.name + ' β ' + LABELS[r.status]);
});
});
// --- Export weekly report ---
A.interact.toolbarButton('π', () => {
const elements = A.query.byClass('*');
const rows = elements.map(el => ({
GUID: el.guid,
Class: el.ifc_class,
Name: el.name,
Storey: el.storey,
Discipline: el.discipline,
Status: LABELS[A.store.get('progress_' + el.guid) || 0],
Notes: A.store.get('notes_' + el.guid) || '',
Has_Photo: A.store.get('photo_' + el.guid) ? 'Yes' : 'No'
}));
A.export.excel(rows, 'Progress_Report_' + new Date().toISOString().slice(0, 10));
A.display.toast('Exported ' + rows.length + ' elements');
});
}
Log output when running:
Β§PLUGIN_LOAD setupProgressTracker
Β§TOOLBAR added π button
Β§TOOLBAR added π button
Β§READY click π for progress view, π for report
[user clicks element]
Β§INTERACT onPick guid=2O2Fr$t4X7Zf8NOew3FLOH class=IfcWall
Β§INTERACT prompt 3 fields (select, camera, text)
Β§STORE set progress_2O2Fr$t4X7Zf8NOew3FLOH = 2
Β§DISPLAY toast "Basic Wall:223 β Complete"
[user clicks π]
Β§QUERY totals(discipline, count) β 3 rows
Β§STORE keys(progress_) β 47 entries
Β§DISPLAY colorBy applied to 1119 elements
Β§DISPLAY table "Progress Summary" 4 rows
[user clicks π]
Β§QUERY byClass(*) β 1119 rows
Β§STORE read 1119 progress entries, 12 photos, 8 notes
Β§EXPORT excel "Progress_Report_2026-04-22.xlsx" 1119 rows
Β§DISPLAY toast "Exported 1119 elements"
Β§PLUGIN_PASS all API calls valid
5. API Schema Spec¶
This schema is designed to be pasted into any AI model (Claude, GPT, etc.) so users can describe what they want in plain English and the AI returns working plugin code.
Prompt Template for AI-Assisted Plugin Development¶
You are writing a BIM OOTB plugin. The plugin runs in a browser against
a SQLite database containing IFC building data. You have access to the
APP object (referred to as A in the setup function).
THE DATABASE SCHEMA (extracted DB):
elements_meta: guid TEXT PK, ifc_class TEXT, name TEXT, building TEXT,
storey TEXT, discipline TEXT, material TEXT
element_instances: guid TEXT PK, x REAL, y REAL, z REAL,
rx REAL, ry REAL, rz REAL, geometry_hash TEXT
project_metadata: key TEXT PK, value TEXT
building_summary: building TEXT, discipline TEXT, count INTEGER,
min_x REAL, min_y REAL, min_z REAL,
max_x REAL, max_y REAL, max_z REAL
THE API (5 categories, ~30 methods):
QUERY β returns arrays of objects
A.query.byClass(ifc_class) β [{guid, ifc_class, name, storey, discipline, material, x, y, z}]
A.query.byStorey(storey_name) β same shape
A.query.byDisc(discipline) β same shape
A.query.byGuid(guid) β single element object
A.query.neighbours(guid, radius_metres) β [{a: element, b: element, dist: number}]
A.query.path(classA, classB) β [{from: element, to: element, relation: string}]
A.query.schedule(ifc_class) β [{guid, name, type_name, width, height, properties...}]
A.query.areas() β [{space_name, storey, area_m2, perimeter_m, height_m}]
A.query.totals(group_by_field, metric, options?) β [{field_value, count/sum/avg}]
DISPLAY β visual output
A.display.colorBy(fn) β fn(element) returns hex colour or null (keep original)
A.display.highlight(guid_array) β yellow glow on listed elements
A.display.hide(guid_array) β hide elements
A.display.isolate(guid_array) β hide everything except these
A.display.label(guid, text) β floating 3D text above element
A.display.overlay(html_string) β 2D panel overlaid on viewer
A.display.chart({type, data, label, group, title}) β Chart.js in a panel
A.display.table(row_array, title) β sortable HTML table in a panel
A.display.toast(message) β brief notification
INTERACT β user input
A.interact.onPick(fn) β fn(element) called when user clicks an element
A.interact.onStoreyChange(fn) β fn(storey_name) called on filter change
A.interact.toolbarButton(emoji_icon, fn) β adds button, fn called on click
A.interact.contextMenu([{label, fn}]) β right-click menu items
A.interact.prompt(field_array) β Promise<{field_name: value}>
field types: text, number, select, slider, date, camera, checkbox
A.interact.drag(ifc_class, onDrop_fn) β drag-and-drop from palette
EXPORT β get data out
A.export.excel(row_array, filename) β .xlsx download via SheetJS
A.export.csv(row_array, filename) β .csv download
A.export.pdf(html_string, filename) β print-friendly PDF
A.export.screenshot(options?) β canvas capture as PNG
A.export.share(text, blob?) β Web Share API (mobile)
A.export.qr(guid) β QR code image linking to element in viewer
STORE β persist plugin data (IndexedDB, survives reload)
A.store.get(key) β value
A.store.set(key, value)
A.store.keys(prefix) β [key_strings]
A.store.table(name, schema) β create in-memory SQL table for plugin use
A.store.sync(remote_path) β push/pull to OCI bucket
PLUGIN FORMAT:
function setupPluginName(A) {
// register buttons, hooks, panels
// A is the full APP object
}
RULES:
- One setup function per plugin
- Register toolbar buttons for main actions
- Use A.interact.onPick for element-level actions
- Use A.store for persistence across reloads
- Use A.display.toast for user feedback
- Use A.export for data output
- Every action must produce a log-visible effect
- Return working JS β no pseudocode, no placeholders
Example AI Interaction¶
User prompt:
"I want a plugin that finds all doors without fire ratings and highlights them red. Export a list to Excel."
AI returns:
function setupFireRatingChecker(A) {
A.interact.toolbarButton('π₯', () => {
const doors = A.query.schedule('IfcDoor');
const unrated = doors.filter(d => !d.fire_rating || d.fire_rating === 'None');
A.display.highlight(unrated.map(d => d.guid));
A.display.colorBy(el =>
unrated.some(d => d.guid === el.guid) ? 0xff0000 : null
);
A.display.table(unrated.map(d => ({
Name: d.name, Storey: d.storey, Type: d.type_name, Rating: d.fire_rating || 'MISSING'
})), 'Doors Missing Fire Rating');
A.display.toast(unrated.length + ' doors without fire rating');
A.export.excel(unrated, 'Fire_Rating_Audit');
});
}
User pastes into IDE β Run β sees red doors β exports Excel. Done.
AI + IDE Workflow (the standard practice)¶
AI Assist modes (user chooses in IDE Settings):
| Mode | Cost | API key needed | How it works |
|---|---|---|---|
| None | Free | No | No AI button. User writes JS manually. IDE still works. |
| Paste Mode (default) | Free | No | Button copies API schema + user intent to clipboard. User pastes into their own Claude/ChatGPT/any free AI. Pastes response back into IDE. |
| Claude API | ~$0.01-0.05/request | Yes (user's own) | IDE calls Claude API directly from browser. Key in localStorage, never sent to OCI. |
| OpenAI API | ~$0.01-0.05/request | Yes (user's own) | Same, different endpoint. |
| Local (Ollama) | Free | No | IDE calls localhost:11434. Runs on user's machine. Private, offline. |
IDE Settings:
AI Provider: [Paste Mode βΌ] β default, zero cost
API Key: [not needed]
Model: [not needed]
Paste Mode flow (zero cost, zero config):
1. User opens IDE (ide.html)
2. Clicks [π€ AI Assist] β opens prompt panel
3. Types: "find all doors without fire ratings, highlight red, export Excel"
4. Clicks [π Copy Prompt] β clipboard gets API schema + DB schema + user intent
5. User pastes into their own Claude / ChatGPT / any AI (free tier works)
6. AI returns plugin code
7. User pastes response into IDE code editor
8. Clicks [βΆ Run]
9. Log panel proves it works (or shows Β§PLUGIN_FAIL with reason)
10. If fail β describe error to AI β AI fixes β paste β Run again
11. If pass β Save β Package β Publish
API Mode flow (direct, no copy-paste):
1. User opens IDE, sets API key once in Settings
2. Clicks [π€ AI Assist] β types intent
3. Clicks [Send] β IDE prepends API schema + DB schema automatically
4. AI response lands directly in code editor
5. Clicks [βΆ Run] β log proves it
The API schema in Β§5 is the same system prompt regardless of mode. Paste Mode copies it to clipboard. API Mode sends it as the system message. Same result, different plumbing.
The IDE is mandatory regardless of AI mode because: - AI code may have bugs β the log catches them with real data - AI may use wrong column names β the query runs against real Duplex DB - AI may call non-existent API methods β the harness rejects them - The 3D preview shows visual correctness that no AI can guarantee
The AI writes. The IDE verifies. The log is the proof.
6. Plugin Types Reference¶
| Type | What it adds | Entry point pattern |
|---|---|---|
| tool | Toolbar button + action | toolbarButton + query/display |
| template | Rates, forex, grouping for boq_charts | JSON only, no JS needed |
| view | Custom panel or dashboard | addPanel + chart/table |
| connector | Import/export format | toolbarButton + file read/write |
7. DB Schema Quick Reference (for plugin authors)¶
extracted DB (metadata + transforms)¶
-- Every element in the building
SELECT guid, ifc_class, name, building, storey, discipline, material
FROM elements_meta;
-- Position and rotation per element
SELECT guid, x, y, z, rx, ry, rz, geometry_hash
FROM element_instances;
-- Building-level aggregates with bounding boxes
SELECT building, discipline, count, min_x, min_y, min_z, max_x, max_y, max_z
FROM building_summary;
-- Project settings (true north, units, etc.)
SELECT key, value FROM project_metadata;
library DB (geometry BLOBs)¶
-- Raw mesh data per unique shape
SELECT geometry_hash, vertices, faces, vertex_count, face_count
FROM component_geometries;
-- vertices = Float32Array BLOB (x,y,z triples)
-- faces = Uint32Array BLOB (triangle indices)
Common queries plugin authors will use¶
-- Count by class
SELECT ifc_class, COUNT(*) as qty FROM elements_meta GROUP BY ifc_class ORDER BY qty DESC;
-- All doors on level 2
SELECT * FROM elements_meta WHERE ifc_class='IfcDoor' AND storey='Level 2';
-- Elements within 5m of a point
SELECT *, (ABS(x-10.5)+ABS(y-20.3)+ABS(z-3.0)) as dist
FROM elements_meta m JOIN element_instances i ON m.guid=i.guid
WHERE dist < 5 ORDER BY dist;
-- Discipline summary
SELECT discipline, COUNT(*) as elements,
MIN(x) as min_x, MAX(x) as max_x
FROM elements_meta m JOIN element_instances i ON m.guid=i.guid
GROUP BY discipline;
-- Floor area (if IfcSpace present)
SELECT name, storey, area FROM elements_meta WHERE ifc_class='IfcSpace';