forma-embedded-view-sdk
Version:
The Forma Embedded View SDK is a JavaScript library for creating custom extensions in Autodesk Forma Site Design (previously Spacemaker).
289 lines (288 loc) • 11.7 kB
JavaScript
/**
* Access proposal metadata and add new elements to it.
*
* @remarks
* Available via {@link auto.Forma | Forma}.{@link index.EmbeddedViewSdk.proposal | proposal}.
*/
export class ProposalApi {
#iframeMessenger;
/** @hidden */
constructor(iframeMessenger) {
this.#iframeMessenger = iframeMessenger;
}
/**
* Fetch the top level URN for the proposal.
*
* @returns Root URN
*
* @example
* const rootUrn = await Forma.proposal.getRootUrn()
*/
async getRootUrn() {
return await this.#iframeMessenger.sendRequest("proposal/get-root-urn");
}
/**
* Fetch the unique identifier of the proposal.
*
* @returns Proposal ID
*
* @example
* const proposalId = await Forma.proposal.getId()
*/
async getId() {
return await this.#iframeMessenger.sendRequest("proposal/get-id");
}
/**
* Add a new element to the proposal element tree.
*
* Requires edit access. See {@link EmbeddedViewSdk.getCanEdit | getCanEdit} for more info.
*
* @returns { path: string } object with the path of the new element
*
* @example
* const urn = mockRegisterElementInSystem() // See e.g. integrate-elements module
* const { path } = await Forma.proposal.addElement({ urn })
*/
async addElement(request) {
return await this.#iframeMessenger.sendRequest("proposal/add-element", request);
}
/**
* Replace an element in the proposal.
*
* Requires edit access. See {@link EmbeddedViewSdk.getCanEdit | getCanEdit} for more info.
*
* @example
* const urn = mockRegisterElementInSystem() // See e.g. integrate-elements module
* const { path } = await Forma.proposal.addElement({ urn })
* const newUrn = mockRegisterElementInSystem() // See e.g. integrate-elements module
* await Forma.proposal.replaceElement({ path, newUrn })
*/
async replaceElement(request) {
await this.#iframeMessenger.sendRequest("proposal/replace-element", request);
}
/**
* Remove an element in the proposal.
*
* Requires edit access. See {@link EmbeddedViewSdk.getCanEdit | getCanEdit} for more info.
*
* @example
* Remove the first of the selected elements
* ```ts
* const selection = await Forma.selection.getSelection()
* if (selection.length > 0) {
* await Forma.proposal.removeElement({ path: selection[0] })
* }
* ```
*/
async removeElement(request) {
await this.#iframeMessenger.sendRequest("proposal/remove-element", request);
}
/**
* Replace existing terrain on the proposal.
*
* Requires edit access. See {@link EmbeddedViewSdk.getCanEdit | getCanEdit} for more info.
*
* @remarks
* This method is not supported for non-internal terrains.
* Use {@link TerrainApi.isInternal | Forma.terrain.isInternal} to check whether
* the terrain is internal before calling this method.
*
* @example
* const glb = createGlbSomehow()
* await Forma.proposal.replaceTerrain({ glb })
*/
async replaceTerrain(request) {
await this.#iframeMessenger.sendRequest("proposal/terrain/replace", request);
}
/**
* Executes a batch of element operations (`add`, `replace`, `remove`) within a single proposal.
* For more info see {@link ProposalApi.addElement | addElement}, {@link ProposalApi.replaceElement | replaceElement} and {@link ProposalApi.removeElement | removeElement}.
*
* This method allows for efficient bulk modifications of elements by combining multiple add, replace, and/or remove operations into a single call.
* Operations are performed in the sequence they are provided. This method is useful for applying complex changes to a scene with minimal requests.
*
* Note: This operation requires edit access. Use {@link EmbeddedViewSdk.getCanEdit | getCanEdit} to verify edit permissions.
*
* @param request - An array of objects detailing the operations to be performed.
* Each object should specify the `operation` type (`"add"`, `"replace"`, `"remove"`) and the required parameters.
*
* @returns A Promise that resolves to an array of results, one for each operation. For added elements, the result includes the new element path.
*
* @example
* // Example: Adding three new elements to the scene
* const urn1 = mockRegisterElementInSystem(); // Register a new element and obtain its urn
* const urn2 = mockRegisterElementInSystem(); // Register another new element
* const urn3 = mockRegisterElementInSystem(); // Register another new element
*
* const addElementOperations = [
* { type: "add", urn: urn1 },
* { type: "add", urn: urn2 },
* { type: "add", urn: urn3 },
* ];
*
* // Example: Modifying three existing elements in the scene i.e. replacing the element at the first path
* // with the second element and removing the elements at the second and third paths
* const results = await Forma.proposal.updateElements({ operations: addElementOperations })
* const paths = results.map((result) => result?.path);
* console.log(`Added elements at paths: ${paths.join(", ")}`);
*
* const modifyElementOperations = [
* { type: "replace", urn: urn2, path: paths[0] },
* { type: "remove", path: paths[1] },
* { type: "remove", path: paths[2] },
* ];
*
* await Forma.proposal.updateElements({ operations: modifyElementOperations })
*
* @throws {Error} If the user does not have edit access, provides invalid paths or attempts to modify terrain element.
*/
async updateElements(request) {
return await this.#iframeMessenger.sendRequest("proposal/update-elements", request);
}
/**
* Subscribe to changes in the proposal.
*
* By default this will be called for every change in the proposal, including changes that might not be persisted as a separate change.
*
* This can be changed by setting the `debouncedPersistedOnly` option to true, in which case the callback will only be called after the proposal is persisted as well.
* If multiple changes are made in quick succession, the callback will be called only once for all changes.
*
* @example
* Forma.proposal.subscribe(
* async ({ rootUrn }) => {
* console.log(`Proposal ${rootUrn} has been created and persisted`)
* },
* {
* debouncedPersistedOnly: true,
* },
* )
* @param callback event handler for each proposal change
* @param options.debouncedPersistedOnly - if true, the callback will be called only after the proposal is persisted.
* @returns { unsubscribe } - Object with an `unsubscribe` function to stop listening to events.
*/
async subscribe(callback, options) {
return await this.#iframeMessenger.createSubscription("proposal/on-change", callback, options);
}
/**
* This function is resolved when the currently loaded proposal is properly persisted in the system.
* The underlying host application can operate optimistically and return values before the proposal is persisted.
* While other APIs require the proposal to be persisted before they can be used. Examples of such APIs are the elements API.
*
* If you subscribe to proposal updates, you should likely instead look at the `debouncedPersistedOnly` option in the `subscribe` function.
*/
async awaitProposalPersisted() {
await this.#iframeMessenger.sendRequest("proposal/await-current-proposal-persisted");
}
/**
* This will get all proposals belongs to the current project.
*
* @returns ProposalElement[]
*
* @example
* const proposals = await Forma.proposal.getAll()
*/
async getAll() {
return await this.#iframeMessenger.sendRequest("proposal/get-all");
}
/**
* This will get the proposal with the given ID and/or revision.
*
* @returns ProposalElement
*
* @example
* const proposalId = await Forma.proposal.getId()
* const proposal = await Forma.proposal.get({ proposalId: proposalId })
*/
async get(request) {
return await this.#iframeMessenger.sendRequest("proposal/get-by-id", request);
}
/**
* This will create a new proposal with the given name, terrain, base, and children.
*
* @returns { urn: Urn } object with the URN of the new proposal
*
* @example
* const newProposal = await Forma.proposal.create({
* name: "New Proposal",
* terrain: { urn: "urn:adsk-forma-elements:terrain:projectId:elementId:revision", key: "<key>" },
* base: { urn: "urn:adsk-forma-elements:base:projectId:elementId:revision", key: "<key>" },
* children: [],
* })
*/
async create(request) {
return await this.#iframeMessenger.sendRequest("proposal/create", request);
}
/**
* This will update the proposal with the given ID and revision.
*
* @returns { urn: Urn } object with the URN of the updated proposal
*
* @example
* const proposalId = await Forma.proposal.getId()
* const updatedProposal = await Forma.proposal.update({
* proposalId: proposalId,
* revision: "revisionId",
* proposal: {
* name: "Updated Proposal",
* terrain: { urn: "urn:adsk-forma-elements:terrain:projectId:elementId:revision", key: "<key>" },
* base: { urn: "urn:adsk-forma-elements:base:projectId:elementId:revision", key: "<key>" },
* children: [],
* }
* })
*/
async update(request) {
return await this.#iframeMessenger.sendRequest("proposal/update", request);
}
/**
* This will delete the proposal with the given ID. And load the next available proposal in the canvas.
*
* @example
* const proposalId = await Forma.proposal.getId()
* await Forma.proposal.delete({
* proposalId: proposalId,
* })
*
* @remarks
* This is a soft delete, meaning the proposal can be restored.
*
* @experimental
*/
async delete(request) {
await this.#iframeMessenger.sendRequest("proposal/delete", request);
}
/**
* This will create a new proposal with the same elements as the original.
*
* @experimental
*
* @returns { urn: Urn } object with the URN of the duplicated proposal
*
* @example
* const proposalId = await Forma.proposal.getId()
* const duplicatedProposal = await Forma.proposal.duplicate({
* proposalId: proposalId,
* })
*/
async duplicate(request) {
return await this.#iframeMessenger.sendRequest("proposal/duplicate", request);
}
/**
* Switch to a different proposal.
*
* @example
* const proposals = await Forma.proposal.getAll()
* await Forma.proposal.switch({
* proposalId: proposals[0].urn.split(":")[4],
* revision: proposals[0].urn.split(":")[5],
* })
*
* @remarks
* This will switch to the proposal with the given ID and/or revision.
* If your extension loads data from the proposal or operates on the current proposal in any way,
* you should listen to the `proposal change` event using the {@link subscribe} method to know when the proposal has been switched.
* Otherwise, you might be operating on the wrong proposal or the proposal contextual data might be incorrect.
*/
async switch(request) {
await this.#iframeMessenger.sendEvent("proposal/switch", request);
}
}