784 lines
26 KiB
JavaScript
784 lines
26 KiB
JavaScript
import { onMounted, ref, computed, watch, markRaw, inject } from "vue";
|
||
import { getDashboardDevice } from "@/apis/dashboard";
|
||
import useSearchParams from "@/hooks/useSearchParam";
|
||
import useSystemHeatmap from "./useSystemHeatmap";
|
||
|
||
export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
||
const rawData = ref([]);
|
||
const forgeViewer = ref(null);
|
||
const urn = ref("");
|
||
const lightColorMap = {
|
||
1: "#009100",
|
||
2: "#c9bf00",
|
||
4: "#ff1212",
|
||
};
|
||
const { searchParams } = useSearchParams();
|
||
|
||
// DataVisualization 擴充套件的全域變數
|
||
let dataVizExtn = null;
|
||
let spriteAnimations = new Map(); // 用 Map 來追蹤多個動畫 {dbId: {interval, arrow, viewable}}
|
||
let cameraEventAdded = false; // 追蹤是否已經添加了相機事件監聽器
|
||
|
||
const initialData = ref(null);
|
||
|
||
const updateInitialData = (data = false) => {
|
||
initialData.value = data;
|
||
};
|
||
|
||
const { updateHeatMapData, updateTemp, initHeatMap } =
|
||
useSystemHeatmap(updateHeatBarIsShow);
|
||
|
||
const updateForgeViewer = (viewer) => {
|
||
if (!viewer) {
|
||
forgeViewer.value = null;
|
||
return;
|
||
}
|
||
forgeViewer.value = markRaw(viewer);
|
||
initHeatMap(viewer);
|
||
};
|
||
|
||
const getSubPoint = (normal, close, error, sub_points = []) => {
|
||
let points = {
|
||
...Object.fromEntries(sub_points.map((p) => [p, ""])),
|
||
};
|
||
if (normal) points[normal] = "";
|
||
if (close) points[close] = "";
|
||
if (error) points[error] = "";
|
||
return points;
|
||
};
|
||
const subscribeData = ref({});
|
||
|
||
watch(rawData, () => {
|
||
let sub_data = {};
|
||
|
||
rawData.value.forEach((d) => {
|
||
sub_data = {
|
||
...sub_data,
|
||
...Object.fromEntries(
|
||
(d.device || []).map((dev) => [
|
||
dev.device_number,
|
||
{
|
||
...dev,
|
||
labelText: d.labelText,
|
||
show_value: d.labelText,
|
||
device_normal_point_name: d.device_normal_point_name,
|
||
device_close_point_name: d.device_close_point_name,
|
||
device_error_point_name: d.device_error_point_name,
|
||
device_normal_point_value: d.device_normal_point_value,
|
||
device_close_point_value: d.device_close_point_value,
|
||
device_error_point_value: d.device_error_point_value,
|
||
device_normal_color: d.device_normal_color,
|
||
device_close_color: d.device_close_color,
|
||
device_error_color: d.device_error_color,
|
||
forge_dbid: parseInt(dev.forge_dbid),
|
||
device_coordinate_3d: dev.device_coordinate_3d
|
||
? JSON.parse(dev.device_coordinate_3d)
|
||
: { x: 0, y: 0 },
|
||
points: getSubPoint(
|
||
d.device_normal_point_name,
|
||
d.device_close_point_name,
|
||
d.device_error_point_name,
|
||
d.points || []
|
||
),
|
||
subSys: d.subSys,
|
||
is_show: true,
|
||
currentColor: d.device_normal_color,
|
||
},
|
||
])
|
||
),
|
||
};
|
||
});
|
||
subscribeData.value = sub_data;
|
||
updateHeatMapData(sub_data);
|
||
updateSubscribeDataFromBaja(sub_data);
|
||
});
|
||
|
||
const visibleDbid = computed(() => {
|
||
let visible = [];
|
||
|
||
rawData.value.forEach((d) => {
|
||
visible = [
|
||
...visible,
|
||
...d.device.map((dev) => parseInt(dev.forge_dbid)),
|
||
];
|
||
});
|
||
|
||
// return visible;
|
||
return [
|
||
26, 32, 38, 43, 48, 53, 58, 63, 70, 76, 879, 1068, 1011, 1065, 855, 1109,
|
||
867,
|
||
963,1044,966,1139,936,1136,957,1133,954,610,1130,951,1041,939,
|
||
1145,987,999,1148,1002,1151,981,
|
||
813,1088,825,1091,804
|
||
];
|
||
});
|
||
|
||
const getDevice = async (option = 1) => {
|
||
const res = await getDashboardDevice({
|
||
option: parseInt(option),
|
||
});
|
||
rawData.value = res.data.map((d) => ({
|
||
...d,
|
||
key: d.subSys,
|
||
}));
|
||
};
|
||
|
||
// subscribe from baja
|
||
const booleanPointFacets = ref({});
|
||
const updateFacets = (point, facets) => {
|
||
booleanPointFacets.value = {
|
||
...booleanPointFacets,
|
||
[point]: facets,
|
||
};
|
||
};
|
||
const updateDeviceData = (device_number, point, value) => {
|
||
// 檢查 subscribeData.value[device_number] 是否存在
|
||
if (!subscribeData.value[device_number]) {
|
||
console.log(`Device ${device_number} is not initialized.`);
|
||
return;
|
||
}
|
||
|
||
const correspondPoint = initialData.value.points.find(
|
||
({ name }) => name === point
|
||
);
|
||
// console.log("sub 回傳值 ", typeof value)
|
||
const text = correspondPoint
|
||
? correspondPoint.values.find(
|
||
({ value: pValue }) => pValue === parseInt(value)
|
||
)?.text || ""
|
||
: value;
|
||
|
||
// console.log("baja update data sub", correspondPoint, device_number, point, value);
|
||
subscribeData.value[device_number].points[point] = text;
|
||
if (
|
||
point.toLowerCase() === "temp" &&
|
||
parseInt(searchParams.value.option) > 1
|
||
) {
|
||
updateTemp(device_number, value);
|
||
}
|
||
// 當點位是 "light",且目前顏色不是錯誤顏色時,從 lightColorMap 取顏色
|
||
if (
|
||
point.toLowerCase() === "light" &&
|
||
subscribeData.value[device_number].currentColor !==
|
||
subscribeData.value[device_number].device_error_color
|
||
) {
|
||
subscribeData.value[device_number].currentColor =
|
||
lightColorMap[Number(value)] ||
|
||
subscribeData.value[device_number].device_normal_color;
|
||
}
|
||
|
||
// 當點位是錯誤點時,判斷值是否等於錯誤值,決定是否使用錯誤顏色
|
||
if (point === subscribeData.value[device_number].device_error_point_name) {
|
||
subscribeData.value[device_number].currentColor =
|
||
value === subscribeData.value[device_number].device_error_point_value
|
||
? subscribeData.value[device_number].device_error_color
|
||
: subscribeData.value[device_number].device_normal_color;
|
||
}
|
||
|
||
updateLabelText(device_number, point, text);
|
||
};
|
||
|
||
const transformDeviceNumber = (device_number) => {
|
||
const transformed = device_number.replaceAll("_", "/");
|
||
// 找到最後一個 / 的位置,並截取到該位置之前
|
||
const lastSlashIndex = transformed.lastIndexOf("/");
|
||
return lastSlashIndex !== -1
|
||
? transformed.substring(0, lastSlashIndex)
|
||
: transformed;
|
||
};
|
||
|
||
const updateLabelText = (key, point, value) => {
|
||
let text = subscribeData.value[key].labelText.replace(`%${point}`, value);
|
||
Object.keys(subscribeData.value[key].points)
|
||
.filter((p) => p !== point)
|
||
.forEach((p) => {
|
||
text = text.replace(`%${p}`, subscribeData.value[key].points[p]);
|
||
});
|
||
subscribeData.value[key].show_value = text;
|
||
};
|
||
|
||
const subComponents = ref(null);
|
||
const updateSubscribeDataFromBaja = (data) => {
|
||
for (let [key, value] of Object.entries(data)) {
|
||
window.require &&
|
||
window.requirejs(["baja!"], (baja) => {
|
||
console.log("進入 bajaSubscriber 準備執行BQL訂閱");
|
||
|
||
const ordKey = key;
|
||
const ordPath = transformDeviceNumber(key);
|
||
console.log("ordPath", ordPath, "ordKey", ordKey, value);
|
||
const fullOrdPath = `local:|foxs:|station:|slot:/Drivers/NiagaraNetwork/PCCV/points/${ordPath}/${ordKey}`; // 完整路徑
|
||
console.log("嘗試訪問路徑:", fullOrdPath); // 打印完整路徑
|
||
|
||
baja.Ord.make(fullOrdPath)
|
||
.get()
|
||
.then((folder) => {
|
||
console.log("成功獲取 folder:", folder);
|
||
const batch = new baja.comm.Batch();
|
||
const sub = new baja.Subscriber();
|
||
sub.attach({
|
||
changed: function (prop, cx) {
|
||
console.log("數據變更觸發:", prop.$getDisplayName());
|
||
if (prop.$getDisplayName() !== "Out") return;
|
||
if (
|
||
Object.hasOwn(
|
||
booleanPointFacets.value,
|
||
prop.$complex.$propInParent.$slotName
|
||
)
|
||
) {
|
||
const facets =
|
||
booleanPointFacets.value[
|
||
prop.$complex.$propInParent.$slotName
|
||
];
|
||
|
||
for (let [facetKey, facetValue] of Object.entries(facets)) {
|
||
if (facetValue === prop.$getValue().getValueDisplay()) {
|
||
updateDeviceData(
|
||
key,
|
||
prop.$complex.$propInParent.$slotName,
|
||
facetKey
|
||
);
|
||
}
|
||
}
|
||
} else {
|
||
updateDeviceData(
|
||
key,
|
||
prop.$complex.$propInParent.$slotName,
|
||
prop.$getValue().getValueDisplay()
|
||
);
|
||
}
|
||
},
|
||
});
|
||
console.log("開始遍歷控制點");
|
||
folder
|
||
.getSlots()
|
||
.is("control:ControlPoint")
|
||
.eachValue((point) => {
|
||
console.log("找到控制點:", point.getDisplayName());
|
||
console.log("配置的點位:", Object.keys(value.points));
|
||
|
||
if (
|
||
Object.keys(value.points).includes(point.getDisplayName())
|
||
) {
|
||
console.log(
|
||
"匹配到點位,開始訂閱:",
|
||
point.getDisplayName()
|
||
);
|
||
baja.Ord.make(
|
||
`local:|foxs:|station:|slot:/Drivers/NiagaraNetwork/PCCV/points/${ordPath}/${ordKey}/${point.getDisplayName()}`
|
||
)
|
||
.get()
|
||
.then((component) => {
|
||
console.log("獲取到 component:", component);
|
||
if (
|
||
point.getType().getTypeSpec() ===
|
||
"control:BooleanWritable"
|
||
) {
|
||
const facets = component.getFacets1().toObject();
|
||
updateFacets(point.getDisplayName(), facets);
|
||
for (let [facetKey, facetValue] of Object.entries(
|
||
facets
|
||
)) {
|
||
if (
|
||
facetValue ===
|
||
component.getOut().getValue().toString()
|
||
) {
|
||
updateDeviceData(
|
||
key,
|
||
point.getDisplayName(),
|
||
facetKey
|
||
);
|
||
}
|
||
}
|
||
} else {
|
||
updateDeviceData(
|
||
key,
|
||
point.getDisplayName(),
|
||
component.getOut().getValue()
|
||
);
|
||
}
|
||
|
||
sub
|
||
.subscribe({
|
||
comps: component, // Can also just be an singular Component instance
|
||
batch, // if defined, any network calls will be batched into this object (optional)
|
||
})
|
||
.then(() => {
|
||
console.log("subscribed successfully");
|
||
subComponents.value = sub;
|
||
})
|
||
.catch(function (err) {
|
||
baja.error(
|
||
"some components failed to subscribe: " + err
|
||
);
|
||
});
|
||
});
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
};
|
||
|
||
const updateDbidPosition = (viewer, data) => {
|
||
if (!viewer) return;
|
||
if (!forgeViewer.value) forgeViewer.value = markRaw(viewer);
|
||
const tree = viewer.model.getData().instanceTree;
|
||
const fragList = viewer.model.getFragmentList();
|
||
for (let [key, value] of Object.entries(data)) {
|
||
const nodebBox = new window.THREE.Box3();
|
||
|
||
// for each fragId on the list, get the bounding box
|
||
tree.enumNodeFragments(
|
||
value.forge_dbid,
|
||
(fragId) => {
|
||
const fragbBox = new window.THREE.Box3();
|
||
fragList.getWorldBounds(fragId, fragbBox);
|
||
nodebBox.union(fragbBox); // create a unifed bounding box
|
||
},
|
||
true
|
||
);
|
||
subscribeData.value[key].device_coordinate_3d = viewer.worldToClient(
|
||
nodebBox.getCenter()
|
||
);
|
||
subscribeData.value[key].is_show = viewer.isNodeVisible(value.forge_dbid);
|
||
}
|
||
};
|
||
|
||
const fitToView = () => {
|
||
const { x, y, z } = JSON.parse(searchParams.value.camera_position);
|
||
const newPosition = new THREE.Vector3(x, y, z); //!<<< 相机的新位置
|
||
|
||
const {
|
||
x: x1,
|
||
y: y1,
|
||
z: z1,
|
||
} = JSON.parse(searchParams.value.target_position); //!<<< 计算新焦点位置
|
||
const newTarget = new THREE.Vector3(x1, y1, z1); //!<<< 焦點的新位置
|
||
|
||
forgeViewer.value.navigation.getCamera().setView({
|
||
position: newPosition.clone(),
|
||
target: newTarget.clone(),
|
||
});
|
||
setTimeout(() => {
|
||
updateDbidPosition(forgeViewer.value, subscribeData.value);
|
||
}, 700);
|
||
};
|
||
|
||
const hideAllObjects = (instanceTree, filDbids = []) => {
|
||
if (!forgeViewer.value || !instanceTree) return;
|
||
const allDbIds = Object.keys(instanceTree.nodeAccess.dbIdToIndex).map(
|
||
Number
|
||
);
|
||
|
||
forgeViewer.value.hide(allDbIds);
|
||
if (filDbids.length > 0) {
|
||
forgeViewer.value.show(filDbids);
|
||
}
|
||
fitToView();
|
||
forgeViewer.value.impl.invalidate(true);
|
||
};
|
||
|
||
const loadModel = (viewer, filePath) => {
|
||
return new Promise(function (resolve, reject) {
|
||
// 使用 loadModel 直接加載本地 .svf 文件
|
||
viewer.loadModel(
|
||
filePath,
|
||
{},
|
||
(model) => {
|
||
viewer.setGroundShadow(false);
|
||
viewer.setLightPreset(18);
|
||
|
||
viewer.impl.renderer().setClearAlpha(0); // 清除 alpha 通道
|
||
viewer.impl.glrenderer().setClearColor(0xffffff, 0); // 設置透明背景,顏色碼無關緊要
|
||
viewer.impl.invalidate(true); // 觸發渲染
|
||
resolve(model);
|
||
},
|
||
(error) => {
|
||
console.error("模型加載失敗: ", error);
|
||
reject(error);
|
||
}
|
||
);
|
||
});
|
||
};
|
||
|
||
const reloadModal = () => {};
|
||
|
||
let themingInterval = null;
|
||
let blinkingInterval = null; // 計時器 ID 的引用
|
||
|
||
// 增強版:停止閃爍並徹底清理
|
||
const stopBlinking = () => {
|
||
console.log("正在停止基於 Selection 的閃爍效果...");
|
||
if (blinkingInterval) {
|
||
clearInterval(blinkingInterval);
|
||
blinkingInterval = null;
|
||
}
|
||
if (forgeViewer.value) {
|
||
// 核心清理:取消所有選取
|
||
forgeViewer.value.clearSelection();
|
||
}
|
||
};
|
||
|
||
const startBlinking = (dbidToBlink) => {
|
||
console.log(`正在為 dbId: ${dbidToBlink} 啟動基於 Selection 的閃爍...`);
|
||
if (!forgeViewer.value) return;
|
||
|
||
// 步驟 1: 設定一次我們希望的閃爍顏色和樣式
|
||
const blinkColor = new THREE.Color(0x00aaff); // 水藍色
|
||
forgeViewer.value.setSelectionColor(
|
||
blinkColor,
|
||
Autodesk.Viewing.SelectionType.OVERLAYED
|
||
);
|
||
|
||
let isSelected = false; // 用一個旗標來追蹤目前的選取狀態
|
||
|
||
blinkingInterval = setInterval(() => {
|
||
// 在計時器內部也要檢查 viewer 是否還存在
|
||
if (!forgeViewer.value) {
|
||
stopBlinking();
|
||
return;
|
||
}
|
||
|
||
if (isSelected) {
|
||
// 如果已選取,就取消選取 (閃爍 OFF)
|
||
forgeViewer.value.clearSelection();
|
||
} else {
|
||
// 如果未選取,就選取目標物件 (閃爍 ON)
|
||
// 注意:select 方法需要傳入 model 物件
|
||
forgeViewer.value.select(
|
||
dbidToBlink,
|
||
forgeViewer.value.model,
|
||
Autodesk.Viewing.SelectionType.OVERLAYED
|
||
);
|
||
}
|
||
|
||
// 切換狀態,為下一次循環做準備
|
||
isSelected = !isSelected;
|
||
}, 700); // 我們可以稍微調整一下頻率,例如 700 毫秒
|
||
};
|
||
|
||
watch([forgeViewer, visibleDbid], ([viewer, dbids]) => {
|
||
console.log("監聽到 forgeViewer 或 visibleDbid 的變化", viewer, dbids);
|
||
stopBlinking();
|
||
clearSprites(); // 清理之前的 Sprites
|
||
|
||
if (viewer) {
|
||
hideAllObjects(viewer.model.getData().instanceTree, dbids);
|
||
}
|
||
|
||
// 判斷新狀態是否需要閃爍和 Sprites
|
||
if (viewer && dbids.includes(879)) {
|
||
// startBlinking([879, 1068, 1011, 1065, 855, 1109, 867,963,1044,966,1139,936,1136,957,1133,954,610,1130,951,1041,939,1145,987,999,1148,1002,1151,981,813,1088,825,1091,804]);
|
||
const spriteConfigs = [
|
||
{ dbId: 879, reverse: false },
|
||
{ dbId: 1011, reverse: false },
|
||
{ dbId: 855, reverse: true },
|
||
{ dbId: 867, reverse: true },
|
||
{ dbId: 963, reverse: false },
|
||
{ dbId: 966, reverse: true },
|
||
{ dbId: 936, reverse: true },
|
||
{ dbId: 957, reverse: true },
|
||
{ dbId: 954, reverse: true },
|
||
{ dbId: 951, reverse: true },
|
||
{ dbId: 939, reverse: true },
|
||
{ dbId: 987, reverse: true },
|
||
{ dbId: 999, reverse: true },
|
||
{ dbId: 1002, reverse: true },
|
||
{ dbId: 981, reverse: true },
|
||
{ dbId: 813, reverse: true },
|
||
{ dbId: 825, reverse: false },
|
||
{ dbId: 804, reverse: false },
|
||
];
|
||
spriteConfigs.forEach(({ dbId, reverse }) => {
|
||
createSprites(viewer, dbId, reverse);
|
||
});
|
||
}
|
||
});
|
||
|
||
watch(initialData, (newValue) => {
|
||
if (newValue) {
|
||
getDevice(searchParams.value.option);
|
||
}
|
||
});
|
||
|
||
watch(
|
||
searchParams,
|
||
(newValue) => {
|
||
getDevice(newValue.option);
|
||
},
|
||
{
|
||
deep: true,
|
||
}
|
||
);
|
||
|
||
// 動態Sprites創建函數
|
||
const createSprites = async (viewer, dbId, reverse = false) => {
|
||
try {
|
||
// 先清理這個 dbId 的舊 Sprite(如果存在)
|
||
clearSingleSprite(dbId);
|
||
|
||
// 1. 載入 DataVisualization 擴充套件
|
||
if (!dataVizExtn) {
|
||
dataVizExtn = await viewer.loadExtension("Autodesk.DataVisualization");
|
||
}
|
||
|
||
// 2. 建立 ViewableStyle
|
||
const DataVizCore = Autodesk.DataVisualization.Core;
|
||
const viewableType = DataVizCore.ViewableType.SPRITE;
|
||
const spriteColor = new THREE.Color(0x00ffff);
|
||
const spriteIconUrl = "spot.svg";
|
||
const style = new DataVizCore.ViewableStyle(
|
||
viewableType,
|
||
spriteColor,
|
||
spriteIconUrl
|
||
);
|
||
const viewableData = new DataVizCore.ViewableData();
|
||
viewableData.spriteSize = 12;
|
||
|
||
// 3. 取得 dbId 的 3D 位置
|
||
viewer.getObjectTree(function (instanceTree) {
|
||
const fragList = viewer.model.getFragmentList();
|
||
let fragIds = [];
|
||
instanceTree.enumNodeFragments(dbId, function (fragId) {
|
||
fragIds.push(fragId);
|
||
});
|
||
|
||
if (fragIds.length > 0) {
|
||
let box = new THREE.Box3();
|
||
fragIds.forEach((fragId) => {
|
||
let fragBox = new THREE.Box3();
|
||
fragList.getWorldBounds(fragId, fragBox);
|
||
box.union(fragBox);
|
||
});
|
||
|
||
// 算出主軸方向
|
||
const size = new THREE.Vector3();
|
||
box.getSize(size);
|
||
// 找出最長的軸
|
||
let axis = "x";
|
||
if (size.y >= size.x && size.y >= size.z) axis = "y";
|
||
if (size.z >= size.x && size.z >= size.y) axis = "z";
|
||
|
||
// 取得所有 fragment 的中心點
|
||
let centers = fragIds.map((fragId) => {
|
||
let fragBox = new THREE.Box3();
|
||
fragList.getWorldBounds(fragId, fragBox);
|
||
return fragBox.getCenter(new THREE.Vector3());
|
||
});
|
||
|
||
// 找出距離最遠的兩點作為動畫路徑
|
||
let maxDist = 0;
|
||
let animStart = centers[0],
|
||
animEnd = centers[0];
|
||
for (let i = 0; i < centers.length; i++) {
|
||
for (let j = i + 1; j < centers.length; j++) {
|
||
let dist = centers[i].distanceTo(centers[j]);
|
||
if (dist > maxDist) {
|
||
maxDist = dist;
|
||
animStart = centers[i];
|
||
animEnd = centers[j];
|
||
}
|
||
}
|
||
}
|
||
|
||
// fallback: 若只有一個 fragment 或所有中心點重疊,改用主軸方向的端點
|
||
if (centers.length < 2 || maxDist === 0) {
|
||
const boxCenter = box.getCenter(new THREE.Vector3());
|
||
const halfSize = size.clone().multiplyScalar(0.5);
|
||
|
||
animStart = boxCenter.clone();
|
||
animEnd = boxCenter.clone();
|
||
|
||
// 根據主軸設定起點和終點
|
||
if (axis === "x") {
|
||
animStart.x -= halfSize.x;
|
||
animEnd.x += halfSize.x;
|
||
} else if (axis === "y") {
|
||
animStart.y -= halfSize.y;
|
||
animEnd.y += halfSize.y;
|
||
} else {
|
||
// z軸
|
||
animStart.z -= halfSize.z;
|
||
animEnd.z += halfSize.z;
|
||
}
|
||
}
|
||
|
||
// === 依主軸方向排序,確保動畫方向一致 ===
|
||
// 根據主軸來排序起點和終點,確保動畫方向一致
|
||
if (axis === "x" && animStart.x > animEnd.x) {
|
||
const tmp = animStart;
|
||
animStart = animEnd;
|
||
animEnd = tmp;
|
||
}
|
||
if (axis === "y" && animStart.y > animEnd.y) {
|
||
const tmp = animStart;
|
||
animStart = animEnd;
|
||
animEnd = tmp;
|
||
}
|
||
if (axis === "z" && animStart.z > animEnd.z) {
|
||
const tmp = animStart;
|
||
animStart = animEnd;
|
||
animEnd = tmp;
|
||
}
|
||
|
||
// === 若 reverse 為 true,則反向 ===
|
||
if (reverse) {
|
||
const tmp = animStart;
|
||
animStart = animEnd;
|
||
animEnd = tmp;
|
||
}
|
||
|
||
console.log(`dbId ${dbId} 動畫資訊:`, {
|
||
axis: axis,
|
||
size: size,
|
||
animStart: animStart,
|
||
animEnd: animEnd,
|
||
distance: animStart.distanceTo(animEnd),
|
||
reverse: reverse,
|
||
});
|
||
|
||
// 4. 建立 SpriteViewable,初始在 animStart
|
||
const viewable = new DataVizCore.SpriteViewable(
|
||
animStart.clone(),
|
||
style,
|
||
dbId
|
||
);
|
||
viewableData.addViewable(viewable);
|
||
viewableData.finish().then(() => {
|
||
dataVizExtn.addViewables(viewableData);
|
||
|
||
// 5. 單向從起點移動到終點,然後重頭再來
|
||
// === 新增 HTML箭頭動畫,會跟隨Sprite移動並指向終點 ===
|
||
let arrow = document.createElement("img");
|
||
arrow.src = "arrow.png";
|
||
arrow.style.position = "absolute";
|
||
arrow.style.width = "22px";
|
||
arrow.style.height = "22px";
|
||
arrow.style.pointerEvents = "none";
|
||
arrow.style.zIndex = 10;
|
||
arrow.className = `custom-3d-arrow-${dbId}`; // 加上 dbId 作為唯一識別
|
||
viewer.container.appendChild(arrow);
|
||
|
||
function updateArrow(pos) {
|
||
const pos2d = viewer.worldToClient(pos.clone());
|
||
const end2d = viewer.worldToClient(animEnd.clone());
|
||
arrow.style.left = `${pos2d.x - 16}px`;
|
||
arrow.style.top = `${pos2d.y - 16}px`;
|
||
// 算角度:pos 指向 animEnd
|
||
const dx = end2d.x - pos2d.x;
|
||
const dy = end2d.y - pos2d.y;
|
||
const angle = (Math.atan2(dy, dx) * 180) / Math.PI;
|
||
arrow.style.transform = `rotate(${angle}deg)`;
|
||
}
|
||
|
||
// camera 移動時也要更新所有箭頭(只添加一次事件監聽器)
|
||
if (!cameraEventAdded) {
|
||
viewer.addEventListener(
|
||
Autodesk.Viewing.CAMERA_CHANGE_EVENT,
|
||
() => {
|
||
spriteAnimations.forEach((animation) => {
|
||
animation.updateArrow(animation.currentPos);
|
||
});
|
||
}
|
||
);
|
||
cameraEventAdded = true;
|
||
}
|
||
|
||
// 動畫主程式
|
||
let t = 0;
|
||
let currentPos = animStart.clone();
|
||
const animationInterval = setInterval(() => {
|
||
t += 0.01;
|
||
if (t > 1) t = 0; // 到終點就重頭
|
||
currentPos = animStart.clone().lerp(animEnd, t);
|
||
dataVizExtn.invalidateViewables([dbId], (v) => {
|
||
return {
|
||
position: {
|
||
x: currentPos.x,
|
||
y: currentPos.y,
|
||
z: currentPos.z,
|
||
},
|
||
};
|
||
});
|
||
updateArrow(currentPos);
|
||
|
||
// 更新保存的 currentPos
|
||
if (spriteAnimations.has(dbId)) {
|
||
spriteAnimations.get(dbId).currentPos = currentPos;
|
||
}
|
||
}, 30);
|
||
|
||
// 保存這個 dbId 的動畫資訊
|
||
spriteAnimations.set(dbId, {
|
||
interval: animationInterval,
|
||
arrow: arrow,
|
||
viewable: viewable,
|
||
updateArrow: updateArrow,
|
||
currentPos: currentPos,
|
||
});
|
||
});
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error("創建動態Sprites時發生錯誤:", error);
|
||
}
|
||
};
|
||
|
||
// 清理單個 Sprite 的函數
|
||
const clearSingleSprite = (dbId) => {
|
||
if (spriteAnimations.has(dbId)) {
|
||
const animation = spriteAnimations.get(dbId);
|
||
|
||
// 清理動畫計時器
|
||
if (animation.interval) {
|
||
clearInterval(animation.interval);
|
||
}
|
||
|
||
// 清理箭頭元素
|
||
if (animation.arrow && animation.arrow.parentNode) {
|
||
animation.arrow.parentNode.removeChild(animation.arrow);
|
||
}
|
||
|
||
// 移除這個 dbId 的記錄
|
||
spriteAnimations.delete(dbId);
|
||
|
||
console.log(`已清理 dbId ${dbId} 的 Sprite 資源`);
|
||
}
|
||
};
|
||
|
||
// 清理所有 Sprites 和相關資源的函數
|
||
const clearSprites = () => {
|
||
// 清理所有動畫
|
||
spriteAnimations.forEach((animation, dbId) => {
|
||
clearSingleSprite(dbId);
|
||
});
|
||
|
||
// 清理所有 viewables
|
||
if (dataVizExtn) {
|
||
dataVizExtn.removeAllViewables();
|
||
}
|
||
|
||
// 重置相機事件標記
|
||
cameraEventAdded = false;
|
||
|
||
console.log("已清理所有 Sprites 和相關資源");
|
||
};
|
||
|
||
return {
|
||
subscribeData,
|
||
visibleDbid,
|
||
updateDbidPosition,
|
||
hideAllObjects,
|
||
updateForgeViewer,
|
||
forgeViewer,
|
||
loadModel,
|
||
urn,
|
||
updateInitialData,
|
||
subComponents,
|
||
createSprites,
|
||
clearSprites,
|
||
clearSingleSprite,
|
||
};
|
||
}
|