diff --git a/src/SmilesBlock.ts b/src/SmilesBlock.ts index 331ad22..b8e2f90 100644 --- a/src/SmilesBlock.ts +++ b/src/SmilesBlock.ts @@ -43,9 +43,6 @@ export class SmilesBlock extends MarkdownRenderChild { const cell = table.createDiv({ cls: 'chem-cell' }); const svgcell = cell.createSvg('svg'); this.renderCell(row, svgcell); - - // option1: keep the original size, according to the max - // option2: resize and limiting this if (parseFloat(svgcell.style.width) > maxWidth) svgcell.style.width = `${maxWidth.toString()}px`; }); @@ -67,7 +64,8 @@ export class SmilesBlock extends MarkdownRenderChild { : this.settings.lightTheme ); }); - console.log(this.settings.options.scale); + if (this.settings.options.scale == 0) + target.style.width = `${this.settings.imgWidth}px`; }; async onload() { diff --git a/src/blocks.ts b/src/blocks.ts index f220aee..7abd474 100644 --- a/src/blocks.ts +++ b/src/blocks.ts @@ -23,3 +23,9 @@ export const refreshBlocks = () => { block.render(); }); }; + +export const clearBlocks = () => { + gBlocks.forEach((block) => { + removeBlock(block); + }); +}; diff --git a/src/drawer.ts b/src/drawer.ts index 69431dd..d530a2b 100644 --- a/src/drawer.ts +++ b/src/drawer.ts @@ -6,3 +6,7 @@ export let gDrawer = new SmilesDrawer.SvgDrawer(DEFAULT_SD_OPTIONS); export const setDrawer = (options: Partial) => { gDrawer = new SmilesDrawer.SvgDrawer({ ...DEFAULT_SD_OPTIONS, ...options }); }; + +export const clearDrawer = () => { + gDrawer = {}; +}; diff --git a/src/main.ts b/src/main.ts index cbfeba7..a2f087d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,9 +2,9 @@ import { Plugin, MarkdownPostProcessorContext } from 'obsidian'; import { DEFAULT_SETTINGS, ChemPluginSettings } from './settings/base'; import { ChemSettingTab } from './settings/SettingTab'; import { SmilesBlock } from './SmilesBlock'; -import { setBlocks } from './blocks'; -import { setDrawer } from './drawer'; +import { setBlocks, clearBlocks } from './blocks'; +import { setDrawer, clearDrawer } from './drawer'; import { setObserver, detachObserver } from './themeObserver'; export default class ChemPlugin extends Plugin { @@ -29,6 +29,8 @@ export default class ChemPlugin extends Plugin { async onunload() { detachObserver(); + clearBlocks(); + clearDrawer(); } async loadSettings() { diff --git a/src/settings/LivePreview.ts b/src/settings/LivePreview.ts new file mode 100644 index 0000000..2a55156 --- /dev/null +++ b/src/settings/LivePreview.ts @@ -0,0 +1,91 @@ +import { ChemPluginSettings } from '../settings/base'; + +import SmilesDrawer from 'smiles-drawer'; +import { gDrawer } from '../drawer'; + +/** + * Refer to plugin abcjs + * This class abstraction is needed to support load/unload hooks + * "If your post processor requires lifecycle management, for example, to clear an interval, kill a subprocess, etc when this element is removed from the app..." + * https://marcus.se.net/obsidian-plugin-docs/reference/typescript/interfaces/MarkdownPostProcessorContext#addchild + */ +export class LivePreview { + container: HTMLDivElement; + lightCard: HTMLDivElement; + darkCard: HTMLDivElement; + settings: ChemPluginSettings; + + constructor( + private readonly el: HTMLElement, + private readonly argSettings: ChemPluginSettings + ) { + this.container = this.el.createEl('div'); + this.container.style.display = `flex`; + this.container.style.flexWrap = `wrap`; + this.container.style.justifyContent = `center`; + + this.lightCard = this.container.createEl('div', { + cls: 'chemcard theme-light', + }); + this.darkCard = this.container.createEl('div', { + cls: 'chemcard theme-dark', + }); + + this.settings = this.argSettings; + } + + render = () => { + this.lightCard.empty(); + const lightWidth = this.renderCell( + this.settings.sample1, + this.lightCard, + this.settings.lightTheme + ); + + this.darkCard.empty(); + const darkWidth = this.renderCell( + this.settings.sample2, + this.darkCard, + this.settings.darkTheme + ); + + if (this.settings.options.scale == 0) + this.container.style.gridTemplateColumns = `repeat(auto-fill, minmax(${ + this.settings?.imgWidth ?? '300' + }px, 1fr)`; + else + this.container.style.gridTemplateColumns = `repeat(auto-fill, minmax(${(lightWidth > + darkWidth + ? lightWidth + : darkWidth + ).toString()}px, 1fr)`; + }; + + updateSettings = (argSettings: ChemPluginSettings) => { + this.settings = argSettings; + }; + + private renderCell = ( + source: string, + target: HTMLElement, + style: string + ) => { + const svg = target.createSvg('svg') as SVGSVGElement; + SmilesDrawer.parse(source, (tree: object) => { + gDrawer.draw(tree, svg, style); + }); + if (this.settings.options.scale == 0) + svg.style.width = `${this.settings?.imgWidth ?? '300'}px`; + else if ( + parseFloat(svg.style.width) > (this.settings.options?.width ?? 300) + ) { + svg.style.width = `${( + this.settings.options?.width ?? 300 + ).toString()}px`; + svg.style.height = `${( + this.settings.options?.height ?? 300 + ).toString()}px`; + } + return parseFloat(svg.style.width); + }; +} diff --git a/src/settings/SettingTab.ts b/src/settings/SettingTab.ts index d75fa98..4d896f5 100644 --- a/src/settings/SettingTab.ts +++ b/src/settings/SettingTab.ts @@ -1,10 +1,16 @@ import { App, PluginSettingTab, Setting, SliderComponent } from 'obsidian'; import ChemPlugin from '../main'; -import { DEFAULT_SD_OPTIONS, SAMPLE_SMILES, themeList } from './base'; -import { gDrawer, setDrawer } from 'src/drawer'; +import { + DEFAULT_SD_OPTIONS, + SAMPLE_SMILES_1, + SAMPLE_SMILES_2, + themeList, +} from './base'; + +import { setDrawer } from 'src/drawer'; import { refreshBlocks } from 'src/blocks'; -import SmilesDrawer from 'smiles-drawer'; +import { LivePreview } from './LivePreview'; //Reference: https://smilesdrawer.surge.sh/playground.html @@ -21,32 +27,53 @@ export class ChemSettingTab extends PluginSettingTab { containerEl.empty(); - // TODO: Check styling instructions and remove this - containerEl.createEl('h2', { text: 'Style Preferences' }); - - // ban this - new Setting(containerEl) - .setName('Image Width') - .setDesc('Adjust the width of the molecule image.') - .addText((text) => - text - .setValue( - this.plugin.settings.options?.width?.toString() ?? '300' - ) - .setPlaceholder('300') - .onChange(async (value) => { - if (value == '') { - value = '300'; - } - this.plugin.settings.options.width = parseInt(value); - this.plugin.settings.options.height = parseInt(value); + const scaleSetting = new Setting(containerEl) + .setName('Scale') + .setDesc( + 'Adjust the global molecule scale. Set to zero to unify image widths, otherwise the structures will share the same bond length.' + ) + .addExtraButton((button) => { + button + .setIcon('rotate-ccw') + .setTooltip('Restore default') + .onClick(async () => { + this.plugin.settings.options.scale = 1; + scaleSlider.setValue(50); await this.plugin.saveSettings(); - onOptionsChange(); - }) - ); + setDrawer({ + ...DEFAULT_SD_OPTIONS, + ...this.plugin.settings.options, + }); + onSettingsChange(); + unifyBondLength(); + }); + }); + + const scaleLabel = scaleSetting.controlEl.createDiv('slider-readout'); + scaleLabel.setText( + (this.plugin.settings.options.scale ?? 1.0).toFixed(2).toString() + ); + + const scaleSlider = new SliderComponent(scaleSetting.controlEl) + .setValue(50 * (this.plugin.settings.options.scale ?? 1.0)) + .setLimits(0.0, 100, 0.5) + .onChange(async (value) => { + this.plugin.settings.options.scale = value / 50; + scaleLabel.setText((value / 50).toFixed(2).toString()); + await this.plugin.saveSettings(); + setDrawer({ + ...DEFAULT_SD_OPTIONS, + ...this.plugin.settings.options, + }); + onSettingsChange(); + if (value == 0) unifyImageWidth(); + else unifyBondLength(); + }); + + const widthSettings = new Setting(containerEl); new Setting(containerEl) - .setName('Light Theme') + .setName('Light theme') .setDesc('Active when Obsidian is under light mode.') .addDropdown((dropdown) => dropdown @@ -55,12 +82,12 @@ export class ChemSettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.lightTheme = value; await this.plugin.saveSettings(); - onLightStyleChange(value); + onSettingsChange(); }) ); new Setting(containerEl) - .setName('Dark Theme') + .setName('Dark theme') .setDesc('Active when Obsidian is under dark mode.') .addDropdown((dropdown) => dropdown @@ -69,173 +96,145 @@ export class ChemSettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.darkTheme = value; await this.plugin.saveSettings(); - onDarkStyleChange(value); + onSettingsChange(); }) ); - containerEl.createEl('h2', { text: 'Live Preview' }); + new Setting(containerEl).setName('Live Preview').setHeading(); new Setting(containerEl) - .setName('Sample Smiles') + .setName('Sample SMILES strings') .setDesc('Input smiles strings to see the styled structure.') .addText((text) => text - .setPlaceholder(SAMPLE_SMILES) - .setValue(this.plugin.settings.sample) + .setPlaceholder(SAMPLE_SMILES_1) + .setValue(this.plugin.settings.sample1) + .onChange(async (value) => { + if (value == '') { + value = SAMPLE_SMILES_1; + } + this.plugin.settings.sample1 = value; + await this.plugin.saveSettings(); + onSettingsChange(); + }) + ) + .addText((text) => + text + .setPlaceholder(SAMPLE_SMILES_2) + .setValue(this.plugin.settings.sample2) .onChange(async (value) => { if (value == '') { - value = SAMPLE_SMILES; + value = SAMPLE_SMILES_2; } - this.plugin.settings.sample = value; + this.plugin.settings.sample2 = value; await this.plugin.saveSettings(); - onSampleChange(value); + onSettingsChange(); }) ); - const div = containerEl.createEl('div'); - div.style.display = 'grid'; - div.style.gridTemplateColumns = `repeat(auto-fill, minmax(${this.plugin.settings.width}px, 1fr)`; + const preview = new LivePreview(containerEl, this.plugin.settings); - const lightCard = div.createEl('div', { cls: 'chemcard theme-light' }); - const darkCard = div.createEl('div', { cls: 'chemcard theme-dark' }); + new Setting(containerEl).setName('Advanced Settings').setHeading(); new Setting(containerEl) - .setName('Advanced Settings') - .setDesc('Configure smiles drawer options.') - .setHeading(); - - const scaleSetting = new Setting(containerEl) - .setName('Scale') - .setDesc('Adjust the global molecule scale.') - // for reset - .addExtraButton((button) => { - button - .setIcon('rotate-ccw') - .setTooltip('Restore default') - .onClick(async () => { - this.plugin.settings.options.scale = 1; - scaleSlider.setValue(50); + .setName('Compact drawing') + .setDesc('Linearize simple structures and functional groups.') + .addToggle((toggle) => + toggle + .setValue( + this.plugin.settings.options?.compactDrawing ?? false + ) + .onChange(async (value) => { + this.plugin.settings.options.compactDrawing = value; await this.plugin.saveSettings(); - onOptionsChange(); - }); - }); - - const scaleLabel = scaleSetting.controlEl.createDiv('slider-readout'); - scaleLabel.setText( - (this.plugin.settings.options.scale ?? 1.0).toFixed(2).toString() - ); - - const scaleSlider = new SliderComponent(scaleSetting.controlEl) - .setValue(50 * (this.plugin.settings.options.scale ?? 1.0)) - .setLimits(0.0, 100, 0.5) - .onChange(async (value) => { - this.plugin.settings.options.scale = value / 50; - scaleLabel.setText((value / 50).toFixed(2).toString()); - await this.plugin.saveSettings(); - onOptionsChange(); - }); + setDrawer({ + ...DEFAULT_SD_OPTIONS, + ...this.plugin.settings.options, + }); + onSettingsChange(); + }) + ); new Setting(containerEl) - .setName('Compact Drawing') - .setDesc('Enable to linearize simple structures. (Unrecommanded)') + .setName('Show terminal carbons') + .setDesc('Explictly draw terminal carbons.') .addToggle((toggle) => toggle .setValue( - this.plugin.settings.options.compactDrawing ?? false + this.plugin.settings.options?.terminalCarbons ?? false ) .onChange(async (value) => { - this.plugin.settings.options.compactDrawing = value; + this.plugin.settings.options.terminalCarbons = value; await this.plugin.saveSettings(); - onOptionsChange(); + setDrawer({ + ...DEFAULT_SD_OPTIONS, + ...this.plugin.settings.options, + }); + onSettingsChange(); }) ); - const onOptionsChange = () => { - setDrawer({ - ...DEFAULT_SD_OPTIONS, - ...this.plugin.settings.options, - }); - - lightCard.empty(); - const lightSvg = lightCard.createSvg('svg'); - SmilesDrawer.parse(this.plugin.settings.sample, (tree: object) => { - gDrawer.draw(tree, lightSvg, this.plugin.settings.lightTheme); - }); - - darkCard.empty(); - const darkSvg = darkCard.createSvg('svg'); - SmilesDrawer.parse(this.plugin.settings.sample, (tree: object) => { - gDrawer.draw(tree, darkSvg, this.plugin.settings.darkTheme); - }); - }; - - const onLightStyleChange = (style: string) => { - lightCard.empty(); - const lightSvg = lightCard.createSvg('svg'); - SmilesDrawer.parse(this.plugin.settings.sample, (tree: object) => { - gDrawer.draw(tree, lightSvg, style); - }); + const onSettingsChange = () => { + preview.updateSettings(this.plugin.settings); + preview.render(); }; - const onDarkStyleChange = (style: string) => { - darkCard.empty(); - const darkSvg = darkCard.createSvg('svg'); - SmilesDrawer.parse(this.plugin.settings.sample, (tree: object) => { - gDrawer.draw(tree, darkSvg, style); - }); + const unifyBondLength = () => { + widthSettings.controlEl.empty(); + widthSettings + .setName('Maximum width') + .setDesc( + 'Crop structure images that are too large in a multiline block.' + ) + .addText((text) => + text + .setValue( + this.plugin.settings.options.width?.toString() ?? + '300' + ) + .onChange(async (value) => { + if (value == '') { + value = '300'; + } + this.plugin.settings.options.width = + parseInt(value); + this.plugin.settings.options.height = + parseInt(value); + await this.plugin.saveSettings(); + setDrawer({ + ...DEFAULT_SD_OPTIONS, + ...this.plugin.settings.options, + }); + onSettingsChange(); + }) + ); }; - const onSampleChange = (example: string) => { - lightCard.empty(); - const lightSvg = lightCard.createSvg('svg'); - SmilesDrawer.parse(example, (tree: object) => { - gDrawer.draw(tree, lightSvg, this.plugin.settings.lightTheme); - }); - - darkCard.empty(); - const darkSvg = darkCard.createSvg('svg'); - SmilesDrawer.parse(example, (tree: object) => { - gDrawer.draw(tree, darkSvg, this.plugin.settings.darkTheme); - }); + const unifyImageWidth = () => { + widthSettings.controlEl.empty(); + widthSettings + .setName('Image width') + .setDesc( + "Adjust the width of the molecule image. Only valid when 'scale' is set to zero." + ) + .addText((text) => { + text.setValue(this.plugin.settings?.imgWidth ?? '300') + .setPlaceholder('300') + .onChange(async (value) => { + if (value == '') { + value = '300'; + } + this.plugin.settings.imgWidth = value; + await this.plugin.saveSettings(); + onSettingsChange(); + }); + }); }; // initialize - const initialize = ( - sample: string, - lightTheme: string, - darkTheme: string - ) => { - lightCard.empty(); - const lightSvg = lightCard.createSvg('svg'); - SmilesDrawer.parse( - sample == '' ? SAMPLE_SMILES : sample, - (tree: object) => { - gDrawer.draw( - tree, - lightSvg, - lightTheme == '' ? 'light' : lightTheme - ); - } - ); - - darkCard.empty(); - const darkSvg = darkCard.createSvg('svg'); - SmilesDrawer.parse( - sample == '' ? SAMPLE_SMILES : sample, - (tree: object) => { - gDrawer.draw( - tree, - darkSvg, - darkTheme == '' ? 'dark' : darkTheme - ); - } - ); - }; - initialize( - this.plugin.settings.sample, - this.plugin.settings.lightTheme, - this.plugin.settings.darkTheme - ); + preview.render(); + if ((this.plugin.settings.options?.scale ?? 1) == 0) unifyImageWidth(); + else unifyBondLength(); } hide(): void { diff --git a/src/settings/base.ts b/src/settings/base.ts index 3079758..f787a96 100644 --- a/src/settings/base.ts +++ b/src/settings/base.ts @@ -1,24 +1,29 @@ // Global consts -export const SAMPLE_SMILES = 'CC(=O)NC1=C-C=C-C=C1-C(=O)O'; +export const SAMPLE_SMILES_1 = 'OC(=O)C(C)=CC1=CC=CC=C1'; +export const SAMPLE_SMILES_2 = + 'OC(C(=O)O[C@H]1C[N+]2(CCCOC3=CC=CC=C3)CCC1CC2)(C1=CC=CS1)C1=CC=CS1'; // Plugin settings export interface ChemPluginSettings { - width: string; darkTheme: string; lightTheme: string; - sample: string; + sample1: string; + sample2: string; + imgWidth: string; options: Partial; } export const DEFAULT_SETTINGS: ChemPluginSettings = { - width: '300', darkTheme: 'dark', lightTheme: 'light', - sample: SAMPLE_SMILES, + sample1: SAMPLE_SMILES_1, + sample2: SAMPLE_SMILES_2, + imgWidth: '300', options: {}, }; // Smiles-drawer options +// Reference: https://smilesdrawer.surge.sh/playground.html export interface SMILES_DRAWER_OPTIONS { width: number; height: number; diff --git a/styles.css b/styles.css index fff21ed..59307ea 100644 --- a/styles.css +++ b/styles.css @@ -12,15 +12,11 @@ If your plugin does not need CSS, delete this file. border-radius: var(--radius-m); border: 1px solid var(--background-modifier-border); margin: 0 10px; - padding: 15px 30px; - display: flex; - flex-direction: column; - flex-grow: 1; - flex-wrap: nowrap; - align-content: center; - align-items: center; - justify-content: center; - align-items: center; + padding: 15px 10px; + display: grid; + place-content: center; + place-items: center; + user-select: none; } .chem-table { @@ -30,6 +26,6 @@ If your plugin does not need CSS, delete this file. } .chem-cell { - padding: 15px 30px; + padding: 15px 10px; user-select: none; } \ No newline at end of file