Skip to content

Commit

Permalink
#6027 - Support for single atom properties in macromolecules mode
Browse files Browse the repository at this point in the history
  • Loading branch information
rrodionov91 committed Dec 23, 2024
1 parent 0fb0c60 commit 99ccbbf
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,13 @@ export class MacromoleculesConverter {
monomer,
atomId,
atom.label as AtomLabel,
{
charge: atom.charge,
explicitValence: atom.explicitValence,
isotope: atom.isotope,
radical: atom.radical,
alias: atom.alias,
},
);

command.merge(atomAddCommand);
Expand Down
185 changes: 175 additions & 10 deletions packages/ketcher-core/src/application/render/renderers/AtomRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { Coordinates } from 'application/editor/shared/coordinates';
import { CoreEditor, ZoomTool } from 'application/editor';
import { AtomLabel, ElementColor, Elements } from 'domain/constants';
import { D3SvgElementSelection } from 'application/render/types';
import { VALENCE_MAP } from 'application/render/restruct/constants';

export class AtomRenderer extends BaseRenderer {
private selectionElement?: D3SvgElementSelection<SVGEllipseElement, void>;
private textElement?: D3SvgElementSelection<SVGTextElement, void>;
private radicalElement?: D3SvgElementSelection<SVGGElement, void>;

constructor(public atom: Atom) {
super(atom);
Expand Down Expand Up @@ -58,7 +60,10 @@ export class AtomRenderer extends BaseRenderer {
}

private appendSelectionContour() {
if (this.labelLength < 2 || !this.isLabelVisible) {
if (
(this.labelLength < 2 || !this.isLabelVisible) &&
!this.atom.hasCharge
) {
return this.rootElement
?.insert('circle', ':first-child')
.attr('r', 10)
Expand Down Expand Up @@ -134,6 +139,10 @@ export class AtomRenderer extends BaseRenderer {
return false;
}

public get labelText() {
return this.atom.properties.alias || this.atom.label;
}

public get isLabelVisible() {
const editor = CoreEditor.provideEditorInstance();
const viewModel = editor.viewModel;
Expand All @@ -143,19 +152,41 @@ export class AtomRenderer extends BaseRenderer {
const isAtomTerminal =
!atomNeighborsHalfEdges?.length || atomNeighborsHalfEdges.length === 1;
const isAtomInMiddleOfChain = (atomNeighborsHalfEdges?.length || 0) >= 2;

if (isCarbon && !isAtomTerminal) {
const hasCharge = this.atom.hasCharge;
const hasRadical = this.atom.hasRadical;
const hasAlias = this.atom.properties.alias;
const hasExplicitValence = this.atom.hasExplicitValence;
const hasExplicitIsotope = this.atom.hasExplicitIsotope;

if (
isCarbon &&
!isAtomTerminal &&
!hasCharge &&
!hasRadical &&
!hasAlias &&
!hasExplicitValence &&
!hasExplicitIsotope
) {
return false;
}

if ((isAtomTerminal && visibleTerminal) || isAtomInMiddleOfChain)
if ((isAtomTerminal && visibleTerminal) || isAtomInMiddleOfChain) {
return true;
}

return false;
}

public get labelLength() {
const { hydrogenAmount } = this.atom.calculateValence();
let { hydrogenAmount } = this.atom.calculateValence();

if (this.labelText.length > 1) {
return this.labelText.length;
}

if (!this.shouldDisplayHydrogen) {
hydrogenAmount = 0;
}

if (hydrogenAmount === 0) {
return 1;
Expand All @@ -164,18 +195,54 @@ export class AtomRenderer extends BaseRenderer {
return hydrogenAmount === 1 ? 2 : 3;
}

private get labelColor() {
return this.atom.properties.alias ? 'black' : ElementColor[this.atom.label];
}

public get labelBBoxes() {
// TODO calculate label bboxes after creation of text element
// and store them in atom renderer to optimize performance
if (!this.textElement) {
return [];
}

const labelBboxes: DOMRect[] = [];
const radicalElementBbox = this.radicalElement?.node()?.getBBox();

this.textElement
.selectAll<SVGTSpanElement, this>('tspan')
.each(function (_atomRenderer, tspanIndex, tspans) {
labelBboxes.push(tspans[tspanIndex].getBBox());
});

if (radicalElementBbox) {
labelBboxes.push(radicalElementBbox);
}

return labelBboxes;
}

public get shouldDisplayHydrogen() {
// Remove when rules for displaying hydrogen are implemented same as in molecules mode
return this.atom.label !== AtomLabel.C;
}

private appendLabel() {
if (!this.isLabelVisible) {
return;
}

const { hydrogenAmount } = this.atom.calculateValence();
let { hydrogenAmount } = this.atom.calculateValence();
const shouldHydrogenBeOnLeft = this.shouldHydrogenBeOnLeft;

if (!this.shouldDisplayHydrogen) {
hydrogenAmount = 0;
}

const textElement = this.rootElement
?.append('text')
.attr('y', 5)
.attr('fill', ElementColor[this.atom.label])
.attr('fill', this.labelColor)
.attr(
'style',
'user-select: none; font-family: Arial; letter-spacing: 1.2px;',
Expand All @@ -184,11 +251,17 @@ export class AtomRenderer extends BaseRenderer {
.attr('pointer-events', 'none');

if (!shouldHydrogenBeOnLeft) {
textElement?.append('tspan').text(this.atom.label);
textElement
?.append('tspan')
.attr('dy', this.atom.hasExplicitIsotope ? 4 : 0)
.text(this.atom.properties.alias || this.atom.label);
}

if (hydrogenAmount > 0) {
textElement?.append('tspan').text('H');
textElement
?.append('tspan')
.attr('dy', this.atom.hasExplicitIsotope ? 4 : 0)
.text('H');
}

if (hydrogenAmount > 1) {
Expand All @@ -198,7 +271,7 @@ export class AtomRenderer extends BaseRenderer {
if (shouldHydrogenBeOnLeft) {
textElement
?.append('tspan')
.text(this.atom.label)
.text(this.atom.properties.alias || this.atom.label)
.attr('dy', hydrogenAmount > 1 ? -3 : 0);
}

Expand Down Expand Up @@ -255,10 +328,102 @@ export class AtomRenderer extends BaseRenderer {
this.move();
}

private appendCharge() {
if (this.atom.hasCharge) {
const charge = this.atom.properties.charge as number;

this.textElement
?.append('tspan')
.text(
(Math.abs(charge) > 1 ? Math.abs(charge) : '') +
(charge > 0 ? '+' : '-'),
)
.attr('fill', this.labelColor)
.attr('dy', -4);
}
}

private appendRadical() {
if (!this.atom.hasRadical) {
return;
}

const radical = this.atom.properties.radical;

this.radicalElement = this.rootElement?.append('g');

switch (radical) {
case 1:
this.radicalElement
?.append('circle')
.attr('cx', 3)
.attr('cy', -10)
.attr('r', 2)
.attr('fill', this.labelColor);
this.radicalElement
?.append('circle')
.attr('cx', -3)
.attr('cy', -10)
.attr('r', 2)
.attr('fill', this.labelColor);
break;
case 2:
this.radicalElement
?.append('circle')
.attr('cx', 0)
.attr('cy', -10)
.attr('r', 2)
.attr('fill', this.labelColor);
break;
case 3:
this.radicalElement
?.append('path')
.attr('d', `M 0 -5 L 2 -10 L 4 -5 M -6 -5 L -4 -10 L -2 -5`)
.attr('fill', 'none')
.attr('stroke', this.labelColor)
.attr('stroke-width', 1.4);
break;
}
}

private appendExplicitValence() {
if (this.atom.hasExplicitValence) {
const explicitValence = this.atom.properties.explicitValence as number;

this.textElement
?.append('tspan')
.text(`(${VALENCE_MAP[explicitValence]})`)
.attr('fill', this.labelColor)
.attr('letter-spacing', 0.2)
.attr('dy', -4);
}
}

private appendExplicitIsotope() {
if (this.atom.hasExplicitIsotope) {
const explicitIsotope = this.atom.properties.isotope as number;

this.textElement
?.insert('tspan', ':first-child')
.text(explicitIsotope)
.attr('fill', this.labelColor)
.attr('letter-spacing', 0.2)
.attr('dy', -4);
}
}

private appendAtomProperties() {
this.appendExplicitIsotope();
this.appendCharge();
this.appendRadical();
this.appendExplicitValence();
}

show() {
this.rootElement = this.appendRootElement();
this.bodyElement = this.appendBody();
this.textElement = this.appendLabel();
this.appendAtomProperties();
this.hoverElement = this.appendHover();
this.drawSelection();
}
Expand Down
Loading

0 comments on commit 99ccbbf

Please sign in to comment.