diff --git a/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts b/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts index 34a16cf825..e696fd8584 100644 --- a/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts +++ b/packages/ketcher-core/src/application/editor/MacromoleculesConverter.ts @@ -1,4 +1,5 @@ import { + AmbiguousMonomer, Atom, Bond, FunctionalGroup, @@ -88,14 +89,15 @@ export class MacromoleculesConverter { const attachmentPoint = monomer.monomerItem.attachmentPoints?.[attachmentPointIndex]; const atomIdMap = monomerToAtomIdMap.get(monomer); + const attachmentPointAtomId = + monomer instanceof AmbiguousMonomer ? 0 : attachmentPoint?.attachmentAtom; return { globalAttachmentAtomId: - isNumber(attachmentPoint?.attachmentAtom) && - atomIdMap?.get(attachmentPoint?.attachmentAtom as number), + isNumber(attachmentPointAtomId) && + atomIdMap?.get(attachmentPointAtomId as number), attachmentAtomId: - isNumber(attachmentPoint?.attachmentAtom) && - attachmentPoint?.attachmentAtom, + isNumber(attachmentPointAtomId) && attachmentPointAtomId, attachmentPointNumber, }; } @@ -132,8 +134,16 @@ export class MacromoleculesConverter { monomerMicromolecule.id, new ReSGroup(monomerMicromolecule), ); - - monomer.monomerItem.struct.atoms.forEach((oldAtom, oldAtomId) => { + const monomerAtoms = + monomer instanceof AmbiguousMonomer + ? monomer.monomers[0].monomerItem.struct.atoms + : monomer.monomerItem.struct.atoms; + const monomerBonds = + monomer instanceof AmbiguousMonomer + ? monomer.monomers[0].monomerItem.struct.bonds + : monomer.monomerItem.struct.bonds; + + monomerAtoms.forEach((oldAtom, oldAtomId) => { const { atom, atomId } = this.addMonomerAtomToStruct( oldAtom, monomer, @@ -162,9 +172,10 @@ export class MacromoleculesConverter { ); }, ) || [], + false, ); struct.sGroupForest.insert(monomerMicromolecule); - monomer.monomerItem.struct.bonds.forEach((bond) => { + monomerBonds.forEach((bond) => { const bondClone = bond.clone(); bondClone.begin = atomIdsMap.get(bondClone.begin) as number; bondClone.end = atomIdsMap.get(bondClone.end) as number; @@ -224,10 +235,16 @@ export class MacromoleculesConverter { sgroupToMonomer: Map, ) { const command = new Command(); - const monomerAdditionCommand = drawingEntitiesManager.addMonomer( - monomerMicromolecule.monomer.monomerItem, - monomerMicromolecule.pp as Vec2, - ); + const monomerAdditionCommand = + monomerMicromolecule.monomer instanceof AmbiguousMonomer + ? drawingEntitiesManager.addAmbiguousMonomer( + monomerMicromolecule.monomer.variantMonomerItem, + monomerMicromolecule.monomer.position, + ) + : drawingEntitiesManager.addMonomer( + monomerMicromolecule.monomer.monomerItem, + monomerMicromolecule.pp as Vec2, + ); command.merge(monomerAdditionCommand); sgroupToMonomer.set( monomerMicromolecule, diff --git a/packages/ketcher-core/src/application/render/renderers/sequence/RNASequenceItemRenderer.ts b/packages/ketcher-core/src/application/render/renderers/sequence/RNASequenceItemRenderer.ts index 41d6c19ec7..3315d4f29c 100644 --- a/packages/ketcher-core/src/application/render/renderers/sequence/RNASequenceItemRenderer.ts +++ b/packages/ketcher-core/src/application/render/renderers/sequence/RNASequenceItemRenderer.ts @@ -32,10 +32,8 @@ export abstract class RNASequenceItemRenderer extends BaseSequenceItemRenderer { get symbolToDisplay(): string { return this.node.rnaBase instanceof AmbiguousMonomer - ? this.node.monomer.label - : this.node.monomer.attachmentPointsToBonds.R3?.getAnotherMonomer( - this.node.monomer, - )?.monomerItem?.props.MonomerNaturalAnalogCode || '@'; + ? this.node.rnaBase.label + : this.node.rnaBase.monomerItem?.props.MonomerNaturalAnalogCode || '@'; } protected drawCommonModification(node: Nucleoside | Nucleotide) { diff --git a/packages/ketcher-core/src/application/render/restruct/rebond.ts b/packages/ketcher-core/src/application/render/restruct/rebond.ts index 1519bd3c40..4dfa99fbe8 100644 --- a/packages/ketcher-core/src/application/render/restruct/rebond.ts +++ b/packages/ketcher-core/src/application/render/restruct/rebond.ts @@ -15,6 +15,7 @@ ***************************************************************************/ import { + AmbiguousMonomer, Atom, Bond, FunctionalGroup, @@ -64,7 +65,8 @@ class ReBond extends ReObject { atomId: number, sgroup?: SGroup, ) { - return sgroup instanceof MonomerMicromolecule + return sgroup instanceof MonomerMicromolecule && + !(sgroup.monomer instanceof AmbiguousMonomer) ? (sgroup.getAttachmentAtomId() as number) : sgroup?.isContracted() ? sgroup?.getContractedPosition(struct).atomId diff --git a/packages/ketcher-core/src/domain/entities/sgroup.ts b/packages/ketcher-core/src/domain/entities/sgroup.ts index c10124eed2..b04ff05186 100644 --- a/packages/ketcher-core/src/domain/entities/sgroup.ts +++ b/packages/ketcher-core/src/domain/entities/sgroup.ts @@ -243,14 +243,17 @@ export class SGroup { return this.getConnectionPointsCount(struct) >= 1; } - addAttachmentPoint(attachmentPoint: SGroupAttachmentPoint): void { + addAttachmentPoint( + attachmentPoint: SGroupAttachmentPoint, + validateUniqueness = true, + ): void { const isAttachmentPointAlreadyExist = this.attachmentPoints.some( ({ atomId, leaveAtomId }) => attachmentPoint.atomId === atomId && attachmentPoint.leaveAtomId === leaveAtomId, ); - if (isAttachmentPointAlreadyExist) { + if (isAttachmentPointAlreadyExist && validateUniqueness) { throw new Error( 'The same attachment point cannot be added to an S-group more than once', ); @@ -263,9 +266,10 @@ export class SGroup { attachmentPoints: | ReadonlyArray | SGroupAttachmentPoint[], + validateUniqueness = true, ): void { for (const attachmentPoint of attachmentPoints) { - this.addAttachmentPoint(attachmentPoint); + this.addAttachmentPoint(attachmentPoint, validateUniqueness); } } diff --git a/packages/ketcher-core/src/domain/helpers/monomers.ts b/packages/ketcher-core/src/domain/helpers/monomers.ts index ca40b92c86..bb8a4591e9 100644 --- a/packages/ketcher-core/src/domain/helpers/monomers.ts +++ b/packages/ketcher-core/src/domain/helpers/monomers.ts @@ -207,9 +207,9 @@ export const isRnaBaseVariantMonomer = ( ) => monomer.monomerClass === KetMonomerClass.Base; export function isAmbiguousMonomerLibraryItem( - monomer: MonomerOrAmbiguousType, + monomer?: MonomerOrAmbiguousType, ): monomer is AmbiguousMonomerType { - return Boolean(monomer.isAmbiguous); + return Boolean(monomer && monomer.isAmbiguous); } export function isPeptideOrAmbiguousPeptide( diff --git a/packages/ketcher-macromolecules/src/EditorEvents.tsx b/packages/ketcher-macromolecules/src/EditorEvents.tsx index 5885cc01e7..8bd8db10c2 100644 --- a/packages/ketcher-macromolecules/src/EditorEvents.tsx +++ b/packages/ketcher-macromolecules/src/EditorEvents.tsx @@ -15,12 +15,6 @@ ***************************************************************************/ import { useCallback, useEffect } from 'react'; import { - BondPreviewState, - MonomerPreviewState, - PresetPosition, - PresetPreviewState, - PreviewStyle, - PreviewType, selectEditor, selectEditorActiveTool, selectTool, @@ -33,14 +27,30 @@ import { } from 'components/modal/modalContainer'; import { useAppDispatch, useAppSelector } from 'hooks'; import { debounce } from 'lodash'; -import { Nucleoside, Nucleotide } from 'ketcher-core'; import { + AmbiguousMonomer, + BaseMonomer, + Nucleoside, + Nucleotide, + PolymerBond, +} from 'ketcher-core'; +import { + calculateAmbiguousMonomerPreviewLeft, + calculateAmbiguousMonomerPreviewTop, calculateBondPreviewPosition, calculateMonomerPreviewTop, calculateNucleoElementPreviewTop, } from 'helpers'; import { selectAllPresets } from 'state/rna-builder'; -import { PolymerBond } from 'ketcher-core/dist/domain/entities/PolymerBond'; +import { + AmbiguousMonomerPreviewState, + BondPreviewState, + MonomerPreviewState, + PresetPosition, + PresetPreviewState, + PreviewStyle, + PreviewType, +} from 'state/types'; const noPreviewTools = ['bond-single']; @@ -173,7 +183,27 @@ export const EditorEvents = () => { const cardCoordinates = e.target.getBoundingClientRect(); const left = `${cardCoordinates.left + cardCoordinates.width / 2}px`; const sequenceNode = e.target.__data__?.node; - const monomer = e.target.__data__?.monomer || sequenceNode?.monomer; + const monomer: BaseMonomer | AmbiguousMonomer = + e.target.__data__?.monomer || sequenceNode?.monomer; + + if (monomer instanceof AmbiguousMonomer) { + const ambiguousMonomerPreviewData: AmbiguousMonomerPreviewState = { + type: PreviewType.AmbiguousMonomer, + monomer: monomer.variantMonomerItem, + style: { + left: `${calculateAmbiguousMonomerPreviewLeft( + cardCoordinates.left, + )}px`, + top: calculateAmbiguousMonomerPreviewTop( + monomer.variantMonomerItem, + )(cardCoordinates), + }, + }; + + debouncedShowPreview(ambiguousMonomerPreviewData); + return; + } + const monomerItem = monomer.monomerItem; const attachmentPointsToBonds = { ...monomer.attachmentPointsToBonds }; const isNucleotideOrNucleoside = @@ -193,6 +223,25 @@ export const EditorEvents = () => { sequenceNode.rnaBase.monomerItem, ]; + if (sequenceNode.rnaBase instanceof AmbiguousMonomer) { + const ambiguousMonomerPreviewData: AmbiguousMonomerPreviewState = { + type: PreviewType.AmbiguousMonomer, + monomer: sequenceNode.rnaBase.variantMonomerItem, + presetMonomers: monomers, + style: { + left: `${calculateAmbiguousMonomerPreviewLeft( + cardCoordinates.left, + )}px`, + top: calculateAmbiguousMonomerPreviewTop( + sequenceNode.rnaBase.variantMonomerItem, + )(cardCoordinates), + }, + }; + + debouncedShowPreview(ambiguousMonomerPreviewData); + return; + } + const existingPreset = presets.find((preset) => { const presetMonomers = [preset.sugar, preset.base, preset.phosphate]; return monomers.every((monomer, index) => { diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx index 8f64b900f1..889d5bbe40 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx @@ -14,9 +14,12 @@ * limitations under the License. ***************************************************************************/ -import { calculateNucleoElementPreviewTop } from 'helpers'; +import { + calculateAmbiguousMonomerPreviewTop, + calculateNucleoElementPreviewTop, +} from 'helpers'; import { useAppSelector } from 'hooks'; -import { MonomerItemType } from 'ketcher-core'; +import { MonomerItemType, isAmbiguousMonomerLibraryItem } from 'ketcher-core'; import { debounce } from 'lodash'; import React, { ReactElement, useCallback } from 'react'; import { @@ -32,18 +35,17 @@ import { GroupContainerRow, ItemsContainer, } from 'components/monomerLibrary/monomerLibraryGroup/styles'; -import { - PresetPosition, - PresetPreviewState, - PreviewType, - selectEditor, - selectShowPreview, - showPreview, -} from 'state/common'; +import { selectEditor, selectShowPreview, showPreview } from 'state/common'; import { RNAContextMenu } from 'components/contextMenu/RNAContextMenu'; import { CONTEXT_MENU_ID } from 'components/contextMenu/types'; import { useContextMenu } from 'react-contexify'; import { IRnaPreset } from '../RnaBuilder/types'; +import { + AmbiguousMonomerPreviewState, + PresetPosition, + PresetPreviewState, + PreviewType, +} from 'state'; export const RnaPresetGroup = ({ presets, duplicatePreset, editPreset }) => { const activePreset = useAppSelector(selectActivePreset); @@ -143,18 +145,27 @@ export const RnaPresetGroup = ({ presets, duplicatePreset, editPreset }) => { const cardCoordinates = e.currentTarget.getBoundingClientRect(); const style = { left: `${cardCoordinates.left + cardCoordinates.width}px`, - top: preset ? calculateNucleoElementPreviewTop(cardCoordinates) : '', + top: isAmbiguousMonomerLibraryItem(preset.base) + ? calculateAmbiguousMonomerPreviewTop(preset.base)(cardCoordinates) + : calculateNucleoElementPreviewTop(cardCoordinates), transform: 'translate(-100%, 0)', }; - - const previewData: PresetPreviewState = { - type: PreviewType.Preset, - monomers, - name: preset.name, - idtAliases: preset.idtAliases, - position: PresetPosition.Library, - style, - }; + const previewData: PresetPreviewState | AmbiguousMonomerPreviewState = + isAmbiguousMonomerLibraryItem(preset.base) + ? { + type: PreviewType.AmbiguousMonomer, + monomer: preset.base, + presetMonomers: monomers, + style, + } + : { + type: PreviewType.Preset, + monomers, + name: preset.name, + idtAliases: preset.idtAliases, + position: PresetPosition.Library, + style, + }; debouncedShowPreview(previewData); }; // endregion # Preview diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryGroup/MonomerGroup.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryGroup/MonomerGroup.tsx index 35e04cabe9..83ba361b5d 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryGroup/MonomerGroup.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryGroup/MonomerGroup.tsx @@ -14,7 +14,11 @@ * limitations under the License. ***************************************************************************/ import { useCallback } from 'react'; -import { calculateMonomerPreviewTop, EmptyFunction } from 'helpers'; +import { + calculateAmbiguousMonomerPreviewTop, + calculateMonomerPreviewTop, + EmptyFunction, +} from 'helpers'; import { debounce } from 'lodash'; import { MonomerItem } from '../monomerLibraryItem'; import { GroupContainerColumn, GroupTitle, ItemsContainer } from './styles'; @@ -25,14 +29,9 @@ import { MonomerOrAmbiguousType, } from 'ketcher-core'; import { useAppDispatch, useAppSelector } from 'hooks'; -import { - MonomerPreviewState, - PreviewType, - selectEditor, - selectTool, - showPreview, -} from 'state/common'; +import { selectEditor, selectTool, showPreview } from 'state/common'; import { selectGroupItemValidations } from 'state/rna-builder'; +import { PreviewStyle, PreviewType } from 'state'; const MonomerGroup = ({ items, @@ -89,18 +88,30 @@ const MonomerGroup = ({ e: React.MouseEvent, ) => { handleItemMouseLeave(); + const cardCoordinates = e.currentTarget.getBoundingClientRect(); + let style: PreviewStyle; + let previewType: PreviewType; + let top: string; + if (isAmbiguousMonomerLibraryItem(monomer)) { - return; + top = monomer + ? calculateAmbiguousMonomerPreviewTop(monomer)(cardCoordinates) + : ''; + const left = `${cardCoordinates.left + cardCoordinates.width / 2}px`; + previewType = PreviewType.AmbiguousMonomer; + style = { left, top }; + } else { + top = monomer ? calculateMonomerPreviewTop(cardCoordinates) : ''; + style = { right: '-88px', top }; + previewType = PreviewType.Monomer; } - const cardCoordinates = e.currentTarget.getBoundingClientRect(); - const top = monomer ? calculateMonomerPreviewTop(cardCoordinates) : ''; - const style = { right: '-88px', top }; - const previewData: MonomerPreviewState = { - type: PreviewType.Monomer, + const previewData = { + type: previewType, monomer, style, }; + debouncedShowPreview(previewData); }; diff --git a/packages/ketcher-macromolecules/src/components/preview/Preview.tsx b/packages/ketcher-macromolecules/src/components/preview/Preview.tsx index 9b278c9b1d..4c0193bfb1 100644 --- a/packages/ketcher-macromolecules/src/components/preview/Preview.tsx +++ b/packages/ketcher-macromolecules/src/components/preview/Preview.tsx @@ -14,10 +14,12 @@ * limitations under the License. ***************************************************************************/ import { useAppSelector } from 'hooks'; -import { PreviewType, selectShowPreview } from 'state/common'; +import { selectShowPreview } from 'state/common'; import MonomerPreview from './components/MonomerPreview/MonomerPreview'; import PresetPreview from './components/PresetPreview/PresetPreview'; import BondPreview from './components/BondPreview/BondPreview'; +import AmbiguousMonomerPreview from './components/AmbiguousMonomerPreview/AmbiguousMonomerPreview'; +import { PreviewType } from 'state'; export const Preview = () => { const preview = useAppSelector(selectShowPreview); @@ -33,6 +35,8 @@ export const Preview = () => { return ; case PreviewType.Bond: return ; + case PreviewType.AmbiguousMonomer: + return ; default: return null; } diff --git a/packages/ketcher-macromolecules/src/components/preview/components/AmbiguousMonomerPreview/AmbiguousMonomerPreview.styles.ts b/packages/ketcher-macromolecules/src/components/preview/components/AmbiguousMonomerPreview/AmbiguousMonomerPreview.styles.ts new file mode 100644 index 0000000000..fd7a07f62c --- /dev/null +++ b/packages/ketcher-macromolecules/src/components/preview/components/AmbiguousMonomerPreview/AmbiguousMonomerPreview.styles.ts @@ -0,0 +1,55 @@ +import styled from '@emotion/styled'; + +export const Container = styled.div` + display: flex; + flex-direction: column; + align-items: start; + gap: 16px; + padding: 8px; + background: #ffffff; + border: 1px solid #cad3dd; + border-radius: 4px; + box-shadow: 0px 1px 1px rgba(197, 203, 207, 0.7); +`; + +export const Header = styled.div` + font-weight: 600; +`; + +export const Content = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +`; + +export const ContentLine = styled.div` + height: 24px; + display: flex; + align-items: center; + gap: 8px; + white-space: nowrap; +`; + +interface RatioBarProps { + ratio: number; +} + +export const RatioBar = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 36px; + border-right: 0.5px solid #b4b9d6; + background: ${({ ratio }) => `linear-gradient( + 90deg, + #ffffff 0%, + #ffffff ${100 - ratio}%, + #e7f7ea ${100 - ratio + 0.5}%, + #e7f7ea 100% + )`}; +`; + +export const MonomerName = styled.div` + flex: 1; +`; diff --git a/packages/ketcher-macromolecules/src/components/preview/components/AmbiguousMonomerPreview/AmbiguousMonomerPreview.tsx b/packages/ketcher-macromolecules/src/components/preview/components/AmbiguousMonomerPreview/AmbiguousMonomerPreview.tsx new file mode 100644 index 0000000000..f76fd828bc --- /dev/null +++ b/packages/ketcher-macromolecules/src/components/preview/components/AmbiguousMonomerPreview/AmbiguousMonomerPreview.tsx @@ -0,0 +1,114 @@ +import { useMemo } from 'react'; +import styled from '@emotion/styled'; +import { + Container, + Content, + ContentLine, + Header, + RatioBar, +} from './AmbiguousMonomerPreview.styles'; +// Usage of aliases instead of relative import path causes error in the following import +// because this component used directly from ketcher-react package and there are no such aliases in that package. +// It needs to create shared components library and use it in both packages. +import { AmbiguousMonomerPreviewState } from '../../../../state/types'; + +interface Props { + className?: string; + preview: AmbiguousMonomerPreviewState; +} + +const AmbiguousMonomerPreview = ({ className, preview }: Props) => { + const { monomer, presetMonomers, style } = preview; + + const isAlternatives = monomer.subtype === 'alternatives'; + + const ContainerDynamic = useMemo(() => { + if (!style) { + return styled(Container)``; + } + + return styled(Container)` + top: ${style?.top || ''}; + left: ${style?.left || ''}; + right: ${style?.right || ''}; + transform: ${style.transform || ''}; + `; + }, [style]); + + const header = isAlternatives ? 'Alternatives' : 'Mixed'; + + const aminoAcidFallback = monomer.label === 'X' ? 'Any amino acid' : null; + const baseFallback = monomer.label === 'N' ? 'Any base' : null; + const fallback = aminoAcidFallback || baseFallback; + + const { monomers, options } = monomer; + + const previewData: { monomerName: string; ratio?: number }[] = useMemo(() => { + if (fallback) { + return []; + } + + return monomers.map((monomer) => { + const option = options.find( + (option) => option.templateId === monomer.monomerItem.props.id, + ); + + let monomerName: string; + if (presetMonomers) { + const [sugar, , phosphate] = presetMonomers; + const sugarName = sugar?.label ?? ''; + const phosphateName = phosphate?.label ?? ''; + monomerName = `${sugarName}(${monomer.label})${phosphateName}`; + } else { + monomerName = monomer.monomerItem.props.Name; + } + + return { + monomerName, + ratio: option?.ratio, + }; + }); + }, [fallback, monomers, presetMonomers, options]); + + const preparedPreviewData = useMemo(() => { + const sortedData = previewData.sort((a, b) => { + if (isAlternatives) { + return a.monomerName.localeCompare(b.monomerName); + } else { + if (!a.ratio || !b.ratio) { + return 0; + } + return b.ratio - a.ratio; + } + }); + + return sortedData.slice(0, 5); + }, [previewData, isAlternatives]); + + return ( + +
{header}
+ + {fallback ?? + preparedPreviewData.map((entry) => ( + + {entry.ratio && ( + {entry.ratio}% + )} + {entry.monomerName} + + ))} + +
+ ); +}; + +const StyledPreview = styled(AmbiguousMonomerPreview)` + z-index: 5; + position: absolute; +`; + +export default StyledPreview; diff --git a/packages/ketcher-macromolecules/src/components/preview/components/BondPreview/BondPreview.tsx b/packages/ketcher-macromolecules/src/components/preview/components/BondPreview/BondPreview.tsx index 5cd7720dcc..c4b7263f8f 100644 --- a/packages/ketcher-macromolecules/src/components/preview/components/BondPreview/BondPreview.tsx +++ b/packages/ketcher-macromolecules/src/components/preview/components/BondPreview/BondPreview.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { useMemo } from 'react'; import { useAppSelector } from 'hooks'; -import { BondPreviewState, selectShowPreview } from 'state/common'; +import { selectShowPreview } from 'state/common'; import ConnectionOverview from 'components/shared/ConnectionOverview/ConnectionOverview'; import MonomerOverview from 'components/shared/ConnectionOverview/components/MonomerOverview/MonomerOverview'; import { useAttachmentPoints } from '../../hooks/useAttachmentPoints'; @@ -9,6 +9,7 @@ import { Container } from './BondPreview.styles'; import BondAttachmentPoints from 'components/preview/components/BondAttachmentPoints/BondAttachmentPoints'; import { preview } from '../../../../constants'; import { UsageInMacromolecule } from 'ketcher-core'; +import { BondPreviewState } from 'state'; interface Props { className?: string; diff --git a/packages/ketcher-macromolecules/src/components/preview/components/MonomerPreview/MonomerPreview.tsx b/packages/ketcher-macromolecules/src/components/preview/components/MonomerPreview/MonomerPreview.tsx index 0208329fd6..df008f6367 100644 --- a/packages/ketcher-macromolecules/src/components/preview/components/MonomerPreview/MonomerPreview.tsx +++ b/packages/ketcher-macromolecules/src/components/preview/components/MonomerPreview/MonomerPreview.tsx @@ -22,7 +22,7 @@ import { StyledStructRender, } from './MonomerPreview.styles'; import styled from '@emotion/styled'; -import { MonomerPreviewState, selectShowPreview } from 'state/common'; +import { selectShowPreview } from 'state/common'; import { useAppSelector } from 'hooks'; import { useAttachmentPoints } from '../../hooks/useAttachmentPoints'; import useIDTAliasesTextForMonomer from '../../hooks/useIDTAliasesTextForMonomer'; @@ -31,6 +31,7 @@ import AttachmentPoints from '../AttachmentPoints/AttachmentPoints'; import IDTAliases from '../IDTAliases/IDTAliases'; import { preview } from '../../../../constants'; import { UsageInMacromolecule } from 'ketcher-core'; +import { MonomerPreviewState } from 'state'; interface Props { className?: string; diff --git a/packages/ketcher-macromolecules/src/components/preview/components/PresetPreview/PresetPreview.tsx b/packages/ketcher-macromolecules/src/components/preview/components/PresetPreview/PresetPreview.tsx index 57b0b8b853..5ed2c016e8 100644 --- a/packages/ketcher-macromolecules/src/components/preview/components/PresetPreview/PresetPreview.tsx +++ b/packages/ketcher-macromolecules/src/components/preview/components/PresetPreview/PresetPreview.tsx @@ -24,11 +24,12 @@ import { } from './PresetPreview.styles'; import { preview } from '../../../../constants'; import styled from '@emotion/styled'; -import { PresetPreviewState, selectShowPreview } from 'state/common'; -import { IconName } from 'ketcher-react/dist/components/Icon/types'; +import { selectShowPreview } from 'state/common'; +import { IconName } from 'ketcher-react'; import useIDTAliasesTextForPreset from '../../hooks/useIDTAliasesTextForPreset'; import { useAppSelector } from 'hooks'; import IDTAliases from '../IDTAliases/IDTAliases'; +import { PresetPreviewState } from 'state'; const icons: Extract[] = [ 'sugar', diff --git a/packages/ketcher-macromolecules/src/components/preview/hooks/useIDTAliasesTextForPreset.ts b/packages/ketcher-macromolecules/src/components/preview/hooks/useIDTAliasesTextForPreset.ts index 55a45f9af1..3022820c49 100644 --- a/packages/ketcher-macromolecules/src/components/preview/hooks/useIDTAliasesTextForPreset.ts +++ b/packages/ketcher-macromolecules/src/components/preview/hooks/useIDTAliasesTextForPreset.ts @@ -1,6 +1,7 @@ import { IKetIdtAliases } from 'ketcher-core'; import { useMemo } from 'react'; -import { PresetPosition } from 'state/common'; + +import { PresetPosition } from 'state'; type Props = { presetName: string | undefined; diff --git a/packages/ketcher-macromolecules/src/helpers/calculatePreviewTop.ts b/packages/ketcher-macromolecules/src/helpers/calculatePreviewPosition.ts similarity index 59% rename from packages/ketcher-macromolecules/src/helpers/calculatePreviewTop.ts rename to packages/ketcher-macromolecules/src/helpers/calculatePreviewPosition.ts index b68be082ae..c190111047 100644 --- a/packages/ketcher-macromolecules/src/helpers/calculatePreviewTop.ts +++ b/packages/ketcher-macromolecules/src/helpers/calculatePreviewPosition.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. ***************************************************************************/ -import { PolymerBond } from 'ketcher-core'; +import { AmbiguousMonomerType, PolymerBond, ZoomTool } from 'ketcher-core'; import { preview } from '../constants'; -import { PreviewStyle } from 'state/common'; +import { PreviewStyle } from '../state/types'; import assert from 'assert'; export const calculateMonomerPreviewTop = createCalculatePreviewTopFunction( @@ -24,16 +24,29 @@ export const calculateMonomerPreviewTop = createCalculatePreviewTopFunction( export const calculateNucleoElementPreviewTop = createCalculatePreviewTopFunction(preview.heightForNucleotide); -function calculateTop(target: DOMRect, height: number): number { - return target.top > height + preview.gap + preview.topPadding - ? target.top - preview.gap - height +type CalculatePreviewTopPayload = { left: number; top: number; bottom: number }; + +function calculateTop( + target: CalculatePreviewTopPayload, + height: number, +): number { + const canvasWrapperBoundingClientRect = ZoomTool.instance.canvasWrapper + .node() + ?.getBoundingClientRect(); + const canvasWrapperTopOffset = canvasWrapperBoundingClientRect?.top || 0; + + return target.top - canvasWrapperTopOffset > + height + preview.gap + preview.topPadding + ? target.top - preview.gap - height - preview.topPadding : target.bottom + preview.gap; } function createCalculatePreviewTopFunction( height: number, -): (target?: DOMRect) => string { - return function calculatePreviewTop(target?: DOMRect): string { +): (target?: CalculatePreviewTopPayload) => string { + return function calculatePreviewTop( + target?: CalculatePreviewTopPayload, + ): string { if (!target) { return ''; } @@ -43,6 +56,38 @@ function createCalculatePreviewTopFunction( }; } +const calculateAmbiguousPreviewHeight = (monomersCount: number) => { + const headingHeight = 16; + const monomersHeight = 35 * monomersCount; + return headingHeight + monomersHeight; +}; + +export const calculateAmbiguousMonomerPreviewTop = ( + monomer: AmbiguousMonomerType, +) => { + const shouldHaveOneLine = monomer.label === 'X' || monomer.label === 'N'; + const monomersCount = shouldHaveOneLine ? 1 : monomer.monomers.length; + const monomersCountToUse = Math.min(5, monomersCount); + const height = calculateAmbiguousPreviewHeight(monomersCountToUse); + + return createCalculatePreviewTopFunction(height); +}; + +export function calculateAmbiguousMonomerPreviewLeft(initialLeft: number) { + const canvasWrapperBoundingClientRect = ZoomTool.instance.canvasWrapper + .node() + ?.getBoundingClientRect(); + const PREVIEW_WIDTH = 70; + const canvasWrapperRight = canvasWrapperBoundingClientRect?.right || 0; + const canvasWrapperLeft = canvasWrapperBoundingClientRect?.left || 0; + + return initialLeft + PREVIEW_WIDTH / 2 > canvasWrapperRight + ? canvasWrapperRight - PREVIEW_WIDTH + : initialLeft - PREVIEW_WIDTH / 2 < canvasWrapperLeft + ? canvasWrapperLeft + : initialLeft - PREVIEW_WIDTH / 2; +} + export const calculateBondPreviewPosition = ( bond: PolymerBond, bondCoordinates: DOMRect, diff --git a/packages/ketcher-macromolecules/src/helpers/index.ts b/packages/ketcher-macromolecules/src/helpers/index.ts index edb2555004..f5fee57313 100644 --- a/packages/ketcher-macromolecules/src/helpers/index.ts +++ b/packages/ketcher-macromolecules/src/helpers/index.ts @@ -16,6 +16,6 @@ export {}; export * from './emptyFunction'; -export * from './calculatePreviewTop'; +export * from './calculatePreviewPosition'; export * from './getPreset'; export * from './getConnectedAttachmentPoints'; diff --git a/packages/ketcher-macromolecules/src/state/common/editorSlice.ts b/packages/ketcher-macromolecules/src/state/common/editorSlice.ts index 9f25e0e807..55e42e9bc1 100644 --- a/packages/ketcher-macromolecules/src/state/common/editorSlice.ts +++ b/packages/ketcher-macromolecules/src/state/common/editorSlice.ts @@ -15,66 +15,12 @@ ***************************************************************************/ import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit'; -import { - CoreEditor, - IKetIdtAliases, - MonomerItemType, - PolymerBond, - AttachmentPointsToBonds, -} from 'ketcher-core'; -import { RootState } from 'state'; +import { CoreEditor } from 'ketcher-core'; +import { EditorStatePreview, RootState } from 'state'; +import { PreviewType } from 'state/types'; import { ThemeType } from 'theming/defaultTheme'; import { DeepPartial } from '../../types'; -export enum PreviewType { - Monomer = 'monomer', - Preset = 'preset', - Bond = 'bond', -} - -export interface PreviewStyle { - readonly top?: string; - readonly left?: string; - readonly right?: string; - readonly transform?: string; -} - -interface BasePreviewState { - readonly type: PreviewType; - readonly style?: PreviewStyle; -} - -export interface MonomerPreviewState extends BasePreviewState { - readonly type: PreviewType.Monomer; - readonly monomer: MonomerItemType | undefined; - readonly attachmentPointsToBonds?: AttachmentPointsToBonds; -} - -export enum PresetPosition { - Library = 'library', - ChainStart = 'chainStart', - ChainMiddle = 'chainMiddle', - ChainEnd = 'chainEnd', -} - -export interface PresetPreviewState extends BasePreviewState { - readonly type: PreviewType.Preset; - readonly monomers: ReadonlyArray; - readonly position: PresetPosition; - readonly name?: string; - readonly idtAliases?: IKetIdtAliases; -} - -export interface BondPreviewState extends BasePreviewState { - readonly type: PreviewType.Bond; - readonly polymerBond: PolymerBond; -} - -type EditorStatePreview = - | MonomerPreviewState - | PresetPreviewState - | BondPreviewState; - // TODO: Looks like we do not use `isReady`. Delete? interface EditorState { isReady: boolean | null; diff --git a/packages/ketcher-macromolecules/src/state/index.ts b/packages/ketcher-macromolecules/src/state/index.ts index c20918a74c..47c05e7528 100644 --- a/packages/ketcher-macromolecules/src/state/index.ts +++ b/packages/ketcher-macromolecules/src/state/index.ts @@ -16,3 +16,4 @@ export * from './store'; export * from './rootSaga'; +export * from 'state/types'; diff --git a/packages/ketcher-macromolecules/src/state/types.ts b/packages/ketcher-macromolecules/src/state/types.ts new file mode 100644 index 0000000000..4d5e2855d8 --- /dev/null +++ b/packages/ketcher-macromolecules/src/state/types.ts @@ -0,0 +1,64 @@ +import { + AmbiguousMonomerType, + AttachmentPointsToBonds, + IKetIdtAliases, + MonomerItemType, + PolymerBond, +} from 'ketcher-core'; + +export enum PreviewType { + Monomer = 'monomer', + Preset = 'preset', + Bond = 'bond', + AmbiguousMonomer = 'ambiguousMonomer', +} + +export interface PreviewStyle { + readonly top?: string; + readonly left?: string; + readonly right?: string; + readonly transform?: string; +} + +interface BasePreviewState { + readonly type: PreviewType; + readonly style?: PreviewStyle; +} + +export interface MonomerPreviewState extends BasePreviewState { + readonly type: PreviewType.Monomer; + readonly monomer: MonomerItemType | undefined; + readonly attachmentPointsToBonds?: AttachmentPointsToBonds; +} + +export enum PresetPosition { + Library = 'library', + ChainStart = 'chainStart', + ChainMiddle = 'chainMiddle', + ChainEnd = 'chainEnd', +} + +export interface PresetPreviewState extends BasePreviewState { + readonly type: PreviewType.Preset; + readonly monomers: ReadonlyArray; + readonly position: PresetPosition; + readonly name?: string; + readonly idtAliases?: IKetIdtAliases; +} + +export interface BondPreviewState extends BasePreviewState { + readonly type: PreviewType.Bond; + readonly polymerBond: PolymerBond; +} + +export interface AmbiguousMonomerPreviewState extends BasePreviewState { + readonly type: PreviewType.AmbiguousMonomer; + readonly monomer: AmbiguousMonomerType; + readonly presetMonomers?: ReadonlyArray; +} + +export type EditorStatePreview = + | MonomerPreviewState + | PresetPreviewState + | BondPreviewState + | AmbiguousMonomerPreviewState; diff --git a/packages/ketcher-react/src/script/ui/views/components/StructEditor/InfoPanel.tsx b/packages/ketcher-react/src/script/ui/views/components/StructEditor/InfoPanel.tsx index 385bdbf64e..e7e2084638 100644 --- a/packages/ketcher-react/src/script/ui/views/components/StructEditor/InfoPanel.tsx +++ b/packages/ketcher-react/src/script/ui/views/components/StructEditor/InfoPanel.tsx @@ -22,6 +22,8 @@ import { Struct, SGroup, CoordinateTransformation, + MonomerMicromolecule, + AmbiguousMonomer, } from 'ketcher-core'; import SGroupDataRender from './SGroupDataRender'; @@ -29,8 +31,10 @@ import { functionGroupInfoSelector } from '../../../state/functionalGroups/selec import { connect } from 'react-redux'; import clsx from 'clsx'; import { StructRender } from 'components'; - import classes from './InfoPanel.module.less'; +import { calculateAmbiguousMonomerPreviewTop } from 'src/../../ketcher-macromolecules/src/helpers/calculatePreviewPosition'; +import { PreviewType } from 'src/../../ketcher-macromolecules/src/state/types'; +import AmbiguousMonomerPreview from 'src/../../ketcher-macromolecules/src/components/preview/components/AmbiguousMonomerPreview/AmbiguousMonomerPreview'; const HOVER_PANEL_PADDING = 20; const MAX_INFO_PANEL_SIZE = 200; @@ -129,7 +133,29 @@ const InfoPanel: FC = (props) => { !SGroup.isDataSGroup(sGroup) && !SGroup.isQuerySGroup(sGroup); - return showMolecule ? ( + const isAmbiguousMonomer = + sGroup instanceof MonomerMicromolecule && + sGroup.monomer instanceof AmbiguousMonomer; + + return isAmbiguousMonomer ? ( + + ) : showMolecule ? (