Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 25 additions & 32 deletions packages/ketcher-core/src/application/render/restruct/reatom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ class ReAtom extends ReObject {
const ret = this.makeHoverPlate(render, drawOutline);

render.ctab.addReObjectPath(LayerMap.atom, this.visel, ret);
this.attachHighlightTriggerForAttachmentPointAtom(ret, render);
this.drawHoverForPotentialAttachmentPointAtomsInMonomerCreationWizard(
render,
drawOutline,
Expand All @@ -121,17 +120,16 @@ class ReAtom extends ReObject {
return ret;
}

private attachHighlightTriggerForAttachmentPointAtom(
hoverElement: any,
render: Render,
) {
// The attachment point name this atom belongs to (as AA or LGA), or null.
// Used to sync the Attributes panel highlight with canvas hover.
private getAttachmentPointName(render: Render): AttachmentPointName | null {
if (!render.monomerCreationState) {
return;
return null;
}

const atomId = render.ctab.molecule.atoms.keyOf(this.a);
if (atomId === null) {
return;
return null;
}

const { assignedAttachmentPoints } = render.monomerCreationState;
Expand All @@ -143,31 +141,7 @@ class ReAtom extends ReObject {
return attachmentAtomId === atomId || leavingAtomId === atomId;
});

if (attachmentPointEntry) {
const [attachmentPointName] = attachmentPointEntry;
hoverElement.hover(
() => {
window.dispatchEvent(
new CustomEvent<AttachmentPointName>(
'highlightAttachmentPointControls',
{
detail: attachmentPointName,
},
),
);
},
() => {
window.dispatchEvent(
new CustomEvent<AttachmentPointName>(
'resetHighlightAttachmentPointControls',
{
detail: attachmentPointName,
},
),
);
},
);
}
return attachmentPointEntry ? attachmentPointEntry[0] : null;
}

private drawHoverForPotentialAttachmentPointAtomsInMonomerCreationWizard(
Expand Down Expand Up @@ -217,8 +191,27 @@ class ReAtom extends ReObject {
}

setHover(hover: boolean, render: Render, drawOutline = true) {
const hoverChanged = hover !== this.hover;
super.setHover(hover, render, drawOutline);

// Sync the Attributes panel highlight on actual hover transitions. This is
// driven by the editor's geometric hover tracking rather than DOM events on
// the hover plate, so it stays stable even when the cursor is directly over
// the atom label (which would otherwise steal pointer events from the plate).
if (hoverChanged) {
const attachmentPointName = this.getAttachmentPointName(render);
if (attachmentPointName) {
window.dispatchEvent(
new CustomEvent<AttachmentPointName>(
hover
? 'highlightAttachmentPointControls'
: 'resetHighlightAttachmentPointControls',
{ detail: attachmentPointName },
),
);
}
}

if (!hover || this.selected) {
this.expandedMonomerAttachmentPoints?.hide();

Expand Down
44 changes: 44 additions & 0 deletions packages/ketcher-core/src/application/render/restruct/rebond.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type { RenderOptions, RenderOptionStyles } from '../render.types';
import { isNumber } from 'lodash';
import Visel from './visel';
import { Coordinates } from 'application/editor/shared/coordinates';
import type { AttachmentPointName } from 'domain/types';

class ReBond extends ReObject {
b: Bond;
Expand Down Expand Up @@ -138,6 +139,49 @@ class ReBond extends ReObject {
return ret;
}

setHover(hover: boolean, render: Render, drawOutline = true) {
const hoverChanged = hover !== this.hover;
super.setHover(hover, render, drawOutline);

// Sync the Attributes panel highlight when hovering the bond that connects
// an attachment atom and its leaving group, mirroring the AA/LGA atom hover.
if (hoverChanged) {
const attachmentPointName = this.getAttachmentPointName(render);
if (attachmentPointName) {
window.dispatchEvent(
new CustomEvent<AttachmentPointName>(
hover
? 'highlightAttachmentPointControls'
: 'resetHighlightAttachmentPointControls',
{ detail: attachmentPointName },
),
);
}
}
}

// The attachment point name this bond connects (AA ↔ LGA), or null.
private getAttachmentPointName(render: Render): AttachmentPointName | null {
if (!render.monomerCreationState) {
return null;
}

const { assignedAttachmentPoints } = render.monomerCreationState;
const { begin, end } = this.b;

const attachmentPointEntry = Array.from(
assignedAttachmentPoints.entries(),
).find(([, atomsPair]) => {
const [attachmentAtomId, leavingAtomId] = atomsPair;
return (
(begin === attachmentAtomId && end === leavingAtomId) ||
(begin === leavingAtomId && end === attachmentAtomId)
);
});

return attachmentPointEntry ? attachmentPointEntry[0] : null;
}

getSelectionPoints(render: Render, isHighlight = false) {
// please refer to: ketcher-core/docs/data/hover_selection_1.png
const bond: Bond = this.b;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,35 @@ const AttachmentPoint = ({
const [highlight, setHighlight] = useState(false);

useEffect(() => {
// The same AP is represented on canvas by several elements (the R-label,
// the attachment atom, the leaving-group atom, and the bond between them),
// each dispatching its own enter/leave events. Moving the cursor between
// two of them can deliver a stale "reset" right after a fresh "highlight",
// which would wrongly clear the row. Counting active hovers keeps the row
// highlighted until the cursor has truly left every element of this AP.
let hoverCount = 0;

const handleAttachmentPointHighlight = (event: Event) => {
const attachmentPointName = (event as CustomEvent<AttachmentPointName>)
.detail;
setHighlight(attachmentPointName === name);
if (attachmentPointName === name) {
hoverCount += 1;
setHighlight(true);
} else {
// Only one AP is hovered at a time, so another AP's highlight means
// this row is no longer hovered (also recovers any unbalanced count).
hoverCount = 0;
setHighlight(false);
}
};
const handleResetAttachmentPointHighlight = (event: Event) => {
const attachmentPointName = (event as CustomEvent<AttachmentPointName>)
.detail;
if (attachmentPointName === name) {
setHighlight(false);
hoverCount = Math.max(0, hoverCount - 1);
if (hoverCount === 0) {
setHighlight(false);
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,29 @@ const ReadonlyAttachmentPoint = ({

// Canvas hover → panel highlight
useEffect(() => {
// Several canvas elements can represent the same AP, each dispatching its
// own enter/leave events. Counting active hovers keeps the row highlighted
// until the cursor has left every element of this AP, so a stale "reset"
// arriving after a fresh "highlight" cannot wrongly clear it.
let hoverCount = 0;

const handleHighlight = (event: Event) => {
const apName = (event as CustomEvent<AttachmentPointName>).detail;
setHighlight(apName === name);
if (apName === name) {
hoverCount += 1;
setHighlight(true);
} else {
hoverCount = 0;
setHighlight(false);
}
};
const handleReset = (event: Event) => {
const apName = (event as CustomEvent<AttachmentPointName>).detail;
if (apName === name) {
setHighlight(false);
hoverCount = Math.max(0, hoverCount - 1);
if (hoverCount === 0) {
setHighlight(false);
}
}
};

Expand Down
Loading