Select elements from a work schedule
Choose a wall-installation activity to highlight the walls assigned to it in the building model.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Select elements from a work schedule</title>
<meta name="description" content="Choose a wall-installation activity to highlight the walls assigned to it in the building model.">
<style>
html, body { height: 100%; margin: 0; }
#example-viewer-mount { position: fixed; inset: 0; }
bim-grid#ifc-viewer { display: block; position: absolute; inset: 0; width: 100%; height: 100%; min-width: 0; min-height: 0; }
.example-ui { position: fixed; z-index: 10; box-sizing: border-box; color: #253041; font: 12px/1.4 system-ui, sans-serif; }
.example-ui.example-controls { right: 10px; bottom: 64px; width: 224px; max-width: calc(100vw - 64px); display: grid; gap: 6px; padding: 10px; background: #fff; border: 1px solid #dfe3e8; border-radius: 4px; box-shadow: 0 1px 5px #14203310; }
.example-ui :is(h2, p, fieldset) { margin: 0; }
.example-ui h2 { font-size: 13px; line-height: 1.3; font-weight: 650; }
.example-ui p { color: #5a6575; }
.example-ui fieldset { max-height: 180px; overflow: auto; display: grid; gap: 6px; min-width: 0; padding: 0; border: 0; }
.example-ui button { box-sizing: border-box; min-width: 0; max-width: 100%; font: inherit; padding: 5px 7px; border: 1px solid #cbd2dc; border-radius: 3px; background: #fff; color: #253041; cursor: pointer; }
.example-ui button:hover { background: #f3f6fa; }
.example-ui button[aria-pressed=true] { border-color: #3568ba; background: #edf4ff; color: #174788; }
.example-ui button:focus-visible { outline: 2px solid #3568ba; outline-offset: 2px; }
.example-ui :disabled { cursor: default; opacity: .55; }
.example-ui output { font-variant-numeric: tabular-nums; }
.example-ui.example-credit { right: 8px; bottom: 32px; width: max-content; max-width: calc(100vw - 64px); padding: 2px 5px; background: #ffffffdf; border-radius: 2px; font-size: 9px; text-align: right; overflow-wrap: anywhere; }
.example-ui.example-credit a { color: #566171; }
@media (max-width: 600px) {
.example-ui.example-controls { right: 8px; padding: 8px; }
.example-ui button { min-height: 30px; }
}
</style>
<!-- generated:example-seo:start -->
<link rel="canonical" href="https://viewer.flinker.app/examples/selection/index.html">
<meta name="robots" content="index,follow">
<meta property="og:title" content="Select elements from a work schedule">
<meta property="og:description" content="Choose a wall-installation activity to highlight the walls assigned to it in the building model.">
<meta property="og:url" content="https://viewer.flinker.app/examples/selection/index.html"><meta property="og:type" content="article">
<meta property="og:site_name" content="Flinker IFC Viewer"><meta property="og:image" content="https://viewer.flinker.app/examples/selection/thumbnail.webp">
<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="Select elements from a work schedule">
<meta name="twitter:description" content="Choose a wall-installation activity to highlight the walls assigned to it in the building model."><meta name="twitter:image" content="https://viewer.flinker.app/examples/selection/thumbnail.webp">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Select elements from a work schedule","description":"Choose a wall-installation activity to highlight the walls assigned to it in the building model.","url":"https://viewer.flinker.app/examples/selection/index.html","mainEntityOfPage":"https://viewer.flinker.app/examples/selection/index.html","image":"https://viewer.flinker.app/examples/selection/thumbnail.webp","inLanguage":"en","author":{"@type":"Organization","name":"Flinker","url":"https://www.flinker.app/"}}</script>
<!-- generated:example-seo:end -->
<script type="application/json" id="example-data">
{
"category": "Host integration",
"formats": [
"IFC",
"BCF"
],
"capabilities": [
"Selection"
],
"audience": "Developer integration",
"order": 16,
"assets": [
{
"id": "bim-whale-advanced",
"name": "AdvancedProject.ifc",
"format": "IFC",
"bytes": 44337371,
"sha256": "86b068a7ca6947e398c1daaa8bede9d31048c3990adc308429faeb56ee01db94",
"url": "https://raw.githubusercontent.com/andrewisen/bim-whale-ifc-samples/595fa90e3af7120d004fcb37a79d8657f1d1c9c2/AdvancedProject/IFC/AdvancedProject.ifc",
"sourceUrl": "https://github.com/andrewisen/bim-whale-ifc-samples/blob/595fa90e3af7120d004fcb37a79d8657f1d1c9c2/AdvancedProject/IFC/AdvancedProject.ifc",
"license": "MIT",
"licenseUrl": "https://github.com/andrewisen/bim-whale-ifc-samples/blob/595fa90e3af7120d004fcb37a79d8657f1d1c9c2/LICENSE",
"noticeUrl": "https://viewer.flinker.app/examples/selection/index.html#example-notices",
"attribution": "BIM Whale Advanced Project. Copyright (c) 2020 André \"MG\" Wisén.",
"notes": "Original IFC2x3 file; contents unchanged."
}
],
"viewer": {
"assetIds": [
"bim-whale-advanced"
],
"presentation": "default"
}
}
</script>
</head>
<body>
<main id="example-viewer-mount" aria-label="Model viewer"><bim-grid id="ifc-viewer"></bim-grid></main>
<aside class="example-ui example-controls" aria-label="Work activity">
<h2>Work activity</h2>
<p>Choose an activity to highlight the walls assigned to it.</p>
<fieldset id="work-packages" disabled aria-label="Wall installation activities"></fieldset><output id="activity-count" aria-live="polite">Choose an activity</output>
</aside>
<div class="example-ui example-credit" aria-label="Sample source and license"><a href="https://github.com/andrewisen/bim-whale-ifc-samples/blob/595fa90e3af7120d004fcb37a79d8657f1d1c9c2/AdvancedProject/IFC/AdvancedProject.ifc" target="_blank" rel="noopener noreferrer">BIM Whale · André Wisén</a> · <a href="https://github.com/andrewisen/bim-whale-ifc-samples/blob/595fa90e3af7120d004fcb37a79d8657f1d1c9c2/LICENSE" target="_blank" rel="noopener noreferrer">MIT</a></div>
<template id="example-guide">
<h2 data-example-title>Select elements from a work schedule</h2>
<p data-example-description>Choose a wall-installation activity to highlight the walls assigned to it in the building model.</p>
<h2>Developer integration</h2><p>The page queries walls and their containing storeys through <code>viewer.IfcAPI</code>, then builds a wall-installation activity for each returned storey. Labels, membership and counts come from the loaded model. The click handler builds a selection-only BCF update, writes it with <code>bcf.save()</code>, then applies it with <code>viewer.add()</code> and <code>viewer.selectTopic()</code>. One topic is reused; no camera, colors or visibility filters are supplied.</p><p>Copy this folder and serve it over HTTP, for example <code>python -m http.server 8080</code>. Open <code>http://localhost:8080/</code>; the SDK and sample downloads require internet access.</p>
<p>The page opens the original source bytes with <code>await viewer.IfcAPI</code>, reads native model data, then closes the model and disposes WASM before displaying the source with <code>viewer.add()</code>. See the <a href="https://thatopen.github.io/engine_web-ifc/docs/classes/web-ifc.IfcAPI.html">complete web-ifc API reference</a>.</p>
</template>
<template id="example-notices">
<h2>Model source and license</h2><p>BIM Whale Advanced Project. Copyright (c) 2020 André "MG" Wisén. <a href="https://github.com/andrewisen/bim-whale-ifc-samples/blob/595fa90e3af7120d004fcb37a79d8657f1d1c9c2/AdvancedProject/IFC/AdvancedProject.ifc" target="_blank" rel="noopener noreferrer">Original IFC at the pinned revision</a> · <a href="https://github.com/andrewisen/bim-whale-ifc-samples/blob/595fa90e3af7120d004fcb37a79d8657f1d1c9c2/LICENSE" target="_blank" rel="noopener noreferrer">MIT License</a>. The original IFC2x3 file is imported unchanged.</p>
</template>
<script type="module">
async function withIfcModel(blob, read) {
const api = await viewer.IfcAPI;
await api.Init();
let modelID = -1;
try {
modelID = api.OpenModel(new Uint8Array(await blob.arrayBuffer()));
if (modelID < 0) throw new Error('The native IFC API could not open the model.');
return await read(api, modelID);
} finally {
if (modelID >= 0) api.CloseModel(modelID);
api.Dispose();
}
}
import { Viewer, bcf } from "https://cdn.flinker.app/ifc-viewer/v4.0.3/ifc-viewer.es.js";
const { assets: [model] } = JSON.parse(document.getElementById('example-data').textContent);
// The host builds generated XML documents; the SDK validates and writes the BCF archive.
function createExampleProject(title, selection = [], identity = {}) {
const element = (name, attributes = {}, content = []) => ({ name, namespaceURI: '', attributes, content });
const text = (name, value) => element(name, {}, [String(value)]);
const document = (name, root) => ({ document: { kind: 'xml', schema: 'bcf-2.1/' + name, root } });
const topicGuid = identity.topicGuid ?? crypto.randomUUID();
const viewpointGuid = identity.viewpointGuid ?? crypto.randomUUID();
const topic = element('Topic', { Guid: topicGuid, TopicType: 'Coordination', TopicStatus: 'Open' }, [
text('Title', title), text('CreationDate', new Date().toISOString()), text('CreationAuthor', 'examples@example.com'),
]);
const reference = element('Viewpoints', { Guid: viewpointGuid }, [text('Viewpoint', viewpointGuid + '.bcfv')]);
const selected = element('Selection', {}, selection.map(IfcGuid => element('Component', { IfcGuid })));
const viewpoint = element('VisualizationInfo', { Guid: viewpointGuid }, [element('Components', {}, [
...(selection.length ? [selected] : []), element('Visibility', { DefaultVisibility: 'true' }),
])]);
return { topic, project: { kind: 'archive', specification: 'bcf-2.1', entries: {
'bcf.version': document('version', element('Version', { VersionId: '2.1' })),
[topicGuid + '/markup.bcf']: document('markup', element('Markup', {}, [topic, reference])),
[topicGuid + '/' + viewpointGuid + '.bcfv']: document('visinfo', viewpoint),
} } };
}
async function applyExampleProject(project, topic, name = 'example.bcf') {
const result = await viewer.add(name, await bcf.save(project));
if (result?.isError) throw new Error('The viewer could not import the BCF file.');
const applied = await viewer.selectTopic(topic.attributes.Guid);
if (applied?.isError) throw new Error('The viewer could not apply the BCF viewpoint.');
}
async function loadExampleFile(name, url, load = blob => viewer.add(name, blob), size = 0) {
const show = stage => viewer.setMessage(name + ': ' + stage, { type: 'loading' });
const chunks = [], chunkSize = 4 * 1024 * 1024;
let offset = 0, attempts = 0, action = 'download', type = '';
try {
await show('Downloading');
while (!size || offset < size) {
try {
const end = Math.min(offset + chunkSize, size || Infinity) - 1;
const response = await fetch(url, {
headers: { Range: 'bytes=' + offset + '-' + end },
cache: 'no-store', signal: AbortSignal.timeout(60000),
});
if (!response.ok) {
await response.body?.cancel();
// GitHub raw hides Content-Range from browser JavaScript: probe EOF.
if (response.status === 416 && offset && !size) break;
throw Object.assign(new Error('HTTP ' + response.status), { retry: [408, 429].includes(response.status) || response.status >= 500 });
}
const range = response.headers.get('content-range')?.match(/^bytes (\d+)-(\d+)\/(\d+)$/);
if (range && Number(range[1]) !== offset) throw new TypeError('Unexpected byte range');
size ||= Number(range?.[3]) || 0;
const bytes = await response.arrayBuffer();
const expected = response.status === 206 ? (size ? Math.min(chunkSize, size - offset) : 0) : size;
if (!bytes.byteLength || (expected && bytes.byteLength !== expected) || (response.status === 206 && bytes.byteLength > chunkSize))
throw new TypeError('Incomplete download at byte ' + offset);
if (response.status === 200) chunks.length = 0; // Server ignored Range.
chunks.push(bytes);
offset = response.status === 200 ? bytes.byteLength : offset + bytes.byteLength;
type = response.headers.get('content-type') || '';
attempts = 0;
if (response.status === 200) break;
await show('Downloading' + (size ? ' ' + Math.round(offset / size * 100) + '%' : ''));
}
catch (error) {
if (error.retry === false || ++attempts === 3) throw error;
await show('Retrying download');
await new Promise(resolve => setTimeout(resolve, 500 * 2 ** (attempts - 1)));
}
}
action = 'open';
const result = await load(new Blob(chunks, { type }), show);
if (result?.isError) throw new Error('The viewer could not open the file');
await viewer.setMessage(null);
return result;
}
catch (error) {
throw new Error('Could not ' + action + ' ' + name + (action === 'download' ? ' from ' + url : '') + ': ' + error.message, { cause: error });
}
}
const viewer = new Viewer('#ifc-viewer');
window.exampleViewer = viewer;
document.body.dataset.exampleState = 'loading';
// Source storey containment at revision 47281b50; the host owns the activities.
let workPackages = [];
function scalar(value) {
if (value === null || value === undefined) return null;
if (typeof value !== 'object') return value;
return scalar(Object.hasOwn(value, 'value') ? value.value : value._representationValue);
}
async function readIfcLines(api, modelID, ids, recursive = false) {
const rows = [];
for (let start = 0; start < ids.length; start += 96) {
rows.push(...api.GetLines(modelID, ids.slice(start, start + 96), recursive));
}
return rows;
}
async function queryType(api, modelID, name, recursive = false) {
const vector = api.GetLineIDsWithType(modelID, api.GetTypeCodeFromName(name), true);
try {
const ids = Array.from({ length: vector.size() }, (_, index) => vector.get(index));
return await readIfcLines(api, modelID, ids, recursive);
} finally { vector.delete(); }
}
function storeyIndex(root) {
const result = new Map();
function visit(node, floor = '') {
if (String(node.type).toUpperCase() === 'IFCBUILDINGSTOREY') floor = String(scalar(node.Name) ?? 'Unnamed storey');
if (node.expressID !== undefined) result.set(node.expressID, floor);
for (const child of node.children ?? []) visit(child, floor);
}
visit(root); return result;
}
async function readWorkPackages(api, modelID) {
await viewer.setMessage('Reading walls by storey', { type: 'loading' });
const floors = storeyIndex(await api.properties.getSpatialStructure(modelID, true));
const groups = new Map();
for (const wall of await queryType(api, modelID, 'IFCWALL')) {
const floor = floors.get(wall.expressID) || 'Unassigned', guid = scalar(wall.GlobalId);
if (!guid) continue;
if (!groups.has(floor)) groups.set(floor, []); groups.get(floor).push(guid);
}
workPackages = [...groups].sort(([a],[b]) => a.localeCompare(b, undefined, { numeric: true })).map(([floor,guids],index) => ({ id: String(index), label: floor + ' wall installation', guids: [...new Set(guids)] }));
if (!workPackages.length) throw new Error('The model has no walls to select.');
}
const controls = document.getElementById('work-packages');
const identity = { topicGuid: crypto.randomUUID(), viewpointGuid: crypto.randomUUID() };
let updates = Promise.resolve();
function renderPackages() {
controls.replaceChildren();
for (const item of [...workPackages, { id: '', label: 'Clear selection', guids: [] }]) {
const button = document.createElement('button');
button.type = 'button';
button.dataset.package = item.id;
button.setAttribute('aria-pressed', 'false');
button.textContent = item.label + (item.guids.length ? ' · ' + item.guids.length + ' walls' : '');
button.addEventListener('click', () => {
controls.disabled = true;
document.body.dataset.exampleState = 'loading';
updates = updates.then(async () => {
const { project, topic } = createExampleProject('Work schedule selection', item.guids, identity);
await applyExampleProject(project, topic, 'schedule-selection.bcf');
for (const option of controls.querySelectorAll('button')) option.setAttribute('aria-pressed', String(option === button && Boolean(item.id)));
document.getElementById('activity-count').textContent = item.guids.length ? item.guids.length + ' walls selected' : 'Selection cleared';
document.body.dataset.exampleState = 'loaded';
}).catch(error => {
document.body.dataset.exampleState = 'error';
return viewer.setMessage(error.message, { type: 'error' });
})
.finally(() => { controls.disabled = false; });
});
controls.append(button);
}
}
await viewer.ready;
try {
await loadExampleFile(model.name, model.url, async blob => {
await withIfcModel(blob, readWorkPackages);
await viewer.add(model.name, blob);
}, model.bytes);
renderPackages();
controls.disabled = false;
await viewer.setMessage(null);
document.body.dataset.exampleState = 'loaded';
controls.querySelector('button').click();
} catch (error) {
document.body.dataset.exampleState = 'error';
await viewer.setMessage(error.message, { type: 'error' });
}
</script>
</body>
</html>
How it works
Developer integration
The page queries walls and their containing storeys through viewer.IfcAPI, then builds a wall-installation activity for each returned storey. Labels, membership and counts come from the loaded model. The click handler builds a selection-only BCF update, writes it with bcf.save(), then applies it with viewer.add() and viewer.selectTopic(). One topic is reused; no camera, colors or visibility filters are supplied.
Copy this folder and serve it over HTTP, for example python -m http.server 8080. Open http://localhost:8080/; the SDK and sample downloads require internet access.
The page opens the original source bytes with await viewer.IfcAPI, reads native model data, then closes the model and disposes WASM before displaying the source with viewer.add(). See the complete web-ifc API reference.
Model source and license
BIM Whale Advanced Project. Copyright (c) 2020 André "MG" Wisén. Original IFC at the pinned revision · MIT License. The original IFC2x3 file is imported unchanged.