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
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
| Parameter | Type | Description |
|---|---|---|
productId | number | the product (template) to add an instance of. |
owner? | ProductUser | null | the 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
| Parameter | Type | Description |
|---|---|---|
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
| Parameter | Type | Description |
|---|---|---|
kind | "part" | "assembly" | 'part' or 'assembly'. |
wanted | string | the 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
| Parameter | Type | Description |
|---|---|---|
drawingId? | DrawingID | null | the 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
| Parameter | Type | Description |
|---|---|---|
options? | ExportDefinitionOptions | how 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
| Parameter | Type | Default value | Description |
|---|---|---|---|
id | number | undefined | the 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
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
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
| Parameter | Type | Description |
|---|---|---|
id | number | the object (a product, say). |
keys | string[] | 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
| Parameter | Type | Description |
|---|---|---|
source | InsertSource | where 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
| Parameter | Type | Description |
|---|---|---|
id | number | the 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
| Parameter | Type | Default value | Description |
|---|---|---|---|
source? | EditorSource | EditorDefinition | undefined | the 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
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
| Parameter | Type | Default value | Description |
|---|---|---|---|
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
| Parameter | Type | Description |
|---|---|---|
id | number | the object (a product, say). |
data | Record<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
| Parameter | Type | Description |
|---|---|---|
listener | (status) => void | called 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
| Parameter | Type | Description |
|---|---|---|
listener | (parts) => void | called 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
| Parameter | Type | Description |
|---|---|---|
productId | number | the product (template). |
Returns
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
| Parameter | Type | Description |
|---|---|---|
controller | EditorController | the controller to follow. |
Returns
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
| Parameter | Type | Description |
|---|---|---|
controller | EditorController | the 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
| Parameter | Type | Description |
|---|---|---|
controller | EditorController | the controller to follow. |
Returns
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
| Parameter | Type | Description |
|---|---|---|
controller | EditorController | the 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
| Parameter | Type | Description |
|---|---|---|
controller | EditorController | the 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? | boolean | false stops the autosave; by default true. |
options.intervalMs | number | how long the drawing is dirty before it is saved, in milliseconds. |
options.onError? | (e) => void | receives 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
| Parameter | Type | Description |
|---|---|---|
fileName | string | the 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
| Property | Type | Description |
|---|---|---|
data | string | The definition as JSON text. |
format | "JSON" | Always 'JSON': what tells a definition from an EditorSource. |
name | string | Drawing 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
| Property | Type | Description |
|---|---|---|
data | ArrayBuffer | The file's content (await file.arrayBuffer(), say). |
format | EditorLoadFormat | The file's format; loadFormatOf finds it from the file name. |
name | string | Drawing 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
| Property | Type | Description |
|---|---|---|
drawingId | DrawingID | null | The drawing that is open; null while none is, also while an open is under way. |
error? | string | Message 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. |
savedState | number | null | Marker of the undo state that was last opened or saved (see EditorController.currentState); null while no drawing is open. |
state | EditorState | What 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
| Property | Type | Description |
|---|---|---|
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. |
json | Record<string, unknown> | The root assembly's definition. As JSON text, it opens again with EditorController.open. |
rootFile? | string | With 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
| Property | Type | Description |
|---|---|---|
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). |
name | string | Name of the product (template) in the drawing; made unique among the products. |
url | string | Location 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
| Property | Type | Description |
|---|---|---|
id | ObjectID | Object id of the product (the template) in the drawing. |
kind | "part" | "assembly" | 'part', or 'assembly' for a sub-assembly. |
name | string | Its 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
| Property | Type | Description |
|---|---|---|
baseUrl? | string | With 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? | string | With 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";
}
| Name | Type | Description |
|---|---|---|
content | string | The part's content, base64-encoded. |
encoding | "base64" | Always 'base64'. |
name | string | File 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";
}
| Name | Type | Description |
|---|---|---|
content | unknown | The definition, as a parsed JSON value. |
name | string | File 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
| Property | Type | Description |
|---|---|---|
id | ObjectID | Object id of the assembly. |
name | string | Its name in the drawing. |
root | boolean | true for the root assembly. |