UNPKG

@webwriter/chemlab

Version:

WIP - Prepare virtual laboratory environments for various topics in chemistry. Includes the building of molecules as well as applications for electrochemistry and acid/base theory.

1,826 lines (1,784 loc) 193 kB
import { html, LitElement, PropertyValueMap } from "lit"; import { LitElementWw } from "@webwriter/lit"; import { customElement, property, query, } from "lit/decorators.js"; import { style } from "chemlab-style"; import * as PIXI from "pixi.js"; import * as chemlib from "chemlib"; import SmilesDrawer from "smiles-drawer" import SlRange from "@shoelace-style/shoelace/dist/components/range/range.component.js"; import SlDetails from "@shoelace-style/shoelace/dist/components/details/details.component.js"; import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js"; import SlIconButton from "@shoelace-style/shoelace/dist/components/icon-button/icon-button.component.js"; import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js"; import SlTag from "@shoelace-style/shoelace/dist/components/tag/tag.component.js"; import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js"; import SlOption from "@shoelace-style/shoelace/dist/components/option/option.component.js"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"; import SlAlert from "@shoelace-style/shoelace/dist/components/alert/alert.component.js"; import SlBadge from "@shoelace-style/shoelace/dist/components/badge/badge.component.js"; import SlTab from "@shoelace-style/shoelace/dist/components/tab/tab.component.js"; import SlCard from "@shoelace-style/shoelace/dist/components/card/card.component.js"; import SlButtonGroup from "@shoelace-style/shoelace/dist/components/button-group/button-group.component.js"; import SlTabGroup from "@shoelace-style/shoelace/dist/components/tab-group/tab-group.component.js"; import SlTabPanel from "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.component.js"; import SlSplitPanel from "@shoelace-style/shoelace/dist/components/split-panel/split-panel.component.js"; import SlDivider from "@shoelace-style/shoelace/dist/components/divider/divider.component.js"; import SlRating from "@shoelace-style/shoelace/dist/components/rating/rating.component.js"; import SlMenu from "@shoelace-style/shoelace/dist/components/menu/menu.component.js"; import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js"; import SlMenuItem from "@shoelace-style/shoelace/dist/components/menu-item/menu-item.component.js"; import SlMenuLabel from "@shoelace-style/shoelace/dist/components/menu-label/menu-label.component.js"; import SlCarousel from "@shoelace-style/shoelace/dist/components/carousel/carousel.component.js"; import SlCarouselItem from "@shoelace-style/shoelace/dist/components/carousel-item/carousel-item.component.js"; import SlPopup from "@shoelace-style/shoelace/dist/components/popup/popup.component.js"; import SlCheckbox from "@shoelace-style/shoelace/dist/components/checkbox/checkbox.component.js"; import SlDrawer from "@shoelace-style/shoelace/dist/components/drawer/drawer.component.js"; import SlTooltip from "@shoelace-style/shoelace/dist/components/tooltip/tooltip.component.js"; import SlRadio from "@shoelace-style/shoelace/dist/components/radio/radio.component.js"; import SlSwitch from "@shoelace-style/shoelace/dist/components/switch/switch.component.js"; import SlRadioGroup from "@shoelace-style/shoelace/dist/components/radio-group/radio-group.component.js"; import "@shoelace-style/shoelace/dist/themes/light.css"; import { setAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry.js"; import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js"; setBasePath("@shoelace-style/shoelace/dist"); import { HelpOverlay, HelpPopup, } from "@webwriter/wui/dist/helpSystem/helpSystem.js"; import { uniqueNamesGenerator, Config, adjectives, colors, animals, } from "unique-names-generator"; import Chart from "chart.js/auto"; // @ts-ignore import beaker_svg from "./media/beaker.svg"; // @ts-ignore import beaker_fl_svg from "./media/beaker_full_l.svg"; // @ts-ignore import beaker_fs_svg from "./media/beaker_full_s.svg"; // @ts-ignore import flask_svg from "./media/flask.svg"; // @ts-ignore import flask_fl_svg from "./media/flask_full_l.svg"; // @ts-ignore import flask_fs_svg from "./media/flask_full_s.svg"; // @ts-ignore import burette_svg from "./media/burette.svg"; // @ts-ignore import burette_fl_svg from "./media/burette_full_l.svg"; // @ts-ignore import burette_fs_svg from "./media/burette_full_s.svg"; // @ts-ignore import testtube_svg from "./media/testtube.svg"; // @ts-ignore import testtube_fl_svg from "./media/testtube_full_l.svg"; // @ts-ignore import testtube_fs_svg from "./media/testtube_full_S.svg"; // @ts-ignore import cable_svg from "./media/cable.svg"; // @ts-ignore import source_off_svg from "./media/source_off.svg"; // @ts-ignore import source_on_svg from "./media/source_on.svg"; // @ts-ignore import voltmeter_off_svg from "./media/voltmeter_off.svg"; // @ts-ignore import voltmeter_on_svg from "./media/voltmeter_on.svg"; // @ts-ignore import default_svg from "./media/default.svg"; // @ts-ignore import bg_svg from "./media/bg.png"; import { HALF_PI, isNumber } from "chart.js/helpers"; /** * chemlab webwriter widget for presenting interactive chemistry experiments * * @export * @class ChemLab * @typedef {ChemLab} * @extends {LitElementWw} */ @customElement("webwriter-chemlab") export class ChemLab extends LitElementWw { /** * css styles * * @static * @type {*} */ static styles = style; /** * main container of the canvas * * @private * @type {HTMLDivElement} */ @query("#upper") private accessor containerDiv: HTMLDivElement; /** * molecule part of context menu in teacher view * * @private * @type {SlMenu} */ @query("#molConfig") private accessor molConfig: SlMenu; /** * mixture part of context menu in teacher view * * @private * @type {SlMenu} */ @query("#mixConfig") private accessor mixConfig: SlMenu; /** * component part of context menu in teacher view * * @private * @type {SlMenu} */ @query("#compConfig") private accessor compConfig: SlMenu; /** * molecule list element inside the tab panel * * @private * @type {SlCarousel} */ @query("#mixCarousel") private accessor mixCarousel: SlCarousel; /** * main context menu element for both views * * @private * @type {SlMenu} */ @query("#contextMenu") private accessor contextMenu: SlMenu; /** * confirmation dialog box element * * @private * @type {SlDialog} */ @query("#confirmDialog") private accessor confirmDialog: SlDialog; /** * input for molar amounts of molecules * * @private * @type {SlInput} */ @query("#molAmount") private accessor molAmount: SlInput; /** * search field for filtering element data * * @private * @type {SlInput} */ @query("#elemSearch") private accessor elemSearch: SlInput; /** * shoelace tab container for the panel * * @private * @type {SlTabGroup} */ @query("#tab_select") private accessor tab_select: SlTabGroup; /** * main panel tab for current component * * @private * @type {SlTab} */ @query("#settings_drawer") private accessor settings_drawer: SlTab; /** * shoelace badge for labeling possible conent in a component * * @private * @type {SlTag} */ @query("#property_badge") private accessor property_tag: SlTag; /** * molecule select for new bonds * * @private * @type {SlSelect} */ @query("#bond_1") private accessor bond_sel_1: SlSelect; /** * molecule select for new bonds * * @private * @type {SlSelect} */ @query("#bond_2") private accessor bond_sel_2: SlSelect; /** * bond type select * * @private * @type {SlSelect} */ @query("#bond_3") private accessor bond_sel_3: SlSelect; /** * select for e-component connection partners * * @private * @type {SlSelect} */ @query("#newECon") private accessor conSelect: SlSelect; /** * slider for bond magnitude of new bonds * * @private * @type {HTMLInputElement} */ @query("#bondMag") private accessor bondMag: HTMLInputElement; /** * container for the connection view of the panel * * @private * @type {HTMLDivElement} */ @query("#conInfo") private accessor eConInfo: HTMLDivElement; /** * container for the information view of the panel * * @private * @type {HTMLDivElement} */ @query("#compInfo") private accessor eCompInfo: HTMLDivElement; /** * shoelace alert element for all messages * * @private * @type {SlAlert} */ @query("#dialogOverview") private accessor dialogAlert: SlAlert; /** * shoelace popup containing the object context menu element * * @private * @type {SlPopup} */ @query("#contextPopup") private accessor contextPopup: SlPopup; /** * container inside object context menu showing molar info * * @private * @type {HTMLDivElement} */ @query("#contextMolInfo") private accessor contextMolInfo: HTMLDivElement; /** * container inside object context menu showing general info on current mixture * * @private * @type {HTMLDivElement} */ @query("#contextMixInfo") private accessor contextMixInfo: HTMLDivElement; /** * button inside object context menu to open burette components * * @private * @type {HTMLButtonElement} */ @query("#openComp") private accessor openBtn: HTMLButtonElement; /** * button inside object context menu to activate electric components * * @private * @type {HTMLButtonElement} */ @query("#activateEComp") private accessor activateEComp: HTMLButtonElement; /** * container inside object context menu showing general info on current e-component * * @private * @type {HTMLButtonElement} */ @query("#contextEchemInfo") private accessor contextEchemInfo: HTMLButtonElement; /** * shoelace popup containing the tab panel * * @private * @type {SlPopup} */ @query("#tabPopup") private accessor tabPopup: SlPopup; /** * anchor element of the panel popup * * @private * @type {HTMLSpanElement} */ @query("#tabPopupAnchor") private accessor tabPopupAnchor: HTMLSpanElement; /** * anchor element of the context popup * * @private * @type {HTMLSpanElement} */ @query("#contextPopupAnchor") private accessor contextPopupAnchor: HTMLSpanElement; /** * part of options, toggles xAPI statements * * @private * @type {SlCheckbox} */ @query("#xAPICheck") private accessor xAPICheck: SlCheckbox; /** * xAPI API key input * * @private * @type {SlInput} */ @query("#xAPIKeyInput") private accessor xAPIKeyInput: SlInput; /** * select for components to get deactivated for students * * @private * @type {SlSelect} */ @query("#invCompSelect") private accessor invCompSelect: SlSelect; /** * select for premade molecules inside the panel * * @private * @type {SlSelect} */ @query("#molSelect") private accessor molSelect: SlSelect; /** * select for premade mixtures inside the panel * * @private * @type {SlSelect} */ @query("#mixSelect") private accessor mixSelect: SlSelect; /** * input for the volume of premade mixtures * * @private * @type {SlInput} */ @query("#mixVolAmount") private accessor mixVolAmount: SlInput; /** * shoelace drawer containing diagramm containers * * @private * @type {SlDrawer} */ @query("#diagramDrawer") private accessor diagramDrawer: SlDrawer; /** * main container inside diagram drawer for all diagrams and headings * * @private * @type {HTMLDivElement} */ @query("#canvasContainer") private accessor canvasContainer: HTMLDivElement; /** * part of options, toggles different panel variants * * @private * @type {SlRadioGroup} */ @query("#panelVariantOptions") private accessor panelVariantOptions: SlRadioGroup; /** * part of the tab panels split panel element, contains element overview and molecule split panel * * @private * @type {SlSplitPanel} */ @query("#atom_panel") private accessor atomPanel: SlSplitPanel; /** * part of the tab panels split panel element, contains molecule carousel and molecule editor * * @private * @type {SlSplitPanel} */ @query("#molecule_panel") private accessor moleculePanel: SlSplitPanel; @query("#smilesSwitch") private accessor smilesSwitch: SlSwitch /** * color palet for pH indication * * @private * @type {object} */ @property({ attribute: false }) private accessor pHColors: object = { // *pride* 0: "#ee1c25", 1: "#f26724", 2: "#f9c511", 3: "#f5ed1c", 4: "#b5d333", 5: "#83c241", 6: "#33a94b", 7: "#33a94b", 8: "#22b46b", 9: "#0bb8b6", 10: "#4690cd", 11: "#3853a4", 12: "#5a51a2", 13: "#63459d", 14: "#462c83", }; /** * dictionary for german translation of all internal keywords * * @private * @type {object} */ @property({ attribute: false }) private accessor dictionary: object = { testtube: "Reagenzglas", beaker: "Becherglas", burette: "Bürette", flask: "Kolben", condenser: "Kühler", separatingfunnel: "Scheidetrichter", cable: "Kabel", electrode: "Elektrode", source: "Spannungsquelle", voltmeter: "Voltmeter", hydrogen: "Wasserstoff", helium: "Helium", lithium: "Lithium", beryllium: "Beryllium", boron: "Bor", carbon: "Kohlenstoff", nitrogen: "Stickstoff", oxygen: "Sauerstoff", fluorine: "Fluor", neon: "Neon", sodium: "Natrium", magnesium: "Magnesium", aluminum: "Aluminium", silicon: "Silizium", phosphorus: "Phosphor", sulfur: "Schwefel", chlorine: "Chlor", argon: "Argon", potassium: "Kalium", calcium: "Calcium", scandium: "Scandium", titanium: "Titan", vanadium: "Vanadium", chromium: "Chrom", manganese: "Mangan", iron: "Eisen", cobalt: "Cobalt", nickel: "Nickel", copper: "Kupfer", zinc: "Zink", gallium: "Gallium", germanium: "Germanium", arsenic: "Arsen", selenium: "Selen", bromine: "Brom", krypton: "Krypton", rubidium: "Rubidium", strontium: "Strontium", yttrium: "Yttrium", zirconium: "Zirkonium", niobium: "Niob", molybdenum: "Molybdän", technetium: "Technetium", ruthenium: "Ruthenium", rhodium: "Rhodium", palladium: "Palladium", silver: "Silber", cadmium: "Cadmium", indium: "Indium", tin: "Zinn", antimony: "Antimon", tellurium: "Tellur", iodine: "Iod", xenon: "Xenon", cesium: "Caesium", barium: "Barium", lanthanum: "Lanthan", cerium: "Cer", praseodymium: "Praseodym", neodymium: "Neodym", promethium: "Promethium", samarium: "Samarium", europium: "Europium", gadolinium: "Gadolinium", terbium: "Terbium", dysprosium: "Dysprosium", holmium: "Holmium", erbium: "Erbium", thulium: "Thulium", ytterbium: "Ytterbium", lutetium: "Lutetium", hafnium: "Hafnium", tantalum: "Tantal", tungsten: "Wolfram", rhenium: "Rhenium", osmium: "Osmium", iridium: "Iridium", platinum: "Platin", gold: "Gold", mercury: "Quecksilber", thallium: "Thallium", lead: "Blei", bismuth: "Wismut", polonium: "Polonium", astatine: "Astat", radon: "Radon", francium: "Francium", radium: "Radium", actinium: "Actinium", thorium: "Thorium", protactinium: "Protactinium", uranium: "Uran", neptunium: "Neptunium", plutonium: "Plutonium", americium: "Americium", curium: "Curium", berkelium: "Berkelium", californium: "Californium", einsteinium: "Einsteinium", fermium: "Fermium", mendelevium: "Mendelevium", nobelium: "Nobelium", lawrencium: "Lawrencium", rutherfordium: "Rutherfordium", dubnium: "Dubnium", seaborgium: "Seaborgium", bohrium: "Bohrium", hassium: "Hassium", meitnerium: "Meitnerium", darmstadtium: "Darmstadtium", roentgenium: "Roentgenium", copernicium: "Copernicium", nihonium: "Nihonium", flerovium: "Flerovium", moscovium: "Moscovium", livermorium: "Livermorium", tennessine: "Tenness", oganesson: "Oganesson", ununennium: "Ununennium", "alkali metal": "Alkalimetall", "alkaline earth metal": "Erdalkalimetall", "transition metal": "Übergangsmetall", "post transition metal": "Metall der Erdmetalle", metalloid: "Halbmetall", "polyatomic nonmetal": "Polyatomares Nichtmetall", "diatomic nonmetal": "Diatomares Nichtmetall", halogen: "Halogen", "noble gas": "Edelgas", lanthanide: "Lanthanoid", actinide: "Actinoid", ionic: "ionische Bindung", acidbase: "Säure/Base Bindung", covalent: "kovalente Bindung" }; /** * reflected data on available molecules * * @private * @type {Array<object>} */ @property({ type: Array<object>, reflect: true, converter: { fromAttribute: (value: string) => { return JSON.parse(value); }, toAttribute: (value: Array<object>) => { return JSON.stringify(value); }, }, hasChanged(newVal: Array<object>, oldVal: Array<object>) { return newVal.length != oldVal.length; }, }) private accessor availableMolecules: Array<object> = [ { formula: "OH2", atoms: ["oxygen", "hydrogen", "hydrogen"], bonds: ["0-1:covalent:1", "0-2:covalent:1"], }, ]; /** * reflected data on available mixtures * * @private * @type {Array<object>} */ @property({ type: Array<object>, reflect: true, converter: { fromAttribute: (value: string) => { return JSON.parse(value); }, toAttribute: (value: Array<object>) => { return JSON.stringify(value); }, }, hasChanged(newVal: Array<object>, oldVal: Array<object>) { return newVal.length != oldVal.length; }, }) private accessor availableMixtures: Array<object> = [ ]; /** * reflected data on available components * * @private * @type {Array<object>} */ @property({ type: Array<object>, reflect: true, converter: { fromAttribute: (value: string) => { return JSON.parse(value); }, toAttribute: (value: Array<object>) => { return JSON.stringify(value); }, }, hasChanged(newVal: Array<object>, oldVal: Array<object>) { return newVal.length != oldVal.length; }, }) private accessor availableComponents: Array<object> = []; /** * list of all sprites of available components * * @private * @type {Array<[PIXI.Sprite, number]>} */ @property({ attribute: false }) private accessor avComponentsSprites: Array<[PIXI.Sprite, number]> = []; /** * bounding rectangle of the main canvas for relative positioning * * @private * @type {DOMRect} */ @property({ attribute: false }) private accessor canvasBoundingRect: DOMRect; /** * list of functions to be iteratively called by the PixiJS ticker system * * @private * @type {Array<any>} */ @property({ attribute: false }) private accessor tickerFunctionList: Array<any> = []; /** * list of current diagrams, contains charts and corresponding div containers * * @private * @type {Array<any>} */ @property({ attribute: false }) private accessor diagramList: Array<any> = []; /** * database to store update intervals for ticker functions * * @private * @type {object} */ @property({ attribute: false }) private accessor tickerTimeObj: object = {}; /** * show diagram lines * * @private * @type {boolean} */ @property({ attribute: false }) private accessor diagShowLine: boolean = false; /** * device has touch abilities * * @private * @type {boolean} */ @property({ attribute: false }) private accessor hasTouch: boolean = false; /** * touch input mode (drag/click) * * @private * @type {boolean} */ @property({ attribute: false }) private accessor dragToggle: boolean = true; /** * simulate mouseover event when clicking once on a component sprite (for touch use) * * @private * @type {boolean} */ @property({ attribute: false }) private accessor touchHighlight: boolean = false; /** * show colored element symbols * * @private * @type {number} */ @property({ attribute: false }) private accessor useColoredElements: number = 1; /** * currently dragged sprite object * * @private * @type {*} */ @property({ attribute: false }) private accessor dragObject = undefined; /** * current mouse position * * @private * @type {number[]} */ @property({ attribute: false }) private accessor mousePos: number[] = [0, 0]; /** * seperate container element for the PixiJS Application * * @private * @type {*} */ @property({ attribute: false }) private accessor pix_canvas = document.createElement("p_c"); /** * the PixiJS Application * * @private * @type {*} */ @property({ attribute: false }) private accessor app = null; /** * signalizes which datasets (avail. components/e-components/atoms) have already been loaded into their selects * * @private * @type {boolean[]} */ @property({ attribute: false }) private accessor components_signal: boolean[] = [false, false, false]; /** * which e-component info to show in the panel * * @private * @type {boolean[]} */ @property({ attribute: false }) private accessor eCompPanelVisible: boolean[] = [false, false, false]; /** * list of available electrode materials * * @private * @type {{}} */ @property({ attribute: false }) private accessor electrodeMaterials = []; /** * list of all current component objects * * @private * @type {chemlib.ChemComponent[]} */ @property({ attribute: false }) private accessor allChemComponents: chemlib.ChemComponent[] = []; /** * list of all current e-component objects * * @private * @type {chemlib.ElectricComponent[]} */ @property({ attribute: false }) private accessor allEComponents: chemlib.ElectricComponent[] = []; /** * list of currently filtered element categories * * @private * @type {string[]} */ @property({ attribute: false }) private accessor filterCategories: string[] = []; /** * the currently selected component/e-component object * * @private * @type {*} */ @property({ attribute: false }) private accessor curr_obj: any = null; /** * the currently modified molecule object * * @private * @type {chemlib.Molecule} */ @property({ attribute: false }) private accessor curr_mol: chemlib.Molecule = null; /** * the name of the current object * * @private * @type {string} */ @property({ attribute: false }) private accessor curr_name: string = ""; /** * the index of the sprite of the selected object in the PixiJS stage * * @private * @type {number} */ @property({ attribute: false }) private accessor curr_stage_index: number = -1; /** * the current panel variant * * @private * @type {string} */ @property({ type: String, reflect: true }) private accessor panelVariant: string = "1"; /** * if an action is permitted or aborted * * @private * @type {number} */ @property({ attribute: false }) private accessor confirmAction: number = 0; //0-pending,1-true,2-false /** * the random pseudonym of the current session * * @private * @type {string} */ @property({ attribute: false }) private accessor RndName: string = uniqueNamesGenerator({ dictionaries: [adjectives, colors, animals], }); /** * reflected list of disabled compoents/e-components in the student view * * @private * @type {string[]} */ @property({ type: Array<String>, reflect: true, converter: { fromAttribute: (value: string, type: string[]) => { return value.split(","); }, toAttribute: (value: string[], type: string) => { return value.toString(); }, }, }) private accessor invComponents: string[] = []; /** * reflected, if xAPI statements should be sent * * @private * @type {boolean} */ @property({ type: Boolean, reflect: true, converter: { fromAttribute: (value: string, type: boolean) => { if (value === "true") { return true; } return false; }, toAttribute: (value: boolean, type: string) => { if (value) { return "true"; } return "false"; }, }, }) private accessor enablexAPI: boolean = false; /** * reflected, the current xAPI key * * @private * @type {string} */ @property({ type: String, reflect: true }) private accessor xAPIKey: string = ""; /** * SmilesDrawer instance to draw molecules in a 100x100 canvas * * @private * @type {SmilesDrawer} */ @property({attribute: false}) private accessor smilesDrawer100: SmilesDrawer = null /** * SmilesDrawer instance to draw molecules in a 150x150 canvas * * @private * @type {SmilesDrawer} */ @property({attribute: false}) private accessor smilesDrawer150: SmilesDrawer = null /** * wether to use the SmilesDrawer package * * @private * @type {number} */ @property({type: Number, reflect: true}) private accessor newSmilesDrawer: number = 1 /** * Creates an instance of ChemLab. * * @constructor */ constructor() { super(); console.log(SmilesDrawer) this.smilesDrawer100 = new SmilesDrawer.SvgDrawer({ width: 100, height: 100, explicitHydrogens: true, compactDrawing: false, terminalCarbons: true}); this.smilesDrawer150 = new SmilesDrawer.SvgDrawer({ width: 150, height: 150, explicitHydrogens: true, compactDrawing: false }); this.hasTouch = navigator.maxTouchPoints > 0; this.addEventListener("fullscreenchange", () => { this.containerDiv.style.width = this.clientWidth + "px"; this.containerDiv.style.height = this.clientHeight + "px"; this.app.resizeTo = this.containerDiv; this.app.resize(); this.requestUpdate(); }); } /** * early fix for webwriter focus issues * * @static * @type {*} */ static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; /** * all used custom elements, mainly shoelace elements and the webwriter helpOverlay * * @static * @readonly * @type {{ "sl-details": any; "sl-button": any; "sl-icon-button": any; "sl-icon": any; "sl-badge": any; "sl-button-group": any; "sl-tab": any; "sl-tab-group": any; "sl-tab-panel": any; "sl-split-panel": any; "sl-tag": any; "sl-divider": any; ... 20 more ...; "webwriter-helppopup": any; }} */ static get scopedElements() { return { "sl-details": SlDetails, "sl-button": SlButton, "sl-icon-button": SlIconButton, "sl-icon": SlIcon, "sl-badge": SlBadge, "sl-button-group": SlButtonGroup, "sl-tab": SlTab, "sl-tab-group": SlTabGroup, "sl-tab-panel": SlTabPanel, "sl-split-panel": SlSplitPanel, "sl-tag": SlTag, "sl-divider": SlDivider, "sl-range": SlRange, "sl-select": SlSelect, "sl-option": SlOption, "sl-rating": SlRating, "sl-input": SlInput, "sl-menu": SlMenu, "sl-menu-item": SlMenuItem, "sl-menu-label": SlMenuLabel, "sl-alert": SlAlert, "sl-carousel": SlCarousel, "sl-carousel-item": SlCarouselItem, "sl-popup": SlPopup, "sl-card": SlCard, "sl-checkbox": SlCheckbox, "sl-drawer": SlDrawer, "sl-tooltip": SlTooltip, "sl-dialog": SlDialog, "sl-radio": SlRadio, "sl-switch": SlSwitch, "sl-radio-group": SlRadioGroup, "webwriter-helpoverlay": HelpOverlay, "webwriter-helppopup": HelpPopup, }; } connectedCallback(): void { super.connectedCallback(); } /** * loads canvas and initial state of the widget upon loading, initializes ticker * * @protected * @param {(PropertyValueMap<any> | Map<PropertyKey, unknown>)} _changedProperties */ protected firstUpdated( _changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown> ): void { this.addEventListener("contextmenu", (event) => { event.preventDefault(); }); super.firstUpdated(_changedProperties); this._canvas_init(); this.focus(); this.canvasBoundingRect = this.containerDiv.getBoundingClientRect(); if (this.hasTouch) { let changeInput: HTMLButtonElement = this.shadowRoot.getElementById( "changeInput" ) as HTMLButtonElement; let toggleDrag: HTMLButtonElement = this.shadowRoot.getElementById( "toggleDrag" ) as HTMLButtonElement; changeInput.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="4"><path d="M12.566 26.182Q10 27.941 10 32c0 4.06 4.975 11 9.462 11h11.48C35.332 43 38 39.15 38 36.06V23.01a3 3 0 0 0-3-3h-.01A2.99 2.99 0 0 0 32 23"/><path d="M13.981 28.445V8.005a3 3 0 0 1 3.006-2.997a3.014 3.014 0 0 1 3.006 3.015v15.569"/><path stroke-linejoin="round" d="M19.993 23.008v-3.992a3.016 3.016 0 0 1 6.03 0v3.992"/><path stroke-linejoin="round" d="M26 22.716v-2.713a3 3 0 0 1 6 0v3"/></g></svg> `; toggleDrag.style.visibility = "visible"; } setAnimation(this.dialogAlert, "alert.show", { keyframes: [{ opacity: "0" }, { opacity: "1" }], options: { duration: 50, }, }); setAnimation(this.dialogAlert, "alert.hide", { keyframes: [{ opacity: "1" }, { opacity: "0" }], options: { duration: 50, }, }); let bg_sprite = PIXI.Sprite.from(bg_svg); bg_sprite.name = "bg_sprite"; bg_sprite.anchor.set(0); bg_sprite.x = 0; bg_sprite.y = 0; bg_sprite.width = this.containerDiv.clientWidth; bg_sprite.height = this.containerDiv.clientHeight; bg_sprite.eventMode = "none"; this.app.stage.addChild(bg_sprite); this.app.ticker.add(() => { let indexCount: number = 0; while (typeof this.tickerFunctionList[indexCount] != "undefined") { this.tickerFunctionList[indexCount][1].apply( this, this.tickerFunctionList[indexCount][2] ); indexCount += 1; } }); this._pushTickerFunction( this._updateTickerPopup.name, this._updateTickerPopup, [] ); this._pushTickerFunction( this._updateCablePos.name, this._updateCablePos, [] ); this._pushTickerFunction( this._updateSpriteTextures.name, this._updateSpriteTextures, [] ); this._listComponents(); this._listEComponents(); this.invCompSelect.setAttribute( "value", this.invComponents.toString().replace(",", " ") ); this.filterComponents(); this._listMolecules(); this._listMixtures(); this._listMolConfig(); this._listMixConfig(); this._loadComponents(); this._listCompConfig(); this.panelVariantOptions.value = this.panelVariant; this._changePanelVariant(); if (!this.hasAttribute("contenteditable")) { this.shadowRoot.getElementById("teacherContext").remove(); } else { this.shadowRoot.getElementById("studentContext").remove(); this.settings_drawer.innerHTML = "neues Molekül"; this._newMol(); this._listAtoms(); this.moleculePanel.position = 0; this.shadowRoot.getElementById("addToElectrode").style.visibility = "hidden"; this.molAmount.style.visibility = "hidden"; this.molSelect.style.visibility = "hidden"; this.shadowRoot.getElementById("comp_panel").style.display = ""; this.shadowRoot.getElementById("ecomp_panel").style.display = "none"; this.settings_drawer.removeAttribute("disabled"); // this.curr_mol._bondTypes.forEach((key) => { // if(key in this.dictionary && key!= "hydrogen"){ // // @ts-ignore // let bondType: SlOption = this.shadowRoot.createElement( // "sl-option" // ) as SlOption; // bondType.innerHTML = this.dictionary[key]; // bondType.value = key; // this.bond_sel_3.appendChild(bondType); // } // }); } this.smilesSwitch.checked = this.newSmilesDrawer===1 this._sendxAPiStatement("registered", "chemlab"); this._emitAlert( "klicke zum Starten in das Fenster!", "success", true, 10000 ); } /** * puts a new function in the ticker list, creates new diagram if neccessary * * @private * @param {string} name - the name of the function * @param {Function} fn - the function reference * @param {Array<any>} args - additional arguments of the function */ private _pushTickerFunction( name: string, fn: Function, args: Array<any> ): void { this.tickerFunctionList.push([name, fn, args]); this.tickerTimeObj[name] = 0; // console.log(this.tickerTimeObj); if (name.includes("transfer") || name.includes("echem")) { for (let i = 0; i < this.diagramList.length; i++) { if ( (this.diagramList[i][1] as HTMLDivElement).children[0] .id === name ) { return; } } // @ts-ignore let canvasBundle: HTMLDivElement = this.shadowRoot.createElement( "div" ) as HTMLDivElement; canvasBundle.style.borderTopStyle = "solid"; canvasBundle.style.borderTopWidth = "2px"; canvasBundle.style.borderTopColor = "#e5e5e5"; canvasBundle.style.marginBlock = "20px"; // @ts-ignore let canvasDiv: HTMLDivElement = this.shadowRoot.createElement( "div" ) as HTMLDivElement; canvasDiv.style.width = "100%"; canvasDiv.style.maxWidth = "500px"; canvasDiv.style.height = "400px"; // @ts-ignore let canvas: HTMLCanvasElement = this.shadowRoot.createElement( "canvas" ) as HTMLCanvasElement; canvas.id = name; // @ts-ignore let canvasHeading: HTMLHeadingElement = this.shadowRoot.createElement("h5") as HTMLHeadingElement; canvasHeading.innerText = this.diagramList.length + ": " + (name.includes("transfer") ? "Titration" : name.includes("source") ? "Elektrolyse" : "Galvanische Zelle"); canvasDiv.appendChild(canvas); // @ts-ignore let canvasHeadingContainer: HTMLDivElement = this.shadowRoot.createElement("div") as HTMLDivElement; canvasHeadingContainer.style.display = "flex"; canvasHeadingContainer.style.alignItems = "baseline"; // @ts-ignore let trashButton: HTMLButtonElement = this.shadowRoot.createElement( "button" ) as HTMLButtonElement; trashButton.innerHTML += `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16"><path fill="#d10000" d="M2.5 1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1H3v9a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4h.5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1zm3 4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5M8 5a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7A.5.5 0 0 1 8 5m3 .5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 1 0"/></svg>`; trashButton.onclick = (event) => { this.diagramList.forEach((diag) => { if ((diag[1] as HTMLDivElement).children[0].id === name) { this.diagramList.splice(diag, 1); return; } }); canvasBundle.remove(); this._sendxAPiStatement("removed", "diagram-" + name); }; canvasHeadingContainer.appendChild(canvasHeading); canvasHeadingContainer.appendChild(trashButton); canvasBundle.appendChild(canvasHeadingContainer); canvasBundle.appendChild(canvasDiv); this.canvasContainer.appendChild(canvasBundle); let chartLabel: string; let chartTitleX: string; let chartTitleY: string; if (name.includes("transfer")) { chartLabel = "pH"; chartTitleX = "Volumen [ml]"; chartTitleY = "pH Wert"; } if (name.includes("echem")) { if (name.includes("source")) { chartLabel = "n"; chartTitleX = "Zeit [s]"; chartTitleY = "umgesetzte Stoffmenge [mol]"; } if (name.includes("voltmeter")) { chartLabel = "Spannung"; chartTitleX = "Zeit [s]"; chartTitleY = "Zellspannung [V]"; } } let chart = new Chart(canvas, { type: "line", data: { labels: [], datasets: [], }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { beginAtZero: true, title: { text: chartTitleX, display: true, }, grid: { drawOnChartArea: false, }, }, y: { beginAtZero: true, title: { text: chartTitleY, display: true, }, grid: { drawOnChartArea: false, }, }, }, }, }); chart.data.datasets.push({ label: chartLabel, data: [], tension: 0.4, showLine: this.diagShowLine, }); if (name.includes("source")) { chart.data.datasets.push({ label: chartLabel, data: [], tension: 0.4, showLine: this.diagShowLine, }); let electrodes: chemlib.ElectricComponent[] = chemlib.ComponentManager.isCircuit( args[1] as chemlib.ElectricComponent ); chart.data.datasets[0].label = "Stoffmenge " + electrodes[0]._material._atoms[0].getSymbol() + " Ionen [mol]"; chart.data.datasets[1].label = "Stoffmenge " + electrodes[1]._material._atoms[0].getSymbol() + " Ionen [mol]"; } this.diagramList.push([chart, canvasDiv]); this._sendxAPiStatement("created", "diagram-" + name); } } /** * removes a ticker function from the list * * @private * @param {string} name - the name of the function */ private _remTickerFunction(name: string): void { let toRemove: number = -1; for (let i = 0; i < this.tickerFunctionList.length; i++) { if (this.tickerFunctionList[i][0] === name) { toRemove = i; } } if (toRemove > -1) { this.tickerFunctionList.splice(toRemove, 1); } } /** * ticker function to update popup contents regulary * * @private */ private _updateTickerPopup(): void { if ( this.contextPopup.active && (this.shadowRoot.getElementById("contextInfoBox") as HTMLDivElement) .style.visibility === "visible" ) { this.tickerTimeObj[this._updateTickerPopup.name] += 1; // @ts-ignore if ( this.tickerTimeObj[this._updateTickerPopup.name] >= Math.round( 0.5 * chemlib.Simulation._simTimeStep * (this.app as PIXI.Application).ticker.FPS ) ) { this.tickerTimeObj[this._updateTickerPopup.name] = 0; this._updatePopup(); } } } /** * ticker function to update sprite textures according to the components content * * @private */ private _updateSpriteTextures(): void { this.app.stage.children.forEach((sprite: PIXI.Sprite) => { let name: string = sprite.name .split(":")[0] .replace( sprite.name.split(":")[0][0], sprite.name.split(":")[0][0].toUpperCase() ); let id: number = Number.parseInt(sprite.name.split(":")[1]); if (Object.keys(chemlib.ComponentType).includes(name)) { this.allChemComponents.forEach((comp) => { if (comp._type === name.toLowerCase() && comp._id === id) { let tex: undefined; if (comp._content._activeMolecules.length > 0) { if (comp._content._isSolution) { switch (comp._type) { case "beaker": tex = beaker_fl_svg; break; case "testtube": tex = testtube_fl_svg; break; case "flask": tex = flask_fl_svg; break; case "burette": tex = burette_fl_svg; break; default: break; } sprite.texture = PIXI.Texture.from(tex); } else { switch (comp._type) { case "beaker": tex = beaker_fs_svg; break; case "testtube": tex = testtube_fs_svg; break; case "flask": tex = flask_fs_svg; break; case "burette": tex = burette_fs_svg; break; default: break; } sprite.texture = PIXI.Texture.from(tex); } } else { switch (comp._type) { case "beaker": tex = beaker_svg; break; case "testtube": tex = testtube_svg; break; case "flask": tex = flask_svg; break; case "burette": tex = burette_svg; break; default: break; } sprite.texture = PIXI.Texture.from(tex); } } }); } }); } /** * ticker function to update cables and their positions * * @private */ private _updateCablePos(): void { let index0: number; this.app.stage.children.forEach((sprite: PIXI.Sprite) => { index0 = this.app.stage.children.indexOf(sprite); if (sprite.name.includes("cable")) { let id: number = Number.parseInt(sprite.name.split(":")[1]); this.allEComponents.forEach((eComp) => { if ( eComp._type === chemlib.EComponentType.Cable && eComp._id === id ) { let con1: chemlib.ElectricComponent | undefined = eComp._connections[0]; let con2: chemlib.ElectricComponent | undefined = eComp._connections[1]; if ( typeof con1 != "undefined" && typeof con2 != "undefined" ) { let pos1, pos2: number[]; let index1, index2: number; this.app.stage.children.forEach((conSprite) => { if ( conSprite.name.includes( con1._type + ":" + con1._id ) ) { index1 = this.app.stage.children.indexOf( conSprite ); pos1 = [conSprite.x, conSprite.y]; } if ( conSprite.name.includes( con2._type + ":" + con2._id ) ) { index2 = this.app.stage.children.indexOf( conSprite ); pos2 = [conSprite.x, conSprite.y]; } }); let minIndex: number = Math.min( index0, index1, index2 ); if (minIndex != index0) { let buffer: any = this.app.stage.children[minIndex]; this.app.stage.children[minIndex] = sprite; this.app.stage.children[index0] = buffer; } sprite.height = Math.sqrt( Math.pow(pos1[0] - pos2[0], 2) + Math.pow(pos1[1] - pos2[1], 2) ); sprite.x = (pos1[0] + pos2[0]) / 2; sprite.y = (pos1[1] + pos2[1]) / 2; sprite.rotation = Math.atan( (pos1[1] - pos2[1]) / (pos1[0] - pos2[0]) ) + HALF_PI; } else { this._getObject(sprite); this._removeSprite(); } } }); } }); } /** * ticker function for titraton experiments, transfers content to another component dynamically on correct sprite positioning and updates the diagram * * @private * @param {PIXI.Sprite} currSprite - the sprite of the burette * @param {chemlib.ChemComponent} source - the chemlib object of the burette */ private _updateTickerTransfer( currSprite: PIXI.Sprite, source: chemlib.ChemComponent ): void { let keyName = "transfer" + source._type + ":" + source._id + this.app.stage.children.indexOf(currSprite); this.tickerTimeObj[keyName] += 1; if ( this.tickerTimeObj[keyName] >= Math.round( chemlib.Simulation._simTimeStep * (this.app as PIXI.Application).ticker.FPS ) ) { this.tickerTimeObj[keyName] = 0; if (source._content._activeMolecules.length > 0) { let destSprite: PIXI.Sprite = undefined; this.app.stage.children.forEach((sprite) => { if (sprite != currSprite) { if ( sprite.y - sprite.height / 2 > currSprite.y + currSprite.height / 2 && (sprite.x < currSprite.x + currSprite.width / 2 || sprite.x > currSprite.x - currSprite.width / 2) ) { if (typeof destSprite === "undefined") { destSprite = sprite; } else { if (destSprite.y - sprite.y > 0) { destSprite = sprite; } } } } }); let destObj: chemlib.ChemComponent = undefined; this.allChemComponents.forEach((comp) => { if (typeof destSprite != "undefined") { if ( destSprite.name.split(":")[0] === comp._type && Number.parseInt(destSprite.name.split(":")[1]) === comp._id ) { destObj = comp; } } }); if (typeof destObj != "undefined") { if (source._content._activeMolecules.length > 0) { source.transferContents({}, destObj); for (let i = 0; i < this.diagramList.length; i++) { if ( ( this.diagramList[i][1] as HTMLDivElement ).children[0].id.includes( source._type + ":" + source._id ) ) { let chart: Chart = this.diagramList[i][0]; chart.data.labels.push( (chart.data.labels.length > 0 ? Number.parseFloat( chart.data.labels[ chart.data.labels.length - 1 ] as string ) : 0) + source._outVol * 1000 ); chart.data.datasets.forEach((dataset) => { dataset.data.push( chemlib.Simulation.getpH( destObj._content ) ); }); chart.update(); } } } } } } } /** * ticker function for e-chem experiments, checks circuit and updates diagram * * @private * @param {PIXI.Sprite} currSprite - the sprite of the e-component * @param {chemlib.ElectricComponent} source - the e-component object */ private _updateTickerEChem( currSprite: PIXI.Sprite, source: chemlib.ElectricComponent ): void { let keyName = "echem" + source._type + ":" + source._id + this.app.stage.children.indexOf(currSprite); this.tickerTimeObj[keyName] += 1; if ( this.tickerTimeObj[keyName] >= Math.round( chemlib.Simulation._simTimeStep * (this.app as PIXI.Application).ticker.FPS ) ) { this.tickerTimeObj[keyName] = 0; let circuitComps: Array<chemlib.ElectricComponent> = chemlib.ComponentManager.isCircuit(source); if (circuitComps.length != 0) { chemlib.Simulation.sEChem( circuitComps[0], circuitComps[1], circuitComps[2] ); for (let i = 0; i < this.diagramList.length; i++) { if ( ( this.diagramList[i][1] as HTMLDivElement ).children[0].id.includes( source._type + ":" + source._id ) ) { let chart: Chart = this.diagramList[i][0]; chart.data.labels.push( (chart.data.labels.length > 0 ? Number.parseFloat( chart.data.labels[ chart.data.labels.length - 1 ] as string ) : 0) + 1 ); chart.data.datasets.forEach((dataset) => { if ( source._type === chemlib.EComponentType.Voltmeter ) { dataset.data.push(source._visibleVoltage); } else { let datasetId: number = chart.data.datasets.indexOf(dataset); circuitComps[ datasetId ]._connectedMix._activeMolecules.forEach( (mol) => { // console.log( // mol._atomCount, // mol._totalCharge, // mol._atoms[0].getName(), // circuitComps[ // datasetId // ]._material._atoms[0].getName(), // circuitComps[datasetId] // ._connectedMix._molProperties // ); if ( mol._atomCount === 1 && mol._totalCharge != 0 && mol._atoms[0].getName() === circuitComps[ datasetId ]._material._atoms[0].getName() ) { dataset.data.push( circuitComps[datasetId] ._connectedMix ._molProperties[ mol._id + "n" ] ); } } ); } }); chart.update(); } } } } } /** * adds a new component * * @private * @param {boolean} e - compoent is an electric component * @param {string} type - the component type */ private _addComponent(e: boolean, type: string): void { let component: chemlib.ChemComponent | chemlib.ElectricComponent = null; if (!e) { component = chemlib.ComponentManager.newChemComponent( type.toLowerCase() as chemlib.ComponentType ); this.allChemComponents.push(component); } else { component = chemlib.ComponentManager.newElectricComponent( type.toLowerCase() as chemlib.EComponentType ); this.allEComponents.push(component); } this._addSprite(component._type + ":" + component._id); } /** * adds a new sprite to the canvas * * @private * @param {string} info - a string containing the name of the object and its id */ private _addSprite(info: string): void { let spriteHeight: number = 0; let spriteWidth: number = 0; let spriteTexture: any = null; switch (info.split(":")[0]) { case "beaker": spriteHeight = 130; spriteWidth = 100; spriteTexture =