Welcome to the workshop, we will learn the basics of how to interact with Speckle using the most popular web programming language. We will learn how to get some info from a project using the GraphQL API, how to load some data using the @speckle/objectloader
and how to publish data using the recently added @speckle/objectsender
.
The data used during the workshop is publicly available on the following Speckle Project: https://app.speckle.systems/projects/9ff253b70b/
You are welcome to write to me at [email protected] if you have any questions.
For this session you will need to:
- Install Node.js.
- A code editor, I will be using Visual Studio Code.
- A Speckle account.
Now that you have everything installed. Let’s start a Javascript/Typescript project.
- Create an empty folder called
speckle-js
- Open that folder using your preferred code editor.
- In a terminal, type
npm init -y
to start a project.
Setting up Typescript.
- Install Typescript and tsx by running
npm install -D typescript tsx @types/node
- Initialize a Typescript configuration file running
npx tsc —-init
The easiest way to get information about a project is through the GraphQL API. You will have access to project name, collaborators, models, and update dates.
💡What’s GraphQL is a query language for an API. It makes API discoverable, typed and allows for clients to fetch finely grained data with a graph like approach.
Let’s start by sketching our first query on the Speckle GraphQL Apollo Studio. Make sure you have logged in on Speckle before trying.
query UserProjects {
activeUser {
projects {
items {
name
description
id
role
}
}
}
}
Let’s move to TypeScript and learn how to use that query.
- Install the
dotenv
runningnpm i dotenv
library and configure it on index.ts
export async function projectsInfo() {
const query = `#graphql
query UserProjects {
activeUser {
projects {
items {
name
description
id
role
}
}
}
}
`;
const response = await fetch("https://app.speckle.systems/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.SPECKLE_TOKEN as string}`,
},
body: JSON.stringify({ query }),
});
const parsed = await response.json();
const projects = parsed.data.activeUser.projects.items;
console.log(projects);
}
- Developing against a GraphQL API using the native fetch API can be easier using clients built specifically for GraphQL. Check out Apollo, graphql-request or other options.
- GraphQL APIs are fully typed and there are solutions to build TypeScript types automatically so that your queries become even easier. Have a look at GraphQL Codegen.
On this exercise we will see how to load data from Speckle using the @speckle/objectloader
library. Let’s see if we manage to calculate the total area on the model below.
To use the object loader, we will need to pieces of information, an object id and the project id to which this object belongs. We can inspect this model version and get the root object id we would like to load.
- Object ID: d8f03ed8f1de1a9697bfd85c2f18fd35
- Project ID: 9ff253b70b
Let’s write a function that loads the object above.
import ObjectLoader from "@speckle/objectloader";
const projectId = "9ff253b70b";
const objectId = "d8f03ed8f1de1a9697bfd85c2f18fd35";
export async function loadData() {
const loader = new ObjectLoader({
objectId,
token: process.env.SPECKLE_TOKEN as string,
serverUrl: "https://app.speckle.systems",
streamId: projectId,
options: {
excludeProps: ["__closure"],
},
});
const model: any = await loader.getAndConstructObject(() => undefined);
console.log(model);
// TODO: Calculate the total area sum of the rooms in the model.
}
-
Calculate total area
function calculateTotalArea(model: any) { const roomsCollection = model.elements[0]; const rooms = roomsCollection.elements; let totalArea = 0; const units = rooms[0].units; for (const room of rooms) { totalArea += room.area; } console.log(`Total area: ${(totalArea as number).toFixed(2)} ${units}`); }
We will make use of the @speckle/objectsender
library to make our own sender. Let’s look at some basic objects we can create using it.
const message = new Base({
message:
"A wizard is never late, nor is he early, he arrives precisely when he means to.",
author: "Gandalf, the Grey",
});
const result = await send(message, {
projectId,
serverUrl: "https://app.speckle.systems/",
token: process.env.SPECKLE_TOKEN as string,
});
console.log("Object sent with ID:", result.hash);
For Speckle to make start making use of the message we just sent, we need to create a version using that object as the root of our version.
-
create-version.ts
import { SendResult } from "@speckle/objectsender"; interface CreateVersionParams { serverUrl: string; projectId: string; modelId: string; token: string; } export async function createVersion( res: SendResult, { serverUrl, projectId, modelId, token }: CreateVersionParams ) { const response = await fetch(serverUrl + "/graphql", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ query: `#graphql mutation CreateVersion($input: CreateVersionInput!) { versionMutations { create(input: $input) { id } } } `, variables: { input: { projectId, modelId, objectId: res.hash, message: `Hello from Javascript!`, }, }, }), }); return (await response.json()).data.versionMutations.create; }
const version = await createVersion(result, {
modelId,
projectId,
serverUrl: "https://app.speckle.systems",
token: process.env.SPECKLE_TOKEN as string,
});
To be able to visualize your objects you will need to create Meshes. Let’s make an implementation of the Objects.Geometry.Mesh class from the Objects .NET library.
https://github.com/specklesystems/speckle-sharp/blob/main/Objects/Objects/Geometry/Mesh.cs
import { Base, Chunkable, Detach } from "@speckle/objectsender";
export class Mesh extends Base {
speckle_type: string = "Objects.Geometry.Mesh";
@Detach()
@Chunkable(31250)
vertices?: number[];
@Detach()
@Chunkable(62500)
faces?: number[];
@Detach()
@Chunkable(62500)
colors?: number[] = [];
@Detach()
@Chunkable(31250)
textureCoordinates?: number[] = [];
units?: string = "";
constructor(props?: Record<string, unknown>) {
super(props);
}
}
To exercise what we learned, we will implement another useful object type, Render Material. Adding a render material will affect how our mesh object is displayed on the Speckle Viewer.
import { Base } from "@speckle/objectsender";
export class RenderMaterial extends Base {
speckle_type: string = "Objects.Other.RenderMaterial";
name?: string;
opacity: number = 1;
metalness: number = 0;
roughness: number = 1;
diffuse: number = 4292072403;
emissive: number = 4278190080;
constructor(props?: Record<string, unknown>) {
super(props);
}
}
Some sample data can be found here: https://github.com/vwnd/speckle-js/tree/dev/data
import { Base, send } from "@speckle/objectsender";
import { createVersion } from "./utils/create-version";
import { readFileSync } from "fs";
import { Mesh } from "./utils/mesh";
import { RenderMaterial } from "./utils/render-material";
import { hexToArgb } from "hex-argb-converter";
const projectId = "9ff253b70b";
const modelId = "31fdd47483";
export async function sendData() {
console.log("3 - Send Data");
var data = JSON.parse(readFileSync("data/js.json", "utf-8"));
const foregroundGeometry = data.fg;
const backgroundGeometry = data.bg;
const fgMesh = new Mesh();
fgMesh.vertices = foregroundGeometry.vertices;
fgMesh.faces = foregroundGeometry.faces;
fgMesh.colors = foregroundGeometry.colors;
fgMesh.textureCoordinates = foregroundGeometry.textureCoordinates;
const fgMaterial = new RenderMaterial();
fgMaterial.name = "JavaScript Foreground";
fgMaterial.diffuse = hexToArgb("#323330");
fgMaterial.metalness = 1;
fgMaterial.roughness = 0.05;
fgMesh.renderMaterial = fgMaterial;
const bgMesh = new Mesh();
bgMesh.vertices = backgroundGeometry.vertices;
bgMesh.faces = backgroundGeometry.faces;
bgMesh.colors = backgroundGeometry.colors;
bgMesh.textureCoordinates = backgroundGeometry.textureCoordinates;
const bgMaterial = new RenderMaterial();
bgMaterial.name = "JavaScript Background";
bgMaterial.diffuse = hexToArgb("#F0DB4F");
bgMaterial.roughness = 0.05;
bgMesh.renderMaterial = bgMaterial;
const message = new Base({
speckle_type: "TypeScript.Model",
name: "JavaScript Model",
displayValue: [fgMesh, bgMesh],
});
const result = await send(message, {
projectId,
serverUrl: "https://app.speckle.systems/",
token: process.env.SPECKLE_TOKEN as string,
});
console.log("Object sent with ID:", result.hash);
const version = await createVersion(result, {
modelId,
projectId,
serverUrl: "https://app.speckle.systems",
token: process.env.SPECKLE_TOKEN as string,
});
console.log("Version created with ID:", version.id);
}
Whether to represent Layers, Categories, Tags, Collections, Groups, or hierarchical containers, it is common to see a natural grouping of objects within a 3D model. The Collection
type provides a unified way to represent hierarchical collections of Speckle objects.
import { Base, Detach } from "@speckle/objectsender";
export class Collection<T extends Base> extends Base {
@Detach()
elements: T[];
speckle_type = "Speckle.Core.Models.Collection";
constructor(
name: string,
collectionType: string,
elements: T[] = [],
props?: Record<string, unknown>
) {
super(props);
this.name = name;
this.collectionType = collectionType;
this.elements = elements;
}
}