Hello!
I’m working on offline model data caching. I implemented offline loader what works with a simple JSON structure and it uses speckle/objectloader2.
I would like to implement JSON export from Speckle scene. Somebody can help me to a give an example to query JSON model data with GrapQL? For example I would like to load this scene from JSON:
I just would like to show this scene (geometry, material etc.) with offline loader from JSON.
This is my JSON offline loader code:
import {
Viewer,
DefaultViewerParams,
CameraController,
SelectionExtension,
SpeckleLoader
} from '@speckle/viewer';
import { ObjectLoader2Factory } from '@speckle/objectloader2';
class CustomSpeckleOfflineLoader extends SpeckleLoader {
constructor(targetTree: any, resourceData: unknown) {
super(targetTree, 'about:blank', undefined, undefined, resourceData);
this.loader = ObjectLoader2Factory.createFromObjects(
Array.isArray(resourceData) ? resourceData : [resourceData]
);
}
protected initObjectLoader(
_resource: string,
_authToken?: string,
_enableCaching?: boolean
): any {
return this.loader;
}
}
async function main() {
const container = document.getElementById('renderer') as HTMLCanvasElement;
if (!container) throw new Error('Canvas element not found');
const viewer = new Viewer(container, { ...DefaultViewerParams, showStats: false, verbose: true });
await viewer.init();
viewer.createExtension(CameraController);
viewer.createExtension(SelectionExtension);
const loadButton = document.createElement('button');
loadButton.textContent = 'Load JSON';
loadButton.style.position = 'absolute';
loadButton.style.top = '10px';
loadButton.style.left = '10px';
loadButton.style.zIndex = '10';
document.body.appendChild(loadButton);
loadButton.addEventListener('click', async () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.click();
fileInput.onchange = async () => {
const files = fileInput.files;
if (!files || files.length === 0) {
alert('Please select a JSON file!');
return;
}
const file = files[0];
try {
const text = await file.text();
const jsonData = JSON.parse(text);
const loader = new CustomSpeckleOfflineLoader(viewer.getWorldTree(), jsonData);
await viewer.loadObject(loader, true);
console.log(`File ${file.name} successfully loaded!`);
} catch (err) {
console.error('Error loading JSON:', err);
alert('An error occurred while loading the JSON. Please check the file!');
}
};
});
}
main();
And it is simple JSON structure what the loader can eat:
{
"speckle_type": "Objects.Other.Base",
"id": "offline_base_root",
"applicationId": "offline",
"totalChildrenCount": 1,
"createdAt": "2025-09-29T00:00:00.000Z",
"createdBy": "offline",
"units": "meters",
"elements": [
{
"speckle_type": "Objects.Other.Base",
"id": "cube_mesh_base_1",
"applicationId": "offline",
"totalChildrenCount": 1,
"createdAt": "2025-09-29T00:00:00.000Z",
"createdBy": "offline",
"units": "meters",
"elements": [
{
"speckle_type": "Objects.Geometry.Mesh",
"id": "cube_mesh_1",
"vertices": [
0,0,0,1,0,0,1,1,0,0,1,0,
0,0,1,1,0,1,1,1,1,0,1,1
],
"faces": [
0,0,1,2,0,2,3,0,
0,4,5,6,0,6,7,4,
0,0,1,5,0,5,4,0,
0,1,2,6,0,6,5,1,
0,2,3,7,0,7,6,2,
0,3,0,4,0,4,7,3
],
"colors": [4294967295],
"units": "meters"
}
]
}
]
}