pccv_front/src/hooks/baja/useSystemStatusByBaja.js

784 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};
}