BimCT SDK / WebGL2 Viewer
    Preparing search index...

    ๐ŸŽฎ Interactive Examples

    Learn BimCT Viewer through hands-on examples. Each example includes complete source code, live demos, and detailed explanations.

    ๐Ÿ’ก New to BimCT? Start with Example 1 and work your way through sequentially.

    ๐Ÿš€ Try It Live!

    Edit and run code examples in real-time with our interactive playground

    Open Playground โ†’

    Example Topic Resources
    Example 1 Minimal Initialization View ยท Download ยท Try
    Example 2 Initialization Options View ยท Download ยท Try
    Example 3 Event Handling View ยท Download ยท Try
    Example 4 Custom Context Menus View ยท Download ยท Try
    Example 5 Element Control View ยท Download ยท Try
    Example 6 Scene Views & State View ยท Download ยท Try
    Example 7 Working with Pins View ยท Download ยท Try
    Example 8 IoT Sensors View ยท Download ยท Try
    Example 9 Dynamic Elements View ยท Download ยท Try

    ๐Ÿš€ Try in Playground

    What you'll learn: Initialize the BIM Engine and renderer, then load a 3D model with minimal code.

    window.onload = async function () {
    let element = document.getElementById("myViewer");

    // Step 1: Initialize the BIM Engine Core
    await bimctViewer.initializeBimEngineCore();

    // Step 2: Initialize the Renderer
    let renderer = new bimctViewer.BimCTWebGL2Renderer(element, {
    key: "YOUR_LICENSE_KEY",
    assetsPath: "https://bimct-converter.nomitech.com/converter/resources/...",
    });
    await renderer.init();

    // Step 3: Load a Model
    await renderer.loadModelFromUrl("House.bimct");

    // Step 4: Start the Rendering Loop
    renderer.beginDrawLoop();
    };

    Key APIs: BimCTWebGL2Renderer.loadModelFromUrl

    ๐Ÿ”— View Live Demo | ๐Ÿ“ฅ Download Source


    ๐Ÿš€ Try in Playground

    What you'll learn: Configure the viewer with comprehensive initialization options including UI components, rendering quality, and behavior settings.

    let renderOptions = {
    key: "YOUR_LICENSE_KEY",
    ecoRenderingOn: true, // Auto-pause after inactivity
    skymapId: "grass", // Use skymap background
    sunLight: true,
    shadowMaps: true,

    // Navigation cube configuration
    cube: {
    show: true,
    compassShow: false,
    showRotationArrows: true,
    },

    // Enable BimCT built-in UIs
    gui: {
    useContextMenu: true,
    useToolbar: true,
    useLeftToolbar: true,
    useHomeToolbar: true,
    useSettingsWindow: true,
    useDesignTreeWindow: true,
    usePropertiesWindow: true,
    useFilterElementsWindow: true,
    useWebXRWindow: true,
    },

    snappingEnabled: true,
    measurementSystem: bimctViewer.MEASUREMENT_SYSTEM.METRIC,
    assetsPath: "https://bimct-converter.nomitech.com/converter/resources/...",
    };

    // Initialize with options
    let renderer = new bimctViewer.BimCTWebGL2Renderer(element, renderOptions);
    await renderer.init();

    // Listen to model loaded event
    renderer.modelLoadedEvent.subscribe(async (renderer, modelIds) => {
    renderer.overlay.clearLoading();
    renderer.beginDrawLoop();
    });

    await renderer.loadModelFromUrl("House.bimct");

    Key APIs: RendererInitializationOptions, BimCTWebGL2Renderer.modelLoadedEvent

    ๐Ÿ”— View Live Demo | ๐Ÿ“ฅ Download Source


    ๐Ÿš€ Try in Playground

    What you'll learn: Register and respond to viewer events including selection, visibility, measurements, and state changes.

    async function registerViewerEvents(renderer) {
    // Selection Change Event
    renderer.selectionChangeEvent.subscribe((renderer, elements) => {
    console.log("SELECTION CHANGED, ELEMENTS ARRAY: ", elements);
    });

    // Visibility Change Event
    renderer.visibilityChangeEvent.subscribe(async (renderer) => {
    console.log(
    "VISIBILITY CHANGED, NEW VISIBILITY: ",
    await renderer.getVisibilityState()
    );
    });

    // Context Menu Event
    renderer.contextMenuEvent.subscribe((renderer, event) => {
    console.log("CUSTOM CONTEXT MENU", event);
    });

    // Engine State Changed Event
    renderer.stateChangedEvent.subscribe((renderer) => {
    console.log("ENGINE STATE CHANGED");
    });

    // Measurement Events
    renderer.measurementsPointPushBackEvent.subscribe(
    (renderer, measurements) => {
    console.log(
    "MEASUREMENT POINT PUSHED BACK, MEASUREMENTS ARRAY: ",
    measurements
    );
    }
    );

    renderer.measurementsUpdatedEvent.subscribe((renderer, measurements) => {
    console.log("MEASUREMENTS UPDATED, MEASUREMENTS ARRAY: ", measurements);
    });

    renderer.measurementsSelectedEvent.subscribe((renderer, measurements) => {
    console.log("MEASUREMENTS SELECTED, MEASUREMENTS ARRAY: ", measurements);
    });

    renderer.pickLinesUpdatedEvent.subscribe((renderer, measurements) => {
    console.log("PICK LINES UPDATED, PICKED LINES ARRAY: ", measurements);
    });
    }

    // Initialize and register events
    window.onload = async function () {
    let element = document.getElementById("myViewer");
    await bimctViewer.initializeBimEngineCore();
    let renderer = new bimctViewer.BimCTWebGL2Renderer(element, renderOptions);
    await renderer.init();

    await registerViewerEvents(renderer);

    await renderer.loadModelFromUrl("House.bimct");
    renderer.beginDrawLoop();
    };

    Key APIs: BimCTWebGL2Renderer.selectionChangeEvent, BimCTWebGL2Renderer.visibilityChangeEvent, BimCTWebGL2Renderer.stateChangedEvent, BimCTWebGL2Renderer.contextMenuEvent, BimCTWebGL2Renderer.measurementsUpdatedEvent

    ๐Ÿ”— View Live Demo | ๐Ÿ“ฅ Download Source


    ๐Ÿš€ Try in Playground

    What you'll learn: Create custom right-click context menu items to access element metadata, take snapshots, and save/load scene views.

    // Helper function to download files
    function downloadURI(uri, name) {
    const link = document.createElement("a");
    link.download = name;
    link.href = uri;
    link.click();
    link.remove();
    }

    // Register custom context menu items
    function registerCustomMenuItems(renderer) {
    renderer.contextMenu.setCustomMenuText("My Custom Menu");

    // Get metadata of selected elements
    renderer.contextMenu.addCustomMenuItem({
    text: "Get Metadata of Selected Elements",
    click: async (d) => {
    const selectedElements = renderer.selectedElements;
    if (selectedElements.length > 0) {
    renderer.messageBox("Hit F12 to check metadata in console...");

    // Get element name and type
    const elemNameType = renderer.metadataAccessor.getElementDescription(
    selectedElements[0].modelId,
    selectedElements[0].elementId
    );
    console.log("First Element Name and Type:", elemNameType);

    // Get element with all properties
    const elementWithProperties = renderer.metadataAccessor.getElementData(
    selectedElements[0].modelId,
    selectedElements[0].elementId,
    true // Include properties
    );
    console.log("First Element with Properties:", elementWithProperties);
    } else {
    renderer.messageBox("Nothing is selected...");
    }
    },
    });

    // Download snapshot
    renderer.contextMenu.addCustomMenuItem({
    text: "Download Snapshot PNG",
    click: async (d) => {
    const uri = await renderer.takeSnapshot();
    downloadURI(uri, "BimCT_Snapshot.png");
    },
    });

    // Save scene view
    renderer.contextMenu.addCustomMenuItem({
    text: "Save Scene View",
    click: async (d) => {
    const currentView = await renderer.getSceneView();
    localStorage.setItem("currentView", JSON.stringify(currentView));
    renderer.messageBox("Scene saved to local storage...");
    },
    });

    // Load scene view
    renderer.contextMenu.addCustomMenuItem({
    text: "Load Scene View",
    click: async (d) => {
    const currentView = localStorage.getItem("currentView");
    if (currentView) {
    const data = JSON.parse(currentView);
    await renderer.setSceneView(data, true);
    renderer.messageBox("Scene loaded from local storage...");
    }
    },
    });
    }

    // Initialize
    window.onload = async function () {
    let element = document.getElementById("myViewer");
    await bimctViewer.initializeBimEngineCore();

    let renderer = new bimctViewer.BimCTWebGL2Renderer(element, renderOptions);
    await renderer.init();

    registerCustomMenuItems(renderer);

    await renderer.loadModelFromUrl("House.bimct");
    renderer.overlay.clearLoading();
    renderer.beginDrawLoop();
    };

    Key APIs: BimCTWebGL2Renderer.contextMenuEvent, BimCTMetadataAccessor.getElementData, BimCTWebGL2Renderer.takeSnapshot, BimCTWebGL2Renderer.getSceneView

    ๐Ÿ”— View Live Demo | ๐Ÿ“ฅ Download Source


    ๐Ÿš€ Try in Playground

    What you'll learn: Control element visibility, selection, highlighting, and colorization with complete state management.

    // Register custom menu items for element control
    function registerCustomMenuItems(renderer) {
    renderer.contextMenu.setCustomMenuText("Example 5");

    let _highlightedState = null;
    let _colorizationState = null;

    // Select elements by class
    renderer.contextMenu.addCustomMenuItem({
    text: "Highlight all Roofs",
    click: async (d) => {
    renderer.setPickMode(bimctViewer.RENDERER_PICK_MODE.ELEMENT);
    renderer.selectByClass("Roof");
    },
    });

    // Save/Load highlighted state
    renderer.contextMenu.addCustomMenuItem({
    text: "Save Highlighted State",
    click: async (d) => {
    _highlightedState = await renderer.getHighlightedState();
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Highlighted State",
    click: async (d) => {
    if (_highlightedState != null) {
    await renderer.setHighlightedState(_highlightedState);
    }
    },
    });

    // Selection controls
    renderer.contextMenu.addCustomMenuItem({
    text: "Clear Selection",
    click: async (d) => {
    await renderer.clearSelection();
    },
    });

    // Visibility controls
    renderer.contextMenu.addCustomMenuItem({
    text: "Isolate Selected Element(s)",
    click: async (d) => {
    renderer.isolateSelectedElements();
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Hide Selected Element(s)",
    click: async (d) => {
    renderer.hideSelectedElements();
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Show all Elements",
    click: async (d) => {
    renderer.showAllElements(true, true);
    },
    });

    // Colorization controls
    renderer.contextMenu.addCustomMenuItem({
    text: "Colorize Selected Elements(s)",
    click: async (d) => {
    renderer.colorizeSelectedElements(0.8, 0.2, 0.2); // Red color
    // Also works with faces:
    // renderer.colorizeInternalIds(await renderer.getHighlightedInternalIds(), 0.8, 0.2, 0.2)
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "De-Colorize Selected Elements(s)",
    click: async (d) => {
    renderer.decolorizeSelectedElements();
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "De-Colorize all Elements",
    click: async (d) => {
    renderer.decolorizeAllElements();
    },
    });

    // Save/Load colorization state
    renderer.contextMenu.addCustomMenuItem({
    text: "Save Colorization State",
    click: async (d) => {
    _colorizationState = await renderer.getColorizationState();
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Colorization State",
    click: async (d) => {
    if (_colorizationState != null) {
    await renderer.setColorizationState(_colorizationState);
    }
    },
    });
    }

    // Auto-select columns on model load
    function registerModelEvents(renderer) {
    renderer.modelLoadedEvent.subscribe((renderer, modelIds) => {
    renderer.selectByClass("Column");
    });
    }

    // Initialize
    window.onload = async function () {
    let element = document.getElementById("myViewer");
    await bimctViewer.initializeBimEngineCore();

    let renderer = new bimctViewer.BimCTWebGL2Renderer(element, renderOptions);
    await renderer.init();

    registerCustomMenuItems(renderer);
    registerModelEvents(renderer);

    await renderer.loadModelFromUrl("House.bimct");
    renderer.overlay.clearLoading();
    renderer.beginDrawLoop();
    };

    Key APIs: BimCTWebGL2Renderer.clearSelection, BimCTWebGL2Renderer.isolateSelectedElements, BimCTWebGL2Renderer.hideSelectedElements, BimCTWebGL2Renderer.showAllElements, BimCTWebGL2Renderer.colorizeSelectedElements, BimCTWebGL2Renderer.decolorizeAllElements, BimCTWebGL2Renderer.getColorizationState, BimCTWebGL2Renderer.setColorizationState

    ๐Ÿ”— View Live Demo | ๐Ÿ“ฅ Download Source


    ๐Ÿš€ Try in Playground

    What you'll learn: Save and restore complete viewer states including camera position, visibility, colors, measurements, and settings. Demonstrates comprehensive state management and model loading/unloading.

    // Register state management menu items
    function registerCustomMenuItems(renderer) {
    renderer.contextMenu.setCustomMenuText("Example 6");

    // Save/Load full scene view
    renderer.contextMenu.addCustomMenuItem({
    text: "Save Full Scene View",
    click: async (d) => {
    const currentView = await renderer.getSceneView(true, true);
    localStorage.setItem("currentView", JSON.stringify(currentView));
    console.log("Scene view saved:", currentView);
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Full Scene View",
    click: async (d) => {
    const currentView = localStorage.getItem("currentView");
    if (currentView) {
    const data = JSON.parse(currentView);
    await renderer.setSceneView(data, true);
    }
    },
    });

    // Camera state management
    let cameraState = null;
    renderer.contextMenu.addCustomMenuItem({
    text: "Save Current Camera",
    click: async (d) => {
    cameraState = await renderer.getCameraState();
    console.log("Camera state:", cameraState);
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Current Camera",
    click: async (d) => {
    await renderer.setCameraState(cameraState, false); // No transition
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Current Camera Transition",
    click: async (d) => {
    await renderer.setCameraState(cameraState, true); // With transition
    },
    });

    // Visibility state management
    let visibilityState = null;
    renderer.contextMenu.addCustomMenuItem({
    text: "Save Visibility State",
    click: async (d) => {
    visibilityState = await renderer.getVisibilityState();
    console.log("Visibility state:", visibilityState);
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Visibility State",
    click: async (d) => {
    await renderer.setVisibilityState(visibilityState);
    },
    });

    // Colorization state management
    let colorizationState = null;
    renderer.contextMenu.addCustomMenuItem({
    text: "Save Colorization State",
    click: async (d) => {
    colorizationState = await renderer.getColorizationState();
    console.log("Colorization state:", colorizationState);
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Colorization State",
    click: async (d) => {
    await renderer.setColorizationState(colorizationState);
    },
    });

    // Measurements state management
    let measurementsState = null;
    renderer.contextMenu.addCustomMenuItem({
    text: "Save Measurement State",
    click: async (d) => {
    measurementsState = await renderer.getMeasurementsState();
    console.log("Measurements state:", measurementsState);
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Measurement State",
    click: async (d) => {
    await renderer.setMeasurementsState(measurementsState);
    },
    });

    // Settings state management
    let settingsState = null;
    renderer.contextMenu.addCustomMenuItem({
    text: "Save Settings State",
    click: async (d) => {
    settingsState = await renderer.getViewerSettingsState();
    console.log("Settings state:", settingsState);
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Settings State",
    click: async (d) => {
    await renderer.setViewerSettingsState(settingsState);
    },
    });

    // Model management
    renderer.contextMenu.addCustomMenuItem({
    text: "Unload all Models",
    click: async (d) => {
    await renderer.unloadAll();
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Another Model",
    click: async (d) => {
    await renderer.loadModelFromUrl("RacBasicV7.bimct");
    },
    });
    }

    // Auto-select and position camera on model load
    function registerModelEvents(renderer, bimctViewer) {
    renderer.modelLoadedEvent.subscribe((renderer, modelIds) => {
    renderer.selectByClass("Window");
    setTimeout(() => {
    renderer.resetCamera3D(
    bimctViewer.BIM_CAMERA_POSITION.BCP_FRONT_TOP_RIGHT,
    true
    );
    }, 2000);
    });
    }

    // Initialize
    window.onload = async function () {
    let element = document.getElementById("myViewer");
    await bimctViewer.initializeBimEngineCore();

    let renderer = new bimctViewer.BimCTWebGL2Renderer(element, renderOptions);
    await renderer.init();

    registerCustomMenuItems(renderer);
    registerModelEvents(renderer, bimctViewer);

    await renderer.loadModelFromUrl("House.bimct");
    renderer.overlay.clearLoading();
    renderer.beginDrawLoop();
    };

    Key APIs: BimCTWebGL2Renderer.getSceneView, BimCTWebGL2Renderer.setSceneView, BimCTWebGL2Renderer.unloadModel

    ๐Ÿ”— View Live Demo | ๐Ÿ“ฅ Download Source


    ๐Ÿš€ Try in Playground

    What you'll learn: Add visual markers (pins) to your models with custom colors or images, manage pin state, and implement interactive features like dragging and camera states.

    // Register custom context menu items for pin insertion
    function registerCustomMenuItems(renderer) {
    renderer.contextMenu.setCustomMenuText("Example 7");

    // Add colored pins
    renderer.contextMenu.addCustomMenuItem({
    text: "Insert a RED Pin",
    click: async (d) => {
    renderer.startPinAdditionByColor(
    "red", // Color
    { name: "Red Pin" } // User Object
    );
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Insert a GREEN Pin",
    click: async (d) => {
    renderer.startPinAdditionByColor("green", { name: "Green Pin" });
    },
    });

    // Add custom image as pin
    renderer.contextMenu.addCustomMenuItem({
    text: "Insert an Labor Image as Pin",
    click: async (d) => {
    const pinText = await renderer.inputTextMessageBox(
    "Type a name for this Labor",
    "Insert a Labor",
    "Tony Worker",
    bimctViewer.MessageIcon.INFORMATION
    );

    renderer.startPinAdditionByImage(
    "https://developers.nomitech.com/examples/labor32.png",
    32, // width
    32, // height
    { name: pinText } // User Object
    );
    },
    });

    // Move pin with animation
    renderer.contextMenu.addCustomMenuItem({
    text: "Move last Pin to here (animated)",
    enabled: () => renderer.pins.pins.length > 0,
    click: async (d) => {
    const pos = await renderer.screenToWorldPosition(d.x, d.y);
    if (pos) {
    const lastPin = renderer.pins.pins[renderer.pins.pins.length - 1];
    renderer.pins.animationInterpolations = 40;
    renderer.pins.animationTime = 300; // milliseconds
    renderer.pins.movePin3D(lastPin, pos.x, pos.y, pos.z, true, true);
    }
    },
    });

    // Save/Load pin state
    let _pinState = null;
    renderer.contextMenu.addCustomMenuItem({
    text: "Save Pin(s) State",
    enabled: () => renderer.pins.pins.length > 0,
    click: async (d) => {
    _pinState = await renderer.getPinState();
    renderer.messageBox("Pin(s) state saved!", "Pin State Saved");
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Pin(s) State",
    click: async (d) => {
    if (_pinState) {
    await renderer.setPinState(_pinState);
    renderer.messageBox("Pin(s) state loaded!", "Pin State Loaded");
    }
    },
    });
    }

    // Register pin events
    function registerPinEvents(renderer) {
    // Pin insertion event - store camera state
    renderer.pinInsertEvent.subscribe(async (renderer, pinWidget) => {
    const res = await renderer.messageBox(
    `Do you want to make ${pinWidget.userObject.name} draggable?`,
    "Pin added",
    bimctViewer.MessageOption.YESNO,
    bimctViewer.MessageIcon.QUESTION
    );

    if (res == bimctViewer.MessageResult.YES) {
    pinWidget.draggingAllowed = true;
    }

    // Store camera state in user object
    pinWidget.userObject.cameraState = await renderer.getCameraState();
    });

    // Pin clicked - restore camera state
    renderer.pinClickedEvent.subscribe(async (renderer, pinWidgetEvent) => {
    await renderer.messageBox(
    `You clicked on ${pinWidgetEvent.pinWidget.userObject.name}! Close to load camera.`,
    "Pin Clicked!"
    );
    const cameraState = pinWidgetEvent.pinWidget.userObject.cameraState;
    renderer.setCameraState(cameraState, true);
    });

    // Pin moved - update camera state
    renderer.pinMovedEvent.subscribe(async (renderer, pinWidgetEvent) => {
    pinWidgetEvent.pinWidget.userObject.cameraState =
    await renderer.getCameraState();
    await renderer.messageBox(
    `You moved ${pinWidgetEvent.pinWidget.userObject.name}! Camera state updated.`,
    "Pin Moved!"
    );
    });

    // Show label on hover
    renderer.pinHighlightedEvent.subscribe(async (renderer, pinWidget) => {
    pinWidget.label = pinWidget.userObject.name;
    });

    renderer.pinDehighlightedEvent.subscribe(async (renderer, pinWidget) => {
    pinWidget.label = undefined;
    });
    }

    // Initialize viewer
    window.onload = async function () {
    let element = document.getElementById("myViewer");
    await bimctViewer.initializeBimEngineCore();

    let renderer = new bimctViewer.BimCTWebGL2Renderer(element, renderOptions);
    await renderer.init();

    registerCustomMenuItems(renderer);
    registerPinEvents(renderer);

    await renderer.loadModelFromUrl("House.bimct");
    renderer.overlay.clearLoading();
    renderer.beginDrawLoop();
    };

    Key APIs: BimCTWebGL2Renderer.startPinAdditionByColor, BimCTWebGL2Renderer.pinInsertEvent, BimCTWebGL2Renderer.pinClickedEvent, BimCTWebGL2Renderer.clearPins, BimCTWebGL2Renderer.getPinState, BimCTWebGL2Renderer.setPinState

    ๐Ÿ”— View Live Demo | ๐Ÿ“ฅ Download Source


    ๐Ÿš€ Try in Playground

    What you'll learn: Create an IoT sensor simulation using pins to represent temperature sensors that colorize elements based on readings. Uses the BimCT Sensors API for advanced temperature visualization.

    const minTemperature = -10;
    const maxTemperature = 60;
    const factor = 0.4;

    // Generate unique sensor ID
    function generateSensorGlobalId() {
    const unique =
    Date.now().toString(36) + Math.random().toString(36).substring(2);
    return `sensor${unique}`;
    }

    // Create sensor channel and group for selected elements
    async function createSessionGroup(renderer) {
    await renderer.sensors.cleanup();
    await renderer.pins.removeAll();

    const channel = await renderer.sensors.addChannel(
    "MyChannel",
    minTemperature,
    maxTemperature,
    factor
    );

    const group = await channel.addGroup("MyGroup");

    for (let element of renderer.sceneState.selectedElements) {
    await group.addElementByInternalId(element.internalId);
    }

    renderer.sensors.setActive(channel);
    return group;
    }

    // Register custom context menu items
    function registerCustomMenuItems(renderer) {
    renderer.contextMenu.setCustomMenuText("Example 8");

    // Create channel for selected elements
    renderer.contextMenu.addCustomMenuItem({
    text: "Create Channel for Selected Elements",
    click: async (d) => {
    await createSessionGroup(renderer);
    await renderer.messageBox(
    `Sensor channel created for ${renderer.sceneState.selectedElements.length} elements`
    );
    renderer.clearSelection();
    },
    });

    // Insert temperature sensors
    renderer.contextMenu.addCustomMenuItem({
    text: "Insert Cold Temperature Sensor",
    click: async (d) => {
    if (!renderer.sensors.findChannelByName("MyChannel")) {
    renderer.messageBox("Please create a Sensor Channel first.");
    return;
    }
    renderer.startPinAdditionByColor("DarkBlue", {
    temperature: -10,
    sensorName: generateSensorGlobalId(),
    });
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Insert Hot Temperature Sensor",
    click: async (d) => {
    if (!renderer.sensors.findChannelByName("MyChannel")) {
    renderer.messageBox("Please create a Sensor Channel first.");
    return;
    }
    renderer.startPinAdditionByColor("DarkRed", {
    temperature: 35,
    sensorName: generateSensorGlobalId(),
    });
    },
    });

    // Save/Load sensor state
    let _sensorsState = null;
    let _pinsState = null;

    renderer.contextMenu.addCustomMenuItem({
    text: "Save Sensors State",
    click: async (d) => {
    _sensorsState = await renderer.getSensorsState();
    _pinsState = await renderer.getPinState();
    renderer.messageBox("Sensors state saved!", "State Saved");
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load Sensors State",
    click: async (d) => {
    if (_pinsState) {
    await renderer.setPinState(_pinsState);
    await renderer.setSensorsState(_sensorsState);
    renderer.messageBox("Sensors state loaded!", "State Loaded");
    }
    },
    });
    }

    // Register pin events for sensor interaction
    function registerPinEvents(renderer) {
    // Add sensor when pin is inserted
    renderer.pinInsertEvent.subscribe(async (renderer, pinWidget) => {
    const group = renderer.sensors.findGroupByName("MyChannel", "MyGroup");
    if (group) {
    await group.addSensor(
    pinWidget.userObject.sensorName,
    pinWidget.position3D,
    pinWidget.userObject.temperature
    );
    pinWidget.draggingAllowed = true;
    }
    });

    // Update sensor when pin is moved
    renderer.pinMovedEvent.subscribe(async (renderer, event) => {
    const sensor = renderer.sensors.findSensorByName(
    "MyChannel",
    "MyGroup",
    event.pinWidget.userObject.sensorName
    );
    if (sensor) {
    sensor.update(
    event.pinWidget.position3D,
    event.pinWidget.userObject.temperature
    );
    }
    });

    // Edit sensor value on click
    renderer.pinClickedEvent.subscribe(async (renderer, event) => {
    const sensor = renderer.sensors.findSensorByName(
    "MyChannel",
    "MyGroup",
    event.pinWidget.userObject.sensorName
    );

    if (sensor) {
    const channel = renderer.sensors.findChannelByName("MyChannel");
    const result = await renderer.inputNumberMessageBox(
    `Set Temperature (${channel.low} to ${channel.high}):`,
    "Set Sensor Value",
    sensor.value
    );

    if (result !== null) {
    event.pinWidget.loadByColor("black");
    await sensor.update(sensor.point, result);
    event.pinWidget.userObject.temperature = sensor.value;
    }
    }
    });
    }

    // Initialize viewer
    window.onload = async function () {
    let element = document.getElementById("myViewer");
    await bimctViewer.initializeBimEngineCore();

    let renderer = new bimctViewer.BimCTWebGL2Renderer(element, renderOptions);
    await renderer.init();

    registerCustomMenuItems(renderer);
    registerPinEvents(renderer);

    await renderer.loadModelFromUrl("House.bimct");
    renderer.overlay.clearLoading();
    renderer.beginDrawLoop();
    };

    Key APIs: BimCTWebGL2Renderer.startPinAdditionByColor, BimCTWebGL2Renderer.pinInsertEvent, BimCTWebGL2Renderer.pinMovedEvent, BimCTWebGL2Renderer.pinClickedEvent, BimCTWebGL2Renderer.getSensorsState, BimCTWebGL2Renderer.setSensorsState

    ๐Ÿ”— View Live Demo | ๐Ÿ“ฅ Download Source


    ๐Ÿš€ Try in Playground

    What you'll learn: Work with dynamic 3D objects that can be positioned, rotated, cloned, and transformed at runtime. This example demonstrates a CCTV camera placement system with full transformation controls.

    // Add new dynamic element at position with surface alignment
    async function addNewDynamicElement(renderer, position, normalToPlace) {
    let loadedModel = null;
    const sourceDynamicElementModel = findSourceDynamicElement(renderer);

    const state = await renderer.getCameraState();

    // Clone or load CCTV model
    if (sourceDynamicElementModel == null) {
    loadedModel = await renderer.loadModelFromUrl(
    "CCTV_Camera_Cylindrical.bimct",
    undefined,
    false
    );
    } else {
    loadedModel = await renderer.cloneModel(
    sourceDynamicElementModel.id64,
    undefined,
    true
    );
    }

    await renderer.setCameraState(state, false);

    const elements = renderer.sceneState.getElementsByModelId(loadedModel.id64);
    const internalIds = elements.map((e) => e.internalId);
    let bBox = await renderer.getElementsBoundingBox(internalIds);

    // Align to surface normal
    if (normalToPlace != null) {
    renderer.dynamicElements.lookAt(
    internalIds,
    bBox.center,
    normalToPlace.rotateFrontToRight()
    );

    bBox = await renderer.getElementsBoundingBox(internalIds);
    const translationVector = bBox.translationToReachFace(
    position,
    normalToPlace
    );
    await renderer.dynamicElements.translateTo(internalIds, translationVector);
    } else {
    await renderer.dynamicElements.moveTo(internalIds, position, bBox.center);
    }

    await renderer.selectByInternalIds(internalIds);
    await renderer.zoomToSelectedElements();
    }

    // Transform selected dynamic elements
    async function translateSelectedElements(renderer, tx, ty, tz) {
    const internalIds = renderer.selectedElements.map((e) => e.internalId);
    await renderer.dynamicElements.translateTo(
    internalIds,
    new bimctViewer.Vector3d(tx, ty, tz)
    );
    }

    async function rotateSelectedElements(renderer, rx, ry, rz) {
    const internalIds = renderer.selectedElements.map((e) => e.internalId);
    const bBox = await renderer.getElementsBoundingBox(internalIds);
    await renderer.dynamicElements.rotateAroundXYZ(
    internalIds,
    bBox.center,
    new bimctViewer.Vector3d(rx, ry, rz)
    );
    }

    // Context menu for CCTV placement and control
    async function installApplicationMenu(renderer) {
    renderer.contextMenu.setCustomMenuText("CCTV Demo");

    // Add CCTV camera
    renderer.contextMenu.addCustomMenuItem({
    text: "Add a CCTV Camera (3D)",
    click: async (d) => {
    renderer.startPinAdditionByImage("CCTV_Camera_Square.png", 32, 32, {
    cameraType: "CCTV 3D",
    });
    },
    });

    // Remove selected cameras
    renderer.contextMenu.addCustomMenuItem({
    text: "Remove Selected CCTV Camera(s)",
    enabled: () => hasSelectedDynamicElements(renderer),
    click: async (d) => {
    await deleteSelectedDynamicElements(renderer);
    },
    });

    // Save/Load state
    let dynamicElementsState = null;
    let cameraModelsState = null;

    renderer.contextMenu.addCustomMenuItem({
    text: "Save CCTV Cameras State",
    click: async (d) => {
    const cctvCamerasModelIds = renderer.sceneState.models
    .filter((m) => m.cacheInfo?.url === "CCTV_Camera_Cylindrical.bimct")
    .map((m) => m.id64);
    cameraModelsState = await renderer.getModelsWithURLState(
    cctvCamerasModelIds
    );
    dynamicElementsState = await renderer.getDynamicElementsState();
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Load CCTV Cameras State",
    enabled: () => dynamicElementsState != null && cameraModelsState != null,
    click: async (d) => {
    const state = await renderer.getCameraState();
    await deleteAllDynamicElements(renderer);
    await renderer.setModelsWithURLState(cameraModelsState);
    await renderer.setDynamicElementsState(dynamicElementsState);
    await renderer.setCameraState(state, false);
    },
    });

    // Movement controls
    renderer.contextMenu.addCustomMenuItem({
    text: "Move to Right",
    enabled: () => hasSelectedDynamicElements(renderer),
    click: async (d) => {
    await translateSelectedElements(
    renderer,
    await askUserForMillimeters(renderer),
    0,
    0
    );
    },
    });

    renderer.contextMenu.addCustomMenuItem({
    text: "Rotate on Y",
    enabled: () => hasSelectedDynamicElements(renderer),
    click: async (d) => {
    await rotateSelectedElements(
    renderer,
    0,
    await askUserForDegrees(renderer),
    0
    );
    },
    });
    }

    // Register pin insert event for CCTV placement
    renderer.pinInsertEvent.subscribe(async (renderer, pinWidget) => {
    if (pinWidget.userObject.cameraType == "CCTV 3D") {
    renderer.pins.removePin(pinWidget, true);
    const position = await renderer.worldToScreenSpacePosition(
    pinWidget.position3D
    );
    const normalBehind = await renderer.pickNormal(
    position.point.x,
    position.point.y
    );
    await addNewDynamicElement(renderer, pinWidget.position3D, normalBehind);
    }
    });

    // Initialize
    window.onload = async function () {
    let element = document.getElementById("myViewer");
    await bimctViewer.initializeBimEngineCore();

    let renderer = new bimctViewer.BimCTWebGL2Renderer(element, renderOptions);
    await renderer.init();

    await registerViewerEvents(renderer);
    await installApplicationMenu(renderer);

    await renderer.loadModelFromUrl("RacAdvanced.bimct");
    renderer.overlay.clearLoading();
    renderer.beginDrawLoop();
    };

    Key APIs: BimCTWebGL2Renderer.loadModelFromUrl, BimCTWebGL2Renderer.cloneModel, BimCTWebGL2Renderer.startPinAdditionByImage, BimCTWebGL2Renderer.pinInsertEvent, BimCTWebGL2Renderer.getDynamicElementsState, BimCTWebGL2Renderer.setDynamicElementsState, BimCTWebGL2Renderer.getModelsWithURLState, BimCTWebGL2Renderer.setModelsWithURLState

    Note: To create .bimct models with dynamic elements, you must specify which elements should be dynamic during conversion using the Converter API.

    ๐Ÿ”— View Live Demo | ๐Ÿ“ฅ Download Source


    Recommended order for beginners:

    1. Example 1 โ†’ 2: Learn basics of initialization
    2. Example 3 โ†’ 4: Master event handling and interaction
    3. Example 5 โ†’ 6: Control visualization and state
    4. Example 7 โ†’ 8 โ†’ 9: Advanced features


    • Download the source code to see the complete implementation with HTML and CSS
    • Try the live demos to interact with features before implementing
    • Combine examples to build more complex applications
    • Check the console in live demos to see event logging