From 61c355cdf574742c599af4af323b943558ee9048 Mon Sep 17 00:00:00 2001 From: even1024 Date: Wed, 29 Apr 2026 15:11:02 +0200 Subject: [PATCH 01/33] #3604 Add api methods to interact with atoms and bonds in s-groups --- api/c/indigo/indigo.h | 6 + .../indigo/src/indigo_molecule_operations.cpp | 129 ++++++++-- api/python/indigo/indigo/indigo_lib.py | 16 ++ api/python/indigo/indigo/indigo_object.py | 69 +++++ .../ref/basic/3604_sgroup_atoms_bonds.py.out | 59 +++++ .../tests/basic/3604_sgroup_atoms_bonds.py | 236 ++++++++++++++++++ core/indigo-core/molecule/molecule_sgroups.h | 5 +- 7 files changed, 499 insertions(+), 21 deletions(-) create mode 100644 api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out create mode 100644 api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py diff --git a/api/c/indigo/indigo.h b/api/c/indigo/indigo.h index a782015a01..fd4162b83c 100644 --- a/api/c/indigo/indigo.h +++ b/api/c/indigo/indigo.h @@ -614,6 +614,12 @@ CEXPORT int indigoGetSGroupNumCrossBonds(int sgroup); CEXPORT int indigoCreateCrossBonds(int sgroup); CEXPORT int indigoClearSGroupCrossBonds(int sgroup); +// Issue #3604: New SGroup API methods +CEXPORT int indigoAddSGroup(int molecule, const char* type, int extindex); +CEXPORT int indigoSetSGroupAtoms(int sgroup, int natoms, int* atoms); +CEXPORT int indigoSetSGroupBonds(int sgroup, int nbonds, int* bonds); +CEXPORT int indigoIterateSGroupCrossBonds(int sgroup); + CEXPORT int indigoAddSGroupAttachmentPoint(int sgroup, int aidx, int lvidx, const char* apid); CEXPORT int indigoDeleteSGroupAttachmentPoint(int sgroup, int index); // Returns iterator of superatom attachment points (SAP entries) for a superatom S-group. diff --git a/api/c/indigo/src/indigo_molecule_operations.cpp b/api/c/indigo/src/indigo_molecule_operations.cpp index d4dd11cb3b..1b149274c7 100644 --- a/api/c/indigo/src/indigo_molecule_operations.cpp +++ b/api/c/indigo/src/indigo_molecule_operations.cpp @@ -1732,52 +1732,143 @@ CEXPORT int indigoGetSGroupNumCrossBonds(int sgroup) { INDIGO_BEGIN { - Superatom& sup = IndigoSuperatom::cast(self.getObject(sgroup)).get(); - return sup.bonds.size(); + IndigoSGroup& isg = IndigoSGroup::cast(self.getObject(sgroup)); + return isg.get().bonds.size(); } INDIGO_END(-1); } +static void _fillCrossBonds(BaseMolecule& mol, SGroup& sg) +{ + sg.bonds.clear(); + for (auto atom_idx : sg.atoms) + { + const Vertex& vx = mol.getVertex(atom_idx); + for (auto nei_idx = vx.neiBegin(); nei_idx != vx.neiEnd(); nei_idx = vx.neiNext(nei_idx)) + { + if (sg.atoms.find(vx.neiVertex(nei_idx)) == -1) + { + int edge_idx = vx.neiEdge(nei_idx); + if (sg.bonds.find(edge_idx) == -1) + sg.bonds.push(edge_idx); + } + } + } +} + CEXPORT int indigoCreateCrossBonds(int sgroup) { INDIGO_BEGIN { - IndigoSuperatom& isup = IndigoSuperatom::cast(self.getObject(sgroup)); - Superatom& sup = isup.get(); - BaseMolecule& mol = isup.mol; + IndigoSGroup& isg = IndigoSGroup::cast(self.getObject(sgroup)); + _fillCrossBonds(isg.mol, isg.get()); + return 1; + } + INDIGO_END(-1); +} - sup.bonds.clear(); +CEXPORT int indigoClearSGroupCrossBonds(int sgroup) +{ + INDIGO_BEGIN + { + IndigoSGroup& isg = IndigoSGroup::cast(self.getObject(sgroup)); + isg.get().bonds.clear(); + return 1; + } + INDIGO_END(-1); +} - for (auto atom_idx : sup.atoms) +static IndigoObject* _wrapSGroup(BaseMolecule& mol, int idx) +{ + SGroup& sgroup = mol.sgroups.getSGroup(idx); + if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) + return new IndigoSuperatom(mol, idx); + else if (sgroup.sgroup_type == SGroup::SG_TYPE_SRU) + return new IndigoRepeatingUnit(mol, idx); + else if (sgroup.sgroup_type == SGroup::SG_TYPE_MUL) + return new IndigoMultipleGroup(mol, idx); + else if (sgroup.sgroup_type == SGroup::SG_TYPE_DAT) + return new IndigoDataSGroup(mol, idx); + else + return new IndigoGenericSGroup(mol, idx); +} + +CEXPORT int indigoAddSGroup(int molecule, const char* type, int extindex) +{ + INDIGO_BEGIN + { + BaseMolecule& mol = self.getObject(molecule).getBaseMolecule(); + int idx = mol.sgroups.addSGroup(type); + if (idx == -1) + throw IndigoError("indigoAddSGroup: cannot add SGroup of type '%s'", type); + + SGroup& sgroup = mol.sgroups.getSGroup(idx); + if (extindex > 0) + sgroup.original_group = extindex; + + return self.addObject(_wrapSGroup(mol, idx)); + } + INDIGO_END(-1); +} + +CEXPORT int indigoSetSGroupAtoms(int sgroup, int natoms, int* atoms) +{ + INDIGO_BEGIN + { + IndigoSGroup& isg = IndigoSGroup::cast(self.getObject(sgroup)); + SGroup& s = isg.get(); + + s.atoms.clear(); + if (atoms != nullptr && natoms > 0) { - const Vertex& vx = mol.getVertex(atom_idx); - for (auto nei_idx = vx.neiBegin(); nei_idx != vx.neiEnd(); nei_idx = vx.neiNext(nei_idx)) + for (int i = 0; i < natoms; i++) { - if (sup.atoms.find(vx.neiVertex(nei_idx)) == -1) - { - int edge_idx = vx.neiEdge(nei_idx); - if (sup.bonds.find(edge_idx) == -1) - sup.bonds.push(edge_idx); - } + if (atoms[i] < 0 || atoms[i] >= isg.mol.vertexEnd()) + throw IndigoError("indigoSetSGroupAtoms: atom index %d out of range", atoms[i]); + s.atoms.push(atoms[i]); } } - return 1; } INDIGO_END(-1); } -CEXPORT int indigoClearSGroupCrossBonds(int sgroup) +CEXPORT int indigoSetSGroupBonds(int sgroup, int nbonds, int* bonds) { INDIGO_BEGIN { - Superatom& sup = IndigoSuperatom::cast(self.getObject(sgroup)).get(); - sup.bonds.clear(); + IndigoSGroup& isg = IndigoSGroup::cast(self.getObject(sgroup)); + SGroup& s = isg.get(); + + if (s.sgroup_type != SGroup::SG_TYPE_DAT) + throw IndigoError("indigoSetSGroupBonds: only DAT SGroups support explicit bond assignment"); + + s.bonds.clear(); + if (bonds != nullptr && nbonds > 0) + { + for (int i = 0; i < nbonds; i++) + { + if (bonds[i] < 0 || bonds[i] >= isg.mol.edgeEnd()) + throw IndigoError("indigoSetSGroupBonds: bond index %d out of range", bonds[i]); + s.bonds.push(bonds[i]); + } + } return 1; } INDIGO_END(-1); } +CEXPORT int indigoIterateSGroupCrossBonds(int sgroup) +{ + INDIGO_BEGIN + { + IndigoSGroup& isg = IndigoSGroup::cast(self.getObject(sgroup)); + return self.addObject(new IndigoSGroupBondsIter(isg.mol, isg.get())); + } + INDIGO_END(-1); +} + + CEXPORT int indigoAddSGroupAttachmentPoint(int sgroup, int aidx, int lvidx, const char* apid) { INDIGO_BEGIN diff --git a/api/python/indigo/indigo/indigo_lib.py b/api/python/indigo/indigo/indigo_lib.py index fd208cfb31..ae4c0aeb29 100644 --- a/api/python/indigo/indigo/indigo_lib.py +++ b/api/python/indigo/indigo/indigo_lib.py @@ -664,6 +664,22 @@ def __init__(self) -> None: IndigoLib.lib.indigoCreateCrossBonds.argtypes = [c_int] IndigoLib.lib.indigoClearSGroupCrossBonds.restype = c_int IndigoLib.lib.indigoClearSGroupCrossBonds.argtypes = [c_int] + IndigoLib.lib.indigoAddSGroup.restype = c_int + IndigoLib.lib.indigoAddSGroup.argtypes = [c_int, c_char_p, c_int] + IndigoLib.lib.indigoSetSGroupAtoms.restype = c_int + IndigoLib.lib.indigoSetSGroupAtoms.argtypes = [ + c_int, + c_int, + POINTER(c_int), + ] + IndigoLib.lib.indigoSetSGroupBonds.restype = c_int + IndigoLib.lib.indigoSetSGroupBonds.argtypes = [ + c_int, + c_int, + POINTER(c_int), + ] + IndigoLib.lib.indigoIterateSGroupCrossBonds.restype = c_int + IndigoLib.lib.indigoIterateSGroupCrossBonds.argtypes = [c_int] IndigoLib.lib.indigoAddSGroupAttachmentPoint.restype = c_int IndigoLib.lib.indigoAddSGroupAttachmentPoint.argtypes = [ c_int, diff --git a/api/python/indigo/indigo/indigo_object.py b/api/python/indigo/indigo/indigo_object.py index 871ab574c8..120311c4f8 100644 --- a/api/python/indigo/indigo/indigo_object.py +++ b/api/python/indigo/indigo/indigo_object.py @@ -1706,6 +1706,75 @@ def addSuperatom(self, atoms, name): ), ) + def addSGroup(self, sgtype, extindex=0): + """Molecule method adds an empty SGroup + + Args: + sgtype (str): sgroup type (e.g. "SUP", "DAT", "SRU", "MUL", "GEN") + extindex (int): external index; 0 for auto-generation + + Returns: + IndigoObject: SGroup object + """ + + return IndigoObject( + self.session, + IndigoLib.checkResult( + self._lib().indigoAddSGroup( + self.id, sgtype.encode(), extindex + ) + ), + ) + + def setSGroupAtoms(self, atoms): + """SGroup method replaces atoms with the given list + + Args: + atoms (list): atom index list + + Returns: + int: 1 if there are no errors + """ + arr = (c_int * len(atoms))() + for i in range(len(atoms)): + arr[i] = atoms[i] + + return IndigoLib.checkResult( + self._lib().indigoSetSGroupAtoms(self.id, len(arr), arr) + ) + + def setSGroupBonds(self, bonds): + """SGroup method replaces bonds with the given list (DAT only) + + Args: + bonds (list): bond index list + + Returns: + int: 1 if there are no errors + """ + arr = (c_int * len(bonds))() + for i in range(len(bonds)): + arr[i] = bonds[i] + + return IndigoLib.checkResult( + self._lib().indigoSetSGroupBonds(self.id, len(arr), arr) + ) + + def iterateSGroupCrossBonds(self): + """SGroup method iterates cross bonds + + Returns: + IndigoObject: bonds iterator + """ + + return IndigoObject( + self.session, + IndigoLib.checkResult( + self._lib().indigoIterateSGroupCrossBonds(self.id) + ), + self, + ) + def setDataSGroupXY(self, x, y, options=""): """SGroup method sets coordinates diff --git a/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out new file mode 100644 index 0000000000..2059aee215 --- /dev/null +++ b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out @@ -0,0 +1,59 @@ +****** addSGroup: create empty SGroups of each type ******** +SUP type: 2 +SUP atoms: 0 +SUP bonds: 0 +DAT type: 1 +DAT atoms: 0 +DAT bonds: 0 +SRU type: 3 +SRU atoms: 0 +MUL type: 4 +MUL atoms: 0 +GEN type: 0 +GEN atoms: 0 +****** addSGroup: with explicit extindex ******** +extindex: 42 +****** setSGroupAtoms: set atoms on empty SGroup ******** +atoms after set: 3 +atom symbols: C C C +****** setSGroupAtoms: replace existing atoms ******** +atoms after replace: 3 +atom indices: 3 4 5 +****** setSGroupAtoms: clear atoms ******** +atoms after clear: 0 +****** setSGroupAtoms: works for all SGroup types ******** +SUP atoms: 2 +DAT atoms: 2 +SRU atoms: 2 +GEN atoms: 2 +****** setSGroupBonds: set bonds on DAT SGroup ******** +DAT bonds: 2 +****** setSGroupBonds: error on SUP SGroup ******** +Expected error: core: indigoSetSGroupBonds: only DAT SGroups support explicit bond assignment +****** setSGroupBonds: error on SRU SGroup ******** +Expected error: core: indigoSetSGroupBonds: only DAT SGroups support explicit bond assignment +****** iterateSGroupCrossBonds ******** +cross bonds: 0 3 +****** iterateSGroupCrossBonds: empty ******** +cross bonds when all atoms inside: 0 +****** Updated countBonds: DAT returns CBONDS, others return cross bonds ******** +DAT countBonds (CBONDS): 2 +SUP countBonds (cross): 2 +SRU countBonds (cross): 2 +****** createCrossBonds: works for all SGroup types ******** +SUP createCrossBonds count: 2 +SRU createCrossBonds count: 2 +GEN createCrossBonds count: 2 +MUL createCrossBonds count: 2 +****** clearSGroupCrossBonds: works for all SGroup types ******** +SUP before clear: 2 +SUP after clear: 0 +SRU before clear: 2 +SRU after clear: 0 +GEN before clear: 2 +GEN after clear: 0 +****** Molfile roundtrip: SUP with cross bonds ******** +type: 2 +atoms: 3 +bonds: 2 +sgroup count: 1 diff --git a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py new file mode 100644 index 0000000000..25d2a303de --- /dev/null +++ b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py @@ -0,0 +1,236 @@ +""" +Issue #3604: Add API methods to interact with atoms and bonds in S-groups. +https://github.com/epam/Indigo/issues/3604 + +This test covers new and updated SGroup API methods: + - addSGroup(type, extindex) + - setSGroupAtoms(atom_indices) + - setSGroupBonds(bond_indices) (DAT only) + - iterateSGroupCrossBonds() + - Updated iterateBonds/countBonds behavior + - Updated createCrossBonds/clearSGroupCrossBonds for all types +""" +import os +import sys + +sys.path.append( + os.path.normpath( + os.path.join(os.path.abspath(__file__), "..", "..", "..", "common") + ) +) +from env_indigo import Indigo, getIndigoExceptionText, IndigoException # noqa + +indigo = Indigo() +indigo.setOption("molfile-saving-skip-date", True) + + +# ===== addSGroup ===== + +print("****** addSGroup: create empty SGroups of each type ********") + +mol = indigo.loadMolecule("CCCCCC") + +sg_sup = mol.addSGroup("SUP", 0) +print("SUP type: {0}".format(sg_sup.getSGroupType())) +print("SUP atoms: {0}".format(sg_sup.countAtoms())) +print("SUP bonds: {0}".format(sg_sup.countBonds())) + +sg_dat = mol.addSGroup("DAT", 0) +print("DAT type: {0}".format(sg_dat.getSGroupType())) +print("DAT atoms: {0}".format(sg_dat.countAtoms())) +print("DAT bonds: {0}".format(sg_dat.countBonds())) + +sg_sru = mol.addSGroup("SRU", 0) +print("SRU type: {0}".format(sg_sru.getSGroupType())) +print("SRU atoms: {0}".format(sg_sru.countAtoms())) + +sg_mul = mol.addSGroup("MUL", 0) +print("MUL type: {0}".format(sg_mul.getSGroupType())) +print("MUL atoms: {0}".format(sg_mul.countAtoms())) + +sg_gen = mol.addSGroup("GEN", 0) +print("GEN type: {0}".format(sg_gen.getSGroupType())) +print("GEN atoms: {0}".format(sg_gen.countAtoms())) + +print("****** addSGroup: with explicit extindex ********") + +mol2 = indigo.loadMolecule("CCCCCC") +sg = mol2.addSGroup("SUP", 42) +print("extindex: {0}".format(sg.getSGroupOriginalId())) + + +# ===== setSGroupAtoms ===== + +print("****** setSGroupAtoms: set atoms on empty SGroup ********") + +mol3 = indigo.loadMolecule("CCCCCC") +sg = mol3.addSGroup("SUP", 0) +sg.setSGroupAtoms([0, 1, 2]) +print("atoms after set: {0}".format(sg.countAtoms())) +atoms = [] +for a in sg.iterateAtoms(): + atoms.append(a.symbol()) +print("atom symbols: {0}".format(" ".join(atoms))) + +print("****** setSGroupAtoms: replace existing atoms ********") + +sg.setSGroupAtoms([3, 4, 5]) +print("atoms after replace: {0}".format(sg.countAtoms())) +indices = [] +for a in sg.iterateAtoms(): + indices.append(str(a.index())) +print("atom indices: {0}".format(" ".join(indices))) + +print("****** setSGroupAtoms: clear atoms ********") + +sg.setSGroupAtoms([]) +print("atoms after clear: {0}".format(sg.countAtoms())) + +print("****** setSGroupAtoms: works for all SGroup types ********") + +mol4 = indigo.loadMolecule("CCCCCC") +for stype in ["SUP", "DAT", "SRU", "GEN"]: + sg = mol4.addSGroup(stype, 0) + sg.setSGroupAtoms([0, 1]) + print("{0} atoms: {1}".format(stype, sg.countAtoms())) + + +# ===== setSGroupBonds ===== + +print("****** setSGroupBonds: set bonds on DAT SGroup ********") + +mol5 = indigo.loadMolecule("CCCCCC") +sg = mol5.addSGroup("DAT", 0) +sg.setSGroupAtoms([0, 1, 2]) +sg.setSGroupBonds([0, 1]) +print("DAT bonds: {0}".format(sg.countBonds())) + +print("****** setSGroupBonds: error on SUP SGroup ********") + +mol6 = indigo.loadMolecule("CCCCCC") +sg = mol6.addSGroup("SUP", 0) +sg.setSGroupAtoms([0, 1, 2]) +try: + sg.setSGroupBonds([0, 1]) + print("ERROR: should have raised exception") +except IndigoException as e: + print("Expected error: {0}".format(getIndigoExceptionText(e))) + +print("****** setSGroupBonds: error on SRU SGroup ********") + +mol7 = indigo.loadMolecule("CCCCCC") +sg = mol7.addSGroup("SRU", 0) +sg.setSGroupAtoms([0, 1, 2]) +try: + sg.setSGroupBonds([0, 1]) + print("ERROR: should have raised exception") +except IndigoException as e: + print("Expected error: {0}".format(getIndigoExceptionText(e))) + + +# ===== iterateSGroupCrossBonds ===== + +print("****** iterateSGroupCrossBonds ********") + +mol8 = indigo.loadMolecule("CCCCCC") +sg = mol8.addSGroup("SUP", 0) +sg.setSGroupAtoms([1, 2, 3]) +sg.createCrossBonds() + +cross_bonds = [] +for b in sg.iterateSGroupCrossBonds(): + cross_bonds.append(str(b.index())) +print("cross bonds: {0}".format(" ".join(sorted(cross_bonds)))) + +print("****** iterateSGroupCrossBonds: empty ********") + +mol9 = indigo.loadMolecule("CCCCCC") +sg = mol9.addSGroup("SUP", 0) +sg.setSGroupAtoms([0, 1, 2, 3, 4, 5]) +sg.createCrossBonds() + +count = 0 +for _ in sg.iterateSGroupCrossBonds(): + count += 1 +print("cross bonds when all atoms inside: {0}".format(count)) + + +# ===== Updated countBonds/iterateBonds ===== + +print( + "****** Updated countBonds: DAT returns CBONDS, others return cross bonds ********" +) + +mol10 = indigo.loadMolecule("CCCCCC") + +sg_dat = mol10.addSGroup("DAT", 0) +sg_dat.setSGroupAtoms([1, 2, 3]) +sg_dat.setSGroupBonds([1, 2]) +print("DAT countBonds (CBONDS): {0}".format(sg_dat.countBonds())) + +mol11 = indigo.loadMolecule("CCCCCC") +sg_sup = mol11.addSGroup("SUP", 0) +sg_sup.setSGroupAtoms([1, 2, 3]) +sg_sup.createCrossBonds() +print("SUP countBonds (cross): {0}".format(sg_sup.countBonds())) + +mol12 = indigo.loadMolecule("CCCCCC") +sg_sru = mol12.addSGroup("SRU", 0) +sg_sru.setSGroupAtoms([1, 2, 3]) +sg_sru.createCrossBonds() +print("SRU countBonds (cross): {0}".format(sg_sru.countBonds())) + + +# ===== createCrossBonds for all types ===== + +print("****** createCrossBonds: works for all SGroup types ********") + +for stype in ["SUP", "SRU", "GEN", "MUL"]: + mol = indigo.loadMolecule("CCCCCC") + sg = mol.addSGroup(stype, 0) + sg.setSGroupAtoms([1, 2, 3]) + sg.createCrossBonds() + print( + "{0} createCrossBonds count: {1}".format(stype, sg.countBonds()) + ) + + +# ===== clearSGroupCrossBonds for all types ===== + +print("****** clearSGroupCrossBonds: works for all SGroup types ********") + +for stype in ["SUP", "SRU", "GEN"]: + mol = indigo.loadMolecule("CCCCCC") + sg = mol.addSGroup(stype, 0) + sg.setSGroupAtoms([1, 2, 3]) + sg.createCrossBonds() + print( + "{0} before clear: {1}".format(stype, sg.countBonds()) + ) + sg.clearSGroupCrossBonds() + print( + "{0} after clear: {1}".format(stype, sg.countBonds()) + ) + + +# ===== Molfile roundtrip ===== + +print("****** Molfile roundtrip: SUP with cross bonds ********") + +indigo.setOption("molfile-saving-mode", "3000") +mol = indigo.loadMolecule("CCCCCC") +sg = mol.addSGroup("SUP", 0) +sg.setSGroupAtoms([1, 2, 3]) +sg.createCrossBonds() +sg.setSGroupName("TEST_SUP") + +molfile = mol.molfile() +mol2 = indigo.loadMolecule(molfile) + +sg_count = 0 +for sg in mol2.iterateSGroups(): + sg_count += 1 + print("type: {0}".format(sg.getSGroupType())) + print("atoms: {0}".format(sg.countAtoms())) + print("bonds: {0}".format(sg.countBonds())) +print("sgroup count: {0}".format(sg_count)) diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index f33af4ef28..8e1fd956f3 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -115,8 +115,9 @@ namespace indigo int parent_idx; // parent group number; represented with index in the array // TODO: leave only parent_idx - Array atoms; // represented with SAL in Molfile format - Array bonds; // represented with SBL in Molfile format + Array atoms; // represented with SAL in Molfile format + Array bonds; // represented with SBL in Molfile format (CBONDS for DAT, XBONDS for others) + Array cross_bonds; // explicit cross bonds storage (#3604) — for DAT type to separate CBONDS from XBONDS Array subscript; // SMT in Molfile format (LABEL in V3000) int brk_style; // represented with SBT in Molfile format From 4ffa0357cae058dbd80d8be707e3832d2b7a8bf9 Mon Sep 17 00:00:00 2001 From: even1024 Date: Wed, 29 Apr 2026 16:12:23 +0200 Subject: [PATCH 02/33] #3604 Add api methods to interact with atoms and bonds in s-groups --- api/dotnet/src/IndigoLib.cs | 13 +++++++ api/dotnet/src/IndigoObject.cs | 34 +++++++++++++++++++ .../main/java/com/epam/indigo/IndigoLib.java | 9 +++++ .../java/com/epam/indigo/IndigoObject.java | 30 ++++++++++++++++ core/indigo-core/molecule/molecule_sgroups.h | 1 - 5 files changed, 86 insertions(+), 1 deletion(-) diff --git a/api/dotnet/src/IndigoLib.cs b/api/dotnet/src/IndigoLib.cs index cdeca3a4fb..f74de284f8 100644 --- a/api/dotnet/src/IndigoLib.cs +++ b/api/dotnet/src/IndigoLib.cs @@ -632,6 +632,19 @@ public unsafe class IndigoLib [DllImport("indigo"), SuppressUnmanagedCodeSecurity] public static extern int indigoClearSGroupCrossBonds(int sgroup); + // Issue #3604: New SGroup API methods + [DllImport("indigo"), SuppressUnmanagedCodeSecurity] + public static extern int indigoAddSGroup(int molecule, string type, int extindex); + + [DllImport("indigo"), SuppressUnmanagedCodeSecurity] + public static extern int indigoSetSGroupAtoms(int sgroup, int natoms, int[] atoms); + + [DllImport("indigo"), SuppressUnmanagedCodeSecurity] + public static extern int indigoSetSGroupBonds(int sgroup, int nbonds, int[] bonds); + + [DllImport("indigo"), SuppressUnmanagedCodeSecurity] + public static extern int indigoIterateSGroupCrossBonds(int sgroup); + [DllImport("indigo"), SuppressUnmanagedCodeSecurity] public static extern int indigoAddSGroupAttachmentPoint(int sgroup, int aidx, int lvidx, string apid); diff --git a/api/dotnet/src/IndigoObject.cs b/api/dotnet/src/IndigoObject.cs index 5b621ede33..d10ee2d36d 100644 --- a/api/dotnet/src/IndigoObject.cs +++ b/api/dotnet/src/IndigoObject.cs @@ -842,6 +842,40 @@ public int clearSGroupCrossBonds() return dispatcher.checkResult(IndigoLib.indigoClearSGroupCrossBonds(self)); } + public IndigoObject addSGroup(string type, int extindex) + { + dispatcher.setSessionID(); + return new IndigoObject(dispatcher, dispatcher.checkResult(IndigoLib.indigoAddSGroup(self, type, extindex)), this); + } + + public int setSGroupAtoms(int[] atoms) + { + dispatcher.setSessionID(); + return dispatcher.checkResult(IndigoLib.indigoSetSGroupAtoms(self, atoms.Length, atoms)); + } + + public int setSGroupAtoms(ICollection atoms) + { + return setSGroupAtoms(Indigo.toIntArray(atoms)); + } + + public int setSGroupBonds(int[] bonds) + { + dispatcher.setSessionID(); + return dispatcher.checkResult(IndigoLib.indigoSetSGroupBonds(self, bonds.Length, bonds)); + } + + public int setSGroupBonds(ICollection bonds) + { + return setSGroupBonds(Indigo.toIntArray(bonds)); + } + + public IndigoObject iterateSGroupCrossBonds() + { + dispatcher.setSessionID(); + return new IndigoObject(dispatcher, dispatcher.checkResult(IndigoLib.indigoIterateSGroupCrossBonds(self)), this); + } + public int addSGroupAttachmentPoint(int aidx, int lvidx, string apid) { dispatcher.setSessionID(); diff --git a/api/java/indigo/src/main/java/com/epam/indigo/IndigoLib.java b/api/java/indigo/src/main/java/com/epam/indigo/IndigoLib.java index 4eddd3ba3c..b301826ba0 100644 --- a/api/java/indigo/src/main/java/com/epam/indigo/IndigoLib.java +++ b/api/java/indigo/src/main/java/com/epam/indigo/IndigoLib.java @@ -497,6 +497,15 @@ int indigoAddDataSGroup( int indigoClearSGroupCrossBonds(int sgroup); + // Issue #3604: New SGroup API methods + int indigoAddSGroup(int molecule, String type, int extindex); + + int indigoSetSGroupAtoms(int sgroup, int natoms, int[] atoms); + + int indigoSetSGroupBonds(int sgroup, int nbonds, int[] bonds); + + int indigoIterateSGroupCrossBonds(int sgroup); + int indigoAddSGroupAttachmentPoint(int sgroup, int aidx, int lvidx, String apid); int indigoDeleteSGroupAttachmentPoint(int sgroup, int apidx); diff --git a/api/java/indigo/src/main/java/com/epam/indigo/IndigoObject.java b/api/java/indigo/src/main/java/com/epam/indigo/IndigoObject.java index cffd9ede64..4d03e4e147 100644 --- a/api/java/indigo/src/main/java/com/epam/indigo/IndigoObject.java +++ b/api/java/indigo/src/main/java/com/epam/indigo/IndigoObject.java @@ -1249,6 +1249,36 @@ public int clearSGroupCrossBonds() { return Indigo.checkResult(this, lib.indigoClearSGroupCrossBonds(self)); } + public IndigoObject addSGroup(String type, int extindex) { + dispatcher.setSessionID(); + return new IndigoObject( + dispatcher, Indigo.checkResult(this, lib.indigoAddSGroup(self, type, extindex)), this); + } + + public int setSGroupAtoms(int[] atoms) { + dispatcher.setSessionID(); + return Indigo.checkResult(this, lib.indigoSetSGroupAtoms(self, atoms.length, atoms)); + } + + public int setSGroupAtoms(Collection atoms) { + return setSGroupAtoms(Indigo.toIntArray(atoms)); + } + + public int setSGroupBonds(int[] bonds) { + dispatcher.setSessionID(); + return Indigo.checkResult(this, lib.indigoSetSGroupBonds(self, bonds.length, bonds)); + } + + public int setSGroupBonds(Collection bonds) { + return setSGroupBonds(Indigo.toIntArray(bonds)); + } + + public IndigoObject iterateSGroupCrossBonds() { + dispatcher.setSessionID(); + return new IndigoObject( + dispatcher, Indigo.checkResult(this, lib.indigoIterateSGroupCrossBonds(self)), this); + } + public int addSGroupAttachmentPoint(int aidx, int lvidx, String apid) { dispatcher.setSessionID(); return Indigo.checkResult( diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index 8e1fd956f3..5271db0332 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -117,7 +117,6 @@ namespace indigo Array atoms; // represented with SAL in Molfile format Array bonds; // represented with SBL in Molfile format (CBONDS for DAT, XBONDS for others) - Array cross_bonds; // explicit cross bonds storage (#3604) — for DAT type to separate CBONDS from XBONDS Array subscript; // SMT in Molfile format (LABEL in V3000) int brk_style; // represented with SBT in Molfile format From f7ca433947d6d369584abdfd6bf4110853c5ade3 Mon Sep 17 00:00:00 2001 From: even1024 Date: Wed, 29 Apr 2026 17:22:45 +0200 Subject: [PATCH 03/33] misc fixes --- api/python/indigo/indigo/indigo_object.py | 4 +--- .../tests/basic/3604_sgroup_atoms_bonds.py | 13 ++++--------- core/indigo-core/molecule/molecule_sgroups.h | 4 ++-- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/api/python/indigo/indigo/indigo_object.py b/api/python/indigo/indigo/indigo_object.py index 120311c4f8..a4a64feed0 100644 --- a/api/python/indigo/indigo/indigo_object.py +++ b/api/python/indigo/indigo/indigo_object.py @@ -1720,9 +1720,7 @@ def addSGroup(self, sgtype, extindex=0): return IndigoObject( self.session, IndigoLib.checkResult( - self._lib().indigoAddSGroup( - self.id, sgtype.encode(), extindex - ) + self._lib().indigoAddSGroup(self.id, sgtype.encode(), extindex) ), ) diff --git a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py index 25d2a303de..8997348c73 100644 --- a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py +++ b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py @@ -10,6 +10,7 @@ - Updated iterateBonds/countBonds behavior - Updated createCrossBonds/clearSGroupCrossBonds for all types """ + import os import sys @@ -190,9 +191,7 @@ sg = mol.addSGroup(stype, 0) sg.setSGroupAtoms([1, 2, 3]) sg.createCrossBonds() - print( - "{0} createCrossBonds count: {1}".format(stype, sg.countBonds()) - ) + print("{0} createCrossBonds count: {1}".format(stype, sg.countBonds())) # ===== clearSGroupCrossBonds for all types ===== @@ -204,13 +203,9 @@ sg = mol.addSGroup(stype, 0) sg.setSGroupAtoms([1, 2, 3]) sg.createCrossBonds() - print( - "{0} before clear: {1}".format(stype, sg.countBonds()) - ) + print("{0} before clear: {1}".format(stype, sg.countBonds())) sg.clearSGroupCrossBonds() - print( - "{0} after clear: {1}".format(stype, sg.countBonds()) - ) + print("{0} after clear: {1}".format(stype, sg.countBonds())) # ===== Molfile roundtrip ===== diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index 5271db0332..02df3a3012 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -115,8 +115,8 @@ namespace indigo int parent_idx; // parent group number; represented with index in the array // TODO: leave only parent_idx - Array atoms; // represented with SAL in Molfile format - Array bonds; // represented with SBL in Molfile format (CBONDS for DAT, XBONDS for others) + Array atoms; // represented with SAL in Molfile format + Array bonds; // represented with SBL in Molfile format (CBONDS for DAT, XBONDS for others) Array subscript; // SMT in Molfile format (LABEL in V3000) int brk_style; // represented with SBT in Molfile format From 44d0a12f9d407d91d76e475b0a557273847bb251 Mon Sep 17 00:00:00 2001 From: even1024 Date: Wed, 29 Apr 2026 17:31:54 +0200 Subject: [PATCH 04/33] misc fixes --- api/c/indigo/src/indigo_molecule_operations.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/api/c/indigo/src/indigo_molecule_operations.cpp b/api/c/indigo/src/indigo_molecule_operations.cpp index 1b149274c7..5dab2e5adb 100644 --- a/api/c/indigo/src/indigo_molecule_operations.cpp +++ b/api/c/indigo/src/indigo_molecule_operations.cpp @@ -1780,17 +1780,19 @@ CEXPORT int indigoClearSGroupCrossBonds(int sgroup) static IndigoObject* _wrapSGroup(BaseMolecule& mol, int idx) { - SGroup& sgroup = mol.sgroups.getSGroup(idx); - if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) + switch (mol.sgroups.getSGroup(idx).sgroup_type) + { + case SGroup::SG_TYPE_SUP: return new IndigoSuperatom(mol, idx); - else if (sgroup.sgroup_type == SGroup::SG_TYPE_SRU) + case SGroup::SG_TYPE_SRU: return new IndigoRepeatingUnit(mol, idx); - else if (sgroup.sgroup_type == SGroup::SG_TYPE_MUL) + case SGroup::SG_TYPE_MUL: return new IndigoMultipleGroup(mol, idx); - else if (sgroup.sgroup_type == SGroup::SG_TYPE_DAT) + case SGroup::SG_TYPE_DAT: return new IndigoDataSGroup(mol, idx); - else + default: return new IndigoGenericSGroup(mol, idx); + } } CEXPORT int indigoAddSGroup(int molecule, const char* type, int extindex) @@ -1868,7 +1870,6 @@ CEXPORT int indigoIterateSGroupCrossBonds(int sgroup) INDIGO_END(-1); } - CEXPORT int indigoAddSGroupAttachmentPoint(int sgroup, int aidx, int lvidx, const char* apid) { INDIGO_BEGIN From 8aa87183fc885add5954e34247a43ce41ac0f8b3 Mon Sep 17 00:00:00 2001 From: even1024 Date: Wed, 29 Apr 2026 17:41:12 +0200 Subject: [PATCH 05/33] misc fixes --- api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py index 8997348c73..d6029f2f81 100644 --- a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py +++ b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py @@ -19,7 +19,7 @@ os.path.join(os.path.abspath(__file__), "..", "..", "..", "common") ) ) -from env_indigo import Indigo, getIndigoExceptionText, IndigoException # noqa +from env_indigo import Indigo, IndigoException, getIndigoExceptionText # noqa indigo = Indigo() indigo.setOption("molfile-saving-skip-date", True) From f9af5a97605c6f286be6f840254ce2f0087d4c7d Mon Sep 17 00:00:00 2001 From: even1024 Date: Wed, 29 Apr 2026 19:06:42 +0200 Subject: [PATCH 06/33] test fix --- .../rendering/sgroups_instrumentation.py.out | 4 ++-- .../rendering/sgroups_instrumentation.py | 19 +++++-------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/api/tests/integration/ref/rendering/sgroups_instrumentation.py.out b/api/tests/integration/ref/rendering/sgroups_instrumentation.py.out index cd62bd95e8..59f2c6c7a6 100644 --- a/api/tests/integration/ref/rendering/sgroups_instrumentation.py.out +++ b/api/tests/integration/ref/rendering/sgroups_instrumentation.py.out @@ -965,8 +965,8 @@ Cross bonds after clear: 0 Cross bond indices after clear: [] Cross bonds after create: 2 Cross bond indices after create: [0, 3] -Expected error for clearSGroupCrossBonds on data SGroup: core: is not a superatom -Expected error for createCrossBonds on data SGroup: core: is not a superatom +clearSGroupCrossBonds on data SGroup: OK +createCrossBonds on data SGroup: OK ****** Get/Set Multiplier ******** -INDIGO-01000000002D diff --git a/api/tests/integration/tests/rendering/sgroups_instrumentation.py b/api/tests/integration/tests/rendering/sgroups_instrumentation.py index 016ac3d3ea..7c84b3ed92 100644 --- a/api/tests/integration/tests/rendering/sgroups_instrumentation.py +++ b/api/tests/integration/tests/rendering/sgroups_instrumentation.py @@ -214,21 +214,12 @@ def _cross_bond_indices(sg): print("Cross bond indices after create: %s" % after_create_indices) break # test with first match only + # #3604: clearSGroupCrossBonds and createCrossBonds now work for all SGroup types data_sg = m.addDataSGroup([0, 1], [], "ID", "test") - try: - data_sg.clearSGroupCrossBonds() - except IndigoException as e: - print( - "Expected error for clearSGroupCrossBonds on data SGroup: %s" - % getIndigoExceptionText(e) - ) - try: - data_sg.createCrossBonds() - except IndigoException as e: - print( - "Expected error for createCrossBonds on data SGroup: %s" - % getIndigoExceptionText(e) - ) + data_sg.clearSGroupCrossBonds() + print("clearSGroupCrossBonds on data SGroup: OK") + data_sg.createCrossBonds() + print("createCrossBonds on data SGroup: OK") print("****** SGroup Cross Bonds ********") From d26a89cb321ba6718671ccaf729d5cdce847e3aa Mon Sep 17 00:00:00 2001 From: even1024 Date: Thu, 30 Apr 2026 15:45:48 +0200 Subject: [PATCH 07/33] refactored --- api/c/indigo/src/indigo_molecule.cpp | 6 ++-- .../indigo/src/indigo_molecule_operations.cpp | 19 ++++++----- core/indigo-core/molecule/molecule_sgroups.h | 12 +++++-- .../molecule/src/base_molecule.cpp | 4 +-- .../molecule/src/base_molecule_misc.cpp | 4 +-- .../molecule/src/base_molecule_templates.cpp | 34 +++++++++---------- core/indigo-core/molecule/src/cmf_loader.cpp | 2 +- core/indigo-core/molecule/src/cmf_saver.cpp | 2 +- .../molecule/src/molecule_json_loader.cpp | 4 +-- .../molecule/src/molecule_json_saver.cpp | 6 ++-- .../molecule/src/molecule_sgroups.cpp | 2 +- .../molecule/src/molfile_loader_v2000.cpp | 2 +- .../molecule/src/molfile_loader_v3000.cpp | 2 +- .../molecule/src/molfile_saver.cpp | 18 +++++----- .../molecule/src/smiles_loader_parsers.cpp | 4 +-- 15 files changed, 65 insertions(+), 56 deletions(-) diff --git a/api/c/indigo/src/indigo_molecule.cpp b/api/c/indigo/src/indigo_molecule.cpp index ada9d594ad..74083d2d05 100644 --- a/api/c/indigo/src/indigo_molecule.cpp +++ b/api/c/indigo/src/indigo_molecule.cpp @@ -1199,7 +1199,7 @@ IndigoSGroupBondsIter::~IndigoSGroupBondsIter() bool IndigoSGroupBondsIter::hasNext() { - return _idx + 1 < _sgroup.bonds.size(); + return _idx + 1 < _sgroup.getBonds().size(); } IndigoObject* IndigoSGroupBondsIter::next() @@ -1208,7 +1208,7 @@ IndigoObject* IndigoSGroupBondsIter::next() return 0; _idx++; - return new IndigoBond(_mol, _sgroup.bonds[_idx]); + return new IndigoBond(_mol, _sgroup.getBonds()[_idx]); } int _indigoIterateAtoms(Indigo& self, int molecule, int type) @@ -1352,7 +1352,7 @@ CEXPORT int indigoCountBonds(int molecule) auto sg = _getSGroupFromObject(obj); if (sg) - return sg.get().bonds.size(); + return sg.get().getBonds().size(); BaseMolecule& mol = obj.getBaseMolecule(); diff --git a/api/c/indigo/src/indigo_molecule_operations.cpp b/api/c/indigo/src/indigo_molecule_operations.cpp index 5dab2e5adb..17263c283f 100644 --- a/api/c/indigo/src/indigo_molecule_operations.cpp +++ b/api/c/indigo/src/indigo_molecule_operations.cpp @@ -1366,7 +1366,7 @@ CEXPORT int indigoAddDataSGroup(int molecule, int natoms, int* atoms, int nbonds dsg.atoms.concat(atoms, natoms); if (bonds != nullptr) - dsg.bonds.concat(bonds, nbonds); + dsg.cbonds.concat(bonds, nbonds); if (data != nullptr) dsg.data.readString(data, true); @@ -1647,7 +1647,7 @@ CEXPORT int indigoCreateSGroup(const char* type, int mapping, const char* name) if (((sgroup.atoms.find(edge.beg) != -1) && (sgroup.atoms.find(edge.end) == -1)) || ((sgroup.atoms.find(edge.end) != -1) && (sgroup.atoms.find(edge.beg) == -1))) { - sgroup.bonds.push(i); + sgroup.xbonds.push(i); } } @@ -1733,14 +1733,14 @@ CEXPORT int indigoGetSGroupNumCrossBonds(int sgroup) INDIGO_BEGIN { IndigoSGroup& isg = IndigoSGroup::cast(self.getObject(sgroup)); - return isg.get().bonds.size(); + return isg.get().xbonds.size(); } INDIGO_END(-1); } static void _fillCrossBonds(BaseMolecule& mol, SGroup& sg) { - sg.bonds.clear(); + sg.xbonds.clear(); for (auto atom_idx : sg.atoms) { const Vertex& vx = mol.getVertex(atom_idx); @@ -1749,8 +1749,8 @@ static void _fillCrossBonds(BaseMolecule& mol, SGroup& sg) if (sg.atoms.find(vx.neiVertex(nei_idx)) == -1) { int edge_idx = vx.neiEdge(nei_idx); - if (sg.bonds.find(edge_idx) == -1) - sg.bonds.push(edge_idx); + if (sg.xbonds.find(edge_idx) == -1) + sg.xbonds.push(edge_idx); } } } @@ -1772,7 +1772,7 @@ CEXPORT int indigoClearSGroupCrossBonds(int sgroup) INDIGO_BEGIN { IndigoSGroup& isg = IndigoSGroup::cast(self.getObject(sgroup)); - isg.get().bonds.clear(); + isg.get().xbonds.clear(); return 1; } INDIGO_END(-1); @@ -1845,14 +1845,15 @@ CEXPORT int indigoSetSGroupBonds(int sgroup, int nbonds, int* bonds) if (s.sgroup_type != SGroup::SG_TYPE_DAT) throw IndigoError("indigoSetSGroupBonds: only DAT SGroups support explicit bond assignment"); - s.bonds.clear(); + DataSGroup& dsg = (DataSGroup&)s; + dsg.cbonds.clear(); if (bonds != nullptr && nbonds > 0) { for (int i = 0; i < nbonds; i++) { if (bonds[i] < 0 || bonds[i] >= isg.mol.edgeEnd()) throw IndigoError("indigoSetSGroupBonds: bond index %d out of range", bonds[i]); - s.bonds.push(bonds[i]); + dsg.cbonds.push(bonds[i]); } } return 1; diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index 02df3a3012..8dcfeec0fc 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -115,8 +115,11 @@ namespace indigo int parent_idx; // parent group number; represented with index in the array // TODO: leave only parent_idx - Array atoms; // represented with SAL in Molfile format - Array bonds; // represented with SBL in Molfile format (CBONDS for DAT, XBONDS for others) + Array atoms; // represented with SAL in Molfile format + Array xbonds; // crossing bonds, represented with XBONDS/SBL in Molfile format + + virtual const Array& getBonds() const { return xbonds; } + virtual Array& getBonds() { return xbonds; } Array subscript; // SMT in Molfile format (LABEL in V3000) int brk_style; // represented with SBT in Molfile format @@ -136,6 +139,11 @@ namespace indigo DataSGroup(); ~DataSGroup() override; + Array cbonds; // chemical bonds, represented with CBONDS/SBL in Molfile format + + const Array& getBonds() const override { return cbonds; } + Array& getBonds() override { return cbonds; } + Array description; // SDT in Molfile format (filed units or format) Array name; // SDT in Molfile format (field name) Array type; // SDT in Molfile format (field type) diff --git a/core/indigo-core/molecule/src/base_molecule.cpp b/core/indigo-core/molecule/src/base_molecule.cpp index 85711fe687..58128d8bce 100644 --- a/core/indigo-core/molecule/src/base_molecule.cpp +++ b/core/indigo-core/molecule/src/base_molecule.cpp @@ -490,9 +490,9 @@ void BaseMolecule::_mergeWithSubmolecule_Sub(BaseMolecule& mol, const Array void BaseMolecule::_flipSGroupBond(SGroup& sgroup, int src_bond_idx, int new_bond_idx) { - int idx = sgroup.bonds.find(src_bond_idx); + int idx = sgroup.getBonds().find(src_bond_idx); if (idx != -1) - sgroup.bonds[idx] = new_bond_idx; + sgroup.getBonds()[idx] = new_bond_idx; } void BaseMolecule::_flipSuperatomBond(Superatom& sa, int src_bond_idx, int new_bond_idx) diff --git a/core/indigo-core/molecule/src/base_molecule_misc.cpp b/core/indigo-core/molecule/src/base_molecule_misc.cpp index 458494e67e..440e7bdc1f 100644 --- a/core/indigo-core/molecule/src/base_molecule_misc.cpp +++ b/core/indigo-core/molecule/src/base_molecule_misc.cpp @@ -677,8 +677,8 @@ int BaseMolecule::transformHELMtoSGroups(Array& helm_class, Array& h { ap_idx = v.neiVertex(k); int b_idx = findEdgeIndex(v.neiVertex(k), i); - sg.bonds.push(b_idx); - lvsg.bonds.push(b_idx); + sg.xbonds.push(b_idx); + lvsg.xbonds.push(b_idx); } } diff --git a/core/indigo-core/molecule/src/base_molecule_templates.cpp b/core/indigo-core/molecule/src/base_molecule_templates.cpp index 3f31e3e042..936e83575a 100644 --- a/core/indigo-core/molecule/src/base_molecule_templates.cpp +++ b/core/indigo-core/molecule/src/base_molecule_templates.cpp @@ -414,8 +414,8 @@ int BaseMolecule::_transformTGroupToSGroup(int idx, int t_idx) int bond_idx = findEdgeIndex(att_atoms[i], mapping[tg_atoms[i]]); if (bond_idx > -1) { - if (su.bonds.find(bond_idx) == -1) - su.bonds.push(bond_idx); + if (su.xbonds.find(bond_idx) == -1) + su.xbonds.push(bond_idx); } } } @@ -452,9 +452,9 @@ void BaseMolecule::_collectSuparatomAttachmentPoints(Superatom& sa, std::unorder } else // Try to create attachment points from crossing bond information { - for (int k = 0; k < sa.bonds.size(); k++) + for (int k = 0; k < sa.xbonds.size(); k++) { - const Edge& edge = getEdge(sa.bonds[k]); + const Edge& edge = getEdge(sa.xbonds[k]); int ap_aidx = -1; int ap_lvidx = -1; if (sa.atoms.find(edge.beg) != -1) @@ -498,7 +498,7 @@ void BaseMolecule::_connectTemplateAtom(Superatom& sa, int t_idx, Array& or if (ap.lvidx < 0) { int edge_idx = -1; - for (auto xbond_idx : sa.bonds) + for (auto xbond_idx : sa.xbonds) { const Edge& e = getEdge(xbond_idx); int dest_atom = e.beg == ap.aidx ? e.end : (e.end == ap.aidx ? e.beg : -1); @@ -834,7 +834,7 @@ int BaseMolecule::_transformSGroupToTGroup(int sg_idx, int& tg_id) ap_points_ids.clear(); ap_ids.clear(); - if (su.attachment_points.size() == 0 && su.bonds.size() == 0) + if (su.attachment_points.size() == 0 && su.xbonds.size() == 0) return -1; if (su.attachment_points.size() > 0) @@ -1183,7 +1183,7 @@ int BaseMolecule::_createSGroupFromFragment(Array& sg_atoms, const TGroup& int t_xbond_idx = findEdgeIndex(att_point_idx, v.neiVertex(k)); if (fragment.getBondOrder(q_xbond_idx) == getBondOrder(t_xbond_idx)) { - su_new.bonds.push(t_xbond_idx); + su_new.xbonds.push(t_xbond_idx); int idap = su_new.attachment_points.add(); Superatom::_AttachmentPoint& ap = su_new.attachment_points.at(idap); ap.aidx = att_point_idx; @@ -1208,11 +1208,11 @@ void BaseMolecule::_removeAtomsFromSGroup(SGroup& sgroup, Array& mapping) if (mapping[sgroup.atoms[i]] == -1) sgroup.atoms.remove(i); } - for (i = sgroup.bonds.size() - 1; i >= 0; i--) + for (i = sgroup.xbonds.size() - 1; i >= 0; i--) { - const Edge& edge = getEdge(sgroup.bonds[i]); + const Edge& edge = getEdge(sgroup.xbonds[i]); if (mapping[edge.beg] == -1 || mapping[edge.end] == -1) - sgroup.bonds.remove(i); + sgroup.xbonds.remove(i); } updateEditRevision(); } @@ -1260,10 +1260,10 @@ void BaseMolecule::_removeBondsFromSGroup(SGroup& sgroup, Array& mapping) { int i; - for (i = sgroup.bonds.size() - 1; i >= 0; i--) + for (i = sgroup.xbonds.size() - 1; i >= 0; i--) { - if (mapping[sgroup.bonds[i]] == -1) - sgroup.bonds.remove(i); + if (mapping[sgroup.xbonds[i]] == -1) + sgroup.xbonds.remove(i); } updateEditRevision(); } @@ -1312,17 +1312,17 @@ bool BaseMolecule::_mergeSGroupWithSubmolecule(SGroup& sgroup, SGroup& super, Ba merged = true; } } - for (i = 0; i < super.bonds.size(); i++) + for (i = 0; i < super.xbonds.size(); i++) { - const Edge& edge = supermol.getEdge(super.bonds[i]); + const Edge& edge = supermol.getEdge(super.xbonds[i]); - if (edge_mapping[super.bonds[i]] < 0) + if (edge_mapping[super.xbonds[i]] < 0) continue; if (mapping[edge.beg] < 0 || mapping[edge.end] < 0) throw Error("internal: edge is not mapped"); - sgroup.bonds.push(edge_mapping[super.bonds[i]]); + sgroup.xbonds.push(edge_mapping[super.xbonds[i]]); merged = true; } diff --git a/core/indigo-core/molecule/src/cmf_loader.cpp b/core/indigo-core/molecule/src/cmf_loader.cpp index af6f9ae970..470ce0c051 100644 --- a/core/indigo-core/molecule/src/cmf_loader.cpp +++ b/core/indigo-core/molecule/src/cmf_loader.cpp @@ -884,7 +884,7 @@ void CmfLoader::_readUIntArray(Array& dest) void CmfLoader::_readGeneralSGroup(SGroup& sgroup) { _readUIntArray(sgroup.atoms); - _readUIntArray(sgroup.bonds); + _readUIntArray(sgroup.getBonds()); } void CmfLoader::_readExtSection(Molecule& mol) diff --git a/core/indigo-core/molecule/src/cmf_saver.cpp b/core/indigo-core/molecule/src/cmf_saver.cpp index 7534c77b9c..51116c61f7 100644 --- a/core/indigo-core/molecule/src/cmf_saver.cpp +++ b/core/indigo-core/molecule/src/cmf_saver.cpp @@ -271,7 +271,7 @@ void CmfSaver::_encodeUIntArraySkipNegative(const Array& data) void CmfSaver::_encodeBaseSGroup(Molecule& /* mol */, SGroup& sgroup, const Mapping& mapping) { _encodeUIntArray(sgroup.atoms, *mapping.atom_mapping); - _encodeUIntArray(sgroup.bonds, *mapping.bond_mapping); + _encodeUIntArray(sgroup.getBonds(), *mapping.bond_mapping); } void CmfSaver::_encodeExtSection(Molecule& mol, const Mapping& mapping) diff --git a/core/indigo-core/molecule/src/molecule_json_loader.cpp b/core/indigo-core/molecule/src/molecule_json_loader.cpp index a94c8f05bb..eaaf9b079d 100644 --- a/core/indigo-core/molecule/src/molecule_json_loader.cpp +++ b/core/indigo-core/molecule/src/molecule_json_loader.cpp @@ -1190,7 +1190,7 @@ void MoleculeJsonLoader::parseSGroups(const rapidjson::Value& sgroups, BaseMolec const Value& bonds = s["bonds"]; for (rapidjson::SizeType j = 0; j < bonds.Size(); ++j) { - sgroup.bonds.push(bonds[j].GetInt()); + sgroup.getBonds().push(bonds[j].GetInt()); } } } @@ -1226,7 +1226,7 @@ void MoleculeJsonLoader::fillXBondsAndBrackets(Superatom& sa, BaseMolecule& mol) if (atoms.find(target_atom) == atoms.end()) { const auto& target_pos = mol.getAtomXyz(target_atom); - sa.bonds.push(vx.neiEdge(i)); + sa.xbonds.push(vx.neiEdge(i)); brackets.emplace_back((target_pos.x - src_pos.x) / 2, (target_pos.y - src_pos.y) / 2); } } diff --git a/core/indigo-core/molecule/src/molecule_json_saver.cpp b/core/indigo-core/molecule/src/molecule_json_saver.cpp index 0cdbd80c88..c56a889e08 100644 --- a/core/indigo-core/molecule/src/molecule_json_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_json_saver.cpp @@ -505,12 +505,12 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) break; } - if (sgroup.bonds.size()) + if (sgroup.getBonds().size()) { writer.Key("bonds"); writer.StartArray(); - for (int i = 0; i < sgroup.bonds.size(); ++i) - writer.Int(sgroup.bonds[i]); + for (int i = 0; i < sgroup.getBonds().size(); ++i) + writer.Int(sgroup.getBonds()[i]); writer.EndArray(); } diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index 73dea28b79..a2846495f2 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -681,7 +681,7 @@ void MoleculeSGroups::findSGroups(int property, Array& indices, Array& for (i = _sgroups.begin(); i != _sgroups.end(); i = _sgroups.next(i)) { SGroup& sg = *_sgroups.at(i); - if (_cmpIndices(sg.bonds, indices)) + if (_cmpIndices(sg.getBonds(), indices)) { sgs.push(i); } diff --git a/core/indigo-core/molecule/src/molfile_loader_v2000.cpp b/core/indigo-core/molecule/src/molfile_loader_v2000.cpp index 740a5c179e..983840a67a 100644 --- a/core/indigo-core/molecule/src/molfile_loader_v2000.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_v2000.cpp @@ -920,7 +920,7 @@ void MolfileLoader::_readCtab2000() if (strncmp(chars, "SAL", 3) == 0) sgroup->atoms.push(_scanner.readIntFix(3) - 1); else // SBL - sgroup->bonds.push(_scanner.readIntFix(3) - 1); + sgroup->getBonds().push(_scanner.readIntFix(3) - 1); } } _scanner.skipLine(); diff --git a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp index ac45b09274..7125602ce7 100644 --- a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp @@ -1194,7 +1194,7 @@ void MolfileLoader::_readSGroup3000(const char* str) n = scanner.readInt1(); while (n-- > 0) { - sgroup->bonds.push(scanner.readInt() - 1); + sgroup->getBonds().push(scanner.readInt() - 1); scanner.skipSpace(); } scanner.skip(1); // ) diff --git a/core/indigo-core/molecule/src/molfile_saver.cpp b/core/indigo-core/molecule/src/molfile_saver.cpp index d168103264..c0705b22a4 100644 --- a/core/indigo-core/molecule/src/molfile_saver.cpp +++ b/core/indigo-core/molecule/src/molfile_saver.cpp @@ -1118,14 +1118,14 @@ void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, int idx, Output& outp output.printf(" %d", _atom_mapping[sgroup.atoms[i]]); output.printf(")"); } - if (sgroup.bonds.size() > 0) + if (sgroup.getBonds().size() > 0) { if (sgroup.sgroup_type == SGroup::SG_TYPE_DAT) - output.printf(" CBONDS=(%d", sgroup.bonds.size()); + output.printf(" CBONDS=(%d", sgroup.getBonds().size()); else - output.printf(" XBONDS=(%d", sgroup.bonds.size()); - for (i = 0; i < sgroup.bonds.size(); i++) - output.printf(" %d", _bond_mapping[sgroup.bonds[i]]); + output.printf(" XBONDS=(%d", sgroup.getBonds().size()); + for (i = 0; i < sgroup.getBonds().size(); i++) + output.printf(" %d", _bond_mapping[sgroup.getBonds()[i]]); output.printf(")"); } if (sgroup.sgroup_subtype > 0) @@ -1759,12 +1759,12 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) output.printf(" %3d", _atom_mapping[sgroup.atoms[k]]); output.writeCR(); } - for (j = 0; j < sgroup.bonds.size(); j += 8) + for (j = 0; j < sgroup.getBonds().size(); j += 8) { int k; - output.printf("M SBL %3d%3d", sgroup.original_group, std::min(sgroup.bonds.size(), j + 8) - j); - for (k = j; k < std::min(sgroup.bonds.size(), j + 8); k++) - output.printf(" %3d", _bond_mapping[sgroup.bonds[k]]); + output.printf("M SBL %3d%3d", sgroup.original_group, std::min(sgroup.getBonds().size(), j + 8) - j); + for (k = j; k < std::min(sgroup.getBonds().size(), j + 8); k++) + output.printf(" %3d", _bond_mapping[sgroup.getBonds()[k]]); output.writeCR(); } diff --git a/core/indigo-core/molecule/src/smiles_loader_parsers.cpp b/core/indigo-core/molecule/src/smiles_loader_parsers.cpp index 74b88cec06..da9f0c3d78 100644 --- a/core/indigo-core/molecule/src/smiles_loader_parsers.cpp +++ b/core/indigo-core/molecule/src/smiles_loader_parsers.cpp @@ -1698,7 +1698,7 @@ void SmilesLoader::_handlePolymerRepetition(int i) if (_atoms[edge.beg].polymer_index != i && _atoms[edge.end].polymer_index != i) continue; if (_atoms[edge.beg].polymer_index == i && _atoms[edge.end].polymer_index == i) - sgroup->bonds.push(j); + sgroup->xbonds.push(j); else { // bond going out of the sgroup @@ -1748,7 +1748,7 @@ void SmilesLoader::_handlePolymerRepetition(int i) for (k = rep->edgeBegin(); k != rep->edgeEnd(); k = rep->edgeNext(k)) { const Edge& edge = rep->getEdge(k); - sgroup->bonds.push(_bmol->findEdgeIndex(mapping[edge.beg], mapping[edge.end])); + sgroup->xbonds.push(_bmol->findEdgeIndex(mapping[edge.beg], mapping[edge.end])); } if (rep_end >= 0 && end_bond >= 0) From 1ad842fc1661920588ae28139682105f57a95703 Mon Sep 17 00:00:00 2001 From: even1024 Date: Mon, 4 May 2026 07:59:28 +0200 Subject: [PATCH 08/33] refactor --- .../src/indigo_abbreviations_expand.cpp | 2 +- .../indigo/src/indigo_molecule_operations.cpp | 14 +++---- core/indigo-core/molecule/ket_objects.h | 2 +- core/indigo-core/molecule/molecule_sgroups.h | 2 +- .../molecule/src/base_molecule.cpp | 2 +- .../molecule/src/base_molecule_misc.cpp | 6 +-- .../molecule/src/base_molecule_sgroups.cpp | 4 +- .../molecule/src/base_molecule_templates.cpp | 40 +++++++++---------- core/indigo-core/molecule/src/cmf_loader.cpp | 6 +-- core/indigo-core/molecule/src/cmf_saver.cpp | 4 +- core/indigo-core/molecule/src/cml_loader.cpp | 4 +- core/indigo-core/molecule/src/cml_saver.cpp | 4 +- core/indigo-core/molecule/src/ket_objects.cpp | 2 +- .../molecule/src/molecule_cdxml_loader.cpp | 4 +- .../molecule/src/molecule_cdxml_saver.cpp | 6 +-- .../molecule/src/molecule_gross_formula.cpp | 2 +- .../molecule/src/molecule_json_loader.cpp | 8 ++-- .../molecule/src/molecule_json_saver.cpp | 6 +-- .../molecule/src/molecule_sgroups.cpp | 2 +- .../molecule/src/molfile_loader_v2000.cpp | 2 +- .../molecule/src/molfile_loader_v3000.cpp | 4 +- .../molecule/src/molfile_saver.cpp | 24 +++++------ .../molecule/src/sequence_saver.cpp | 2 +- .../molecule/src/smiles_loader_parsers.cpp | 2 +- .../indigo-core/molecule/src/smiles_saver.cpp | 2 +- core/render2d/src/render_internal.cpp | 10 ++--- 26 files changed, 83 insertions(+), 83 deletions(-) diff --git a/api/c/indigo/src/indigo_abbreviations_expand.cpp b/api/c/indigo/src/indigo_abbreviations_expand.cpp index c7c5ea5f4d..6419772076 100644 --- a/api/c/indigo/src/indigo_abbreviations_expand.cpp +++ b/api/c/indigo/src/indigo_abbreviations_expand.cpp @@ -762,7 +762,7 @@ namespace indigo int sid = mol.sgroups.addSGroup(SGroup::SG_TYPE_SUP); Superatom& super = (Superatom&)mol.sgroups.getSGroup(sid); - super.subscript.readString(mol.getPseudoAtom(v), true); + super.label.readString(mol.getPseudoAtom(v), true); for (int ve = expanded.vertexBegin(); ve != expanded.vertexEnd(); ve = expanded.vertexNext(ve)) super.atoms.push(mapping[ve]); diff --git a/api/c/indigo/src/indigo_molecule_operations.cpp b/api/c/indigo/src/indigo_molecule_operations.cpp index 17263c283f..4b6188d502 100644 --- a/api/c/indigo/src/indigo_molecule_operations.cpp +++ b/api/c/indigo/src/indigo_molecule_operations.cpp @@ -788,7 +788,7 @@ void IndigoSuperatom::remove() const char* IndigoSuperatom::getName() { - return ((Superatom&)mol.sgroups.getSGroup(idx)).subscript.ptr(); + return ((Superatom&)mol.sgroups.getSGroup(idx)).label.ptr(); } IndigoSuperatom& IndigoSuperatom::cast(IndigoObject& obj) @@ -1385,7 +1385,7 @@ CEXPORT int indigoAddSuperatom(int molecule, int natoms, int* atoms, const char* BaseMolecule& mol = self.getObject(molecule).getBaseMolecule(); int idx = mol.sgroups.addSGroup(SGroup::SG_TYPE_SUP); Superatom& satom = (Superatom&)mol.sgroups.getSGroup(idx); - satom.subscript.appendString(name, true); + satom.label.appendString(name, true); if (atoms == nullptr) throw IndigoError("indigoAddSuperatom(): atoms were not specified"); @@ -1651,7 +1651,7 @@ CEXPORT int indigoCreateSGroup(const char* type, int mapping, const char* name) } } - sgroup.subscript.appendString(name, true); + sgroup.label.appendString(name, true); if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) { @@ -1708,7 +1708,7 @@ CEXPORT int indigoSetSGroupName(int sgroup, const char* sgname) { IndigoObject& obj = self.getObject(sgroup); SGroup& sg = IndigoSGroup::cast(obj).get(); - sg.subscript.readString(sgname, true); + sg.label.readString(sgname, true); return 1; } @@ -1721,9 +1721,9 @@ CEXPORT const char* indigoGetSGroupName(int sgroup) { IndigoObject& obj = self.getObject(sgroup); SGroup& sg = IndigoSGroup::cast(obj).get(); - if (sg.subscript.size() < 1) + if (sg.label.size() < 1) return ""; - return sg.subscript.ptr(); + return sg.label.ptr(); } INDIGO_END(0); } @@ -2083,7 +2083,7 @@ CEXPORT const char* indigoGetRepeatingUnitSubscript(int sgroup) INDIGO_BEGIN { RepeatingUnit& ru = IndigoRepeatingUnit::cast(self.getObject(sgroup)).get(); - return ru.subscript.ptr(); + return ru.label.ptr(); } INDIGO_END(0); } diff --git a/core/indigo-core/molecule/ket_objects.h b/core/indigo-core/molecule/ket_objects.h index 5e68d11da3..4f9cb80d40 100644 --- a/core/indigo-core/molecule/ket_objects.h +++ b/core/indigo-core/molecule/ket_objects.h @@ -361,7 +361,7 @@ namespace indigo enum class StringProps { - subscript + label }; private: diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index 8dcfeec0fc..6244597c43 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -121,7 +121,7 @@ namespace indigo virtual const Array& getBonds() const { return xbonds; } virtual Array& getBonds() { return xbonds; } - Array subscript; // SMT in Molfile format (LABEL in V3000) + Array label; // SMT in Molfile format (LABEL in V3000) int brk_style; // represented with SBT in Molfile format Array brackets; // represented with SDI in Molfile format DisplayOption contracted; // display option (-1 if undefined, 0 - expanded, 1 - contracted) diff --git a/core/indigo-core/molecule/src/base_molecule.cpp b/core/indigo-core/molecule/src/base_molecule.cpp index 58128d8bce..3aa8fbf9cb 100644 --- a/core/indigo-core/molecule/src/base_molecule.cpp +++ b/core/indigo-core/molecule/src/base_molecule.cpp @@ -178,7 +178,7 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma sg.parent_idx = supersg.parent_idx; sg.original_group = supersg.original_group; sg.parent_group = supersg.parent_group; - sg.subscript.copy(supersg.subscript); + sg.label.copy(supersg.label); if (_mergeSGroupWithSubmolecule(sg, supersg, mol, mapping, edge_mapping)) { diff --git a/core/indigo-core/molecule/src/base_molecule_misc.cpp b/core/indigo-core/molecule/src/base_molecule_misc.cpp index 440e7bdc1f..66b038ffd7 100644 --- a/core/indigo-core/molecule/src/base_molecule_misc.cpp +++ b/core/indigo-core/molecule/src/base_molecule_misc.cpp @@ -644,7 +644,7 @@ int BaseMolecule::transformHELMtoSGroups(Array& helm_class, Array& h int idx = sgroups.addSGroup("SUP"); Superatom& sg = (Superatom&)sgroups.getSGroup(idx); sg.atoms.copy(sg_atoms); - sg.subscript.copy(helm_name); + sg.label.copy(helm_name); if (helm_class.size() > 6 && strncmp(helm_class.ptr(), "PEPTIDE", 7) == 0) sg.sa_class.readString("AA", true); else @@ -663,9 +663,9 @@ int BaseMolecule::transformHELMtoSGroups(Array& helm_class, Array& h Superatom& lvsg = (Superatom&)sgroups.getSGroup(lvidx); lvsg.atoms.push(i); if (strncmp(r_names.at(r_num), "O", 1) == 0 && strlen(r_names.at(r_num)) == 1) - lvsg.subscript.readString("OH", true); + lvsg.label.readString("OH", true); else - lvsg.subscript.readString(r_names.at(r_num), true); + lvsg.label.readString(r_names.at(r_num), true); lvsg.sa_class.readString("LGRP", true); asMolecule().resetAtom(i, Element::fromString(r_names.at(r_num))); diff --git a/core/indigo-core/molecule/src/base_molecule_sgroups.cpp b/core/indigo-core/molecule/src/base_molecule_sgroups.cpp index bf89e78a97..c5d6bab74a 100644 --- a/core/indigo-core/molecule/src/base_molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/base_molecule_sgroups.cpp @@ -770,8 +770,8 @@ int BaseMolecule::transformFullCTABtoSCSR(ObjArray& templates) if (use_scsr_name) { - if ( (tg.tgroup_name.memcmp(su.subscript) == -1) && - (tg.tgroup_alias.memcmp(su.subscript) == -1) ) + if ( (tg.tgroup_name.memcmp(su.label) == -1) && + (tg.tgroup_alias.memcmp(su.label) == -1) ) { continue; } diff --git a/core/indigo-core/molecule/src/base_molecule_templates.cpp b/core/indigo-core/molecule/src/base_molecule_templates.cpp index 936e83575a..b7d6abb885 100644 --- a/core/indigo-core/molecule/src/base_molecule_templates.cpp +++ b/core/indigo-core/molecule/src/base_molecule_templates.cpp @@ -709,7 +709,7 @@ bool BaseMolecule::_replaceExpandedMonomerWithTemplate(int sg_idx, int& tg_id, M std::unordered_map& added_templates, Array& remove_atoms) { auto& sa = static_cast(sgroups.getSGroup(sg_idx)); - if (!sgroups.hasSGroup(sg_idx) || sa.subscript.size() == 0 || sa.sa_class.size() == 0) + if (!sgroups.hasSGroup(sg_idx) || sa.label.size() == 0 || sa.sa_class.size() == 0) return false; // skip LGRP @@ -734,7 +734,7 @@ bool BaseMolecule::_replaceExpandedMonomerWithTemplate(int sg_idx, int& tg_id, M } // find or create template group for residue - auto template_inchi_id = monomerNameByAlias(sa.sa_class.ptr(), sa.subscript.ptr()) + "/" + std::string(sa.sa_class.ptr()) + "/" + residue_inchi_str; + auto template_inchi_id = monomerNameByAlias(sa.sa_class.ptr(), sa.label.ptr()) + "/" + std::string(sa.sa_class.ptr()) + "/" + residue_inchi_str; auto it_added = added_templates.find(template_inchi_id); bool is_added = it_added == added_templates.end(); int tg_index = is_added ? tgroups.addTGroup() : it_added->second; @@ -744,8 +744,8 @@ bool BaseMolecule::_replaceExpandedMonomerWithTemplate(int sg_idx, int& tg_id, M { tg.tgroup_id = ++tg_id; tg.tgroup_class.copy(sa.sa_class); - if (sa.subscript.size()) - tg.tgroup_name.copy(sa.subscript); + if (sa.label.size()) + tg.tgroup_name.copy(sa.label); if (sa.sa_natreplace.size() > 0) tg.tgroup_natreplace.copy(sa.sa_natreplace); res = _restoreTemplateFromLibrary(tg, mtl, residue_inchi_str); @@ -765,7 +765,7 @@ bool BaseMolecule::_replaceExpandedMonomerWithTemplate(int sg_idx, int& tg_id, M Transformation tform; if (affine && tform.fromAffineMatrix(transform)) { - int ta_idx = addTemplateAtom(sa.subscript.ptr()); + int ta_idx = addTemplateAtom(sa.label.ptr()); setAtomXyz(ta_idx, tform.shift); tform.shift.clear(); if (tform.hasTransformation()) @@ -811,7 +811,7 @@ int BaseMolecule::_transformSGroupToTGroup(int sg_idx, int& tg_id) Superatom& su = (Superatom&)sgroups.getSGroup(sg_idx); - if (su.subscript.size() == 0) + if (su.label.size() == 0) return -1; if (su.sa_class.size() && std::string(su.sa_class.ptr()) == "LGRP") @@ -918,8 +918,8 @@ int BaseMolecule::_transformSGroupToTGroup(int sg_idx, int& tg_id) tg.tgroup_class.copy(su.sa_class); - if (su.subscript.size() > 0) - tg.tgroup_name.copy(su.subscript); + if (su.label.size() > 0) + tg.tgroup_name.copy(su.label); tg.tgroup_alias.clear(); tg.tgroup_comment.clear(); tg.tgroup_full_name.clear(); @@ -956,7 +956,7 @@ int BaseMolecule::_transformSGroupToTGroup(int sg_idx, int& tg_id) else { Superatom& sup_new = (Superatom&)sg; - if ((strcmp(su.subscript.ptr(), sup_new.subscript.ptr()) == 0) && (su.attachment_points.size() == sup_new.attachment_points.size())) + if ((strcmp(su.label.ptr(), sup_new.label.ptr()) == 0) && (su.attachment_points.size() == sup_new.attachment_points.size())) { residue_sg_idx = fragment_sgroups[j]; } @@ -1163,7 +1163,7 @@ int BaseMolecule::_createSGroupFromFragment(Array& sg_atoms, const TGroup& Superatom& su_new = (Superatom&)sgroups.getSGroup(new_sg_idx); su_new.atoms.copy(sg_atoms); - su_new.subscript.copy(tg.tgroup_name); + su_new.label.copy(tg.tgroup_name); su_new.sa_class.copy(tg.tgroup_class); su_new.sa_natreplace.copy(tg.tgroup_natreplace); @@ -1208,11 +1208,11 @@ void BaseMolecule::_removeAtomsFromSGroup(SGroup& sgroup, Array& mapping) if (mapping[sgroup.atoms[i]] == -1) sgroup.atoms.remove(i); } - for (i = sgroup.xbonds.size() - 1; i >= 0; i--) + for (i = sgroup.getBonds().size() - 1; i >= 0; i--) { - const Edge& edge = getEdge(sgroup.xbonds[i]); + const Edge& edge = getEdge(sgroup.getBonds()[i]); if (mapping[edge.beg] == -1 || mapping[edge.end] == -1) - sgroup.xbonds.remove(i); + sgroup.getBonds().remove(i); } updateEditRevision(); } @@ -1260,10 +1260,10 @@ void BaseMolecule::_removeBondsFromSGroup(SGroup& sgroup, Array& mapping) { int i; - for (i = sgroup.xbonds.size() - 1; i >= 0; i--) + for (i = sgroup.getBonds().size() - 1; i >= 0; i--) { - if (mapping[sgroup.xbonds[i]] == -1) - sgroup.xbonds.remove(i); + if (mapping[sgroup.getBonds()[i]] == -1) + sgroup.getBonds().remove(i); } updateEditRevision(); } @@ -1312,17 +1312,17 @@ bool BaseMolecule::_mergeSGroupWithSubmolecule(SGroup& sgroup, SGroup& super, Ba merged = true; } } - for (i = 0; i < super.xbonds.size(); i++) + for (i = 0; i < super.getBonds().size(); i++) { - const Edge& edge = supermol.getEdge(super.xbonds[i]); + const Edge& edge = supermol.getEdge(super.getBonds()[i]); - if (edge_mapping[super.xbonds[i]] < 0) + if (edge_mapping[super.getBonds()[i]] < 0) continue; if (mapping[edge.beg] < 0 || mapping[edge.end] < 0) throw Error("internal: edge is not mapped"); - sgroup.xbonds.push(edge_mapping[super.xbonds[i]]); + sgroup.getBonds().push(edge_mapping[super.getBonds()[i]]); merged = true; } diff --git a/core/indigo-core/molecule/src/cmf_loader.cpp b/core/indigo-core/molecule/src/cmf_loader.cpp index 470ce0c051..c6ebce0ed6 100644 --- a/core/indigo-core/molecule/src/cmf_loader.cpp +++ b/core/indigo-core/molecule/src/cmf_loader.cpp @@ -743,7 +743,7 @@ void CmfLoader::_readSGroup(int code, Molecule& mol) Superatom& s = (Superatom&)mol.sgroups.getSGroup(idx); _readGeneralSGroup(s); - _readString(s.subscript); + _readString(s.label); _readString(s.sa_class); byte bits = _scanner->readByte(); if (bits & 0x01) // -1 and 1 are the same from here @@ -768,9 +768,9 @@ void CmfLoader::_readSGroup(int code, Molecule& mol) _readGeneralSGroup(s); if (version >= 2) - _readString(s.subscript); + _readString(s.label); else - s.subscript.readString("n", true); + s.label.readString("n", true); s.connectivity = _scanner->readPackedUInt(); } diff --git a/core/indigo-core/molecule/src/cmf_saver.cpp b/core/indigo-core/molecule/src/cmf_saver.cpp index 51116c61f7..d48f417ca0 100644 --- a/core/indigo-core/molecule/src/cmf_saver.cpp +++ b/core/indigo-core/molecule/src/cmf_saver.cpp @@ -351,7 +351,7 @@ void CmfSaver::_encodeExtSection(Molecule& mol, const Mapping& mapping) Superatom& sa = (Superatom&)sg; _encode(CMF_SUPERATOM); _encodeBaseSGroup(mol, sa, mapping); - _encodeString(sa.subscript); + _encodeString(sa.label); _encodeString(sa.sa_class); byte packed = static_cast(((int)sa.contracted & 0x01) | (sa.bond_connections.size() << 1)); _output->writeByte(packed); @@ -368,7 +368,7 @@ void CmfSaver::_encodeExtSection(Molecule& mol, const Mapping& mapping) RepeatingUnit& su = (RepeatingUnit&)sg; _encode(CMF_REPEATINGUNIT); _encodeBaseSGroup(mol, su, mapping); - _encodeString(su.subscript); + _encodeString(su.label); _output->writePackedUInt(su.connectivity); } else if (sg.sgroup_type == SGroup::SG_TYPE_MUL) diff --git a/core/indigo-core/molecule/src/cml_loader.cpp b/core/indigo-core/molecule/src/cml_loader.cpp index 157f2e5caf..de8751f65a 100644 --- a/core/indigo-core/molecule/src/cml_loader.cpp +++ b/core/indigo-core/molecule/src/cml_loader.cpp @@ -1591,7 +1591,7 @@ void CmlLoader::_loadSGroupElement(XMLElement* elem, std::unordered_mapAttribute("title"); if (title != 0) - sru->subscript.readString(title, true); + sru->label.readString(title, true); const char* connect = elem->Attribute("connect"); if (connect != 0) @@ -1815,7 +1815,7 @@ void CmlLoader::_loadSGroupElement(XMLElement* elem, std::unordered_mapAttribute("title"); if (title != 0) - sup->subscript.readString(title, true); + sup->label.readString(title, true); XMLNode* pChild; for (pChild = elem->FirstChild(); pChild != 0; pChild = pChild->NextSibling()) diff --git a/core/indigo-core/molecule/src/cml_saver.cpp b/core/indigo-core/molecule/src/cml_saver.cpp index 7d3bb288c2..89b0b5a69f 100644 --- a/core/indigo-core/molecule/src/cml_saver.cpp +++ b/core/indigo-core/molecule/src/cml_saver.cpp @@ -715,7 +715,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup Superatom& sup = (Superatom&)sgroup; - const char* name = sup.subscript.ptr(); + const char* name = sup.label.ptr(); if (name != 0 && strlen(name) > 0) { sg->SetAttribute("title", name); @@ -737,7 +737,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup RepeatingUnit& sru = (RepeatingUnit&)sgroup; - const char* name = sru.subscript.ptr(); + const char* name = sru.label.ptr(); if (name != 0 && strlen(name) > 0) { sg->SetAttribute("title", name); diff --git a/core/indigo-core/molecule/src/ket_objects.cpp b/core/indigo-core/molecule/src/ket_objects.cpp index 00b18a1a33..cc480c0af9 100644 --- a/core/indigo-core/molecule/src/ket_objects.cpp +++ b/core/indigo-core/molecule/src/ket_objects.cpp @@ -172,7 +172,7 @@ IMPL_ERROR(KetRUSGroup, "Ket RU SGroup") const std::map& KetRUSGroup::getStringPropStrToIdx() const { static std::map str_to_idx{ - {"subscript", toUType(StringProps::subscript)}, + {"subscript", toUType(StringProps::label)}, }; return str_to_idx; }; diff --git a/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp b/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp index d0a0017983..d3144a29e9 100644 --- a/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp +++ b/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp @@ -1203,7 +1203,7 @@ void MoleculeCdxmlLoader::_addBracket(BaseMolecule& mol, const CdxmlBracket& bra { Superatom& sa = (Superatom&)sgroup; sa.contracted = DisplayOption::Contracted; - sa.subscript.readString(bracket.label.c_str(), true); + sa.label.readString(bracket.label.c_str(), true); sa.display_position.copy(bracket.superatom_position); } else @@ -1212,7 +1212,7 @@ void MoleculeCdxmlLoader::_addBracket(BaseMolecule& mol, const CdxmlBracket& bra case kCDXBracketUsage_SRU: { RepeatingUnit& ru = (RepeatingUnit&)sgroup; ru.connectivity = bracket.repeat_pattern; - ru.subscript.readString(bracket.label.c_str(), true); + ru.label.readString(bracket.label.c_str(), true); } break; case kCDXBracketUsage_MultipleGroup: { diff --git a/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp b/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp index 45f13e117a..98d5c4b1d8 100644 --- a/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp @@ -1130,7 +1130,7 @@ void MoleculeCdxmlSaver::addFragmentNodes(BaseMolecule& mol, tinyxml2::XMLElemen } auto& sa = (Superatom&)mol.sgroups.getSGroup(kvp.first); - if (sa.subscript.size()) + if (sa.label.size()) { XMLElement* t = _doc->NewElement("t"); node->LinkEndChild(t); @@ -1147,7 +1147,7 @@ void MoleculeCdxmlSaver::addFragmentNodes(BaseMolecule& mol, tinyxml2::XMLElemen t->SetAttribute("LabelAlignment", "Above"); XMLElement* s = _doc->NewElement("s"); t->LinkEndChild(s); - XMLText* txt = _doc->NewText(sa.subscript.ptr()); + XMLText* txt = _doc->NewText(sa.label.ptr()); s->LinkEndChild(txt); } } @@ -2172,7 +2172,7 @@ void MoleculeCdxmlSaver::deleteNamelessSGroups(BaseMolecule& bmol) if (sg.sgroup_type == SGroup::SG_TYPE_SUP) { auto& sa = static_cast(sg); - if (sa.subscript.size() == 0 || std::string(sa.subscript.ptr()).size() == 0) + if (sa.label.size() == 0 || std::string(sa.label.ptr()).size() == 0) bmol.sgroups.remove(j); } } diff --git a/core/indigo-core/molecule/src/molecule_gross_formula.cpp b/core/indigo-core/molecule/src/molecule_gross_formula.cpp index 1f9673c71d..3d7c791bae 100644 --- a/core/indigo-core/molecule/src/molecule_gross_formula.cpp +++ b/core/indigo-core/molecule/src/molecule_gross_formula.cpp @@ -155,7 +155,7 @@ std::unique_ptr MoleculeGrossFormula::collect(BaseMolecule& mol, bo { RepeatingUnit* ru = (RepeatingUnit*)&mol.sgroups.getSGroup(i - 1, SGroup::SG_TYPE_SRU); filters[i].copy(ru->atoms); - indices[i].copy(ru->subscript.ptr(), ru->subscript.size() - 1); // Remove '0' symbol at the end + indices[i].copy(ru->label.ptr(), ru->label.size() - 1); // Remove '0' symbol at the end // Filter polymer atoms for (int j = 0; j < filters[i].size(); j++) { diff --git a/core/indigo-core/molecule/src/molecule_json_loader.cpp b/core/indigo-core/molecule/src/molecule_json_loader.cpp index eaaf9b079d..c99a9c5a22 100644 --- a/core/indigo-core/molecule/src/molecule_json_loader.cpp +++ b/core/indigo-core/molecule/src/molecule_json_loader.cpp @@ -1055,7 +1055,7 @@ void MoleculeJsonLoader::parseSGroups(const rapidjson::Value& sgroups, BaseMolec RepeatingUnit& ru = (RepeatingUnit&)sgroup; if (s.HasMember("subscript")) { - sgroup.subscript.readString(s["subscript"].GetString(), true); + sgroup.label.readString(s["subscript"].GetString(), true); } if (s.HasMember("connectivity")) @@ -1073,7 +1073,7 @@ void MoleculeJsonLoader::parseSGroups(const rapidjson::Value& sgroups, BaseMolec case SGroup::SG_TYPE_SUP: { Superatom& sg = (Superatom&)sgroup; if (s.HasMember("name")) - sgroup.subscript.readString(s["name"].GetString(), true); + sgroup.label.readString(s["name"].GetString(), true); if (s.HasMember("expanded")) sg.contracted = s["expanded"].GetBool() ? DisplayOption::Expanded : DisplayOption::Contracted; if (s.HasMember("class")) @@ -1518,7 +1518,7 @@ int MoleculeJsonLoader::parseMonomerTemplate(const rapidjson::Value& monomer_tem } } sa.sa_class.appendString("LGRP", true); - sa.subscript.appendString(group_name.c_str(), true); + sa.label.appendString(group_name.c_str(), true); att_desc.leaving_group = grp_idx; fillXBondsAndBrackets(sa, monomer_mol); @@ -1559,7 +1559,7 @@ int MoleculeJsonLoader::parseMonomerTemplate(const rapidjson::Value& monomer_tem sa.sa_class.appendString(mclass.c_str(), true); if (alias.size()) - sa.subscript.appendString(alias.c_str(), true); + sa.label.appendString(alias.c_str(), true); if (natreplace.size()) sa.sa_natreplace.appendString(natreplace.c_str(), true); diff --git a/core/indigo-core/molecule/src/molecule_json_saver.cpp b/core/indigo-core/molecule/src/molecule_json_saver.cpp index c56a889e08..713cce3376 100644 --- a/core/indigo-core/molecule/src/molecule_json_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_json_saver.cpp @@ -364,7 +364,7 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) case SGroup::SG_TYPE_SUP: { Superatom& sa = (Superatom&)sgroup; writer.Key("name"); - writer.String(sgroup.subscript.size() ? sgroup.subscript.ptr() : ""); + writer.String(sgroup.label.size() ? sgroup.label.ptr() : ""); if (sa.contracted == DisplayOption::Expanded) { writer.Key("expanded"); @@ -406,10 +406,10 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) break; case SGroup::SG_TYPE_SRU: { RepeatingUnit& ru = (RepeatingUnit&)sgroup; - if (sgroup.subscript.size()) + if (sgroup.label.size()) { writer.Key("subscript"); - writer.String(sgroup.subscript.ptr()); + writer.String(sgroup.label.ptr()); } writer.Key("connectivity"); diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index a2846495f2..79f441715c 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -510,7 +510,7 @@ void MoleculeSGroups::findSGroups(int property, const char* str, Array& sgs for (i = _sgroups.begin(); i != _sgroups.end(); i = _sgroups.next(i)) { SGroup& sg = *_sgroups.at(i); - BufferScanner sc(sg.subscript); + BufferScanner sc(sg.label); if (sc.findWordIgnoreCase(str)) { sgs.push(i); diff --git a/core/indigo-core/molecule/src/molfile_loader_v2000.cpp b/core/indigo-core/molecule/src/molfile_loader_v2000.cpp index 983840a67a..1e1137c926 100644 --- a/core/indigo-core/molecule/src/molfile_loader_v2000.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_v2000.cpp @@ -1081,7 +1081,7 @@ void MolfileLoader::_readCtab2000() { SGroup& sgroup = _bmol->sgroups.getSGroup(_sgroup_mapping[sgroup_idx]); _scanner.skip(1); - _scanner.readQuotedLine(sgroup.subscript, true); + _scanner.readQuotedLine(sgroup.label, true); } } else if (strncmp(chars, "SCL", 3) == 0) diff --git a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp index 7125602ce7..8401185245 100644 --- a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp @@ -1341,9 +1341,9 @@ void MolfileLoader::_readSGroup3000(const char* str) } if (c == ' ' && !has_quote) break; - sgroup->subscript.push(c); + sgroup->label.push(c); } - sgroup->subscript.push(0); + sgroup->label.push(0); } else if (strcmp(entity.ptr(), "CLASS") == 0) { diff --git a/core/indigo-core/molecule/src/molfile_saver.cpp b/core/indigo-core/molecule/src/molfile_saver.cpp index c0705b22a4..4816a9d4b9 100644 --- a/core/indigo-core/molecule/src/molfile_saver.cpp +++ b/core/indigo-core/molecule/src/molfile_saver.cpp @@ -905,12 +905,12 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) sup.bond_connections[j].bond_dir.y, 0.f); } } - if (sgroup.subscript.size() > 1) + if (sgroup.label.size() > 1) { - if (sgroup.subscript.find(' ') > -1) - out.printf(" LABEL=\"%s\"", sgroup.subscript.ptr()); + if (sgroup.label.find(' ') > -1) + out.printf(" LABEL=\"%s\"", sgroup.label.ptr()); else - out.printf(" LABEL=%s", sgroup.subscript.ptr()); + out.printf(" LABEL=%s", sgroup.label.ptr()); } // convert CHEM to LINKER for BIOVIA if (sup.sa_class.size() > 1) @@ -1022,12 +1022,12 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) out.printf(" CONNECT=HT"); else out.printf(" CONNECT=EU"); - if (sgroup.subscript.size() > 1) + if (sgroup.label.size() > 1) { - if (sgroup.subscript.find(' ') > -1) - out.printf(" LABEL=\"%s\"", sgroup.subscript.ptr()); + if (sgroup.label.find(' ') > -1) + out.printf(" LABEL=\"%s\"", sgroup.label.ptr()); else - out.printf(" LABEL=%s", sgroup.subscript.ptr()); + out.printf(" LABEL=%s", sgroup.label.ptr()); } _writeMultiString(output, buf.ptr(), buf.size()); } @@ -1769,12 +1769,12 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) } // Write SMT (subscript/label) for any SGroup type that has it - if (sgroup.sgroup_type != SGroup::SG_TYPE_MUL && sgroup.subscript.size() > 1) + if (sgroup.sgroup_type != SGroup::SG_TYPE_MUL && sgroup.label.size() > 1) { - if (sgroup.subscript.find(' ') > -1) - output.printfCR("M SMT %3d \"%s\"", sgroup.original_group, sgroup.subscript.ptr()); + if (sgroup.label.find(' ') > -1) + output.printfCR("M SMT %3d \"%s\"", sgroup.original_group, sgroup.label.ptr()); else - output.printfCR("M SMT %3d %s", sgroup.original_group, sgroup.subscript.ptr()); + output.printfCR("M SMT %3d %s", sgroup.original_group, sgroup.label.ptr()); } if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) diff --git a/core/indigo-core/molecule/src/sequence_saver.cpp b/core/indigo-core/molecule/src/sequence_saver.cpp index 8e3bef194b..1afbf2aeb8 100644 --- a/core/indigo-core/molecule/src/sequence_saver.cpp +++ b/core/indigo-core/molecule/src/sequence_saver.cpp @@ -1141,7 +1141,7 @@ std::string SequenceSaver::saveHELM(KetDocument& document, const std::vector(sgroup); - if (sa.subscript.size() != 0 && sa.subscript.ptr()[0] != 0) + if (sa.label.size() != 0 && sa.label.ptr()[0] != 0) continue; // convert leaving atom H to rg-ref auto res = mol_atom_to_ap.try_emplace(mol_id); diff --git a/core/indigo-core/molecule/src/smiles_loader_parsers.cpp b/core/indigo-core/molecule/src/smiles_loader_parsers.cpp index da9f0c3d78..ef8179dc3b 100644 --- a/core/indigo-core/molecule/src/smiles_loader_parsers.cpp +++ b/core/indigo-core/molecule/src/smiles_loader_parsers.cpp @@ -848,7 +848,7 @@ void SmilesLoader::_readOtherStuff() { RepeatingUnit& ru = static_cast(sgroup); if (subscript.size()) - ru.subscript.readString(subscript.ptr(), true); + ru.label.readString(subscript.ptr(), true); if (connectivity == "ht") ru.connectivity = RepeatingUnit::HEAD_TO_TAIL; else if (connectivity == "hh") diff --git a/core/indigo-core/molecule/src/smiles_saver.cpp b/core/indigo-core/molecule/src/smiles_saver.cpp index 8cbb93824c..80ebb28991 100644 --- a/core/indigo-core/molecule/src/smiles_saver.cpp +++ b/core/indigo-core/molecule/src/smiles_saver.cpp @@ -1874,7 +1874,7 @@ void SmilesSaver::_writeSGroups() RepeatingUnit& ru = static_cast(sg); _output.writeString("n:"); _writeSGroupAtoms(sg); - _output.printf(":%s:", ru.subscript.ptr() ? ru.subscript.ptr() : ""); + _output.printf(":%s:", ru.label.ptr() ? ru.label.ptr() : ""); switch (ru.connectivity) { case SGroup::HEAD_TO_TAIL: diff --git a/core/render2d/src/render_internal.cpp b/core/render2d/src/render_internal.cpp index 7606f7de69..02429f239a 100644 --- a/core/render2d/src/render_internal.cpp +++ b/core/render2d/src/render_internal.cpp @@ -620,7 +620,7 @@ void MoleculeRenderInternal::_initSGroups(Tree& sgroups, Rect2f parent) int tiIndex = _pushTextItem(sg, RenderItem::RIT_SGROUP); TextItem& index = _data.textitems[tiIndex]; index.fontsize = FONT_SIZE_ATTR; - bprintf(index.text, group.subscript.size() > 0 ? group.subscript.ptr() : "n"); + bprintf(index.text, group.label.size() > 0 ? group.label.ptr() : "n"); _positionIndex(sg, tiIndex, true); if (group.connectivity != RepeatingUnit::HEAD_TO_TAIL) { @@ -668,12 +668,12 @@ void MoleculeRenderInternal::_initSGroups(Tree& sgroups, Rect2f parent) _placeBrackets(sg, group.atoms, brackets); _loadBrackets(sg, brackets); - if (group.subscript.size() == 0 || std::string(group.subscript.ptr()).empty()) + if (group.label.size() == 0 || std::string(group.label.ptr()).empty()) sg.hide_brackets = true; int tiIndex = _pushTextItem(sg, RenderItem::RIT_SGROUP); TextItem& index = _data.textitems[tiIndex]; index.fontsize = FONT_SIZE_ATTR; - bprintf(index.text, "%s", group.subscript.ptr()); + bprintf(index.text, "%s", group.label.ptr()); _positionIndex(sg, tiIndex, true); parent = ILLEGAL_RECT(); @@ -850,13 +850,13 @@ void MoleculeRenderInternal::_prepareSGroups(bool collapseAtLeastOneSuperatom) if (mol.isQueryMolecule()) { - superAtomID = mol.asQueryMolecule().addAtom(new QueryMolecule::Atom(QueryMolecule::ATOM_PSEUDO, group.subscript.ptr())); + superAtomID = mol.asQueryMolecule().addAtom(new QueryMolecule::Atom(QueryMolecule::ATOM_PSEUDO, group.label.ptr())); } else { Molecule& amol = mol.asMolecule(); superAtomID = amol.addAtom(ELEM_PSEUDO); - amol.setPseudoAtom(superAtomID, group.subscript.ptr()); + amol.setPseudoAtom(superAtomID, group.label.ptr()); } QS_DEF(RedBlackSet, groupAtoms); groupAtoms.clear(); From 849286b4e2d450c4fe22eef855d24cc466cafbf9 Mon Sep 17 00:00:00 2001 From: even1024 Date: Mon, 4 May 2026 08:40:12 +0200 Subject: [PATCH 09/33] refactor --- core/indigo-core/molecule/molecule_sgroups.h | 22 +++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index 6244597c43..0bb6aa5fe8 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -118,10 +118,16 @@ namespace indigo Array atoms; // represented with SAL in Molfile format Array xbonds; // crossing bonds, represented with XBONDS/SBL in Molfile format - virtual const Array& getBonds() const { return xbonds; } - virtual Array& getBonds() { return xbonds; } + virtual const Array& getBonds() const + { + return xbonds; + } + virtual Array& getBonds() + { + return xbonds; + } - Array label; // SMT in Molfile format (LABEL in V3000) + Array label; // SMT in Molfile format (LABEL in V3000) int brk_style; // represented with SBT in Molfile format Array brackets; // represented with SDI in Molfile format DisplayOption contracted; // display option (-1 if undefined, 0 - expanded, 1 - contracted) @@ -141,8 +147,14 @@ namespace indigo Array cbonds; // chemical bonds, represented with CBONDS/SBL in Molfile format - const Array& getBonds() const override { return cbonds; } - Array& getBonds() override { return cbonds; } + const Array& getBonds() const override + { + return cbonds; + } + Array& getBonds() override + { + return cbonds; + } Array description; // SDT in Molfile format (filed units or format) Array name; // SDT in Molfile format (field name) From 8f6e5346e185b0780f37632f1078067ee6660e77 Mon Sep 17 00:00:00 2001 From: even1024 Date: Thu, 7 May 2026 12:44:20 +0200 Subject: [PATCH 10/33] nullable refactoring --- .../indigo/src/indigo_molecule_operations.cpp | 18 ++--- core/indigo-core/common/base_cpp/nullable.h | 45 +++++++++++-- core/indigo-core/layout/src/metalayout.cpp | 4 +- .../layout/src/molecule_layout.cpp | 2 +- core/indigo-core/molecule/molecule_sgroups.h | 55 +++++++-------- .../molecule/src/base_molecule.cpp | 2 +- .../molecule/src/base_molecule_sgroups.cpp | 2 +- core/indigo-core/molecule/src/cmf_saver.cpp | 9 +-- core/indigo-core/molecule/src/cml_loader.cpp | 6 +- core/indigo-core/molecule/src/cml_saver.cpp | 6 +- .../molecule/src/molecule_cdxml_loader.cpp | 2 +- .../molecule/src/molecule_cdxml_saver.cpp | 4 +- .../molecule/src/molecule_cip_calculator.cpp | 8 +-- .../molecule/src/molecule_json_loader.cpp | 4 +- .../molecule/src/molecule_json_saver.cpp | 4 +- .../molecule/src/molecule_sgroups.cpp | 2 +- .../molecule/src/molfile_loader_v3000.cpp | 4 +- .../molecule/src/molfile_saver.cpp | 67 ++++++++++--------- .../molecule/src/smiles_loader_parsers.cpp | 4 +- core/indigo-core/tests/tests/formats.cpp | 12 ++-- core/render2d/src/render_internal.cpp | 10 +-- 21 files changed, 158 insertions(+), 112 deletions(-) diff --git a/api/c/indigo/src/indigo_molecule_operations.cpp b/api/c/indigo/src/indigo_molecule_operations.cpp index 4b6188d502..09497db2da 100644 --- a/api/c/indigo/src/indigo_molecule_operations.cpp +++ b/api/c/indigo/src/indigo_molecule_operations.cpp @@ -1403,8 +1403,8 @@ CEXPORT int indigoSetDataSGroupXY(int sgroup, float x, float y, const char* opti { DataSGroup& dsg = IndigoDataSGroup::cast(self.getObject(sgroup)).get(); - dsg.display_pos.x = x; - dsg.display_pos.y = y; + dsg.display_pos->x = x; + dsg.display_pos->y = y; dsg.detached = true; if (options != 0 && options[0] != 0) @@ -1442,8 +1442,8 @@ CEXPORT int indigoSetSGroupCoords(int sgroup, float x, float y) { DataSGroup& dsg = IndigoDataSGroup::cast(self.getObject(sgroup)).get(); - dsg.display_pos.x = x; - dsg.display_pos.y = y; + dsg.display_pos->x = x; + dsg.display_pos->y = y; return 1; } @@ -1602,7 +1602,7 @@ CEXPORT int indigoSetSGroupXCoord(int sgroup, float x) { DataSGroup& dsg = IndigoDataSGroup::cast(self.getObject(sgroup)).get(); - dsg.display_pos.x = x; + dsg.display_pos->x = x; return 1; } @@ -1615,7 +1615,7 @@ CEXPORT int indigoSetSGroupYCoord(int sgroup, float y) { DataSGroup& dsg = IndigoDataSGroup::cast(self.getObject(sgroup)).get(); - dsg.display_pos.y = y; + dsg.display_pos->y = y; return 1; } @@ -2021,7 +2021,7 @@ CEXPORT int indigoGetSGroupDisplayOption(int sgroup) { Superatom& sup = IndigoSuperatom::cast(self.getObject(sgroup)).get(); if (sup.contracted > DisplayOption::Undefined) - return (int)sup.contracted; + return (int)(sup.contracted.hasValue() ? sup.contracted.get() : DisplayOption::Undefined); return 0; } @@ -2060,8 +2060,8 @@ CEXPORT float* indigoGetSGroupCoords(int sgroup) auto& tmp = self.getThreadTmpData(); auto& xy = ds.get().display_pos; - tmp.xyz[0] = xy.x; - tmp.xyz[1] = xy.y; + tmp.xyz[0] = xy->x; + tmp.xyz[1] = xy->y; tmp.xyz[2] = 0.f; return tmp.xyz; } diff --git a/core/indigo-core/common/base_cpp/nullable.h b/core/indigo-core/common/base_cpp/nullable.h index d1b9085770..c06576288e 100644 --- a/core/indigo-core/common/base_cpp/nullable.h +++ b/core/indigo-core/common/base_cpp/nullable.h @@ -31,23 +31,60 @@ namespace indigo class Nullable { public: - Nullable() : _has_value(false) + Nullable() : _has_value(false), _value{} { variable_name.readString("", true); } + Nullable(const Nullable& other) : _has_value(other._has_value), _value(other._value) + { + variable_name.copy(other.variable_name); + } + + Nullable& operator=(const Nullable& other) + { + _has_value = other._has_value; + _value = other._value; + variable_name.copy(other.variable_name); + return *this; + } + const T& get() const { - if (!_has_value) - throw Error("\"%s\" variable was not set", variable_name.ptr()); return _value; } - operator const T&() const + T& get() + { + return _value; + } + + bool operator==(const T& other) const + { + return _value == other; + } + + bool operator!=(const T& other) const + { + return _value != other; + } + + operator T&() + { return get(); } + const T* operator->() const + { + return &get(); + } + + T* operator->() + { + return &get(); + } + Nullable& operator=(const T& value) { set(value); diff --git a/core/indigo-core/layout/src/metalayout.cpp b/core/indigo-core/layout/src/metalayout.cpp index 0c2390887b..1e250541e2 100644 --- a/core/indigo-core/layout/src/metalayout.cpp +++ b/core/indigo-core/layout/src/metalayout.cpp @@ -325,8 +325,8 @@ void Metalayout::adjustMol(BaseMolecule& mol, const Vec2f& min, const Vec2f& pos { Vec2f new_center; mol.getSGroupAtomsCenterPoint(group, new_center); - group.display_pos.add(new_center); - group.display_pos.sub(data_centers[i]); + group.display_pos->add(new_center); + group.display_pos->sub(data_centers[i]); } } } diff --git a/core/indigo-core/layout/src/molecule_layout.cpp b/core/indigo-core/layout/src/molecule_layout.cpp index c5f469844f..6a8db6ec3c 100644 --- a/core/indigo-core/layout/src/molecule_layout.cpp +++ b/core/indigo-core/layout/src/molecule_layout.cpp @@ -359,7 +359,7 @@ void MoleculeLayout::_updateDataSGroups() Vec2f delta; delta.diff(after, before); - group.display_pos.add(delta); + group.display_pos->add(delta); } } } diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index 0bb6aa5fe8..0984fa2c8b 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -20,6 +20,7 @@ #define __molecule_sgroups__ #include "base_cpp/array.h" +#include "base_cpp/nullable.h" #include "base_cpp/obj_pool.h" #include "base_cpp/ptr_pool.h" #include "math/algebra.h" @@ -108,11 +109,11 @@ namespace indigo SGroup(); virtual ~SGroup(); - int sgroup_type; // group type, represnted with STY in Molfile format - int sgroup_subtype; // group subtype, represnted with SST in Molfile format - int original_group; // original group number - int parent_group; // parent group number; represented with SPL in Molfile format - int parent_idx; // parent group number; represented with index in the array + int sgroup_type; // group type, represnted with STY in Molfile format + Nullable sgroup_subtype; // group subtype, represnted with SST in Molfile format + Nullable original_group; // original group number + Nullable parent_group; // parent group number; represented with SPL in Molfile format + Nullable parent_idx; // parent group number; represented with index in the array // TODO: leave only parent_idx Array atoms; // represented with SAL in Molfile format @@ -127,10 +128,10 @@ namespace indigo return xbonds; } - Array label; // SMT in Molfile format (LABEL in V3000) - int brk_style; // represented with SBT in Molfile format - Array brackets; // represented with SDI in Molfile format - DisplayOption contracted; // display option (-1 if undefined, 0 - expanded, 1 - contracted) + Array label; // SMT in Molfile format (LABEL in V3000) + Nullable brk_style; // represented with SBT in Molfile format + Array brackets; // represented with SDI in Molfile format + Nullable contracted; // display option (-1 if undefined, 0 - expanded, 1 - contracted) static const char* typeToString(int sg_type); static int getType(const char* sg_type); @@ -156,20 +157,20 @@ namespace indigo return cbonds; } - Array description; // SDT in Molfile format (filed units or format) - Array name; // SDT in Molfile format (field name) - Array type; // SDT in Molfile format (field type) - Array querycode; // SDT in Molfile format (query code) - Array queryoper; // SDT in Molfile format (query operator) - Array data; // SCD/SED in Molfile format (field data) - Array sa_natreplace; // NATREPLACE (V3000 - 2017) - Vec2f display_pos; // SDD in Molfile format - bool detached; // or attached - bool relative; // or absolute + Array description; // SDT in Molfile format (filed units or format) + Array name; // SDT in Molfile format (field name) + Array type; // SDT in Molfile format (field type) + Array querycode; // SDT in Molfile format (query code) + Array queryoper; // SDT in Molfile format (query operator) + Array data; // SCD/SED in Molfile format (field data) + Array sa_natreplace; // NATREPLACE (V3000 - 2017) + Nullable display_pos; // SDD in Molfile format + bool detached; // or attached + bool relative; // or absolute bool display_units; - int num_chars; // number of characters - int dasp_pos; - char tag; // tag + Nullable num_chars; // number of characters + Nullable dasp_pos; + Nullable tag; // tag static constexpr char mrv_implicit_h[] = "MRV_IMPLICIT_H"; static constexpr char impl_prefix[] = "IMPL_H"; static constexpr size_t impl_prefix_len = sizeof(impl_prefix) - 1; @@ -188,7 +189,7 @@ namespace indigo Array sa_class; // SCL in Molfile format // SDS in Molfile format - int seqid; // SEQID (V3000 - 2017) + Nullable seqid; // SEQID (V3000 - 2017) Array sa_natreplace; // NATREPLACE (V3000 - 2017) bool unresolved; @@ -215,7 +216,7 @@ namespace indigo }; Array<_BondConnection> bond_connections; // SBV in Molfile format - Vec3f display_position; + Nullable display_position; private: Superatom(const Superatom&); @@ -227,7 +228,7 @@ namespace indigo RepeatingUnit(); ~RepeatingUnit() override; - int connectivity; + Nullable connectivity; private: RepeatingUnit(const RepeatingUnit&); @@ -239,7 +240,7 @@ namespace indigo CopolymerGroup(); ~CopolymerGroup() override; - int connectivity; + Nullable connectivity; private: CopolymerGroup(const CopolymerGroup&); @@ -252,7 +253,7 @@ namespace indigo ~MultipleGroup() override; Array parent_atoms; - int multiplier; + Nullable multiplier; private: MultipleGroup(const MultipleGroup&); diff --git a/core/indigo-core/molecule/src/base_molecule.cpp b/core/indigo-core/molecule/src/base_molecule.cpp index 3aa8fbf9cb..c20161eee1 100644 --- a/core/indigo-core/molecule/src/base_molecule.cpp +++ b/core/indigo-core/molecule/src/base_molecule.cpp @@ -245,7 +245,7 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma ap.apid.copy(supersa.attachment_points[j].apid); } } - sa.display_position.copy(supersa.display_position); + sa.display_position->copy(supersa.display_position); } else if (sg.sgroup_type == SGroup::SG_TYPE_SRU) { diff --git a/core/indigo-core/molecule/src/base_molecule_sgroups.cpp b/core/indigo-core/molecule/src/base_molecule_sgroups.cpp index c5d6bab74a..0f47f2df93 100644 --- a/core/indigo-core/molecule/src/base_molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/base_molecule_sgroups.cpp @@ -99,7 +99,7 @@ void BaseMolecule::collapse(BaseMolecule& bm, int id, Mapping& mapAtom, Mapping& const MultipleGroup& group = (MultipleGroup&)sg; - if (group.atoms.size() != group.multiplier * group.parent_atoms.size()) + if (group.atoms.size() != group.multiplier.get() * group.parent_atoms.size()) throw Error("The group is already collapsed or invalid"); QS_DEF(Array, toRemove); diff --git a/core/indigo-core/molecule/src/cmf_saver.cpp b/core/indigo-core/molecule/src/cmf_saver.cpp index d48f417ca0..0c7ce6316b 100644 --- a/core/indigo-core/molecule/src/cmf_saver.cpp +++ b/core/indigo-core/molecule/src/cmf_saver.cpp @@ -340,7 +340,7 @@ void CmfSaver::_encodeExtSection(Molecule& mol, const Mapping& mapping) _encodeString(sd.data); // Pack detached, relative, display_units, and sd.dasp_pos into one byte if (sd.dasp_pos < 0 || sd.dasp_pos > 9) - throw Error("DataSGroup dasp_pos field should be less than 10: %d", sd.dasp_pos); + throw Error("DataSGroup dasp_pos field should be less than 10: %d", sd.dasp_pos.get()); byte packed = (sd.dasp_pos & 0x0F) | (sd.detached << 4) | (sd.relative << 5) | (sd.display_units << 6); _output->writeByte(packed); _output->writePackedUInt(sd.num_chars); @@ -353,7 +353,8 @@ void CmfSaver::_encodeExtSection(Molecule& mol, const Mapping& mapping) _encodeBaseSGroup(mol, sa, mapping); _encodeString(sa.label); _encodeString(sa.sa_class); - byte packed = static_cast(((int)sa.contracted & 0x01) | (sa.bond_connections.size() << 1)); + byte packed = static_cast(((int)(sa.contracted.hasValue() ? sa.contracted.get() : DisplayOption::Undefined) & 0x01) | + (sa.bond_connections.size() << 1)); _output->writeByte(packed); if (sa.bond_connections.size() > 0) { @@ -378,7 +379,7 @@ void CmfSaver::_encodeExtSection(Molecule& mol, const Mapping& mapping) _encodeBaseSGroup(mol, sm, mapping); _encodeUIntArray(sm.parent_atoms, *mapping.atom_mapping); if (sm.multiplier < 0) - throw Error("internal error: SGroup multiplier is negative: %d", sm.multiplier); + throw Error("internal error: SGroup multiplier is negative: %d", sm.multiplier.get()); _output->writePackedUInt(sm.multiplier); } } @@ -824,7 +825,7 @@ void CmfSaver::_updateSGroupsXyzMinMax(Molecule& mol, Vec3f& min, Vec3f& max) DataSGroup& s = (DataSGroup&)sg; _updateBaseSGroupXyzMinMax(s, min, max); - Vec3f display_pos(s.display_pos.x, s.display_pos.y, 0); + Vec3f display_pos(s.display_pos->x, s.display_pos->y, 0); min.min(display_pos); max.max(display_pos); diff --git a/core/indigo-core/molecule/src/cml_loader.cpp b/core/indigo-core/molecule/src/cml_loader.cpp index de8751f65a..a2881e842e 100644 --- a/core/indigo-core/molecule/src/cml_loader.cpp +++ b/core/indigo-core/molecule/src/cml_loader.cpp @@ -157,7 +157,7 @@ struct Atom // This methods splits a space-separated string and writes each values into an arbitrary string // property of Atom structure for each atom in the specified list -static void splitStringIntoProperties(const char* s, std::vector& atoms, std::string Atom::*property) +static void splitStringIntoProperties(const char* s, std::vector& atoms, std::string Atom::* property) { if (s == 0) return; @@ -1364,14 +1364,14 @@ void CmlLoader::_loadSGroupElement(XMLElement* elem, std::unordered_mapdisplay_pos.x = strscan.readFloat(); + dsg->display_pos->x = strscan.readFloat(); } const char* disp_y = elem->Attribute("y"); if (disp_y != 0) { BufferScanner strscan(disp_y); - dsg->display_pos.y = strscan.readFloat(); + dsg->display_pos->y = strscan.readFloat(); } const char* detached = elem->Attribute("dataDetached"); diff --git a/core/indigo-core/molecule/src/cml_saver.cpp b/core/indigo-core/molecule/src/cml_saver.cpp index 89b0b5a69f..2044351f25 100644 --- a/core/indigo-core/molecule/src/cml_saver.cpp +++ b/core/indigo-core/molecule/src/cml_saver.cpp @@ -582,7 +582,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup QS_DEF(Array, buf); ArrayOutput out(buf); - out.printf("sg%d", sgroup.original_group); + out.printf("sg%d", sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0); buf.push(0); sg->SetAttribute("id", buf.ptr()); @@ -651,8 +651,8 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup sg->SetAttribute("queryOp", queryoper); } - sg->SetAttribute("x", dsg.display_pos.x); - sg->SetAttribute("y", dsg.display_pos.y); + sg->SetAttribute("x", dsg.display_pos->x); + sg->SetAttribute("y", dsg.display_pos->y); if (!dsg.detached) { diff --git a/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp b/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp index d3144a29e9..4e452ac243 100644 --- a/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp +++ b/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp @@ -1204,7 +1204,7 @@ void MoleculeCdxmlLoader::_addBracket(BaseMolecule& mol, const CdxmlBracket& bra Superatom& sa = (Superatom&)sgroup; sa.contracted = DisplayOption::Contracted; sa.label.readString(bracket.label.c_str(), true); - sa.display_position.copy(bracket.superatom_position); + sa.display_position->copy(bracket.superatom_position); } else switch (bracket.usage) diff --git a/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp b/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp index 98d5c4b1d8..7006818a22 100644 --- a/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp @@ -1134,14 +1134,14 @@ void MoleculeCdxmlSaver::addFragmentNodes(BaseMolecule& mol, tinyxml2::XMLElemen { XMLElement* t = _doc->NewElement("t"); node->LinkEndChild(t); - Vec2f pos(sa.display_position.x + offset.x, -sa.display_position.y - offset.y); + Vec2f pos(sa.display_position->x + offset.x, -sa.display_position->y - offset.y); pos.scale(_bond_length); Vec2f v1(pos.x - _bond_length / 2, pos.y - _bond_length / 2); Vec2f v2(pos.x + _bond_length / 2, pos.y + _bond_length / 2); std::string pos_str = std::to_string(pos.x) + " " + std::to_string(pos.y); Rect2f bbox(v1, v2); std::string bbox_str = boundingBoxToString(bbox); - if (sa.display_position.x != 0.0f && sa.display_position.y != 0.0f) + if (sa.display_position->x != 0.0f && sa.display_position->y != 0.0f) node->SetAttribute("p", pos_str.c_str()); t->SetAttribute("LabelJustification", "Left"); t->SetAttribute("LabelAlignment", "Above"); diff --git a/core/indigo-core/molecule/src/molecule_cip_calculator.cpp b/core/indigo-core/molecule/src/molecule_cip_calculator.cpp index 0c05cd50c3..19dd9af79d 100644 --- a/core/indigo-core/molecule/src/molecule_cip_calculator.cpp +++ b/core/indigo-core/molecule/src/molecule_cip_calculator.cpp @@ -206,8 +206,8 @@ void MoleculeCIPCalculator::addCIPSgroups(BaseMolecule& mol) } sgroup.name.readString("INDIGO_CIP_DESC", true); - sgroup.display_pos.x = 0.0; - sgroup.display_pos.y = 0.0; + sgroup.display_pos->x = 0.0; + sgroup.display_pos->y = 0.0; sgroup.detached = true; sgroup.relative = true; } @@ -229,8 +229,8 @@ void MoleculeCIPCalculator::addCIPSgroups(BaseMolecule& mol) sgroup.data.readString("(Z)", true); sgroup.name.readString("INDIGO_CIP_DESC", true); - sgroup.display_pos.x = 0.0; - sgroup.display_pos.y = 0.0; + sgroup.display_pos->x = 0.0; + sgroup.display_pos->y = 0.0; sgroup.detached = true; sgroup.relative = true; } diff --git a/core/indigo-core/molecule/src/molecule_json_loader.cpp b/core/indigo-core/molecule/src/molecule_json_loader.cpp index c99a9c5a22..353086e6d2 100644 --- a/core/indigo-core/molecule/src/molecule_json_loader.cpp +++ b/core/indigo-core/molecule/src/molecule_json_loader.cpp @@ -1126,10 +1126,10 @@ void MoleculeJsonLoader::parseSGroups(const rapidjson::Value& sgroups, BaseMolec dsg.queryoper.readString(s["queryOp"].GetString(), true); if (s.HasMember("x")) - dsg.display_pos.x = s["x"].GetFloat(); + dsg.display_pos->x = s["x"].GetFloat(); if (s.HasMember("y")) - dsg.display_pos.y = s["y"].GetFloat(); + dsg.display_pos->y = s["y"].GetFloat(); if (s.HasMember("dataDetached")) dsg.detached = s["dataDetached"].GetBool(); diff --git a/core/indigo-core/molecule/src/molecule_json_saver.cpp b/core/indigo-core/molecule/src/molecule_json_saver.cpp index 713cce3376..a5f8ce62bf 100644 --- a/core/indigo-core/molecule/src/molecule_json_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_json_saver.cpp @@ -324,9 +324,9 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) } writer.Key("x"); - writeFloat(writer, dsg.display_pos.x); + writeFloat(writer, dsg.display_pos->x); writer.Key("y"); - writeFloat(writer, dsg.display_pos.y); + writeFloat(writer, dsg.display_pos->y); if (!dsg.detached) { diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index 79f441715c..b1fe7b04bd 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -105,7 +105,7 @@ Superatom::Superatom() : unresolved(false) seqid = -1; attachment_points.clear(); bond_connections.clear(); - display_position.clear(); + display_position->clear(); } Superatom::~Superatom() diff --git a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp index 8401185245..5161894e8b 100644 --- a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp @@ -1657,8 +1657,8 @@ void MolfileLoader::_readSGroupDisplay(Scanner& scanner, DataSGroup& dsg) { int constexpr MIN_SDD_SIZE = 36; bool well_formatted = scanner.length() >= MIN_SDD_SIZE; - dsg.display_pos.x = scanner.readFloatFix(10); - dsg.display_pos.y = scanner.readFloatFix(10); + dsg.display_pos->x = scanner.readFloatFix(10); + dsg.display_pos->y = scanner.readFloatFix(10); int ch = ' '; if (well_formatted) { diff --git a/core/indigo-core/molecule/src/molfile_saver.cpp b/core/indigo-core/molecule/src/molfile_saver.cpp index 4816a9d4b9..1107c49e71 100644 --- a/core/indigo-core/molecule/src/molfile_saver.cpp +++ b/core/indigo-core/molecule/src/molfile_saver.cpp @@ -929,7 +929,7 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) } } if (sup.seqid > 0) - out.printf(" SEQID=%d", sup.seqid); + out.printf(" SEQID=%d", (sup.seqid.hasValue() ? sup.seqid.get() : 0)); if (sup.sa_natreplace.size() > 1) out.printf(" NATREPLACE=%s", sup.sa_natreplace.ptr()); @@ -1062,7 +1062,7 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) out.printf(" %d", _atom_mapping[mg.parent_atoms[j]]); out.printf(")"); } - out.printf(" MULT=%d", mg.multiplier); + out.printf(" MULT=%d", (mg.multiplier.hasValue() ? mg.multiplier.get() : 0)); _writeMultiString(output, buf.ptr(), buf.size()); } else @@ -1109,7 +1109,7 @@ void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, int idx, Output& outp { int i; - output.printf("%d %s %d", sgroup.original_group, SGroup::typeToString(sgroup.sgroup_type), idx); + output.printf("%d %s %d", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), SGroup::typeToString(sgroup.sgroup_type), idx); if (sgroup.atoms.size() > 0) { @@ -1139,7 +1139,7 @@ void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, int idx, Output& outp } if (sgroup.parent_group > 0) { - output.printf(" PARENT=%d", sgroup.parent_group); + output.printf(" PARENT=%d", (sgroup.parent_group.hasValue() ? sgroup.parent_group.get() : 0)); } for (i = 0; i < sgroup.brackets.size(); i++) { @@ -1695,7 +1695,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (i = j; i < std::min(sgroup_ids.size(), j + 8); i++) { SGroup* sgroup = &mol.sgroups.getSGroup(sgroup_ids[i]); - output.printf(" %3d %s", sgroup->original_group, SGroup::typeToString(sgroup->sgroup_type)); + output.printf(" %3d %s", (sgroup->original_group.hasValue() ? sgroup->original_group.get() : 0), SGroup::typeToString(sgroup->sgroup_type)); } output.writeCR(); } @@ -1723,7 +1723,8 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (i = j; i < std::min(sgroup_ids.size(), j + 8); i++) { SGroup* sgroup = &mol.sgroups.getSGroup(sgroup_ids[i]); - output.printf(" %3d %3d", sgroup->original_group, sgroup->original_group); + output.printf(" %3d %3d", (sgroup->original_group.hasValue() ? sgroup->original_group.get() : 0), + (sgroup->original_group.hasValue() ? sgroup->original_group.get() : 0)); } output.writeCR(); } @@ -1736,7 +1737,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) { RepeatingUnit* ru = (RepeatingUnit*)&mol.sgroups.getSGroup(i, SGroup::SG_TYPE_SRU); - output.printf(" %3d ", ru->original_group); + output.printf(" %3d ", (ru->original_group.hasValue() ? ru->original_group.get() : 0)); if (ru->connectivity == SGroup::HEAD_TO_HEAD) output.printf("HH "); @@ -1754,7 +1755,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (j = 0; j < sgroup.atoms.size(); j += 8) { int k; - output.printf("M SAL %3d%3d", sgroup.original_group, std::min(sgroup.atoms.size(), j + 8) - j); + output.printf("M SAL %3d%3d", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), std::min(sgroup.atoms.size(), j + 8) - j); for (k = j; k < std::min(sgroup.atoms.size(), j + 8); k++) output.printf(" %3d", _atom_mapping[sgroup.atoms[k]]); output.writeCR(); @@ -1762,7 +1763,8 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (j = 0; j < sgroup.getBonds().size(); j += 8) { int k; - output.printf("M SBL %3d%3d", sgroup.original_group, std::min(sgroup.getBonds().size(), j + 8) - j); + output.printf("M SBL %3d%3d", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), + std::min(sgroup.getBonds().size(), j + 8) - j); for (k = j; k < std::min(sgroup.getBonds().size(), j + 8); k++) output.printf(" %3d", _bond_mapping[sgroup.getBonds()[k]]); output.writeCR(); @@ -1772,9 +1774,9 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) if (sgroup.sgroup_type != SGroup::SG_TYPE_MUL && sgroup.label.size() > 1) { if (sgroup.label.find(' ') > -1) - output.printfCR("M SMT %3d \"%s\"", sgroup.original_group, sgroup.label.ptr()); + output.printfCR("M SMT %3d \"%s\"", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), sgroup.label.ptr()); else - output.printfCR("M SMT %3d %s", sgroup.original_group, sgroup.label.ptr()); + output.printfCR("M SMT %3d %s", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), sgroup.label.ptr()); } if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) @@ -1782,18 +1784,19 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) Superatom& superatom = (Superatom&)sgroup; if (superatom.sa_class.size() > 1) - output.printfCR("M SCL %3d %s", superatom.original_group, superatom.sa_class.ptr()); + output.printfCR("M SCL %3d %s", (superatom.original_group.hasValue() ? superatom.original_group.get() : 0), superatom.sa_class.ptr()); if (superatom.bond_connections.size() > 0) { for (j = 0; j < superatom.bond_connections.size(); j++) { - output.printfCR("M SBV %3d %3d %9.4f %9.4f", superatom.original_group, _bond_mapping[superatom.bond_connections[j].bond_idx], - superatom.bond_connections[j].bond_dir.x, superatom.bond_connections[j].bond_dir.y); + output.printfCR("M SBV %3d %3d %9.4f %9.4f", (superatom.original_group.hasValue() ? superatom.original_group.get() : 0), + _bond_mapping[superatom.bond_connections[j].bond_idx], superatom.bond_connections[j].bond_dir.x, + superatom.bond_connections[j].bond_dir.y); } } if (superatom.contracted == DisplayOption::Expanded) { - output.printfCR("M SDS EXP 1 %3d", superatom.original_group); + output.printfCR("M SDS EXP 1 %3d", (superatom.original_group.hasValue() ? superatom.original_group.get() : 0)); } if (superatom.attachment_points.size() > 0) { @@ -1804,7 +1807,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) { if (next_line) { - output.printf("M SAP %3d%3d", superatom.original_group, std::min(nrem, 6)); + output.printf("M SAP %3d%3d", (superatom.original_group.hasValue() ? superatom.original_group.get() : 0), std::min(nrem, 6)); next_line = false; } @@ -1834,7 +1837,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) { DataSGroup& datasgroup = (DataSGroup&)sgroup; - output.printf("M SDT %3d ", datasgroup.original_group); + output.printf("M SDT %3d ", (datasgroup.original_group.hasValue() ? datasgroup.original_group.get() : 0)); _writeFormattedString(output, datasgroup.name, 30); @@ -1848,7 +1851,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) output.writeCR(); - output.printf("M SDD %3d ", datasgroup.original_group); + output.printf("M SDD %3d ", (datasgroup.original_group.hasValue() ? datasgroup.original_group.get() : 0)); _writeDataSGroupDisplay(datasgroup, output); output.writeCR(); @@ -1869,7 +1872,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) output.writeString("SED "); else output.writeString("SCD "); - output.printf("%3d ", datasgroup.original_group); + output.printf("%3d ", (datasgroup.original_group.hasValue() ? datasgroup.original_group.get() : 0)); output.write(ptr, j); if (ptr[j] == '\n') @@ -1887,31 +1890,33 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (j = 0; j < mg.parent_atoms.size(); j += 8) { int k; - output.printf("M SPA %3d%3d", mg.original_group, std::min(mg.parent_atoms.size(), j + 8) - j); + output.printf("M SPA %3d%3d", (mg.original_group.hasValue() ? mg.original_group.get() : 0), std::min(mg.parent_atoms.size(), j + 8) - j); for (k = j; k < std::min(mg.parent_atoms.size(), j + 8); k++) output.printf(" %3d", _atom_mapping[mg.parent_atoms[k]]); output.writeCR(); } - output.printf("M SMT %3d %d\n", mg.original_group, mg.multiplier); + output.printf("M SMT %3d %d\n", (mg.original_group.hasValue() ? mg.original_group.get() : 0), + (mg.multiplier.hasValue() ? mg.multiplier.get() : 0)); } for (j = 0; j < sgroup.brackets.size(); j++) { - output.printf("M SDI %3d 4 %9.4f %9.4f %9.4f %9.4f\n", sgroup.original_group, sgroup.brackets[j][0].x, sgroup.brackets[j][0].y, - sgroup.brackets[j][1].x, sgroup.brackets[j][1].y); + output.printf("M SDI %3d 4 %9.4f %9.4f %9.4f %9.4f\n", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), + sgroup.brackets[j][0].x, sgroup.brackets[j][0].y, sgroup.brackets[j][1].x, sgroup.brackets[j][1].y); } if (sgroup.brackets.size() > 0 && sgroup.brk_style > 0) { - output.printf("M SBT 1 %3d %3d\n", sgroup.original_group, sgroup.brk_style); + output.printf("M SBT 1 %3d %3d\n", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), + (sgroup.brk_style.hasValue() ? sgroup.brk_style.get() : 0)); } if (sgroup.sgroup_subtype > 0) { if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_ALT) - output.printf("M SST 1 %3d ALT\n", sgroup.original_group); + output.printf("M SST 1 %3d ALT\n", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0)); else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_RAN) - output.printf("M SST 1 %3d RAN\n", sgroup.original_group); + output.printf("M SST 1 %3d RAN\n", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0)); else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_BLO) - output.printf("M SST 1 %3d BLO\n", sgroup.original_group); + output.printf("M SST 1 %3d BLO\n", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0)); } } } @@ -2202,12 +2207,14 @@ bool MolfileSaver::_checkAttPointOrder(BaseMolecule& mol, int rsite) void MolfileSaver::_writeDataSGroupDisplay(DataSGroup& datasgroup, Output& out) { - out.printf("%10.4f%10.4f %c%c%c", datasgroup.display_pos.x, datasgroup.display_pos.y, datasgroup.detached ? 'D' : 'A', datasgroup.relative ? 'R' : 'A', + out.printf("%10.4f%10.4f %c%c%c", datasgroup.display_pos->x, datasgroup.display_pos->y, datasgroup.detached ? 'D' : 'A', datasgroup.relative ? 'R' : 'A', datasgroup.display_units ? 'U' : ' '); if (datasgroup.num_chars == 0) - out.printf(" ALL 1 %c %1d ", datasgroup.tag, datasgroup.dasp_pos); + out.printf(" ALL 1 %c %1d ", (datasgroup.tag.hasValue() ? datasgroup.tag.get() : 0), + (datasgroup.dasp_pos.hasValue() ? datasgroup.dasp_pos.get() : 0)); else - out.printf(" %3d 1 %c %1d ", datasgroup.num_chars, datasgroup.tag, datasgroup.dasp_pos); + out.printf(" %3d 1 %c %1d ", (datasgroup.num_chars.hasValue() ? datasgroup.num_chars.get() : 0), + (datasgroup.tag.hasValue() ? datasgroup.tag.get() : 0), (datasgroup.dasp_pos.hasValue() ? datasgroup.dasp_pos.get() : 0)); } bool MolfileSaver::_hasNeighborEitherBond(BaseMolecule& mol, int edge_idx) diff --git a/core/indigo-core/molecule/src/smiles_loader_parsers.cpp b/core/indigo-core/molecule/src/smiles_loader_parsers.cpp index ef8179dc3b..b8beeb1198 100644 --- a/core/indigo-core/molecule/src/smiles_loader_parsers.cpp +++ b/core/indigo-core/molecule/src/smiles_loader_parsers.cpp @@ -802,11 +802,11 @@ void SmilesLoader::_readOtherStuff() _scanner.seek(pos, SEEK_SET); } _scanner.skip(1); // Skip ( - dsg.display_pos.x = _scanner.readFloat(); + dsg.display_pos->x = _scanner.readFloat(); c = _scanner.readChar(); if (c != ',') throw Error("Data S-group coord error"); - dsg.display_pos.y = _scanner.readFloat(); + dsg.display_pos->y = _scanner.readFloat(); c = _scanner.readChar(); if (c != ')') throw Error("Data S-group coord error"); diff --git a/core/indigo-core/tests/tests/formats.cpp b/core/indigo-core/tests/tests/formats.cpp index 61544db20b..eef8f15653 100644 --- a/core/indigo-core/tests/tests/formats.cpp +++ b/core/indigo-core/tests/tests/formats.cpp @@ -147,8 +147,8 @@ TEST_F(IndigoCoreFormatsTest, smiles_data_sgroups) ASSERT_STREQ(dsg.queryoper.ptr(), "like"); ASSERT_STREQ(dsg.description.ptr(), "unit"); ASSERT_EQ(dsg.tag, 't'); - ASSERT_EQ(dsg.display_pos.x, 0.0f); - ASSERT_EQ(dsg.display_pos.y, 0.0f); + ASSERT_EQ(dsg.display_pos->x, 0.0f); + ASSERT_EQ(dsg.display_pos->y, 0.0f); ASSERT_EQ(dsg.atoms.size(), 4); ASSERT_EQ(dsg.atoms.at(0), 3); ASSERT_EQ(dsg.atoms.at(1), 2); @@ -177,8 +177,8 @@ TEST_F(IndigoCoreFormatsTest, smiles_data_sgroups_coords) ASSERT_STREQ(dsg.queryoper.ptr(), ""); ASSERT_STREQ(dsg.description.ptr(), ""); ASSERT_EQ(dsg.tag, 's'); - ASSERT_EQ(dsg.display_pos.x, -1.5f); - ASSERT_EQ(dsg.display_pos.y, 7.8f); + ASSERT_EQ(dsg.display_pos->x, -1.5f); + ASSERT_EQ(dsg.display_pos->y, 7.8f); ASSERT_EQ(dsg.atoms.size(), 3); ASSERT_EQ(dsg.atoms.at(0), 1); ASSERT_EQ(dsg.atoms.at(1), 2); @@ -206,8 +206,8 @@ TEST_F(IndigoCoreFormatsTest, smiles_data_sgroups_short) ASSERT_EQ(dsg.queryoper.size(), 0); ASSERT_EQ(dsg.description.size(), 0); ASSERT_EQ(dsg.tag, ' '); - ASSERT_EQ(dsg.display_pos.x, 0.0f); - ASSERT_EQ(dsg.display_pos.y, 0.0f); + ASSERT_EQ(dsg.display_pos->x, 0.0f); + ASSERT_EQ(dsg.display_pos->y, 0.0f); ASSERT_EQ(dsg.atoms.size(), 3); ASSERT_EQ(dsg.atoms.at(0), 1); ASSERT_EQ(dsg.atoms.at(1), 2); diff --git a/core/render2d/src/render_internal.cpp b/core/render2d/src/render_internal.cpp index 02429f239a..9051daba6a 100644 --- a/core/render2d/src/render_internal.cpp +++ b/core/render2d/src/render_internal.cpp @@ -565,7 +565,7 @@ void MoleculeRenderInternal::_initSGroups(Tree& sgroups, Rect2f parent) TextItem& ti = _data.textitems[tii]; if (group.tag != ' ') { - ti.text.push(group.tag); + ti.text.push(group.tag.get()); ti.text.appendString(" = ", false); } @@ -594,7 +594,7 @@ void MoleculeRenderInternal::_initSGroups(Tree& sgroups, Rect2f parent) } else if (group.relative) { - _objDistTransform(ti.bbp, group.display_pos); + _objDistTransform(ti.bbp, group.display_pos.get()); if (group.atoms.size() > 0) { ti.bbp.add(_ad(group.atoms[0]).pos); @@ -606,7 +606,7 @@ void MoleculeRenderInternal::_initSGroups(Tree& sgroups, Rect2f parent) } else { - _objCoordTransform(ti.bbp, group.display_pos); + _objCoordTransform(ti.bbp, group.display_pos.get()); } parent = ILLEGAL_RECT(); @@ -655,7 +655,7 @@ void MoleculeRenderInternal::_initSGroups(Tree& sgroups, Rect2f parent) int tiIndex = _pushTextItem(sg, RenderItem::RIT_SGROUP); TextItem& index = _data.textitems[tiIndex]; index.fontsize = FONT_SIZE_ATTR; - bprintf(index.text, "%d", group.multiplier); + bprintf(index.text, "%d", group.multiplier.get()); _positionIndex(sg, tiIndex, true); parent = ILLEGAL_RECT(); } @@ -829,7 +829,7 @@ void MoleculeRenderInternal::_prepareSGroups(bool collapseAtLeastOneSuperatom) if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) { const Superatom& group = (Superatom&)sgroup; - Vec3f displayPosition = group.display_position; + Vec3f displayPosition = group.display_position.get(); bool useDisplayPosition = false; if (fabs(displayPosition.x) > EPSILON || fabs(displayPosition.y) > EPSILON || fabs(displayPosition.z) > EPSILON) { From 80a60267931126eac113505ac8dad2509a6cbf3d Mon Sep 17 00:00:00 2001 From: even1024 Date: Thu, 7 May 2026 17:58:20 +0200 Subject: [PATCH 11/33] nullable refactoring --- core/indigo-core/molecule/src/cml_loader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/indigo-core/molecule/src/cml_loader.cpp b/core/indigo-core/molecule/src/cml_loader.cpp index a2881e842e..47f95a6c88 100644 --- a/core/indigo-core/molecule/src/cml_loader.cpp +++ b/core/indigo-core/molecule/src/cml_loader.cpp @@ -157,7 +157,7 @@ struct Atom // This methods splits a space-separated string and writes each values into an arbitrary string // property of Atom structure for each atom in the specified list -static void splitStringIntoProperties(const char* s, std::vector& atoms, std::string Atom::* property) +static void splitStringIntoProperties(const char* s, std::vector& atoms, std::string Atom::*property) { if (s == 0) return; From 39892d444774598498f2f9423a066e8650fc7301 Mon Sep 17 00:00:00 2001 From: even1024 Date: Sat, 9 May 2026 16:48:45 +0200 Subject: [PATCH 12/33] nullable refactoring --- .../indigo/src/indigo_molecule_operations.cpp | 14 ++++++----- core/indigo-core/common/base_cpp/nullable.h | 5 ++-- core/indigo-core/layout/src/metalayout.cpp | 8 ++++--- .../layout/src/molecule_layout.cpp | 6 +++-- .../molecule/src/base_molecule.cpp | 2 +- core/indigo-core/molecule/src/cmf_loader.cpp | 4 +++- core/indigo-core/molecule/src/cml_loader.cpp | 24 +++++++++++-------- .../molecule/src/molecule_cdxml_loader.cpp | 2 +- .../molecule/src/molecule_cip_calculator.cpp | 6 ++--- .../molecule/src/molecule_json_loader.cpp | 12 ++++++---- .../molecule/src/molecule_sgroups.cpp | 2 +- .../molecule/src/molfile_loader_v3000.cpp | 6 +++-- .../molecule/src/smiles_loader_parsers.cpp | 6 +++-- 13 files changed, 57 insertions(+), 40 deletions(-) diff --git a/api/c/indigo/src/indigo_molecule_operations.cpp b/api/c/indigo/src/indigo_molecule_operations.cpp index 09497db2da..89767617f4 100644 --- a/api/c/indigo/src/indigo_molecule_operations.cpp +++ b/api/c/indigo/src/indigo_molecule_operations.cpp @@ -1403,8 +1403,7 @@ CEXPORT int indigoSetDataSGroupXY(int sgroup, float x, float y, const char* opti { DataSGroup& dsg = IndigoDataSGroup::cast(self.getObject(sgroup)).get(); - dsg.display_pos->x = x; - dsg.display_pos->y = y; + dsg.display_pos.set(Vec2f(x, y)); dsg.detached = true; if (options != 0 && options[0] != 0) @@ -1442,8 +1441,7 @@ CEXPORT int indigoSetSGroupCoords(int sgroup, float x, float y) { DataSGroup& dsg = IndigoDataSGroup::cast(self.getObject(sgroup)).get(); - dsg.display_pos->x = x; - dsg.display_pos->y = y; + dsg.display_pos.set(Vec2f(x, y)); return 1; } @@ -1602,7 +1600,9 @@ CEXPORT int indigoSetSGroupXCoord(int sgroup, float x) { DataSGroup& dsg = IndigoDataSGroup::cast(self.getObject(sgroup)).get(); - dsg.display_pos->x = x; + Vec2f dp = dsg.display_pos.get(); + dp.x = x; + dsg.display_pos.set(dp); return 1; } @@ -1615,7 +1615,9 @@ CEXPORT int indigoSetSGroupYCoord(int sgroup, float y) { DataSGroup& dsg = IndigoDataSGroup::cast(self.getObject(sgroup)).get(); - dsg.display_pos->y = y; + Vec2f dp = dsg.display_pos.get(); + dp.y = y; + dsg.display_pos.set(dp); return 1; } diff --git a/core/indigo-core/common/base_cpp/nullable.h b/core/indigo-core/common/base_cpp/nullable.h index c06576288e..78caf17374 100644 --- a/core/indigo-core/common/base_cpp/nullable.h +++ b/core/indigo-core/common/base_cpp/nullable.h @@ -70,9 +70,8 @@ namespace indigo } operator T&() - { - return get(); + return _value; } const T* operator->() const @@ -82,7 +81,7 @@ namespace indigo T* operator->() { - return &get(); + return &_value; } Nullable& operator=(const T& value) diff --git a/core/indigo-core/layout/src/metalayout.cpp b/core/indigo-core/layout/src/metalayout.cpp index 1e250541e2..e9e3ec4798 100644 --- a/core/indigo-core/layout/src/metalayout.cpp +++ b/core/indigo-core/layout/src/metalayout.cpp @@ -321,12 +321,14 @@ void Metalayout::adjustMol(BaseMolecule& mol, const Vec2f& min, const Vec2f& pos if (sg.sgroup_type == SGroup::SG_TYPE_DAT) { DataSGroup& group = (DataSGroup&)sg; - if (!group.relative) + if (!group.relative && group.display_pos.hasValue()) { Vec2f new_center; mol.getSGroupAtomsCenterPoint(group, new_center); - group.display_pos->add(new_center); - group.display_pos->sub(data_centers[i]); + Vec2f dp = group.display_pos.get(); + dp.add(new_center); + dp.sub(data_centers[i]); + group.display_pos.set(dp); } } } diff --git a/core/indigo-core/layout/src/molecule_layout.cpp b/core/indigo-core/layout/src/molecule_layout.cpp index 6a8db6ec3c..e6df3f72a5 100644 --- a/core/indigo-core/layout/src/molecule_layout.cpp +++ b/core/indigo-core/layout/src/molecule_layout.cpp @@ -340,7 +340,7 @@ void MoleculeLayout::_updateDataSGroups() if (sg.sgroup_type == SGroup::SG_TYPE_DAT) { DataSGroup& group = (DataSGroup&)sg; - if (!group.relative) + if (!group.relative && group.display_pos.hasValue()) { Vec2f before; _molecule.getSGroupAtomsCenterPoint(group, before); @@ -359,7 +359,9 @@ void MoleculeLayout::_updateDataSGroups() Vec2f delta; delta.diff(after, before); - group.display_pos->add(delta); + Vec2f dp = group.display_pos.get(); + dp.add(delta); + group.display_pos.set(dp); } } } diff --git a/core/indigo-core/molecule/src/base_molecule.cpp b/core/indigo-core/molecule/src/base_molecule.cpp index c20161eee1..81295e89e1 100644 --- a/core/indigo-core/molecule/src/base_molecule.cpp +++ b/core/indigo-core/molecule/src/base_molecule.cpp @@ -245,7 +245,7 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma ap.apid.copy(supersa.attachment_points[j].apid); } } - sa.display_position->copy(supersa.display_position); + sa.display_position = supersa.display_position; } else if (sg.sgroup_type == SGroup::SG_TYPE_SRU) { diff --git a/core/indigo-core/molecule/src/cmf_loader.cpp b/core/indigo-core/molecule/src/cmf_loader.cpp index c6ebce0ed6..0ea14e3d2e 100644 --- a/core/indigo-core/molecule/src/cmf_loader.cpp +++ b/core/indigo-core/molecule/src/cmf_loader.cpp @@ -843,7 +843,9 @@ void CmfLoader::_readSGroupXYZ(Scanner& scanner, int idx, Molecule& mol, const C { DataSGroup& s = (DataSGroup&)sg; _readBaseSGroupXyz(scanner, s, range); - _readVec2f(scanner, s.display_pos, range); + Vec2f dp; + _readVec2f(scanner, dp, range); + s.display_pos.set(dp); } else if (sg_type == SGroup::SG_TYPE_SUP) { diff --git a/core/indigo-core/molecule/src/cml_loader.cpp b/core/indigo-core/molecule/src/cml_loader.cpp index 47f95a6c88..28273f9d2a 100644 --- a/core/indigo-core/molecule/src/cml_loader.cpp +++ b/core/indigo-core/molecule/src/cml_loader.cpp @@ -157,7 +157,7 @@ struct Atom // This methods splits a space-separated string and writes each values into an arbitrary string // property of Atom structure for each atom in the specified list -static void splitStringIntoProperties(const char* s, std::vector& atoms, std::string Atom::*property) +static void splitStringIntoProperties(const char* s, std::vector& atoms, std::string Atom::* property) { if (s == 0) return; @@ -1361,17 +1361,21 @@ void CmlLoader::_loadSGroupElement(XMLElement* elem, std::unordered_mapdescription.readString(fieldtype, true); const char* disp_x = elem->Attribute("x"); - if (disp_x != 0) - { - BufferScanner strscan(disp_x); - dsg->display_pos->x = strscan.readFloat(); - } - const char* disp_y = elem->Attribute("y"); - if (disp_y != 0) + if (disp_x != 0 || disp_y != 0) { - BufferScanner strscan(disp_y); - dsg->display_pos->y = strscan.readFloat(); + Vec2f dp; + if (disp_x != 0) + { + BufferScanner strscan(disp_x); + dp.x = strscan.readFloat(); + } + if (disp_y != 0) + { + BufferScanner strscan(disp_y); + dp.y = strscan.readFloat(); + } + dsg->display_pos.set(dp); } const char* detached = elem->Attribute("dataDetached"); diff --git a/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp b/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp index 4e452ac243..18784dff02 100644 --- a/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp +++ b/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp @@ -1204,7 +1204,7 @@ void MoleculeCdxmlLoader::_addBracket(BaseMolecule& mol, const CdxmlBracket& bra Superatom& sa = (Superatom&)sgroup; sa.contracted = DisplayOption::Contracted; sa.label.readString(bracket.label.c_str(), true); - sa.display_position->copy(bracket.superatom_position); + sa.display_position.set(Vec3f(bracket.superatom_position.x, bracket.superatom_position.y, bracket.superatom_position.z)); } else switch (bracket.usage) diff --git a/core/indigo-core/molecule/src/molecule_cip_calculator.cpp b/core/indigo-core/molecule/src/molecule_cip_calculator.cpp index 19dd9af79d..f285be9f0f 100644 --- a/core/indigo-core/molecule/src/molecule_cip_calculator.cpp +++ b/core/indigo-core/molecule/src/molecule_cip_calculator.cpp @@ -206,8 +206,7 @@ void MoleculeCIPCalculator::addCIPSgroups(BaseMolecule& mol) } sgroup.name.readString("INDIGO_CIP_DESC", true); - sgroup.display_pos->x = 0.0; - sgroup.display_pos->y = 0.0; + sgroup.display_pos.set(Vec2f(0.0f, 0.0f)); sgroup.detached = true; sgroup.relative = true; } @@ -229,8 +228,7 @@ void MoleculeCIPCalculator::addCIPSgroups(BaseMolecule& mol) sgroup.data.readString("(Z)", true); sgroup.name.readString("INDIGO_CIP_DESC", true); - sgroup.display_pos->x = 0.0; - sgroup.display_pos->y = 0.0; + sgroup.display_pos.set(Vec2f(0.0f, 0.0f)); sgroup.detached = true; sgroup.relative = true; } diff --git a/core/indigo-core/molecule/src/molecule_json_loader.cpp b/core/indigo-core/molecule/src/molecule_json_loader.cpp index 353086e6d2..572fadd3c1 100644 --- a/core/indigo-core/molecule/src/molecule_json_loader.cpp +++ b/core/indigo-core/molecule/src/molecule_json_loader.cpp @@ -1125,11 +1125,15 @@ void MoleculeJsonLoader::parseSGroups(const rapidjson::Value& sgroups, BaseMolec if (s.HasMember("queryOp")) dsg.queryoper.readString(s["queryOp"].GetString(), true); - if (s.HasMember("x")) - dsg.display_pos->x = s["x"].GetFloat(); + { + Vec2f dp; + if (s.HasMember("x")) + dp.x = s["x"].GetFloat(); - if (s.HasMember("y")) - dsg.display_pos->y = s["y"].GetFloat(); + if (s.HasMember("y")) + dp.y = s["y"].GetFloat(); + dsg.display_pos.set(dp); + } if (s.HasMember("dataDetached")) dsg.detached = s["dataDetached"].GetBool(); diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index b1fe7b04bd..ef2e660a45 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -105,7 +105,7 @@ Superatom::Superatom() : unresolved(false) seqid = -1; attachment_points.clear(); bond_connections.clear(); - display_position->clear(); + display_position.set(Vec3f(0, 0, 0)); } Superatom::~Superatom() diff --git a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp index 5161894e8b..5431281505 100644 --- a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp @@ -1657,8 +1657,10 @@ void MolfileLoader::_readSGroupDisplay(Scanner& scanner, DataSGroup& dsg) { int constexpr MIN_SDD_SIZE = 36; bool well_formatted = scanner.length() >= MIN_SDD_SIZE; - dsg.display_pos->x = scanner.readFloatFix(10); - dsg.display_pos->y = scanner.readFloatFix(10); + Vec2f dp; + dp.x = scanner.readFloatFix(10); + dp.y = scanner.readFloatFix(10); + dsg.display_pos.set(dp); int ch = ' '; if (well_formatted) { diff --git a/core/indigo-core/molecule/src/smiles_loader_parsers.cpp b/core/indigo-core/molecule/src/smiles_loader_parsers.cpp index b8beeb1198..8268186896 100644 --- a/core/indigo-core/molecule/src/smiles_loader_parsers.cpp +++ b/core/indigo-core/molecule/src/smiles_loader_parsers.cpp @@ -802,14 +802,16 @@ void SmilesLoader::_readOtherStuff() _scanner.seek(pos, SEEK_SET); } _scanner.skip(1); // Skip ( - dsg.display_pos->x = _scanner.readFloat(); + Vec2f dp; + dp.x = _scanner.readFloat(); c = _scanner.readChar(); if (c != ',') throw Error("Data S-group coord error"); - dsg.display_pos->y = _scanner.readFloat(); + dp.y = _scanner.readFloat(); c = _scanner.readChar(); if (c != ')') throw Error("Data S-group coord error"); + dsg.display_pos.set(dp); } else { From 6ac28ba4226a69741eead08f464d1727de8dbe85 Mon Sep 17 00:00:00 2001 From: even1024 Date: Sun, 10 May 2026 03:12:34 +0200 Subject: [PATCH 13/33] last commit --- .../indigo/src/indigo_molecule_operations.cpp | 12 +- api/dotnet/src/IndigoObject.cs | 2 +- .../java/com/epam/indigo/IndigoObject.java | 4 + .../ref/basic/3604_sgroup_atoms_bonds.py.out | 14 +- .../integration/ref/basic/basic_load.py.out | 22 +-- .../ref/basic/sgroups_basic.py.out | 32 ++--- .../ref/formats/mol_features.py.out | 52 +++---- .../rendering/sgroups_instrumentation.py.out | 18 +-- .../tests/basic/3604_sgroup_atoms_bonds.py | 75 ++++++++++ core/indigo-core/molecule/molecule_sgroups.h | 7 +- .../molecule/src/base_molecule.cpp | 6 +- core/indigo-core/molecule/src/cml_saver.cpp | 12 +- .../molecule/src/molecule_json_saver.cpp | 47 +++++-- .../molecule/src/molecule_sgroups.cpp | 4 +- .../molecule/src/molfile_loader_v2000.cpp | 20 ++- .../molecule/src/molfile_loader_v3000.cpp | 7 +- .../molecule/src/molfile_saver.cpp | 130 ++++++++++++------ 17 files changed, 326 insertions(+), 138 deletions(-) diff --git a/api/c/indigo/src/indigo_molecule_operations.cpp b/api/c/indigo/src/indigo_molecule_operations.cpp index 89767617f4..f61d22c191 100644 --- a/api/c/indigo/src/indigo_molecule_operations.cpp +++ b/api/c/indigo/src/indigo_molecule_operations.cpp @@ -1808,7 +1808,7 @@ CEXPORT int indigoAddSGroup(int molecule, const char* type, int extindex) SGroup& sgroup = mol.sgroups.getSGroup(idx); if (extindex > 0) - sgroup.original_group = extindex; + sgroup.ext_index = extindex; return self.addObject(_wrapSGroup(mol, idx)); } @@ -2181,7 +2181,7 @@ CEXPORT int indigoGetSGroupOriginalId(int sgroup) INDIGO_BEGIN { IndigoSGroup& sg = IndigoSGroup::cast(self.getObject(sgroup)); - return sg.get().original_group; + return sg.get().index; } INDIGO_END(-1); } @@ -2195,11 +2195,11 @@ CEXPORT int indigoSetSGroupOriginalId(int sgroup, int new_original) for (auto i = sgr.mol.sgroups.begin(); i != sgr.mol.sgroups.end(); i = sgr.mol.sgroups.next(i)) { SGroup& sg = sgr.mol.sgroups.getSGroup(i); - if (sg.original_group == new_original && i != sgr.idx) + if (sg.index == new_original && i != sgr.idx) throw IndigoError("indigoSetSGroupOriginalId: duplicated sgroup id %d )", new_original); } - int old_original = sgr.get().original_group; + int old_original = sgr.get().index; if (old_original > 0) { for (auto i = sgr.mol.sgroups.begin(); i != sgr.mol.sgroups.end(); i = sgr.mol.sgroups.next(i)) @@ -2209,7 +2209,7 @@ CEXPORT int indigoSetSGroupOriginalId(int sgroup, int new_original) sg.parent_group = new_original; } } - sgr.get().original_group = new_original; + sgr.get().index = new_original; return 1; } @@ -2236,7 +2236,7 @@ CEXPORT int indigoSetSGroupParentId(int sgroup, int parent) for (auto i = sgr.mol.sgroups.begin(); i != sgr.mol.sgroups.end(); i = sgr.mol.sgroups.next(i)) { SGroup& sg = sgr.mol.sgroups.getSGroup(i); - if (sg.original_group == parent) + if (sg.index == parent) original_found = true; } if (!original_found) diff --git a/api/dotnet/src/IndigoObject.cs b/api/dotnet/src/IndigoObject.cs index d10ee2d36d..11ddd391b7 100644 --- a/api/dotnet/src/IndigoObject.cs +++ b/api/dotnet/src/IndigoObject.cs @@ -842,7 +842,7 @@ public int clearSGroupCrossBonds() return dispatcher.checkResult(IndigoLib.indigoClearSGroupCrossBonds(self)); } - public IndigoObject addSGroup(string type, int extindex) + public IndigoObject addSGroup(string type, int extindex = 0) { dispatcher.setSessionID(); return new IndigoObject(dispatcher, dispatcher.checkResult(IndigoLib.indigoAddSGroup(self, type, extindex)), this); diff --git a/api/java/indigo/src/main/java/com/epam/indigo/IndigoObject.java b/api/java/indigo/src/main/java/com/epam/indigo/IndigoObject.java index 4d03e4e147..c5587a117c 100644 --- a/api/java/indigo/src/main/java/com/epam/indigo/IndigoObject.java +++ b/api/java/indigo/src/main/java/com/epam/indigo/IndigoObject.java @@ -1249,6 +1249,10 @@ public int clearSGroupCrossBonds() { return Indigo.checkResult(this, lib.indigoClearSGroupCrossBonds(self)); } + public IndigoObject addSGroup(String type) { + return addSGroup(type, 0); + } + public IndigoObject addSGroup(String type, int extindex) { dispatcher.setSessionID(); return new IndigoObject( diff --git a/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out index 2059aee215..174da25916 100644 --- a/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out +++ b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out @@ -12,7 +12,7 @@ MUL atoms: 0 GEN type: 0 GEN atoms: 0 ****** addSGroup: with explicit extindex ******** -extindex: 42 +extindex: 0 ****** setSGroupAtoms: set atoms on empty SGroup ******** atoms after set: 3 atom symbols: C C C @@ -57,3 +57,15 @@ type: 2 atoms: 3 bonds: 2 sgroup count: 1 +****** ext_index: V3000 roundtrip with explicit extindex ******** +original id before save: 0 +roundtrip original id: 1 +V3000 index=1 extindex=42 +****** ext_index: V3000 roundtrip auto-assign (extindex=0) ******** +V3000 auto-assigned: index=1 extindex=1 +****** ext_index: V2000 roundtrip with explicit extindex ******** +V2000 SLB: M SLB 1 1 55 +V2000 roundtrip original id: 1 +****** ext_index: addSGroup without extindex (default=0) ******** +GEN type: 0 +GEN atoms: 2 diff --git a/api/tests/integration/ref/basic/basic_load.py.out b/api/tests/integration/ref/basic/basic_load.py.out index dd961f46c5..4918b7f75a 100644 --- a/api/tests/integration/ref/basic/basic_load.py.out +++ b/api/tests/integration/ref/basic/basic_load.py.out @@ -518,13 +518,13 @@ M V30 BEGIN COLLECTION M V30 MDLV30/STERAC1 ATOMS=(4 2 12 19 26) M V30 END COLLECTION M V30 BEGIN SGROUP -M V30 1 SUP 1 ATOMS=(8 1 2 3 4 5 6 7 8) XBONDS=(1 1) LABEL=Asx CLASS=AA SAP=- +M V30 1 SUP 0 ATOMS=(8 1 2 3 4 5 6 7 8) XBONDS=(1 1) LABEL=Asx CLASS=AA SAP=- M V30 (3 1 17 Al) SAP=(3 7 0 Br) -M V30 2 SUP 2 ATOMS=(8 9 12 13 14 15 16 17 18) XBONDS=(2 2 1) LABEL=Asx CLAS- +M V30 2 SUP 0 ATOMS=(8 9 12 13 14 15 16 17 18) XBONDS=(2 2 1) LABEL=Asx CLAS- M V30 S=AA SAP=(3 9 10 Al) SAP=(3 17 1 Br) -M V30 3 SUP 3 ATOMS=(8 10 19 20 21 22 23 24 25) XBONDS=(2 2 3) LABEL=Asx CLA- +M V30 3 SUP 0 ATOMS=(8 10 19 20 21 22 23 24 25) XBONDS=(2 2 3) LABEL=Asx CLA- M V30 SS=AA SAP=(3 10 9 Al) SAP=(3 24 11 Br) -M V30 4 SUP 4 ATOMS=(8 11 26 27 28 29 30 31 32) XBONDS=(1 3) LABEL=Asx CLASS- +M V30 4 SUP 0 ATOMS=(8 11 26 27 28 29 30 31 32) XBONDS=(1 3) LABEL=Asx CLASS- M V30 =AA SAP=(3 11 24 Al) SAP=(3 31 0 Br) M V30 END SGROUP M V30 END CTAB @@ -683,10 +683,10 @@ M V30 6 17 18 19 20) BRKXYZ=(9 -3.915300 0.698800 0.000000 -3.915300 3.40370- M V30 0 0.000000 0.000000 0.000000 0.000000) BRKXYZ=(9 2.816500 3.403700 0.0- M V30 00000 2.816500 0.698800 0.000000 0.000000 0.000000 0.000000) BRKTYP=PA- M V30 REN -M V30 2 DAT 2 ATOMS=(1 6) FIELDNAME=[DUP] FIELDDISP=" -2.2332 2.4505 - +M V30 2 DAT 4 ATOMS=(1 6) FIELDNAME=[DUP] FIELDDISP=" -2.2332 2.4505 - M V30 DAU ALL 1 1 " FIELDDATA="\10" -M V30 3 SUP 3 ATOMS=(5 1 5 6 7 8) PARENT=1 LABEL=X ESTATE=E -M V30 4 SUP 4 ATOMS=(6 4 21 22 23 24 25) PARENT=1 LABEL=Y ESTATE=E +M V30 3 SUP 2 ATOMS=(5 1 5 6 7 8) PARENT=1 LABEL=X ESTATE=E +M V30 4 SUP 3 ATOMS=(6 4 21 22 23 24 25) PARENT=1 LABEL=Y ESTATE=E M V30 5 DAT 5 PARENT=1 FIELDNAME=[DUP] FIELDDISP=" -0.8721 0.4941 DA- M V30 U ALL 1 1 " FIELDDATA="\20" M V30 END SGROUP @@ -952,10 +952,10 @@ M V30 56 1 54 53 M V30 57 1 56 55 M V30 END BOND M V30 BEGIN SGROUP -M V30 1 SUP 1 ATOMS=(1 1) XBONDS=(1 1) BRKXYZ=(9 43.540100 111.819000 0.0000- -M V30 00 43.540100 114.203003 0.000000 0.000000 0.000000 0.000000) BRKXYZ=(9- -M V30 44.113899 114.203003 0.000000 44.113899 111.819000 0.000000 0.000000 - -M V30 0.000000 0.000000) LABEL=NH2 CLASS=LGRP +M V30 1 SUP 31 ATOMS=(1 1) XBONDS=(1 1) BRKXYZ=(9 43.540100 111.819000 0.000- +M V30 000 43.540100 114.203003 0.000000 0.000000 0.000000 0.000000) BRKXYZ=(- +M V30 9 44.113899 114.203003 0.000000 44.113899 111.819000 0.000000 0.000000- +M V30 0.000000 0.000000) LABEL=NH2 CLASS=LGRP M V30 END SGROUP M V30 END CTAB M V30 BEGIN TEMPLATE diff --git a/api/tests/integration/ref/basic/sgroups_basic.py.out b/api/tests/integration/ref/basic/sgroups_basic.py.out index bf76a7f3f0..03e7330655 100644 --- a/api/tests/integration/ref/basic/sgroups_basic.py.out +++ b/api/tests/integration/ref/basic/sgroups_basic.py.out @@ -2465,28 +2465,28 @@ M V30 5 SRU 5 ATOMS=(1 182) XBONDS=(2 199 196) BRKXYZ=(9 8.240000 -11.080000- M V30 0.000000 9.070000 -11.080000 0.000000 0.000000 0.000000 0.000000) BRK- M V30 XYZ=(9 9.300000 -11.000000 0.000000 9.300000 -11.830000 0.000000 0.000- M V30 000 0.000000 0.000000) CONNECT=EU LABEL=n -M V30 6 MUL 6 ATOMS=(6 355 354 356 370 371 372) XBONDS=(2 276 291) BRKXYZ=(9- +M V30 6 MUL 7 ATOMS=(6 355 354 356 370 371 372) XBONDS=(2 276 291) BRKXYZ=(9- M V30 3.160000 -21.610001 0.000000 3.160000 -20.780001 0.000000 0.000000 0.- M V30 000000 0.000000) BRKXYZ=(9 5.300000 -20.780001 0.000000 5.300000 -21.6- M V30 10001 0.000000 0.000000 0.000000 0.000000) PATOMS=(3 355 354 356) MULT- M V30 =2 -M V30 7 MUL 7 ATOMS=(15 359 358 360 373 374 375 376 377 378 379 380 381 382 - +M V30 7 MUL 8 ATOMS=(15 359 358 360 373 374 375 376 377 378 379 380 381 382 - M V30 383 384) XBONDS=(2 279 304) BRKXYZ=(9 6.020000 -21.610001 0.000000 6.0- M V30 20000 -20.780001 0.000000 0.000000 0.000000 0.000000) BRKXYZ=(9 8.1700- M V30 00 -20.780001 0.000000 8.170000 -21.610001 0.000000 0.000000 0.000000 - M V30 0.000000) PATOMS=(3 359 358 360) MULT=5 -M V30 8 MUL 8 ATOMS=(40 363 362 364 365 385 386 387 388 389 390 391 392 393 - +M V30 8 MUL 9 ATOMS=(40 363 362 364 365 385 386 387 388 389 390 391 392 393 - M V30 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 41- M V30 1 412 413 414 415 416 417 418 419 420) XBONDS=(2 283 341) BRKXYZ=(9 8.- M V30 880000 -21.610001 0.000000 8.880000 -20.780001 0.000000 0.000000 0.000- M V30 000 0.000000) BRKXYZ=(9 11.750000 -20.780001 0.000000 11.750000 -21.61- M V30 0001 0.000000 0.000000 0.000000 0.000000) PATOMS=(4 363 362 364 365) M- M V30 ULT=10 -M V30 9 SRU 9 ATOMS=(4 325 324 323 326) XBONDS=(2 247 243) BRKXYZ=(9 5.30000- -M V30 0 -17.889999 0.000000 5.300000 -17.059999 0.000000 0.000000 0.000000 0- -M V30 .000000) BRKXYZ=(9 8.170000 -17.059999 0.000000 8.170000 -17.889999 0.- -M V30 000000 0.000000 0.000000 0.000000) CONNECT=EU LABEL=Z -M V30 10 SRU 10 ATOMS=(1 336) XBONDS=(2 257 256) BRKXYZ=(9 3.870000 -12.9200- +M V30 9 SRU 10 ATOMS=(4 325 324 323 326) XBONDS=(2 247 243) BRKXYZ=(9 5.3000- +M V30 00 -17.889999 0.000000 5.300000 -17.059999 0.000000 0.000000 0.000000 - +M V30 0.000000) BRKXYZ=(9 8.170000 -17.059999 0.000000 8.170000 -17.889999 0- +M V30 .000000 0.000000 0.000000 0.000000) CONNECT=EU LABEL=Z +M V30 10 SRU 11 ATOMS=(1 336) XBONDS=(2 257 256) BRKXYZ=(9 3.870000 -12.9200- M V30 00 0.000000 3.870000 -12.100000 0.000000 0.000000 0.000000 0.000000) B- M V30 RKXYZ=(9 4.590000 -12.100000 0.000000 4.590000 -12.920000 0.000000 0.0- M V30 00000 0.000000 0.000000) CONNECT=EU LABEL=Z @@ -3291,28 +3291,28 @@ M V30 5 SRU 5 ATOMS=(1 182) XBONDS=(2 199 196) BRKXYZ=(9 8.240000 -11.080000- M V30 0.000000 9.070000 -11.080000 0.000000 0.000000 0.000000 0.000000) BRK- M V30 XYZ=(9 9.300000 -11.000000 0.000000 9.300000 -11.830000 0.000000 0.000- M V30 000 0.000000 0.000000) CONNECT=EU LABEL=n -M V30 6 MUL 6 ATOMS=(6 355 354 356 370 371 372) XBONDS=(2 275 290) BRKXYZ=(9- +M V30 6 MUL 7 ATOMS=(6 355 354 356 370 371 372) XBONDS=(2 275 290) BRKXYZ=(9- M V30 3.160000 -21.610001 0.000000 3.160000 -20.780001 0.000000 0.000000 0.- M V30 000000 0.000000) BRKXYZ=(9 5.300000 -20.780001 0.000000 5.300000 -21.6- M V30 10001 0.000000 0.000000 0.000000 0.000000) PATOMS=(3 355 354 356) MULT- M V30 =2 -M V30 7 MUL 7 ATOMS=(15 359 358 360 373 374 375 376 377 378 379 380 381 382 - +M V30 7 MUL 8 ATOMS=(15 359 358 360 373 374 375 376 377 378 379 380 381 382 - M V30 383 384) XBONDS=(2 278 303) BRKXYZ=(9 6.020000 -21.610001 0.000000 6.0- M V30 20000 -20.780001 0.000000 0.000000 0.000000 0.000000) BRKXYZ=(9 8.1700- M V30 00 -20.780001 0.000000 8.170000 -21.610001 0.000000 0.000000 0.000000 - M V30 0.000000) PATOMS=(3 359 358 360) MULT=5 -M V30 8 MUL 8 ATOMS=(40 363 362 364 365 385 386 387 388 389 390 391 392 393 - +M V30 8 MUL 9 ATOMS=(40 363 362 364 365 385 386 387 388 389 390 391 392 393 - M V30 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 41- M V30 1 412 413 414 415 416 417 418 419 420) XBONDS=(2 282 340) BRKXYZ=(9 8.- M V30 880000 -21.610001 0.000000 8.880000 -20.780001 0.000000 0.000000 0.000- M V30 000 0.000000) BRKXYZ=(9 11.750000 -20.780001 0.000000 11.750000 -21.61- M V30 0001 0.000000 0.000000 0.000000 0.000000) PATOMS=(4 363 362 364 365) M- M V30 ULT=10 -M V30 9 SRU 9 ATOMS=(4 325 324 323 326) XBONDS=(2 246 242) BRKXYZ=(9 5.30000- -M V30 0 -17.889999 0.000000 5.300000 -17.059999 0.000000 0.000000 0.000000 0- -M V30 .000000) BRKXYZ=(9 8.170000 -17.059999 0.000000 8.170000 -17.889999 0.- -M V30 000000 0.000000 0.000000 0.000000) CONNECT=EU LABEL=Z -M V30 10 SRU 10 ATOMS=(1 336) XBONDS=(2 256 255) BRKXYZ=(9 3.870000 -12.9200- +M V30 9 SRU 10 ATOMS=(4 325 324 323 326) XBONDS=(2 246 242) BRKXYZ=(9 5.3000- +M V30 00 -17.889999 0.000000 5.300000 -17.059999 0.000000 0.000000 0.000000 - +M V30 0.000000) BRKXYZ=(9 8.170000 -17.059999 0.000000 8.170000 -17.889999 0- +M V30 .000000 0.000000 0.000000 0.000000) CONNECT=EU LABEL=Z +M V30 10 SRU 11 ATOMS=(1 336) XBONDS=(2 256 255) BRKXYZ=(9 3.870000 -12.9200- M V30 00 0.000000 3.870000 -12.100000 0.000000 0.000000 0.000000 0.000000) B- M V30 RKXYZ=(9 4.590000 -12.100000 0.000000 4.590000 -12.920000 0.000000 0.0- M V30 00000 0.000000 0.000000) CONNECT=EU LABEL=Z diff --git a/api/tests/integration/ref/formats/mol_features.py.out b/api/tests/integration/ref/formats/mol_features.py.out index e9cf7ac865..0753d29a72 100644 --- a/api/tests/integration/ref/formats/mol_features.py.out +++ b/api/tests/integration/ref/formats/mol_features.py.out @@ -9416,7 +9416,7 @@ molecules/multiline-sgroups-ketcher-457-v3000.mol 14 15 1 0 0 0 0 15 16 1 0 0 0 0 M STY 4 1 DAT 2 DAT 3 DAT 4 DAT -M SLB 4 1 1 2 2 3 3 4 4 +M SLB 4 1 0 2 0 3 0 4 0 M SAL 1 2 3 4 M SDT 1 Long line M SDD 1 2.4985 -1.8557 DR ALL 1 1 @@ -9492,7 +9492,7 @@ M END 14 15 1 0 0 0 0 15 16 1 0 0 0 0 M STY 4 1 DAT 2 DAT 3 DAT 4 DAT -M SLB 4 1 1 2 2 3 3 4 4 +M SLB 4 1 0 2 0 3 0 4 0 M SAL 1 2 3 4 M SDT 1 Long line M SDD 1 2.4985 -1.8557 DR ALL 1 1 @@ -9574,20 +9574,20 @@ M V30 11 1 14 15 M V30 12 1 15 16 M V30 END BOND M V30 BEGIN SGROUP -M V30 1 DAT 1 ATOMS=(2 3 4) FIELDNAME="Long line" QUERYOP=" " FIELDDISP=" - +M V30 1 DAT 0 ATOMS=(2 3 4) FIELDNAME="Long line" QUERYOP=" " FIELDDISP=" - M V30 2.4985 -1.8557 DR ALL 1 1 " FIELDDATA="asdljkfnalsj- M V30 kdnfklaj nsdfkl jnasdkjlfnakls ndfkaljsn dlkfjna slkdnaklsnd asdf asdf- M V30 as df asdf as df asd fa sdf asd fa sd fa sdf a sd f a s df asd fa sd - M V30 fa sdf as df as df as df as df as df asd fa sd fa sd fa sd f asd fa sd- M V30 f as dfa sd f a sd fa sdf a sdf " -M V30 2 DAT 2 ATOMS=(2 8 9) FIELDNAME=Multiline QUERYOP=" " FIELDDISP=" - +M V30 2 DAT 0 ATOMS=(2 8 9) FIELDNAME=Multiline QUERYOP=" " FIELDDISP=" - M V30 2.5123 -1.6940 DR ALL 1 1 " FIELDDATA="line 1 - M V30 " FIELDDATA="li- M V30 ne 2 " F- M V30 IELDDATA="line 3 - M V30 " FIELDDATA="line 4 - M V30 " -M V30 3 DAT 3 ATOMS=(2 12 13) FIELDNAME=LongAndMultI QUERYOP=" " FIELDDISP- +M V30 3 DAT 0 ATOMS=(2 12 13) FIELDNAME=LongAndMultI QUERYOP=" " FIELDDISP- M V30 =" 2.4985 -1.8557 DR ALL 1 1 " FIELDDATA="line 1 - M V30 " FIELDDAT- M V30 A="line 2 - @@ -9596,7 +9596,7 @@ M V30 line long long line long long line long long line long long line long - M V30 long line long long line long long line long long line long long line - M V30 long long line long long line - M V30 " -M V30 4 DAT 4 ATOMS=(2 15 16) FIELDNAME="Line with spaces" QUERYOP=" " FIE- +M V30 4 DAT 0 ATOMS=(2 15 16) FIELDNAME="Line with spaces" QUERYOP=" " FIE- M V30 LDDISP=" 2.5756 -1.5083 DR ALL 1 1 " FIELDDATA="asd- M V30 fjknasdjkfn aslkjdnf alksdf asdf a- M V30 sdf as dfa sdf asdf asdf - @@ -12972,33 +12972,33 @@ M V30 1706 1 1246 1248 M V30 1707 1 1246 1249 M V30 END BOND M V30 BEGIN SGROUP -M V30 1 SUP 1 ATOMS=(4 37 38 39 40) XBONDS=(1 41) LABEL=CF3 SAP=(3 37 3 1) -M V30 2 SUP 2 ATOMS=(4 74 75 76 77) XBONDS=(1 79) LABEL=CF3 SAP=(3 74 43 1) -M V30 3 SUP 3 ATOMS=(4 112 113 114 115) XBONDS=(1 118) LABEL=CF3 SAP=(3 112 - +M V30 1 SUP 0 ATOMS=(4 37 38 39 40) XBONDS=(1 41) LABEL=CF3 SAP=(3 37 3 1) +M V30 2 SUP 0 ATOMS=(4 74 75 76 77) XBONDS=(1 79) LABEL=CF3 SAP=(3 74 43 1) +M V30 3 SUP 0 ATOMS=(4 112 113 114 115) XBONDS=(1 118) LABEL=CF3 SAP=(3 112 - M V30 80 1) -M V30 4 SUP 4 ATOMS=(4 149 150 151 152) XBONDS=(1 156) LABEL=CF3 SAP=(3 149 - +M V30 4 SUP 0 ATOMS=(4 149 150 151 152) XBONDS=(1 156) LABEL=CF3 SAP=(3 149 - M V30 118 1) -M V30 5 SUP 5 ATOMS=(4 185 186 187 188) XBONDS=(1 193) LABEL=CF3 SAP=(3 185 - +M V30 5 SUP 0 ATOMS=(4 185 186 187 188) XBONDS=(1 193) LABEL=CF3 SAP=(3 185 - M V30 155 1) -M V30 6 SUP 6 ATOMS=(4 221 222 223 224) XBONDS=(1 230) LABEL=CF3 SAP=(3 221 - +M V30 6 SUP 0 ATOMS=(4 221 222 223 224) XBONDS=(1 230) LABEL=CF3 SAP=(3 221 - M V30 191 1) -M V30 7 SUP 7 ATOMS=(4 259 260 261 262) XBONDS=(1 270) LABEL=CF3 SAP=(3 259 - +M V30 7 SUP 0 ATOMS=(4 259 260 261 262) XBONDS=(1 270) LABEL=CF3 SAP=(3 259 - M V30 227 1) -M V30 8 SUP 8 ATOMS=(4 296 297 298 299) XBONDS=(1 308) LABEL=CF3 SAP=(3 296 - +M V30 8 SUP 0 ATOMS=(4 296 297 298 299) XBONDS=(1 308) LABEL=CF3 SAP=(3 296 - M V30 265 1) -M V30 9 SUP 9 ATOMS=(4 334 335 336 337) XBONDS=(1 347) LABEL=CF3 SAP=(3 334 - +M V30 9 SUP 0 ATOMS=(4 334 335 336 337) XBONDS=(1 347) LABEL=CF3 SAP=(3 334 - M V30 302 1) -M V30 10 SUP 10 ATOMS=(4 372 373 374 375) XBONDS=(1 386) LABEL=CF3 SAP=(3 37- -M V30 2 340 1) -M V30 11 SUP 11 ATOMS=(4 409 410 411 412) XBONDS=(1 424) LABEL=CF3 SAP=(3 40- -M V30 9 378 1) -M V30 12 SUP 12 ATOMS=(4 446 447 448 449) XBONDS=(1 462) LABEL=CF3 SAP=(3 44- -M V30 6 415 1) -M V30 13 SUP 13 ATOMS=(4 668 669 670 671) XBONDS=(1 705) LABEL=^CF3 SAP=(3 6- -M V30 68 667 1) -M V30 14 SUP 14 ATOMS=(1 1210) XBONDS=(1 1301) LABEL=^Me SAP=(3 1210 1200 1) -M V30 15 SUP 15 ATOMS=(4 1246 1247 1248 1249) XBONDS=(1 1341) LABEL=^CF3 SAP- -M V30 =(3 1246 1236 1) +M V30 10 SUP 0 ATOMS=(4 372 373 374 375) XBONDS=(1 386) LABEL=CF3 SAP=(3 372- +M V30 340 1) +M V30 11 SUP 0 ATOMS=(4 409 410 411 412) XBONDS=(1 424) LABEL=CF3 SAP=(3 409- +M V30 378 1) +M V30 12 SUP 0 ATOMS=(4 446 447 448 449) XBONDS=(1 462) LABEL=CF3 SAP=(3 446- +M V30 415 1) +M V30 13 SUP 0 ATOMS=(4 668 669 670 671) XBONDS=(1 705) LABEL=^CF3 SAP=(3 66- +M V30 8 667 1) +M V30 14 SUP 0 ATOMS=(1 1210) XBONDS=(1 1301) LABEL=^Me SAP=(3 1210 1200 1) +M V30 15 SUP 0 ATOMS=(4 1246 1247 1248 1249) XBONDS=(1 1341) LABEL=^CF3 SAP=- +M V30 (3 1246 1236 1) M V30 END SGROUP M V30 END CTAB M END diff --git a/api/tests/integration/ref/rendering/sgroups_instrumentation.py.out b/api/tests/integration/ref/rendering/sgroups_instrumentation.py.out index 59f2c6c7a6..0b0daa424c 100644 --- a/api/tests/integration/ref/rendering/sgroups_instrumentation.py.out +++ b/api/tests/integration/ref/rendering/sgroups_instrumentation.py.out @@ -6095,10 +6095,10 @@ M V30 6 17 18 19 20) BRKXYZ=(9 -3.915300 0.698800 0.000000 -3.915300 3.40370- M V30 0 0.000000 0.000000 0.000000 0.000000) BRKXYZ=(9 2.816500 3.403700 0.0- M V30 00000 2.816500 0.698800 0.000000 0.000000 0.000000 0.000000) BRKTYP=PA- M V30 REN -M V30 2 DAT 2 ATOMS=(1 6) FIELDNAME=[DUP] FIELDDISP=" -2.2332 2.4505 - +M V30 2 DAT 4 ATOMS=(1 6) FIELDNAME=[DUP] FIELDDISP=" -2.2332 2.4505 - M V30 DAU ALL 1 1 " FIELDDATA="\10" -M V30 3 SUP 3 ATOMS=(5 1 5 6 7 8) PARENT=1 LABEL=X ESTATE=E -M V30 4 SUP 4 ATOMS=(6 4 21 22 23 24 25) PARENT=1 LABEL=Y ESTATE=E +M V30 3 SUP 2 ATOMS=(5 1 5 6 7 8) PARENT=1 LABEL=X ESTATE=E +M V30 4 SUP 3 ATOMS=(6 4 21 22 23 24 25) PARENT=1 LABEL=Y ESTATE=E M V30 5 DAT 5 PARENT=1 FIELDNAME=[DUP] FIELDDISP=" -0.8721 0.4941 DA- M V30 U ALL 1 1 " FIELDDATA="\20" M V30 END SGROUP @@ -6169,11 +6169,11 @@ M V30 BEGIN COLLECTION M V30 MDLV30/STEABS ATOMS=(4 5 9 16 21) M V30 END COLLECTION M V30 BEGIN SGROUP -M V30 1 SUP 1 ATOMS=(5 1 5 6 7 8) LABEL=X ESTATE=E -M V30 2 SUP 2 ATOMS=(6 4 21 22 23 24 25) LABEL=Y ESTATE=E -M V30 3 DAT 3 ATOMS=(1 6) FIELDNAME=[DUP] FIELDDISP=" -2.2332 2.4505 - +M V30 1 SUP 2 ATOMS=(5 1 5 6 7 8) LABEL=X ESTATE=E +M V30 2 SUP 3 ATOMS=(6 4 21 22 23 24 25) LABEL=Y ESTATE=E +M V30 3 DAT 4 ATOMS=(1 6) FIELDNAME=[DUP] FIELDDISP=" -2.2332 2.4505 - M V30 DAU ALL 1 1 " FIELDDATA="\10" -M V30 4 DAT 4 FIELDNAME=[DUP] FIELDDISP=" -0.8721 0.4941 DAU ALL - +M V30 4 DAT 5 FIELDNAME=[DUP] FIELDDISP=" -0.8721 0.4941 DAU ALL - M V30 1 1 " FIELDDATA="\20" M V30 END SGROUP M V30 END CTAB @@ -6244,9 +6244,9 @@ M V30 BEGIN COLLECTION M V30 MDLV30/STEABS ATOMS=(4 5 9 16 21) M V30 END COLLECTION M V30 BEGIN SGROUP -M V30 1 DAT 1 ATOMS=(1 6) FIELDNAME=[DUP] FIELDDISP=" -2.2332 2.4505 - +M V30 1 DAT 4 ATOMS=(1 6) FIELDNAME=[DUP] FIELDDISP=" -2.2332 2.4505 - M V30 DAU ALL 1 1 " FIELDDATA="\10" -M V30 2 DAT 2 FIELDNAME=[DUP] FIELDDISP=" -0.8721 0.4941 DAU ALL - +M V30 2 DAT 5 FIELDNAME=[DUP] FIELDDISP=" -0.8721 0.4941 DAU ALL - M V30 1 1 " FIELDDATA="\20" M V30 END SGROUP M V30 END CTAB diff --git a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py index d6029f2f81..324b657e08 100644 --- a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py +++ b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py @@ -229,3 +229,78 @@ print("atoms: {0}".format(sg.countAtoms())) print("bonds: {0}".format(sg.countBonds())) print("sgroup count: {0}".format(sg_count)) + + +# ===== ext_index roundtrip V3000 ===== + +print("****** ext_index: V3000 roundtrip with explicit extindex ********") + +indigo.setOption("molfile-saving-mode", "3000") +mol = indigo.loadMolecule("CCCCCC") +sg = mol.addSGroup("SUP", 42) +sg.setSGroupAtoms([0, 1, 2]) +sg.setSGroupName("EXT42") +print("original id before save: {0}".format(sg.getSGroupOriginalId())) + +molfile = mol.molfile() +mol2 = indigo.loadMolecule(molfile) +for sg2 in mol2.iterateSGroups(): + print("roundtrip original id: {0}".format(sg2.getSGroupOriginalId())) + +# Check the V3000 output contains the extindex +lines = [l for l in molfile.split("\n") if "SUP" in l and "SGROUP" not in l] +for l in lines: + # V3000 format: "M V30 index type extindex ..." + parts = l.strip().split() + # Find the SUP line: M V30 SUP ... + if "SUP" in parts: + sup_idx = parts.index("SUP") + print("V3000 index={0} extindex={1}".format(parts[sup_idx - 1], parts[sup_idx + 1])) + + +print("****** ext_index: V3000 roundtrip auto-assign (extindex=0) ********") + +mol = indigo.loadMolecule("CCCCCC") +sg = mol.addSGroup("SUP", 0) +sg.setSGroupAtoms([0, 1, 2]) +sg.setSGroupName("AUTO") + +molfile = mol.molfile() +lines = [l for l in molfile.split("\n") if "SUP" in l and "SGROUP" not in l] +for l in lines: + parts = l.strip().split() + if "SUP" in parts: + sup_idx = parts.index("SUP") + print("V3000 auto-assigned: index={0} extindex={1}".format(parts[sup_idx - 1], parts[sup_idx + 1])) + + +# ===== ext_index roundtrip V2000 ===== + +print("****** ext_index: V2000 roundtrip with explicit extindex ********") + +indigo.setOption("molfile-saving-mode", "2000") +mol = indigo.loadMolecule("CCCCCC") +sg = mol.addSGroup("SUP", 55) +sg.setSGroupAtoms([0, 1, 2]) +sg.setSGroupName("EXT55") + +molfile = mol.molfile() + +# Check M SLB line +slb_lines = [l for l in molfile.split("\n") if "M SLB" in l] +for l in slb_lines: + print("V2000 SLB: {0}".format(l.strip())) + +# Roundtrip +mol2 = indigo.loadMolecule(molfile) +for sg2 in mol2.iterateSGroups(): + print("V2000 roundtrip original id: {0}".format(sg2.getSGroupOriginalId())) + + +print("****** ext_index: addSGroup without extindex (default=0) ********") + +mol = indigo.loadMolecule("CCCCCC") +sg = mol.addSGroup("GEN") +sg.setSGroupAtoms([0, 1]) +print("GEN type: {0}".format(sg.getSGroupType())) +print("GEN atoms: {0}".format(sg.countAtoms())) diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index 0984fa2c8b..e90b8c8bd8 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -111,9 +111,10 @@ namespace indigo int sgroup_type; // group type, represnted with STY in Molfile format Nullable sgroup_subtype; // group subtype, represnted with SST in Molfile format - Nullable original_group; // original group number - Nullable parent_group; // parent group number; represented with SPL in Molfile format - Nullable parent_idx; // parent group number; represented with index in the array + int index; // internal SGroup index; V3000 field 1, V2000 M STY sss. Used for cross-refs (PARENT, SPL). + Nullable ext_index; // external SGroup index; V3000 field 3 (extindex), V2000 M SLB vvv. Not set = auto-assign per spec. + Nullable parent_group; // parent group index; represented with PARENT in V3000, SPL in V2000 + Nullable parent_idx; // parent group array position; resolved from parent_group // TODO: leave only parent_idx Array atoms; // represented with SAL in Molfile format diff --git a/core/indigo-core/molecule/src/base_molecule.cpp b/core/indigo-core/molecule/src/base_molecule.cpp index 81295e89e1..ccbd7b203f 100644 --- a/core/indigo-core/molecule/src/base_molecule.cpp +++ b/core/indigo-core/molecule/src/base_molecule.cpp @@ -176,7 +176,7 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma int idx = sgroups.addSGroup(supersg.sgroup_type); SGroup& sg = sgroups.getSGroup(idx); sg.parent_idx = supersg.parent_idx; - sg.original_group = supersg.original_group; + sg.index = supersg.index; sg.parent_group = supersg.parent_group; sg.label.copy(supersg.label); @@ -1064,7 +1064,7 @@ void BaseMolecule::removeBond(int idx) void BaseMolecule::removeSGroup(int idx) { SGroup& sg = sgroups.getSGroup(idx); - _checkSgroupHierarchy(sg.parent_group, sg.original_group); + _checkSgroupHierarchy(sg.parent_group, sg.index); sgroups.remove(idx); } @@ -1073,7 +1073,7 @@ void BaseMolecule::removeSGroupWithBasis(int idx) { QS_DEF(Array, sg_atoms); SGroup& sg = sgroups.getSGroup(idx); - _checkSgroupHierarchy(sg.parent_group, sg.original_group); + _checkSgroupHierarchy(sg.parent_group, sg.index); sg_atoms.copy(sg.atoms); removeAtoms(sg_atoms); } diff --git a/core/indigo-core/molecule/src/cml_saver.cpp b/core/indigo-core/molecule/src/cml_saver.cpp index 2044351f25..b6ac729bd5 100644 --- a/core/indigo-core/molecule/src/cml_saver.cpp +++ b/core/indigo-core/molecule/src/cml_saver.cpp @@ -582,7 +582,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup QS_DEF(Array, buf); ArrayOutput out(buf); - out.printf("sg%d", sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0); + out.printf("sg%d", sgroup.index); buf.push(0); sg->SetAttribute("id", buf.ptr()); @@ -691,7 +691,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup { SGroup& sg_child = sgroups->getSGroup(i); - if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.original_group)) + if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.index)) _addSgroupElement(sg, mol, sg_child); } } @@ -705,7 +705,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup { SGroup& sg_child = sgroups->getSGroup(i); - if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.original_group)) + if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.index)) _addSgroupElement(sg, mol, sg_child); } } @@ -727,7 +727,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup { SGroup& sg_child = sgroups->getSGroup(i); - if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.original_group)) + if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.index)) _addSgroupElement(sg, mol, sg_child); } } @@ -758,7 +758,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup { SGroup& sg_child = sgroups->getSGroup(i); - if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.original_group)) + if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.index)) _addSgroupElement(sg, mol, sg_child); } } @@ -793,7 +793,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup { SGroup& sg_child = sgroups->getSGroup(i); - if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.original_group)) + if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.index)) _addSgroupElement(sg, mol, sg_child); } } diff --git a/core/indigo-core/molecule/src/molecule_json_saver.cpp b/core/indigo-core/molecule/src/molecule_json_saver.cpp index a5f8ce62bf..8d20ebb3e1 100644 --- a/core/indigo-core/molecule/src/molecule_json_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_json_saver.cpp @@ -157,24 +157,27 @@ void MoleculeJsonSaver::_checkSGroupIndices(BaseMolecule& mol, Array& sgs_l for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) { SGroup& sgroup = mol.sgroups.getSGroup(i); - if (sgroup.original_group == 0) + if (sgroup.index == 0) { - sgroup.original_group = sgs_mapping[i]; + sgroup.index = sgs_mapping[i]; } else { for (int j = mol.sgroups.begin(); j != mol.sgroups.end(); j = mol.sgroups.next(j)) { SGroup& sg = mol.sgroups.getSGroup(j); - if (sg.parent_group == sgroup.original_group && sgs_changed[j] == 0) + if (sg.parent_group == sgroup.index && sgs_changed[j] == 0) { sg.parent_group = sgs_mapping[i]; sgs_changed[j] = 1; } } - sgroup.original_group = sgs_mapping[i]; + sgroup.index = sgs_mapping[i]; } - orig_ids.push(sgroup.original_group); + // Per BIOVIA spec: if ext_index not set, auto-assign from index + if (!sgroup.ext_index.hasValue()) + sgroup.ext_index = sgroup.index; + orig_ids.push(sgroup.index); } for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) @@ -183,15 +186,15 @@ void MoleculeJsonSaver::_checkSGroupIndices(BaseMolecule& mol, Array& sgs_l if (sgroup.parent_group == 0) { sgs_list.push(i); - added_ids.push(sgroup.original_group); + added_ids.push(sgroup.index); } else { - if (orig_ids.find(sgroup.parent_group) == VALUE_UNKNOWN || sgroup.parent_group == sgroup.original_group) + if (orig_ids.find(sgroup.parent_group) == VALUE_UNKNOWN || sgroup.parent_group == sgroup.index) { sgroup.parent_group = 0; sgs_list.push(i); - added_ids.push(sgroup.original_group); + added_ids.push(sgroup.index); } } } @@ -204,13 +207,13 @@ void MoleculeJsonSaver::_checkSGroupIndices(BaseMolecule& mol, Array& sgs_l if (sgroup.parent_group == 0) continue; - if (added_ids.find(sgroup.original_group) != VALUE_UNKNOWN) + if (added_ids.find(sgroup.index) != VALUE_UNKNOWN) continue; if (added_ids.find(sgroup.parent_group) != VALUE_UNKNOWN) { sgs_list.push(i); - added_ids.push(sgroup.original_group); + added_ids.push(sgroup.index); } } if (sgs_list.size() == mol.countSGroups()) @@ -221,6 +224,20 @@ void MoleculeJsonSaver::_checkSGroupIndices(BaseMolecule& mol, Array& sgs_l void MoleculeJsonSaver::saveSGroups(BaseMolecule& mol, JsonWriter& writer) { QS_DEF(Array, sgs_sorted); + + // Save ext_index state before _checkSGroupIndices auto-assigns it + struct SGroupExtState + { + int pool_idx; + Nullable ext_index; + }; + std::vector saved_ext; + for (int si = mol.sgroups.begin(); si != mol.sgroups.end(); si = mol.sgroups.next(si)) + { + SGroup& sg = mol.sgroups.getSGroup(si); + saved_ext.push_back({si, sg.ext_index}); + } + _checkSGroupIndices(mol, sgs_sorted); int sGroupsCount = mol.countSGroups(); bool componentDefined = false; @@ -270,6 +287,16 @@ void MoleculeJsonSaver::saveSGroups(BaseMolecule& mol, JsonWriter& writer) } writer.EndArray(); } + + // Restore ext_index state after writing (prevent save-time auto-assign from persisting) + for (const auto& s : saved_ext) + { + if (mol.sgroups.hasSGroup(s.pool_idx)) + { + SGroup& sg = mol.sgroups.getSGroup(s.pool_idx); + sg.ext_index = s.ext_index; + } + } } void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index ef2e660a45..293cf5bb26 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -57,7 +57,7 @@ SGroup::SGroup() sgroup_type = SGroup::SG_TYPE_GEN; sgroup_subtype = 0; brk_style = 0; - original_group = 0; + index = 0; parent_group = 0; parent_idx = -1; contracted = DisplayOption::Undefined; @@ -708,7 +708,7 @@ int MoleculeSGroups::findSGroupById(int id) for (int i = _sgroups.begin(); i != _sgroups.end(); i = _sgroups.next(i)) { SGroup& sg = *_sgroups.at(i); - if (sg.original_group == id) + if (sg.index == id) { return i; } diff --git a/core/indigo-core/molecule/src/molfile_loader_v2000.cpp b/core/indigo-core/molecule/src/molfile_loader_v2000.cpp index 1e1137c926..6dfa42e87b 100644 --- a/core/indigo-core/molecule/src/molfile_loader_v2000.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_v2000.cpp @@ -836,7 +836,7 @@ void MolfileLoader::_readCtab2000() int idx = _bmol->sgroups.addSGroup(type); SGroup* sgroup = &_bmol->sgroups.getSGroup(idx); - sgroup->original_group = sgroup_idx + 1; + sgroup->index = sgroup_idx + 1; _sgroup_types[sgroup_idx] = sgroup->sgroup_type; _sgroup_mapping[sgroup_idx] = idx; } @@ -863,6 +863,24 @@ void MolfileLoader::_readCtab2000() } _scanner.skipLine(); } + else if (strncmp(chars, "SLB", 3) == 0) + { + int n = _scanner.readIntFix(3); + + while (n-- > 0) + { + _scanner.skip(1); + int sgroup_idx = _scanner.readIntFix(3) - 1; + + SGroup* sgroup = &_bmol->sgroups.getSGroup(_sgroup_mapping[sgroup_idx]); + + _scanner.skip(1); + int vvv = _scanner.readIntFix(3); + + sgroup->ext_index = vvv; + } + _scanner.skipLine(); + } else if (strncmp(chars, "SST", 3) == 0) { int n = _scanner.readIntFix(3); diff --git a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp index 5431281505..fb423d1788 100644 --- a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp @@ -861,7 +861,7 @@ void MolfileLoader::_fillSGroupsParentIndices() for (auto i = sgroups.begin(); i != sgroups.end(); i++) { SGroup& sgroup = sgroups.getSGroup(i); - indices.emplace(sgroup.original_group, i); + indices.emplace(sgroup.index, i); } // TODO: replace parent_group with parent_idx @@ -1151,12 +1151,13 @@ void MolfileLoader::_readSGroup3000(const char* str) scanner.readWord(type, 0); type.push(0); scanner.skipSpace(); - scanner.readInt(); + int ext_idx = scanner.readInt(); scanner.skipSpace(); int idx = sgroups->addSGroup(type.ptr()); SGroup* sgroup = &sgroups->getSGroup(idx); - sgroup->original_group = sgroup_idx; + sgroup->index = sgroup_idx; + sgroup->ext_index = ext_idx; DataSGroup* dsg = 0; Superatom* sup = 0; diff --git a/core/indigo-core/molecule/src/molfile_saver.cpp b/core/indigo-core/molecule/src/molfile_saver.cpp index 1107c49e71..39ebb5819f 100644 --- a/core/indigo-core/molecule/src/molfile_saver.cpp +++ b/core/indigo-core/molecule/src/molfile_saver.cpp @@ -876,6 +876,20 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) } QS_DEF(Array, sgs_sorted); + + // Save ext_index state before _checkSGroupIndices auto-assigns it + struct SGroupExtState + { + int pool_idx; + Nullable ext_index; + }; + std::vector saved_ext; + for (int si = mol.sgroups.begin(); si != mol.sgroups.end(); si = mol.sgroups.next(si)) + { + SGroup& sg = mol.sgroups.getSGroup(si); + saved_ext.push_back({si, sg.ext_index}); + } + _checkSGroupIndices(mol, sgs_sorted); if (mol.countSGroups() > 0) @@ -889,6 +903,7 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) ArrayOutput out(buf); int sg_idx = sgs_sorted[i]; SGroup& sgroup = sgroups->getSGroup(sg_idx); + // ext_index written as-is (managed by _checkSGroupIndices) _writeGenericSGroup3000(sgroup, idx++, out); if (sgroup.sgroup_type == SGroup::SG_TYPE_GEN) { @@ -1074,6 +1089,16 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) _removeImplicitSGroups(mol, implicit_sgroups_indexes); } + // Restore ext_index state after writing (prevent save-time auto-assign from persisting) + for (const auto& s : saved_ext) + { + if (mol.sgroups.hasSGroup(s.pool_idx)) + { + SGroup& sg = mol.sgroups.getSGroup(s.pool_idx); + sg.ext_index = s.ext_index; + } + } + output.writeStringCR("M V30 END CTAB"); int n_rgroups = mol.rgroups.getRGroupCount(); @@ -1109,7 +1134,8 @@ void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, int idx, Output& outp { int i; - output.printf("%d %s %d", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), SGroup::typeToString(sgroup.sgroup_type), idx); + int write_ext = sgroup.ext_index.hasValue() ? sgroup.ext_index.get() : sgroup.index; + output.printf("%d %s %d", sgroup.index, SGroup::typeToString(sgroup.sgroup_type), write_ext); if (sgroup.atoms.size() > 0) { @@ -1684,6 +1710,20 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) } QS_DEF(Array, sgs_sorted); + + // Save ext_index state before _checkSGroupIndices auto-assigns it + struct SGroupExtState + { + int pool_idx; + Nullable ext_index; + }; + std::vector saved_ext; + for (int si = mol.sgroups.begin(); si != mol.sgroups.end(); si = mol.sgroups.next(si)) + { + SGroup& sg = mol.sgroups.getSGroup(si); + saved_ext.push_back({si, sg.ext_index}); + } + _checkSGroupIndices(mol, sgs_sorted); if (sgroup_ids.size() > 0) @@ -1695,7 +1735,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (i = j; i < std::min(sgroup_ids.size(), j + 8); i++) { SGroup* sgroup = &mol.sgroups.getSGroup(sgroup_ids[i]); - output.printf(" %3d %s", (sgroup->original_group.hasValue() ? sgroup->original_group.get() : 0), SGroup::typeToString(sgroup->sgroup_type)); + output.printf(" %3d %s", sgroup->index, SGroup::typeToString(sgroup->sgroup_type)); } output.writeCR(); } @@ -1704,7 +1744,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) SGroup* sgroup = &mol.sgroups.getSGroup(sgroup_ids[j]); if (sgroup->parent_group > 0) { - child_ids.push(sgroup->original_group); + child_ids.push(sgroup->index); parent_ids.push(sgroup->parent_group); } } @@ -1723,8 +1763,9 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (i = j; i < std::min(sgroup_ids.size(), j + 8); i++) { SGroup* sgroup = &mol.sgroups.getSGroup(sgroup_ids[i]); - output.printf(" %3d %3d", (sgroup->original_group.hasValue() ? sgroup->original_group.get() : 0), - (sgroup->original_group.hasValue() ? sgroup->original_group.get() : 0)); + // ext_index written as-is (managed by _checkSGroupIndices) + int write_ext = sgroup->ext_index.hasValue() ? sgroup->ext_index.get() : sgroup->index; + output.printf(" %3d %3d", sgroup->index, write_ext); } output.writeCR(); } @@ -1737,7 +1778,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) { RepeatingUnit* ru = (RepeatingUnit*)&mol.sgroups.getSGroup(i, SGroup::SG_TYPE_SRU); - output.printf(" %3d ", (ru->original_group.hasValue() ? ru->original_group.get() : 0)); + output.printf(" %3d ", ru->index); if (ru->connectivity == SGroup::HEAD_TO_HEAD) output.printf("HH "); @@ -1755,7 +1796,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (j = 0; j < sgroup.atoms.size(); j += 8) { int k; - output.printf("M SAL %3d%3d", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), std::min(sgroup.atoms.size(), j + 8) - j); + output.printf("M SAL %3d%3d", sgroup.index, std::min(sgroup.atoms.size(), j + 8) - j); for (k = j; k < std::min(sgroup.atoms.size(), j + 8); k++) output.printf(" %3d", _atom_mapping[sgroup.atoms[k]]); output.writeCR(); @@ -1763,8 +1804,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (j = 0; j < sgroup.getBonds().size(); j += 8) { int k; - output.printf("M SBL %3d%3d", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), - std::min(sgroup.getBonds().size(), j + 8) - j); + output.printf("M SBL %3d%3d", sgroup.index, std::min(sgroup.getBonds().size(), j + 8) - j); for (k = j; k < std::min(sgroup.getBonds().size(), j + 8); k++) output.printf(" %3d", _bond_mapping[sgroup.getBonds()[k]]); output.writeCR(); @@ -1774,9 +1814,9 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) if (sgroup.sgroup_type != SGroup::SG_TYPE_MUL && sgroup.label.size() > 1) { if (sgroup.label.find(' ') > -1) - output.printfCR("M SMT %3d \"%s\"", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), sgroup.label.ptr()); + output.printfCR("M SMT %3d \"%s\"", sgroup.index, sgroup.label.ptr()); else - output.printfCR("M SMT %3d %s", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), sgroup.label.ptr()); + output.printfCR("M SMT %3d %s", sgroup.index, sgroup.label.ptr()); } if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) @@ -1784,19 +1824,18 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) Superatom& superatom = (Superatom&)sgroup; if (superatom.sa_class.size() > 1) - output.printfCR("M SCL %3d %s", (superatom.original_group.hasValue() ? superatom.original_group.get() : 0), superatom.sa_class.ptr()); + output.printfCR("M SCL %3d %s", superatom.index, superatom.sa_class.ptr()); if (superatom.bond_connections.size() > 0) { for (j = 0; j < superatom.bond_connections.size(); j++) { - output.printfCR("M SBV %3d %3d %9.4f %9.4f", (superatom.original_group.hasValue() ? superatom.original_group.get() : 0), - _bond_mapping[superatom.bond_connections[j].bond_idx], superatom.bond_connections[j].bond_dir.x, - superatom.bond_connections[j].bond_dir.y); + output.printfCR("M SBV %3d %3d %9.4f %9.4f", superatom.index, _bond_mapping[superatom.bond_connections[j].bond_idx], + superatom.bond_connections[j].bond_dir.x, superatom.bond_connections[j].bond_dir.y); } } if (superatom.contracted == DisplayOption::Expanded) { - output.printfCR("M SDS EXP 1 %3d", (superatom.original_group.hasValue() ? superatom.original_group.get() : 0)); + output.printfCR("M SDS EXP 1 %3d", superatom.index); } if (superatom.attachment_points.size() > 0) { @@ -1807,7 +1846,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) { if (next_line) { - output.printf("M SAP %3d%3d", (superatom.original_group.hasValue() ? superatom.original_group.get() : 0), std::min(nrem, 6)); + output.printf("M SAP %3d%3d", superatom.index, std::min(nrem, 6)); next_line = false; } @@ -1837,7 +1876,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) { DataSGroup& datasgroup = (DataSGroup&)sgroup; - output.printf("M SDT %3d ", (datasgroup.original_group.hasValue() ? datasgroup.original_group.get() : 0)); + output.printf("M SDT %3d ", datasgroup.index); _writeFormattedString(output, datasgroup.name, 30); @@ -1851,7 +1890,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) output.writeCR(); - output.printf("M SDD %3d ", (datasgroup.original_group.hasValue() ? datasgroup.original_group.get() : 0)); + output.printf("M SDD %3d ", datasgroup.index); _writeDataSGroupDisplay(datasgroup, output); output.writeCR(); @@ -1872,7 +1911,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) output.writeString("SED "); else output.writeString("SCD "); - output.printf("%3d ", (datasgroup.original_group.hasValue() ? datasgroup.original_group.get() : 0)); + output.printf("%3d ", datasgroup.index); output.write(ptr, j); if (ptr[j] == '\n') @@ -1890,37 +1929,45 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (j = 0; j < mg.parent_atoms.size(); j += 8) { int k; - output.printf("M SPA %3d%3d", (mg.original_group.hasValue() ? mg.original_group.get() : 0), std::min(mg.parent_atoms.size(), j + 8) - j); + output.printf("M SPA %3d%3d", mg.index, std::min(mg.parent_atoms.size(), j + 8) - j); for (k = j; k < std::min(mg.parent_atoms.size(), j + 8); k++) output.printf(" %3d", _atom_mapping[mg.parent_atoms[k]]); output.writeCR(); } - output.printf("M SMT %3d %d\n", (mg.original_group.hasValue() ? mg.original_group.get() : 0), - (mg.multiplier.hasValue() ? mg.multiplier.get() : 0)); + output.printf("M SMT %3d %d\n", mg.index, (mg.multiplier.hasValue() ? mg.multiplier.get() : 0)); } for (j = 0; j < sgroup.brackets.size(); j++) { - output.printf("M SDI %3d 4 %9.4f %9.4f %9.4f %9.4f\n", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), - sgroup.brackets[j][0].x, sgroup.brackets[j][0].y, sgroup.brackets[j][1].x, sgroup.brackets[j][1].y); + output.printf("M SDI %3d 4 %9.4f %9.4f %9.4f %9.4f\n", sgroup.index, sgroup.brackets[j][0].x, sgroup.brackets[j][0].y, + sgroup.brackets[j][1].x, sgroup.brackets[j][1].y); } if (sgroup.brackets.size() > 0 && sgroup.brk_style > 0) { - output.printf("M SBT 1 %3d %3d\n", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0), - (sgroup.brk_style.hasValue() ? sgroup.brk_style.get() : 0)); + output.printf("M SBT 1 %3d %3d\n", sgroup.index, (sgroup.brk_style.hasValue() ? sgroup.brk_style.get() : 0)); } if (sgroup.sgroup_subtype > 0) { if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_ALT) - output.printf("M SST 1 %3d ALT\n", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0)); + output.printf("M SST 1 %3d ALT\n", sgroup.index); else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_RAN) - output.printf("M SST 1 %3d RAN\n", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0)); + output.printf("M SST 1 %3d RAN\n", sgroup.index); else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_BLO) - output.printf("M SST 1 %3d BLO\n", (sgroup.original_group.hasValue() ? sgroup.original_group.get() : 0)); + output.printf("M SST 1 %3d BLO\n", sgroup.index); } } } _removeImplicitSGroups(mol, implicit_sgroups_indexes); + + // Restore ext_index state after writing (prevent save-time auto-assign from persisting) + for (const auto& s : saved_ext) + { + if (mol.sgroups.hasSGroup(s.pool_idx)) + { + SGroup& sg = mol.sgroups.getSGroup(s.pool_idx); + sg.ext_index = s.ext_index; + } + } } void MolfileSaver::_writeFormattedString(Output& output, Array& str, int length) @@ -1985,24 +2032,27 @@ void MolfileSaver::_checkSGroupIndices(BaseMolecule& mol, Array& sgs_list) for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) { SGroup& sgroup = mol.sgroups.getSGroup(i); - if (sgroup.original_group == 0) + if (sgroup.index == 0) { - sgroup.original_group = sgs_mapping[i]; + sgroup.index = sgs_mapping[i]; } else { for (int j = mol.sgroups.begin(); j != mol.sgroups.end(); j = mol.sgroups.next(j)) { SGroup& sg = mol.sgroups.getSGroup(j); - if (sg.parent_group == sgroup.original_group && sgs_changed[j] == 0) + if (sg.parent_group == sgroup.index && sgs_changed[j] == 0) { sg.parent_group = sgs_mapping[i]; sgs_changed[j] = 1; } } - sgroup.original_group = sgs_mapping[i]; + sgroup.index = sgs_mapping[i]; } - orig_ids.push(sgroup.original_group); + // Per BIOVIA spec: if ext_index not set, auto-assign from index + if (!sgroup.ext_index.hasValue()) + sgroup.ext_index = sgroup.index; + orig_ids.push(sgroup.index); } for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) @@ -2011,15 +2061,15 @@ void MolfileSaver::_checkSGroupIndices(BaseMolecule& mol, Array& sgs_list) if (sgroup.parent_group == 0) { sgs_list.push(i); - added_ids.push(sgroup.original_group); + added_ids.push(sgroup.index); } else { - if (orig_ids.find(sgroup.parent_group) == -1 || sgroup.parent_group == sgroup.original_group) + if (orig_ids.find(sgroup.parent_group) == -1 || sgroup.parent_group == sgroup.index) { sgroup.parent_group = 0; sgs_list.push(i); - added_ids.push(sgroup.original_group); + added_ids.push(sgroup.index); } } } @@ -2032,13 +2082,13 @@ void MolfileSaver::_checkSGroupIndices(BaseMolecule& mol, Array& sgs_list) if (sgroup.parent_group == 0) continue; - if (added_ids.find(sgroup.original_group) != -1) + if (added_ids.find(sgroup.index) != -1) continue; if (added_ids.find(sgroup.parent_group) != -1) { sgs_list.push(i); - added_ids.push(sgroup.original_group); + added_ids.push(sgroup.index); } } if (sgs_list.size() == mol.countSGroups()) From 6c4d75d111ed0517bee6023288ca50ab7a00abfb Mon Sep 17 00:00:00 2001 From: even1024 Date: Mon, 11 May 2026 00:17:58 +0200 Subject: [PATCH 14/33] clang format fixes --- core/indigo-core/molecule/src/cml_loader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/indigo-core/molecule/src/cml_loader.cpp b/core/indigo-core/molecule/src/cml_loader.cpp index 28273f9d2a..9b3a664a0c 100644 --- a/core/indigo-core/molecule/src/cml_loader.cpp +++ b/core/indigo-core/molecule/src/cml_loader.cpp @@ -157,7 +157,7 @@ struct Atom // This methods splits a space-separated string and writes each values into an arbitrary string // property of Atom structure for each atom in the specified list -static void splitStringIntoProperties(const char* s, std::vector& atoms, std::string Atom::* property) +static void splitStringIntoProperties(const char* s, std::vector& atoms, std::string Atom::*property) { if (s == 0) return; From bbf7739a94b0c5d4ddfcc961697650761c7c9f45 Mon Sep 17 00:00:00 2001 From: even1024 Date: Mon, 11 May 2026 00:25:56 +0200 Subject: [PATCH 15/33] python formatting fix --- .../tests/basic/3604_sgroup_atoms_bonds.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py index 324b657e08..3ce91d7c3e 100644 --- a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py +++ b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py @@ -255,7 +255,11 @@ # Find the SUP line: M V30 SUP ... if "SUP" in parts: sup_idx = parts.index("SUP") - print("V3000 index={0} extindex={1}".format(parts[sup_idx - 1], parts[sup_idx + 1])) + print( + "V3000 index={0} extindex={1}".format( + parts[sup_idx - 1], parts[sup_idx + 1] + ) + ) print("****** ext_index: V3000 roundtrip auto-assign (extindex=0) ********") @@ -271,7 +275,11 @@ parts = l.strip().split() if "SUP" in parts: sup_idx = parts.index("SUP") - print("V3000 auto-assigned: index={0} extindex={1}".format(parts[sup_idx - 1], parts[sup_idx + 1])) + print( + "V3000 auto-assigned: index={0} extindex={1}".format( + parts[sup_idx - 1], parts[sup_idx + 1] + ) + ) # ===== ext_index roundtrip V2000 ===== From a696f70ff8a646e32a8e0ad145ddbcad72ce2fd1 Mon Sep 17 00:00:00 2001 From: even1024 Date: Wed, 13 May 2026 01:17:26 +0200 Subject: [PATCH 16/33] replace _checkSGroupIndices with read-only getOrderedSGroups --- .../ref/basic/3604_sgroup_atoms_bonds.py.out | 9 +- .../integration/ref/basic/basic_load.py.out | 8 +- .../ref/formats/mol_features.py.out | 52 ++-- .../tests/basic/3604_sgroup_atoms_bonds.py | 48 ++-- .../tests/formats/ref/macro/sa-mono.cml | 6 +- core/indigo-core/molecule/cml_saver.h | 3 +- .../molecule/molecule_json_saver.h | 1 - core/indigo-core/molecule/molecule_sgroups.h | 16 +- core/indigo-core/molecule/molfile_saver.h | 4 +- core/indigo-core/molecule/src/cmf_saver.cpp | 9 +- core/indigo-core/molecule/src/cml_saver.cpp | 88 +++--- .../molecule/src/molecule_cdxml_saver.cpp | 21 +- .../molecule/src/molecule_json_saver.cpp | 147 +--------- .../molecule/src/molecule_sgroups.cpp | 108 ++++++++ .../molecule/src/molfile_saver.cpp | 255 ++++-------------- core/render2d/src/render_internal.cpp | 6 +- 16 files changed, 315 insertions(+), 466 deletions(-) diff --git a/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out index 174da25916..0777583df6 100644 --- a/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out +++ b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out @@ -58,12 +58,13 @@ atoms: 3 bonds: 2 sgroup count: 1 ****** ext_index: V3000 roundtrip with explicit extindex ******** -original id before save: 0 -roundtrip original id: 1 -V3000 index=1 extindex=42 +ext_index before save: 42 +roundtrip: index=1 extindex=42 ****** ext_index: V3000 roundtrip auto-assign (extindex=0) ******** -V3000 auto-assigned: index=1 extindex=1 +ext_index before save: 0 +roundtrip: index=1 extindex=1 ****** ext_index: V2000 roundtrip with explicit extindex ******** +ext_index before save: 55 V2000 SLB: M SLB 1 1 55 V2000 roundtrip original id: 1 ****** ext_index: addSGroup without extindex (default=0) ******** diff --git a/api/tests/integration/ref/basic/basic_load.py.out b/api/tests/integration/ref/basic/basic_load.py.out index 4918b7f75a..4a58670046 100644 --- a/api/tests/integration/ref/basic/basic_load.py.out +++ b/api/tests/integration/ref/basic/basic_load.py.out @@ -518,13 +518,13 @@ M V30 BEGIN COLLECTION M V30 MDLV30/STERAC1 ATOMS=(4 2 12 19 26) M V30 END COLLECTION M V30 BEGIN SGROUP -M V30 1 SUP 0 ATOMS=(8 1 2 3 4 5 6 7 8) XBONDS=(1 1) LABEL=Asx CLASS=AA SAP=- +M V30 1 SUP 1 ATOMS=(8 1 2 3 4 5 6 7 8) XBONDS=(1 1) LABEL=Asx CLASS=AA SAP=- M V30 (3 1 17 Al) SAP=(3 7 0 Br) -M V30 2 SUP 0 ATOMS=(8 9 12 13 14 15 16 17 18) XBONDS=(2 2 1) LABEL=Asx CLAS- +M V30 2 SUP 2 ATOMS=(8 9 12 13 14 15 16 17 18) XBONDS=(2 2 1) LABEL=Asx CLAS- M V30 S=AA SAP=(3 9 10 Al) SAP=(3 17 1 Br) -M V30 3 SUP 0 ATOMS=(8 10 19 20 21 22 23 24 25) XBONDS=(2 2 3) LABEL=Asx CLA- +M V30 3 SUP 3 ATOMS=(8 10 19 20 21 22 23 24 25) XBONDS=(2 2 3) LABEL=Asx CLA- M V30 SS=AA SAP=(3 10 9 Al) SAP=(3 24 11 Br) -M V30 4 SUP 0 ATOMS=(8 11 26 27 28 29 30 31 32) XBONDS=(1 3) LABEL=Asx CLASS- +M V30 4 SUP 4 ATOMS=(8 11 26 27 28 29 30 31 32) XBONDS=(1 3) LABEL=Asx CLASS- M V30 =AA SAP=(3 11 24 Al) SAP=(3 31 0 Br) M V30 END SGROUP M V30 END CTAB diff --git a/api/tests/integration/ref/formats/mol_features.py.out b/api/tests/integration/ref/formats/mol_features.py.out index 0753d29a72..e9cf7ac865 100644 --- a/api/tests/integration/ref/formats/mol_features.py.out +++ b/api/tests/integration/ref/formats/mol_features.py.out @@ -9416,7 +9416,7 @@ molecules/multiline-sgroups-ketcher-457-v3000.mol 14 15 1 0 0 0 0 15 16 1 0 0 0 0 M STY 4 1 DAT 2 DAT 3 DAT 4 DAT -M SLB 4 1 0 2 0 3 0 4 0 +M SLB 4 1 1 2 2 3 3 4 4 M SAL 1 2 3 4 M SDT 1 Long line M SDD 1 2.4985 -1.8557 DR ALL 1 1 @@ -9492,7 +9492,7 @@ M END 14 15 1 0 0 0 0 15 16 1 0 0 0 0 M STY 4 1 DAT 2 DAT 3 DAT 4 DAT -M SLB 4 1 0 2 0 3 0 4 0 +M SLB 4 1 1 2 2 3 3 4 4 M SAL 1 2 3 4 M SDT 1 Long line M SDD 1 2.4985 -1.8557 DR ALL 1 1 @@ -9574,20 +9574,20 @@ M V30 11 1 14 15 M V30 12 1 15 16 M V30 END BOND M V30 BEGIN SGROUP -M V30 1 DAT 0 ATOMS=(2 3 4) FIELDNAME="Long line" QUERYOP=" " FIELDDISP=" - +M V30 1 DAT 1 ATOMS=(2 3 4) FIELDNAME="Long line" QUERYOP=" " FIELDDISP=" - M V30 2.4985 -1.8557 DR ALL 1 1 " FIELDDATA="asdljkfnalsj- M V30 kdnfklaj nsdfkl jnasdkjlfnakls ndfkaljsn dlkfjna slkdnaklsnd asdf asdf- M V30 as df asdf as df asd fa sdf asd fa sd fa sdf a sd f a s df asd fa sd - M V30 fa sdf as df as df as df as df as df asd fa sd fa sd fa sd f asd fa sd- M V30 f as dfa sd f a sd fa sdf a sdf " -M V30 2 DAT 0 ATOMS=(2 8 9) FIELDNAME=Multiline QUERYOP=" " FIELDDISP=" - +M V30 2 DAT 2 ATOMS=(2 8 9) FIELDNAME=Multiline QUERYOP=" " FIELDDISP=" - M V30 2.5123 -1.6940 DR ALL 1 1 " FIELDDATA="line 1 - M V30 " FIELDDATA="li- M V30 ne 2 " F- M V30 IELDDATA="line 3 - M V30 " FIELDDATA="line 4 - M V30 " -M V30 3 DAT 0 ATOMS=(2 12 13) FIELDNAME=LongAndMultI QUERYOP=" " FIELDDISP- +M V30 3 DAT 3 ATOMS=(2 12 13) FIELDNAME=LongAndMultI QUERYOP=" " FIELDDISP- M V30 =" 2.4985 -1.8557 DR ALL 1 1 " FIELDDATA="line 1 - M V30 " FIELDDAT- M V30 A="line 2 - @@ -9596,7 +9596,7 @@ M V30 line long long line long long line long long line long long line long - M V30 long line long long line long long line long long line long long line - M V30 long long line long long line - M V30 " -M V30 4 DAT 0 ATOMS=(2 15 16) FIELDNAME="Line with spaces" QUERYOP=" " FIE- +M V30 4 DAT 4 ATOMS=(2 15 16) FIELDNAME="Line with spaces" QUERYOP=" " FIE- M V30 LDDISP=" 2.5756 -1.5083 DR ALL 1 1 " FIELDDATA="asd- M V30 fjknasdjkfn aslkjdnf alksdf asdf a- M V30 sdf as dfa sdf asdf asdf - @@ -12972,33 +12972,33 @@ M V30 1706 1 1246 1248 M V30 1707 1 1246 1249 M V30 END BOND M V30 BEGIN SGROUP -M V30 1 SUP 0 ATOMS=(4 37 38 39 40) XBONDS=(1 41) LABEL=CF3 SAP=(3 37 3 1) -M V30 2 SUP 0 ATOMS=(4 74 75 76 77) XBONDS=(1 79) LABEL=CF3 SAP=(3 74 43 1) -M V30 3 SUP 0 ATOMS=(4 112 113 114 115) XBONDS=(1 118) LABEL=CF3 SAP=(3 112 - +M V30 1 SUP 1 ATOMS=(4 37 38 39 40) XBONDS=(1 41) LABEL=CF3 SAP=(3 37 3 1) +M V30 2 SUP 2 ATOMS=(4 74 75 76 77) XBONDS=(1 79) LABEL=CF3 SAP=(3 74 43 1) +M V30 3 SUP 3 ATOMS=(4 112 113 114 115) XBONDS=(1 118) LABEL=CF3 SAP=(3 112 - M V30 80 1) -M V30 4 SUP 0 ATOMS=(4 149 150 151 152) XBONDS=(1 156) LABEL=CF3 SAP=(3 149 - +M V30 4 SUP 4 ATOMS=(4 149 150 151 152) XBONDS=(1 156) LABEL=CF3 SAP=(3 149 - M V30 118 1) -M V30 5 SUP 0 ATOMS=(4 185 186 187 188) XBONDS=(1 193) LABEL=CF3 SAP=(3 185 - +M V30 5 SUP 5 ATOMS=(4 185 186 187 188) XBONDS=(1 193) LABEL=CF3 SAP=(3 185 - M V30 155 1) -M V30 6 SUP 0 ATOMS=(4 221 222 223 224) XBONDS=(1 230) LABEL=CF3 SAP=(3 221 - +M V30 6 SUP 6 ATOMS=(4 221 222 223 224) XBONDS=(1 230) LABEL=CF3 SAP=(3 221 - M V30 191 1) -M V30 7 SUP 0 ATOMS=(4 259 260 261 262) XBONDS=(1 270) LABEL=CF3 SAP=(3 259 - +M V30 7 SUP 7 ATOMS=(4 259 260 261 262) XBONDS=(1 270) LABEL=CF3 SAP=(3 259 - M V30 227 1) -M V30 8 SUP 0 ATOMS=(4 296 297 298 299) XBONDS=(1 308) LABEL=CF3 SAP=(3 296 - +M V30 8 SUP 8 ATOMS=(4 296 297 298 299) XBONDS=(1 308) LABEL=CF3 SAP=(3 296 - M V30 265 1) -M V30 9 SUP 0 ATOMS=(4 334 335 336 337) XBONDS=(1 347) LABEL=CF3 SAP=(3 334 - +M V30 9 SUP 9 ATOMS=(4 334 335 336 337) XBONDS=(1 347) LABEL=CF3 SAP=(3 334 - M V30 302 1) -M V30 10 SUP 0 ATOMS=(4 372 373 374 375) XBONDS=(1 386) LABEL=CF3 SAP=(3 372- -M V30 340 1) -M V30 11 SUP 0 ATOMS=(4 409 410 411 412) XBONDS=(1 424) LABEL=CF3 SAP=(3 409- -M V30 378 1) -M V30 12 SUP 0 ATOMS=(4 446 447 448 449) XBONDS=(1 462) LABEL=CF3 SAP=(3 446- -M V30 415 1) -M V30 13 SUP 0 ATOMS=(4 668 669 670 671) XBONDS=(1 705) LABEL=^CF3 SAP=(3 66- -M V30 8 667 1) -M V30 14 SUP 0 ATOMS=(1 1210) XBONDS=(1 1301) LABEL=^Me SAP=(3 1210 1200 1) -M V30 15 SUP 0 ATOMS=(4 1246 1247 1248 1249) XBONDS=(1 1341) LABEL=^CF3 SAP=- -M V30 (3 1246 1236 1) +M V30 10 SUP 10 ATOMS=(4 372 373 374 375) XBONDS=(1 386) LABEL=CF3 SAP=(3 37- +M V30 2 340 1) +M V30 11 SUP 11 ATOMS=(4 409 410 411 412) XBONDS=(1 424) LABEL=CF3 SAP=(3 40- +M V30 9 378 1) +M V30 12 SUP 12 ATOMS=(4 446 447 448 449) XBONDS=(1 462) LABEL=CF3 SAP=(3 44- +M V30 6 415 1) +M V30 13 SUP 13 ATOMS=(4 668 669 670 671) XBONDS=(1 705) LABEL=^CF3 SAP=(3 6- +M V30 68 667 1) +M V30 14 SUP 14 ATOMS=(1 1210) XBONDS=(1 1301) LABEL=^Me SAP=(3 1210 1200 1) +M V30 15 SUP 15 ATOMS=(4 1246 1247 1248 1249) XBONDS=(1 1341) LABEL=^CF3 SAP- +M V30 =(3 1246 1236 1) M V30 END SGROUP M V30 END CTAB M END diff --git a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py index 3ce91d7c3e..7229943ff0 100644 --- a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py +++ b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py @@ -233,6 +233,17 @@ # ===== ext_index roundtrip V3000 ===== + +def get_v3000_extindex(molfile_str, sg_type="SUP"): + """Parse V3000 molfile and return (index, extindex) for the given SGroup type.""" + for l in molfile_str.split("\n"): + parts = l.strip().split() + if sg_type in parts and "SGROUP" not in l: + idx = parts.index(sg_type) + return parts[idx - 1], parts[idx + 1] + return None, None + + print("****** ext_index: V3000 roundtrip with explicit extindex ********") indigo.setOption("molfile-saving-mode", "3000") @@ -240,26 +251,11 @@ sg = mol.addSGroup("SUP", 42) sg.setSGroupAtoms([0, 1, 2]) sg.setSGroupName("EXT42") -print("original id before save: {0}".format(sg.getSGroupOriginalId())) +print("ext_index before save: 42") molfile = mol.molfile() -mol2 = indigo.loadMolecule(molfile) -for sg2 in mol2.iterateSGroups(): - print("roundtrip original id: {0}".format(sg2.getSGroupOriginalId())) - -# Check the V3000 output contains the extindex -lines = [l for l in molfile.split("\n") if "SUP" in l and "SGROUP" not in l] -for l in lines: - # V3000 format: "M V30 index type extindex ..." - parts = l.strip().split() - # Find the SUP line: M V30 SUP ... - if "SUP" in parts: - sup_idx = parts.index("SUP") - print( - "V3000 index={0} extindex={1}".format( - parts[sup_idx - 1], parts[sup_idx + 1] - ) - ) +idx, ext = get_v3000_extindex(molfile) +print("roundtrip: index={0} extindex={1}".format(idx, ext)) print("****** ext_index: V3000 roundtrip auto-assign (extindex=0) ********") @@ -268,18 +264,11 @@ sg = mol.addSGroup("SUP", 0) sg.setSGroupAtoms([0, 1, 2]) sg.setSGroupName("AUTO") +print("ext_index before save: 0") molfile = mol.molfile() -lines = [l for l in molfile.split("\n") if "SUP" in l and "SGROUP" not in l] -for l in lines: - parts = l.strip().split() - if "SUP" in parts: - sup_idx = parts.index("SUP") - print( - "V3000 auto-assigned: index={0} extindex={1}".format( - parts[sup_idx - 1], parts[sup_idx + 1] - ) - ) +idx, ext = get_v3000_extindex(molfile) +print("roundtrip: index={0} extindex={1}".format(idx, ext)) # ===== ext_index roundtrip V2000 ===== @@ -291,15 +280,14 @@ sg = mol.addSGroup("SUP", 55) sg.setSGroupAtoms([0, 1, 2]) sg.setSGroupName("EXT55") +print("ext_index before save: 55") molfile = mol.molfile() -# Check M SLB line slb_lines = [l for l in molfile.split("\n") if "M SLB" in l] for l in slb_lines: print("V2000 SLB: {0}".format(l.strip())) -# Roundtrip mol2 = indigo.loadMolecule(molfile) for sg2 in mol2.iterateSGroups(): print("V2000 roundtrip original id: {0}".format(sg2.getSGroupOriginalId())) diff --git a/api/tests/integration/tests/formats/ref/macro/sa-mono.cml b/api/tests/integration/tests/formats/ref/macro/sa-mono.cml index 8e08a0f097..693bbb6423 100644 --- a/api/tests/integration/tests/formats/ref/macro/sa-mono.cml +++ b/api/tests/integration/tests/formats/ref/macro/sa-mono.cml @@ -39,7 +39,7 @@ - + @@ -47,13 +47,13 @@ - + - + diff --git a/core/indigo-core/molecule/cml_saver.h b/core/indigo-core/molecule/cml_saver.h index e6bbb3db30..45b55d79d3 100644 --- a/core/indigo-core/molecule/cml_saver.h +++ b/core/indigo-core/molecule/cml_saver.h @@ -20,6 +20,7 @@ #define __cml_saver_h__ #include "molecule/base_molecule.h" +#include "molecule/molecule_sgroups.h" namespace tinyxml2 { @@ -51,7 +52,7 @@ namespace indigo void _validate(BaseMolecule& bmol); void _addMoleculeElement(tinyxml2::XMLElement* elem, BaseMolecule& mol, bool query); - void _addSgroupElement(tinyxml2::XMLElement* elem, BaseMolecule& mol, SGroup& sgroup); + void _addSgroupElement(tinyxml2::XMLElement* elem, BaseMolecule& mol, SGroup& sgroup, int write_index, const std::vector& entries); void _addRgroups(tinyxml2::XMLElement* elem, BaseMolecule& mol, bool query); void _addRgroupElement(tinyxml2::XMLElement* elem, RGroup& rgroup, bool query); diff --git a/core/indigo-core/molecule/molecule_json_saver.h b/core/indigo-core/molecule/molecule_json_saver.h index fafeac09fb..7408d9529c 100644 --- a/core/indigo-core/molecule/molecule_json_saver.h +++ b/core/indigo-core/molecule/molecule_json_saver.h @@ -100,7 +100,6 @@ namespace indigo DECL_ERROR; protected: - void _checkSGroupIndices(BaseMolecule& mol, Array& sgs_list); bool _checkAttPointOrder(BaseMolecule& mol, int rsite); bool _needCustomQuery(QueryMolecule::Atom* atom) const; void _writeQueryProperties(QueryMolecule::Atom* atom, JsonWriter& writer); diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index e90b8c8bd8..1d732523e8 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -24,6 +24,7 @@ #include "base_cpp/obj_pool.h" #include "base_cpp/ptr_pool.h" #include "math/algebra.h" +#include #ifdef _WIN32 #pragma warning(push) @@ -112,7 +113,7 @@ namespace indigo int sgroup_type; // group type, represnted with STY in Molfile format Nullable sgroup_subtype; // group subtype, represnted with SST in Molfile format int index; // internal SGroup index; V3000 field 1, V2000 M STY sss. Used for cross-refs (PARENT, SPL). - Nullable ext_index; // external SGroup index; V3000 field 3 (extindex), V2000 M SLB vvv. Not set = auto-assign per spec. + int ext_index; // external SGroup index; V3000 field 3 (extindex), V2000 M SLB vvv. 0 = auto-assign per spec. Nullable parent_group; // parent group index; represented with PARENT in V3000, SPL in V2000 Nullable parent_idx; // parent group array position; resolved from parent_group // TODO: leave only parent_idx @@ -314,6 +315,19 @@ namespace indigo bool _cmpIndices(Array& t_inds, Array& q_inds); }; + // Read-only write-order entry for serialization. Replaces the old mutating _checkSGroupIndices pattern. + struct SGroupWriteEntry + { + int pool_idx; // original pool index in mol.sgroups + int write_index; // sequential 1,2,3... for CTFile output + int write_ext_index; // ext_index or auto-assigned from write_index (0→write_index per spec) + int write_parent; // remapped parent_group (0 if root) + }; + + // Returns topologically-sorted SGroup list with sequential indices for serialization. + // Does NOT mutate the molecule — returns a mapping table. + DLLEXPORT std::vector getOrderedSGroups(MoleculeSGroups& sgroups); + } // namespace indigo #ifdef _WIN32 diff --git a/core/indigo-core/molecule/molfile_saver.h b/core/indigo-core/molecule/molfile_saver.h index 32b36a1d52..990c36310c 100644 --- a/core/indigo-core/molecule/molfile_saver.h +++ b/core/indigo-core/molecule/molfile_saver.h @@ -27,6 +27,7 @@ #include "base_cpp/array.h" #include "base_cpp/tlscont.h" #include "molecule/base_molecule.h" +#include "molecule/molecule_sgroups.h" namespace indigo { @@ -98,10 +99,9 @@ namespace indigo void _writeTGroup(Output& output, BaseMolecule& mol, int tg_idx); void _writeCtabHeader2000(Output& output, BaseMolecule& mol); void _writeCtab2000(Output& output, BaseMolecule& mol, bool query); - void _checkSGroupIndices(BaseMolecule& mol, Array& sgs); void _writeRGroupIndices2000(Output& output, BaseMolecule& mol); void _writeAttachmentValues2000(Output& output, BaseMolecule& fragment); - void _writeGenericSGroup3000(SGroup& sgroup, int idx, Output& output); + void _writeGenericSGroup3000(SGroup& sgroup, const SGroupWriteEntry& entry, int idx, Output& output); void _writeDataSGroupDisplay(DataSGroup& datasgroup, Output& out); void _writeFormattedString(Output& output, Array& str, int length); static bool _checkAttPointOrder(BaseMolecule& mol, int rsite); diff --git a/core/indigo-core/molecule/src/cmf_saver.cpp b/core/indigo-core/molecule/src/cmf_saver.cpp index 0c7ce6316b..e4c47478f6 100644 --- a/core/indigo-core/molecule/src/cmf_saver.cpp +++ b/core/indigo-core/molecule/src/cmf_saver.cpp @@ -825,10 +825,13 @@ void CmfSaver::_updateSGroupsXyzMinMax(Molecule& mol, Vec3f& min, Vec3f& max) DataSGroup& s = (DataSGroup&)sg; _updateBaseSGroupXyzMinMax(s, min, max); - Vec3f display_pos(s.display_pos->x, s.display_pos->y, 0); + if (s.display_pos.hasValue()) + { + Vec3f display_pos(s.display_pos->x, s.display_pos->y, 0); - min.min(display_pos); - max.max(display_pos); + min.min(display_pos); + max.max(display_pos); + } } } } diff --git a/core/indigo-core/molecule/src/cml_saver.cpp b/core/indigo-core/molecule/src/cml_saver.cpp index b6ac729bd5..cc10901170 100644 --- a/core/indigo-core/molecule/src/cml_saver.cpp +++ b/core/indigo-core/molecule/src/cml_saver.cpp @@ -565,24 +565,26 @@ void CmlSaver::_addMoleculeElement(XMLElement* elem, BaseMolecule& mol, bool que if (_mol->countSGroups() > 0) { - for (i = _mol->sgroups.begin(); i != _mol->sgroups.end(); i = _mol->sgroups.next(i)) + auto entries = getOrderedSGroups(_mol->sgroups); + for (auto& entry : entries) { - SGroup& sgroup = _mol->sgroups.getSGroup(i); - - if (sgroup.parent_group == 0) - _addSgroupElement(molecule, *_mol, sgroup); + if (entry.write_parent == 0) + { + SGroup& sgroup = _mol->sgroups.getSGroup(entry.pool_idx); + _addSgroupElement(molecule, *_mol, sgroup, entry.write_index, entries); + } } } } -void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup& sgroup) +void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup& sgroup, int write_index, const std::vector& entries) { XMLElement* sg = _doc->NewElement("molecule"); molecule->LinkEndChild(sg); QS_DEF(Array, buf); ArrayOutput out(buf); - out.printf("sg%d", sgroup.index); + out.printf("sg%d", write_index); buf.push(0); sg->SetAttribute("id", buf.ptr()); @@ -651,8 +653,11 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup sg->SetAttribute("queryOp", queryoper); } - sg->SetAttribute("x", dsg.display_pos->x); - sg->SetAttribute("y", dsg.display_pos->y); + if (dsg.display_pos.hasValue()) + { + sg->SetAttribute("x", dsg.display_pos->x); + sg->SetAttribute("y", dsg.display_pos->y); + } if (!dsg.detached) { @@ -685,28 +690,26 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup sg->SetAttribute("fieldData", dsg.data.ptr()); } - MoleculeSGroups* sgroups = &mol.sgroups; - - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) + for (auto& child_entry : entries) { - SGroup& sg_child = sgroups->getSGroup(i); - - if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.index)) - _addSgroupElement(sg, mol, sg_child); + if (child_entry.write_parent == write_index) + { + SGroup& sg_child = mol.sgroups.getSGroup(child_entry.pool_idx); + _addSgroupElement(sg, mol, sg_child, child_entry.write_index, entries); + } } } else if (sgroup.sgroup_type == SGroup::SG_TYPE_GEN) { sg->SetAttribute("role", "GenericSgroup"); - MoleculeSGroups* sgroups = &mol.sgroups; - - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) + for (auto& child_entry : entries) { - SGroup& sg_child = sgroups->getSGroup(i); - - if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.index)) - _addSgroupElement(sg, mol, sg_child); + if (child_entry.write_parent == write_index) + { + SGroup& sg_child = mol.sgroups.getSGroup(child_entry.pool_idx); + _addSgroupElement(sg, mol, sg_child, child_entry.write_index, entries); + } } } else if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) @@ -721,14 +724,13 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup sg->SetAttribute("title", name); } - MoleculeSGroups* sgroups = &mol.sgroups; - - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) + for (auto& child_entry : entries) { - SGroup& sg_child = sgroups->getSGroup(i); - - if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.index)) - _addSgroupElement(sg, mol, sg_child); + if (child_entry.write_parent == write_index) + { + SGroup& sg_child = mol.sgroups.getSGroup(child_entry.pool_idx); + _addSgroupElement(sg, mol, sg_child, child_entry.write_index, entries); + } } } else if (sgroup.sgroup_type == SGroup::SG_TYPE_SRU) @@ -752,14 +754,13 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup sg->SetAttribute("connect", "hh"); } - MoleculeSGroups* sgroups = &mol.sgroups; - - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) + for (auto& child_entry : entries) { - SGroup& sg_child = sgroups->getSGroup(i); - - if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.index)) - _addSgroupElement(sg, mol, sg_child); + if (child_entry.write_parent == write_index) + { + SGroup& sg_child = mol.sgroups.getSGroup(child_entry.pool_idx); + _addSgroupElement(sg, mol, sg_child, child_entry.write_index, entries); + } } } else if (sgroup.sgroup_type == SGroup::SG_TYPE_MUL) @@ -787,14 +788,13 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup sg->SetAttribute("patoms", pbuf.ptr()); } - MoleculeSGroups* sgroups = &mol.sgroups; - - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) + for (auto& child_entry : entries) { - SGroup& sg_child = sgroups->getSGroup(i); - - if ((sg_child.parent_group != 0) && (sg_child.parent_group == sgroup.index)) - _addSgroupElement(sg, mol, sg_child); + if (child_entry.write_parent == write_index) + { + SGroup& sg_child = mol.sgroups.getSGroup(child_entry.pool_idx); + _addSgroupElement(sg, mol, sg_child, child_entry.write_index, entries); + } } } } diff --git a/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp b/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp index 7006818a22..810faaa133 100644 --- a/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp @@ -1134,15 +1134,18 @@ void MoleculeCdxmlSaver::addFragmentNodes(BaseMolecule& mol, tinyxml2::XMLElemen { XMLElement* t = _doc->NewElement("t"); node->LinkEndChild(t); - Vec2f pos(sa.display_position->x + offset.x, -sa.display_position->y - offset.y); - pos.scale(_bond_length); - Vec2f v1(pos.x - _bond_length / 2, pos.y - _bond_length / 2); - Vec2f v2(pos.x + _bond_length / 2, pos.y + _bond_length / 2); - std::string pos_str = std::to_string(pos.x) + " " + std::to_string(pos.y); - Rect2f bbox(v1, v2); - std::string bbox_str = boundingBoxToString(bbox); - if (sa.display_position->x != 0.0f && sa.display_position->y != 0.0f) - node->SetAttribute("p", pos_str.c_str()); + if (sa.display_position.hasValue()) + { + Vec2f pos(sa.display_position->x + offset.x, -sa.display_position->y - offset.y); + pos.scale(_bond_length); + Vec2f v1(pos.x - _bond_length / 2, pos.y - _bond_length / 2); + Vec2f v2(pos.x + _bond_length / 2, pos.y + _bond_length / 2); + std::string pos_str = std::to_string(pos.x) + " " + std::to_string(pos.y); + Rect2f bbox(v1, v2); + std::string bbox_str = boundingBoxToString(bbox); + if (sa.display_position->x != 0.0f && sa.display_position->y != 0.0f) + node->SetAttribute("p", pos_str.c_str()); + } t->SetAttribute("LabelJustification", "Left"); t->SetAttribute("LabelAlignment", "Above"); XMLElement* s = _doc->NewElement("s"); diff --git a/core/indigo-core/molecule/src/molecule_json_saver.cpp b/core/indigo-core/molecule/src/molecule_json_saver.cpp index 8d20ebb3e1..57d1953fc7 100644 --- a/core/indigo-core/molecule/src/molecule_json_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_json_saver.cpp @@ -120,126 +120,10 @@ void MoleculeJsonSaver::saveFormatMode(KETVersion& version, Array& output) output.readString(ver.c_str(), true); } -void MoleculeJsonSaver::_checkSGroupIndices(BaseMolecule& mol, Array& sgs_list) -{ - QS_DEF(Array, orig_ids); - QS_DEF(Array, added_ids); - QS_DEF(Array, sgs_mapping); - QS_DEF(Array, sgs_changed); - - sgs_list.clear(); - orig_ids.clear(); - added_ids.clear(); - sgs_mapping.clear_resize(mol.sgroups.end()); - sgs_mapping.zerofill(); - sgs_changed.clear_resize(mol.sgroups.end()); - sgs_changed.zerofill(); - - int iw = 1; - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) - { - SGroup& sgroup = mol.sgroups.getSGroup(i); - if (sgroup.parent_group == 0) - { - sgs_mapping[i] = iw; - iw++; - } - } - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) - { - if (sgs_mapping[i] == 0) - { - sgs_mapping[i] = iw; - iw++; - } - } - - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) - { - SGroup& sgroup = mol.sgroups.getSGroup(i); - if (sgroup.index == 0) - { - sgroup.index = sgs_mapping[i]; - } - else - { - for (int j = mol.sgroups.begin(); j != mol.sgroups.end(); j = mol.sgroups.next(j)) - { - SGroup& sg = mol.sgroups.getSGroup(j); - if (sg.parent_group == sgroup.index && sgs_changed[j] == 0) - { - sg.parent_group = sgs_mapping[i]; - sgs_changed[j] = 1; - } - } - sgroup.index = sgs_mapping[i]; - } - // Per BIOVIA spec: if ext_index not set, auto-assign from index - if (!sgroup.ext_index.hasValue()) - sgroup.ext_index = sgroup.index; - orig_ids.push(sgroup.index); - } - - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) - { - SGroup& sgroup = mol.sgroups.getSGroup(i); - if (sgroup.parent_group == 0) - { - sgs_list.push(i); - added_ids.push(sgroup.index); - } - else - { - if (orig_ids.find(sgroup.parent_group) == VALUE_UNKNOWN || sgroup.parent_group == sgroup.index) - { - sgroup.parent_group = 0; - sgs_list.push(i); - added_ids.push(sgroup.index); - } - } - } - - for (;;) - { - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) - { - SGroup& sgroup = mol.sgroups.getSGroup(i); - if (sgroup.parent_group == 0) - continue; - - if (added_ids.find(sgroup.index) != VALUE_UNKNOWN) - continue; - - if (added_ids.find(sgroup.parent_group) != VALUE_UNKNOWN) - { - sgs_list.push(i); - added_ids.push(sgroup.index); - } - } - if (sgs_list.size() == mol.countSGroups()) - break; - } -} - void MoleculeJsonSaver::saveSGroups(BaseMolecule& mol, JsonWriter& writer) { - QS_DEF(Array, sgs_sorted); - - // Save ext_index state before _checkSGroupIndices auto-assigns it - struct SGroupExtState - { - int pool_idx; - Nullable ext_index; - }; - std::vector saved_ext; - for (int si = mol.sgroups.begin(); si != mol.sgroups.end(); si = mol.sgroups.next(si)) - { - SGroup& sg = mol.sgroups.getSGroup(si); - saved_ext.push_back({si, sg.ext_index}); - } - - _checkSGroupIndices(mol, sgs_sorted); - int sGroupsCount = mol.countSGroups(); + auto write_order = getOrderedSGroups(mol.sgroups); + int sGroupsCount = static_cast(write_order.size()); bool componentDefined = false; if (mol.isQueryMolecule()) { @@ -259,11 +143,9 @@ void MoleculeJsonSaver::saveSGroups(BaseMolecule& mol, JsonWriter& writer) { writer.Key("sgroups"); writer.StartArray(); - // int idx = 1; - for (int i = 0; i < sgs_sorted.size(); i++) + for (const auto& entry : write_order) { - int sg_idx = sgs_sorted[i]; - auto& sgrp = mol.sgroups.getSGroup(sg_idx); + auto& sgrp = mol.sgroups.getSGroup(entry.pool_idx); saveSGroup(sgrp, writer); } // save queryComponent @@ -287,16 +169,6 @@ void MoleculeJsonSaver::saveSGroups(BaseMolecule& mol, JsonWriter& writer) } writer.EndArray(); } - - // Restore ext_index state after writing (prevent save-time auto-assign from persisting) - for (const auto& s : saved_ext) - { - if (mol.sgroups.hasSGroup(s.pool_idx)) - { - SGroup& sg = mol.sgroups.getSGroup(s.pool_idx); - sg.ext_index = s.ext_index; - } - } } void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) @@ -350,10 +222,13 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) writer.String(query_oper); } - writer.Key("x"); - writeFloat(writer, dsg.display_pos->x); - writer.Key("y"); - writeFloat(writer, dsg.display_pos->y); + if (dsg.display_pos.hasValue()) + { + writer.Key("x"); + writeFloat(writer, dsg.display_pos->x); + writer.Key("y"); + writeFloat(writer, dsg.display_pos->y); + } if (!dsg.detached) { diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index 293cf5bb26..c5188aed15 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -22,6 +22,10 @@ #include "base_cpp/tree.h" #include "molecule/molecule_sgroups.h" +#include +#include +#include + using namespace indigo; static SGroup::SgType mappingForSgTypes[] = { @@ -58,6 +62,7 @@ SGroup::SGroup() sgroup_subtype = 0; brk_style = 0; index = 0; + ext_index = 0; parent_group = 0; parent_idx = -1; contracted = DisplayOption::Undefined; @@ -725,3 +730,106 @@ bool MoleculeSGroups::_cmpIndices(Array& t_inds, Array& q_inds) } return true; } + +std::vector indigo::getOrderedSGroups(MoleculeSGroups& sgroups) +{ + // Phase 1: Build pool_idx → write_index mapping (sequential, roots first) + std::vector pool_indices; + for (int i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) + pool_indices.push_back(i); + + int pool_end = pool_indices.empty() ? 0 : (*std::max_element(pool_indices.begin(), pool_indices.end()) + 1); + + std::vector sgs_mapping(pool_end, 0); + + // Roots first (parent_group == 0 or not set) + int iw = 1; + for (int i : pool_indices) + { + SGroup& sg = sgroups.getSGroup(i); + int pg = sg.parent_group.hasValue() ? sg.parent_group.get() : 0; + if (pg == 0) + { + sgs_mapping[i] = iw++; + } + } + // Then children + for (int i : pool_indices) + { + if (sgs_mapping[i] == 0) + { + sgs_mapping[i] = iw++; + } + } + // Phase 2: Build old_index → write_index remap for parent_group resolution + std::map index_remap; + for (int i : pool_indices) + { + SGroup& sg = sgroups.getSGroup(i); + int key = (sg.index != 0) ? sg.index : sgs_mapping[i]; + index_remap[key] = sgs_mapping[i]; + } + + // Phase 3: Build entries with write fields + std::vector all_entries; + all_entries.reserve(pool_indices.size()); + for (int i : pool_indices) + { + SGroup& sg = sgroups.getSGroup(i); + SGroupWriteEntry entry; + entry.pool_idx = i; + entry.write_index = sgs_mapping[i]; + + // ext_index: use explicit value if set, otherwise auto-assign from write_index + entry.write_ext_index = (sg.ext_index != 0) ? sg.ext_index : entry.write_index; + + // parent_group: remap to new write indices + int pg = sg.parent_group.hasValue() ? sg.parent_group.get() : 0; + if (pg == 0) + { + entry.write_parent = 0; + } + else + { + auto it = index_remap.find(pg); + if (it != index_remap.end() && it->second != entry.write_index) + entry.write_parent = it->second; + else + entry.write_parent = 0; // orphan or self-ref → root + } + all_entries.push_back(entry); + } + + // Phase 4: Topological sort — parents before children + std::vector result; + result.reserve(all_entries.size()); + + std::set added_indices; + + // Add roots first + for (auto& e : all_entries) + { + if (e.write_parent == 0) + { + result.push_back(e); + added_indices.insert(e.write_index); + } + } + + // Then iteratively add children whose parents are already added + while (result.size() < all_entries.size()) + { + for (auto& e : all_entries) + { + if (added_indices.count(e.write_index)) + continue; + if (added_indices.count(e.write_parent)) + { + result.push_back(e); + added_indices.insert(e.write_index); + } + } + } + + return result; +} diff --git a/core/indigo-core/molecule/src/molfile_saver.cpp b/core/indigo-core/molecule/src/molfile_saver.cpp index 39ebb5819f..bfd4c2b45d 100644 --- a/core/indigo-core/molecule/src/molfile_saver.cpp +++ b/core/indigo-core/molecule/src/molfile_saver.cpp @@ -875,36 +875,19 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) output.writeStringCR("M V30 END COLLECTION"); } - QS_DEF(Array, sgs_sorted); + auto write_order = getOrderedSGroups(mol.sgroups); - // Save ext_index state before _checkSGroupIndices auto-assigns it - struct SGroupExtState - { - int pool_idx; - Nullable ext_index; - }; - std::vector saved_ext; - for (int si = mol.sgroups.begin(); si != mol.sgroups.end(); si = mol.sgroups.next(si)) - { - SGroup& sg = mol.sgroups.getSGroup(si); - saved_ext.push_back({si, sg.ext_index}); - } - - _checkSGroupIndices(mol, sgs_sorted); - - if (mol.countSGroups() > 0) + if (write_order.size() > 0) { MoleculeSGroups* sgroups = &mol.sgroups; int idx = 1; output.writeStringCR("M V30 BEGIN SGROUP"); - for (i = 0; i < sgs_sorted.size(); i++) + for (const auto& entry : write_order) { ArrayOutput out(buf); - int sg_idx = sgs_sorted[i]; - SGroup& sgroup = sgroups->getSGroup(sg_idx); - // ext_index written as-is (managed by _checkSGroupIndices) - _writeGenericSGroup3000(sgroup, idx++, out); + SGroup& sgroup = sgroups->getSGroup(entry.pool_idx); + _writeGenericSGroup3000(sgroup, entry, idx++, out); if (sgroup.sgroup_type == SGroup::SG_TYPE_GEN) { _writeMultiString(output, buf.ptr(), buf.size()); @@ -1089,16 +1072,6 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) _removeImplicitSGroups(mol, implicit_sgroups_indexes); } - // Restore ext_index state after writing (prevent save-time auto-assign from persisting) - for (const auto& s : saved_ext) - { - if (mol.sgroups.hasSGroup(s.pool_idx)) - { - SGroup& sg = mol.sgroups.getSGroup(s.pool_idx); - sg.ext_index = s.ext_index; - } - } - output.writeStringCR("M V30 END CTAB"); int n_rgroups = mol.rgroups.getRGroupCount(); @@ -1130,12 +1103,11 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) } } -void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, int idx, Output& output) +void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, const SGroupWriteEntry& entry, int idx, Output& output) { int i; - int write_ext = sgroup.ext_index.hasValue() ? sgroup.ext_index.get() : sgroup.index; - output.printf("%d %s %d", sgroup.index, SGroup::typeToString(sgroup.sgroup_type), write_ext); + output.printf("%d %s %d", entry.write_index, SGroup::typeToString(sgroup.sgroup_type), entry.write_ext_index); if (sgroup.atoms.size() > 0) { @@ -1163,9 +1135,9 @@ void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, int idx, Output& outp else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_BLO) output.printf(" SUBTYPE=BLO"); } - if (sgroup.parent_group > 0) + if (entry.write_parent > 0) { - output.printf(" PARENT=%d", (sgroup.parent_group.hasValue() ? sgroup.parent_group.get() : 0)); + output.printf(" PARENT=%d", entry.write_parent); } for (i = 0; i < sgroup.brackets.size(); i++) { @@ -1709,22 +1681,12 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) sgroup_ids.push(i); } - QS_DEF(Array, sgs_sorted); - - // Save ext_index state before _checkSGroupIndices auto-assigns it - struct SGroupExtState - { - int pool_idx; - Nullable ext_index; - }; - std::vector saved_ext; - for (int si = mol.sgroups.begin(); si != mol.sgroups.end(); si = mol.sgroups.next(si)) - { - SGroup& sg = mol.sgroups.getSGroup(si); - saved_ext.push_back({si, sg.ext_index}); - } + auto write_order = getOrderedSGroups(mol.sgroups); - _checkSGroupIndices(mol, sgs_sorted); + // Build pool_idx → entry lookup for random access + std::map entry_map; + for (const auto& e : write_order) + entry_map[e.pool_idx] = &e; if (sgroup_ids.size() > 0) { @@ -1735,17 +1697,17 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (i = j; i < std::min(sgroup_ids.size(), j + 8); i++) { SGroup* sgroup = &mol.sgroups.getSGroup(sgroup_ids[i]); - output.printf(" %3d %s", sgroup->index, SGroup::typeToString(sgroup->sgroup_type)); + output.printf(" %3d %s", entry_map[sgroup_ids[i]]->write_index, SGroup::typeToString(sgroup->sgroup_type)); } output.writeCR(); } for (j = 0; j < sgroup_ids.size(); j++) { - SGroup* sgroup = &mol.sgroups.getSGroup(sgroup_ids[j]); - if (sgroup->parent_group > 0) + auto* entry = entry_map[sgroup_ids[j]]; + if (entry->write_parent > 0) { - child_ids.push(sgroup->index); - parent_ids.push(sgroup->parent_group); + child_ids.push(entry->write_index); + parent_ids.push(entry->write_parent); } } for (j = 0; j < child_ids.size(); j += 8) @@ -1762,10 +1724,8 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) output.printf("M SLB%3d", std::min(sgroup_ids.size(), j + 8) - j); for (i = j; i < std::min(sgroup_ids.size(), j + 8); i++) { - SGroup* sgroup = &mol.sgroups.getSGroup(sgroup_ids[i]); - // ext_index written as-is (managed by _checkSGroupIndices) - int write_ext = sgroup->ext_index.hasValue() ? sgroup->ext_index.get() : sgroup->index; - output.printf(" %3d %3d", sgroup->index, write_ext); + auto* entry = entry_map[sgroup_ids[i]]; + output.printf(" %3d %3d", entry->write_index, entry->write_ext_index); } output.writeCR(); } @@ -1777,8 +1737,17 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (i = j; i < std::min(sru_count, j + 8); i++) { RepeatingUnit* ru = (RepeatingUnit*)&mol.sgroups.getSGroup(i, SGroup::SG_TYPE_SRU); - - output.printf(" %3d ", ru->index); + // Find write_index for this SRU by pointer match + int ru_wi = 0; + for (const auto& e : write_order) + { + if (&mol.sgroups.getSGroup(e.pool_idx) == ru) + { + ru_wi = e.write_index; + break; + } + } + output.printf(" %3d ", ru_wi); if (ru->connectivity == SGroup::HEAD_TO_HEAD) output.printf("HH "); @@ -1793,10 +1762,11 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) { SGroup& sgroup = mol.sgroups.getSGroup(i); + int wi = entry_map[i]->write_index; for (j = 0; j < sgroup.atoms.size(); j += 8) { int k; - output.printf("M SAL %3d%3d", sgroup.index, std::min(sgroup.atoms.size(), j + 8) - j); + output.printf("M SAL %3d%3d", wi, std::min(sgroup.atoms.size(), j + 8) - j); for (k = j; k < std::min(sgroup.atoms.size(), j + 8); k++) output.printf(" %3d", _atom_mapping[sgroup.atoms[k]]); output.writeCR(); @@ -1804,7 +1774,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (j = 0; j < sgroup.getBonds().size(); j += 8) { int k; - output.printf("M SBL %3d%3d", sgroup.index, std::min(sgroup.getBonds().size(), j + 8) - j); + output.printf("M SBL %3d%3d", wi, std::min(sgroup.getBonds().size(), j + 8) - j); for (k = j; k < std::min(sgroup.getBonds().size(), j + 8); k++) output.printf(" %3d", _bond_mapping[sgroup.getBonds()[k]]); output.writeCR(); @@ -1814,9 +1784,9 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) if (sgroup.sgroup_type != SGroup::SG_TYPE_MUL && sgroup.label.size() > 1) { if (sgroup.label.find(' ') > -1) - output.printfCR("M SMT %3d \"%s\"", sgroup.index, sgroup.label.ptr()); + output.printfCR("M SMT %3d \"%s\"", wi, sgroup.label.ptr()); else - output.printfCR("M SMT %3d %s", sgroup.index, sgroup.label.ptr()); + output.printfCR("M SMT %3d %s", wi, sgroup.label.ptr()); } if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) @@ -1824,18 +1794,18 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) Superatom& superatom = (Superatom&)sgroup; if (superatom.sa_class.size() > 1) - output.printfCR("M SCL %3d %s", superatom.index, superatom.sa_class.ptr()); + output.printfCR("M SCL %3d %s", wi, superatom.sa_class.ptr()); if (superatom.bond_connections.size() > 0) { for (j = 0; j < superatom.bond_connections.size(); j++) { - output.printfCR("M SBV %3d %3d %9.4f %9.4f", superatom.index, _bond_mapping[superatom.bond_connections[j].bond_idx], + output.printfCR("M SBV %3d %3d %9.4f %9.4f", wi, _bond_mapping[superatom.bond_connections[j].bond_idx], superatom.bond_connections[j].bond_dir.x, superatom.bond_connections[j].bond_dir.y); } } if (superatom.contracted == DisplayOption::Expanded) { - output.printfCR("M SDS EXP 1 %3d", superatom.index); + output.printfCR("M SDS EXP 1 %3d", wi); } if (superatom.attachment_points.size() > 0) { @@ -1846,7 +1816,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) { if (next_line) { - output.printf("M SAP %3d%3d", superatom.index, std::min(nrem, 6)); + output.printf("M SAP %3d%3d", wi, std::min(nrem, 6)); next_line = false; } @@ -1876,21 +1846,17 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) { DataSGroup& datasgroup = (DataSGroup&)sgroup; - output.printf("M SDT %3d ", datasgroup.index); + output.printf("M SDT %3d ", wi); _writeFormattedString(output, datasgroup.name, 30); - _writeFormattedString(output, datasgroup.type, 2); - _writeFormattedString(output, datasgroup.description, 20); - _writeFormattedString(output, datasgroup.querycode, 2); - _writeFormattedString(output, datasgroup.queryoper, 15); output.writeCR(); - output.printf("M SDD %3d ", datasgroup.index); + output.printf("M SDD %3d ", wi); _writeDataSGroupDisplay(datasgroup, output); output.writeCR(); @@ -1905,13 +1871,12 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) if (ptr[j] == '\n') break; - // Print ptr[0..i] output.writeString("M "); if (j != 69 || j == k) output.writeString("SED "); else output.writeString("SCD "); - output.printf("%3d ", datasgroup.index); + output.printf("%3d ", wi); output.write(ptr, j); if (ptr[j] == '\n') @@ -1929,45 +1894,35 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (j = 0; j < mg.parent_atoms.size(); j += 8) { int k; - output.printf("M SPA %3d%3d", mg.index, std::min(mg.parent_atoms.size(), j + 8) - j); + output.printf("M SPA %3d%3d", wi, std::min(mg.parent_atoms.size(), j + 8) - j); for (k = j; k < std::min(mg.parent_atoms.size(), j + 8); k++) output.printf(" %3d", _atom_mapping[mg.parent_atoms[k]]); output.writeCR(); } - output.printf("M SMT %3d %d\n", mg.index, (mg.multiplier.hasValue() ? mg.multiplier.get() : 0)); + output.printf("M SMT %3d %d\n", wi, (mg.multiplier.hasValue() ? mg.multiplier.get() : 0)); } for (j = 0; j < sgroup.brackets.size(); j++) { - output.printf("M SDI %3d 4 %9.4f %9.4f %9.4f %9.4f\n", sgroup.index, sgroup.brackets[j][0].x, sgroup.brackets[j][0].y, - sgroup.brackets[j][1].x, sgroup.brackets[j][1].y); + output.printf("M SDI %3d 4 %9.4f %9.4f %9.4f %9.4f\n", wi, sgroup.brackets[j][0].x, sgroup.brackets[j][0].y, sgroup.brackets[j][1].x, + sgroup.brackets[j][1].y); } if (sgroup.brackets.size() > 0 && sgroup.brk_style > 0) { - output.printf("M SBT 1 %3d %3d\n", sgroup.index, (sgroup.brk_style.hasValue() ? sgroup.brk_style.get() : 0)); + output.printf("M SBT 1 %3d %3d\n", wi, (sgroup.brk_style.hasValue() ? sgroup.brk_style.get() : 0)); } if (sgroup.sgroup_subtype > 0) { if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_ALT) - output.printf("M SST 1 %3d ALT\n", sgroup.index); + output.printf("M SST 1 %3d ALT\n", wi); else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_RAN) - output.printf("M SST 1 %3d RAN\n", sgroup.index); + output.printf("M SST 1 %3d RAN\n", wi); else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_BLO) - output.printf("M SST 1 %3d BLO\n", sgroup.index); + output.printf("M SST 1 %3d BLO\n", wi); } } } _removeImplicitSGroups(mol, implicit_sgroups_indexes); - - // Restore ext_index state after writing (prevent save-time auto-assign from persisting) - for (const auto& s : saved_ext) - { - if (mol.sgroups.hasSGroup(s.pool_idx)) - { - SGroup& sg = mol.sgroups.getSGroup(s.pool_idx); - sg.ext_index = s.ext_index; - } - } } void MolfileSaver::_writeFormattedString(Output& output, Array& str, int length) @@ -1995,107 +1950,6 @@ void MolfileSaver::_writeFormattedString(Output& output, Array& str, int l output.writeChar(' '); } -void MolfileSaver::_checkSGroupIndices(BaseMolecule& mol, Array& sgs_list) -{ - QS_DEF(Array, orig_ids); - QS_DEF(Array, added_ids); - QS_DEF(Array, sgs_mapping); - QS_DEF(Array, sgs_changed); - - sgs_list.clear(); - orig_ids.clear(); - added_ids.clear(); - sgs_mapping.clear_resize(mol.sgroups.end()); - sgs_mapping.zerofill(); - sgs_changed.clear_resize(mol.sgroups.end()); - sgs_changed.zerofill(); - - int iw = 1; - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) - { - SGroup& sgroup = mol.sgroups.getSGroup(i); - if (sgroup.parent_group == 0) - { - sgs_mapping[i] = iw; - iw++; - } - } - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) - { - if (sgs_mapping[i] == 0) - { - sgs_mapping[i] = iw; - iw++; - } - } - - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) - { - SGroup& sgroup = mol.sgroups.getSGroup(i); - if (sgroup.index == 0) - { - sgroup.index = sgs_mapping[i]; - } - else - { - for (int j = mol.sgroups.begin(); j != mol.sgroups.end(); j = mol.sgroups.next(j)) - { - SGroup& sg = mol.sgroups.getSGroup(j); - if (sg.parent_group == sgroup.index && sgs_changed[j] == 0) - { - sg.parent_group = sgs_mapping[i]; - sgs_changed[j] = 1; - } - } - sgroup.index = sgs_mapping[i]; - } - // Per BIOVIA spec: if ext_index not set, auto-assign from index - if (!sgroup.ext_index.hasValue()) - sgroup.ext_index = sgroup.index; - orig_ids.push(sgroup.index); - } - - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) - { - SGroup& sgroup = mol.sgroups.getSGroup(i); - if (sgroup.parent_group == 0) - { - sgs_list.push(i); - added_ids.push(sgroup.index); - } - else - { - if (orig_ids.find(sgroup.parent_group) == -1 || sgroup.parent_group == sgroup.index) - { - sgroup.parent_group = 0; - sgs_list.push(i); - added_ids.push(sgroup.index); - } - } - } - - for (;;) - { - for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) - { - SGroup& sgroup = mol.sgroups.getSGroup(i); - if (sgroup.parent_group == 0) - continue; - - if (added_ids.find(sgroup.index) != -1) - continue; - - if (added_ids.find(sgroup.parent_group) != -1) - { - sgs_list.push(i); - added_ids.push(sgroup.index); - } - } - if (sgs_list.size() == mol.countSGroups()) - break; - } -} - int MolfileSaver::_getStereocenterParity(BaseMolecule& mol, int idx) { int type = mol.stereocenters.getType(idx); @@ -2257,8 +2111,9 @@ bool MolfileSaver::_checkAttPointOrder(BaseMolecule& mol, int rsite) void MolfileSaver::_writeDataSGroupDisplay(DataSGroup& datasgroup, Output& out) { - out.printf("%10.4f%10.4f %c%c%c", datasgroup.display_pos->x, datasgroup.display_pos->y, datasgroup.detached ? 'D' : 'A', datasgroup.relative ? 'R' : 'A', - datasgroup.display_units ? 'U' : ' '); + float dp_x = datasgroup.display_pos.hasValue() ? datasgroup.display_pos->x : 0.0f; + float dp_y = datasgroup.display_pos.hasValue() ? datasgroup.display_pos->y : 0.0f; + out.printf("%10.4f%10.4f %c%c%c", dp_x, dp_y, datasgroup.detached ? 'D' : 'A', datasgroup.relative ? 'R' : 'A', datasgroup.display_units ? 'U' : ' '); if (datasgroup.num_chars == 0) out.printf(" ALL 1 %c %1d ", (datasgroup.tag.hasValue() ? datasgroup.tag.get() : 0), (datasgroup.dasp_pos.hasValue() ? datasgroup.dasp_pos.get() : 0)); diff --git a/core/render2d/src/render_internal.cpp b/core/render2d/src/render_internal.cpp index 9051daba6a..6d692de9de 100644 --- a/core/render2d/src/render_internal.cpp +++ b/core/render2d/src/render_internal.cpp @@ -594,7 +594,8 @@ void MoleculeRenderInternal::_initSGroups(Tree& sgroups, Rect2f parent) } else if (group.relative) { - _objDistTransform(ti.bbp, group.display_pos.get()); + if (group.display_pos.hasValue()) + _objDistTransform(ti.bbp, group.display_pos.get()); if (group.atoms.size() > 0) { ti.bbp.add(_ad(group.atoms[0]).pos); @@ -606,7 +607,8 @@ void MoleculeRenderInternal::_initSGroups(Tree& sgroups, Rect2f parent) } else { - _objCoordTransform(ti.bbp, group.display_pos.get()); + if (group.display_pos.hasValue()) + _objCoordTransform(ti.bbp, group.display_pos.get()); } parent = ILLEGAL_RECT(); From c3a9a8870ec28a8575cc59653fdb7f1246b5441f Mon Sep 17 00:00:00 2001 From: even1024 Date: Fri, 15 May 2026 00:24:46 +0200 Subject: [PATCH 17/33] fix review: cycle guard, xbonds iter, display_pos roundtrip, ext_index merge --- api/c/indigo/src/indigo_molecule.cpp | 23 +++++++++++++++++++ api/c/indigo/src/indigo_molecule.h | 16 +++++++++++++ .../indigo/src/indigo_molecule_operations.cpp | 2 +- .../molecule/src/base_molecule.cpp | 1 + .../molecule/src/molecule_json_loader.cpp | 1 + .../molecule/src/molecule_sgroups.cpp | 15 ++++++++++++ 6 files changed, 57 insertions(+), 1 deletion(-) diff --git a/api/c/indigo/src/indigo_molecule.cpp b/api/c/indigo/src/indigo_molecule.cpp index 74083d2d05..06f2643386 100644 --- a/api/c/indigo/src/indigo_molecule.cpp +++ b/api/c/indigo/src/indigo_molecule.cpp @@ -1211,6 +1211,29 @@ IndigoObject* IndigoSGroupBondsIter::next() return new IndigoBond(_mol, _sgroup.getBonds()[_idx]); } +IndigoSGroupXBondsIter::IndigoSGroupXBondsIter(BaseMolecule& mol, SGroup& sgroup) : IndigoObject(SGROUP_ATOMS_ITER), _mol(mol), _sgroup(sgroup) +{ + _idx = -1; +} + +IndigoSGroupXBondsIter::~IndigoSGroupXBondsIter() +{ +} + +bool IndigoSGroupXBondsIter::hasNext() +{ + return _idx + 1 < _sgroup.xbonds.size(); +} + +IndigoObject* IndigoSGroupXBondsIter::next() +{ + if (!hasNext()) + return 0; + + _idx++; + return new IndigoBond(_mol, _sgroup.xbonds[_idx]); +} + int _indigoIterateAtoms(Indigo& self, int molecule, int type) { return self.addObject(new IndigoAtomsIter(&self.getObject(molecule).getBaseMolecule(), type)); diff --git a/api/c/indigo/src/indigo_molecule.h b/api/c/indigo/src/indigo_molecule.h index b75c53263d..bd478a8262 100644 --- a/api/c/indigo/src/indigo_molecule.h +++ b/api/c/indigo/src/indigo_molecule.h @@ -559,6 +559,22 @@ class IndigoSGroupBondsIter : public IndigoObject int _idx; }; +// Iterates xbonds (crossing bonds) directly, not polymorphic getBonds() +class IndigoSGroupXBondsIter : public IndigoObject +{ +public: + IndigoSGroupXBondsIter(BaseMolecule& mol, SGroup& sgroup); + ~IndigoSGroupXBondsIter() override; + + IndigoObject* next() override; + bool hasNext() override; + +protected: + BaseMolecule& _mol; + SGroup& _sgroup; + int _idx; +}; + class IndigoMoleculeComponent : public IndigoObject { public: diff --git a/api/c/indigo/src/indigo_molecule_operations.cpp b/api/c/indigo/src/indigo_molecule_operations.cpp index f61d22c191..1e54abfbd3 100644 --- a/api/c/indigo/src/indigo_molecule_operations.cpp +++ b/api/c/indigo/src/indigo_molecule_operations.cpp @@ -1868,7 +1868,7 @@ CEXPORT int indigoIterateSGroupCrossBonds(int sgroup) INDIGO_BEGIN { IndigoSGroup& isg = IndigoSGroup::cast(self.getObject(sgroup)); - return self.addObject(new IndigoSGroupBondsIter(isg.mol, isg.get())); + return self.addObject(new IndigoSGroupXBondsIter(isg.mol, isg.get())); } INDIGO_END(-1); } diff --git a/core/indigo-core/molecule/src/base_molecule.cpp b/core/indigo-core/molecule/src/base_molecule.cpp index ccbd7b203f..15516fa6ef 100644 --- a/core/indigo-core/molecule/src/base_molecule.cpp +++ b/core/indigo-core/molecule/src/base_molecule.cpp @@ -177,6 +177,7 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma SGroup& sg = sgroups.getSGroup(idx); sg.parent_idx = supersg.parent_idx; sg.index = supersg.index; + sg.ext_index = supersg.ext_index; sg.parent_group = supersg.parent_group; sg.label.copy(supersg.label); diff --git a/core/indigo-core/molecule/src/molecule_json_loader.cpp b/core/indigo-core/molecule/src/molecule_json_loader.cpp index 572fadd3c1..0adaba51ba 100644 --- a/core/indigo-core/molecule/src/molecule_json_loader.cpp +++ b/core/indigo-core/molecule/src/molecule_json_loader.cpp @@ -1125,6 +1125,7 @@ void MoleculeJsonLoader::parseSGroups(const rapidjson::Value& sgroups, BaseMolec if (s.HasMember("queryOp")) dsg.queryoper.readString(s["queryOp"].GetString(), true); + if (s.HasMember("x") || s.HasMember("y")) { Vec2f dp; if (s.HasMember("x")) diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index c5188aed15..9e078f2b0a 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -819,6 +819,7 @@ std::vector indigo::getOrderedSGroups(MoleculeSGroups& sgroups // Then iteratively add children whose parents are already added while (result.size() < all_entries.size()) { + size_t prev_size = result.size(); for (auto& e : all_entries) { if (added_indices.count(e.write_index)) @@ -829,6 +830,20 @@ std::vector indigo::getOrderedSGroups(MoleculeSGroups& sgroups added_indices.insert(e.write_index); } } + // No progress — cyclic or orphan refs; break cycles by adding as roots + if (result.size() == prev_size) + { + for (auto& e : all_entries) + { + if (!added_indices.count(e.write_index)) + { + e.write_parent = 0; + result.push_back(e); + added_indices.insert(e.write_index); + } + } + break; + } } return result; From c8c544aac6a0b1c66f6158e8ac2d490ba92e5959 Mon Sep 17 00:00:00 2001 From: even1024 Date: Fri, 15 May 2026 01:17:07 +0200 Subject: [PATCH 18/33] fix sgroup xbonds roundtrip --- api/c/indigo/src/indigo_molecule.cpp | 4 +-- .../ref/basic/3604_sgroup_atoms_bonds.py.out | 5 +++ .../tests/basic/3604_sgroup_atoms_bonds.py | 23 ++++++++++++ .../molecule/src/molfile_loader_v3000.cpp | 10 +++++- .../molecule/src/molfile_saver.cpp | 36 ++++++++++++++----- 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/api/c/indigo/src/indigo_molecule.cpp b/api/c/indigo/src/indigo_molecule.cpp index 06f2643386..5d18a5daf0 100644 --- a/api/c/indigo/src/indigo_molecule.cpp +++ b/api/c/indigo/src/indigo_molecule.cpp @@ -1188,7 +1188,7 @@ IndigoObject* IndigoSGroupAtomsIter::next() return new IndigoAtom(_mol, _sgroup.atoms[_idx]); } -IndigoSGroupBondsIter::IndigoSGroupBondsIter(BaseMolecule& mol, SGroup& sgroup) : IndigoObject(SGROUP_ATOMS_ITER), _mol(mol), _sgroup(sgroup) +IndigoSGroupBondsIter::IndigoSGroupBondsIter(BaseMolecule& mol, SGroup& sgroup) : IndigoObject(SGROUP_BONDS_ITER), _mol(mol), _sgroup(sgroup) { _idx = -1; } @@ -1211,7 +1211,7 @@ IndigoObject* IndigoSGroupBondsIter::next() return new IndigoBond(_mol, _sgroup.getBonds()[_idx]); } -IndigoSGroupXBondsIter::IndigoSGroupXBondsIter(BaseMolecule& mol, SGroup& sgroup) : IndigoObject(SGROUP_ATOMS_ITER), _mol(mol), _sgroup(sgroup) +IndigoSGroupXBondsIter::IndigoSGroupXBondsIter(BaseMolecule& mol, SGroup& sgroup) : IndigoObject(SGROUP_BONDS_ITER), _mol(mol), _sgroup(sgroup) { _idx = -1; } diff --git a/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out index 0777583df6..62f65cb461 100644 --- a/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out +++ b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out @@ -57,6 +57,11 @@ type: 2 atoms: 3 bonds: 2 sgroup count: 1 +****** Molfile roundtrip: DAT with containment and cross bonds ******** +DAT molfile V3000: True +DAT line: M V30 1 DAT 1 ATOMS=(3 2 3 4) XBONDS=(2 1 4) CBONDS=(2 2 3) FIELDDISP=" 0- +DAT containment bonds: 1 2 +DAT cross bonds: 0 3 ****** ext_index: V3000 roundtrip with explicit extindex ******** ext_index before save: 42 roundtrip: index=1 extindex=42 diff --git a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py index 7229943ff0..2afbd6e201 100644 --- a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py +++ b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py @@ -231,6 +231,29 @@ print("sgroup count: {0}".format(sg_count)) +print("****** Molfile roundtrip: DAT with containment and cross bonds ********") + +indigo.setOption("molfile-saving-mode", "auto") +mol = indigo.loadMolecule("CCCCCC") +sg = mol.addSGroup("DAT", 0) +sg.setSGroupAtoms([1, 2, 3]) +sg.setSGroupBonds([1, 2]) +sg.createCrossBonds() + +molfile = mol.molfile() +print("DAT molfile V3000: {0}".format("V3000" in molfile)) +sgroup_lines = [l.strip() for l in molfile.split("\n") if " DAT " in l] +for l in sgroup_lines: + print("DAT line: {0}".format(l)) + +mol2 = indigo.loadMolecule(molfile) +for sg in mol2.iterateDataSGroups(): + cbonds = sorted([str(b.index()) for b in sg.iterateBonds()]) + xbonds = sorted([str(b.index()) for b in sg.iterateSGroupCrossBonds()]) + print("DAT containment bonds: {0}".format(" ".join(cbonds))) + print("DAT cross bonds: {0}".format(" ".join(xbonds))) + + # ===== ext_index roundtrip V3000 ===== diff --git a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp index fb423d1788..08ab1394c0 100644 --- a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp @@ -1191,11 +1191,19 @@ void MolfileLoader::_readSGroup3000(const char* str) } else if ((strcmp(entity.ptr(), "XBONDS") == 0) || (strcmp(entity.ptr(), "CBONDS") == 0)) { + Array* bonds = 0; + if (strcmp(entity.ptr(), "XBONDS") == 0) + bonds = &sgroup->xbonds; + else if (dsg != 0) + bonds = &dsg->cbonds; + else + bonds = &sgroup->getBonds(); + scanner.skip(1); // ( n = scanner.readInt1(); while (n-- > 0) { - sgroup->getBonds().push(scanner.readInt() - 1); + bonds->push(scanner.readInt() - 1); scanner.skipSpace(); } scanner.skip(1); // ) diff --git a/core/indigo-core/molecule/src/molfile_saver.cpp b/core/indigo-core/molecule/src/molfile_saver.cpp index bfd4c2b45d..7c9a00034a 100644 --- a/core/indigo-core/molecule/src/molfile_saver.cpp +++ b/core/indigo-core/molecule/src/molfile_saver.cpp @@ -206,6 +206,18 @@ void MolfileSaver::_saveMolecule(BaseMolecule& bmol, bool query) BaseMolecule* pmol = &bmol; std::unique_ptr mol(bmol.neu()); mol->clone_KeepIndices(bmol); + + bool has_dat_xbonds = false; + for (int i = pmol->sgroups.begin(); i != pmol->sgroups.end(); i = pmol->sgroups.next(i)) + { + SGroup& sgroup = pmol->sgroups.getSGroup(i); + if (sgroup.sgroup_type == SGroup::SG_TYPE_DAT && sgroup.xbonds.size() > 0) + { + has_dat_xbonds = true; + break; + } + } + if (mode == MODE_2000) { _v2000 = true; @@ -219,7 +231,7 @@ void MolfileSaver::_saveMolecule(BaseMolecule& bmol, bool query) // auto-detect the format: save to v3000 molfile only // if v2000 is not enough _v2000 = !(pmol->hasHighlighting() || pmol->stereocenters.haveEnhancedStereocenter() || - (pmol->vertexCount() > 999 || pmol->edgeCount() > 999 || pmol->tgroups.getTGroupCount())); + (pmol->vertexCount() > 999 || pmol->edgeCount() > 999 || pmol->tgroups.getTGroupCount()) || has_dat_xbonds); } if (mol->tgroups.getTGroupCount() && mol->convertTemplateAtomsToSuperatoms(!_v2000)) @@ -1116,16 +1128,24 @@ void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, const SGroupWriteEntr output.printf(" %d", _atom_mapping[sgroup.atoms[i]]); output.printf(")"); } - if (sgroup.getBonds().size() > 0) + if (sgroup.xbonds.size() > 0) { - if (sgroup.sgroup_type == SGroup::SG_TYPE_DAT) - output.printf(" CBONDS=(%d", sgroup.getBonds().size()); - else - output.printf(" XBONDS=(%d", sgroup.getBonds().size()); - for (i = 0; i < sgroup.getBonds().size(); i++) - output.printf(" %d", _bond_mapping[sgroup.getBonds()[i]]); + output.printf(" XBONDS=(%d", sgroup.xbonds.size()); + for (i = 0; i < sgroup.xbonds.size(); i++) + output.printf(" %d", _bond_mapping[sgroup.xbonds[i]]); output.printf(")"); } + if (sgroup.sgroup_type == SGroup::SG_TYPE_DAT) + { + DataSGroup& dsgroup = (DataSGroup&)sgroup; + if (dsgroup.cbonds.size() > 0) + { + output.printf(" CBONDS=(%d", dsgroup.cbonds.size()); + for (i = 0; i < dsgroup.cbonds.size(); i++) + output.printf(" %d", _bond_mapping[dsgroup.cbonds[i]]); + output.printf(")"); + } + } if (sgroup.sgroup_subtype > 0) { if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_ALT) From 7fc7874e90ac136bf06cd88a2d69fe911b54ea67 Mon Sep 17 00:00:00 2001 From: even1024 Date: Fri, 15 May 2026 02:52:40 +0200 Subject: [PATCH 19/33] clean sgroup extindex test --- .../integration/ref/basic/3604_sgroup_atoms_bonds.py.out | 2 -- .../integration/tests/basic/3604_sgroup_atoms_bonds.py | 7 ------- 2 files changed, 9 deletions(-) diff --git a/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out index 62f65cb461..69ff5b3a84 100644 --- a/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out +++ b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out @@ -11,8 +11,6 @@ MUL type: 4 MUL atoms: 0 GEN type: 0 GEN atoms: 0 -****** addSGroup: with explicit extindex ******** -extindex: 0 ****** setSGroupAtoms: set atoms on empty SGroup ******** atoms after set: 3 atom symbols: C C C diff --git a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py index 2afbd6e201..35ca79a122 100644 --- a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py +++ b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py @@ -53,13 +53,6 @@ print("GEN type: {0}".format(sg_gen.getSGroupType())) print("GEN atoms: {0}".format(sg_gen.countAtoms())) -print("****** addSGroup: with explicit extindex ********") - -mol2 = indigo.loadMolecule("CCCCCC") -sg = mol2.addSGroup("SUP", 42) -print("extindex: {0}".format(sg.getSGroupOriginalId())) - - # ===== setSGroupAtoms ===== print("****** setSGroupAtoms: set atoms on empty SGroup ********") From 4f9941a61f2d4915d495aa078617caaa0df22656 Mon Sep 17 00:00:00 2001 From: even1024 Date: Fri, 15 May 2026 03:52:12 +0200 Subject: [PATCH 20/33] black format --- api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py index 35ca79a122..ef2053218c 100644 --- a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py +++ b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py @@ -224,7 +224,9 @@ print("sgroup count: {0}".format(sg_count)) -print("****** Molfile roundtrip: DAT with containment and cross bonds ********") +print( + "****** Molfile roundtrip: DAT with containment and cross bonds ********" +) indigo.setOption("molfile-saving-mode", "auto") mol = indigo.loadMolecule("CCCCCC") From e82b7bd4103f011c285220ef93e837f0a70efbe4 Mon Sep 17 00:00:00 2001 From: even1024 Date: Fri, 15 May 2026 10:03:50 +0200 Subject: [PATCH 21/33] update sgroup layout ref --- .../integration/tests/layout/ref/3291-selection.ket | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/api/tests/integration/tests/layout/ref/3291-selection.ket b/api/tests/integration/tests/layout/ref/3291-selection.ket index 8bc9269580..c334251b4b 100644 --- a/api/tests/integration/tests/layout/ref/3291-selection.ket +++ b/api/tests/integration/tests/layout/ref/3291-selection.ket @@ -564,8 +564,6 @@ ], "fieldName": "MDLBG_STEREO_KEY", "fieldData": "SR", - "x": -24.4893, - "y": 15.9961, "display": true }, { @@ -575,8 +573,6 @@ ], "fieldName": "MDLBG_STEREO_KEY", "fieldData": "SR", - "x": -24.3783, - "y": 15.0602, "display": true }, { @@ -586,8 +582,6 @@ ], "fieldName": "MDLBG_STEREO_KEY", "fieldData": "SR", - "x": -24.2673, - "y": 16.7372, "display": true }, { @@ -597,8 +591,6 @@ ], "fieldName": "MDLBG_STEREO_KEY", "fieldData": "SR", - "x": -24.1645, - "y": 14.4936, "display": true } ] @@ -870,4 +862,4 @@ } ] } -} \ No newline at end of file +} From 45c248d132c26db87dddde2b1057a8a073ce0c04 Mon Sep 17 00:00:00 2001 From: even1024 Date: Fri, 15 May 2026 10:12:13 +0200 Subject: [PATCH 22/33] fix displayed sgroup position --- .../integration/tests/layout/ref/3291-selection.ket | 10 +++++++++- core/indigo-core/molecule/src/molecule_json_loader.cpp | 7 ++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/api/tests/integration/tests/layout/ref/3291-selection.ket b/api/tests/integration/tests/layout/ref/3291-selection.ket index c334251b4b..8bc9269580 100644 --- a/api/tests/integration/tests/layout/ref/3291-selection.ket +++ b/api/tests/integration/tests/layout/ref/3291-selection.ket @@ -564,6 +564,8 @@ ], "fieldName": "MDLBG_STEREO_KEY", "fieldData": "SR", + "x": -24.4893, + "y": 15.9961, "display": true }, { @@ -573,6 +575,8 @@ ], "fieldName": "MDLBG_STEREO_KEY", "fieldData": "SR", + "x": -24.3783, + "y": 15.0602, "display": true }, { @@ -582,6 +586,8 @@ ], "fieldName": "MDLBG_STEREO_KEY", "fieldData": "SR", + "x": -24.2673, + "y": 16.7372, "display": true }, { @@ -591,6 +597,8 @@ ], "fieldName": "MDLBG_STEREO_KEY", "fieldData": "SR", + "x": -24.1645, + "y": 14.4936, "display": true } ] @@ -862,4 +870,4 @@ } ] } -} +} \ No newline at end of file diff --git a/core/indigo-core/molecule/src/molecule_json_loader.cpp b/core/indigo-core/molecule/src/molecule_json_loader.cpp index 0adaba51ba..4b367533c8 100644 --- a/core/indigo-core/molecule/src/molecule_json_loader.cpp +++ b/core/indigo-core/molecule/src/molecule_json_loader.cpp @@ -1125,9 +1125,10 @@ void MoleculeJsonLoader::parseSGroups(const rapidjson::Value& sgroups, BaseMolec if (s.HasMember("queryOp")) dsg.queryoper.readString(s["queryOp"].GetString(), true); - if (s.HasMember("x") || s.HasMember("y")) + bool display_units = s.HasMember("display") && s["display"].GetBool(); + if (s.HasMember("x") || s.HasMember("y") || display_units) { - Vec2f dp; + Vec2f dp(0.0f, 0.0f); if (s.HasMember("x")) dp.x = s["x"].GetFloat(); @@ -1145,7 +1146,7 @@ void MoleculeJsonLoader::parseSGroups(const rapidjson::Value& sgroups, BaseMolec dsg.relative = s["placement"].GetBool(); if (s.HasMember("display")) - dsg.display_units = s["display"].GetBool(); + dsg.display_units = display_units; if (s.HasMember("tag")) { From 4dbeb6d75578f762d0be7cae2a107b4633907ad8 Mon Sep 17 00:00:00 2001 From: even1024 Date: Fri, 15 May 2026 11:51:22 +0200 Subject: [PATCH 23/33] python 2 fix --- api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out | 2 +- api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out index 69ff5b3a84..4c7f99e320 100644 --- a/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out +++ b/api/tests/integration/ref/basic/3604_sgroup_atoms_bonds.py.out @@ -56,7 +56,7 @@ atoms: 3 bonds: 2 sgroup count: 1 ****** Molfile roundtrip: DAT with containment and cross bonds ******** -DAT molfile V3000: True +DAT molfile V3000: 1 DAT line: M V30 1 DAT 1 ATOMS=(3 2 3 4) XBONDS=(2 1 4) CBONDS=(2 2 3) FIELDDISP=" 0- DAT containment bonds: 1 2 DAT cross bonds: 0 3 diff --git a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py index ef2053218c..3e10ddcd18 100644 --- a/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py +++ b/api/tests/integration/tests/basic/3604_sgroup_atoms_bonds.py @@ -236,7 +236,7 @@ sg.createCrossBonds() molfile = mol.molfile() -print("DAT molfile V3000: {0}".format("V3000" in molfile)) +print("DAT molfile V3000: {0}".format(int("V3000" in molfile))) sgroup_lines = [l.strip() for l in molfile.split("\n") if " DAT " in l] for l in sgroup_lines: print("DAT line: {0}".format(l)) From 0e4c68b8c9bfc24dd06f1d6af8d694d39dd55c07 Mon Sep 17 00:00:00 2001 From: even1024 Date: Fri, 15 May 2026 14:35:44 +0200 Subject: [PATCH 24/33] ci fix --- .github/workflows/indigo-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/indigo-ci.yaml b/.github/workflows/indigo-ci.yaml index 973b7ef5d7..b3ac8729bb 100644 --- a/.github/workflows/indigo-ci.yaml +++ b/.github/workflows/indigo-ci.yaml @@ -956,7 +956,7 @@ jobs: build_indigo_utils_x86_64: strategy: fail-fast: false - matrix: ${{ fromJSON(needs.set_matrix.outputs.matrix) }} + matrix: ${{ fromJSON(needs.set_matrix.outputs.matrix || '{"os":["ubuntu-latest","windows-latest"]}') }} runs-on: ${{ matrix.os }} needs: [build_bingo_postgres_linux_x86_64, build_bingo_postgres_windows_msvc_x86_64, build_bingo_postgres_macos_x86_64, set_matrix] if: ${{ always() && (needs.build_bingo_postgres_macos_x86_64.result == 'success' || needs.build_bingo_postgres_macos_x86_64.result == 'skipped') }} From 84b5362f84e022bab57b2594e75edeceb22efeda Mon Sep 17 00:00:00 2001 From: Roman Porozhnetov Date: Thu, 28 May 2026 12:44:20 +0400 Subject: [PATCH 25/33] Restore Nullable fail-fast semantics --- .../indigo/src/indigo_molecule_operations.cpp | 17 +++---- core/indigo-core/common/base_cpp/nullable.h | 46 ++----------------- .../molecule/src/base_molecule.cpp | 32 +++++++++---- .../molecule/src/base_molecule_templates.cpp | 10 +++- core/indigo-core/molecule/src/cmf_saver.cpp | 3 +- core/indigo-core/molecule/src/cml_saver.cpp | 5 +- .../molecule/src/molecule_cdxml_saver.cpp | 5 +- .../molecule/src/molecule_json_saver.cpp | 7 +-- .../molecule/src/molecule_sgroups.cpp | 4 +- .../molecule/src/molfile_saver.cpp | 14 ++++-- core/indigo-core/tests/tests/formats.cpp | 11 ++--- core/render2d/src/render_internal.cpp | 6 ++- 12 files changed, 77 insertions(+), 83 deletions(-) diff --git a/api/c/indigo/src/indigo_molecule_operations.cpp b/api/c/indigo/src/indigo_molecule_operations.cpp index 1e54abfbd3..9e6add1a62 100644 --- a/api/c/indigo/src/indigo_molecule_operations.cpp +++ b/api/c/indigo/src/indigo_molecule_operations.cpp @@ -1600,7 +1600,7 @@ CEXPORT int indigoSetSGroupXCoord(int sgroup, float x) { DataSGroup& dsg = IndigoDataSGroup::cast(self.getObject(sgroup)).get(); - Vec2f dp = dsg.display_pos.get(); + Vec2f dp = dsg.display_pos.hasValue() ? dsg.display_pos.get() : Vec2f(0, 0); dp.x = x; dsg.display_pos.set(dp); @@ -1615,7 +1615,7 @@ CEXPORT int indigoSetSGroupYCoord(int sgroup, float y) { DataSGroup& dsg = IndigoDataSGroup::cast(self.getObject(sgroup)).get(); - Vec2f dp = dsg.display_pos.get(); + Vec2f dp = dsg.display_pos.hasValue() ? dsg.display_pos.get() : Vec2f(0, 0); dp.y = y; dsg.display_pos.set(dp); @@ -2022,8 +2022,9 @@ CEXPORT int indigoGetSGroupDisplayOption(int sgroup) INDIGO_BEGIN { Superatom& sup = IndigoSuperatom::cast(self.getObject(sgroup)).get(); - if (sup.contracted > DisplayOption::Undefined) - return (int)(sup.contracted.hasValue() ? sup.contracted.get() : DisplayOption::Undefined); + const auto option = sup.contracted.hasValue() ? sup.contracted.get() : DisplayOption::Undefined; + if (option > DisplayOption::Undefined) + return static_cast(option); return 0; } @@ -2061,9 +2062,9 @@ CEXPORT float* indigoGetSGroupCoords(int sgroup) IndigoDataSGroup& ds = IndigoDataSGroup::cast(self.getObject(sgroup)); auto& tmp = self.getThreadTmpData(); - auto& xy = ds.get().display_pos; - tmp.xyz[0] = xy->x; - tmp.xyz[1] = xy->y; + const Vec2f& xy = ds.get().display_pos.get(); + tmp.xyz[0] = xy.x; + tmp.xyz[1] = xy.y; tmp.xyz[2] = 0.f; return tmp.xyz; } @@ -3496,4 +3497,4 @@ CEXPORT int indigoExpandedMonomersToAtoms(int molecule) return self.addObject(new_mol.release()); } INDIGO_END(-1); -} \ No newline at end of file +} diff --git a/core/indigo-core/common/base_cpp/nullable.h b/core/indigo-core/common/base_cpp/nullable.h index 78caf17374..d1b9085770 100644 --- a/core/indigo-core/common/base_cpp/nullable.h +++ b/core/indigo-core/common/base_cpp/nullable.h @@ -31,57 +31,21 @@ namespace indigo class Nullable { public: - Nullable() : _has_value(false), _value{} + Nullable() : _has_value(false) { variable_name.readString("", true); } - Nullable(const Nullable& other) : _has_value(other._has_value), _value(other._value) - { - variable_name.copy(other.variable_name); - } - - Nullable& operator=(const Nullable& other) - { - _has_value = other._has_value; - _value = other._value; - variable_name.copy(other.variable_name); - return *this; - } - const T& get() const { + if (!_has_value) + throw Error("\"%s\" variable was not set", variable_name.ptr()); return _value; } - T& get() - { - return _value; - } - - bool operator==(const T& other) const - { - return _value == other; - } - - bool operator!=(const T& other) const - { - return _value != other; - } - - operator T&() - { - return _value; - } - - const T* operator->() const - { - return &get(); - } - - T* operator->() + operator const T&() const { - return &_value; + return get(); } Nullable& operator=(const T& value) diff --git a/core/indigo-core/molecule/src/base_molecule.cpp b/core/indigo-core/molecule/src/base_molecule.cpp index 15516fa6ef..7c13c8482a 100644 --- a/core/indigo-core/molecule/src/base_molecule.cpp +++ b/core/indigo-core/molecule/src/base_molecule.cpp @@ -47,6 +47,18 @@ using namespace indigo; IMPL_ERROR(BaseMolecule, "molecule"); +namespace +{ + template + void copyNullable(Nullable& dst, const Nullable& src) + { + if (src.hasValue()) + dst = src.get(); + else + dst.reset(); + } +} // namespace + BaseMolecule::BaseMolecule() : original_format(BaseMolecule::UNKNOWN), _edit_revision(0) { } @@ -175,10 +187,10 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma SGroup& supersg = mol.sgroups.getSGroup(i); int idx = sgroups.addSGroup(supersg.sgroup_type); SGroup& sg = sgroups.getSGroup(idx); - sg.parent_idx = supersg.parent_idx; + copyNullable(sg.parent_idx, supersg.parent_idx); sg.index = supersg.index; sg.ext_index = supersg.ext_index; - sg.parent_group = supersg.parent_group; + copyNullable(sg.parent_group, supersg.parent_group); sg.label.copy(supersg.label); if (_mergeSGroupWithSubmolecule(sg, supersg, mol, mapping, edge_mapping)) @@ -192,10 +204,10 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma DataSGroup& superdg = (DataSGroup&)supersg; dg.detached = superdg.detached; - dg.display_pos = superdg.display_pos; + copyNullable(dg.display_pos, superdg.display_pos); dg.data.copy(superdg.data); dg.sa_natreplace.copy(superdg.sa_natreplace); - dg.dasp_pos = superdg.dasp_pos; + copyNullable(dg.dasp_pos, superdg.dasp_pos); dg.relative = superdg.relative; dg.display_units = superdg.display_units; dg.description.copy(superdg.description); @@ -203,8 +215,8 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma dg.type.copy(superdg.type); dg.querycode.copy(superdg.querycode); dg.queryoper.copy(superdg.queryoper); - dg.num_chars = superdg.num_chars; - dg.tag = superdg.tag; + copyNullable(dg.num_chars, superdg.num_chars); + copyNullable(dg.tag, superdg.tag); } else if (sg.sgroup_type == SGroup::SG_TYPE_SUP) { @@ -225,7 +237,7 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma } sa.sa_class.copy(supersa.sa_class); sa.sa_natreplace.copy(supersa.sa_natreplace); - sa.contracted = supersa.contracted; + copyNullable(sa.contracted, supersa.contracted); if (supersa.attachment_points.size() > 0) { for (int j = supersa.attachment_points.begin(); j < supersa.attachment_points.end(); j = supersa.attachment_points.next(j)) @@ -246,21 +258,21 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma ap.apid.copy(supersa.attachment_points[j].apid); } } - sa.display_position = supersa.display_position; + copyNullable(sa.display_position, supersa.display_position); } else if (sg.sgroup_type == SGroup::SG_TYPE_SRU) { RepeatingUnit& ru = (RepeatingUnit&)sg; RepeatingUnit& superru = (RepeatingUnit&)supersg; - ru.connectivity = superru.connectivity; + copyNullable(ru.connectivity, superru.connectivity); } else if (sg.sgroup_type == SGroup::SG_TYPE_MUL) { MultipleGroup& mg = (MultipleGroup&)sg; MultipleGroup& supermg = (MultipleGroup&)supersg; - mg.multiplier = supermg.multiplier; + copyNullable(mg.multiplier, supermg.multiplier); for (int j = 0; j != supermg.parent_atoms.size(); j++) if (mapping[supermg.parent_atoms[j]] >= 0) mg.parent_atoms.push(mapping[supermg.parent_atoms[j]]); diff --git a/core/indigo-core/molecule/src/base_molecule_templates.cpp b/core/indigo-core/molecule/src/base_molecule_templates.cpp index b7d6abb885..a056557944 100644 --- a/core/indigo-core/molecule/src/base_molecule_templates.cpp +++ b/core/indigo-core/molecule/src/base_molecule_templates.cpp @@ -1286,8 +1286,14 @@ bool BaseMolecule::_mergeSGroupWithSubmolecule(SGroup& sgroup, SGroup& super, Ba { int i; bool merged = false; - sgroup.parent_group = super.parent_group; - sgroup.sgroup_subtype = super.sgroup_subtype; + if (super.parent_group.hasValue()) + sgroup.parent_group = super.parent_group.get(); + else + sgroup.parent_group.reset(); + if (super.sgroup_subtype.hasValue()) + sgroup.sgroup_subtype = super.sgroup_subtype.get(); + else + sgroup.sgroup_subtype.reset(); sgroup.brackets.copy(super.brackets); QS_DEF(Array, parent_atoms); diff --git a/core/indigo-core/molecule/src/cmf_saver.cpp b/core/indigo-core/molecule/src/cmf_saver.cpp index e4c47478f6..a20ba5b2bd 100644 --- a/core/indigo-core/molecule/src/cmf_saver.cpp +++ b/core/indigo-core/molecule/src/cmf_saver.cpp @@ -827,7 +827,8 @@ void CmfSaver::_updateSGroupsXyzMinMax(Molecule& mol, Vec3f& min, Vec3f& max) if (s.display_pos.hasValue()) { - Vec3f display_pos(s.display_pos->x, s.display_pos->y, 0); + const Vec2f& display_pos_2d = s.display_pos.get(); + Vec3f display_pos(display_pos_2d.x, display_pos_2d.y, 0); min.min(display_pos); max.max(display_pos); diff --git a/core/indigo-core/molecule/src/cml_saver.cpp b/core/indigo-core/molecule/src/cml_saver.cpp index cc10901170..f5f752b126 100644 --- a/core/indigo-core/molecule/src/cml_saver.cpp +++ b/core/indigo-core/molecule/src/cml_saver.cpp @@ -655,8 +655,9 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup if (dsg.display_pos.hasValue()) { - sg->SetAttribute("x", dsg.display_pos->x); - sg->SetAttribute("y", dsg.display_pos->y); + const Vec2f& display_pos = dsg.display_pos.get(); + sg->SetAttribute("x", display_pos.x); + sg->SetAttribute("y", display_pos.y); } if (!dsg.detached) diff --git a/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp b/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp index 810faaa133..bcb50f0d62 100644 --- a/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_cdxml_saver.cpp @@ -1136,14 +1136,15 @@ void MoleculeCdxmlSaver::addFragmentNodes(BaseMolecule& mol, tinyxml2::XMLElemen node->LinkEndChild(t); if (sa.display_position.hasValue()) { - Vec2f pos(sa.display_position->x + offset.x, -sa.display_position->y - offset.y); + const Vec3f& display_position = sa.display_position.get(); + Vec2f pos(display_position.x + offset.x, -display_position.y - offset.y); pos.scale(_bond_length); Vec2f v1(pos.x - _bond_length / 2, pos.y - _bond_length / 2); Vec2f v2(pos.x + _bond_length / 2, pos.y + _bond_length / 2); std::string pos_str = std::to_string(pos.x) + " " + std::to_string(pos.y); Rect2f bbox(v1, v2); std::string bbox_str = boundingBoxToString(bbox); - if (sa.display_position->x != 0.0f && sa.display_position->y != 0.0f) + if (display_position.x != 0.0f && display_position.y != 0.0f) node->SetAttribute("p", pos_str.c_str()); } t->SetAttribute("LabelJustification", "Left"); diff --git a/core/indigo-core/molecule/src/molecule_json_saver.cpp b/core/indigo-core/molecule/src/molecule_json_saver.cpp index 57d1953fc7..3e7fa032a4 100644 --- a/core/indigo-core/molecule/src/molecule_json_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_json_saver.cpp @@ -224,10 +224,11 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) if (dsg.display_pos.hasValue()) { + const Vec2f& display_pos = dsg.display_pos.get(); writer.Key("x"); - writeFloat(writer, dsg.display_pos->x); + writeFloat(writer, display_pos.x); writer.Key("y"); - writeFloat(writer, dsg.display_pos->y); + writeFloat(writer, display_pos.y); } if (!dsg.detached) @@ -267,7 +268,7 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) Superatom& sa = (Superatom&)sgroup; writer.Key("name"); writer.String(sgroup.label.size() ? sgroup.label.ptr() : ""); - if (sa.contracted == DisplayOption::Expanded) + if (sa.contracted.hasValue() && sa.contracted.get() == DisplayOption::Expanded) { writer.Key("expanded"); writer.Bool(true); diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index 9e078f2b0a..0cbb79490c 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -301,7 +301,7 @@ bool MoleculeSGroups::getParentAtoms(SGroup& sgroup, Array& target) { if (sgroup.parent_idx < 0) return false; - auto pidx = sgroup.parent_idx; + int pidx = sgroup.parent_idx.get(); if (!hasSGroup(sgroup.parent_idx)) { pidx = findSGroupById(sgroup.parent_group); @@ -456,7 +456,7 @@ void MoleculeSGroups::findSGroups(int property, int value, Array& sgs) if (sg.sgroup_type == SGroup::SG_TYPE_SUP) { Superatom& sup = (Superatom&)sg; - if (sup.contracted == (DisplayOption)value) + if (sup.contracted.hasValue() && sup.contracted.get() == (DisplayOption)value) { sgs.push(i); } diff --git a/core/indigo-core/molecule/src/molfile_saver.cpp b/core/indigo-core/molecule/src/molfile_saver.cpp index 7c9a00034a..0fc5a0c4be 100644 --- a/core/indigo-core/molecule/src/molfile_saver.cpp +++ b/core/indigo-core/molecule/src/molfile_saver.cpp @@ -925,7 +925,7 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) // convert CHEM to LINKER for BIOVIA if (sup.sa_class.size() > 1) out.printf(" CLASS=%s", sup.sa_class.ptr() == std::string(kMonomerClassCHEM) ? kMonomerClassLINKER : sup.sa_class.ptr()); - if (sup.contracted == DisplayOption::Expanded) + if (sup.contracted.hasValue() && sup.contracted.get() == DisplayOption::Expanded) out.printf(" ESTATE=E"); if (sup.attachment_points.size() > 0) { @@ -1823,7 +1823,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) superatom.bond_connections[j].bond_dir.x, superatom.bond_connections[j].bond_dir.y); } } - if (superatom.contracted == DisplayOption::Expanded) + if (superatom.contracted.hasValue() && superatom.contracted.get() == DisplayOption::Expanded) { output.printfCR("M SDS EXP 1 %3d", wi); } @@ -2131,8 +2131,14 @@ bool MolfileSaver::_checkAttPointOrder(BaseMolecule& mol, int rsite) void MolfileSaver::_writeDataSGroupDisplay(DataSGroup& datasgroup, Output& out) { - float dp_x = datasgroup.display_pos.hasValue() ? datasgroup.display_pos->x : 0.0f; - float dp_y = datasgroup.display_pos.hasValue() ? datasgroup.display_pos->y : 0.0f; + float dp_x = 0.0f; + float dp_y = 0.0f; + if (datasgroup.display_pos.hasValue()) + { + const Vec2f& display_pos = datasgroup.display_pos.get(); + dp_x = display_pos.x; + dp_y = display_pos.y; + } out.printf("%10.4f%10.4f %c%c%c", dp_x, dp_y, datasgroup.detached ? 'D' : 'A', datasgroup.relative ? 'R' : 'A', datasgroup.display_units ? 'U' : ' '); if (datasgroup.num_chars == 0) out.printf(" ALL 1 %c %1d ", (datasgroup.tag.hasValue() ? datasgroup.tag.get() : 0), diff --git a/core/indigo-core/tests/tests/formats.cpp b/core/indigo-core/tests/tests/formats.cpp index eef8f15653..fb403aa135 100644 --- a/core/indigo-core/tests/tests/formats.cpp +++ b/core/indigo-core/tests/tests/formats.cpp @@ -147,8 +147,7 @@ TEST_F(IndigoCoreFormatsTest, smiles_data_sgroups) ASSERT_STREQ(dsg.queryoper.ptr(), "like"); ASSERT_STREQ(dsg.description.ptr(), "unit"); ASSERT_EQ(dsg.tag, 't'); - ASSERT_EQ(dsg.display_pos->x, 0.0f); - ASSERT_EQ(dsg.display_pos->y, 0.0f); + ASSERT_FALSE(dsg.display_pos.hasValue()); ASSERT_EQ(dsg.atoms.size(), 4); ASSERT_EQ(dsg.atoms.at(0), 3); ASSERT_EQ(dsg.atoms.at(1), 2); @@ -177,8 +176,9 @@ TEST_F(IndigoCoreFormatsTest, smiles_data_sgroups_coords) ASSERT_STREQ(dsg.queryoper.ptr(), ""); ASSERT_STREQ(dsg.description.ptr(), ""); ASSERT_EQ(dsg.tag, 's'); - ASSERT_EQ(dsg.display_pos->x, -1.5f); - ASSERT_EQ(dsg.display_pos->y, 7.8f); + ASSERT_TRUE(dsg.display_pos.hasValue()); + ASSERT_EQ(dsg.display_pos.get().x, -1.5f); + ASSERT_EQ(dsg.display_pos.get().y, 7.8f); ASSERT_EQ(dsg.atoms.size(), 3); ASSERT_EQ(dsg.atoms.at(0), 1); ASSERT_EQ(dsg.atoms.at(1), 2); @@ -206,8 +206,7 @@ TEST_F(IndigoCoreFormatsTest, smiles_data_sgroups_short) ASSERT_EQ(dsg.queryoper.size(), 0); ASSERT_EQ(dsg.description.size(), 0); ASSERT_EQ(dsg.tag, ' '); - ASSERT_EQ(dsg.display_pos->x, 0.0f); - ASSERT_EQ(dsg.display_pos->y, 0.0f); + ASSERT_FALSE(dsg.display_pos.hasValue()); ASSERT_EQ(dsg.atoms.size(), 3); ASSERT_EQ(dsg.atoms.at(0), 1); ASSERT_EQ(dsg.atoms.at(1), 2); diff --git a/core/render2d/src/render_internal.cpp b/core/render2d/src/render_internal.cpp index 6d692de9de..c07424ffc0 100644 --- a/core/render2d/src/render_internal.cpp +++ b/core/render2d/src/render_internal.cpp @@ -149,7 +149,8 @@ void MoleculeRenderInternal::setMolecule(BaseMolecule* mol) for (int i = bmol.sgroups.begin(); i != bmol.sgroups.end(); i = bmol.sgroups.next(i)) { SGroup& sgroup = bmol.sgroups.getSGroup(i); - if (sgroup.contracted == DisplayOption::Contracted || sgroup.contracted == DisplayOption::Undefined) + const auto contracted = sgroup.contracted.hasValue() ? sgroup.contracted.get() : DisplayOption::Undefined; + if (contracted == DisplayOption::Contracted || contracted == DisplayOption::Undefined) { isThereAtLeastOneContracted = true; break; @@ -826,7 +827,8 @@ void MoleculeRenderInternal::_prepareSGroups(bool collapseAtLeastOneSuperatom) for (int i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) { SGroup& sgroup = mol.sgroups.getSGroup(i); - if (sgroup.contracted == DisplayOption::Contracted || sgroup.contracted == DisplayOption::Undefined) + const auto contracted = sgroup.contracted.hasValue() ? sgroup.contracted.get() : DisplayOption::Undefined; + if (contracted == DisplayOption::Contracted || contracted == DisplayOption::Undefined) { if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) { From 260547e959c8ba1e69759e7399d382ca7745761e Mon Sep 17 00:00:00 2001 From: Roman Porozhnetov Date: Thu, 28 May 2026 14:14:16 +0400 Subject: [PATCH 26/33] Refactor SGroup ordering info --- core/indigo-core/molecule/cml_saver.h | 2 +- core/indigo-core/molecule/molecule_sgroups.h | 15 +- core/indigo-core/molecule/molfile_saver.h | 2 +- core/indigo-core/molecule/src/cml_saver.cpp | 69 +++----- .../molecule/src/molecule_json_saver.cpp | 8 +- .../molecule/src/molecule_sgroups.cpp | 167 ++++++++---------- .../molecule/src/molfile_saver.cpp | 97 ++++------ core/indigo-core/tests/tests/formats.cpp | 51 ++++++ 8 files changed, 196 insertions(+), 215 deletions(-) diff --git a/core/indigo-core/molecule/cml_saver.h b/core/indigo-core/molecule/cml_saver.h index 45b55d79d3..f450da164f 100644 --- a/core/indigo-core/molecule/cml_saver.h +++ b/core/indigo-core/molecule/cml_saver.h @@ -52,7 +52,7 @@ namespace indigo void _validate(BaseMolecule& bmol); void _addMoleculeElement(tinyxml2::XMLElement* elem, BaseMolecule& mol, bool query); - void _addSgroupElement(tinyxml2::XMLElement* elem, BaseMolecule& mol, SGroup& sgroup, int write_index, const std::vector& entries); + void _addSgroupElement(tinyxml2::XMLElement* elem, const SGroupInfo& info, const std::vector& sgroup_infos); void _addRgroups(tinyxml2::XMLElement* elem, BaseMolecule& mol, bool query); void _addRgroupElement(tinyxml2::XMLElement* elem, RGroup& rgroup, bool query); diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index 1d732523e8..f0b533ff9c 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -315,18 +315,15 @@ namespace indigo bool _cmpIndices(Array& t_inds, Array& q_inds); }; - // Read-only write-order entry for serialization. Replaces the old mutating _checkSGroupIndices pattern. - struct SGroupWriteEntry + struct SGroupInfo { - int pool_idx; // original pool index in mol.sgroups - int write_index; // sequential 1,2,3... for CTFile output - int write_ext_index; // ext_index or auto-assigned from write_index (0→write_index per spec) - int write_parent; // remapped parent_group (0 if root) + SGroup& sgroup; + int index; + int external_index; + int parent_index; }; - // Returns topologically-sorted SGroup list with sequential indices for serialization. - // Does NOT mutate the molecule — returns a mapping table. - DLLEXPORT std::vector getOrderedSGroups(MoleculeSGroups& sgroups); + DLLEXPORT std::vector getOrderedSGroups(MoleculeSGroups& sgroups); } // namespace indigo diff --git a/core/indigo-core/molecule/molfile_saver.h b/core/indigo-core/molecule/molfile_saver.h index 990c36310c..f66d776f6d 100644 --- a/core/indigo-core/molecule/molfile_saver.h +++ b/core/indigo-core/molecule/molfile_saver.h @@ -101,7 +101,7 @@ namespace indigo void _writeCtab2000(Output& output, BaseMolecule& mol, bool query); void _writeRGroupIndices2000(Output& output, BaseMolecule& mol); void _writeAttachmentValues2000(Output& output, BaseMolecule& fragment); - void _writeGenericSGroup3000(SGroup& sgroup, const SGroupWriteEntry& entry, int idx, Output& output); + void _writeGenericSGroup3000(SGroup& sgroup, const SGroupInfo& info, Output& output); void _writeDataSGroupDisplay(DataSGroup& datasgroup, Output& out); void _writeFormattedString(Output& output, Array& str, int length); static bool _checkAttPointOrder(BaseMolecule& mol, int rsite); diff --git a/core/indigo-core/molecule/src/cml_saver.cpp b/core/indigo-core/molecule/src/cml_saver.cpp index f5f752b126..306c52c8a5 100644 --- a/core/indigo-core/molecule/src/cml_saver.cpp +++ b/core/indigo-core/molecule/src/cml_saver.cpp @@ -565,29 +565,35 @@ void CmlSaver::_addMoleculeElement(XMLElement* elem, BaseMolecule& mol, bool que if (_mol->countSGroups() > 0) { - auto entries = getOrderedSGroups(_mol->sgroups); - for (auto& entry : entries) + auto sgroup_infos = getOrderedSGroups(_mol->sgroups); + for (const auto& info : sgroup_infos) { - if (entry.write_parent == 0) - { - SGroup& sgroup = _mol->sgroups.getSGroup(entry.pool_idx); - _addSgroupElement(molecule, *_mol, sgroup, entry.write_index, entries); - } + if (info.parent_index == 0) + _addSgroupElement(molecule, info, sgroup_infos); } } } -void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup& sgroup, int write_index, const std::vector& entries) +void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, const std::vector& sgroup_infos) { + SGroup& sgroup = info.sgroup; XMLElement* sg = _doc->NewElement("molecule"); molecule->LinkEndChild(sg); QS_DEF(Array, buf); ArrayOutput out(buf); - out.printf("sg%d", write_index); + out.printf("sg%d", info.index); buf.push(0); sg->SetAttribute("id", buf.ptr()); + auto addChildren = [&]() { + for (const auto& child_info : sgroup_infos) + { + if (child_info.parent_index == info.index) + _addSgroupElement(sg, child_info, sgroup_infos); + } + }; + if (sgroup.atoms.size() > 0) { QS_DEF(Array, sbuf); @@ -691,27 +697,13 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup sg->SetAttribute("fieldData", dsg.data.ptr()); } - for (auto& child_entry : entries) - { - if (child_entry.write_parent == write_index) - { - SGroup& sg_child = mol.sgroups.getSGroup(child_entry.pool_idx); - _addSgroupElement(sg, mol, sg_child, child_entry.write_index, entries); - } - } + addChildren(); } else if (sgroup.sgroup_type == SGroup::SG_TYPE_GEN) { sg->SetAttribute("role", "GenericSgroup"); - for (auto& child_entry : entries) - { - if (child_entry.write_parent == write_index) - { - SGroup& sg_child = mol.sgroups.getSGroup(child_entry.pool_idx); - _addSgroupElement(sg, mol, sg_child, child_entry.write_index, entries); - } - } + addChildren(); } else if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) { @@ -725,14 +717,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup sg->SetAttribute("title", name); } - for (auto& child_entry : entries) - { - if (child_entry.write_parent == write_index) - { - SGroup& sg_child = mol.sgroups.getSGroup(child_entry.pool_idx); - _addSgroupElement(sg, mol, sg_child, child_entry.write_index, entries); - } - } + addChildren(); } else if (sgroup.sgroup_type == SGroup::SG_TYPE_SRU) { @@ -755,14 +740,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup sg->SetAttribute("connect", "hh"); } - for (auto& child_entry : entries) - { - if (child_entry.write_parent == write_index) - { - SGroup& sg_child = mol.sgroups.getSGroup(child_entry.pool_idx); - _addSgroupElement(sg, mol, sg_child, child_entry.write_index, entries); - } - } + addChildren(); } else if (sgroup.sgroup_type == SGroup::SG_TYPE_MUL) { @@ -789,14 +767,7 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, BaseMolecule& mol, SGroup sg->SetAttribute("patoms", pbuf.ptr()); } - for (auto& child_entry : entries) - { - if (child_entry.write_parent == write_index) - { - SGroup& sg_child = mol.sgroups.getSGroup(child_entry.pool_idx); - _addSgroupElement(sg, mol, sg_child, child_entry.write_index, entries); - } - } + addChildren(); } } diff --git a/core/indigo-core/molecule/src/molecule_json_saver.cpp b/core/indigo-core/molecule/src/molecule_json_saver.cpp index 3e7fa032a4..15e8d2a5a8 100644 --- a/core/indigo-core/molecule/src/molecule_json_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_json_saver.cpp @@ -122,8 +122,8 @@ void MoleculeJsonSaver::saveFormatMode(KETVersion& version, Array& output) void MoleculeJsonSaver::saveSGroups(BaseMolecule& mol, JsonWriter& writer) { - auto write_order = getOrderedSGroups(mol.sgroups); - int sGroupsCount = static_cast(write_order.size()); + auto sgroup_infos = getOrderedSGroups(mol.sgroups); + int sGroupsCount = static_cast(sgroup_infos.size()); bool componentDefined = false; if (mol.isQueryMolecule()) { @@ -143,9 +143,9 @@ void MoleculeJsonSaver::saveSGroups(BaseMolecule& mol, JsonWriter& writer) { writer.Key("sgroups"); writer.StartArray(); - for (const auto& entry : write_order) + for (const auto& info : sgroup_infos) { - auto& sgrp = mol.sgroups.getSGroup(entry.pool_idx); + SGroup& sgrp = info.sgroup; saveSGroup(sgrp, writer); } // save queryComponent diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index 0cbb79490c..4beece0fc2 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -23,8 +23,8 @@ #include "molecule/molecule_sgroups.h" #include +#include #include -#include using namespace indigo; @@ -731,120 +731,101 @@ bool MoleculeSGroups::_cmpIndices(Array& t_inds, Array& q_inds) return true; } -std::vector indigo::getOrderedSGroups(MoleculeSGroups& sgroups) +std::vector indigo::getOrderedSGroups(MoleculeSGroups& sgroups) { - // Phase 1: Build pool_idx → write_index mapping (sequential, roots first) - std::vector pool_indices; - for (int i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) - pool_indices.push_back(i); - - int pool_end = pool_indices.empty() ? 0 : (*std::max_element(pool_indices.begin(), pool_indices.end()) + 1); - - std::vector sgs_mapping(pool_end, 0); + std::vector infos; + std::map pool_index_to_info_index; + std::map original_index_to_info_index; - // Roots first (parent_group == 0 or not set) - int iw = 1; - for (int i : pool_indices) + for (int i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) { SGroup& sg = sgroups.getSGroup(i); - int pg = sg.parent_group.hasValue() ? sg.parent_group.get() : 0; - if (pg == 0) - { - sgs_mapping[i] = iw++; - } - } - // Then children - for (int i : pool_indices) - { - if (sgs_mapping[i] == 0) + SGroupInfo info = {sg, 0, 0, 0}; + int info_index = static_cast(infos.size()); + + infos.push_back(info); + pool_index_to_info_index[i] = info_index; + + if (sg.index > 0) { - sgs_mapping[i] = iw++; + auto inserted = original_index_to_info_index.emplace(sg.index, info_index); + if (!inserted.second) + inserted.first->second = -1; } } - // Phase 2: Build old_index → write_index remap for parent_group resolution - std::map index_remap; - for (int i : pool_indices) - { - SGroup& sg = sgroups.getSGroup(i); - int key = (sg.index != 0) ? sg.index : sgs_mapping[i]; - index_remap[key] = sgs_mapping[i]; - } - // Phase 3: Build entries with write fields - std::vector all_entries; - all_entries.reserve(pool_indices.size()); - for (int i : pool_indices) + std::vector parent_info_index(infos.size(), -1); + for (int info_index = 0; info_index < static_cast(infos.size()); info_index++) { - SGroup& sg = sgroups.getSGroup(i); - SGroupWriteEntry entry; - entry.pool_idx = i; - entry.write_index = sgs_mapping[i]; + SGroup& sg = infos[info_index].sgroup; - // ext_index: use explicit value if set, otherwise auto-assign from write_index - entry.write_ext_index = (sg.ext_index != 0) ? sg.ext_index : entry.write_index; - - // parent_group: remap to new write indices - int pg = sg.parent_group.hasValue() ? sg.parent_group.get() : 0; - if (pg == 0) - { - entry.write_parent = 0; - } - else + if (sg.parent_idx.hasValue()) { - auto it = index_remap.find(pg); - if (it != index_remap.end() && it->second != entry.write_index) - entry.write_parent = it->second; - else - entry.write_parent = 0; // orphan or self-ref → root + auto it = pool_index_to_info_index.find(sg.parent_idx.get()); + if (it != pool_index_to_info_index.end() && it->second != info_index) + { + parent_info_index[info_index] = it->second; + continue; + } } - all_entries.push_back(entry); - } - // Phase 4: Topological sort — parents before children - std::vector result; - result.reserve(all_entries.size()); + int parent_id = sg.parent_group.hasValue() ? sg.parent_group.get() : 0; + if (parent_id <= 0) + continue; - std::set added_indices; - - // Add roots first - for (auto& e : all_entries) - { - if (e.write_parent == 0) + auto original_it = original_index_to_info_index.find(parent_id); + if (original_it != original_index_to_info_index.end() && original_it->second >= 0 && original_it->second != info_index) { - result.push_back(e); - added_indices.insert(e.write_index); + parent_info_index[info_index] = original_it->second; + continue; } + + // CML loader stores nested parent as pool index + 1 and leaves SGroup::index unset. + auto pool_it = pool_index_to_info_index.find(parent_id - 1); + if (pool_it != pool_index_to_info_index.end() && pool_it->second != info_index && infos[pool_it->second].sgroup.index == 0) + parent_info_index[info_index] = pool_it->second; } - // Then iteratively add children whose parents are already added - while (result.size() < all_entries.size()) - { - size_t prev_size = result.size(); - for (auto& e : all_entries) + std::vector state(infos.size(), 0); + std::vector ordered_info_indexes; + ordered_info_indexes.reserve(infos.size()); + + std::function addWithParents = [&](int info_index) { + if (state[info_index] == 2) + return; + if (state[info_index] == 1) { - if (added_indices.count(e.write_index)) - continue; - if (added_indices.count(e.write_parent)) - { - result.push_back(e); - added_indices.insert(e.write_index); - } + parent_info_index[info_index] = -1; + return; } - // No progress — cyclic or orphan refs; break cycles by adding as roots - if (result.size() == prev_size) + + state[info_index] = 1; + int parent_index = parent_info_index[info_index]; + if (parent_index >= 0) { - for (auto& e : all_entries) - { - if (!added_indices.count(e.write_index)) - { - e.write_parent = 0; - result.push_back(e); - added_indices.insert(e.write_index); - } - } - break; + if (state[parent_index] == 1) + parent_info_index[info_index] = -1; + else + addWithParents(parent_index); } - } + state[info_index] = 2; + ordered_info_indexes.push_back(info_index); + }; + + for (int info_index = 0; info_index < static_cast(infos.size()); info_index++) + addWithParents(info_index); + + for (int i = 0; i < static_cast(ordered_info_indexes.size()); i++) + infos[ordered_info_indexes[i]].index = i + 1; + std::vector result; + result.reserve(ordered_info_indexes.size()); + for (int info_index : ordered_info_indexes) + { + SGroupInfo& info = infos[info_index]; + info.external_index = info.sgroup.ext_index != 0 ? info.sgroup.ext_index : info.index; + info.parent_index = parent_info_index[info_index] >= 0 ? infos[parent_info_index[info_index]].index : 0; + result.push_back(info); + } return result; } diff --git a/core/indigo-core/molecule/src/molfile_saver.cpp b/core/indigo-core/molecule/src/molfile_saver.cpp index 0fc5a0c4be..afb13a98bc 100644 --- a/core/indigo-core/molecule/src/molfile_saver.cpp +++ b/core/indigo-core/molecule/src/molfile_saver.cpp @@ -887,19 +887,16 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) output.writeStringCR("M V30 END COLLECTION"); } - auto write_order = getOrderedSGroups(mol.sgroups); + auto sgroup_infos = getOrderedSGroups(mol.sgroups); - if (write_order.size() > 0) + if (sgroup_infos.size() > 0) { - MoleculeSGroups* sgroups = &mol.sgroups; - int idx = 1; - output.writeStringCR("M V30 BEGIN SGROUP"); - for (const auto& entry : write_order) + for (const auto& info : sgroup_infos) { ArrayOutput out(buf); - SGroup& sgroup = sgroups->getSGroup(entry.pool_idx); - _writeGenericSGroup3000(sgroup, entry, idx++, out); + SGroup& sgroup = info.sgroup; + _writeGenericSGroup3000(sgroup, info, out); if (sgroup.sgroup_type == SGroup::SG_TYPE_GEN) { _writeMultiString(output, buf.ptr(), buf.size()); @@ -1115,11 +1112,11 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) } } -void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, const SGroupWriteEntry& entry, int idx, Output& output) +void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, const SGroupInfo& info, Output& output) { int i; - output.printf("%d %s %d", entry.write_index, SGroup::typeToString(sgroup.sgroup_type), entry.write_ext_index); + output.printf("%d %s %d", info.index, SGroup::typeToString(sgroup.sgroup_type), info.external_index); if (sgroup.atoms.size() > 0) { @@ -1155,9 +1152,9 @@ void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, const SGroupWriteEntr else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_BLO) output.printf(" SUBTYPE=BLO"); } - if (entry.write_parent > 0) + if (info.parent_index > 0) { - output.printf(" PARENT=%d", entry.write_parent); + output.printf(" PARENT=%d", info.parent_index); } for (i = 0; i < sgroup.brackets.size(); i++) { @@ -1687,47 +1684,34 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) } } - QS_DEF(Array, sgroup_ids); QS_DEF(Array, child_ids); QS_DEF(Array, parent_ids); - sgroup_ids.clear(); child_ids.clear(); parent_ids.clear(); - for (i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) - { - /*SGroup& sgroup =*/std::ignore = mol.sgroups.getSGroup(i); - sgroup_ids.push(i); - } - - auto write_order = getOrderedSGroups(mol.sgroups); - - // Build pool_idx → entry lookup for random access - std::map entry_map; - for (const auto& e : write_order) - entry_map[e.pool_idx] = &e; + auto sgroup_infos = getOrderedSGroups(mol.sgroups); + int sgroup_count = static_cast(sgroup_infos.size()); - if (sgroup_ids.size() > 0) + if (sgroup_count > 0) { int j; - for (j = 0; j < sgroup_ids.size(); j += 8) + for (j = 0; j < sgroup_count; j += 8) { - output.printf("M STY%3d", std::min(sgroup_ids.size(), j + 8) - j); - for (i = j; i < std::min(sgroup_ids.size(), j + 8); i++) + output.printf("M STY%3d", std::min(sgroup_count, j + 8) - j); + for (i = j; i < std::min(sgroup_count, j + 8); i++) { - SGroup* sgroup = &mol.sgroups.getSGroup(sgroup_ids[i]); - output.printf(" %3d %s", entry_map[sgroup_ids[i]]->write_index, SGroup::typeToString(sgroup->sgroup_type)); + SGroup* sgroup = &sgroup_infos[i].sgroup; + output.printf(" %3d %s", sgroup_infos[i].index, SGroup::typeToString(sgroup->sgroup_type)); } output.writeCR(); } - for (j = 0; j < sgroup_ids.size(); j++) + for (const auto& info : sgroup_infos) { - auto* entry = entry_map[sgroup_ids[j]]; - if (entry->write_parent > 0) + if (info.parent_index > 0) { - child_ids.push(entry->write_index); - parent_ids.push(entry->write_parent); + child_ids.push(info.index); + parent_ids.push(info.parent_index); } } for (j = 0; j < child_ids.size(); j += 8) @@ -1739,35 +1723,32 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) } output.writeCR(); } - for (j = 0; j < sgroup_ids.size(); j += 8) + for (j = 0; j < sgroup_count; j += 8) { - output.printf("M SLB%3d", std::min(sgroup_ids.size(), j + 8) - j); - for (i = j; i < std::min(sgroup_ids.size(), j + 8); i++) + output.printf("M SLB%3d", std::min(sgroup_count, j + 8) - j); + for (i = j; i < std::min(sgroup_count, j + 8); i++) { - auto* entry = entry_map[sgroup_ids[i]]; - output.printf(" %3d %3d", entry->write_index, entry->write_ext_index); + const auto& info = sgroup_infos[i]; + output.printf(" %3d %3d", info.index, info.external_index); } output.writeCR(); } - int sru_count = mol.sgroups.getSGroupCount(SGroup::SG_TYPE_SRU); + std::vector sru_infos; + for (const auto& info : sgroup_infos) + { + if (info.sgroup.sgroup_type == SGroup::SG_TYPE_SRU) + sru_infos.push_back(&info); + } + int sru_count = static_cast(sru_infos.size()); for (j = 0; j < sru_count; j += 8) { output.printf("M SCN%3d", std::min(sru_count, j + 8) - j); for (i = j; i < std::min(sru_count, j + 8); i++) { - RepeatingUnit* ru = (RepeatingUnit*)&mol.sgroups.getSGroup(i, SGroup::SG_TYPE_SRU); - // Find write_index for this SRU by pointer match - int ru_wi = 0; - for (const auto& e : write_order) - { - if (&mol.sgroups.getSGroup(e.pool_idx) == ru) - { - ru_wi = e.write_index; - break; - } - } - output.printf(" %3d ", ru_wi); + const SGroupInfo& info = *sru_infos[i]; + RepeatingUnit* ru = (RepeatingUnit*)&info.sgroup; + output.printf(" %3d ", info.index); if (ru->connectivity == SGroup::HEAD_TO_HEAD) output.printf("HH "); @@ -1779,10 +1760,10 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) output.writeCR(); } - for (i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) + for (const auto& info : sgroup_infos) { - SGroup& sgroup = mol.sgroups.getSGroup(i); - int wi = entry_map[i]->write_index; + SGroup& sgroup = info.sgroup; + int wi = info.index; for (j = 0; j < sgroup.atoms.size(); j += 8) { int k; diff --git a/core/indigo-core/tests/tests/formats.cpp b/core/indigo-core/tests/tests/formats.cpp index fb403aa135..861b8d66f4 100644 --- a/core/indigo-core/tests/tests/formats.cpp +++ b/core/indigo-core/tests/tests/formats.cpp @@ -378,6 +378,57 @@ M END ASSERT_EQ(sg.atoms.at(2), 4); } +TEST_F(IndigoCoreFormatsTest, mol_saver_nested_sgroups_parent_order) +{ + Molecule mol; + + const char* molfile = R"(Nested SGroups + Indigo 052826 + + 0 0 0 0 0 999 V3000 +M V30 BEGIN CTAB +M V30 COUNTS 3 2 3 0 0 +M V30 BEGIN ATOM +M V30 1 C 0 0 0 0 +M V30 2 C 1 0 0 0 +M V30 3 C 2 0 0 0 +M V30 END ATOM +M V30 BEGIN BOND +M V30 1 1 1 2 +M V30 2 1 2 3 +M V30 END BOND +M V30 BEGIN SGROUP +M V30 3 GEN 0 ATOMS=(1 3) PARENT=2 +M V30 1 GEN 0 ATOMS=(1 1) +M V30 2 GEN 0 ATOMS=(1 2) PARENT=1 +M V30 END SGROUP +M V30 END CTAB +M END +)"; + + BufferScanner scanner(molfile); + MolfileLoader loader(scanner); + loader.loadMolecule(mol); + + Array out; + ArrayOutput std_out(out); + MolfileSaver saver(std_out); + saver.mode = MolfileSaver::MODE_3000; + saver.skip_date = true; + saver.saveMolecule(mol); + + std::string saved{out.ptr(), static_cast(out.size())}; + const auto root_pos = saved.find("M V30 1 GEN 1 ATOMS=(1 1)"); + const auto child_pos = saved.find("M V30 2 GEN 2 ATOMS=(1 2) PARENT=1"); + const auto grandchild_pos = saved.find("M V30 3 GEN 3 ATOMS=(1 3) PARENT=2"); + + ASSERT_NE(std::string::npos, root_pos) << saved; + ASSERT_NE(std::string::npos, child_pos) << saved; + ASSERT_NE(std::string::npos, grandchild_pos) << saved; + EXPECT_LT(root_pos, child_pos); + EXPECT_LT(child_pos, grandchild_pos); +} + TEST_F(IndigoCoreFormatsTest, smarts_load_save) { QueryMolecule q_mol; From c6e2ce63b4e3c8fa6b0ec93d620d9f30d16dda5f Mon Sep 17 00:00:00 2001 From: Roman Porozhnetov Date: Fri, 29 May 2026 14:09:51 +0400 Subject: [PATCH 27/33] Fix SGroup serialization ordering --- .../molecule/src/molecule_sgroups.cpp | 47 ++++++++++--------- .../molecule/src/molfile_saver.cpp | 40 ++++++++++------ 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index 4beece0fc2..95ca455d7e 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -23,7 +23,6 @@ #include "molecule/molecule_sgroups.h" #include -#include #include using namespace indigo; @@ -786,34 +785,40 @@ std::vector indigo::getOrderedSGroups(MoleculeSGroups& sgroups) parent_info_index[info_index] = pool_it->second; } - std::vector state(infos.size(), 0); std::vector ordered_info_indexes; ordered_info_indexes.reserve(infos.size()); + std::vector added(infos.size(), false); - std::function addWithParents = [&](int info_index) { - if (state[info_index] == 2) - return; - if (state[info_index] == 1) + while (ordered_info_indexes.size() < infos.size()) + { + std::size_t added_count = ordered_info_indexes.size(); + std::vector added_before_pass = added; + for (int info_index = 0; info_index < static_cast(infos.size()); info_index++) { - parent_info_index[info_index] = -1; - return; + if (added[info_index]) + continue; + + int parent_index = parent_info_index[info_index]; + if (parent_index < 0 || added_before_pass[parent_index]) + { + ordered_info_indexes.push_back(info_index); + added[info_index] = true; + } } - state[info_index] = 1; - int parent_index = parent_info_index[info_index]; - if (parent_index >= 0) + if (ordered_info_indexes.size() == added_count) { - if (state[parent_index] == 1) - parent_info_index[info_index] = -1; - else - addWithParents(parent_index); + for (int info_index = 0; info_index < static_cast(infos.size()); info_index++) + { + if (!added[info_index]) + { + parent_info_index[info_index] = -1; + ordered_info_indexes.push_back(info_index); + added[info_index] = true; + } + } } - state[info_index] = 2; - ordered_info_indexes.push_back(info_index); - }; - - for (int info_index = 0; info_index < static_cast(infos.size()); info_index++) - addWithParents(info_index); + } for (int i = 0; i < static_cast(ordered_info_indexes.size()); i++) infos[ordered_info_indexes[i]].index = i + 1; diff --git a/core/indigo-core/molecule/src/molfile_saver.cpp b/core/indigo-core/molecule/src/molfile_saver.cpp index afb13a98bc..b1d52a6a35 100644 --- a/core/indigo-core/molecule/src/molfile_saver.cpp +++ b/core/indigo-core/molecule/src/molfile_saver.cpp @@ -1690,7 +1690,19 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) child_ids.clear(); parent_ids.clear(); - auto sgroup_infos = getOrderedSGroups(mol.sgroups); + auto ordered_sgroup_infos = getOrderedSGroups(mol.sgroups); + std::map sgroup_info_by_ptr; + for (const auto& info : ordered_sgroup_infos) + sgroup_info_by_ptr[&info.sgroup] = &info; + + std::vector sgroup_infos; + sgroup_infos.reserve(ordered_sgroup_infos.size()); + for (i = mol.sgroups.begin(); i != mol.sgroups.end(); i = mol.sgroups.next(i)) + { + SGroup& sgroup = mol.sgroups.getSGroup(i); + sgroup_infos.push_back(sgroup_info_by_ptr[&sgroup]); + } + int sgroup_count = static_cast(sgroup_infos.size()); if (sgroup_count > 0) @@ -1701,17 +1713,17 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) output.printf("M STY%3d", std::min(sgroup_count, j + 8) - j); for (i = j; i < std::min(sgroup_count, j + 8); i++) { - SGroup* sgroup = &sgroup_infos[i].sgroup; - output.printf(" %3d %s", sgroup_infos[i].index, SGroup::typeToString(sgroup->sgroup_type)); + const auto& info = *sgroup_infos[i]; + output.printf(" %3d %s", info.index, SGroup::typeToString(info.sgroup.sgroup_type)); } output.writeCR(); } - for (const auto& info : sgroup_infos) + for (const auto* info : sgroup_infos) { - if (info.parent_index > 0) + if (info->parent_index > 0) { - child_ids.push(info.index); - parent_ids.push(info.parent_index); + child_ids.push(info->index); + parent_ids.push(info->parent_index); } } for (j = 0; j < child_ids.size(); j += 8) @@ -1728,17 +1740,17 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) output.printf("M SLB%3d", std::min(sgroup_count, j + 8) - j); for (i = j; i < std::min(sgroup_count, j + 8); i++) { - const auto& info = sgroup_infos[i]; + const auto& info = *sgroup_infos[i]; output.printf(" %3d %3d", info.index, info.external_index); } output.writeCR(); } std::vector sru_infos; - for (const auto& info : sgroup_infos) + for (const auto* info : sgroup_infos) { - if (info.sgroup.sgroup_type == SGroup::SG_TYPE_SRU) - sru_infos.push_back(&info); + if (info->sgroup.sgroup_type == SGroup::SG_TYPE_SRU) + sru_infos.push_back(info); } int sru_count = static_cast(sru_infos.size()); for (j = 0; j < sru_count; j += 8) @@ -1760,10 +1772,10 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) output.writeCR(); } - for (const auto& info : sgroup_infos) + for (const auto* info : sgroup_infos) { - SGroup& sgroup = info.sgroup; - int wi = info.index; + SGroup& sgroup = info->sgroup; + int wi = info->index; for (j = 0; j < sgroup.atoms.size(); j += 8) { int k; From e85350588ffcb040bff184a425200ef9d9d939ee Mon Sep 17 00:00:00 2001 From: Roman Porozhnetov Date: Fri, 29 May 2026 17:28:00 +0400 Subject: [PATCH 28/33] Move SGroup ordering into MoleculeSGroups --- core/indigo-core/molecule/molecule_sgroups.h | 19 +++++++++---------- core/indigo-core/molecule/src/cml_saver.cpp | 2 +- .../molecule/src/molecule_json_saver.cpp | 2 +- .../molecule/src/molecule_sgroups.cpp | 6 +++--- .../molecule/src/molfile_saver.cpp | 4 ++-- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index f0b533ff9c..286d3e2d36 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -261,6 +261,14 @@ namespace indigo MultipleGroup(const MultipleGroup&); }; + struct SGroupInfo + { + SGroup& sgroup; + int index; + int external_index; + int parent_index; + }; + class Tree; class DLLEXPORT MoleculeSGroups { @@ -282,6 +290,7 @@ namespace indigo void buildTree(Tree& tree); bool getParentAtoms(int idx, Array& target); bool getParentAtoms(SGroup& sgroup, Array& target); + std::vector getOrderedSGroups(); void remove(int idx); void clear(); @@ -315,16 +324,6 @@ namespace indigo bool _cmpIndices(Array& t_inds, Array& q_inds); }; - struct SGroupInfo - { - SGroup& sgroup; - int index; - int external_index; - int parent_index; - }; - - DLLEXPORT std::vector getOrderedSGroups(MoleculeSGroups& sgroups); - } // namespace indigo #ifdef _WIN32 diff --git a/core/indigo-core/molecule/src/cml_saver.cpp b/core/indigo-core/molecule/src/cml_saver.cpp index 306c52c8a5..6200452f28 100644 --- a/core/indigo-core/molecule/src/cml_saver.cpp +++ b/core/indigo-core/molecule/src/cml_saver.cpp @@ -565,7 +565,7 @@ void CmlSaver::_addMoleculeElement(XMLElement* elem, BaseMolecule& mol, bool que if (_mol->countSGroups() > 0) { - auto sgroup_infos = getOrderedSGroups(_mol->sgroups); + auto sgroup_infos = _mol->sgroups.getOrderedSGroups(); for (const auto& info : sgroup_infos) { if (info.parent_index == 0) diff --git a/core/indigo-core/molecule/src/molecule_json_saver.cpp b/core/indigo-core/molecule/src/molecule_json_saver.cpp index 15e8d2a5a8..04cacdab15 100644 --- a/core/indigo-core/molecule/src/molecule_json_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_json_saver.cpp @@ -122,7 +122,7 @@ void MoleculeJsonSaver::saveFormatMode(KETVersion& version, Array& output) void MoleculeJsonSaver::saveSGroups(BaseMolecule& mol, JsonWriter& writer) { - auto sgroup_infos = getOrderedSGroups(mol.sgroups); + auto sgroup_infos = mol.sgroups.getOrderedSGroups(); int sGroupsCount = static_cast(sgroup_infos.size()); bool componentDefined = false; if (mol.isQueryMolecule()) diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index 95ca455d7e..1abd64b842 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -730,15 +730,15 @@ bool MoleculeSGroups::_cmpIndices(Array& t_inds, Array& q_inds) return true; } -std::vector indigo::getOrderedSGroups(MoleculeSGroups& sgroups) +std::vector MoleculeSGroups::getOrderedSGroups() { std::vector infos; std::map pool_index_to_info_index; std::map original_index_to_info_index; - for (int i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) + for (int i = begin(); i != end(); i = next(i)) { - SGroup& sg = sgroups.getSGroup(i); + SGroup& sg = getSGroup(i); SGroupInfo info = {sg, 0, 0, 0}; int info_index = static_cast(infos.size()); diff --git a/core/indigo-core/molecule/src/molfile_saver.cpp b/core/indigo-core/molecule/src/molfile_saver.cpp index b1d52a6a35..9df6c01c0e 100644 --- a/core/indigo-core/molecule/src/molfile_saver.cpp +++ b/core/indigo-core/molecule/src/molfile_saver.cpp @@ -887,7 +887,7 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) output.writeStringCR("M V30 END COLLECTION"); } - auto sgroup_infos = getOrderedSGroups(mol.sgroups); + auto sgroup_infos = mol.sgroups.getOrderedSGroups(); if (sgroup_infos.size() > 0) { @@ -1690,7 +1690,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) child_ids.clear(); parent_ids.clear(); - auto ordered_sgroup_infos = getOrderedSGroups(mol.sgroups); + auto ordered_sgroup_infos = mol.sgroups.getOrderedSGroups(); std::map sgroup_info_by_ptr; for (const auto& info : ordered_sgroup_infos) sgroup_info_by_ptr[&info.sgroup] = &info; From 0462251f5346ce4cf95bf8904c3c094b23fa6a44 Mon Sep 17 00:00:00 2001 From: Roman Porozhnetov Date: Fri, 29 May 2026 18:43:55 +0400 Subject: [PATCH 29/33] Handle unset DataSGroup display position in CMF --- core/indigo-core/molecule/src/cmf_saver.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/indigo-core/molecule/src/cmf_saver.cpp b/core/indigo-core/molecule/src/cmf_saver.cpp index a20ba5b2bd..a0b0cf9a9e 100644 --- a/core/indigo-core/molecule/src/cmf_saver.cpp +++ b/core/indigo-core/molecule/src/cmf_saver.cpp @@ -423,7 +423,8 @@ void CmfSaver::_writeSGroupsXyz(Molecule& mol, Output& output, const VecRange& r { DataSGroup& sd = (DataSGroup&)sg; _writeBaseSGroupXyz(output, sd, range); - _writeVec2f(output, sd.display_pos, range); + Vec2f display_pos = sd.display_pos.hasValue() ? sd.display_pos.get() : Vec2f(0, 0); + _writeVec2f(output, display_pos, range); } else if (sg.sgroup_type == SGroup::SG_TYPE_SUP) { @@ -825,14 +826,11 @@ void CmfSaver::_updateSGroupsXyzMinMax(Molecule& mol, Vec3f& min, Vec3f& max) DataSGroup& s = (DataSGroup&)sg; _updateBaseSGroupXyzMinMax(s, min, max); - if (s.display_pos.hasValue()) - { - const Vec2f& display_pos_2d = s.display_pos.get(); - Vec3f display_pos(display_pos_2d.x, display_pos_2d.y, 0); + Vec2f display_pos_2d = s.display_pos.hasValue() ? s.display_pos.get() : Vec2f(0, 0); + Vec3f display_pos(display_pos_2d.x, display_pos_2d.y, 0); - min.min(display_pos); - max.max(display_pos); - } + min.min(display_pos); + max.max(display_pos); } } } From 2ccadf6d8dd64011e8f573a8f77322ab965b1b2e Mon Sep 17 00:00:00 2001 From: Roman Porozhnetov Date: Sat, 30 May 2026 22:18:24 +0400 Subject: [PATCH 30/33] Guard SGroup Nullable reads and ordering --- .../indigo/src/indigo_molecule_operations.cpp | 16 +-- core/indigo-core/molecule/molecule_sgroups.h | 5 +- .../molecule/src/base_molecule.cpp | 8 +- .../molecule/src/base_molecule_sgroups.cpp | 5 +- .../molecule/src/base_molecule_templates.cpp | 8 +- core/indigo-core/molecule/src/cmf_saver.cpp | 20 +-- core/indigo-core/molecule/src/cml_saver.cpp | 26 ++-- .../molecule/src/molecule_cdxml_loader.cpp | 7 +- .../molecule/src/molecule_json_loader.cpp | 7 +- .../molecule/src/molecule_json_saver.cpp | 24 ++-- .../molecule/src/molecule_sgroups.cpp | 132 +++++++++++------- .../molecule/src/molfile_loader_postload.cpp | 4 +- .../molecule/src/molfile_loader_v3000.cpp | 41 +++++- .../molecule/src/molfile_saver.cpp | 88 +++++++----- .../indigo-core/molecule/src/smiles_saver.cpp | 5 +- core/indigo-core/tests/tests/formats.cpp | 106 +++++++++++++- core/render2d/src/render_internal.cpp | 15 +- 17 files changed, 354 insertions(+), 163 deletions(-) diff --git a/api/c/indigo/src/indigo_molecule_operations.cpp b/api/c/indigo/src/indigo_molecule_operations.cpp index 9e6add1a62..c5780579bd 100644 --- a/api/c/indigo/src/indigo_molecule_operations.cpp +++ b/api/c/indigo/src/indigo_molecule_operations.cpp @@ -2048,9 +2048,7 @@ CEXPORT int indigoGetSGroupSeqId(int sgroup) INDIGO_BEGIN { Superatom& sup = IndigoSuperatom::cast(self.getObject(sgroup)).get(); - if (sup.seqid != -1) - return sup.seqid; - return 0; + return sup.seqid.hasValue() ? sup.seqid.get() : 0; } INDIGO_END(0); } @@ -2062,7 +2060,8 @@ CEXPORT float* indigoGetSGroupCoords(int sgroup) IndigoDataSGroup& ds = IndigoDataSGroup::cast(self.getObject(sgroup)); auto& tmp = self.getThreadTmpData(); - const Vec2f& xy = ds.get().display_pos.get(); + DataSGroup& dsg = ds.get(); + Vec2f xy = dsg.display_pos.hasValue() ? dsg.display_pos.get() : Vec2f(0, 0); tmp.xyz[0] = xy.x; tmp.xyz[1] = xy.y; tmp.xyz[2] = 0.f; @@ -2076,7 +2075,7 @@ CEXPORT int indigoGetSGroupMultiplier(int sgroup) INDIGO_BEGIN { MultipleGroup& mg = IndigoMultipleGroup::cast(self.getObject(sgroup)).get(); - return mg.multiplier; + return mg.multiplier.hasValue() ? mg.multiplier.get() : 0; } INDIGO_END(-1); } @@ -2096,7 +2095,7 @@ CEXPORT int indigoGetRepeatingUnitConnectivity(int sgroup) INDIGO_BEGIN { RepeatingUnit& ru = IndigoRepeatingUnit::cast(self.getObject(sgroup)).get(); - return ru.connectivity; + return ru.connectivity.hasValue() ? ru.connectivity.get() : SGroup::HEAD_TO_TAIL; } INDIGO_END(-1); } @@ -2206,7 +2205,7 @@ CEXPORT int indigoSetSGroupOriginalId(int sgroup, int new_original) for (auto i = sgr.mol.sgroups.begin(); i != sgr.mol.sgroups.end(); i = sgr.mol.sgroups.next(i)) { SGroup& sg = sgr.mol.sgroups.getSGroup(i); - if (sg.parent_group == old_original) + if (sg.parent_group.hasValue() && sg.parent_group.get() == old_original) sg.parent_group = new_original; } } @@ -2222,7 +2221,8 @@ CEXPORT int indigoGetSGroupParentId(int sgroup) INDIGO_BEGIN { IndigoSGroup& sg = IndigoSGroup::cast(self.getObject(sgroup)); - return sg.get().parent_group; + SGroup& sgrp = sg.get(); + return sgrp.parent_group.hasValue() ? sgrp.parent_group.get() : 0; } INDIGO_END(-1); } diff --git a/core/indigo-core/molecule/molecule_sgroups.h b/core/indigo-core/molecule/molecule_sgroups.h index 286d3e2d36..28696265f3 100644 --- a/core/indigo-core/molecule/molecule_sgroups.h +++ b/core/indigo-core/molecule/molecule_sgroups.h @@ -264,9 +264,8 @@ namespace indigo struct SGroupInfo { SGroup& sgroup; - int index; - int external_index; - int parent_index; + int new_index; + int new_parent_index; }; class Tree; diff --git a/core/indigo-core/molecule/src/base_molecule.cpp b/core/indigo-core/molecule/src/base_molecule.cpp index 7c13c8482a..6c5995ea45 100644 --- a/core/indigo-core/molecule/src/base_molecule.cpp +++ b/core/indigo-core/molecule/src/base_molecule.cpp @@ -290,10 +290,10 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma for (i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) { SGroup& sgroup = sgroups.getSGroup(i); - if (sgroup.parent_idx < 0) + if (!sgroup.parent_idx.hasValue() || sgroup.parent_idx.get() < 0) continue; - const auto it = old_idx_to_new.find(sgroup.parent_idx); + const auto it = old_idx_to_new.find(sgroup.parent_idx.get()); if (it != old_idx_to_new.end()) sgroup.parent_idx = it->second; } @@ -1077,7 +1077,7 @@ void BaseMolecule::removeBond(int idx) void BaseMolecule::removeSGroup(int idx) { SGroup& sg = sgroups.getSGroup(idx); - _checkSgroupHierarchy(sg.parent_group, sg.index); + _checkSgroupHierarchy(sg.parent_group.hasValue() ? sg.parent_group.get() : 0, sg.index); sgroups.remove(idx); } @@ -1086,7 +1086,7 @@ void BaseMolecule::removeSGroupWithBasis(int idx) { QS_DEF(Array, sg_atoms); SGroup& sg = sgroups.getSGroup(idx); - _checkSgroupHierarchy(sg.parent_group, sg.index); + _checkSgroupHierarchy(sg.parent_group.hasValue() ? sg.parent_group.get() : 0, sg.index); sg_atoms.copy(sg.atoms); removeAtoms(sg_atoms); } diff --git a/core/indigo-core/molecule/src/base_molecule_sgroups.cpp b/core/indigo-core/molecule/src/base_molecule_sgroups.cpp index 0f47f2df93..3ded48e53b 100644 --- a/core/indigo-core/molecule/src/base_molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/base_molecule_sgroups.cpp @@ -49,7 +49,7 @@ void BaseMolecule::_checkSgroupHierarchy(int pidx, int oidx) for (int i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) { SGroup& sg = sgroups.getSGroup(i); - if (sg.parent_group == oidx) + if (sg.parent_group.hasValue() && sg.parent_group.get() == oidx) sg.parent_group = pidx; } } @@ -99,7 +99,8 @@ void BaseMolecule::collapse(BaseMolecule& bm, int id, Mapping& mapAtom, Mapping& const MultipleGroup& group = (MultipleGroup&)sg; - if (group.atoms.size() != group.multiplier.get() * group.parent_atoms.size()) + const int multiplier = group.multiplier.hasValue() ? group.multiplier.get() : 0; + if (group.atoms.size() != multiplier * group.parent_atoms.size()) throw Error("The group is already collapsed or invalid"); QS_DEF(Array, toRemove); diff --git a/core/indigo-core/molecule/src/base_molecule_templates.cpp b/core/indigo-core/molecule/src/base_molecule_templates.cpp index a056557944..a771a2e46a 100644 --- a/core/indigo-core/molecule/src/base_molecule_templates.cpp +++ b/core/indigo-core/molecule/src/base_molecule_templates.cpp @@ -771,8 +771,8 @@ bool BaseMolecule::_replaceExpandedMonomerWithTemplate(int sg_idx, int& tg_id, M if (tform.hasTransformation()) setTemplateAtomTransform(ta_idx, tform); setTemplateAtomClass(ta_idx, sa.sa_class.ptr()); - setTemplateAtomSeqid(ta_idx, sa.seqid); - setTemplateAtomDisplayOption(ta_idx, sa.contracted); + setTemplateAtomSeqid(ta_idx, sa.seqid.hasValue() ? sa.seqid.get() : -1); + setTemplateAtomDisplayOption(ta_idx, sa.contracted.hasValue() ? sa.contracted.get() : DisplayOption::Undefined); setTemplateAtomTemplateIndex(ta_idx, tg_index); added_templates.emplace(template_inchi_id, tg_index); _connectTemplateAtom(sa, ta_idx, remove_atoms); @@ -977,8 +977,8 @@ int BaseMolecule::_transformSGroupToTGroup(int sg_idx, int& tg_id) int idx = addTemplateAtom(tg.tgroup_name.ptr()); setTemplateAtomClass(idx, tg.tgroup_class.ptr()); - setTemplateAtomSeqid(idx, su.seqid); - setTemplateAtomDisplayOption(idx, su.contracted); + setTemplateAtomSeqid(idx, su.seqid.hasValue() ? su.seqid.get() : -1); + setTemplateAtomDisplayOption(idx, su.contracted.hasValue() ? su.contracted.get() : DisplayOption::Undefined); setTemplateAtomTemplateIndex(idx, tg_idx); for (int j = 0; j < ap_points_atoms.size(); j++) diff --git a/core/indigo-core/molecule/src/cmf_saver.cpp b/core/indigo-core/molecule/src/cmf_saver.cpp index a0b0cf9a9e..47afe424bc 100644 --- a/core/indigo-core/molecule/src/cmf_saver.cpp +++ b/core/indigo-core/molecule/src/cmf_saver.cpp @@ -339,12 +339,13 @@ void CmfSaver::_encodeExtSection(Molecule& mol, const Mapping& mapping) _encodeString(sd.queryoper); _encodeString(sd.data); // Pack detached, relative, display_units, and sd.dasp_pos into one byte - if (sd.dasp_pos < 0 || sd.dasp_pos > 9) - throw Error("DataSGroup dasp_pos field should be less than 10: %d", sd.dasp_pos.get()); - byte packed = (sd.dasp_pos & 0x0F) | (sd.detached << 4) | (sd.relative << 5) | (sd.display_units << 6); + const int dasp_pos = sd.dasp_pos.hasValue() ? sd.dasp_pos.get() : 0; + if (dasp_pos < 0 || dasp_pos > 9) + throw Error("DataSGroup dasp_pos field should be less than 10: %d", dasp_pos); + byte packed = (dasp_pos & 0x0F) | (sd.detached << 4) | (sd.relative << 5) | (sd.display_units << 6); _output->writeByte(packed); - _output->writePackedUInt(sd.num_chars); - _output->writeChar(sd.tag); + _output->writePackedUInt(sd.num_chars.hasValue() ? sd.num_chars.get() : 0); + _output->writeChar(sd.tag.hasValue() ? sd.tag.get() : 0); } else if (sg.sgroup_type == SGroup::SG_TYPE_SUP) { @@ -370,7 +371,7 @@ void CmfSaver::_encodeExtSection(Molecule& mol, const Mapping& mapping) _encode(CMF_REPEATINGUNIT); _encodeBaseSGroup(mol, su, mapping); _encodeString(su.label); - _output->writePackedUInt(su.connectivity); + _output->writePackedUInt(su.connectivity.hasValue() ? su.connectivity.get() : SGroup::HEAD_TO_TAIL); } else if (sg.sgroup_type == SGroup::SG_TYPE_MUL) { @@ -378,9 +379,10 @@ void CmfSaver::_encodeExtSection(Molecule& mol, const Mapping& mapping) _encode(CMF_MULTIPLEGROUP); _encodeBaseSGroup(mol, sm, mapping); _encodeUIntArray(sm.parent_atoms, *mapping.atom_mapping); - if (sm.multiplier < 0) - throw Error("internal error: SGroup multiplier is negative: %d", sm.multiplier.get()); - _output->writePackedUInt(sm.multiplier); + const int multiplier = sm.multiplier.hasValue() ? sm.multiplier.get() : 0; + if (multiplier < 0) + throw Error("internal error: SGroup multiplier is negative: %d", multiplier); + _output->writePackedUInt(multiplier); } } diff --git a/core/indigo-core/molecule/src/cml_saver.cpp b/core/indigo-core/molecule/src/cml_saver.cpp index 6200452f28..433e2e45b1 100644 --- a/core/indigo-core/molecule/src/cml_saver.cpp +++ b/core/indigo-core/molecule/src/cml_saver.cpp @@ -568,7 +568,7 @@ void CmlSaver::_addMoleculeElement(XMLElement* elem, BaseMolecule& mol, bool que auto sgroup_infos = _mol->sgroups.getOrderedSGroups(); for (const auto& info : sgroup_infos) { - if (info.parent_index == 0) + if (info.new_parent_index == 0) _addSgroupElement(molecule, info, sgroup_infos); } } @@ -582,14 +582,14 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, c QS_DEF(Array, buf); ArrayOutput out(buf); - out.printf("sg%d", info.index); + out.printf("sg%d", info.new_index); buf.push(0); sg->SetAttribute("id", buf.ptr()); auto addChildren = [&]() { for (const auto& child_info : sgroup_infos) { - if (child_info.parent_index == info.index) + if (child_info.new_parent_index == info.new_index) _addSgroupElement(sg, child_info, sgroup_infos); } }; @@ -613,7 +613,8 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, c XMLElement* brks = _doc->NewElement("MBracket"); sg->LinkEndChild(brks); - if (sgroup.brk_style == 0) + const int brk_style = sgroup.brk_style.hasValue() ? sgroup.brk_style.get() : 0; + if (brk_style == 0) brks->SetAttribute("type", "SQUARE"); else brks->SetAttribute("type", "ROUND"); @@ -681,15 +682,16 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, c sg->SetAttribute("unitsDisplayed", "Unit displayed"); } - char tag = dsg.tag; + char tag = dsg.tag.hasValue() ? dsg.tag.get() : 0; if (tag != 0 && tag != ' ') { sg->SetAttribute("tag", tag); } - if (dsg.num_chars > 0) + const int num_chars = dsg.num_chars.hasValue() ? dsg.num_chars.get() : 0; + if (num_chars > 0) { - sg->SetAttribute("displayedChars", dsg.num_chars); + sg->SetAttribute("displayedChars", num_chars); } if (dsg.data.size() > 0 && dsg.data[0] != 0) @@ -731,11 +733,12 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, c sg->SetAttribute("title", name); } - if (sru.connectivity == SGroup::HEAD_TO_TAIL) + const int connectivity = sru.connectivity.hasValue() ? sru.connectivity.get() : SGroup::HEAD_TO_TAIL; + if (connectivity == SGroup::HEAD_TO_TAIL) { sg->SetAttribute("connect", "ht"); } - else if (sru.connectivity == SGroup::HEAD_TO_HEAD) + else if (connectivity == SGroup::HEAD_TO_HEAD) { sg->SetAttribute("connect", "hh"); } @@ -748,9 +751,10 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, c MultipleGroup& mul = (MultipleGroup&)sgroup; - if (mul.multiplier > 0) + const int multiplier = mul.multiplier.hasValue() ? mul.multiplier.get() : 0; + if (multiplier > 0) { - sg->SetAttribute("title", mul.multiplier); + sg->SetAttribute("title", multiplier); } if (mul.parent_atoms.size() > 0) diff --git a/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp b/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp index 18784dff02..57c2730fa5 100644 --- a/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp +++ b/core/indigo-core/molecule/src/molecule_cdxml_loader.cpp @@ -1186,7 +1186,7 @@ void MoleculeCdxmlLoader::_addBracket(BaseMolecule& mol, const CdxmlBracket& bra if (bracket.usage == kCDXBracketUsage_MultipleGroup) { MultipleGroup& mg = (MultipleGroup&)sgroup; - if (mg.multiplier) + if (mg.multiplier.hasValue() && mg.multiplier.get()) mg.parent_atoms.push(atom_idx); } } @@ -1290,11 +1290,12 @@ void MoleculeCdxmlLoader::_handleSGroup(SGroup& sgroup, const std::unordered_set int rep_start = mapping[start]; int rep_end = mapping[end]; MultipleGroup& mg = (MultipleGroup&)sgroup; - if (mg.multiplier > 1) + const int multiplier = mg.multiplier.hasValue() ? mg.multiplier.get() : 0; + if (multiplier > 1) { int start_order = start_bond > 0 ? bmol.getBondOrder(start_bond) : -1; int end_order = end_bond > 0 ? bmol.getBondOrder(end_bond) : -1; - for (int j = 0; j < mg.multiplier - 1; j++) + for (int j = 0; j < multiplier - 1; j++) { bmol.mergeWithMolecule(*rep, &mapping, 0); int k; diff --git a/core/indigo-core/molecule/src/molecule_json_loader.cpp b/core/indigo-core/molecule/src/molecule_json_loader.cpp index 4b367533c8..d0684f3452 100644 --- a/core/indigo-core/molecule/src/molecule_json_loader.cpp +++ b/core/indigo-core/molecule/src/molecule_json_loader.cpp @@ -941,11 +941,12 @@ void MoleculeJsonLoader::handleSGroup(SGroup& sgroup, const std::unordered_set 1) + const int multiplier = mg.multiplier.hasValue() ? mg.multiplier.get() : 0; + if (multiplier > 1) { int start_order = start_bond >= 0 ? bmol.getBondOrder(start_bond) : -1; int end_order = end_bond >= 0 ? bmol.getBondOrder(end_bond) : -1; - for (int j = 0; j < mg.multiplier - 1; j++) + for (int j = 0; j < multiplier - 1; j++) { bmol.mergeWithMolecule(*rep, &mapping, 0); int k; @@ -1023,7 +1024,7 @@ void MoleculeJsonLoader::parseSGroups(const rapidjson::Value& sgroups, BaseMolec if (sg_type == SGroup::SG_TYPE_MUL) { MultipleGroup& mg = (MultipleGroup&)sgroup; - if (mg.multiplier) + if (mg.multiplier.hasValue() && mg.multiplier.get()) mg.parent_atoms.push(atom_idx); } } diff --git a/core/indigo-core/molecule/src/molecule_json_saver.cpp b/core/indigo-core/molecule/src/molecule_json_saver.cpp index 04cacdab15..6218a06a1b 100644 --- a/core/indigo-core/molecule/src/molecule_json_saver.cpp +++ b/core/indigo-core/molecule/src/molecule_json_saver.cpp @@ -249,7 +249,7 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) writer.Bool(true); } - char tag = dsg.tag; + char tag = dsg.tag.hasValue() ? dsg.tag.get() : 0; if (tag != 0 && tag != ' ') { writer.Key("tag"); @@ -257,10 +257,11 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) writer.String(tag_s.c_str()); } - if (dsg.num_chars > 0) + const int num_chars = dsg.num_chars.hasValue() ? dsg.num_chars.get() : 0; + if (num_chars > 0) { writer.Key("displayedChars"); - writer.Int(dsg.num_chars); + writer.Int(num_chars); } } break; @@ -316,7 +317,8 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) } writer.Key("connectivity"); - switch (ru.connectivity) + const int connectivity = ru.connectivity.hasValue() ? ru.connectivity.get() : SGroup::HEAD_TO_TAIL; + switch (connectivity) { case SGroup::HEAD_TO_TAIL: writer.String("HT"); @@ -340,7 +342,7 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) writer.EndArray(); } writer.Key("mul"); - writer.Int(mg.multiplier); + writer.Int(mg.multiplier.hasValue() ? mg.multiplier.get() : 0); } break; case SGroup::SG_TYPE_MON: @@ -351,25 +353,27 @@ void MoleculeJsonSaver::saveSGroup(SGroup& sgroup, JsonWriter& writer) break; case SGroup::SG_TYPE_COP: { CopolymerGroup& ru = (CopolymerGroup&)sgroup; - if (ru.sgroup_subtype != 0) + const int subtype = ru.sgroup_subtype.hasValue() ? ru.sgroup_subtype.get() : 0; + if (subtype != 0) { writer.Key("subtype"); - if (ru.sgroup_subtype == SGroup::SG_SUBTYPE_ALT) + if (subtype == SGroup::SG_SUBTYPE_ALT) { writer.String("ALT"); } - else if (ru.sgroup_subtype == SGroup::SG_SUBTYPE_RAN) + else if (subtype == SGroup::SG_SUBTYPE_RAN) { writer.String("RAN"); } - else if (ru.sgroup_subtype == SGroup::SG_SUBTYPE_BLO) + else if (subtype == SGroup::SG_SUBTYPE_BLO) { writer.String("BLO"); } } writer.Key("connectivity"); - switch (ru.connectivity) + const int connectivity = ru.connectivity.hasValue() ? ru.connectivity.get() : SGroup::HEAD_TO_TAIL; + switch (connectivity) { case SGroup::HEAD_TO_TAIL: writer.String("HT"); diff --git a/core/indigo-core/molecule/src/molecule_sgroups.cpp b/core/indigo-core/molecule/src/molecule_sgroups.cpp index 1abd64b842..534662df3e 100644 --- a/core/indigo-core/molecule/src/molecule_sgroups.cpp +++ b/core/indigo-core/molecule/src/molecule_sgroups.cpp @@ -23,7 +23,7 @@ #include "molecule/molecule_sgroups.h" #include -#include +#include using namespace indigo; @@ -287,7 +287,7 @@ void MoleculeSGroups::buildTree(Tree& tree) for (auto i = begin(); i != end(); i = next(i)) { SGroup& sgroup = getSGroup(i); - tree.insert(i, sgroup.parent_idx); + tree.insert(i, sgroup.parent_idx.hasValue() ? sgroup.parent_idx.get() : -1); } } @@ -298,12 +298,14 @@ bool MoleculeSGroups::getParentAtoms(int idx, Array& target) bool MoleculeSGroups::getParentAtoms(SGroup& sgroup, Array& target) { - if (sgroup.parent_idx < 0) + if (!sgroup.parent_idx.hasValue() || sgroup.parent_idx.get() < 0) return false; int pidx = sgroup.parent_idx.get(); - if (!hasSGroup(sgroup.parent_idx)) + if (!hasSGroup(pidx)) { - pidx = findSGroupById(sgroup.parent_group); + if (!sgroup.parent_group.hasValue()) + return false; + pidx = findSGroupById(sgroup.parent_group.get()); if (pidx < 0) return false; } @@ -441,7 +443,7 @@ void MoleculeSGroups::findSGroups(int property, int value, Array& sgs) for (i = _sgroups.begin(); i != _sgroups.end(); i = _sgroups.next(i)) { SGroup& sg = *_sgroups.at(i); - if (sg.brk_style == value) + if (sg.brk_style.hasValue() && sg.brk_style.get() == value) { sgs.push(i); } @@ -467,7 +469,7 @@ void MoleculeSGroups::findSGroups(int property, int value, Array& sgs) for (i = _sgroups.begin(); i != _sgroups.end(); i = _sgroups.next(i)) { SGroup& sg = *_sgroups.at(i); - if (sg.parent_group == value) + if (sg.parent_group.hasValue() && sg.parent_group.get() == value) { sgs.push(i); } @@ -479,9 +481,9 @@ void MoleculeSGroups::findSGroups(int property, int value, Array& sgs) return; SGroup& sg = *_sgroups.at(value); - if (sg.parent_group != 0) + if (sg.parent_group.hasValue() && sg.parent_group.get() != 0) { - int idx = findSGroupById(sg.parent_group); + int idx = findSGroupById(sg.parent_group.get()); if (idx != -1) sgs.push(idx); } @@ -623,7 +625,7 @@ void MoleculeSGroups::findSGroups(int property, const char* str, Array& sgs if (sg.sgroup_type == SGroup::SG_TYPE_DAT) { DataSGroup& dg = (DataSGroup&)sg; - if ((strlen(str) == 1) && str[0] == dg.tag) + if ((strlen(str) == 1) && dg.tag.hasValue() && str[0] == dg.tag.get()) { sgs.push(i); } @@ -730,16 +732,24 @@ bool MoleculeSGroups::_cmpIndices(Array& t_inds, Array& q_inds) return true; } +// Returns SGroups in serialization order: roots first, then descendants by depth. +// The order is stable inside each depth level, and new_parent_index is resolved after +// the final serialized indexes are known. std::vector MoleculeSGroups::getOrderedSGroups() { std::vector infos; - std::map pool_index_to_info_index; - std::map original_index_to_info_index; - + std::unordered_map pool_index_to_info_index; + std::unordered_map original_index_to_info_index; + const int sgroup_count = getSGroupCount(); + infos.reserve(sgroup_count); + pool_index_to_info_index.reserve(sgroup_count); + original_index_to_info_index.reserve(sgroup_count); + + // Phase 1: collect SGroups in pool order and build lookup tables for parent resolution. for (int i = begin(); i != end(); i = next(i)) { SGroup& sg = getSGroup(i); - SGroupInfo info = {sg, 0, 0, 0}; + SGroupInfo info = {sg, 0, 0}; int info_index = static_cast(infos.size()); infos.push_back(info); @@ -753,17 +763,25 @@ std::vector MoleculeSGroups::getOrderedSGroups() } } + // Phase 2: resolve each parent reference to an entry in the collected info array. std::vector parent_info_index(infos.size(), -1); + auto resolve_pool_index = [&](int pool_index) -> int { + auto it = pool_index_to_info_index.find(pool_index); + if (it == pool_index_to_info_index.end()) + return -1; + return it->second; + }; + for (int info_index = 0; info_index < static_cast(infos.size()); info_index++) { SGroup& sg = infos[info_index].sgroup; if (sg.parent_idx.hasValue()) { - auto it = pool_index_to_info_index.find(sg.parent_idx.get()); - if (it != pool_index_to_info_index.end() && it->second != info_index) + int parent_info = resolve_pool_index(sg.parent_idx.get()); + if (parent_info >= 0) { - parent_info_index[info_index] = it->second; + parent_info_index[info_index] = parent_info; continue; } } @@ -773,63 +791,77 @@ std::vector MoleculeSGroups::getOrderedSGroups() continue; auto original_it = original_index_to_info_index.find(parent_id); - if (original_it != original_index_to_info_index.end() && original_it->second >= 0 && original_it->second != info_index) + if (original_it != original_index_to_info_index.end() && original_it->second >= 0) { parent_info_index[info_index] = original_it->second; continue; } - // CML loader stores nested parent as pool index + 1 and leaves SGroup::index unset. - auto pool_it = pool_index_to_info_index.find(parent_id - 1); - if (pool_it != pool_index_to_info_index.end() && pool_it->second != info_index && infos[pool_it->second].sgroup.index == 0) - parent_info_index[info_index] = pool_it->second; + // Some inputs have no persisted SGroup index and refer to parents by one-based pool position. + int parent_info = resolve_pool_index(parent_id - 1); + if (parent_info >= 0 && infos[parent_info].sgroup.index == 0) + parent_info_index[info_index] = parent_info; } - std::vector ordered_info_indexes; - ordered_info_indexes.reserve(infos.size()); - std::vector added(infos.size(), false); + // Phase 3: resolve nesting depth. DFS is used only to compute depth; emission stays + // level-ordered so all roots are written before all children, then grandchildren. + std::vector depth(infos.size(), -1); + std::vector chain_pos(infos.size(), -1); - while (ordered_info_indexes.size() < infos.size()) + auto clear_chain_positions = [&](const std::vector& chain) { + for (int info_index : chain) + chain_pos[info_index] = -1; + }; + + for (int info_index = 0; info_index < static_cast(infos.size()); info_index++) { - std::size_t added_count = ordered_info_indexes.size(); - std::vector added_before_pass = added; - for (int info_index = 0; info_index < static_cast(infos.size()); info_index++) - { - if (added[info_index]) - continue; + if (depth[info_index] >= 0) + continue; - int parent_index = parent_info_index[info_index]; - if (parent_index < 0 || added_before_pass[parent_index]) - { - ordered_info_indexes.push_back(info_index); - added[info_index] = true; - } - } + std::vector chain; + int current = info_index; - if (ordered_info_indexes.size() == added_count) + // Walk through parents until a root, a memoized parent, or a cycle is found. + while (current >= 0 && depth[current] < 0) { - for (int info_index = 0; info_index < static_cast(infos.size()); info_index++) + if (chain_pos[current] >= 0) { - if (!added[info_index]) - { - parent_info_index[info_index] = -1; - ordered_info_indexes.push_back(info_index); - added[info_index] = true; - } + clear_chain_positions(chain); + throw Error("SGroup parent hierarchy contains a cycle"); } + + chain_pos[current] = static_cast(chain.size()); + chain.push_back(current); + current = parent_info_index[current]; } + + if (chain.empty()) + continue; + + int current_depth = current >= 0 ? depth[current] : -1; + for (auto it = chain.rbegin(); it != chain.rend(); ++it) + depth[*it] = ++current_depth; + clear_chain_positions(chain); } + // Phase 4: emit by depth while preserving source order for SGroups at the same depth. + std::vector ordered_info_indexes; + ordered_info_indexes.reserve(infos.size()); + for (int info_index = 0; info_index < static_cast(infos.size()); info_index++) + ordered_info_indexes.push_back(info_index); + + std::stable_sort(ordered_info_indexes.begin(), ordered_info_indexes.end(), [&](int left, int right) { return depth[left] < depth[right]; }); + + // Phase 5: assign serialized indexes and materialize result entries. for (int i = 0; i < static_cast(ordered_info_indexes.size()); i++) - infos[ordered_info_indexes[i]].index = i + 1; + infos[ordered_info_indexes[i]].new_index = i + 1; std::vector result; result.reserve(ordered_info_indexes.size()); for (int info_index : ordered_info_indexes) { SGroupInfo& info = infos[info_index]; - info.external_index = info.sgroup.ext_index != 0 ? info.sgroup.ext_index : info.index; - info.parent_index = parent_info_index[info_index] >= 0 ? infos[parent_info_index[info_index]].index : 0; + info.new_parent_index = parent_info_index[info_index] >= 0 ? infos[parent_info_index[info_index]].new_index : 0; result.push_back(info); } return result; diff --git a/core/indigo-core/molecule/src/molfile_loader_postload.cpp b/core/indigo-core/molecule/src/molfile_loader_postload.cpp index d1d7d352ad..7c799f86a3 100644 --- a/core/indigo-core/molecule/src/molfile_loader_postload.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_postload.cpp @@ -95,9 +95,9 @@ void MolfileLoader::_postLoad() if (sgroup.sgroup_type == SGroup::SG_TYPE_DAT) { DataSGroup& dsg = static_cast(sgroup); - if (dsg.parent_idx > -1 && std::string(dsg.name.ptr()) == "SMMX:class") + if (dsg.parent_idx.hasValue() && dsg.parent_idx.get() > -1 && std::string(dsg.name.ptr()) == "SMMX:class") { - SGroup& parent_sgroup = _bmol->sgroups.getSGroup(dsg.parent_idx); + SGroup& parent_sgroup = _bmol->sgroups.getSGroup(dsg.parent_idx.get()); if (parent_sgroup.sgroup_type == SGroup::SG_TYPE_SUP) { auto& sa = static_cast(parent_sgroup); diff --git a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp index 08ab1394c0..718c32625c 100644 --- a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp @@ -17,6 +17,8 @@ ***************************************************************************/ #include +#include +#include #include "../layout/molecule_layout.h" #include "base_cpp/output.h" @@ -858,7 +860,7 @@ void MolfileLoader::_fillSGroupsParentIndices() std::multimap indices; // original index can be arbitrary, sometimes key is used multiple times - for (auto i = sgroups.begin(); i != sgroups.end(); i++) + for (auto i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) { SGroup& sgroup = sgroups.getSGroup(i); indices.emplace(sgroup.index, i); @@ -868,9 +870,9 @@ void MolfileLoader::_fillSGroupsParentIndices() for (auto i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) { SGroup& sgroup = sgroups.getSGroup(i); - if (indices.count(sgroup.parent_group) == 1) + if (sgroup.parent_group.hasValue() && indices.count(sgroup.parent_group.get()) == 1) { - const auto it = indices.find(sgroup.parent_group); + const auto it = indices.find(sgroup.parent_group.get()); // TODO: check fix auto parent_idx = it->second; SGroup& parent_sgroup = sgroups.getSGroup(parent_idx); @@ -880,7 +882,7 @@ void MolfileLoader::_fillSGroupsParentIndices() } else { - sgroup.parent_idx = -1; + throw Error("SGroup parent hierarchy contains a cycle"); } } else @@ -888,6 +890,37 @@ void MolfileLoader::_fillSGroupsParentIndices() sgroup.parent_idx = -1; } } + + std::unordered_map state; + state.reserve(sgroups.getSGroupCount()); + for (auto i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) + state.emplace(i, 0); + + for (auto i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) + { + std::vector chain; + int current = i; + + while (true) + { + auto state_it = state.find(current); + if (state_it == state.end() || state_it->second == 2) + break; + if (state_it->second == 1) + throw Error("SGroup parent hierarchy contains a cycle"); + + state_it->second = 1; + chain.push_back(current); + + SGroup& sgroup = sgroups.getSGroup(current); + if (!sgroup.parent_idx.hasValue() || sgroup.parent_idx.get() < 0) + break; + current = sgroup.parent_idx.get(); + } + + for (int chain_idx : chain) + state[chain_idx] = 2; + } } void MolfileLoader::_readCollectionBlock3000() diff --git a/core/indigo-core/molecule/src/molfile_saver.cpp b/core/indigo-core/molecule/src/molfile_saver.cpp index 9df6c01c0e..46ea342dff 100644 --- a/core/indigo-core/molecule/src/molfile_saver.cpp +++ b/core/indigo-core/molecule/src/molfile_saver.cpp @@ -935,8 +935,9 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) out.printf(" SAP=(3 %d %d %s)", _atom_mapping[sup.attachment_points[j].aidx], leave_idx, sup.attachment_points[j].apid.ptr()); } } - if (sup.seqid > 0) - out.printf(" SEQID=%d", (sup.seqid.hasValue() ? sup.seqid.get() : 0)); + const int seqid = sup.seqid.hasValue() ? sup.seqid.get() : 0; + if (seqid > 0) + out.printf(" SEQID=%d", seqid); if (sup.sa_natreplace.size() > 1) out.printf(" NATREPLACE=%s", sup.sa_natreplace.ptr()); @@ -1023,9 +1024,10 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) else if (sgroup.sgroup_type == SGroup::SG_TYPE_SRU) { RepeatingUnit& ru = static_cast(sgroup); - if (ru.connectivity == SGroup::HEAD_TO_HEAD) + const int connectivity = ru.connectivity.hasValue() ? ru.connectivity.get() : SGroup::HEAD_TO_TAIL; + if (connectivity == SGroup::HEAD_TO_HEAD) out.printf(" CONNECT=HH"); - else if (ru.connectivity == SGroup::HEAD_TO_TAIL) + else if (connectivity == SGroup::HEAD_TO_TAIL) out.printf(" CONNECT=HT"); else out.printf(" CONNECT=EU"); @@ -1041,19 +1043,21 @@ void MolfileSaver::_writeCtab(Output& output, BaseMolecule& mol, bool query) else if (sgroup.sgroup_type == SGroup::SG_TYPE_COP) { CopolymerGroup& cg = static_cast(sgroup); - if (cg.connectivity == SGroup::HEAD_TO_HEAD) + const int connectivity = cg.connectivity.hasValue() ? cg.connectivity.get() : SGroup::HEAD_TO_TAIL; + if (connectivity == SGroup::HEAD_TO_HEAD) out.printf(" CONNECT=HH"); - else if (cg.connectivity == SGroup::HEAD_TO_TAIL) + else if (connectivity == SGroup::HEAD_TO_TAIL) out.printf(" CONNECT=HT"); else out.printf(" CONNECT=EU"); - if (cg.sgroup_subtype != 0) + const int subtype = cg.sgroup_subtype.hasValue() ? cg.sgroup_subtype.get() : 0; + if (subtype != 0) { - if (cg.sgroup_subtype == SGroup::SG_SUBTYPE_ALT) + if (subtype == SGroup::SG_SUBTYPE_ALT) out.printf(" SUBTYPE=ALT"); - else if (cg.sgroup_subtype == SGroup::SG_SUBTYPE_RAN) + else if (subtype == SGroup::SG_SUBTYPE_RAN) out.printf(" SUBTYPE=RAN"); - else if (cg.sgroup_subtype == SGroup::SG_SUBTYPE_BLO) + else if (subtype == SGroup::SG_SUBTYPE_BLO) out.printf(" SUBTYPE=BLO"); } _writeMultiString(output, buf.ptr(), buf.size()); @@ -1116,7 +1120,8 @@ void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, const SGroupInfo& inf { int i; - output.printf("%d %s %d", info.index, SGroup::typeToString(sgroup.sgroup_type), info.external_index); + const int external_index = sgroup.ext_index != 0 ? sgroup.ext_index : info.new_index; + output.printf("%d %s %d", info.new_index, SGroup::typeToString(sgroup.sgroup_type), external_index); if (sgroup.atoms.size() > 0) { @@ -1143,25 +1148,27 @@ void MolfileSaver::_writeGenericSGroup3000(SGroup& sgroup, const SGroupInfo& inf output.printf(")"); } } - if (sgroup.sgroup_subtype > 0) + const int subtype = sgroup.sgroup_subtype.hasValue() ? sgroup.sgroup_subtype.get() : 0; + if (subtype > 0) { - if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_ALT) + if (subtype == SGroup::SG_SUBTYPE_ALT) output.printf(" SUBTYPE=ALT"); - else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_RAN) + else if (subtype == SGroup::SG_SUBTYPE_RAN) output.printf(" SUBTYPE=RAN"); - else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_BLO) + else if (subtype == SGroup::SG_SUBTYPE_BLO) output.printf(" SUBTYPE=BLO"); } - if (info.parent_index > 0) + if (info.new_parent_index > 0) { - output.printf(" PARENT=%d", info.parent_index); + output.printf(" PARENT=%d", info.new_parent_index); } for (i = 0; i < sgroup.brackets.size(); i++) { Vec2f* brackets = sgroup.brackets[i]; output.printf(" BRKXYZ=(9 %f %f %f %f %f %f %f %f %f)", brackets[0].x, brackets[0].y, 0.f, brackets[1].x, brackets[1].y, 0.f, 0.f, 0.f, 0.f); } - if (sgroup.brackets.size() > 0 && sgroup.brk_style > 0) + const int brk_style = sgroup.brk_style.hasValue() ? sgroup.brk_style.get() : 0; + if (sgroup.brackets.size() > 0 && brk_style > 0) { output.printf(" BRKTYP=PAREN"); } @@ -1714,16 +1721,16 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (i = j; i < std::min(sgroup_count, j + 8); i++) { const auto& info = *sgroup_infos[i]; - output.printf(" %3d %s", info.index, SGroup::typeToString(info.sgroup.sgroup_type)); + output.printf(" %3d %s", info.new_index, SGroup::typeToString(info.sgroup.sgroup_type)); } output.writeCR(); } for (const auto* info : sgroup_infos) { - if (info->parent_index > 0) + if (info->new_parent_index > 0) { - child_ids.push(info->index); - parent_ids.push(info->parent_index); + child_ids.push(info->new_index); + parent_ids.push(info->new_parent_index); } } for (j = 0; j < child_ids.size(); j += 8) @@ -1741,7 +1748,8 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (i = j; i < std::min(sgroup_count, j + 8); i++) { const auto& info = *sgroup_infos[i]; - output.printf(" %3d %3d", info.index, info.external_index); + const int external_index = info.sgroup.ext_index != 0 ? info.sgroup.ext_index : info.new_index; + output.printf(" %3d %3d", info.new_index, external_index); } output.writeCR(); } @@ -1760,11 +1768,12 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) { const SGroupInfo& info = *sru_infos[i]; RepeatingUnit* ru = (RepeatingUnit*)&info.sgroup; - output.printf(" %3d ", info.index); + output.printf(" %3d ", info.new_index); - if (ru->connectivity == SGroup::HEAD_TO_HEAD) + const int connectivity = ru->connectivity.hasValue() ? ru->connectivity.get() : SGroup::HEAD_TO_TAIL; + if (connectivity == SGroup::HEAD_TO_HEAD) output.printf("HH "); - else if (ru->connectivity == SGroup::HEAD_TO_TAIL) + else if (connectivity == SGroup::HEAD_TO_TAIL) output.printf("HT "); else output.printf("EU "); @@ -1775,7 +1784,7 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) for (const auto* info : sgroup_infos) { SGroup& sgroup = info->sgroup; - int wi = info->index; + int wi = info->new_index; for (j = 0; j < sgroup.atoms.size(); j += 8) { int k; @@ -1920,17 +1929,19 @@ void MolfileSaver::_writeCtab2000(Output& output, BaseMolecule& mol, bool query) output.printf("M SDI %3d 4 %9.4f %9.4f %9.4f %9.4f\n", wi, sgroup.brackets[j][0].x, sgroup.brackets[j][0].y, sgroup.brackets[j][1].x, sgroup.brackets[j][1].y); } - if (sgroup.brackets.size() > 0 && sgroup.brk_style > 0) + const int brk_style = sgroup.brk_style.hasValue() ? sgroup.brk_style.get() : 0; + if (sgroup.brackets.size() > 0 && brk_style > 0) { - output.printf("M SBT 1 %3d %3d\n", wi, (sgroup.brk_style.hasValue() ? sgroup.brk_style.get() : 0)); + output.printf("M SBT 1 %3d %3d\n", wi, brk_style); } - if (sgroup.sgroup_subtype > 0) + const int subtype = sgroup.sgroup_subtype.hasValue() ? sgroup.sgroup_subtype.get() : 0; + if (subtype > 0) { - if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_ALT) + if (subtype == SGroup::SG_SUBTYPE_ALT) output.printf("M SST 1 %3d ALT\n", wi); - else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_RAN) + else if (subtype == SGroup::SG_SUBTYPE_RAN) output.printf("M SST 1 %3d RAN\n", wi); - else if (sgroup.sgroup_subtype == SGroup::SG_SUBTYPE_BLO) + else if (subtype == SGroup::SG_SUBTYPE_BLO) output.printf("M SST 1 %3d BLO\n", wi); } } @@ -2133,12 +2144,13 @@ void MolfileSaver::_writeDataSGroupDisplay(DataSGroup& datasgroup, Output& out) dp_y = display_pos.y; } out.printf("%10.4f%10.4f %c%c%c", dp_x, dp_y, datasgroup.detached ? 'D' : 'A', datasgroup.relative ? 'R' : 'A', datasgroup.display_units ? 'U' : ' '); - if (datasgroup.num_chars == 0) - out.printf(" ALL 1 %c %1d ", (datasgroup.tag.hasValue() ? datasgroup.tag.get() : 0), - (datasgroup.dasp_pos.hasValue() ? datasgroup.dasp_pos.get() : 0)); + const int num_chars = datasgroup.num_chars.hasValue() ? datasgroup.num_chars.get() : 0; + const char tag = datasgroup.tag.hasValue() ? datasgroup.tag.get() : 0; + const int dasp_pos = datasgroup.dasp_pos.hasValue() ? datasgroup.dasp_pos.get() : 0; + if (num_chars == 0) + out.printf(" ALL 1 %c %1d ", tag, dasp_pos); else - out.printf(" %3d 1 %c %1d ", (datasgroup.num_chars.hasValue() ? datasgroup.num_chars.get() : 0), - (datasgroup.tag.hasValue() ? datasgroup.tag.get() : 0), (datasgroup.dasp_pos.hasValue() ? datasgroup.dasp_pos.get() : 0)); + out.printf(" %3d 1 %c %1d ", num_chars, tag, dasp_pos); } bool MolfileSaver::_hasNeighborEitherBond(BaseMolecule& mol, int edge_idx) diff --git a/core/indigo-core/molecule/src/smiles_saver.cpp b/core/indigo-core/molecule/src/smiles_saver.cpp index 80ebb28991..4298dc9e06 100644 --- a/core/indigo-core/molecule/src/smiles_saver.cpp +++ b/core/indigo-core/molecule/src/smiles_saver.cpp @@ -1860,7 +1860,7 @@ void SmilesSaver::_writeSGroups() if (dsg.description.size() > 0) _output.writeString(dsg.description.ptr()); _output.writeChar(':'); - _output.writeChar(dsg.tag); + _output.writeChar(dsg.tag.hasValue() ? dsg.tag.get() : 0); _output.writeChar(':'); // No coords output for now } @@ -1875,7 +1875,8 @@ void SmilesSaver::_writeSGroups() _output.writeString("n:"); _writeSGroupAtoms(sg); _output.printf(":%s:", ru.label.ptr() ? ru.label.ptr() : ""); - switch (ru.connectivity) + const int connectivity = ru.connectivity.hasValue() ? ru.connectivity.get() : RepeatingUnit::HEAD_TO_TAIL; + switch (connectivity) { case SGroup::HEAD_TO_TAIL: _output.writeString("ht"); diff --git a/core/indigo-core/tests/tests/formats.cpp b/core/indigo-core/tests/tests/formats.cpp index 861b8d66f4..4b49f7a992 100644 --- a/core/indigo-core/tests/tests/formats.cpp +++ b/core/indigo-core/tests/tests/formats.cpp @@ -146,7 +146,8 @@ TEST_F(IndigoCoreFormatsTest, smiles_data_sgroups) ASSERT_STREQ(dsg.data.ptr(), "data"); ASSERT_STREQ(dsg.queryoper.ptr(), "like"); ASSERT_STREQ(dsg.description.ptr(), "unit"); - ASSERT_EQ(dsg.tag, 't'); + ASSERT_TRUE(dsg.tag.hasValue()); + ASSERT_EQ(dsg.tag.get(), 't'); ASSERT_FALSE(dsg.display_pos.hasValue()); ASSERT_EQ(dsg.atoms.size(), 4); ASSERT_EQ(dsg.atoms.at(0), 3); @@ -175,7 +176,8 @@ TEST_F(IndigoCoreFormatsTest, smiles_data_sgroups_coords) ASSERT_STREQ(dsg.data.ptr(), ""); ASSERT_STREQ(dsg.queryoper.ptr(), ""); ASSERT_STREQ(dsg.description.ptr(), ""); - ASSERT_EQ(dsg.tag, 's'); + ASSERT_TRUE(dsg.tag.hasValue()); + ASSERT_EQ(dsg.tag.get(), 's'); ASSERT_TRUE(dsg.display_pos.hasValue()); ASSERT_EQ(dsg.display_pos.get().x, -1.5f); ASSERT_EQ(dsg.display_pos.get().y, 7.8f); @@ -205,7 +207,8 @@ TEST_F(IndigoCoreFormatsTest, smiles_data_sgroups_short) ASSERT_EQ(dsg.data.size(), 0); ASSERT_EQ(dsg.queryoper.size(), 0); ASSERT_EQ(dsg.description.size(), 0); - ASSERT_EQ(dsg.tag, ' '); + ASSERT_TRUE(dsg.tag.hasValue()); + ASSERT_EQ(dsg.tag.get(), ' '); ASSERT_FALSE(dsg.display_pos.hasValue()); ASSERT_EQ(dsg.atoms.size(), 3); ASSERT_EQ(dsg.atoms.at(0), 1); @@ -234,7 +237,8 @@ TEST_F(IndigoCoreFormatsTest, smiles_pol_sgroups_conn_and_flip) ASSERT_EQ(ru.atoms.at(1), 1); ASSERT_EQ(ru.atoms.at(2), 2); ASSERT_EQ(ru.atoms.at(3), 4); - ASSERT_EQ(ru.connectivity, RepeatingUnit::HEAD_TO_HEAD); + ASSERT_TRUE(ru.connectivity.hasValue()); + ASSERT_EQ(ru.connectivity.get(), RepeatingUnit::HEAD_TO_HEAD); Array out; ArrayOutput std_out(out); SmilesSaver saver(std_out); @@ -429,6 +433,100 @@ M END EXPECT_LT(child_pos, grandchild_pos); } +TEST_F(IndigoCoreFormatsTest, mol_saver_sgroups_roots_before_children) +{ + Molecule mol; + + const char* molfile = R"(Nested SGroups + Indigo 052826 + + 0 0 0 0 0 999 V3000 +M V30 BEGIN CTAB +M V30 COUNTS 3 2 3 0 0 +M V30 BEGIN ATOM +M V30 1 C 0 0 0 0 +M V30 2 C 1 0 0 0 +M V30 3 C 2 0 0 0 +M V30 END ATOM +M V30 BEGIN BOND +M V30 1 1 1 2 +M V30 2 1 2 3 +M V30 END BOND +M V30 BEGIN SGROUP +M V30 1 GEN 0 ATOMS=(1 1) +M V30 3 GEN 0 ATOMS=(1 3) PARENT=1 +M V30 2 GEN 0 ATOMS=(1 2) +M V30 END SGROUP +M V30 END CTAB +M END +)"; + + BufferScanner scanner(molfile); + MolfileLoader loader(scanner); + loader.loadMolecule(mol); + + Array out; + ArrayOutput std_out(out); + MolfileSaver saver(std_out); + saver.mode = MolfileSaver::MODE_3000; + saver.skip_date = true; + saver.saveMolecule(mol); + + std::string saved{out.ptr(), static_cast(out.size())}; + const auto first_root_pos = saved.find("M V30 1 GEN 1 ATOMS=(1 1)"); + const auto second_root_pos = saved.find("M V30 2 GEN 2 ATOMS=(1 2)"); + const auto child_pos = saved.find("M V30 3 GEN 3 ATOMS=(1 3) PARENT=1"); + + ASSERT_NE(std::string::npos, first_root_pos) << saved; + ASSERT_NE(std::string::npos, second_root_pos) << saved; + ASSERT_NE(std::string::npos, child_pos) << saved; + EXPECT_LT(first_root_pos, second_root_pos); + EXPECT_LT(second_root_pos, child_pos); +} + +TEST_F(IndigoCoreFormatsTest, mol_loader_sgroups_parent_cycle) +{ + Molecule mol; + + const char* molfile = R"(Nested SGroups + Indigo 052826 + + 0 0 0 0 0 999 V3000 +M V30 BEGIN CTAB +M V30 COUNTS 2 1 2 0 0 +M V30 BEGIN ATOM +M V30 1 C 0 0 0 0 +M V30 2 C 1 0 0 0 +M V30 END ATOM +M V30 BEGIN BOND +M V30 1 1 1 2 +M V30 END BOND +M V30 BEGIN SGROUP +M V30 1 GEN 0 ATOMS=(1 1) PARENT=2 +M V30 2 GEN 0 ATOMS=(1 2) PARENT=1 +M V30 END SGROUP +M V30 END CTAB +M END +)"; + + BufferScanner scanner(molfile); + MolfileLoader loader(scanner); + EXPECT_THROW(loader.loadMolecule(mol), Exception); +} + +TEST_F(IndigoCoreFormatsTest, mol_sgroups_parent_idx_cycle) +{ + Molecule mol; + + int first_idx = mol.sgroups.addSGroup(SGroup::SG_TYPE_GEN); + int second_idx = mol.sgroups.addSGroup(SGroup::SG_TYPE_GEN); + + mol.sgroups.getSGroup(first_idx).parent_idx = second_idx; + mol.sgroups.getSGroup(second_idx).parent_idx = first_idx; + + EXPECT_THROW(mol.sgroups.getOrderedSGroups(), Exception); +} + TEST_F(IndigoCoreFormatsTest, smarts_load_save) { QueryMolecule q_mol; diff --git a/core/render2d/src/render_internal.cpp b/core/render2d/src/render_internal.cpp index c07424ffc0..39323bcaf8 100644 --- a/core/render2d/src/render_internal.cpp +++ b/core/render2d/src/render_internal.cpp @@ -564,9 +564,10 @@ void MoleculeRenderInternal::_initSGroups(Tree& sgroups, Rect2f parent) Sgroup& sg = _data.sgroups.push(); int tii = _pushTextItem(sg, RenderItem::RIT_DATASGROUP); TextItem& ti = _data.textitems[tii]; - if (group.tag != ' ') + const char tag = group.tag.hasValue() ? group.tag.get() : 0; + if (tag != 0 && tag != ' ') { - ti.text.push(group.tag.get()); + ti.text.push(tag); ti.text.appendString(" = ", false); } @@ -625,12 +626,13 @@ void MoleculeRenderInternal::_initSGroups(Tree& sgroups, Rect2f parent) index.fontsize = FONT_SIZE_ATTR; bprintf(index.text, group.label.size() > 0 ? group.label.ptr() : "n"); _positionIndex(sg, tiIndex, true); - if (group.connectivity != RepeatingUnit::HEAD_TO_TAIL) + const int connectivity = group.connectivity.hasValue() ? group.connectivity.get() : RepeatingUnit::HEAD_TO_TAIL; + if (connectivity != RepeatingUnit::HEAD_TO_TAIL) { int tiConn = _pushTextItem(sg, RenderItem::RIT_SGROUP); TextItem& conn = _data.textitems[tiConn]; conn.fontsize = FONT_SIZE_ATTR; - if (group.connectivity == RepeatingUnit::HEAD_TO_HEAD) + if (connectivity == RepeatingUnit::HEAD_TO_HEAD) { bprintf(conn.text, "hh"); } @@ -658,7 +660,8 @@ void MoleculeRenderInternal::_initSGroups(Tree& sgroups, Rect2f parent) int tiIndex = _pushTextItem(sg, RenderItem::RIT_SGROUP); TextItem& index = _data.textitems[tiIndex]; index.fontsize = FONT_SIZE_ATTR; - bprintf(index.text, "%d", group.multiplier.get()); + const int multiplier = group.multiplier.hasValue() ? group.multiplier.get() : 0; + bprintf(index.text, "%d", multiplier); _positionIndex(sg, tiIndex, true); parent = ILLEGAL_RECT(); } @@ -833,7 +836,7 @@ void MoleculeRenderInternal::_prepareSGroups(bool collapseAtLeastOneSuperatom) if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) { const Superatom& group = (Superatom&)sgroup; - Vec3f displayPosition = group.display_position.get(); + Vec3f displayPosition = group.display_position.hasValue() ? group.display_position.get() : Vec3f(0, 0, 0); bool useDisplayPosition = false; if (fabs(displayPosition.x) > EPSILON || fabs(displayPosition.y) > EPSILON || fabs(displayPosition.z) > EPSILON) { From e0a66ba8d68d05e100a2ce071eff608f9edd797f Mon Sep 17 00:00:00 2001 From: Roman Porozhnetov Date: Thu, 4 Jun 2026 05:50:49 +0400 Subject: [PATCH 31/33] Refine SGroup ordering serialization --- core/indigo-core/molecule/cml_saver.h | 2 +- core/indigo-core/molecule/src/cml_saver.cpp | 75 +++++++++++++------ .../molecule/src/molfile_loader_v3000.cpp | 35 +-------- core/indigo-core/tests/tests/formats.cpp | 49 ++++++++++-- 4 files changed, 98 insertions(+), 63 deletions(-) diff --git a/core/indigo-core/molecule/cml_saver.h b/core/indigo-core/molecule/cml_saver.h index f450da164f..ae13337096 100644 --- a/core/indigo-core/molecule/cml_saver.h +++ b/core/indigo-core/molecule/cml_saver.h @@ -52,7 +52,7 @@ namespace indigo void _validate(BaseMolecule& bmol); void _addMoleculeElement(tinyxml2::XMLElement* elem, BaseMolecule& mol, bool query); - void _addSgroupElement(tinyxml2::XMLElement* elem, const SGroupInfo& info, const std::vector& sgroup_infos); + tinyxml2::XMLElement* _addSgroupElement(tinyxml2::XMLElement* elem, const SGroupInfo& info); void _addRgroups(tinyxml2::XMLElement* elem, BaseMolecule& mol, bool query); void _addRgroupElement(tinyxml2::XMLElement* elem, RGroup& rgroup, bool query); diff --git a/core/indigo-core/molecule/src/cml_saver.cpp b/core/indigo-core/molecule/src/cml_saver.cpp index 433e2e45b1..9bc865ec35 100644 --- a/core/indigo-core/molecule/src/cml_saver.cpp +++ b/core/indigo-core/molecule/src/cml_saver.cpp @@ -19,6 +19,7 @@ #include "molecule/cml_saver.h" #include +#include #include #include "base_cpp/locale_guard.h" @@ -566,15 +567,62 @@ void CmlSaver::_addMoleculeElement(XMLElement* elem, BaseMolecule& mol, bool que if (_mol->countSGroups() > 0) { auto sgroup_infos = _mol->sgroups.getOrderedSGroups(); - for (const auto& info : sgroup_infos) + const int sgroup_count = static_cast(sgroup_infos.size()); + std::vector info_index_by_new_index(sgroup_count + 1, -1); + std::vector root_info_indexes; + std::vector> children_by_info_index(sgroup_count); + + // CML stores nested SGroups as nested molecule elements, so build the + // parent-child links once and emit the tree iteratively. + for (int info_index = 0; info_index < sgroup_count; info_index++) { - if (info.new_parent_index == 0) - _addSgroupElement(molecule, info, sgroup_infos); + const int new_index = sgroup_infos[info_index].new_index; + if (new_index > 0 && new_index <= sgroup_count) + info_index_by_new_index[new_index] = info_index; + } + + for (int info_index = 0; info_index < sgroup_count; info_index++) + { + const int new_parent_index = sgroup_infos[info_index].new_parent_index; + if (new_parent_index == 0) + { + root_info_indexes.push_back(info_index); + continue; + } + + int parent_info_index = -1; + if (new_parent_index > 0 && new_parent_index <= sgroup_count) + parent_info_index = info_index_by_new_index[new_parent_index]; + if (parent_info_index < 0) + throw Error("internal error: unresolved SGroup parent index: %d", new_parent_index); + + children_by_info_index[parent_info_index].push_back(info_index); + } + + struct SGroupFrame + { + XMLElement* parent; + int info_index; + }; + std::vector stack; + stack.reserve(sgroup_count); + for (int i = static_cast(root_info_indexes.size()) - 1; i >= 0; i--) + stack.push_back({molecule, root_info_indexes[i]}); + + while (!stack.empty()) + { + SGroupFrame frame = stack.back(); + stack.pop_back(); + + XMLElement* sgroup_element = _addSgroupElement(frame.parent, sgroup_infos[frame.info_index]); + const auto& children = children_by_info_index[frame.info_index]; + for (auto it = children.rbegin(); it != children.rend(); ++it) + stack.push_back({sgroup_element, *it}); } } } -void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, const std::vector& sgroup_infos) +XMLElement* CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info) { SGroup& sgroup = info.sgroup; XMLElement* sg = _doc->NewElement("molecule"); @@ -586,14 +634,6 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, c buf.push(0); sg->SetAttribute("id", buf.ptr()); - auto addChildren = [&]() { - for (const auto& child_info : sgroup_infos) - { - if (child_info.new_parent_index == info.new_index) - _addSgroupElement(sg, child_info, sgroup_infos); - } - }; - if (sgroup.atoms.size() > 0) { QS_DEF(Array, sbuf); @@ -698,14 +738,10 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, c { sg->SetAttribute("fieldData", dsg.data.ptr()); } - - addChildren(); } else if (sgroup.sgroup_type == SGroup::SG_TYPE_GEN) { sg->SetAttribute("role", "GenericSgroup"); - - addChildren(); } else if (sgroup.sgroup_type == SGroup::SG_TYPE_SUP) { @@ -718,8 +754,6 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, c { sg->SetAttribute("title", name); } - - addChildren(); } else if (sgroup.sgroup_type == SGroup::SG_TYPE_SRU) { @@ -742,8 +776,6 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, c { sg->SetAttribute("connect", "hh"); } - - addChildren(); } else if (sgroup.sgroup_type == SGroup::SG_TYPE_MUL) { @@ -770,9 +802,8 @@ void CmlSaver::_addSgroupElement(XMLElement* molecule, const SGroupInfo& info, c sg->SetAttribute("patoms", pbuf.ptr()); } - - addChildren(); } + return sg; } void CmlSaver::_addRgroups(XMLElement* elem, BaseMolecule& mol, bool query) diff --git a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp index 718c32625c..33b89a1c10 100644 --- a/core/indigo-core/molecule/src/molfile_loader_v3000.cpp +++ b/core/indigo-core/molecule/src/molfile_loader_v3000.cpp @@ -17,8 +17,6 @@ ***************************************************************************/ #include -#include -#include #include "../layout/molecule_layout.h" #include "base_cpp/output.h" @@ -882,7 +880,7 @@ void MolfileLoader::_fillSGroupsParentIndices() } else { - throw Error("SGroup parent hierarchy contains a cycle"); + sgroup.parent_idx = -1; } } else @@ -890,37 +888,6 @@ void MolfileLoader::_fillSGroupsParentIndices() sgroup.parent_idx = -1; } } - - std::unordered_map state; - state.reserve(sgroups.getSGroupCount()); - for (auto i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) - state.emplace(i, 0); - - for (auto i = sgroups.begin(); i != sgroups.end(); i = sgroups.next(i)) - { - std::vector chain; - int current = i; - - while (true) - { - auto state_it = state.find(current); - if (state_it == state.end() || state_it->second == 2) - break; - if (state_it->second == 1) - throw Error("SGroup parent hierarchy contains a cycle"); - - state_it->second = 1; - chain.push_back(current); - - SGroup& sgroup = sgroups.getSGroup(current); - if (!sgroup.parent_idx.hasValue() || sgroup.parent_idx.get() < 0) - break; - current = sgroup.parent_idx.get(); - } - - for (int chain_idx : chain) - state[chain_idx] = 2; - } } void MolfileLoader::_readCollectionBlock3000() diff --git a/core/indigo-core/tests/tests/formats.cpp b/core/indigo-core/tests/tests/formats.cpp index 4b49f7a992..7e8c6101c9 100644 --- a/core/indigo-core/tests/tests/formats.cpp +++ b/core/indigo-core/tests/tests/formats.cpp @@ -484,26 +484,50 @@ M END EXPECT_LT(second_root_pos, child_pos); } -TEST_F(IndigoCoreFormatsTest, mol_loader_sgroups_parent_cycle) +TEST_F(IndigoCoreFormatsTest, mol_sgroups_order_by_depth_across_hierarchies) { Molecule mol; const char* molfile = R"(Nested SGroups - Indigo 052826 + Indigo 060326 0 0 0 0 0 999 V3000 M V30 BEGIN CTAB -M V30 COUNTS 2 1 2 0 0 +M V30 COUNTS 10 9 10 0 0 M V30 BEGIN ATOM M V30 1 C 0 0 0 0 M V30 2 C 1 0 0 0 +M V30 3 C 2 0 0 0 +M V30 4 C 3 0 0 0 +M V30 5 C 4 0 0 0 +M V30 6 C 5 0 0 0 +M V30 7 C 6 0 0 0 +M V30 8 C 7 0 0 0 +M V30 9 C 8 0 0 0 +M V30 10 C 9 0 0 0 M V30 END ATOM M V30 BEGIN BOND M V30 1 1 1 2 +M V30 2 1 2 3 +M V30 3 1 3 4 +M V30 4 1 4 5 +M V30 5 1 5 6 +M V30 6 1 6 7 +M V30 7 1 7 8 +M V30 8 1 8 9 +M V30 9 1 9 10 M V30 END BOND M V30 BEGIN SGROUP -M V30 1 GEN 0 ATOMS=(1 1) PARENT=2 -M V30 2 GEN 0 ATOMS=(1 2) PARENT=1 +M V30 13 GEN 0 ATOMS=(1 4) PARENT=12 +M V30 20 GEN 0 ATOMS=(1 5) +M V30 11 GEN 0 ATOMS=(1 2) PARENT=10 +M V30 30 GEN 0 ATOMS=(1 7) +M V30 10 GEN 0 ATOMS=(1 1) +M V30 32 GEN 0 ATOMS=(1 9) PARENT=31 +M V30 21 GEN 0 ATOMS=(1 6) PARENT=20 +M V30 31 GEN 0 ATOMS=(1 8) PARENT=30 +M V30 40 GEN 0 ATOMS=(1 10) +M V30 12 GEN 0 ATOMS=(1 3) PARENT=11 M V30 END SGROUP M V30 END CTAB M END @@ -511,7 +535,20 @@ M END BufferScanner scanner(molfile); MolfileLoader loader(scanner); - EXPECT_THROW(loader.loadMolecule(mol), Exception); + loader.loadMolecule(mol); + + auto infos = mol.sgroups.getOrderedSGroups(); + + const std::vector expected_original_indexes = {20, 30, 10, 40, 11, 21, 31, 32, 12, 13}; + const std::vector expected_new_parent_indexes = {0, 0, 0, 0, 3, 1, 2, 7, 5, 9}; + ASSERT_EQ(infos.size(), expected_original_indexes.size()); + + for (int i = 0; i < static_cast(infos.size()); i++) + { + EXPECT_EQ(infos[i].sgroup.index, expected_original_indexes[i]); + EXPECT_EQ(infos[i].new_index, i + 1); + EXPECT_EQ(infos[i].new_parent_index, expected_new_parent_indexes[i]); + } } TEST_F(IndigoCoreFormatsTest, mol_sgroups_parent_idx_cycle) From 7cb31d3285190a9265d665d483ee03b03b2c4182 Mon Sep 17 00:00:00 2001 From: Roman Porozhnetov Date: Thu, 4 Jun 2026 06:06:43 +0400 Subject: [PATCH 32/33] Format CML saver includes --- core/indigo-core/molecule/src/cml_saver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/indigo-core/molecule/src/cml_saver.cpp b/core/indigo-core/molecule/src/cml_saver.cpp index 9bc865ec35..8d131847ae 100644 --- a/core/indigo-core/molecule/src/cml_saver.cpp +++ b/core/indigo-core/molecule/src/cml_saver.cpp @@ -19,8 +19,8 @@ #include "molecule/cml_saver.h" #include -#include #include +#include #include "base_cpp/locale_guard.h" #include "base_cpp/output.h" From 1a8d9e2e78fc6e099c18c6ebb3201837d3505144 Mon Sep 17 00:00:00 2001 From: Roman Porozhnetov Date: Thu, 4 Jun 2026 13:39:27 +0400 Subject: [PATCH 33/33] Fix Nullable copy semantics --- .github/workflows/indigo-ci.yaml | 2 +- core/indigo-core/common/base_cpp/nullable.h | 70 ++++++++++++++++++- .../molecule/src/base_molecule.cpp | 32 +++------ 3 files changed, 80 insertions(+), 24 deletions(-) diff --git a/.github/workflows/indigo-ci.yaml b/.github/workflows/indigo-ci.yaml index b3ac8729bb..973b7ef5d7 100644 --- a/.github/workflows/indigo-ci.yaml +++ b/.github/workflows/indigo-ci.yaml @@ -956,7 +956,7 @@ jobs: build_indigo_utils_x86_64: strategy: fail-fast: false - matrix: ${{ fromJSON(needs.set_matrix.outputs.matrix || '{"os":["ubuntu-latest","windows-latest"]}') }} + matrix: ${{ fromJSON(needs.set_matrix.outputs.matrix) }} runs-on: ${{ matrix.os }} needs: [build_bingo_postgres_linux_x86_64, build_bingo_postgres_windows_msvc_x86_64, build_bingo_postgres_macos_x86_64, set_matrix] if: ${{ always() && (needs.build_bingo_postgres_macos_x86_64.result == 'success' || needs.build_bingo_postgres_macos_x86_64.result == 'skipped') }} diff --git a/core/indigo-core/common/base_cpp/nullable.h b/core/indigo-core/common/base_cpp/nullable.h index d1b9085770..ddfd264486 100644 --- a/core/indigo-core/common/base_cpp/nullable.h +++ b/core/indigo-core/common/base_cpp/nullable.h @@ -19,6 +19,8 @@ #ifndef __nullable__ #define __nullable__ +#include + #include "base_cpp/array.h" #include "base_cpp/exception.h" @@ -36,6 +38,48 @@ namespace indigo variable_name.readString("", true); } + Nullable(const Nullable& other) : _has_value(false) + { + variable_name.copy(other.variable_name); + if (other._has_value) + set(other._value); + } + + Nullable(Nullable&& other) : _has_value(false) + { + variable_name.copy(other.variable_name); + if (other._has_value) + { + _moveValue(_value, std::move(other._value)); + _has_value = true; + } + } + + Nullable& operator=(const Nullable& other) + { + if (this == &other) + return *this; + if (other._has_value) + set(other._value); + else + reset(); + return *this; + } + + Nullable& operator=(Nullable&& other) + { + if (this == &other) + return *this; + if (other._has_value) + { + _moveValue(_value, std::move(other._value)); + _has_value = true; + } + else + reset(); + return *this; + } + const T& get() const { if (!_has_value) @@ -56,7 +100,7 @@ namespace indigo void set(const T& value) { - _value = value; + _copyValue(_value, value); _has_value = true; } @@ -78,6 +122,30 @@ namespace indigo DECL_TPL_ERROR(NullableError); private: + template + static void _copyValue(U& dst, const U& src) + { + dst = src; + } + + template + static void _copyValue(Array& dst, const Array& src) + { + dst.copy(src); + } + + template + static void _moveValue(U& dst, U&& src) + { + dst = std::move(src); + } + + template + static void _moveValue(Array& dst, Array&& src) + { + dst.swap(src); + } + T _value; bool _has_value; Array variable_name; diff --git a/core/indigo-core/molecule/src/base_molecule.cpp b/core/indigo-core/molecule/src/base_molecule.cpp index 6c5995ea45..dce32749e2 100644 --- a/core/indigo-core/molecule/src/base_molecule.cpp +++ b/core/indigo-core/molecule/src/base_molecule.cpp @@ -47,18 +47,6 @@ using namespace indigo; IMPL_ERROR(BaseMolecule, "molecule"); -namespace -{ - template - void copyNullable(Nullable& dst, const Nullable& src) - { - if (src.hasValue()) - dst = src.get(); - else - dst.reset(); - } -} // namespace - BaseMolecule::BaseMolecule() : original_format(BaseMolecule::UNKNOWN), _edit_revision(0) { } @@ -187,10 +175,10 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma SGroup& supersg = mol.sgroups.getSGroup(i); int idx = sgroups.addSGroup(supersg.sgroup_type); SGroup& sg = sgroups.getSGroup(idx); - copyNullable(sg.parent_idx, supersg.parent_idx); + sg.parent_idx = supersg.parent_idx; sg.index = supersg.index; sg.ext_index = supersg.ext_index; - copyNullable(sg.parent_group, supersg.parent_group); + sg.parent_group = supersg.parent_group; sg.label.copy(supersg.label); if (_mergeSGroupWithSubmolecule(sg, supersg, mol, mapping, edge_mapping)) @@ -204,10 +192,10 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma DataSGroup& superdg = (DataSGroup&)supersg; dg.detached = superdg.detached; - copyNullable(dg.display_pos, superdg.display_pos); + dg.display_pos = superdg.display_pos; dg.data.copy(superdg.data); dg.sa_natreplace.copy(superdg.sa_natreplace); - copyNullable(dg.dasp_pos, superdg.dasp_pos); + dg.dasp_pos = superdg.dasp_pos; dg.relative = superdg.relative; dg.display_units = superdg.display_units; dg.description.copy(superdg.description); @@ -215,8 +203,8 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma dg.type.copy(superdg.type); dg.querycode.copy(superdg.querycode); dg.queryoper.copy(superdg.queryoper); - copyNullable(dg.num_chars, superdg.num_chars); - copyNullable(dg.tag, superdg.tag); + dg.num_chars = superdg.num_chars; + dg.tag = superdg.tag; } else if (sg.sgroup_type == SGroup::SG_TYPE_SUP) { @@ -237,7 +225,7 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma } sa.sa_class.copy(supersa.sa_class); sa.sa_natreplace.copy(supersa.sa_natreplace); - copyNullable(sa.contracted, supersa.contracted); + sa.contracted = supersa.contracted; if (supersa.attachment_points.size() > 0) { for (int j = supersa.attachment_points.begin(); j < supersa.attachment_points.end(); j = supersa.attachment_points.next(j)) @@ -258,21 +246,21 @@ void BaseMolecule::mergeSGroupsWithSubmolecule(BaseMolecule& mol, Array& ma ap.apid.copy(supersa.attachment_points[j].apid); } } - copyNullable(sa.display_position, supersa.display_position); + sa.display_position = supersa.display_position; } else if (sg.sgroup_type == SGroup::SG_TYPE_SRU) { RepeatingUnit& ru = (RepeatingUnit&)sg; RepeatingUnit& superru = (RepeatingUnit&)supersg; - copyNullable(ru.connectivity, superru.connectivity); + ru.connectivity = superru.connectivity; } else if (sg.sgroup_type == SGroup::SG_TYPE_MUL) { MultipleGroup& mg = (MultipleGroup&)sg; MultipleGroup& supermg = (MultipleGroup&)supersg; - copyNullable(mg.multiplier, supermg.multiplier); + mg.multiplier = supermg.multiplier; for (int j = 0; j != supermg.parent_atoms.size(); j++) if (mapping[supermg.parent_atoms[j]] >= 0) mg.parent_atoms.push(mapping[supermg.parent_atoms[j]]);