# @itwin/scenes-client

## About

This package provides a TypeScript client and types for interacting with the [Scenes API](https://developer.bentley.com/apis/scenes/).

## Installation

```bash
npm install @itwin/scenes-client
# or
pnpm add @itwin/scenes-client
```

## Usage

### Basic Client Setup

```ts
import { SceneClient } from "@itwin/scenes-client";

const client = new SceneClient(async () => "<itwin_platform_auth_token>");
```

---

### Working with Scenes

#### Get a Scene

```ts
import { OrderByProperties } from "@itwin/scenes-client";

const sceneResponse = await client.getScene({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  orderBy: OrderByProperties.NAME, // Optional property to order scene data by
});

console.log(sceneResponse.scene);
/*
{
  id: "<scene_id>",
  displayName: "My Scene",
  iTwinId: "<itwin_id>",
  createdById: "<creator_id>",
  creationTime: "2025-01-01T10:00:00.000Z",
  lastModified: "2025-01-01T10:01:00.000Z",
  sceneData: { objects: [...] },
  isPartial: false
}
*/
```

#### Get List of Scenes for an iTwin

```ts
import { SceneMinimal } from "@itwin/scenes-client";

// Get a single page of scenes (for UI pagination)
const listResponse = await client.getScenes({
  iTwinId: "<itwin_id>",
  top: 10, // Optional, defaults to 100
  skip: 5, // Optional, defaults to 0
});

console.log(`Found ${listResponse.scenes.length} scenes on this page.`);
listResponse.scenes.forEach((scene: SceneMinimal) => {
  console.log(`${scene.displayName} (${scene.id})`);
});
```

#### Get All Scenes with Iterator

```ts
import { SceneMinimal } from "@itwin/scenes-client";

// Get all scenes using async iterator
const allScenesIterator = await client.getAllScenes({
  iTwinId: "<itwin_id>",
});

let totalScenes = 0;
for await (const page of allScenesIterator) {
  console.log(`Processing ${page.scenes.length} scenes...`);
  page.scenes.forEach((scene: SceneMinimal) => {
    console.log(`${scene.displayName}`);
    totalScenes++;
  });
}

console.log(`Processed ${totalScenes} total scenes`);
```

#### Create a Scene

```ts
const createResponse = await client.postScene({
  iTwinId: "<itwin_id>",
  scene: {
    displayName: "Construction Site Overview",
    sceneData: {
      objects: [
        /** (optional) objects to create */
      ],
    },
  },
});

console.log(`Created scene: ${createResponse.scene!.displayName}`);
```

#### Replace a Scene

```ts
// Fully replace an existing scene by id. Will create a new scene if provided sceneId does not exist.
const putResponse = await client.putScene({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  scene: {
    displayName: "Full Scene Payload",
    sceneData: {
      objects: [
        /** (optional) full list of scene objects */
      ],
    },
  },
});

console.log(`Created or replaced scene: ${putResponse.scene!.displayName}`);
```

#### Update a Scene's metadata

```ts
const updateResponse = await client.patchScene({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  scene: {
    displayName: "Updated Scene Name",
  },
});

console.log(`Updated scene: ${updateResponse.scene!.displayName}`);
```

#### Delete a Scene

```ts
await client.deleteScene({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
});

console.log("Scene deleted successfully");
```

### Working with Scene Objects

#### Get a Scene Object

```ts
const objectResponse = await client.getObject({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  objectId: "<object_id>",
});

console.log(objectResponse.object);
/*
{
  id: "<object_id>",
  sceneId: "<scene_id>",
  displayName: "My View3d Object",
  kind: "View3d",
  version: "1.0.0",
  data: { ... },
  createdById: "<creator_id>",
  creationTime: "2025-01-01T10:00:00.000Z",
  lastModified: "2025-01-01T10:01:00.000Z"
}
*/
```

#### Get List of Objects in a Scene

```ts
import { OrderByProperties, SceneObject } from "@itwin/scenes-client";

// Get a single page of objects (for UI pagination)
const listResponse = await client.getObjects({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  orderBy: OrderByProperties.NAME, // Optional property to order results by
  top: 10, // Optional, defaults to 100
  skip: 5, // Optional, defaults to 0
});

console.log(`Found ${listResponse.objects.length} objects on this page.`);
listResponse.objects.forEach((object: SceneObject) => {
  console.log(`${object.displayName} (${object.id})`);
});
```

#### Get All Objects with Iterator

```ts
import { SceneObject } from "@itwin/scenes-client";

// Get all objects in a scene using async iterator
const allObjectsIterator = await client.getAllObjects({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
});

let totalObjects = 0;
for await (const page of allObjectsIterator) {
  console.log(`Processing ${page.objects.length} objects...`);
  page.objects.forEach((object: SceneObject) => {
    console.log(`${object.id}`);
    totalObjects++;
  });
}

console.log(`Processed ${totalObjects} total objects`);
```

#### Create Scene Objects

```ts
import {
  SceneObject,
  iModelVisibilityCreate,
  LayerCreate,
  RepositoryCreate,
  RepositoryResourceCreate,
  View3dCreate,
} from "@itwin/scenes-client";

// Create objects with strongly typed interfaces
// Note: LayerCreate is an alias for StandardObjectCreate<"Layer", "1.0.0">
const layer: LayerCreate = {
  id: "<layer_id>", // Optional, will be auto-generated if not provided
  kind: "Layer",
  version: "1.0.0",
  displayName: "Buildings", // Optional
  visible: true, // Optional, sets initial visibility state
  data: {},
};

// Note: RepositoryResourceCreate is an alias for StandardObjectCreate<"RepositoryResource", "1.0.0">
const baseMapResource: RepositoryResourceCreate = {
  kind: "RepositoryResource",
  version: "1.0.0",
  displayName: "Cesium Base Map Imagery", // Optional
  parentId: "<layer_id>", // Organize under the layer
  visible: true, // Optional, sets initial visibility state
  displayOrder: 0, // Optional, sets the rendering order
  data: {
    class: "Cesium",
    repositoryId: "cesium",
    id: "<cesium_imagery_id>",
  },
};

const iModelResource: RepositoryResourceCreate = {
  id: "<imodel_object_id>", // Optional
  kind: "RepositoryResource",
  version: "1.0.0",
  displayName: "Main Building Model", // Optional
  parentId: "<layer_id>", // Organize under the layer
  visible: true, // Optional, sets initial visibility state
  data: {
    iTwinId: "<itwin_id>",
    class: "iModels",
    repositoryId: "imodels",
    id: "<imodel_id>",
    version: "<imodel_changeset_id>" // Optional, if omitted latest should be used
  },
};

// Note: iModelVisibilityCreate is an alias for ResourceStylingObjectCreate<"iModelVisibility", "1.0.0">
const iModelStyling: iModelVisibilityCreate = {
  kind: "iModelVisibility",
  version: "1.0.0",
  displayName: "Hide Building Elements",
  relatedId: "<imodel_object_id>", // References the iModel resource to style
  data: {
    categories: {
      shownList: "",
      hiddenList: "+300000000A0+ED1+3*2+4+D+3*2+8+4*3+3*5+2+3*4+4+3*2+4*2+3*3+5+4+5+4+8+3*2+5+4+7F",
    },
    models: {
      shownList: "",
      hiddenList: "+20000000002",
    },
    perModelCategoryVisibility: [
      { modelId: "0x20000000079", categoryId: "0x2000000003e", visible: false },
    ],
    alwaysDrawn: "+1*4+4*3",
    neverDrawn: "+21234567890+10000000000*2+20000000000",
  },
};

// Note: View3dCreate is an alias for StandardObjectCreate<"View3d", "1.0.0">
const view3d: View3dCreate = {
  kind: "View3d",
  version: "1.0.0",
  displayName: "Aerial View", // Optional
  data: {
    position: { x: -50.0, y: 75.0, z: 150.0 },
    direction: { x: 0.2, y: 0.2, z: -0.96 },
    isOrthographic: false,
    up: { x: 0, y: 1, z: 0 },
    aspectRatio: 1.33,
    near: 1,
    far: 1000,
    ecefTransform: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
  },
};

// Note: RepositoryCreate is an alias for StandardObjectCreate<"Repository", "1.0.0">
const formsRepository: RepositoryCreate = {
  kind: "Repository",
  version: "1.0.0",
  displayName: "iTwin A Forms", // Optional
  data: {
    iTwinId: "<itwin_id>",
    repositoryId: "forms",
    class: "Forms",
  },
};

// Create objects in bulk
const createResponse = await client.postObjects({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  objects: [layer, view3d, baseMapResource, iModelResource, iModelStyling, formsRepository],
});

console.log(`Created ${createResponse.objects.length} objects:`);
createResponse.objects.forEach((obj: SceneObject) => {
  console.log(`Created ${obj.kind} object: ${obj.displayName} (${obj.id})`);
});
```

#### Update Scene Objects

```ts
import {
  SceneObject,
  SceneObjectUpdate,
  SceneObjectOperation,
  OperationType,
} from "@itwin/scenes-client";

// Update data for a single object with type safety
const objectUpdate: SceneObjectUpdate<"GoogleTilesStyling", "1.0.0"> = {
  displayName: "Updated Global Styling Options",
  data: {
    // Fully typed - IntelliSense shows available properties
    quality: 0.30000001192092896,
    adjustment: [1.309999942779541, 72.0326, -75.6275],
  },
};

const updateResponse = await client.patchObject({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  objectId: "<object_id>",
  object: objectUpdate,
});

console.log(`Updated object: ${updateResponse.object.displayName}`);

// Update many scene objects with atomic operations
// All operations execute in provided order
const operations: SceneObjectOperation[] = [
  // 1: Add a new Layer object
  {
    op: OperationType.CREATE,
    payload: {
      id: "<new_layer_id>", // Optional, will be auto-generated if not provided
      kind: "Layer",
      version: "1.0.0",
      displayName: "New Construction Phase", // Optional
      visible: true, // Optional, sets initial visibility state
      data: {},
    },
  },
  //2: Move existing object to the new layer
  {
    op: OperationType.UPDATE,
    id: "<existing_object_id>",
    payload: { parentId: "<new_layer_id>" },
  },
  // 3: Remove old Layer
  { op: OperationType.DELETE, id: "<old_layer_id>" },
  // 4-6: Reorder scene objects
  { op: OperationType.UPDATE, id: "<object_id_1>", payload: { order: 1 } },
  { op: OperationType.UPDATE, id: "<object_id_2>", payload: { order: 2 } },
  { op: OperationType.UPDATE, id: "<object_id_3>", payload: { order: 3 } },
  // 7-8: Set rendering order
  { op: OperationType.UPDATE, id: "<object_id_3>", payload: { displayOrder: 1 } },
  { op: OperationType.UPDATE, id: "<object_id_1>", payload: { displayOrder: 0 } },
];

const bulkUpdateResponse = await client.patchObjectsOperations({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  operations, // Maximum 100 operations per request
});

bulkUpdateResponse.objects.forEach((obj: SceneObject) => {
  console.log(`Created or updated ${obj.kind} object: ${obj.displayName} (${obj.id})`);
});
```

#### Delete Scene Objects

```ts
// Delete a single object by id
await client.deleteObject({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  objectId: "<object_id>",
});

// Delete objects in bulk
await client.deleteObjects({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  objectIds: ["<object_id_1>", "<object_id_2>", "<object_id_3>"],
});
```

### Working with Tags

Tags are iTwin-scoped labels that can be created independently and then applied to scenes. Scenes expose their tags as an array of `TagMinimal` objects (`id` and `displayName`). Assigning or replacing a scene's tags is done via the `tagIds` field when creating or updating a scene.

#### Create a Tag

```ts
import { Tag } from "@itwin/scenes-client";

const createTagResponse = await client.postTag({
  iTwinId: "<itwin_id>",
  tag: {
    id: "<optional_tag_id>", // Optional, will be auto-generated if not provided
    displayName: "Structural",
  },
});

const tag: Tag = createTagResponse.tag;
console.log(`Created tag: ${tag.displayName} (${tag.id})`);
```

#### Get a Tag

```ts
const tagResponse = await client.getTag({
  iTwinId: "<itwin_id>",
  tagId: "<tag_id>",
});

console.log(tagResponse.tag);
/*
{
  id: "<tag_id>",
  displayName: "Structural",
  iTwinId: "<itwin_id>",
  createdById: "<creator_id>",
  creationTime: "2025-01-01T10:00:00.000Z",
  lastModified: "2025-01-01T10:01:00.000Z"
}
*/
```

#### Get List of Tags for an iTwin

```ts
// Get a single page of tags (for UI pagination)
const listResponse = await client.getTags({
  iTwinId: "<itwin_id>",
  top: 10, // Optional, defaults to 100
  skip: 0, // Optional, defaults to 0
});

console.log(`Found ${listResponse.tags.length} tags on this page.`);
listResponse.tags.forEach((tag) => {
  console.log(`${tag.displayName} (${tag.id})`);
});
```

#### Get All Tags with Iterator

```ts
// Get all tags using async iterator
const allTagsIterator = await client.getAllTags({
  iTwinId: "<itwin_id>",
});

let totalTags = 0;
for await (const page of allTagsIterator) {
  page.tags.forEach((tag) => {
    console.log(`${tag.displayName} (${tag.id})`);
    totalTags++;
  });
}

console.log(`Processed ${totalTags} total tags`);
```

#### Update a Tag

```ts
const updateTagResponse = await client.patchTag({
  iTwinId: "<itwin_id>",
  tagId: "<tag_id>",
  tag: {
    displayName: "Structural",
  },
});

console.log(`Updated tag: ${updateTagResponse.tag.displayName}`);
```

#### Delete a Tag

```ts
await client.deleteTag({
  iTwinId: "<itwin_id>",
  tagId: "<tag_id>",
});

console.log("Tag deleted successfully");
```

#### Assign Tags to a Scene

Pass `tagIds` when creating a scene to apply existing tags to it:

```ts
const createResponse = await client.postScene({
  iTwinId: "<itwin_id>",
  scene: {
    displayName: "Tagged Scene",
    tagIds: ["<tag_id_1>", "<tag_id_2>"],
    sceneData: {
      objects: [
        /** (optional) full list of scene objects */
      ]
    },
  },
});

console.log(`Scene tags:`, createResponse.scene!.tags);
// [{ id: "<tag_id_1>", displayName: "Structural" }, { id: "<tag_id_2>", displayName: "Civil" }]
```

#### Update a Scene's Tags

Use `patchScene` with `tagIds` to replace the full set of tags on an existing scene. Provide an empty array to remove all tags:

```ts
// Replace the scene's tags with a new set
const updateResponse = await client.patchScene({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  scene: {
    tagIds: ["<tag_id_3>"], // Replaces all existing tags
  },
});

console.log(`Updated scene tags:`, updateResponse.scene!.tags);
// [{ id: "<tag_id_3>", displayName: "Site Perimeter" }]

// Remove all tags from a scene
await client.patchScene({
  iTwinId: "<itwin_id>",
  sceneId: "<scene_id>",
  scene: {
    tagIds: [],
  },
});
```

---

### Type Safety

This client provides strongly typed interfaces for all scene object operations, giving you compile-time validation:

```ts
import { StandardObjectCreate, ResourceStylingObjectCreate } from "@itwin/scenes-client";

// Each schema has its own typed interface
const camera: StandardObjectCreate<"CameraAnimation", "1.0.0"> = {
  kind: "CameraAnimation",
  version: "1.0.0",
  displayName: "Site Walkthrough",
  data: {
    input: [0, 5, 10, 15], // TypeScript knows this should be number[]
    output: [
      {
        camera: {
          position: { x: 0, y: 0, z: 100 },
          direction: { x: 0, y: 1, z: 0 },
          up: { x: 0, y: 0, z: 1 },
          // IntelliSense shows all available camera properties
        },
      },
    ],
  },
};

// Resource styling objects automatically require relatedId
const styling: ResourceStylingObjectCreate<"iModelVisibility", "1.0.0"> = {
  kind: "iModelVisibility",
  version: "1.0.0",
  relatedId: "<repository_object_id>", // TypeScript enforces this field
  data: {
    // IntelliSense shows iModelVisibility specific properties
    categories: { ... },
    models: { ... },
  },
};
```

### Error Handling

```ts
import { SceneApiError } from "@itwin/scenes-client";

try {
  const scene = await client.getScene({
    iTwinId: "<itwin_id>",
    sceneId: "<invalid_scene_id>",
  });
} catch (error) {
  if (error instanceof SceneApiError) {
    console.error(`Error getting scene: ${error.status} - ${error.message}`);
    console.error(`Details ${error.details}`);
  }
}
```

## Issues

Please report bugs, feature requests, or questions using the [GitHub Issues](https://github.com/iTwin/scenes-client/issues) page.

---

Copyright © Bentley Systems, Incorporated. All rights reserved.
See [LICENSE.md](./LICENSE.md) for license terms and full copyright notice.
