ibms-dome/Frontend/js/forge/forgemodel.js

1494 lines
50 KiB
JavaScript
Raw Normal View History

2022-11-18 21:27:54 +08:00
var viewer;
2022-11-24 00:20:37 +08:00
let fragProxy;
var targetFloorZ;
var elevatorSpeed;
var selector = "#forgeViewer";
var myDataList;
var lightDataList;
var lightList = []; //燈光清單
var levels; //剖面用
var lowerIdx = 0; //剖面的下方樓層
var upperIdx = 0; //剖面的上方樓層
2022-11-18 21:27:54 +08:00
function launchViewer(urn, callback, failCallback, _selector = "#forgeViewer") {
selector = _selector;
2022-11-18 21:27:54 +08:00
var options = {
env: "AutodeskProduction",
getAccessToken: getForgeToken,
settings: {
ambientShadows: false,
groundShadows: false,
},
2022-11-18 21:27:54 +08:00
};
Autodesk.Viewing.Initializer(options, () => {
viewer = new Autodesk.Viewing.GuiViewer3D($(selector)[0]);
//viewer = new Autodesk.Viewing.Viewer3D(document.getElementById('forgeViewer'));
2022-11-18 21:27:54 +08:00
viewer.start();
var documentId = "urn:" + urn;
Autodesk.Viewing.Document.load(
documentId,
onDocumentLoadSuccess,
onDocumentLoadFailure
);
setShadowShow();
2022-11-18 21:27:54 +08:00
$(selector).on("autodesk:loaded", function (e, nodeIds) {
callback ? callback(viewer, nodeIds) : "";
});
$(selector).on("autodesk:loaded:fail", function (e) {
failCallback ? failCallback(viewer) : "";
});
//test
// for (let i = 0; i < urn.length; i++) {
// Autodesk.Viewing.Document.load(urn[i]["urn"], async (doc) => {
// let viewables = doc.getRoot().getDefaultGeometry();
// let model = await viewer.loadDocumentNode(doc, viewables, {
// preserveView: false,
// keepCurrentModels: true,
// placementTransform: (new THREE.Matrix4()).setPosition(urn[i]["xform"]),
// keepCurrentModels: true,
// globalOffset: {
// x: 0,
// y: 0,
// z: 0
// }
// });
// await viewer.waitForLoadDone(); //!<<< Wait for loading materials, properties and geometries for this model (URN)
// });
// }
//loadHeatmaps(viewer.getAllModels()[0]); //!<<< equals to viewer.model
2022-11-18 21:27:54 +08:00
});
}
function launchViewerForHotspot(
urn,
callback,
failCallback,
_selector = "#forgeViewer"
) {
selector = _selector;
var av = Autodesk.Viewing;
var options = {
env: "AutodeskProduction",
getAccessToken: getForgeToken,
settings: {
ambientShadows: false,
groundShadows: false,
},
};
Autodesk.Viewing.Initializer(options, () => {
viewer = new Autodesk.Viewing.GuiViewer3D($(selector)[0]);
viewer.start();
var documentId = "urn:" + urn;
viewer.addEventListener(av.GEOMETRY_LOADED_EVENT, addHotPoint, {
once: true,
});
Autodesk.Viewing.Document.load(
documentId,
onDocumentLoadSuccess,
onDocumentLoadFailure
);
setShadowShow();
$(selector).on("autodesk:loaded", function (e, nodeIds) {
callback ? callback(viewer, nodeIds) : "";
});
$(selector).on("autodesk:loaded:fail", function (e) {
failCallback ? failCallback(viewer) : "";
});
});
}
function launchViewerNoTools(
urn,
callback,
failCallback,
_selector = "#forgeViewer"
) {
selector = _selector;
var options = {
env: "AutodeskProduction",
getAccessToken: getForgeToken,
settings: {
ambientShadows: false,
groundShadows: false,
},
};
Autodesk.Viewing.Initializer(options, () => {
//viewer = new Autodesk.Viewing.GuiViewer3D(document.getElementById('forgeViewer'));
viewer = new Autodesk.Viewing.Viewer3D($(selector)[0]);
viewer.start();
var documentId = "urn:" + urn;
Autodesk.Viewing.Document.load(
documentId,
onDocumentLoadSuccess,
onDocumentLoadFailure
);
setShadowShow();
$(selector).on("autodesk:loaded", function (e, nodeIds) {
callback ? callback(viewer, nodeIds) : "";
});
$(selector).on("autodesk:loaded:fail", function (e) {
failCallback ? failCallback(viewer) : "";
});
});
}
2022-11-24 00:20:37 +08:00
function getAllLeafComponents(viewer, callback) {
var cbCount = 0;
var tree;
var jsData = [];
2022-11-24 00:20:37 +08:00
function getLeafComponentsRec(current, parent) {
cbCount++;
if (tree.getChildCount(current) != 0) {
tree.enumNodeChildren(
current,
function (children) {
getLeafComponentsRec(children, current);
},
false
);
2022-11-24 00:20:37 +08:00
}
var nodeName = viewer.model.getInstanceTree().getNodeName(current);
jsData.push({ id: current, parent: parent, text: nodeName });
2022-11-24 00:20:37 +08:00
if (--cbCount == 0) callback(jsData);
}
viewer.getObjectTree(function (objectTree) {
tree = objectTree;
var rootId = tree.getRootId();
var nodeName = viewer.model.getInstanceTree().getNodeName(rootId);
jsData.push({ id: rootId, parent: "#", text: nodeName });
var allLeafComponents = getLeafComponentsRec(rootId, "#");
2022-11-24 00:20:37 +08:00
});
}
class elevator3D {
constructor(option = {}) {
this.ele = option.element;
this.viewer = option.viewer;
this.nodeId = option.nodeId;
2023-05-23 11:40:09 +08:00
this.speed = option.speed ?? 0.18;
this.tagValue = option.tagValue ?? "";
this.fragProxys = [];
this.fragProxy = null;
this.initCallback = option.inited ?? null;
this.movStatus = 0;
this.targetFloorZ = 0;
this.floorHeight = option.floorHeight ?? [{}];
this.sensorObjs = option.sensorObjs ?? null;
this.init();
}
// 定義遍歷模型中所有片段的異步函數
enumFragments = function (nodeId) {
return new Promise((resolve, reject) => {
// 定義結果陣列
let result = [];
// 使用 InstanceTree.enumNodeFragments 遍歷模型中的所有片段
this.viewer?.model?.getData().instanceTree.enumNodeFragments(
nodeId,
(fragId) => {
// 將遍歷到的片段 ID 添加到結果陣列中
result.push(fragId);
},
(nodeId) => {
// 如果遍歷到的節點是葉節點,则返回 true表示繼續遍歷該節點的片段
return model.getData().instanceTree.isLeaf(nodeId);
}
);
// 将结果陣列作為参数调用 resolve 函数
resolve(result);
});
};
// 設置樹狀結構中片段
setTreeFrag = async function (callback) {
let tree = this.viewer?.model?.getData().instanceTree;
if (!tree) {
return;
}
let nodeId = this.nodeId;
if (nodeId) {
// 刪除 fragmentProxys 中與當前 nodeId 相同的項目
this.fragProxys
.filter((x) => x.nodeId == nodeId)
.forEach((x) => {
let idx = this.fragProxys.indexOf(x);
this.fragProxys.splice(idx, 1);
});
// 遍歷當前 nodeId 中的所有片段
let fragments = await this.enumFragments(nodeId);
fragments.forEach((frag) => {
// 取得當前片段的 FragmentProxy 物件
let fragProxy = this.viewer.impl.getFragmentProxy(
this.viewer.model,
frag
);
// 將當前片段的信息添加到 fragProxys 陣列中
this.fragProxys.push({ nodeId: nodeId, fragId: frag, frag: fragProxy });
// 取得當前片段的動畫變換矩陣
fragProxy.getAnimTransform();
// 定義當前片段的位置向量
let fragPosition = new THREE.Vector3(0, 0, 0); // 一樓0 二樓15 三樓 26
// 設置當前片段的位置
fragProxy.position = fragPosition;
// 更新當前片段的動畫變換矩陣
fragProxy.updateAnimTransform();
});
}
// 通知檢視器更新場景
this.viewer.impl.sceneUpdated(true);
if (!$(this.ele)[0]) {
callback ? callback() : "";
return;
}
if (typeof $(this.ele)[0]._elevator3D == "undefined") {
$(this.ele)[0]._elevator3D = [];
}
$(this.ele)[0]._elevator3D.push({ nodeId: nodeId, obj: this });
this.initCallback ? this.initCallback() : "";
callback ? callback() : "";
};
init = function (callback = null) {
this.setTreeFrag(callback);
};
setElevatorFloor = function (floor) {
this.targetFloorZ =
this.floorHeight.filter((x) => x.floor == floor)[0]?.height ?? 0;
};
setElevatorSpeed = function (speed) {
//0.01 ~ 1
this.speed = speed;
};
// 電梯移動
movElevator = async function () {
let nodeId = this.nodeId;
let fragProxyZ = 0; // 定義 fragProxyZ 變量,用於存儲當前片段的 z 坐標
// 取得當前 nodeId 的片段代理對象
let fragProxy = this.fragProxys.filter((x) => x.nodeId == nodeId)[0]?.frag;
if (!fragProxy) {
return;
}
changeColor(nodeId);
if (
(this.movStatus == 2 &&
fragProxy.position.z.roundDecimal(2) <
this.targetFloorZ.roundDecimal(2)) ||
(this.movStatus == 1 &&
fragProxy.position.z.roundDecimal(2) >
this.targetFloorZ.roundDecimal(2)) ||
fragProxy.position.z.roundDecimal(2) == this.targetFloorZ.roundDecimal(2)
) {
stoped(this);
}
if (
fragProxy.position.z.roundDecimal(2) > this.targetFloorZ.roundDecimal(2)
) {
this.movStatus = 2;
} else if (
fragProxy.position.z.roundDecimal(2) < this.targetFloorZ.roundDecimal(2)
) {
this.movStatus = 1;
}
// 遍歷當前 nodeId 中的所有片段
let fragments = await this.enumFragments(nodeId);
fragments.forEach((frag) => {
let fragProxy = this.viewer.impl.getFragmentProxy(
this.viewer.model,
frag
);
fragProxy.getAnimTransform();
if (this.movStatus == 2) {
fragProxy.position.z -= this.speed;
} else if (this.movStatus == 1) {
fragProxy.position.z += this.speed;
}
let tarFrag = this.fragProxys.filter(
(x) => x.nodeId == nodeId && x.fragId == frag
)[0];
if (tarFrag) {
tarFrag.frag.position.z = fragProxy.position.z;
}
fragProxyZ = fragProxy.position.z;
// 改變感測器熱點位置
this.sensorObjs && this.sensorObjs[0]?.changePos(nodeId + 2, fragProxyZ);
// if(nodeId === 15200) {
// }
fragProxy.updateAnimTransform();
});
this.viewer.impl.sceneUpdated(true);
let movElevator = $(this.ele)[0]
._elevator3D.filter((x) => x.nodeId == this.nodeId)[0]
?.obj.movElevator.bind(this);
if (this.movStatus == 2) {
if (fragProxyZ >= this.targetFloorZ) {
requestAnimationFrame(() => {
movElevator();
});
} else {
stoped(this);
}
} else if (this.movStatus == 1) {
if (fragProxyZ <= this.targetFloorZ) {
requestAnimationFrame(() => {
movElevator();
});
} else {
stoped(this);
}
} else {
stoped(this);
}
function stoped(obj) {
obj.movStatus = 0;
hideColor(nodeId);
return;
}
};
}
function onDocumentLoadSuccess(doc, eleOption) {
2022-11-18 21:27:54 +08:00
var viewables = doc.getRoot().getDefaultGeometry();
viewer.loadDocumentNode(doc, viewables).then((i) => {
2022-11-18 21:27:54 +08:00
// documented loaded, any action?
});
// viewer.addEventListener(Autodesk.Viewing.AGGREGATE_SELECTION_CHANGED_EVENT, (args) => {
// var currSelection = viewer.getSelection();
// var domElem = document.getElementById('id_printer');
// domElem.innerText = currSelection[0];
// });
viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, function () {
let instanceTree = viewer.model?.getData().instanceTree;
// console.log(instanceTree.nodeAccess)
allDbIdsStr = Object.keys(instanceTree.nodeAccess.dbIdToIndex);
/*getNodeIdByDbIds("【電梯】", (nodeIds) => {*/
$(selector).trigger("autodesk:loaded");
/*});*/
});
2022-11-24 00:20:37 +08:00
}
// 輔助函數,使用 Promise 封裝 viewer.getProperties 函數
function viewerGetProperties(dbIds, attributeName = null) {
// 在這裡,我們使用 viewer.getProperties 函數的成功回調函數作為 resolve 函數,
// 並使用 viewer.getProperties 函數的失敗回調函數作為 resolve 函數的參數
// 這樣,當 viewer.getProperties 函數成功時Promise 會傳回 properties 物件;
// 當 viewer.getProperties 函數失敗時Promise 會傳回 null。
return new Promise((resolve, reject) => {
let option = {};
if (attributeName != null) {
option.propFilter = [attributeName]; // 限制只返回指定的屬性
}
viewer.model.getBulkProperties2(
dbIds,
option, // 取得指定元素的屬性信息
function (elements) {
resolve(elements); // 成功時傳回 elements 物件
}
);
});
}
// 輔助函數,使用 Promise 封裝 viewer.search 函數
async function getNodeIdBySearch(text) {
return new Promise((resolve, reject) => {
viewer.search(
text,
(e) => {
// 在 3D 模型中搜索具有指定文本的元素
resolve(e); // 成功時傳回搜索結果
},
(e) => {
resolve(null); // 失敗時傳回 null
}
);
});
}
// 主函數 - 透過 model 全部 node 取得特定 nodeId
async function getNodeIdByDbIds(checkValue = [], callback = null) {
let evelMap = new Map();
let hasElement = false; // 設置是否有 【tag_id】的node
let targetNodeIds = await getNodeIdBySearch(checkValue);
let elements = await viewerGetProperties(targetNodeIds, "【tag_id】");
// 從 elements 中篩選出包含 【tag_id】 屬性的元素
elements = elements.filter(
(x) =>
x.properties.findIndex(
(y) =>
y.displayName == "【tag_id】" &&
checkValue.indexOf(y.displayValue) != -1
) != -1
);
if (elements) {
hasElement = true;
}
for (var e of elements) {
let name = e.properties[0].displayValue; // 獲取元素的 【tag_id】 屬性
let chiElements = null;
// 獲取 node 元素的子元素信息
targetNodeIds = await viewerGetProperties([e.dbId], "child");
// 二次篩選
chiElements = targetNodeIds.filter(
(x) => x.properties.findIndex((y) => y.displayName == "child") != -1
);
// 獲取子元素的 nodeId
targetNodeIds = chiElements.map((x) => x.properties[0].displayValue);
// 獲取子元素的屬性信息
chiElements = await viewerGetProperties(targetNodeIds);
// 將 node 元素的 【tag_id】 屬性和子元素的 nodeId 加入 Map 中
evelMap.set(name, chiElements[0]?.dbId);
}
if (!hasElement) {
callback ? callback([]) : "";
return [];
}
callback ? callback(evelMap) : "";
return evelMap;
}
2022-11-24 00:20:37 +08:00
function setElevatorFloor(floor) {
if (floor == 0) targetFloorZ = 0;
else if (floor == 1) targetFloorZ = 15;
else if (floor == 2) targetFloorZ = 26;
2022-11-24 00:20:37 +08:00
}
function setElevatorSpeed(speed) {
//0.01 ~ 1
2022-11-24 00:20:37 +08:00
elevatorSpeed = speed;
}
function movElevator() {
let tree = viewer.model?.getData().instanceTree;
2022-11-24 00:20:37 +08:00
let nodeId = 10952;
let fragProxyZ = 0;
var movStatus = 0; // 0=no 1=up 2=down
if (fragProxy.position.z > targetFloorZ) {
movStatus = 2;
} else if (fragProxy.position.z < targetFloorZ) {
movStatus = 1;
2022-11-24 00:20:37 +08:00
}
if (movStatus == 0) {
return;
}
tree.enumNodeFragments(nodeId, function (frag) {
fragProxy = viewer.impl.getFragmentProxy(viewer.model, frag);
fragProxy.getAnimTransform();
//let fragPosition = new THREE.Vector3(0, 0, 15);// 一樓0 二樓15 三樓 26
2022-11-24 00:20:37 +08:00
if (movStatus == 2) {
fragProxy.position.z -= elevatorSpeed;
} else if (movStatus == 1) {
2022-11-24 00:20:37 +08:00
fragProxy.position.z += elevatorSpeed;
}
fragProxyZ = fragProxy.position.z;
fragProxy.updateAnimTransform();
2022-11-24 00:20:37 +08:00
});
viewer.impl.sceneUpdated(true);
if (movStatus == 2) {
if (fragProxyZ >= targetFloorZ) {
requestAnimationFrame(movElevator);
}
} else if (movStatus == 1) {
2022-11-24 00:20:37 +08:00
if (fragProxyZ <= targetFloorZ) {
requestAnimationFrame(movElevator);
}
}
//let fragPosition = new THREE.Vector3(position);// 一樓0 二樓15 三樓 26
//fragProxy.position = fragPosition;
//fragProxy.updateAnimTransform();
//viewer.impl.sceneUpdated(true);
2022-11-24 00:20:37 +08:00
}
function getAllDbIds(viewer) {
var instanceTree = viewer.model?.getData().instanceTree;
2022-11-24 00:20:37 +08:00
var allDbIdsStr = Object.keys(instanceTree.nodeAccess.dbIdToIndex);
2022-11-18 21:27:54 +08:00
return allDbIdsStr.map(function (id) {
return parseInt(id);
});
2022-11-18 21:27:54 +08:00
}
/**
* Autodesk.Viewing.Document.load() failuire callback.
*/
function onDocumentLoadFailure(viewerErrorCode) {
$(selector).trigger("autodesk:loaded:fail");
2022-11-18 21:27:54 +08:00
console.error("onDocumentLoadFailure() - errorCode:" + viewerErrorCode);
}
function getForgeToken(callback) {
fetch(forgeTokenBase.url).then((res) => {
res.json().then((data) => {
let limitResType = ["object", "undefined"];
let access_token = data;
$.each(forgeTokenBase.res_access_token, (idx, dict) => {
access_token = access_token[dict];
});
let expires_in = data;
$.each(forgeTokenBase.res_expires_in, (idx, dict) => {
expires_in = expires_in[dict];
});
if (
limitResType.includes(typeof access_token) ||
limitResType.includes(typeof expires_in)
) {
(access_token = ""), (expires_in = "");
throw "access_token or expires_in on the ForgeTokenBase object for getting forge token by URL is not the correct path.";
}
callback(access_token, expires_in);
2022-11-18 21:27:54 +08:00
});
});
//callback("eyJhbGciOiJSUzI1NiIsImtpZCI6IlU3c0dGRldUTzlBekNhSzBqZURRM2dQZXBURVdWN2VhIn0.eyJzY29wZSI6WyJkYXRhOndyaXRlIiwiZGF0YTpyZWFkIiwiYnVja2V0OnJlYWQiLCJidWNrZXQ6dXBkYXRlIiwiYnVja2V0OmNyZWF0ZSJdLCJjbGllbnRfaWQiOiJUQTNocXNGZnpRYk5PVVhLcGxkS1VLU2V3NFNKMjF3NSIsImF1ZCI6Imh0dHBzOi8vYXV0b2Rlc2suY29tL2F1ZC9hand0ZXhwNjAiLCJqdGkiOiJiemxzWE5qWElvZ2R1UjUzTUJkdlhrTTNTT01qeVB1bHJrMmdTVWJudGNTeDg1b01kRG1xejg3Z05jenJkRzhpIiwiZXhwIjoxNjY4MTgzMDM2fQ.VU3qLwTJ9nlXnomKCdk4y5UcgszGEO_zlvE7w5mWWajeBMwKLo-zw7LJEqUEajRksvssppR9SbVsjLSx-vDVc3DRhCo3jYTWKPT1T3wQrlkOSqLeIrAdnKdBDNBWKgrGJt_xcmXc3dZ3XNKf9L_F6Ex808rUlo6cem1mcPpKl1jCBDqKu1mAX7aDtZ65TTQZbGGhbG4HdnET-d1i5w4LunGN11UAHhDUW3n0SWWIBL27PiiUQONZttajhD5st6IngYLcjr93BYVyJmDF7-wm4WZlHSw2OnXIfbJcFXEd83uVv_Rej4UXjzZ0e6kHwzc2nvGvKSIFu3Nt7CabdR8CkA", 3599);
}
//------------------ 熱圖 -------------------------------
class ADHeatMaps {
constructor(option = {}) {
this.devices = option.devices ?? [];
this.checkNodeString = "【ROOM】";
this.tempVals = [];
this.roomDbIds = []; //房間 dbId
this.model = null;
this.dataVizExtn = null;
this.shadingData = null;
this.onComplete = option.onComplete ?? null;
this.init();
}
init = async function () {
// 載入 Autodesk Viewer 的資料視覺化擴充功能
const dataVizExtn = await viewer.loadExtension(
"Autodesk.DataVisualization"
);
this.dataVizExtn = dataVizExtn;
// 儲存 Viewer 的模型
this.model = viewer.model;
this.addHeatMaps();
};
async addHeatMaps() {
const {
SurfaceShadingData,
SurfaceShadingPoint,
SurfaceShadingNode,
SurfaceShadingGroup,
} = Autodesk.DataVisualization.Core;
// test
const shadingGroup = new SurfaceShadingGroup("iot-heatmap");
const rooms = new Map();
console.log(this.devices);
for (const { id, roomDbId, position, sensorTypes } of this.devices) {
if (!id || roomDbId == -1) {
continue;
}
if (!rooms.has(roomDbId)) {
const room = new SurfaceShadingNode(id, roomDbId);
shadingGroup.addChild(room);
rooms.set(roomDbId, room);
}
const room = rooms.get(roomDbId);
room.addPoint(new SurfaceShadingPoint(id, position, sensorTypes));
}
this.shadingData = new SurfaceShadingData();
this.shadingData.addChild(shadingGroup);
this.shadingData.initialize(this.model);
if (pageAct.sysSubTag == "L1") {
await this.dataVizExtn.setupSurfaceShading(this.model, this.shadingData, {
type: "PlanarHeatmap",
placementPosition: 0.7,
confidence: 240.0,
});
this.dataVizExtn.registerSurfaceShadingColors(
"temperature",
[0x000000, 0xffd524]
);
} else {
await this.dataVizExtn.setupSurfaceShading(this.model, this.shadingData);
this.dataVizExtn.registerSurfaceShadingColors(
"temperature",
[0x0000ff, 0x00ff00, 0xffff00, 0xff0000]
);
}
this.dataVizExtn.renderSurfaceShading(
"iot-heatmap",
"temperature",
this.getSensorValue.bind(this)
);
//let nodeIds = await getNodeIdByDbIds(this.checkNodeString);
//nodeIds = Array.from(nodeIds);
//nodeIds = nodeIds.map(x => { return { room: x[0] , nodeId: x[1]} });
// this.roomDbIds = this.devices.filter(x => x.roomDbId != -1).map(x => x.roomDbId).Distinct();
// // 建立一個 SurfaceShadingData 物件,並將 SurfaceShadingNode 加入到該物件中
// const heatmapData = new SurfaceShadingData();
// $.each(this.roomDbIds, (idx, rDbid) => {
// // 建立一個名為 "Room Panel" 的 SurfaceShadingNode 物件,並將房間的模型給傳入,只在該房間呈現溫度
// const shadingNode = new SurfaceShadingNode("RoomPanel" + rDbid, rDbid);
// // 遍歷每個設備,建立一個 SurfaceShadingPoint 物件並加入到 SurfaceShadingNode 中,透過這些設備渲染溫度
// this.devices.filter(x => x.roomDbId == rDbid).forEach((device) => {
// const shadingPoint = new SurfaceShadingPoint(
// device.id,
// undefined,
// device.sensorTypes
// );
// shadingPoint.positionFromDBId(this.model, device.roomDbId)
// shadingNode.addPoint(shadingPoint);
// // device.temp = 0;
// });
// heatmapData.addChild(shadingNode);
// })
// // 將資料初始化並顯示在模型上
// heatmapData.initialize(this.model);
// // 對模型做表面顏色的渲染
// await this.dataVizExtn.setupSurfaceShading(this.model, heatmapData);
// // 對 "temperature" 的溫度設定兩種顏色:紅色和藍色
// this.dataVizExtn.registerSurfaceShadingColors("temperature", [0x0000ff, 0x00ff00, 0xffff00, 0xff0000]);
// $.each(this.roomDbIds, (idx, rDbid) => {
// this.dataVizExtn.renderSurfaceShading("RoomPanel" + rDbid, "temperature", this.getSensorValue.bind(this));
// })
// /*this.changeTemp(this.tempVal);*/
this.onComplete ? this.onComplete() : "";
}
getSensorValue = function (device, sensorType) {
let dev = this.devices.filter((x) => x.id == device.id)[0];
// shadingPoint.positionFromDBId(this.model, device.roomDbId)
return dev.temp / 40;
};
// 改變溫度
changeTemp = async function (devId, temp) {
// console.log("changeTemp",devId, temp)
this.tempVal = temp;
// 透過 device id 取得 roomDbId
this.devices.forEach((dev) => {
if (devId == dev.id) {
dev.temp = temp;
}
});
if (!this.shadingData) {
await this.addHeatMaps();
this.dataVizExtn.renderSurfaceShading(
"iot-heatmap",
"temperature",
this.getSensorValue.bind(this)
);
} else {
this.dataVizExtn.updateSurfaceShading(this.getSensorValue.bind(this));
}
// $.each(this.roomDbIds, async (idx, rDbid) => {
// this.dataVizExtn.renderSurfaceShading("RoomPanel" + rDbid, "temperature", this.getSensorValue.bind(this));
// })
//if (rDbid != null) {
// // 取得新的溫度值
// let getSensorValue = (device, sensorType) => {
// return this.tempVal / 40;
// }
// // 對 "Room Panel" 做表面顏色的渲染,並使用新的溫度值
// this.dataVizExtn.renderSurfaceShading("RoomPanel" + rDbid, "temperature", getSensorValue);
//}
};
}
//全部物件 透明度: 輸入0:透明輸入1:不透明
function setTransparentBuilding(transparent, filDbids = []) {
for (var i = 0; i < allDbIdsStr.length; i++) {
setTransparency(parseInt(allDbIdsStr[i]), transparent);
}
for (var i = 0; i < filDbids.length; i++) {
setTransparency(parseInt(filDbids[i]), transparent == 0 ? 1 : 0);
}
}
//設定模型 透明度
function setTransparency(nodeId, opacity) {
var model = viewer.model;
var fragList = viewer.model.getFragmentList();
var fragIds = [];
model.getData().instanceTree.enumNodeFragments(
nodeId,
(fragId) => {
// 將遍歷到的片段 ID 添加到結果陣列中
fragIds.push(fragId);
},
(nodeId) => {
// 如果遍歷到的節點是葉節點,则返回 true表示繼續遍歷該節點的片段
return model.getData().instanceTree.isLeaf(nodeId);
}
);
fragIds.forEach((fragId) => {
//获取材质
var material = fragList.getMaterial(fragId);
if (material) {
//设置透明度
material.opacity = opacity; //0.5;
material.transparent = true;
//标记更新
material.needsUpdate = true;
}
});
//更新viewer
viewer.impl.invalidate(true, true, true);
}
function changeColor(nodeId, color = [0, 255, 0, 1]) {
//電梯變綠色
let model = viewer.model;
let fragList = viewer.model.getFragmentList();
let instanceTree = viewer.model?.getData().instanceTree;
color = color.map((x, i) => (i < 3 ? x / 255 : x));
color = new THREE.Vector4().fromArray(color);
model.getData().instanceTree.enumNodeFragments(
nodeId,
(fragId) => {
// 將遍歷到的片段 ID 添加到結果陣列中
let material = fragList.getMaterial(fragId);
if (material) {
//设置透明度
material.opacity = 1; //0.5;
material.transparent = true;
//标记更新
material.needsUpdate = true;
}
},
(nodeId) => {
// 如果遍歷到的節點是葉節點,则返回 true表示繼續遍歷該節點的片段
return model.getData().instanceTree.isLeaf(nodeId);
}
);
instanceTree.enumNodeChildren(
nodeId,
function (chiNodeId) {
viewer.setThemingColor(chiNodeId, color);
},
(chiNodeId) => {
// 如果遍歷到的節點是葉節點,则返回 true表示繼續遍歷該節點的片段
return model.getData().instanceTree.isLeaf(chiNodeId);
}
);
}
function hideColor(nodeId) {
//顏色改成透明
let instanceTree = viewer.model?.getData().instanceTree;
instanceTree.enumNodeChildren(
nodeId,
function (chiNodeId) {
var color = new THREE.Vector4(0, 1, 0, 0);
viewer.setThemingColor(chiNodeId, color);
},
true
);
}
//紀錄燈具座標
async function getLightData(data) {
lightList = [];
lightDataList = data;
}
async function testNewLight(dataList) {
dataList.forEach((myData, index) => {
const position = myData.position;
if (lightList.findIndex((x) => x.device_guid == myData.device_guid) == -1) {
lightList.push({
dbid: myData.forge_dbid,
device_guid: myData.device_guid,
lightObject: new THREE.SpotLight(0xe1cf18, 10, 200, 1, 1, 2),
});
} else if (
lightList.findIndex((x) => x.device_guid == myData.device_node_guid) == -1
) {
lightList.push({
dbid: myData.forge_dbid,
device_guid: myData.device_node_guid,
lightObject: new THREE.SpotLight(0xe1cf18, 10, 200, 1, 1, 2),
});
}
if (position && position.x && position.y && position.z) {
lightList[index].lightObject.position.set(
position.x,
position.y,
position.z
);
lightList[index].lightObject.castShadow = true;
lightList[index].lightObject.visible = true;
lightList[index].lightObject.target.position.set(
position.x,
position.y,
position.z - 100
);
viewer.scene.add(lightList[index].lightObject.target);
viewer.scene.add(lightList[index].lightObject);
}
//let spotLightHelper = new THREE.SpotLightHelper(lightList[index].lightObject);
//viewer.scene.add(spotLightHelper);
});
viewer.impl.sceneUpdated(true);
}
//------------------- 加入熱點 -----------------
async function addHotPoint(data) {
var viewer = data.target ? data.target : data;
const dataVizExtn = await viewer.loadExtension("Autodesk.DataVisualization");
const DataVizCore = Autodesk.DataVisualization.Core;
const viewableType = Autodesk.DataVisualization.Core.ViewableType.SPRITE; //DataVizCore.ViewableType.SPRITE;
let spriteColor = null;
let spriteIcon = "";
const dbIdStart = 10;
const dbIdEnd = 19;
if (pageAct.sysSubTag === "M12") {
return;
} else {
spriteColor = new THREE.Color(0xffffff);
spriteIcon = "/file/img/forge/hotspot.svg";
if (location.href.indexOf("localhost:5966") != -1) {
spriteIcon = "/img/forge/hotspot.svg";
}
}
const style = new DataVizCore.ViewableStyle(
viewableType,
spriteColor,
spriteIcon
);
if (
lightDataList != undefined &&
lightDataList != null &&
lightDataList.length > 0
) {
testNewLight(lightDataList);
}
//熱點 點擊事件註冊
viewer.addEventListener(DataVizCore.MOUSE_CLICK, onSpriteClicked); // SPRITE_SELECTED
// viewer.addEventListener(DataVizCore.MOUSE_CLICK, onSpriteClickedOut);
//viewer.addEventListener(
// Autodesk.Viewing.SELECTION_CHANGED_EVENT,
// onSelectionChange
//);
const viewableData = new DataVizCore.ViewableData();
viewableData.spriteSize = 24; // Sprites as points of size 24 x 24 pixels
myDataList.forEach((myData, index) => {
const dbId = 10 + index;
const myPosition = myData.position;
const viewable = new DataVizCore.SpriteViewable(myPosition, style, dbId);
myData._dbId = dbId;
viewableData.addViewable(viewable);
});
await viewableData.finish();
dataVizExtn.addViewables(viewableData);
// console.log(dataVizExtn)
$(selector).trigger("autodesk:complete:sprite", { myDataList });
//---------------- 熱點點擊事件 --------------------
function onSpriteClicked(event) {
event.hasStopped = true;
if (event != undefined && event != null) {
if (event.dbId >= dbIdStart) {
//event.dbId > 0 && event.dbId < 19
// console.log(`Sprite clicked: ${event.dbId}`);
for (let i = dbIdStart; i <= myDataList.length + 10; i++) {
!alarmDbIdList.some((dbId) => dbId == i) &&
pageAct.sysSubTag !== "L1" &&
changeColorForHotspot(i);
changeScaleForHotspot(i, true);
}
let myData = myDataList.filter((x) => x._dbId == event.dbId)[0];
if (
lightList != undefined &&
lightList != null &&
lightList.length > 0
) {
//setLightOpenOrClose(false, myData.device_guid);//關燈測試
//setLightValues(myData.device_guid, 20, 0x00ff00);//更改燈光顏色和強度的測試
moveViewToDevice(myData.forge_dbid); //移動視角至該設備
}
$(selector).trigger("autodesk:click:sprite", { event, myData });
} else {
!alarmDbIdList.some((dbId) => dbId == i) &&
pageAct.sysSubTag !== "L1" &&
changeColorForHotspot(event.dbId, null);
changeScaleForHotspot(event.dbId, false);
$(selector).trigger("autodesk:clickOut:sprite", { event });
}
if (event.clickInfo != null) {
//document.getElementById('deviceName').innerHTML = viewer.model.getInstanceTree().getNodeName(event.clickInfo.dbId);
//document.getElementById('deviceDbid').innerHTML = event.clickInfo.dbId;
//document.getElementById('devicePosition').innerHTML = "(" + (event.clickInfo.point.x).toFixed(2) + ", " + (event.clickInfo.point.y).toFixed(2) + ", " + (event.clickInfo.point.z).toFixed(2) + ")";
// console.log(`event>> dbId: ${event.clickInfo.dbId}, id: ${event.clickInfo.object.id}, position.x: ${event.clickInfo.point.x}, y: ${event.clickInfo.point.y}, z: ${event.clickInfo.point.z}, name: ${viewer.model.getInstanceTree().getNodeName(event.clickInfo.dbId)}`);
}
}
}
// function onSpriteClickedOut(event){
// event.hasStopped = true;
// if (event != undefined && event != null) {
// if (event.dbId >= dbIdStart) {
// for (let i = dbIdStart; i <= myDataList.length + 10; i++) {
// changeColorForHotspot(event.dbId);
// changeScaleForHotspot(event.dbId, false);
// }
// let myData = myDataList.filter(x => x._dbId == event.dbId)[0];
// debugger
// $(selector).trigger("autodesk:clickOut:sprite", { event });
// }
// }
// }
//function onSelectionChange(event) {
// if (event != undefined && event != null) {
// const dbIds = event.dbIdArray;
// if (dbIds.length > 0) {
// // 處理已選取元件的邏輯
// $(selector).trigger("autodesk:click:sprite", event);
// console.log(`------ name: ${viewer.model.getInstanceTree().getNodeName(dbIds)} , dbId: ${dbIds}`);//, id: ${event.clickInfo.object.id}, position.x: ${event.clickInfo.point.x}, y: ${event.clickInfo.point.y}, z: ${event.clickInfo.point.z}
// } else {
// // 處理沒有選取元件的邏輯
// $(selector).trigger("autodesk:click:sprite", event);
// console.log("no item");
// }
// }
//}
}
// 熱點 更換顏色
async function changeColorForHotspot(dbId, type = null, lightOn = false) {
const dataVizExtn = await viewer.loadExtension("Autodesk.DataVisualization");
let spriteColorFocus = new THREE.Color(0xffffff);
if (pageAct.sysSubTag == "L1") {
spriteColorFocus = lightOn
? new THREE.Color(0xffd524)
: new THREE.Color(0x40525e);
if (lightOn) {
console.log(dbId);
}
} else if (type == "focus" && pageAct.sysSubTag !== "M12") {
spriteColorFocus = new THREE.Color(0x00ffe1);
} else if (type == "error") {
spriteColorFocus = new THREE.Color(0xff0000);
}
const viewablesToUpdate = dbId;
dataVizExtn.invalidateViewables(viewablesToUpdate, (viewable) => {
return {
color: spriteColorFocus,
};
});
}
// 熱點 更換大小
async function changeScaleForHotspot(dbId, type = true) {
const dataVizExtn = await viewer.loadExtension("Autodesk.DataVisualization");
let scale = 1;
if (type) {
scale = 1.2;
}
const viewablesToUpdate = dbId;
dataVizExtn.invalidateViewables(viewablesToUpdate, (viewable) => {
return {
scale: scale, // Restore the viewable size
//url: "https://.../circle.svg",
};
});
}
//------------------- end --------------
//------------------ 熱圖 -------------------------------
async function loadHeatmaps(model) {
//console.log("熱圖 loadHeat")
const dataVizExtn = await viewer.loadExtension("Autodesk.DataVisualization");
//取三個空調設備的位置打點
const devices = [
{
id: "Oficina 5",
//name: "Oficina-",
position: { x: 6.98, y: -19.0, z: 16.86 }, // x: 0, y: 25, z: -2.5 (-4.93, -20.61, 16.86)
sensorTypes: ["temperature", "humidity"],
},
{
id: "Oficina 4",
//name: "Oficina-",
position: { x: 35.85, y: -2.24, z: 16.86 }, // x: 0, y: 25.03, z: -2.52 (23.94, -3.85, 16.86)
sensorTypes: ["temperature", "humidity"],
},
{
id: "Oficina 3",
//name: "Oficina-",
position: { x: 6.98, y: -2.24, z: 16.86 }, // x: 0, y: 25.03, z: -2.52 (-4.93, -3.85, 16.86)
sensorTypes: ["temperature", "humidity"],
},
];
//冷氣N5: (-4.93, -20.61, 16.86), N4: (23.94, -3.85, 16.86), N3: (-4.93, -3.85, 16.86)
// Initialize sensor values
let sensorVals = [];
for (let i = 0; i < devices.length; i++) {
sensorVals[i] = Math.random();
}
const roomDbIds = [];
roomDbIds.push(7567);
const { SurfaceShadingData, SurfaceShadingPoint, SurfaceShadingNode } =
Autodesk.DataVisualization.Core;
const shadingNode = new SurfaceShadingNode("Room Panel", roomDbIds);
devices.forEach((device) => {
const shadingPoint = new SurfaceShadingPoint(
device.id,
device.position,
device.sensorTypes
);
shadingNode.addPoint(shadingPoint);
});
const heatmapData = new SurfaceShadingData();
heatmapData.addChild(shadingNode);
heatmapData.initialize(model);
// Setup surface shading
await dataVizExtn.setupSurfaceShading(model, heatmapData);
dataVizExtn.registerSurfaceShadingColors("temperature", [0xff0000, 0x0000ff]);
function getSensorValue(device, sensorType) {
return sensorVals[parseInt(device.id.slice(-1)) - 1];
}
dataVizExtn.renderSurfaceShading("Room Panel", "temperature", getSensorValue);
setInterval(() => {
// Modify sensor values.
for (let i = 0; i < devices.length; i++) {
sensorVals[i] = Math.random();
}
dataVizExtn.updateSurfaceShading(getSensorValue);
}, 2000);
}
//------------------ end --------------------------------
async function loadHeatmapsForFloor(model) {
const dataVizExtn = await viewer.loadExtension("Autodesk.DataVisualization");
//x: -17.33, y: 51.03, z: -2.52
const devices = [
{
id: "Oficina 5",
//name: "Oficina-",
position: { x: 6.98, y: -19.0, z: 16.86 }, // x: 0, y: 25, z: -2.5 (-4.93, -20.61, 16.86)
sensorTypes: ["temperature", "humidity"],
},
{
id: "Oficina 4",
//name: "Oficina-",
position: { x: 35.85, y: -2.24, z: 16.86 }, // x: 0, y: 25.03, z: -2.52 (23.94, -3.85, 16.86)
sensorTypes: ["temperature", "humidity"],
},
{
id: "Oficina 3",
//name: "Oficina-",
position: { x: 6.98, y: -2.24, z: 16.86 }, // x: 0, y: 25.03, z: -2.52 (-4.93, -3.85, 16.86)
sensorTypes: ["temperature", "humidity"],
},
];
// Initialize sensor values
let sensorVals = [];
for (let i = 0; i < devices.length; i++) {
sensorVals[i] = Math.random();
}
const roomDbIds = [];
roomDbIds.push(7567);
const { SurfaceShadingData, SurfaceShadingPoint, SurfaceShadingNode } =
Autodesk.DataVisualization.Core;
const shadingNode = new SurfaceShadingNode("Room Panel", roomDbIds);
devices.forEach((device) => {
const shadingPoint = new SurfaceShadingPoint(
device.id,
device.position,
device.sensorTypes
);
shadingNode.addPoint(shadingPoint);
});
const heatmapData = new SurfaceShadingData();
heatmapData.addChild(shadingNode);
heatmapData.initialize(model);
// Setup surface shading
await dataVizExtn.setupSurfaceShading(model, heatmapData);
//dataVizExtn.registerSurfaceShadingColors("co2", [0x00ff00, 0xff0000]);
dataVizExtn.registerSurfaceShadingColors("temperature", [0xff0000, 0x0000ff]);
function getSensorValue(device, sensorType) {
return sensorVals[parseInt(device.id.slice(-1)) - 1];
}
dataVizExtn.renderSurfaceShading("Room Panel", "temperature", getSensorValue);
setInterval(() => {
// Modify sensor values.
for (let i = 0; i < devices.length; i++) {
sensorVals[i] = Math.random();
}
dataVizExtn.updateSurfaceShading(getSensorValue);
}, 2000);
}
//
async function loadHeatmap() {
const model = viewer.model;
loadHeatmaps(model);
}
async function loadHeatmapForFloor() {
const model = viewer.model;
loadHeatmapsForFloor(model);
}
//------------ 剖面 ----------------------
function findLevels(model) {
return new Promise((resolve, reject) => {
model.search(
"layer",
(nodeIds) => {
let levels = [];
const tree = viewer.model.getInstanceTree();
for (let i = 0; i < nodeIds.length; i++) {
const dbId = nodeIds[i];
const name = tree.getNodeName(dbId);
if (!name || name.includes("<沒有層級>")) continue;
levels.push({
guid: dbId,
name,
dbId,
extension: {
buildingStory: true,
structure: false,
computationHeight: 0,
groundPlane: false,
hasAssociatedViewPlans: false,
},
});
}
resolve(levels);
},
(e) => {
reject(e);
}
);
});
}
async function getRemoteLevels() {
let aecData = await Autodesk.Viewing.Document.getAecModelData(
this.viewer.model.getDocumentNode()
);
let levels;
if (!aecData || !aecData.levels) {
const levelExt = await viewer.loadExtension("Autodesk.AEC.LevelsExtension");
levelExt.setAecModelData(undefined, viewer.model); //!<<< Clear before reset
levels = await findLevels(viewer.model);
aecdata = Autodesk.AEC.AecModelData.computeLevels(levels, viewer.model); //!<<< Rebuild aec model data
} else {
levels = aecData.levels;
}
levels.sort((a, b) => b.elevation - a.elevation);
return levels;
}
async function getLevelsData(lowerFloor, upperFloor, callback = null) {
// 樓層正規化 取得樓層
const floorRegex = /[\d|\w]+F/gim;
const floorRegex2 = /^FL[\d|\w]+/gim;
const data = await this.getRemoteLevels();
for (var i = 0; i < data.length; i++) {
let name =
data[i].name?.match(floorRegex) || data[i].name?.match(floorRegex2);
if (
name &&
(name[0] == lowerFloor || name[0].split("L")[1] + "F" == lowerFloor)
) {
lowerIdx = i;
}
if (
name &&
(name[0] == upperFloor || name[0].split("L")[1] + "F" == upperFloor)
) {
if (i > upperIdx && lowerFloor == upperFloor) {
} else {
upperIdx = i;
}
}
}
this.levels = data;
profile(callback);
}
function getCutPlaneParam(idx, n) {
if (idx < 0 || !n) return;
const level = this.levels[idx];
if (!level) return;
const model = this.viewer.model;
const globalOffset = model.getData().globalOffset;
const units = model.getUnitString();
const elevRaw = Autodesk.Viewing.Private.convertUnits(
"ft",
units,
1,
level.elevation
);
let d = elevRaw - globalOffset.z - 0.5;
if (n == 1) d = -1 * d;
return new THREE.Vector4(0, 0, n, d);
}
function profile(callback = null) {
//const upperIdx = 6;
const upperCutPlaneParam = this.getCutPlaneParam(upperIdx, 1);
//const lowerIdx = 7;
const lowerCutPlaneParam = this.getCutPlaneParam(lowerIdx, -1);
this.viewer.setCutPlanes([upperCutPlaneParam, lowerCutPlaneParam]);
callback ? callback() : "";
}
//----------------- end -----------------------------------------------
//新增燈光
async function newLight(lightPosition) {
//聚光燈
var spotLight = new THREE.SpotLight(0xe1cf18, 0, 20, 0.6, 0.5, 10);
spotLight.position.set(lightPosition.x, lightPosition.y, lightPosition.z);
// console.log(lightPosition.x, lightPosition.y, lightPosition.z)
spotLight.castShadow = false;
spotLight.visible = true;
spotLight.target.position.set(
lightPosition.x,
lightPosition.y,
lightPosition.z - 20
);
viewer.scene.add(spotLight.target);
viewer.scene.add(spotLight);
viewer.impl.sceneUpdated(true);
return spotLight;
}
//調整燈光 強度、顏色
async function setLightValues(deviceGuid, intensity, color) {
const light = lightList.find(({ device_guid }) => device_guid === deviceGuid);
if (light) {
console.log(light, intensity, color);
light.lightObject.intensity = intensity;
var tempcolor = new THREE.Color().setHex(color);
light.lightObject.color = tempcolor;
}
// for (var i = 0; i < lightList.length; i++) {
// if (lightList[i].device_guid == deviceGuid) {
// lightList[i].lightObject.intensity = intensity;
// var tempcolor = new THREE.Color().setHex(color);
// lightList[i].lightObject.color = tempcolor;
// }
// }
viewer.impl.sceneUpdated(true);
}
//燈光開關
async function setLightOpenOrClose(value, deviceGuid) {
for (var i = 0; i < lightList.length; i++) {
if (lightList[i].device_guid == deviceGuid) {
lightList[i].lightObject.visible = value;
}
}
viewer.impl.sceneUpdated(true);
}
//透過nodeId更改物件顏色或顯示與否color請填寫THREE.Vector4
function changeColorTransparency(nodeId, color) {
//變綠色
//var color = new THREE.Vector4(0, 1, 0, 1);//綠色前三個代表r、g、b; 亦可填入255/255
//var color = new THREE.Vector4(0, 1, 0, 0);//不顯示顏色;最後的參數為透明度
viewer.setThemingColor(nodeId, color);
}
//隱藏全物件
function hideAllObjects(filDbids = []) {
//viewer.hide(4);//只針對一個物件(dbid為4)做隱藏
for (var i = 0; i < allDbIdsStr.length; i++) {
viewer.hide(parseInt(allDbIdsStr[i]));
}
for (var i = 0; i < filDbids.length; i++) {
viewer.show(parseInt(filDbids[i]), viewer.model);
}
}
//顯示全物件
function showAllObjects() {
//viewer.show(4); //只針對一個物件(dbid為4)做顯示
for (var i = 0; i < allDbIdsStr.length; i++) {
viewer.show(parseInt(allDbIdsStr[i]));
}
}
function showHeat(selector) {
const labels = [
`${(10).toFixed(2)}${"°C"}`,
`${(40 / 2).toFixed(2)}${"°C"}`,
`${(30).toFixed(2)}${"°C"}`,
];
const colorStops = ["blue", "green", "yellow", "red"];
createHeatmapRect(labels, colorStops, selector);
}
function createHeatmapRect(labels, colorStops, selector) {
if (!$(selector)[0]) {
return;
}
const context = $(selector)[0].getContext("2d");
let i, len;
context.clearRect(0, 0, 200, 50);
context.fillStyle = "back"; //white
for (i = 0, len = labels.length; i < len; i++) {
let x = 10 + (180 * i) / (len - 1);
if (i === len - 1) {
x -= context.measureText(labels[i]).width;
} else if (i > 0) {
x -= 0.5 * context.measureText(labels[i]).width;
}
context.fillText(labels[i], x, 10);
}
const gradient = context.createLinearGradient(0, 0, 200, 0);
for (i = 0, len = colorStops.length; i < len; i++) {
gradient.addColorStop(i / (len - 1), colorStops[i]);
}
context.fillStyle = gradient;
context.fillRect(10, 20, 200, 25);
}
//======================== 外部呼叫function ===========================
//紀錄熱點座標
function getHopspotPoint(data) {
myDataList = data;
}
//呼叫載入熱圖
async function toLoadHeatmap(roomArr) {
const model = viewer.model;
loadHeatmaps(model, roomArr);
}
function setShadowShow(type = false) {
viewer.setGroundShadow(type);
viewer.impl.sceneUpdated(true);
}
//============================= end ===================================
function moveViewToDevice(letter) {
if (letter != "") {
viewer.clearSelection();
viewer.select(letter);
viewer.fitToView([letter]);
} else {
viewer.clearSelection();
}
}