Files
molecular_frontend/src/Components/MoleculeViewer/MoleculeViewer.tsx
2026-03-16 22:05:19 +03:00

270 lines
7.7 KiB
TypeScript
Executable File
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 Viewer from "miew-react";
import Miew from "miew";
import {
Paper,
UnstyledButton,
useMantineColorScheme,
useMantineTheme,
} from "@mantine/core";
import { useEffect, useMemo, useRef, useState } from "react";
import "./MoleculeViewer.css";
import { notifications } from "@mantine/notifications";
import {
IconAlertHexagon,
IconArrowBarBoth,
IconRefresh,
} from "@tabler/icons-react";
import MoleculeViewerMenu from "./MoleculeViewerMenu";
import type { SelectedAtom } from "Types/Experiment/MoleculeEdit/MoleculeEdit";
const callbackOnResizeFinish = (
dom_elem: HTMLElement,
beginning_callback: () => void,
end_callback: () => void,
delay = 100, // ms after resize "finishes"
) => {
let timeoutId: number | undefined;
const resizeObserver = new ResizeObserver(() => {
beginning_callback();
if (timeoutId) {
window.clearTimeout(timeoutId);
}
timeoutId = window.setTimeout(() => {
end_callback();
}, delay);
});
resizeObserver.observe(dom_elem);
return () => resizeObserver.disconnect(); // cleanup helper
};
interface MoleculeViewerProps {
moleculeData: string;
selectedAtom: SelectedAtom | null;
setSelectedAtom: (selectedAtom: SelectedAtom | null) => void;
}
function MoleculeViewer(props: MoleculeViewerProps) {
const theme = useMantineTheme();
const { colorScheme } = useMantineColorScheme();
// объект загруженного редактора молекул для редактирования
const [miew, setMiew] = useState<Miew | null>(null);
const molViewerRef = useRef<HTMLDivElement>(null);
const [isResizing, setIsResizing] = useState<boolean>(false);
const [viewingData, setViewingData] = useState<string>(props.moleculeData);
//при загрузке сохраняем объект miew
const onInitMiew = (miew: Miew) => {
setMiew(miew);
if (miew && viewingData) {
//прогружаем молекулу
miew
.load(viewingData, {
sourceType: "immediate",
fileType: "xyz",
})
.then(() => {
if (props.selectedAtom) {
props.setSelectedAtom(null);
}
})
.catch((error) => {
if (error.message == "Operation cancelled") {
return;
}
notifications.show({
color: "orange",
radius: "md",
title: "Ошибка при отображении молекулы",
message:
"Проверте правильность написания кода молекулы, в нем содержатся ошибки",
icon: <IconAlertHexagon />,
style: { paddingLeft: "5px" },
});
});
}
miew.setOptions({
settings: {
bg: {
color: theme.colors.secondaryDark[7],
},
fogAlpha: 0.7,
axes: true,
},
});
};
//мемоизируем объект чтобы он не перегружаля при изменении [miew] состояния
const MemoViewer = useMemo(
() => <Viewer onInit={onInitMiew} />,
[viewingData],
);
//Замена станартного обработчика нажатия на молекулы
useEffect(() => {
if (molViewerRef.current && miew) {
//INFO: при изменении размера окна обносить webgl
const cleanup = callbackOnResizeFinish(
molViewerRef.current,
handleResizeStart,
handleResizeEnd,
);
//@ts-expect-error Miew class not implementing typescript correctly
miew.removeEventListener("newpick");
//@ts-expect-error Miew class not implementing typescript correctly
miew.addEventListener("newpick", handleClick);
return () => {
cleanup();
//@ts-expect-error Miew class not implementing typescript correctly
miew.removeEventListener("newpick");
};
}
}, [molViewerRef, miew]);
//On selected Atom change, highlight it
useEffect(() => {
if (miew) {
if (!props.selectedAtom) {
//@ts-expect-error Miew class not implementing typescript correctly
miew.select("");
return;
}
//@ts-expect-error Miew class not implementing typescript correctly
miew.select("serial " + props.selectedAtom.serial, false);
//спрятать информацию встроенную
//@ts-expect-error Miew class not implementing typescript correctly
miew._msgAtomInfo.style.opacity = 0.0;
//@ts-expect-error Miew class not implementing typescript correctly
miew._msgAtomInfo.style.height = "0px";
//@ts-expect-error Miew class not implementing typescript correctly
miew._msgAtomInfo.style.overflow = "hidden";
}
}, [props.selectedAtom, miew]);
//при смене темы обновляем фон MoleculeViewer
useEffect(() => {
if (miew) {
miew.setOptions({
settings: {
bg: {
color: theme.colors.secondaryDark[7],
},
},
});
}
}, [colorScheme]);
//при нажятии на атом выбираем его
const handleClick = (pick: { type: string; obj: { atom: SelectedAtom } }) => {
if (miew) {
if (pick.obj.atom) {
props.setSelectedAtom(pick.obj.atom);
} else {
props.setSelectedAtom(null);
}
}
};
const handleResizeStart = () => {
setIsResizing(true);
};
const handleResizeEnd = () => {
//@ts-expect-error Miew class not implementing typescript correctly
miew._onResize();
//@ts-expect-error Miew class not implementing typescript correctly
miew._picker.handleResize();
setTimeout(() => {
setIsResizing(false);
}, 50);
};
const handleMouseEnter = () => {
document.body.style.overflow = "hidden"; // disable page scroll
};
const handleMouseLeave = () => {
document.body.style.overflow = "auto"; // re-enable scroll
};
const refreshDisplay = () => {
if (props.moleculeData != viewingData) {
setViewingData(props.moleculeData);
} else {
if (miew) {
//@ts-expect-error Miew class not implementing typescript correctly
miew.resetView();
}
}
};
return (
<div ref={molViewerRef} className="moleculeViewer">
<div
style={{
width: "100%",
height: "100%",
backgroundColor: theme.colors.secondaryDark[7],
borderRadius: "5px",
overflow: "hidden",
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{MemoViewer}
{isResizing && viewingData && (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: theme.colors.secondaryDark[7], // optional dim
opacity: 0.5,
zIndex: 10,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<IconArrowBarBoth size={80} />
</div>
</div>
)}
</div>
{props.selectedAtom && (
<Paper className="atomEditor" radius="md" p="md">
<MoleculeViewerMenu selectedAtom={props.selectedAtom} miew={miew} />
</Paper>
)}
<div
style={{
position: "absolute",
left: "10px",
top: "10px",
display: "flex",
zIndex: 5,
width: "auto",
}}
>
<UnstyledButton onClick={refreshDisplay}>
<IconRefresh size={25} />
</UnstyledButton>
</div>
</div>
);
}
export default MoleculeViewer;