270 lines
7.7 KiB
TypeScript
Executable File
270 lines
7.7 KiB
TypeScript
Executable File
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;
|