<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>SceneLayer upload 3D models and applyEdits | Sample | ArcGIS Maps SDK for JavaScript</title>
<!-- Load the ArcGIS Maps SDK for JavaScript from CDN -->
<script type="module" src="https://js.arcgis.com/5.0/"></script>
transform: translateX(-50%);
animation-duration: 0.2s;
transform: translateY(-30%);
transform: translateY(0%);
background-color: var(--calcite-color-foreground-1);
color: var(--calcite-color-text-1);
font-size: var(--calcite-font-size-1);
font-weight: var(--calcite-font-weight-medium);
<arcgis-scene item-id="48a900de3c124a47993de5fe9090671d">
<arcgis-zoom slot="top-left"></arcgis-zoom>
<arcgis-navigation-toggle slot="top-left"></arcgis-navigation-toggle>
<arcgis-compass slot="top-left"></arcgis-compass>
<arcgis-expand id="downloads-expand" slot="bottom-left" icon="download" expanded>
<a download href="https://developers.arcgis.com/javascript/latest/assets/sample-code/editing-scenelayer-applyedits/Tower.glb">
<img slot="thumbnail" alt="" src="https://developers.arcgis.com/javascript/latest/assets/sample-code/editing-scenelayer-applyedits/Tower.png" />
<calcite-icon scale="s" icon="arrow-bold-down"></calcite-icon>
<a download href="https://developers.arcgis.com/javascript/latest/assets/sample-code/editing-scenelayer-applyedits/GreenBuilding.fbx">
src="https://developers.arcgis.com/javascript/latest/assets/sample-code/editing-scenelayer-applyedits/GreenBuilding.png" />
<calcite-icon scale="s" icon="arrow-bold-down"></calcite-icon>
<a download href="https://developers.arcgis.com/javascript/latest/assets/sample-code/editing-scenelayer-applyedits/ApartmentBlock.ifc">
src="https://developers.arcgis.com/javascript/latest/assets/sample-code/editing-scenelayer-applyedits/ApartmentBlock.png" />
<span>Apartment Block (Georeferenced)</span>
<calcite-icon scale="s" icon="arrow-bold-down"></calcite-icon>
<calcite-panel id="panel" slot="bottom-right">
<!-- loading indicator -->
<calcite-scrim id="loader" loading></calcite-scrim>
<!-- area to upload files -->
<h1>Add your own model</h1>
<section aria-hidden="true">
alignment="icon-start-space-between">
<p>or drag a file here</p>
<calcite-notice id="size-warning" kind="danger" icon="" closable="">
<div slot="title">File was to large</div>
<div slot="message">The uploaded file can be 50Mb maximum</div>
<calcite-notice id="georeferenced-model" kind="info" icon="" closable="">
<div slot="title">Georeferenced 3D Model</div>
Uploaded 3D model is georeferenced and positioned in its real-world location.
<calcite-notice id="georeferenced-model-warning" kind="warning" icon="" closable="">
<div slot="title">Georeferenced 3D Model - Reprojected</div>
The model contained georeferencing information, but the spatial reference did not match the layer. The model was only approximately placed, which may result in some loss of precision.
const [reactiveUtils, SketchViewModel, GraphicsLayer, Point, projectOperator, Graphic] =
"@arcgis/core/core/reactiveUtils.js",
"@arcgis/core/widgets/Sketch/SketchViewModel.js",
"@arcgis/core/layers/GraphicsLayer.js",
"@arcgis/core/geometry/Point.js",
"@arcgis/core/geometry/operators/projectOperator.js",
"@arcgis/core/Graphic.js",
/*****************************************************************
* Get a reference to the HTML elements
*****************************************************************/
const viewElement = document.querySelector("arcgis-scene");
// Wait for the view to initialize
await viewElement.viewOnReady();
const MAX_FILESIZE_MB = 50;
// to allow persistence of your models we assign a random id to every device using the app.
// this method is useful for demonstration purposes but should utilize proper authentication/authorization in a production application
let deviceId = localStorage.getItem("deviceId");
deviceId = crypto.randomUUID();
localStorage.setItem("deviceId", deviceId);
const sceneLayer = viewElement.map.layers
.filter((layer) => layer.title === "EditableFeatures3DObject")
sceneLayer.popupEnabled = false;
sceneLayer.definitionExpression = `deviceId = '${deviceId}' OR deviceId = 'initial-model'`;
const sketchLayer = new GraphicsLayer();
viewElement.map.add(sketchLayer);
const sketchVM = new SketchViewModel({
// enables snapping on the site layer
const siteLayer = viewElement.map.layers
.filter((layer) => layer.title === "EditableFeatures3DObjectProjectSite")
sketchVM.snappingOptions = {
featureSources: [{ layer: siteLayer, enabled: true }],
/*****************************************************************
*****************************************************************/
// This section sets up the functionality to upload a 3d model from the users file system.
// It registers event handlers on the #drop-zone element, to handle the input files, either through dragging and dropping or through the users file browser.
const dropZone = document.getElementById("drop-zone");
// this makes sure we prevent the browser default drag over behaviour
// see mdn: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop#define_the_drop_zone
dropZone.addEventListener("dragover", (event) => event.preventDefault());
dropZone.addEventListener("drop", async (event) => {
// Prevent the browsers default drop behavior, so it doesn't try to open the file
// Makes sure the items we're dealing with are a File
const files = event.dataTransfer.items
? Array.from(event.dataTransfer.items)
.filter((item) => item.kind === "file")
.map((item) => item.getAsFile())
: Array.from(event.dataTransfer.files);
if (file) await disableUI(uploadModel(sceneLayer, file));
// allows users to also browse for files
const input = dropZone.querySelector("input");
input.addEventListener("change", async (event) => {
const input = event.target;
const [file] = Array.from(input.files);
if (file) await disableUI(uploadModel(sceneLayer, file));
// if the user tries to upload the same file again, we need to clear the input value so that the change event is triggered again
dropZone.querySelector("calcite-button").addEventListener("click", () => input.click());
// takes a file and a scene layer, creates a mesh and places it into the scene
async function uploadModel(sceneLayer, file) {
const mb = bytes / (1024 * 1024);
if (mb > MAX_FILESIZE_MB) {
// Make sure the scene layer has loaded before doing anything
if (!sceneLayer.loaded) await sceneLayer.load();
// convertMesh returns the Mesh plus optional georeferenceInfo (when the uploaded model contains georeferencing metadata)
const { mesh, georeferenceInfo } = await sceneLayer.convertMesh([file]);
// Georeferenced model: auto-place at correct location
if (georeferenceInfo.origin.spatialReference.equals(mesh.spatialReference) === false) {
showGeoreferencedModelWarning(georeferenceInfo.origin.spatialReference, mesh.spatialReference);
showGeoreferencedModel();
const meshGraphic = new Graphic({
attributes: { deviceId: deviceId },
type: "mesh-3d", // autocasts as new MeshSymbol3D()
type: "fill", // autocasts as new FillSymbol3DLayer()
sketchLayer.add(meshGraphic);
sketchVM.update(meshGraphic); // make it editable
viewElement.goTo({ target: mesh }).catch(() => {});
attributes: { deviceId: deviceId },
/*****************************************************************
*****************************************************************/
// Sets up listeners on the SketchViewModel to persist edits to the model on creation and updates
sketchVM.on("update", async (event) => {
if (isUpdating) return sketchVM.cancel();
const objectIdField = sceneLayer.objectIdField;
// Synchronizes the hiding/showing of the model on the scene and sketch layers,
// so that the model is always visible in at least one of the layers
if (event.state === "start") {
const [graphic] = event.graphics;
// After the graphic is added to the sketch layer the layerview goes through an update cycle
// It updates once, and when it stops updating the graphic is visible so we can safely exclude the scene layer feature
// This is needed to prevent flickering when selecting a model that has not been interacted with already
const sv = viewElement.layerViews
.filter((view) => view.layer === sketchLayer)
await reactiveUtils.whenOnce(() => sv.updating);
await reactiveUtils.whenOnce(() => !sv.updating);
sceneLayer.excludeObjectIds.add(graphic.attributes[objectIdField]);
if (event.state === "complete" && !event.aborted) {
const graphic = event.graphics[0];
// if the graphic has an object id it means it has already been added to the scene layer and we should use `updateFeatures` instead of `addFeatures`
const hasBeenAdded = graphic.attributes[objectIdField] != null;
const edits = hasBeenAdded ? { updateFeatures: [graphic] } : { addFeatures: [graphic] };
const applyEdits = sceneLayer.applyEdits(edits).then(({ addFeatureResults }) => {
const id = addFeatureResults[0].objectId;
// To allow further updates to the model, we need to associate the graphic that is on the sketch layer with the features object id
// We do this by adding the object id into the graphics attributes
graphic.attributes[objectIdField] = id;
// To keep interactions snappy we hide the feature on the scene layer and instead use the graphic that is already present on the sketch layer
// The model is still in the scene layer, only hidden for the duration of this session
sceneLayer.excludeObjectIds.add(id);
sketchVM.on("create", async (event) => {
if (isUpdating) return sketchVM.cancel();
if (event.state === "complete" && !event.aborted) {
sketchVM.update(event.graphic);
sketchVM.on("delete", async (event) => {
const applyEdits = sceneLayer.applyEdits({
deleteFeatures: event.graphics,
/*****************************************************************
* Click-to-edit selection
*****************************************************************/
viewElement.addEventListener("arcgisViewClick", async (event) => {
// disable the click handler when there are updates being processed
if (isUploading || isUpdating) return;
// Listen for the click event
const hitTestResults = await viewElement.hitTest(event);
// Search for features where the user clicked
if (hitTestResults.results) {
const graphicHits = hitTestResults.results.filter((result) => "graphic" in result);
const [userGraphic] = graphicHits
.map((result) => result.graphic)
.filter((graphic) => graphic.attributes?.deviceid === deviceId);
if (!sketchLayer.graphics.includes(userGraphic)) {
const objectIdField = sceneLayer.objectIdField;
const query = sceneLayer.createQuery();
query.returnGeometry = true;
query.objectIds = [userGraphic.attributes[objectIdField]];
const res = await sceneLayer.queryFeatures(query);
const mesh = res.features.find((feature) => feature.geometry.type === "mesh");
// the default graphic symbol will color the mesh orange. We simply give it an empty fill so the look of the graphic is not changed.
type: "mesh-3d", // autocasts as new MeshSymbol3D()
type: "fill", // autocasts as new FillSymbol3DLayer()
/*****************************************************************
*****************************************************************/
const loader = document.getElementById("loader");
const sizeWarning = document.getElementById("size-warning");
const georeferencedModel = document.getElementById("georeferenced-model");
const georeferencedModelWarning = document.getElementById("georeferenced-model-warning");
async function disableUI(action) {
document.body.style.cursor = "progress";
document.body.style.cursor = "pointer";
function showSizeWarning() {
function showGeoreferencedModel() {
georeferencedModel.open = true;
function showGeoreferencedModelWarning(modelSpatialReference, targetSpatialReference) {
const downloadsExpand = document.getElementById("downloads-expand");
if (downloadsExpand) downloadsExpand.expanded = false;
const messageEl = georeferencedModelWarning.querySelector("[slot='message']");
const formatSpatialReference = (sr) => {
if (!sr) return "unknown";
return sr.wkid ?? sr.latestWkid ?? "unknown";
if (messageEl && modelSpatialReference && targetSpatialReference) {
const modelSrId = formatSpatialReference(modelSpatialReference);
const targetSrId = formatSpatialReference(targetSpatialReference);
`The model contained georeferencing information, but the spatial reference did not match the layer (WKID ${modelSrId} → WKID ${targetSrId}). The model was only approximately placed, which may result in some loss of precision.`;
georeferencedModelWarning.open = true;