[Proposal] Add a high-level declarative syntax for diagrams - #20
[Proposal] Add a high-level declarative syntax for diagrams#20anko9801 wants to merge 43 commits into
Conversation
|
I'll take time to look at it more in-depth. It seems like awesome work. Thanks ! Also, maybe skeletize should be able to directly detect a single string instead of doing |
…re for molecule-ring tests
…r outgoing direction
|
Doesn't the typed-smiles package already exist? I feel like I usually use alchemist for better control over the angles, so maybe a chemfig-like syntax would be better. |
|
Just found the molchemist package that supports SMILES and uses alchemist under the hood. For fine control,
From the molchemist documentation. |
b4479c9 to
3f64e10
Compare
Replace the recursion-limited pure-Typst molecule parser with a declarative engine under src/elements/chem/. A DSL/SMILES string is parsed by a Rust engine (engine/ -> chem.wasm), laid out by CoordgenLibs (coordgen/ -> coordgen.wasm), then drawn by render.typ with IUPAC GR-2008 orientation, labels, stereo and the GR feature set. One public entry point, chem(), returns stand-alone content; reaction()/rxn-arrow()/curly-arrow() build schemes; electron-pushing arrows are addressed by atom id via chem's `arrows` option. `molecule` stays the hand-drawn fragment alias. DSL ring bodies list vertices with inferred bonds — @6 benzene, @6(N) pyridine, @6(C(-CH3)CCCCC) toluene — auto-aromatised via a graph-level kekulize shared with the SMILES front-end; explicit bonds override (@6(------) cyclohexane). The build sources live next to the chem Typst code and are excluded from the package. Assisted-by: Claude Code (model: Opus 4.8)
Five visual-regression galleries covering the engine pipeline (chem-parse, chem-layout, chem-render, chem-compose, chem-gallery), alongside the hand-drawn fragment/link tests (cetz-skeleton-anchors, resonance, molecules-*) kept intact now that `molecule` aliases `fragment`. Assisted-by: Claude Code (model: Opus 4.8)
Add a Part III "Molecule engine" chapter — chem(), the DSL (incl. inferred-bond rings), SMILES, stereochemistry, render options, electron-pushing arrows, reactions and formula — plus a command reference. Assisted-by: Claude Code (model: Opus 4.8)
There was a problem hiding this comment.
Why are you building a new renderer? I don't think it's a good idea. If you miss any features on the current renderer, I can add them if you don't have time/don't want to do it. This duplicates the logic and as far as I can see is not compatible with the current renderer
I just talked with the author of this module, and apparently, the smiles support of molchemist was only there as someone requested it (rice8y/molchemist#1). If I understood correctly, the author is quite ok with making this a part of alchemist instead of keeping it in the molchemist module. |
Yeah, I think this is the right approach. I'm just very slowly working on the smiles implementation but it's going to have inchi support as well as some other smaller file types. I was thinking of making a new renderer for it, since it's going to output more coordinate-based information. I even started working on it, but I kind of want to keep alchemist as the go to solution. I feel kind of guilty that my smiles implementation is taking so long. There are already packages trying to fill the vacuum, creating more fragmentation. Even though typed-smiles is useful, it'd be cooler if there was just one package & styling system to use for everything. The diagrams do look very different and it would be hard to use both in the same document. |
|
Hi everyone, I’m the author of molchemist. molchemist was originally created for rendering Molfile/SDF data. SMILES support began as experimental work in response to rice8y/molchemist#1, and v0.1.2 was the first release to include it. I’m therefore positive about consolidating the overlapping SMILES functionality into Alchemist rather than maintaining multiple partially overlapping implementations. I checked out the current PR head ( For the SMILES path specifically, both implementations use CoordgenLibs for 2D layout, but their parser and intermediate-representation boundaries differ. molchemist uses a vendored copy of If the current shared graph and layout/depiction pipeline are intended to remain, one option would be to evaluate Regarding rendering, I share Robotechnic’s concern about duplicating behavior that already exists in At the same time, I understand Ants-Aare’s point that coordinate-based output may benefit from a different rendering layer. I do not yet have a strong conclusion about whether reusing |
|
I have to say that the current rendering engine fully supports coordinate based rendering as the current way it works is by first placing all the fragments and anchors and then drawing the links between them. The api is just not exposed. We can discuss it a bit but I can provide a more computer friendly api that takes fragments, their coordinates and the links between them to do the rendering. It's just connecting the right piece together. My main concern with a separate renderer is that if it's not fully compatible with the current one, you can't really mix up the two ways of rendering a molecule and the cetz anchors won't be the same if you want to integrate a molecule in cetz. This would be more confusing for the end user. https://github.com/Typsium/alchemist/blob/master/src%2Fdrawer.typ#L434 This call is the place where all the links are drawn and it's just a matter of giving the right context to this function |
|
Thanks — and you're right that those pieces are addable. I think we actually converge here. The way I see it, the renderer's job is "draw a molecule whose atoms already have coordinates" — that part I want to share, not fork. The hard part is upstream: parsing + IUPAC 2D layout (fused/bridged/macrocycles), which has no counterpart in the current renderer to improve. It's a coordinate solver (CoordgenLibs, same as molchemist) + orientation/stereo, and it has to run as a compiled WASM plugin, not in Typst itself — the earlier pure-Typst version here hit Typst's recursion limit and had to fake rings with invisible bonds. So the WASM engine isn't a reimplementation of your renderer; it's the missing layer. And what I'd need from you is exactly what you offered. Reading In return I'd drop @rice8y — following your suggestion, I'll use opensmiles as the SMILES frontend in this PR. @Ants-Aare — I don't want to step on your InChI/Molfile work; it can feed the same graph + renderer, and I'm happy to coordinate however you prefer. So PR would be DSL + opensmiles → engine → that coordinate entry, render.typ's core removed and anchors/config unified. Want to design the entry's signature together? |
Replace the dedicated coordinate renderer (`render.typ`) with a pipeline that drives alchemist's own drawing, so molecules share one rendering path — and therefore identical CeTZ anchors, spacing and styling — with hand-drawn `skeletize` molecules instead of duplicating the logic: - `skeleton.typ`: place each engine atom as a native fragment at its absolute coordinate, then draw bonds turtle-free through the existing `draw-link-decoration`. Reuses link/lewis/fragment drawing; one bond-length unit maps to `atom-sep` (resolved to canvas units via `convert-length` so the manual geometry stays float-valued), keeping engine and hand-drawn molecules at the same scale. - `decorations.typ`: thin overlay for the non-bond GR features that have no counterpart in alchemist (ionic dots, brackets, aromatic circles, curly arrows, partial charges, hapto/Markush, delocalized/bent/wavy bonds). - `labels.typ`: shared GR label/colour helpers (subscripts, isotope/charge/ oxidation superscripts, Jmol CPK colours). - styling now flows from alchemist's `default` config (single styling system). `draw-chem` is exposed alongside `chem` so several molecules, reaction arrows and cross-molecule electron-pushing arrows compose in one shared canvas. Assisted-by: Claude Code (model: claude-opus-4-8)
The alchemist-primitive rendering and atom-sep scaling change the output, so refresh the golden images for the chem test suite. Assisted-by: Claude Code (model: claude-opus-4-8)
Improve how the coordinate renderer draws bonds against atom labels: - Connect each bond to the heavy atom itself: a plain label like "CH2" is split into element groups with the element symbol on the atom coordinate (GR-2.1.5), so the bond meets the "C", not the centre of the whole box. - Trim the bond length at each labelled end to the measured label box plus a uniform margin, instead of masking with a white box/ellipse. Both lines of a double bond then come out equal, every end is a clean perpendicular cap, and no fill is painted over the drawing. - Draw acyclic double bonds symmetric (ChemDraw default); ring double bonds keep their inner line offset toward the centroid. Rebuilds the wasm engine. Reference renders regenerated. Assisted-by: Claude Code (model: claude-opus-4-8)
Reorganize the coordinate renderer around standard structure and fix a batch of depiction issues. Design: - style.typ: single source for every tunable weight/ratio (no more magic numbers inline); geometry.typ: the shared label-clearance trim used by both bonds and overlays. - chem-defaults: one config resolved by both skeleton and decorations (previously two divergent default sets). - responsibilities: skeleton draws every bond (normal, special styles and aromatic-circle singles); decorations only draws non-bond overlays. One shared bond stroke, so nothing looks thicker or thinner than the bonds. - derive rather than hardcode: the delocalized inner line reuses alchemist's double.gap; the aromatic circle is sized from the ring apothem (engine now emits the inscribed radius). Rendering: - labels are typeset in math mode, so subscripts, isotope/charge superscripts and the ± signs get real math typography. - fused ring systems (naphthalene, indole, ...) rest flat and horizontal; benzene and cyclohexane share one orientation. - hapto / variable-attachment lines are trimmed to clear the atom label. - acyclic double bonds are symmetric; bonds connect to the heavy atom and stop a uniform gap short of each label. Reference renders regenerated; wasm engine rebuilt. Assisted-by: Claude Code (model: claude-opus-4-8)
A behavior-preserving pass over the coordinate engine and renderer, cutting the chem module from ~3960 to ~3500 lines while fixing two chemistry bugs. Engine: - coordgen now emits the rings it perceives (cg_plugin.cpp), so the Rust SSSR reimplementation is gone — ring perception lives in one place. - SMILES parsing is rewritten on the `winnow` combinator library; the DSL keeps its hand parser (its context-sensitive grammar is a poor fit). - valence logic (bond_order_sum / implicit_h / charge-adjust) consolidated onto Graph; layout geometry shares centroid/unit/angle/rotate helpers; ring_edges / lay_ring_path / try_label / skip_while remove parser dupes; Node derives Default; output::build splits into build_atoms/bonds/rings. Fixes: - molecular formula no longer double-counts hydrogens (benzene C6H6, not C6H12). - pyrrole/indole-type rings kekulize correctly (implicit H seeded before matching + most-constrained-first greedy), giving the right Kekulé. Reference renders reflect the two rendering fixes; everything else is unchanged (tt stays green without updating refs). Assisted-by: Claude Code (model: claude-opus-4-8)
|
I added a way to place fragments at arbitrary positions. I did it in a way that is fully compatible with the current turtle api (so it's possible to use it with parentheses and operators). I'm adding the same for links (placing arbitrary links between two elements) but it's not ready yet. If you want to can have a look. For instance:
|
Cross-links (`fragment(links: ...)`) are what connects two fragments placed at absolute coordinates, so they need the same control turtle links have: - honour `from`/`to` on the link element instead of always letting the angle pick a side of the fragment, as `drawer/link.typ` already does; - compute the link angle even when both indices are pinned, which used to panic with `dictionary does not contain key "angle"`, and measure it between the two connected sub-atoms so the link points exactly from one atom to the other; - keep the name a user gives a cross-link (`link.at(mol-name)` looked up the molecule name as a key, so the name was always overwritten). Also speeds the link machinery up: coordinates that are already plain numbers skip cetz's resolver, the connected sub-atom centres are resolved once and reused for both the angle and the ellipse anchors, an unnamed link no longer gets a cetz node it could not be referenced by (`over` still gets one), and the identity anchor transform of an untranslated group is skipped.
The renderer no longer draws bonds and labels itself. `skeleton.typ` now turns the engine's LayoutOut into native alchemist elements — one `place`d fragment per atom, centred on its element symbol, and one cross-link per bond, pinned to the element symbol on both ends — and hands them to `draw-skeleton`. Fragment text, lone pairs, label margins, link styles, cetz anchors and the configuration therefore all come from alchemist itself, so a generated molecule is indistinguishable from a hand-drawn one and the two can be mixed in one drawing. The GR bond styles that have no counterpart (delocalized, bent, wavy) are ordinary `build-link` links, and the duplicated `dative` definition is dropped in favour of the one in `elements/links.typ`. Reference images change by a fraction of a percent of their pixels, at the bond ends only: the clearance around a label is now alchemist's fragment margin rather than a box trim of our own.
`molecule` was removed upstream and nothing uses it any more. The Makefile gains the `plugin`, `plugin-test` and `coordgen` targets that build the two wasm plugins, and loses `gen-templates`, whose script no longer exists.
This pull request introduces a proposal for a new
moleculemodule, intended to offer a more declarative and concise way to write chemical diagrams.The goal is to provide a syntax that is even more intuitive and concise than established tools like ChemFig, allowing the natural way of writing a structure to yield an IUPAC preferred diagram.
This module is designed to integrate with core Alchemist functions, enabling a powerful hybrid workflow. While core Alchemist provides ultimate flexibility through direct cetz integration, this new module, at the cost of that flexibility, offers exceptional conciseness for the most common use cases.
Proposed Features
#molecule("CH3-CH2-OH")) and parse it into a diagram.:labelfor points,::labelfor lines, and alabel: "..."argument) is included to allow for the creation of more complex structures with non-sequential bonds or mechanism arrows.Example Usage
What's implemented
CH3->$C$ $H_3$)NH3+,COO-)^13C,^2H)-=#><:><:|><|)@6(-=-=-(-CH3)=)-CH3)O(lewis: (dots(0), dots(180)))-(angle: 60deg))O:O1=::bond)Refereneces
This is an initial proposal, and I would be very grateful for any feedback, suggestions, or critiques to help improve it. Thank you for your consideration.