UNPKG

openchemlib

Version:
1,341 lines (1,325 loc) 3.58 MB
// 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