1082 lines
33 KiB
Vue
1082 lines
33 KiB
Vue
<script setup>
|
|
import { createApp, ref, watch, h, nextTick } from "vue";
|
|
import { difference, throttle, debounce } from "es-toolkit";
|
|
import { utopiaUrn, blankUrn, getAccessToken, loadModel } from "@/components/viewer/utils/aps-init";
|
|
import {
|
|
addTemSelectionListener,
|
|
applyLayerSetting,
|
|
findTemInside,
|
|
getTradeCodeFromTem,
|
|
objIsLand,
|
|
objIsTem,
|
|
unloadUnusedExtensions,
|
|
} from "@/components/viewer/utils/aps-viewer";
|
|
import { html, renderSvg } from "@/components/viewer/utils/dom";
|
|
import {
|
|
colorStrToVector4,
|
|
extractPoints,
|
|
GeometryCallback,
|
|
getBounds,
|
|
sortByAngle,
|
|
} from "@/components/viewer/utils/geometry";
|
|
import Legends from "@/components/viewer/Legends.vue";
|
|
import LayerSetting from "@/components/viewer/LayerSetting.vue";
|
|
import ProductNoteHistory from "@/components/viewer/ProductNoteHistory.vue";
|
|
|
|
const { $exportpdf, $getdata, $connectWebSocket, $snackbar, $store, $subscribe } = useNuxtApp();
|
|
const { dealer, layersetting } = $store;
|
|
const viewerId = "viewer";
|
|
const viewerRef = useTemplateRef(viewerId);
|
|
|
|
const products = ref([]);
|
|
const productStatuses = ref([]);
|
|
const saleStatuses = ref([]);
|
|
const carts = ref([]);
|
|
const dealers = ref([]);
|
|
const selectedProductForNote = ref(null);
|
|
|
|
const statusModes = {
|
|
SIMPLE: "Đơn giản",
|
|
DETAILED: "Chi tiết",
|
|
};
|
|
|
|
const statusMode = ref(statusModes.SIMPLE);
|
|
|
|
const statuses = computed(() => (statusMode.value === statusModes.SIMPLE ? saleStatuses.value : productStatuses.value));
|
|
|
|
const defaultStatusIds = computed(() => statuses.value.map((x) => x.id));
|
|
const defaultCartIds = computed(() => carts.value.map((x) => x.id));
|
|
const defaultDealerIds = computed(() => dealers.value.map((x) => x.id));
|
|
|
|
const filters = ref({
|
|
statusIds: defaultStatusIds.value,
|
|
cartIds: defaultCartIds.value,
|
|
dealerIds: defaultDealerIds.value,
|
|
});
|
|
|
|
function addMeshes(viewer) {
|
|
viewer.toolController.setMouseWheelInputEnabled(false); // disable zoom wheel until colors are added
|
|
|
|
if (window.colorModel) {
|
|
viewer.unloadModel(window.colorModel);
|
|
}
|
|
|
|
viewer.getObjectTree(async (instanceTree) => {
|
|
const sceneBuilder = await viewer.getExtension("Autodesk.Viewing.SceneBuilder");
|
|
const modelBuilder = await sceneBuilder.addNewModel({
|
|
conserveMemory: false,
|
|
});
|
|
window.colorModel = modelBuilder.model;
|
|
viewer.setBackgroundColor(255, 255, 255, 255, 255, 255);
|
|
|
|
const vpId = 1;
|
|
const vpXform = viewer.model.getPageToModelTransform(vpId);
|
|
const inverseXform = new THREE.Matrix4();
|
|
const pt = new THREE.Vector3();
|
|
|
|
const material = new THREE.MeshBasicMaterial({
|
|
depthWrite: false,
|
|
depthTest: true,
|
|
});
|
|
|
|
function createMesh(lines, productId) {
|
|
// inverse viewer's matrix to convert CAD coords (~400000) to viewer coords (~10)
|
|
inverseXform.copy(vpXform).invert();
|
|
|
|
// Extract all unique points to form a polygon
|
|
const worldPoints = extractPoints(lines);
|
|
const viewerPoints = [];
|
|
|
|
for (let i = 0; i < worldPoints.length; i++) {
|
|
const { x, y } = worldPoints[i];
|
|
pt.set(x, y, 0).applyMatrix4(inverseXform);
|
|
viewerPoints.push(new THREE.Vector2(pt.x, pt.y));
|
|
}
|
|
|
|
let sortedPoints = sortByAngle(viewerPoints);
|
|
if (productId === 9290) {
|
|
// special case - Z.E02.02A, fix manually
|
|
sortedPoints = sortedPoints.filter((coor) => coor.y < 4);
|
|
}
|
|
|
|
const shape = new THREE.Shape(sortedPoints);
|
|
const shapeGeo = new THREE.ShapeGeometry(shape);
|
|
const bufferGeo = new THREE.BufferGeometry().fromGeometry(shapeGeo);
|
|
|
|
const pos = bufferGeo.getAttribute("position");
|
|
// only set Z after calling ShapeGeometry()
|
|
for (let i = 0; i < pos.count; i++) {
|
|
pos.setZ(i, -1);
|
|
}
|
|
pos.needsUpdate = true;
|
|
|
|
const mesh = new THREE.Mesh(bufferGeo, material);
|
|
mesh.dbId = productId; // to be able to find product from an added mesh (in applyColors)
|
|
|
|
return mesh;
|
|
}
|
|
|
|
const dbIdMap = instanceTree.nodeAccess.dbIdToIndex;
|
|
const allDbIds = Object.keys(dbIdMap).map((id) => parseInt(id));
|
|
|
|
viewer.model.getBulkProperties(
|
|
allDbIds,
|
|
{
|
|
propFilter: ["name", "Layer", "Global width", "LO"],
|
|
},
|
|
(results) => {
|
|
const frags = viewer.model.getFragmentList();
|
|
window.meshes = [];
|
|
let tems = [];
|
|
let lands = [];
|
|
|
|
for (const obj of results) {
|
|
if (objIsTem(obj)) tems.push({ ...obj, bounds: getBounds(obj.dbId, frags) });
|
|
else if (objIsLand(obj)) lands.push({ ...obj, bounds: getBounds(obj.dbId, frags) });
|
|
}
|
|
|
|
const temsInside = lands.map((land) => findTemInside(land, tems));
|
|
|
|
lands.forEach((land, i) => {
|
|
const temInside = temsInside[i];
|
|
if (!temInside) {
|
|
if (land.dbId === 70520) {
|
|
return; // redundant polyline in Z.E02.02A from "26.01.16 - Export TMB - Phan mem.dwg"
|
|
}
|
|
console.error("Can't find temInside for land", land);
|
|
return;
|
|
}
|
|
const trade_code = getTradeCodeFromTem(temInside);
|
|
const product = products.value.find((product) => product.trade_code === trade_code);
|
|
if (!product) return;
|
|
|
|
const geoCb = new GeometryCallback(viewer, vpXform);
|
|
|
|
instanceTree.enumNodeFragments(land.dbId, (fragId) => {
|
|
const renderProxy = viewer.impl.getRenderProxy(viewer.model, fragId);
|
|
const vbr = new Autodesk.Viewing.Private.VertexBufferReader(
|
|
renderProxy.geometry,
|
|
viewer.impl.use2dInstancing,
|
|
);
|
|
vbr.enumGeomsForObject(land.dbId, geoCb);
|
|
const mesh = createMesh(geoCb.lines, product.id);
|
|
window.meshes.push(mesh);
|
|
modelBuilder.addMesh(mesh);
|
|
});
|
|
});
|
|
|
|
viewer.impl.invalidate(true, true, true);
|
|
applyColors();
|
|
viewer.toolController.setMouseWheelInputEnabled(true); // re-enable zoom wheel
|
|
const legend = document.querySelector("#legend.docking-panel.docking-panel-container-solid-color-a");
|
|
if (legend) {
|
|
legend.style.top = "initial";
|
|
legend.classList.remove("top-initial-important");
|
|
}
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show/hide certain colors based on filters, using `viewer.setThemingColor`,
|
|
* not modifying the materials of the added meshes.
|
|
*/
|
|
function applyColors() {
|
|
const { viewer, colorModel } = window;
|
|
if (!viewer || !colorModel) return;
|
|
|
|
const { statusIds, cartIds, dealerIds } = filters.value;
|
|
const white = colorStrToVector4("white");
|
|
|
|
colorModel.getObjectTree((instanceTree) => {
|
|
const dbIdMap = instanceTree.nodeAccess.dbIdToIndex;
|
|
const allDbIds = Object.keys(dbIdMap).map((id) => parseInt(id));
|
|
|
|
for (const dbId of allDbIds) {
|
|
instanceTree.enumNodeFragments(dbId, () => {
|
|
const product = products.value.find((p) => p.id === dbId);
|
|
const {
|
|
status,
|
|
status__code,
|
|
status__color,
|
|
status__sale_status,
|
|
status__sale_status__color,
|
|
cart,
|
|
cart__dealer,
|
|
} = product;
|
|
|
|
const notSoldDealerCondition = dealer && status__code === "not-sold";
|
|
const currentStatus = statusMode.value === statusModes.SIMPLE ? status__sale_status : status;
|
|
const statusCondition = statusIds.includes(currentStatus);
|
|
const statusColor = colorStrToVector4(
|
|
statusMode.value === statusModes.SIMPLE ? status__sale_status__color : status__color,
|
|
);
|
|
|
|
const diffCartFilter = difference(defaultCartIds.value, cartIds);
|
|
const diffDealerFilter = difference(defaultDealerIds.value, dealerIds);
|
|
const noCartFilter = diffCartFilter.length === 0;
|
|
const noDealerFilter = diffDealerFilter.length === 0;
|
|
const cartCondition = noCartFilter ? cartIds.includes(cart) || cart === null : cartIds.includes(cart);
|
|
const dealerCondition = noDealerFilter
|
|
? dealerIds.includes(cart__dealer) || cart__dealer === null
|
|
: dealerIds.includes(cart__dealer);
|
|
|
|
const showColor = statusCondition && !notSoldDealerCondition && cartCondition && dealerCondition;
|
|
viewer.setThemingColor(dbId, showColor ? statusColor : white, colorModel);
|
|
});
|
|
}
|
|
viewer.impl.invalidate(true, true, true);
|
|
});
|
|
}
|
|
|
|
const debouncedApplyColors = debounce(applyColors, 50);
|
|
|
|
const showProductViewModal = ref();
|
|
|
|
function closeProductViewModal() {
|
|
showProductViewModal.value = undefined;
|
|
}
|
|
|
|
function openProductViewModal(product) {
|
|
selectedProductForNote.value = product;
|
|
showProductViewModal.value = {
|
|
component: "product/ProductEdit",
|
|
title: "Thông tin chi tiết",
|
|
vbind: {
|
|
row: product,
|
|
view: true,
|
|
},
|
|
width: "80%",
|
|
height: "700px",
|
|
};
|
|
}
|
|
|
|
function openNoPermissionModal() {
|
|
showProductViewModal.value = {
|
|
component: "application/NoPermission",
|
|
title: "Không có quyền truy cập",
|
|
width: "35%",
|
|
height: "auto",
|
|
};
|
|
}
|
|
|
|
const legendsSFC = ref(null);
|
|
const isRefreshing = ref(false);
|
|
|
|
function mountLegends() {
|
|
if (legendsSFC.value) legendsSFC.value.unmount();
|
|
const content = document.querySelector(".content.docking-panel-scroll");
|
|
|
|
legendsSFC.value = createApp({
|
|
name: "Legends",
|
|
render() {
|
|
return h(Legends, {
|
|
products,
|
|
carts,
|
|
dealers,
|
|
defaultCartIds,
|
|
defaultDealerIds,
|
|
filters,
|
|
statuses,
|
|
statusModes,
|
|
statusMode,
|
|
onSwitchStatusMode: (newMode) => {
|
|
statusMode.value = newMode;
|
|
},
|
|
onUpdateFilters: (payload) => {
|
|
filters.value = {
|
|
...filters.value,
|
|
...payload,
|
|
};
|
|
},
|
|
onResetCartDealerFilters: () => {
|
|
filters.value = {
|
|
...filters.value,
|
|
cartIds: defaultCartIds.value,
|
|
dealerIds: defaultDealerIds.value,
|
|
};
|
|
},
|
|
onResetFilters: () => {
|
|
filters.value = {
|
|
statusIds: defaultStatusIds.value,
|
|
cartIds: defaultCartIds.value,
|
|
dealerIds: defaultDealerIds.value,
|
|
};
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
legendsSFC.value.mount(content);
|
|
return content;
|
|
}
|
|
|
|
async function refreshFromWebSocket() {
|
|
if (isRefreshing.value) return;
|
|
|
|
isRefreshing.value = true;
|
|
try {
|
|
await loadData(false);
|
|
debouncedApplyColors();
|
|
} finally {
|
|
isRefreshing.value = false;
|
|
}
|
|
}
|
|
|
|
function initViewer(container) {
|
|
const customToolbarId = "customToolbar";
|
|
const printId = "print";
|
|
const legendId = "legend";
|
|
const noteHistoryId = "noteHistory";
|
|
|
|
class LegendPanel extends Autodesk.Viewing.UI.DockingPanel {
|
|
constructor(container, id, title, options) {
|
|
super(container, id, title, options);
|
|
this.viewer = viewer;
|
|
Autodesk.Viewing.UI.DockingPanel.call(this, container, id, "");
|
|
|
|
this.container.classList.add(
|
|
"docking-panel-container-solid-color-a",
|
|
"is-flex",
|
|
"is-flex-direction-column",
|
|
"top-initial-important",
|
|
);
|
|
this.container.style["overflow-x"] = "unset";
|
|
this.container.style["overflow-y"] = "unset";
|
|
|
|
const panelTitle = this.container.querySelector(".docking-panel-title");
|
|
panelTitle.innerText = "Chú giải";
|
|
|
|
const panelCloseBtn = this.container.querySelector(".docking-panel-close");
|
|
panelCloseBtn.onclick = () => {
|
|
const legendBtnElem = document.getElementById(`${customToolbarId}-${legendId}Tool`);
|
|
this.container.classList.toggle("is-flex");
|
|
legendBtnElem.classList.toggle("active");
|
|
legendBtnElem.classList.toggle("inactive");
|
|
};
|
|
|
|
const content = html("div", { class: "content docking-panel-scroll" });
|
|
this.container.append(content);
|
|
mountLegends();
|
|
}
|
|
}
|
|
|
|
class NoteHistoryPanel extends Autodesk.Viewing.UI.DockingPanel {
|
|
constructor(container, id, title, options) {
|
|
super(container, id, title, options);
|
|
this.viewer = viewer;
|
|
this.app = null;
|
|
this.isVisibleState = false;
|
|
Autodesk.Viewing.UI.DockingPanel.call(this, container, id, "");
|
|
|
|
this.container.classList.add("docking-panel-container-solid-color-a", "is-flex-direction-column", "is-hidden");
|
|
|
|
const panelTitle = this.container.querySelector(".docking-panel-title");
|
|
panelTitle.innerText = "Lịch sử ghi chú";
|
|
|
|
const panelCloseBtn = this.container.querySelector(".docking-panel-close");
|
|
panelCloseBtn.onclick = () => {
|
|
this.toggleVisibility();
|
|
this.container.classList.toggle("is-flex");
|
|
this.container.classList.toggle("is-hidden");
|
|
};
|
|
|
|
const content = html("div", {
|
|
id: "note-history-content",
|
|
class: "content docking-panel-scroll",
|
|
});
|
|
this.container.append(content);
|
|
this.contentElement = content;
|
|
this.mountVueComponent(content);
|
|
}
|
|
|
|
toggleVisibility() {
|
|
console.log("[NoteHistoryPanel] toggleVisibility called, current state:", this.isVisibleState);
|
|
const noteHistoryBtnElem = document.getElementById(`${customToolbarId}-${noteHistoryId}Tool`);
|
|
if (this.isVisibleState) {
|
|
this.hide();
|
|
noteHistoryBtnElem?.classList.remove("active");
|
|
noteHistoryBtnElem?.classList.add("inactive");
|
|
} else {
|
|
this.show();
|
|
noteHistoryBtnElem?.classList.add("active");
|
|
noteHistoryBtnElem?.classList.remove("inactive");
|
|
}
|
|
}
|
|
|
|
show() {
|
|
console.log("[NoteHistoryPanel] show() called");
|
|
this.isVisibleState = true;
|
|
this.container.style.display = "flex";
|
|
|
|
// Đảm bảo content element giữ nguyên style
|
|
if (this.contentElement) {
|
|
this.contentElement.style.display = "flex";
|
|
this.contentElement.style.flex = "1";
|
|
this.contentElement.style.overflowY = "auto";
|
|
this.contentElement.style.overflowX = "hidden";
|
|
this.contentElement.style.minHeight = "0";
|
|
}
|
|
|
|
this.setVisible(true);
|
|
|
|
// Force re-render Vue component nếu cần
|
|
if (this.app) {
|
|
this.$forceUpdate?.();
|
|
}
|
|
}
|
|
|
|
hide() {
|
|
console.log("[NoteHistoryPanel] hide() called");
|
|
this.isVisibleState = false;
|
|
this.container.style.display = "none";
|
|
this.setVisible(false);
|
|
}
|
|
|
|
mountVueComponent(element) {
|
|
if (this.app) {
|
|
this.app.unmount();
|
|
}
|
|
this.app = createApp({
|
|
name: "ProductNoteHistory",
|
|
render: () =>
|
|
h(ProductNoteHistory, {
|
|
selectedProduct: selectedProductForNote.value,
|
|
}),
|
|
});
|
|
this.app.mount(element);
|
|
}
|
|
|
|
// Thêm method để refresh Vue component khi cần
|
|
refreshComponent() {
|
|
if (this.contentElement) {
|
|
this.mountVueComponent(this.contentElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
class Print extends Autodesk.Viewing.Extension {
|
|
load() {
|
|
if (this.viewer.toolbar) {
|
|
this.createUI();
|
|
} else {
|
|
this.onToolbarCreatedBinded = this.onToolbarCreated.bind(this);
|
|
this.viewer.addEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
unload() {
|
|
console.info("Print extension is now unloaded!");
|
|
return true;
|
|
}
|
|
|
|
createUI() {
|
|
const viewer = this.viewer;
|
|
let toolbarGroup = viewer.toolbar.getControl(customToolbarId);
|
|
if (!toolbarGroup) {
|
|
toolbarGroup = new Autodesk.Viewing.UI.ControlGroup(customToolbarId);
|
|
viewer.toolbar.addControl(toolbarGroup);
|
|
}
|
|
|
|
this.subToolbar = toolbarGroup;
|
|
const printBtn = new Autodesk.Viewing.UI.Button(`${customToolbarId}-${printId}Tool`);
|
|
const adskButtonIcon = printBtn.container.children.item(0);
|
|
renderSvg("/icon/print.svg").then((icon) => {
|
|
adskButtonIcon.append(icon);
|
|
});
|
|
|
|
printBtn.onClick = () => this.print(viewer);
|
|
printBtn.setToolTip(printId);
|
|
this.subToolbar.addControl(printBtn);
|
|
}
|
|
|
|
print(viewer) {
|
|
const getPdf = async (blob) => {
|
|
const docId = "bieu-do";
|
|
const div = html("div", { id: docId, style: "width: 1650px; height: 1030px" }, [
|
|
html("img", {
|
|
src: blob,
|
|
style: "object-fit: contain; max-height: 100%",
|
|
}),
|
|
]);
|
|
document.body.append(div);
|
|
const fileName = docId;
|
|
$exportpdf(docId, fileName, "a3", "landscape");
|
|
$snackbar("Đang xuất PDF...", { type: "is-info" });
|
|
div.remove();
|
|
};
|
|
|
|
const canvasBounds = viewer.impl.getCanvasBoundingClientRect();
|
|
const cvWidth = canvasBounds.width;
|
|
const cvHeight = canvasBounds.height;
|
|
const pageWidth = viewer.model.getMetadata("page_dimensions", "page_width"); // 11
|
|
|
|
const DPI = 150;
|
|
const scale = 3;
|
|
const widthInPixels = Math.floor(pageWidth * DPI * scale);
|
|
|
|
const canvasRatio = cvHeight / cvWidth;
|
|
const width = widthInPixels;
|
|
const height = Math.floor(widthInPixels * canvasRatio);
|
|
const screenshotOptions = { fullPage: false, margin: 0 };
|
|
|
|
Autodesk.Viewing.ScreenShot.getScreenShotWithBounds(window.viewer, width, height, getPdf, screenshotOptions);
|
|
}
|
|
|
|
onToolbarCreated() {
|
|
this.viewer.removeEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
|
|
this.onToolbarCreatedBinded = null;
|
|
this.createUI();
|
|
}
|
|
}
|
|
|
|
class Legend extends Autodesk.Viewing.Extension {
|
|
load() {
|
|
if (this.viewer.toolbar) {
|
|
this.createUI();
|
|
} else {
|
|
this.onToolbarCreatedBinded = this.onToolbarCreated.bind(this);
|
|
this.viewer.addEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
unload() {
|
|
console.info("Legend extension is now unloaded!");
|
|
return true;
|
|
}
|
|
|
|
createUI() {
|
|
const viewer = this.viewer;
|
|
let panel = this.panel;
|
|
if (!panel) {
|
|
// auto open panel on first load
|
|
panel = new LegendPanel(viewer.container, legendId, legendId);
|
|
viewer.addPanel(panel);
|
|
panel.setVisible(!panel.isVisible());
|
|
}
|
|
let toolbarGroup = viewer.toolbar.getControl(customToolbarId);
|
|
if (!toolbarGroup) {
|
|
toolbarGroup = new Autodesk.Viewing.UI.ControlGroup(customToolbarId);
|
|
viewer.toolbar.addControl(toolbarGroup);
|
|
}
|
|
|
|
this.subToolbar = toolbarGroup;
|
|
const legendBtn = new Autodesk.Viewing.UI.Button(`${customToolbarId}-${legendId}Tool`);
|
|
const legendBtnElem = legendBtn.container;
|
|
const adskButtonIcon = legendBtnElem.children.item(0);
|
|
|
|
renderSvg("/icon/legend.svg").then((icon) => {
|
|
adskButtonIcon.append(icon);
|
|
// because legend panel is open on first load
|
|
legendBtnElem.classList.add("active");
|
|
legendBtnElem.classList.remove("inactive");
|
|
});
|
|
|
|
legendBtn.onClick = () => {
|
|
panel.container.classList.toggle("is-flex");
|
|
panel.container.classList.toggle("is-hidden");
|
|
legendBtnElem.classList.toggle("active");
|
|
legendBtnElem.classList.toggle("inactive");
|
|
};
|
|
legendBtn.setToolTip(legendId);
|
|
this.subToolbar.addControl(legendBtn);
|
|
}
|
|
|
|
onToolbarCreated(e) {
|
|
this.viewer.removeEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
|
|
this.onToolbarCreatedBinded = null;
|
|
if (e.type === "toolbarCreated") {
|
|
this.createUI();
|
|
}
|
|
}
|
|
}
|
|
|
|
class NoteHistory extends Autodesk.Viewing.Extension {
|
|
constructor(viewer, options) {
|
|
super(viewer, options);
|
|
this.panel = null;
|
|
this.button = null;
|
|
}
|
|
|
|
load() {
|
|
if (this.viewer.toolbar) {
|
|
this.createUI();
|
|
} else {
|
|
this.onToolbarCreatedBinded = this.onToolbarCreated.bind(this);
|
|
this.viewer.addEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
unload() {
|
|
if (this.panel) {
|
|
this.panel.uninitialize();
|
|
this.panel = null;
|
|
}
|
|
if (this.button) {
|
|
const toolbarGroup = this.viewer.toolbar.getControl(customToolbarId);
|
|
toolbarGroup.removeControl(this.button);
|
|
this.button = null;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
createUI() {
|
|
const viewer = this.viewer;
|
|
let panel = this.panel;
|
|
if (!panel) {
|
|
panel = new NoteHistoryPanel(viewer.container, noteHistoryId, "Note History");
|
|
viewer.addPanel(panel);
|
|
this.panel = panel;
|
|
}
|
|
|
|
let toolbarGroup = viewer.toolbar.getControl(customToolbarId);
|
|
if (!toolbarGroup) {
|
|
toolbarGroup = new Autodesk.Viewing.UI.ControlGroup(customToolbarId);
|
|
viewer.addControl(toolbarGroup);
|
|
}
|
|
|
|
this.button = new Autodesk.Viewing.UI.Button(`${customToolbarId}-${noteHistoryId}Tool`);
|
|
this.button.setToolTip("Lịch sử ghi chú");
|
|
|
|
const adskButtonIcon = this.button.container.children.item(0);
|
|
if (!selectedProductForNote.value) {
|
|
adskButtonIcon.style.filter = "grayscale(100%)";
|
|
}
|
|
renderSvg("/icon/notes.svg").then((icon) => {
|
|
adskButtonIcon.append(icon);
|
|
});
|
|
|
|
this.button.onClick = () => {
|
|
panel.toggleVisibility();
|
|
panel.container.classList.toggle("is-flex");
|
|
panel.container.classList.toggle("is-hidden");
|
|
this.button.container.classList.toggle("active");
|
|
this.button.container.classList.toggle("inactive");
|
|
};
|
|
|
|
toolbarGroup.addControl(this.button);
|
|
}
|
|
|
|
onToolbarCreated() {
|
|
this.viewer.removeEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
|
|
this.onToolbarCreatedBinded = null;
|
|
this.createUI();
|
|
}
|
|
}
|
|
|
|
return new Promise((resolve, _reject) => {
|
|
Autodesk.Viewing.theExtensionManager.registerExtension(printId, Print);
|
|
Autodesk.Viewing.theExtensionManager.registerExtension(legendId, Legend);
|
|
Autodesk.Viewing.theExtensionManager.registerExtension(noteHistoryId, NoteHistory);
|
|
Autodesk.Viewing.Initializer({ env: "AutodeskProduction", getAccessToken }, () => {
|
|
const config = {
|
|
extensions: [printId, legendId, noteHistoryId, "Autodesk.ModelStructure", "Autodesk.Viewing.SceneBuilder"],
|
|
};
|
|
|
|
const viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
|
|
viewer.start();
|
|
viewer.setTheme("light-theme");
|
|
viewer.setProgressiveRendering(false);
|
|
viewer.setReverseZoomDirection(true);
|
|
viewer.toolController.setMouseWheelInputEnabled(false);
|
|
const zoomOutFactor = 0.3; // larger means zoom out more - https://stackoverflow.com/a/67918257
|
|
viewer.navigation.FIT_TO_VIEW_VERTICAL_MARGIN = zoomOutFactor;
|
|
viewer.navigation.FIT_TO_VIEW_HORIZONTAL_MARGIN = zoomOutFactor;
|
|
|
|
// disable Autodesk's "fit everything in view" mouse & keyboard shortcuts
|
|
viewer.canvas.addEventListener("dblclick", (e) => {
|
|
e.stopPropagation();
|
|
});
|
|
|
|
document.addEventListener(
|
|
"keydown",
|
|
(e) => {
|
|
const canvas = viewerRef.value.querySelector("canvas");
|
|
if (canvas && (e.key === "F" || e.key === "f")) {
|
|
e.stopImmediatePropagation();
|
|
}
|
|
},
|
|
true,
|
|
);
|
|
|
|
viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, function onceLoaded(e) {
|
|
const viewer = e.target;
|
|
viewer.removeEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, onceLoaded);
|
|
applyLayerSetting(layersetting, $store);
|
|
addMeshes(viewer);
|
|
});
|
|
|
|
function resetCameraIfZoomError() {
|
|
const { position, target } = viewer.navigation.getCamera();
|
|
const threshold = 500;
|
|
const pxErr = position.x > threshold || position.x < 0;
|
|
const pyErr = position.y > threshold || position.y < 0;
|
|
const pzErr = position.z > threshold;
|
|
const txErr = target.x > threshold || target.x < 0;
|
|
const tyErr = target.y > threshold || target.y < 0;
|
|
const tzErr = target.z > threshold;
|
|
const zoomError = pxErr || pyErr || pzErr || txErr || tyErr || tzErr;
|
|
|
|
if (zoomError) {
|
|
viewer.navigation.setRequestHomeView(true);
|
|
/*
|
|
// Debug log
|
|
console.log('Reset because zoom error:');
|
|
if (pxErr) {
|
|
console.log('px', position.x)
|
|
} else if (pyErr) {
|
|
console.log('py', position.y)
|
|
} else if (pzErr) {
|
|
console.log('pz', position.z)
|
|
} else if (txErr) {
|
|
console.log('tx', target.z)
|
|
} else if (tyErr) {
|
|
console.log('ty', target.z)
|
|
} else if (tzErr) {
|
|
console.log('tz', target.z)
|
|
} */
|
|
}
|
|
}
|
|
|
|
viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, throttle(resetCameraIfZoomError, 300));
|
|
addTemSelectionListener(viewer, products.value, openProductViewModal, openNoPermissionModal);
|
|
unloadUnusedExtensions(viewer);
|
|
resolve(viewer);
|
|
});
|
|
});
|
|
}
|
|
|
|
const componentActivated = ref(true);
|
|
const isViewerLoading = ref(false);
|
|
|
|
async function loadViewer() {
|
|
if (!viewerRef.value) return;
|
|
|
|
isViewerLoading.value = true;
|
|
const scale = 0.25;
|
|
|
|
try {
|
|
const viewer = await initViewer(viewerRef.value);
|
|
window.viewer = viewer;
|
|
const transformMatrix = new THREE.Matrix4().scale(new THREE.Vector3(scale, scale, scale));
|
|
const mainModel = await loadModel(viewer, utopiaUrn);
|
|
const blankModel = await loadModel(viewer, blankUrn, transformMatrix);
|
|
isViewerLoading.value = false;
|
|
|
|
window.mainModel = mainModel;
|
|
window.blankModel = blankModel;
|
|
|
|
blankModel.getObjectTree((instanceTree) => {
|
|
const dbIdMap = instanceTree.nodeAccess.dbIdToIndex;
|
|
const allDbIds = Object.keys(dbIdMap).map((id) => parseInt(id));
|
|
allDbIds.forEach((dbId) => {
|
|
viewer.setThemingColor(dbId, new THREE.Vector4(1, 1, 1, 1), window.blankModel); // turns white to simulate invisibility
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.log("Error in loadViewer", error);
|
|
}
|
|
}
|
|
|
|
function mountLayerSetting() {
|
|
const observer = new MutationObserver((mutations, observer) => {
|
|
mutations.forEach((mutation) => {
|
|
if (mutation.addedNodes.length > 0) {
|
|
const addedNode = mutation.addedNodes.item(0);
|
|
if (addedNode.nodeType === 1 && addedNode.nodeName === "DIV" && addedNode.id === "ViewerLayersPanel") {
|
|
const layerSettingSFCContainer = html("div", {
|
|
id: "layerSettingSFCContainer",
|
|
});
|
|
addedNode.append(layerSettingSFCContainer);
|
|
|
|
const layerSettingSFC = createApp({
|
|
name: "LayerSetting",
|
|
render() {
|
|
return h(LayerSetting);
|
|
},
|
|
});
|
|
|
|
layerSettingSFC.mount(layerSettingSFCContainer);
|
|
observer.disconnect();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
observer.observe(viewerRef.value, {
|
|
subtree: true,
|
|
childList: true,
|
|
});
|
|
}
|
|
|
|
async function loadData(first = true) {
|
|
const [productsData, productStatusesData, saleStatusesData, cartsData, dealersData] = await Promise.all([
|
|
$getdata("product", {
|
|
cart__dealer__code: dealer?.code,
|
|
}),
|
|
$getdata("productstatus", undefined, { sort: "index" }),
|
|
$getdata("salestatus", undefined, { sort: "name" }),
|
|
$getdata("cart", undefined, {
|
|
filter: { dealer: dealer?.id },
|
|
values: "id,code,name,dealer,index",
|
|
distinct_values: {
|
|
product_count: { type: "Count", field: "prdcart" },
|
|
},
|
|
summary: "annotate",
|
|
sort: "index",
|
|
}),
|
|
$getdata("dealer"),
|
|
]);
|
|
|
|
products.value = productsData;
|
|
productStatuses.value = productStatusesData;
|
|
saleStatuses.value = saleStatusesData;
|
|
carts.value = cartsData;
|
|
dealers.value = dealersData;
|
|
|
|
if (first) {
|
|
filters.value = {
|
|
statusIds: defaultStatusIds.value,
|
|
cartIds: defaultCartIds.value,
|
|
dealerIds: defaultDealerIds.value,
|
|
};
|
|
}
|
|
}
|
|
|
|
watch([products, filters], debouncedApplyColors, { deep: true });
|
|
|
|
watch(
|
|
statuses,
|
|
(val, oldVal) => {
|
|
if (oldVal.length !== val.length) {
|
|
// only reset status filter if statuses changes because of statusMode,
|
|
// not when new data is fetched
|
|
filters.value.statusIds = val.map((s) => s.id);
|
|
}
|
|
},
|
|
{ deep: true },
|
|
);
|
|
|
|
// Watch for product selection to enable/disable the note history button
|
|
watch(selectedProductForNote, (newProduct) => {
|
|
const noteHistoryBtn = document.getElementById("customToolbar-noteHistoryTool");
|
|
if (noteHistoryBtn) {
|
|
const icon = noteHistoryBtn.querySelector(".adsk-button-icon");
|
|
if (newProduct) {
|
|
if (icon) icon.style.filter = "none"; // Enabled look
|
|
} else {
|
|
if (icon) icon.style.filter = "grayscale(100%)"; // Disabled look
|
|
const ext = window.viewer?.getExtension?.("noteHistory");
|
|
if (ext?.panel) {
|
|
ext.panel.setVisible(false);
|
|
}
|
|
noteHistoryBtn.classList.remove("active");
|
|
noteHistoryBtn.classList.add("inactive");
|
|
}
|
|
}
|
|
});
|
|
|
|
function handleNavigateToProduct(event) {
|
|
const { productId } = event.detail;
|
|
if (!window.viewer || !window.meshes || !productId) return;
|
|
|
|
const mesh = window.meshes.find((m) => m.dbId === productId);
|
|
if (mesh) {
|
|
const boundingBox = new THREE.Box3().setFromObject(mesh);
|
|
window.viewer.navigation.fitBounds(false, boundingBox);
|
|
} else {
|
|
console.warn(`Product mesh with ID ${productId} not found.`);
|
|
}
|
|
}
|
|
|
|
async function handleWsMessage({ detail: response }) {
|
|
if (response.type === "realtime_update") {
|
|
const { name, change_type, record } = response.payload;
|
|
if (name.toLowerCase() !== "product") return;
|
|
|
|
console.log(`[Viewer] Received WS update for Product: ${change_type}`, record);
|
|
if (dealer) {
|
|
if (record.cart__dealer__code === dealer.code) {
|
|
console.log(dealer.code);
|
|
$snackbar("Căn " + record.trade_code + " đã có cập nhật mới", "Thành công", "Success");
|
|
}
|
|
} else {
|
|
$snackbar("Căn " + record.trade_code + " đã có cập nhật mới", "Thành công", "Success");
|
|
}
|
|
|
|
await nextTick();
|
|
await refreshFromWebSocket();
|
|
}
|
|
}
|
|
|
|
const viewerSDKSharedPath = "https://developer.api.autodesk.com/modelderivative/v2/viewers/7.115.0";
|
|
const viewerSDKPattern = `script[src^="${viewerSDKSharedPath}"],link[href^="${viewerSDKSharedPath}"]`;
|
|
|
|
async function initialize() {
|
|
await loadData();
|
|
mountLayerSetting();
|
|
|
|
// Connect to WebSocket
|
|
$connectWebSocket();
|
|
|
|
if (dealer?.id) {
|
|
$subscribe("product", { cart__dealer__code: dealer?.code }, (initialData) => {
|
|
if (initialData && initialData.rows) {
|
|
products.value = initialData.rows;
|
|
}
|
|
});
|
|
} else {
|
|
$subscribe("product", undefined, (initialData) => {
|
|
if (initialData && initialData.rows) {
|
|
products.value = initialData.rows;
|
|
}
|
|
});
|
|
}
|
|
|
|
window.addEventListener("navigateToProduct", handleNavigateToProduct);
|
|
window.addEventListener("ws_message", handleWsMessage);
|
|
|
|
if (window.Autodesk) {
|
|
loadViewer();
|
|
} else {
|
|
const viewerScript = html("script", {
|
|
src: `${viewerSDKSharedPath}/viewer3D.js`,
|
|
onload: loadViewer,
|
|
});
|
|
const viewerStyle = html("link", {
|
|
href: `${viewerSDKSharedPath}/style.css`,
|
|
rel: "stylesheet",
|
|
});
|
|
document.head.append(viewerScript, viewerStyle);
|
|
}
|
|
}
|
|
|
|
function cleanup() {
|
|
const viewerSDKs = document.querySelectorAll(viewerSDKPattern);
|
|
|
|
if (viewerSDKs.length > 0) {
|
|
viewer.finish();
|
|
viewer = null;
|
|
Autodesk.Viewing.shutdown();
|
|
Autodesk = undefined;
|
|
|
|
viewerSDKs.forEach((script) => script.remove());
|
|
isViewerLoading.value = false;
|
|
componentActivated.value = false;
|
|
|
|
window.removeEventListener("navigateToProduct", handleNavigateToProduct);
|
|
window.removeEventListener("ws_message", handleWsMessage);
|
|
}
|
|
}
|
|
|
|
onMounted(cleanup);
|
|
onUnmounted(cleanup);
|
|
|
|
onActivated(() => {
|
|
const viewerSDKs = document.querySelectorAll(viewerSDKPattern);
|
|
|
|
if (viewerSDKs.length === 0) {
|
|
componentActivated.value = true;
|
|
initialize();
|
|
}
|
|
});
|
|
|
|
onDeactivated(() => {
|
|
if (isViewerLoading.value) cleanup();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-if="componentActivated"
|
|
id="ViewerMaster"
|
|
>
|
|
<div
|
|
:ref="viewerId"
|
|
:id="viewerId"
|
|
></div>
|
|
<div id="overlay"></div>
|
|
<Modal
|
|
@close="closeProductViewModal"
|
|
v-if="showProductViewModal"
|
|
v-bind="showProductViewModal"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
#ViewerMaster {
|
|
--top: 94px;
|
|
|
|
position: absolute;
|
|
width: 100%;
|
|
top: var(--top);
|
|
left: 0;
|
|
}
|
|
|
|
.adsk-viewing-viewer {
|
|
font-family: var(--bulma-body-family) !important;
|
|
|
|
.docking-panel-footer-resizer {
|
|
cursor: nwse-resize !important;
|
|
}
|
|
}
|
|
|
|
#viewer,
|
|
#overlay {
|
|
position: absolute;
|
|
width: 100%;
|
|
left: 0;
|
|
height: calc(100vh - var(--top));
|
|
}
|
|
|
|
#modelTools,
|
|
#toolbar-fullscreenTool {
|
|
display: none;
|
|
}
|
|
|
|
.viewcube {
|
|
display: none !important;
|
|
}
|
|
|
|
.homeViewWrapper {
|
|
position: absolute;
|
|
right: 12px;
|
|
top: 10px;
|
|
margin: 0;
|
|
}
|
|
|
|
#customToolbar {
|
|
.adsk-button-icon {
|
|
padding-top: 0;
|
|
|
|
svg {
|
|
width: 28px;
|
|
height: 28px;
|
|
}
|
|
}
|
|
|
|
.adsk-button.active svg {
|
|
color: #00bfff; /* from Autodesk styles */
|
|
}
|
|
}
|
|
|
|
#overlay {
|
|
z-index: 1;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
padding: 1em;
|
|
display: none;
|
|
|
|
> .notification {
|
|
margin: auto;
|
|
padding: 1em;
|
|
max-width: 50%;
|
|
background: white;
|
|
}
|
|
}
|
|
</style>
|