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).
310 lines (309 loc) • 10.9 kB
JavaScript
/**
* Interact with the terrain in the 3D scene.
*
* @remarks
* Available via {@link auto.Forma | Forma}.{@link index.EmbeddedViewSdk.terrain | terrain}.
*/
export class TerrainApi {
#iframeMessenger;
groundTexture;
/** @hidden */
constructor(iframeMessenger) {
this.#iframeMessenger = iframeMessenger;
this.groundTexture = new GroundTextureApi(iframeMessenger);
}
/**
* Fetch the bounding box for the terrain.
*
* @returns Axis-aligned bounding box for the terrain.
* More specifically, the minimum and maximum (x,y,z) values, in the local coordinate system.
*/
async getBbox() {
return await this.#iframeMessenger.sendRequest("scene/terrain/bbox/get");
}
/**
* Retrieves the elevation of the terrain (in meters above sea level) at a specific point within the scene.
*
* If the coordinates are outside the terrain mesh, it returns the minimum elevation in the terrain.
*/
async getElevationAt(request) {
return await this.#iframeMessenger.sendRequest("scene/terrain/get-elevation-at", request);
}
/**
* Returns whether the terrain is internal.
*
* @remarks
* Internal terrain is managed fully within Forma Site Design.
* Non-internal terrain can potentially be coming from other cloud applications,
* including Revit Cloud Model and other Forma Connected Clients.
*
* Certain operations such as {@link TerrainApi.addPads | addPads},
* {@link TerrainApi.applyPads | applyPads}, and
* {@link ProposalApi.replaceTerrain | replaceTerrain} are only supported
* for internal terrains.
*/
async isInternal() {
return await this.#iframeMessenger.sendRequest("scene/terrain/is-internal");
}
/**
* Retrieves all terrain pads defined in the current terrain.
*
* @remarks
* Terrain pads are flat areas applied to the terrain surface.
*
* Each pad defines a polygon boundary at a specific elevation. The terrain mesh
* within the polygon is modified to match the pad's elevation. An optional slope
* angle defines how the terrain transitions from surrounding areas to the pad.
*
* @returns An array of {@link TerrainPad} objects representing all terrain pads.
* Returns an empty array if no pads are defined on the terrain.
*
* @example
* ```typescript
* // Retrieve all terrain pads
* const pads = await Forma.terrain.getPads();
*
* // Log information about each pad
* for (const pad of pads) {
* console.log(`Pad ID: ${pad.id}`);
* console.log(` Elevation: ${pad.elevation}m`);
* console.log(` Vertices: ${pad.coordinates.length}`);
* if (pad.slopeAngle !== undefined) {
* console.log(` Slope angle: ${pad.slopeAngle}°`);
* }
* }
* ```
*/
async getPads() {
return await this.#iframeMessenger.sendRequest("scene/terrain/get-pads");
}
/**
* Adds new terrain pads to the existing pads on the terrain.
*
* @remarks
* This method appends the provided pads to the current set of terrain pads
* without removing or modifying existing ones. Use this when you want to
* incrementally add new flat areas to the terrain while preserving existing pads.
*
* This method is only supported when the terrain is internal.
* Use {@link TerrainApi.isInternal | isInternal} to check whether the terrain is internal.
*
* Each pad must specify its slope using either `slopeAngle` (in degrees) or
* `slopePercentage`, but not both.
*
* @param pads - Array of terrain pads to add.
*
* @example
* ```typescript
* // Add multiple pads with different slope specifications
* await Forma.terrain.addPads([
* {
* id: "7ab85ec80b3e2",
* coordinates: [
* { x: 0, y: 0 },
* { x: 100, y: 0 },
* { x: 100, y: 100 },
* { x: 0, y: 100 }
* ],
* elevation: 50,
* applySlope: true,
* slopeAngle: 30 // Using degrees
* },
* {
* id: "70234fcf791b2",
* coordinates: [
* { x: 200, y: 0 },
* { x: 300, y: 0 },
* { x: 300, y: 100 },
* { x: 200, y: 100 }
* ],
* elevation: 45,
* applySlope: true,
* slopePercentage: 50 // Using percentage
* },
* {
* id: "70234fcf791b3",
* coordinates: [
* { x: 200, y: 0 },
* { x: 300, y: 0 },
* { x: 300, y: 100 },
* { x: 200, y: 100 }
* ],
* elevation: 45,
* applySlope: false // No slope applied; abrupt pad
* }
* ]);
* ```
*
* @see {@link TerrainApi.applyPads} to replace all existing pads.
* @see {@link TerrainApi.getPads} to retrieve current pads.
*/
async addPads(pads) {
this.#validatePadsInput(pads);
await this.#iframeMessenger.sendRequest("scene/terrain/add-pads", pads);
}
/**
* Replaces all existing terrain pads with the provided array of pads.
*
* @remarks
* This method unconditionally sets the terrain pads to the provided array,
* removing any existing pads that are not included in the new array.
* Use this when you want complete control over the terrain pads, or when
* you need to remove pads.
*
* This method is only supported when the terrain is internal.
* Use {@link TerrainApi.isInternal | isInternal} to check whether the terrain is internal.
*
* To remove all pads, call this method with an empty array.
*
* @param pads - Array of terrain pads to set. Pass an empty array to remove all pads.
*
* @example
* ```typescript
* // Replace all pads with a new set
* await Forma.terrain.applyPads([
* {
* id: "7ab85ec80b3e2",
* coordinates: [
* { x: 0, y: 0 },
* { x: 100, y: 0 },
* { x: 100, y: 100 },
* { x: 0, y: 100 }
* ],
* elevation: 50,
* applySlope: true,
* slopeAngle: 30
* }
* ]);
* ```
*
* @example
* ```typescript
* // Remove all terrain pads
* await Forma.terrain.applyPads([]);
* ```
*
* @see {@link TerrainApi.addPads} to add pads without removing existing ones.
* @see {@link TerrainApi.getPads} to retrieve current pads.
*/
async applyPads(pads) {
this.#validatePadsInput(pads);
await this.#iframeMessenger.sendRequest("scene/terrain/apply-pads", pads);
}
#validatePadsInput(pads) {
pads.forEach((value, index) => {
const error = this.#validatePadInput(value);
if (error) {
throw new Error(`Error at index ${index}: ${error}`);
}
});
}
#validatePadInput(pad) {
if (pad.applySlope &&
pad.slopeAngle === undefined &&
pad.slopePercentage === undefined) {
return "Either slopeAngle or slopePercentage must be specified";
}
if (pad.slopeAngle !== undefined &&
(pad.slopeAngle <= 0 || pad.slopeAngle >= 90)) {
return "slopeAngle must be greater than 0 and strictly lower than 90 degrees";
}
if (pad.slopePercentage !== undefined && pad.slopePercentage <= 0) {
return "slopePercentage must be larger than 0";
}
return undefined;
}
}
/**
* Manage ground textures applied to the terrain object in the 3D scene.
*
* @remarks
* Available via {@link auto.Forma | Forma}.{@link index.EmbeddedViewSdk.terrain | terrain}.{@link terrain.TerrainApi.groundTexture | groundTexture}.
*/
export class GroundTextureApi {
#iframeMessenger;
/** @hidden */
constructor(iframeMessenger) {
this.#iframeMessenger = iframeMessenger;
}
/**
* Add a ground texture to the terrain.
*
* @example
* // Create canvas
* const canvas = document.createElement("canvas");
* canvas.width = 100;
* canvas.height = 100;
*
* // Fill canvas with blue color
* const ctx = canvas.getContext("2d");
* if (ctx) {
* ctx.fillStyle = "blue";
* ctx.fillRect(0, 0, 100, 100);
*
* // Add canvas as ground texture to position (0, 0) in the local coordinate system.
* // The texture will cover a 100x100 meter square area on the terrain,
* // with lower left corner in (x: -50, y: -50) and upper right corner in (x: 50, y: 50).
* await Forma.terrain.groundTexture.add({
* name: "myGroundTexture",
* canvas,
* position: { x: 0, y: 0, z: 1 },
* scale: { x: 1, y: 1 },
* });
* }
*/
async add(request) {
const canvasUrl = request.canvas.toDataURL();
await this.#iframeMessenger.sendRequest("scene/terrain/ground-texture/add", {
name: request.name,
canvasUrl,
position: request.position,
scale: request.scale,
});
}
/**
* Update the texture data for an existing ground texture object.
*
* @example
* // Create a new canvas filled with red and update the ground texture with this new texture
* const newCanvas = document.createElement("newCanvas");
* newCanvas.width = 100;
* newCanvas.height = 100;
* const ctx = newCanvas.getContext("2d");
* if (ctx) {
* ctx.fillStyle = "red";
* ctx.fillRect(0, 0, 100, 100);
* await Forma.terrain.groundTexture.updateTextureData({
* name: "myGroundTexture",
* canvas: newCanvas,
* });
* }
*/
async updateTextureData(request) {
const canvasUrl = request.canvas.toDataURL();
await this.#iframeMessenger.sendRequest("scene/terrain/ground-texture/update-texture-data", { name: request.name, canvasUrl });
}
/**
* Update the placement of an existing ground texture object.
*
* @example
* // Move "myGroundTexture" to (100, 100) in the local coordinate system.
* await Forma.terrain.groundTexture.updatePosition({
* name: "myGroundTexture",
* position: { x: 100, y: 100, z: 1 }
* })
*/
async updatePosition(request) {
await this.#iframeMessenger.sendRequest("scene/terrain/ground-texture/update-position", request);
}
/**
* Remove an existing ground texture object.
*
* @example
* // Remove "myGroundTexture".
* await Forma.terrain.groundTexture.remove({ name: "myGroundTexture" })
*/
async remove(request) {
await this.#iframeMessenger.sendRequest("scene/terrain/ground-texture/remove", request);
}
}