smiles-js 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +162 -0
- package/README.md +39 -0
- package/docs/MIRROR_PLAN.md +204 -0
- package/package.json +1 -1
- package/scripts/coverage-summary.js +1 -1
- package/src/codegen/branch-crossing-ring.js +27 -6
- package/src/codegen/interleaved-fused-ring.js +24 -0
- package/src/decompiler.js +27 -2
- package/src/manipulation.js +409 -4
- package/src/manipulation.test.js +359 -1
- package/src/method-attachers.js +29 -0
- package/src/node-creators.js +7 -0
- package/src/parser/ast-builder.js +23 -8
- package/src/parser/ring-group-builder.js +14 -2
- package/src/parser/ring-utils.js +28 -0
- package/test-integration/__snapshots__/adjuvant-analgesics.test.js.snap +1 -1
- package/test-integration/__snapshots__/cholesterol-drugs.test.js.snap +176 -0
- package/test-integration/__snapshots__/endocannabinoids.test.js.snap +2 -2
- package/test-integration/__snapshots__/hypertension-medication.test.js.snap +1 -1
- package/test-integration/__snapshots__/nsaids-otc.test.js.snap +1 -1
- package/test-integration/__snapshots__/nsaids-prescription.test.js.snap +2 -2
- package/test-integration/__snapshots__/opioids.test.js.snap +4 -4
- package/test-integration/__snapshots__/steroids.test.js.snap +2 -2
- package/test-integration/cholesterol-drugs.test.js +41 -0
- package/test-integration/cholesterol.test.js +112 -0
- package/test-integration/mirror.test.js +151 -0
- package/test-integration/polymer.test.js +148 -0
- package/todo +0 -2
package/src/manipulation.js
CHANGED
|
@@ -12,8 +12,13 @@ import {
|
|
|
12
12
|
cloneSubstitutions,
|
|
13
13
|
cloneComponents,
|
|
14
14
|
deepCloneRing,
|
|
15
|
+
deepCloneLinear,
|
|
16
|
+
deepCloneFusedRing,
|
|
17
|
+
deepCloneMolecule,
|
|
15
18
|
} from './constructors.js';
|
|
16
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
validatePosition, isLinearNode, isMoleculeNode, isRingNode, isFusedRingNode,
|
|
21
|
+
} from './ast.js';
|
|
17
22
|
import { computeFusedRingPositions } from './layout/index.js';
|
|
18
23
|
|
|
19
24
|
/**
|
|
@@ -29,10 +34,16 @@ export function ringAttach(ring, position, attachment, options = {}) {
|
|
|
29
34
|
updatedAttachments[position] = [];
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
// Clone the attachment and set metaIsSibling if provided in options
|
|
37
|
+
// Clone the attachment and set metaIsSibling/metaBeforeInline if provided in options
|
|
33
38
|
let attachmentToAdd = attachment;
|
|
34
|
-
if (options.sibling !== undefined) {
|
|
35
|
-
attachmentToAdd = { ...attachment
|
|
39
|
+
if (options.sibling !== undefined || options.beforeInline !== undefined) {
|
|
40
|
+
attachmentToAdd = { ...attachment };
|
|
41
|
+
if (options.sibling !== undefined) {
|
|
42
|
+
attachmentToAdd.metaIsSibling = options.sibling;
|
|
43
|
+
}
|
|
44
|
+
if (options.beforeInline !== undefined) {
|
|
45
|
+
attachmentToAdd.metaBeforeInline = options.beforeInline;
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
updatedAttachments[position] = [...updatedAttachments[position], attachmentToAdd];
|
|
@@ -627,3 +638,397 @@ export function moleculeReplaceComponent(molecule, index, newComponent) {
|
|
|
627
638
|
newComponents[index] = newComponent;
|
|
628
639
|
return createMoleculeNode(newComponents);
|
|
629
640
|
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Generic repeat / polymer methods
|
|
644
|
+
*/
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Deep-clone any AST node
|
|
648
|
+
*/
|
|
649
|
+
function cloneNode(node) {
|
|
650
|
+
if (isRingNode(node)) return deepCloneRing(node);
|
|
651
|
+
if (isLinearNode(node)) return deepCloneLinear(node);
|
|
652
|
+
if (isFusedRingNode(node)) return deepCloneFusedRing(node);
|
|
653
|
+
if (isMoleculeNode(node)) return deepCloneMolecule(node);
|
|
654
|
+
throw new Error(`Unknown node type: ${node?.type}`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Find the maximum ring number used anywhere in a node tree.
|
|
659
|
+
* This is needed to renumber ring copies so ring markers don't collide.
|
|
660
|
+
*/
|
|
661
|
+
function maxRingNumber(node) {
|
|
662
|
+
if (isRingNode(node)) {
|
|
663
|
+
let max = node.ringNumber || 0;
|
|
664
|
+
Object.values(node.attachments || {}).forEach((list) => {
|
|
665
|
+
list.forEach((att) => { max = Math.max(max, maxRingNumber(att)); });
|
|
666
|
+
});
|
|
667
|
+
return max;
|
|
668
|
+
}
|
|
669
|
+
if (isFusedRingNode(node)) {
|
|
670
|
+
let max = 0;
|
|
671
|
+
(node.rings || []).forEach((r) => { max = Math.max(max, r.ringNumber || 0); });
|
|
672
|
+
return max;
|
|
673
|
+
}
|
|
674
|
+
if (isMoleculeNode(node)) {
|
|
675
|
+
let max = 0;
|
|
676
|
+
(node.components || []).forEach((c) => { max = Math.max(max, maxRingNumber(c)); });
|
|
677
|
+
return max;
|
|
678
|
+
}
|
|
679
|
+
if (isLinearNode(node)) {
|
|
680
|
+
let max = 0;
|
|
681
|
+
Object.values(node.attachments || {}).forEach((list) => {
|
|
682
|
+
list.forEach((att) => { max = Math.max(max, maxRingNumber(att)); });
|
|
683
|
+
});
|
|
684
|
+
return max;
|
|
685
|
+
}
|
|
686
|
+
return 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Shift all ring numbers in a node by a given delta.
|
|
691
|
+
* Returns a new node (immutable).
|
|
692
|
+
*/
|
|
693
|
+
function shiftRingNumbers(node, delta) {
|
|
694
|
+
if (delta === 0) return node;
|
|
695
|
+
|
|
696
|
+
if (isRingNode(node)) {
|
|
697
|
+
const newAttachments = {};
|
|
698
|
+
Object.entries(node.attachments || {}).forEach(([pos, list]) => {
|
|
699
|
+
newAttachments[pos] = list.map((att) => shiftRingNumbers(att, delta));
|
|
700
|
+
});
|
|
701
|
+
return createRingNode(
|
|
702
|
+
node.atoms,
|
|
703
|
+
node.size,
|
|
704
|
+
node.ringNumber + delta,
|
|
705
|
+
node.offset,
|
|
706
|
+
node.substitutions,
|
|
707
|
+
newAttachments,
|
|
708
|
+
node.bonds || [],
|
|
709
|
+
node.metaBranchDepths || null,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (isFusedRingNode(node)) {
|
|
714
|
+
const newRings = (node.rings || []).map((r) => ({
|
|
715
|
+
...r,
|
|
716
|
+
ringNumber: (r.ringNumber || 1) + delta,
|
|
717
|
+
}));
|
|
718
|
+
return createFusedRingNode(newRings);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (isMoleculeNode(node)) {
|
|
722
|
+
const newComponents = (node.components || []).map((c) => shiftRingNumbers(c, delta));
|
|
723
|
+
return createMoleculeNode(newComponents);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (isLinearNode(node)) {
|
|
727
|
+
const newAttachments = {};
|
|
728
|
+
Object.entries(node.attachments || {}).forEach(([pos, list]) => {
|
|
729
|
+
newAttachments[pos] = list.map((att) => shiftRingNumbers(att, delta));
|
|
730
|
+
});
|
|
731
|
+
return createLinearNode(node.atoms, node.bonds, newAttachments, node.metaLeadingBond);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return node;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Repeat a monomer unit n times, creating a polymer chain.
|
|
739
|
+
*
|
|
740
|
+
* leftId and rightId are 1-indexed positions defining the attachment points.
|
|
741
|
+
* For Linear nodes, leftId=1 and rightId=atoms.length are the natural endpoints.
|
|
742
|
+
* For Ring nodes, leftId=1 and rightId=size are the natural SMILES endpoints
|
|
743
|
+
* (the first and last atoms emitted in the ring's SMILES).
|
|
744
|
+
*
|
|
745
|
+
* The resulting Molecule concatenates n copies. In SMILES, concatenation
|
|
746
|
+
* bonds the last atom of component i to the first atom of component i+1.
|
|
747
|
+
*
|
|
748
|
+
* @param {Object} node - Any AST node (Ring, Linear, FusedRing, Molecule)
|
|
749
|
+
* @param {number} n - Number of repeating units (>= 1)
|
|
750
|
+
* @param {number} leftId - 1-indexed left (incoming) attachment point
|
|
751
|
+
* @param {number} rightId - 1-indexed right (outgoing) attachment point
|
|
752
|
+
* @returns {Object} Molecule node (or clone for n=1)
|
|
753
|
+
*/
|
|
754
|
+
export function repeat(node, n, leftId, rightId) {
|
|
755
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
756
|
+
throw new Error('Repeat count n must be an integer >= 1');
|
|
757
|
+
}
|
|
758
|
+
if (!Number.isInteger(leftId) || leftId < 1) {
|
|
759
|
+
throw new Error('leftId must be a positive integer');
|
|
760
|
+
}
|
|
761
|
+
if (!Number.isInteger(rightId) || rightId < 1) {
|
|
762
|
+
throw new Error('rightId must be a positive integer');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (n === 1) {
|
|
766
|
+
return cloneNode(node);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// For simple Linear nodes (no attachments), efficiently concatenate atoms arrays
|
|
770
|
+
const hasAttachments = isLinearNode(node) && Object.keys(node.attachments || {}).length > 0;
|
|
771
|
+
if (isLinearNode(node) && !hasAttachments && leftId === 1 && rightId === node.atoms.length) {
|
|
772
|
+
let atoms = [];
|
|
773
|
+
let bonds = [];
|
|
774
|
+
for (let i = 0; i < n; i += 1) {
|
|
775
|
+
atoms = [...atoms, ...node.atoms];
|
|
776
|
+
if (node.bonds.length > 0) {
|
|
777
|
+
bonds = [...bonds, ...node.bonds];
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return createLinearNode(atoms, bonds, {});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const maxRing = maxRingNumber(node);
|
|
784
|
+
const copies = [];
|
|
785
|
+
for (let i = 0; i < n; i += 1) {
|
|
786
|
+
const delta = i * maxRing;
|
|
787
|
+
const copy = delta > 0 ? shiftRingNumbers(cloneNode(node), delta) : cloneNode(node);
|
|
788
|
+
copies.push(copy);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return createMoleculeNode(copies);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Repeat a ring n times by fusing, creating an acene-like system.
|
|
796
|
+
*
|
|
797
|
+
* Each new ring shares `offset` atoms with the previous ring,
|
|
798
|
+
* producing linear fused ring systems (naphthalene, anthracene, etc.).
|
|
799
|
+
*
|
|
800
|
+
* @param {Object} ring - Ring AST node
|
|
801
|
+
* @param {number} n - Total number of rings (>= 1)
|
|
802
|
+
* @param {number} offset - Fusion offset (shared atoms between adjacent rings)
|
|
803
|
+
* @returns {Object} Ring (n=1) or FusedRing (n>=2) node
|
|
804
|
+
*/
|
|
805
|
+
export function fusedRepeat(ring, n, offset) {
|
|
806
|
+
if (!isRingNode(ring)) {
|
|
807
|
+
throw new Error('fusedRepeat requires a Ring node');
|
|
808
|
+
}
|
|
809
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
810
|
+
throw new Error('Repeat count n must be an integer >= 1');
|
|
811
|
+
}
|
|
812
|
+
if (!Number.isInteger(offset) || offset < 1) {
|
|
813
|
+
throw new Error('Fusion offset must be a positive integer');
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (n === 1) {
|
|
817
|
+
return deepCloneRing(ring);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// First fusion: ring1.fuse(offset, ring2)
|
|
821
|
+
const ring1 = createRingNode(
|
|
822
|
+
ring.atoms,
|
|
823
|
+
ring.size,
|
|
824
|
+
1,
|
|
825
|
+
0,
|
|
826
|
+
ring.substitutions,
|
|
827
|
+
ring.attachments,
|
|
828
|
+
ring.bonds || [],
|
|
829
|
+
ring.metaBranchDepths || null,
|
|
830
|
+
);
|
|
831
|
+
const ring2 = createRingNode(
|
|
832
|
+
ring.atoms,
|
|
833
|
+
ring.size,
|
|
834
|
+
2,
|
|
835
|
+
offset,
|
|
836
|
+
ring.substitutions,
|
|
837
|
+
{},
|
|
838
|
+
ring.bonds || [],
|
|
839
|
+
null,
|
|
840
|
+
);
|
|
841
|
+
let result = createFusedRingNode([ring1, ring2]);
|
|
842
|
+
|
|
843
|
+
// Additional fusions
|
|
844
|
+
for (let i = 2; i < n; i += 1) {
|
|
845
|
+
const nextRing = createRingNode(
|
|
846
|
+
ring.atoms,
|
|
847
|
+
ring.size,
|
|
848
|
+
i + 1,
|
|
849
|
+
offset,
|
|
850
|
+
ring.substitutions,
|
|
851
|
+
{},
|
|
852
|
+
ring.bonds || [],
|
|
853
|
+
null,
|
|
854
|
+
);
|
|
855
|
+
result = fusedRingAddRing(result, offset + i * (ring.size - offset), nextRing);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Mirror / symmetry methods
|
|
863
|
+
*/
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Mirror a linear chain around a pivot atom to create a palindromic structure.
|
|
867
|
+
*
|
|
868
|
+
* Takes atoms [0..pivotId-1] (left half including pivot), then appends
|
|
869
|
+
* atoms [pivotId-2..0] in reverse. The pivot atom appears once in the center.
|
|
870
|
+
* Bonds and attachments are mirrored accordingly.
|
|
871
|
+
*
|
|
872
|
+
* @param {Object} linear - Linear AST node
|
|
873
|
+
* @param {number} [pivotId] - 1-indexed pivot position (default: atoms.length)
|
|
874
|
+
* @returns {Object} New Linear or Molecule node
|
|
875
|
+
*/
|
|
876
|
+
export function linearMirror(linear, pivotId) {
|
|
877
|
+
const pivot = pivotId !== undefined ? pivotId : linear.atoms.length;
|
|
878
|
+
if (!Number.isInteger(pivot) || pivot < 1 || pivot > linear.atoms.length) {
|
|
879
|
+
throw new Error(
|
|
880
|
+
`pivotId must be an integer between 1 and ${linear.atoms.length}`,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Left half: atoms [0..pivot-1] (includes pivot)
|
|
885
|
+
const leftAtoms = linear.atoms.slice(0, pivot);
|
|
886
|
+
// Right half: atoms [0..pivot-2] reversed (excludes pivot)
|
|
887
|
+
const rightAtoms = linear.atoms.slice(0, pivot - 1).reverse();
|
|
888
|
+
|
|
889
|
+
const newAtoms = [...leftAtoms, ...rightAtoms];
|
|
890
|
+
|
|
891
|
+
// Mirror bonds
|
|
892
|
+
// In main-chain format, bonds[i] is the bond between atoms[i] and atoms[i+1]
|
|
893
|
+
// Pad bonds to full length (pivot-1 entries for the left half) so mirror is correct
|
|
894
|
+
const paddedLeftBonds = Array.from(
|
|
895
|
+
{ length: pivot - 1 },
|
|
896
|
+
(_, i) => linear.bonds[i] || null,
|
|
897
|
+
);
|
|
898
|
+
const rightBonds = paddedLeftBonds.slice(0, pivot - 1).reverse();
|
|
899
|
+
const newBonds = [...paddedLeftBonds, ...rightBonds];
|
|
900
|
+
|
|
901
|
+
// Mirror attachments
|
|
902
|
+
const leftAttachments = linear.attachments || {};
|
|
903
|
+
const hasAtt = Object.keys(leftAttachments).length > 0;
|
|
904
|
+
|
|
905
|
+
if (!hasAtt) {
|
|
906
|
+
return createLinearNode(newAtoms, newBonds, {});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Attachments need ring renumbering for the mirrored half
|
|
910
|
+
const totalMaxRing = maxRingNumber(linear);
|
|
911
|
+
const newAttachments = cloneAttachments(leftAttachments);
|
|
912
|
+
|
|
913
|
+
// Mirror each attachment position: position p mirrors to (2*pivot - p)
|
|
914
|
+
Object.entries(leftAttachments).forEach(([posStr, attList]) => {
|
|
915
|
+
const pos = Number(posStr);
|
|
916
|
+
if (pos === pivot) return; // pivot attachments stay, no mirror
|
|
917
|
+
const mirrorPos = 2 * pivot - pos;
|
|
918
|
+
if (mirrorPos < 1 || mirrorPos > newAtoms.length) return;
|
|
919
|
+
if (!newAttachments[mirrorPos]) {
|
|
920
|
+
newAttachments[mirrorPos] = [];
|
|
921
|
+
}
|
|
922
|
+
attList.forEach((att) => {
|
|
923
|
+
const shifted = totalMaxRing > 0
|
|
924
|
+
? shiftRingNumbers(cloneNode(att), totalMaxRing)
|
|
925
|
+
: cloneNode(att);
|
|
926
|
+
newAttachments[mirrorPos] = [...newAttachments[mirrorPos], shifted];
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
return createLinearNode(newAtoms, newBonds, newAttachments);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Mirror a molecule's component sequence to create an ABA-like structure.
|
|
935
|
+
*
|
|
936
|
+
* Takes components [0..pivot], then appends components [pivot-1..0] in reverse
|
|
937
|
+
* with ring numbers shifted to avoid collisions.
|
|
938
|
+
*
|
|
939
|
+
* @param {Object} molecule - Molecule AST node
|
|
940
|
+
* @param {number} [pivotComponent] - 0-indexed pivot component (default: last)
|
|
941
|
+
* @returns {Object} New Molecule node
|
|
942
|
+
*/
|
|
943
|
+
export function moleculeMirror(molecule, pivotComponent) {
|
|
944
|
+
const pivot = pivotComponent !== undefined
|
|
945
|
+
? pivotComponent
|
|
946
|
+
: molecule.components.length - 1;
|
|
947
|
+
if (!Number.isInteger(pivot) || pivot < 0 || pivot >= molecule.components.length) {
|
|
948
|
+
throw new Error(
|
|
949
|
+
`pivotComponent must be an integer between 0 and ${molecule.components.length - 1}`,
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Left half: components [0..pivot] (includes pivot)
|
|
954
|
+
const leftComponents = molecule.components.slice(0, pivot + 1);
|
|
955
|
+
// Right half: components [0..pivot-1] reversed (excludes pivot)
|
|
956
|
+
const rightSources = molecule.components.slice(0, pivot).reverse();
|
|
957
|
+
|
|
958
|
+
// Shift ring numbers in mirrored components
|
|
959
|
+
let totalMaxRing = 0;
|
|
960
|
+
leftComponents.forEach((c) => {
|
|
961
|
+
totalMaxRing = Math.max(totalMaxRing, maxRingNumber(c));
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
const rightComponents = rightSources.map((c) => {
|
|
965
|
+
const cloned = cloneNode(c);
|
|
966
|
+
return totalMaxRing > 0 ? shiftRingNumbers(cloned, totalMaxRing) : cloned;
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
return createMoleculeNode([...leftComponents, ...rightComponents]);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Mirror a ring's attachments and substitutions to create symmetric patterns.
|
|
974
|
+
*
|
|
975
|
+
* For each attachment/substitution at position p, adds a copy at the mirror
|
|
976
|
+
* position relative to the pivot. The mirror position for p around pivot v
|
|
977
|
+
* on a ring of size s is: ((2*v - p - 1) mod s) + 1 (1-indexed).
|
|
978
|
+
*
|
|
979
|
+
* @param {Object} ring - Ring AST node
|
|
980
|
+
* @param {number} [pivotId=1] - 1-indexed pivot position
|
|
981
|
+
* @returns {Object} New Ring node
|
|
982
|
+
*/
|
|
983
|
+
export function ringMirror(ring, pivotId) {
|
|
984
|
+
const pivot = pivotId !== undefined ? pivotId : 1;
|
|
985
|
+
validatePosition(pivot, ring.size);
|
|
986
|
+
|
|
987
|
+
const { size } = ring;
|
|
988
|
+
|
|
989
|
+
// Helper: compute mirror position (1-indexed)
|
|
990
|
+
// For a ring, position p mirrors around pivot v as:
|
|
991
|
+
// mirror = ((2*v - p - 1) mod s) + 1
|
|
992
|
+
// This reflects p across v on the ring.
|
|
993
|
+
const mirrorPos = (p) => ((((2 * (pivot - 1) - (p - 1)) % size) + size) % size) + 1;
|
|
994
|
+
|
|
995
|
+
// Mirror substitutions
|
|
996
|
+
const newSubstitutions = { ...ring.substitutions };
|
|
997
|
+
Object.entries(ring.substitutions).forEach(([posStr, atom]) => {
|
|
998
|
+
const pos = Number(posStr);
|
|
999
|
+
const mp = mirrorPos(pos);
|
|
1000
|
+
if (mp !== pos && !newSubstitutions[mp]) {
|
|
1001
|
+
newSubstitutions[mp] = atom;
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// Mirror attachments
|
|
1006
|
+
const newAttachments = cloneAttachments(ring.attachments);
|
|
1007
|
+
const totalMaxRing = maxRingNumber(ring);
|
|
1008
|
+
|
|
1009
|
+
Object.entries(ring.attachments).forEach(([posStr, attList]) => {
|
|
1010
|
+
const pos = Number(posStr);
|
|
1011
|
+
const mp = mirrorPos(pos);
|
|
1012
|
+
if (mp === pos) return; // pivot position or self-symmetric, skip
|
|
1013
|
+
if (!newAttachments[mp]) {
|
|
1014
|
+
newAttachments[mp] = [];
|
|
1015
|
+
}
|
|
1016
|
+
attList.forEach((att) => {
|
|
1017
|
+
const shifted = totalMaxRing > 0
|
|
1018
|
+
? shiftRingNumbers(cloneNode(att), totalMaxRing)
|
|
1019
|
+
: cloneNode(att);
|
|
1020
|
+
newAttachments[mp] = [...newAttachments[mp], shifted];
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
return createRingNode(
|
|
1025
|
+
ring.atoms,
|
|
1026
|
+
ring.size,
|
|
1027
|
+
ring.ringNumber,
|
|
1028
|
+
ring.offset,
|
|
1029
|
+
newSubstitutions,
|
|
1030
|
+
newAttachments,
|
|
1031
|
+
ring.bonds || [],
|
|
1032
|
+
ring.metaBranchDepths || null,
|
|
1033
|
+
);
|
|
1034
|
+
}
|