openchemlib
Version:
Manipulate molecules
1,341 lines (1,325 loc) • 3.58 MB
JavaScript
// lib/canvas_editor/clipboard_handler.js
var ClipboardHandler = class {
copyMolecule(molecule) {
const data = molecule.getIDCodeAndCoordinates();
navigator.clipboard.writeText(`${data.idCode} ${data.coordinates}`);
}
pasteMolecule() {
return null;
}
};
// lib/canvas_editor/utils.js
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var lookup = new Uint8Array(256);
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i;
}
function decodeBase64(base64) {
let bufferLength = base64.length * 0.75;
let len = base64.length;
let i;
let p = 0;
let encoded1;
let encoded2;
let encoded3;
let encoded4;
if (base64.at(-1) === "=") {
bufferLength--;
if (base64.at(-2) === "=") {
bufferLength--;
}
}
const arraybuffer = new ArrayBuffer(bufferLength);
const bytes = new Uint8Array(arraybuffer);
for (i = 0; i < len; i += 4) {
encoded1 = lookup[base64.charCodeAt(i)];
encoded2 = lookup[base64.charCodeAt(i + 1)];
encoded3 = lookup[base64.charCodeAt(i + 2)];
encoded4 = lookup[base64.charCodeAt(i + 3)];
bytes[p++] = encoded1 << 2 | encoded2 >> 4;
bytes[p++] = (encoded2 & 15) << 4 | encoded3 >> 2;
bytes[p++] = (encoded3 & 3) << 6 | encoded4 & 63;
}
return arraybuffer;
}
function toHex(v) {
return v.toString(16).padStart(2, "0");
}
var hidpiScaleFactor = globalThis.devicePixelRatio || 1;
// lib/canvas_editor/draw_context.js
var DrawContext = class {
/**
*
* @param {CanvasRenderingContext2D} ctx
*/
constructor(ctx2) {
this.ctx = ctx2;
this.ctx.textAlign = "left";
this.ctx.textBaseline = "top";
this.currentFontSize = 12;
this.currentFont = "12px sans-serif";
this.ctx.font = this.currentFont;
this.currentColor = "#000000";
this.currentLineWidth = 1;
this.canvasCache = /* @__PURE__ */ new Map();
}
clearRect(x, y, w, h) {
this.ctx.clearRect(x, y, w, h);
}
getBackgroundRGB() {
return 16777215;
}
getForegroundRGB() {
return 0;
}
getSelectionBackgroundRGB() {
return 12310268;
}
getLineWidth() {
return this.currentLineWidth;
}
setRGB(rgb) {
const r = rgb >>> 16 & 255;
const g = rgb >>> 8 & 255;
const b = rgb >>> 0 & 255;
this.currentColor = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
this.ctx.fillStyle = this.currentColor;
this.ctx.strokeStyle = this.currentColor;
}
setFont(size, isBold, isItalic) {
this.currentFontSize = size;
this.currentFont = `${isBold ? "bold" : ""} ${isItalic ? "italic" : ""} ${size}px sans-serif`;
this.ctx.font = this.currentFont;
}
getFontSize() {
return this.currentFontSize;
}
getBounds(s) {
const metrics = this.ctx.measureText(s);
return {
x: metrics.actualBoundingBoxLeft,
y: metrics.actualBoundingBoxAscent,
width: metrics.actualBoundingBoxRight,
height: metrics.actualBoundingBoxAscent
};
}
drawString(x, y, s) {
this.ctx.fillText(s, x, y);
}
drawCenteredString(x, y, s) {
this.ctx.textAlign = "center";
this.ctx.textBaseline = "middle";
this.ctx.fillText(s, x, y);
this.ctx.textAlign = "left";
this.ctx.textBaseline = "top";
}
setLineWidth(lineWidth) {
this.currentLineWidth = lineWidth;
this.ctx.lineWidth = lineWidth;
}
fillRectangle(x, y, w, h) {
this.ctx.fillRect(x, y, w, h);
}
fillCircle(x, y, d) {
const r = d / 2;
this.ctx.beginPath();
this.ctx.arc(x + r, y + r, r, 0, 2 * Math.PI);
this.ctx.fill();
}
drawLine(x1, y1, x2, y2) {
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.stroke();
}
drawPolygon(p) {
this.ctx.beginPath();
this.ctx.moveTo(p.getX(0), p.getY(0));
for (let i = 1; i < p.getSize(); i++) {
this.ctx.lineTo(p.getX(i), p.getY(i));
}
this.ctx.stroke();
}
drawRectangle(x, y, w, h) {
this.ctx.strokeRect(x, y, w, h);
}
fillPolygon(p) {
this.ctx.beginPath();
this.ctx.moveTo(p.getX(0), p.getY(0));
for (let i = 1; i < p.getSize(); i++) {
this.ctx.lineTo(p.getX(i), p.getY(i));
}
this.ctx.fill();
}
drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) {
if (arguments.length !== 9) {
throw new Error(
`drawImage call with ${arguments.length} arguments unimplemented`
);
}
let fullScaleCanvas = this.canvasCache.get(image);
if (!fullScaleCanvas) {
fullScaleCanvas = document.createElement("canvas");
const imageData = image.imageData;
fullScaleCanvas.width = imageData.width;
fullScaleCanvas.height = imageData.height;
const fullScaleContext = fullScaleCanvas.getContext("2d");
fullScaleContext.globalAlpha = 0;
fullScaleContext.putImageData(imageData, 0, 0);
this.canvasCache.set(image, fullScaleCanvas);
}
this.ctx.drawImage(fullScaleCanvas, sx, sy, sw, sh, dx, dy, dw, dh);
}
isDarkBackground() {
return false;
}
};
// lib/canvas_editor/editor_area.js
var EditorArea = class {
constructor(canvasElement, onChange) {
this.canvasElement = canvasElement;
this.changeListener = onChange;
this.drawContext = new DrawContext(this.canvasElement.getContext("2d"));
}
getBackgroundRGB() {
return 16777215;
}
getCanvasWidth() {
return this.canvasElement.width;
}
getCanvasHeight() {
return this.canvasElement.height;
}
getDrawContext() {
return this.drawContext;
}
onChange(what, isUserEvent) {
this.changeListener?.({ what, isUserEvent });
}
getClipboardHandler() {
return new ClipboardHandler();
}
};
// lib/canvas_editor/editor_stylesheet.js
var styles = `
/* We can customize editor styles here. */
`;
var stylesheet;
function getEditorStylesheet() {
if (stylesheet) {
return stylesheet;
}
const sheet = new CSSStyleSheet();
sheet.replaceSync(styles);
stylesheet = sheet;
return sheet;
}
// lib/canvas_editor/events.js
function addPointerListeners(canvasElement, drawArea, JavaEditorArea) {
let pointerDownId = -1;
function fireMouseEvent(what, ev, clickCount = 0) {
if (ev.button > 0) {
return;
}
drawArea.fireMouseEvent(
what,
ev.button + 1,
clickCount,
Math.round(ev.offsetX * hidpiScaleFactor),
Math.round(ev.offsetY * hidpiScaleFactor),
ev.shiftKey,
ev.ctrlKey,
ev.altKey,
ev.button === 2
);
}
canvasElement.addEventListener("pointerdown", (ev) => {
if (pointerDownId === -1) {
pointerDownId = ev.pointerId;
fireMouseEvent(JavaEditorArea.MOUSE_EVENT_PRESSED, ev);
}
});
function handlePointerUp(ev) {
if (pointerDownId === ev.pointerId) {
pointerDownId = -1;
fireMouseEvent(JavaEditorArea.MOUSE_EVENT_RELEASED, ev);
}
}
document.addEventListener("pointerup", handlePointerUp);
canvasElement.addEventListener("click", (ev) => {
fireMouseEvent(JavaEditorArea.MOUSE_EVENT_CLICKED, ev, ev.detail);
});
canvasElement.addEventListener("pointerenter", (ev) => {
fireMouseEvent(JavaEditorArea.MOUSE_EVENT_ENTERED, ev);
});
canvasElement.addEventListener("pointerleave", (ev) => {
fireMouseEvent(JavaEditorArea.MOUSE_EVENT_EXITED, ev);
});
canvasElement.addEventListener("pointermove", (ev) => {
if (pointerDownId !== -1) {
if (pointerDownId === ev.pointerId) {
fireMouseEvent(JavaEditorArea.MOUSE_EVENT_DRAGGED, ev);
}
} else {
fireMouseEvent(JavaEditorArea.MOUSE_EVENT_MOVED, ev);
}
});
return () => {
document.removeEventListener("pointerup", handlePointerUp);
};
}
function addKeyboardListeners(parentElement, canvasElement, editorArea, JavaEditorArea, Molecule2) {
const isMac = typeof navigator !== "undefined" && navigator.platform === "MacIntel";
const isMenuKey = (ev) => isMac && ev.metaKey || !isMac && ev.ctrlKey;
function fireKeyEvent(what, ev) {
const key = getKeyFromEvent(ev, JavaEditorArea);
if (key === null) return;
editorArea.fireKeyEvent(
what,
key,
ev.altKey,
ev.ctrlKey,
ev.shiftKey,
isMenuKey(ev)
);
}
canvasElement.addEventListener("keydown", (ev) => {
if (isMenuKey(ev) && ev.key === "c") return;
if (isMenuKey(ev) && ev.key === "v") return;
fireKeyEvent(JavaEditorArea.KEY_EVENT_PRESSED, ev);
});
canvasElement.addEventListener("keyup", (ev) => {
fireKeyEvent(JavaEditorArea.KEY_EVENT_RELEASED, ev);
});
parentElement.addEventListener("paste", (ev) => {
const textData = ev.clipboardData.getData("text");
const molecule = Molecule2.fromText(textData);
if (molecule && molecule.getAllAtoms() > 0) {
editorArea.addPastedOrDropped(molecule);
}
});
return () => {
};
}
function getKeyFromEvent(ev, JavaEditorArea) {
switch (ev.key) {
case "Control":
return JavaEditorArea.KEY_CTRL;
case "Alt":
return JavaEditorArea.KEY_ALT;
case "Shift":
return JavaEditorArea.KEY_SHIFT;
case "Delete":
case "Backspace":
return JavaEditorArea.KEY_DELETE;
// Backspace is currently unused by the Java code, so we remap it.
// return JavaEditorArea.KEY_BACKSPACE;
case "F1":
return JavaEditorArea.KEY_HELP;
case "Escape":
return JavaEditorArea.KEY_ESCAPE;
case "Enter":
return JavaEditorArea.KEY_ENTER;
default:
if (ev.key.length === 1) {
return ev.key.codePointAt(0);
} else {
return null;
}
}
}
// lib/canvas_editor/toolbar.js
var Toolbar = class {
constructor(canvasElement) {
this.canvasElement = canvasElement;
this.drawContext = new DrawContext(this.canvasElement.getContext("2d"));
}
setDimensions(width, height) {
this.canvasElement.width = width;
this.canvasElement.style.width = `${width / hidpiScaleFactor}px`;
this.canvasElement.height = height;
this.canvasElement.style.height = `${height / hidpiScaleFactor}px`;
}
getDrawContext() {
return this.drawContext;
}
getBackgroundRGB() {
return 16777215;
}
getForegroundRGB() {
return 0;
}
};
// lib/canvas_editor/cursors_24px.js
var cursors = {
"chain.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAB2UlEQVR4XmNgGJmAkYGJgQULZGXAhCygIGICQg4GdjTIARRDh5xAEQwNKYzXeK4xY1Oez3SNfz8TmoYUxq+6m9bfqLjDI4tmfhzLff9VR17FoWhwZXpkOm2v1C+79+cbD4gga8lneuhrfdv6/+ItSBpSGB+bTDos9IfzP9N/k3cX6+7wwByWz/TWR/E1w/+kO+8j4BpSGL9rz9wh+Efs14TF3heY/tu/vlF2hxtkSxzLY2/FVyz/om69jT0G8kM6QyNDM9MT41nbZX9y/J/ct1/4iU7aIYb/9q8uVx0UbmF64mV7i/1/0s0HSVM5dRgYrjB84/8U+bh80iH+34z/17Xc51ZhUGX8qpkK1GL6+krltyClVyz/c+69C57CwQsK1v+Sl9rdvgr8Z/7P9WvKxNs8fsD44ABqeWLkdYHhv9sL8e+s/xJuv4k4yajEAI6H/+6llxn+g6DUh75gSXhgugEdmXIUJB5150HSFA59oHKIBteGSyBhpv+q9wsVQaZDICdDKuNnneRjCY9exU3l5AUrh2gQelAQ/8Tgve2Tb25NjIgYBkmnMl5Sumi+hQmiGKqhgaGB57Pts4Bfek0sjkjmIxQhs8BpiZFBh0EPCLGlTnQxcGod8gAAp9es6fCW8G8AAAAASUVORK5CYII=)",
"eraser.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAC1UlEQVR4XnWU309SYRjHn8OYTTM3zTmjC8NFFBR10dJu27wx11VX/QNdeGebTsvua93mRbUmLhURzYgKRZRJUJLibLPpRDCZC3/b1AMc4Tw97+EAR2e8FzD4fr7v53nPOQDCDkT/s2KAKiwROOXvFP8JLnDDGHhgArzgg28wCQGYgiDMw6E+1pY2rsECLEIIIrAC1O2CV/AWzPAOLGCFQbDDJ/hC+CQktU5bXWJoMGlaVv0iPAy/GeCmeBf0QB/YYECKO6kkzAl6Z/+t+BmsSJotgmmN4lFYZcA4tfdQez+1D8Fnio+S1oHe5TAcFuJHVBNit4nGLYrHGOCBbin+ntodMEzxKMkMWy8LpfgSEUN4niC7TTBtqzYYMCG3Z9xHadgD/bC1hi+U4uw1TkhFsqs3adpngJdGHSAZB8VHYAZ4vdt+RSjED3KcvYWYWMJuFY0E+MjdQe5MZgYE7YhFL8soCJzDU1iKwdcE+HOjBoG/NNJbq5DJI40EPNiIPCLgO/Wzk4lS3D2kPyaTQe5hETZsLDfxRRQLkLuLDjJd7u8wJs/mRs22J7CR4re3Qq3xIpENPUUHG+biuqX2UVsVX41epTp9ZjINm0ttqTIEZECQboK0ztd/Ya951t+nRi2GFYgksxluSlO7BMTYLVblMVcmOOTEZ9N+i4YvkREmAySz2JaNE4CqtM5jvrZHcbbE5lmLU8NrJTEmc3drsT1VlmmXdsCbgTdSu7wKUi2zPqsaq7GWZCjeJBZnwxlAN9dRyecBtsvzH/4+DV+MNdsLT4/GJSU0jHde3T2KtMxYnA8X5tvT5cp2eehDLnVxrPPoLgWpx4Hd+8Lp/Zx7boYoe/g40eDtViIF6Se+xPX8qPl96DqECImrRMOY2fg3e1atwd16UX1cR75wEXr4VmGdxL52atbpX2HnxfTenfiJcelKr9CjvQp/IMWJ57AO62ndOKk7890/iSWBU4XaZc0AAAAASUVORK5CYII=)",
"fist.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAABj0lEQVR4Xu2S3yuDYRTHz7IZsfdCUrYLG4VCpJV6d0mS/A1zg79ArRArP4q5s4ulVousFCn/gX/g5WK7wkZMzIXMxjZ793XeMdn7vpM/wDn1PD1P53PO95znIfq3rwmYyU5tZPr7PJYoTjGaZ8xB1t+xZQqyP60iAEeWsThFGKpqbiE9CRECrnCHLnTAj17QbLV4E3mFXALbcOAGSXRiClnMgZLkZR/QYg0UEpDBDuxI4IHzT6OADdRiEK2gE12gBWmWE8Yj7weQIGOT650zaoiqAVtjDx2KeEWR8xZ4lUt+jEWkWJxJBYgkWa7NLy5WXWl5FilzhfoKwEAzzkwYI3DhTQUoVYpYR10FYCTPaDbJ2heQUwGfR0kFELmt6Utu9Jmz6dmFpmmR7v141w1XBiCBNFNacyKmCxT59kgHGBIQ4g7UkpRzESuwaCoQ+YZxqwGUGaUwrvvStqazLeRLj/bTZUTQB5rQfg1jjac9e8qAzFgZKbDMALpj1Kz3Z0WKjiGIfex++x6H98PgK4d/APiDpdwStyBoAAAAAElFTkSuQmCC)",
"hand.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAB50lEQVR4Xq2TwUsUcRzF3yy52TboHiKVLBCElb1479g5kgKPCkKs4ME6eAiiP0GELileStew6NLJq5Sihw6BG2VlECymB2PVXd10Zl7vN+OuOjNLl35fZhiY+fzee9/vb4D/tAow9fafu90BsqrvnXzNBwSTTBAZ2I3BkSbeYBMzLHNRn9/nEFHCvYZAutDHDfb5wDvanOVnoozBhkBzIUeHOQEHfO8DW0QV0xjC3VjoUh2onAC/ZPAme4giemMQo+BKoaeusCmdSa41MmYL8HxLgUKe27q/UKLzSSxcwUOMwk7VgTKX6sBz7oeA61hJHSUP8ST1raZQA7Z8hb0QkO1WE+d5lZdlx+UbTsjSR95i0bc0E7GUzfIHdziouRqFPzwU5ii4IzOP+SFiqe3i8jyP1A1bgKPyVGa5/M2S7iaDdWZ8KUwNe1Uus923FHx8ujwZPA9YGO48+CK/Ywp4un8N8SIZgG78fCTvFV1n9w+ePe6GFMzMJ7u4zuOInQDYYaKcDB3Ba63FGQWPLk8mt2MAWKv96kk0sKM2TBFfkQ4fvkx6fyHSH9NalwO0PkXPaktirt8rhRpqelblbV6IASzkmivP9NrEdE9AM/FXbCPG4v+6p+3VcZ2ql5zTZSqv+XcQ6+gKgL8a/RiIJPdySgAAAABJRU5ErkJggg==)",
"handPlus.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAACAUlEQVR4Xq2Tz0sUcRjGn1lytW2wPUQamSAIK3vp7kk6i1LgMUGIFTqUBw9BiPsPhNAlpUtuKhpdOnVUKqlDh6CNsh+CsJQdjC13ddOZeXq+M7uTzu7SpXmZLwvzfr7P8/5Y4D89eZh48s/bhoC04nMXH/EmwThjRAp2c/B6C7vZwhRLXFP6NY4RRVxpCiTzw/zCYR94RpsP+Z4oYbQp0JbP0GFGwB6f+8A2UcF9jOFyQ+hkCJSrwDcZ7GcfUcDFBohRcKXQFyp8lc4s3zYzZgvwfEuBwgK/65xXRccrsXAGE7gBOxECJb4IgQfcjQAX8DJxEN/HVOJTTaEGbPsKvyJAuldNXOZZnpIdl485I0tveIkF31KuzlI6zU3ucFRzNQq/uS/MUeGOzNzm6zpLHa3ryzxQN2wBjsIT4JE6f7Co09RgHRlfAnPjXoXr7PQtmdSsNqn2eDJ4HLAw3rX3QX4nVaC534sA0bYCvdi6Je9lveb+mkLw2+PPiIKZ+WwPN3ioj9MyczQMsMNYKR5ZwfOnCzkVTq7KTpYDgrKCp5XuyGw9AOvViHoSmPhryaS7nCM+IhldvlRy92nYmaBLQWtdXqX1rn5X22OLI16xihhjBjA9q3CQJxoAFjJt5Xv6bBLdKmgmvsIOYrLxv+5uZ+WOtmqJi3pNLGj+54gN9ATAH0reBFwT+FQbAAAAAElFTkSuQmCC)",
"invisible.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAACjUlEQVR4XnWTS09TURSF178wMUExvhIfiY+0Q6MThwwEHYk6MODAqHHgzD9gHDgWYhQBAUWMqY/SogSkCGJJNAiFUiyV2qqURkFa7cPvnJZiBXMH7Tl3rbXX2ntf5bWgyH+emPI8v8reAn8nj3r0Qr3qU78GNKghDWtEfk3ot2LKKq6AphTUjGYF26ObuqU7alabOvRQj/VEz6APKS23jvI/rY/6AD2ksCH0AG9Sq9r1QJ0W/hyREFbcOqQNcqiL/3FuIpozhJeot6J+H/VHegrci60lSA7t0VVtlRNqTvPAY4bQqxYL70LdxSsvd2l164B2qVHLVN4L0U2VhL4aQl9RveDdS9gl4IcBNWpRUShNUJxUT3OG0E/UTo4u4N0a1U9SHdRu3QOQIOw8N+0Yc/A2ZwgDeHfh3ZgZpbBH+zDTAPy7Jok6qU/K0Jht2k91CL5SVD9aHh1BvYHiCxYe1nV9w9h5bVcdk4DwGn3TmYg1Y9Rb8P2FQYVQbOM+qjPaDDwMguMw3j00Mst0narUDaUgBICPI5VgAhe5rUVguZBhhMaGOEzrrU6pQpfpdoqeB8k2hvoF7UB9mhxmsyD4WYIs0c/ifEKnoVxSEsptQke5rdQ5vGdRt4SYXTEf2lWq1l1s1GojlDiAgpmTLN4KHEKegw+NKvsco8oglApd0XtoOzEzhZmCuq2QJ7RRX3lqmO84xjaxGltUj/dVcJEwhuIqwVRptsZMZwL/wG2FPIHryijVVBnUNdJlS1ZKlmJEzkApr1JDj5KsyeJaQsR+fDlW8G/KCWyl1tgpziEIxczwVcnYcT6p5LpwO7gZ5jzH7mT0hmbW0+IO/UBixXX5L4RZlmpOn8u6vT7Y3P4BKLGmkBK1qvgAAAAASUVORK5CYII=)",
"lasso.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAADg0lEQVR4Xk2UXWxTZRjHn7XYfWhh4DKlIFLRdlucImbGr2gEwYwsAjGRXXihRlTOFrOoF0RcdWwhEVnQdgOmbgzoliEhAZJlvZwdMQg6No/U2IYOWVrK1q1d19q1Pef8fU7bsy3/nIvznv/ved/n47xEZKDivNpK8RoE6XC4J9gX7A/2hXukwxCwta1UcxhUu4PcLFHnqZnaf+/E0FhTqDpiiaqqjjSFhsbvnZja76kRdarLQRzbTSjAuozgczaHVoJYhShX1krlSmH2bSWaQz5nRsA6MJQDrHMtx0Y3y4QS7Eo0eTpG3K5rP7tdHSNNnl2JNQxtlo+NzrXAmgXEDcnWA/5ihbAtOeyKtKf3wmIvs6+2l8GS3hv51u3aliQUKwf8yVZxA2GNIvSIBCNaJmPt/jrR6KZfyL6omQdQF2tvmTTyPt2iIhDq/f1mth/1LtjCVTa9QAIN8dEl8tK8mgJBj6oF21GvEWb4+wlft08Sds+FHbPmDl2A/mOl2Ran7SQylkV0MIcdu+cI7J3utUb0aL2q1DZSlDiRvObpKdpCZymVf1dqW6/qYY1QsM8SXYULw3jcq8XLWlSghOrJpYUwXxpeDUuUZn6smNWheUx6l7uxGD8HrKXz9A2doQidJnnPoWt6VEQItm4/4aX0LSd2wLCEqMB6+otmqYt8K1LVd3+oTRA6/yXUTXe+mCHsjE048QbWw8R6EHoNAMlGbA+d/PSOAZXKZC9tKbr19m+DFZztc/E/B2/3jp//x5logyC/8oyBd9DBIjeODj4/X8hTcO4PZZ/a6RKpYfBKWb6CuTQ+i41elD5A5cLHga7jPjOvWDIDv6e/EE25WXo49N0mPmF21OR3wm/FTahMf3/dNtLoe4GPa1LeC9y4xGUxacNnvHnykfTG+IWxJzOlyhnPzHH7bUJRvgINiSuu+CE8i2Jw8NwORq/jidRDktMz4COU46twnVyEx+brwxfFQDc+weu4P0drQFHq/Z+uP73UZq7vh8FAV6oeL2OTZl4OFMCUaZw4NXDjyNTBWENylaLDwXFpH+5Tm5mku4sN1XZQFwrkR+OvyjvxpvT5kTuEHcmpDh5i/qIQ/1l5LQd4RifUOS1A1XTnRq76r5dhzRlTPBxx7vlSDstmiA0r8NGXUcLZm6jJARkGZvnJAtqtod4JmhJbT/9tiZ4bS9QsX83dGv8DIrFVpnd+f4UAAAAASUVORK5CYII=)",
"lassoPlus.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAADiklEQVR4XlVUbWxTVRh+12K3oYWBy9SCSAHbbXGRaGb8SoygmJFFICayH/5AIyrdYhb1BxFXHWswQRa03WVM3RzYjaEhYcZl/Tk6QhB0bN62xjV0yNJStm7tutbetvfex3N7e3XmyUnuOXme9+O8zz1EZKDyIhwVeBE28VisLzIQGYwMxPrEY7Bhh6NCYxgUuou8DLwuUD936G736GRrtC5uSSioi7dGR6fuds8dCtTzOoXlIhbbSyjBhrwt6G6LrgExlKJKfkiskksLuzVoiwbdeRs2gIlUgXWp/eTEdomwGnvSrYGuca/n2g9eT9d4a2BPej0TbZdOTiy1w1oQ8JsyHYdD5TJhZ2bME+/M7YfFWelc56yEJbc//oXXszNDKJcPhzId/CbCetnWxxOMaJ9NdoYaeaOXLpHg8HOCw0lOWrgPjcnO9lkjy9PLyzZCU2jQzOgnpgV7rNaut5GNRsnPEXzcstICQY9awX5i2ggzQoOEzzpnCXuXYq5Fc5cuTH8z5IoCnkRVooM55tq7RGDc+X5rXI+Oq3JDCyVILhTj54bPEYbPKV+CoyAhuaHjqh7WOEUGLIm1uDCGbdOFeEoxK+Hjilvz8Ng6WBK08E31og5tk+IBNg3GFBw+zlfMoHwJjrMUpzMk7Tt6TY/qOMHeGyI8l7vpxi4YtOBq0xvJR4vUQ8FV2bo7XzekCdxfhMZ57tk8YXdyxo1XsBEmhvv93ZoAJBnxcvT0B7cNqJFn++mJspuv/zJSzcb2VOr3kVv9Uz/+6U47BI/vZ+FzlkEHi9QyMfL0cilzwfnf5IPKpFeLzSOXK4s3qDb8YXLiovg2aoT3wj2ngmZ2YskP/Zr7mDepXnow+uVWVmHBatIbsddSJtTkvrpuH28JPsPKNclvhm8Ms2sxaeYz+k8/nNucujD5WL5CPhtYOOW8RSgr3kBz+rIndRRPohwsuJrBOO16NPuA6A4MBQlV+DTWKJVhy3JT7CIf7sX7eAn3qmpNUJZ969vrj7PGNRjwTiTck23C89iqkVcKSmDKt8x8N3Tj+NyRZHNmrazDkSnxIO5RhpmhO/9G0jIoByXSI6kXpN14Vfzo+G3CrsxcFzOx4iJif1YRKwXMozNssZi189xmdutXfoJVJWaZOVJs5v/18H+/rcK7nyQI3/tRrwryTLDIVkGgvRrKm6AhvePMH5bE+cl0/cpT9dX4B08ibwJFFp9KAAAAAElFTkSuQmCC)",
"pointingHand.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAADe0lEQVR4Xq2V+yvrYRzHnWwuaaUst9mY21iyHcVySghxJJLLDy7L2Y9S4geHThx+5ISc0vCDf0AS6fj1rPxkRJ2dJjvtjNolt1y+NMY+5/N5yonZbIunvm179nyf1/N+fy5PSMgLo6OjwxAWFgaxsbHQ2NhonJiYELy0Pqj/CgoKPqSmpjoGBgZgfHwciouLna2tre+D2uSlxXK5XJ2UlMTt7e2B2WyGjIwMiImJ+fhmADy9OjMzk7PZbHB0dAT4HXp6en6+GSA5OVktk8k4u90Ox8fHTEF3d/fvNwNIpVKm4AFACgYHB//Mzc0pUZ3o1SBPgEKhgKysLFd5ebkdlekTExMLUKUU56VVVVVhQQM9AdnZ2dDe3g5bW1tQU1MDKSkpNrTtr0qlsvT3939/NSA9PR2mp6fh7u4OhoaGWG2gXdDZ2Qnx8fFWBH3CR4P1IvMFe4feqoqKir7k5OQoSQEF2Wq1siwiwMzMDLjdbhgeHoa8vDw4OztjiiIiIqCyshLq6uoA39mRSCSKZ5DQ0FAxnuQXeutG+Yb8/Hwt1gIDHB4eMsDs7Czc399TsAFPCxzHwe7uLojFYlhdXQWDwQBKpZLDfdTeVMhra2vBaDRCV1cX2zAtLQ0IcHp6yuYWFxeZRfPz89DX18cAVIgYbNje3oaTkxPAJODwsN4BLS0tTPbGxgagTBCJRECFdnt7C+fn53B9fc0UXF1dweXlJbhcLnA4HAxmsVhYvVRUVHA8Hu85IDIyUl5SUgImk4mduKmp6T+ANiXvHz+Ag+ZJEYHpkxTgHt4VCIVCCTY009TUFNzc3MDk5CTlPRwcHLCN/A2CE6C0tNQ7IDw8nI8Z8LWhoeGWFGxubkJvby+THSyAz+d7jUEIpp46ISGB0+l0zHeKh9PpDAhACimd0QUuKirKO4BSC3uOTqPRwMXFBQvig/+BWER9C+8RLjo62jcAPayhvqPX6xmAgkf++hu0huKF2cdhivsGjIyMCAsLC81jY2PMHgIEMgiwvr4OcXFxHNaCbwDZ1NbW9q2srAz29/fZ6QOB0LrR0VHAdPd/ZwgEAhF6uaPVap/EwbMOHpTRPGUbKgfsBv5vPawJXn19/WfsR04qPMoozzg8hlEirK2tMUBzc3Pg93Zubq6uurqa9aCVlZUnz/LyMvu9tLQECwsLgPVD1f/Ds8n9AyubuM4FiwqZAAAAAElFTkSuQmCC)",
"rect.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAACc0lEQVR4Xo2UT0hUURTGf7uipMy8GbUwigx0WbiwpCgQKmgl1CYj3Plahtg/eguFUIoamNxUCEFCGlIgEhSORmQRNeVMSkhpWcSU1KSOQ9i8vvfmpiMZOB8Xzjv3fPPd8+dertPHQICX/OAVnTRTz1GOaNXL7pQvSdTG9KHwCVJMyzlMFw1UsJFC8lmtVSi7Qr4u7SUVk1KsmCkyZtJETchUmTKh3NSYk6ZBq0a276nSXtRMGc+kfMIkiRY3shQkWiZ9wjjxMN5SEAuPQ5qegBALe07a+eiMCj9lxZ0ZJ+N483A9oQf6qbWED6rITW4IcQGaGJuT/Y0f7rm1cJliS+jnLjNkAviEDXTwVbZ/2gwjobLESKgY6iiwhI5gyw/N/T2zKtljF0CN6p39SAZbCVyhTpwVLKOUM8wGmn4lEy35yGH+Jp1TKF9nrRp3mzbOMcp9m0MhtLPrvwRDr/6/mwda6cZ4ON24Hd5z1RISfGNIAdmDnWabtHtlzxLjtY1pgucM2o8rPOUhj+UZY0qDWGEJad5y0ca8mO90LLyKA+yjkirN6QWeaO0kwjvucZw8m0MwGinLzmelUi1nP3vYQrUCH3GNY5Qp+UpbpelcQjNnWcchVWRQhc2jiPWsYTl7aZRetvTBtP5VaJW7la0c5g4npFbCbk5xSzlNKPGRUKk6vYCwScIH2ax2lagH1VJ6wxdbNVUumKUFhGFNaMxiyEk4v3Jn1VmE4LluBK804W/5zdDA/+OxCtk58dzeNjcS7vYJ/v1bzGMJnm7rPDLmkzlvdph2E5/zRs13M2BS/p3OvhoLkVSBi7jE5zn/VPCy+K/GH86K+gV5WsL6AAAAAElFTkSuQmCC)",
"rectPlus.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAACi0lEQVR4Xo2UT0hUQRzHP6c2KmrTnYoKjCAP+45FB0uKAqECT0FdMsKbr2OIUdE76EUpamHzUmoEGWmIgUnQn9UOWURp7ksJKSuLeCW1qesStq/fvB11lzr4vgzM/Ob3md+b3/xmuEYfA4Fe8ZMhOmmklmMclVYr/U6xpRg0Pn2I+yRpZsQ4Shd1lLGJCGHWSItIv0xsXTKXEp+0+AqZJqum1KCKqQpliXaqKnVK1Umrkr62VMjcoJpWvkprYAqvyUksRV7TlAYmcOP4S1EyPgEZegMgGfftjP3JHhf9kp5rz9pZ21+U44t6oZ9qA3yUjNygReSKoIEPC2H/oN19pxouUWKAfrqZJRtIAxvp4Jv09d9mGYtZ3lisBGooMkBHMKVd5QsRlSbfcxMl99tFUCX5zg1SwZSHI6qJ4q+wNHWGuSCmzqTXFEYMan7TeYlyBSi2ItymjXOMc9/sIQLt7C4AMiE36ka7K/HDlUXR1uh0qIcH9JCpd+OZ+u3wnisG8PjOCEOydr6S1hxJXhufBnjBsBlc5hkPeRS6bt2zbuUiWK1WJpThLReMz8vFk07GV3OQ/ZRTIXWqN11t7SLBO+5yglVmD0FppA0dZqWkcScH2MtmAVqsJ1zlOJZUbbnJ0kw+0MhZ1lEpGRnGCS2zVGgDa1nOPup5ahYNqnU+QrOYm9nGEe5wUqKVsofT3JRdTspZjMWictIFwBYJfIitcsClFHNYIr3hq7iapAW1VACMSoUmjUZsz/6dX6t2rvgKAN9xEvhRT0/oZaXg/7EYIFcnvvO4zUnEezSg79//LAbw5bYuKqs+q/Nqh2pX7oJ1UP1QAyqt73Tu1ShUShK8not8WbBPBy+LfjX+AjC0Avv8MQHTAAAAAElFTkSuQmCC)",
"zoom.png": "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAACiUlEQVR4XpVTTUgUYRh+nJWCxUsNevBiYGm7UjAnFyoI9ORKSxt4a71Ei9I10EPg0VO7hOE1ikQSofXiYv6QRX9bkTAjbbM/B2mz8id3Z2f/ZvbtndR0NYn9XuYbPuZ55n3f730efMQCZjCLOX4/xyJe4BXe4C3e4QNi0JDCBu8JRPkURxIMm8BjjOMJJvEUU8jadUn36D79Zs5X8JhSyb6AX/iBzxxfLMIMwx/gIcYwXRMTNa86Mhrp23QXOwx3sW9zNKKOaF5NzAlrnCWBFXAx43jE8PeC0b4U9KstJmh/tJh+dSlotJeEDYZ/Bea5GAtudoZDkm5jcCP1poejd+XhaG+6kc82kvRwiDpLwha+A1RX8K4OaDfCU038UTQGk/GA2qO4lDbFpfbEA4NJ0QA1UThktOeFDBPOLd+/qF9PtOZBDi30LNU91FBfgz+rvmaoIdUVmnZo4CxLQU3cYsLJ7G1HebvqC7nIhHJ6G7y7mmsNKTQtGjbyq5p3Dnzv2au31i24QK35cEhpriQAzbUp92AS1GKqI7odL5F1jM9bzV7KzoyVLvfbDhKAoYZ4wGp/NFKU8Br6scS9VrM7Fw8a56/8A271ovb0pkF9m2UPy2AWGd/k4sYdpal/7+fH4cTxvaPsGo6C3EXysWYWQafIR2cqSnFyV237CM6ADOowyM8Si+xkkCsztB2RIYZCFT2QB1mYVdwSSSza/N85nD1qDl27cyA76EQ1k6bqtESiRagrXFsdyBxQq+ySnfIhtbJ6gHX2bBprQvmAHwKH/GDBmRCDyibP4Od/HLcNZ0KcCSnkmLQsKEd4mmvfgTMhybGCb0zI4RPb1bSbUtnDUvHz4yGJ7BUWx2+5Iu7Rc33AhQAAAABJRU5ErkJggg==)"
};
// lib/canvas_editor/cursor_manager_24.js
var computedCursors = /* @__PURE__ */ Object.create(null);
var scaleFactor = 3 / 4;
var CursorManager = class {
constructor(JavaEditorArea) {
this.HOTSPOT_32 = JavaEditorArea.HOTSPOT_32;
this.IMAGE_NAME_32 = JavaEditorArea.IMAGE_NAME_32;
this.cPointerCursor = JavaEditorArea.cPointerCursor;
this.cTextCursor = JavaEditorArea.cTextCursor;
}
getCursor(cursor) {
if (computedCursors[cursor]) {
return computedCursors[cursor];
}
if (this.IMAGE_NAME_32[cursor]) {
return this.buildCursor(cursor);
}
switch (cursor) {
case this.cPointerCursor:
return "default";
case this.cTextCursor:
return "text";
default:
throw new Error(`Unknown cursor: ${cursor}`);
}
}
buildCursor(cursor) {
const cursorName = this.IMAGE_NAME_32[cursor];
const cursorUrl = cursors[cursorName];
const builtCursor = `${cursorUrl} ${this.HOTSPOT_32[cursor * 2] * scaleFactor} ${this.HOTSPOT_32[cursor * 2 + 1] * scaleFactor}, default`;
computedCursors[cursor] = builtCursor;
return builtCursor;
}
};
// lib/canvas_editor/editor_dialog.js
var EditorDialog = class {
/**
*
* @param {string} title
* @param {HTMLElement} rootElement
*/
constructor(title, rootElement) {
this.title = title;
this.rootElement = rootElement;
this.elements = [];
this.dialogElement = null;
}
setLayout(hLayout, vLayout) {
this.hLayout = generateLayout(hLayout);
this.vLayout = generateLayout(vLayout);
}
add(component, x, y, x2, y2) {
this.elements.push({ component, x, y, x2, y2 });
}
createTextField(width, height) {
return new TextField(width, height);
}
createLabel(text) {
return new Label(text);
}
createComboBox() {
return new ComboBox();
}
createCheckBox(text) {
return new CheckBox(text);
}
setEventConsumer(consumer) {
this.consumer = consumer;
}
showMessage(message) {
window.alert(message);
}
showDialog() {
const dialog = document.createElement("dialog");
const rootElementBounds = this.rootElement.getBoundingClientRect();
Object.assign(dialog.style, {
position: "absolute",
marginBlock: 0,
left: `${rootElementBounds.left}px`,
right: `${document.body.parentElement.clientWidth - rootElementBounds.right}px`,
top: `${this.rootElement.offsetTop + 30}px`
});
this.dialogElement = dialog;
this.rootElement.getRootNode().append(dialog);
const grid = document.createElement("div");
grid.style.display = "grid";
grid.style.gridTemplateColumns = this.hLayout;
grid.style.gridTemplateRows = this.vLayout;
dialog.append(grid);
for (const { component, x, y, x2, y2 } of this.elements) {
const div = document.createElement("div");
if (x2 === void 0) {
div.style.gridColumn = `${x + 1} / ${x + 2}`;
div.style.gridRow = `${y + 1} / ${y + 2}`;
} else {
div.style.gridColumn = `${x + 1} / ${x2 + 2}`;
div.style.gridRow = `${y + 1} / ${y2 + 2}`;
}
div.append(component.getElement());
grid.append(div);
}
const buttons = document.createElement("div");
buttons.style.display = "flex";
buttons.style.flexDirection = "row-reverse";
buttons.style.gap = "15px";
const okButton = document.createElement("button");
okButton.textContent = "OK";
okButton.addEventListener("click", () => {
this.consumer.fireOk();
});
buttons.append(okButton);
const cancelButton = document.createElement("button");
cancelButton.textContent = "Cancel";
cancelButton.addEventListener("click", () => {
this.consumer.fireCancel();
});
buttons.append(cancelButton);
dialog.append(buttons);
dialog.showModal();
dialog.addEventListener("cancel", () => {
this.consumer.fireCancel();
});
}
disposeDialog() {
if (this.dialogElement !== null) {
this.dialogElement.remove();
this.dialogElement = null;
}
}
};
var Component = class {
setEventHandler(eventHandler) {
this.eventHandler = eventHandler;
}
fireEvent(what, value) {
this.eventHandler(what, value);
}
};
var Label = class extends Component {
constructor(text) {
super();
this.element = document.createElement("label");
this.setText(text);
}
setText(text) {
this.element.textContent = text;
}
getElement() {
return this.element;
}
};
var TextField = class extends Component {
constructor() {
super();
this.element = document.createElement("input");
this.element.type = "text";
}
setText(text) {
this.element.value = text;
}
getText() {
return this.element.value;
}
getElement() {
return this.element;
}
};
var ComboBox = class extends Component {
constructor() {
super();
this.element = document.createElement("select");
this.element.addEventListener("change", () => {
this.fireEvent(2, this.element.selectedIndex);
});
}
setEnabled(enabled) {
this.element.disabled = !enabled;
}
addItem(item) {
const option = document.createElement("option");
option.textContent = item;
this.element.append(option);
}
getSelectedIndex() {
return this.element.selectedIndex;
}
setSelectedIndex(index) {
this.element.selectedIndex = index;
}
setSelectedItem(item) {
const options = this.element.options;
for (let i = 0; i < options.length; i++) {
if (options[i].textContent === item) {
this.element.selectedIndex = i;
}
}
}
getSelectedItem() {
return this.element.options[this.element.selectedIndex].textContent;
}
removeAllItems() {
this.element.innerHTML = "";
}
getElement() {
return this.element;
}
};
var CheckBox = class extends Component {
constructor(text) {
super();
const label = document.createElement("label");
const input = document.createElement("input");
input.type = "checkbox";
input.addEventListener("change", () => {
this.fireEvent(3, input.checked ? 1 : 0);
});
label.append(input);
label.append(text);
this.element = label;
this.checkBox = input;
}
setEnabled(enabled) {
this.checkBox.disabled = !enabled;
}
isSelected() {
return this.checkBox.checked;
}
setSelected(selected) {
this.checkBox.checked = selected;
}
getElement() {
return this.element;
}
};
function generateLayout(layout) {
return layout.map((dim) => {
if (dim > 0) {
return `${dim}px`;
} else {
return "auto";
}
}).join(" ");
}
// lib/canvas_editor/editor_image.js
var EditorImage = class {
/**
*
* @param {ImageData} imageData
*/
constructor(imageData) {
this.imageData = imageData;
this.dataView = new DataView(imageData.data.buffer);
}
getWidth() {
return this.imageData.width;
}
getHeight() {
return this.imageData.height;
}
getRGB(x, y) {
const color = this.dataView.getInt32(
(y * this.imageData.width + x) * 4,
false
);
const alpha = color & 255;
return alpha << 24 | color >>> 8;
}
setRGB(x, y, argb) {
const alpha = argb >>> 24 & 255;
const rgb = argb << 8 | alpha;
this.dataView.setInt32((y * this.imageData.width + x) * 4, rgb, false);
}
toDataURL() {
const canvas = document.createElement("canvas");
const ctx2 = canvas.getContext("2d");
canvas.width = this.imageData.width;
canvas.height = this.imageData.height;
ctx2.putImageData(this.imageData, 0, 0);
return canvas.toDataURL("image/png");
}
};
// lib/canvas_editor/ui_helper.js
var UIHelper = class {
/**
*
* @param {HTMLCanvasElement} canvasElement
* @param {HTMLElement} dialogRoot
* @param JavaEditorArea
*/
constructor(canvasElement, dialogRoot, JavaEditorArea) {
this.canvasElement = canvasElement;
this.dialogRoot = dialogRoot;
this.JavaEditorArea = JavaEditorArea;
}
register(javaUiHelper) {
this.javaUiHelper = javaUiHelper;
this.cursorManager = new CursorManager(this.JavaEditorArea, javaUiHelper);
}
grabFocus() {
this.canvasElement.focus({ preventScroll: true });
}
setCursor(cursor) {
this.canvasElement.style.cursor = this.cursorManager.getCursor(cursor);
}
showHelpDialog() {
}
createImage(width, height) {
const imageData = new ImageData(width, height);
return new EditorImage(imageData);
}
createImageFromBase64(width, height, base64) {
base64 = base64.replaceAll(
/%\d+%/g,
(match) => "A".repeat(Number(match.slice(1, -1)))
);
const decoded = decodeBase64(base64);
const typedArray = new Uint8ClampedArray(decoded);
const imageData = new ImageData(typedArray, width, height);
return new EditorImage(imageData);
}
createDialog(title) {
return new EditorDialog(title, this.dialogRoot);
}
runLater(fn) {
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(fn);
} else if (typeof setImmediate === "function") {
setImmediate(fn);
} else {
setTimeout(fn, 0);
}
}
};
// lib/canvas_editor/create_editor.js
function createEditor(parentElement, options, onChange, JavaEditorArea, JavaEditorToolbar, JavaUIHelper, Molecule2, Reaction2) {
const {
readOnly = false,
initialMode = "molecule",
initialFragment = false
} = options;
const rootElement = document.createElement("div");
rootElement.dataset.openchemlibCanvasEditor = "true";
Object.assign(rootElement.style, {
width: "100%",
height: "100%",
display: "flex",
flexDirection: "row",
alignItems: "start",
backgroundColor: "white",
// Prevent side effects of pointer events, like scrolling the page or text
// selection.
touchAction: "none",
userSelect: "none",
webkitUserSelect: "none"
});
const shadowRoot = rootElement.attachShadow({ mode: "open" });
shadowRoot.adoptedStyleSheets = [getEditorStylesheet()];
let toolbarCanvas = null;
if (!readOnly) {
toolbarCanvas = document.createElement("canvas");
shadowRoot.append(toolbarCanvas);
}
const editorContainer = document.createElement("div");
Object.assign(editorContainer.style, {
width: "100%",
height: "100%"
});
shadowRoot.append(editorContainer);
const editorCanvas = document.createElement("canvas");
editorCanvas.tabIndex = 0;
Object.assign(editorCanvas.style, {
outline: "none"
});
editorContainer.append(editorCanvas);
parentElement.append(rootElement);
const uiHelper = new JavaUIHelper(
new UIHelper(editorCanvas, editorContainer, JavaEditorArea)
);
const editorArea = new JavaEditorArea(
computeMode(initialMode, JavaEditorArea),
new EditorArea(editorCanvas, onChange),
uiHelper
);
if (initialFragment) {
if (initialMode === "molecule") {
const fragmentMolecule = new Molecule2(0, 0);
fragmentMolecule.setFragment(true);
editorArea.setMolecule(fragmentMolecule);
} else {
const fragmentReaction = Reaction2.create();
fragmentReaction.setFragment(true);
editorArea.setReaction(fragmentReaction);
}
}
uiHelper.setEditorArea(editorArea);
const toolbar = readOnly ? null : new JavaEditorToolbar(editorArea, new Toolbar(toolbarCanvas), uiHelper);
function updateCanvasDimensions(containerSize2) {
editorCanvas.style.width = `${containerSize2.width}px`;
editorCanvas.width = Math.floor(containerSize2.width * hidpiScaleFactor);
editorCanvas.style.height = `${containerSize2.height}px`;
editorCanvas.height = Math.floor(containerSize2.height * hidpiScaleFactor);
if (containerSize2.width > 0 && containerSize2.height > 0) {
editorArea.repaint();
}
}
const containerSize = editorContainer.getBoundingClientRect();
updateCanvasDimensions(containerSize);
const resizeObserver = new ResizeObserver(([entry]) => {
updateCanvasDimensions(entry.contentRect);
});
resizeObserver.observe(editorContainer);
let removePointerListeners = null;
let removeKeyboardListeners = null;
let removeToolbarPointerListeners = null;
if (!readOnly) {
removePointerListeners = addPointerListeners(
editorCanvas,
editorArea,
JavaEditorArea
);
removeKeyboardListeners = addKeyboardListeners(
editorContainer,
editorCanvas,
editorArea,
JavaEditorArea,
Molecule2
);
removeToolbarPointerListeners = addPointerListeners(
toolbarCanvas,
toolbar,
JavaEditorArea
);
}
function destroy() {
rootElement.remove();
resizeObserver.disconnect();
removePointerListeners?.();
removeKeyboardListeners?.();
removeToolbarPointerListeners?.();
}
return { editorArea, toolbar, uiHelper, destroy };
}
function computeMode(initialMode, JavaEditorArea) {
switch (initialMode) {
case "molecule":
return 0;
case "reaction":
return JavaEditorArea.MODE_REACTION | JavaEditorArea.MODE_MULTIPLE_FRAGMENTS;
default:
throw new Error(`Invalid initial mode: ${initialMode}`);
}
}
// lib/canvas_editor/init/canvas_editor.js
function initCanvasEditor(JavaEditorArea, JavaEditorToolbar, JavaUIHelper, Molecule2, Reaction2) {
class CanvasEditor2 {
#editorArea;
// Can be useful for debugging.
/* eslint-disable no-unused-private-class-members */
#toolbar;
#uiHelper;
/* eslint-enable no-unused-private-class-members */
#onChange;
#changeEventMap;
#destroy;
constructor(parentElement, options = {}) {
const { editorArea, toolbar, uiHelper, destroy } = createEditor(
parentElement,
options,
(event) => this.#handleChange(event),
JavaEditorArea,
JavaEditorToolbar,
JavaUIHelper,
Molecule2,
Reaction2
);
this.#editorArea = editorArea;
this.#toolbar = toolbar;
this.#uiHelper = uiHelper;
this.#onChange = null;
this.#changeEventMap = {
[JavaEditorArea.EDITOR_EVENT_MOLECULE_CHANGED]: "molecule",
[JavaEditorArea.EDITOR_EVENT_SELECTION_CHANGED]: "selection",
[JavaEditorArea.EDITOR_EVENT_HIGHLIGHT_ATOM_CHANGED]: "highlight-atom",
[JavaEditorArea.EDITOR_EVENT_HIGHLIGHT_BOND_CHANGED]: "highlight-bond"
};
this.#destroy = destroy;
}
getMode() {
this.#checkNotDestroyed();
const mode = this.#editorArea.getMode();
const isReaction = mode & JavaEditorArea.MODE_REACTION !== 0;
return isReaction ? "reaction" : "molecule";
}
setMolecule(molecule) {
this.#checkNotDestroyed();
this.#editorArea.setMolecule(molecule);
}
getMolecule() {
this.#checkNotDestroyed();
return this.#editorArea.getMolecule();
}
setReaction(reaction) {
this.#checkNotDestroyed();
this.#editorArea.setReaction(reaction);
}
getReaction() {
this.#checkNotDestroyed();
return this.#editorArea.getReaction();
}
setOnChangeListener(onChange) {
this.#checkNotDestroyed();
this.#onChange = onChange;
}
removeOnChangeListener() {
this.#checkNotDestroyed();
this.#onChange = null;
}
clearAll() {
this.#checkNotDestroyed();
this.#editorArea.clearAll();
}
destroy() {
this.#checkNotDestroyed();
this.#destroy();
this.#editorArea = null;
this.#toolbar = null;
this.#uiHelper = null;
this.#onChange = null;
this.#destroy = null;
}
get isDestroyed() {
return !this.#editorArea;
}
moleculeChanged() {
this.#checkNotDestroyed();
this.#editorArea.moleculeChanged();
}
#checkNotDestroyed() {
if (this.isDestroyed) {
throw new Error("CanvasEditor has been destroyed");
}
}
/**
* @param {{ what: number; isUserEvent: boolean; }} event
*/
#handleChange(event) {
if (!this.#onChange) return;
const { what, isUserEvent } = event;
this.#onChange({
type: this.#changeEventMap[what],
isUserEvent
});
}
}
return CanvasEditor2;
}
// lib/canvas_editor/init/canvas_editor_element.js
function initCanvasEditorElement(CanvasEditor2, Molecule2, Reaction2, ReactionEncoder2) {
class CanvasEditorElement extends HTMLElement {
/** @type {{MOLECULE: 'molecule', REACTION: 'reaction'}} */
static MODE = Object.freeze(
/* @__PURE__ */ Object.create({
MOLECULE: "molecule",
REACTION: "reaction"
})
);
static observedAttributes = Object.freeze([
"idcode",
"fragment",
"mode",
"readonly"
]);
/**
* @type {{mode: 'molecule' | 'reaction', fragment: boolean, idcode: string, readonly: boolean}}
*/
#state = {
idcode: "",
fragment: false,
mode: CanvasEditorElement.MODE_MOLECULE,
readonly: false
};
get idcode() {
return this.#state.idcode;
}
set idcode(value) {
this.#state.idcode = String(value);
this.setAttribute("idcode", this.#state.idcode);
}
get fragment() {
return this.#state.fragment;
}
set fragment(value) {
this.#state.fragment = Boolean(value);
if (this.#state.fragment) {
this.setAttribute("fragment", "");
} else {
this.removeAttribute("fragment");
}
}
get mode() {
return this.#state.mode;
}
set mode(value) {
this.#state.mode = String(value);
this.setAttribute("mode", this.#state.mode);
}
get readonly() {
return this.#state.readonly;
}
set readonly(value) {
this.#state.readonly = Boolean(value);
if (this.#state.readonly) {
this.setAttribute("readonly", "");
} else {
this.removeAttribute("readonly");
}
}
/* --- custom element api --- */
/**
* @param {Molecule} molecule
* @this {CanvasEditorElement}
*/
setMolecule(molecule) {
this.fragment = molecule.isFragment();
this.idcode = `${molecule.getIDCode()} ${molecule.getIDCoordinates()}`;
this.#editor.setMolecule(molecule);
}
/**
* @return {Molecule}
* @this {CanvasEditorElement}
*/
getMolecule() {
return this.#editor.getMolecule();
}
/**
* @param {Reaction} reaction
* @this {CanvasEditorElement}
*/
setReaction(reaction) {
this.fragment = reaction.isFragment();
this.idcode = ReactionEncoder2.encode(reaction, {
keepAbsoluteCoordinates: true,
mode: ReactionEncoder2.INCLUDE_MAPPING | ReactionEncoder2.INCLUDE_COORDS | ReactionEncoder2.RETAIN_REACTANT_AND_PRODUCT_ORDER
}) ?? "";
this.#editor.setReaction(reaction);
}
/**
* @return {Reaction}
* @this {CanvasEditorElement}
*/
getReaction() {
return this.#editor.getReaction();
}
/**
* @this {CanvasEditorElement}
*/
clearAll() {
this.#editor.clearAll();
this.idcode = "";
}
/**
* @this {CanvasEditorElement}
*/
moleculeChanged() {
this.#editor.moleculeChanged();
}
/* --- internals --- */
/** @type {CanvasEditor} */
#editor;
/**
* @this {CanvasEditorElement}
*/
#initEditor() {
if (this.#editor) return;
this.#editor = new CanvasEditor2(this, {
readOnly: this.readonly,
initialMode: this.mode
});
this.#editor.setOnChangeListener(this.#handleChange);
requestIdleCallback(() => this.#initIdCode());
}
/**
* @this {CanvasEditorElement}
*/
#initIdCode() {
switch (this.mode) {
case CanvasEditorElement.MODE.MOLECULE: {
return this.#initMolecule();
}
case CanvasEditorElement.MODE.REACTION: {
return this.#initReaction();
}
default:
throw new Error(`Mode ${this.mode} is not supported`);
}
}
/**
* @param {string} idcodeAndCoordinates
*/
#moleculeFromIdCode(idcodeAndCoordinates) {
const index = idcodeAndCoordinates.indexOf(" ");
if (index === -1) {
return Molecule2.fromIDCode(idcodeAndCoordinates);
}
const idcode = idcodeAndCoordinates.slice(0, index);
const coordinates = idcodeAndCoordinates.slice(index + 1);
return Molecule2.fromIDCode(idcode, coordinates);
}
/**
* @this {CanvasEditorElement}
*/
#initMolecule() {
const molecule = this.#moleculeFromIdCode(this.idcode);
molecule.setFragment(this.fragment);
this.#editor.setMolecule(molecule);
}
/**
* @this {CanvasEditorElement}
*/
#initReaction() {
const reaction = ReactionEncoder2.decode(this.idcode, {
ensureCoordinates: true
}) ?? Reaction2.create();
reaction.setFragment(this.fragment);
this.#editor.setReaction(reaction);
}
#ignoreAttributeChange = false;
/**
* @param {() => void} fn
* @this {CanvasEditorElement}
*/
#wrapIgnoreAttributeChange(fn) {
this.#ignoreAttributeChange = true;
try {
fn();
} finally {
this.#ignoreAttributeChange = false;
}
}
/**
* @param {{
* type: 'molecule' | 'selection' | 'highlight-atom' | 'highlight-bond';
* isUserEvent: boolean;
* }} editorEventOnChange
*/
#handleChange = (editorEventOnChange) => {
const idcode = this.idcode;
const fragment = this.fragment;
this.#wrapIgnoreAttributeChange(() => {
if (editorEventOnChange.type !== "molecule") return;
switch (this.mode) {
case CanvasEditorElement.MODE.MOLECULE: {
const molecule = this.getMolecule();
this.idcode = `${molecule.getIDCode()} ${molecule.getIDCoordinates()}`;
this.fragment = molecule.isFragment();
break;
}
case CanvasEditorElement.MODE.REACTION: {
const reaction = this.getReaction();
this.idcode = ReactionEncoder2.encode(reaction, {
keepAbsoluteCoordinates: true,
mode: ReactionEncoder2.INCLUDE_MAPPING | ReactionEncoder2.INCLUDE_COORDS | ReactionEncoder2.RETAIN_REACTANT_AND_PRODUCT_ORDER
});
this.fragment = reaction.isFragment();
break;
}
default:
throw new Error(`Unsupported mode ${this.mode}`);
}
});
const domEvent = new CustomEvent("change", {
detail: editorEventOnChange,
bubbles: true
});
this.dispatchEvent(domEvent);
if (editorEventOnChange.mode !== "molecule") return;
if (this.idcode !== idcode) {
const idcodeChangeEvent = new CustomEvent("idcode-changed", {
detail: this.idcode,
bubbles: true
});
this.dispatchEvent(idcodeChangeEvent);
}
if (this.fragment !== fragment) {
const fragmentChangeEvent = new CustomEvent("fragment-changed", {
detail: this.fragment,
bubbles: true
});
this.dispatchEvent(fragmentChangeEvent);
}
};
#destroyEditor() {
if (!this.#editor) return;
this.#editor.destroy();
this.#editor = void 0;
}
#resetEditor() {
this.#destroyEditor();
this.#initEditor();
}
/* --- lifecycle hooks --- */
/**
* Custom element added to page.
*/
connectedCallback() {
this.#state = {
idcode: this.getAttribute("idcode") || "",
fragment: this.hasAttribute("fragment"),
mode: this.getAttribute("mode") || CanvasEditorElement.MODE.MOLECULE,
readonly: this.hasAttribute("readonly")
};
this.#initEditor();
}
/**
* Custom element removed from page.
*/
disconnectedCallback() {
this.#destroyEditor();
}
/**
* Custom element moved to new page.
*/
adoptedCallback() {
this.connectedCallback();
}
/**
* Attribute ${name} has changed from ${oldValue} to ${newValue}
*
* Sync attribute changes to internal state.
* propagate changes to editor.
*/
attributeChangedCallback(name, oldValue, newValue) {
if (!this.#editor) return;
if (this.#ignoreAttributeChange) return;
const mutatorHandler = (() => {
switch (name) {
case "idcode": {
this.#state.idcode = String(newValue);
return () => this.#initIdCode();
}
case "fragment": {
this.#state.fragment = newValue !== null;
return () => this.#initIdCode();
}
case "mode": {
this.#state.mode = String(newValue);
return () => this.#resetEditor();
}
case "readonly": {
this.#state.readonly = newValue !== null;
return () => this.#resetEditor();
}
default:
throw new Error("unsupported attribute change");
}
})();
mutatorHandler();
}
}
return CanvasEditorElement;
}
// lib/canvas_editor/init/index.js
function init(OCL) {
const {
GenericEditorArea: JavaEditorArea,
GenericEditorToolbar: JavaEditorToolbar,
GenericUIHelper: JavaUIHelper,
Molecule: Molecule2,
Reaction: Reaction2,
ReactionEncoder: ReactionEncoder2
} = OCL;
const CanvasEditor2 = initCanvasEditor(
JavaEditorArea,
JavaEditorToolbar,
JavaUIHelper,
Molecule2,
Reaction2
);
function registerCustomElement2() {
const constructor = customElements.get("openchemlib-editor");
if (constructor) return constructor;
const CanvasEditorElement = initCanvasEditorElement(
CanvasEditor2,
Molecule2,
Reaction2,
ReactionEncoder2
);
customElements.define("openchemlib-editor", CanvasEditorElement);
const style = document.createElement("style");
style.id = "openchemlib-editor-default-style";
style.innerHTML = `
/* dynamicaly added from openchemlib registerCustomElement with low priority */
openchemlib-editor:defined {
display: block;
height: 400px;
width: 600px;
}
`;
document.head.prepend(style);
return CanvasEditorElement;
}
OCL.CanvasEditor = CanvasEditor2;
OCL.registerCustomElement = registerCustomElement2;
delete OCL.GenericEditorArea;
delete OCL.GenericEditorToolbar;
delete OCL.GenericUIHelper;
}
// lib/extend/index.js
function extendOCL(OCL) {
const { ConformerGenerator: ConformerGenerator2, ForceFieldMMFF94: ForceFieldMMFF942, Molecule: Molecule2 } = OCL;
ConformerGenerator2.prototype.molecules = function* molecules() {
let nextConformer;
while ((nextConformer = this.getNextConformerAsMolecule()) !== null) {
yield nextConformer;
}
};
const defaultMinimiseOptions = {
maxIts: 4e3,
gradTol: 1e-4,
funcTol: 1e-6
};
const _minimise = ForceFieldMMFF942.prototype._minimise;
delete ForceFieldMMFF942.prototype._minimise;
ForceFieldMMFF942.prototype.minimise = function minimise(options) {
options = { ...defaultMinimiseOptions, ...options };
return _minimise.call(
this,
options.maxIts,
options.gradTol,
options.funcTol
);
};
function parseMoleculeFromText(text) {
if (!text) {
return null;
}
if (text.includes("V2000") || text.includes("V3000")) {
return Molecule2.fromMolfile(text);
}
try {
return Molecule2.fromSmiles(text);
} catch {
}
try {
return Molecule2.fromIDCode(text);
} catch {
}
return null;
}
Molecule2.fromText = function fromText(text) {
const molecule = parseMoleculeFromText(text);
if (molecule && molecule.getAllAtoms() > 0) {
return molecule;
}
return null;
};
Molecule2.prototype.getOCL = function getOCL() {
ret