Skip to main content

Documents

Opening, saving and closing documents from the host's side, and following them in React. A host needs it when it keeps the files itself (a PDM, a portal) and renders <Buerligons welcome={false} />; the app's own File menu needs none of it.

Classes​

EditorController​

Opens, saves and closes the documents of a host that keeps its files itself, in an embedded Buerligons.

It opens a source (or a new part) in a fresh drawing, saves it back as bytes and closes the drawing. The host (a PDM, say) owns files, versions and permissions; the controller only talks to the ClassCAD session of its drawing, which it makes the active one, so that Buerligons shows it. Next to it the host renders <Buerligons welcome={false} />, which shows nothing while no document is open.

buerli's init (@buerli.io/classcad) must have been called once per page before open.

A controller has one drawing at a time: an open replaces the drawing before. In an assembly it also tells which products the user changed (getEditedParts), so that the host saves those one by one, and adds products (insertProduct, createProduct). In React the hooks follow it: useEditorStatus, useEditorDirty, useEditedParts, useCurrentPart and useAutosave.

Example​

import React from 'react'
import { init, WASMClient } from '@buerli.io/classcad'
import { Buerligons, EditorController, useEditorDirty } from '@buerli.io/buerligons'

declare function store(ofb: Uint8Array): Promise<void> // the host's own storage

init(drawingId => new WASMClient(drawingId, { token: 'ccpk_…' }))
const editor = new EditorController()

export function App() {
const dirty = useEditorDirty(editor)
// A new part to begin with, closed again when the app goes (StrictMode's second run leaves one drawing)
React.useEffect(() => {
editor.open(undefined, 'Bracket').catch(console.error)
return () => void editor.close()
}, [])
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<button disabled={!dirty} onClick={async () => store(await editor.save('OFB'))}>
Save
</button>
<div style={{ flex: 1, minHeight: 0 }}>
<Buerligons welcome={false} menu={null} />
</div>
</div>
)
}

Accessors​

drawingId​
Get Signature​
get drawingId(): DrawingID | null;

The drawing the controller has open; null while none is, also while an open is under way.

Example​
import { createApi } from '@buerli.io/classcad'
import { EditorController, type EditedPart } from '@buerli.io/buerligons'

declare const editor: EditorController
declare const product: EditedPart

// ClassCAD calls of the host's own go to the controller's drawing
const drawingId = editor.drawingId
if (drawingId) {
const api = createApi(drawingId)
await api.v1.common.setObjectName({ id: product.id, name: 'Housing (rev. B)' })
}
Returns​

DrawingID | null

Constructors​

Constructor​
new EditorController(): EditorController;
Returns​

EditorController

Methods​

addInstance()​
addInstance(productId, owner?): Promise<number>;

Adds an instance of a product of the drawing to an assembly: the insert target, or owner.

The assembly that received it counts as edited (unless it is the root).

Parameters​
ParameterTypeDescription
productIdnumberthe product (template) to add an instance of.
owner?ProductUser | nullthe assembly that gets the instance; by default the insert target (insertTarget).
Returns​

Promise<number>

The id of the new instance.

Throws​

Without an assembly to add it to (while the user works inside a part), or where ClassCAD could not create the instance (with its message).

Example​
import { EditorController } from '@buerli.io/buerligons'

declare const editor: EditorController

// One more screw of the kind already there, in the assembly the user is in
const screw = editor.products().find(p => p.name === 'Screw')
if (screw && editor.insertTarget()) await editor.addInstance(screw.id)
clearEditedParts()​
clearEditedParts(ids?): void;

Forgets edited products, all of them or the given ones (after the host has saved them).

Parameters​
ParameterTypeDescription
ids?number[]the products to forget; omitted, all of them.
Returns​

void

close()​
close(): Promise<void>;

Closes the document: removes the drawing from the buerli store, which ends its ClassCAD session.

With an open under way, it waits for it and removes the drawing it opened. The status goes to closed. What was not saved is lost.

Returns​

Promise<void>

Example​
import { EditorController, setLooks } from '@buerli.io/buerligons'

declare const editor: EditorController

// The host leaves the document: it asks before changes are lost, and clears what belonged to it
export async function leaveDocument() {
if (editor.isDirty() && !confirm('Discard the changes?')) return false
setLooks({})
await editor.close()
return true
}
createProduct()​
createProduct(kind, wanted): Promise<EditedPart>;

Creates an empty part or sub-assembly, and adds an instance of it to the insert target.

The product (v1.assembly.partTemplate or v1.assembly.assemblyTemplate) gets the name wanted, made unique among the products (Name_2, Name_3, …). The instance goes into the insert target (insertTarget) as it was before the product was created. The assembly that received it counts as edited (unless it is the root), the new product does not.

Parameters​
ParameterTypeDescription
kind"part" | "assembly"'part' or 'assembly'.
wantedstringthe name the product is to have.
Returns​

Promise<EditedPart>

The new product (template), with the name it got.

Throws​

While the user works inside a part, or where ClassCAD could not create the product (with its message).

Example​
import { EditorController } from '@buerli.io/buerligons'

declare const editor: EditorController
declare function reserveNumber(): Promise<string> // the host's numbering

// The host's "New part" in an assembly: an empty part, an instance of it, and the host's number on it
export async function newPart() {
const part = await editor.createProduct('part', 'Bracket') // 'Bracket', or 'Bracket_2' where that is taken
await editor.setUserData(part.id, { partNumber: await reserveNumber() })
}
currentState()​
currentState(drawingId?): number | null;

Marker of the undo state the drawing is at now; null where there is none (no drawing).

It changes whenever the model does. On a runtime that reports its undo stack (WASM, Socket.IO) it is the state's id, so undoing back to the saved state makes the drawing clean again. The ClassCAD WebSocket client (WSClient) does not report the stack; there it counts the commands that changed the model. EditorStatus.savedState holds the marker last opened or saved.

Parameters​
ParameterTypeDescription
drawingId?DrawingID | nullthe drawing to look at; by default the controller's.
Returns​

number | null

exportDefinition()​
exportDefinition(options?): Promise<ExportedDefinition>;

Writes the assembly of the drawing as a definition (v1.assembly.exportDefinition) and marks its current undo state as saved.

The host stores the definition, and opens it again with open (format: 'JSON'). With mode: 'REFERENCED' the parts and sub-assemblies come as files of their own. The status is saving meanwhile, as with save. It needs an assembly drawing: a part drawing has no assembly to write.

Parameters​
ParameterTypeDescription
options?ExportDefinitionOptionshow the definition is written; by default one self-contained JSON.
Returns​

Promise<ExportedDefinition>

The root assembly's definition, and with REFERENCED its files.

Throws​

Where no drawing is open, or ClassCAD returned no definition (with its message).

Example​
import { EditorController } from '@buerli.io/buerligons'

declare const editor: EditorController
declare const doc: { id: string; kind: 'part' | 'assembly' } // the host's record of the document
declare function putFile(id: string, body: Blob): Promise<void> // the host's server

// The host's Save: an assembly as its definition, a part as OFB
export async function saveDocument() {
if (doc.kind === 'assembly') {
const { json } = await editor.exportDefinition() // one JSON, the parts in it as base64
await putFile(doc.id, new Blob([JSON.stringify(json)], { type: 'application/json' }))
} else {
await putFile(doc.id, new Blob([(await editor.save('OFB')).slice()]))
}
}
exportPart()​
exportPart(id, format?): Promise<Uint8Array<ArrayBufferLike>>;

One product of the drawing as bytes (v1.assembly.exportNode): the part alone, as it would be saved on its own.

It changes neither the status nor the saved state, and the product stays edited until the host clears it.

Parameters​
ParameterTypeDefault valueDescription
idnumberundefinedthe product (template), as EditedPart.id has it.
format?"OFB" | "STP"'OFB''OFB' or 'STP'.
Returns​

Promise<Uint8Array<ArrayBufferLike>>

The file's content.

Throws​

Where no drawing is open, or ClassCAD returned no content (with its message).

Example​
import { EditorController } from '@buerli.io/buerligons'

declare const editor: EditorController
declare function sendToSupplier(fileName: string, content: Uint8Array): Promise<void> // the host's

// The housing alone, as STEP, for a supplier
const housing = editor.products().find(p => p.name === 'Housing')
if (housing) await sendToSupplier('Housing.stp', await editor.exportPart(housing.id, 'STP'))
getEditedParts()​
getEditedParts(): EditedPart[];

The products that were changed since the drawing was opened, or since the host cleared them.

A product is edited when a command that changes the model was issued while it was the current product. In an assembly these are the part templates (and sub-assemblies) the user went into and worked on; the host saves exactly those (exportPart) and clears them (clearEditedParts). In a part drawing it is the part. The root assembly is never one: a change to it shows in isDirty alone. Commands that move around in the model without changing it (going into a part, opening a feature) do not count.

It is the same array until the list changes. In React, useEditedParts follows it.

Returns​

EditedPart[]

Example​
import { EditorController } from '@buerli.io/buerligons'

declare const editor: EditorController
declare function uploadPart(partNumber: string, ofb: Uint8Array): Promise<void> // the host's

// The host's Save in an assembly: each part the user changed goes back on its own, by the host's number
export async function saveChangedParts() {
for (const part of editor.getEditedParts()) {
if (part.kind !== 'part') continue // a sub-assembly goes as a definition (exportDefinition)
const { partNumber } = await editor.getUserData(part.id, ['partNumber'])
if (!partNumber) continue
await uploadPart(partNumber, await editor.exportPart(part.id))
editor.clearEditedParts([part.id])
}
}
getStatus()​
getStatus(): EditorStatus;

The status as it is now: what the controller is doing, its drawing, and the state last saved.

It is the same object until the status changes. In React, useEditorStatus follows it.

Returns​

EditorStatus

getUserData()​
getUserData(id, keys): Promise<Record<string, string>>;

User data of an object of the drawing (v1.common.getUserData), read key by key.

The examples of products and getEditedParts read the host's number of a product with it.

Parameters​
ParameterTypeDescription
idnumberthe object (a product, say).
keysstring[]the keys to read.
Returns​

Promise<Record<string, string>>

The values by key; keys without a value are left out.

Throws​

Where no drawing is open.

insertProduct()​
insertProduct(source): Promise<EditedPart>;

Inserts a product into the assembly: loads it, names it, puts the host's user data on it, and adds an instance.

ClassCAD loads the product from source.url itself: a part with v1.assembly.loadProduct, an assembly definition with v1.assembly.importDefinition as a template. The name is made unique among the products (Name_2, Name_3, …), since a host recognises a template by it. The instance goes into the insert target (insertTarget). The assembly that received it counts as edited (unless it is the root), the loaded product does not. The steps are commands of their own: a failure partway leaves those before it done.

Parameters​
ParameterTypeDescription
sourceInsertSourcewhere the product comes from, its name and the host's user data.
Returns​

Promise<EditedPart>

The new product (template), with the name it got.

Throws​

While the user works inside a part, or where ClassCAD could not load the product (with its message).

Example​
import { EditorController, useCurrentPart } from '@buerli.io/buerligons'

declare const editor: EditorController
type CatalogueItem = { id: string; title: string; ofbUrl: string } // an entry of the host's catalogue

// A catalogue entry, inserted into the assembly the user is in
export function InsertButton({ item }: { item: CatalogueItem }) {
useCurrentPart(editor) // renders again when the user goes into a part, or out of it
const target = editor.insertTarget() // null inside a part
const insert = async () => {
const product = await editor.insertProduct({
url: item.ofbUrl, // fetched by the ClassCAD process itself
format: 'OFB',
name: item.title, // 'Screw', then 'Screw_2', …
userData: { catalogueId: item.id },
})
console.log(`${product.name} is in ${target?.name}`)
}
return (
<button disabled={!target} onClick={insert}>
Insert {item.title}
</button>
)
}
insertTarget()​
insertTarget(): ProductUser | null;

The assembly an inserted product goes into: the sub-assembly the user has gone into, else the root.

null while the user works inside a part of an assembly, and while no drawing is open. In a part drawing it is the part itself, which takes no instances. It is read once: the example of insertProduct keeps an Insert button up to date with it.

Returns​

ProductUser | null

isDirty()​
isDirty(): boolean;

Whether the drawing moved to another undo state since the last open or save; false while no drawing is open.

It is read once; in React, useEditorDirty follows it.

Returns​

boolean

isImportedPart()​
isImportedPart(id): boolean;

Whether a part starts from imported geometry (CC_Import, a STEP file's, say).

Such a part has no feature tree that ClassCAD could rebuild its geometry from, so a host that loaded it without its BRep (as SCG) must not save it. false while no drawing is open.

Parameters​
ParameterTypeDescription
idnumberthe part (template).
Returns​

boolean

Example​
import { EditorController } from '@buerli.io/buerligons'

declare const editor: EditorController

// A host that loads its parts as SCG (graphics without a BRep) saves only the parts ClassCAD can rebuild
const toSave = editor.getEditedParts().filter(p => p.kind === 'part' && !editor.isImportedPart(p.id))
console.log(toSave)
open()​
open(source?, name?): Promise<DrawingID>;

Opens a document in a new drawing: loads source into it, or without a source creates an empty part named name.

The drawing gets the source's name (or name) and becomes the active drawing, which Buerligons shows. A drawing this controller opened before is removed first. An EditorDefinition (format: 'JSON') builds an assembly from its definition (v1.assembly.importDefinition), an EditorSource is loaded with v1.common.load. The status goes to opening, then to ready with the new drawing, whose state counts as saved. Where the document cannot be opened, the drawing goes again (and its ClassCAD session with it), and the status is failed, with ClassCAD's message in error.

Opens and closes take turns: one that comes while another is under way waits for it, so that two in a row (an effect React's StrictMode runs twice, a second pick before the first has loaded) leave the later document open and no drawing behind.

Parameters​
ParameterTypeDefault valueDescription
source?EditorSource | EditorDefinitionundefinedthe file to open, or an assembly definition; omitted, a new part.
name?string'Part'the new part's name, where there is no source.
Returns​

Promise<DrawingID>

The id of the new drawing.

Throws​

An Error with ClassCAD's message, where the document could not be opened.

Example​
import { EditorController, loadFormatOf } from '@buerli.io/buerligons'

declare const editor: EditorController
declare const file: File // picked by the user
declare const definition: string // an assembly definition the host stored (JSON text)

// A new part, named
await editor.open(undefined, 'Bracket')

// A file: OFB, STEP or IWP
const format = loadFormatOf(file.name)
if (format) await editor.open({ name: file.name, format, data: await file.arrayBuffer() })

// An assembly from its definition; a failure says why (and so does editor.getStatus().error)
try {
await editor.open({ name: 'Gearbox', format: 'JSON', data: definition })
} catch (error) {
alert(`The gearbox could not be opened: ${(error as Error).message}`)
}
products()​
products(): EditedPart[];

The products (templates) of the drawing: its parts and sub-assemblies, without the root assembly.

In a part drawing it is the part. None while no drawing is open.

Returns​

EditedPart[]

Example​
import { EditorController } from '@buerli.io/buerligons'

declare const editor: EditorController

// The host's product list: each product with the number the host put on it (setUserData)
for (const product of editor.products()) {
const { partNumber } = await editor.getUserData(product.id, ['partNumber'])
console.log(product.kind, product.name, partNumber ?? '(new)')
}
save()​
save(format?): Promise<Uint8Array<ArrayBufferLike>>;

Saves the drawing as bytes (v1.common.save) and marks its current undo state as saved.

The status is saving meanwhile, then ready again. Every format marks the state as saved, a STEP or STL export too: isDirty is false afterwards. A failure leaves the status ready, with its message in error.

Parameters​
ParameterTypeDefault valueDescription
format?EditorSaveFormat'OFB''OFB' (ClassCAD's own), 'STP' or 'STL'.
Returns​

Promise<Uint8Array<ArrayBufferLike>>

The file's content.

Throws​

Where no drawing is open, or the save failed.

Example​
import { EditorController } from '@buerli.io/buerligons'

declare const editor: EditorController
declare const documentUrl: string // where the host keeps the document

// The host's Save: the bytes go to its server
export async function saveToServer() {
const ofb = await editor.save('OFB') // from here on the drawing counts as saved
// (slice: the bytes on an ArrayBuffer of their own, as the DOM's types want a body)
const response = await fetch(documentUrl, { method: 'PUT', body: ofb.slice() })
if (!response.ok) alert('The server did not take the document. Save it again.')
}
setUserData()​
setUserData(id, data): Promise<void>;

Sets user data on an object of the drawing (v1.common.setUserData): the host's bookkeeping on it.

It is not a change of the model: it is not undoable and does not make the drawing or a part edited. ClassCAD keeps the value of a key that exists already, so the key is removed before it is set.

Parameters​
ParameterTypeDescription
idnumberthe object (a product, say).
dataRecord<string, string>the values by key; other keys stay as they are.
Returns​

Promise<void>

Throws​

Where no drawing is open.

Example​
import { EditorController, type EditedPart } from '@buerli.io/buerligons'

declare const editor: EditorController
declare const part: EditedPart
declare const saved: { number: string; revision: number } // what the host's server answered

// After the host stored a part: its number and revision on the part, for the next save
await editor.setUserData(part.id, { partNumber: saved.number, revision: String(saved.revision) })
subscribe()​
subscribe(listener): () => void;

Calls listener with each new status, from the next change on.

It is for code outside React (the example of EditorStatus shows it); in React, useEditorStatus.

Parameters​
ParameterTypeDescription
listener(status) => voidcalled with the new status.
Returns​

A function that removes the listener again.

() => void

subscribeEditedParts()​
subscribeEditedParts(listener): () => void;

Calls listener with the edited products each time the list changes.

It is for code outside React; in React, useEditedParts.

Parameters​
ParameterTypeDescription
listener(parts) => voidcalled with the new list.
Returns​

A function that removes the listener again.

() => void

usersOf()​
usersOf(productId): ProductUser[];

The assemblies that use a product directly: those with an instance of it among their own children.

A template that sits in two sub-assemblies has both as users; the root assembly is marked (root).

Parameters​
ParameterTypeDescription
productIdnumberthe product (template).
Returns​

ProductUser[]

The assemblies; none while no drawing is open.

Example​
import { EditorController, type EditedPart } from '@buerli.io/buerligons'

declare const editor: EditorController

// Before the host replaces a template: which assemblies use it
export function mayReplace(product: EditedPart) {
const users = editor.usersOf(product.id).map(user => (user.root ? 'the main assembly' : user.name))
return confirm(`${product.name} is used in ${users.join(', ')}. Replace it?`)
}

Hooks​

useEditorStatus()​

function useEditorStatus(controller): EditorStatus;

The status of an EditorController: the component renders again on every change.

Parameters​

ParameterTypeDescription
controllerEditorControllerthe controller to follow.

Returns​

EditorStatus

Its status as it is now (EditorController.getStatus).

Example​

import { EditorController, useEditorStatus } from '@buerli.io/buerligons'

// Where an open failed: what ClassCAD said, and the host's way to try again
export function OpenFailed({ editor, retry }: { editor: EditorController; retry: () => void }) {
const status = useEditorStatus(editor)
if (status.state !== 'failed') return null
return (
<div role="alert">
{status.error} <button onClick={retry}>Try again</button>
</div>
)
}

useEditorDirty()​

function useEditorDirty(controller): boolean;

Whether the drawing is at another undo state than the one last opened or saved, following every change.

It is EditorController.isDirty in React: false while no drawing is open, and again after a save.

Parameters​

ParameterTypeDescription
controllerEditorControllerthe controller to follow.

Returns​

boolean

true while the drawing has changes that were not saved.

Example​

import React from 'react'
import { EditorController, useEditorDirty } from '@buerli.io/buerligons'

// A mark beside the title while there are changes, and the browser asks before the page goes
export function UnsavedMark({ editor }: { editor: EditorController }) {
const dirty = useEditorDirty(editor)
React.useEffect(() => {
if (!dirty) return
const ask = (e: BeforeUnloadEvent) => e.preventDefault()
window.addEventListener('beforeunload', ask)
return () => window.removeEventListener('beforeunload', ask)
}, [dirty])
return dirty ? <span title="Not saved">●</span> : null
}

useEditedParts()​

function useEditedParts(controller): EditedPart[];

The products that were changed since the drawing was opened, or since the host cleared them.

It is EditorController.getEditedParts in React: the component renders again when the list changes.

Parameters​

ParameterTypeDescription
controllerEditorControllerthe controller to follow.

Returns​

EditedPart[]

The edited products.

Example​

import { EditorController, useEditedParts } from '@buerli.io/buerligons'

// The host's list of changed parts beside the app, with its Save for them (see getEditedParts)
export function ChangedParts({ editor, onSave }: { editor: EditorController; onSave: () => void }) {
const parts = useEditedParts(editor)
if (!parts.length) return null
return (
<aside>
<ul>
{parts.map(part => (
<li key={part.id}>
{part.name} ({part.kind})
</li>
))}
</ul>
<button onClick={onSave}>Save {parts.length} changed</button>
</aside>
)
}

useCurrentPart()​

function useCurrentPart(controller): EditedPart | null;

The template the user is working in while an assembly is open: a part or a sub-assembly they went into.

It is the current product when it is a part or a sub-assembly and the drawing is an assembly; null in the root assembly itself, in a part drawing, and while no drawing is open.

Parameters​

ParameterTypeDescription
controllerEditorControllerthe controller to follow.

Returns​

EditedPart | null

The product, or null.

Example​

import { EditorController, useCurrentPart } from '@buerli.io/buerligons'

// The host's breadcrumb: the assembly, and the part or sub-assembly the user went into
export function Breadcrumb({ editor, title }: { editor: EditorController; title: string }) {
const current = useCurrentPart(editor)
return (
<nav>
{title}
{current && ` › ${current.name}`}
</nav>
)
}

useAutosave()​

function useAutosave(controller, options): void;

Saves the drawing as OFB whenever it has been dirty for intervalMs, and hands the bytes to onSnapshot.

It saves while it is enabled and the controller is ready. The save marks the state as saved, as EditorController.save does, before onSnapshot runs: an onSnapshot that fails is not called again until the next change. Errors of the save and of onSnapshot go to onError, and do not stop the autosave. The wait starts again whenever an option changes, so callbacks keep their identity (React.useCallback).

Parameters​

ParameterTypeDescription
controllerEditorControllerthe controller whose drawing is saved.
options{ enabled?: boolean; intervalMs: number; onError?: (e) => void; onSnapshot: (bytes) => Promise<void>; }when to save, and where the bytes go.
options.enabled?booleanfalse stops the autosave; by default true.
options.intervalMsnumberhow long the drawing is dirty before it is saved, in milliseconds.
options.onError?(e) => voidreceives the errors of the save and of onSnapshot.
options.onSnapshot(bytes) => Promise<void>stores the bytes (OFB). A save that comes due while it still runs is left out.

Returns​

void

Example​

import React from 'react'
import { EditorController, useAutosave } from '@buerli.io/buerligons'

declare function saveDraft(documentId: string, ofb: Uint8Array): Promise<void> // the host's server

// A draft after 30 seconds of changes, while the user may edit
export function Autosave(props: { editor: EditorController; documentId: string; mayEdit: boolean }) {
const { editor, documentId, mayEdit } = props
// (callbacks that keep their identity: a new one would start the wait again)
const onSnapshot = React.useCallback((ofb: Uint8Array) => saveDraft(documentId, ofb), [documentId])
const onError = React.useCallback((e: unknown) => console.warn('[autosave]', e), [])
useAutosave(editor, { intervalMs: 30_000, enabled: mayEdit, onSnapshot, onError })
return null
}

Functions​

loadFormatOf()​

function loadFormatOf(fileName): EditorLoadFormat | null;

The load format of a file, by the extension of its name: .ofb, .stp or .step, .iwp, in any case.

Parameters​

ParameterTypeDescription
fileNamestringthe file's name.

Returns​

EditorLoadFormat | null

The format, or null for a type that EditorController.open does not load.

Example​

import { EditorController, loadFormatOf } from '@buerli.io/buerligons'

declare const editor: EditorController

// The host's Open button: a file input for what Buerligons opens
export const OpenButton = () => (
<input
type="file"
accept=".ofb,.stp,.step,.iwp"
onChange={async e => {
const file = e.target.files?.[0]
if (!file) return
const format = loadFormatOf(file.name) // 'OFB', 'STP', 'IWP' or null
if (!format) return alert(`${file.name} is not a file Buerligons opens.`)
await editor.open({ name: file.name, format, data: await file.arrayBuffer() })
}}
/>
)

Types​

EditorDefinition​

type EditorDefinition = {
data: string;
format: "JSON";
name: string;
};

An assembly as a definition (v1.assembly.importDefinition, JSON), for EditorController.open.

The definition holds structure, instances and constraints, with templates that carry their part inline or point at locations the ClassCAD process loads itself. Opened from data, it has no folder that relative locations resolve against: they are URLs or absolute paths. EditorController.exportDefinition writes one.

Example​

import { EditorController, type EditorDefinition } from '@buerli.io/buerligons'

declare const editor: EditorController

// An assembly definition the host keeps (as exportDefinition wrote it), opened in the editor
const response = await fetch('/api/documents/gearbox/definition')
const definition: EditorDefinition = { name: 'Gearbox', format: 'JSON', data: await response.text() }
await editor.open(definition)

Properties​

PropertyTypeDescription
datastringThe definition as JSON text.
format"JSON"Always 'JSON': what tells a definition from an EditorSource.
namestringDrawing name.

EditorSource​

type EditorSource = {
data: ArrayBuffer;
format: EditorLoadFormat;
name: string;
};

A file that EditorController.open opens: its name, format and content.

Example​

import React from 'react'
import { EditorController, loadFormatOf, type EditorSource } from '@buerli.io/buerligons'

declare const editor: EditorController

// A file dropped onto the host's page opens in the editor
export function DropZone({ children }: { children: React.ReactNode }) {
const onDrop = async (e: React.DragEvent) => {
e.preventDefault()
const file = e.dataTransfer.files.item(0)
const format = file && loadFormatOf(file.name)
if (!file || !format) return
const source: EditorSource = { name: file.name, format, data: await file.arrayBuffer() }
await editor.open(source)
}
return (
<div onDragOver={e => e.preventDefault()} onDrop={onDrop} style={{ height: '100vh' }}>
{children}
</div>
)
}

Properties​

PropertyTypeDescription
dataArrayBufferThe file's content (await file.arrayBuffer(), say).
formatEditorLoadFormatThe file's format; loadFormatOf finds it from the file name.
namestringDrawing name, typically the file name of the source.

EditorStatus​

type EditorStatus = {
drawingId: DrawingID | null;
error?: string;
savedState: number | null;
state: EditorState;
};

The status of an EditorController: what it is doing, its drawing, and the state last saved.

EditorController.subscribe and useEditorStatus follow it.

Example​

import { EditorController, type EditorStatus } from '@buerli.io/buerligons'

declare const editor: EditorController
declare const telemetry: { track(event: string, data?: object): void } // the host's

// Outside React: the host follows the status, for its logs
const report = (status: EditorStatus) => {
if (status.state === 'failed') telemetry.track('open-failed', { error: status.error })
if (status.state === 'ready' && status.error) telemetry.track('save-failed', { error: status.error })
}
const unsubscribe = editor.subscribe(report)
// …and when the host's view goes:
unsubscribe()

Properties​

PropertyTypeDescription
drawingIdDrawingID | nullThe drawing that is open; null while none is, also while an open is under way.
error?stringMessage of the last failure: an open's with failed, a save's or an export's with ready. The next open or save clears it.
savedStatenumber | nullMarker of the undo state that was last opened or saved (see EditorController.currentState); null while no drawing is open.
stateEditorStateWhat the controller is doing.

ExportedDefinition​

type ExportedDefinition = {
fileNames?: string[];
files?: ExportedFile[];
json: Record<string, unknown>;
rootFile?: string;
};

What EditorController.exportDefinition returns (the result of v1.assembly.exportDefinition).

Example​

import { EditorController, type ExportedFile } from '@buerli.io/buerligons'

declare const editor: EditorController
declare function toBlob(file: ExportedFile): Blob // see ExportedFile
declare function upload(path: string, body: Blob): Promise<void> // to the host's file server

// Save: each part and sub-assembly as a file of its own, the definitions pointing at the file server
const base = 'https://files.example.com/gearbox/'
const { files = [], rootFile } = await editor.exportDefinition({ mode: 'REFERENCED', baseUrl: base })
for (const file of files) await upload(`gearbox/${file.name}`, toBlob(file))

// Later: the root definition opens again, and its templates load from the file server
const data = await (await fetch(base + rootFile)).text()
await editor.open({ name: 'Gearbox', format: 'JSON', data })

Properties​

PropertyTypeDescription
fileNames?string[]With REFERENCED: names of all files, the root assembly's JSON last.
files?ExportedFile[]With REFERENCED and no dir: the content of all files, the root definition last.
jsonRecord<string, unknown>The root assembly's definition. As JSON text, it opens again with EditorController.open.
rootFile?stringWith REFERENCED: file name of the root assembly's JSON.

InsertSource​

type InsertSource = {
format: "OFB" | "STP" | "SCG" | "JSON";
name: string;
url: string;
userData?: Record<string, string>;
};

A product that EditorController.insertProduct inserts: where ClassCAD loads it from, how the host knows it.

Example​

import { EditorController, type InsertSource } from '@buerli.io/buerligons'

declare const editor: EditorController

// A sub-assembly from the host's library: its definition, which the ClassCAD process fetches itself
const gearStage: InsertSource = {
url: 'https://files.example.com/library/gear-stage/root.json',
format: 'JSON',
name: 'Gear stage',
userData: { libraryId: 'GS-200', revision: 'C' },
}
const product = await editor.insertProduct(gearStage) // kind 'assembly', in the assembly the user is in
console.log(product.name)

Properties​

PropertyTypeDescription
format"OFB" | "STP" | "SCG" | "JSON"OFB, STP or SCG: a part (v1.assembly.loadProduct). JSON: an assembly definition, added as a sub-assembly (importDefinition with asTemplate).
namestringName of the product (template) in the drawing; made unique among the products.
urlstringLocation the ClassCAD process loads the product from: it fetches it itself.
userData?Record<string, string>The host's bookkeeping on the product (see EditorController.setUserData).

Advanced​

Most apps do not need these directly: types they meet through the exports above, and what is there for special cases or for building on buerli itself.

EditedPart​

type EditedPart = {
id: ObjectID;
kind: "part" | "assembly";
name: string;
};

A product of the drawing as a host tracks it: a part, or a sub-assembly (an assembly template, never the root).

EditorController.getEditedParts lists those the user changed since the drawing was opened (or since the host cleared them); EditorController.products, EditorController.insertProduct, EditorController.createProduct and useCurrentPart name products the same way. Their examples show it in use.

Properties​

PropertyTypeDescription
idObjectIDObject id of the product (the template) in the drawing.
kind"part" | "assembly"'part', or 'assembly' for a sub-assembly.
namestringIts name in the drawing.

EditorLoadFormat​

type EditorLoadFormat = "OFB" | "STP" | "IWP";

Formats that EditorController.open loads: OFB (ClassCAD's own), STP (STEP) and IWP (SMLib's).

They are those of v1.common.load. loadFormatOf finds the format from a file name.

Example​

import { EditorController, type EditorLoadFormat } from '@buerli.io/buerligons'

declare const editor: EditorController
// A document as the host's server lists it
declare const doc: { title: string; mimeType: 'application/x-ofb' | 'model/step'; url: string }

// The host's record says what the file is; its bytes come from the host's server
const format: EditorLoadFormat = doc.mimeType === 'model/step' ? 'STP' : 'OFB'
const data = await (await fetch(doc.url)).arrayBuffer()
await editor.open({ name: doc.title, format, data })

EditorSaveFormat​

type EditorSaveFormat = "OFB" | "STP" | "STL";

Formats EditorController.save writes (v1.common.save): OFB (ClassCAD's own), STP (STEP), STL.

STL is a mesh alone, which cannot be opened again.

Example​

import { EditorController, type EditorSaveFormat } from '@buerli.io/buerligons'

declare const editor: EditorController

// The host's Download menu: ClassCAD's own file, STEP for other CAD systems, STL for a printer
const downloads: { format: EditorSaveFormat; label: string; ending: string }[] = [
{ format: 'OFB', label: 'ClassCAD', ending: 'ofb' },
{ format: 'STP', label: 'STEP', ending: 'stp' },
{ format: 'STL', label: 'STL (mesh)', ending: 'stl' },
]

async function download(format: EditorSaveFormat, ending: string) {
const link = document.createElement('a')
link.href = URL.createObjectURL(new Blob([(await editor.save(format)).slice()]))
link.download = `model.${ending}`
link.click()
}

export const DownloadMenu = () => (
<menu>
{downloads.map(d => (
<li key={d.format}>
<button onClick={() => download(d.format, d.ending)}>{d.label}</button>
</li>
))}
</menu>
)

EditorState​

type EditorState = "idle" | "opening" | "ready" | "saving" | "failed" | "closed";

What an EditorController is doing, as its EditorStatus says.

  • idle: nothing opened yet.
  • opening: an open is under way.
  • ready: a drawing is open.
  • saving: a save or a definition export is under way; the drawing stays open.
  • failed: the last open failed; no drawing is open.
  • closed: the drawing was closed. Every open passes through it as it removes the drawing before.

Example​

import { EditorController, useEditorStatus, type EditorState } from '@buerli.io/buerligons'

// What the host's header says in each state
const label: Record<EditorState, string> = {
idle: 'No document',
opening: 'Opening…',
ready: '',
saving: 'Saving…',
failed: 'The document could not be opened',
closed: 'No document',
}

export function StateLabel({ editor }: { editor: EditorController }) {
const { state } = useEditorStatus(editor)
return <span>{label[state]}</span>
}

ExportDefinitionOptions​

type ExportDefinitionOptions = {
baseUrl?: string;
dir?: string;
mode?: "INLINE" | "REFERENCED";
partFormat?: "OFB" | "SCG";
};

Options of EditorController.exportDefinition, which writes the root assembly as a definition.

They are those of v1.assembly.exportDefinition.

Example​

import { EditorController, type ExportDefinitionOptions } from '@buerli.io/buerligons'

declare const editor: EditorController

// One JSON with everything in it (the default)
const inline: ExportDefinitionOptions = { mode: 'INLINE' }
// A file per part and sub-assembly, returned in `files`; the definitions point at the host's file server
const referenced: ExportDefinitionOptions = { mode: 'REFERENCED', baseUrl: 'https://files.example.com/gearbox/' }
// The same, written by a ClassCAD worker (WSClient) into a folder of its machine
const toFolder: ExportDefinitionOptions = { mode: 'REFERENCED', dir: '/var/pdm/gearbox', partFormat: 'OFB' }

const exported = await editor.exportDefinition(referenced)
console.log(inline, toFolder, exported.fileNames)

Properties​

PropertyTypeDescription
baseUrl?stringWith REFERENCED: prefix of the locations the templates point at. By default the file name alone, which importDefinition resolves against the folder of the JSON it loads.
dir?stringWith REFERENCED: full path of a folder the ClassCAD process writes the files to (a local or UNC path it reaches). Without it the content of the files comes back in ExportedDefinition.files.
mode?"INLINE" | "REFERENCED"INLINE (the default): one self-contained JSON, the parts in it as base64 and the sub-assemblies inline. REFERENCED: one file per part template and one JSON per sub-assembly template, which the templates point at.
partFormat?"OFB" | "SCG"Format the parts are written in: OFB (the default) or SCG, which carries the tessellated graphics without a BRep (see EditorController.isImportedPart).

ExportedFile​

type ExportedFile =
| {
content: string;
encoding: "base64";
name: string;
type: "ofb" | "scg";
}
| {
content: unknown;
name: string;
type: "json";
};

A file of a definition exported with mode: 'REFERENCED' and no dir: a part, or the JSON of an assembly.

Union Members​

Type Literal​
{
content: string;
encoding: "base64";
name: string;
type: "ofb" | "scg";
}
NameTypeDescription
contentstringThe part's content, base64-encoded.
encoding"base64"Always 'base64'.
namestringFile name, as the definitions refer to it.
type"ofb" | "scg"A part, in the ExportDefinitionOptions.partFormat it was written in.

Type Literal​
{
content: unknown;
name: string;
type: "json";
}
NameTypeDescription
contentunknownThe definition, as a parsed JSON value.
namestringFile name, as the definitions refer to it.
type"json"The definition of an assembly: a sub-assembly template's, or the root's.

Example​

import type { ExportedFile } from '@buerli.io/buerligons'

// An exported file as a Blob, for an upload: a part's base64 decoded, a definition as JSON text
export function toBlob(file: ExportedFile): Blob {
if (file.type === 'json') return new Blob([JSON.stringify(file.content)], { type: 'application/json' })
const bytes = Uint8Array.from(atob(file.content), c => c.charCodeAt(0))
return new Blob([bytes], { type: 'application/octet-stream' })
}

ProductUser​

type ProductUser = {
id: ObjectID;
name: string;
root: boolean;
};

An assembly of the drawing: the root assembly or an assembly template.

EditorController.usersOf names the assemblies that use a product, EditorController.insertTarget the one an inserted product goes into; their examples show it in use.

Properties​

PropertyTypeDescription
idObjectIDObject id of the assembly.
namestringIts name in the drawing.
rootbooleantrue for the root assembly.