From 64f04ade957a0b297cdc9e06d058034100134298 Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Sat, 10 Aug 2024 11:45:51 +0200 Subject: [PATCH 01/24] Upload alchemist v0.1.0 --- packages/preview/alchemist/0.1.0/LICENSE | 22 + packages/preview/alchemist/0.1.0/README.md | 90 ++ packages/preview/alchemist/0.1.0/lib.typ | 137 +++ .../preview/alchemist/0.1.0/src/default.typ | 6 + .../preview/alchemist/0.1.0/src/drawer.typ | 927 ++++++++++++++++++ .../preview/alchemist/0.1.0/src/links.typ | 287 ++++++ .../preview/alchemist/0.1.0/src/molecule.typ | 64 ++ .../preview/alchemist/0.1.0/src/utils.typ | 72 ++ packages/preview/alchemist/0.1.0/typst.toml | 12 + 9 files changed, 1617 insertions(+) create mode 100644 packages/preview/alchemist/0.1.0/LICENSE create mode 100644 packages/preview/alchemist/0.1.0/README.md create mode 100644 packages/preview/alchemist/0.1.0/lib.typ create mode 100644 packages/preview/alchemist/0.1.0/src/default.typ create mode 100644 packages/preview/alchemist/0.1.0/src/drawer.typ create mode 100644 packages/preview/alchemist/0.1.0/src/links.typ create mode 100644 packages/preview/alchemist/0.1.0/src/molecule.typ create mode 100644 packages/preview/alchemist/0.1.0/src/utils.typ create mode 100644 packages/preview/alchemist/0.1.0/typst.toml diff --git a/packages/preview/alchemist/0.1.0/LICENSE b/packages/preview/alchemist/0.1.0/LICENSE new file mode 100644 index 0000000000..25dc239757 --- /dev/null +++ b/packages/preview/alchemist/0.1.0/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.0/README.md b/packages/preview/alchemist/0.1.0/README.md new file mode 100644 index 0000000000..04d571daa4 --- /dev/null +++ b/packages/preview/alchemist/0.1.0/README.md @@ -0,0 +1,90 @@ +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + molecule(name: "A", "A") + single() + molecule("B") + branch({ + single(angle: 1) + molecule( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + molecule(name: "X", "X") + }) + branch({ + single(angle: -1) + molecule("Y") + single() + molecule( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + molecule( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Robotechnic/alchemist/master/images/links1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + molecule("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Robotechnic/alchemist/master/images/cetz1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.1.0": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). + +## Changelog + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.1.0/lib.typ b/packages/preview/alchemist/0.1.0/lib.typ new file mode 100644 index 0000000000..20afc9290a --- /dev/null +++ b/packages/preview/alchemist/0.1.0/lib.typ @@ -0,0 +1,137 @@ +#import "@preview/cetz:0.2.2" +#import "src/default.typ": default +#import "src/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ" : skeletize +#import "src/links.typ" : * +#import "src/molecule.typ" : * + +#let transparent = color.rgb(100%,0,0,0) + +/// === Molecule function +/// Build a molecule group based on mol +/// Each molecule is represented as an optional count followed by a molecule name +/// starting by a capital letter followed by an optional indice +/// #example(``` +/// #skeletize({ +/// molecule("H_2O") +/// }) +///```) +/// It is possible to use an equation as a molecule. In this case, the spliting of the equation uses the same rules as in the string case. However, you can use parenthesis to group elements together. +/// #example(``` +/// #skeletize({ +/// molecule($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the molecule. It is used as the cetz name of the molecule and to link other molecules to it. +/// - links (dictionary): The links between this molecule and previous molecules or hooks. The key is the name of the molecule or hook and the value is the link function. +/// +/// Note that the antom-sep and angle arguments are ignored +/// - mol (string, equation): The string representing the molecule or an equation of the molecule +/// - vertical (boolean): If true, the molecule is drawn vertically +/// #example(``` +/// #skeletize({ +/// molecule("ABCD", vertical: true) +/// }) +///```) +/// -> drawable +#let molecule(name: none, links: (:), vertical: false, mol) = { + let atoms = if type(mol) == str { + split-string(mol) + } else if mol.func() == math.equation { + split-equation(mol, equation: true) + } else { + panic("Invalid molecule content") + } + ( + ( + type: "molecule", + name: name, + atoms: atoms, + links: links, + vertical: vertical, + count: atoms.len(), + ), + ) +} + +/// === Hooks +/// Create a hook in the molecule. It allows tu connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the molecule. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current molecule, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// branch({ +/// single(angle:1) +/// molecule("B") +/// }) +/// branch({ +/// double(angle: -1) +/// molecule("D") +/// }) +/// single() +/// double() +/// single() +/// molecule("C") +/// }) +///```) +#let branch(..args) = { + if args.pos().len() != 1 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", draw: args.pos().at(0), args: args.named()),) +} + +/// Create a regular cycle of molecules +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +#let cycle(..args) = { + if args.pos().len() != 2 { + panic("Cycle takes two positional arguments: number of faces and body") + } + let faces = args.pos().at(0) + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + draw: args.pos().at(1), + args: args.named(), + ), + ) +} diff --git a/packages/preview/alchemist/0.1.0/src/default.typ b/packages/preview/alchemist/0.1.0/src/default.typ new file mode 100644 index 0000000000..81fea0ac60 --- /dev/null +++ b/packages/preview/alchemist/0.1.0/src/default.typ @@ -0,0 +1,6 @@ +#let default = ( + atom-sep: 3em, + delta: 0.2em, + angle-increment: 45deg, + base-angle: 0deg, +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.0/src/drawer.typ b/packages/preview/alchemist/0.1.0/src/drawer.typ new file mode 100644 index 0000000000..6fd35078bc --- /dev/null +++ b/packages/preview/alchemist/0.1.0/src/drawer.typ @@ -0,0 +1,927 @@ +#import "default.typ": default +#import "@preview/cetz:0.2.2" +#import "utils.typ" +#import cetz.draw + +#let default-anchor = (type: "coord", anchor: (0, 0)) +#let max-int = 9223372036854775807 + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + group-id: 0, // id of the current group + link-id: 0, // id of the current link + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + + // branch + first-branch: false, // true if the next element is the first in a branch + + // cycle + first-molecule: none, // name of the first molecule in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices +) + +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" { + ctx.links.push(ctx.last-anchor) + } + (..ctx, last-anchor: anchor) +} + +#let link-molecule-index(angle, end, count, vertical) = { + if not end { + if vertical and utils.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if utils.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and utils.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if utils.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let molecule-link-anchor(name, id, count) = { + if count <= id { + panic("The last molecule only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "center")) +} + +#let link-molecule-anchor(name: none, id, count) = { + if id >= count { + panic("This molecule only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the molecule to link to must be defined") + } + if name == none { + (name: str(id), anchor: "center") + } else { + (name: name, anchor: (str(id), "center")) + } +} + +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +/// Draw a triangle between two molecules +#let cram(from, to, ctx, args) = { + import cetz.draw: * + + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let base-length = utils.convert-length( + ctx, + args.at("base-length", default: .8em), + ) + line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: none), + fill: args.at("fill", default: black), + ) +} + +/// Draw a dashed triangle between two molecules +#let dashed-cram(from, to, length, ctx, args) = { + import cetz.draw: * + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let base-length = utils.convert-length( + ctx, + args.at("base-length", default: .8em), + ) + hide({ + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - 0.05)) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + 0.05), + ) + }) + let stroke = args.at("stroke", default: black + .05em) + let dash-gap = utils.convert-length(ctx, args.at("dash-gap", default: .3em)) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(ctx, dash-width) + let length = utils.convert-length(ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} + +#let draw-molecule-text(mol) = { + for (id, eq) in mol.atoms.enumerate() { + let name = str(id) + // draw atoms of the group one after the other from left to right + draw.content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "west" + }, + ( + if id == 0 { + (0, 0) + } else if mol.vertical { + (to: str(id - 1) + ".south", rel: (0, -.2em)) + } else { + str(id - 1) + ".east" + } + ), + { + show math.equation: math.upright + eq + }, + ) + id += 1 + } +} + +#let draw-molecule(mol, ctx) = { + let name = mol.name + if name != none { + if name in ctx.hooks { + panic("Molecule with name " + name + " already exists") + } + ctx.hooks.insert(name, mol) + } else { + name = "molecule" + str(ctx.group-id) + } + let (anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("east", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-molecule-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let anchor = link-molecule-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule must be linked to a coord or a link") + } + ctx = set-last-anchor( + ctx, + (type: "molecule", name: name, count: mol.at("count"), vertical: mol.vertical), + ) + ctx.group-id += 1 + ( + ctx, + { + draw.group( + anchor: if side { + anchor + } else { + "from" + str(ctx.group-id) + }, + name: name, + { + draw.set-origin(coord) + draw.anchor("default", (0, 0)) + draw-molecule-text(mol) + if not side { + draw.anchor("from" + str(ctx.group-id), anchor) + } + }, + ) + }, + ) +} + +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} + +#let draw-last-cycle-link(link, ctx) = { + let from-name = none + let from-pos = none + if ctx.last-anchor.type == "molecule" { + from-name = ctx.last-anchor.name + from-pos = (name: from-name, anchor: "center") + if from-name not in ctx.hooks { + ctx.hooks.insert(from-name, ctx.last-anchor) + } + } else if ctx.last-anchor.type == "link" { + from-pos = ctx.last-anchor.name + "-end-anchor" + } else { + panic("A cycle link must be linked to a molecule or a link") + } + ctx.links.push(( + type: "link", + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: from-pos, + from-name: from-name, + to-name: ctx.first-molecule, + from: link.at("from", default: none), + to: link.at("to", default: none), + override: (offset: "left"), + draw: link.draw, + )) + ctx.link-id += 1 + (ctx, ()) +} + +#let draw-link(link, ctx) = { + let link-angle = 0deg + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-molecule != none { + return draw-last-cycle-link(link, ctx) + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = utils.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let from-pos = if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "molecule" { + from-connection = link-molecule-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + molecule-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name", default: "link" + str(ctx.link-id)) + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = set-last-anchor( + ctx, + ( + type: "link", + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: none, + to: to-connection, + angle: link-angle, + draw: link.draw, + ), + ) + ctx.link-id += 1 + ( + ctx, + { + let anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + draw.line(from-pos, anchor, stroke: blue + .1em) + } + draw.group( + name: link-name + "-end-anchor", + { + draw.anchor("default", anchor) + }, + ) + }, + ) +} + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + 0, + )) + } + vertex +} + +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + draw.circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius) +} + +#let draw-cycle-center-arc(ctx, name, arc) = { + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + draw.get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius) = cycle-center-radius(ctx, cetz-ctx, vertex) + if name != none { + draw.group( + name: name, + { + draw.anchor("default", center) + }, + ) + } + if arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= arc.at("radius", default: 0.7) + } else { + min-radius *= arc.at("radius", default: 0.5) + } + let start = arc.at("start", default: 0deg) + let end = arc.at("end", default: 360deg) + let delta = arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + draw.arc( + center, + ..arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) + +} + +#let draw-hooks-links(links, name, ctx, from-mol) = { + for (to-name, (link,)) in links { + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "molecule" { + ctx.links.push(( + type: "link", + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: if from-mol { + (name: name, anchor: "center") + } else { + name + "-end-anchor" + }, + from-name: if from-mol { + name + }, + to-name: to-name, + from: none, + to: none, + override: angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: if from-mol { + (name: name, anchor: "center") + } else { + name + "-end-anchor" + }, + from-name: if from-mol { + name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + ctx.link-id += 1 + } + ctx +} + +#let update-parent-context(parent-ctx, ctx) = { + ( + ..parent-ctx, + hooks: parent-ctx.hooks + ctx.hooks, + hooks-links: parent-ctx.hooks-links + ctx.hooks-links, + links: parent-ctx.links + ctx.links, + group-id: ctx.group-id, + link-id: ctx.link-id, + ) +} + +#let draw-molecules-and-link(ctx, body) = { + let drawing = () + let cetz-drawing = () + ( + { + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + if type(element) == function { + cetz-drawing.push(element) + } else if "type" not in element { + panic("Element " + str(element) + " has no type") + } else if element.type == "molecule" { + if ctx.first-branch { + panic("A molecule can not be the first element in a cycle") + } + (ctx, drawing) = draw-molecule(element, ctx) + drawing + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + } else if element.type == "link" { + ctx.first-branch = false + (ctx, drawing) = draw-link(element, ctx) + ctx.faces-count += 1 + drawing + if element.links.len() != 0 { + ctx.hooks-links.push((element.links, ctx.last-anchor.name, false)) + } + } else if element.type == "branch" { + let angle = angle-from-ctx(ctx, element.args, cycle-angle(ctx)) + let (drawing, branch-ctx, cetz-rec) = draw-molecules-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + element.draw, + ) + ctx = update-parent-context(ctx, branch-ctx) + cetz-drawing += cetz-rec + drawing + } else if element.type == "cycle" { + let cycle-step-angle = 360deg / element.faces + let angle = angle-from-ctx(ctx, element.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ctx.relative-angle == 0deg and ctx.angle == 0deg and not element.args.at( + "align", + default: false, + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-molecule = none + if ctx.last-anchor.type == "molecule" { + first-molecule = ctx.last-anchor.name + if first-molecule not in ctx.hooks { + ctx.hooks.insert(first-molecule, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in element.args { + name = element.args.at("name") + record-vertex = true + } else if "arc" in element.args { + record-vertex = true + } + let (drawing, cycle-ctx, cetz-rec) = draw-molecules-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: element.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-molecule: first-molecule, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + element.draw, + ) + ctx = update-parent-context(ctx, cycle-ctx) + cetz-drawing += cetz-rec + drawing + if record-vertex { + draw-cycle-center-arc(cycle-ctx, name, element.args.at("arc", default: none)) + } + } else if element.type == "hook" { + if element.name in ctx.hooks { + panic("Hook " + element.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert(element.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(element.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + } else { + panic("Unknown element type " + element.type) + } + } + if ctx.last-anchor.type == "link" { + ctx.links.push(ctx.last-anchor) + } + }, + ctx, + cetz-drawing, + ) +} + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +#let molecule-anchor(ctx, cetz-ctx, angle, molecule, id) = { + let delta = utils.convert-length(cetz-ctx, ctx.config.delta) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "center")), + ) + + let (a, b) = if utils.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, delta, molecule, id) + } else if utils.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, delta, molecule, id) + } else if utils.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, delta, molecule, id) + } else { + anchor-south-east(cetz-ctx, center, delta, molecule, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if utils.angle-in-range-inclusive(angle, 0deg, 90deg) or utils.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "center") + if link.to == none or link.from == none { + let angle = utils.angle-between(cetz-ctx, link.from-pos, to-pos) + link.angle = angle + if link.from == none { + link.from = link-molecule-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-molecule-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + let start = molecule-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) + let end = molecule-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) + ((start, end), utils.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = utils.angle-correction( + utils.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "center"), + ), + ) + link.to = link-molecule-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = utils.angle-correction( + utils.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "center")), + ), + ) + } + let end-anchor = molecule-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + utils.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + molecule-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = utils.angle-correction( + utils.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) + let from = link-molecule-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = molecule-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) + ( + ( + start-anchor, + hook.hook, + ), + utils.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + utils.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + +#let draw-link-decoration(ctx) = { + import cetz.draw: * + ( + get-ctx(cetz-ctx => { + for link in ctx.links { + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = utils.distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + group({ + set-origin(from) + rotate(angle) + (link.draw)(length, cetz-ctx, override: link.override) + }) + } + }), + ctx, + ) +} + +#let draw-skeleton(config: default, body) = { + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + let (draw, ctx, cetz-drawing) = draw-molecules-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + let (links, _) = draw-link-decoration(ctx) + { + draw + links + cetz-drawing + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + for (key, value) in default { + if key not in config { + config.insert(key, value) + } + } + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas( + debug: debug, + background: background, + draw-skeleton(config: config, body), + ) +} diff --git a/packages/preview/alchemist/0.1.0/src/links.typ b/packages/preview/alchemist/0.1.0/src/links.typ new file mode 100644 index 0000000000..29acfd3804 --- /dev/null +++ b/packages/preview/alchemist/0.1.0/src/links.typ @@ -0,0 +1,287 @@ +#import "@preview/cetz:0.2.2" +#import "drawer.typ" +#import "utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes three arguments: the length of the link, the context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// single() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// single(stroke: red + 5pt) +/// molecule("B") +/// }) +///```) +#let single = build-link((length, _, args) => { + import cetz.draw: * + line((0, 0), (length, 0), stroke: args.at("stroke", default: black)) +}) + +/// Draw a double line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// molecule("B") +/// }) +///```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double(offset: "right") +/// molecule("B") +/// double(offset: "left") +/// molecule("C") +/// double(offset: "center") +/// molecule("D") +/// }) +///```) +#let double = build-link((length, ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(ctx, args.at("gap", default: .25em)) / 2 + let offset = args.at("offset", default: "center") + let coeff = args.at("offset-coeff", default: 0.85) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + if offset == "right" { + translate((0, -gap)) + } else if offset == "left" { + translate((0, gap)) + } else if offset == "center" { } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + translate((0, -gap)) + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: black), + ) + translate((0, 2 * gap)) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: black), + ) +}) + +/// Draw a triple line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// triple() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// molecule("B") +/// }) +///```) +#let triple = build-link((length, ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(ctx, args.at("gap", default: .25em)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: black)) + translate((0, -gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: black)) + translate((0, 2 * gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: black)) +}) + +/// Draw a filled cram between two molecules with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-right() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, args) => drawer.cram( + (0, 0), + (length, 0), + ctx, + args, +)) + +/// Draw a filled cram between two molecules with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-left() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, args) => drawer.cram( + (length, 0), + (0, 0), + ctx, + args, +)) + +/// Draw a hollow cram between two molecules with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + drawer.cram((0, 0), (length, 0), ctx, args) +}) + +/// Draw a hollow cram between two molecules with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + drawer.cram((length, 0), (0, 0), ctx, args) +}) + +/// Draw a dashed cram between two molecules with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-right() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-dashed-right = build-link((length, ctx, args) => drawer.dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + args, +)) + +/// Draw a dashed cram between two molecules with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-left() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, args) => drawer.dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + args, +)) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.0/src/molecule.typ b/packages/preview/alchemist/0.1.0/src/molecule.typ new file mode 100644 index 0000000000..a2fbc185a9 --- /dev/null +++ b/packages/preview/alchemist/0.1.0/src/molecule.typ @@ -0,0 +1,64 @@ +#let split-equation(mol, equation: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + for m in mol { + let last-number-hold = last-number + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + result.push(m) + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in molecule") + } + last-number = true + result.push(m) + } else { + panic("Invalid molecule content") + } + } else if m.func() == math.attach or m.func() == math.lr { + result.push(m) + } else if m == [ ] { + continue + } else { + panic("Invalid molecule content") + } + if last-number-hold { + result.at(-2) = result.at(-2) + result.at(-1) + let _ = result.pop() + last-number = false + } + } + result +} + +#let split-string(mol) = { + let aux(str) = { + let match = str.match(regex("^ *([0-9]*[A-Z][a-z]*)(_[0-9]+)?")) + if match == none { + panic(str + " is not a valid atom") + } + let eq = "\"" + match.captures.at(0) + "\"" + if match.captures.len() >= 2 { + eq += match.captures.at(1) + } + let eq = math.equation(eval(eq, mode: "math")) + (eq, match.end) + } + + while not mol.len() == 0 { + let (eq, end) = aux(mol) + mol = mol.slice(end) + (eq,) + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.0/src/utils.typ b/packages/preview/alchemist/0.1.0/src/utils.typ new file mode 100644 index 0000000000..10f3e7850b --- /dev/null +++ b/packages/preview/alchemist/0.1.0/src/utils.typ @@ -0,0 +1,72 @@ +#import "@preview/cetz:0.2.2" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + if type(num) == length { + if repr(num).ends-with("em") { + float(repr(num).slice(0, -2)) * ctx.em-size.width / ctx.length + } else { + float(num / ctx.length) + } + } else { + float(num) + } +} + +/// Convert any angle to an angle between -360deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a = a + 360 + } + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.0/typst.toml b/packages/preview/alchemist/0.1.0/typst.toml new file mode 100644 index 0000000000..44331b3e0f --- /dev/null +++ b/packages/preview/alchemist/0.1.0/typst.toml @@ -0,0 +1,12 @@ +[package] +name = "alchemist" +version = "0.1.0" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.11.0" +repository = "https://github.com/Robotechnic/alchemist" +description = "A package to render skeletal formulas using cetz" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization"] +disciplines = ["chemistry", "biology", ] From 8f4ee7c49dddc58ae6d52f32753d5c3d428ab0cd Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Sun, 18 Aug 2024 23:46:36 +0200 Subject: [PATCH 02/24] Upload alchemist v0.1.1 --- packages/preview/alchemist/0.1.1/LICENSE | 22 + packages/preview/alchemist/0.1.1/README.md | 95 ++ packages/preview/alchemist/0.1.1/lib.typ | 137 +++ .../preview/alchemist/0.1.1/src/default.typ | 7 + .../preview/alchemist/0.1.1/src/drawer.typ | 945 ++++++++++++++++++ .../preview/alchemist/0.1.1/src/links.typ | 287 ++++++ .../preview/alchemist/0.1.1/src/molecule.typ | 64 ++ .../preview/alchemist/0.1.1/src/utils.typ | 72 ++ packages/preview/alchemist/0.1.1/typst.toml | 12 + 9 files changed, 1641 insertions(+) create mode 100644 packages/preview/alchemist/0.1.1/LICENSE create mode 100644 packages/preview/alchemist/0.1.1/README.md create mode 100644 packages/preview/alchemist/0.1.1/lib.typ create mode 100644 packages/preview/alchemist/0.1.1/src/default.typ create mode 100644 packages/preview/alchemist/0.1.1/src/drawer.typ create mode 100644 packages/preview/alchemist/0.1.1/src/links.typ create mode 100644 packages/preview/alchemist/0.1.1/src/molecule.typ create mode 100644 packages/preview/alchemist/0.1.1/src/utils.typ create mode 100644 packages/preview/alchemist/0.1.1/typst.toml diff --git a/packages/preview/alchemist/0.1.1/LICENSE b/packages/preview/alchemist/0.1.1/LICENSE new file mode 100644 index 0000000000..25dc239757 --- /dev/null +++ b/packages/preview/alchemist/0.1.1/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.1/README.md b/packages/preview/alchemist/0.1.1/README.md new file mode 100644 index 0000000000..1d457feaed --- /dev/null +++ b/packages/preview/alchemist/0.1.1/README.md @@ -0,0 +1,95 @@ +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + molecule(name: "A", "A") + single() + molecule("B") + branch({ + single(angle: 1) + molecule( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + molecule(name: "X", "X") + }) + branch({ + single(angle: -1) + molecule("Y") + single() + molecule( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + molecule( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Robotechnic/alchemist/master/images/links1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + molecule("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Robotechnic/alchemist/master/images/cetz1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.1.1": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). + +## Changelog + +### 0.1.1 + +- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. +- Fixed multiples bugs that causes overdraws of links. + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.1.1/lib.typ b/packages/preview/alchemist/0.1.1/lib.typ new file mode 100644 index 0000000000..f72c3c8c3c --- /dev/null +++ b/packages/preview/alchemist/0.1.1/lib.typ @@ -0,0 +1,137 @@ +#import "@preview/cetz:0.2.2" +#import "src/default.typ": default +#import "src/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ" : skeletize, draw-skeleton +#import "src/links.typ" : * +#import "src/molecule.typ" : * + +#let transparent = color.rgb(100%,0,0,0) + +/// === Molecule function +/// Build a molecule group based on mol +/// Each molecule is represented as an optional count followed by a molecule name +/// starting by a capital letter followed by an optional indice +/// #example(``` +/// #skeletize({ +/// molecule("H_2O") +/// }) +///```) +/// It is possible to use an equation as a molecule. In this case, the spliting of the equation uses the same rules as in the string case. However, you can use parenthesis to group elements together. +/// #example(``` +/// #skeletize({ +/// molecule($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the molecule. It is used as the cetz name of the molecule and to link other molecules to it. +/// - links (dictionary): The links between this molecule and previous molecules or hooks. The key is the name of the molecule or hook and the value is the link function. +/// +/// Note that the antom-sep and angle arguments are ignored +/// - mol (string, equation): The string representing the molecule or an equation of the molecule +/// - vertical (boolean): If true, the molecule is drawn vertically +/// #example(``` +/// #skeletize({ +/// molecule("ABCD", vertical: true) +/// }) +///```) +/// -> drawable +#let molecule(name: none, links: (:), vertical: false, mol) = { + let atoms = if type(mol) == str { + split-string(mol) + } else if mol.func() == math.equation { + split-equation(mol, equation: true) + } else { + panic("Invalid molecule content") + } + ( + ( + type: "molecule", + name: name, + atoms: atoms, + links: links, + vertical: vertical, + count: atoms.len(), + ), + ) +} + +/// === Hooks +/// Create a hook in the molecule. It allows tu connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the molecule. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current molecule, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// branch({ +/// single(angle:1) +/// molecule("B") +/// }) +/// branch({ +/// double(angle: -1) +/// molecule("D") +/// }) +/// single() +/// double() +/// single() +/// molecule("C") +/// }) +///```) +#let branch(..args) = { + if args.pos().len() != 1 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", draw: args.pos().at(0), args: args.named()),) +} + +/// Create a regular cycle of molecules +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +#let cycle(..args) = { + if args.pos().len() != 2 { + panic("Cycle takes two positional arguments: number of faces and body") + } + let faces = args.pos().at(0) + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + draw: args.pos().at(1), + args: args.named(), + ), + ) +} diff --git a/packages/preview/alchemist/0.1.1/src/default.typ b/packages/preview/alchemist/0.1.1/src/default.typ new file mode 100644 index 0000000000..8a84fe6581 --- /dev/null +++ b/packages/preview/alchemist/0.1.1/src/default.typ @@ -0,0 +1,7 @@ +#let default = ( + atom-sep: 3em, + delta: 0.2em, + angle-increment: 45deg, + base-angle: 0deg, + debug: false, +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.1/src/drawer.typ b/packages/preview/alchemist/0.1.1/src/drawer.typ new file mode 100644 index 0000000000..de665a3fcc --- /dev/null +++ b/packages/preview/alchemist/0.1.1/src/drawer.typ @@ -0,0 +1,945 @@ +#import "default.typ": default +#import "@preview/cetz:0.2.2" +#import "utils.typ" +#import cetz.draw: * + +#let default-anchor = (type: "coord", anchor: (0, 0)) +#let max-int = 9223372036854775807 + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + group-id: 0, // id of the current group + link-id: 0, // id of the current link + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + + // branch + first-branch: false, // true if the next element is the first in a branch + + // cycle + first-molecule: none, // name of the first molecule in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices +) + +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + } + (..ctx, last-anchor: anchor) +} + +/// Return the index to choose if the link connection is not overridden +#let link-molecule-index(angle, end, count, vertical) = { + if not end { + if vertical and utils.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if utils.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and utils.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if utils.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let molecule-link-anchor(name, id, count) = { + if count <= id { + panic("The last molecule only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "center")) +} + +#let link-molecule-anchor(name: none, id, count) = { + if id >= count { + panic("This molecule only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the molecule to link to must be defined") + } + if name == none { + (name: str(id), anchor: "center") + } else { + (name: name, anchor: (str(id), "center")) + } +} + +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +/// Draw a triangle between two molecules +#let cram(from, to, ctx, args) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let base-length = utils.convert-length( + ctx, + args.at("base-length", default: .8em), + ) + line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: none), + fill: args.at("fill", default: black), + ) +} + +/// Draw a dashed triangle between two molecules +#let dashed-cram(from, to, length, ctx, args) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let base-length = utils.convert-length( + ctx, + args.at("base-length", default: .8em), + ) + hide({ + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - 0.05)) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + 0.05), + ) + }) + let stroke = args.at("stroke", default: black + .05em) + let dash-gap = utils.convert-length(ctx, args.at("dash-gap", default: .3em)) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(ctx, dash-width) + let length = utils.convert-length(ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} + +#let draw-molecule-text(mol) = { + for (id, eq) in mol.atoms.enumerate() { + let name = str(id) + // draw atoms of the group one after the other from left to right + content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "west" + }, + ( + if id == 0 { + (0, 0) + } else if mol.vertical { + (to: str(id - 1) + ".south", rel: (0, -.2em)) + } else { + str(id - 1) + ".east" + } + ), + { + show math.equation: math.upright + eq + }, + ) + id += 1 + } +} + +#let draw-molecule(mol, ctx) = { + let name = mol.name + if name != none { + if name in ctx.hooks { + panic("Molecule with name " + name + " already exists") + } + ctx.hooks.insert(name, mol) + } else { + name = "molecule" + str(ctx.group-id) + } + let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("west", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-molecule-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let group-anchor = link-molecule-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (group-anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule must be linked to a coord or a link") + } + ctx = set-last-anchor( + ctx, + (type: "molecule", name: name, count: mol.at("count"), vertical: mol.vertical), + ) + ctx.group-id += 1 + ( + ctx, + { + group( + anchor: if side { + group-anchor + } else { + "from" + str(ctx.group-id) + }, + name: name, + { + set-origin(coord) + anchor("default", (0, 0)) + draw-molecule-text(mol) + if not side { + anchor("from" + str(ctx.group-id), group-anchor) + } + }, + ) + }, + ) +} + +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} + +#let draw-last-cycle-link(link, ctx) = { + let from-name = none + let from-pos = none + if ctx.last-anchor.type == "molecule" { + from-name = ctx.last-anchor.name + from-pos = (name: from-name, anchor: "center") + if from-name not in ctx.hooks { + ctx.hooks.insert(from-name, ctx.last-anchor) + } + } else if ctx.last-anchor.type == "link" { + from-pos = ctx.last-anchor.name + "-end-anchor" + } else { + panic("A cycle link must be linked to a molecule or a link") + } + ctx.links.push(( + type: "link", + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: from-pos, + from-name: from-name, + to-name: ctx.first-molecule, + from: link.at("from", default: none), + to: link.at("to", default: none), + override: (offset: "left"), + draw: link.draw, + )) + ctx.link-id += 1 + (ctx, ()) +} + +#let draw-link(link, ctx) = { + let link-angle = 0deg + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-molecule != none { + return draw-last-cycle-link(link, ctx) + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = utils.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let from-pos = if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "molecule" { + from-connection = link-molecule-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + molecule-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name", default: "link" + str(ctx.link-id)) + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = set-last-anchor( + ctx, + ( + type: "link", + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: none, + to: to-connection, + angle: link-angle, + draw: link.draw, + ), + ) + ctx.link-id += 1 + ( + ctx, + { + let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + line(from-pos, end-anchor, stroke: blue + .1em) + } + group( + name: link-name + "-end-anchor", + { + anchor("default", end-anchor) + }, + ) + }, + ) +} + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + 0, + )) + } + vertex +} + +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius) +} + +#let draw-cycle-center-arc(ctx, name, center-arc) = { + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius) = cycle-center-radius(ctx, cetz-ctx, vertex) + if name != none { + group( + name: name, + { + anchor("default", center) + }, + ) + } + if center-arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= center-arc.at("radius", default: 0.7) + } else { + min-radius *= center-arc.at("radius", default: 0.5) + } + let start = center-arc.at("start", default: 0deg) + let end = center-arc.at("end", default: 360deg) + let delta = center-arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + arc( + center, + ..center-arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) + +} + +#let draw-hooks-links(links, name, ctx, from-mol) = { + for (to-name, (link,)) in links { + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "molecule" { + ctx.links.push(( + type: "link", + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: if from-mol { + (name: name, anchor: "center") + } else { + name + "-end-anchor" + }, + from-name: if from-mol { + name + }, + to-name: to-name, + from: none, + to: none, + override: angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: if from-mol { + (name: name, anchor: "center") + } else { + name + "-end-anchor" + }, + from-name: if from-mol { + name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + ctx.link-id += 1 + } + ctx +} + +#let update-parent-context(parent-ctx, ctx) = { + let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { + ( + ..parent-ctx.last-anchor, + drew: true, + ) + } else { + parent-ctx.last-anchor + } + ( + ..parent-ctx, + last-anchor: last-anchor, + hooks: ctx.hooks, + hooks-links: ctx.hooks-links, + links: ctx.links, + group-id: ctx.group-id, + link-id: ctx.link-id, + ) +} + +#let draw-molecules-and-link(ctx, body) = { + let drawing = () + let cetz-drawing = () + ( + { + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + if type(element) == function { + cetz-drawing.push(element) + } else if "type" not in element { + panic("Element " + str(element) + " has no type") + } else if element.type == "molecule" { + if ctx.first-branch { + panic("A molecule can not be the first element in a cycle") + } + (ctx, drawing) = draw-molecule(element, ctx) + drawing + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + } else if element.type == "link" { + ctx.first-branch = false + (ctx, drawing) = draw-link(element, ctx) + ctx.faces-count += 1 + drawing + if element.links.len() != 0 { + ctx.hooks-links.push((element.links, ctx.last-anchor.name, false)) + } + } else if element.type == "branch" { + let angle = angle-from-ctx(ctx, element.args, cycle-angle(ctx)) + let (drawing, branch-ctx, cetz-rec) = draw-molecules-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + element.draw, + ) + ctx = update-parent-context(ctx, branch-ctx) + cetz-drawing += cetz-rec + drawing + } else if element.type == "cycle" { + let cycle-step-angle = 360deg / element.faces + let angle = angle-from-ctx(ctx, element.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ctx.relative-angle == 0deg and ctx.angle == 0deg and not element.args.at( + "align", + default: false, + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-molecule = none + if ctx.last-anchor.type == "molecule" { + first-molecule = ctx.last-anchor.name + if first-molecule not in ctx.hooks { + ctx.hooks.insert(first-molecule, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in element.args { + name = element.args.at("name") + record-vertex = true + } else if "arc" in element.args { + record-vertex = true + } + let (drawing, cycle-ctx, cetz-rec) = draw-molecules-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: element.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-molecule: first-molecule, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + element.draw, + ) + ctx = update-parent-context(ctx, cycle-ctx) + cetz-drawing += cetz-rec + drawing + if record-vertex { + draw-cycle-center-arc(cycle-ctx, name, element.args.at("arc", default: none)) + } + } else if element.type == "hook" { + if element.name in ctx.hooks { + panic("Hook " + element.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert(element.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(element.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + } else { + panic("Unknown element type " + element.type) + } + } + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + } + }, + ctx, + cetz-drawing, + ) +} + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +#let molecule-anchor(ctx, cetz-ctx, angle, molecule, id) = { + let delta = utils.convert-length(cetz-ctx, ctx.config.delta) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "center")), + ) + + let (a, b) = if utils.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, delta, molecule, id) + } else if utils.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, delta, molecule, id) + } else if utils.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, delta, molecule, id) + } else { + anchor-south-east(cetz-ctx, center, delta, molecule, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if utils.angle-in-range-inclusive(angle, 0deg, 90deg) or utils.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "center") + if link.to == none or link.from == none { + let angle = utils.angle-between(cetz-ctx, link.from-pos, to-pos) + link.angle = angle + if link.from == none { + link.from = link-molecule-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-molecule-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + let start = molecule-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) + let end = molecule-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) + ((start, end), utils.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = utils.angle-correction( + utils.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "center"), + ), + ) + link.to = link-molecule-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = utils.angle-correction( + utils.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "center")), + ), + ) + } + let end-anchor = molecule-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + utils.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + molecule-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = utils.angle-correction( + utils.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) + let from = link-molecule-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = molecule-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) + ( + ( + start-anchor, + hook.hook, + ), + utils.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + utils.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + +#let draw-link-decoration(ctx) = { + ( + get-ctx(cetz-ctx => { + for link in ctx.links { + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = utils.distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + group({ + set-origin(from) + rotate(angle) + (link.draw)(length, cetz-ctx, override: link.override) + }) + } + }), + ctx, + ) +} + +#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { + for (key, value) in default { + if key not in config { + config.insert(key, value) + } + } + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + let (atoms, ctx, cetz-drawing) = draw-molecules-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + let (links, _) = draw-link-decoration(ctx) + + if name == none { + atoms + links + cetz-drawing + } else { + group( + name: name, + anchor: mol-anchor, + { + anchor("default", (0,0)) + atoms + links + cetz-drawing + }, + ) + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas( + debug: debug, + background: background, + draw-skeleton(config: config, body), + ) +} diff --git a/packages/preview/alchemist/0.1.1/src/links.typ b/packages/preview/alchemist/0.1.1/src/links.typ new file mode 100644 index 0000000000..29acfd3804 --- /dev/null +++ b/packages/preview/alchemist/0.1.1/src/links.typ @@ -0,0 +1,287 @@ +#import "@preview/cetz:0.2.2" +#import "drawer.typ" +#import "utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes three arguments: the length of the link, the context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// single() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// single(stroke: red + 5pt) +/// molecule("B") +/// }) +///```) +#let single = build-link((length, _, args) => { + import cetz.draw: * + line((0, 0), (length, 0), stroke: args.at("stroke", default: black)) +}) + +/// Draw a double line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// molecule("B") +/// }) +///```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double(offset: "right") +/// molecule("B") +/// double(offset: "left") +/// molecule("C") +/// double(offset: "center") +/// molecule("D") +/// }) +///```) +#let double = build-link((length, ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(ctx, args.at("gap", default: .25em)) / 2 + let offset = args.at("offset", default: "center") + let coeff = args.at("offset-coeff", default: 0.85) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + if offset == "right" { + translate((0, -gap)) + } else if offset == "left" { + translate((0, gap)) + } else if offset == "center" { } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + translate((0, -gap)) + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: black), + ) + translate((0, 2 * gap)) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: black), + ) +}) + +/// Draw a triple line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// triple() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// molecule("B") +/// }) +///```) +#let triple = build-link((length, ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(ctx, args.at("gap", default: .25em)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: black)) + translate((0, -gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: black)) + translate((0, 2 * gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: black)) +}) + +/// Draw a filled cram between two molecules with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-right() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, args) => drawer.cram( + (0, 0), + (length, 0), + ctx, + args, +)) + +/// Draw a filled cram between two molecules with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-left() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, args) => drawer.cram( + (length, 0), + (0, 0), + ctx, + args, +)) + +/// Draw a hollow cram between two molecules with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + drawer.cram((0, 0), (length, 0), ctx, args) +}) + +/// Draw a hollow cram between two molecules with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + drawer.cram((length, 0), (0, 0), ctx, args) +}) + +/// Draw a dashed cram between two molecules with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-right() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-dashed-right = build-link((length, ctx, args) => drawer.dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + args, +)) + +/// Draw a dashed cram between two molecules with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-left() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, args) => drawer.dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + args, +)) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.1/src/molecule.typ b/packages/preview/alchemist/0.1.1/src/molecule.typ new file mode 100644 index 0000000000..a2fbc185a9 --- /dev/null +++ b/packages/preview/alchemist/0.1.1/src/molecule.typ @@ -0,0 +1,64 @@ +#let split-equation(mol, equation: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + for m in mol { + let last-number-hold = last-number + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + result.push(m) + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in molecule") + } + last-number = true + result.push(m) + } else { + panic("Invalid molecule content") + } + } else if m.func() == math.attach or m.func() == math.lr { + result.push(m) + } else if m == [ ] { + continue + } else { + panic("Invalid molecule content") + } + if last-number-hold { + result.at(-2) = result.at(-2) + result.at(-1) + let _ = result.pop() + last-number = false + } + } + result +} + +#let split-string(mol) = { + let aux(str) = { + let match = str.match(regex("^ *([0-9]*[A-Z][a-z]*)(_[0-9]+)?")) + if match == none { + panic(str + " is not a valid atom") + } + let eq = "\"" + match.captures.at(0) + "\"" + if match.captures.len() >= 2 { + eq += match.captures.at(1) + } + let eq = math.equation(eval(eq, mode: "math")) + (eq, match.end) + } + + while not mol.len() == 0 { + let (eq, end) = aux(mol) + mol = mol.slice(end) + (eq,) + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.1/src/utils.typ b/packages/preview/alchemist/0.1.1/src/utils.typ new file mode 100644 index 0000000000..10f3e7850b --- /dev/null +++ b/packages/preview/alchemist/0.1.1/src/utils.typ @@ -0,0 +1,72 @@ +#import "@preview/cetz:0.2.2" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + if type(num) == length { + if repr(num).ends-with("em") { + float(repr(num).slice(0, -2)) * ctx.em-size.width / ctx.length + } else { + float(num / ctx.length) + } + } else { + float(num) + } +} + +/// Convert any angle to an angle between -360deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a = a + 360 + } + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.1/typst.toml b/packages/preview/alchemist/0.1.1/typst.toml new file mode 100644 index 0000000000..f0fd9d158a --- /dev/null +++ b/packages/preview/alchemist/0.1.1/typst.toml @@ -0,0 +1,12 @@ +[package] +name = "alchemist" +version = "0.1.1" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.11.0" +repository = "https://github.com/Robotechnic/alchemist" +description = "A package to render skeletal formulas using cetz" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization"] +disciplines = ["chemistry", "biology", ] From 71c7919b07b4ba7e2be0e1a8f5d327fff8b0d40f Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Mon, 11 Nov 2024 23:18:48 +0100 Subject: [PATCH 03/24] Upload alchemist v0.1.2 --- packages/preview/alchemist/0.1.2/LICENSE | 22 + packages/preview/alchemist/0.1.2/README.md | 100 ++ packages/preview/alchemist/0.1.2/lib.typ | 137 +++ .../preview/alchemist/0.1.2/src/default.typ | 30 + .../preview/alchemist/0.1.2/src/drawer.typ | 941 ++++++++++++++++++ .../preview/alchemist/0.1.2/src/links.typ | 291 ++++++ .../preview/alchemist/0.1.2/src/molecule.typ | 64 ++ .../preview/alchemist/0.1.2/src/utils.typ | 84 ++ packages/preview/alchemist/0.1.2/typst.toml | 12 + 9 files changed, 1681 insertions(+) create mode 100644 packages/preview/alchemist/0.1.2/LICENSE create mode 100644 packages/preview/alchemist/0.1.2/README.md create mode 100644 packages/preview/alchemist/0.1.2/lib.typ create mode 100644 packages/preview/alchemist/0.1.2/src/default.typ create mode 100644 packages/preview/alchemist/0.1.2/src/drawer.typ create mode 100644 packages/preview/alchemist/0.1.2/src/links.typ create mode 100644 packages/preview/alchemist/0.1.2/src/molecule.typ create mode 100644 packages/preview/alchemist/0.1.2/src/utils.typ create mode 100644 packages/preview/alchemist/0.1.2/typst.toml diff --git a/packages/preview/alchemist/0.1.2/LICENSE b/packages/preview/alchemist/0.1.2/LICENSE new file mode 100644 index 0000000000..25dc239757 --- /dev/null +++ b/packages/preview/alchemist/0.1.2/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.2/README.md b/packages/preview/alchemist/0.1.2/README.md new file mode 100644 index 0000000000..d635a7c865 --- /dev/null +++ b/packages/preview/alchemist/0.1.2/README.md @@ -0,0 +1,100 @@ +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + molecule(name: "A", "A") + single() + molecule("B") + branch({ + single(angle: 1) + molecule( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + molecule(name: "X", "X") + }) + branch({ + single(angle: -1) + molecule("Y") + single() + molecule( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + molecule( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Robotechnic/alchemist/master/images/links1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + molecule("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Robotechnic/alchemist/master/images/cetz1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.1.2": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). + +## Changelog + +### 0.1.2 + +- Added default values for link style properties. +- Updated `cetz` to version 0.3.1 + +### 0.1.1 + +- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. +- Fixed multiples bugs that causes overdraws of links. + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.1.2/lib.typ b/packages/preview/alchemist/0.1.2/lib.typ new file mode 100644 index 0000000000..64d4f03fcd --- /dev/null +++ b/packages/preview/alchemist/0.1.2/lib.typ @@ -0,0 +1,137 @@ +#import "@preview/cetz:0.3.1" +#import "src/default.typ": default +#import "src/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ" : skeletize, draw-skeleton +#import "src/links.typ" : * +#import "src/molecule.typ" : * + +#let transparent = color.rgb(100%,0,0,0) + +/// === Molecule function +/// Build a molecule group based on mol +/// Each molecule is represented as an optional count followed by a molecule name +/// starting by a capital letter followed by an optional indice +/// #example(``` +/// #skeletize({ +/// molecule("H_2O") +/// }) +///```) +/// It is possible to use an equation as a molecule. In this case, the spliting of the equation uses the same rules as in the string case. However, you can use parenthesis to group elements together. +/// #example(``` +/// #skeletize({ +/// molecule($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the molecule. It is used as the cetz name of the molecule and to link other molecules to it. +/// - links (dictionary): The links between this molecule and previous molecules or hooks. The key is the name of the molecule or hook and the value is the link function. +/// +/// Note that the antom-sep and angle arguments are ignored +/// - mol (string, equation): The string representing the molecule or an equation of the molecule +/// - vertical (boolean): If true, the molecule is drawn vertically +/// #example(``` +/// #skeletize({ +/// molecule("ABCD", vertical: true) +/// }) +///```) +/// -> drawable +#let molecule(name: none, links: (:), vertical: false, mol) = { + let atoms = if type(mol) == str { + split-string(mol) + } else if mol.func() == math.equation { + split-equation(mol, equation: true) + } else { + panic("Invalid molecule content") + } + ( + ( + type: "molecule", + name: name, + atoms: atoms, + links: links, + vertical: vertical, + count: atoms.len(), + ), + ) +} + +/// === Hooks +/// Create a hook in the molecule. It allows tu connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the molecule. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current molecule, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// branch({ +/// single(angle:1) +/// molecule("B") +/// }) +/// branch({ +/// double(angle: -1) +/// molecule("D") +/// }) +/// single() +/// double() +/// single() +/// molecule("C") +/// }) +///```) +#let branch(..args) = { + if args.pos().len() != 1 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", draw: args.pos().at(0), args: args.named()),) +} + +/// Create a regular cycle of molecules +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +#let cycle(..args) = { + if args.pos().len() != 2 { + panic("Cycle takes two positional arguments: number of faces and body") + } + let faces = args.pos().at(0) + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + draw: args.pos().at(1), + args: args.named(), + ), + ) +} diff --git a/packages/preview/alchemist/0.1.2/src/default.typ b/packages/preview/alchemist/0.1.2/src/default.typ new file mode 100644 index 0000000000..d910a9f670 --- /dev/null +++ b/packages/preview/alchemist/0.1.2/src/default.typ @@ -0,0 +1,30 @@ +#let default = ( + atom-sep: 3em, + delta: 0.2em, + angle-increment: 45deg, + base-angle: 0deg, + debug: false, + single: ( + stroke: black + ), + double: ( + gap: .25em, + offset: "center", + offset-coeff: 0.85, + stroke: black + ), + triple: ( + gap: .25em, + stroke: black + ), + filled-cram: ( + stroke: none, + fill: black, + base-length: .8em + ), + dashed-cram: ( + stroke: black + .05em, + dash-gap: .3em, + base-length: .8em + ), +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.2/src/drawer.typ b/packages/preview/alchemist/0.1.2/src/drawer.typ new file mode 100644 index 0000000000..2c24064af2 --- /dev/null +++ b/packages/preview/alchemist/0.1.2/src/drawer.typ @@ -0,0 +1,941 @@ +#import "default.typ": default +#import "@preview/cetz:0.3.1" +#import "utils.typ" +#import cetz.draw: * + +#let default-anchor = (type: "coord", anchor: (0, 0)) +#let max-int = 9223372036854775807 + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + group-id: 0, // id of the current group + link-id: 0, // id of the current link + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + + // branch + first-branch: false, // true if the next element is the first in a branch + + // cycle + first-molecule: none, // name of the first molecule in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices +) + +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + } + (..ctx, last-anchor: anchor) +} + +/// Return the index to choose if the link connection is not overridden +#let link-molecule-index(angle, end, count, vertical) = { + if not end { + if vertical and utils.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if utils.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and utils.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if utils.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let molecule-link-anchor(name, id, count) = { + if count <= id { + panic("The last molecule only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "center")) +} + +#let link-molecule-anchor(name: none, id, count) = { + if id >= count { + panic("This molecule only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the molecule to link to must be defined") + } + if name == none { + (name: str(id), anchor: "center") + } else { + (name: name, anchor: (str(id), "center")) + } +} + +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +/// Draw a triangle between two molecules +#let cram(from, to, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.filled-cram.base-length), + ) + line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: ctx.config.filled-cram.stroke), + fill: args.at("fill", default: ctx.config.filled-cram.fill), + ) +} + +/// Draw a dashed triangle between two molecules +#let dashed-cram(from, to, length, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.dashed-cram.base-length), + ) + hide({ + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - 0.05)) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + 0.05), + ) + }) + let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) + let dash-gap = utils.convert-length(cetz-ctx, args.at("dash-gap", default: ctx.config.dashed-cram.dash-gap)) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(cetz-ctx, dash-width) + let length = utils.convert-length(cetz-ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} + +#let draw-molecule-text(mol) = { + for (id, eq) in mol.atoms.enumerate() { + let name = str(id) + // draw atoms of the group one after the other from left to right + content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "west" + }, + ( + if id == 0 { + (0, 0) + } else if mol.vertical { + (to: str(id - 1) + ".south", rel: (0, -.2em)) + } else { + str(id - 1) + ".east" + } + ), + { + show math.equation: math.upright + eq + }, + ) + id += 1 + } +} + +#let draw-molecule(mol, ctx) = { + let name = mol.name + if name != none { + if name in ctx.hooks { + panic("Molecule with name " + name + " already exists") + } + ctx.hooks.insert(name, mol) + } else { + name = "molecule" + str(ctx.group-id) + } + let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("west", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-molecule-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let group-anchor = link-molecule-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (group-anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule must be linked to a coord or a link") + } + ctx = set-last-anchor( + ctx, + (type: "molecule", name: name, count: mol.at("count"), vertical: mol.vertical), + ) + ctx.group-id += 1 + ( + ctx, + { + group( + anchor: if side { + group-anchor + } else { + "from" + str(ctx.group-id) + }, + name: name, + { + set-origin(coord) + anchor("default", (0, 0)) + draw-molecule-text(mol) + if not side { + anchor("from" + str(ctx.group-id), group-anchor) + } + }, + ) + }, + ) +} + +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} + +#let draw-last-cycle-link(link, ctx) = { + let from-name = none + let from-pos = none + if ctx.last-anchor.type == "molecule" { + from-name = ctx.last-anchor.name + from-pos = (name: from-name, anchor: "center") + if from-name not in ctx.hooks { + ctx.hooks.insert(from-name, ctx.last-anchor) + } + } else if ctx.last-anchor.type == "link" { + from-pos = ctx.last-anchor.name + "-end-anchor" + } else { + panic("A cycle link must be linked to a molecule or a link") + } + ctx.links.push(( + type: "link", + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: from-pos, + from-name: from-name, + to-name: ctx.first-molecule, + from: link.at("from", default: none), + to: link.at("to", default: none), + override: (offset: "left"), + draw: link.draw, + )) + ctx.link-id += 1 + (ctx, ()) +} + +#let draw-link(link, ctx) = { + let link-angle = 0deg + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-molecule != none { + return draw-last-cycle-link(link, ctx) + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = utils.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let from-pos = if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "molecule" { + from-connection = link-molecule-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + molecule-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name", default: "link" + str(ctx.link-id)) + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = set-last-anchor( + ctx, + ( + type: "link", + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: none, + to: to-connection, + angle: link-angle, + draw: link.draw, + ), + ) + ctx.link-id += 1 + ( + ctx, + { + let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + line(from-pos, end-anchor, stroke: blue + .1em) + } + group( + name: link-name + "-end-anchor", + { + anchor("default", end-anchor) + }, + ) + }, + ) +} + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + 0, + )) + } + vertex +} + +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius) +} + +#let draw-cycle-center-arc(ctx, name, center-arc) = { + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius) = cycle-center-radius(ctx, cetz-ctx, vertex) + if name != none { + group( + name: name, + { + anchor("default", center) + }, + ) + } + if center-arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= center-arc.at("radius", default: 0.7) + } else { + min-radius *= center-arc.at("radius", default: 0.5) + } + let start = center-arc.at("start", default: 0deg) + let end = center-arc.at("end", default: 360deg) + let delta = center-arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + arc( + center, + ..center-arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) + +} + +#let draw-hooks-links(links, name, ctx, from-mol) = { + for (to-name, (link,)) in links { + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "molecule" { + ctx.links.push(( + type: "link", + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: if from-mol { + (name: name, anchor: "center") + } else { + name + "-end-anchor" + }, + from-name: if from-mol { + name + }, + to-name: to-name, + from: none, + to: none, + override: angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: if from-mol { + (name: name, anchor: "center") + } else { + name + "-end-anchor" + }, + from-name: if from-mol { + name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + ctx.link-id += 1 + } + ctx +} + +#let update-parent-context(parent-ctx, ctx) = { + let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { + ( + ..parent-ctx.last-anchor, + drew: true, + ) + } else { + parent-ctx.last-anchor + } + ( + ..parent-ctx, + last-anchor: last-anchor, + hooks: ctx.hooks, + hooks-links: ctx.hooks-links, + links: ctx.links, + group-id: ctx.group-id, + link-id: ctx.link-id, + ) +} + +#let draw-molecules-and-link(ctx, body) = { + let drawing = () + let cetz-drawing = () + ( + { + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + if type(element) == function { + cetz-drawing.push(element) + } else if "type" not in element { + panic("Element " + str(element) + " has no type") + } else if element.type == "molecule" { + if ctx.first-branch { + panic("A molecule can not be the first element in a cycle") + } + (ctx, drawing) = draw-molecule(element, ctx) + drawing + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + } else if element.type == "link" { + ctx.first-branch = false + (ctx, drawing) = draw-link(element, ctx) + ctx.faces-count += 1 + drawing + if element.links.len() != 0 { + ctx.hooks-links.push((element.links, ctx.last-anchor.name, false)) + } + } else if element.type == "branch" { + let angle = angle-from-ctx(ctx, element.args, cycle-angle(ctx)) + let (drawing, branch-ctx, cetz-rec) = draw-molecules-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + element.draw, + ) + ctx = update-parent-context(ctx, branch-ctx) + cetz-drawing += cetz-rec + drawing + } else if element.type == "cycle" { + let cycle-step-angle = 360deg / element.faces + let angle = angle-from-ctx(ctx, element.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ctx.relative-angle == 0deg and ctx.angle == 0deg and not element.args.at( + "align", + default: false, + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-molecule = none + if ctx.last-anchor.type == "molecule" { + first-molecule = ctx.last-anchor.name + if first-molecule not in ctx.hooks { + ctx.hooks.insert(first-molecule, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in element.args { + name = element.args.at("name") + record-vertex = true + } else if "arc" in element.args { + record-vertex = true + } + let (drawing, cycle-ctx, cetz-rec) = draw-molecules-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: element.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-molecule: first-molecule, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + element.draw, + ) + ctx = update-parent-context(ctx, cycle-ctx) + cetz-drawing += cetz-rec + drawing + if record-vertex { + draw-cycle-center-arc(cycle-ctx, name, element.args.at("arc", default: none)) + } + } else if element.type == "hook" { + if element.name in ctx.hooks { + panic("Hook " + element.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert(element.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(element.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + } else { + panic("Unknown element type " + element.type) + } + } + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + } + }, + ctx, + cetz-drawing, + ) +} + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +#let molecule-anchor(ctx, cetz-ctx, angle, molecule, id) = { + let delta = utils.convert-length(cetz-ctx, ctx.config.delta) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "center")), + ) + + let (a, b) = if utils.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, delta, molecule, id) + } else if utils.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, delta, molecule, id) + } else if utils.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, delta, molecule, id) + } else { + anchor-south-east(cetz-ctx, center, delta, molecule, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if utils.angle-in-range-inclusive(angle, 0deg, 90deg) or utils.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "center") + if link.to == none or link.from == none { + let angle = utils.angle-between(cetz-ctx, link.from-pos, to-pos) + link.angle = angle + if link.from == none { + link.from = link-molecule-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-molecule-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + let start = molecule-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) + let end = molecule-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) + ((start, end), utils.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = utils.angle-correction( + utils.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "center"), + ), + ) + link.to = link-molecule-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = utils.angle-correction( + utils.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "center")), + ), + ) + } + let end-anchor = molecule-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + utils.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + molecule-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = utils.angle-correction( + utils.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) + let from = link-molecule-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = molecule-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) + ( + ( + start-anchor, + hook.hook, + ), + utils.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + utils.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + +#let draw-link-decoration(ctx) = { + ( + get-ctx(cetz-ctx => { + for link in ctx.links { + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = utils.distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + group({ + set-origin(from) + rotate(angle) + (link.draw)(length, ctx, cetz-ctx, override: link.override) + }) + } + }), + ctx, + ) +} + +#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { + let config = utils.merge-dictionaries(config, default) + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + let (atoms, ctx, cetz-drawing) = draw-molecules-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + let (links, _) = draw-link-decoration(ctx) + + if name == none { + atoms + links + cetz-drawing + } else { + group( + name: name, + anchor: mol-anchor, + { + anchor("default", (0,0)) + atoms + links + cetz-drawing + }, + ) + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas( + debug: debug, + background: background, + draw-skeleton(config: config, body), + ) +} diff --git a/packages/preview/alchemist/0.1.2/src/links.typ b/packages/preview/alchemist/0.1.2/src/links.typ new file mode 100644 index 0000000000..e5f89d9dc6 --- /dev/null +++ b/packages/preview/alchemist/0.1.2/src/links.typ @@ -0,0 +1,291 @@ +#import "@preview/cetz:0.3.1" +#import "drawer.typ" +#import "utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes three arguments: the length of the link, the context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, cetz-ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, cetz-ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// single() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// single(stroke: red + 5pt) +/// molecule("B") +/// }) +///```) +#let single = build-link((length, ctx, _, args) => { + import cetz.draw: * + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.single.stroke)) +}) + +/// Draw a double line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// molecule("B") +/// }) +///```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double(offset: "right") +/// molecule("B") +/// double(offset: "left") +/// molecule("C") +/// double(offset: "center") +/// molecule("D") +/// }) +///```) +#let double = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.double.gap)) / 2 + let offset = args.at("offset", default: ctx.config.double.offset) + let coeff = args.at("offset-coeff", default: ctx.config.double.offset-coeff) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + if offset == "right" { + translate((0, -gap)) + } else if offset == "left" { + translate((0, gap)) + } else if offset == "center" { } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + translate((0, -gap)) + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) + translate((0, 2 * gap)) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) +}) + +/// Draw a triple line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// triple() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// molecule("B") +/// }) +///```) +#let triple = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.triple.gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + translate((0, -gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + translate((0, 2 * gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) +}) + +/// Draw a filled cram between two molecules with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-right() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, cetz-ctx, args) => drawer.cram( + (0, 0), + (length, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a filled cram between two molecules with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-left() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, cetz-ctx, args) => drawer.cram( + (length, 0), + (0, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a hollow cram between two molecules with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + drawer.cram((0, 0), (length, 0), ctx, cetz-ctx, args) +}) + +/// Draw a hollow cram between two molecules with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + drawer.cram((length, 0), (0, 0), ctx, cetz-ctx, args) +}) + +/// Draw a dashed cram between two molecules with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-right() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-dashed-right = build-link((length, ctx, cetz-ctx, args) => drawer.dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + cetz-ctx, + args, +)) + +/// Draw a dashed cram between two molecules with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-left() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, cetz-ctx, args) => drawer.dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + cetz-ctx, + args, +)) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.2/src/molecule.typ b/packages/preview/alchemist/0.1.2/src/molecule.typ new file mode 100644 index 0000000000..a2fbc185a9 --- /dev/null +++ b/packages/preview/alchemist/0.1.2/src/molecule.typ @@ -0,0 +1,64 @@ +#let split-equation(mol, equation: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + for m in mol { + let last-number-hold = last-number + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + result.push(m) + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in molecule") + } + last-number = true + result.push(m) + } else { + panic("Invalid molecule content") + } + } else if m.func() == math.attach or m.func() == math.lr { + result.push(m) + } else if m == [ ] { + continue + } else { + panic("Invalid molecule content") + } + if last-number-hold { + result.at(-2) = result.at(-2) + result.at(-1) + let _ = result.pop() + last-number = false + } + } + result +} + +#let split-string(mol) = { + let aux(str) = { + let match = str.match(regex("^ *([0-9]*[A-Z][a-z]*)(_[0-9]+)?")) + if match == none { + panic(str + " is not a valid atom") + } + let eq = "\"" + match.captures.at(0) + "\"" + if match.captures.len() >= 2 { + eq += match.captures.at(1) + } + let eq = math.equation(eval(eq, mode: "math")) + (eq, match.end) + } + + while not mol.len() == 0 { + let (eq, end) = aux(mol) + mol = mol.slice(end) + (eq,) + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.2/src/utils.typ b/packages/preview/alchemist/0.1.2/src/utils.typ new file mode 100644 index 0000000000..374373c288 --- /dev/null +++ b/packages/preview/alchemist/0.1.2/src/utils.typ @@ -0,0 +1,84 @@ +#import "@preview/cetz:0.3.1" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + return if type(num) == length { + float(num.to-absolute() / ctx.length) + } else if type(num) == ratio { + num + } else { + float(num) + } +} + +/// Convert any angle to an angle between -360deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a = a + 360 + } + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} + +/// merge two imbricated dictionaries together +/// The second dictionary is the default value if the key is not present in the first dictionary +#let merge-dictionaries(dict1, default) = { + let result = default + for (key, value) in dict1 { + if type(value) == dictionary { + result.at(key) = merge-dictionaries(value, default.at(key)) + } else { + result.at(key) = value + } + } + result +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.2/typst.toml b/packages/preview/alchemist/0.1.2/typst.toml new file mode 100644 index 0000000000..4028eabd52 --- /dev/null +++ b/packages/preview/alchemist/0.1.2/typst.toml @@ -0,0 +1,12 @@ +[package] +name = "alchemist" +version = "0.1.2" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.11.0" +repository = "https://github.com/Robotechnic/alchemist" +description = "A package to render skeletal formulas using cetz" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization"] +disciplines = ["chemistry", "biology", ] From 093a609a64acacae0abb25ee6f6375a3c560426d Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Wed, 13 Nov 2024 14:53:45 +0100 Subject: [PATCH 04/24] fixe a bug --- .../preview/alchemist/0.1.2/src/default.typ | 3 +- .../preview/alchemist/0.1.2/src/drawer.typ | 81 ++++++++----------- .../preview/alchemist/0.1.2/src/links.typ | 4 +- 3 files changed, 38 insertions(+), 50 deletions(-) diff --git a/packages/preview/alchemist/0.1.2/src/default.typ b/packages/preview/alchemist/0.1.2/src/default.typ index d910a9f670..aeae5a372a 100644 --- a/packages/preview/alchemist/0.1.2/src/default.typ +++ b/packages/preview/alchemist/0.1.2/src/default.typ @@ -25,6 +25,7 @@ dashed-cram: ( stroke: black + .05em, dash-gap: .3em, - base-length: .8em + base-length: .8em, + tip-length: .1em ), ) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.2/src/drawer.typ b/packages/preview/alchemist/0.1.2/src/drawer.typ index 2c24064af2..48a8fe38dc 100644 --- a/packages/preview/alchemist/0.1.2/src/drawer.typ +++ b/packages/preview/alchemist/0.1.2/src/drawer.typ @@ -65,7 +65,7 @@ if id == -1 { id = count - 1 } - (name: name, anchor: (str(id), "center")) + (name: name, anchor: (str(id), "mid")) } #let link-molecule-anchor(name: none, id, count) = { @@ -76,9 +76,9 @@ panic("The index of the molecule to link to must be defined") } if name == none { - (name: str(id), anchor: "center") + (name: str(id), anchor: "mid") } else { - (name: name, anchor: (str(id), "center")) + (name: name, anchor: (str(id), "mid")) } } @@ -132,12 +132,16 @@ cetz-ctx, args.at("base-length", default: ctx.config.dashed-cram.base-length), ) + let tip-length = utils.convert-length( + cetz-ctx, + args.at("tip-length", default: ctx.config.dashed-cram.tip-length), + ) hide({ - line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - 0.05)) + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - tip-length / 2)) line( name: "bottom", (from-x, from-y + base-length / 2), - (to-x, to-y + 0.05), + (to-x, to-y + tip-length / 2), ) }) let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) @@ -169,7 +173,7 @@ anchor: if mol.vertical { "north" } else { - "west" + "mid-west" }, ( if id == 0 { @@ -177,7 +181,7 @@ } else if mol.vertical { (to: str(id - 1) + ".south", rel: (0, -.2em)) } else { - str(id - 1) + ".east" + str(id - 1) + ".mid-east" } ), { @@ -252,40 +256,12 @@ } } -#let draw-last-cycle-link(link, ctx) = { - let from-name = none - let from-pos = none - if ctx.last-anchor.type == "molecule" { - from-name = ctx.last-anchor.name - from-pos = (name: from-name, anchor: "center") - if from-name not in ctx.hooks { - ctx.hooks.insert(from-name, ctx.last-anchor) - } - } else if ctx.last-anchor.type == "link" { - from-pos = ctx.last-anchor.name + "-end-anchor" - } else { - panic("A cycle link must be linked to a molecule or a link") - } - ctx.links.push(( - type: "link", - name: link.at("name", default: "link" + str(ctx.link-id)), - from-pos: from-pos, - from-name: from-name, - to-name: ctx.first-molecule, - from: link.at("from", default: none), - to: link.at("to", default: none), - override: (offset: "left"), - draw: link.draw, - )) - ctx.link-id += 1 - (ctx, ()) -} - #let draw-link(link, ctx) = { let link-angle = 0deg + let to-name = none if ctx.in-cycle { if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-molecule != none { - return draw-last-cycle-link(link, ctx) + to-name = ctx.first-molecule } if ctx.faces-count == 0 { link-angle = ctx.relative-angle @@ -343,7 +319,7 @@ from-pos: from-pos, from-name: from-name, from: from-connection, - to-name: none, + to-name: to-name, to: to-connection, angle: link-angle, draw: link.draw, @@ -469,7 +445,7 @@ type: "link", name: link.at("name", default: "link" + str(ctx.link-id)), from-pos: if from-mol { - (name: name, anchor: "center") + (name: name, anchor: "mid") } else { name + "-end-anchor" }, @@ -491,7 +467,7 @@ }, name: link.at("name", default: "link" + str(ctx.link-id)), from-pos: if from-mol { - (name: name, anchor: "center") + (name: name, anchor: "mid") } else { name + "-end-anchor" }, @@ -716,7 +692,7 @@ let delta = utils.convert-length(cetz-ctx, ctx.config.delta) let (cetz-ctx, center) = cetz.coordinate.resolve( cetz-ctx, - (name: molecule, anchor: (id, "center")), + (name: molecule, anchor: (id, "mid")), ) let (a, b) = if utils.angle-in-range(angle, 0deg, 90deg) { @@ -748,9 +724,12 @@ } #let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { - let to-pos = (name: link.to-name, anchor: "center") + let to-pos = (name: link.to-name, anchor: "mid") if link.to == none or link.from == none { - let angle = utils.angle-between(cetz-ctx, link.from-pos, to-pos) + let angle = link.at( + "angle", + default: utils.angle-between(cetz-ctx, link.from-pos, to-pos), + ) link.angle = angle if link.from == none { link.from = link-molecule-index( @@ -769,6 +748,12 @@ ) } } + if link.from == -1 { + link.from = 0 + } + if link.to == -1 { + link.to = ctx.hooks.at(link.to-name).count - 1 + } let start = molecule-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) let end = molecule-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) ((start, end), utils.angle-between(cetz-ctx, start, end)) @@ -780,7 +765,7 @@ utils.angle-between( cetz-ctx, link.from-pos, - (name: link.to-name, anchor: "center"), + (name: link.to-name, anchor: "mid"), ), ) link.to = link-molecule-index( @@ -795,7 +780,7 @@ utils.angle-between( cetz-ctx, link.from-pos, - (name: link.to-name, anchor: (str(link.to), "center")), + (name: link.to-name, anchor: (str(link.to), "mid")), ), ) } @@ -900,7 +885,7 @@ } #let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { - let config = utils.merge-dictionaries(config, default) + let config = utils.merge-dictionaries(config, default) let ctx = default-ctx ctx.angle = config.base-angle ctx.config = config @@ -917,9 +902,9 @@ } else { group( name: name, - anchor: mol-anchor, + anchor: mol-anchor, { - anchor("default", (0,0)) + anchor("default", (0, 0)) atoms links cetz-drawing diff --git a/packages/preview/alchemist/0.1.2/src/links.typ b/packages/preview/alchemist/0.1.2/src/links.typ index e5f89d9dc6..79ea70a653 100644 --- a/packages/preview/alchemist/0.1.2/src/links.typ +++ b/packages/preview/alchemist/0.1.2/src/links.typ @@ -239,13 +239,15 @@ /// }) ///```) /// It is possible to change the stroke of the lines in the arrow -/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// with the `stroke` argument. You can also change the arrow base length with the `base-length` argument and the tip length with the `tip-length` argument. +// You can also change distance between the dashes with the `dash-gap` argument /// #example(``` /// #skeletize({ /// molecule("A") /// cram-dashed-right( /// stroke: red + 2pt, /// base-length: 2em, +/// tip-length: 1em, /// dash-gap: .5em /// ) /// molecule("B") From 68048a4aa35769f54dad8e2237b40a989e95feaf Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Wed, 13 Nov 2024 14:56:44 +0100 Subject: [PATCH 05/24] updated changelog --- packages/preview/alchemist/0.1.2/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/preview/alchemist/0.1.2/README.md b/packages/preview/alchemist/0.1.2/README.md index d635a7c865..a572eafaea 100644 --- a/packages/preview/alchemist/0.1.2/README.md +++ b/packages/preview/alchemist/0.1.2/README.md @@ -88,7 +88,8 @@ For more information, check the [manual](https://raw.githubusercontent.com/Robot ### 0.1.2 - Added default values for link style properties. -- Updated `cetz` to version 0.3.1 +- Updated `cetz` to version 0.3.1. +- Added a `tip-lenght` argument to dashed cram links. ### 0.1.1 From 9c7a496ca307802851e71ea317a438882ed605d6 Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Sun, 15 Dec 2024 17:06:31 +0100 Subject: [PATCH 06/24] Upload alchemist v0.1.3 --- packages/preview/alchemist/0.1.3/LICENSE | 22 + packages/preview/alchemist/0.1.3/README.md | 105 ++ packages/preview/alchemist/0.1.3/lib.typ | 144 +++ .../preview/alchemist/0.1.3/src/default.typ | 31 + .../preview/alchemist/0.1.3/src/drawer.typ | 926 ++++++++++++++++++ .../preview/alchemist/0.1.3/src/links.typ | 293 ++++++ .../preview/alchemist/0.1.3/src/molecule.typ | 67 ++ .../preview/alchemist/0.1.3/src/utils.typ | 84 ++ packages/preview/alchemist/0.1.3/typst.toml | 12 + 9 files changed, 1684 insertions(+) create mode 100644 packages/preview/alchemist/0.1.3/LICENSE create mode 100644 packages/preview/alchemist/0.1.3/README.md create mode 100644 packages/preview/alchemist/0.1.3/lib.typ create mode 100644 packages/preview/alchemist/0.1.3/src/default.typ create mode 100644 packages/preview/alchemist/0.1.3/src/drawer.typ create mode 100644 packages/preview/alchemist/0.1.3/src/links.typ create mode 100644 packages/preview/alchemist/0.1.3/src/molecule.typ create mode 100644 packages/preview/alchemist/0.1.3/src/utils.typ create mode 100644 packages/preview/alchemist/0.1.3/typst.toml diff --git a/packages/preview/alchemist/0.1.3/LICENSE b/packages/preview/alchemist/0.1.3/LICENSE new file mode 100644 index 0000000000..25dc239757 --- /dev/null +++ b/packages/preview/alchemist/0.1.3/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.3/README.md b/packages/preview/alchemist/0.1.3/README.md new file mode 100644 index 0000000000..67ccdfec53 --- /dev/null +++ b/packages/preview/alchemist/0.1.3/README.md @@ -0,0 +1,105 @@ +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + molecule(name: "A", "A") + single() + molecule("B") + branch({ + single(angle: 1) + molecule( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + molecule(name: "X", "X") + }) + branch({ + single(angle: -1) + molecule("Y") + single() + molecule( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + molecule( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Robotechnic/alchemist/master/images/links1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + molecule("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Robotechnic/alchemist/master/images/cetz1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.1.3": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). + +## Changelog + +### 0.1.3 + +- Added the possibility to add exponent in the string of a molecule. + +### 0.1.2 + +- Added default values for link style properties. +- Updated `cetz` to version 0.3.1. +- Added a `tip-lenght` argument to dashed cram links. + +### 0.1.1 + +- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. +- Fixed multiples bugs that causes overdraws of links. + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.1.3/lib.typ b/packages/preview/alchemist/0.1.3/lib.typ new file mode 100644 index 0000000000..90bafd2bf4 --- /dev/null +++ b/packages/preview/alchemist/0.1.3/lib.typ @@ -0,0 +1,144 @@ +#import "@preview/cetz:0.3.1" +#import "src/default.typ": default +#import "src/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ" : skeletize, draw-skeleton +#import "src/links.typ" : * +#import "src/molecule.typ" : * + +#let transparent = color.rgb(100%,0,0,0) + +/// === Molecule function +/// Build a molecule group based on mol +/// Each molecule is represented as an optional count followed by a molecule name +/// starting by a capital letter followed by an optional exponent followed by an optional indice. +/// #example(``` +/// #skeletize({ +/// molecule("H_2O") +/// }) +///```) +/// #example(``` +/// #skeletize({ +/// molecule("H^A_EF^5_4") +/// }) +/// ```) +/// It is possible to use an equation as a molecule. In this case, the splitting of the equation uses the same rules as in the string case. However, you get some advantages over the string version: +/// - You can use parenthesis to group elements together. +/// - You have no restriction about what you can put in exponent or indice. +/// #example(``` +/// #skeletize({ +/// molecule($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the molecule. It is used as the cetz name of the molecule and to link other molecules to it. +/// - links (dictionary): The links between this molecule and previous molecules or hooks. The key is the name of the molecule or hook and the value is the link function. +/// +/// Note that the atom-sep and angle arguments are ignored +/// - mol (string, equation): The string representing the molecule or an equation of the molecule +/// - vertical (boolean): If true, the molecule is drawn vertically +/// #example(``` +/// #skeletize({ +/// molecule("ABCD", vertical: true) +/// }) +///```) +/// -> drawable +#let molecule(name: none, links: (:), vertical: false, mol) = { + let atoms = if type(mol) == str { + split-string(mol) + } else if mol.func() == math.equation { + split-equation(mol, equation: true) + } else { + panic("Invalid molecule content") + } + ( + ( + type: "molecule", + name: name, + atoms: atoms, + links: links, + vertical: vertical, + count: atoms.len(), + ), + ) +} + +/// === Hooks +/// Create a hook in the molecule. It allows tu connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the molecule. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current molecule, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// branch({ +/// single(angle:1) +/// molecule("B") +/// }) +/// branch({ +/// double(angle: -1) +/// molecule("D") +/// }) +/// single() +/// double() +/// single() +/// molecule("C") +/// }) +///```) +#let branch(..args) = { + if args.pos().len() != 1 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", draw: args.pos().at(0), args: args.named()),) +} + +/// Create a regular cycle of molecules +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +#let cycle(..args) = { + if args.pos().len() != 2 { + panic("Cycle takes two positional arguments: number of faces and body") + } + let faces = args.pos().at(0) + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + draw: args.pos().at(1), + args: args.named(), + ), + ) +} diff --git a/packages/preview/alchemist/0.1.3/src/default.typ b/packages/preview/alchemist/0.1.3/src/default.typ new file mode 100644 index 0000000000..aeae5a372a --- /dev/null +++ b/packages/preview/alchemist/0.1.3/src/default.typ @@ -0,0 +1,31 @@ +#let default = ( + atom-sep: 3em, + delta: 0.2em, + angle-increment: 45deg, + base-angle: 0deg, + debug: false, + single: ( + stroke: black + ), + double: ( + gap: .25em, + offset: "center", + offset-coeff: 0.85, + stroke: black + ), + triple: ( + gap: .25em, + stroke: black + ), + filled-cram: ( + stroke: none, + fill: black, + base-length: .8em + ), + dashed-cram: ( + stroke: black + .05em, + dash-gap: .3em, + base-length: .8em, + tip-length: .1em + ), +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.3/src/drawer.typ b/packages/preview/alchemist/0.1.3/src/drawer.typ new file mode 100644 index 0000000000..48a8fe38dc --- /dev/null +++ b/packages/preview/alchemist/0.1.3/src/drawer.typ @@ -0,0 +1,926 @@ +#import "default.typ": default +#import "@preview/cetz:0.3.1" +#import "utils.typ" +#import cetz.draw: * + +#let default-anchor = (type: "coord", anchor: (0, 0)) +#let max-int = 9223372036854775807 + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + group-id: 0, // id of the current group + link-id: 0, // id of the current link + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + + // branch + first-branch: false, // true if the next element is the first in a branch + + // cycle + first-molecule: none, // name of the first molecule in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices +) + +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + } + (..ctx, last-anchor: anchor) +} + +/// Return the index to choose if the link connection is not overridden +#let link-molecule-index(angle, end, count, vertical) = { + if not end { + if vertical and utils.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if utils.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and utils.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if utils.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let molecule-link-anchor(name, id, count) = { + if count <= id { + panic("The last molecule only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "mid")) +} + +#let link-molecule-anchor(name: none, id, count) = { + if id >= count { + panic("This molecule only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the molecule to link to must be defined") + } + if name == none { + (name: str(id), anchor: "mid") + } else { + (name: name, anchor: (str(id), "mid")) + } +} + +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +/// Draw a triangle between two molecules +#let cram(from, to, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.filled-cram.base-length), + ) + line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: ctx.config.filled-cram.stroke), + fill: args.at("fill", default: ctx.config.filled-cram.fill), + ) +} + +/// Draw a dashed triangle between two molecules +#let dashed-cram(from, to, length, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.dashed-cram.base-length), + ) + let tip-length = utils.convert-length( + cetz-ctx, + args.at("tip-length", default: ctx.config.dashed-cram.tip-length), + ) + hide({ + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - tip-length / 2)) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + tip-length / 2), + ) + }) + let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) + let dash-gap = utils.convert-length(cetz-ctx, args.at("dash-gap", default: ctx.config.dashed-cram.dash-gap)) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(cetz-ctx, dash-width) + let length = utils.convert-length(cetz-ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} + +#let draw-molecule-text(mol) = { + for (id, eq) in mol.atoms.enumerate() { + let name = str(id) + // draw atoms of the group one after the other from left to right + content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "mid-west" + }, + ( + if id == 0 { + (0, 0) + } else if mol.vertical { + (to: str(id - 1) + ".south", rel: (0, -.2em)) + } else { + str(id - 1) + ".mid-east" + } + ), + { + show math.equation: math.upright + eq + }, + ) + id += 1 + } +} + +#let draw-molecule(mol, ctx) = { + let name = mol.name + if name != none { + if name in ctx.hooks { + panic("Molecule with name " + name + " already exists") + } + ctx.hooks.insert(name, mol) + } else { + name = "molecule" + str(ctx.group-id) + } + let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("west", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-molecule-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let group-anchor = link-molecule-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (group-anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule must be linked to a coord or a link") + } + ctx = set-last-anchor( + ctx, + (type: "molecule", name: name, count: mol.at("count"), vertical: mol.vertical), + ) + ctx.group-id += 1 + ( + ctx, + { + group( + anchor: if side { + group-anchor + } else { + "from" + str(ctx.group-id) + }, + name: name, + { + set-origin(coord) + anchor("default", (0, 0)) + draw-molecule-text(mol) + if not side { + anchor("from" + str(ctx.group-id), group-anchor) + } + }, + ) + }, + ) +} + +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} + +#let draw-link(link, ctx) = { + let link-angle = 0deg + let to-name = none + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-molecule != none { + to-name = ctx.first-molecule + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = utils.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let from-pos = if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "molecule" { + from-connection = link-molecule-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + molecule-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name", default: "link" + str(ctx.link-id)) + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = set-last-anchor( + ctx, + ( + type: "link", + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: to-name, + to: to-connection, + angle: link-angle, + draw: link.draw, + ), + ) + ctx.link-id += 1 + ( + ctx, + { + let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + line(from-pos, end-anchor, stroke: blue + .1em) + } + group( + name: link-name + "-end-anchor", + { + anchor("default", end-anchor) + }, + ) + }, + ) +} + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + 0, + )) + } + vertex +} + +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius) +} + +#let draw-cycle-center-arc(ctx, name, center-arc) = { + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius) = cycle-center-radius(ctx, cetz-ctx, vertex) + if name != none { + group( + name: name, + { + anchor("default", center) + }, + ) + } + if center-arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= center-arc.at("radius", default: 0.7) + } else { + min-radius *= center-arc.at("radius", default: 0.5) + } + let start = center-arc.at("start", default: 0deg) + let end = center-arc.at("end", default: 360deg) + let delta = center-arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + arc( + center, + ..center-arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) + +} + +#let draw-hooks-links(links, name, ctx, from-mol) = { + for (to-name, (link,)) in links { + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "molecule" { + ctx.links.push(( + type: "link", + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: if from-mol { + (name: name, anchor: "mid") + } else { + name + "-end-anchor" + }, + from-name: if from-mol { + name + }, + to-name: to-name, + from: none, + to: none, + override: angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: if from-mol { + (name: name, anchor: "mid") + } else { + name + "-end-anchor" + }, + from-name: if from-mol { + name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + ctx.link-id += 1 + } + ctx +} + +#let update-parent-context(parent-ctx, ctx) = { + let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { + ( + ..parent-ctx.last-anchor, + drew: true, + ) + } else { + parent-ctx.last-anchor + } + ( + ..parent-ctx, + last-anchor: last-anchor, + hooks: ctx.hooks, + hooks-links: ctx.hooks-links, + links: ctx.links, + group-id: ctx.group-id, + link-id: ctx.link-id, + ) +} + +#let draw-molecules-and-link(ctx, body) = { + let drawing = () + let cetz-drawing = () + ( + { + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + if type(element) == function { + cetz-drawing.push(element) + } else if "type" not in element { + panic("Element " + str(element) + " has no type") + } else if element.type == "molecule" { + if ctx.first-branch { + panic("A molecule can not be the first element in a cycle") + } + (ctx, drawing) = draw-molecule(element, ctx) + drawing + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + } else if element.type == "link" { + ctx.first-branch = false + (ctx, drawing) = draw-link(element, ctx) + ctx.faces-count += 1 + drawing + if element.links.len() != 0 { + ctx.hooks-links.push((element.links, ctx.last-anchor.name, false)) + } + } else if element.type == "branch" { + let angle = angle-from-ctx(ctx, element.args, cycle-angle(ctx)) + let (drawing, branch-ctx, cetz-rec) = draw-molecules-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + element.draw, + ) + ctx = update-parent-context(ctx, branch-ctx) + cetz-drawing += cetz-rec + drawing + } else if element.type == "cycle" { + let cycle-step-angle = 360deg / element.faces + let angle = angle-from-ctx(ctx, element.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ctx.relative-angle == 0deg and ctx.angle == 0deg and not element.args.at( + "align", + default: false, + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-molecule = none + if ctx.last-anchor.type == "molecule" { + first-molecule = ctx.last-anchor.name + if first-molecule not in ctx.hooks { + ctx.hooks.insert(first-molecule, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in element.args { + name = element.args.at("name") + record-vertex = true + } else if "arc" in element.args { + record-vertex = true + } + let (drawing, cycle-ctx, cetz-rec) = draw-molecules-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: element.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-molecule: first-molecule, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + element.draw, + ) + ctx = update-parent-context(ctx, cycle-ctx) + cetz-drawing += cetz-rec + drawing + if record-vertex { + draw-cycle-center-arc(cycle-ctx, name, element.args.at("arc", default: none)) + } + } else if element.type == "hook" { + if element.name in ctx.hooks { + panic("Hook " + element.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert(element.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(element.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + } else { + panic("Unknown element type " + element.type) + } + } + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + } + }, + ctx, + cetz-drawing, + ) +} + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +#let molecule-anchor(ctx, cetz-ctx, angle, molecule, id) = { + let delta = utils.convert-length(cetz-ctx, ctx.config.delta) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "mid")), + ) + + let (a, b) = if utils.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, delta, molecule, id) + } else if utils.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, delta, molecule, id) + } else if utils.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, delta, molecule, id) + } else { + anchor-south-east(cetz-ctx, center, delta, molecule, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if utils.angle-in-range-inclusive(angle, 0deg, 90deg) or utils.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "mid") + if link.to == none or link.from == none { + let angle = link.at( + "angle", + default: utils.angle-between(cetz-ctx, link.from-pos, to-pos), + ) + link.angle = angle + if link.from == none { + link.from = link-molecule-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-molecule-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + if link.from == -1 { + link.from = 0 + } + if link.to == -1 { + link.to = ctx.hooks.at(link.to-name).count - 1 + } + let start = molecule-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) + let end = molecule-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) + ((start, end), utils.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = utils.angle-correction( + utils.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "mid"), + ), + ) + link.to = link-molecule-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = utils.angle-correction( + utils.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "mid")), + ), + ) + } + let end-anchor = molecule-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + utils.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + molecule-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = utils.angle-correction( + utils.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) + let from = link-molecule-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = molecule-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) + ( + ( + start-anchor, + hook.hook, + ), + utils.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + utils.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + +#let draw-link-decoration(ctx) = { + ( + get-ctx(cetz-ctx => { + for link in ctx.links { + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = utils.distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + group({ + set-origin(from) + rotate(angle) + (link.draw)(length, ctx, cetz-ctx, override: link.override) + }) + } + }), + ctx, + ) +} + +#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { + let config = utils.merge-dictionaries(config, default) + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + let (atoms, ctx, cetz-drawing) = draw-molecules-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + let (links, _) = draw-link-decoration(ctx) + + if name == none { + atoms + links + cetz-drawing + } else { + group( + name: name, + anchor: mol-anchor, + { + anchor("default", (0, 0)) + atoms + links + cetz-drawing + }, + ) + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas( + debug: debug, + background: background, + draw-skeleton(config: config, body), + ) +} diff --git a/packages/preview/alchemist/0.1.3/src/links.typ b/packages/preview/alchemist/0.1.3/src/links.typ new file mode 100644 index 0000000000..79ea70a653 --- /dev/null +++ b/packages/preview/alchemist/0.1.3/src/links.typ @@ -0,0 +1,293 @@ +#import "@preview/cetz:0.3.1" +#import "drawer.typ" +#import "utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes three arguments: the length of the link, the context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, cetz-ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, cetz-ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// single() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// single(stroke: red + 5pt) +/// molecule("B") +/// }) +///```) +#let single = build-link((length, ctx, _, args) => { + import cetz.draw: * + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.single.stroke)) +}) + +/// Draw a double line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// molecule("B") +/// }) +///```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double(offset: "right") +/// molecule("B") +/// double(offset: "left") +/// molecule("C") +/// double(offset: "center") +/// molecule("D") +/// }) +///```) +#let double = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.double.gap)) / 2 + let offset = args.at("offset", default: ctx.config.double.offset) + let coeff = args.at("offset-coeff", default: ctx.config.double.offset-coeff) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + if offset == "right" { + translate((0, -gap)) + } else if offset == "left" { + translate((0, gap)) + } else if offset == "center" { } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + translate((0, -gap)) + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) + translate((0, 2 * gap)) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) +}) + +/// Draw a triple line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// triple() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// molecule("B") +/// }) +///```) +#let triple = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.triple.gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + translate((0, -gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + translate((0, 2 * gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) +}) + +/// Draw a filled cram between two molecules with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-right() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, cetz-ctx, args) => drawer.cram( + (0, 0), + (length, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a filled cram between two molecules with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-left() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, cetz-ctx, args) => drawer.cram( + (length, 0), + (0, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a hollow cram between two molecules with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + drawer.cram((0, 0), (length, 0), ctx, cetz-ctx, args) +}) + +/// Draw a hollow cram between two molecules with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + drawer.cram((length, 0), (0, 0), ctx, cetz-ctx, args) +}) + +/// Draw a dashed cram between two molecules with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-right() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the arrow base length with the `base-length` argument and the tip length with the `tip-length` argument. +// You can also change distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// tip-length: 1em, +/// dash-gap: .5em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-dashed-right = build-link((length, ctx, cetz-ctx, args) => drawer.dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + cetz-ctx, + args, +)) + +/// Draw a dashed cram between two molecules with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-left() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, cetz-ctx, args) => drawer.dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + cetz-ctx, + args, +)) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.3/src/molecule.typ b/packages/preview/alchemist/0.1.3/src/molecule.typ new file mode 100644 index 0000000000..b742e14ea1 --- /dev/null +++ b/packages/preview/alchemist/0.1.3/src/molecule.typ @@ -0,0 +1,67 @@ +#let split-equation(mol, equation: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + for m in mol { + let last-number-hold = last-number + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + result.push(m) + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in molecule") + } + last-number = true + result.push(m) + } else { + panic("Invalid molecule content") + } + } else if m.func() == math.attach or m.func() == math.lr { + result.push(m) + } else if m == [ ] { + continue + } else { + panic("Invalid molecule content") + } + if last-number-hold { + result.at(-2) = result.at(-2) + result.at(-1) + let _ = result.pop() + last-number = false + } + } + result +} + +#let split-string(mol) = { + let aux(str) = { + let match = str.match(regex("^ *([0-9]*[A-Z][a-z]*)(\\^[0-9]+|\\^[A-Z])?(_[0-9]+|_[A-Z])?")) + if match == none { + panic(str + " is not a valid atom") + } + let eq = "\"" + match.captures.at(0) + "\"" + if match.captures.len() >= 2 { + eq += match.captures.at(1) + } + if match.captures.len() >= 3 { + eq += match.captures.at(2) + } + let eq = math.equation(eval(eq, mode: "math")) + (eq, match.end) + } + + while not mol.len() == 0 { + let (eq, end) = aux(mol) + mol = mol.slice(end) + (eq,) + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.3/src/utils.typ b/packages/preview/alchemist/0.1.3/src/utils.typ new file mode 100644 index 0000000000..374373c288 --- /dev/null +++ b/packages/preview/alchemist/0.1.3/src/utils.typ @@ -0,0 +1,84 @@ +#import "@preview/cetz:0.3.1" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + return if type(num) == length { + float(num.to-absolute() / ctx.length) + } else if type(num) == ratio { + num + } else { + float(num) + } +} + +/// Convert any angle to an angle between -360deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a = a + 360 + } + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} + +/// merge two imbricated dictionaries together +/// The second dictionary is the default value if the key is not present in the first dictionary +#let merge-dictionaries(dict1, default) = { + let result = default + for (key, value) in dict1 { + if type(value) == dictionary { + result.at(key) = merge-dictionaries(value, default.at(key)) + } else { + result.at(key) = value + } + } + result +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.3/typst.toml b/packages/preview/alchemist/0.1.3/typst.toml new file mode 100644 index 0000000000..ba13753f1c --- /dev/null +++ b/packages/preview/alchemist/0.1.3/typst.toml @@ -0,0 +1,12 @@ +[package] +name = "alchemist" +version = "0.1.3" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.11.0" +repository = "https://github.com/Robotechnic/alchemist" +description = "A package to render skeletal formulas using cetz" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization"] +disciplines = ["chemistry", "biology", ] From 72c31872601aa690b4a2cba3c4f4fd0148c700ed Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Thu, 30 Jan 2025 18:49:08 +0100 Subject: [PATCH 07/24] Upload alchemist v0.1.4 --- packages/preview/alchemist/0.1.4/LICENSE | 22 ++ packages/preview/alchemist/0.1.4/README.md | 110 +++++++ packages/preview/alchemist/0.1.4/lib.typ | 206 ++++++++++++ .../preview/alchemist/0.1.4/src/default.typ | 54 ++++ .../preview/alchemist/0.1.4/src/drawer.typ | 200 ++++++++++++ .../alchemist/0.1.4/src/drawer/branch.typ | 32 ++ .../alchemist/0.1.4/src/drawer/cram.typ | 61 ++++ .../alchemist/0.1.4/src/drawer/cycle.typ | 160 ++++++++++ .../alchemist/0.1.4/src/drawer/hook.typ | 14 + .../alchemist/0.1.4/src/drawer/link.typ | 116 +++++++ .../alchemist/0.1.4/src/drawer/molecule.typ | 133 ++++++++ .../0.1.4/src/drawer/parenthesis.typ | 161 ++++++++++ .../alchemist/0.1.4/src/elements/lewis.typ | 133 ++++++++ .../alchemist/0.1.4/src/elements/links.typ | 292 +++++++++++++++++ .../alchemist/0.1.4/src/elements/molecule.typ | 67 ++++ .../alchemist/0.1.4/src/utils/anchors.typ | 296 ++++++++++++++++++ .../alchemist/0.1.4/src/utils/angles.typ | 98 ++++++ .../alchemist/0.1.4/src/utils/context.typ | 35 +++ .../alchemist/0.1.4/src/utils/utils.typ | 61 ++++ packages/preview/alchemist/0.1.4/typst.toml | 12 + 20 files changed, 2263 insertions(+) create mode 100644 packages/preview/alchemist/0.1.4/LICENSE create mode 100644 packages/preview/alchemist/0.1.4/README.md create mode 100644 packages/preview/alchemist/0.1.4/lib.typ create mode 100644 packages/preview/alchemist/0.1.4/src/default.typ create mode 100644 packages/preview/alchemist/0.1.4/src/drawer.typ create mode 100644 packages/preview/alchemist/0.1.4/src/drawer/branch.typ create mode 100644 packages/preview/alchemist/0.1.4/src/drawer/cram.typ create mode 100644 packages/preview/alchemist/0.1.4/src/drawer/cycle.typ create mode 100644 packages/preview/alchemist/0.1.4/src/drawer/hook.typ create mode 100644 packages/preview/alchemist/0.1.4/src/drawer/link.typ create mode 100644 packages/preview/alchemist/0.1.4/src/drawer/molecule.typ create mode 100644 packages/preview/alchemist/0.1.4/src/drawer/parenthesis.typ create mode 100644 packages/preview/alchemist/0.1.4/src/elements/lewis.typ create mode 100644 packages/preview/alchemist/0.1.4/src/elements/links.typ create mode 100644 packages/preview/alchemist/0.1.4/src/elements/molecule.typ create mode 100644 packages/preview/alchemist/0.1.4/src/utils/anchors.typ create mode 100644 packages/preview/alchemist/0.1.4/src/utils/angles.typ create mode 100644 packages/preview/alchemist/0.1.4/src/utils/context.typ create mode 100644 packages/preview/alchemist/0.1.4/src/utils/utils.typ create mode 100644 packages/preview/alchemist/0.1.4/typst.toml diff --git a/packages/preview/alchemist/0.1.4/LICENSE b/packages/preview/alchemist/0.1.4/LICENSE new file mode 100644 index 0000000000..25dc239757 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.4/README.md b/packages/preview/alchemist/0.1.4/README.md new file mode 100644 index 0000000000..4475891299 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/README.md @@ -0,0 +1,110 @@ +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + molecule(name: "A", "A") + single() + molecule("B") + branch({ + single(angle: 1) + molecule( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + molecule(name: "X", "X") + }) + branch({ + single(angle: -1) + molecule("Y") + single() + molecule( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + molecule( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Robotechnic/alchemist/master/images/links1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + molecule("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Robotechnic/alchemist/master/images/cetz1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.1.4": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). + +## Changelog + +### 0.1.4 + +- Added the possibility to crate lewis formulae +- Added parenthesis element to create groups and polymer + +### 0.1.3 + +- Added the possibility to add exponent in the string of a molecule. + +### 0.1.2 + +- Added default values for link style properties. +- Updated `cetz` to version 0.3.1. +- Added a `tip-lenght` argument to dashed cram links. + +### 0.1.1 + +- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. +- Fixed multiples bugs that causes overdraws of links. + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.1.4/lib.typ b/packages/preview/alchemist/0.1.4/lib.typ new file mode 100644 index 0000000000..17bc6d31ff --- /dev/null +++ b/packages/preview/alchemist/0.1.4/lib.typ @@ -0,0 +1,206 @@ +#import "@preview/cetz:0.3.1" +#import "src/default.typ": default +#import "src/utils/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ": skeletize, draw-skeleton +#import "src/elements/links.typ": * +#import "src/elements/molecule.typ": * +#import "src/elements/lewis.typ": * + +#let transparent = color.rgb(100%, 0, 0, 0) + +/// === Molecule function +/// Build a molecule group based on mol +/// Each molecule is represented as an optional count followed by a molecule name +/// starting by a capital letter followed by an optional exponent followed by an optional indice. +/// #example(``` +/// #skeletize({ +/// molecule("H_2O") +/// }) +///```) +/// #example(``` +/// #skeletize({ +/// molecule("H^A_EF^5_4") +/// }) +/// ```) +/// It is possible to use an equation as a molecule. In this case, the splitting of the equation uses the same rules as in the string case. However, you get some advantages over the string version: +/// - You can use parenthesis to group elements together. +/// - You have no restriction about what you can put in exponent or indice. +/// #example(``` +/// #skeletize({ +/// molecule($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the molecule. It is used as the cetz name of the molecule and to link other molecules to it. +/// - links (dictionary): The links between this molecule and previous molecules or hooks. The key is the name of the molecule or hook and the value is the link function. See @links. +/// +/// Note that the atom-sep and angle arguments are ignored +/// - lewis (list): The list of lewis structures to draw around the molecules. See @lewis +/// - mol (string, equation): The string representing the molecule or an equation of the molecule +/// - vertical (boolean): If true, the molecule is drawn vertically +/// #example(``` +/// #skeletize({ +/// molecule("ABCD", vertical: true) +/// }) +///```) +/// -> drawable +#let molecule(name: none, links: (:), lewis: (), vertical: false, mol) = { + let atoms = if type(mol) == str { + split-string(mol) + } else if mol.func() == math.equation { + split-equation(mol, equation: true) + } else { + panic("Invalid molecule content") + } + + if type(lewis) != array { + panic("Lewis formulae elements must be in a list") + } + + ( + ( + type: "molecule", + name: name, + atoms: atoms, + links: links, + lewis: lewis, + vertical: vertical, + count: atoms.len(), + ), + ) +} + +/// === Hooks +/// Create a hook in the molecule. It allows to connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the molecule. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current molecule, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// branch({ +/// single(angle:1) +/// molecule("B") +/// }) +/// branch({ +/// double(angle: -1) +/// molecule("D") +/// }) +/// single() +/// double() +/// single() +/// molecule("C") +/// }) +///```) +/// - body (drawable): the body of the branch. It must start with a link. +/// -> drawable +#let branch(body, ..args) = { + if args.pos().len() != 0 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", body: body, args: args.named()),) +} + +/// Create a regular cycle of molecules +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +/// - faces (int): the number of faces of the cycle +/// - body (drawable): the body of the cycle. It must start and end with a molecule or a link. +/// -> drawable +#let cycle(faces, body, ..args) = { + if args.pos().len() != 0 { + panic("Cycle takes two positional arguments: number of faces and body") + } + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + body: body, + args: args.named(), + ), + ) +} + +/// === Parenthesis +/// Encapsulate a drawable between two parenthesis. The left parenthesis is placed at the left of the first element of the body and by default the right parenthesis is placed at the right of the last element of the body. +/// +/// #example(``` +/// #skeletize( +/// config: ( +/// angle-increment: 30deg +/// ), { +/// parenthesis( +/// l:"[", r:"]", +/// br: $n$, { +/// single(angle: 1) +/// single(angle: -1) +/// single(angle: 1) +/// }) +/// }) +/// ```) +/// For more examples, see @examples +/// +/// - body (drawable): the body of the parenthesis. It must start and end with a molecule or a link. +/// - l (string): the left parenthesis +/// - r (string): the right parenthesis +/// - align (true): if true, the parenthesis will have the same y position. They will also get sized and aligned according to the body height. If false, they are not aligned and the height argument must be specified. +/// +/// - height (float, length): the height of the parenthesis. If align is true, this argument is optional. +/// - yoffset (float, length, list): the vertical offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - xoffset (float, length, list): the horizontal offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - right (string): Sometime, it is not convenient to place the right parenthesis at the end of the body. In this case, you can specify the name of the molecule or link where the right parenthesis should be placed. It is especially useful when the body end by a cycle. See @polySulfide +/// - tr (content): the exponent content of the right parenthesis +/// - br (content): the indice content of the right parenthesis +/// -> drawable +#let parenthesis(body, l: "(", r: ")", align: true, height: none, yoffset: none, xoffset: none, right: none, tr: none, br: none) = { + ( + ( + type: "parenthesis", + body: body, + calc: calc, + l: l, + r: r, + align: align, + tr: tr, + br: br, + height: height, + xoffset: xoffset, + yoffset: yoffset, + right: right, + ), + ) +} diff --git a/packages/preview/alchemist/0.1.4/src/default.typ b/packages/preview/alchemist/0.1.4/src/default.typ new file mode 100644 index 0000000000..d0a1d6379e --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/default.typ @@ -0,0 +1,54 @@ +#let default = ( + atom-sep: 3em, + molecule-margin: 0.2em, + angle-increment: 45deg, + base-angle: 0deg, + debug: false, + single: ( + stroke: black + ), + double: ( + gap: .25em, + offset: "center", + offset-coeff: 0.85, + stroke: black + ), + triple: ( + gap: .25em, + stroke: black + ), + filled-cram: ( + stroke: none, + fill: black, + base-length: .8em + ), + dashed-cram: ( + stroke: black + .05em, + dash-gap: .3em, + base-length: .8em, + tip-length: .1em + ), + lewis-single: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + offset: "top" + ), + lewis-double: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + ), + lewis-line: ( + stroke: black, + length: .7em, + ), + lewis-rectangle: ( + stroke: .08em + black, + fill: white, + height: .7em, + width: .3em + ) +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.4/src/drawer.typ b/packages/preview/alchemist/0.1.4/src/drawer.typ new file mode 100644 index 0000000000..03401dd328 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/drawer.typ @@ -0,0 +1,200 @@ +#import "default.typ": default +#import "@preview/cetz:0.3.1" +#import "utils/utils.typ": * +#import "utils/anchors.typ": * +#import "drawer/molecule.typ" as molecule +#import "drawer/link.typ" as link +#import "drawer/branch.typ" as branch +#import "drawer/cycle.typ" as cycle +#import "drawer/parenthesis.typ" as parenthesis +#import "drawer/hook.typ" as hook + +#import cetz.draw: * + +#let default-anchor = (type: "coord", anchor: (0, 0)) + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + group-id: 0, // id of the current group + link-id: 0, // id of the current link + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + // branch + first-branch: false, // true if the next element is the first in a branch + // cycle + first-molecule: none, // name of the first molecule in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices +) + +#let draw-hooks-links(links, name, ctx, from-mol) = { + for (to-name, (link,)) in links { + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "molecule" { + ctx.links.push(( + type: "link", + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: if from-mol { + (name: name, anchor: "mid") + } else { + name + "-end-anchor" + }, + from-name: if from-mol { + name + }, + to-name: to-name, + from: none, + to: none, + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + name: link.at("name", default: "link" + str(ctx.link-id)), + from-pos: if from-mol { + (name: name, anchor: "mid") + } else { + name + "-end-anchor" + }, + from-name: if from-mol { + name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + ctx.link-id += 1 + } + ctx +} + +#let draw-molecules-and-link(ctx, body) = { + let molecule-drawing = () + let parenthesis-drawing = () + let cetz-drawing = () + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + let drawing = () + let parenthesis-drawing-rec = () + let cetz-rec = () + if type(element) == function { + cetz-drawing.push(element) + } else if "type" not in element { + panic("Element " + str(element) + " has no type") + } else if element.type == "molecule" { + (ctx, drawing) = molecule.draw-molecule(element, ctx) + } else if element.type == "link" { + (ctx, drawing) = link.draw-link(element, ctx) + } else if element.type == "branch" { + (ctx, drawing, parenthesis-drawing-rec, cetz-rec) = branch.draw-branch(element, ctx, draw-molecules-and-link) + } else if element.type == "cycle" { + (ctx, drawing, parenthesis-drawing-rec, cetz-rec) = cycle.draw-cycle(element, ctx, draw-molecules-and-link) + } else if element.type == "hook" { + ctx = hook.draw-hook(element, ctx) + } else if element.type == "parenthesis" { + (ctx, drawing, parenthesis-drawing-rec, cetz-rec) = parenthesis.draw-parenthesis(element, ctx, draw-molecules-and-link) + } else { + panic("Unknown element type " + element.type) + } + molecule-drawing += drawing + cetz-drawing += cetz-rec + parenthesis-drawing += parenthesis-drawing-rec + } + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + ctx.last-anchor.drew = true + } + ( + ctx, + molecule-drawing, + parenthesis-drawing, + cetz-drawing, + ) +} + +#let draw-link-decoration(ctx) = { + ( + ctx, + get-ctx(cetz-ctx => { + for link in ctx.links { + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + scope({ + set-origin(from) + rotate(angle) + (link.draw)(length, ctx, cetz-ctx, override: link.override) + }) + } + }), + ) +} + +#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { + let config = merge-dictionaries(config, default) + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + let (ctx, atoms, parenthesis, cetz-drawing) = draw-molecules-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + let links = draw-link-decoration(ctx).at(1) + + if name == none { + atoms + links + parenthesis + cetz-drawing + } else { + group( + name: name, + anchor: mol-anchor, + { + anchor("default", (0, 0)) + atoms + links + parenthesis + cetz-drawing + }, + ) + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas( + debug: debug, + background: background, + draw-skeleton(config: config, body), + ) +} diff --git a/packages/preview/alchemist/0.1.4/src/drawer/branch.typ b/packages/preview/alchemist/0.1.4/src/drawer/branch.typ new file mode 100644 index 0000000000..f0420b47ed --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/drawer/branch.typ @@ -0,0 +1,32 @@ +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +/// Compute the angle the branch should take in order to be "in the center" +/// of the cycle angle +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +#let draw-branch(branch, ctx, draw-molecules-and-link) = { + let angle = angles.angle-from-ctx(ctx, branch.args, cycle-angle(ctx)) + let (branch-ctx, drawing, parenthesis-drawing-rec, cetz-rec) = draw-molecules-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + branch.body, + ) + ctx = context_.update-parent-context(ctx, branch-ctx) + (ctx, drawing, parenthesis-drawing-rec, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.4/src/drawer/cram.typ b/packages/preview/alchemist/0.1.4/src/drawer/cram.typ new file mode 100644 index 0000000000..5fc933a601 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/drawer/cram.typ @@ -0,0 +1,61 @@ +#import "@preview/cetz:0.3.2" +#import "../utils/utils.typ" + +/// Draw a triangle between two molecules +#let cram(from, to, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.filled-cram.base-length), + ) + cetz.draw.line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: ctx.config.filled-cram.stroke), + fill: args.at("fill", default: ctx.config.filled-cram.fill), + ) +} + +/// Draw a dashed triangle between two molecules +#let dashed-cram(from, to, length, ctx, cetz-ctx, args) = { + import cetz.draw: * + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.dashed-cram.base-length), + ) + let tip-length = utils.convert-length( + cetz-ctx, + args.at("tip-length", default: ctx.config.dashed-cram.tip-length), + ) + hide({ + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - tip-length / 2)) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + tip-length / 2), + ) + }) + let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) + let dash-gap = utils.convert-length(cetz-ctx, args.at("dash-gap", default: ctx.config.dashed-cram.dash-gap)) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(cetz-ctx, dash-width) + let length = utils.convert-length(cetz-ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.4/src/drawer/cycle.typ b/packages/preview/alchemist/0.1.4/src/drawer/cycle.typ new file mode 100644 index 0000000000..5c35b4562f --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/drawer/cycle.typ @@ -0,0 +1,160 @@ +#import "@preview/cetz:0.3.2" +#import "../utils/utils.typ" +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +#let max-int = 9223372036854775807 + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + 0, + )) + } + vertex +} + +/// extrapolate the center and the radius of the cycle +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + let debug = () + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + debug += cetz.draw.circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius, debug) +} + +#let draw-cycle-center-arc(ctx, name, center-arc) = { + import cetz.draw: * + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius, debug) = cycle-center-radius(ctx, cetz-ctx, vertex) + debug + if name != none { + group( + name: name, + { + anchor("default", center) + }, + ) + } + if center-arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= center-arc.at("radius", default: 0.7) + } else { + min-radius *= center-arc.at("radius", default: 0.5) + } + let start = center-arc.at("start", default: 0deg) + let end = center-arc.at("end", default: 360deg) + let delta = center-arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + arc( + center, + ..center-arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) +} + +#let draw-cycle(cycle, ctx, draw-molecules-and-link) = { + let cycle-step-angle = 360deg / cycle.faces + let angle = angles.angle-from-ctx(ctx, cycle.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ( + ctx.relative-angle == 0deg + and ctx.angle == 0deg + and not cycle.args.at( + "align", + default: false, + ) + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-molecule = none + if ctx.last-anchor.type == "molecule" { + first-molecule = ctx.last-anchor.name + if first-molecule not in ctx.hooks { + ctx.hooks.insert(first-molecule, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in cycle.args { + name = cycle.args.at("name") + record-vertex = true + } else if "arc" in cycle.args { + record-vertex = true + } + let (cycle-ctx, drawing, parenthesis-drawing-rec, cetz-rec) = draw-molecules-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: cycle.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-molecule: first-molecule, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + cycle.body, + ) + ctx = context_.update-parent-context(ctx, cycle-ctx) + if record-vertex { + drawing += draw-cycle-center-arc(cycle-ctx, name, cycle.args.at("arc", default: none)) + } + (ctx, drawing, parenthesis-drawing-rec, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.4/src/drawer/hook.typ b/packages/preview/alchemist/0.1.4/src/drawer/hook.typ new file mode 100644 index 0000000000..1c5dafb9d1 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/drawer/hook.typ @@ -0,0 +1,14 @@ + +#let draw-hook(hook, ctx) = { + if hook.name in ctx.hooks { + panic("Hook " + hook.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + ctx +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.4/src/drawer/link.typ b/packages/preview/alchemist/0.1.4/src/drawer/link.typ new file mode 100644 index 0000000000..ef9b56cc48 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/drawer/link.typ @@ -0,0 +1,116 @@ +#import "../utils/angles.typ" +#import "../utils/anchors.typ": * + +#let create-link-decorations-anchors(link, ctx) = { + let link-angle = 0deg + let to-name = none + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-molecule != none { + to-name = ctx.first-molecule + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angles.angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = angles.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angles.angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let init-point = false + let from-pos = if ctx.last-anchor.type == "coord" { + init-point = true + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "molecule" { + from-connection = link-molecule-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + molecule-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name", default: "link" + str(ctx.link-id)) + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = context_.set-last-anchor( + ctx, + ( + type: "link", + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: to-name, + to: to-connection, + angle: link-angle, + draw: link.draw, + ), + ) + ctx.link-id += 1 + ( + ctx, + { + if init-point { + hide( + { + circle(from-pos, radius: .25em) + }, + bounds: true, + ) + } + let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + line(from-pos, end-anchor, stroke: blue + .1em) + } + group( + name: link-name + "-end-anchor", + { + anchor("default", end-anchor) + hide( + { + circle(end-anchor, radius: .25em) + }, + bounds: true, + ) + }, + ) + }, + ) +} + +#let draw-link(link, ctx) = { + ctx.first-branch = false + let drawing + (ctx, drawing) = create-link-decorations-anchors(link, ctx) + ctx.faces-count += 1 + if link.links.len() != 0 { + ctx.hooks-links.push((link.links, ctx.last-anchor.name, false)) + } + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.4/src/drawer/molecule.typ b/packages/preview/alchemist/0.1.4/src/drawer/molecule.typ new file mode 100644 index 0000000000..5e9788b27f --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/drawer/molecule.typ @@ -0,0 +1,133 @@ +#import "../utils/context.typ": * +#import "../utils/anchors.typ": * +#import "@preview/cetz:0.3.2" + +#let draw-molecule-text(mol) = { + import cetz.draw: * + for (id, eq) in mol.atoms.enumerate() { + let name = str(id) + // draw atoms of the group one after the other from left to right + content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "mid-west" + }, + ( + if id == 0 { + (0, 0) + } else if mol.vertical { + (to: str(id - 1) + ".south", rel: (0, -.2em)) + } else { + str(id - 1) + ".mid-east" + } + ), + auto-scale: false, + { + show math.equation: math.upright + eq + }, + ) + id += 1 + } +} + +#let draw-molecule-lewis(ctx, group-name, count, lewis) = { + if lewis.len() == 0 { + return () + } + import cetz.draw: * + get-ctx(cetz-ctx => { + for (id, (angle: lewis-angle, molecule-margin, draw)) in lewis.enumerate() { + let lewis-angle = angles.angle-correction(lewis-angle) + let mol-id = if angles.angle-in-range-inclusive(lewis-angle, 90deg, 270deg) { + 0 + } else { + count - 1 + } + let anchor = molecule-anchor( + ctx, + cetz-ctx, + lewis-angle, + group-name, + str(mol-id), + margin: molecule-margin, + ) + scope({ + set-origin(anchor) + rotate(lewis-angle) + draw(ctx, cetz-ctx) + }) + } + }) +} + +#let draw-molecule-elements(mol, ctx) = { + let name = mol.name + if name != none { + if name in ctx.hooks { + panic("Molecule with name " + name + " already exists") + } + ctx.hooks.insert(name, mol) + } else { + name = "molecule" + str(ctx.group-id) + } + let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("west", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-molecule-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let group-anchor = link-molecule-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (group-anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule must be linked to a coord or a link") + } + ctx = context_.set-last-anchor( + ctx, + (type: "molecule", name: name, count: mol.at("count"), vertical: mol.vertical), + ) + ctx.group-id += 1 + ( + ctx, + { + import cetz.draw: * + group( + anchor: if side { + group-anchor + } else { + "from" + str(ctx.group-id) + }, + name: name, + { + set-origin(coord) + anchor("default", (0, 0)) + draw-molecule-text(mol) + if not side { + anchor("from" + str(ctx.group-id), group-anchor) + } + }, + ) + draw-molecule-lewis(ctx, name, mol.count, mol.at("lewis")) + }, + ) +} + +#let draw-molecule(element, ctx) = { + if ctx.first-branch { + panic("A molecule can not be the first element in a cycle") + } + let (ctx, drawing) = draw-molecule-elements(element, ctx) + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + (ctx, drawing) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.4/src/drawer/parenthesis.typ b/packages/preview/alchemist/0.1.4/src/drawer/parenthesis.typ new file mode 100644 index 0000000000..baa64cb75a --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/drawer/parenthesis.typ @@ -0,0 +1,161 @@ +#import "../utils/utils.typ" +#import "@preview/cetz:0.3.2" + +#let bounding-box-height(bounds) = { + calc.abs(bounds.high.at(1) - bounds.low.at(1)) +} + +#let bounding-box-width(bounds) = { + calc.abs(bounds.high.at(0) - bounds.low.at(0)) +} + +#let left-parenthesis-anchor(parenthesis, ctx) = { + let anchor = if parenthesis.body.at(0).type == "molecule" { + let name = parenthesis.body.at(0).name + if name == none { + name = "molecule" + str(ctx.group-id) + } + ctx.group-id += 1 + parenthesis.body.at(0).name = name + (name: name, anchor: "west") + } else if parenthesis.body.at(0).type == "link" { + let name = parenthesis.body.at(0).at("name", default: "link" + str(ctx.link-id)) + ctx.link-id += 1 + parenthesis.body.at(0).name = name + (name: name, anchor: 50%) + } else { + panic("The first element of a parenthesis must be a molecule or a link") + } + (ctx, parenthesis, anchor) +} + +#let right-parenthesis-anchor(parenthesis, ctx) = { + let right-name = parenthesis.at("right") + let right-type = "" + if right-name != none { + right-type = utils.get-element-type(parenthesis.body, right-name) + if right-type == none { + panic("The right element of the parenthesis does not exist") + } + } else { + right-type = parenthesis.body.at(-1).type + if right-type == "molecule" { + right-name = "molecule" + str(ctx.group-id) + ctx.group-id += 1 + parenthesis.body.at(-1).name = right-name + } else if right-type == "link" { + right-name = "link" + str(ctx.link-id) + ctx.link-id += 1 + parenthesis.body.at(-1).name = right-name + } + } + + let anchor = if right-type == "molecule" { + (name: right-name, anchor: "east") + } else if right-type == "link" { + (name: right-name, anchor: 50%) + } else { + panic("The last element of a parenthesis must be a molecule or a link but got " + right-type) + } + (ctx, parenthesis, anchor) +} + +#let draw-parenthesis(parenthesis, ctx, draw-molecules-and-link) = { + let (ctx, parenthesis, left-anchor) = left-parenthesis-anchor(parenthesis, ctx) + let (ctx, parenthesis, right-anchor) = right-parenthesis-anchor(parenthesis, ctx) + + let (parenthesis-ctx, drawing, parenthesis-rec, cetz-rec) = draw-molecules-and-link( + ctx, + parenthesis.body, + ) + ctx = parenthesis-ctx + parenthesis-rec += { + import cetz.draw: * + get-ctx(cetz-ctx => { + let sub-bounds = cetz.process.many(cetz-ctx, { + set-transform(none) + drawing + }).bounds + + let sub-height = bounding-box-height(sub-bounds) + let sub-v-mid = sub-bounds.low.at(1) + sub-height / 2 + + let sub-width = bounding-box-width(sub-bounds) + + let height = parenthesis.at("height") + if height == none { + if not parenthesis.align { + panic("You must specify the height of the parenthesis if they are not aligned") + } + height = sub-height + } else { + height = utils.convert-length(cetz-ctx, height) + } + let block = block(height: height * cetz-ctx.length * 1.2, width: 0pt) + let left-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($parenthesis.l block$, size: 100%) + } + let right-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($block parenthesis.r$, size: 100%) + } + + let right-parenthesis-with-attach = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.attach(right-parenthesis, br: parenthesis.br, tr: parenthesis.tr) + } + + let (_, (lx, ly, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, left-anchor) + let (_, (rx, ry, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, right-anchor) + + if ctx.config.debug { + circle((lx, ly), radius: 1pt, fill: orange, stroke: orange) + circle((rx, ry), radius: 1pt, fill: orange, stroke: orange) + rect(sub-bounds.low, sub-bounds.high, stroke: orange) + } + + let hoffset = calc.abs(sub-width - calc.abs(rx - lx)) + + if parenthesis.align { + ly += calc.abs(ly - sub-v-mid) + ry += calc.abs(ry - sub-v-mid) + ry = ly + } else if type(parenthesis.yoffset) == array { + if parenthesis.yoffset.len() != 2 { + panic("The parenthesis yoffset must be a list of length 2 or a number") + } + ly += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(0)) + ry += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(1)) + } else if parenthesis.yoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.yoffset) + ly += offset + ry += offset + } + + if type(parenthesis.xoffset) == array { + if parenthesis.xoffset.len() != 2 { + panic("The parenthesis xoffset must be a list of length 2 or a number") + } + lx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(0)) + rx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(1)) + } else if parenthesis.xoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.xoffset) + lx -= offset + rx += offset + } + + let right-bounds = cetz.process.many(cetz-ctx, content((0,0),right-parenthesis, auto-scale: false)).bounds + let right-with-attach-bounds = cetz.process.many(cetz-ctx, content((0,0),right-parenthesis-with-attach, auto-scale: false)).bounds + let right-voffset = calc.abs(right-bounds.low.at(1) - right-with-attach-bounds.low.at(1)) + if (parenthesis.tr != none and parenthesis.br != none) { + right-voffset /= 2 + } else if (parenthesis.tr != none) { + right-voffset *= -1 + } + content((lx , ly), anchor: "mid-east", left-parenthesis, auto-scale: false) + content((rx, ry - right-voffset), anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }) + } + (ctx, drawing, parenthesis-rec, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.4/src/elements/lewis.typ b/packages/preview/alchemist/0.1.4/src/elements/lewis.typ new file mode 100644 index 0000000000..d33067b2a8 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/elements/lewis.typ @@ -0,0 +1,133 @@ +#import "@preview/cetz:0.3.1" + +/// Create a lewis function that is then used to draw a lewis +/// formulae element around the molecule +/// +/// - draw-function (function): The function that will be used to draw the lewis element. It should takes three arguments: the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-lewis(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Lewis function takes no positional arguments") + } + let args = args.named() + let angle = args.at("angle", default: 0) + let molecule-margin = args.at("molecule-margin", default: none) + ( + angle: angle, + molecule-margin: molecule-margin, + draw: (ctx, cetz-ctx) => draw-function(ctx, cetz-ctx, args) + ) + } +} + +/// draw a sigle electron around the molecule +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// +/// The position of the electron is set by the `offset` argument. Available values are: +/// - "top": the electron is placed above the molecule center line +/// - "bottom": the electron is placed below the molecule center line +/// - "center": the electron is placed at the molecule center line +/// +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// molecule("A", lewis:( +/// lewis-single(offset: "top"), +/// )) +/// single(angle:-2) +/// molecule("B", lewis:( +/// lewis-single(offset: "bottom"), +/// )) +/// single(angle:-2) +/// molecule("C", lewis:( +/// lewis-single(offset: "center"), +/// )) +/// }) +/// ```) +#let lewis-single = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-single.radius) + let gap = args.at("gap", default: ctx.config.lewis-single.gap) + let offset = args.at("offset", default: ctx.config.lewis-single.offset) + if offset == "top" { + translate((0, gap)) + } else if offset == "bottom" { + translate((0, -gap)) + } else if offset != "center" { + panic("Invalid position, expected 'top', 'bottom' or 'center'") + } + let fill = args.at("fill", default: ctx.config.lewis-single.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-single.stroke) + circle((0, 0), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron around the molecule +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// molecule("A", lewis:( +/// lewis-double(), +/// lewis-double(angle: 90deg), +/// lewis-double(angle: 180deg), +/// lewis-double(angle: -90deg) +/// )) +/// }) +/// ```) +#let lewis-double = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-double.radius) + let gap = args.at("gap", default: ctx.config.lewis-double.gap) + let fill = args.at("fill", default: ctx.config.lewis-double.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-double.stroke) + circle((0, -gap), radius: radius, fill: fill, stroke: stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron liked by a single line +/// +/// It is possible to change the length of the line with the `lenght` argument. +/// It is also possible to change the `stroke` agument +/// #example(``` +/// #skeletize({ +/// molecule("B", lewis:( +/// lewis-line(angle: 45deg), +/// lewis-line(angle: 135deg), +/// lewis-line(angle: -45deg), +/// lewis-line(angle: -135deg) +/// )) +/// }) +/// ```) +#let lewis-line = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let length = args.at("length", default: ctx.config.lewis-line.length) + let stroke = args.at("stroke", default: ctx.config.lewis-line.stroke) + line((0, -length / 2), (0, length / 2), stroke: stroke) +}) + + +/// Draw a rectangle to denote a lone pair of electrons +/// +/// It is possible to change the height and width of the rectangle with the `height` and `width` arguments. +/// It is also possible to change the `fill` and `stroke` arguments +/// #example(``` +/// #skeletize({ +/// molecule("C", lewis:( +/// lewis-rectangle(), +/// lewis-rectangle(angle: 180deg) +/// )) +/// }) +/// ```) +#let lewis-rectangle = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let height = args.at("height", default: ctx.config.lewis-rectangle.height) + let width = args.at("width", default: ctx.config.lewis-rectangle.width) + let fill = args.at("fill", default: ctx.config.lewis-rectangle.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-rectangle.stroke) + rect((-width / 2, -height / 2), (width / 2, height / 2), fill: fill, stroke: stroke) +}) diff --git a/packages/preview/alchemist/0.1.4/src/elements/links.typ b/packages/preview/alchemist/0.1.4/src/elements/links.typ new file mode 100644 index 0000000000..b02a0402ed --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/elements/links.typ @@ -0,0 +1,292 @@ +#import "@preview/cetz:0.3.1" +#import "../drawer/cram.typ": * +#import "../utils/utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes four arguments: the length of the link, the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, cetz-ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, cetz-ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// single() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// single(stroke: red + 5pt) +/// molecule("B") +/// }) +///```) +#let single = build-link((length, ctx, _, args) => { + import cetz.draw: * + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.single.stroke)) +}) + +/// Draw a double line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// molecule("B") +/// }) +///```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// double(offset: "right") +/// molecule("B") +/// double(offset: "left") +/// molecule("C") +/// double(offset: "center") +/// molecule("D") +/// }) +///```) +#let double = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.double.gap)) / 2 + let offset = args.at("offset", default: ctx.config.double.offset) + let coeff = args.at("offset-coeff", default: ctx.config.double.offset-coeff) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + if offset == "right" { + translate((0, -gap)) + } else if offset == "left" { + translate((0, gap)) + } else if offset == "center" { } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + translate((0, -gap)) + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) + translate((0, 2 * gap)) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) +}) + +/// Draw a triple line between two molecules +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// triple() +/// molecule("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// molecule("B") +/// }) +///```) +#let triple = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.triple.gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + translate((0, -gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + translate((0, 2 * gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) +}) + +/// Draw a filled cram between two molecules with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-right() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, cetz-ctx, args) => cram( + (0, 0), + (length, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a filled cram between two molecules with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-left() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, cetz-ctx, args) => cram( + (length, 0), + (0, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a hollow cram between two molecules with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((0, 0), (length, 0), ctx, cetz-ctx, args) +}) + +/// Draw a hollow cram between two molecules with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((length, 0), (0, 0), ctx, cetz-ctx, args) +}) + +/// Draw a dashed cram between two molecules with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-right() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the arrow base length with the `base-length` argument and the tip length with the `tip-length` argument. +// You can also change distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// tip-length: 1em, +/// dash-gap: .5em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-dashed-right = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + cetz-ctx, + args, +)) + +/// Draw a dashed cram between two molecules with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-left() +/// molecule("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// molecule("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// molecule("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + cetz-ctx, + args, +)) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.4/src/elements/molecule.typ b/packages/preview/alchemist/0.1.4/src/elements/molecule.typ new file mode 100644 index 0000000000..1fc5f788d9 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/elements/molecule.typ @@ -0,0 +1,67 @@ +#let split-equation(mol, equation: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + for m in mol { + let last-number-hold = last-number + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + result.push(m) + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in molecule") + } + last-number = true + result.push(m) + } else { + panic("Invalid molecule content") + } + } else if m.func() == math.attach or m.func() == math.lr { + result.push(m) + } else if m == [ ] { + continue + } else { + panic("Invalid molecule content") + } + if last-number-hold { + result.at(-2) = result.at(-2) + result.at(-1) + let _ = result.pop() + last-number = false + } + } + result +} + +#let split-string(mol) = { + let aux(str) = { + let match = str.match(regex("^ *([0-9]*[A-Z][a-z]*)(\\^[0-9]+|\\^[A-Z])?(_[0-9]+|_[A-Z])?")) + if match == none { + panic(str + " is not a valid atom") + } + let eq = "\"" + match.captures.at(0) + "\"" + if match.captures.len() >= 2 { + eq += match.captures.at(1) + } + if match.captures.len() >= 3 { + eq += match.captures.at(2) + } + let eq = math.equation(eval(eq, mode: "math")) + (eq, match.end) + } + + while not mol.len() == 0 { + let (eq, end) = aux(mol) + mol = mol.slice(end) + (eq,) + } +} diff --git a/packages/preview/alchemist/0.1.4/src/utils/anchors.typ b/packages/preview/alchemist/0.1.4/src/utils/anchors.typ new file mode 100644 index 0000000000..9eeda41b12 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/utils/anchors.typ @@ -0,0 +1,296 @@ +#import "angles.typ" +#import "context.typ" as context_ +#import "utils.typ": * +#import "@preview/cetz:0.3.2" +#import cetz.draw: * + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "east")), + ) + + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, molecule, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +/// Calculate an anchor position around a molecule using an ellipse +/// at a given angle +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - cetz-ctx (cetz-ctx): the cetz context +/// - angle (float, int, angle): the angle of the anchor +/// - molecule (string): the molecule name +/// - id (string): the molecule subpart id +/// - margin (length, none): the margin around the molecule +/// -> anchor: the anchor position around the molecule +#let molecule-anchor(ctx, cetz-ctx, angle, molecule, id, margin: none) = { + let angle = angles.angle-correction(angle) + let molecule-margin = if margin == none { + ctx.config.molecule-margin + } else { + margin + } + molecule-margin = convert-length(cetz-ctx, molecule-margin) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: molecule, anchor: (id, "mid")), + ) + let (a, b) = if angles.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, molecule-margin, molecule, id) + } else if angles.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, molecule-margin, molecule, id) + } else if angles.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, molecule-margin, molecule, id) + } else { + anchor-south-east(cetz-ctx, center, molecule-margin, molecule, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if angles.angle-in-range-inclusive(angle, 0deg, 90deg) or angles.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +/// Return the index to choose if the link connection is not overridden +#let link-molecule-index(angle, end, count, vertical) = { + if not end { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let molecule-link-anchor(name, id, count) = { + if count <= id { + panic("The last molecule only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "mid")) +} + +#let link-molecule-anchor(name: none, id, count) = { + if id >= count { + panic("This molecule only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the molecule to link to must be defined") + } + if name == none { + (name: str(id), anchor: "mid") + } else { + (name: name, anchor: (str(id), "mid")) + } +} + + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "mid") + if link.to == none or link.from == none { + let angle = link.at( + "angle", + default: angles.angle-between(cetz-ctx, link.from-pos, to-pos), + ) + link.angle = angle + if link.from == none { + link.from = link-molecule-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-molecule-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + if link.from == -1 { + link.from = 0 + } + if link.to == -1 { + link.to = ctx.hooks.at(link.to-name).count - 1 + } + let start = molecule-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) + let end = molecule-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) + ((start, end), angles.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "mid"), + ), + ) + link.to = link-molecule-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "mid")), + ), + ) + } + let end-anchor = molecule-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + angles.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + molecule-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = angles.angle-correction(angles.angle-between(cetz-ctx, link.from-pos, hook.hook)) + let from = link-molecule-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = molecule-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) + ( + ( + start-anchor, + hook.hook, + ), + angles.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + angles.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + diff --git a/packages/preview/alchemist/0.1.4/src/utils/angles.typ b/packages/preview/alchemist/0.1.4/src/utils/angles.typ new file mode 100644 index 0000000000..f51218c795 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/utils/angles.typ @@ -0,0 +1,98 @@ +#import "@preview/cetz:0.3.2" + +/// Convert any angle to an angle between 0deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a += 360 + } + + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +/// Check if the angle is in the range ]from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +/// Check if the angle is in the range [from, to] +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +/// +/// - ctx (cetz-ctx): The cetz context +/// - from (anchor): The first anchor +/// - to (anchor): The second anchor +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// Calculate the angle from an object with "relative", "absolute" or "angle" key +/// according to the alchemist context (see the manual to see how angles are calculated) +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - object (dict): the object to get the angle from +/// - default (angle): the default angle +/// -> the calculated angle +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +/// Overwrite the offset based on the angle +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} diff --git a/packages/preview/alchemist/0.1.4/src/utils/context.typ b/packages/preview/alchemist/0.1.4/src/utils/context.typ new file mode 100644 index 0000000000..d83a114a83 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/utils/context.typ @@ -0,0 +1,35 @@ +#let update-parent-context(parent-ctx, ctx) = { + let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { + ( + ..parent-ctx.last-anchor, + drew: true, + ) + } else { + parent-ctx.last-anchor + } + ( + ..parent-ctx, + last-anchor: last-anchor, + hooks: ctx.hooks, + hooks-links: ctx.hooks-links, + links: ctx.links, + group-id: ctx.group-id, + link-id: ctx.link-id, + ) +} + +/// Set the last anchor in the context to the given anchor and save it if needed +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" { + let drew = ctx.last-anchor.at("drew", default: false) + if drew and anchor.type == "link" and anchor.name == ctx.last-anchor.name { + drew = false + let _ = ctx.links.pop() + } + if not drew { + ctx.links.push(ctx.last-anchor) + } + ctx.last-anchor.drew = true + } + (..ctx, last-anchor: anchor) +} diff --git a/packages/preview/alchemist/0.1.4/src/utils/utils.typ b/packages/preview/alchemist/0.1.4/src/utils/utils.typ new file mode 100644 index 0000000000..318550a064 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/src/utils/utils.typ @@ -0,0 +1,61 @@ +#import "@preview/cetz:0.3.1" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + return if type(num) == length { + float(num.to-absolute() / ctx.length) + } else if type(num) == ratio { + num + } else { + float(num) + } +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} + +/// merge two imbricated dictionaries together +/// The second dictionary is the default value if the key is not present in the first dictionary +#let merge-dictionaries(dict1, default) = { + let result = default + for (key, value) in dict1 { + if type(value) == dictionary { + result.at(key) = merge-dictionaries(value, default.at(key)) + } else { + result.at(key) = value + } + } + result +} + + +/// get the type of an element by its name +/// +/// - body (drawable): the chemfig body of a molecule +/// - name (string): the name of the element to get the type +/// -> string +#let get-element-type(body, name) = { + for element in body { + if type(element) != dictionary { + continue + } + if "name" in element and element.name == name { + return element.type + } + if element.type == "branch" or element.type == "cycle" or element.type == "parenthesis" { + let type = get-element-type(element.body, name) + if type != none { + return type + } + } + } + none +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.4/typst.toml b/packages/preview/alchemist/0.1.4/typst.toml new file mode 100644 index 0000000000..9ed91d51a6 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/typst.toml @@ -0,0 +1,12 @@ +[package] +name = "alchemist" +version = "0.1.4" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.11.0" +repository = "https://github.com/Robotechnic/alchemist" +description = "A package to render skeletal formulas using cetz" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization"] +disciplines = ["chemistry", "biology", ] From 0d2ab5cbf457d473d970ed5df9dd7bdcc679bdc8 Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Thu, 30 Jan 2025 18:54:02 +0100 Subject: [PATCH 08/24] fixed the licence and a typo --- packages/preview/alchemist/0.1.4/LICENSE | 2 +- packages/preview/alchemist/0.1.4/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/preview/alchemist/0.1.4/LICENSE b/packages/preview/alchemist/0.1.4/LICENSE index 25dc239757..a85204e590 100644 --- a/packages/preview/alchemist/0.1.4/LICENSE +++ b/packages/preview/alchemist/0.1.4/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Robotechnic +Copyright (c) 2025 Robotechnic Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/packages/preview/alchemist/0.1.4/README.md b/packages/preview/alchemist/0.1.4/README.md index 4475891299..240ca80b99 100644 --- a/packages/preview/alchemist/0.1.4/README.md +++ b/packages/preview/alchemist/0.1.4/README.md @@ -87,7 +87,7 @@ For more information, check the [manual](https://raw.githubusercontent.com/Robot ### 0.1.4 -- Added the possibility to crate lewis formulae +- Added the possibility to create Lewis formulae - Added parenthesis element to create groups and polymer ### 0.1.3 From 07906da6532f361256c49b7943f468f5ec80660b Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Thu, 5 Jun 2025 16:39:23 +0200 Subject: [PATCH 09/24] Upload alchemist v0.1.6 --- .../preview/alchemist/0.1.6/0.1.6/LICENSE | 22 ++ .../preview/alchemist/0.1.6/0.1.6/README.md | 126 +++++++ .../preview/alchemist/0.1.6/0.1.6/lib.typ | 255 ++++++++++++++ .../alchemist/0.1.6/0.1.6/src/default.typ | 58 ++++ .../alchemist/0.1.6/0.1.6/src/drawer.typ | 319 ++++++++++++++++++ .../0.1.6/0.1.6/src/drawer/branch.typ | 32 ++ .../alchemist/0.1.6/0.1.6/src/drawer/cram.typ | 61 ++++ .../0.1.6/0.1.6/src/drawer/cycle.typ | 160 +++++++++ .../0.1.6/0.1.6/src/drawer/fragment.typ | 148 ++++++++ .../alchemist/0.1.6/0.1.6/src/drawer/hook.typ | 14 + .../alchemist/0.1.6/0.1.6/src/drawer/link.typ | 108 ++++++ .../0.1.6/0.1.6/src/drawer/operator.typ | 36 ++ .../0.1.6/0.1.6/src/drawer/parenthesis.typ | 215 ++++++++++++ .../0.1.6/0.1.6/src/elements/fragment.typ | 74 ++++ .../0.1.6/0.1.6/src/elements/lewis.typ | 133 ++++++++ .../0.1.6/0.1.6/src/elements/links.typ | 316 +++++++++++++++++ .../0.1.6/0.1.6/src/utils/anchors.typ | 296 ++++++++++++++++ .../0.1.6/0.1.6/src/utils/angles.typ | 98 ++++++ .../0.1.6/0.1.6/src/utils/context.typ | 33 ++ .../alchemist/0.1.6/0.1.6/src/utils/utils.typ | 75 ++++ .../preview/alchemist/0.1.6/0.1.6/typst.toml | 13 + 21 files changed, 2592 insertions(+) create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/LICENSE create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/README.md create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/lib.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/default.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/drawer.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/drawer/branch.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cram.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cycle.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/drawer/fragment.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/drawer/hook.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/drawer/link.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/drawer/operator.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/drawer/parenthesis.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/elements/fragment.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/elements/lewis.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/elements/links.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/utils/anchors.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/utils/angles.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/utils/context.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/src/utils/utils.typ create mode 100644 packages/preview/alchemist/0.1.6/0.1.6/typst.toml diff --git a/packages/preview/alchemist/0.1.6/0.1.6/LICENSE b/packages/preview/alchemist/0.1.6/0.1.6/LICENSE new file mode 100644 index 0000000000..a85204e590 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.6/0.1.6/README.md b/packages/preview/alchemist/0.1.6/0.1.6/README.md new file mode 100644 index 0000000000..0655e4f7ac --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/README.md @@ -0,0 +1,126 @@ +[![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FTypsium%2Falchemist%2Fmaster%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://github.com/Typsium/alchemist) +[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/alchemist/blob/master/LICENSE) +[![User Manual](https://img.shields.io/badge/manual-.pdf-purple)](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf) + +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + fragment(name: "A", "A") + single() + fragment("B") + branch({ + single(angle: 1) + fragment( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + fragment(name: "X", "X") + }) + branch({ + single(angle: -1) + fragment("Y") + single() + fragment( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + fragment( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic1/ref/1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + fragment("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic2/ref/1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.1.4": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). + +## Changelog + +### 0.1.6 + +- Fixed the parenthesis height to work with the new typst version +- Renamed `molecule` into `fragment` +- Added support for charges and apostrophes in the string of a molecule +- Fixed the parenthesis auto positioning and alignment +- Added a new operator element and a new parameter in parenthesis to write resonance formulae + +### 0.1.5 + +- Update to compiler 0.13.1 and Cetz 0.3.4 + +### 0.1.4 + +- Added the possibility to create Lewis formulae +- Added parenthesis element to create groups and polymer + +### 0.1.3 + +- Added the possibility to add exponent in the string of a molecule. + +### 0.1.2 + +- Added default values for link style properties. +- Updated `cetz` to version 0.3.1. +- Added a `tip-length` argument to dashed cram links. + +### 0.1.1 + +- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. +- Fixed multiples bugs that causes overdraws of links. + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.1.6/0.1.6/lib.typ b/packages/preview/alchemist/0.1.6/0.1.6/lib.typ new file mode 100644 index 0000000000..f332dcc3e3 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/lib.typ @@ -0,0 +1,255 @@ +#import "@preview/cetz:0.3.4" +#import "src/default.typ": default +#import "src/utils/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ": skeletize, draw-skeleton +#import "src/elements/links.typ": * +#import "src/elements/fragment.typ": * +#import "src/elements/lewis.typ": * + +#let transparent = color.rgb(100%, 0, 0, 0) + +/// === Fragment function +/// Build a fragment group based on mol +/// Each fragment is represented as an optional count followed by a fragment name +/// starting by a capital letter followed by an optional exponent followed by an optional indice. +/// #example(``` +/// #skeletize({ +/// fragment("H_2O") +/// }) +///```) +/// #example(``` +/// #skeletize({ +/// fragment("H^A_EF^5_4") +/// }) +/// ```) +/// It is possible to use an equation as a fragment. In this case, the splitting of the equation uses the same rules as in the string case. However, you get some advantages over the string version: +/// - You can use parenthesis to group elements together. +/// - You have no restriction about what you can put in exponent or indice. +/// #example(``` +/// #skeletize({ +/// fragment($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the fragment. It is used as the cetz name of the fragment and to link other fragments to it. +/// - links (dictionary): The links between this fragment and previous fragments or hooks. The key is the name of the fragment or hook and the value is the link function. See @links. +/// +/// Note that the atom-sep and angle arguments are ignored +/// - lewis (list): The list of lewis structures to draw around the fragments. See @lewis +/// - mol (string, equation): The string representing the fragment or an equation of the fragment +/// - vertical (boolean): If true, the fragment is drawn vertically +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", vertical: true) +/// }) +///```) +/// - colors (color|list): The color of the fragment. If a list is provided, it colors each group of the fragment with the corresponding color from right to left. If the number of colors is less than the number of groups, the last color is used for the remaining groups. If the number of colors is greater than the number of groups, the extra colors are ignored. +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", colors: (red, green, blue)) +/// single() +/// fragment("EFGH", colors: (orange)) +/// }) +/// ```) +/// -> drawable +#let fragment(name: none, links: (:), lewis: (), vertical: false, colors: none, mol) = { + let atoms = if type(mol) == str { + split-string(mol) + } else if mol.func() == math.equation { + split-equation(mol, equation: true) + } else { + panic("Invalid fragment content") + } + + if type(lewis) != array { + panic("Lewis formulae elements must be in a list") + } + + ( + ( + type: "fragment", + name: name, + atoms: atoms, + colors: colors, + links: links, + lewis: lewis, + vertical: vertical, + count: atoms.len(), + ), + ) +} +#let molecule(name: none, links: (:), lewis: (), vertical: false, mol) = fragment(name: name, links: links, lewis: lewis, vertical: vertical, mol) + +/// === Hooks +/// Create a hook in the fragment. It allows to connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the fragment. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current fragment, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// branch({ +/// single(angle:1) +/// fragment("B") +/// }) +/// branch({ +/// double(angle: -1) +/// fragment("D") +/// }) +/// single() +/// double() +/// single() +/// fragment("C") +/// }) +///```) +/// - body (drawable): the body of the branch. It must start with a link. +/// -> drawable +#let branch(body, ..args) = { + if args.pos().len() != 0 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", body: body, args: args.named()),) +} + +/// Create a regular cycle of fragments +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +/// - faces (int): the number of faces of the cycle +/// - body (drawable): the body of the cycle. It must start and end with a fragment or a link. +/// -> drawable +#let cycle(faces, body, ..args) = { + if args.pos().len() != 0 { + panic("Cycle takes two positional arguments: number of faces and body") + } + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + body: body, + args: args.named(), + ), + ) +} + +/// === Parenthesis +/// Encapsulate a drawable between two parenthesis. The left parenthesis is placed at the left of the first element of the body and by default the right parenthesis is placed at the right of the last element of the body. +/// +/// #example(``` +/// #skeletize( +/// config: ( +/// angle-increment: 30deg +/// ), { +/// parenthesis( +/// l:"[", r:"]", +/// br: $n$, { +/// single(angle: 1) +/// single(angle: -1) +/// single(angle: 1) +/// }) +/// }) +/// ```) +/// For more examples, see @examples +/// +/// - body (drawable): the body of the parenthesis. It must start and end with a fragment or a link. +/// - l (string): the left parenthesis +/// - r (string): the right parenthesis +/// - align (true): if true, the parenthesis will have the same y position. They will also get sized and aligned according to the body height. If false, they are not aligned and the height argument must be specified. +/// - resonance (boolean): if true, the parenthesis will be drawn in resonance mode. This means that the left and right parenthesis will be placed outside the molecule. Also, the parenthesis will be separated from the previous and next molecule. This can be true only if the parenthesis is the first element of the skeletal formula or if the previous element is an operator. See @resonance for more details. +/// - height (float, length): the height of the parenthesis. If align is true, this argument is optional. +/// - yoffset (float, length, list): the vertical offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - xoffset (float, length, list): the horizontal offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - right (string): Sometime, it is not convenient to place the right parenthesis at the end of the body. In this case, you can specify the name of the fragment or link where the right parenthesis should be placed. It is especially useful when the body end by a cycle. See @polySulfide +/// - tr (content): the exponent content of the right parenthesis +/// - br (content): the indice content of the right parenthesis +/// -> drawable +#let parenthesis(body, l: "(", r: ")", align: true, resonance: false, height: none, yoffset: none, xoffset: none, right: none, tr: none, br: none) = { + if l.len() > 2 { + panic("Left can be at most 2 characters") + } + let l = eval(l, mode: "math") + if r.len() > 2 { + panic("Right can be at most 2 characters") + } + let r = eval(r, mode: "math") + ( + ( + type: "parenthesis", + body: body, + calc: calc, + l: l, + r: r, + align: align, + tr: tr, + br: br, + height: height, + xoffset: xoffset, + yoffset: yoffset, + right: right, + resonance: resonance, + ), + ) +} + + +/// === Operator +/// Create an operator between two fragments. Creating an operator "reset" the placement of the next fragment. +/// This allow to add multiple molecules in the same skeletal formula. Without this, the next fragment would be placed at the end of the previous one. +/// An important point is that you can't use previous hooks to link two molecules separate by an operator. +/// This element is used in resonance structures (@resonance) and in some cases to put multiples molecules in the same skeletal formula (as you can set op to none). +/// +/// - op (content | string | none): The operator content. It can be a string or a content. A none value won't display anything. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// operator($->$, margin: 1em) +/// fragment("B") +/// }) +/// ```) +/// See @resonance for more examples. +/// - name (string): The name of the operator. +/// - margin (float, length): The margin between the operator and previous / next molecule. +/// -> drawable +#let operator(name: none, margin: 1em, op) = { + ( + ( + type: "operator", + name: name, + op: op, + margin: margin, + ), + ) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/default.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/default.typ new file mode 100644 index 0000000000..5f8c1ac004 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/default.typ @@ -0,0 +1,58 @@ +#let default = ( + atom-sep: 3em, + fragment-margin: 0.2em, + angle-increment: 45deg, + base-angle: 0deg, + debug: false, + single: ( + stroke: black + ), + double: ( + gap: .25em, + offset: "center", + offset-coeff: 0.85, + stroke: black + ), + triple: ( + gap: .25em, + stroke: black + ), + filled-cram: ( + stroke: none, + fill: black, + base-length: .8em + ), + dashed-cram: ( + stroke: black + .05em, + dash-gap: .3em, + base-length: .8em, + tip-length: .1em + ), + lewis: ( + angle: 0deg, + radius: 0.2em, + ), + lewis-single: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + offset: "top" + ), + lewis-double: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + ), + lewis-line: ( + stroke: black, + length: .7em, + ), + lewis-rectangle: ( + stroke: .08em + black, + fill: white, + height: .7em, + width: .3em + ) +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer.typ new file mode 100644 index 0000000000..908f9e626b --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer.typ @@ -0,0 +1,319 @@ +#import "default.typ": default +#import "@preview/cetz:0.3.4" +#import "utils/utils.typ": * +#import "utils/anchors.typ": * +#import "utils/utils.typ" as utils +#import "drawer/fragment.typ" as fragment +#import "drawer/link.typ" as link +#import "drawer/branch.typ" as branch +#import "drawer/cycle.typ" as cycle +#import "drawer/parenthesis.typ" as parenthesis +#import "drawer/hook.typ" as hook +#import "drawer/operator.typ" as operator + +#import cetz.draw: * + +#let default-anchor = (type: "coord", anchor: (0, 0)) + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + last-name: "", // last name used to draw + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + id: 0, // an id used to name things with an unique name + // branch + first-branch: false, // true if the next element is the first in a branch + // cycle + first-fragment: none, // name of the first fragment in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices +) + +#let draw-hooks-links(links, mol-name, ctx, from-mol) = { + let hook-id = 0 + for (to-name, (link,)) in links { + if link.at(mol-name, default: none) == none { + link.name = mol-name + "-hook-" + str(hook-id) + hook-id += 1 + } + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "fragment" { + ctx.links.push(( + type: "link", + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + from: none, + to: none, + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + } + ctx +} + +#let draw-fragments-and-link(ctx, body) = { + let fragment-drawing = () + let cetz-drawing = () + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + let drawing = () + let cetz-rec = () + if type(element) == function { + cetz-drawing.push(element) + } else if "type" not in element { + panic("Element " + repr(element) + " has no type") + } else if element.type == "fragment" { + (ctx, drawing) = fragment.draw-fragment(element, ctx) + } else if element.type == "link" { + (ctx, drawing) = link.draw-link(element, ctx) + } else if element.type == "branch" { + (ctx, drawing, cetz-rec) = branch.draw-branch(element, ctx, draw-fragments-and-link) + } else if element.type == "cycle" { + (ctx, drawing, cetz-rec) = cycle.draw-cycle(element, ctx, draw-fragments-and-link) + } else if element.type == "hook" { + ctx = hook.draw-hook(element, ctx) + } else if element.type == "parenthesis" { + (ctx, drawing, cetz-rec) = parenthesis.draw-parenthesis( + element, + ctx, + draw-fragments-and-link, + ) + } else if element.type == "operator" { + (ctx, drawing) = operator.draw-operator(element, fragment-drawing, ctx) + } else { + panic("Unknown element type " + element.type) + } + fragment-drawing += drawing + cetz-drawing += cetz-rec + } + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + ctx.last-anchor.drew = true + } + ( + ctx, + fragment-drawing, + cetz-drawing, + ) +} + +#let draw-link-decoration(ctx) = { + ( + ctx, + get-ctx(cetz-ctx => { + for link in ctx.links { + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + scope({ + set-origin(from) + rotate(angle) + (link.draw)(length, ctx, cetz-ctx, override: link.override) + }) + } + }), + ) +} + +/// set elements names and split the molecule into sub-groups +#let preprocessing(body, group-id: 0, link-id: 0, operator-id: 0) = { + let result = ((),) + for element in body { + if type(element) == dictionary { + if element.at("name", default: none) == none { + if element.type == "fragment" { + element.name = "fragment-" + str(group-id) + group-id += 1 + } else if element.type == "link" { + element.name = "link-" + str(link-id) + link-id += 1 + } else if element.type == "operator" { + element.name = "operator-" + str(operator-id) + operator-id += 1 + } + } + if element.at("body", default: none) != none { + let child-body + (child-body, group-id, link-id, operator-id) = preprocessing( + element.body, + group-id: group-id, + link-id: link-id, + operator-id: operator-id, + ) + if element.type == "parenthesis" and element.resonance { + element.body = child-body + } else { + element.body = child-body.at(0) + } + } + if element.type == "operator" or (element.type == "parenthesis" and element.resonance) { + result.push(element) + result.push(()) + } else { + result.at(-1).push(element) + } + } else { + result.at(-1).push(element) + } + } + (result, group-id, link-id, operator-id) +} + +#let draw-groups(ctx, bodies, after-operator: false) = { + ctx.last-name = "" + + let drawables = for (i, body) in bodies.enumerate() { + if type(body) == array { + if body.len() > 0 { + ctx.last-name = str(i) + get-ctx(cetz-ctx => { + let ctx = ctx + if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor = cetz.coordinate.resolve(cetz-ctx, ctx.last-anchor.anchor).at(1) + } + let last-anchor = ctx.last-anchor + let (ctx, atoms, cetz-drawing) = draw-fragments-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + let links = draw-link-decoration(ctx).at(1) + + let molecule = { + atoms + links + on-layer(2, cetz-drawing) + } + + group( + name: str(i), + { + if after-operator { + let (ctx: cetz-ctx, drawables, bounds: molecule-bounds) = cetz.process.many(cetz-ctx, atoms + links) + molecule-bounds = cetz.util.revert-transform(cetz-ctx.transform, molecule-bounds) + + let (_, origin-anchor) = cetz.coordinate.resolve(cetz-ctx, last-anchor.anchor) + ( + ( + cetz-ctx => ( + ctx: cetz-ctx, + drawables: cetz.drawable.apply-transform( + cetz.matrix.transform-translate( + origin-anchor.at(0) - molecule-bounds.low.at(0), + -origin-anchor.at(1) + + ( + molecule-bounds.low.at(1) + molecule-bounds.high.at(1) + ) + / 2, + 0, + ), + drawables, + ), + ) + ), + ) + } else { + molecule + } + }, + ) + }) + } + } else if body.type == "operator" { + let (op-ctx, drawable) = operator.draw-operator(body, ctx) + after-operator = true + ctx = op-ctx + drawable + } else if body.type == "parenthesis" { + let (parenthesis-ctx, drawable) = parenthesis.draw-resonance-parenthesis(body, draw-groups, ctx) + ctx = parenthesis-ctx + drawable + } else { + panic("Unexpected element type: " + body.type) + } + } + (drawables, ctx) +} + +#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { + let config = merge-dictionaries(config, default) + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + + let bodies = preprocessing(body).at(0) + let (final-drawing, ctx) = draw-groups(ctx, bodies) + + if name == none { + final-drawing + } else { + group( + name: name, + anchor: mol-anchor, + { + anchor("default", (0, 0)) + final-drawing + }, + ) + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas( + debug: debug, + background: background, + draw-skeleton(config: config, body), + ) +} diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/branch.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/branch.typ new file mode 100644 index 0000000000..2c9e062843 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/branch.typ @@ -0,0 +1,32 @@ +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +/// Compute the angle the branch should take in order to be "in the center" +/// of the cycle angle +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +#let draw-branch(branch, ctx, draw-fragments-and-link) = { + let angle = angles.angle-from-ctx(ctx, branch.args, cycle-angle(ctx)) + let (branch-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + branch.body, + ) + ctx = context_.update-parent-context(ctx, branch-ctx) + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cram.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cram.typ new file mode 100644 index 0000000000..e366fd5847 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cram.typ @@ -0,0 +1,61 @@ +#import "@preview/cetz:0.3.4" +#import "../utils/utils.typ" + +/// Draw a triangle between two fragments +#let cram(from, to, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.filled-cram.base-length), + ) + cetz.draw.line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: ctx.config.filled-cram.stroke), + fill: args.at("fill", default: ctx.config.filled-cram.fill), + ) +} + +/// Draw a dashed triangle between two molecules fragments +#let dashed-cram(from, to, length, ctx, cetz-ctx, args) = { + import cetz.draw: * + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.dashed-cram.base-length), + ) + let tip-length = utils.convert-length( + cetz-ctx, + args.at("tip-length", default: ctx.config.dashed-cram.tip-length), + ) + hide({ + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - tip-length / 2)) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + tip-length / 2), + ) + }) + let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) + let dash-gap = utils.convert-length(cetz-ctx, args.at("dash-gap", default: ctx.config.dashed-cram.dash-gap)) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(cetz-ctx, dash-width) + let length = utils.convert-length(cetz-ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cycle.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cycle.typ new file mode 100644 index 0000000000..32307c34bd --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cycle.typ @@ -0,0 +1,160 @@ +#import "@preview/cetz:0.3.4" +#import "../utils/utils.typ" +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +#let max-int = 9223372036854775807 + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + 0, + )) + } + vertex +} + +/// extrapolate the center and the radius of the cycle +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + let debug = () + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + debug += cetz.draw.circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius, debug) +} + +#let draw-cycle-center-arc(ctx, name, center-arc) = { + import cetz.draw: * + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius, debug) = cycle-center-radius(ctx, cetz-ctx, vertex) + debug + if name != none { + group( + name: name, + { + anchor("default", center) + }, + ) + } + if center-arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= center-arc.at("radius", default: 0.7) + } else { + min-radius *= center-arc.at("radius", default: 0.5) + } + let start = center-arc.at("start", default: 0deg) + let end = center-arc.at("end", default: 360deg) + let delta = center-arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + arc( + center, + ..center-arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) +} + +#let draw-cycle(cycle, ctx, draw-fragments-and-link) = { + let cycle-step-angle = 360deg / cycle.faces + let angle = angles.angle-from-ctx(ctx, cycle.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ( + ctx.relative-angle == 0deg + and ctx.angle == 0deg + and not cycle.args.at( + "align", + default: false, + ) + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-fragment = none + if ctx.last-anchor.type == "fragment" { + first-fragment = ctx.last-anchor.name + if first-fragment not in ctx.hooks { + ctx.hooks.insert(first-fragment, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in cycle.args { + name = cycle.args.at("name") + record-vertex = true + } else if "arc" in cycle.args { + record-vertex = true + } + let (cycle-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: cycle.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-fragment: first-fragment, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + cycle.body, + ) + ctx = context_.update-parent-context(ctx, cycle-ctx) + if record-vertex { + drawing += draw-cycle-center-arc(cycle-ctx, name, cycle.args.at("arc", default: none)) + } + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/fragment.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/fragment.typ new file mode 100644 index 0000000000..e22186a3ba --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/fragment.typ @@ -0,0 +1,148 @@ +#import "../utils/context.typ": * +#import "../utils/anchors.typ": * +#import "@preview/cetz:0.3.4" + +#let draw-fragment-text(mol) = { + import cetz.draw: * + for (id, eq) in mol.atoms.enumerate() { + let name = str(id) + let color = if mol.colors != none { + if type(mol.colors) == color { + mol.colors + } else { + mol.colors.at(calc.min(id, mol.colors.len() - 1)) + } + } else { + none + } + // draw atoms of the group one after the other from left to right + content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "mid-west" + }, + ( + if id == 0 { + (0, 0) + } else if mol.vertical { + (to: str(id - 1) + ".south", rel: (0, -.2em)) + } else { + str(id - 1) + ".mid-east" + } + ), + auto-scale: false, + { + show math.equation: math.upright + set text(fill: color) if color != none + eq + }, + ) + id += 1 + } +} + +#let draw-fragment-lewis(ctx, group-name, count, lewis) = { + if lewis.len() == 0 { + return () + } + import cetz.draw: * + get-ctx(cetz-ctx => { + for (id, (angle: lewis-angle, radius, draw)) in lewis.enumerate() { + if (lewis-angle == none) { + lewis-angle = ctx.config.lewis.angle + } + if (radius == none) { + radius = ctx.config.lewis.radius + } + let lewis-angle = angles.angle-correction(lewis-angle) + let mol-id = if angles.angle-in-range-inclusive(lewis-angle, 90deg, 270deg) { + 0 + } else { + count - 1 + } + let anchor = fragment-anchor( + ctx, + cetz-ctx, + lewis-angle, + group-name, + str(mol-id), + margin: radius, + ) + scope({ + set-origin(anchor) + rotate(lewis-angle) + draw(ctx, cetz-ctx) + }) + } + }) +} + +#let draw-fragment-elements(mol, ctx) = { + let name = mol.name + if name in ctx.hooks { + panic("Molecule fragment with name " + name + " already exists : " + ctx.hooks.keys().join(", ")) + } + ctx.hooks.insert(name, mol) + + let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("west", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-fragment-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let group-anchor = link-fragment-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (group-anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule fragment must be linked to a coord or a link") + } + ctx = context_.set-last-anchor( + ctx, + (type: "fragment", name: name, count: mol.at("count"), vertical: mol.vertical), + ) + if (side) { + ctx.id += 1 + } + ( + ctx, + { + import cetz.draw: * + group( + anchor: if side { + group-anchor + } else { + "from" + str(ctx.id) + }, + name: name, + { + set-origin(coord) + anchor("default", (0, 0)) + draw-fragment-text(mol) + if not side { + anchor("from" + str(ctx.id), group-anchor) + } + }, + ) + draw-fragment-lewis(ctx, name, mol.count, mol.at("lewis")) + }, + ) +} + +#let draw-fragment(element, ctx) = { + if ctx.first-branch { + panic("A molecule fragment can not be the first element in a cycle") + } + let (ctx, drawing) = draw-fragment-elements(element, ctx) + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + (ctx, drawing) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/hook.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/hook.typ new file mode 100644 index 0000000000..1c5dafb9d1 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/hook.typ @@ -0,0 +1,14 @@ + +#let draw-hook(hook, ctx) = { + if hook.name in ctx.hooks { + panic("Hook " + hook.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + ctx +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/link.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/link.typ new file mode 100644 index 0000000000..7fdc475225 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/link.typ @@ -0,0 +1,108 @@ +#import "../utils/angles.typ" +#import "../utils/anchors.typ": * + +#let create-link-decorations-anchors(link, ctx) = { + let link-angle = 0deg + let to-name = none + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-fragment != none { + to-name = ctx.first-fragment + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angles.angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = angles.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angles.angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let from-pos = if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "fragment" { + from-connection = link-fragment-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + fragment-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name") + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = context_.set-last-anchor( + ctx, + ( + type: "link", + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: to-name, + to: to-connection, + angle: link-angle, + draw: link.draw, + ), + ) + ( + ctx, + { + hide({ + circle(name: link-name + "-start-anchor", from-pos, radius: .25em) + }) + let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + line(from-pos, end-anchor, stroke: blue + .1em) + } + group( + name: link-name + "-end-anchor", + { + anchor("default", end-anchor) + hide( + { + circle(end-anchor, radius: .25em) + }, + bounds: true, + ) + }, + ) + }, + ) +} + +#let draw-link(link, ctx) = { + ctx.first-branch = false + let drawing + (ctx, drawing) = create-link-decorations-anchors(link, ctx) + ctx.faces-count += 1 + if link.links.len() != 0 { + ctx.hooks-links.push((link.links, ctx.last-anchor.name, false)) + } + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/operator.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/operator.typ new file mode 100644 index 0000000000..a4cd00f98a --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/operator.typ @@ -0,0 +1,36 @@ +#import "@preview/cetz:0.3.4": draw, process, util + +#let draw-operator(operator, ctx) = { + import draw: * + + let op-name = operator.name + ctx.last-anchor = (type: "coord", anchor: (rel: (operator.margin, 0), to: (name: op-name, anchor: "east"))) + + ( + ctx, + get-ctx(cetz-ctx => { + + let west-previous-mol-anchor = (name: ctx.last-name, anchor: "east") + + let east-op-anchor = (rel: (operator.margin, 0), to: west-previous-mol-anchor) + + let op = if (operator.op == none) { + "" + } else { + operator.op + } + + content( + name: op-name, + anchor: "west", + east-op-anchor, + op, + ) + + if (ctx.config.debug) { + circle(west-previous-mol-anchor, radius: 0.05, fill: yellow, stroke: none) + circle(ctx.last-anchor.anchor, radius: 0.05, fill: yellow, stroke: none) + } + }), + ) +} diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/parenthesis.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/parenthesis.typ new file mode 100644 index 0000000000..5b2d39bba2 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/parenthesis.typ @@ -0,0 +1,215 @@ +#import "../utils/utils.typ" +#import "@preview/cetz:0.3.4" + +#let left-parenthesis-anchor(parenthesis, ctx) = { + let anchor = if parenthesis.body.at(0).type == "fragment" { + let name = parenthesis.body.at(0).name + parenthesis.body.at(0).name = name + (name: name, anchor: "west") + } else if parenthesis.body.at(0).type == "link" { + let name = parenthesis.body.at(0).at("name") + parenthesis.body.at(0).name = name + (name + "-start-anchor", 45%, name + "-end-anchor") + } else if not left { } else { + panic("The first element of a parenthesis must be a molecule fragment or a link") + } + (ctx, parenthesis, anchor) +} + +#let right-parenthesis-anchor(parenthesis, ctx) = { + let right-name = parenthesis.at("right") + let right-type = "" + if right-name != none { + right-type = utils.get-element-type(parenthesis.body, right-name) + if right-type == none { + panic("The right element of the parenthesis does not exist") + } + } else { + right-type = parenthesis.body.at(-1).type + right-name = parenthesis.body.at(-1).at("name", default: none) + } + + let anchor = if right-type == "fragment" { + (name: right-name, anchor: "east") + } else if right-type == "link" { + (right-name + "-start-anchor", 55%, right-name + "-end-anchor") + } else { + panic("The last element of a parenthesis must be a molecule fragment or a link but got " + right-type) + } + (ctx, parenthesis, anchor) +} + +#let parenthesis-content(parenthesis, height, cetz-ctx) = { + let block = block(height: height * cetz-ctx.length * 1.2, width: 0pt) + let left-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($parenthesis.l block$, size: 100%) + } + let right-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($block parenthesis.r$, size: 100%) + } + + let right-parenthesis-with-attach = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.attach(right-parenthesis, br: parenthesis.br, tr: parenthesis.tr) + } + + (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) +} + +#let draw-parenthesis(parenthesis, ctx, draw-molecules-and-link) = { + let (parenthesis-ctx, drawing, cetz-rec) = draw-molecules-and-link( + ctx, + parenthesis.body, + ) + ctx = parenthesis-ctx + + + drawing += { + import cetz.draw: * + get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + { + drawing + }, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + + let sub-height = utils.bounding-box-height(sub-bounds) + let sub-v-mid = sub-bounds.low.at(1) - sub-height / 2 + + let sub-width = utils.bounding-box-width(sub-bounds) + + let (ctx, parenthesis, left-anchor) = left-parenthesis-anchor(parenthesis, ctx) + + let (ctx, parenthesis, right-anchor) = right-parenthesis-anchor(parenthesis, ctx) + + let height = parenthesis.at("height") + if height == none { + if not parenthesis.align { + panic("You must specify the height of the parenthesis if they are not aligned") + } + height = sub-height + } else { + height = utils.convert-length(cetz-ctx, height) + } + + let (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) = parenthesis-content( + parenthesis, + height, + cetz-ctx, + ) + + let (_, (lx, ly, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, left-anchor) + let (_, (rx, ry, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, right-anchor) + + if ctx.config.debug { + circle((lx, ly), radius: 1pt, fill: orange, stroke: orange) + circle((rx, ry), radius: 1pt, fill: orange, stroke: orange) + rect((lx, sub-bounds.low.at(1)), (rx, sub-bounds.high.at(1)), stroke: green) + line((lx, sub-v-mid), (rx, sub-v-mid), stroke: green) + } + + let hoffset = calc.abs(sub-width - calc.abs(rx - lx)) + + if parenthesis.align { + ly -= ly - sub-v-mid + ry = ly + } else if type(parenthesis.yoffset) == array { + if parenthesis.yoffset.len() != 2 { + panic("The parenthesis yoffset must be a list of length 2 or a number") + } + ly += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(0)) + ry += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(1)) + } else if parenthesis.yoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.yoffset) + ly += offset + ry += offset + } + + if type(parenthesis.xoffset) == array { + if parenthesis.xoffset.len() != 2 { + panic("The parenthesis xoffset must be a list of length 2 or a number") + } + lx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(0)) + rx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(1)) + } else if parenthesis.xoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.xoffset) + lx -= offset + rx += offset + } + + let right-bounds = cetz.process.many(cetz-ctx, content((0, 0), right-parenthesis, auto-scale: false)).bounds + let right-with-attach-bounds = cetz + .process + .many(cetz-ctx, content((0, 0), right-parenthesis-with-attach, auto-scale: false)) + .bounds + let right-voffset = calc.abs(right-bounds.low.at(1) - right-with-attach-bounds.low.at(1)) + if parenthesis.tr != none and parenthesis.br != none { + right-voffset /= 2 + } else if (parenthesis.tr != none) { + right-voffset *= -1 + } + + ( + _ => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + content((lx, ly), anchor: "mid-east", left-parenthesis, auto-scale: false) + content((rx, ry - right-voffset), anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }) + } + + (ctx, drawing, cetz-rec) +} + + +#let draw-resonance-parenthesis(parenthesis, draw-function, ctx) = { + import cetz.draw: * + let left-name = "parenthesis-" + str(ctx.id) + let right-name = "parenthesis-" + str(ctx.id + 1) + ctx.id += 2 + let last-anchor = ctx.last-anchor + let (drawing, ctx) = draw-function(ctx, parenthesis.body, after-operator: true) + + let drawing = get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + drawing, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + let height = parenthesis.height + if height == none { + height = utils.bounding-box-height(sub-bounds) + } else { + height = utils.convert-length(cetz-ctx, height) + } + let width = utils.bounding-box-width(sub-bounds) + + let (left-parenthesis, _, right-parenthesis-with-attach) = parenthesis-content(parenthesis, height, cetz-ctx) + + let right-anchor = (rel: (width, 0), to: (name: left-name, anchor: "east")) + on-layer( + 1, + { + content(last-anchor.anchor, name: left-name, anchor: "mid-east", left-parenthesis, auto-scale: false) + content(right-anchor, name: right-name, anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }, + ) + ( + ctx => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + }) + + ctx.last-anchor = (type: "coord", anchor: (name: right-name, anchor: "mid-east")) + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/elements/fragment.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/elements/fragment.typ new file mode 100644 index 0000000000..e7b66eb50f --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/elements/fragment.typ @@ -0,0 +1,74 @@ +#let split-equation(mol, equation: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + for m in mol { + let last-number-hold = last-number + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + result.push(m) + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in fragment fragment") + } + last-number = true + result.push(m) + } else { + panic("Invalid fragment fragment content") + } + } else if m.func() == math.attach or m.func() == math.lr { + result.push(m) + } else if m == [ ] { + continue + } else { + panic("Invalid fragment fragment content") + } + if last-number-hold { + result.at(-2) = result.at(-2) + result.at(-1) + let _ = result.pop() + last-number = false + } + } + result +} + +#let fragment-cor-regex = "[0-9]*[A-Z][a-z]*'*" +#let exponent-regex = "((?:[0-9]+(?:\\+|\\-)?)|[A-Z]|\\+|\\-)" +#let exponent-base-regex = "(?:(\\^|_)" + exponent-regex + ")?(?:(\\^|_)" + exponent-regex + ")?" +#let fragment-regex = regex("^ *(" + fragment-cor-regex + ")" + exponent-base-regex) + +#let split-string(mol) = { + let aux(str) = { + let match = str.match(fragment-regex) + if match == none { + panic(str + " is not a valid fragment") + } else if match.captures.at(1) == match.captures.at(3) and match.captures.at(1) != none { + panic("You cannot use an exponent and a subscript twice") + } + let eq = "\"" + match.captures.at(0) + "\"" + if match.captures.at(1) != none { + eq += match.captures.at(1) + "(" + match.captures.at(2) + ")" + } + if match.captures.at(3) != none { + eq += match.captures.at(3) + "(" + match.captures.at(4) + ")" + } + let eq = math.equation(eval(eq, mode: "math")) + (eq, match.end) + } + + while not mol.len() == 0 { + let (eq, end) = aux(mol) + mol = mol.slice(end) + (eq,) + } +} diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/elements/lewis.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/elements/lewis.typ new file mode 100644 index 0000000000..a8c0a939c6 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/elements/lewis.typ @@ -0,0 +1,133 @@ +#import "@preview/cetz:0.3.4" + +/// Create a lewis function that is then used to draw a lewis +/// formulae element around the fragment +/// +/// - draw-function (function): The function that will be used to draw the lewis element. It should takes three arguments: the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-lewis(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Lewis function takes no positional arguments") + } + let args = args.named() + let angle = args.at("angle", default: none) + let radius = args.at("radius", default: none) + ( + angle: angle, + radius: radius, + draw: (ctx, cetz-ctx) => draw-function(ctx, cetz-ctx, args) + ) + } +} + +/// draw a sigle electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// +/// The position of the electron is set by the `offset` argument. Available values are: +/// - "top": the electron is placed above the fragment center line +/// - "bottom": the electron is placed below the fragment center line +/// - "center": the electron is placed at the fragment center line +/// +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-single(offset: "top"), +/// )) +/// single(angle:-2) +/// fragment("B", lewis:( +/// lewis-single(offset: "bottom"), +/// )) +/// single(angle:-2) +/// fragment("C", lewis:( +/// lewis-single(offset: "center"), +/// )) +/// }) +/// ```) +#let lewis-single = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-single.radius) + let gap = args.at("gap", default: ctx.config.lewis-single.gap) + let offset = args.at("offset", default: ctx.config.lewis-single.offset) + if offset == "top" { + translate((0, gap)) + } else if offset == "bottom" { + translate((0, -gap)) + } else if offset != "center" { + panic("Invalid position, expected 'top', 'bottom' or 'center'") + } + let fill = args.at("fill", default: ctx.config.lewis-single.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-single.stroke) + circle((0, 0), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-double(), +/// lewis-double(angle: 90deg), +/// lewis-double(angle: 180deg), +/// lewis-double(angle: -90deg) +/// )) +/// }) +/// ```) +#let lewis-double = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-double.radius) + let gap = args.at("gap", default: ctx.config.lewis-double.gap) + let fill = args.at("fill", default: ctx.config.lewis-double.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-double.stroke) + circle((0, -gap), radius: radius, fill: fill, stroke: stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron liked by a single line +/// +/// It is possible to change the length of the line with the `lenght` argument. +/// It is also possible to change the `stroke` agument +/// #example(``` +/// #skeletize({ +/// fragment("B", lewis:( +/// lewis-line(angle: 45deg), +/// lewis-line(angle: 135deg), +/// lewis-line(angle: -45deg), +/// lewis-line(angle: -135deg) +/// )) +/// }) +/// ```) +#let lewis-line = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let length = args.at("length", default: ctx.config.lewis-line.length) + let stroke = args.at("stroke", default: ctx.config.lewis-line.stroke) + line((0, -length / 2), (0, length / 2), stroke: stroke) +}) + + +/// Draw a rectangle to denote a lone pair of electrons +/// +/// It is possible to change the height and width of the rectangle with the `height` and `width` arguments. +/// It is also possible to change the `fill` and `stroke` arguments +/// #example(``` +/// #skeletize({ +/// fragment("C", lewis:( +/// lewis-rectangle(), +/// lewis-rectangle(angle: 180deg) +/// )) +/// }) +/// ```) +#let lewis-rectangle = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let height = args.at("height", default: ctx.config.lewis-rectangle.height) + let width = args.at("width", default: ctx.config.lewis-rectangle.width) + let fill = args.at("fill", default: ctx.config.lewis-rectangle.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-rectangle.stroke) + rect((-width / 2, -height / 2), (width / 2, height / 2), fill: fill, stroke: stroke) +}) diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/elements/links.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/elements/links.typ new file mode 100644 index 0000000000..5c3f9c5e9f --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/elements/links.typ @@ -0,0 +1,316 @@ +#import "@preview/cetz:0.3.4" +#import "../drawer/cram.typ": * +#import "../utils/utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes four arguments: the length of the link, the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, cetz-ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, cetz-ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single(stroke: red + 5pt) +/// fragment("B") +/// }) +///```) +#let single = build-link((length, ctx, _, args) => { + import cetz.draw: * + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.single.stroke)) +}) + +/// Draw a double line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// fragment("B") +/// }) +///```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double(offset: "right") +/// fragment("B") +/// double(offset: "left") +/// fragment("C") +/// double(offset: "center") +/// fragment("D") +/// }) +///```) +#let double = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.double.gap)) / 2 + let offset = args.at("offset", default: ctx.config.double.offset) + let coeff = args.at("offset-coeff", default: ctx.config.double.offset-coeff) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + if offset == "right" { + translate((0, -gap)) + } else if offset == "left" { + translate((0, gap)) + } else if offset == "center" { } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + translate((0, -gap)) + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) + translate((0, 2 * gap)) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, 0), (x + length * coeff, 0)) + } else { + ((0, 0), (length, 0)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) +}) + +/// Draw a triple line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// fragment("B") +/// }) +///```) +#let triple = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.triple.gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + translate((0, -gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + translate((0, 2 * gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) +}) + +/// Draw a filled cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, cetz-ctx, args) => cram( + (0, 0), + (length, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a filled cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, cetz-ctx, args) => cram( + (length, 0), + (0, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a hollow cram between two fragments with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((0, 0), (length, 0), ctx, cetz-ctx, args) +}) + +/// Draw a hollow cram between two fragments with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((length, 0), (0, 0), ctx, cetz-ctx, args) +}) + +/// Draw a dashed cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the arrow base length with the `base-length` argument and the tip length with the `tip-length` argument. +// You can also change distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// tip-length: 1em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-right = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + cetz-ctx, + args, +)) + +/// Draw a dashed cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + cetz-ctx, + args, +)) + + +/// Draw a plus sign between two fragments +/// #example(```` +/// #skeletize({ +/// fragment("A") +/// plus() +/// fragment("B") +/// }) +/// ````) +/// You can change the filling, size and stroke of the glyph with the `fill`, `size` and `stoke` arguments. The default values are the parent `text` parameters. +#let plus = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + content( + anchor: "mid", + (length / 2, 0), + { + set text(fill: args.fill) if "fill" in args + set text(stroke: args.stroke) if "stroke" in args + set text(size: args.size) if "size" in args + text($+$) + } + ) +}) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/utils/anchors.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/utils/anchors.typ new file mode 100644 index 0000000000..c4d9a891a3 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/utils/anchors.typ @@ -0,0 +1,296 @@ +#import "angles.typ" +#import "context.typ" as context_ +#import "utils.typ": * +#import "@preview/cetz:0.3.4" +#import cetz.draw: * + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +/// Calculate an anchor position around a fragment using an ellipse +/// at a given angle +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - cetz-ctx (cetz-ctx): the cetz context +/// - angle (float, int, angle): the angle of the anchor +/// - fragment (string): the fragment name +/// - id (string): the fragment subpart id +/// - margin (length, none): the margin around the fragment +/// -> anchor: the anchor position around the fragment +#let fragment-anchor(ctx, cetz-ctx, angle, fragment, id, margin: none) = { + let angle = angles.angle-correction(angle) + let fragment-margin = if margin == none { + ctx.config.fragment-margin + } else { + margin + } + fragment-margin = convert-length(cetz-ctx, fragment-margin) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "mid")), + ) + let (a, b) = if angles.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, fragment-margin, fragment, id) + } else { + anchor-south-east(cetz-ctx, center, fragment-margin, fragment, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if angles.angle-in-range-inclusive(angle, 0deg, 90deg) or angles.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +/// Return the index to choose if the link connection is not overridden +#let link-fragment-index(angle, end, count, vertical) = { + if not end { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let fragment-link-anchor(name, id, count) = { + if count <= id { + panic("The last fragment only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "mid")) +} + +#let link-fragment-anchor(name: none, id, count) = { + if id >= count { + panic("This fragment only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the fragment to link to must be defined") + } + if name == none { + (name: str(id), anchor: "mid") + } else { + (name: name, anchor: (str(id), "mid")) + } +} + + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "mid") + if link.to == none or link.from == none { + let angle = link.at( + "angle", + default: angles.angle-between(cetz-ctx, link.from-pos, to-pos), + ) + link.angle = angle + if link.from == none { + link.from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + if link.from == -1 { + link.from = 0 + } + if link.to == -1 { + link.to = ctx.hooks.at(link.to-name).count - 1 + } + let start = fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) + let end = fragment-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) + ((start, end), angles.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "mid"), + ), + ) + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "mid")), + ), + ) + } + let end-anchor = fragment-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + angles.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = angles.angle-correction(angles.angle-between(cetz-ctx, link.from-pos, hook.hook)) + let from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = fragment-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) + ( + ( + start-anchor, + hook.hook, + ), + angles.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + angles.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/utils/angles.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/utils/angles.typ new file mode 100644 index 0000000000..418e6472f1 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/utils/angles.typ @@ -0,0 +1,98 @@ +#import "@preview/cetz:0.3.4" + +/// Convert any angle to an angle between 0deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a += 360 + } + + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +/// Check if the angle is in the range ]from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +/// Check if the angle is in the range [from, to] +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +/// +/// - ctx (cetz-ctx): The cetz context +/// - from (anchor): The first anchor +/// - to (anchor): The second anchor +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// Calculate the angle from an object with "relative", "absolute" or "angle" key +/// according to the alchemist context (see the manual to see how angles are calculated) +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - object (dict): the object to get the angle from +/// - default (angle): the default angle +/// -> the calculated angle +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +/// Overwrite the offset based on the angle +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/utils/context.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/utils/context.typ new file mode 100644 index 0000000000..4021bb5177 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/utils/context.typ @@ -0,0 +1,33 @@ +#let update-parent-context(parent-ctx, ctx) = { + let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { + ( + ..parent-ctx.last-anchor, + drew: true, + ) + } else { + parent-ctx.last-anchor + } + ( + ..parent-ctx, + last-anchor: last-anchor, + hooks: ctx.hooks, + hooks-links: ctx.hooks-links, + links: ctx.links, + ) +} + +/// Set the last anchor in the context to the given anchor and save it if needed +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" { + let drew = ctx.last-anchor.at("drew", default: false) + if drew and anchor.type == "link" and anchor.name == ctx.last-anchor.name { + drew = false + let _ = ctx.links.pop() + } + if not drew { + ctx.links.push(ctx.last-anchor) + } + ctx.last-anchor.drew = true + } + (..ctx, last-anchor: anchor) +} diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/utils/utils.typ b/packages/preview/alchemist/0.1.6/0.1.6/src/utils/utils.typ new file mode 100644 index 0000000000..ab2e6b661c --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/src/utils/utils.typ @@ -0,0 +1,75 @@ +#import "@preview/cetz:0.3.4" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + return if type(num) == length { + float(num.to-absolute() / ctx.length) + } else if type(num) == ratio { + num + } else { + float(num) + } +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} + +/// merge two imbricated dictionaries together +/// The second dictionary is the default value if the key is not present in the first dictionary +#let merge-dictionaries(dict1, default) = { + let result = default + for (key, value) in dict1 { + if type(value) == dictionary { + result.at(key) = merge-dictionaries(value, default.at(key)) + } else { + result.at(key) = value + } + } + result +} + + +/// get the type of an element by its name +/// +/// - body (drawable): the chemfig body of a molecule +/// - name (string): the name of the element to get the type +/// -> string +#let get-element-type(body, name) = { + for element in body { + if type(element) != dictionary { + continue + } + if "name" in element and element.name == name { + return element.type + } + if element.type == "branch" or element.type == "cycle" or element.type == "parenthesis" { + let type = get-element-type(element.body, name) + if type != none { + return type + } + } + } + none +} + +/// Calculate the height of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-height(bounds) = { + calc.abs(bounds.high.at(1) - bounds.low.at(1)) +} + +/// Calculate the width of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-width(bounds) = { + calc.abs(bounds.high.at(0) - bounds.low.at(0)) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.6/0.1.6/typst.toml b/packages/preview/alchemist/0.1.6/0.1.6/typst.toml new file mode 100644 index 0000000000..8de6ec7df2 --- /dev/null +++ b/packages/preview/alchemist/0.1.6/0.1.6/typst.toml @@ -0,0 +1,13 @@ +[package] +name = "alchemist" +version = "0.1.6" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.13.1" +repository = "https://github.com/Typsium/alchemist" +description = "A package to render skeletal formulas using CeTZ" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization", "paper"] +disciplines = ["education", "chemistry", "biology"] +keywords = ["chemistry", "chemical", "formula", "molecule", "chemiviz", "structure", "organic", "lewis", "bond", "aromatic"] From 692aefda2358a463ee95bb37ef9e8e1afd057b43 Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Thu, 5 Jun 2025 20:00:48 +0200 Subject: [PATCH 10/24] fixed wrong folder structure --- packages/preview/alchemist/0.1.6/{0.1.6 => }/LICENSE | 0 packages/preview/alchemist/0.1.6/{0.1.6 => }/README.md | 0 packages/preview/alchemist/0.1.6/{0.1.6 => }/lib.typ | 0 packages/preview/alchemist/0.1.6/{0.1.6 => }/src/default.typ | 0 packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer.typ | 0 .../preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/branch.typ | 0 packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/cram.typ | 0 packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/cycle.typ | 0 .../preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/fragment.typ | 0 packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/hook.typ | 0 packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/link.typ | 0 .../preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/operator.typ | 0 .../alchemist/0.1.6/{0.1.6 => }/src/drawer/parenthesis.typ | 0 .../preview/alchemist/0.1.6/{0.1.6 => }/src/elements/fragment.typ | 0 .../preview/alchemist/0.1.6/{0.1.6 => }/src/elements/lewis.typ | 0 .../preview/alchemist/0.1.6/{0.1.6 => }/src/elements/links.typ | 0 .../preview/alchemist/0.1.6/{0.1.6 => }/src/utils/anchors.typ | 0 packages/preview/alchemist/0.1.6/{0.1.6 => }/src/utils/angles.typ | 0 .../preview/alchemist/0.1.6/{0.1.6 => }/src/utils/context.typ | 0 packages/preview/alchemist/0.1.6/{0.1.6 => }/src/utils/utils.typ | 0 packages/preview/alchemist/0.1.6/{0.1.6 => }/typst.toml | 0 21 files changed, 0 insertions(+), 0 deletions(-) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/LICENSE (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/README.md (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/lib.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/default.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/branch.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/cram.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/cycle.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/fragment.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/hook.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/link.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/operator.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/drawer/parenthesis.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/elements/fragment.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/elements/lewis.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/elements/links.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/utils/anchors.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/utils/angles.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/utils/context.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/src/utils/utils.typ (100%) rename packages/preview/alchemist/0.1.6/{0.1.6 => }/typst.toml (100%) diff --git a/packages/preview/alchemist/0.1.6/0.1.6/LICENSE b/packages/preview/alchemist/0.1.6/LICENSE similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/LICENSE rename to packages/preview/alchemist/0.1.6/LICENSE diff --git a/packages/preview/alchemist/0.1.6/0.1.6/README.md b/packages/preview/alchemist/0.1.6/README.md similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/README.md rename to packages/preview/alchemist/0.1.6/README.md diff --git a/packages/preview/alchemist/0.1.6/0.1.6/lib.typ b/packages/preview/alchemist/0.1.6/lib.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/lib.typ rename to packages/preview/alchemist/0.1.6/lib.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/default.typ b/packages/preview/alchemist/0.1.6/src/default.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/default.typ rename to packages/preview/alchemist/0.1.6/src/default.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer.typ b/packages/preview/alchemist/0.1.6/src/drawer.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/drawer.typ rename to packages/preview/alchemist/0.1.6/src/drawer.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/branch.typ b/packages/preview/alchemist/0.1.6/src/drawer/branch.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/drawer/branch.typ rename to packages/preview/alchemist/0.1.6/src/drawer/branch.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cram.typ b/packages/preview/alchemist/0.1.6/src/drawer/cram.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cram.typ rename to packages/preview/alchemist/0.1.6/src/drawer/cram.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cycle.typ b/packages/preview/alchemist/0.1.6/src/drawer/cycle.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/drawer/cycle.typ rename to packages/preview/alchemist/0.1.6/src/drawer/cycle.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/fragment.typ b/packages/preview/alchemist/0.1.6/src/drawer/fragment.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/drawer/fragment.typ rename to packages/preview/alchemist/0.1.6/src/drawer/fragment.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/hook.typ b/packages/preview/alchemist/0.1.6/src/drawer/hook.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/drawer/hook.typ rename to packages/preview/alchemist/0.1.6/src/drawer/hook.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/link.typ b/packages/preview/alchemist/0.1.6/src/drawer/link.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/drawer/link.typ rename to packages/preview/alchemist/0.1.6/src/drawer/link.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/operator.typ b/packages/preview/alchemist/0.1.6/src/drawer/operator.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/drawer/operator.typ rename to packages/preview/alchemist/0.1.6/src/drawer/operator.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/drawer/parenthesis.typ b/packages/preview/alchemist/0.1.6/src/drawer/parenthesis.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/drawer/parenthesis.typ rename to packages/preview/alchemist/0.1.6/src/drawer/parenthesis.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/elements/fragment.typ b/packages/preview/alchemist/0.1.6/src/elements/fragment.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/elements/fragment.typ rename to packages/preview/alchemist/0.1.6/src/elements/fragment.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/elements/lewis.typ b/packages/preview/alchemist/0.1.6/src/elements/lewis.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/elements/lewis.typ rename to packages/preview/alchemist/0.1.6/src/elements/lewis.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/elements/links.typ b/packages/preview/alchemist/0.1.6/src/elements/links.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/elements/links.typ rename to packages/preview/alchemist/0.1.6/src/elements/links.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/utils/anchors.typ b/packages/preview/alchemist/0.1.6/src/utils/anchors.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/utils/anchors.typ rename to packages/preview/alchemist/0.1.6/src/utils/anchors.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/utils/angles.typ b/packages/preview/alchemist/0.1.6/src/utils/angles.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/utils/angles.typ rename to packages/preview/alchemist/0.1.6/src/utils/angles.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/utils/context.typ b/packages/preview/alchemist/0.1.6/src/utils/context.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/utils/context.typ rename to packages/preview/alchemist/0.1.6/src/utils/context.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/src/utils/utils.typ b/packages/preview/alchemist/0.1.6/src/utils/utils.typ similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/src/utils/utils.typ rename to packages/preview/alchemist/0.1.6/src/utils/utils.typ diff --git a/packages/preview/alchemist/0.1.6/0.1.6/typst.toml b/packages/preview/alchemist/0.1.6/typst.toml similarity index 100% rename from packages/preview/alchemist/0.1.6/0.1.6/typst.toml rename to packages/preview/alchemist/0.1.6/typst.toml From 5c9f935e777a276d455aef3850ca5c003a1d68e7 Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Sat, 9 Aug 2025 20:11:16 +0300 Subject: [PATCH 11/24] Upload alchemist v0.1.7 --- .../preview/alchemist/0.1.7/0.1.7/LICENSE | 22 + .../preview/alchemist/0.1.7/0.1.7/README.md | 134 ++++++ .../preview/alchemist/0.1.7/0.1.7/lib.typ | 255 +++++++++++ .../0.1.7/0.1.7/src/cetz/process.typ | 91 ++++ .../alchemist/0.1.7/0.1.7/src/default.typ | 61 +++ .../alchemist/0.1.7/0.1.7/src/drawer.typ | 403 ++++++++++++++++++ .../0.1.7/0.1.7/src/drawer/branch.typ | 32 ++ .../alchemist/0.1.7/0.1.7/src/drawer/cram.typ | 61 +++ .../0.1.7/0.1.7/src/drawer/cycle.typ | 155 +++++++ .../0.1.7/0.1.7/src/drawer/fragment.typ | 149 +++++++ .../alchemist/0.1.7/0.1.7/src/drawer/hook.typ | 14 + .../alchemist/0.1.7/0.1.7/src/drawer/link.typ | 103 +++++ .../0.1.7/0.1.7/src/drawer/operator.typ | 36 ++ .../0.1.7/0.1.7/src/drawer/parenthesis.typ | 215 ++++++++++ .../0.1.7/0.1.7/src/elements/fragment.typ | 74 ++++ .../0.1.7/0.1.7/src/elements/lewis.typ | 133 ++++++ .../0.1.7/0.1.7/src/elements/links.typ | 314 ++++++++++++++ .../0.1.7/0.1.7/src/utils/anchors.typ | 296 +++++++++++++ .../0.1.7/0.1.7/src/utils/angles.typ | 98 +++++ .../0.1.7/0.1.7/src/utils/context.typ | 33 ++ .../alchemist/0.1.7/0.1.7/src/utils/utils.typ | 75 ++++ .../preview/alchemist/0.1.7/0.1.7/typst.toml | 13 + 22 files changed, 2767 insertions(+) create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/LICENSE create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/README.md create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/lib.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/cetz/process.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/default.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/branch.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cram.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cycle.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/fragment.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/hook.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/link.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/operator.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/parenthesis.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/elements/fragment.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/elements/lewis.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/elements/links.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/utils/anchors.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/utils/angles.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/utils/context.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/utils/utils.typ create mode 100644 packages/preview/alchemist/0.1.7/0.1.7/typst.toml diff --git a/packages/preview/alchemist/0.1.7/0.1.7/LICENSE b/packages/preview/alchemist/0.1.7/0.1.7/LICENSE new file mode 100644 index 0000000000..a85204e590 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.7/0.1.7/README.md b/packages/preview/alchemist/0.1.7/0.1.7/README.md new file mode 100644 index 0000000000..c968af64b7 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/README.md @@ -0,0 +1,134 @@ +[![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FTypsium%2Falchemist%2Fmaster%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://github.com/Typsium/alchemist) +[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/alchemist/blob/master/LICENSE) +[![User Manual](https://img.shields.io/badge/manual-.pdf-purple)](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf) + +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + fragment(name: "A", "A") + single() + fragment("B") + branch({ + single(angle: 1) + fragment( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + fragment(name: "X", "X") + }) + branch({ + single(angle: -1) + fragment("Y") + single() + fragment( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + fragment( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic1/ref/1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + fragment("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic2/ref/1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.1.4": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). + +## Changelog + +### 0.1.7 + +- Updated cetz to version 0.4.1 +- Added default values for `color` and `font` for `fragment` elements +- Added a `skeletize-config` function to create a `skeletize` function with a specific configuration +- Fixed a bug with cetz anchors not being correctly translated +- Added an `over` argument to links to allow hide overlapped links + +### 0.1.6 + +- Fixed the parenthesis height to work with the new typst version +- Renamed `molecule` into `fragment` +- Added support for charges and apostrophes in the string of a molecule +- Fixed the parenthesis auto positioning and alignment +- Added a new operator element and a new parameter in parenthesis to write resonance formulae + +### 0.1.5 + +- Update to compiler 0.13.1 and Cetz 0.3.4 + +### 0.1.4 + +- Added the possibility to create Lewis formulae +- Added parenthesis element to create groups and polymer + +### 0.1.3 + +- Added the possibility to add exponent in the string of a molecule. + +### 0.1.2 + +- Added default values for link style properties. +- Updated `cetz` to version 0.3.1. +- Added a `tip-length` argument to dashed cram links. + +### 0.1.1 + +- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. +- Fixed multiples bugs that causes overdraws of links. + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.1.7/0.1.7/lib.typ b/packages/preview/alchemist/0.1.7/0.1.7/lib.typ new file mode 100644 index 0000000000..7d1f5c1a64 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/lib.typ @@ -0,0 +1,255 @@ +#import "@preview/cetz:0.4.1" +#import "src/default.typ": default +#import "src/utils/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ": skeletize, draw-skeleton, skeletize-config, draw-skeleton-config +#import "src/elements/links.typ": * +#import "src/elements/fragment.typ": * +#import "src/elements/lewis.typ": * + +#let transparent = color.rgb(100%, 0, 0, 0) + +/// === Fragment function +/// Build a fragment group based on mol +/// Each fragment is represented as an optional count followed by a fragment name +/// starting by a capital letter followed by an optional exponent followed by an optional indice. +/// #example(``` +/// #skeletize({ +/// fragment("H_2O") +/// }) +///```) +/// #example(``` +/// #skeletize({ +/// fragment("H^A_EF^5_4") +/// }) +/// ```) +/// It is possible to use an equation as a fragment. In this case, the splitting of the equation uses the same rules as in the string case. However, you get some advantages over the string version: +/// - You can use parenthesis to group elements together. +/// - You have no restriction about what you can put in exponent or indice. +/// #example(``` +/// #skeletize({ +/// fragment($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the fragment. It is used as the cetz name of the fragment and to link other fragments to it. +/// - links (dictionary): The links between this fragment and previous fragments or hooks. The key is the name of the fragment or hook and the value is the link function. See @links. +/// +/// Note that the atom-sep and angle arguments are ignored +/// - lewis (list): The list of lewis structures to draw around the fragments. See @lewis +/// - mol (string, equation): The string representing the fragment or an equation of the fragment +/// - vertical (boolean): If true, the fragment is drawn vertically +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", vertical: true) +/// }) +///```) +/// - colors (color|list): The color of the fragment. If a list is provided, it colors each group of the fragment with the corresponding color from right to left. If the number of colors is less than the number of groups, the last color is used for the remaining groups. If the number of colors is greater than the number of groups, the extra colors are ignored. +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", colors: (red, green, blue)) +/// single() +/// fragment("EFGH", colors: (orange)) +/// }) +/// ```) +/// -> drawable +#let fragment(name: none, links: (:), lewis: (), vertical: false, colors: none, mol) = { + let atoms = if type(mol) == str { + split-string(mol) + } else if mol.func() == math.equation { + split-equation(mol, equation: true) + } else { + panic("Invalid fragment content") + } + + if type(lewis) != array { + panic("Lewis formulae elements must be in a list") + } + + ( + ( + type: "fragment", + name: name, + atoms: atoms, + colors: colors, + links: links, + lewis: lewis, + vertical: vertical, + count: atoms.len(), + ), + ) +} +#let molecule(name: none, links: (:), lewis: (), vertical: false, mol) = fragment(name: name, links: links, lewis: lewis, vertical: vertical, mol) + +/// === Hooks +/// Create a hook in the fragment. It allows to connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the fragment. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current fragment, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// branch({ +/// single(angle:1) +/// fragment("B") +/// }) +/// branch({ +/// double(angle: -1) +/// fragment("D") +/// }) +/// single() +/// double() +/// single() +/// fragment("C") +/// }) +///```) +/// - body (drawable): the body of the branch. It must start with a link. +/// -> drawable +#let branch(body, ..args) = { + if args.pos().len() != 0 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", body: body, args: args.named()),) +} + +/// Create a regular cycle of fragments +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +/// - faces (int): the number of faces of the cycle +/// - body (drawable): the body of the cycle. It must start and end with a fragment or a link. +/// -> drawable +#let cycle(faces, body, ..args) = { + if args.pos().len() != 0 { + panic("Cycle takes two positional arguments: number of faces and body") + } + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + body: body, + args: args.named(), + ), + ) +} + +/// === Parenthesis +/// Encapsulate a drawable between two parenthesis. The left parenthesis is placed at the left of the first element of the body and by default the right parenthesis is placed at the right of the last element of the body. +/// +/// #example(``` +/// #skeletize( +/// config: ( +/// angle-increment: 30deg +/// ), { +/// parenthesis( +/// l:"[", r:"]", +/// br: $n$, { +/// single(angle: 1) +/// single(angle: -1) +/// single(angle: 1) +/// }) +/// }) +/// ```) +/// For more examples, see @examples +/// +/// - body (drawable): the body of the parenthesis. It must start and end with a fragment or a link. +/// - l (string): the left parenthesis +/// - r (string): the right parenthesis +/// - align (true): if true, the parenthesis will have the same y position. They will also get sized and aligned according to the body height. If false, they are not aligned and the height argument must be specified. +/// - resonance (boolean): if true, the parenthesis will be drawn in resonance mode. This means that the left and right parenthesis will be placed outside the molecule. Also, the parenthesis will be separated from the previous and next molecule. This can be true only if the parenthesis is the first element of the skeletal formula or if the previous element is an operator. See @resonance for more details. +/// - height (float, length): the height of the parenthesis. If align is true, this argument is optional. +/// - yoffset (float, length, list): the vertical offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - xoffset (float, length, list): the horizontal offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - right (string): Sometime, it is not convenient to place the right parenthesis at the end of the body. In this case, you can specify the name of the fragment or link where the right parenthesis should be placed. It is especially useful when the body end by a cycle. See @polySulfide +/// - tr (content): the exponent content of the right parenthesis +/// - br (content): the indice content of the right parenthesis +/// -> drawable +#let parenthesis(body, l: "(", r: ")", align: true, resonance: false, height: none, yoffset: none, xoffset: none, right: none, tr: none, br: none) = { + if l.len() > 2 { + panic("Left can be at most 2 characters") + } + let l = eval(l, mode: "math") + if r.len() > 2 { + panic("Right can be at most 2 characters") + } + let r = eval(r, mode: "math") + ( + ( + type: "parenthesis", + body: body, + calc: calc, + l: l, + r: r, + align: align, + tr: tr, + br: br, + height: height, + xoffset: xoffset, + yoffset: yoffset, + right: right, + resonance: resonance, + ), + ) +} + + +/// === Operator +/// Create an operator between two fragments. Creating an operator "reset" the placement of the next fragment. +/// This allow to add multiple molecules in the same skeletal formula. Without this, the next fragment would be placed at the end of the previous one. +/// An important point is that you can't use previous hooks to link two molecules separate by an operator. +/// This element is used in resonance structures (@resonance) and in some cases to put multiples molecules in the same skeletal formula (as you can set op to none). +/// +/// - op (content | string | none): The operator content. It can be a string or a content. A none value won't display anything. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// operator($->$, margin: 1em) +/// fragment("B") +/// }) +/// ```) +/// See @resonance for more examples. +/// - name (string): The name of the operator. +/// - margin (float, length): The margin between the operator and previous / next molecule. +/// -> drawable +#let operator(name: none, margin: 1em, op) = { + ( + ( + type: "operator", + name: name, + op: op, + margin: margin, + ), + ) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/cetz/process.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/cetz/process.typ new file mode 100644 index 0000000000..decb454d2c --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/cetz/process.typ @@ -0,0 +1,91 @@ +// Temporary custom process.typ file to override default behavior + +#import "@preview/cetz:0.4.1": util, path-util, vector, drawable, process.aabb + + +/// Processes an element's function to get its drawables and bounds. Returns a {{dictionary}} with the key-values: `ctx` The modified context object, `bounds` The {{aabb}} of the element's drawables, `drawables` An {{array}} of the element's {{drawable}}s. +/// +/// - ctx (ctx): The current context object. +/// - element-func (function): A function that when passed {{ctx}}, it should return an element dictionary. +#let element(ctx, element-func) = { + let bounds = none + let element + let anchors = () + + (ctx, ..element,) = element-func(ctx) + if "drawables" in element { + if type(element.drawables) == dictionary { + element.drawables = (element.drawables,) + } + for drawable in element.drawables { + if drawable.bounds { + bounds = aabb.aabb( + if drawable.type == "path" { + path-util.bounds(drawable.segments) + } else if drawable.type == "content" { + let (x, y, _, w, h,) = drawable.pos + (drawable.width, drawable.height) + ((x + w / 2, y - h / 2, 0.0), (x - w / 2, y + h / 2, 0.0)) + }, + init: bounds + ) + } + } + } + + let name = element.at("name", default: none) + if name != none { + assert.eq(type(name), str, + message: "Element name must be a string") + assert(not name.contains("."), + message: "Invalid name for element '" + element.name + "'; name must not contain '.'") + + if "anchors" in element { + anchors.push(name) + ctx.nodes.insert(name, element) + if ctx.groups.len() > 0 { + ctx.groups.last().push(name) + } + } + } + + if ctx.debug and bounds != none { + element.drawables.push(drawable.path( + ((bounds.low, true, ( + ("l", (bounds.high.at(0), bounds.low.at(1), 0)), + ("l", bounds.high), + ("l", (bounds.low.at(0), bounds.high.at(1), 0)))),), + stroke: red + )) + } + + return ( + ctx: ctx, + bounds: bounds, + drawables: element.at("drawables", default: ()), + anchors: anchors + ) +} + +/// Runs the `element` function for a list of element functions and aggregates the results. +/// - ctx (ctx): The current context object. +/// - body (array): The array of element functions to process. +/// -> dictionary +#let many(ctx, body) = { + let drawables = () + let bounds = none + let anchors = () + + for el in body { + let r = element(ctx, el) + if r != none { + if r.bounds != none { + let pts = (r.bounds.low, r.bounds.high,) + bounds = aabb.aabb(pts, init: bounds) + } + ctx = r.ctx + drawables += r.drawables + anchors += r.anchors + } + } + return (ctx: ctx, bounds: bounds, drawables: drawables, anchors: anchors) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/default.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/default.typ new file mode 100644 index 0000000000..a5d0222af3 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/default.typ @@ -0,0 +1,61 @@ +#let default = ( + atom-sep: 3em, + fragment-margin: 0.2em, + fragment-font: none, + fragment-color: none, + link-over-radius: .2, + angle-increment: 45deg, + base-angle: 0deg, + debug: false, + single: ( + stroke: black + ), + double: ( + gap: .25em, + offset: "center", + offset-coeff: 0.85, + stroke: black + ), + triple: ( + gap: .25em, + stroke: black + ), + filled-cram: ( + stroke: none, + fill: black, + base-length: .8em + ), + dashed-cram: ( + stroke: black + .05em, + dash-gap: .3em, + base-length: .8em, + tip-length: .1em + ), + lewis: ( + angle: 0deg, + radius: 0.2em, + ), + lewis-single: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + offset: "top" + ), + lewis-double: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + ), + lewis-line: ( + stroke: black, + length: .7em, + ), + lewis-rectangle: ( + stroke: .08em + black, + fill: white, + height: .7em, + width: .3em + ) +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer.typ new file mode 100644 index 0000000000..ef1c0774ad --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer.typ @@ -0,0 +1,403 @@ +#import "default.typ": default +#import "@preview/cetz:0.4.1" +#import "cetz/process.typ" as custom-process +#import "utils/utils.typ": * +#import "utils/anchors.typ": * +#import "utils/utils.typ" as utils +#import "drawer/fragment.typ" as fragment +#import "drawer/link.typ" as link +#import "drawer/branch.typ" as branch +#import "drawer/cycle.typ" as cycle +#import "drawer/parenthesis.typ" as parenthesis +#import "drawer/hook.typ" as hook +#import "drawer/operator.typ" as operator + +#import cetz.draw: * + +#let default-anchor = (type: "coord", anchor: (0, 0)) + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + last-name: "", // last name used to draw + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + id: 0, // an id used to name things with an unique name + // branch + first-branch: false, // true if the next element is the first in a branch + // cycle + first-fragment: none, // name of the first fragment in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices +) + +#let draw-hooks-links(links, mol-name, ctx, from-mol) = { + let hook-id = 0 + for (to-name, (link,)) in links { + if link.at(mol-name, default: none) == none { + link.name = mol-name + "-hook-" + str(hook-id) + hook-id += 1 + } + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "fragment" { + ctx.links.push(( + type: "link", + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + from: none, + to: none, + over: link.at("over", default: none), + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + over: link.at("over", default: none), + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + } + ctx +} + +#let draw-fragments-and-link(ctx, body) = { + let fragment-drawing = () + let cetz-drawing = () + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + let drawing = () + let cetz-rec = () + if type(element) == function { + cetz-drawing.push(element) + } else if "type" not in element { + panic("Element " + repr(element) + " has no type") + } else if element.type == "fragment" { + (ctx, drawing) = fragment.draw-fragment(element, ctx) + } else if element.type == "link" { + (ctx, drawing) = link.draw-link(element, ctx) + } else if element.type == "branch" { + (ctx, drawing, cetz-rec) = branch.draw-branch(element, ctx, draw-fragments-and-link) + } else if element.type == "cycle" { + (ctx, drawing, cetz-rec) = cycle.draw-cycle(element, ctx, draw-fragments-and-link) + } else if element.type == "hook" { + ctx = hook.draw-hook(element, ctx) + } else if element.type == "parenthesis" { + (ctx, drawing, cetz-rec) = parenthesis.draw-parenthesis( + element, + ctx, + draw-fragments-and-link, + ) + } else if element.type == "operator" { + (ctx, drawing) = operator.draw-operator(element, fragment-drawing, ctx) + } else { + panic("Unknown element type " + element.type) + } + fragment-drawing += drawing + cetz-drawing += cetz-rec + } + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + ctx.last-anchor.drew = true + } + ( + ctx, + fragment-drawing, + cetz-drawing, + ) +} + +#let draw-link-over(ctx, link, over, angle) = { + let name = link.name + "-over" + let (over, length, radius) = if type(over) == str { + (over, link-over-radius, link-over-radius) + } else if type(over) == dictionary { + let name = over.at("name", default: none) + if name == none { + panic("Over argument must have a name") + } + ( + name, + over.at("length", default: link-over-radius), + over.at("radius", default: link-over-radius) + ) + } else { + panic("Over must be a string or a dictionary, got " + type(link.at("over"))) + } + intersections(name, over, link.name) + let color = if ctx.config.debug { + red + } else { + white + } + scope({ + rotate(angle) + rect( + anchor: "center", + (to: name + ".0", rel: (-length/2,-radius/2)), + (rel: (length, radius)), + fill: color, + stroke: color + ) + }) +} + +#let draw-link-decoration(ctx) = { + ( + ctx, + get-ctx(cetz-ctx => { + for link in ctx.links { + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + if link.at("over") != none { + if type(link.at("over")) == array { + for over in link.at("over") { + draw-link-over(ctx, link, over, angle) + } + } else { + draw-link-over(ctx, link, link.at("over"), angle) + } + } + + scope({ + set-origin(from) + rotate(angle) + (link.draw)(length, ctx, cetz-ctx, override: link.override) + }) + } + }), + ) +} + +/// set elements names and split the molecule into sub-groups +#let preprocessing(body, group-id: 0, link-id: 0, operator-id: 0) = { + let result = ((),) + for element in body { + if type(element) == dictionary { + if element.at("name", default: none) == none { + if element.type == "fragment" { + element.name = "fragment-" + str(group-id) + group-id += 1 + } else if element.type == "link" { + element.name = "link-" + str(link-id) + link-id += 1 + } else if element.type == "operator" { + element.name = "operator-" + str(operator-id) + operator-id += 1 + } + } + if element.at("body", default: none) != none { + let child-body + (child-body, group-id, link-id, operator-id) = preprocessing( + element.body, + group-id: group-id, + link-id: link-id, + operator-id: operator-id, + ) + if element.type == "parenthesis" and element.resonance { + element.body = child-body + } else { + element.body = child-body.at(0) + } + } + if element.type == "operator" or (element.type == "parenthesis" and element.resonance) { + result.push(element) + result.push(()) + } else { + result.at(-1).push(element) + } + } else { + result.at(-1).push(element) + } + } + (result, group-id, link-id, operator-id) +} + +#let operator-group-name = id => { + "operator-group-" + str(id) +} + +#let draw-groups(ctx, bodies, after-operator: false) = { + ctx.last-name = "" + + let drawables = for (i, body) in bodies.enumerate() { + if type(body) == array { + if body.len() > 0 { + ctx.last-name = operator-group-name(i) + get-ctx(cetz-ctx => { + let ctx = ctx + if ctx.last-anchor.type == "coord" { + (cetz-ctx, ctx.last-anchor.anchor) = cetz.coordinate.resolve(cetz-ctx, ctx.last-anchor.anchor) + } + let last-anchor = ctx.last-anchor + let (ctx, atoms, cetz-drawing) = draw-fragments-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + + let molecule = { + atoms + } + + let (ctx: cetz-ctx, drawables, bounds: molecule-bounds, anchors) = custom-process.many(cetz-ctx, molecule) + molecule-bounds = cetz.util.revert-transform(cetz-ctx.transform, molecule-bounds) + + let (translate-x, translate-y) = if after-operator { + let (_, origin-anchor) = cetz.coordinate.resolve(cetz-ctx, last-anchor.anchor) + ( + origin-anchor.at(0) - molecule-bounds.low.at(0), + -origin-anchor.at(1) + + ( + molecule-bounds.low.at(1) + molecule-bounds.high.at(1) + ) + / 2, + ) + } else { + (0, 0) + } + let transform-matrix = cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ) + // panic(anchors) + for name in anchors { + let hold-anchors = cetz-ctx.nodes.at(name).anchors + cetz-ctx.nodes.at(name).anchors = (name) => { + if name != () { + cetz.matrix.mul4x4-vec3(transform-matrix, hold-anchors(name)) + } else { + hold-anchors(name) + } + } + } + ( + _ => ( + ctx: cetz-ctx, + drawables: cetz.drawable.apply-transform( + cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ), + drawables, + ), + ), + ) + scope({ + translate(x: translate-x, y: -translate-y) + draw-link-decoration(ctx).at(1) + on-layer(2, cetz-drawing) + let bound-rect = cetz.draw.rect( + molecule-bounds.low, + molecule-bounds.high, + name: ctx.last-name, + stroke: purple, + ) + if ctx.config.debug { + bound-rect + } else { + cetz.draw.hide(bound-rect) + } + }) + }) + } + } else if body.type == "operator" { + let (op-ctx, drawable) = operator.draw-operator(body, ctx) + after-operator = true + ctx = op-ctx + drawable + } else if body.type == "parenthesis" { + let (parenthesis-ctx, drawable) = parenthesis.draw-resonance-parenthesis(body, draw-groups, ctx) + ctx = parenthesis-ctx + drawable + } else { + panic("Unexpected element type: " + body.type) + } + } + (drawables, ctx) +} + +#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { + let config = merge-dictionaries(config, default) + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + + let bodies = preprocessing(body).at(0) + let (final-drawing, ctx) = draw-groups(ctx, bodies) + + if name == none { + final-drawing + } else { + group(name: name, anchor: mol-anchor, { + anchor("default", (0, 0)) + final-drawing + }) + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas(debug: debug, background: background, draw-skeleton(config: config, body)) +} + +#let skeletize-config(default-config) = { + let config-function(debug: false, background: none, config: (:), body) = { + skeletize(debug: debug, background: background, config: merge-dictionaries(config, default-config), body) + } + config-function +} + +#let draw-skeleton-config(default-config) = { + let config-function(config: (:), name: none, mol-anchor: none, body) = { + draw-skeleton(config: merge-dictionaries(config, default-config), body) + } + config-function +} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/branch.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/branch.typ new file mode 100644 index 0000000000..2c9e062843 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/branch.typ @@ -0,0 +1,32 @@ +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +/// Compute the angle the branch should take in order to be "in the center" +/// of the cycle angle +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +#let draw-branch(branch, ctx, draw-fragments-and-link) = { + let angle = angles.angle-from-ctx(ctx, branch.args, cycle-angle(ctx)) + let (branch-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + branch.body, + ) + ctx = context_.update-parent-context(ctx, branch-ctx) + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cram.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cram.typ new file mode 100644 index 0000000000..c66f6c127d --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cram.typ @@ -0,0 +1,61 @@ +#import "@preview/cetz:0.4.1" +#import "../utils/utils.typ" + +/// Draw a triangle between two fragments +#let cram(from, to, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.filled-cram.base-length), + ) + cetz.draw.line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: ctx.config.filled-cram.stroke), + fill: args.at("fill", default: ctx.config.filled-cram.fill), + ) +} + +/// Draw a dashed triangle between two molecules fragments +#let dashed-cram(from, to, length, ctx, cetz-ctx, args) = { + import cetz.draw: * + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.dashed-cram.base-length), + ) + let tip-length = utils.convert-length( + cetz-ctx, + args.at("tip-length", default: ctx.config.dashed-cram.tip-length), + ) + hide({ + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - tip-length / 2)) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + tip-length / 2), + ) + }) + let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) + let dash-gap = utils.convert-length(cetz-ctx, args.at("dash-gap", default: ctx.config.dashed-cram.dash-gap)) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(cetz-ctx, dash-width) + let length = utils.convert-length(cetz-ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cycle.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cycle.typ new file mode 100644 index 0000000000..9a81f12930 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cycle.typ @@ -0,0 +1,155 @@ +#import "@preview/cetz:0.4.1" +#import "../utils/utils.typ" +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +#let max-int = 9223372036854775807 + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + 0, + )) + } + vertex +} + +/// extrapolate the center and the radius of the cycle +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + let debug = () + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + debug += cetz.draw.circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius, debug) +} + +#let draw-cycle-center-arc(ctx, name, center-arc) = { + import cetz.draw: * + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius, debug) = cycle-center-radius(ctx, cetz-ctx, vertex) + debug + if name != none { + anchor(name, center) + } + if center-arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= center-arc.at("radius", default: 0.7) + } else { + min-radius *= center-arc.at("radius", default: 0.5) + } + let start = center-arc.at("start", default: 0deg) + let end = center-arc.at("end", default: 360deg) + let delta = center-arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + arc( + center, + ..center-arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) +} + +#let draw-cycle(cycle, ctx, draw-fragments-and-link) = { + let cycle-step-angle = 360deg / cycle.faces + let angle = angles.angle-from-ctx(ctx, cycle.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ( + ctx.relative-angle == 0deg + and ctx.angle == 0deg + and not cycle.args.at( + "align", + default: false, + ) + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-fragment = none + if ctx.last-anchor.type == "fragment" { + first-fragment = ctx.last-anchor.name + if first-fragment not in ctx.hooks { + ctx.hooks.insert(first-fragment, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in cycle.args { + name = cycle.args.at("name") + record-vertex = true + } else if "arc" in cycle.args { + record-vertex = true + } + let (cycle-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: cycle.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-fragment: first-fragment, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + cycle.body, + ) + ctx = context_.update-parent-context(ctx, cycle-ctx) + if record-vertex { + drawing += draw-cycle-center-arc(cycle-ctx, name, cycle.args.at("arc", default: none)) + } + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/fragment.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/fragment.typ new file mode 100644 index 0000000000..b1db6aa0c3 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/fragment.typ @@ -0,0 +1,149 @@ +#import "../utils/context.typ": * +#import "../utils/anchors.typ": * +#import "@preview/cetz:0.4.1" + +#let draw-fragment-text(ctx, mol, pos) = { + import cetz.draw: * + for (id, eq) in mol.atoms.enumerate() { + let name = str(id) + let color = if mol.colors != none { + if type(mol.colors) == color { + mol.colors + } else { + mol.colors.at(calc.min(id, mol.colors.len() - 1)) + } + } else { + fragment-color + } + + // draw atoms of the group one after the other from left to right + content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "mid-west" + }, + ( + if id == 0 { + pos + } else if mol.vertical { + (to: str(id - 1) + ".south", rel: (0, -.2em)) + } else { + str(id - 1) + ".mid-east" + } + ), + auto-scale: false, + { + show math.equation: math.upright + set text(fill: color) if color != none + set text(font: fragment-font) if fragment-font != none + eq + }, + ) + id += 1 + } +} + +#let draw-fragment-lewis(ctx, group-name, count, lewis) = { + if lewis.len() == 0 { + return () + } + import cetz.draw: * + get-ctx(cetz-ctx => { + for (id, (angle: lewis-angle, radius, draw)) in lewis.enumerate() { + if (lewis-angle == none) { + lewis-angle = ctx.config.lewis.angle + } + if (radius == none) { + radius = ctx.config.lewis.radius + } + let lewis-angle = angles.angle-correction(lewis-angle) + let mol-id = if angles.angle-in-range-inclusive(lewis-angle, 90deg, 270deg) { + 0 + } else { + count - 1 + } + let anchor = fragment-anchor( + ctx, + cetz-ctx, + lewis-angle, + group-name, + str(mol-id), + margin: radius, + ) + scope({ + set-origin(anchor) + rotate(lewis-angle) + draw(ctx, cetz-ctx) + }) + } + }) +} + +#let draw-fragment-elements(mol, ctx) = { + let name = mol.name + if name in ctx.hooks { + panic("Molecule fragment with name " + name + " already exists : " + ctx.hooks.keys().join(", ")) + } + ctx.hooks.insert(name, mol) + + let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("west", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-fragment-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let group-anchor = link-fragment-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (group-anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule fragment must be linked to a coord or a link") + } + ctx = context_.set-last-anchor( + ctx, + (type: "fragment", name: name, count: mol.at("count"), vertical: mol.vertical), + ) + if (side) { + ctx.id += 1 + } + ( + ctx, + { + import cetz.draw: * + group( + anchor: if side { + group-anchor + } else { + "from" + str(ctx.id) + }, + name: name, + { + anchor("default", coord) + draw-fragment-text(ctx, mol, coord) + if not side { + anchor("from" + str(ctx.id), group-anchor) + } + }, + ) + draw-fragment-lewis(ctx, name, mol.count, mol.at("lewis")) + }, + ) +} + +#let draw-fragment(element, ctx) = { + if ctx.first-branch { + panic("A molecule fragment can not be the first element in a cycle") + } + let (ctx, drawing) = draw-fragment-elements(element, ctx) + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + (ctx, drawing) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/hook.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/hook.typ new file mode 100644 index 0000000000..1c5dafb9d1 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/hook.typ @@ -0,0 +1,14 @@ + +#let draw-hook(hook, ctx) = { + if hook.name in ctx.hooks { + panic("Hook " + hook.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + ctx +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/link.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/link.typ new file mode 100644 index 0000000000..d6c0c0876a --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/link.typ @@ -0,0 +1,103 @@ +#import "../utils/angles.typ" +#import "../utils/anchors.typ": * + +#let create-link-decorations-anchors(link, ctx) = { + let link-angle = 0deg + let to-name = none + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-fragment != none { + to-name = ctx.first-fragment + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angles.angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = angles.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angles.angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let from-pos = if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "fragment" { + from-connection = link-fragment-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + fragment-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name") + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = context_.set-last-anchor( + ctx, + ( + type: "link", + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: to-name, + to: to-connection, + angle: link-angle, + over: link.at("over", default: none), + draw: link.draw, + ), + ) + ( + ctx, + { + hide({ + circle(name: link-name + "-start-anchor", from-pos, radius: .25em) + }) + let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + line(from-pos, end-anchor, stroke: blue + .1em) + } + hide( + { + circle(name: link-name + "-end-anchor", end-anchor, radius: .25em) + }, + bounds: true, + ) + }, + ) +} + +#let draw-link(link, ctx) = { + ctx.first-branch = false + let drawing + (ctx, drawing) = create-link-decorations-anchors(link, ctx) + ctx.faces-count += 1 + if link.links.len() != 0 { + ctx.hooks-links.push((link.links, ctx.last-anchor.name, false)) + } + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/operator.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/operator.typ new file mode 100644 index 0000000000..ff1fd2792b --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/operator.typ @@ -0,0 +1,36 @@ +#import "@preview/cetz:0.4.1": draw, process, util + +#let draw-operator(operator, ctx) = { + import draw: * + + let op-name = operator.name + ctx.last-anchor = (type: "coord", anchor: (rel: (operator.margin, 0), to: (name: op-name, anchor: "east"))) + + ( + ctx, + get-ctx(cetz-ctx => { + + let west-previous-mol-anchor = (name: ctx.last-name, anchor: "east") + + let east-op-anchor = (rel: (operator.margin, 0), to: west-previous-mol-anchor) + + let op = if (operator.op == none) { + "" + } else { + operator.op + } + + content( + name: op-name, + anchor: "west", + east-op-anchor, + op, + ) + + if (ctx.config.debug) { + circle(west-previous-mol-anchor, radius: 0.05, fill: yellow, stroke: none) + circle(ctx.last-anchor.anchor, radius: 0.05, fill: yellow, stroke: none) + } + }), + ) +} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/parenthesis.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/parenthesis.typ new file mode 100644 index 0000000000..75d6b07a0a --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/parenthesis.typ @@ -0,0 +1,215 @@ +#import "../utils/utils.typ" +#import "@preview/cetz:0.4.1" + +#let left-parenthesis-anchor(parenthesis, ctx) = { + let anchor = if parenthesis.body.at(0).type == "fragment" { + let name = parenthesis.body.at(0).name + parenthesis.body.at(0).name = name + (name: name, anchor: "west") + } else if parenthesis.body.at(0).type == "link" { + let name = parenthesis.body.at(0).at("name") + parenthesis.body.at(0).name = name + (name + "-start-anchor", 45%, name + "-end-anchor") + } else if not left { } else { + panic("The first element of a parenthesis must be a molecule fragment or a link") + } + (ctx, parenthesis, anchor) +} + +#let right-parenthesis-anchor(parenthesis, ctx) = { + let right-name = parenthesis.at("right") + let right-type = "" + if right-name != none { + right-type = utils.get-element-type(parenthesis.body, right-name) + if right-type == none { + panic("The right element of the parenthesis does not exist") + } + } else { + right-type = parenthesis.body.at(-1).type + right-name = parenthesis.body.at(-1).at("name", default: none) + } + + let anchor = if right-type == "fragment" { + (name: right-name, anchor: "east") + } else if right-type == "link" { + (right-name + "-start-anchor", 55%, right-name + "-end-anchor") + } else { + panic("The last element of a parenthesis must be a molecule fragment or a link but got " + right-type) + } + (ctx, parenthesis, anchor) +} + +#let parenthesis-content(parenthesis, height, cetz-ctx) = { + let block = block(height: height * cetz-ctx.length * 1.2, width: 0pt) + let left-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($parenthesis.l block$, size: 100%) + } + let right-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($block parenthesis.r$, size: 100%) + } + + let right-parenthesis-with-attach = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.attach(right-parenthesis, br: parenthesis.br, tr: parenthesis.tr) + } + + (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) +} + +#let draw-parenthesis(parenthesis, ctx, draw-molecules-and-link) = { + let (parenthesis-ctx, drawing, cetz-rec) = draw-molecules-and-link( + ctx, + parenthesis.body, + ) + ctx = parenthesis-ctx + + + drawing += { + import cetz.draw: * + get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + { + drawing + }, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + + let sub-height = utils.bounding-box-height(sub-bounds) + let sub-v-mid = sub-bounds.low.at(1) - sub-height / 2 + + let sub-width = utils.bounding-box-width(sub-bounds) + + let (ctx, parenthesis, left-anchor) = left-parenthesis-anchor(parenthesis, ctx) + + let (ctx, parenthesis, right-anchor) = right-parenthesis-anchor(parenthesis, ctx) + + let height = parenthesis.at("height") + if height == none { + if not parenthesis.align { + panic("You must specify the height of the parenthesis if they are not aligned") + } + height = sub-height + } else { + height = utils.convert-length(cetz-ctx, height) + } + + let (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) = parenthesis-content( + parenthesis, + height, + cetz-ctx, + ) + + let (_, (lx, ly, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, left-anchor) + let (_, (rx, ry, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, right-anchor) + + if ctx.config.debug { + circle((lx, ly), radius: 1pt, fill: orange, stroke: orange) + circle((rx, ry), radius: 1pt, fill: orange, stroke: orange) + rect((lx, sub-bounds.low.at(1)), (rx, sub-bounds.high.at(1)), stroke: green) + line((lx, sub-v-mid), (rx, sub-v-mid), stroke: green) + } + + let hoffset = calc.abs(sub-width - calc.abs(rx - lx)) + + if parenthesis.align { + ly -= ly - sub-v-mid + ry = ly + } else if type(parenthesis.yoffset) == array { + if parenthesis.yoffset.len() != 2 { + panic("The parenthesis yoffset must be a list of length 2 or a number") + } + ly += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(0)) + ry += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(1)) + } else if parenthesis.yoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.yoffset) + ly += offset + ry += offset + } + + if type(parenthesis.xoffset) == array { + if parenthesis.xoffset.len() != 2 { + panic("The parenthesis xoffset must be a list of length 2 or a number") + } + lx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(0)) + rx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(1)) + } else if parenthesis.xoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.xoffset) + lx -= offset + rx += offset + } + + let right-bounds = cetz.process.many(cetz-ctx, content((0, 0), right-parenthesis, auto-scale: false)).bounds + let right-with-attach-bounds = cetz + .process + .many(cetz-ctx, content((0, 0), right-parenthesis-with-attach, auto-scale: false)) + .bounds + let right-voffset = calc.abs(right-bounds.low.at(1) - right-with-attach-bounds.low.at(1)) + if parenthesis.tr != none and parenthesis.br != none { + right-voffset /= 2 + } else if (parenthesis.tr != none) { + right-voffset *= -1 + } + + ( + _ => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + content((lx, ly), anchor: "mid-east", left-parenthesis, auto-scale: false) + content((rx, ry - right-voffset), anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }) + } + + (ctx, drawing, cetz-rec) +} + + +#let draw-resonance-parenthesis(parenthesis, draw-function, ctx) = { + import cetz.draw: * + let left-name = "parenthesis-" + str(ctx.id) + let right-name = "parenthesis-" + str(ctx.id + 1) + ctx.id += 2 + let last-anchor = ctx.last-anchor + let (drawing, ctx) = draw-function(ctx, parenthesis.body, after-operator: true) + + let drawing = get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + drawing, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + let height = parenthesis.height + if height == none { + height = utils.bounding-box-height(sub-bounds) + } else { + height = utils.convert-length(cetz-ctx, height) + } + let width = utils.bounding-box-width(sub-bounds) + + let (left-parenthesis, _, right-parenthesis-with-attach) = parenthesis-content(parenthesis, height, cetz-ctx) + + let right-anchor = (rel: (width, 0), to: (name: left-name, anchor: "east")) + on-layer( + 1, + { + content(last-anchor.anchor, name: left-name, anchor: "mid-east", left-parenthesis, auto-scale: false) + content(right-anchor, name: right-name, anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }, + ) + ( + ctx => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + }) + + ctx.last-anchor = (type: "coord", anchor: (name: right-name, anchor: "mid-east")) + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/elements/fragment.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/elements/fragment.typ new file mode 100644 index 0000000000..e7b66eb50f --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/elements/fragment.typ @@ -0,0 +1,74 @@ +#let split-equation(mol, equation: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + for m in mol { + let last-number-hold = last-number + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + result.push(m) + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in fragment fragment") + } + last-number = true + result.push(m) + } else { + panic("Invalid fragment fragment content") + } + } else if m.func() == math.attach or m.func() == math.lr { + result.push(m) + } else if m == [ ] { + continue + } else { + panic("Invalid fragment fragment content") + } + if last-number-hold { + result.at(-2) = result.at(-2) + result.at(-1) + let _ = result.pop() + last-number = false + } + } + result +} + +#let fragment-cor-regex = "[0-9]*[A-Z][a-z]*'*" +#let exponent-regex = "((?:[0-9]+(?:\\+|\\-)?)|[A-Z]|\\+|\\-)" +#let exponent-base-regex = "(?:(\\^|_)" + exponent-regex + ")?(?:(\\^|_)" + exponent-regex + ")?" +#let fragment-regex = regex("^ *(" + fragment-cor-regex + ")" + exponent-base-regex) + +#let split-string(mol) = { + let aux(str) = { + let match = str.match(fragment-regex) + if match == none { + panic(str + " is not a valid fragment") + } else if match.captures.at(1) == match.captures.at(3) and match.captures.at(1) != none { + panic("You cannot use an exponent and a subscript twice") + } + let eq = "\"" + match.captures.at(0) + "\"" + if match.captures.at(1) != none { + eq += match.captures.at(1) + "(" + match.captures.at(2) + ")" + } + if match.captures.at(3) != none { + eq += match.captures.at(3) + "(" + match.captures.at(4) + ")" + } + let eq = math.equation(eval(eq, mode: "math")) + (eq, match.end) + } + + while not mol.len() == 0 { + let (eq, end) = aux(mol) + mol = mol.slice(end) + (eq,) + } +} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/elements/lewis.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/elements/lewis.typ new file mode 100644 index 0000000000..fb0d3547bc --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/elements/lewis.typ @@ -0,0 +1,133 @@ +#import "@preview/cetz:0.4.1" + +/// Create a lewis function that is then used to draw a lewis +/// formulae element around the fragment +/// +/// - draw-function (function): The function that will be used to draw the lewis element. It should takes three arguments: the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-lewis(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Lewis function takes no positional arguments") + } + let args = args.named() + let angle = args.at("angle", default: none) + let radius = args.at("radius", default: none) + ( + angle: angle, + radius: radius, + draw: (ctx, cetz-ctx) => draw-function(ctx, cetz-ctx, args) + ) + } +} + +/// draw a sigle electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// +/// The position of the electron is set by the `offset` argument. Available values are: +/// - "top": the electron is placed above the fragment center line +/// - "bottom": the electron is placed below the fragment center line +/// - "center": the electron is placed at the fragment center line +/// +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-single(offset: "top"), +/// )) +/// single(angle:-2) +/// fragment("B", lewis:( +/// lewis-single(offset: "bottom"), +/// )) +/// single(angle:-2) +/// fragment("C", lewis:( +/// lewis-single(offset: "center"), +/// )) +/// }) +/// ```) +#let lewis-single = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-single.radius) + let gap = args.at("gap", default: ctx.config.lewis-single.gap) + let offset = args.at("offset", default: ctx.config.lewis-single.offset) + let gap = if offset == "top" { + gap + } else if offset == "bottom" { + -gap + } else if offset != "center" { + panic("Invalid position, expected 'top', 'bottom' or 'center'") + } + let fill = args.at("fill", default: ctx.config.lewis-single.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-single.stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-double(), +/// lewis-double(angle: 90deg), +/// lewis-double(angle: 180deg), +/// lewis-double(angle: -90deg) +/// )) +/// }) +/// ```) +#let lewis-double = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-double.radius) + let gap = args.at("gap", default: ctx.config.lewis-double.gap) + let fill = args.at("fill", default: ctx.config.lewis-double.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-double.stroke) + circle((0, -gap), radius: radius, fill: fill, stroke: stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron liked by a single line +/// +/// It is possible to change the length of the line with the `lenght` argument. +/// It is also possible to change the `stroke` agument +/// #example(``` +/// #skeletize({ +/// fragment("B", lewis:( +/// lewis-line(angle: 45deg), +/// lewis-line(angle: 135deg), +/// lewis-line(angle: -45deg), +/// lewis-line(angle: -135deg) +/// )) +/// }) +/// ```) +#let lewis-line = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let length = args.at("length", default: ctx.config.lewis-line.length) + let stroke = args.at("stroke", default: ctx.config.lewis-line.stroke) + line((0, -length / 2), (0, length / 2), stroke: stroke) +}) + + +/// Draw a rectangle to denote a lone pair of electrons +/// +/// It is possible to change the height and width of the rectangle with the `height` and `width` arguments. +/// It is also possible to change the `fill` and `stroke` arguments +/// #example(``` +/// #skeletize({ +/// fragment("C", lewis:( +/// lewis-rectangle(), +/// lewis-rectangle(angle: 180deg) +/// )) +/// }) +/// ```) +#let lewis-rectangle = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let height = args.at("height", default: ctx.config.lewis-rectangle.height) + let width = args.at("width", default: ctx.config.lewis-rectangle.width) + let fill = args.at("fill", default: ctx.config.lewis-rectangle.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-rectangle.stroke) + rect((-width / 2, -height / 2), (width / 2, height / 2), fill: fill, stroke: stroke) +}) diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/elements/links.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/elements/links.typ new file mode 100644 index 0000000000..a96321367a --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/elements/links.typ @@ -0,0 +1,314 @@ +#import "@preview/cetz:0.4.1" +#import "../drawer/cram.typ": * +#import "../utils/utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes four arguments: the length of the link, the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, cetz-ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, cetz-ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single(stroke: red + 5pt) +/// fragment("B") +/// }) +///```) +#let single = build-link((length, ctx, _, args) => { + import cetz.draw: * + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.single.stroke)) +}) + +/// Draw a double line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// fragment("B") +/// }) +///```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double(offset: "right") +/// fragment("B") +/// double(offset: "left") +/// fragment("C") +/// double(offset: "center") +/// fragment("D") +/// }) +///```) +#let double = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.double.gap)) / 2 + let offset = args.at("offset", default: ctx.config.double.offset) + let coeff = args.at("offset-coeff", default: ctx.config.double.offset-coeff) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + let gap-offset = if offset == "right" { + -gap + } else if offset == "left" { + gap + } else if offset == "center" { + 0 + } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, -gap + gap-offset), (x + length * coeff, -gap + gap-offset)) + } else { + ((0, -gap + gap-offset), (length, -gap + gap-offset)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, gap + gap-offset), (x + length * coeff, gap + gap-offset)) + } else { + ((0, gap + gap-offset), (length, gap + gap-offset)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) +}) + +/// Draw a triple line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// fragment("B") +/// }) +///```) +#let triple = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.triple.gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + line((0, -gap), (length, -gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + line((0, gap), (length, gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) +}) + +/// Draw a filled cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, cetz-ctx, args) => cram( + (0, 0), + (length, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a filled cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, cetz-ctx, args) => cram( + (length, 0), + (0, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a hollow cram between two fragments with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((0, 0), (length, 0), ctx, cetz-ctx, args) +}) + +/// Draw a hollow cram between two fragments with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((length, 0), (0, 0), ctx, cetz-ctx, args) +}) + +/// Draw a dashed cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the arrow base length with the `base-length` argument and the tip length with the `tip-length` argument. +// You can also change distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// tip-length: 1em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-right = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + cetz-ctx, + args, +)) + +/// Draw a dashed cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + cetz-ctx, + args, +)) + + +/// Draw a plus sign between two fragments +/// #example(```` +/// #skeletize({ +/// fragment("A") +/// plus() +/// fragment("B") +/// }) +/// ````) +/// You can change the filling, size and stroke of the glyph with the `fill`, `size` and `stoke` arguments. The default values are the parent `text` parameters. +#let plus = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + content( + anchor: "mid", + (length / 2, 0), + { + set text(fill: args.fill) if "fill" in args + set text(stroke: args.stroke) if "stroke" in args + set text(size: args.size) if "size" in args + text($+$) + } + ) +}) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/anchors.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/anchors.typ new file mode 100644 index 0000000000..285efcde8c --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/anchors.typ @@ -0,0 +1,296 @@ +#import "angles.typ" +#import "context.typ" as context_ +#import "utils.typ": * +#import "@preview/cetz:0.4.1" +#import cetz.draw: * + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +/// Calculate an anchor position around a fragment using an ellipse +/// at a given angle +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - cetz-ctx (cetz-ctx): the cetz context +/// - angle (float, int, angle): the angle of the anchor +/// - fragment (string): the fragment name +/// - id (string): the fragment subpart id +/// - margin (length, none): the margin around the fragment +/// -> anchor: the anchor position around the fragment +#let fragment-anchor(ctx, cetz-ctx, angle, fragment, id, margin: none) = { + let angle = angles.angle-correction(angle) + let fragment-margin = if margin == none { + ctx.config.fragment-margin + } else { + margin + } + fragment-margin = convert-length(cetz-ctx, fragment-margin) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "mid")), + ) + let (a, b) = if angles.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, fragment-margin, fragment, id) + } else { + anchor-south-east(cetz-ctx, center, fragment-margin, fragment, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if angles.angle-in-range-inclusive(angle, 0deg, 90deg) or angles.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +/// Return the index to choose if the link connection is not overridden +#let link-fragment-index(angle, end, count, vertical) = { + if not end { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let fragment-link-anchor(name, id, count) = { + if count <= id { + panic("The last fragment only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "mid")) +} + +#let link-fragment-anchor(name: none, id, count) = { + if id >= count { + panic("This fragment only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the fragment to link to must be defined") + } + if name == none { + (name: str(id), anchor: "mid") + } else { + (name: name, anchor: (str(id), "mid")) + } +} + + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "mid") + if link.to == none or link.from == none { + let angle = link.at( + "angle", + default: angles.angle-between(cetz-ctx, link.from-pos, to-pos), + ) + link.angle = angle + if link.from == none { + link.from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + if link.from == -1 { + link.from = 0 + } + if link.to == -1 { + link.to = ctx.hooks.at(link.to-name).count - 1 + } + let start = fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) + let end = fragment-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) + ((start, end), angles.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "mid"), + ), + ) + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "mid")), + ), + ) + } + let end-anchor = fragment-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + angles.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = angles.angle-correction(angles.angle-between(cetz-ctx, link.from-pos, hook.hook)) + let from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = fragment-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) + ( + ( + start-anchor, + hook.hook, + ), + angles.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + angles.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/angles.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/angles.typ new file mode 100644 index 0000000000..2b19a3d0b3 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/angles.typ @@ -0,0 +1,98 @@ +#import "@preview/cetz:0.4.1" + +/// Convert any angle to an angle between 0deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a += 360 + } + + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +/// Check if the angle is in the range ]from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +/// Check if the angle is in the range [from, to] +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +/// +/// - ctx (cetz-ctx): The cetz context +/// - from (anchor): The first anchor +/// - to (anchor): The second anchor +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// Calculate the angle from an object with "relative", "absolute" or "angle" key +/// according to the alchemist context (see the manual to see how angles are calculated) +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - object (dict): the object to get the angle from +/// - default (angle): the default angle +/// -> the calculated angle +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +/// Overwrite the offset based on the angle +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/context.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/context.typ new file mode 100644 index 0000000000..4021bb5177 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/context.typ @@ -0,0 +1,33 @@ +#let update-parent-context(parent-ctx, ctx) = { + let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { + ( + ..parent-ctx.last-anchor, + drew: true, + ) + } else { + parent-ctx.last-anchor + } + ( + ..parent-ctx, + last-anchor: last-anchor, + hooks: ctx.hooks, + hooks-links: ctx.hooks-links, + links: ctx.links, + ) +} + +/// Set the last anchor in the context to the given anchor and save it if needed +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" { + let drew = ctx.last-anchor.at("drew", default: false) + if drew and anchor.type == "link" and anchor.name == ctx.last-anchor.name { + drew = false + let _ = ctx.links.pop() + } + if not drew { + ctx.links.push(ctx.last-anchor) + } + ctx.last-anchor.drew = true + } + (..ctx, last-anchor: anchor) +} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/utils.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/utils.typ new file mode 100644 index 0000000000..bac944c011 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/utils.typ @@ -0,0 +1,75 @@ +#import "@preview/cetz:0.4.1" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + return if type(num) == length { + float(num.to-absolute() / ctx.length) + } else if type(num) == ratio { + num + } else { + float(num) + } +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} + +/// merge two imbricated dictionaries together +/// The second dictionary is the default value if the key is not present in the first dictionary +#let merge-dictionaries(dict1, default) = { + let result = default + for (key, value) in dict1 { + if type(value) == dictionary { + result.insert(key, merge-dictionaries(value, default.at(key))) + } else { + result.insert(key, value) + } + } + result +} + + +/// get the type of an element by its name +/// +/// - body (drawable): the chemfig body of a molecule +/// - name (string): the name of the element to get the type +/// -> string +#let get-element-type(body, name) = { + for element in body { + if type(element) != dictionary { + continue + } + if "name" in element and element.name == name { + return element.type + } + if element.type == "branch" or element.type == "cycle" or element.type == "parenthesis" { + let type = get-element-type(element.body, name) + if type != none { + return type + } + } + } + none +} + +/// Calculate the height of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-height(bounds) = { + calc.abs(bounds.high.at(1) - bounds.low.at(1)) +} + +/// Calculate the width of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-width(bounds) = { + calc.abs(bounds.high.at(0) - bounds.low.at(0)) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/typst.toml b/packages/preview/alchemist/0.1.7/0.1.7/typst.toml new file mode 100644 index 0000000000..e272238a75 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/0.1.7/typst.toml @@ -0,0 +1,13 @@ +[package] +name = "alchemist" +version = "0.1.7" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.13.1" +repository = "https://github.com/Typsium/alchemist" +description = "A package to render skeletal formulas using CeTZ" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization", "paper"] +disciplines = ["education", "chemistry", "biology"] +keywords = ["chemistry", "chemical", "formula", "molecule", "chemiviz", "structure", "organic", "lewis", "bond", "aromatic"] From ce2aa9c5aac8294ba2ba2939af3a6a67b92a6be0 Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Sat, 9 Aug 2025 20:22:05 +0300 Subject: [PATCH 12/24] Upload alchemist v0.1.7 --- bundler/src/main.rs | 3 +- bundler/src/timestamp.rs | 2 +- packages/preview/alchemist/0.1.7/LICENSE | 22 + packages/preview/alchemist/0.1.7/README.md | 134 ++++++ packages/preview/alchemist/0.1.7/lib.typ | 255 +++++++++++ .../alchemist/0.1.7/src/cetz/process.typ | 91 ++++ .../preview/alchemist/0.1.7/src/default.typ | 61 +++ .../preview/alchemist/0.1.7/src/drawer.typ | 403 ++++++++++++++++++ .../alchemist/0.1.7/src/drawer/branch.typ | 32 ++ .../alchemist/0.1.7/src/drawer/cram.typ | 61 +++ .../alchemist/0.1.7/src/drawer/cycle.typ | 155 +++++++ .../alchemist/0.1.7/src/drawer/fragment.typ | 149 +++++++ .../alchemist/0.1.7/src/drawer/hook.typ | 14 + .../alchemist/0.1.7/src/drawer/link.typ | 103 +++++ .../alchemist/0.1.7/src/drawer/operator.typ | 36 ++ .../0.1.7/src/drawer/parenthesis.typ | 215 ++++++++++ .../alchemist/0.1.7/src/elements/fragment.typ | 74 ++++ .../alchemist/0.1.7/src/elements/lewis.typ | 133 ++++++ .../alchemist/0.1.7/src/elements/links.typ | 314 ++++++++++++++ .../alchemist/0.1.7/src/utils/anchors.typ | 296 +++++++++++++ .../alchemist/0.1.7/src/utils/angles.typ | 98 +++++ .../alchemist/0.1.7/src/utils/context.typ | 33 ++ .../alchemist/0.1.7/src/utils/utils.typ | 75 ++++ packages/preview/alchemist/0.1.7/typst.toml | 13 + .../preview/codly-languages/0.1.6/README.md | 2 +- .../preview/codly-languages/0.1.6/lib.typ | 2 +- .../0.1.6/tests/basic-output/test.typ | 2 +- packages/preview/numty/0.0.5/typst.toml | 1 + packages/preview/prooftrees/0.1.0/README.md | 10 + packages/preview/prooftrees/0.1.0/typst.toml | 2 +- .../1.1.0/content/example/main.pdf | Bin 208514 -> 0 bytes packages/preview/wrap-it/0.1.1/README.md | 4 +- 32 files changed, 2786 insertions(+), 9 deletions(-) create mode 100644 packages/preview/alchemist/0.1.7/LICENSE create mode 100644 packages/preview/alchemist/0.1.7/README.md create mode 100644 packages/preview/alchemist/0.1.7/lib.typ create mode 100644 packages/preview/alchemist/0.1.7/src/cetz/process.typ create mode 100644 packages/preview/alchemist/0.1.7/src/default.typ create mode 100644 packages/preview/alchemist/0.1.7/src/drawer.typ create mode 100644 packages/preview/alchemist/0.1.7/src/drawer/branch.typ create mode 100644 packages/preview/alchemist/0.1.7/src/drawer/cram.typ create mode 100644 packages/preview/alchemist/0.1.7/src/drawer/cycle.typ create mode 100644 packages/preview/alchemist/0.1.7/src/drawer/fragment.typ create mode 100644 packages/preview/alchemist/0.1.7/src/drawer/hook.typ create mode 100644 packages/preview/alchemist/0.1.7/src/drawer/link.typ create mode 100644 packages/preview/alchemist/0.1.7/src/drawer/operator.typ create mode 100644 packages/preview/alchemist/0.1.7/src/drawer/parenthesis.typ create mode 100644 packages/preview/alchemist/0.1.7/src/elements/fragment.typ create mode 100644 packages/preview/alchemist/0.1.7/src/elements/lewis.typ create mode 100644 packages/preview/alchemist/0.1.7/src/elements/links.typ create mode 100644 packages/preview/alchemist/0.1.7/src/utils/anchors.typ create mode 100644 packages/preview/alchemist/0.1.7/src/utils/angles.typ create mode 100644 packages/preview/alchemist/0.1.7/src/utils/context.typ create mode 100644 packages/preview/alchemist/0.1.7/src/utils/utils.typ create mode 100644 packages/preview/alchemist/0.1.7/typst.toml delete mode 100644 packages/preview/touying-flow/1.1.0/content/example/main.pdf diff --git a/bundler/src/main.rs b/bundler/src/main.rs index 75e8d7034c..2beb7d09f0 100644 --- a/bundler/src/main.rs +++ b/bundler/src/main.rs @@ -308,6 +308,7 @@ fn build_archive(dir_path: &Path, manifest: &PackageManifest) -> anyhow::Result< let mut buf = vec![]; let compressed = flate2::write::GzEncoder::new(&mut buf, flate2::Compression::default()); let mut builder = tar::Builder::new(compressed); + builder.mode(tar::HeaderMode::Deterministic); let mut overrides = ignore::overrides::OverrideBuilder::new(dir_path); for exclusion in &manifest.package.exclude { @@ -456,7 +457,7 @@ fn validate_typst_file(path: &Path, name: &str) -> anyhow::Result<()> { bail!("{name} is missing"); } - if path.extension().map_or(true, |ext| ext != "typ") { + if path.extension().is_none_or(|ext| ext != "typ") { bail!("{name} must have a .typ extension"); } diff --git a/bundler/src/timestamp.rs b/bundler/src/timestamp.rs index 1831cedee2..ae5123a413 100644 --- a/bundler/src/timestamp.rs +++ b/bundler/src/timestamp.rs @@ -23,7 +23,7 @@ pub fn determine_timestamps( .par_iter() .map(|p| { if has_git { - timestamp_for_path_with_git(p) + timestamp_for_path_with_git(p).or_else(|_| timestamp_for_path_with_fs(p)) } else { timestamp_for_path_with_fs(p) } diff --git a/packages/preview/alchemist/0.1.7/LICENSE b/packages/preview/alchemist/0.1.7/LICENSE new file mode 100644 index 0000000000..a85204e590 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.7/README.md b/packages/preview/alchemist/0.1.7/README.md new file mode 100644 index 0000000000..c968af64b7 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/README.md @@ -0,0 +1,134 @@ +[![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FTypsium%2Falchemist%2Fmaster%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://github.com/Typsium/alchemist) +[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/alchemist/blob/master/LICENSE) +[![User Manual](https://img.shields.io/badge/manual-.pdf-purple)](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf) + +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + fragment(name: "A", "A") + single() + fragment("B") + branch({ + single(angle: 1) + fragment( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + fragment(name: "X", "X") + }) + branch({ + single(angle: -1) + fragment("Y") + single() + fragment( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + fragment( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic1/ref/1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + fragment("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic2/ref/1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.1.4": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). + +## Changelog + +### 0.1.7 + +- Updated cetz to version 0.4.1 +- Added default values for `color` and `font` for `fragment` elements +- Added a `skeletize-config` function to create a `skeletize` function with a specific configuration +- Fixed a bug with cetz anchors not being correctly translated +- Added an `over` argument to links to allow hide overlapped links + +### 0.1.6 + +- Fixed the parenthesis height to work with the new typst version +- Renamed `molecule` into `fragment` +- Added support for charges and apostrophes in the string of a molecule +- Fixed the parenthesis auto positioning and alignment +- Added a new operator element and a new parameter in parenthesis to write resonance formulae + +### 0.1.5 + +- Update to compiler 0.13.1 and Cetz 0.3.4 + +### 0.1.4 + +- Added the possibility to create Lewis formulae +- Added parenthesis element to create groups and polymer + +### 0.1.3 + +- Added the possibility to add exponent in the string of a molecule. + +### 0.1.2 + +- Added default values for link style properties. +- Updated `cetz` to version 0.3.1. +- Added a `tip-length` argument to dashed cram links. + +### 0.1.1 + +- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. +- Fixed multiples bugs that causes overdraws of links. + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.1.7/lib.typ b/packages/preview/alchemist/0.1.7/lib.typ new file mode 100644 index 0000000000..7d1f5c1a64 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/lib.typ @@ -0,0 +1,255 @@ +#import "@preview/cetz:0.4.1" +#import "src/default.typ": default +#import "src/utils/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ": skeletize, draw-skeleton, skeletize-config, draw-skeleton-config +#import "src/elements/links.typ": * +#import "src/elements/fragment.typ": * +#import "src/elements/lewis.typ": * + +#let transparent = color.rgb(100%, 0, 0, 0) + +/// === Fragment function +/// Build a fragment group based on mol +/// Each fragment is represented as an optional count followed by a fragment name +/// starting by a capital letter followed by an optional exponent followed by an optional indice. +/// #example(``` +/// #skeletize({ +/// fragment("H_2O") +/// }) +///```) +/// #example(``` +/// #skeletize({ +/// fragment("H^A_EF^5_4") +/// }) +/// ```) +/// It is possible to use an equation as a fragment. In this case, the splitting of the equation uses the same rules as in the string case. However, you get some advantages over the string version: +/// - You can use parenthesis to group elements together. +/// - You have no restriction about what you can put in exponent or indice. +/// #example(``` +/// #skeletize({ +/// fragment($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the fragment. It is used as the cetz name of the fragment and to link other fragments to it. +/// - links (dictionary): The links between this fragment and previous fragments or hooks. The key is the name of the fragment or hook and the value is the link function. See @links. +/// +/// Note that the atom-sep and angle arguments are ignored +/// - lewis (list): The list of lewis structures to draw around the fragments. See @lewis +/// - mol (string, equation): The string representing the fragment or an equation of the fragment +/// - vertical (boolean): If true, the fragment is drawn vertically +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", vertical: true) +/// }) +///```) +/// - colors (color|list): The color of the fragment. If a list is provided, it colors each group of the fragment with the corresponding color from right to left. If the number of colors is less than the number of groups, the last color is used for the remaining groups. If the number of colors is greater than the number of groups, the extra colors are ignored. +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", colors: (red, green, blue)) +/// single() +/// fragment("EFGH", colors: (orange)) +/// }) +/// ```) +/// -> drawable +#let fragment(name: none, links: (:), lewis: (), vertical: false, colors: none, mol) = { + let atoms = if type(mol) == str { + split-string(mol) + } else if mol.func() == math.equation { + split-equation(mol, equation: true) + } else { + panic("Invalid fragment content") + } + + if type(lewis) != array { + panic("Lewis formulae elements must be in a list") + } + + ( + ( + type: "fragment", + name: name, + atoms: atoms, + colors: colors, + links: links, + lewis: lewis, + vertical: vertical, + count: atoms.len(), + ), + ) +} +#let molecule(name: none, links: (:), lewis: (), vertical: false, mol) = fragment(name: name, links: links, lewis: lewis, vertical: vertical, mol) + +/// === Hooks +/// Create a hook in the fragment. It allows to connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the fragment. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current fragment, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// branch({ +/// single(angle:1) +/// fragment("B") +/// }) +/// branch({ +/// double(angle: -1) +/// fragment("D") +/// }) +/// single() +/// double() +/// single() +/// fragment("C") +/// }) +///```) +/// - body (drawable): the body of the branch. It must start with a link. +/// -> drawable +#let branch(body, ..args) = { + if args.pos().len() != 0 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", body: body, args: args.named()),) +} + +/// Create a regular cycle of fragments +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +/// - faces (int): the number of faces of the cycle +/// - body (drawable): the body of the cycle. It must start and end with a fragment or a link. +/// -> drawable +#let cycle(faces, body, ..args) = { + if args.pos().len() != 0 { + panic("Cycle takes two positional arguments: number of faces and body") + } + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + body: body, + args: args.named(), + ), + ) +} + +/// === Parenthesis +/// Encapsulate a drawable between two parenthesis. The left parenthesis is placed at the left of the first element of the body and by default the right parenthesis is placed at the right of the last element of the body. +/// +/// #example(``` +/// #skeletize( +/// config: ( +/// angle-increment: 30deg +/// ), { +/// parenthesis( +/// l:"[", r:"]", +/// br: $n$, { +/// single(angle: 1) +/// single(angle: -1) +/// single(angle: 1) +/// }) +/// }) +/// ```) +/// For more examples, see @examples +/// +/// - body (drawable): the body of the parenthesis. It must start and end with a fragment or a link. +/// - l (string): the left parenthesis +/// - r (string): the right parenthesis +/// - align (true): if true, the parenthesis will have the same y position. They will also get sized and aligned according to the body height. If false, they are not aligned and the height argument must be specified. +/// - resonance (boolean): if true, the parenthesis will be drawn in resonance mode. This means that the left and right parenthesis will be placed outside the molecule. Also, the parenthesis will be separated from the previous and next molecule. This can be true only if the parenthesis is the first element of the skeletal formula or if the previous element is an operator. See @resonance for more details. +/// - height (float, length): the height of the parenthesis. If align is true, this argument is optional. +/// - yoffset (float, length, list): the vertical offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - xoffset (float, length, list): the horizontal offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - right (string): Sometime, it is not convenient to place the right parenthesis at the end of the body. In this case, you can specify the name of the fragment or link where the right parenthesis should be placed. It is especially useful when the body end by a cycle. See @polySulfide +/// - tr (content): the exponent content of the right parenthesis +/// - br (content): the indice content of the right parenthesis +/// -> drawable +#let parenthesis(body, l: "(", r: ")", align: true, resonance: false, height: none, yoffset: none, xoffset: none, right: none, tr: none, br: none) = { + if l.len() > 2 { + panic("Left can be at most 2 characters") + } + let l = eval(l, mode: "math") + if r.len() > 2 { + panic("Right can be at most 2 characters") + } + let r = eval(r, mode: "math") + ( + ( + type: "parenthesis", + body: body, + calc: calc, + l: l, + r: r, + align: align, + tr: tr, + br: br, + height: height, + xoffset: xoffset, + yoffset: yoffset, + right: right, + resonance: resonance, + ), + ) +} + + +/// === Operator +/// Create an operator between two fragments. Creating an operator "reset" the placement of the next fragment. +/// This allow to add multiple molecules in the same skeletal formula. Without this, the next fragment would be placed at the end of the previous one. +/// An important point is that you can't use previous hooks to link two molecules separate by an operator. +/// This element is used in resonance structures (@resonance) and in some cases to put multiples molecules in the same skeletal formula (as you can set op to none). +/// +/// - op (content | string | none): The operator content. It can be a string or a content. A none value won't display anything. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// operator($->$, margin: 1em) +/// fragment("B") +/// }) +/// ```) +/// See @resonance for more examples. +/// - name (string): The name of the operator. +/// - margin (float, length): The margin between the operator and previous / next molecule. +/// -> drawable +#let operator(name: none, margin: 1em, op) = { + ( + ( + type: "operator", + name: name, + op: op, + margin: margin, + ), + ) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/src/cetz/process.typ b/packages/preview/alchemist/0.1.7/src/cetz/process.typ new file mode 100644 index 0000000000..decb454d2c --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/cetz/process.typ @@ -0,0 +1,91 @@ +// Temporary custom process.typ file to override default behavior + +#import "@preview/cetz:0.4.1": util, path-util, vector, drawable, process.aabb + + +/// Processes an element's function to get its drawables and bounds. Returns a {{dictionary}} with the key-values: `ctx` The modified context object, `bounds` The {{aabb}} of the element's drawables, `drawables` An {{array}} of the element's {{drawable}}s. +/// +/// - ctx (ctx): The current context object. +/// - element-func (function): A function that when passed {{ctx}}, it should return an element dictionary. +#let element(ctx, element-func) = { + let bounds = none + let element + let anchors = () + + (ctx, ..element,) = element-func(ctx) + if "drawables" in element { + if type(element.drawables) == dictionary { + element.drawables = (element.drawables,) + } + for drawable in element.drawables { + if drawable.bounds { + bounds = aabb.aabb( + if drawable.type == "path" { + path-util.bounds(drawable.segments) + } else if drawable.type == "content" { + let (x, y, _, w, h,) = drawable.pos + (drawable.width, drawable.height) + ((x + w / 2, y - h / 2, 0.0), (x - w / 2, y + h / 2, 0.0)) + }, + init: bounds + ) + } + } + } + + let name = element.at("name", default: none) + if name != none { + assert.eq(type(name), str, + message: "Element name must be a string") + assert(not name.contains("."), + message: "Invalid name for element '" + element.name + "'; name must not contain '.'") + + if "anchors" in element { + anchors.push(name) + ctx.nodes.insert(name, element) + if ctx.groups.len() > 0 { + ctx.groups.last().push(name) + } + } + } + + if ctx.debug and bounds != none { + element.drawables.push(drawable.path( + ((bounds.low, true, ( + ("l", (bounds.high.at(0), bounds.low.at(1), 0)), + ("l", bounds.high), + ("l", (bounds.low.at(0), bounds.high.at(1), 0)))),), + stroke: red + )) + } + + return ( + ctx: ctx, + bounds: bounds, + drawables: element.at("drawables", default: ()), + anchors: anchors + ) +} + +/// Runs the `element` function for a list of element functions and aggregates the results. +/// - ctx (ctx): The current context object. +/// - body (array): The array of element functions to process. +/// -> dictionary +#let many(ctx, body) = { + let drawables = () + let bounds = none + let anchors = () + + for el in body { + let r = element(ctx, el) + if r != none { + if r.bounds != none { + let pts = (r.bounds.low, r.bounds.high,) + bounds = aabb.aabb(pts, init: bounds) + } + ctx = r.ctx + drawables += r.drawables + anchors += r.anchors + } + } + return (ctx: ctx, bounds: bounds, drawables: drawables, anchors: anchors) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/src/default.typ b/packages/preview/alchemist/0.1.7/src/default.typ new file mode 100644 index 0000000000..a5d0222af3 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/default.typ @@ -0,0 +1,61 @@ +#let default = ( + atom-sep: 3em, + fragment-margin: 0.2em, + fragment-font: none, + fragment-color: none, + link-over-radius: .2, + angle-increment: 45deg, + base-angle: 0deg, + debug: false, + single: ( + stroke: black + ), + double: ( + gap: .25em, + offset: "center", + offset-coeff: 0.85, + stroke: black + ), + triple: ( + gap: .25em, + stroke: black + ), + filled-cram: ( + stroke: none, + fill: black, + base-length: .8em + ), + dashed-cram: ( + stroke: black + .05em, + dash-gap: .3em, + base-length: .8em, + tip-length: .1em + ), + lewis: ( + angle: 0deg, + radius: 0.2em, + ), + lewis-single: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + offset: "top" + ), + lewis-double: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + ), + lewis-line: ( + stroke: black, + length: .7em, + ), + lewis-rectangle: ( + stroke: .08em + black, + fill: white, + height: .7em, + width: .3em + ) +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/src/drawer.typ b/packages/preview/alchemist/0.1.7/src/drawer.typ new file mode 100644 index 0000000000..ef1c0774ad --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/drawer.typ @@ -0,0 +1,403 @@ +#import "default.typ": default +#import "@preview/cetz:0.4.1" +#import "cetz/process.typ" as custom-process +#import "utils/utils.typ": * +#import "utils/anchors.typ": * +#import "utils/utils.typ" as utils +#import "drawer/fragment.typ" as fragment +#import "drawer/link.typ" as link +#import "drawer/branch.typ" as branch +#import "drawer/cycle.typ" as cycle +#import "drawer/parenthesis.typ" as parenthesis +#import "drawer/hook.typ" as hook +#import "drawer/operator.typ" as operator + +#import cetz.draw: * + +#let default-anchor = (type: "coord", anchor: (0, 0)) + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + last-name: "", // last name used to draw + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + id: 0, // an id used to name things with an unique name + // branch + first-branch: false, // true if the next element is the first in a branch + // cycle + first-fragment: none, // name of the first fragment in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices +) + +#let draw-hooks-links(links, mol-name, ctx, from-mol) = { + let hook-id = 0 + for (to-name, (link,)) in links { + if link.at(mol-name, default: none) == none { + link.name = mol-name + "-hook-" + str(hook-id) + hook-id += 1 + } + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "fragment" { + ctx.links.push(( + type: "link", + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + from: none, + to: none, + over: link.at("over", default: none), + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + over: link.at("over", default: none), + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + } + ctx +} + +#let draw-fragments-and-link(ctx, body) = { + let fragment-drawing = () + let cetz-drawing = () + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + let drawing = () + let cetz-rec = () + if type(element) == function { + cetz-drawing.push(element) + } else if "type" not in element { + panic("Element " + repr(element) + " has no type") + } else if element.type == "fragment" { + (ctx, drawing) = fragment.draw-fragment(element, ctx) + } else if element.type == "link" { + (ctx, drawing) = link.draw-link(element, ctx) + } else if element.type == "branch" { + (ctx, drawing, cetz-rec) = branch.draw-branch(element, ctx, draw-fragments-and-link) + } else if element.type == "cycle" { + (ctx, drawing, cetz-rec) = cycle.draw-cycle(element, ctx, draw-fragments-and-link) + } else if element.type == "hook" { + ctx = hook.draw-hook(element, ctx) + } else if element.type == "parenthesis" { + (ctx, drawing, cetz-rec) = parenthesis.draw-parenthesis( + element, + ctx, + draw-fragments-and-link, + ) + } else if element.type == "operator" { + (ctx, drawing) = operator.draw-operator(element, fragment-drawing, ctx) + } else { + panic("Unknown element type " + element.type) + } + fragment-drawing += drawing + cetz-drawing += cetz-rec + } + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + ctx.last-anchor.drew = true + } + ( + ctx, + fragment-drawing, + cetz-drawing, + ) +} + +#let draw-link-over(ctx, link, over, angle) = { + let name = link.name + "-over" + let (over, length, radius) = if type(over) == str { + (over, link-over-radius, link-over-radius) + } else if type(over) == dictionary { + let name = over.at("name", default: none) + if name == none { + panic("Over argument must have a name") + } + ( + name, + over.at("length", default: link-over-radius), + over.at("radius", default: link-over-radius) + ) + } else { + panic("Over must be a string or a dictionary, got " + type(link.at("over"))) + } + intersections(name, over, link.name) + let color = if ctx.config.debug { + red + } else { + white + } + scope({ + rotate(angle) + rect( + anchor: "center", + (to: name + ".0", rel: (-length/2,-radius/2)), + (rel: (length, radius)), + fill: color, + stroke: color + ) + }) +} + +#let draw-link-decoration(ctx) = { + ( + ctx, + get-ctx(cetz-ctx => { + for link in ctx.links { + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + if link.at("over") != none { + if type(link.at("over")) == array { + for over in link.at("over") { + draw-link-over(ctx, link, over, angle) + } + } else { + draw-link-over(ctx, link, link.at("over"), angle) + } + } + + scope({ + set-origin(from) + rotate(angle) + (link.draw)(length, ctx, cetz-ctx, override: link.override) + }) + } + }), + ) +} + +/// set elements names and split the molecule into sub-groups +#let preprocessing(body, group-id: 0, link-id: 0, operator-id: 0) = { + let result = ((),) + for element in body { + if type(element) == dictionary { + if element.at("name", default: none) == none { + if element.type == "fragment" { + element.name = "fragment-" + str(group-id) + group-id += 1 + } else if element.type == "link" { + element.name = "link-" + str(link-id) + link-id += 1 + } else if element.type == "operator" { + element.name = "operator-" + str(operator-id) + operator-id += 1 + } + } + if element.at("body", default: none) != none { + let child-body + (child-body, group-id, link-id, operator-id) = preprocessing( + element.body, + group-id: group-id, + link-id: link-id, + operator-id: operator-id, + ) + if element.type == "parenthesis" and element.resonance { + element.body = child-body + } else { + element.body = child-body.at(0) + } + } + if element.type == "operator" or (element.type == "parenthesis" and element.resonance) { + result.push(element) + result.push(()) + } else { + result.at(-1).push(element) + } + } else { + result.at(-1).push(element) + } + } + (result, group-id, link-id, operator-id) +} + +#let operator-group-name = id => { + "operator-group-" + str(id) +} + +#let draw-groups(ctx, bodies, after-operator: false) = { + ctx.last-name = "" + + let drawables = for (i, body) in bodies.enumerate() { + if type(body) == array { + if body.len() > 0 { + ctx.last-name = operator-group-name(i) + get-ctx(cetz-ctx => { + let ctx = ctx + if ctx.last-anchor.type == "coord" { + (cetz-ctx, ctx.last-anchor.anchor) = cetz.coordinate.resolve(cetz-ctx, ctx.last-anchor.anchor) + } + let last-anchor = ctx.last-anchor + let (ctx, atoms, cetz-drawing) = draw-fragments-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + + let molecule = { + atoms + } + + let (ctx: cetz-ctx, drawables, bounds: molecule-bounds, anchors) = custom-process.many(cetz-ctx, molecule) + molecule-bounds = cetz.util.revert-transform(cetz-ctx.transform, molecule-bounds) + + let (translate-x, translate-y) = if after-operator { + let (_, origin-anchor) = cetz.coordinate.resolve(cetz-ctx, last-anchor.anchor) + ( + origin-anchor.at(0) - molecule-bounds.low.at(0), + -origin-anchor.at(1) + + ( + molecule-bounds.low.at(1) + molecule-bounds.high.at(1) + ) + / 2, + ) + } else { + (0, 0) + } + let transform-matrix = cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ) + // panic(anchors) + for name in anchors { + let hold-anchors = cetz-ctx.nodes.at(name).anchors + cetz-ctx.nodes.at(name).anchors = (name) => { + if name != () { + cetz.matrix.mul4x4-vec3(transform-matrix, hold-anchors(name)) + } else { + hold-anchors(name) + } + } + } + ( + _ => ( + ctx: cetz-ctx, + drawables: cetz.drawable.apply-transform( + cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ), + drawables, + ), + ), + ) + scope({ + translate(x: translate-x, y: -translate-y) + draw-link-decoration(ctx).at(1) + on-layer(2, cetz-drawing) + let bound-rect = cetz.draw.rect( + molecule-bounds.low, + molecule-bounds.high, + name: ctx.last-name, + stroke: purple, + ) + if ctx.config.debug { + bound-rect + } else { + cetz.draw.hide(bound-rect) + } + }) + }) + } + } else if body.type == "operator" { + let (op-ctx, drawable) = operator.draw-operator(body, ctx) + after-operator = true + ctx = op-ctx + drawable + } else if body.type == "parenthesis" { + let (parenthesis-ctx, drawable) = parenthesis.draw-resonance-parenthesis(body, draw-groups, ctx) + ctx = parenthesis-ctx + drawable + } else { + panic("Unexpected element type: " + body.type) + } + } + (drawables, ctx) +} + +#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { + let config = merge-dictionaries(config, default) + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + + let bodies = preprocessing(body).at(0) + let (final-drawing, ctx) = draw-groups(ctx, bodies) + + if name == none { + final-drawing + } else { + group(name: name, anchor: mol-anchor, { + anchor("default", (0, 0)) + final-drawing + }) + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas(debug: debug, background: background, draw-skeleton(config: config, body)) +} + +#let skeletize-config(default-config) = { + let config-function(debug: false, background: none, config: (:), body) = { + skeletize(debug: debug, background: background, config: merge-dictionaries(config, default-config), body) + } + config-function +} + +#let draw-skeleton-config(default-config) = { + let config-function(config: (:), name: none, mol-anchor: none, body) = { + draw-skeleton(config: merge-dictionaries(config, default-config), body) + } + config-function +} diff --git a/packages/preview/alchemist/0.1.7/src/drawer/branch.typ b/packages/preview/alchemist/0.1.7/src/drawer/branch.typ new file mode 100644 index 0000000000..2c9e062843 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/drawer/branch.typ @@ -0,0 +1,32 @@ +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +/// Compute the angle the branch should take in order to be "in the center" +/// of the cycle angle +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +#let draw-branch(branch, ctx, draw-fragments-and-link) = { + let angle = angles.angle-from-ctx(ctx, branch.args, cycle-angle(ctx)) + let (branch-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + branch.body, + ) + ctx = context_.update-parent-context(ctx, branch-ctx) + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.7/src/drawer/cram.typ b/packages/preview/alchemist/0.1.7/src/drawer/cram.typ new file mode 100644 index 0000000000..c66f6c127d --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/drawer/cram.typ @@ -0,0 +1,61 @@ +#import "@preview/cetz:0.4.1" +#import "../utils/utils.typ" + +/// Draw a triangle between two fragments +#let cram(from, to, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.filled-cram.base-length), + ) + cetz.draw.line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: ctx.config.filled-cram.stroke), + fill: args.at("fill", default: ctx.config.filled-cram.fill), + ) +} + +/// Draw a dashed triangle between two molecules fragments +#let dashed-cram(from, to, length, ctx, cetz-ctx, args) = { + import cetz.draw: * + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.dashed-cram.base-length), + ) + let tip-length = utils.convert-length( + cetz-ctx, + args.at("tip-length", default: ctx.config.dashed-cram.tip-length), + ) + hide({ + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - tip-length / 2)) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + tip-length / 2), + ) + }) + let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) + let dash-gap = utils.convert-length(cetz-ctx, args.at("dash-gap", default: ctx.config.dashed-cram.dash-gap)) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(cetz-ctx, dash-width) + let length = utils.convert-length(cetz-ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/src/drawer/cycle.typ b/packages/preview/alchemist/0.1.7/src/drawer/cycle.typ new file mode 100644 index 0000000000..9a81f12930 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/drawer/cycle.typ @@ -0,0 +1,155 @@ +#import "@preview/cetz:0.4.1" +#import "../utils/utils.typ" +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +#let max-int = 9223372036854775807 + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + 0, + )) + } + vertex +} + +/// extrapolate the center and the radius of the cycle +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + let debug = () + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + debug += cetz.draw.circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius, debug) +} + +#let draw-cycle-center-arc(ctx, name, center-arc) = { + import cetz.draw: * + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius, debug) = cycle-center-radius(ctx, cetz-ctx, vertex) + debug + if name != none { + anchor(name, center) + } + if center-arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= center-arc.at("radius", default: 0.7) + } else { + min-radius *= center-arc.at("radius", default: 0.5) + } + let start = center-arc.at("start", default: 0deg) + let end = center-arc.at("end", default: 360deg) + let delta = center-arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + arc( + center, + ..center-arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) +} + +#let draw-cycle(cycle, ctx, draw-fragments-and-link) = { + let cycle-step-angle = 360deg / cycle.faces + let angle = angles.angle-from-ctx(ctx, cycle.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ( + ctx.relative-angle == 0deg + and ctx.angle == 0deg + and not cycle.args.at( + "align", + default: false, + ) + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-fragment = none + if ctx.last-anchor.type == "fragment" { + first-fragment = ctx.last-anchor.name + if first-fragment not in ctx.hooks { + ctx.hooks.insert(first-fragment, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in cycle.args { + name = cycle.args.at("name") + record-vertex = true + } else if "arc" in cycle.args { + record-vertex = true + } + let (cycle-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: cycle.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-fragment: first-fragment, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + cycle.body, + ) + ctx = context_.update-parent-context(ctx, cycle-ctx) + if record-vertex { + drawing += draw-cycle-center-arc(cycle-ctx, name, cycle.args.at("arc", default: none)) + } + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.7/src/drawer/fragment.typ b/packages/preview/alchemist/0.1.7/src/drawer/fragment.typ new file mode 100644 index 0000000000..b1db6aa0c3 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/drawer/fragment.typ @@ -0,0 +1,149 @@ +#import "../utils/context.typ": * +#import "../utils/anchors.typ": * +#import "@preview/cetz:0.4.1" + +#let draw-fragment-text(ctx, mol, pos) = { + import cetz.draw: * + for (id, eq) in mol.atoms.enumerate() { + let name = str(id) + let color = if mol.colors != none { + if type(mol.colors) == color { + mol.colors + } else { + mol.colors.at(calc.min(id, mol.colors.len() - 1)) + } + } else { + fragment-color + } + + // draw atoms of the group one after the other from left to right + content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "mid-west" + }, + ( + if id == 0 { + pos + } else if mol.vertical { + (to: str(id - 1) + ".south", rel: (0, -.2em)) + } else { + str(id - 1) + ".mid-east" + } + ), + auto-scale: false, + { + show math.equation: math.upright + set text(fill: color) if color != none + set text(font: fragment-font) if fragment-font != none + eq + }, + ) + id += 1 + } +} + +#let draw-fragment-lewis(ctx, group-name, count, lewis) = { + if lewis.len() == 0 { + return () + } + import cetz.draw: * + get-ctx(cetz-ctx => { + for (id, (angle: lewis-angle, radius, draw)) in lewis.enumerate() { + if (lewis-angle == none) { + lewis-angle = ctx.config.lewis.angle + } + if (radius == none) { + radius = ctx.config.lewis.radius + } + let lewis-angle = angles.angle-correction(lewis-angle) + let mol-id = if angles.angle-in-range-inclusive(lewis-angle, 90deg, 270deg) { + 0 + } else { + count - 1 + } + let anchor = fragment-anchor( + ctx, + cetz-ctx, + lewis-angle, + group-name, + str(mol-id), + margin: radius, + ) + scope({ + set-origin(anchor) + rotate(lewis-angle) + draw(ctx, cetz-ctx) + }) + } + }) +} + +#let draw-fragment-elements(mol, ctx) = { + let name = mol.name + if name in ctx.hooks { + panic("Molecule fragment with name " + name + " already exists : " + ctx.hooks.keys().join(", ")) + } + ctx.hooks.insert(name, mol) + + let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("west", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-fragment-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let group-anchor = link-fragment-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (group-anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule fragment must be linked to a coord or a link") + } + ctx = context_.set-last-anchor( + ctx, + (type: "fragment", name: name, count: mol.at("count"), vertical: mol.vertical), + ) + if (side) { + ctx.id += 1 + } + ( + ctx, + { + import cetz.draw: * + group( + anchor: if side { + group-anchor + } else { + "from" + str(ctx.id) + }, + name: name, + { + anchor("default", coord) + draw-fragment-text(ctx, mol, coord) + if not side { + anchor("from" + str(ctx.id), group-anchor) + } + }, + ) + draw-fragment-lewis(ctx, name, mol.count, mol.at("lewis")) + }, + ) +} + +#let draw-fragment(element, ctx) = { + if ctx.first-branch { + panic("A molecule fragment can not be the first element in a cycle") + } + let (ctx, drawing) = draw-fragment-elements(element, ctx) + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + (ctx, drawing) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/src/drawer/hook.typ b/packages/preview/alchemist/0.1.7/src/drawer/hook.typ new file mode 100644 index 0000000000..1c5dafb9d1 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/drawer/hook.typ @@ -0,0 +1,14 @@ + +#let draw-hook(hook, ctx) = { + if hook.name in ctx.hooks { + panic("Hook " + hook.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + ctx +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/src/drawer/link.typ b/packages/preview/alchemist/0.1.7/src/drawer/link.typ new file mode 100644 index 0000000000..d6c0c0876a --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/drawer/link.typ @@ -0,0 +1,103 @@ +#import "../utils/angles.typ" +#import "../utils/anchors.typ": * + +#let create-link-decorations-anchors(link, ctx) = { + let link-angle = 0deg + let to-name = none + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-fragment != none { + to-name = ctx.first-fragment + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angles.angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = angles.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angles.angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let from-pos = if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "fragment" { + from-connection = link-fragment-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + fragment-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name") + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = context_.set-last-anchor( + ctx, + ( + type: "link", + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: to-name, + to: to-connection, + angle: link-angle, + over: link.at("over", default: none), + draw: link.draw, + ), + ) + ( + ctx, + { + hide({ + circle(name: link-name + "-start-anchor", from-pos, radius: .25em) + }) + let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + line(from-pos, end-anchor, stroke: blue + .1em) + } + hide( + { + circle(name: link-name + "-end-anchor", end-anchor, radius: .25em) + }, + bounds: true, + ) + }, + ) +} + +#let draw-link(link, ctx) = { + ctx.first-branch = false + let drawing + (ctx, drawing) = create-link-decorations-anchors(link, ctx) + ctx.faces-count += 1 + if link.links.len() != 0 { + ctx.hooks-links.push((link.links, ctx.last-anchor.name, false)) + } + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.7/src/drawer/operator.typ b/packages/preview/alchemist/0.1.7/src/drawer/operator.typ new file mode 100644 index 0000000000..ff1fd2792b --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/drawer/operator.typ @@ -0,0 +1,36 @@ +#import "@preview/cetz:0.4.1": draw, process, util + +#let draw-operator(operator, ctx) = { + import draw: * + + let op-name = operator.name + ctx.last-anchor = (type: "coord", anchor: (rel: (operator.margin, 0), to: (name: op-name, anchor: "east"))) + + ( + ctx, + get-ctx(cetz-ctx => { + + let west-previous-mol-anchor = (name: ctx.last-name, anchor: "east") + + let east-op-anchor = (rel: (operator.margin, 0), to: west-previous-mol-anchor) + + let op = if (operator.op == none) { + "" + } else { + operator.op + } + + content( + name: op-name, + anchor: "west", + east-op-anchor, + op, + ) + + if (ctx.config.debug) { + circle(west-previous-mol-anchor, radius: 0.05, fill: yellow, stroke: none) + circle(ctx.last-anchor.anchor, radius: 0.05, fill: yellow, stroke: none) + } + }), + ) +} diff --git a/packages/preview/alchemist/0.1.7/src/drawer/parenthesis.typ b/packages/preview/alchemist/0.1.7/src/drawer/parenthesis.typ new file mode 100644 index 0000000000..75d6b07a0a --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/drawer/parenthesis.typ @@ -0,0 +1,215 @@ +#import "../utils/utils.typ" +#import "@preview/cetz:0.4.1" + +#let left-parenthesis-anchor(parenthesis, ctx) = { + let anchor = if parenthesis.body.at(0).type == "fragment" { + let name = parenthesis.body.at(0).name + parenthesis.body.at(0).name = name + (name: name, anchor: "west") + } else if parenthesis.body.at(0).type == "link" { + let name = parenthesis.body.at(0).at("name") + parenthesis.body.at(0).name = name + (name + "-start-anchor", 45%, name + "-end-anchor") + } else if not left { } else { + panic("The first element of a parenthesis must be a molecule fragment or a link") + } + (ctx, parenthesis, anchor) +} + +#let right-parenthesis-anchor(parenthesis, ctx) = { + let right-name = parenthesis.at("right") + let right-type = "" + if right-name != none { + right-type = utils.get-element-type(parenthesis.body, right-name) + if right-type == none { + panic("The right element of the parenthesis does not exist") + } + } else { + right-type = parenthesis.body.at(-1).type + right-name = parenthesis.body.at(-1).at("name", default: none) + } + + let anchor = if right-type == "fragment" { + (name: right-name, anchor: "east") + } else if right-type == "link" { + (right-name + "-start-anchor", 55%, right-name + "-end-anchor") + } else { + panic("The last element of a parenthesis must be a molecule fragment or a link but got " + right-type) + } + (ctx, parenthesis, anchor) +} + +#let parenthesis-content(parenthesis, height, cetz-ctx) = { + let block = block(height: height * cetz-ctx.length * 1.2, width: 0pt) + let left-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($parenthesis.l block$, size: 100%) + } + let right-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($block parenthesis.r$, size: 100%) + } + + let right-parenthesis-with-attach = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.attach(right-parenthesis, br: parenthesis.br, tr: parenthesis.tr) + } + + (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) +} + +#let draw-parenthesis(parenthesis, ctx, draw-molecules-and-link) = { + let (parenthesis-ctx, drawing, cetz-rec) = draw-molecules-and-link( + ctx, + parenthesis.body, + ) + ctx = parenthesis-ctx + + + drawing += { + import cetz.draw: * + get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + { + drawing + }, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + + let sub-height = utils.bounding-box-height(sub-bounds) + let sub-v-mid = sub-bounds.low.at(1) - sub-height / 2 + + let sub-width = utils.bounding-box-width(sub-bounds) + + let (ctx, parenthesis, left-anchor) = left-parenthesis-anchor(parenthesis, ctx) + + let (ctx, parenthesis, right-anchor) = right-parenthesis-anchor(parenthesis, ctx) + + let height = parenthesis.at("height") + if height == none { + if not parenthesis.align { + panic("You must specify the height of the parenthesis if they are not aligned") + } + height = sub-height + } else { + height = utils.convert-length(cetz-ctx, height) + } + + let (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) = parenthesis-content( + parenthesis, + height, + cetz-ctx, + ) + + let (_, (lx, ly, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, left-anchor) + let (_, (rx, ry, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, right-anchor) + + if ctx.config.debug { + circle((lx, ly), radius: 1pt, fill: orange, stroke: orange) + circle((rx, ry), radius: 1pt, fill: orange, stroke: orange) + rect((lx, sub-bounds.low.at(1)), (rx, sub-bounds.high.at(1)), stroke: green) + line((lx, sub-v-mid), (rx, sub-v-mid), stroke: green) + } + + let hoffset = calc.abs(sub-width - calc.abs(rx - lx)) + + if parenthesis.align { + ly -= ly - sub-v-mid + ry = ly + } else if type(parenthesis.yoffset) == array { + if parenthesis.yoffset.len() != 2 { + panic("The parenthesis yoffset must be a list of length 2 or a number") + } + ly += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(0)) + ry += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(1)) + } else if parenthesis.yoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.yoffset) + ly += offset + ry += offset + } + + if type(parenthesis.xoffset) == array { + if parenthesis.xoffset.len() != 2 { + panic("The parenthesis xoffset must be a list of length 2 or a number") + } + lx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(0)) + rx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(1)) + } else if parenthesis.xoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.xoffset) + lx -= offset + rx += offset + } + + let right-bounds = cetz.process.many(cetz-ctx, content((0, 0), right-parenthesis, auto-scale: false)).bounds + let right-with-attach-bounds = cetz + .process + .many(cetz-ctx, content((0, 0), right-parenthesis-with-attach, auto-scale: false)) + .bounds + let right-voffset = calc.abs(right-bounds.low.at(1) - right-with-attach-bounds.low.at(1)) + if parenthesis.tr != none and parenthesis.br != none { + right-voffset /= 2 + } else if (parenthesis.tr != none) { + right-voffset *= -1 + } + + ( + _ => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + content((lx, ly), anchor: "mid-east", left-parenthesis, auto-scale: false) + content((rx, ry - right-voffset), anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }) + } + + (ctx, drawing, cetz-rec) +} + + +#let draw-resonance-parenthesis(parenthesis, draw-function, ctx) = { + import cetz.draw: * + let left-name = "parenthesis-" + str(ctx.id) + let right-name = "parenthesis-" + str(ctx.id + 1) + ctx.id += 2 + let last-anchor = ctx.last-anchor + let (drawing, ctx) = draw-function(ctx, parenthesis.body, after-operator: true) + + let drawing = get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + drawing, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + let height = parenthesis.height + if height == none { + height = utils.bounding-box-height(sub-bounds) + } else { + height = utils.convert-length(cetz-ctx, height) + } + let width = utils.bounding-box-width(sub-bounds) + + let (left-parenthesis, _, right-parenthesis-with-attach) = parenthesis-content(parenthesis, height, cetz-ctx) + + let right-anchor = (rel: (width, 0), to: (name: left-name, anchor: "east")) + on-layer( + 1, + { + content(last-anchor.anchor, name: left-name, anchor: "mid-east", left-parenthesis, auto-scale: false) + content(right-anchor, name: right-name, anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }, + ) + ( + ctx => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + }) + + ctx.last-anchor = (type: "coord", anchor: (name: right-name, anchor: "mid-east")) + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.7/src/elements/fragment.typ b/packages/preview/alchemist/0.1.7/src/elements/fragment.typ new file mode 100644 index 0000000000..e7b66eb50f --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/elements/fragment.typ @@ -0,0 +1,74 @@ +#let split-equation(mol, equation: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + for m in mol { + let last-number-hold = last-number + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + result.push(m) + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in fragment fragment") + } + last-number = true + result.push(m) + } else { + panic("Invalid fragment fragment content") + } + } else if m.func() == math.attach or m.func() == math.lr { + result.push(m) + } else if m == [ ] { + continue + } else { + panic("Invalid fragment fragment content") + } + if last-number-hold { + result.at(-2) = result.at(-2) + result.at(-1) + let _ = result.pop() + last-number = false + } + } + result +} + +#let fragment-cor-regex = "[0-9]*[A-Z][a-z]*'*" +#let exponent-regex = "((?:[0-9]+(?:\\+|\\-)?)|[A-Z]|\\+|\\-)" +#let exponent-base-regex = "(?:(\\^|_)" + exponent-regex + ")?(?:(\\^|_)" + exponent-regex + ")?" +#let fragment-regex = regex("^ *(" + fragment-cor-regex + ")" + exponent-base-regex) + +#let split-string(mol) = { + let aux(str) = { + let match = str.match(fragment-regex) + if match == none { + panic(str + " is not a valid fragment") + } else if match.captures.at(1) == match.captures.at(3) and match.captures.at(1) != none { + panic("You cannot use an exponent and a subscript twice") + } + let eq = "\"" + match.captures.at(0) + "\"" + if match.captures.at(1) != none { + eq += match.captures.at(1) + "(" + match.captures.at(2) + ")" + } + if match.captures.at(3) != none { + eq += match.captures.at(3) + "(" + match.captures.at(4) + ")" + } + let eq = math.equation(eval(eq, mode: "math")) + (eq, match.end) + } + + while not mol.len() == 0 { + let (eq, end) = aux(mol) + mol = mol.slice(end) + (eq,) + } +} diff --git a/packages/preview/alchemist/0.1.7/src/elements/lewis.typ b/packages/preview/alchemist/0.1.7/src/elements/lewis.typ new file mode 100644 index 0000000000..fb0d3547bc --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/elements/lewis.typ @@ -0,0 +1,133 @@ +#import "@preview/cetz:0.4.1" + +/// Create a lewis function that is then used to draw a lewis +/// formulae element around the fragment +/// +/// - draw-function (function): The function that will be used to draw the lewis element. It should takes three arguments: the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-lewis(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Lewis function takes no positional arguments") + } + let args = args.named() + let angle = args.at("angle", default: none) + let radius = args.at("radius", default: none) + ( + angle: angle, + radius: radius, + draw: (ctx, cetz-ctx) => draw-function(ctx, cetz-ctx, args) + ) + } +} + +/// draw a sigle electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// +/// The position of the electron is set by the `offset` argument. Available values are: +/// - "top": the electron is placed above the fragment center line +/// - "bottom": the electron is placed below the fragment center line +/// - "center": the electron is placed at the fragment center line +/// +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-single(offset: "top"), +/// )) +/// single(angle:-2) +/// fragment("B", lewis:( +/// lewis-single(offset: "bottom"), +/// )) +/// single(angle:-2) +/// fragment("C", lewis:( +/// lewis-single(offset: "center"), +/// )) +/// }) +/// ```) +#let lewis-single = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-single.radius) + let gap = args.at("gap", default: ctx.config.lewis-single.gap) + let offset = args.at("offset", default: ctx.config.lewis-single.offset) + let gap = if offset == "top" { + gap + } else if offset == "bottom" { + -gap + } else if offset != "center" { + panic("Invalid position, expected 'top', 'bottom' or 'center'") + } + let fill = args.at("fill", default: ctx.config.lewis-single.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-single.stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-double(), +/// lewis-double(angle: 90deg), +/// lewis-double(angle: 180deg), +/// lewis-double(angle: -90deg) +/// )) +/// }) +/// ```) +#let lewis-double = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-double.radius) + let gap = args.at("gap", default: ctx.config.lewis-double.gap) + let fill = args.at("fill", default: ctx.config.lewis-double.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-double.stroke) + circle((0, -gap), radius: radius, fill: fill, stroke: stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron liked by a single line +/// +/// It is possible to change the length of the line with the `lenght` argument. +/// It is also possible to change the `stroke` agument +/// #example(``` +/// #skeletize({ +/// fragment("B", lewis:( +/// lewis-line(angle: 45deg), +/// lewis-line(angle: 135deg), +/// lewis-line(angle: -45deg), +/// lewis-line(angle: -135deg) +/// )) +/// }) +/// ```) +#let lewis-line = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let length = args.at("length", default: ctx.config.lewis-line.length) + let stroke = args.at("stroke", default: ctx.config.lewis-line.stroke) + line((0, -length / 2), (0, length / 2), stroke: stroke) +}) + + +/// Draw a rectangle to denote a lone pair of electrons +/// +/// It is possible to change the height and width of the rectangle with the `height` and `width` arguments. +/// It is also possible to change the `fill` and `stroke` arguments +/// #example(``` +/// #skeletize({ +/// fragment("C", lewis:( +/// lewis-rectangle(), +/// lewis-rectangle(angle: 180deg) +/// )) +/// }) +/// ```) +#let lewis-rectangle = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let height = args.at("height", default: ctx.config.lewis-rectangle.height) + let width = args.at("width", default: ctx.config.lewis-rectangle.width) + let fill = args.at("fill", default: ctx.config.lewis-rectangle.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-rectangle.stroke) + rect((-width / 2, -height / 2), (width / 2, height / 2), fill: fill, stroke: stroke) +}) diff --git a/packages/preview/alchemist/0.1.7/src/elements/links.typ b/packages/preview/alchemist/0.1.7/src/elements/links.typ new file mode 100644 index 0000000000..a96321367a --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/elements/links.typ @@ -0,0 +1,314 @@ +#import "@preview/cetz:0.4.1" +#import "../drawer/cram.typ": * +#import "../utils/utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes four arguments: the length of the link, the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, cetz-ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, cetz-ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single(stroke: red + 5pt) +/// fragment("B") +/// }) +///```) +#let single = build-link((length, ctx, _, args) => { + import cetz.draw: * + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.single.stroke)) +}) + +/// Draw a double line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// fragment("B") +/// }) +///```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double(offset: "right") +/// fragment("B") +/// double(offset: "left") +/// fragment("C") +/// double(offset: "center") +/// fragment("D") +/// }) +///```) +#let double = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.double.gap)) / 2 + let offset = args.at("offset", default: ctx.config.double.offset) + let coeff = args.at("offset-coeff", default: ctx.config.double.offset-coeff) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + let gap-offset = if offset == "right" { + -gap + } else if offset == "left" { + gap + } else if offset == "center" { + 0 + } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, -gap + gap-offset), (x + length * coeff, -gap + gap-offset)) + } else { + ((0, -gap + gap-offset), (length, -gap + gap-offset)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, gap + gap-offset), (x + length * coeff, gap + gap-offset)) + } else { + ((0, gap + gap-offset), (length, gap + gap-offset)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) +}) + +/// Draw a triple line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// fragment("B") +/// }) +///```) +#let triple = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.triple.gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + line((0, -gap), (length, -gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + line((0, gap), (length, gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) +}) + +/// Draw a filled cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, cetz-ctx, args) => cram( + (0, 0), + (length, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a filled cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, cetz-ctx, args) => cram( + (length, 0), + (0, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a hollow cram between two fragments with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((0, 0), (length, 0), ctx, cetz-ctx, args) +}) + +/// Draw a hollow cram between two fragments with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((length, 0), (0, 0), ctx, cetz-ctx, args) +}) + +/// Draw a dashed cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the arrow base length with the `base-length` argument and the tip length with the `tip-length` argument. +// You can also change distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// tip-length: 1em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-right = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + cetz-ctx, + args, +)) + +/// Draw a dashed cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + cetz-ctx, + args, +)) + + +/// Draw a plus sign between two fragments +/// #example(```` +/// #skeletize({ +/// fragment("A") +/// plus() +/// fragment("B") +/// }) +/// ````) +/// You can change the filling, size and stroke of the glyph with the `fill`, `size` and `stoke` arguments. The default values are the parent `text` parameters. +#let plus = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + content( + anchor: "mid", + (length / 2, 0), + { + set text(fill: args.fill) if "fill" in args + set text(stroke: args.stroke) if "stroke" in args + set text(size: args.size) if "size" in args + text($+$) + } + ) +}) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/src/utils/anchors.typ b/packages/preview/alchemist/0.1.7/src/utils/anchors.typ new file mode 100644 index 0000000000..285efcde8c --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/utils/anchors.typ @@ -0,0 +1,296 @@ +#import "angles.typ" +#import "context.typ" as context_ +#import "utils.typ": * +#import "@preview/cetz:0.4.1" +#import cetz.draw: * + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +/// Calculate an anchor position around a fragment using an ellipse +/// at a given angle +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - cetz-ctx (cetz-ctx): the cetz context +/// - angle (float, int, angle): the angle of the anchor +/// - fragment (string): the fragment name +/// - id (string): the fragment subpart id +/// - margin (length, none): the margin around the fragment +/// -> anchor: the anchor position around the fragment +#let fragment-anchor(ctx, cetz-ctx, angle, fragment, id, margin: none) = { + let angle = angles.angle-correction(angle) + let fragment-margin = if margin == none { + ctx.config.fragment-margin + } else { + margin + } + fragment-margin = convert-length(cetz-ctx, fragment-margin) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "mid")), + ) + let (a, b) = if angles.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, fragment-margin, fragment, id) + } else { + anchor-south-east(cetz-ctx, center, fragment-margin, fragment, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if angles.angle-in-range-inclusive(angle, 0deg, 90deg) or angles.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +/// Return the index to choose if the link connection is not overridden +#let link-fragment-index(angle, end, count, vertical) = { + if not end { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let fragment-link-anchor(name, id, count) = { + if count <= id { + panic("The last fragment only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "mid")) +} + +#let link-fragment-anchor(name: none, id, count) = { + if id >= count { + panic("This fragment only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the fragment to link to must be defined") + } + if name == none { + (name: str(id), anchor: "mid") + } else { + (name: name, anchor: (str(id), "mid")) + } +} + + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "mid") + if link.to == none or link.from == none { + let angle = link.at( + "angle", + default: angles.angle-between(cetz-ctx, link.from-pos, to-pos), + ) + link.angle = angle + if link.from == none { + link.from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + if link.from == -1 { + link.from = 0 + } + if link.to == -1 { + link.to = ctx.hooks.at(link.to-name).count - 1 + } + let start = fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) + let end = fragment-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) + ((start, end), angles.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "mid"), + ), + ) + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "mid")), + ), + ) + } + let end-anchor = fragment-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + angles.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = angles.angle-correction(angles.angle-between(cetz-ctx, link.from-pos, hook.hook)) + let from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = fragment-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) + ( + ( + start-anchor, + hook.hook, + ), + angles.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + angles.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + diff --git a/packages/preview/alchemist/0.1.7/src/utils/angles.typ b/packages/preview/alchemist/0.1.7/src/utils/angles.typ new file mode 100644 index 0000000000..2b19a3d0b3 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/utils/angles.typ @@ -0,0 +1,98 @@ +#import "@preview/cetz:0.4.1" + +/// Convert any angle to an angle between 0deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a += 360 + } + + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +/// Check if the angle is in the range ]from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +/// Check if the angle is in the range [from, to] +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +/// +/// - ctx (cetz-ctx): The cetz context +/// - from (anchor): The first anchor +/// - to (anchor): The second anchor +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// Calculate the angle from an object with "relative", "absolute" or "angle" key +/// according to the alchemist context (see the manual to see how angles are calculated) +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - object (dict): the object to get the angle from +/// - default (angle): the default angle +/// -> the calculated angle +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +/// Overwrite the offset based on the angle +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} diff --git a/packages/preview/alchemist/0.1.7/src/utils/context.typ b/packages/preview/alchemist/0.1.7/src/utils/context.typ new file mode 100644 index 0000000000..4021bb5177 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/utils/context.typ @@ -0,0 +1,33 @@ +#let update-parent-context(parent-ctx, ctx) = { + let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { + ( + ..parent-ctx.last-anchor, + drew: true, + ) + } else { + parent-ctx.last-anchor + } + ( + ..parent-ctx, + last-anchor: last-anchor, + hooks: ctx.hooks, + hooks-links: ctx.hooks-links, + links: ctx.links, + ) +} + +/// Set the last anchor in the context to the given anchor and save it if needed +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" { + let drew = ctx.last-anchor.at("drew", default: false) + if drew and anchor.type == "link" and anchor.name == ctx.last-anchor.name { + drew = false + let _ = ctx.links.pop() + } + if not drew { + ctx.links.push(ctx.last-anchor) + } + ctx.last-anchor.drew = true + } + (..ctx, last-anchor: anchor) +} diff --git a/packages/preview/alchemist/0.1.7/src/utils/utils.typ b/packages/preview/alchemist/0.1.7/src/utils/utils.typ new file mode 100644 index 0000000000..bac944c011 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/src/utils/utils.typ @@ -0,0 +1,75 @@ +#import "@preview/cetz:0.4.1" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + return if type(num) == length { + float(num.to-absolute() / ctx.length) + } else if type(num) == ratio { + num + } else { + float(num) + } +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} + +/// merge two imbricated dictionaries together +/// The second dictionary is the default value if the key is not present in the first dictionary +#let merge-dictionaries(dict1, default) = { + let result = default + for (key, value) in dict1 { + if type(value) == dictionary { + result.insert(key, merge-dictionaries(value, default.at(key))) + } else { + result.insert(key, value) + } + } + result +} + + +/// get the type of an element by its name +/// +/// - body (drawable): the chemfig body of a molecule +/// - name (string): the name of the element to get the type +/// -> string +#let get-element-type(body, name) = { + for element in body { + if type(element) != dictionary { + continue + } + if "name" in element and element.name == name { + return element.type + } + if element.type == "branch" or element.type == "cycle" or element.type == "parenthesis" { + let type = get-element-type(element.body, name) + if type != none { + return type + } + } + } + none +} + +/// Calculate the height of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-height(bounds) = { + calc.abs(bounds.high.at(1) - bounds.low.at(1)) +} + +/// Calculate the width of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-width(bounds) = { + calc.abs(bounds.high.at(0) - bounds.low.at(0)) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/typst.toml b/packages/preview/alchemist/0.1.7/typst.toml new file mode 100644 index 0000000000..e272238a75 --- /dev/null +++ b/packages/preview/alchemist/0.1.7/typst.toml @@ -0,0 +1,13 @@ +[package] +name = "alchemist" +version = "0.1.7" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.13.1" +repository = "https://github.com/Typsium/alchemist" +description = "A package to render skeletal formulas using CeTZ" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization", "paper"] +disciplines = ["education", "chemistry", "biology"] +keywords = ["chemistry", "chemical", "formula", "molecule", "chemiviz", "structure", "organic", "lewis", "bond", "aromatic"] diff --git a/packages/preview/codly-languages/0.1.6/README.md b/packages/preview/codly-languages/0.1.6/README.md index d1455a7869..7e062ed0db 100644 --- a/packages/preview/codly-languages/0.1.6/README.md +++ b/packages/preview/codly-languages/0.1.6/README.md @@ -10,7 +10,7 @@ Pretty simple. Import `codly`. Initialize it. Import `codly-languages`. Configure `codly` with the languages. Like this: ```typst -#import "@preview/codly:1.1.1": * +#import "@preview/codly:1.2.0": * #show: codly-init #import "@preview/codly-languages:0.1.6": * diff --git a/packages/preview/codly-languages/0.1.6/lib.typ b/packages/preview/codly-languages/0.1.6/lib.typ index 26711b435b..35a1994396 100644 --- a/packages/preview/codly-languages/0.1.6/lib.typ +++ b/packages/preview/codly-languages/0.1.6/lib.typ @@ -4,7 +4,7 @@ // To use: // // ```typst -// #import "@preview/codly:1.1.1": * +// #import "@preview/codly:1.2.0": * // #show: codly-init // // #import "codly-languages.typ": * diff --git a/packages/preview/codly-languages/0.1.6/tests/basic-output/test.typ b/packages/preview/codly-languages/0.1.6/tests/basic-output/test.typ index a1c330c6d7..13903f51ab 100644 --- a/packages/preview/codly-languages/0.1.6/tests/basic-output/test.typ +++ b/packages/preview/codly-languages/0.1.6/tests/basic-output/test.typ @@ -1,5 +1,5 @@ #import "/lib.typ": * -#import "@preview/codly:1.1.1": * +#import "@preview/codly:1.2.0": * #show: codly-init #codly(languages: codly-languages) diff --git a/packages/preview/numty/0.0.5/typst.toml b/packages/preview/numty/0.0.5/typst.toml index 42c1cad063..57dafcaebc 100644 --- a/packages/preview/numty/0.0.5/typst.toml +++ b/packages/preview/numty/0.0.5/typst.toml @@ -8,3 +8,4 @@ description = "Numeric Typst" exclude = ["test.typ"] repository = "https://github.com/PabloRuizCuevas/numty" categories = ["utility","scripting"] +keywords = ["matrix", "array", "matrices", "math", "numpy", "vector"] diff --git a/packages/preview/prooftrees/0.1.0/README.md b/packages/preview/prooftrees/0.1.0/README.md index 16896dfb1b..19e70a5524 100644 --- a/packages/preview/prooftrees/0.1.0/README.md +++ b/packages/preview/prooftrees/0.1.0/README.md @@ -1,3 +1,13 @@ +# Prooftrees [DEPRECATED] + +Deprecated in favour of [curryst](https://github.com/pauladam94/curryst). +This package is no longer maintained and lacks many of the features of `curryst`. + + +# Old Description + +--- + # Prooftrees This package is for constructing proof trees in the style of natural deduction or the sequent calculus, for `typst` `0.7.0`. Please do open issues for bugs etc :) diff --git a/packages/preview/prooftrees/0.1.0/typst.toml b/packages/preview/prooftrees/0.1.0/typst.toml index be329cdd1a..b9c4e8a712 100644 --- a/packages/preview/prooftrees/0.1.0/typst.toml +++ b/packages/preview/prooftrees/0.1.0/typst.toml @@ -4,6 +4,6 @@ version = "0.1.0" entrypoint = "src/prooftrees.typ" authors = ["david-davies "] license = "MIT" -description = "Proof trees for natural deduction and type theories" +description = "[Deprecated in favour of `curryst`; this package is no longer maintained.] Proof trees for natural deduction and type theories" repository = "https://github.com/david-davies/typst-prooftree" exclude = ["examples/"] diff --git a/packages/preview/touying-flow/1.1.0/content/example/main.pdf b/packages/preview/touying-flow/1.1.0/content/example/main.pdf deleted file mode 100644 index 38d0ca17bbfd2a1d71a1ee309450ff3ba0689466..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 208514 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
V5vk$d0+quluGV8P*b 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 The easiest method is to import `wrap-it: wrap-content` from the `@preview` package: -``` typ -#import "@preview/wrap-it:0.1.0": wrap-content -``` +`#import "@preview/wrap-it:0.1.1": wrap-content` # Sample use: From c5086be4bfc49db80cfbd8fc1ee8039c7804cc24 Mon Sep 17 00:00:00 2001 From: Tom <45141234+Robotechnic@users.noreply.github.com> Date: Sat, 9 Aug 2025 19:25:16 +0200 Subject: [PATCH 13/24] Delete packages/preview/alchemist/0.1.7/0.1.7 directory --- .../preview/alchemist/0.1.7/0.1.7/LICENSE | 22 - .../preview/alchemist/0.1.7/0.1.7/README.md | 134 ------ .../preview/alchemist/0.1.7/0.1.7/lib.typ | 255 ----------- .../0.1.7/0.1.7/src/cetz/process.typ | 91 ---- .../alchemist/0.1.7/0.1.7/src/default.typ | 61 --- .../alchemist/0.1.7/0.1.7/src/drawer.typ | 403 ------------------ .../0.1.7/0.1.7/src/drawer/branch.typ | 32 -- .../alchemist/0.1.7/0.1.7/src/drawer/cram.typ | 61 --- .../0.1.7/0.1.7/src/drawer/cycle.typ | 155 ------- .../0.1.7/0.1.7/src/drawer/fragment.typ | 149 ------- .../alchemist/0.1.7/0.1.7/src/drawer/hook.typ | 14 - .../alchemist/0.1.7/0.1.7/src/drawer/link.typ | 103 ----- .../0.1.7/0.1.7/src/drawer/operator.typ | 36 -- .../0.1.7/0.1.7/src/drawer/parenthesis.typ | 215 ---------- .../0.1.7/0.1.7/src/elements/fragment.typ | 74 ---- .../0.1.7/0.1.7/src/elements/lewis.typ | 133 ------ .../0.1.7/0.1.7/src/elements/links.typ | 314 -------------- .../0.1.7/0.1.7/src/utils/anchors.typ | 296 ------------- .../0.1.7/0.1.7/src/utils/angles.typ | 98 ----- .../0.1.7/0.1.7/src/utils/context.typ | 33 -- .../alchemist/0.1.7/0.1.7/src/utils/utils.typ | 75 ---- .../preview/alchemist/0.1.7/0.1.7/typst.toml | 13 - 22 files changed, 2767 deletions(-) delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/LICENSE delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/README.md delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/lib.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/cetz/process.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/default.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/branch.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cram.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cycle.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/fragment.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/hook.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/link.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/operator.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/drawer/parenthesis.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/elements/fragment.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/elements/lewis.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/elements/links.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/utils/anchors.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/utils/angles.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/utils/context.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/src/utils/utils.typ delete mode 100644 packages/preview/alchemist/0.1.7/0.1.7/typst.toml diff --git a/packages/preview/alchemist/0.1.7/0.1.7/LICENSE b/packages/preview/alchemist/0.1.7/0.1.7/LICENSE deleted file mode 100644 index a85204e590..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright (c) 2025 Robotechnic - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.7/0.1.7/README.md b/packages/preview/alchemist/0.1.7/0.1.7/README.md deleted file mode 100644 index c968af64b7..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/README.md +++ /dev/null @@ -1,134 +0,0 @@ -[![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FTypsium%2Falchemist%2Fmaster%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://github.com/Typsium/alchemist) -[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/alchemist/blob/master/LICENSE) -[![User Manual](https://img.shields.io/badge/manual-.pdf-purple)](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf) - -# alchemist - -Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. - - -````typ -#skeletize({ - fragment(name: "A", "A") - single() - fragment("B") - branch({ - single(angle: 1) - fragment( - "W", - links: ( - "A": double(stroke: red), - ), - ) - single() - fragment(name: "X", "X") - }) - branch({ - single(angle: -1) - fragment("Y") - single() - fragment( - name: "Z", - "Z", - links: ( - "X": single(stroke: black + 3pt), - ), - ) - }) - single() - fragment( - "C", - links: ( - "X": cram-filled-left(fill: blue), - "Z": single(), - ), - ) -}) -```` -![links](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic1/ref/1.png) - -Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: - - -````typ -#skeletize({ - import cetz.draw: * - double(absolute: 30deg, name: "l1") - single(absolute: -30deg, name: "l2") - fragment("X", name: "X") - hobby( - "l1.50%", - ("l1.start", 0.5, 90deg, "l1.end"), - "l1.start", - stroke: (paint: red, dash: "dashed"), - mark: (end: ">"), - ) - hobby( - (to: "X.north", rel: (0, 1pt)), - ("l2.end", 0.4, -90deg, "l2.start"), - "l2.50%", - mark: (end: ">"), - ) -}) -```` -![cetz](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic2/ref/1.png) - -## Usage - -To start using alchemist, just use the following code: - -```typ -#import "@preview/alchemist:0.1.4": * - -#skeletize({ - // Your molecule here -}) -``` - -For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). - -## Changelog - -### 0.1.7 - -- Updated cetz to version 0.4.1 -- Added default values for `color` and `font` for `fragment` elements -- Added a `skeletize-config` function to create a `skeletize` function with a specific configuration -- Fixed a bug with cetz anchors not being correctly translated -- Added an `over` argument to links to allow hide overlapped links - -### 0.1.6 - -- Fixed the parenthesis height to work with the new typst version -- Renamed `molecule` into `fragment` -- Added support for charges and apostrophes in the string of a molecule -- Fixed the parenthesis auto positioning and alignment -- Added a new operator element and a new parameter in parenthesis to write resonance formulae - -### 0.1.5 - -- Update to compiler 0.13.1 and Cetz 0.3.4 - -### 0.1.4 - -- Added the possibility to create Lewis formulae -- Added parenthesis element to create groups and polymer - -### 0.1.3 - -- Added the possibility to add exponent in the string of a molecule. - -### 0.1.2 - -- Added default values for link style properties. -- Updated `cetz` to version 0.3.1. -- Added a `tip-length` argument to dashed cram links. - -### 0.1.1 - -- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. -- Fixed multiples bugs that causes overdraws of links. - -### 0.1.0 - -- Initial release diff --git a/packages/preview/alchemist/0.1.7/0.1.7/lib.typ b/packages/preview/alchemist/0.1.7/0.1.7/lib.typ deleted file mode 100644 index 7d1f5c1a64..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/lib.typ +++ /dev/null @@ -1,255 +0,0 @@ -#import "@preview/cetz:0.4.1" -#import "src/default.typ": default -#import "src/utils/utils.typ" -#import "src/drawer.typ" -#import "src/drawer.typ": skeletize, draw-skeleton, skeletize-config, draw-skeleton-config -#import "src/elements/links.typ": * -#import "src/elements/fragment.typ": * -#import "src/elements/lewis.typ": * - -#let transparent = color.rgb(100%, 0, 0, 0) - -/// === Fragment function -/// Build a fragment group based on mol -/// Each fragment is represented as an optional count followed by a fragment name -/// starting by a capital letter followed by an optional exponent followed by an optional indice. -/// #example(``` -/// #skeletize({ -/// fragment("H_2O") -/// }) -///```) -/// #example(``` -/// #skeletize({ -/// fragment("H^A_EF^5_4") -/// }) -/// ```) -/// It is possible to use an equation as a fragment. In this case, the splitting of the equation uses the same rules as in the string case. However, you get some advantages over the string version: -/// - You can use parenthesis to group elements together. -/// - You have no restriction about what you can put in exponent or indice. -/// #example(``` -/// #skeletize({ -/// fragment($C(C H_3)_3$) -/// }) -///```) -/// - name (content): The name of the fragment. It is used as the cetz name of the fragment and to link other fragments to it. -/// - links (dictionary): The links between this fragment and previous fragments or hooks. The key is the name of the fragment or hook and the value is the link function. See @links. -/// -/// Note that the atom-sep and angle arguments are ignored -/// - lewis (list): The list of lewis structures to draw around the fragments. See @lewis -/// - mol (string, equation): The string representing the fragment or an equation of the fragment -/// - vertical (boolean): If true, the fragment is drawn vertically -/// #example(``` -/// #skeletize({ -/// fragment("ABCD", vertical: true) -/// }) -///```) -/// - colors (color|list): The color of the fragment. If a list is provided, it colors each group of the fragment with the corresponding color from right to left. If the number of colors is less than the number of groups, the last color is used for the remaining groups. If the number of colors is greater than the number of groups, the extra colors are ignored. -/// #example(``` -/// #skeletize({ -/// fragment("ABCD", colors: (red, green, blue)) -/// single() -/// fragment("EFGH", colors: (orange)) -/// }) -/// ```) -/// -> drawable -#let fragment(name: none, links: (:), lewis: (), vertical: false, colors: none, mol) = { - let atoms = if type(mol) == str { - split-string(mol) - } else if mol.func() == math.equation { - split-equation(mol, equation: true) - } else { - panic("Invalid fragment content") - } - - if type(lewis) != array { - panic("Lewis formulae elements must be in a list") - } - - ( - ( - type: "fragment", - name: name, - atoms: atoms, - colors: colors, - links: links, - lewis: lewis, - vertical: vertical, - count: atoms.len(), - ), - ) -} -#let molecule(name: none, links: (:), lewis: (), vertical: false, mol) = fragment(name: name, links: links, lewis: lewis, vertical: vertical, mol) - -/// === Hooks -/// Create a hook in the fragment. It allows to connect links to the place where the hook is. -/// Hooks are placed at the end of links or at the beginning of the fragment. -/// - name (string): The name of the hook -/// -> drawable -#let hook(name) = { - ( - ( - type: "hook", - name: name, - ), - ) -} - -/// === Branch and cycles -/// Create a branch from the current fragment, the first element -/// of the branch has to be a link. -/// -/// You can specify an angle argument like for links. This angle will be then -/// used as the `base-angle` for the branch. -/// -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// branch({ -/// single(angle:1) -/// fragment("B") -/// }) -/// branch({ -/// double(angle: -1) -/// fragment("D") -/// }) -/// single() -/// double() -/// single() -/// fragment("C") -/// }) -///```) -/// - body (drawable): the body of the branch. It must start with a link. -/// -> drawable -#let branch(body, ..args) = { - if args.pos().len() != 0 { - panic("Branch takes one positional argument: the body of the branch") - } - ((type: "branch", body: body, args: args.named()),) -} - -/// Create a regular cycle of fragments -/// You can specify an angle argument like for links. This angle will be then -/// the angle of the first link of the cycle. -/// -/// The argument `align` can be used to force align the cycle according to the -/// relative angle of the previous link. -/// -/// #example(``` -/// #skeletize({ -/// cycle(5, { -/// single() -/// double() -/// single() -/// double() -/// single() -/// }) -/// }) -///```) -/// - faces (int): the number of faces of the cycle -/// - body (drawable): the body of the cycle. It must start and end with a fragment or a link. -/// -> drawable -#let cycle(faces, body, ..args) = { - if args.pos().len() != 0 { - panic("Cycle takes two positional arguments: number of faces and body") - } - if faces < 3 { - panic("A cycle must have at least 3 faces") - } - ( - ( - type: "cycle", - faces: faces, - body: body, - args: args.named(), - ), - ) -} - -/// === Parenthesis -/// Encapsulate a drawable between two parenthesis. The left parenthesis is placed at the left of the first element of the body and by default the right parenthesis is placed at the right of the last element of the body. -/// -/// #example(``` -/// #skeletize( -/// config: ( -/// angle-increment: 30deg -/// ), { -/// parenthesis( -/// l:"[", r:"]", -/// br: $n$, { -/// single(angle: 1) -/// single(angle: -1) -/// single(angle: 1) -/// }) -/// }) -/// ```) -/// For more examples, see @examples -/// -/// - body (drawable): the body of the parenthesis. It must start and end with a fragment or a link. -/// - l (string): the left parenthesis -/// - r (string): the right parenthesis -/// - align (true): if true, the parenthesis will have the same y position. They will also get sized and aligned according to the body height. If false, they are not aligned and the height argument must be specified. -/// - resonance (boolean): if true, the parenthesis will be drawn in resonance mode. This means that the left and right parenthesis will be placed outside the molecule. Also, the parenthesis will be separated from the previous and next molecule. This can be true only if the parenthesis is the first element of the skeletal formula or if the previous element is an operator. See @resonance for more details. -/// - height (float, length): the height of the parenthesis. If align is true, this argument is optional. -/// - yoffset (float, length, list): the vertical offset of parenthesis. You can also provide a tuple for left and right parenthesis -/// - xoffset (float, length, list): the horizontal offset of parenthesis. You can also provide a tuple for left and right parenthesis -/// - right (string): Sometime, it is not convenient to place the right parenthesis at the end of the body. In this case, you can specify the name of the fragment or link where the right parenthesis should be placed. It is especially useful when the body end by a cycle. See @polySulfide -/// - tr (content): the exponent content of the right parenthesis -/// - br (content): the indice content of the right parenthesis -/// -> drawable -#let parenthesis(body, l: "(", r: ")", align: true, resonance: false, height: none, yoffset: none, xoffset: none, right: none, tr: none, br: none) = { - if l.len() > 2 { - panic("Left can be at most 2 characters") - } - let l = eval(l, mode: "math") - if r.len() > 2 { - panic("Right can be at most 2 characters") - } - let r = eval(r, mode: "math") - ( - ( - type: "parenthesis", - body: body, - calc: calc, - l: l, - r: r, - align: align, - tr: tr, - br: br, - height: height, - xoffset: xoffset, - yoffset: yoffset, - right: right, - resonance: resonance, - ), - ) -} - - -/// === Operator -/// Create an operator between two fragments. Creating an operator "reset" the placement of the next fragment. -/// This allow to add multiple molecules in the same skeletal formula. Without this, the next fragment would be placed at the end of the previous one. -/// An important point is that you can't use previous hooks to link two molecules separate by an operator. -/// This element is used in resonance structures (@resonance) and in some cases to put multiples molecules in the same skeletal formula (as you can set op to none). -/// -/// - op (content | string | none): The operator content. It can be a string or a content. A none value won't display anything. -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// operator($->$, margin: 1em) -/// fragment("B") -/// }) -/// ```) -/// See @resonance for more examples. -/// - name (string): The name of the operator. -/// - margin (float, length): The margin between the operator and previous / next molecule. -/// -> drawable -#let operator(name: none, margin: 1em, op) = { - ( - ( - type: "operator", - name: name, - op: op, - margin: margin, - ), - ) -} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/cetz/process.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/cetz/process.typ deleted file mode 100644 index decb454d2c..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/cetz/process.typ +++ /dev/null @@ -1,91 +0,0 @@ -// Temporary custom process.typ file to override default behavior - -#import "@preview/cetz:0.4.1": util, path-util, vector, drawable, process.aabb - - -/// Processes an element's function to get its drawables and bounds. Returns a {{dictionary}} with the key-values: `ctx` The modified context object, `bounds` The {{aabb}} of the element's drawables, `drawables` An {{array}} of the element's {{drawable}}s. -/// -/// - ctx (ctx): The current context object. -/// - element-func (function): A function that when passed {{ctx}}, it should return an element dictionary. -#let element(ctx, element-func) = { - let bounds = none - let element - let anchors = () - - (ctx, ..element,) = element-func(ctx) - if "drawables" in element { - if type(element.drawables) == dictionary { - element.drawables = (element.drawables,) - } - for drawable in element.drawables { - if drawable.bounds { - bounds = aabb.aabb( - if drawable.type == "path" { - path-util.bounds(drawable.segments) - } else if drawable.type == "content" { - let (x, y, _, w, h,) = drawable.pos + (drawable.width, drawable.height) - ((x + w / 2, y - h / 2, 0.0), (x - w / 2, y + h / 2, 0.0)) - }, - init: bounds - ) - } - } - } - - let name = element.at("name", default: none) - if name != none { - assert.eq(type(name), str, - message: "Element name must be a string") - assert(not name.contains("."), - message: "Invalid name for element '" + element.name + "'; name must not contain '.'") - - if "anchors" in element { - anchors.push(name) - ctx.nodes.insert(name, element) - if ctx.groups.len() > 0 { - ctx.groups.last().push(name) - } - } - } - - if ctx.debug and bounds != none { - element.drawables.push(drawable.path( - ((bounds.low, true, ( - ("l", (bounds.high.at(0), bounds.low.at(1), 0)), - ("l", bounds.high), - ("l", (bounds.low.at(0), bounds.high.at(1), 0)))),), - stroke: red - )) - } - - return ( - ctx: ctx, - bounds: bounds, - drawables: element.at("drawables", default: ()), - anchors: anchors - ) -} - -/// Runs the `element` function for a list of element functions and aggregates the results. -/// - ctx (ctx): The current context object. -/// - body (array): The array of element functions to process. -/// -> dictionary -#let many(ctx, body) = { - let drawables = () - let bounds = none - let anchors = () - - for el in body { - let r = element(ctx, el) - if r != none { - if r.bounds != none { - let pts = (r.bounds.low, r.bounds.high,) - bounds = aabb.aabb(pts, init: bounds) - } - ctx = r.ctx - drawables += r.drawables - anchors += r.anchors - } - } - return (ctx: ctx, bounds: bounds, drawables: drawables, anchors: anchors) -} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/default.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/default.typ deleted file mode 100644 index a5d0222af3..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/default.typ +++ /dev/null @@ -1,61 +0,0 @@ -#let default = ( - atom-sep: 3em, - fragment-margin: 0.2em, - fragment-font: none, - fragment-color: none, - link-over-radius: .2, - angle-increment: 45deg, - base-angle: 0deg, - debug: false, - single: ( - stroke: black - ), - double: ( - gap: .25em, - offset: "center", - offset-coeff: 0.85, - stroke: black - ), - triple: ( - gap: .25em, - stroke: black - ), - filled-cram: ( - stroke: none, - fill: black, - base-length: .8em - ), - dashed-cram: ( - stroke: black + .05em, - dash-gap: .3em, - base-length: .8em, - tip-length: .1em - ), - lewis: ( - angle: 0deg, - radius: 0.2em, - ), - lewis-single: ( - stroke: black, - fill: black, - radius: .1em, - gap: .25em, - offset: "top" - ), - lewis-double: ( - stroke: black, - fill: black, - radius: .1em, - gap: .25em, - ), - lewis-line: ( - stroke: black, - length: .7em, - ), - lewis-rectangle: ( - stroke: .08em + black, - fill: white, - height: .7em, - width: .3em - ) -) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer.typ deleted file mode 100644 index ef1c0774ad..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer.typ +++ /dev/null @@ -1,403 +0,0 @@ -#import "default.typ": default -#import "@preview/cetz:0.4.1" -#import "cetz/process.typ" as custom-process -#import "utils/utils.typ": * -#import "utils/anchors.typ": * -#import "utils/utils.typ" as utils -#import "drawer/fragment.typ" as fragment -#import "drawer/link.typ" as link -#import "drawer/branch.typ" as branch -#import "drawer/cycle.typ" as cycle -#import "drawer/parenthesis.typ" as parenthesis -#import "drawer/hook.typ" as hook -#import "drawer/operator.typ" as operator - -#import cetz.draw: * - -#let default-anchor = (type: "coord", anchor: (0, 0)) - -#let default-ctx = ( - // general - last-anchor: default-anchor, // keep trace of the place to draw - last-name: "", // last name used to draw - links: (), // list of links to draw - hooks: (:), // list of hooks - hooks-links: (), // list of links to hooks - relative-angle: 0deg, // current global relative angle - angle: 0deg, // current global angle - id: 0, // an id used to name things with an unique name - // branch - first-branch: false, // true if the next element is the first in a branch - // cycle - first-fragment: none, // name of the first fragment in the cycle - in-cycle: false, // true if we are in a cycle - cycle-faces: 0, // number of faces in the current cycle - faces-count: 0, // number of faces already drawn - cycle-step-angle: 0deg, // angle between two faces in the cycle - record-vertex: false, // true if the cycle should keep track of its vertices - vertex-anchors: (), // list of the cycle vertices -) - -#let draw-hooks-links(links, mol-name, ctx, from-mol) = { - let hook-id = 0 - for (to-name, (link,)) in links { - if link.at(mol-name, default: none) == none { - link.name = mol-name + "-hook-" + str(hook-id) - hook-id += 1 - } - if to-name not in ctx.hooks { - panic("Molecule " + to-name + " does not exist") - } - let to-hook = ctx.hooks.at(to-name) - if to-hook.type == "fragment" { - ctx.links.push(( - type: "link", - name: link.at("name"), - from-pos: if from-mol { - (name: mol-name, anchor: "mid") - } else { - mol-name + "-end-anchor" - }, - from-name: if from-mol { - mol-name - }, - to-name: to-name, - from: none, - to: none, - over: link.at("over", default: none), - override: angles.angle-override(ctx.angle, ctx), - draw: link.draw, - )) - } else if to-hook.type == "hook" { - ctx.links.push(( - type: if from-mol { - "mol-hook-link" - } else { - "link-hook-link" - }, - name: link.at("name"), - from-pos: if from-mol { - (name: mol-name, anchor: "mid") - } else { - mol-name + "-end-anchor" - }, - from-name: if from-mol { - mol-name - }, - to-name: to-name, - to-hook: to-hook.hook, - override: angles.angle-override(ctx.angle, ctx), - draw: link.draw, - over: link.at("over", default: none), - )) - } else { - panic("Unknown hook type " + ctx.hook.at(to-name).type) - } - } - ctx -} - -#let draw-fragments-and-link(ctx, body) = { - let fragment-drawing = () - let cetz-drawing = () - for element in body { - if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { - continue - } - let drawing = () - let cetz-rec = () - if type(element) == function { - cetz-drawing.push(element) - } else if "type" not in element { - panic("Element " + repr(element) + " has no type") - } else if element.type == "fragment" { - (ctx, drawing) = fragment.draw-fragment(element, ctx) - } else if element.type == "link" { - (ctx, drawing) = link.draw-link(element, ctx) - } else if element.type == "branch" { - (ctx, drawing, cetz-rec) = branch.draw-branch(element, ctx, draw-fragments-and-link) - } else if element.type == "cycle" { - (ctx, drawing, cetz-rec) = cycle.draw-cycle(element, ctx, draw-fragments-and-link) - } else if element.type == "hook" { - ctx = hook.draw-hook(element, ctx) - } else if element.type == "parenthesis" { - (ctx, drawing, cetz-rec) = parenthesis.draw-parenthesis( - element, - ctx, - draw-fragments-and-link, - ) - } else if element.type == "operator" { - (ctx, drawing) = operator.draw-operator(element, fragment-drawing, ctx) - } else { - panic("Unknown element type " + element.type) - } - fragment-drawing += drawing - cetz-drawing += cetz-rec - } - if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { - ctx.links.push(ctx.last-anchor) - ctx.last-anchor.drew = true - } - ( - ctx, - fragment-drawing, - cetz-drawing, - ) -} - -#let draw-link-over(ctx, link, over, angle) = { - let name = link.name + "-over" - let (over, length, radius) = if type(over) == str { - (over, link-over-radius, link-over-radius) - } else if type(over) == dictionary { - let name = over.at("name", default: none) - if name == none { - panic("Over argument must have a name") - } - ( - name, - over.at("length", default: link-over-radius), - over.at("radius", default: link-over-radius) - ) - } else { - panic("Over must be a string or a dictionary, got " + type(link.at("over"))) - } - intersections(name, over, link.name) - let color = if ctx.config.debug { - red - } else { - white - } - scope({ - rotate(angle) - rect( - anchor: "center", - (to: name + ".0", rel: (-length/2,-radius/2)), - (rel: (length, radius)), - fill: color, - stroke: color - ) - }) -} - -#let draw-link-decoration(ctx) = { - ( - ctx, - get-ctx(cetz-ctx => { - for link in ctx.links { - let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) - if ctx.config.debug { - circle(from, radius: .1em, fill: red, stroke: red) - circle(to, radius: .1em, fill: red, stroke: red) - } - let length = distance-between(cetz-ctx, from, to) - hide(line(from, to, name: link.name)) - if link.at("over") != none { - if type(link.at("over")) == array { - for over in link.at("over") { - draw-link-over(ctx, link, over, angle) - } - } else { - draw-link-over(ctx, link, link.at("over"), angle) - } - } - - scope({ - set-origin(from) - rotate(angle) - (link.draw)(length, ctx, cetz-ctx, override: link.override) - }) - } - }), - ) -} - -/// set elements names and split the molecule into sub-groups -#let preprocessing(body, group-id: 0, link-id: 0, operator-id: 0) = { - let result = ((),) - for element in body { - if type(element) == dictionary { - if element.at("name", default: none) == none { - if element.type == "fragment" { - element.name = "fragment-" + str(group-id) - group-id += 1 - } else if element.type == "link" { - element.name = "link-" + str(link-id) - link-id += 1 - } else if element.type == "operator" { - element.name = "operator-" + str(operator-id) - operator-id += 1 - } - } - if element.at("body", default: none) != none { - let child-body - (child-body, group-id, link-id, operator-id) = preprocessing( - element.body, - group-id: group-id, - link-id: link-id, - operator-id: operator-id, - ) - if element.type == "parenthesis" and element.resonance { - element.body = child-body - } else { - element.body = child-body.at(0) - } - } - if element.type == "operator" or (element.type == "parenthesis" and element.resonance) { - result.push(element) - result.push(()) - } else { - result.at(-1).push(element) - } - } else { - result.at(-1).push(element) - } - } - (result, group-id, link-id, operator-id) -} - -#let operator-group-name = id => { - "operator-group-" + str(id) -} - -#let draw-groups(ctx, bodies, after-operator: false) = { - ctx.last-name = "" - - let drawables = for (i, body) in bodies.enumerate() { - if type(body) == array { - if body.len() > 0 { - ctx.last-name = operator-group-name(i) - get-ctx(cetz-ctx => { - let ctx = ctx - if ctx.last-anchor.type == "coord" { - (cetz-ctx, ctx.last-anchor.anchor) = cetz.coordinate.resolve(cetz-ctx, ctx.last-anchor.anchor) - } - let last-anchor = ctx.last-anchor - let (ctx, atoms, cetz-drawing) = draw-fragments-and-link(ctx, body) - for (links, name, from-mol) in ctx.hooks-links { - ctx = draw-hooks-links(links, name, ctx, from-mol) - } - - let molecule = { - atoms - } - - let (ctx: cetz-ctx, drawables, bounds: molecule-bounds, anchors) = custom-process.many(cetz-ctx, molecule) - molecule-bounds = cetz.util.revert-transform(cetz-ctx.transform, molecule-bounds) - - let (translate-x, translate-y) = if after-operator { - let (_, origin-anchor) = cetz.coordinate.resolve(cetz-ctx, last-anchor.anchor) - ( - origin-anchor.at(0) - molecule-bounds.low.at(0), - -origin-anchor.at(1) - + ( - molecule-bounds.low.at(1) + molecule-bounds.high.at(1) - ) - / 2, - ) - } else { - (0, 0) - } - let transform-matrix = cetz.matrix.transform-translate( - translate-x, - translate-y, - 0, - ) - // panic(anchors) - for name in anchors { - let hold-anchors = cetz-ctx.nodes.at(name).anchors - cetz-ctx.nodes.at(name).anchors = (name) => { - if name != () { - cetz.matrix.mul4x4-vec3(transform-matrix, hold-anchors(name)) - } else { - hold-anchors(name) - } - } - } - ( - _ => ( - ctx: cetz-ctx, - drawables: cetz.drawable.apply-transform( - cetz.matrix.transform-translate( - translate-x, - translate-y, - 0, - ), - drawables, - ), - ), - ) - scope({ - translate(x: translate-x, y: -translate-y) - draw-link-decoration(ctx).at(1) - on-layer(2, cetz-drawing) - let bound-rect = cetz.draw.rect( - molecule-bounds.low, - molecule-bounds.high, - name: ctx.last-name, - stroke: purple, - ) - if ctx.config.debug { - bound-rect - } else { - cetz.draw.hide(bound-rect) - } - }) - }) - } - } else if body.type == "operator" { - let (op-ctx, drawable) = operator.draw-operator(body, ctx) - after-operator = true - ctx = op-ctx - drawable - } else if body.type == "parenthesis" { - let (parenthesis-ctx, drawable) = parenthesis.draw-resonance-parenthesis(body, draw-groups, ctx) - ctx = parenthesis-ctx - drawable - } else { - panic("Unexpected element type: " + body.type) - } - } - (drawables, ctx) -} - -#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { - let config = merge-dictionaries(config, default) - let ctx = default-ctx - ctx.angle = config.base-angle - ctx.config = config - - let bodies = preprocessing(body).at(0) - let (final-drawing, ctx) = draw-groups(ctx, bodies) - - if name == none { - final-drawing - } else { - group(name: name, anchor: mol-anchor, { - anchor("default", (0, 0)) - final-drawing - }) - } -} - -/// setup a molecule skeleton drawer -#let skeletize(debug: false, background: none, config: (:), body) = { - if "debug" not in config { - config.insert("debug", debug) - } - cetz.canvas(debug: debug, background: background, draw-skeleton(config: config, body)) -} - -#let skeletize-config(default-config) = { - let config-function(debug: false, background: none, config: (:), body) = { - skeletize(debug: debug, background: background, config: merge-dictionaries(config, default-config), body) - } - config-function -} - -#let draw-skeleton-config(default-config) = { - let config-function(config: (:), name: none, mol-anchor: none, body) = { - draw-skeleton(config: merge-dictionaries(config, default-config), body) - } - config-function -} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/branch.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/branch.typ deleted file mode 100644 index 2c9e062843..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/branch.typ +++ /dev/null @@ -1,32 +0,0 @@ -#import "../utils/angles.typ" -#import "../utils/context.typ" as context_ - -/// Compute the angle the branch should take in order to be "in the center" -/// of the cycle angle -#let cycle-angle(ctx) = { - if ctx.in-cycle { - if ctx.faces-count == 0 { - ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 - } else { - ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 - } - } else { - ctx.angle - } -} - -#let draw-branch(branch, ctx, draw-fragments-and-link) = { - let angle = angles.angle-from-ctx(ctx, branch.args, cycle-angle(ctx)) - let (branch-ctx, drawing, cetz-rec) = draw-fragments-and-link( - ( - ..ctx, - in-cycle: false, - first-branch: true, - cycle-step-angle: 0, - angle: angle, - ), - branch.body, - ) - ctx = context_.update-parent-context(ctx, branch-ctx) - (ctx, drawing, cetz-rec) -} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cram.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cram.typ deleted file mode 100644 index c66f6c127d..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cram.typ +++ /dev/null @@ -1,61 +0,0 @@ -#import "@preview/cetz:0.4.1" -#import "../utils/utils.typ" - -/// Draw a triangle between two fragments -#let cram(from, to, ctx, cetz-ctx, args) = { - let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) - let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) - let base-length = utils.convert-length( - cetz-ctx, - args.at("base-length", default: ctx.config.filled-cram.base-length), - ) - cetz.draw.line( - (from-x, from-y - base-length / 2), - (from-x, from-y + base-length / 2), - (to-x, to-y), - close: true, - stroke: args.at("stroke", default: ctx.config.filled-cram.stroke), - fill: args.at("fill", default: ctx.config.filled-cram.fill), - ) -} - -/// Draw a dashed triangle between two molecules fragments -#let dashed-cram(from, to, length, ctx, cetz-ctx, args) = { - import cetz.draw: * - let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) - let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) - let base-length = utils.convert-length( - cetz-ctx, - args.at("base-length", default: ctx.config.dashed-cram.base-length), - ) - let tip-length = utils.convert-length( - cetz-ctx, - args.at("tip-length", default: ctx.config.dashed-cram.tip-length), - ) - hide({ - line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - tip-length / 2)) - line( - name: "bottom", - (from-x, from-y + base-length / 2), - (to-x, to-y + tip-length / 2), - ) - }) - let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) - let dash-gap = utils.convert-length(cetz-ctx, args.at("dash-gap", default: ctx.config.dashed-cram.dash-gap)) - let dash-width = stroke.thickness - let converted-dash-width = utils.convert-length(cetz-ctx, dash-width) - let length = utils.convert-length(cetz-ctx, length) - - let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) - let incr = 100% / dash-count - - let percentage = 0% - while percentage <= 100% { - line( - (name: "top", anchor: percentage), - (name: "bottom", anchor: percentage), - stroke: stroke, - ) - percentage += incr - } -} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cycle.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cycle.typ deleted file mode 100644 index 9a81f12930..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/cycle.typ +++ /dev/null @@ -1,155 +0,0 @@ -#import "@preview/cetz:0.4.1" -#import "../utils/utils.typ" -#import "../utils/angles.typ" -#import "../utils/context.typ" as context_ - -#let max-int = 9223372036854775807 - -/// Insert missing vertices in the cycle -#let missing-vertices(ctx, vertex, cetz-ctx) = { - let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) - for i in range(ctx.cycle-faces - vertex.len()) { - let (x, y, _) = vertex.last() - vertex.push(( - x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), - y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), - 0, - )) - } - vertex -} - -/// extrapolate the center and the radius of the cycle -#let cycle-center-radius(ctx, cetz-ctx, vertex) = { - let min-radius = max-int - let center = (0, 0) - let faces = ctx.cycle-faces - let odd = calc.rem(faces, 2) == 1 - let debug = () - for (i, v) in vertex.enumerate() { - if (ctx.config.debug) { - debug += cetz.draw.circle(v, radius: .1em, fill: blue, stroke: blue) - } - let (x, y, _) = v - center = (center.at(0) + x, center.at(1) + y) - if odd { - let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) - let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) - let (ox1, oy1, _) = vertex.at(opposite1) - let (ox2, oy2, _) = vertex.at(opposite2) - let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 - if radius < min-radius { - min-radius = radius - } - } else { - let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) - let (ox, oy, _) = vertex.at(opposite) - let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 - if radius < min-radius { - min-radius = radius - } - } - } - ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius, debug) -} - -#let draw-cycle-center-arc(ctx, name, center-arc) = { - import cetz.draw: * - let faces = ctx.cycle-faces - let vertex = ctx.vertex-anchors - get-ctx(cetz-ctx => { - let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) - if vertex.len() < faces { - vertex = missing-vertices(ctx, cetz-ctx) - } - let (center, min-radius, debug) = cycle-center-radius(ctx, cetz-ctx, vertex) - debug - if name != none { - anchor(name, center) - } - if center-arc != none { - if min-radius == max-int { - panic("The cycle has no opposite vertices") - } - if ctx.cycle-faces > 4 { - min-radius *= center-arc.at("radius", default: 0.7) - } else { - min-radius *= center-arc.at("radius", default: 0.5) - } - let start = center-arc.at("start", default: 0deg) - let end = center-arc.at("end", default: 360deg) - let delta = center-arc.at("delta", default: end - start) - center = ( - center.at(0) + min-radius * calc.cos(start), - center.at(1) + min-radius * calc.sin(start), - ) - arc( - center, - ..center-arc, - radius: min-radius, - start: start, - delta: delta, - ) - } - }) -} - -#let draw-cycle(cycle, ctx, draw-fragments-and-link) = { - let cycle-step-angle = 360deg / cycle.faces - let angle = angles.angle-from-ctx(ctx, cycle.args, none) - if angle == none { - if ctx.in-cycle { - angle = ctx.relative-angle - (180deg - cycle-step-angle) - if ctx.faces-count != 0 { - angle += ctx.cycle-step-angle - } - } else if ( - ctx.relative-angle == 0deg - and ctx.angle == 0deg - and not cycle.args.at( - "align", - default: false, - ) - ) { - angle = cycle-step-angle - 90deg - } else { - angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 - } - } - let first-fragment = none - if ctx.last-anchor.type == "fragment" { - first-fragment = ctx.last-anchor.name - if first-fragment not in ctx.hooks { - ctx.hooks.insert(first-fragment, ctx.last-anchor) - } - } - let name = none - let record-vertex = false - if "name" in cycle.args { - name = cycle.args.at("name") - record-vertex = true - } else if "arc" in cycle.args { - record-vertex = true - } - let (cycle-ctx, drawing, cetz-rec) = draw-fragments-and-link( - ( - ..ctx, - in-cycle: true, - cycle-faces: cycle.faces, - faces-count: 0, - first-branch: true, - cycle-step-angle: cycle-step-angle, - relative-angle: angle, - first-fragment: first-fragment, - angle: angle, - record-vertex: record-vertex, - vertex-anchors: (), - ), - cycle.body, - ) - ctx = context_.update-parent-context(ctx, cycle-ctx) - if record-vertex { - drawing += draw-cycle-center-arc(cycle-ctx, name, cycle.args.at("arc", default: none)) - } - (ctx, drawing, cetz-rec) -} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/fragment.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/fragment.typ deleted file mode 100644 index b1db6aa0c3..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/fragment.typ +++ /dev/null @@ -1,149 +0,0 @@ -#import "../utils/context.typ": * -#import "../utils/anchors.typ": * -#import "@preview/cetz:0.4.1" - -#let draw-fragment-text(ctx, mol, pos) = { - import cetz.draw: * - for (id, eq) in mol.atoms.enumerate() { - let name = str(id) - let color = if mol.colors != none { - if type(mol.colors) == color { - mol.colors - } else { - mol.colors.at(calc.min(id, mol.colors.len() - 1)) - } - } else { - fragment-color - } - - // draw atoms of the group one after the other from left to right - content( - name: name, - anchor: if mol.vertical { - "north" - } else { - "mid-west" - }, - ( - if id == 0 { - pos - } else if mol.vertical { - (to: str(id - 1) + ".south", rel: (0, -.2em)) - } else { - str(id - 1) + ".mid-east" - } - ), - auto-scale: false, - { - show math.equation: math.upright - set text(fill: color) if color != none - set text(font: fragment-font) if fragment-font != none - eq - }, - ) - id += 1 - } -} - -#let draw-fragment-lewis(ctx, group-name, count, lewis) = { - if lewis.len() == 0 { - return () - } - import cetz.draw: * - get-ctx(cetz-ctx => { - for (id, (angle: lewis-angle, radius, draw)) in lewis.enumerate() { - if (lewis-angle == none) { - lewis-angle = ctx.config.lewis.angle - } - if (radius == none) { - radius = ctx.config.lewis.radius - } - let lewis-angle = angles.angle-correction(lewis-angle) - let mol-id = if angles.angle-in-range-inclusive(lewis-angle, 90deg, 270deg) { - 0 - } else { - count - 1 - } - let anchor = fragment-anchor( - ctx, - cetz-ctx, - lewis-angle, - group-name, - str(mol-id), - margin: radius, - ) - scope({ - set-origin(anchor) - rotate(lewis-angle) - draw(ctx, cetz-ctx) - }) - } - }) -} - -#let draw-fragment-elements(mol, ctx) = { - let name = mol.name - if name in ctx.hooks { - panic("Molecule fragment with name " + name + " already exists : " + ctx.hooks.keys().join(", ")) - } - ctx.hooks.insert(name, mol) - - let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { - ("west", true, ctx.last-anchor.anchor) - } else if ctx.last-anchor.type == "link" { - if ctx.last-anchor.to == none { - ctx.last-anchor.to = link-fragment-index( - ctx.last-anchor.angle, - true, - mol.count - 1, - mol.vertical, - ) - } - let group-anchor = link-fragment-anchor(ctx.last-anchor.to, mol.count) - ctx.last-anchor.to-name = name - (group-anchor, false, ctx.last-anchor.name + "-end-anchor") - } else { - panic("A molecule fragment must be linked to a coord or a link") - } - ctx = context_.set-last-anchor( - ctx, - (type: "fragment", name: name, count: mol.at("count"), vertical: mol.vertical), - ) - if (side) { - ctx.id += 1 - } - ( - ctx, - { - import cetz.draw: * - group( - anchor: if side { - group-anchor - } else { - "from" + str(ctx.id) - }, - name: name, - { - anchor("default", coord) - draw-fragment-text(ctx, mol, coord) - if not side { - anchor("from" + str(ctx.id), group-anchor) - } - }, - ) - draw-fragment-lewis(ctx, name, mol.count, mol.at("lewis")) - }, - ) -} - -#let draw-fragment(element, ctx) = { - if ctx.first-branch { - panic("A molecule fragment can not be the first element in a cycle") - } - let (ctx, drawing) = draw-fragment-elements(element, ctx) - if element.links.len() != 0 { - ctx.hooks.insert(ctx.last-anchor.name, element) - ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) - } - (ctx, drawing) -} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/hook.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/hook.typ deleted file mode 100644 index 1c5dafb9d1..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/hook.typ +++ /dev/null @@ -1,14 +0,0 @@ - -#let draw-hook(hook, ctx) = { - if hook.name in ctx.hooks { - panic("Hook " + hook.name + " already exists") - } - if ctx.last-anchor.type == "link" { - ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) - } else if ctx.last-anchor.type == "coord" { - ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.anchor)) - } else { - panic("A hook must placed after a link or at the beginning of the skeleton") - } - ctx -} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/link.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/link.typ deleted file mode 100644 index d6c0c0876a..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/link.typ +++ /dev/null @@ -1,103 +0,0 @@ -#import "../utils/angles.typ" -#import "../utils/anchors.typ": * - -#let create-link-decorations-anchors(link, ctx) = { - let link-angle = 0deg - let to-name = none - if ctx.in-cycle { - if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-fragment != none { - to-name = ctx.first-fragment - } - if ctx.faces-count == 0 { - link-angle = ctx.relative-angle - } else { - link-angle = ctx.relative-angle + ctx.cycle-step-angle - } - } else { - link-angle = angles.angle-from-ctx(ctx, link, ctx.angle) - } - link-angle = angles.angle-correction(link-angle) - ctx.relative-angle = link-angle - let override = angles.angle-override(link-angle, ctx) - - let to-connection = link.at("to", default: none) - let from-connection = none - let from-name = none - - let from-pos = if ctx.last-anchor.type == "coord" { - ctx.last-anchor.anchor - } else if ctx.last-anchor.type == "fragment" { - from-connection = link-fragment-index( - link-angle, - false, - ctx.last-anchor.count - 1, - ctx.last-anchor.vertical, - ) - from-connection = link.at("from", default: from-connection) - from-name = ctx.last-anchor.name - fragment-link-anchor( - ctx.last-anchor.name, - from-connection, - ctx.last-anchor.count, - ) - } else if ctx.last-anchor.type == "link" { - ctx.last-anchor.name + "-end-anchor" - } else { - panic("Unknown anchor type " + ctx.last-anchor.type) - } - let length = link.at("atom-sep", default: ctx.config.atom-sep) - let link-name = link.at("name") - if ctx.record-vertex { - if ctx.faces-count == 0 { - ctx.vertex-anchors.push(from-pos) - } - if ctx.faces-count < ctx.cycle-faces - 1 { - ctx.vertex-anchors.push(link-name + "-end-anchor") - } - } - ctx = context_.set-last-anchor( - ctx, - ( - type: "link", - name: link-name, - override: override, - from-pos: from-pos, - from-name: from-name, - from: from-connection, - to-name: to-name, - to: to-connection, - angle: link-angle, - over: link.at("over", default: none), - draw: link.draw, - ), - ) - ( - ctx, - { - hide({ - circle(name: link-name + "-start-anchor", from-pos, radius: .25em) - }) - let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) - if ctx.config.debug { - line(from-pos, end-anchor, stroke: blue + .1em) - } - hide( - { - circle(name: link-name + "-end-anchor", end-anchor, radius: .25em) - }, - bounds: true, - ) - }, - ) -} - -#let draw-link(link, ctx) = { - ctx.first-branch = false - let drawing - (ctx, drawing) = create-link-decorations-anchors(link, ctx) - ctx.faces-count += 1 - if link.links.len() != 0 { - ctx.hooks-links.push((link.links, ctx.last-anchor.name, false)) - } - (ctx, drawing) -} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/operator.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/operator.typ deleted file mode 100644 index ff1fd2792b..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/operator.typ +++ /dev/null @@ -1,36 +0,0 @@ -#import "@preview/cetz:0.4.1": draw, process, util - -#let draw-operator(operator, ctx) = { - import draw: * - - let op-name = operator.name - ctx.last-anchor = (type: "coord", anchor: (rel: (operator.margin, 0), to: (name: op-name, anchor: "east"))) - - ( - ctx, - get-ctx(cetz-ctx => { - - let west-previous-mol-anchor = (name: ctx.last-name, anchor: "east") - - let east-op-anchor = (rel: (operator.margin, 0), to: west-previous-mol-anchor) - - let op = if (operator.op == none) { - "" - } else { - operator.op - } - - content( - name: op-name, - anchor: "west", - east-op-anchor, - op, - ) - - if (ctx.config.debug) { - circle(west-previous-mol-anchor, radius: 0.05, fill: yellow, stroke: none) - circle(ctx.last-anchor.anchor, radius: 0.05, fill: yellow, stroke: none) - } - }), - ) -} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/parenthesis.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/parenthesis.typ deleted file mode 100644 index 75d6b07a0a..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/drawer/parenthesis.typ +++ /dev/null @@ -1,215 +0,0 @@ -#import "../utils/utils.typ" -#import "@preview/cetz:0.4.1" - -#let left-parenthesis-anchor(parenthesis, ctx) = { - let anchor = if parenthesis.body.at(0).type == "fragment" { - let name = parenthesis.body.at(0).name - parenthesis.body.at(0).name = name - (name: name, anchor: "west") - } else if parenthesis.body.at(0).type == "link" { - let name = parenthesis.body.at(0).at("name") - parenthesis.body.at(0).name = name - (name + "-start-anchor", 45%, name + "-end-anchor") - } else if not left { } else { - panic("The first element of a parenthesis must be a molecule fragment or a link") - } - (ctx, parenthesis, anchor) -} - -#let right-parenthesis-anchor(parenthesis, ctx) = { - let right-name = parenthesis.at("right") - let right-type = "" - if right-name != none { - right-type = utils.get-element-type(parenthesis.body, right-name) - if right-type == none { - panic("The right element of the parenthesis does not exist") - } - } else { - right-type = parenthesis.body.at(-1).type - right-name = parenthesis.body.at(-1).at("name", default: none) - } - - let anchor = if right-type == "fragment" { - (name: right-name, anchor: "east") - } else if right-type == "link" { - (right-name + "-start-anchor", 55%, right-name + "-end-anchor") - } else { - panic("The last element of a parenthesis must be a molecule fragment or a link but got " + right-type) - } - (ctx, parenthesis, anchor) -} - -#let parenthesis-content(parenthesis, height, cetz-ctx) = { - let block = block(height: height * cetz-ctx.length * 1.2, width: 0pt) - let left-parenthesis = { - set text(top-edge: "bounds", bottom-edge: "bounds") - math.lr($parenthesis.l block$, size: 100%) - } - let right-parenthesis = { - set text(top-edge: "bounds", bottom-edge: "bounds") - math.lr($block parenthesis.r$, size: 100%) - } - - let right-parenthesis-with-attach = { - set text(top-edge: "bounds", bottom-edge: "bounds") - math.attach(right-parenthesis, br: parenthesis.br, tr: parenthesis.tr) - } - - (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) -} - -#let draw-parenthesis(parenthesis, ctx, draw-molecules-and-link) = { - let (parenthesis-ctx, drawing, cetz-rec) = draw-molecules-and-link( - ctx, - parenthesis.body, - ) - ctx = parenthesis-ctx - - - drawing += { - import cetz.draw: * - get-ctx(cetz-ctx => { - let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( - cetz-ctx, - { - drawing - }, - ) - let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) - - let sub-height = utils.bounding-box-height(sub-bounds) - let sub-v-mid = sub-bounds.low.at(1) - sub-height / 2 - - let sub-width = utils.bounding-box-width(sub-bounds) - - let (ctx, parenthesis, left-anchor) = left-parenthesis-anchor(parenthesis, ctx) - - let (ctx, parenthesis, right-anchor) = right-parenthesis-anchor(parenthesis, ctx) - - let height = parenthesis.at("height") - if height == none { - if not parenthesis.align { - panic("You must specify the height of the parenthesis if they are not aligned") - } - height = sub-height - } else { - height = utils.convert-length(cetz-ctx, height) - } - - let (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) = parenthesis-content( - parenthesis, - height, - cetz-ctx, - ) - - let (_, (lx, ly, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, left-anchor) - let (_, (rx, ry, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, right-anchor) - - if ctx.config.debug { - circle((lx, ly), radius: 1pt, fill: orange, stroke: orange) - circle((rx, ry), radius: 1pt, fill: orange, stroke: orange) - rect((lx, sub-bounds.low.at(1)), (rx, sub-bounds.high.at(1)), stroke: green) - line((lx, sub-v-mid), (rx, sub-v-mid), stroke: green) - } - - let hoffset = calc.abs(sub-width - calc.abs(rx - lx)) - - if parenthesis.align { - ly -= ly - sub-v-mid - ry = ly - } else if type(parenthesis.yoffset) == array { - if parenthesis.yoffset.len() != 2 { - panic("The parenthesis yoffset must be a list of length 2 or a number") - } - ly += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(0)) - ry += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(1)) - } else if parenthesis.yoffset != none { - let offset = utils.convert-length(cetz-ctx, parenthesis.yoffset) - ly += offset - ry += offset - } - - if type(parenthesis.xoffset) == array { - if parenthesis.xoffset.len() != 2 { - panic("The parenthesis xoffset must be a list of length 2 or a number") - } - lx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(0)) - rx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(1)) - } else if parenthesis.xoffset != none { - let offset = utils.convert-length(cetz-ctx, parenthesis.xoffset) - lx -= offset - rx += offset - } - - let right-bounds = cetz.process.many(cetz-ctx, content((0, 0), right-parenthesis, auto-scale: false)).bounds - let right-with-attach-bounds = cetz - .process - .many(cetz-ctx, content((0, 0), right-parenthesis-with-attach, auto-scale: false)) - .bounds - let right-voffset = calc.abs(right-bounds.low.at(1) - right-with-attach-bounds.low.at(1)) - if parenthesis.tr != none and parenthesis.br != none { - right-voffset /= 2 - } else if (parenthesis.tr != none) { - right-voffset *= -1 - } - - ( - _ => ( - ctx: cetz-ctx, - drawables: drawables, - bounds: bounds, - ), - ) - content((lx, ly), anchor: "mid-east", left-parenthesis, auto-scale: false) - content((rx, ry - right-voffset), anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) - }) - } - - (ctx, drawing, cetz-rec) -} - - -#let draw-resonance-parenthesis(parenthesis, draw-function, ctx) = { - import cetz.draw: * - let left-name = "parenthesis-" + str(ctx.id) - let right-name = "parenthesis-" + str(ctx.id + 1) - ctx.id += 2 - let last-anchor = ctx.last-anchor - let (drawing, ctx) = draw-function(ctx, parenthesis.body, after-operator: true) - - let drawing = get-ctx(cetz-ctx => { - let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( - cetz-ctx, - drawing, - ) - let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) - let height = parenthesis.height - if height == none { - height = utils.bounding-box-height(sub-bounds) - } else { - height = utils.convert-length(cetz-ctx, height) - } - let width = utils.bounding-box-width(sub-bounds) - - let (left-parenthesis, _, right-parenthesis-with-attach) = parenthesis-content(parenthesis, height, cetz-ctx) - - let right-anchor = (rel: (width, 0), to: (name: left-name, anchor: "east")) - on-layer( - 1, - { - content(last-anchor.anchor, name: left-name, anchor: "mid-east", left-parenthesis, auto-scale: false) - content(right-anchor, name: right-name, anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) - }, - ) - ( - ctx => ( - ctx: cetz-ctx, - drawables: drawables, - bounds: bounds, - ), - ) - }) - - ctx.last-anchor = (type: "coord", anchor: (name: right-name, anchor: "mid-east")) - (ctx, drawing) -} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/elements/fragment.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/elements/fragment.typ deleted file mode 100644 index e7b66eb50f..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/elements/fragment.typ +++ /dev/null @@ -1,74 +0,0 @@ -#let split-equation(mol, equation: false) = { - if equation { - mol = mol.body - if mol.has("children") { - mol = mol.children - } else { - mol = (mol,) - } - } - - - let result = () - let last-number = false - for m in mol { - let last-number-hold = last-number - if m.has("text") { - let text = m.text - if str.match(text, regex("^[A-Z][a-z]*$")) != none { - result.push(m) - } else if str.match(text, regex("^[0-9]+$")) != none { - if last-number { - panic("Consecutive numbers in fragment fragment") - } - last-number = true - result.push(m) - } else { - panic("Invalid fragment fragment content") - } - } else if m.func() == math.attach or m.func() == math.lr { - result.push(m) - } else if m == [ ] { - continue - } else { - panic("Invalid fragment fragment content") - } - if last-number-hold { - result.at(-2) = result.at(-2) + result.at(-1) - let _ = result.pop() - last-number = false - } - } - result -} - -#let fragment-cor-regex = "[0-9]*[A-Z][a-z]*'*" -#let exponent-regex = "((?:[0-9]+(?:\\+|\\-)?)|[A-Z]|\\+|\\-)" -#let exponent-base-regex = "(?:(\\^|_)" + exponent-regex + ")?(?:(\\^|_)" + exponent-regex + ")?" -#let fragment-regex = regex("^ *(" + fragment-cor-regex + ")" + exponent-base-regex) - -#let split-string(mol) = { - let aux(str) = { - let match = str.match(fragment-regex) - if match == none { - panic(str + " is not a valid fragment") - } else if match.captures.at(1) == match.captures.at(3) and match.captures.at(1) != none { - panic("You cannot use an exponent and a subscript twice") - } - let eq = "\"" + match.captures.at(0) + "\"" - if match.captures.at(1) != none { - eq += match.captures.at(1) + "(" + match.captures.at(2) + ")" - } - if match.captures.at(3) != none { - eq += match.captures.at(3) + "(" + match.captures.at(4) + ")" - } - let eq = math.equation(eval(eq, mode: "math")) - (eq, match.end) - } - - while not mol.len() == 0 { - let (eq, end) = aux(mol) - mol = mol.slice(end) - (eq,) - } -} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/elements/lewis.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/elements/lewis.typ deleted file mode 100644 index fb0d3547bc..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/elements/lewis.typ +++ /dev/null @@ -1,133 +0,0 @@ -#import "@preview/cetz:0.4.1" - -/// Create a lewis function that is then used to draw a lewis -/// formulae element around the fragment -/// -/// - draw-function (function): The function that will be used to draw the lewis element. It should takes three arguments: the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links -/// -> function -#let build-lewis(draw-function) = { - (..args) => { - if args.pos().len() != 0 { - panic("Lewis function takes no positional arguments") - } - let args = args.named() - let angle = args.at("angle", default: none) - let radius = args.at("radius", default: none) - ( - angle: angle, - radius: radius, - draw: (ctx, cetz-ctx) => draw-function(ctx, cetz-ctx, args) - ) - } -} - -/// draw a sigle electron around the fragment -/// -/// It is possible to change the distance from the center of -/// the electron with the `gap` argument. -/// -/// The position of the electron is set by the `offset` argument. Available values are: -/// - "top": the electron is placed above the fragment center line -/// - "bottom": the electron is placed below the fragment center line -/// - "center": the electron is placed at the fragment center line -/// -/// It is also possible to change the `radius`, `stroke` and `fill` arguments -/// #example(``` -/// #skeletize({ -/// fragment("A", lewis:( -/// lewis-single(offset: "top"), -/// )) -/// single(angle:-2) -/// fragment("B", lewis:( -/// lewis-single(offset: "bottom"), -/// )) -/// single(angle:-2) -/// fragment("C", lewis:( -/// lewis-single(offset: "center"), -/// )) -/// }) -/// ```) -#let lewis-single = build-lewis((ctx, cetz-ctx, args) => { - import cetz.draw: * - let radius = args.at("radius", default: ctx.config.lewis-single.radius) - let gap = args.at("gap", default: ctx.config.lewis-single.gap) - let offset = args.at("offset", default: ctx.config.lewis-single.offset) - let gap = if offset == "top" { - gap - } else if offset == "bottom" { - -gap - } else if offset != "center" { - panic("Invalid position, expected 'top', 'bottom' or 'center'") - } - let fill = args.at("fill", default: ctx.config.lewis-single.fill) - let stroke = args.at("stroke", default: ctx.config.lewis-single.stroke) - circle((0, gap), radius: radius, fill: fill, stroke: stroke) -}) - -/// Draw a pair of electron around the fragment -/// -/// It is possible to change the distance from the center of -/// the electron with the `gap` argument. -/// It is also possible to change the `radius`, `stroke` and `fill` arguments -/// #example(``` -/// #skeletize({ -/// fragment("A", lewis:( -/// lewis-double(), -/// lewis-double(angle: 90deg), -/// lewis-double(angle: 180deg), -/// lewis-double(angle: -90deg) -/// )) -/// }) -/// ```) -#let lewis-double = build-lewis((ctx, cetz-ctx, args) => { - import cetz.draw: * - let radius = args.at("radius", default: ctx.config.lewis-double.radius) - let gap = args.at("gap", default: ctx.config.lewis-double.gap) - let fill = args.at("fill", default: ctx.config.lewis-double.fill) - let stroke = args.at("stroke", default: ctx.config.lewis-double.stroke) - circle((0, -gap), radius: radius, fill: fill, stroke: stroke) - circle((0, gap), radius: radius, fill: fill, stroke: stroke) -}) - -/// Draw a pair of electron liked by a single line -/// -/// It is possible to change the length of the line with the `lenght` argument. -/// It is also possible to change the `stroke` agument -/// #example(``` -/// #skeletize({ -/// fragment("B", lewis:( -/// lewis-line(angle: 45deg), -/// lewis-line(angle: 135deg), -/// lewis-line(angle: -45deg), -/// lewis-line(angle: -135deg) -/// )) -/// }) -/// ```) -#let lewis-line = build-lewis((ctx, cetz-ctx, args) => { - import cetz.draw: * - let length = args.at("length", default: ctx.config.lewis-line.length) - let stroke = args.at("stroke", default: ctx.config.lewis-line.stroke) - line((0, -length / 2), (0, length / 2), stroke: stroke) -}) - - -/// Draw a rectangle to denote a lone pair of electrons -/// -/// It is possible to change the height and width of the rectangle with the `height` and `width` arguments. -/// It is also possible to change the `fill` and `stroke` arguments -/// #example(``` -/// #skeletize({ -/// fragment("C", lewis:( -/// lewis-rectangle(), -/// lewis-rectangle(angle: 180deg) -/// )) -/// }) -/// ```) -#let lewis-rectangle = build-lewis((ctx, cetz-ctx, args) => { - import cetz.draw: * - let height = args.at("height", default: ctx.config.lewis-rectangle.height) - let width = args.at("width", default: ctx.config.lewis-rectangle.width) - let fill = args.at("fill", default: ctx.config.lewis-rectangle.fill) - let stroke = args.at("stroke", default: ctx.config.lewis-rectangle.stroke) - rect((-width / 2, -height / 2), (width / 2, height / 2), fill: fill, stroke: stroke) -}) diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/elements/links.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/elements/links.typ deleted file mode 100644 index a96321367a..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/elements/links.typ +++ /dev/null @@ -1,314 +0,0 @@ -#import "@preview/cetz:0.4.1" -#import "../drawer/cram.typ": * -#import "../utils/utils.typ" - - -/// Create a link function that is then used to draw a link between two points -/// -/// - draw-function (function): The function that will be used to draw the link. It should takes four arguments: the length of the link, the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links -/// -> function -#let build-link(draw-function) = { - (..args) => { - if args.pos().len() != 0 { - panic("Links takes no positional arguments") - } - let args = args.named() - ( - ( - type: "link", - draw: (length, ctx, cetz-ctx, override: (:)) => { - let args = args - for (key, val) in override { - args.insert(key, val) - } - draw-function(length, ctx, cetz-ctx, args) - }, - links: (:), - ..args, - ), - ) - } -} - -/// Draw a single line between two fragments -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// single() -/// fragment("B") -/// }) -///```) -/// It is possible to change the color and width of the line -/// with the `stroke` argument -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// single(stroke: red + 5pt) -/// fragment("B") -/// }) -///```) -#let single = build-link((length, ctx, _, args) => { - import cetz.draw: * - line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.single.stroke)) -}) - -/// Draw a double line between two fragments -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// double() -/// fragment("B") -/// }) -///```) -/// It is possible to change the color and width of the line -/// with the `stroke` argument and the gap between the two lines -/// with the `gap` argument -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// double( -/// stroke: orange + 2pt, -/// gap: .8em -/// ) -/// fragment("B") -/// }) -///```) -/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. -///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// double(offset: "right") -/// fragment("B") -/// double(offset: "left") -/// fragment("C") -/// double(offset: "center") -/// fragment("D") -/// }) -///```) -#let double = build-link((length, ctx, cetz-ctx, args) => { - import cetz.draw: * - let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.double.gap)) / 2 - let offset = args.at("offset", default: ctx.config.double.offset) - let coeff = args.at("offset-coeff", default: ctx.config.double.offset-coeff) - if coeff < 0 or coeff > 1 { - panic("Invalid offset-coeff value: must be between 0 and 1") - } - let gap-offset = if offset == "right" { - -gap - } else if offset == "left" { - gap - } else if offset == "center" { - 0 - } else { - panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") - } - - line( - ..if offset == "right" { - let x = length * (1 - coeff) / 2 - ((x, -gap + gap-offset), (x + length * coeff, -gap + gap-offset)) - } else { - ((0, -gap + gap-offset), (length, -gap + gap-offset)) - }, - stroke: args.at("stroke", default: ctx.config.double.stroke), - ) - line( - ..if offset == "left" { - let x = length * (1 - coeff) / 2 - ((x, gap + gap-offset), (x + length * coeff, gap + gap-offset)) - } else { - ((0, gap + gap-offset), (length, gap + gap-offset)) - }, - stroke: args.at("stroke", default: ctx.config.double.stroke), - ) -}) - -/// Draw a triple line between two fragments -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// triple() -/// fragment("B") -/// }) -///```) -/// It is possible to change the color and width of the line -/// with the `stroke` argument and the gap between the three lines -/// with the `gap` argument -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// triple( -/// stroke: blue + .5pt, -/// gap: .15em -/// ) -/// fragment("B") -/// }) -///```) -#let triple = build-link((length, ctx, cetz-ctx, args) => { - import cetz.draw: * - let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.triple.gap)) - line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) - line((0, -gap), (length, -gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) - line((0, gap), (length, gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) -}) - -/// Draw a filled cram between two fragments with the arrow pointing to the right -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// cram-filled-right() -/// fragment("B") -/// }) -///```) -/// It is possible to change the stroke and fill color of the arrow -/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// cram-filled-right( -/// stroke: red + 2pt, -/// fill: green, -/// base-length: 2em -/// ) -/// fragment("B") -/// }) -///```) -#let cram-filled-right = build-link((length, ctx, cetz-ctx, args) => cram( - (0, 0), - (length, 0), - ctx, - cetz-ctx, - args, -)) - -/// Draw a filled cram between two fragments with the arrow pointing to the left -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// cram-filled-left() -/// fragment("B") -/// }) -///```) -/// It is possible to change the stroke and fill color of the arrow -/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// cram-filled-left( -/// stroke: red + 2pt, -/// fill: green, -/// base-length: 2em -/// ) -/// fragment("B") -/// }) -///```) -#let cram-filled-left = build-link((length, ctx, cetz-ctx, args) => cram( - (length, 0), - (0, 0), - ctx, - cetz-ctx, - args, -)) - -/// Draw a hollow cram between two fragments with the arrow pointing to the right -/// It is a shorthand for `cram-filled-right(fill: none)` -#let cram-hollow-right = build-link((length, ctx, cetz-ctx, args) => { - args.fill = none - args.stroke = args.at("stroke", default: black) - cram((0, 0), (length, 0), ctx, cetz-ctx, args) -}) - -/// Draw a hollow cram between two fragments with the arrow pointing to the left -/// It is a shorthand for `cram-filled-left(fill: none)` -#let cram-hollow-left = build-link((length, ctx, cetz-ctx, args) => { - args.fill = none - args.stroke = args.at("stroke", default: black) - cram((length, 0), (0, 0), ctx, cetz-ctx, args) -}) - -/// Draw a dashed cram between two fragments with the arrow pointing to the right -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// cram-dashed-right() -/// fragment("B") -/// }) -///```) -/// It is possible to change the stroke of the lines in the arrow -/// with the `stroke` argument. You can also change the arrow base length with the `base-length` argument and the tip length with the `tip-length` argument. -// You can also change distance between the dashes with the `dash-gap` argument -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// cram-dashed-right( -/// stroke: red + 2pt, -/// base-length: 2em, -/// tip-length: 1em, -/// dash-gap: .5em -/// ) -/// fragment("B") -/// }) -///```) -#let cram-dashed-right = build-link((length, ctx, cetz-ctx, args) => dashed-cram( - (0, 0), - (length, 0), - length, - ctx, - cetz-ctx, - args, -)) - -/// Draw a dashed cram between two fragments with the arrow pointing to the left -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// cram-dashed-left() -/// fragment("B") -/// }) -///```) -/// It is possible to change the stroke of the lines in the arrow -/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument -/// #example(``` -/// #skeletize({ -/// fragment("A") -/// cram-dashed-left( -/// stroke: red + 2pt, -/// base-length: 2em, -/// dash-gap: .5em -/// ) -/// fragment("B") -/// }) -///```) -#let cram-dashed-left = build-link((length, ctx, cetz-ctx, args) => dashed-cram( - (length, 0), - (0, 0), - length, - ctx, - cetz-ctx, - args, -)) - - -/// Draw a plus sign between two fragments -/// #example(```` -/// #skeletize({ -/// fragment("A") -/// plus() -/// fragment("B") -/// }) -/// ````) -/// You can change the filling, size and stroke of the glyph with the `fill`, `size` and `stoke` arguments. The default values are the parent `text` parameters. -#let plus = build-link((length, ctx, cetz-ctx, args) => { - import cetz.draw: * - content( - anchor: "mid", - (length / 2, 0), - { - set text(fill: args.fill) if "fill" in args - set text(stroke: args.stroke) if "stroke" in args - set text(size: args.size) if "size" in args - text($+$) - } - ) -}) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/anchors.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/anchors.typ deleted file mode 100644 index 285efcde8c..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/anchors.typ +++ /dev/null @@ -1,296 +0,0 @@ -#import "angles.typ" -#import "context.typ" as context_ -#import "utils.typ": * -#import "@preview/cetz:0.4.1" -#import cetz.draw: * - -#let anchor-north-east(cetz-ctx, (x, y, _), delta, fragment, id) = { - let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( - cetz-ctx, - (name: fragment, anchor: (id, "north")), - ) - let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( - cetz-ctx, - (name: fragment, anchor: (id, "east")), - ) - - let a = (a - x) + delta - let b = (b - y) + delta - (a, b) -} - -#let anchor-north-west(cetz-ctx, (x, y, _), delta, fragment, id) = { - let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( - cetz-ctx, - (name: fragment, anchor: (id, "north")), - ) - let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( - cetz-ctx, - (name: fragment, anchor: (id, "west")), - ) - let a = (x - a) + delta - let b = (b - y) + delta - (a, b) -} - -#let anchor-south-west(cetz-ctx, (x, y, _), delta, fragment, id) = { - let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( - cetz-ctx, - (name: fragment, anchor: (id, "south")), - ) - let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( - cetz-ctx, - (name: fragment, anchor: (id, "west")), - ) - let a = (x - a) + delta - let b = (y - b) + delta - (a, b) -} - -#let anchor-south-east(cetz-ctx, (x, y, _), delta, fragment, id) = { - let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( - cetz-ctx, - (name: fragment, anchor: (id, "south")), - ) - let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( - cetz-ctx, - (name: fragment, anchor: (id, "east")), - ) - let a = (a - x) + delta - let b = (y - b) + delta - (a, b) -} - -/// Calculate an anchor position around a fragment using an ellipse -/// at a given angle -/// -/// - ctx (alchemist-ctx): the alchemist context -/// - cetz-ctx (cetz-ctx): the cetz context -/// - angle (float, int, angle): the angle of the anchor -/// - fragment (string): the fragment name -/// - id (string): the fragment subpart id -/// - margin (length, none): the margin around the fragment -/// -> anchor: the anchor position around the fragment -#let fragment-anchor(ctx, cetz-ctx, angle, fragment, id, margin: none) = { - let angle = angles.angle-correction(angle) - let fragment-margin = if margin == none { - ctx.config.fragment-margin - } else { - margin - } - fragment-margin = convert-length(cetz-ctx, fragment-margin) - let (cetz-ctx, center) = cetz.coordinate.resolve( - cetz-ctx, - (name: fragment, anchor: (id, "mid")), - ) - let (a, b) = if angles.angle-in-range(angle, 0deg, 90deg) { - anchor-north-east(cetz-ctx, center, fragment-margin, fragment, id) - } else if angles.angle-in-range(angle, 90deg, 180deg) { - anchor-north-west(cetz-ctx, center, fragment-margin, fragment, id) - } else if angles.angle-in-range(angle, 180deg, 270deg) { - anchor-south-west(cetz-ctx, center, fragment-margin, fragment, id) - } else { - anchor-south-east(cetz-ctx, center, fragment-margin, fragment, id) - } - - // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ - let angle = if angles.angle-in-range-inclusive(angle, 0deg, 90deg) or angles.angle-in-range-strict( - angle, - 270deg, - 360deg, - ) { - calc.atan(calc.tan(angle) * a / b) - } else { - calc.atan(calc.tan(angle) * a / b) - 180deg - } - - - if a == 0 or b == 0 { - panic("Ellipse " + ellipse + " has no width or height") - } - (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) -} - -/// Return the index to choose if the link connection is not overridden -#let link-fragment-index(angle, end, count, vertical) = { - if not end { - if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { - 0 - } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { - 0 - } else { - count - } - } else { - if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { - count - } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { - count - } else { - 0 - } - } -} - -#let fragment-link-anchor(name, id, count) = { - if count <= id { - panic("The last fragment only has " + str(count) + " connections") - } - if id == -1 { - id = count - 1 - } - (name: name, anchor: (str(id), "mid")) -} - -#let link-fragment-anchor(name: none, id, count) = { - if id >= count { - panic("This fragment only has " + str(count) + " anchors") - } - if id == -1 { - panic("The index of the fragment to link to must be defined") - } - if name == none { - (name: str(id), anchor: "mid") - } else { - (name: name, anchor: (str(id), "mid")) - } -} - - -#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { - let to-pos = (name: link.to-name, anchor: "mid") - if link.to == none or link.from == none { - let angle = link.at( - "angle", - default: angles.angle-between(cetz-ctx, link.from-pos, to-pos), - ) - link.angle = angle - if link.from == none { - link.from = link-fragment-index( - angle, - false, - ctx.hooks.at(link.from-name).count - 1, - ctx.hooks.at(link.from-name).vertical, - ) - } - if link.to == none { - link.to = link-fragment-index( - angle, - true, - ctx.hooks.at(link.to-name).count - 1, - ctx.hooks.at(link.to-name).vertical, - ) - } - } - if link.from == -1 { - link.from = 0 - } - if link.to == -1 { - link.to = ctx.hooks.at(link.to-name).count - 1 - } - let start = fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) - let end = fragment-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) - ((start, end), angles.angle-between(cetz-ctx, start, end)) -} - -#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { - if link.to == none { - let angle = angles.angle-correction( - angles.angle-between( - cetz-ctx, - link.from-pos, - (name: link.to-name, anchor: "mid"), - ), - ) - link.to = link-fragment-index( - angle, - true, - ctx.hooks.at(link.to-name).count - 1, - ctx.hooks.at(link.to-name).vertical, - ) - link.angle = angle - } else if "angle" not in link { - link.angle = angles.angle-correction( - angles.angle-between( - cetz-ctx, - link.from-pos, - (name: link.to-name, anchor: (str(link.to), "mid")), - ), - ) - } - let end-anchor = fragment-anchor( - ctx, - cetz-ctx, - link.angle + 180deg, - link.to-name, - str(link.to), - ) - ( - ( - link.from-pos, - end-anchor, - ), - angles.angle-between(cetz-ctx, link.from-pos, end-anchor), - ) -} - -#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { - ( - ( - fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), - link.name + "-end-anchor", - ), - link.angle, - ) -} - -#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { - let hook = ctx.hooks.at(link.to-name) - let angle = angles.angle-correction(angles.angle-between(cetz-ctx, link.from-pos, hook.hook)) - let from = link-fragment-index( - angle, - false, - ctx.hooks.at(link.from-name).count - 1, - ctx.hooks.at(link.from-name).vertical, - ) - let start-anchor = fragment-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) - ( - ( - start-anchor, - hook.hook, - ), - angles.angle-between(cetz-ctx, start-anchor, hook.hook), - ) -} - -#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { - let hook = ctx.hooks.at(link.to-name) - ( - ( - link.from-pos, - hook.hook, - ), - angles.angle-between(cetz-ctx, link.from-pos, hook.hook), - ) -} - -#let calculate-link-link-anchors(link) = { - ((link.from-pos, link.name + "-end-anchor"), link.angle) -} - -#let calculate-link-anchors(ctx, cetz-ctx, link) = { - if link.type == "mol-hook-link" { - calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) - } else if link.type == "link-hook-link" { - calculate-link-hook-link-anchors(ctx, cetz-ctx, link) - } else if link.to-name != none and link.from-name != none { - calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) - } else if link.to-name != none { - calculate-link-mol-anchors(ctx, cetz-ctx, link) - } else if link.from-name != none { - calculate-mol-link-anchors(ctx, cetz-ctx, link) - } else { - calculate-link-link-anchors(link) - } -} - diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/angles.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/angles.typ deleted file mode 100644 index 2b19a3d0b3..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/angles.typ +++ /dev/null @@ -1,98 +0,0 @@ -#import "@preview/cetz:0.4.1" - -/// Convert any angle to an angle between 0deg and 360deg -#let angle-correction(a) = { - if type(a) == angle { - a = a.deg() - } - if type(a) != float and type(a) != int { - panic("angle-correction: The angle must be a number or an angle") - } - while a < 0 { - a += 360 - } - - calc.rem(a, 360) * 1deg -} - -/// Check if the angle is in the range [from, to[ -/// -/// - angle (float, int, angle): The angle to check -/// - from (float): The start of the range -/// - to (float): The end of the range -/// -> true if the angle is in the range -#let angle-in-range(angle, from, to) = { - if to < from { - panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") - } - angle = angle-correction(angle) - angle >= from and angle < to -} - -/// Check if the angle is in the range ]from, to[ -/// -/// - angle (float, int, angle): The angle to check -/// - from (float): The start of the range -/// - to (float): The end of the range -/// -> true if the angle is in the range -#let angle-in-range-strict(angle, from, to) = { - if to < from { - panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") - } - angle = angle-correction(angle) - angle > from and angle < to -} - -/// Check if the angle is in the range [from, to] -/// -/// - angle (float, int, angle): The angle to check -/// - from (float): The start of the range -/// - to (float): The end of the range -/// -> true if the angle is in the range -#let angle-in-range-inclusive(angle, from, to) = { - if to < from { - panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") - } - angle = angle-correction(angle) - angle >= from and angle <= to -} - -/// get the angle between two anchors -/// -/// - ctx (cetz-ctx): The cetz context -/// - from (anchor): The first anchor -/// - to (anchor): The second anchor -#let angle-between(ctx, from, to) = { - let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) - let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) - let angle = calc.atan2(to-x - from-x, to-y - from-y) - angle -} - -/// Calculate the angle from an object with "relative", "absolute" or "angle" key -/// according to the alchemist context (see the manual to see how angles are calculated) -/// -/// - ctx (alchemist-ctx): the alchemist context -/// - object (dict): the object to get the angle from -/// - default (angle): the default angle -/// -> the calculated angle -#let angle-from-ctx(ctx, object, default) = { - if "relative" in object { - object.at("relative") + ctx.relative-angle - } else if "absolute" in object { - object.at("absolute") - } else if "angle" in object { - object.at("angle") * ctx.config.angle-increment - } else { - default - } -} - -/// Overwrite the offset based on the angle -#let angle-override(angle, ctx) = { - if ctx.in-cycle { - ("offset": "left") - } else { - (:) - } -} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/context.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/context.typ deleted file mode 100644 index 4021bb5177..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/context.typ +++ /dev/null @@ -1,33 +0,0 @@ -#let update-parent-context(parent-ctx, ctx) = { - let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { - ( - ..parent-ctx.last-anchor, - drew: true, - ) - } else { - parent-ctx.last-anchor - } - ( - ..parent-ctx, - last-anchor: last-anchor, - hooks: ctx.hooks, - hooks-links: ctx.hooks-links, - links: ctx.links, - ) -} - -/// Set the last anchor in the context to the given anchor and save it if needed -#let set-last-anchor(ctx, anchor) = { - if ctx.last-anchor.type == "link" { - let drew = ctx.last-anchor.at("drew", default: false) - if drew and anchor.type == "link" and anchor.name == ctx.last-anchor.name { - drew = false - let _ = ctx.links.pop() - } - if not drew { - ctx.links.push(ctx.last-anchor) - } - ctx.last-anchor.drew = true - } - (..ctx, last-anchor: anchor) -} diff --git a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/utils.typ b/packages/preview/alchemist/0.1.7/0.1.7/src/utils/utils.typ deleted file mode 100644 index bac944c011..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/src/utils/utils.typ +++ /dev/null @@ -1,75 +0,0 @@ -#import "@preview/cetz:0.4.1" - -#let convert-length(ctx, num) = { - // This function come from the cetz module - return if type(num) == length { - float(num.to-absolute() / ctx.length) - } else if type(num) == ratio { - num - } else { - float(num) - } -} - -/// get the distance between two anchors -#let distance-between(ctx, from, to) = { - let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) - let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) - let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( - to-y - from-y, - 2, - )) - distance -} - -/// merge two imbricated dictionaries together -/// The second dictionary is the default value if the key is not present in the first dictionary -#let merge-dictionaries(dict1, default) = { - let result = default - for (key, value) in dict1 { - if type(value) == dictionary { - result.insert(key, merge-dictionaries(value, default.at(key))) - } else { - result.insert(key, value) - } - } - result -} - - -/// get the type of an element by its name -/// -/// - body (drawable): the chemfig body of a molecule -/// - name (string): the name of the element to get the type -/// -> string -#let get-element-type(body, name) = { - for element in body { - if type(element) != dictionary { - continue - } - if "name" in element and element.name == name { - return element.type - } - if element.type == "branch" or element.type == "cycle" or element.type == "parenthesis" { - let type = get-element-type(element.body, name) - if type != none { - return type - } - } - } - none -} - -/// Calculate the height of a bounding box -/// - bounds (dictionary): the bounding box -/// -> float -#let bounding-box-height(bounds) = { - calc.abs(bounds.high.at(1) - bounds.low.at(1)) -} - -/// Calculate the width of a bounding box -/// - bounds (dictionary): the bounding box -/// -> float -#let bounding-box-width(bounds) = { - calc.abs(bounds.high.at(0) - bounds.low.at(0)) -} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.7/0.1.7/typst.toml b/packages/preview/alchemist/0.1.7/0.1.7/typst.toml deleted file mode 100644 index e272238a75..0000000000 --- a/packages/preview/alchemist/0.1.7/0.1.7/typst.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "alchemist" -version = "0.1.7" -entrypoint = "lib.typ" -authors = ["Robotechnic <@Robotechnic>"] -license = "MIT" -compiler = "0.13.1" -repository = "https://github.com/Typsium/alchemist" -description = "A package to render skeletal formulas using CeTZ" -exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] -categories = ["visualization", "paper"] -disciplines = ["education", "chemistry", "biology"] -keywords = ["chemistry", "chemical", "formula", "molecule", "chemiviz", "structure", "organic", "lewis", "bond", "aromatic"] From b95161b25399da13c7059b088ab4568acfcab67f Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Wed, 13 Aug 2025 16:28:27 +0200 Subject: [PATCH 14/24] udated README alchemist version --- packages/preview/alchemist/0.1.7/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/preview/alchemist/0.1.7/README.md b/packages/preview/alchemist/0.1.7/README.md index c968af64b7..8b93e09160 100644 --- a/packages/preview/alchemist/0.1.7/README.md +++ b/packages/preview/alchemist/0.1.7/README.md @@ -78,7 +78,7 @@ Alchemist uses cetz to draw the molecules. This means that you can draw cetz sha To start using alchemist, just use the following code: ```typ -#import "@preview/alchemist:0.1.4": * +#import "@preview/alchemist:0.1.7": * #skeletize({ // Your molecule here @@ -95,7 +95,7 @@ For more information, check the [manual](https://raw.githubusercontent.com/Robot - Added default values for `color` and `font` for `fragment` elements - Added a `skeletize-config` function to create a `skeletize` function with a specific configuration - Fixed a bug with cetz anchors not being correctly translated -- Added an `over` argument to links to allow hide overlapped links +- Added an `over` argument to links to allow hiding overlapped links ### 0.1.6 From 9955e76c0d8f73dfd6891056b762a848943b50e7 Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Thu, 21 Aug 2025 16:51:41 +0200 Subject: [PATCH 15/24] Upload alchemist v0.1.8 --- packages/preview/alchemist/0.1.8/LICENSE | 22 + packages/preview/alchemist/0.1.8/README.md | 138 ++++++ packages/preview/alchemist/0.1.8/lib.typ | 255 +++++++++++ .../alchemist/0.1.8/src/cetz/process.typ | 91 ++++ .../preview/alchemist/0.1.8/src/default.typ | 61 +++ .../preview/alchemist/0.1.8/src/drawer.typ | 404 ++++++++++++++++++ .../alchemist/0.1.8/src/drawer/branch.typ | 32 ++ .../alchemist/0.1.8/src/drawer/cram.typ | 61 +++ .../alchemist/0.1.8/src/drawer/cycle.typ | 155 +++++++ .../alchemist/0.1.8/src/drawer/fragment.typ | 150 +++++++ .../alchemist/0.1.8/src/drawer/hook.typ | 14 + .../alchemist/0.1.8/src/drawer/link.typ | 103 +++++ .../alchemist/0.1.8/src/drawer/operator.typ | 36 ++ .../0.1.8/src/drawer/parenthesis.typ | 215 ++++++++++ .../alchemist/0.1.8/src/elements/fragment.typ | 74 ++++ .../alchemist/0.1.8/src/elements/lewis.typ | 133 ++++++ .../alchemist/0.1.8/src/elements/links.typ | 314 ++++++++++++++ .../alchemist/0.1.8/src/utils/anchors.typ | 296 +++++++++++++ .../alchemist/0.1.8/src/utils/angles.typ | 98 +++++ .../alchemist/0.1.8/src/utils/context.typ | 33 ++ .../alchemist/0.1.8/src/utils/utils.typ | 75 ++++ packages/preview/alchemist/0.1.8/typst.toml | 13 + 22 files changed, 2773 insertions(+) create mode 100644 packages/preview/alchemist/0.1.8/LICENSE create mode 100644 packages/preview/alchemist/0.1.8/README.md create mode 100644 packages/preview/alchemist/0.1.8/lib.typ create mode 100644 packages/preview/alchemist/0.1.8/src/cetz/process.typ create mode 100644 packages/preview/alchemist/0.1.8/src/default.typ create mode 100644 packages/preview/alchemist/0.1.8/src/drawer.typ create mode 100644 packages/preview/alchemist/0.1.8/src/drawer/branch.typ create mode 100644 packages/preview/alchemist/0.1.8/src/drawer/cram.typ create mode 100644 packages/preview/alchemist/0.1.8/src/drawer/cycle.typ create mode 100644 packages/preview/alchemist/0.1.8/src/drawer/fragment.typ create mode 100644 packages/preview/alchemist/0.1.8/src/drawer/hook.typ create mode 100644 packages/preview/alchemist/0.1.8/src/drawer/link.typ create mode 100644 packages/preview/alchemist/0.1.8/src/drawer/operator.typ create mode 100644 packages/preview/alchemist/0.1.8/src/drawer/parenthesis.typ create mode 100644 packages/preview/alchemist/0.1.8/src/elements/fragment.typ create mode 100644 packages/preview/alchemist/0.1.8/src/elements/lewis.typ create mode 100644 packages/preview/alchemist/0.1.8/src/elements/links.typ create mode 100644 packages/preview/alchemist/0.1.8/src/utils/anchors.typ create mode 100644 packages/preview/alchemist/0.1.8/src/utils/angles.typ create mode 100644 packages/preview/alchemist/0.1.8/src/utils/context.typ create mode 100644 packages/preview/alchemist/0.1.8/src/utils/utils.typ create mode 100644 packages/preview/alchemist/0.1.8/typst.toml diff --git a/packages/preview/alchemist/0.1.8/LICENSE b/packages/preview/alchemist/0.1.8/LICENSE new file mode 100644 index 0000000000..a85204e590 --- /dev/null +++ b/packages/preview/alchemist/0.1.8/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.8/README.md b/packages/preview/alchemist/0.1.8/README.md new file mode 100644 index 0000000000..12dbc526fe --- /dev/null +++ b/packages/preview/alchemist/0.1.8/README.md @@ -0,0 +1,138 @@ +[![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FTypsium%2Falchemist%2Fmaster%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://github.com/Typsium/alchemist) +[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/alchemist/blob/master/LICENSE) +[![User Manual](https://img.shields.io/badge/manual-.pdf-purple)](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf) + +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + fragment(name: "A", "A") + single() + fragment("B") + branch({ + single(angle: 1) + fragment( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + fragment(name: "X", "X") + }) + branch({ + single(angle: -1) + fragment("Y") + single() + fragment( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + fragment( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic1/ref/1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + fragment("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic2/ref/1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.1.8": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). + +## Changelog + +### 0.1.8 + +- Fixed bus introduced in 0.1.7 + +### 0.1.7 + +- Updated cetz to version 0.4.1 +- Added default values for `color` and `font` for `fragment` elements +- Added a `skeletize-config` function to create a `skeletize` function with a specific configuration +- Fixed a bug with cetz anchors not being correctly translated +- Added an `over` argument to links to allow hiding overlapped links + +### 0.1.6 + +- Fixed the parenthesis height to work with the new typst version +- Renamed `molecule` into `fragment` +- Added support for charges and apostrophes in the string of a molecule +- Fixed the parenthesis auto positioning and alignment +- Added a new operator element and a new parameter in parenthesis to write resonance formulae + +### 0.1.5 + +- Update to compiler 0.13.1 and Cetz 0.3.4 + +### 0.1.4 + +- Added the possibility to create Lewis formulae +- Added parenthesis element to create groups and polymer + +### 0.1.3 + +- Added the possibility to add exponent in the string of a molecule. + +### 0.1.2 + +- Added default values for link style properties. +- Updated `cetz` to version 0.3.1. +- Added a `tip-length` argument to dashed cram links. + +### 0.1.1 + +- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. +- Fixed multiples bugs that causes overdraws of links. + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.1.8/lib.typ b/packages/preview/alchemist/0.1.8/lib.typ new file mode 100644 index 0000000000..7d1f5c1a64 --- /dev/null +++ b/packages/preview/alchemist/0.1.8/lib.typ @@ -0,0 +1,255 @@ +#import "@preview/cetz:0.4.1" +#import "src/default.typ": default +#import "src/utils/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ": skeletize, draw-skeleton, skeletize-config, draw-skeleton-config +#import "src/elements/links.typ": * +#import "src/elements/fragment.typ": * +#import "src/elements/lewis.typ": * + +#let transparent = color.rgb(100%, 0, 0, 0) + +/// === Fragment function +/// Build a fragment group based on mol +/// Each fragment is represented as an optional count followed by a fragment name +/// starting by a capital letter followed by an optional exponent followed by an optional indice. +/// #example(``` +/// #skeletize({ +/// fragment("H_2O") +/// }) +///```) +/// #example(``` +/// #skeletize({ +/// fragment("H^A_EF^5_4") +/// }) +/// ```) +/// It is possible to use an equation as a fragment. In this case, the splitting of the equation uses the same rules as in the string case. However, you get some advantages over the string version: +/// - You can use parenthesis to group elements together. +/// - You have no restriction about what you can put in exponent or indice. +/// #example(``` +/// #skeletize({ +/// fragment($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the fragment. It is used as the cetz name of the fragment and to link other fragments to it. +/// - links (dictionary): The links between this fragment and previous fragments or hooks. The key is the name of the fragment or hook and the value is the link function. See @links. +/// +/// Note that the atom-sep and angle arguments are ignored +/// - lewis (list): The list of lewis structures to draw around the fragments. See @lewis +/// - mol (string, equation): The string representing the fragment or an equation of the fragment +/// - vertical (boolean): If true, the fragment is drawn vertically +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", vertical: true) +/// }) +///```) +/// - colors (color|list): The color of the fragment. If a list is provided, it colors each group of the fragment with the corresponding color from right to left. If the number of colors is less than the number of groups, the last color is used for the remaining groups. If the number of colors is greater than the number of groups, the extra colors are ignored. +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", colors: (red, green, blue)) +/// single() +/// fragment("EFGH", colors: (orange)) +/// }) +/// ```) +/// -> drawable +#let fragment(name: none, links: (:), lewis: (), vertical: false, colors: none, mol) = { + let atoms = if type(mol) == str { + split-string(mol) + } else if mol.func() == math.equation { + split-equation(mol, equation: true) + } else { + panic("Invalid fragment content") + } + + if type(lewis) != array { + panic("Lewis formulae elements must be in a list") + } + + ( + ( + type: "fragment", + name: name, + atoms: atoms, + colors: colors, + links: links, + lewis: lewis, + vertical: vertical, + count: atoms.len(), + ), + ) +} +#let molecule(name: none, links: (:), lewis: (), vertical: false, mol) = fragment(name: name, links: links, lewis: lewis, vertical: vertical, mol) + +/// === Hooks +/// Create a hook in the fragment. It allows to connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the fragment. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current fragment, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// branch({ +/// single(angle:1) +/// fragment("B") +/// }) +/// branch({ +/// double(angle: -1) +/// fragment("D") +/// }) +/// single() +/// double() +/// single() +/// fragment("C") +/// }) +///```) +/// - body (drawable): the body of the branch. It must start with a link. +/// -> drawable +#let branch(body, ..args) = { + if args.pos().len() != 0 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", body: body, args: args.named()),) +} + +/// Create a regular cycle of fragments +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +/// - faces (int): the number of faces of the cycle +/// - body (drawable): the body of the cycle. It must start and end with a fragment or a link. +/// -> drawable +#let cycle(faces, body, ..args) = { + if args.pos().len() != 0 { + panic("Cycle takes two positional arguments: number of faces and body") + } + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + body: body, + args: args.named(), + ), + ) +} + +/// === Parenthesis +/// Encapsulate a drawable between two parenthesis. The left parenthesis is placed at the left of the first element of the body and by default the right parenthesis is placed at the right of the last element of the body. +/// +/// #example(``` +/// #skeletize( +/// config: ( +/// angle-increment: 30deg +/// ), { +/// parenthesis( +/// l:"[", r:"]", +/// br: $n$, { +/// single(angle: 1) +/// single(angle: -1) +/// single(angle: 1) +/// }) +/// }) +/// ```) +/// For more examples, see @examples +/// +/// - body (drawable): the body of the parenthesis. It must start and end with a fragment or a link. +/// - l (string): the left parenthesis +/// - r (string): the right parenthesis +/// - align (true): if true, the parenthesis will have the same y position. They will also get sized and aligned according to the body height. If false, they are not aligned and the height argument must be specified. +/// - resonance (boolean): if true, the parenthesis will be drawn in resonance mode. This means that the left and right parenthesis will be placed outside the molecule. Also, the parenthesis will be separated from the previous and next molecule. This can be true only if the parenthesis is the first element of the skeletal formula or if the previous element is an operator. See @resonance for more details. +/// - height (float, length): the height of the parenthesis. If align is true, this argument is optional. +/// - yoffset (float, length, list): the vertical offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - xoffset (float, length, list): the horizontal offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - right (string): Sometime, it is not convenient to place the right parenthesis at the end of the body. In this case, you can specify the name of the fragment or link where the right parenthesis should be placed. It is especially useful when the body end by a cycle. See @polySulfide +/// - tr (content): the exponent content of the right parenthesis +/// - br (content): the indice content of the right parenthesis +/// -> drawable +#let parenthesis(body, l: "(", r: ")", align: true, resonance: false, height: none, yoffset: none, xoffset: none, right: none, tr: none, br: none) = { + if l.len() > 2 { + panic("Left can be at most 2 characters") + } + let l = eval(l, mode: "math") + if r.len() > 2 { + panic("Right can be at most 2 characters") + } + let r = eval(r, mode: "math") + ( + ( + type: "parenthesis", + body: body, + calc: calc, + l: l, + r: r, + align: align, + tr: tr, + br: br, + height: height, + xoffset: xoffset, + yoffset: yoffset, + right: right, + resonance: resonance, + ), + ) +} + + +/// === Operator +/// Create an operator between two fragments. Creating an operator "reset" the placement of the next fragment. +/// This allow to add multiple molecules in the same skeletal formula. Without this, the next fragment would be placed at the end of the previous one. +/// An important point is that you can't use previous hooks to link two molecules separate by an operator. +/// This element is used in resonance structures (@resonance) and in some cases to put multiples molecules in the same skeletal formula (as you can set op to none). +/// +/// - op (content | string | none): The operator content. It can be a string or a content. A none value won't display anything. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// operator($->$, margin: 1em) +/// fragment("B") +/// }) +/// ```) +/// See @resonance for more examples. +/// - name (string): The name of the operator. +/// - margin (float, length): The margin between the operator and previous / next molecule. +/// -> drawable +#let operator(name: none, margin: 1em, op) = { + ( + ( + type: "operator", + name: name, + op: op, + margin: margin, + ), + ) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.8/src/cetz/process.typ b/packages/preview/alchemist/0.1.8/src/cetz/process.typ new file mode 100644 index 0000000000..decb454d2c --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/cetz/process.typ @@ -0,0 +1,91 @@ +// Temporary custom process.typ file to override default behavior + +#import "@preview/cetz:0.4.1": util, path-util, vector, drawable, process.aabb + + +/// Processes an element's function to get its drawables and bounds. Returns a {{dictionary}} with the key-values: `ctx` The modified context object, `bounds` The {{aabb}} of the element's drawables, `drawables` An {{array}} of the element's {{drawable}}s. +/// +/// - ctx (ctx): The current context object. +/// - element-func (function): A function that when passed {{ctx}}, it should return an element dictionary. +#let element(ctx, element-func) = { + let bounds = none + let element + let anchors = () + + (ctx, ..element,) = element-func(ctx) + if "drawables" in element { + if type(element.drawables) == dictionary { + element.drawables = (element.drawables,) + } + for drawable in element.drawables { + if drawable.bounds { + bounds = aabb.aabb( + if drawable.type == "path" { + path-util.bounds(drawable.segments) + } else if drawable.type == "content" { + let (x, y, _, w, h,) = drawable.pos + (drawable.width, drawable.height) + ((x + w / 2, y - h / 2, 0.0), (x - w / 2, y + h / 2, 0.0)) + }, + init: bounds + ) + } + } + } + + let name = element.at("name", default: none) + if name != none { + assert.eq(type(name), str, + message: "Element name must be a string") + assert(not name.contains("."), + message: "Invalid name for element '" + element.name + "'; name must not contain '.'") + + if "anchors" in element { + anchors.push(name) + ctx.nodes.insert(name, element) + if ctx.groups.len() > 0 { + ctx.groups.last().push(name) + } + } + } + + if ctx.debug and bounds != none { + element.drawables.push(drawable.path( + ((bounds.low, true, ( + ("l", (bounds.high.at(0), bounds.low.at(1), 0)), + ("l", bounds.high), + ("l", (bounds.low.at(0), bounds.high.at(1), 0)))),), + stroke: red + )) + } + + return ( + ctx: ctx, + bounds: bounds, + drawables: element.at("drawables", default: ()), + anchors: anchors + ) +} + +/// Runs the `element` function for a list of element functions and aggregates the results. +/// - ctx (ctx): The current context object. +/// - body (array): The array of element functions to process. +/// -> dictionary +#let many(ctx, body) = { + let drawables = () + let bounds = none + let anchors = () + + for el in body { + let r = element(ctx, el) + if r != none { + if r.bounds != none { + let pts = (r.bounds.low, r.bounds.high,) + bounds = aabb.aabb(pts, init: bounds) + } + ctx = r.ctx + drawables += r.drawables + anchors += r.anchors + } + } + return (ctx: ctx, bounds: bounds, drawables: drawables, anchors: anchors) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.8/src/default.typ b/packages/preview/alchemist/0.1.8/src/default.typ new file mode 100644 index 0000000000..a5d0222af3 --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/default.typ @@ -0,0 +1,61 @@ +#let default = ( + atom-sep: 3em, + fragment-margin: 0.2em, + fragment-font: none, + fragment-color: none, + link-over-radius: .2, + angle-increment: 45deg, + base-angle: 0deg, + debug: false, + single: ( + stroke: black + ), + double: ( + gap: .25em, + offset: "center", + offset-coeff: 0.85, + stroke: black + ), + triple: ( + gap: .25em, + stroke: black + ), + filled-cram: ( + stroke: none, + fill: black, + base-length: .8em + ), + dashed-cram: ( + stroke: black + .05em, + dash-gap: .3em, + base-length: .8em, + tip-length: .1em + ), + lewis: ( + angle: 0deg, + radius: 0.2em, + ), + lewis-single: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + offset: "top" + ), + lewis-double: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + ), + lewis-line: ( + stroke: black, + length: .7em, + ), + lewis-rectangle: ( + stroke: .08em + black, + fill: white, + height: .7em, + width: .3em + ) +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.8/src/drawer.typ b/packages/preview/alchemist/0.1.8/src/drawer.typ new file mode 100644 index 0000000000..115448c140 --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/drawer.typ @@ -0,0 +1,404 @@ +#import "default.typ": default +#import "@preview/cetz:0.4.1" +#import "cetz/process.typ" as custom-process +#import "utils/utils.typ": * +#import "utils/anchors.typ": * +#import "utils/utils.typ" as utils +#import "drawer/fragment.typ" as fragment +#import "drawer/link.typ" as link +#import "drawer/branch.typ" as branch +#import "drawer/cycle.typ" as cycle +#import "drawer/parenthesis.typ" as parenthesis +#import "drawer/hook.typ" as hook +#import "drawer/operator.typ" as operator + +#import cetz.draw: * + +#let default-anchor = (type: "coord", anchor: (0, 0)) + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + last-name: "", // last name used to draw + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + id: 0, // an id used to name things with an unique name + // branch + first-branch: false, // true if the next element is the first in a branch + // cycle + first-fragment: none, // name of the first fragment in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices +) + +#let draw-hooks-links(links, mol-name, ctx, from-mol) = { + let hook-id = 0 + for (to-name, (link,)) in links { + if link.at(mol-name, default: none) == none { + link.name = mol-name + "-hook-" + str(hook-id) + hook-id += 1 + } + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "fragment" { + ctx.links.push(( + type: "link", + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + from: none, + to: none, + over: link.at("over", default: none), + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + over: link.at("over", default: none), + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + } + ctx +} + +#let draw-fragments-and-link(ctx, body) = { + let fragment-drawing = () + let cetz-drawing = () + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + let drawing = () + let cetz-rec = () + if type(element) == function { + cetz-drawing.push(element) + } else if "type" not in element { + panic("Element " + repr(element) + " has no type") + } else if element.type == "fragment" { + (ctx, drawing) = fragment.draw-fragment(element, ctx) + } else if element.type == "link" { + (ctx, drawing) = link.draw-link(element, ctx) + } else if element.type == "branch" { + (ctx, drawing, cetz-rec) = branch.draw-branch(element, ctx, draw-fragments-and-link) + } else if element.type == "cycle" { + (ctx, drawing, cetz-rec) = cycle.draw-cycle(element, ctx, draw-fragments-and-link) + } else if element.type == "hook" { + ctx = hook.draw-hook(element, ctx) + } else if element.type == "parenthesis" { + (ctx, drawing, cetz-rec) = parenthesis.draw-parenthesis( + element, + ctx, + draw-fragments-and-link, + ) + } else if element.type == "operator" { + (ctx, drawing) = operator.draw-operator(element, fragment-drawing, ctx) + } else { + panic("Unknown element type " + element.type) + } + fragment-drawing += drawing + cetz-drawing += cetz-rec + } + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + ctx.last-anchor.drew = true + } + ( + ctx, + fragment-drawing, + cetz-drawing, + ) +} + +#let draw-link-over(ctx, link, over, angle) = { + let name = link.name + "-over" + let link-over-radius = ctx.config.link-over-radius + let (over, length, radius) = if type(over) == str { + (over, link-over-radius, link-over-radius) + } else if type(over) == dictionary { + let name = over.at("name", default: none) + if name == none { + panic("Over argument must have a name") + } + ( + name, + over.at("length", default: link-over-radius), + over.at("radius", default: link-over-radius) + ) + } else { + panic("Over must be a string or a dictionary, got " + type(link.at("over"))) + } + intersections(name, over, link.name) + let color = if ctx.config.debug { + red + } else { + white + } + scope({ + rotate(angle) + rect( + anchor: "center", + (to: name + ".0", rel: (-length/2,-radius/2)), + (rel: (length, radius)), + fill: color, + stroke: color + ) + }) +} + +#let draw-link-decoration(ctx) = { + ( + ctx, + get-ctx(cetz-ctx => { + for link in ctx.links { + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + if link.at("over") != none { + if type(link.at("over")) == array { + for over in link.at("over") { + draw-link-over(ctx, link, over, angle) + } + } else { + draw-link-over(ctx, link, link.at("over"), angle) + } + } + + scope({ + set-origin(from) + rotate(angle) + (link.draw)(length, ctx, cetz-ctx, override: link.override) + }) + } + }), + ) +} + +/// set elements names and split the molecule into sub-groups +#let preprocessing(body, group-id: 0, link-id: 0, operator-id: 0) = { + let result = ((),) + for element in body { + if type(element) == dictionary { + if element.at("name", default: none) == none { + if element.type == "fragment" { + element.name = "fragment-" + str(group-id) + group-id += 1 + } else if element.type == "link" { + element.name = "link-" + str(link-id) + link-id += 1 + } else if element.type == "operator" { + element.name = "operator-" + str(operator-id) + operator-id += 1 + } + } + if element.at("body", default: none) != none { + let child-body + (child-body, group-id, link-id, operator-id) = preprocessing( + element.body, + group-id: group-id, + link-id: link-id, + operator-id: operator-id, + ) + if element.type == "parenthesis" and element.resonance { + element.body = child-body + } else { + element.body = child-body.at(0) + } + } + if element.type == "operator" or (element.type == "parenthesis" and element.resonance) { + result.push(element) + result.push(()) + } else { + result.at(-1).push(element) + } + } else { + result.at(-1).push(element) + } + } + (result, group-id, link-id, operator-id) +} + +#let operator-group-name = id => { + "operator-group-" + str(id) +} + +#let draw-groups(ctx, bodies, after-operator: false) = { + ctx.last-name = "" + + let drawables = for (i, body) in bodies.enumerate() { + if type(body) == array { + if body.len() > 0 { + ctx.last-name = operator-group-name(i) + get-ctx(cetz-ctx => { + let ctx = ctx + if ctx.last-anchor.type == "coord" { + (cetz-ctx, ctx.last-anchor.anchor) = cetz.coordinate.resolve(cetz-ctx, ctx.last-anchor.anchor) + } + let last-anchor = ctx.last-anchor + let (ctx, atoms, cetz-drawing) = draw-fragments-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + + let molecule = { + atoms + } + + let (ctx: cetz-ctx, drawables, bounds: molecule-bounds, anchors) = custom-process.many(cetz-ctx, molecule) + molecule-bounds = cetz.util.revert-transform(cetz-ctx.transform, molecule-bounds) + + let (translate-x, translate-y) = if after-operator { + let (_, origin-anchor) = cetz.coordinate.resolve(cetz-ctx, last-anchor.anchor) + ( + origin-anchor.at(0) - molecule-bounds.low.at(0), + -origin-anchor.at(1) + + ( + molecule-bounds.low.at(1) + molecule-bounds.high.at(1) + ) + / 2, + ) + } else { + (0, 0) + } + let transform-matrix = cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ) + // panic(anchors) + for name in anchors { + let hold-anchors = cetz-ctx.nodes.at(name).anchors + cetz-ctx.nodes.at(name).anchors = (name) => { + if name != () { + cetz.matrix.mul4x4-vec3(transform-matrix, hold-anchors(name)) + } else { + hold-anchors(name) + } + } + } + ( + _ => ( + ctx: cetz-ctx, + drawables: cetz.drawable.apply-transform( + cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ), + drawables, + ), + ), + ) + scope({ + translate(x: translate-x, y: -translate-y) + draw-link-decoration(ctx).at(1) + on-layer(2, cetz-drawing) + let bound-rect = cetz.draw.rect( + molecule-bounds.low, + molecule-bounds.high, + name: ctx.last-name, + stroke: purple, + ) + if ctx.config.debug { + bound-rect + } else { + cetz.draw.hide(bound-rect) + } + }) + }) + } + } else if body.type == "operator" { + let (op-ctx, drawable) = operator.draw-operator(body, ctx) + after-operator = true + ctx = op-ctx + drawable + } else if body.type == "parenthesis" { + let (parenthesis-ctx, drawable) = parenthesis.draw-resonance-parenthesis(body, draw-groups, ctx) + ctx = parenthesis-ctx + drawable + } else { + panic("Unexpected element type: " + body.type) + } + } + (drawables, ctx) +} + +#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { + let config = merge-dictionaries(config, default) + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + + let bodies = preprocessing(body).at(0) + let (final-drawing, ctx) = draw-groups(ctx, bodies) + + if name == none { + final-drawing + } else { + group(name: name, anchor: mol-anchor, { + anchor("default", (0, 0)) + final-drawing + }) + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas(debug: debug, background: background, draw-skeleton(config: config, body)) +} + +#let skeletize-config(default-config) = { + let config-function(debug: false, background: none, config: (:), body) = { + skeletize(debug: debug, background: background, config: merge-dictionaries(config, default-config), body) + } + config-function +} + +#let draw-skeleton-config(default-config) = { + let config-function(config: (:), name: none, mol-anchor: none, body) = { + draw-skeleton(config: merge-dictionaries(config, default-config), body) + } + config-function +} diff --git a/packages/preview/alchemist/0.1.8/src/drawer/branch.typ b/packages/preview/alchemist/0.1.8/src/drawer/branch.typ new file mode 100644 index 0000000000..2c9e062843 --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/drawer/branch.typ @@ -0,0 +1,32 @@ +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +/// Compute the angle the branch should take in order to be "in the center" +/// of the cycle angle +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +#let draw-branch(branch, ctx, draw-fragments-and-link) = { + let angle = angles.angle-from-ctx(ctx, branch.args, cycle-angle(ctx)) + let (branch-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + branch.body, + ) + ctx = context_.update-parent-context(ctx, branch-ctx) + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.8/src/drawer/cram.typ b/packages/preview/alchemist/0.1.8/src/drawer/cram.typ new file mode 100644 index 0000000000..c66f6c127d --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/drawer/cram.typ @@ -0,0 +1,61 @@ +#import "@preview/cetz:0.4.1" +#import "../utils/utils.typ" + +/// Draw a triangle between two fragments +#let cram(from, to, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.filled-cram.base-length), + ) + cetz.draw.line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: ctx.config.filled-cram.stroke), + fill: args.at("fill", default: ctx.config.filled-cram.fill), + ) +} + +/// Draw a dashed triangle between two molecules fragments +#let dashed-cram(from, to, length, ctx, cetz-ctx, args) = { + import cetz.draw: * + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.dashed-cram.base-length), + ) + let tip-length = utils.convert-length( + cetz-ctx, + args.at("tip-length", default: ctx.config.dashed-cram.tip-length), + ) + hide({ + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - tip-length / 2)) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + tip-length / 2), + ) + }) + let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) + let dash-gap = utils.convert-length(cetz-ctx, args.at("dash-gap", default: ctx.config.dashed-cram.dash-gap)) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(cetz-ctx, dash-width) + let length = utils.convert-length(cetz-ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.8/src/drawer/cycle.typ b/packages/preview/alchemist/0.1.8/src/drawer/cycle.typ new file mode 100644 index 0000000000..9a81f12930 --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/drawer/cycle.typ @@ -0,0 +1,155 @@ +#import "@preview/cetz:0.4.1" +#import "../utils/utils.typ" +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +#let max-int = 9223372036854775807 + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + 0, + )) + } + vertex +} + +/// extrapolate the center and the radius of the cycle +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + let debug = () + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + debug += cetz.draw.circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius, debug) +} + +#let draw-cycle-center-arc(ctx, name, center-arc) = { + import cetz.draw: * + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius, debug) = cycle-center-radius(ctx, cetz-ctx, vertex) + debug + if name != none { + anchor(name, center) + } + if center-arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= center-arc.at("radius", default: 0.7) + } else { + min-radius *= center-arc.at("radius", default: 0.5) + } + let start = center-arc.at("start", default: 0deg) + let end = center-arc.at("end", default: 360deg) + let delta = center-arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + arc( + center, + ..center-arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) +} + +#let draw-cycle(cycle, ctx, draw-fragments-and-link) = { + let cycle-step-angle = 360deg / cycle.faces + let angle = angles.angle-from-ctx(ctx, cycle.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ( + ctx.relative-angle == 0deg + and ctx.angle == 0deg + and not cycle.args.at( + "align", + default: false, + ) + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-fragment = none + if ctx.last-anchor.type == "fragment" { + first-fragment = ctx.last-anchor.name + if first-fragment not in ctx.hooks { + ctx.hooks.insert(first-fragment, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in cycle.args { + name = cycle.args.at("name") + record-vertex = true + } else if "arc" in cycle.args { + record-vertex = true + } + let (cycle-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: cycle.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-fragment: first-fragment, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + cycle.body, + ) + ctx = context_.update-parent-context(ctx, cycle-ctx) + if record-vertex { + drawing += draw-cycle-center-arc(cycle-ctx, name, cycle.args.at("arc", default: none)) + } + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.8/src/drawer/fragment.typ b/packages/preview/alchemist/0.1.8/src/drawer/fragment.typ new file mode 100644 index 0000000000..a93960a07e --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/drawer/fragment.typ @@ -0,0 +1,150 @@ +#import "../utils/context.typ": * +#import "../utils/anchors.typ": * +#import "@preview/cetz:0.4.1" + +#let draw-fragment-text(ctx, mol, pos) = { + import cetz.draw: * + for (id, eq) in mol.atoms.enumerate() { + let name = str(id) + let color = if mol.colors != none { + if type(mol.colors) == color { + mol.colors + } else { + mol.colors.at(calc.min(id, mol.colors.len() - 1)) + } + } else { + ctx.config.fragment-color + } + let fragment-font = ctx.config.fragment-font + + // draw atoms of the group one after the other from left to right + content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "mid-west" + }, + ( + if id == 0 { + pos + } else if mol.vertical { + (to: str(id - 1) + ".south", rel: (0, -.2em)) + } else { + str(id - 1) + ".mid-east" + } + ), + auto-scale: false, + { + show math.equation: math.upright + set text(fill: color) if color != none + set text(font: fragment-font) if fragment-font != none + eq + }, + ) + id += 1 + } +} + +#let draw-fragment-lewis(ctx, group-name, count, lewis) = { + if lewis.len() == 0 { + return () + } + import cetz.draw: * + get-ctx(cetz-ctx => { + for (id, (angle: lewis-angle, radius, draw)) in lewis.enumerate() { + if (lewis-angle == none) { + lewis-angle = ctx.config.lewis.angle + } + if (radius == none) { + radius = ctx.config.lewis.radius + } + let lewis-angle = angles.angle-correction(lewis-angle) + let mol-id = if angles.angle-in-range-inclusive(lewis-angle, 90deg, 270deg) { + 0 + } else { + count - 1 + } + let anchor = fragment-anchor( + ctx, + cetz-ctx, + lewis-angle, + group-name, + str(mol-id), + margin: radius, + ) + scope({ + set-origin(anchor) + rotate(lewis-angle) + draw(ctx, cetz-ctx) + }) + } + }) +} + +#let draw-fragment-elements(mol, ctx) = { + let name = mol.name + if name in ctx.hooks { + panic("Molecule fragment with name " + name + " already exists : " + ctx.hooks.keys().join(", ")) + } + ctx.hooks.insert(name, mol) + + let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("west", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-fragment-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let group-anchor = link-fragment-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (group-anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule fragment must be linked to a coord or a link") + } + ctx = context_.set-last-anchor( + ctx, + (type: "fragment", name: name, count: mol.at("count"), vertical: mol.vertical), + ) + if (side) { + ctx.id += 1 + } + ( + ctx, + { + import cetz.draw: * + group( + anchor: if side { + group-anchor + } else { + "from" + str(ctx.id) + }, + name: name, + { + anchor("default", coord) + draw-fragment-text(ctx, mol, coord) + if not side { + anchor("from" + str(ctx.id), group-anchor) + } + }, + ) + draw-fragment-lewis(ctx, name, mol.count, mol.at("lewis")) + }, + ) +} + +#let draw-fragment(element, ctx) = { + if ctx.first-branch { + panic("A molecule fragment can not be the first element in a cycle") + } + let (ctx, drawing) = draw-fragment-elements(element, ctx) + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + (ctx, drawing) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.8/src/drawer/hook.typ b/packages/preview/alchemist/0.1.8/src/drawer/hook.typ new file mode 100644 index 0000000000..1c5dafb9d1 --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/drawer/hook.typ @@ -0,0 +1,14 @@ + +#let draw-hook(hook, ctx) = { + if hook.name in ctx.hooks { + panic("Hook " + hook.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + ctx +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.8/src/drawer/link.typ b/packages/preview/alchemist/0.1.8/src/drawer/link.typ new file mode 100644 index 0000000000..d6c0c0876a --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/drawer/link.typ @@ -0,0 +1,103 @@ +#import "../utils/angles.typ" +#import "../utils/anchors.typ": * + +#let create-link-decorations-anchors(link, ctx) = { + let link-angle = 0deg + let to-name = none + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-fragment != none { + to-name = ctx.first-fragment + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angles.angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = angles.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angles.angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let from-pos = if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "fragment" { + from-connection = link-fragment-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + fragment-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name") + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = context_.set-last-anchor( + ctx, + ( + type: "link", + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: to-name, + to: to-connection, + angle: link-angle, + over: link.at("over", default: none), + draw: link.draw, + ), + ) + ( + ctx, + { + hide({ + circle(name: link-name + "-start-anchor", from-pos, radius: .25em) + }) + let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + line(from-pos, end-anchor, stroke: blue + .1em) + } + hide( + { + circle(name: link-name + "-end-anchor", end-anchor, radius: .25em) + }, + bounds: true, + ) + }, + ) +} + +#let draw-link(link, ctx) = { + ctx.first-branch = false + let drawing + (ctx, drawing) = create-link-decorations-anchors(link, ctx) + ctx.faces-count += 1 + if link.links.len() != 0 { + ctx.hooks-links.push((link.links, ctx.last-anchor.name, false)) + } + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.8/src/drawer/operator.typ b/packages/preview/alchemist/0.1.8/src/drawer/operator.typ new file mode 100644 index 0000000000..ff1fd2792b --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/drawer/operator.typ @@ -0,0 +1,36 @@ +#import "@preview/cetz:0.4.1": draw, process, util + +#let draw-operator(operator, ctx) = { + import draw: * + + let op-name = operator.name + ctx.last-anchor = (type: "coord", anchor: (rel: (operator.margin, 0), to: (name: op-name, anchor: "east"))) + + ( + ctx, + get-ctx(cetz-ctx => { + + let west-previous-mol-anchor = (name: ctx.last-name, anchor: "east") + + let east-op-anchor = (rel: (operator.margin, 0), to: west-previous-mol-anchor) + + let op = if (operator.op == none) { + "" + } else { + operator.op + } + + content( + name: op-name, + anchor: "west", + east-op-anchor, + op, + ) + + if (ctx.config.debug) { + circle(west-previous-mol-anchor, radius: 0.05, fill: yellow, stroke: none) + circle(ctx.last-anchor.anchor, radius: 0.05, fill: yellow, stroke: none) + } + }), + ) +} diff --git a/packages/preview/alchemist/0.1.8/src/drawer/parenthesis.typ b/packages/preview/alchemist/0.1.8/src/drawer/parenthesis.typ new file mode 100644 index 0000000000..75d6b07a0a --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/drawer/parenthesis.typ @@ -0,0 +1,215 @@ +#import "../utils/utils.typ" +#import "@preview/cetz:0.4.1" + +#let left-parenthesis-anchor(parenthesis, ctx) = { + let anchor = if parenthesis.body.at(0).type == "fragment" { + let name = parenthesis.body.at(0).name + parenthesis.body.at(0).name = name + (name: name, anchor: "west") + } else if parenthesis.body.at(0).type == "link" { + let name = parenthesis.body.at(0).at("name") + parenthesis.body.at(0).name = name + (name + "-start-anchor", 45%, name + "-end-anchor") + } else if not left { } else { + panic("The first element of a parenthesis must be a molecule fragment or a link") + } + (ctx, parenthesis, anchor) +} + +#let right-parenthesis-anchor(parenthesis, ctx) = { + let right-name = parenthesis.at("right") + let right-type = "" + if right-name != none { + right-type = utils.get-element-type(parenthesis.body, right-name) + if right-type == none { + panic("The right element of the parenthesis does not exist") + } + } else { + right-type = parenthesis.body.at(-1).type + right-name = parenthesis.body.at(-1).at("name", default: none) + } + + let anchor = if right-type == "fragment" { + (name: right-name, anchor: "east") + } else if right-type == "link" { + (right-name + "-start-anchor", 55%, right-name + "-end-anchor") + } else { + panic("The last element of a parenthesis must be a molecule fragment or a link but got " + right-type) + } + (ctx, parenthesis, anchor) +} + +#let parenthesis-content(parenthesis, height, cetz-ctx) = { + let block = block(height: height * cetz-ctx.length * 1.2, width: 0pt) + let left-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($parenthesis.l block$, size: 100%) + } + let right-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($block parenthesis.r$, size: 100%) + } + + let right-parenthesis-with-attach = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.attach(right-parenthesis, br: parenthesis.br, tr: parenthesis.tr) + } + + (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) +} + +#let draw-parenthesis(parenthesis, ctx, draw-molecules-and-link) = { + let (parenthesis-ctx, drawing, cetz-rec) = draw-molecules-and-link( + ctx, + parenthesis.body, + ) + ctx = parenthesis-ctx + + + drawing += { + import cetz.draw: * + get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + { + drawing + }, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + + let sub-height = utils.bounding-box-height(sub-bounds) + let sub-v-mid = sub-bounds.low.at(1) - sub-height / 2 + + let sub-width = utils.bounding-box-width(sub-bounds) + + let (ctx, parenthesis, left-anchor) = left-parenthesis-anchor(parenthesis, ctx) + + let (ctx, parenthesis, right-anchor) = right-parenthesis-anchor(parenthesis, ctx) + + let height = parenthesis.at("height") + if height == none { + if not parenthesis.align { + panic("You must specify the height of the parenthesis if they are not aligned") + } + height = sub-height + } else { + height = utils.convert-length(cetz-ctx, height) + } + + let (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) = parenthesis-content( + parenthesis, + height, + cetz-ctx, + ) + + let (_, (lx, ly, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, left-anchor) + let (_, (rx, ry, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, right-anchor) + + if ctx.config.debug { + circle((lx, ly), radius: 1pt, fill: orange, stroke: orange) + circle((rx, ry), radius: 1pt, fill: orange, stroke: orange) + rect((lx, sub-bounds.low.at(1)), (rx, sub-bounds.high.at(1)), stroke: green) + line((lx, sub-v-mid), (rx, sub-v-mid), stroke: green) + } + + let hoffset = calc.abs(sub-width - calc.abs(rx - lx)) + + if parenthesis.align { + ly -= ly - sub-v-mid + ry = ly + } else if type(parenthesis.yoffset) == array { + if parenthesis.yoffset.len() != 2 { + panic("The parenthesis yoffset must be a list of length 2 or a number") + } + ly += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(0)) + ry += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(1)) + } else if parenthesis.yoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.yoffset) + ly += offset + ry += offset + } + + if type(parenthesis.xoffset) == array { + if parenthesis.xoffset.len() != 2 { + panic("The parenthesis xoffset must be a list of length 2 or a number") + } + lx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(0)) + rx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(1)) + } else if parenthesis.xoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.xoffset) + lx -= offset + rx += offset + } + + let right-bounds = cetz.process.many(cetz-ctx, content((0, 0), right-parenthesis, auto-scale: false)).bounds + let right-with-attach-bounds = cetz + .process + .many(cetz-ctx, content((0, 0), right-parenthesis-with-attach, auto-scale: false)) + .bounds + let right-voffset = calc.abs(right-bounds.low.at(1) - right-with-attach-bounds.low.at(1)) + if parenthesis.tr != none and parenthesis.br != none { + right-voffset /= 2 + } else if (parenthesis.tr != none) { + right-voffset *= -1 + } + + ( + _ => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + content((lx, ly), anchor: "mid-east", left-parenthesis, auto-scale: false) + content((rx, ry - right-voffset), anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }) + } + + (ctx, drawing, cetz-rec) +} + + +#let draw-resonance-parenthesis(parenthesis, draw-function, ctx) = { + import cetz.draw: * + let left-name = "parenthesis-" + str(ctx.id) + let right-name = "parenthesis-" + str(ctx.id + 1) + ctx.id += 2 + let last-anchor = ctx.last-anchor + let (drawing, ctx) = draw-function(ctx, parenthesis.body, after-operator: true) + + let drawing = get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + drawing, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + let height = parenthesis.height + if height == none { + height = utils.bounding-box-height(sub-bounds) + } else { + height = utils.convert-length(cetz-ctx, height) + } + let width = utils.bounding-box-width(sub-bounds) + + let (left-parenthesis, _, right-parenthesis-with-attach) = parenthesis-content(parenthesis, height, cetz-ctx) + + let right-anchor = (rel: (width, 0), to: (name: left-name, anchor: "east")) + on-layer( + 1, + { + content(last-anchor.anchor, name: left-name, anchor: "mid-east", left-parenthesis, auto-scale: false) + content(right-anchor, name: right-name, anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }, + ) + ( + ctx => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + }) + + ctx.last-anchor = (type: "coord", anchor: (name: right-name, anchor: "mid-east")) + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.8/src/elements/fragment.typ b/packages/preview/alchemist/0.1.8/src/elements/fragment.typ new file mode 100644 index 0000000000..e7b66eb50f --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/elements/fragment.typ @@ -0,0 +1,74 @@ +#let split-equation(mol, equation: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + for m in mol { + let last-number-hold = last-number + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + result.push(m) + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in fragment fragment") + } + last-number = true + result.push(m) + } else { + panic("Invalid fragment fragment content") + } + } else if m.func() == math.attach or m.func() == math.lr { + result.push(m) + } else if m == [ ] { + continue + } else { + panic("Invalid fragment fragment content") + } + if last-number-hold { + result.at(-2) = result.at(-2) + result.at(-1) + let _ = result.pop() + last-number = false + } + } + result +} + +#let fragment-cor-regex = "[0-9]*[A-Z][a-z]*'*" +#let exponent-regex = "((?:[0-9]+(?:\\+|\\-)?)|[A-Z]|\\+|\\-)" +#let exponent-base-regex = "(?:(\\^|_)" + exponent-regex + ")?(?:(\\^|_)" + exponent-regex + ")?" +#let fragment-regex = regex("^ *(" + fragment-cor-regex + ")" + exponent-base-regex) + +#let split-string(mol) = { + let aux(str) = { + let match = str.match(fragment-regex) + if match == none { + panic(str + " is not a valid fragment") + } else if match.captures.at(1) == match.captures.at(3) and match.captures.at(1) != none { + panic("You cannot use an exponent and a subscript twice") + } + let eq = "\"" + match.captures.at(0) + "\"" + if match.captures.at(1) != none { + eq += match.captures.at(1) + "(" + match.captures.at(2) + ")" + } + if match.captures.at(3) != none { + eq += match.captures.at(3) + "(" + match.captures.at(4) + ")" + } + let eq = math.equation(eval(eq, mode: "math")) + (eq, match.end) + } + + while not mol.len() == 0 { + let (eq, end) = aux(mol) + mol = mol.slice(end) + (eq,) + } +} diff --git a/packages/preview/alchemist/0.1.8/src/elements/lewis.typ b/packages/preview/alchemist/0.1.8/src/elements/lewis.typ new file mode 100644 index 0000000000..fb0d3547bc --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/elements/lewis.typ @@ -0,0 +1,133 @@ +#import "@preview/cetz:0.4.1" + +/// Create a lewis function that is then used to draw a lewis +/// formulae element around the fragment +/// +/// - draw-function (function): The function that will be used to draw the lewis element. It should takes three arguments: the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-lewis(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Lewis function takes no positional arguments") + } + let args = args.named() + let angle = args.at("angle", default: none) + let radius = args.at("radius", default: none) + ( + angle: angle, + radius: radius, + draw: (ctx, cetz-ctx) => draw-function(ctx, cetz-ctx, args) + ) + } +} + +/// draw a sigle electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// +/// The position of the electron is set by the `offset` argument. Available values are: +/// - "top": the electron is placed above the fragment center line +/// - "bottom": the electron is placed below the fragment center line +/// - "center": the electron is placed at the fragment center line +/// +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-single(offset: "top"), +/// )) +/// single(angle:-2) +/// fragment("B", lewis:( +/// lewis-single(offset: "bottom"), +/// )) +/// single(angle:-2) +/// fragment("C", lewis:( +/// lewis-single(offset: "center"), +/// )) +/// }) +/// ```) +#let lewis-single = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-single.radius) + let gap = args.at("gap", default: ctx.config.lewis-single.gap) + let offset = args.at("offset", default: ctx.config.lewis-single.offset) + let gap = if offset == "top" { + gap + } else if offset == "bottom" { + -gap + } else if offset != "center" { + panic("Invalid position, expected 'top', 'bottom' or 'center'") + } + let fill = args.at("fill", default: ctx.config.lewis-single.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-single.stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-double(), +/// lewis-double(angle: 90deg), +/// lewis-double(angle: 180deg), +/// lewis-double(angle: -90deg) +/// )) +/// }) +/// ```) +#let lewis-double = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-double.radius) + let gap = args.at("gap", default: ctx.config.lewis-double.gap) + let fill = args.at("fill", default: ctx.config.lewis-double.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-double.stroke) + circle((0, -gap), radius: radius, fill: fill, stroke: stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron liked by a single line +/// +/// It is possible to change the length of the line with the `lenght` argument. +/// It is also possible to change the `stroke` agument +/// #example(``` +/// #skeletize({ +/// fragment("B", lewis:( +/// lewis-line(angle: 45deg), +/// lewis-line(angle: 135deg), +/// lewis-line(angle: -45deg), +/// lewis-line(angle: -135deg) +/// )) +/// }) +/// ```) +#let lewis-line = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let length = args.at("length", default: ctx.config.lewis-line.length) + let stroke = args.at("stroke", default: ctx.config.lewis-line.stroke) + line((0, -length / 2), (0, length / 2), stroke: stroke) +}) + + +/// Draw a rectangle to denote a lone pair of electrons +/// +/// It is possible to change the height and width of the rectangle with the `height` and `width` arguments. +/// It is also possible to change the `fill` and `stroke` arguments +/// #example(``` +/// #skeletize({ +/// fragment("C", lewis:( +/// lewis-rectangle(), +/// lewis-rectangle(angle: 180deg) +/// )) +/// }) +/// ```) +#let lewis-rectangle = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let height = args.at("height", default: ctx.config.lewis-rectangle.height) + let width = args.at("width", default: ctx.config.lewis-rectangle.width) + let fill = args.at("fill", default: ctx.config.lewis-rectangle.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-rectangle.stroke) + rect((-width / 2, -height / 2), (width / 2, height / 2), fill: fill, stroke: stroke) +}) diff --git a/packages/preview/alchemist/0.1.8/src/elements/links.typ b/packages/preview/alchemist/0.1.8/src/elements/links.typ new file mode 100644 index 0000000000..a96321367a --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/elements/links.typ @@ -0,0 +1,314 @@ +#import "@preview/cetz:0.4.1" +#import "../drawer/cram.typ": * +#import "../utils/utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes four arguments: the length of the link, the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, cetz-ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, cetz-ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single(stroke: red + 5pt) +/// fragment("B") +/// }) +///```) +#let single = build-link((length, ctx, _, args) => { + import cetz.draw: * + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.single.stroke)) +}) + +/// Draw a double line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// fragment("B") +/// }) +///```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double(offset: "right") +/// fragment("B") +/// double(offset: "left") +/// fragment("C") +/// double(offset: "center") +/// fragment("D") +/// }) +///```) +#let double = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.double.gap)) / 2 + let offset = args.at("offset", default: ctx.config.double.offset) + let coeff = args.at("offset-coeff", default: ctx.config.double.offset-coeff) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + let gap-offset = if offset == "right" { + -gap + } else if offset == "left" { + gap + } else if offset == "center" { + 0 + } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, -gap + gap-offset), (x + length * coeff, -gap + gap-offset)) + } else { + ((0, -gap + gap-offset), (length, -gap + gap-offset)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, gap + gap-offset), (x + length * coeff, gap + gap-offset)) + } else { + ((0, gap + gap-offset), (length, gap + gap-offset)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) +}) + +/// Draw a triple line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// fragment("B") +/// }) +///```) +#let triple = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.triple.gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + line((0, -gap), (length, -gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + line((0, gap), (length, gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) +}) + +/// Draw a filled cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, cetz-ctx, args) => cram( + (0, 0), + (length, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a filled cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, cetz-ctx, args) => cram( + (length, 0), + (0, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a hollow cram between two fragments with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((0, 0), (length, 0), ctx, cetz-ctx, args) +}) + +/// Draw a hollow cram between two fragments with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((length, 0), (0, 0), ctx, cetz-ctx, args) +}) + +/// Draw a dashed cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the arrow base length with the `base-length` argument and the tip length with the `tip-length` argument. +// You can also change distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// tip-length: 1em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-right = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + cetz-ctx, + args, +)) + +/// Draw a dashed cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + cetz-ctx, + args, +)) + + +/// Draw a plus sign between two fragments +/// #example(```` +/// #skeletize({ +/// fragment("A") +/// plus() +/// fragment("B") +/// }) +/// ````) +/// You can change the filling, size and stroke of the glyph with the `fill`, `size` and `stoke` arguments. The default values are the parent `text` parameters. +#let plus = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + content( + anchor: "mid", + (length / 2, 0), + { + set text(fill: args.fill) if "fill" in args + set text(stroke: args.stroke) if "stroke" in args + set text(size: args.size) if "size" in args + text($+$) + } + ) +}) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.8/src/utils/anchors.typ b/packages/preview/alchemist/0.1.8/src/utils/anchors.typ new file mode 100644 index 0000000000..285efcde8c --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/utils/anchors.typ @@ -0,0 +1,296 @@ +#import "angles.typ" +#import "context.typ" as context_ +#import "utils.typ": * +#import "@preview/cetz:0.4.1" +#import cetz.draw: * + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +/// Calculate an anchor position around a fragment using an ellipse +/// at a given angle +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - cetz-ctx (cetz-ctx): the cetz context +/// - angle (float, int, angle): the angle of the anchor +/// - fragment (string): the fragment name +/// - id (string): the fragment subpart id +/// - margin (length, none): the margin around the fragment +/// -> anchor: the anchor position around the fragment +#let fragment-anchor(ctx, cetz-ctx, angle, fragment, id, margin: none) = { + let angle = angles.angle-correction(angle) + let fragment-margin = if margin == none { + ctx.config.fragment-margin + } else { + margin + } + fragment-margin = convert-length(cetz-ctx, fragment-margin) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "mid")), + ) + let (a, b) = if angles.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, fragment-margin, fragment, id) + } else { + anchor-south-east(cetz-ctx, center, fragment-margin, fragment, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if angles.angle-in-range-inclusive(angle, 0deg, 90deg) or angles.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +/// Return the index to choose if the link connection is not overridden +#let link-fragment-index(angle, end, count, vertical) = { + if not end { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let fragment-link-anchor(name, id, count) = { + if count <= id { + panic("The last fragment only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "mid")) +} + +#let link-fragment-anchor(name: none, id, count) = { + if id >= count { + panic("This fragment only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the fragment to link to must be defined") + } + if name == none { + (name: str(id), anchor: "mid") + } else { + (name: name, anchor: (str(id), "mid")) + } +} + + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "mid") + if link.to == none or link.from == none { + let angle = link.at( + "angle", + default: angles.angle-between(cetz-ctx, link.from-pos, to-pos), + ) + link.angle = angle + if link.from == none { + link.from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + if link.from == -1 { + link.from = 0 + } + if link.to == -1 { + link.to = ctx.hooks.at(link.to-name).count - 1 + } + let start = fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) + let end = fragment-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) + ((start, end), angles.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "mid"), + ), + ) + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "mid")), + ), + ) + } + let end-anchor = fragment-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + angles.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = angles.angle-correction(angles.angle-between(cetz-ctx, link.from-pos, hook.hook)) + let from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = fragment-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) + ( + ( + start-anchor, + hook.hook, + ), + angles.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + angles.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + diff --git a/packages/preview/alchemist/0.1.8/src/utils/angles.typ b/packages/preview/alchemist/0.1.8/src/utils/angles.typ new file mode 100644 index 0000000000..2b19a3d0b3 --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/utils/angles.typ @@ -0,0 +1,98 @@ +#import "@preview/cetz:0.4.1" + +/// Convert any angle to an angle between 0deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a += 360 + } + + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +/// Check if the angle is in the range ]from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +/// Check if the angle is in the range [from, to] +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +/// +/// - ctx (cetz-ctx): The cetz context +/// - from (anchor): The first anchor +/// - to (anchor): The second anchor +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// Calculate the angle from an object with "relative", "absolute" or "angle" key +/// according to the alchemist context (see the manual to see how angles are calculated) +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - object (dict): the object to get the angle from +/// - default (angle): the default angle +/// -> the calculated angle +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +/// Overwrite the offset based on the angle +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} diff --git a/packages/preview/alchemist/0.1.8/src/utils/context.typ b/packages/preview/alchemist/0.1.8/src/utils/context.typ new file mode 100644 index 0000000000..4021bb5177 --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/utils/context.typ @@ -0,0 +1,33 @@ +#let update-parent-context(parent-ctx, ctx) = { + let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { + ( + ..parent-ctx.last-anchor, + drew: true, + ) + } else { + parent-ctx.last-anchor + } + ( + ..parent-ctx, + last-anchor: last-anchor, + hooks: ctx.hooks, + hooks-links: ctx.hooks-links, + links: ctx.links, + ) +} + +/// Set the last anchor in the context to the given anchor and save it if needed +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" { + let drew = ctx.last-anchor.at("drew", default: false) + if drew and anchor.type == "link" and anchor.name == ctx.last-anchor.name { + drew = false + let _ = ctx.links.pop() + } + if not drew { + ctx.links.push(ctx.last-anchor) + } + ctx.last-anchor.drew = true + } + (..ctx, last-anchor: anchor) +} diff --git a/packages/preview/alchemist/0.1.8/src/utils/utils.typ b/packages/preview/alchemist/0.1.8/src/utils/utils.typ new file mode 100644 index 0000000000..bac944c011 --- /dev/null +++ b/packages/preview/alchemist/0.1.8/src/utils/utils.typ @@ -0,0 +1,75 @@ +#import "@preview/cetz:0.4.1" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + return if type(num) == length { + float(num.to-absolute() / ctx.length) + } else if type(num) == ratio { + num + } else { + float(num) + } +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} + +/// merge two imbricated dictionaries together +/// The second dictionary is the default value if the key is not present in the first dictionary +#let merge-dictionaries(dict1, default) = { + let result = default + for (key, value) in dict1 { + if type(value) == dictionary { + result.insert(key, merge-dictionaries(value, default.at(key))) + } else { + result.insert(key, value) + } + } + result +} + + +/// get the type of an element by its name +/// +/// - body (drawable): the chemfig body of a molecule +/// - name (string): the name of the element to get the type +/// -> string +#let get-element-type(body, name) = { + for element in body { + if type(element) != dictionary { + continue + } + if "name" in element and element.name == name { + return element.type + } + if element.type == "branch" or element.type == "cycle" or element.type == "parenthesis" { + let type = get-element-type(element.body, name) + if type != none { + return type + } + } + } + none +} + +/// Calculate the height of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-height(bounds) = { + calc.abs(bounds.high.at(1) - bounds.low.at(1)) +} + +/// Calculate the width of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-width(bounds) = { + calc.abs(bounds.high.at(0) - bounds.low.at(0)) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.8/typst.toml b/packages/preview/alchemist/0.1.8/typst.toml new file mode 100644 index 0000000000..7a54ec289f --- /dev/null +++ b/packages/preview/alchemist/0.1.8/typst.toml @@ -0,0 +1,13 @@ +[package] +name = "alchemist" +version = "0.1.8" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.13.1" +repository = "https://github.com/Typsium/alchemist" +description = "A package to render skeletal formulas using CeTZ" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization", "paper"] +disciplines = ["education", "chemistry", "biology"] +keywords = ["chemistry", "chemical", "formula", "molecule", "chemiviz", "structure", "organic", "lewis", "bond", "aromatic"] From 0c3aae79a748e7d8c5fa488c1db030617061ab58 Mon Sep 17 00:00:00 2001 From: Tom <45141234+Robotechnic@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:48:37 +0200 Subject: [PATCH 16/24] Update packages/preview/alchemist/0.1.8/README.md Co-authored-by: Ana Gelez --- packages/preview/alchemist/0.1.8/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preview/alchemist/0.1.8/README.md b/packages/preview/alchemist/0.1.8/README.md index 12dbc526fe..cd0cb65314 100644 --- a/packages/preview/alchemist/0.1.8/README.md +++ b/packages/preview/alchemist/0.1.8/README.md @@ -91,7 +91,7 @@ For more information, check the [manual](https://raw.githubusercontent.com/Robot ### 0.1.8 -- Fixed bus introduced in 0.1.7 +- Fixed bugs introduced in 0.1.7 ### 0.1.7 From eb6b3140025d823d656be0906e3246082cfdcd2c Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Sun, 15 Mar 2026 22:05:38 +0100 Subject: [PATCH 17/24] Upload alchemist v0.1.9 --- packages/preview/alchemist/0.1.9/LICENSE | 22 + packages/preview/alchemist/0.1.9/README.md | 148 ++++++ packages/preview/alchemist/0.1.9/lib.typ | 271 +++++++++++ .../alchemist/0.1.9/src/cetz/process.typ | 96 ++++ .../preview/alchemist/0.1.9/src/default.typ | 61 +++ .../preview/alchemist/0.1.9/src/drawer.typ | 430 ++++++++++++++++++ .../alchemist/0.1.9/src/drawer/branch.typ | 32 ++ .../alchemist/0.1.9/src/drawer/cram.typ | 61 +++ .../alchemist/0.1.9/src/drawer/cycle.typ | 155 +++++++ .../alchemist/0.1.9/src/drawer/fragment.typ | 150 ++++++ .../alchemist/0.1.9/src/drawer/hide.typ | 12 + .../alchemist/0.1.9/src/drawer/hook.typ | 14 + .../alchemist/0.1.9/src/drawer/link.typ | 104 +++++ .../alchemist/0.1.9/src/drawer/operator.typ | 36 ++ .../0.1.9/src/drawer/parenthesis.typ | 215 +++++++++ .../alchemist/0.1.9/src/elements/fragment.typ | 74 +++ .../alchemist/0.1.9/src/elements/lewis.typ | 135 ++++++ .../alchemist/0.1.9/src/elements/links.typ | 314 +++++++++++++ .../alchemist/0.1.9/src/utils/anchors.typ | 296 ++++++++++++ .../alchemist/0.1.9/src/utils/angles.typ | 98 ++++ .../alchemist/0.1.9/src/utils/context.typ | 33 ++ .../alchemist/0.1.9/src/utils/utils.typ | 75 +++ packages/preview/alchemist/0.1.9/typst.toml | 13 + 23 files changed, 2845 insertions(+) create mode 100644 packages/preview/alchemist/0.1.9/LICENSE create mode 100644 packages/preview/alchemist/0.1.9/README.md create mode 100644 packages/preview/alchemist/0.1.9/lib.typ create mode 100644 packages/preview/alchemist/0.1.9/src/cetz/process.typ create mode 100644 packages/preview/alchemist/0.1.9/src/default.typ create mode 100644 packages/preview/alchemist/0.1.9/src/drawer.typ create mode 100644 packages/preview/alchemist/0.1.9/src/drawer/branch.typ create mode 100644 packages/preview/alchemist/0.1.9/src/drawer/cram.typ create mode 100644 packages/preview/alchemist/0.1.9/src/drawer/cycle.typ create mode 100644 packages/preview/alchemist/0.1.9/src/drawer/fragment.typ create mode 100644 packages/preview/alchemist/0.1.9/src/drawer/hide.typ create mode 100644 packages/preview/alchemist/0.1.9/src/drawer/hook.typ create mode 100644 packages/preview/alchemist/0.1.9/src/drawer/link.typ create mode 100644 packages/preview/alchemist/0.1.9/src/drawer/operator.typ create mode 100644 packages/preview/alchemist/0.1.9/src/drawer/parenthesis.typ create mode 100644 packages/preview/alchemist/0.1.9/src/elements/fragment.typ create mode 100644 packages/preview/alchemist/0.1.9/src/elements/lewis.typ create mode 100644 packages/preview/alchemist/0.1.9/src/elements/links.typ create mode 100644 packages/preview/alchemist/0.1.9/src/utils/anchors.typ create mode 100644 packages/preview/alchemist/0.1.9/src/utils/angles.typ create mode 100644 packages/preview/alchemist/0.1.9/src/utils/context.typ create mode 100644 packages/preview/alchemist/0.1.9/src/utils/utils.typ create mode 100644 packages/preview/alchemist/0.1.9/typst.toml diff --git a/packages/preview/alchemist/0.1.9/LICENSE b/packages/preview/alchemist/0.1.9/LICENSE new file mode 100644 index 0000000000..a85204e590 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.9/README.md b/packages/preview/alchemist/0.1.9/README.md new file mode 100644 index 0000000000..f66953d15c --- /dev/null +++ b/packages/preview/alchemist/0.1.9/README.md @@ -0,0 +1,148 @@ +[![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FTypsium%2Falchemist%2Fmaster%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://github.com/Typsium/alchemist) +[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/alchemist/blob/master/LICENSE) +[![User Manual](https://img.shields.io/badge/manual-.pdf-purple)](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf) + +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + fragment(name: "A", "A") + single() + fragment("B") + branch({ + single(angle: 1) + fragment( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + fragment(name: "X", "X") + }) + branch({ + single(angle: -1) + fragment("Y") + single() + fragment( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + fragment( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic1/ref/1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + fragment("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic2/ref/1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.1.8": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). + +## Tests + +The test suite is managed with [tytanic](https://github.com/typst-community/tytanic). + +## Changelog + +### 0.1.9 + +- Updated cetz to version 0.4.2 +- Exposed name in lib.typ to be used by other packages +- Adds a touying support + +### 0.1.8 + +- Fixed bugs introduced in 0.1.7 + +### 0.1.7 + +- Updated cetz to version 0.4.1 +- Added default values for `color` and `font` for `fragment` elements +- Added a `skeletize-config` function to create a `skeletize` function with a specific configuration +- Fixed a bug with cetz anchors not being correctly translated +- Added an `over` argument to links to allow hiding overlapped links + +### 0.1.6 + +- Fixed the parenthesis height to work with the new typst version +- Renamed `molecule` into `fragment` +- Added support for charges and apostrophes in the string of a molecule +- Fixed the parenthesis auto positioning and alignment +- Added a new operator element and a new parameter in parenthesis to write resonance formulae + +### 0.1.5 + +- Update to compiler 0.13.1 and Cetz 0.3.4 + +### 0.1.4 + +- Added the possibility to create Lewis formulae +- Added parenthesis element to create groups and polymer + +### 0.1.3 + +- Added the possibility to add exponent in the string of a molecule. + +### 0.1.2 + +- Added default values for link style properties. +- Updated `cetz` to version 0.3.1. +- Added a `tip-length` argument to dashed cram links. + +### 0.1.1 + +- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. +- Fixed multiples bugs that causes overdraws of links. + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.1.9/lib.typ b/packages/preview/alchemist/0.1.9/lib.typ new file mode 100644 index 0000000000..b3ea5f5e07 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/lib.typ @@ -0,0 +1,271 @@ +#import "@preview/cetz:0.4.2" +#import "src/default.typ": default +#import "src/utils/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ": skeletize, draw-skeleton, skeletize-config, draw-skeleton-config, hide-drawables +#import "src/elements/links.typ": * +#import "src/elements/fragment.typ": * +#import "src/elements/lewis.typ": * + +#let transparent = color.rgb(100%, 0, 0, 0) +#let name = "alchemist" + +/// === Fragment function +/// Build a fragment group based on mol +/// Each fragment is represented as an optional count followed by a fragment name +/// starting by a capital letter followed by an optional exponent followed by an optional indice. +/// #example(``` +/// #skeletize({ +/// fragment("H_2O") +/// }) +///```) +/// #example(``` +/// #skeletize({ +/// fragment("H^A_EF^5_4") +/// }) +/// ```) +/// It is possible to use an equation as a fragment. In this case, the splitting of the equation uses the same rules as in the string case. However, you get some advantages over the string version: +/// - You can use parenthesis to group elements together. +/// - You have no restriction about what you can put in exponent or indice. +/// #example(``` +/// #skeletize({ +/// fragment($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the fragment. It is used as the cetz name of the fragment and to link other fragments to it. +/// - links (dictionary): The links between this fragment and previous fragments or hooks. The key is the name of the fragment or hook and the value is the link function. See @links. +/// +/// Note that the atom-sep and angle arguments are ignored +/// - lewis (list): The list of lewis structures to draw around the fragments. See @lewis +/// - mol (string, equation): The string representing the fragment or an equation of the fragment +/// - vertical (boolean): If true, the fragment is drawn vertically +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", vertical: true) +/// }) +///```) +/// - colors (color|list): The color of the fragment. If a list is provided, it colors each group of the fragment with the corresponding color from right to left. If the number of colors is less than the number of groups, the last color is used for the remaining groups. If the number of colors is greater than the number of groups, the extra colors are ignored. +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", colors: (red, green, blue)) +/// single() +/// fragment("EFGH", colors: (orange)) +/// }) +/// ```) +/// -> drawable +#let fragment(name: none, links: (:), lewis: (), vertical: false, colors: none, mol) = { + let atoms = if type(mol) == str { + split-string(mol) + } else if mol.func() == math.equation { + split-equation(mol, equation: true) + } else { + panic("Invalid fragment content") + } + + if type(lewis) != array { + panic("Lewis formulae elements must be in a list") + } + + ( + ( + type: "fragment", + name: name, + atoms: atoms, + colors: colors, + links: links, + lewis: lewis, + vertical: vertical, + count: atoms.len(), + ), + ) +} +#let molecule(name: none, links: (:), lewis: (), vertical: false, mol) = fragment(name: name, links: links, lewis: lewis, vertical: vertical, mol) + +/// === Hooks +/// Create a hook in the fragment. It allows to connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the fragment. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current fragment, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// branch({ +/// single(angle:1) +/// fragment("B") +/// }) +/// branch({ +/// double(angle: -1) +/// fragment("D") +/// }) +/// single() +/// double() +/// single() +/// fragment("C") +/// }) +///```) +/// - body (drawable): the body of the branch. It must start with a link. +/// -> drawable +#let branch(body, ..args) = { + if args.pos().len() != 0 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", body: body, args: args.named()),) +} + +/// Create a regular cycle of fragments +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +/// - faces (int): the number of faces of the cycle +/// - body (drawable): the body of the cycle. It must start and end with a fragment or a link. +/// -> drawable +#let cycle(faces, body, ..args) = { + if args.pos().len() != 0 { + panic("Cycle takes two positional arguments: number of faces and body") + } + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + body: body, + args: args.named(), + ), + ) +} + +/// === Parenthesis +/// Encapsulate a drawable between two parenthesis. The left parenthesis is placed at the left of the first element of the body and by default the right parenthesis is placed at the right of the last element of the body. +/// +/// #example(``` +/// #skeletize( +/// config: ( +/// angle-increment: 30deg +/// ), { +/// parenthesis( +/// l:"[", r:"]", +/// br: $n$, { +/// single(angle: 1) +/// single(angle: -1) +/// single(angle: 1) +/// }) +/// }) +/// ```) +/// For more examples, see @examples +/// +/// - body (drawable): the body of the parenthesis. It must start and end with a fragment or a link. +/// - l (string): the left parenthesis +/// - r (string): the right parenthesis +/// - align (true): if true, the parenthesis will have the same y position. They will also get sized and aligned according to the body height. If false, they are not aligned and the height argument must be specified. +/// - resonance (boolean): if true, the parenthesis will be drawn in resonance mode. This means that the left and right parenthesis will be placed outside the molecule. Also, the parenthesis will be separated from the previous and next molecule. This can be true only if the parenthesis is the first element of the skeletal formula or if the previous element is an operator. See @resonance for more details. +/// - height (float, length): the height of the parenthesis. If align is true, this argument is optional. +/// - yoffset (float, length, list): the vertical offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - xoffset (float, length, list): the horizontal offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - right (string): Sometime, it is not convenient to place the right parenthesis at the end of the body. In this case, you can specify the name of the fragment or link where the right parenthesis should be placed. It is especially useful when the body end by a cycle. See @polySulfide +/// - tr (content): the exponent content of the right parenthesis +/// - br (content): the indice content of the right parenthesis +/// -> drawable +#let parenthesis(body, l: "(", r: ")", align: true, resonance: false, height: none, yoffset: none, xoffset: none, right: none, tr: none, br: none) = { + if l.len() > 2 { + panic("Left can be at most 2 characters") + } + let l = eval(l, mode: "math") + if r.len() > 2 { + panic("Right can be at most 2 characters") + } + let r = eval(r, mode: "math") + ( + ( + type: "parenthesis", + body: body, + calc: calc, + l: l, + r: r, + align: align, + tr: tr, + br: br, + height: height, + xoffset: xoffset, + yoffset: yoffset, + right: right, + resonance: resonance, + ), + ) +} + + +/// === Operator +/// Create an operator between two fragments. Creating an operator "reset" the placement of the next fragment. +/// This allow to add multiple molecules in the same skeletal formula. Without this, the next fragment would be placed at the end of the previous one. +/// An important point is that you can't use previous hooks to link two molecules separate by an operator. +/// This element is used in resonance structures (@resonance) and in some cases to put multiples molecules in the same skeletal formula (as you can set op to none). +/// +/// - op (content | string | none): The operator content. It can be a string or a content. A none value won't display anything. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// operator($->$, margin: 1em) +/// fragment("B") +/// }) +/// ```) +/// See @resonance for more examples. +/// - name (string): The name of the operator. +/// - margin (float, length): The margin between the operator and previous / next molecule. +/// -> drawable +#let operator(name: none, margin: 1em, op) = { + ( + ( + type: "operator", + name: name, + op: op, + margin: margin, + ), + ) +} + +/// === Hiding part of the molecule +/// This element allows to hide part of the molecule. It can be used to hide the part of the molecule that is not relevant for the current discussion. It can also be used to create some animation effects by hiding and showing different parts of the molecule. Note that the hidden part is still present in the drawing and can be linked to other fragments. This means that you can hide a part of the molecule and still link it to other fragments or hooks. The hidden part is also still present in the cetz record, which means that you can still use it in the cetz drawing. The only thing that is hidden is the drawing of the hidden part. +/// - bounds (boolean): If true, the hidden part keep the same bounding box as if it was not hidden. If false, the hidden part doesn't take any space in the drawing. +/// - body (drawable): The body to hide. It can be any drawable. +/// -> drawable +#let hide(bounds: true, body) = { + ( + ( + type: "hide", + bounds: bounds, + body: body, + ), + ) +} diff --git a/packages/preview/alchemist/0.1.9/src/cetz/process.typ b/packages/preview/alchemist/0.1.9/src/cetz/process.typ new file mode 100644 index 0000000000..cb6e71b058 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/cetz/process.typ @@ -0,0 +1,96 @@ +// Temporary custom process.typ file to override default behavior + +#import "@preview/cetz:0.4.2": util, path-util, vector, drawable, process.aabb + + +/// Processes an element's function to get its drawables and bounds. Returns a {{dictionary}} with the key-values: `ctx` The modified context object, `bounds` The {{aabb}} of the element's drawables, `drawables` An {{array}} of the element's {{drawable}}s. +/// +/// - ctx (ctx): The current context object. +/// - element-func (function): A function that when passed {{ctx}}, it should return an element dictionary. +#let element(ctx, element-func) = { + let bounds = none + let element + let anchors = () + + (ctx, ..element,) = element-func(ctx) + if "drawables" in element { + if type(element.drawables) == dictionary { + element.drawables = (element.drawables,) + } + for drawable in drawable.filter-tagged(element.drawables, drawable.TAG.no-bounds) { + bounds = aabb.aabb( + if drawable.type == "path" { + path-util.bounds(drawable.segments) + } else if drawable.type == "content" { + let (x, y, _, w, h,) = drawable.pos + (drawable.width, drawable.height) + ((x + w / 2, y - h / 2, 0.0), (x - w / 2, y + h / 2, 0.0)) + }, + init: bounds + ) + } + } + + let name = element.at("name", default: none) + element.name = name + if name != none { + assert.eq(type(name), str, + message: "Element name must be a string") + assert(not name.contains("."), + message: "Invalid name for element '" + element.name + "'; name must not contain '.'") + + if "anchors" in element { + anchors.push(name) + ctx.nodes.insert(name, element) + if ctx.groups.len() > 0 { + ctx.groups.last().push(name) + } + } + } + + // Draw a debug bounding box. + if ctx.debug and bounds != none { + element.drawables.push(drawable.line-strip( + (bounds.low, + (bounds.high.at(0), bounds.low.at(1), 0.0), + bounds.high, + (bounds.low.at(0), bounds.high.at(1), 0.0) + ), + close: true, + stroke: red, + tags: (drawable.TAG.debug,))) + } + + return ( + ctx: ctx, + bounds: bounds, + drawables: element.at("drawables", default: ()), + element: element, + anchors: anchors + ) +} + +/// Runs the `element` function for a list of element functions and aggregates the results. +/// - ctx (ctx): The current context object. +/// - body (array): The array of element functions to process. +/// -> dictionary +#let many(ctx, body) = { + let drawables = () + let bounds = none + let elements = () + let anchors = () + + for el in body { + let r = element(ctx, el) + if r != none { + if r.bounds != none { + let pts = (r.bounds.low, r.bounds.high,) + bounds = aabb.aabb(pts, init: bounds) + } + ctx = r.ctx + drawables += r.drawables + anchors += r.anchors + } + elements.push(r.element) + } + return (ctx: ctx, bounds: bounds, drawables: drawables, elements: elements, anchors: anchors) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.9/src/default.typ b/packages/preview/alchemist/0.1.9/src/default.typ new file mode 100644 index 0000000000..a5d0222af3 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/default.typ @@ -0,0 +1,61 @@ +#let default = ( + atom-sep: 3em, + fragment-margin: 0.2em, + fragment-font: none, + fragment-color: none, + link-over-radius: .2, + angle-increment: 45deg, + base-angle: 0deg, + debug: false, + single: ( + stroke: black + ), + double: ( + gap: .25em, + offset: "center", + offset-coeff: 0.85, + stroke: black + ), + triple: ( + gap: .25em, + stroke: black + ), + filled-cram: ( + stroke: none, + fill: black, + base-length: .8em + ), + dashed-cram: ( + stroke: black + .05em, + dash-gap: .3em, + base-length: .8em, + tip-length: .1em + ), + lewis: ( + angle: 0deg, + radius: 0.2em, + ), + lewis-single: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + offset: "top" + ), + lewis-double: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + ), + lewis-line: ( + stroke: black, + length: .7em, + ), + lewis-rectangle: ( + stroke: .08em + black, + fill: white, + height: .7em, + width: .3em + ) +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.9/src/drawer.typ b/packages/preview/alchemist/0.1.9/src/drawer.typ new file mode 100644 index 0000000000..a90fae0293 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/drawer.typ @@ -0,0 +1,430 @@ +#import "default.typ": default +#import "@preview/cetz:0.4.2" +#import "cetz/process.typ" as custom-process +#import "utils/utils.typ": * +#import "utils/anchors.typ": * +#import "utils/utils.typ" as utils +#import "drawer/fragment.typ" as fragment +#import "drawer/link.typ" as link +#import "drawer/branch.typ" as branch +#import "drawer/cycle.typ" as cycle +#import "drawer/parenthesis.typ" as parenthesis +#import "drawer/hook.typ" as hook +#import "drawer/operator.typ" as operator +#import "drawer/hide.typ": draw-hide + +#import cetz.draw: * + +#let default-anchor = (type: "coord", anchor: (0, 0)) + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + last-name: "", // last name used to draw + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + id: 0, // an id used to name things with an unique name + // branch + first-branch: false, // true if the next element is the first in a branch + // cycle + first-fragment: none, // name of the first fragment in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices + hide: false, // true if the current elements should be hidden +) + +#let draw-hooks-links(links, mol-name, ctx, from-mol) = { + let hook-id = 0 + for (to-name, (link,)) in links { + if link.at(mol-name, default: none) == none { + link.name = mol-name + "-hook-" + str(hook-id) + hook-id += 1 + } + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "fragment" { + ctx.links.push(( + type: "link", + hide: ctx.hide, + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + from: none, + to: none, + over: link.at("over", default: none), + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + hide: ctx.hide, + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + over: link.at("over", default: none), + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + } + ctx +} + +#let draw-fragments-and-link(ctx, body) = { + let fragment-drawing = () + let cetz-drawing = () + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + let drawing = () + let cetz-rec = () + if type(element) == function { + cetz-drawing.push(element) + } else if type(element) == dictionary { + if "type" not in element { + panic("Element " + repr(element) + " has no type") + } else if element.type == "fragment" { + (ctx, drawing) = fragment.draw-fragment(element, ctx) + } else if element.type == "link" { + (ctx, drawing) = link.draw-link(element, ctx) + } else if element.type == "branch" { + (ctx, drawing, cetz-rec) = branch.draw-branch(element, ctx, draw-fragments-and-link) + } else if element.type == "cycle" { + (ctx, drawing, cetz-rec) = cycle.draw-cycle(element, ctx, draw-fragments-and-link) + } else if element.type == "hook" { + ctx = hook.draw-hook(element, ctx) + } else if element.type == "parenthesis" { + (ctx, drawing, cetz-rec) = parenthesis.draw-parenthesis( + element, + ctx, + draw-fragments-and-link, + ) + } else if element.type == "operator" { + (ctx, drawing) = operator.draw-operator(element, fragment-drawing, ctx) + } else if element.type == "hide" { + (ctx, drawing, cetz-rec) = draw-hide(element, ctx, draw-fragments-and-link) + } else { + panic("Unknown element type " + element.type) + } + } else if type(element) == type([]) { + if element.func() == metadata { + // ignore metadata + } else { + panic("Unexpected content element: " + repr(element)) + } + } else if element == none { + // ignore empty elements + } else { + panic("Unexpected element type: " + str(type(element)) + " with value " + repr(element)) + } + fragment-drawing += drawing + cetz-drawing += cetz-rec + } + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + ctx.last-anchor.drew = true + } + ( + ctx, + fragment-drawing, + cetz-drawing, + ) +} + +#let draw-link-over(ctx, link, over, angle) = { + let name = link.name + "-over" + let link-over-radius = ctx.config.link-over-radius + let (over, length, radius) = if type(over) == str { + (over, link-over-radius, link-over-radius) + } else if type(over) == dictionary { + let name = over.at("name", default: none) + if name == none { + panic("Over argument must have a name") + } + ( + name, + over.at("length", default: link-over-radius), + over.at("radius", default: link-over-radius) + ) + } else { + panic("Over must be a string or a dictionary, got " + type(link.at("over"))) + } + intersections(name, over, link.name) + let color = if ctx.config.debug { + red + } else { + white + } + scope({ + rotate(angle) + rect( + anchor: "center", + (to: name + ".0", rel: (-length/2,-radius/2)), + (rel: (length, radius)), + fill: color, + stroke: color + ) + }) +} + +#let draw-link-decoration(ctx) = { + ( + ctx, + get-ctx(cetz-ctx => { + for link in ctx.links { + if link.hide { + continue + } + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + if link.at("over") != none { + if type(link.at("over")) == array { + for over in link.at("over") { + draw-link-over(ctx, link, over, angle) + } + } else { + draw-link-over(ctx, link, link.at("over"), angle) + } + } + + scope({ + set-origin(from) + rotate(angle) + (link.draw)(length, ctx, cetz-ctx, override: link.override) + }) + } + }), + ) +} + +/// set elements names and split the molecule into sub-groups +#let preprocessing(body, group-id: 0, link-id: 0, operator-id: 0) = { + let result = ((),) + for element in body { + if type(element) == dictionary { + if element.at("name", default: none) == none { + if element.type == "fragment" { + element.name = "fragment-" + str(group-id) + group-id += 1 + } else if element.type == "link" { + element.name = "link-" + str(link-id) + link-id += 1 + } else if element.type == "operator" { + element.name = "operator-" + str(operator-id) + operator-id += 1 + } + } + if element.at("body", default: none) != none { + let child-body + (child-body, group-id, link-id, operator-id) = preprocessing( + element.body, + group-id: group-id, + link-id: link-id, + operator-id: operator-id, + ) + if element.type == "parenthesis" and element.resonance { + element.body = child-body + } else { + element.body = child-body.at(0) + } + } + if element.type == "operator" or (element.type == "parenthesis" and element.resonance) { + result.push(element) + result.push(()) + } else { + result.at(-1).push(element) + } + } else { + result.at(-1).push(element) + } + } + (result, group-id, link-id, operator-id) +} + +#let operator-group-name = id => { + "operator-group-" + str(id) +} + +#let draw-groups(ctx, bodies, after-operator: false) = { + ctx.last-name = "" + + let drawables = for (i, body) in bodies.enumerate() { + if type(body) == array { + if body.len() > 0 { + ctx.last-name = operator-group-name(i) + get-ctx(cetz-ctx => { + let ctx = ctx + if ctx.last-anchor.type == "coord" { + (cetz-ctx, ctx.last-anchor.anchor) = cetz.coordinate.resolve(cetz-ctx, ctx.last-anchor.anchor) + } + let last-anchor = ctx.last-anchor + let (ctx, atoms, cetz-drawing) = draw-fragments-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + + let molecule = { + atoms + } + + let (ctx: cetz-ctx, drawables, bounds: molecule-bounds, anchors) = custom-process.many(cetz-ctx, molecule) + molecule-bounds = cetz.util.revert-transform(cetz-ctx.transform, molecule-bounds) + + let (translate-x, translate-y) = if after-operator { + let (_, origin-anchor) = cetz.coordinate.resolve(cetz-ctx, last-anchor.anchor) + ( + origin-anchor.at(0) - molecule-bounds.low.at(0), + -origin-anchor.at(1) + + ( + molecule-bounds.low.at(1) + molecule-bounds.high.at(1) + ) + / 2, + ) + } else { + (0, 0) + } + let transform-matrix = cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ) + // panic(anchors) + for name in anchors { + let hold-anchors = cetz-ctx.nodes.at(name).anchors + cetz-ctx.nodes.at(name).anchors = (name) => { + if name != () { + cetz.matrix.mul4x4-vec3(transform-matrix, hold-anchors(name)) + } else { + hold-anchors(name) + } + } + } + ( + _ => ( + ctx: cetz-ctx, + drawables: cetz.drawable.apply-transform( + cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ), + drawables, + ), + ), + ) + scope({ + translate(x: translate-x, y: -translate-y) + draw-link-decoration(ctx).at(1) + on-layer(2, cetz-drawing) + let bound-rect = cetz.draw.rect( + molecule-bounds.low, + molecule-bounds.high, + name: ctx.last-name, + stroke: purple, + ) + if ctx.config.debug { + bound-rect + } else { + cetz.draw.hide(bound-rect) + } + }) + }) + } + } else if body.type == "operator" { + let (op-ctx, drawable) = operator.draw-operator(body, ctx) + after-operator = true + ctx = op-ctx + drawable + } else if body.type == "parenthesis" { + let (parenthesis-ctx, drawable) = parenthesis.draw-resonance-parenthesis(body, draw-groups, ctx) + ctx = parenthesis-ctx + drawable + } else { + panic("Unexpected element type: " + body.type) + } + } + (drawables, ctx) +} + +#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { + let config = merge-dictionaries(config, default) + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + + let bodies = preprocessing(body).at(0) + let (final-drawing, ctx) = draw-groups(ctx, bodies) + + if name == none { + final-drawing + } else { + group(name: name, anchor: mol-anchor, { + anchor("default", (0, 0)) + final-drawing + }) + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas(debug: debug, background: background, draw-skeleton(config: config, body)) +} + +#let skeletize-config(default-config) = { + let config-function(debug: false, background: none, config: (:), body) = { + skeletize(debug: debug, background: background, config: merge-dictionaries(config, default-config), body) + } + config-function +} + +#let draw-skeleton-config(default-config) = { + let config-function(config: (:), name: none, mol-anchor: none, body) = { + draw-skeleton(config: merge-dictionaries(config, default-config), name: name, mol-anchor: mol-anchor, body) + } + config-function +} + + +#let hide-drawables(elements) = { + return elements +} diff --git a/packages/preview/alchemist/0.1.9/src/drawer/branch.typ b/packages/preview/alchemist/0.1.9/src/drawer/branch.typ new file mode 100644 index 0000000000..2c9e062843 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/drawer/branch.typ @@ -0,0 +1,32 @@ +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +/// Compute the angle the branch should take in order to be "in the center" +/// of the cycle angle +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +#let draw-branch(branch, ctx, draw-fragments-and-link) = { + let angle = angles.angle-from-ctx(ctx, branch.args, cycle-angle(ctx)) + let (branch-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + branch.body, + ) + ctx = context_.update-parent-context(ctx, branch-ctx) + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.9/src/drawer/cram.typ b/packages/preview/alchemist/0.1.9/src/drawer/cram.typ new file mode 100644 index 0000000000..4b214842d8 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/drawer/cram.typ @@ -0,0 +1,61 @@ +#import "@preview/cetz:0.4.2" +#import "../utils/utils.typ" + +/// Draw a triangle between two fragments +#let cram(from, to, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.filled-cram.base-length), + ) + cetz.draw.line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: ctx.config.filled-cram.stroke), + fill: args.at("fill", default: ctx.config.filled-cram.fill), + ) +} + +/// Draw a dashed triangle between two molecules fragments +#let dashed-cram(from, to, length, ctx, cetz-ctx, args) = { + import cetz.draw: * + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.dashed-cram.base-length), + ) + let tip-length = utils.convert-length( + cetz-ctx, + args.at("tip-length", default: ctx.config.dashed-cram.tip-length), + ) + hide({ + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - tip-length / 2)) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + tip-length / 2), + ) + }) + let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) + let dash-gap = utils.convert-length(cetz-ctx, args.at("dash-gap", default: ctx.config.dashed-cram.dash-gap)) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(cetz-ctx, dash-width) + let length = utils.convert-length(cetz-ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.9/src/drawer/cycle.typ b/packages/preview/alchemist/0.1.9/src/drawer/cycle.typ new file mode 100644 index 0000000000..50596eb48c --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/drawer/cycle.typ @@ -0,0 +1,155 @@ +#import "@preview/cetz:0.4.2" +#import "../utils/utils.typ" +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +#let max-int = 9223372036854775807 + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + 0, + )) + } + vertex +} + +/// extrapolate the center and the radius of the cycle +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + let debug = () + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + debug += cetz.draw.circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius, debug) +} + +#let draw-cycle-center-arc(ctx, name, center-arc) = { + import cetz.draw: * + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius, debug) = cycle-center-radius(ctx, cetz-ctx, vertex) + debug + if name != none { + anchor(name, center) + } + if center-arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= center-arc.at("radius", default: 0.7) + } else { + min-radius *= center-arc.at("radius", default: 0.5) + } + let start = center-arc.at("start", default: 0deg) + let end = center-arc.at("end", default: 360deg) + let delta = center-arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + arc( + center, + ..center-arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) +} + +#let draw-cycle(cycle, ctx, draw-fragments-and-link) = { + let cycle-step-angle = 360deg / cycle.faces + let angle = angles.angle-from-ctx(ctx, cycle.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ( + ctx.relative-angle == 0deg + and ctx.angle == 0deg + and not cycle.args.at( + "align", + default: false, + ) + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-fragment = none + if ctx.last-anchor.type == "fragment" { + first-fragment = ctx.last-anchor.name + if first-fragment not in ctx.hooks { + ctx.hooks.insert(first-fragment, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in cycle.args { + name = cycle.args.at("name") + record-vertex = true + } else if "arc" in cycle.args { + record-vertex = true + } + let (cycle-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: cycle.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-fragment: first-fragment, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + cycle.body, + ) + ctx = context_.update-parent-context(ctx, cycle-ctx) + if record-vertex { + drawing += draw-cycle-center-arc(cycle-ctx, name, cycle.args.at("arc", default: none)) + } + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.9/src/drawer/fragment.typ b/packages/preview/alchemist/0.1.9/src/drawer/fragment.typ new file mode 100644 index 0000000000..7a5f3bfbae --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/drawer/fragment.typ @@ -0,0 +1,150 @@ +#import "../utils/context.typ": * +#import "../utils/anchors.typ": * +#import "@preview/cetz:0.4.2" + +#let draw-fragment-text(ctx, mol, pos) = { + import cetz.draw: * + for (id, eq) in mol.atoms.enumerate() { + let name = str(id) + let color = if mol.colors != none { + if type(mol.colors) == color { + mol.colors + } else { + mol.colors.at(calc.min(id, mol.colors.len() - 1)) + } + } else { + ctx.config.fragment-color + } + let fragment-font = ctx.config.fragment-font + + // draw atoms of the group one after the other from left to right + content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "mid-west" + }, + ( + if id == 0 { + pos + } else if mol.vertical { + (to: str(id - 1) + ".south", rel: (0, -.2em)) + } else { + str(id - 1) + ".mid-east" + } + ), + auto-scale: false, + { + show math.equation: math.upright + set text(fill: color) if color != none + set text(font: fragment-font) if fragment-font != none + eq + }, + ) + id += 1 + } +} + +#let draw-fragment-lewis(ctx, group-name, count, lewis) = { + if lewis.len() == 0 { + return () + } + import cetz.draw: * + get-ctx(cetz-ctx => { + for (id, (angle: lewis-angle, radius, draw)) in lewis.enumerate() { + if (lewis-angle == none) { + lewis-angle = ctx.config.lewis.angle + } + if (radius == none) { + radius = ctx.config.lewis.radius + } + let lewis-angle = angles.angle-correction(lewis-angle) + let mol-id = if angles.angle-in-range-inclusive(lewis-angle, 90deg, 270deg) { + 0 + } else { + count - 1 + } + let anchor = fragment-anchor( + ctx, + cetz-ctx, + lewis-angle, + group-name, + str(mol-id), + margin: radius, + ) + scope({ + set-origin(anchor) + rotate(lewis-angle) + draw(ctx, cetz-ctx) + }) + } + }) +} + +#let draw-fragment-elements(mol, ctx) = { + let name = mol.name + if name in ctx.hooks { + panic("Molecule fragment with name " + name + " already exists : " + ctx.hooks.keys().join(", ")) + } + ctx.hooks.insert(name, mol) + + let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("west", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-fragment-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let group-anchor = link-fragment-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (group-anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule fragment must be linked to a coord or a link") + } + ctx = context_.set-last-anchor( + ctx, + (type: "fragment", name: name, count: mol.at("count"), vertical: mol.vertical), + ) + if (side) { + ctx.id += 1 + } + ( + ctx, + { + import cetz.draw: * + group( + anchor: if side { + group-anchor + } else { + "from" + str(ctx.id) + }, + name: name, + { + anchor("default", coord) + draw-fragment-text(ctx, mol, coord) + if not side { + anchor("from" + str(ctx.id), group-anchor) + } + }, + ) + draw-fragment-lewis(ctx, name, mol.count, mol.at("lewis")) + }, + ) +} + +#let draw-fragment(element, ctx) = { + if ctx.first-branch { + panic("A molecule fragment can not be the first element in a cycle") + } + let (ctx, drawing) = draw-fragment-elements(element, ctx) + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + (ctx, drawing) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.9/src/drawer/hide.typ b/packages/preview/alchemist/0.1.9/src/drawer/hide.typ new file mode 100644 index 0000000000..b4e940f2d4 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/drawer/hide.typ @@ -0,0 +1,12 @@ +#import "@preview/cetz:0.4.2" + +#let draw-hide(hide, ctx, draw-molecules-and-link) = { + ctx.hide = true + let (hide-ctx, drawing, cetz-rec) = draw-molecules-and-link( + ctx, + hide.body, + ) + hide-ctx.hide = false + drawing = cetz.draw.hide(drawing, bounds: hide.bounds) + (hide-ctx, drawing, cetz-rec) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.9/src/drawer/hook.typ b/packages/preview/alchemist/0.1.9/src/drawer/hook.typ new file mode 100644 index 0000000000..1c5dafb9d1 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/drawer/hook.typ @@ -0,0 +1,14 @@ + +#let draw-hook(hook, ctx) = { + if hook.name in ctx.hooks { + panic("Hook " + hook.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + ctx +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.9/src/drawer/link.typ b/packages/preview/alchemist/0.1.9/src/drawer/link.typ new file mode 100644 index 0000000000..4952edcf2c --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/drawer/link.typ @@ -0,0 +1,104 @@ +#import "../utils/angles.typ" +#import "../utils/anchors.typ": * + +#let create-link-decorations-anchors(link, ctx) = { + let link-angle = 0deg + let to-name = none + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-fragment != none { + to-name = ctx.first-fragment + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angles.angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = angles.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angles.angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let from-pos = if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "fragment" { + from-connection = link-fragment-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + fragment-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name") + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = context_.set-last-anchor( + ctx, + ( + type: "link", + hide: ctx.hide, + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: to-name, + to: to-connection, + angle: link-angle, + over: link.at("over", default: none), + draw: link.draw, + ), + ) + ( + ctx, + { + hide({ + circle(name: link-name + "-start-anchor", from-pos, radius: .25em) + }) + let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + line(from-pos, end-anchor, stroke: blue + .1em) + } + hide( + { + circle(name: link-name + "-end-anchor", end-anchor, radius: .25em) + }, + bounds: true, + ) + }, + ) +} + +#let draw-link(link, ctx) = { + ctx.first-branch = false + let drawing + (ctx, drawing) = create-link-decorations-anchors(link, ctx) + ctx.faces-count += 1 + if link.links.len() != 0 { + ctx.hooks-links.push((link.links, ctx.last-anchor.name, false)) + } + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.9/src/drawer/operator.typ b/packages/preview/alchemist/0.1.9/src/drawer/operator.typ new file mode 100644 index 0000000000..edfc0d0155 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/drawer/operator.typ @@ -0,0 +1,36 @@ +#import "@preview/cetz:0.4.2": draw, process, util + +#let draw-operator(operator, ctx) = { + import draw: * + + let op-name = operator.name + ctx.last-anchor = (type: "coord", anchor: (rel: (operator.margin, 0), to: (name: op-name, anchor: "east"))) + + ( + ctx, + get-ctx(cetz-ctx => { + + let west-previous-mol-anchor = (name: ctx.last-name, anchor: "east") + + let east-op-anchor = (rel: (operator.margin, 0), to: west-previous-mol-anchor) + + let op = if (operator.op == none) { + "" + } else { + operator.op + } + + content( + name: op-name, + anchor: "west", + east-op-anchor, + op, + ) + + if (ctx.config.debug) { + circle(west-previous-mol-anchor, radius: 0.05, fill: yellow, stroke: none) + circle(ctx.last-anchor.anchor, radius: 0.05, fill: yellow, stroke: none) + } + }), + ) +} diff --git a/packages/preview/alchemist/0.1.9/src/drawer/parenthesis.typ b/packages/preview/alchemist/0.1.9/src/drawer/parenthesis.typ new file mode 100644 index 0000000000..cb751785ab --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/drawer/parenthesis.typ @@ -0,0 +1,215 @@ +#import "../utils/utils.typ" +#import "@preview/cetz:0.4.2" + +#let left-parenthesis-anchor(parenthesis, ctx) = { + let anchor = if parenthesis.body.at(0).type == "fragment" { + let name = parenthesis.body.at(0).name + parenthesis.body.at(0).name = name + (name: name, anchor: "west") + } else if parenthesis.body.at(0).type == "link" { + let name = parenthesis.body.at(0).at("name") + parenthesis.body.at(0).name = name + (name + "-start-anchor", 45%, name + "-end-anchor") + } else if not left { } else { + panic("The first element of a parenthesis must be a molecule fragment or a link") + } + (ctx, parenthesis, anchor) +} + +#let right-parenthesis-anchor(parenthesis, ctx) = { + let right-name = parenthesis.at("right") + let right-type = "" + if right-name != none { + right-type = utils.get-element-type(parenthesis.body, right-name) + if right-type == none { + panic("The right element of the parenthesis does not exist") + } + } else { + right-type = parenthesis.body.at(-1).type + right-name = parenthesis.body.at(-1).at("name", default: none) + } + + let anchor = if right-type == "fragment" { + (name: right-name, anchor: "east") + } else if right-type == "link" { + (right-name + "-start-anchor", 55%, right-name + "-end-anchor") + } else { + panic("The last element of a parenthesis must be a molecule fragment or a link but got " + right-type) + } + (ctx, parenthesis, anchor) +} + +#let parenthesis-content(parenthesis, height, cetz-ctx) = { + let block = block(height: height * cetz-ctx.length * 1.2, width: 0pt) + let left-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($parenthesis.l block$, size: 100%) + } + let right-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($block parenthesis.r$, size: 100%) + } + + let right-parenthesis-with-attach = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.attach(right-parenthesis, br: parenthesis.br, tr: parenthesis.tr) + } + + (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) +} + +#let draw-parenthesis(parenthesis, ctx, draw-molecules-and-link) = { + let (parenthesis-ctx, drawing, cetz-rec) = draw-molecules-and-link( + ctx, + parenthesis.body, + ) + ctx = parenthesis-ctx + + + drawing += { + import cetz.draw: * + get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + { + drawing + }, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + + let sub-height = utils.bounding-box-height(sub-bounds) + let sub-v-mid = sub-bounds.low.at(1) - sub-height / 2 + + let sub-width = utils.bounding-box-width(sub-bounds) + + let (ctx, parenthesis, left-anchor) = left-parenthesis-anchor(parenthesis, ctx) + + let (ctx, parenthesis, right-anchor) = right-parenthesis-anchor(parenthesis, ctx) + + let height = parenthesis.at("height") + if height == none { + if not parenthesis.align { + panic("You must specify the height of the parenthesis if they are not aligned") + } + height = sub-height + } else { + height = utils.convert-length(cetz-ctx, height) + } + + let (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) = parenthesis-content( + parenthesis, + height, + cetz-ctx, + ) + + let (_, (lx, ly, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, left-anchor) + let (_, (rx, ry, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, right-anchor) + + if ctx.config.debug { + circle((lx, ly), radius: 1pt, fill: orange, stroke: orange) + circle((rx, ry), radius: 1pt, fill: orange, stroke: orange) + rect((lx, sub-bounds.low.at(1)), (rx, sub-bounds.high.at(1)), stroke: green) + line((lx, sub-v-mid), (rx, sub-v-mid), stroke: green) + } + + let hoffset = calc.abs(sub-width - calc.abs(rx - lx)) + + if parenthesis.align { + ly -= ly - sub-v-mid + ry = ly + } else if type(parenthesis.yoffset) == array { + if parenthesis.yoffset.len() != 2 { + panic("The parenthesis yoffset must be a list of length 2 or a number") + } + ly += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(0)) + ry += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(1)) + } else if parenthesis.yoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.yoffset) + ly += offset + ry += offset + } + + if type(parenthesis.xoffset) == array { + if parenthesis.xoffset.len() != 2 { + panic("The parenthesis xoffset must be a list of length 2 or a number") + } + lx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(0)) + rx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(1)) + } else if parenthesis.xoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.xoffset) + lx -= offset + rx += offset + } + + let right-bounds = cetz.process.many(cetz-ctx, content((0, 0), right-parenthesis, auto-scale: false)).bounds + let right-with-attach-bounds = cetz + .process + .many(cetz-ctx, content((0, 0), right-parenthesis-with-attach, auto-scale: false)) + .bounds + let right-voffset = calc.abs(right-bounds.low.at(1) - right-with-attach-bounds.low.at(1)) + if parenthesis.tr != none and parenthesis.br != none { + right-voffset /= 2 + } else if (parenthesis.tr != none) { + right-voffset *= -1 + } + + ( + _ => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + content((lx, ly), anchor: "mid-east", left-parenthesis, auto-scale: false) + content((rx, ry - right-voffset), anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }) + } + + (ctx, drawing, cetz-rec) +} + + +#let draw-resonance-parenthesis(parenthesis, draw-function, ctx) = { + import cetz.draw: * + let left-name = "parenthesis-" + str(ctx.id) + let right-name = "parenthesis-" + str(ctx.id + 1) + ctx.id += 2 + let last-anchor = ctx.last-anchor + let (drawing, ctx) = draw-function(ctx, parenthesis.body, after-operator: true) + + let drawing = get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + drawing, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + let height = parenthesis.height + if height == none { + height = utils.bounding-box-height(sub-bounds) + } else { + height = utils.convert-length(cetz-ctx, height) + } + let width = utils.bounding-box-width(sub-bounds) + + let (left-parenthesis, _, right-parenthesis-with-attach) = parenthesis-content(parenthesis, height, cetz-ctx) + + let right-anchor = (rel: (width, 0), to: (name: left-name, anchor: "east")) + on-layer( + 1, + { + content(last-anchor.anchor, name: left-name, anchor: "mid-east", left-parenthesis, auto-scale: false) + content(right-anchor, name: right-name, anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }, + ) + ( + ctx => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + }) + + ctx.last-anchor = (type: "coord", anchor: (name: right-name, anchor: "mid-east")) + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.9/src/elements/fragment.typ b/packages/preview/alchemist/0.1.9/src/elements/fragment.typ new file mode 100644 index 0000000000..e7b66eb50f --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/elements/fragment.typ @@ -0,0 +1,74 @@ +#let split-equation(mol, equation: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + for m in mol { + let last-number-hold = last-number + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + result.push(m) + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in fragment fragment") + } + last-number = true + result.push(m) + } else { + panic("Invalid fragment fragment content") + } + } else if m.func() == math.attach or m.func() == math.lr { + result.push(m) + } else if m == [ ] { + continue + } else { + panic("Invalid fragment fragment content") + } + if last-number-hold { + result.at(-2) = result.at(-2) + result.at(-1) + let _ = result.pop() + last-number = false + } + } + result +} + +#let fragment-cor-regex = "[0-9]*[A-Z][a-z]*'*" +#let exponent-regex = "((?:[0-9]+(?:\\+|\\-)?)|[A-Z]|\\+|\\-)" +#let exponent-base-regex = "(?:(\\^|_)" + exponent-regex + ")?(?:(\\^|_)" + exponent-regex + ")?" +#let fragment-regex = regex("^ *(" + fragment-cor-regex + ")" + exponent-base-regex) + +#let split-string(mol) = { + let aux(str) = { + let match = str.match(fragment-regex) + if match == none { + panic(str + " is not a valid fragment") + } else if match.captures.at(1) == match.captures.at(3) and match.captures.at(1) != none { + panic("You cannot use an exponent and a subscript twice") + } + let eq = "\"" + match.captures.at(0) + "\"" + if match.captures.at(1) != none { + eq += match.captures.at(1) + "(" + match.captures.at(2) + ")" + } + if match.captures.at(3) != none { + eq += match.captures.at(3) + "(" + match.captures.at(4) + ")" + } + let eq = math.equation(eval(eq, mode: "math")) + (eq, match.end) + } + + while not mol.len() == 0 { + let (eq, end) = aux(mol) + mol = mol.slice(end) + (eq,) + } +} diff --git a/packages/preview/alchemist/0.1.9/src/elements/lewis.typ b/packages/preview/alchemist/0.1.9/src/elements/lewis.typ new file mode 100644 index 0000000000..7b90c19c03 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/elements/lewis.typ @@ -0,0 +1,135 @@ +#import "@preview/cetz:0.4.2" + +/// Create a lewis function that is then used to draw a lewis +/// formulae element around the fragment +/// +/// - draw-function (function): The function that will be used to draw the lewis element. It should takes three arguments: the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-lewis(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Lewis function takes no positional arguments") + } + let args = args.named() + let angle = args.at("angle", default: none) + let radius = args.at("radius", default: none) + ( + angle: angle, + radius: radius, + draw: (ctx, cetz-ctx) => draw-function(ctx, cetz-ctx, args) + ) + } +} + +/// draw a sigle electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// +/// The position of the electron is set by the `offset` argument. Available values are: +/// - "top": the electron is placed above the fragment center line +/// - "bottom": the electron is placed below the fragment center line +/// - "center": the electron is placed at the fragment center line +/// +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-single(offset: "top"), +/// )) +/// single(angle:-2) +/// fragment("B", lewis:( +/// lewis-single(offset: "bottom"), +/// )) +/// single(angle:-2) +/// fragment("C", lewis:( +/// lewis-single(offset: "center"), +/// )) +/// }) +/// ```) +#let lewis-single = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-single.radius) + let gap = args.at("gap", default: ctx.config.lewis-single.gap) + let offset = args.at("offset", default: ctx.config.lewis-single.offset) + let gap = if offset == "top" { + gap + } else if offset == "bottom" { + -gap + } else if offset == "center" { + 0 + } else { + panic("Invalid position, expected 'top', 'bottom' or 'center'") + } + let fill = args.at("fill", default: ctx.config.lewis-single.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-single.stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-double(), +/// lewis-double(angle: 90deg), +/// lewis-double(angle: 180deg), +/// lewis-double(angle: -90deg) +/// )) +/// }) +/// ```) +#let lewis-double = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-double.radius) + let gap = args.at("gap", default: ctx.config.lewis-double.gap) + let fill = args.at("fill", default: ctx.config.lewis-double.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-double.stroke) + circle((0, -gap), radius: radius, fill: fill, stroke: stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron liked by a single line +/// +/// It is possible to change the length of the line with the `lenght` argument. +/// It is also possible to change the `stroke` agument +/// #example(``` +/// #skeletize({ +/// fragment("B", lewis:( +/// lewis-line(angle: 45deg), +/// lewis-line(angle: 135deg), +/// lewis-line(angle: -45deg), +/// lewis-line(angle: -135deg) +/// )) +/// }) +/// ```) +#let lewis-line = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let length = args.at("length", default: ctx.config.lewis-line.length) + let stroke = args.at("stroke", default: ctx.config.lewis-line.stroke) + line((0, -length / 2), (0, length / 2), stroke: stroke) +}) + + +/// Draw a rectangle to denote a lone pair of electrons +/// +/// It is possible to change the height and width of the rectangle with the `height` and `width` arguments. +/// It is also possible to change the `fill` and `stroke` arguments +/// #example(``` +/// #skeletize({ +/// fragment("C", lewis:( +/// lewis-rectangle(), +/// lewis-rectangle(angle: 180deg) +/// )) +/// }) +/// ```) +#let lewis-rectangle = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let height = args.at("height", default: ctx.config.lewis-rectangle.height) + let width = args.at("width", default: ctx.config.lewis-rectangle.width) + let fill = args.at("fill", default: ctx.config.lewis-rectangle.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-rectangle.stroke) + rect((-width / 2, -height / 2), (width / 2, height / 2), fill: fill, stroke: stroke) +}) diff --git a/packages/preview/alchemist/0.1.9/src/elements/links.typ b/packages/preview/alchemist/0.1.9/src/elements/links.typ new file mode 100644 index 0000000000..6e78d836e2 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/elements/links.typ @@ -0,0 +1,314 @@ +#import "@preview/cetz:0.4.2" +#import "../drawer/cram.typ": * +#import "../utils/utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes four arguments: the length of the link, the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, cetz-ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, cetz-ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single(stroke: red + 5pt) +/// fragment("B") +/// }) +///```) +#let single = build-link((length, ctx, _, args) => { + import cetz.draw: * + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.single.stroke)) +}) + +/// Draw a double line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// fragment("B") +/// }) +///```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double(offset: "right") +/// fragment("B") +/// double(offset: "left") +/// fragment("C") +/// double(offset: "center") +/// fragment("D") +/// }) +///```) +#let double = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.double.gap)) / 2 + let offset = args.at("offset", default: ctx.config.double.offset) + let coeff = args.at("offset-coeff", default: ctx.config.double.offset-coeff) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + let gap-offset = if offset == "right" { + -gap + } else if offset == "left" { + gap + } else if offset == "center" { + 0 + } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, -gap + gap-offset), (x + length * coeff, -gap + gap-offset)) + } else { + ((0, -gap + gap-offset), (length, -gap + gap-offset)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, gap + gap-offset), (x + length * coeff, gap + gap-offset)) + } else { + ((0, gap + gap-offset), (length, gap + gap-offset)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) +}) + +/// Draw a triple line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// fragment("B") +/// }) +///```) +#let triple = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.triple.gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + line((0, -gap), (length, -gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + line((0, gap), (length, gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) +}) + +/// Draw a filled cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, cetz-ctx, args) => cram( + (0, 0), + (length, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a filled cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, cetz-ctx, args) => cram( + (length, 0), + (0, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a hollow cram between two fragments with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((0, 0), (length, 0), ctx, cetz-ctx, args) +}) + +/// Draw a hollow cram between two fragments with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((length, 0), (0, 0), ctx, cetz-ctx, args) +}) + +/// Draw a dashed cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the arrow base length with the `base-length` argument and the tip length with the `tip-length` argument. +// You can also change distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// tip-length: 1em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-right = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + cetz-ctx, + args, +)) + +/// Draw a dashed cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + cetz-ctx, + args, +)) + + +/// Draw a plus sign between two fragments +/// #example(```` +/// #skeletize({ +/// fragment("A") +/// plus() +/// fragment("B") +/// }) +/// ````) +/// You can change the filling, size and stroke of the glyph with the `fill`, `size` and `stoke` arguments. The default values are the parent `text` parameters. +#let plus = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + content( + anchor: "mid", + (length / 2, 0), + { + set text(fill: args.fill) if "fill" in args + set text(stroke: args.stroke) if "stroke" in args + set text(size: args.size) if "size" in args + text($+$) + } + ) +}) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.9/src/utils/anchors.typ b/packages/preview/alchemist/0.1.9/src/utils/anchors.typ new file mode 100644 index 0000000000..12ce5afe56 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/utils/anchors.typ @@ -0,0 +1,296 @@ +#import "angles.typ" +#import "context.typ" as context_ +#import "utils.typ": * +#import "@preview/cetz:0.4.2" +#import cetz.draw: * + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +/// Calculate an anchor position around a fragment using an ellipse +/// at a given angle +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - cetz-ctx (cetz-ctx): the cetz context +/// - angle (float, int, angle): the angle of the anchor +/// - fragment (string): the fragment name +/// - id (string): the fragment subpart id +/// - margin (length, none): the margin around the fragment +/// -> anchor: the anchor position around the fragment +#let fragment-anchor(ctx, cetz-ctx, angle, fragment, id, margin: none) = { + let angle = angles.angle-correction(angle) + let fragment-margin = if margin == none { + ctx.config.fragment-margin + } else { + margin + } + fragment-margin = convert-length(cetz-ctx, fragment-margin) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "mid")), + ) + let (a, b) = if angles.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, fragment-margin, fragment, id) + } else { + anchor-south-east(cetz-ctx, center, fragment-margin, fragment, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if angles.angle-in-range-inclusive(angle, 0deg, 90deg) or angles.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +/// Return the index to choose if the link connection is not overridden +#let link-fragment-index(angle, end, count, vertical) = { + if not end { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let fragment-link-anchor(name, id, count) = { + if count <= id { + panic("The last fragment only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "mid")) +} + +#let link-fragment-anchor(name: none, id, count) = { + if id >= count { + panic("This fragment only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the fragment to link to must be defined") + } + if name == none { + (name: str(id), anchor: "mid") + } else { + (name: name, anchor: (str(id), "mid")) + } +} + + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "mid") + if link.to == none or link.from == none { + let angle = link.at( + "angle", + default: angles.angle-between(cetz-ctx, link.from-pos, to-pos), + ) + link.angle = angle + if link.from == none { + link.from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + if link.from == -1 { + link.from = 0 + } + if link.to == -1 { + link.to = ctx.hooks.at(link.to-name).count - 1 + } + let start = fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) + let end = fragment-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) + ((start, end), angles.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "mid"), + ), + ) + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "mid")), + ), + ) + } + let end-anchor = fragment-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + angles.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = angles.angle-correction(angles.angle-between(cetz-ctx, link.from-pos, hook.hook)) + let from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = fragment-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) + ( + ( + start-anchor, + hook.hook, + ), + angles.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + angles.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + diff --git a/packages/preview/alchemist/0.1.9/src/utils/angles.typ b/packages/preview/alchemist/0.1.9/src/utils/angles.typ new file mode 100644 index 0000000000..60e111292f --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/utils/angles.typ @@ -0,0 +1,98 @@ +#import "@preview/cetz:0.4.2" + +/// Convert any angle to an angle between 0deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a += 360 + } + + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +/// Check if the angle is in the range ]from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +/// Check if the angle is in the range [from, to] +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +/// +/// - ctx (cetz-ctx): The cetz context +/// - from (anchor): The first anchor +/// - to (anchor): The second anchor +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// Calculate the angle from an object with "relative", "absolute" or "angle" key +/// according to the alchemist context (see the manual to see how angles are calculated) +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - object (dict): the object to get the angle from +/// - default (angle): the default angle +/// -> the calculated angle +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +/// Overwrite the offset based on the angle +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} diff --git a/packages/preview/alchemist/0.1.9/src/utils/context.typ b/packages/preview/alchemist/0.1.9/src/utils/context.typ new file mode 100644 index 0000000000..4021bb5177 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/utils/context.typ @@ -0,0 +1,33 @@ +#let update-parent-context(parent-ctx, ctx) = { + let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { + ( + ..parent-ctx.last-anchor, + drew: true, + ) + } else { + parent-ctx.last-anchor + } + ( + ..parent-ctx, + last-anchor: last-anchor, + hooks: ctx.hooks, + hooks-links: ctx.hooks-links, + links: ctx.links, + ) +} + +/// Set the last anchor in the context to the given anchor and save it if needed +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" { + let drew = ctx.last-anchor.at("drew", default: false) + if drew and anchor.type == "link" and anchor.name == ctx.last-anchor.name { + drew = false + let _ = ctx.links.pop() + } + if not drew { + ctx.links.push(ctx.last-anchor) + } + ctx.last-anchor.drew = true + } + (..ctx, last-anchor: anchor) +} diff --git a/packages/preview/alchemist/0.1.9/src/utils/utils.typ b/packages/preview/alchemist/0.1.9/src/utils/utils.typ new file mode 100644 index 0000000000..72220a4987 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/src/utils/utils.typ @@ -0,0 +1,75 @@ +#import "@preview/cetz:0.4.2" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + return if type(num) == length { + float(num.to-absolute() / ctx.length) + } else if type(num) == ratio { + num + } else { + float(num) + } +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} + +/// merge two imbricated dictionaries together +/// The second dictionary is the default value if the key is not present in the first dictionary +#let merge-dictionaries(dict1, default) = { + let result = default + for (key, value) in dict1 { + if type(value) == dictionary { + result.insert(key, merge-dictionaries(value, default.at(key))) + } else { + result.insert(key, value) + } + } + result +} + + +/// get the type of an element by its name +/// +/// - body (drawable): the chemfig body of a molecule +/// - name (string): the name of the element to get the type +/// -> string +#let get-element-type(body, name) = { + for element in body { + if type(element) != dictionary { + continue + } + if "name" in element and element.name == name { + return element.type + } + if element.type == "branch" or element.type == "cycle" or element.type == "parenthesis" { + let type = get-element-type(element.body, name) + if type != none { + return type + } + } + } + none +} + +/// Calculate the height of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-height(bounds) = { + calc.abs(bounds.high.at(1) - bounds.low.at(1)) +} + +/// Calculate the width of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-width(bounds) = { + calc.abs(bounds.high.at(0) - bounds.low.at(0)) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.9/typst.toml b/packages/preview/alchemist/0.1.9/typst.toml new file mode 100644 index 0000000000..2bbe754547 --- /dev/null +++ b/packages/preview/alchemist/0.1.9/typst.toml @@ -0,0 +1,13 @@ +[package] +name = "alchemist" +version = "0.1.9" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.13.1" +repository = "https://github.com/Typsium/alchemist" +description = "A package to render skeletal formulas using CeTZ" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization", "paper"] +disciplines = ["education", "chemistry", "biology"] +keywords = ["chemistry", "chemical", "formula", "molecule", "chemiviz", "structure", "organic", "lewis", "bond", "aromatic"] From 124b304e80f1471140abd3cef1516fd7c72942a4 Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Sun, 15 Mar 2026 22:25:09 +0100 Subject: [PATCH 18/24] updated include version in Readme --- packages/preview/alchemist/0.1.9/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preview/alchemist/0.1.9/README.md b/packages/preview/alchemist/0.1.9/README.md index f66953d15c..c4e40cb090 100644 --- a/packages/preview/alchemist/0.1.9/README.md +++ b/packages/preview/alchemist/0.1.9/README.md @@ -78,7 +78,7 @@ Alchemist uses cetz to draw the molecules. This means that you can draw cetz sha To start using alchemist, just use the following code: ```typ -#import "@preview/alchemist:0.1.8": * +#import "@preview/alchemist:0.1.9": * #skeletize({ // Your molecule here From ee5f06b611623ed010fd8a204e94b469855df2c6 Mon Sep 17 00:00:00 2001 From: Tom <45141234+Robotechnic@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:49:05 +0100 Subject: [PATCH 19/24] Update README with new manual link --- packages/preview/alchemist/0.1.9/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preview/alchemist/0.1.9/README.md b/packages/preview/alchemist/0.1.9/README.md index c4e40cb090..c7c48a7de1 100644 --- a/packages/preview/alchemist/0.1.9/README.md +++ b/packages/preview/alchemist/0.1.9/README.md @@ -85,7 +85,7 @@ To start using alchemist, just use the following code: }) ``` -For more information, check the [manual](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf). +For more information, check the [manual](https://github.com/Typsium/alchemist/blob/7fed688a06d1e98abfd8c9728dd68936058cd627/doc/manual.pdf). ## Tests From e38a344bff381ab80fad3d2444956ae59454937c Mon Sep 17 00:00:00 2001 From: Tom <45141234+Robotechnic@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:01:43 +0100 Subject: [PATCH 20/24] Update README link to the 0.9.0 manual version --- packages/preview/alchemist/0.1.9/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preview/alchemist/0.1.9/README.md b/packages/preview/alchemist/0.1.9/README.md index c7c48a7de1..6b7c44eca9 100644 --- a/packages/preview/alchemist/0.1.9/README.md +++ b/packages/preview/alchemist/0.1.9/README.md @@ -85,7 +85,7 @@ To start using alchemist, just use the following code: }) ``` -For more information, check the [manual](https://github.com/Typsium/alchemist/blob/7fed688a06d1e98abfd8c9728dd68936058cd627/doc/manual.pdf). +For more information, check the [manual](https://raw.githubusercontent.com/Typsium/alchemist/0.1.9/doc/manual.pdf). ## Tests From df77294b19cc8551db8038975f83b423df1d2839 Mon Sep 17 00:00:00 2001 From: Tom <45141234+Robotechnic@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:05:52 +0100 Subject: [PATCH 21/24] Update User Manual badge link to 0.1.9 link in README.md --- packages/preview/alchemist/0.1.9/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preview/alchemist/0.1.9/README.md b/packages/preview/alchemist/0.1.9/README.md index 6b7c44eca9..167adbc27c 100644 --- a/packages/preview/alchemist/0.1.9/README.md +++ b/packages/preview/alchemist/0.1.9/README.md @@ -1,6 +1,6 @@ [![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FTypsium%2Falchemist%2Fmaster%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://github.com/Typsium/alchemist) [![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/alchemist/blob/master/LICENSE) -[![User Manual](https://img.shields.io/badge/manual-.pdf-purple)](https://raw.githubusercontent.com/Robotechnic/alchemist/master/doc/manual.pdf) +[![User Manual](https://img.shields.io/badge/manual-.pdf-purple)](https://raw.githubusercontent.com/Typsium/alchemist/0.1.9/doc/manual.pdf) # alchemist From f94cf5582bf2db100af6b82f30eb7ebd3458f512 Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Mon, 25 May 2026 22:21:40 +0200 Subject: [PATCH 22/24] Upload alchemist v0.1.10 --- packages/preview/alchemist/0.1.10/LICENSE | 22 + packages/preview/alchemist/0.1.10/README.md | 155 ++++++ packages/preview/alchemist/0.1.10/lib.typ | 284 +++++++++++ .../alchemist/0.1.10/src/cetz/process.typ | 96 ++++ .../preview/alchemist/0.1.10/src/default.typ | 61 +++ .../preview/alchemist/0.1.10/src/drawer.typ | 448 ++++++++++++++++++ .../alchemist/0.1.10/src/drawer/branch.typ | 32 ++ .../alchemist/0.1.10/src/drawer/cram.typ | 61 +++ .../alchemist/0.1.10/src/drawer/cycle.typ | 155 ++++++ .../alchemist/0.1.10/src/drawer/fragment.typ | 161 +++++++ .../alchemist/0.1.10/src/drawer/hide.typ | 12 + .../alchemist/0.1.10/src/drawer/hook.typ | 14 + .../alchemist/0.1.10/src/drawer/link.typ | 104 ++++ .../alchemist/0.1.10/src/drawer/operator.typ | 36 ++ .../0.1.10/src/drawer/parenthesis.typ | 215 +++++++++ .../0.1.10/src/elements/fragment.typ | 106 +++++ .../alchemist/0.1.10/src/elements/lewis.typ | 135 ++++++ .../alchemist/0.1.10/src/elements/links.typ | 314 ++++++++++++ .../alchemist/0.1.10/src/utils/anchors.typ | 296 ++++++++++++ .../alchemist/0.1.10/src/utils/angles.typ | 98 ++++ .../alchemist/0.1.10/src/utils/context.typ | 33 ++ .../alchemist/0.1.10/src/utils/utils.typ | 75 +++ packages/preview/alchemist/0.1.10/typst.toml | 13 + 23 files changed, 2926 insertions(+) create mode 100644 packages/preview/alchemist/0.1.10/LICENSE create mode 100644 packages/preview/alchemist/0.1.10/README.md create mode 100644 packages/preview/alchemist/0.1.10/lib.typ create mode 100644 packages/preview/alchemist/0.1.10/src/cetz/process.typ create mode 100644 packages/preview/alchemist/0.1.10/src/default.typ create mode 100644 packages/preview/alchemist/0.1.10/src/drawer.typ create mode 100644 packages/preview/alchemist/0.1.10/src/drawer/branch.typ create mode 100644 packages/preview/alchemist/0.1.10/src/drawer/cram.typ create mode 100644 packages/preview/alchemist/0.1.10/src/drawer/cycle.typ create mode 100644 packages/preview/alchemist/0.1.10/src/drawer/fragment.typ create mode 100644 packages/preview/alchemist/0.1.10/src/drawer/hide.typ create mode 100644 packages/preview/alchemist/0.1.10/src/drawer/hook.typ create mode 100644 packages/preview/alchemist/0.1.10/src/drawer/link.typ create mode 100644 packages/preview/alchemist/0.1.10/src/drawer/operator.typ create mode 100644 packages/preview/alchemist/0.1.10/src/drawer/parenthesis.typ create mode 100644 packages/preview/alchemist/0.1.10/src/elements/fragment.typ create mode 100644 packages/preview/alchemist/0.1.10/src/elements/lewis.typ create mode 100644 packages/preview/alchemist/0.1.10/src/elements/links.typ create mode 100644 packages/preview/alchemist/0.1.10/src/utils/anchors.typ create mode 100644 packages/preview/alchemist/0.1.10/src/utils/angles.typ create mode 100644 packages/preview/alchemist/0.1.10/src/utils/context.typ create mode 100644 packages/preview/alchemist/0.1.10/src/utils/utils.typ create mode 100644 packages/preview/alchemist/0.1.10/typst.toml diff --git a/packages/preview/alchemist/0.1.10/LICENSE b/packages/preview/alchemist/0.1.10/LICENSE new file mode 100644 index 0000000000..a85204e590 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.1.10/README.md b/packages/preview/alchemist/0.1.10/README.md new file mode 100644 index 0000000000..10da6222b4 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/README.md @@ -0,0 +1,155 @@ +[![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FTypsium%2Falchemist%2Fmaster%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://github.com/Typsium/alchemist) +[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/alchemist/blob/master/LICENSE) +[![User Manual](https://img.shields.io/badge/manual-.pdf-purple)](https://raw.githubusercontent.com/Typsium/alchemist/0.1.10/doc/manual.pdf) + +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + fragment(name: "A", "A") + single() + fragment("B") + branch({ + single(angle: 1) + fragment( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + fragment(name: "X", "X") + }) + branch({ + single(angle: -1) + fragment("Y") + single() + fragment( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + fragment( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic1/ref/1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + fragment("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic2/ref/1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.1.10": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Typsium/alchemist/0.1.10/doc/manual.pdf). + +## Tests + +The test suite is managed with [tytanic](https://github.com/typst-community/tytanic). + +## Changelog + +### 0.1.10 + +- Added touying bindings in lib.typ to support automatic integration +- Added a way to ignore charges in the links connection of a fragment with the `ignore-charge` parameter +- Fix [#27](https://github.com/Typsium/alchemist/issues/27) +- Fix [#23](https://github.com/Typsium/alchemist/issues/23) + +### 0.1.9 + +- Updated cetz to version 0.4.2 +- Exposed name in lib.typ to be used by other packages +- Adds a touying support + +### 0.1.8 + +- Fixed bugs introduced in 0.1.7 + +### 0.1.7 + +- Updated cetz to version 0.4.1 +- Added default values for `color` and `font` for `fragment` elements +- Added a `skeletize-config` function to create a `skeletize` function with a specific configuration +- Fixed a bug with cetz anchors not being correctly translated +- Added an `over` argument to links to allow hiding overlapped links + +### 0.1.6 + +- Fixed the parenthesis height to work with the new typst version +- Renamed `molecule` into `fragment` +- Added support for charges and apostrophes in the string of a molecule +- Fixed the parenthesis auto positioning and alignment +- Added a new operator element and a new parameter in parenthesis to write resonance formulae + +### 0.1.5 + +- Update to compiler 0.13.1 and Cetz 0.3.4 + +### 0.1.4 + +- Added the possibility to create Lewis formulae +- Added parenthesis element to create groups and polymer + +### 0.1.3 + +- Added the possibility to add exponent in the string of a molecule. + +### 0.1.2 + +- Added default values for link style properties. +- Updated `cetz` to version 0.3.1. +- Added a `tip-length` argument to dashed cram links. + +### 0.1.1 + +- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. +- Fixed multiples bugs that causes overdraws of links. + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.1.10/lib.typ b/packages/preview/alchemist/0.1.10/lib.typ new file mode 100644 index 0000000000..f4a4d70a98 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/lib.typ @@ -0,0 +1,284 @@ +#import "@preview/cetz:0.4.2" +#import "src/default.typ": default +#import "src/utils/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ": skeletize, draw-skeleton, skeletize-config, draw-skeleton-config, hide-drawables +#import "src/elements/links.typ": * +#import "src/elements/fragment.typ": * +#import "src/elements/lewis.typ": * + +#let transparent = color.rgb(100%, 0, 0, 0) +#let name = "alchemist" + +#let touying-reducer-bindings = ( + "reduce": ("skeletize",), + "cover": ("hide",) +) + +/// === Fragment function +/// Build a fragment group based on mol +/// Each fragment is represented as an optional count followed by a fragment name +/// starting by a capital letter followed by an optional exponent followed by an optional indice. +/// #example(``` +/// #skeletize({ +/// fragment("H_2O") +/// }) +///```) +/// #example(``` +/// #skeletize({ +/// fragment("H^A_EF^5_4") +/// }) +/// ```) +/// It is possible to use an equation as a fragment. In this case, the splitting of the equation uses the same rules as in the string case. However, you get some advantages over the string version: +/// - You can use parenthesis to group elements together. +/// - You have no restriction about what you can put in exponent or indice. +/// #example(``` +/// #skeletize({ +/// fragment($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the fragment. It is used as the cetz name of the fragment and to link other fragments to it. +/// - links (dictionary): The links between this fragment and previous fragments or hooks. The key is the name of the fragment or hook and the value is the link function. See @links. +/// +/// Note that the atom-sep and angle arguments are ignored +/// - lewis (list): The list of lewis structures to draw around the fragments. See @lewis +/// - mol (string, equation): The string representing the fragment or an equation of the fragment +/// - vertical (boolean): If true, the fragment is drawn vertically +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", vertical: true) +/// }) +///```) +/// - ignore-charge (boolean): If true, charge of the fragment are excluded from the links connections. This is useful when you want to have the center of your connection to the text without having the charge in the way. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single(relative: 90deg) +/// fragment("B^-", ignore-charge: true) +/// }) +/// ```) +/// - colors (color, list): The color of the fragment. If a list is provided, it colors each group of the fragment with the corresponding color from right to left. If the number of colors is less than the number of groups, the last color is used for the remaining groups. If the number of colors is greater than the number of groups, the extra colors are ignored. +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", colors: (red, green, blue)) +/// single() +/// fragment("EFGH", colors: (orange)) +/// }) +/// ```) +/// -> drawable +#let fragment(name: none, links: (:), lewis: (), vertical: false, ignore-charge: false, colors: none, mol) = { + let (atoms, count) = if type(mol) == str { + split-fragment-string(mol, split-charge: ignore-charge) + } else if mol.func() == math.equation { + split-equation(mol, equation: true, split-charge: ignore-charge) + } else { + panic("Invalid fragment content") + } + + if type(lewis) != array { + panic("Lewis formulae elements must be in a list") + } + + ( + ( + type: "fragment", + name: name, + atoms: atoms, + colors: colors, + links: links, + lewis: lewis, + vertical: vertical, + count: count + ), + ) +} +#let molecule(name: none, links: (:), lewis: (), vertical: false, mol) = fragment(name: name, links: links, lewis: lewis, vertical: vertical, mol) + +/// === Hooks +/// Create a hook in the fragment. It allows to connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the fragment. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current fragment, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// branch({ +/// single(angle:1) +/// fragment("B") +/// }) +/// branch({ +/// double(angle: -1) +/// fragment("D") +/// }) +/// single() +/// double() +/// single() +/// fragment("C") +/// }) +///```) +/// - body (drawable): the body of the branch. It must start with a link. +/// -> drawable +#let branch(body, ..args) = { + if args.pos().len() != 0 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", body: body, args: args.named()),) +} + +/// Create a regular cycle of fragments +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +/// - faces (int): the number of faces of the cycle +/// - body (drawable): the body of the cycle. It must start and end with a fragment or a link. +/// -> drawable +#let cycle(faces, body, ..args) = { + if args.pos().len() != 0 { + panic("Cycle takes two positional arguments: number of faces and body") + } + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + body: body, + args: args.named(), + ), + ) +} + +/// === Parenthesis +/// Encapsulate a drawable between two parenthesis. The left parenthesis is placed at the left of the first element of the body and by default the right parenthesis is placed at the right of the last element of the body. +/// +/// #example(``` +/// #skeletize( +/// config: ( +/// angle-increment: 30deg +/// ), { +/// parenthesis( +/// l:"[", r:"]", +/// br: $n$, { +/// single(angle: 1) +/// single(angle: -1) +/// single(angle: 1) +/// }) +/// }) +/// ```) +/// For more examples, see @examples +/// +/// - body (drawable): the body of the parenthesis. It must start and end with a fragment or a link. +/// - l (string): the left parenthesis +/// - r (string): the right parenthesis +/// - align (true): if true, the parenthesis will have the same y position. They will also get sized and aligned according to the body height. If false, they are not aligned and the height argument must be specified. +/// - resonance (boolean): if true, the parenthesis will be drawn in resonance mode. This means that the left and right parenthesis will be placed outside the molecule. Also, the parenthesis will be separated from the previous and next molecule. This can be true only if the parenthesis is the first element of the skeletal formula or if the previous element is an operator. See @resonance for more details. +/// - height (float, length): the height of the parenthesis. If align is true, this argument is optional. +/// - yoffset (float, length, list): the vertical offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - xoffset (float, length, list): the horizontal offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - right (string): Sometime, it is not convenient to place the right parenthesis at the end of the body. In this case, you can specify the name of the fragment or link where the right parenthesis should be placed. It is especially useful when the body end by a cycle. See @polySulfide +/// - tr (content): the exponent content of the right parenthesis +/// - br (content): the indice content of the right parenthesis +/// -> drawable +#let parenthesis(body, l: "(", r: ")", align: true, resonance: false, height: none, yoffset: none, xoffset: none, right: none, tr: none, br: none) = { + if l.len() > 2 { + panic("Left can be at most 2 characters") + } + let l = eval(l, mode: "math") + if r.len() > 2 { + panic("Right can be at most 2 characters") + } + let r = eval(r, mode: "math") + ( + ( + type: "parenthesis", + body: body, + calc: calc, + l: l, + r: r, + align: align, + tr: tr, + br: br, + height: height, + xoffset: xoffset, + yoffset: yoffset, + right: right, + resonance: resonance, + ), + ) +} + + +/// === Operator +/// Create an operator between two fragments. Creating an operator "reset" the placement of the next fragment. +/// This allow to add multiple molecules in the same skeletal formula. Without this, the next fragment would be placed at the end of the previous one. +/// An important point is that you can't use previous hooks to link two molecules separate by an operator. +/// This element is used in resonance structures (@resonance) and in some cases to put multiples molecules in the same skeletal formula (as you can set op to none). +/// +/// - op (content, string, none): The operator content. It can be a string or a content. A none value won't display anything. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// operator($->$, margin: 1em) +/// fragment("B") +/// }) +/// ```) +/// See @resonance for more examples. +/// - name (string): The name of the operator. +/// - margin (float, length): The margin between the operator and previous / next molecule. +/// -> drawable +#let operator(name: none, margin: 1em, op) = { + ( + ( + type: "operator", + name: name, + op: op, + margin: margin, + ), + ) +} + +/// === Hiding part of the molecule +/// This element allows to hide part of the molecule. It can be used to hide the part of the molecule that is not relevant for the current discussion. It can also be used to create some animation effects by hiding and showing different parts of the molecule. Note that the hidden part is still present in the drawing and can be linked to other fragments. This means that you can hide a part of the molecule and still link it to other fragments or hooks. The hidden part is also still present in the cetz record, which means that you can still use it in the cetz drawing. The only thing that is hidden is the drawing of the hidden part. +/// - bounds (boolean): If true, the hidden part keep the same bounding box as if it was not hidden. If false, the hidden part doesn't take any space in the drawing. +/// - body (drawable): The body to hide. It can be any drawable. +/// -> drawable +#let hide(bounds: true, body) = { + ( + ( + type: "hide", + bounds: bounds, + body: body, + ), + ) +} diff --git a/packages/preview/alchemist/0.1.10/src/cetz/process.typ b/packages/preview/alchemist/0.1.10/src/cetz/process.typ new file mode 100644 index 0000000000..cb6e71b058 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/cetz/process.typ @@ -0,0 +1,96 @@ +// Temporary custom process.typ file to override default behavior + +#import "@preview/cetz:0.4.2": util, path-util, vector, drawable, process.aabb + + +/// Processes an element's function to get its drawables and bounds. Returns a {{dictionary}} with the key-values: `ctx` The modified context object, `bounds` The {{aabb}} of the element's drawables, `drawables` An {{array}} of the element's {{drawable}}s. +/// +/// - ctx (ctx): The current context object. +/// - element-func (function): A function that when passed {{ctx}}, it should return an element dictionary. +#let element(ctx, element-func) = { + let bounds = none + let element + let anchors = () + + (ctx, ..element,) = element-func(ctx) + if "drawables" in element { + if type(element.drawables) == dictionary { + element.drawables = (element.drawables,) + } + for drawable in drawable.filter-tagged(element.drawables, drawable.TAG.no-bounds) { + bounds = aabb.aabb( + if drawable.type == "path" { + path-util.bounds(drawable.segments) + } else if drawable.type == "content" { + let (x, y, _, w, h,) = drawable.pos + (drawable.width, drawable.height) + ((x + w / 2, y - h / 2, 0.0), (x - w / 2, y + h / 2, 0.0)) + }, + init: bounds + ) + } + } + + let name = element.at("name", default: none) + element.name = name + if name != none { + assert.eq(type(name), str, + message: "Element name must be a string") + assert(not name.contains("."), + message: "Invalid name for element '" + element.name + "'; name must not contain '.'") + + if "anchors" in element { + anchors.push(name) + ctx.nodes.insert(name, element) + if ctx.groups.len() > 0 { + ctx.groups.last().push(name) + } + } + } + + // Draw a debug bounding box. + if ctx.debug and bounds != none { + element.drawables.push(drawable.line-strip( + (bounds.low, + (bounds.high.at(0), bounds.low.at(1), 0.0), + bounds.high, + (bounds.low.at(0), bounds.high.at(1), 0.0) + ), + close: true, + stroke: red, + tags: (drawable.TAG.debug,))) + } + + return ( + ctx: ctx, + bounds: bounds, + drawables: element.at("drawables", default: ()), + element: element, + anchors: anchors + ) +} + +/// Runs the `element` function for a list of element functions and aggregates the results. +/// - ctx (ctx): The current context object. +/// - body (array): The array of element functions to process. +/// -> dictionary +#let many(ctx, body) = { + let drawables = () + let bounds = none + let elements = () + let anchors = () + + for el in body { + let r = element(ctx, el) + if r != none { + if r.bounds != none { + let pts = (r.bounds.low, r.bounds.high,) + bounds = aabb.aabb(pts, init: bounds) + } + ctx = r.ctx + drawables += r.drawables + anchors += r.anchors + } + elements.push(r.element) + } + return (ctx: ctx, bounds: bounds, drawables: drawables, elements: elements, anchors: anchors) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.10/src/default.typ b/packages/preview/alchemist/0.1.10/src/default.typ new file mode 100644 index 0000000000..a5d0222af3 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/default.typ @@ -0,0 +1,61 @@ +#let default = ( + atom-sep: 3em, + fragment-margin: 0.2em, + fragment-font: none, + fragment-color: none, + link-over-radius: .2, + angle-increment: 45deg, + base-angle: 0deg, + debug: false, + single: ( + stroke: black + ), + double: ( + gap: .25em, + offset: "center", + offset-coeff: 0.85, + stroke: black + ), + triple: ( + gap: .25em, + stroke: black + ), + filled-cram: ( + stroke: none, + fill: black, + base-length: .8em + ), + dashed-cram: ( + stroke: black + .05em, + dash-gap: .3em, + base-length: .8em, + tip-length: .1em + ), + lewis: ( + angle: 0deg, + radius: 0.2em, + ), + lewis-single: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + offset: "top" + ), + lewis-double: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + ), + lewis-line: ( + stroke: black, + length: .7em, + ), + lewis-rectangle: ( + stroke: .08em + black, + fill: white, + height: .7em, + width: .3em + ) +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.10/src/drawer.typ b/packages/preview/alchemist/0.1.10/src/drawer.typ new file mode 100644 index 0000000000..044bfd26c2 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/drawer.typ @@ -0,0 +1,448 @@ +#import "default.typ": default +#import "@preview/cetz:0.4.2" +#import "cetz/process.typ" as custom-process +#import "utils/utils.typ": * +#import "utils/anchors.typ": * +#import "utils/utils.typ" as utils +#import "drawer/fragment.typ" as fragment +#import "drawer/link.typ" as link +#import "drawer/branch.typ" as branch +#import "drawer/cycle.typ" as cycle +#import "drawer/parenthesis.typ" as parenthesis +#import "drawer/hook.typ" as hook +#import "drawer/operator.typ" as operator +#import "drawer/hide.typ": draw-hide + +#import cetz.draw: * + +#let default-anchor = (type: "coord", anchor: (0, 0)) + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + last-name: "", // last name used to draw + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + id: 0, // an id used to name things with an unique name + first-molecule: true, // true if we are drawing the first molecule of the current equation + first-draw: true, // true if no element of the current molecule has been drawn yet + // branch + first-branch: false, // true if the next element is the first in a branch + // cycle + first-fragment: none, // name of the first fragment in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices + hide: false, // true if the current elements should be hidden +) + +#let draw-hooks-links(links, mol-name, ctx, from-mol) = { + let hook-id = 0 + for (to-name, (link,)) in links { + if link.at(mol-name, default: none) == none { + link.name = mol-name + "-hook-" + str(hook-id) + hook-id += 1 + } + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "fragment" { + ctx.links.push(( + type: "link", + hide: ctx.hide, + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + from: none, + to: none, + over: link.at("over", default: none), + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + hide: ctx.hide, + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + over: link.at("over", default: none), + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + } + ctx +} + +#let draw-fragments-and-link(ctx, body) = { + let fragment-drawing = () + let cetz-drawing = () + ctx.first-draw = true + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + let drawing = () + let cetz-rec = () + if type(element) == function { + if ctx.hide { + hide( + cetz-drawing.push(element) + ) + } else { + cetz-drawing.push(element) + } + } else if type(element) == dictionary { + if "type" not in element { + panic("Element " + repr(element) + " has no type") + } else if element.type == "fragment" { + (ctx, drawing) = fragment.draw-fragment(element, ctx) + } else if element.type == "link" { + (ctx, drawing) = link.draw-link(element, ctx) + } else if element.type == "branch" { + (ctx, drawing, cetz-rec) = branch.draw-branch(element, ctx, draw-fragments-and-link) + } else if element.type == "cycle" { + (ctx, drawing, cetz-rec) = cycle.draw-cycle(element, ctx, draw-fragments-and-link) + } else if element.type == "hook" { + ctx = hook.draw-hook(element, ctx) + } else if element.type == "parenthesis" { + (ctx, drawing, cetz-rec) = parenthesis.draw-parenthesis( + element, + ctx, + draw-fragments-and-link, + ) + } else if element.type == "operator" { + (ctx, drawing) = operator.draw-operator(element, fragment-drawing, ctx) + } else if element.type == "hide" { + (ctx, drawing, cetz-rec) = draw-hide(element, ctx, draw-fragments-and-link) + } else { + panic("Unknown element type " + element.type) + } + } else if type(element) == type([]) { + if element.func() == metadata { + // ignore metadata + } else { + panic("Unexpected content element: " + repr(element)) + } + } else if element == none { + // ignore empty elements + } else { + panic("Unexpected element type: " + str(type(element)) + " with value " + repr(element)) + } + fragment-drawing += drawing + cetz-drawing += cetz-rec + ctx.first-draw = false + } + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at("drew", default: false) { + ctx.links.push(ctx.last-anchor) + ctx.last-anchor.drew = true + } + ( + ctx, + fragment-drawing, + cetz-drawing, + ) +} + +#let draw-link-over(ctx, link, over, angle) = { + let name = link.name + "-over" + let link-over-radius = ctx.config.link-over-radius + let (over, length, radius) = if type(over) == str { + (over, link-over-radius, link-over-radius) + } else if type(over) == dictionary { + let name = over.at("name", default: none) + if name == none { + panic("Over argument must have a name") + } + ( + name, + over.at("length", default: link-over-radius), + over.at("radius", default: link-over-radius) + ) + } else { + panic("Over must be a string or a dictionary, got " + type(link.at("over"))) + } + intersections(name, over, link.name) + let color = if ctx.config.debug { + red + } else { + white + } + scope({ + rotate(angle) + rect( + anchor: "center", + (to: name + ".0", rel: (-length/2,-radius/2)), + (rel: (length, radius)), + fill: color, + stroke: color + ) + }) +} + +#let draw-link-decoration(ctx) = { + ( + ctx, + get-ctx(cetz-ctx => { + for link in ctx.links { + if link.hide { + continue + } + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + if link.at("over") != none { + if type(link.at("over")) == array { + for over in link.at("over") { + draw-link-over(ctx, link, over, angle) + } + } else { + draw-link-over(ctx, link, link.at("over"), angle) + } + } + + scope({ + set-origin(from) + rotate(angle) + (link.draw)(length, ctx, cetz-ctx, override: link.override) + }) + } + }), + ) +} + +/// set elements names and split the molecule into sub-groups +#let preprocessing(body, group-id: 0, link-id: 0, operator-id: 0) = { + let result = ((),) + let has_element = false + for element in body { + if type(element) == dictionary { + has_element = true + if element.at("name", default: none) == none { + if element.type == "fragment" { + element.name = "fragment-" + str(group-id) + group-id += 1 + } else if element.type == "link" { + element.name = "link-" + str(link-id) + link-id += 1 + } else if element.type == "operator" { + element.name = "operator-" + str(operator-id) + operator-id += 1 + } + } + if element.at("body", default: none) != none { + let child-body + (child-body, group-id, link-id, operator-id) = preprocessing( + element.body, + group-id: group-id, + link-id: link-id, + operator-id: operator-id, + ) + if element.type == "parenthesis" and element.resonance { + element.body = child-body + } else { + element.body = child-body.at(0) + } + } + if element.type == "operator" or (element.type == "parenthesis" and element.resonance) { + result.push(element) + result.push(()) + } else { + result.at(-1).push(element) + } + } else if type(element) == function { + result.at(-1).push(element) + } else { + panic("Unexpected element type: " + str(type(element)) + " with value " + repr(element)) + } + } + if not has_element { + panic("The skeletize body must contain at least one element") + } + (result, group-id, link-id, operator-id) +} + +#let operator-group-name = id => { + "operator-group-" + str(id) +} + +#let draw-groups(ctx, bodies, after-operator: false) = { + ctx.last-name = "" + + let drawables = for (i, body) in bodies.enumerate() { + if type(body) == array { + if body.len() > 0 { + ctx.last-name = operator-group-name(i) + get-ctx(cetz-ctx => { + let ctx = ctx + if ctx.last-anchor.type == "coord" { + (cetz-ctx, ctx.last-anchor.anchor) = cetz.coordinate.resolve(cetz-ctx, ctx.last-anchor.anchor) + } + let last-anchor = ctx.last-anchor + let (ctx, atoms, cetz-drawing) = draw-fragments-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + + let molecule = { + atoms + } + + let (ctx: cetz-ctx, drawables, bounds: molecule-bounds, anchors) = custom-process.many(cetz-ctx, molecule) + molecule-bounds = cetz.util.revert-transform(cetz-ctx.transform, molecule-bounds) + + let (translate-x, translate-y) = if after-operator { + let (_, origin-anchor) = cetz.coordinate.resolve(cetz-ctx, last-anchor.anchor) + ( + origin-anchor.at(0) - molecule-bounds.low.at(0), + -origin-anchor.at(1) + + ( + molecule-bounds.low.at(1) + molecule-bounds.high.at(1) + ) + / 2, + ) + } else { + (0, 0) + } + let transform-matrix = cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ) + // panic(anchors) + for name in anchors { + let hold-anchors = cetz-ctx.nodes.at(name).anchors + cetz-ctx.nodes.at(name).anchors = (name) => { + if name != () { + cetz.matrix.mul4x4-vec3(transform-matrix, hold-anchors(name)) + } else { + hold-anchors(name) + } + } + } + ( + _ => ( + ctx: cetz-ctx, + drawables: cetz.drawable.apply-transform( + cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ), + drawables, + ), + ), + ) + scope({ + translate(x: translate-x, y: -translate-y) + draw-link-decoration(ctx).at(1) + on-layer(2, cetz-drawing) + let bound-rect = cetz.draw.rect( + molecule-bounds.low, + molecule-bounds.high, + name: ctx.last-name, + stroke: purple, + ) + if ctx.config.debug { + bound-rect + } else { + cetz.draw.hide(bound-rect) + } + }) + }) + } + } else if body.type == "operator" { + let (op-ctx, drawable) = operator.draw-operator(body, ctx) + after-operator = true + ctx = op-ctx + drawable + } else if body.type == "parenthesis" { + let (parenthesis-ctx, drawable) = parenthesis.draw-resonance-parenthesis(body, draw-groups, ctx) + ctx = parenthesis-ctx + drawable + } else { + panic("Unexpected element type: " + body.type) + } + ctx.first-molecule = false + } + (drawables, ctx) +} + +#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { + let config = merge-dictionaries(config, default) + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + + let bodies = preprocessing(body).at(0) + let (final-drawing, ctx) = draw-groups(ctx, bodies) + + if name == none { + final-drawing + } else { + group(name: name, anchor: mol-anchor, { + anchor("default", (0, 0)) + final-drawing + }) + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas(debug: debug, background: background, draw-skeleton(config: config, body)) +} + +#let skeletize-config(default-config) = { + let config-function(debug: false, background: none, config: (:), body) = { + skeletize(debug: debug, background: background, config: merge-dictionaries(config, default-config), body) + } + config-function +} + +#let draw-skeleton-config(default-config) = { + let config-function(config: (:), name: none, mol-anchor: none, body) = { + draw-skeleton(config: merge-dictionaries(config, default-config), name: name, mol-anchor: mol-anchor, body) + } + config-function +} + + +#let hide-drawables(elements) = { + return elements +} diff --git a/packages/preview/alchemist/0.1.10/src/drawer/branch.typ b/packages/preview/alchemist/0.1.10/src/drawer/branch.typ new file mode 100644 index 0000000000..2c9e062843 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/drawer/branch.typ @@ -0,0 +1,32 @@ +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +/// Compute the angle the branch should take in order to be "in the center" +/// of the cycle angle +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - (180deg - ctx.cycle-step-angle) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +#let draw-branch(branch, ctx, draw-fragments-and-link) = { + let angle = angles.angle-from-ctx(ctx, branch.args, cycle-angle(ctx)) + let (branch-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + branch.body, + ) + ctx = context_.update-parent-context(ctx, branch-ctx) + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.10/src/drawer/cram.typ b/packages/preview/alchemist/0.1.10/src/drawer/cram.typ new file mode 100644 index 0000000000..4b214842d8 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/drawer/cram.typ @@ -0,0 +1,61 @@ +#import "@preview/cetz:0.4.2" +#import "../utils/utils.typ" + +/// Draw a triangle between two fragments +#let cram(from, to, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.filled-cram.base-length), + ) + cetz.draw.line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: ctx.config.filled-cram.stroke), + fill: args.at("fill", default: ctx.config.filled-cram.fill), + ) +} + +/// Draw a dashed triangle between two molecules fragments +#let dashed-cram(from, to, length, ctx, cetz-ctx, args) = { + import cetz.draw: * + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.dashed-cram.base-length), + ) + let tip-length = utils.convert-length( + cetz-ctx, + args.at("tip-length", default: ctx.config.dashed-cram.tip-length), + ) + hide({ + line(name: "top", (from-x, from-y - base-length / 2), (to-x, to-y - tip-length / 2)) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + tip-length / 2), + ) + }) + let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) + let dash-gap = utils.convert-length(cetz-ctx, args.at("dash-gap", default: ctx.config.dashed-cram.dash-gap)) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(cetz-ctx, dash-width) + let length = utils.convert-length(cetz-ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.10/src/drawer/cycle.typ b/packages/preview/alchemist/0.1.10/src/drawer/cycle.typ new file mode 100644 index 0000000000..50596eb48c --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/drawer/cycle.typ @@ -0,0 +1,155 @@ +#import "@preview/cetz:0.4.2" +#import "../utils/utils.typ" +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +#let max-int = 9223372036854775807 + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * (i + 1)), + 0, + )) + } + vertex +} + +/// extrapolate the center and the radius of the cycle +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + let debug = () + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + debug += cetz.draw.circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between(cetz-ctx, (x, y), ((ox1 + ox2) / 2, (oy1 + oy2) / 2)) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ((center.at(0) / vertex.len(), center.at(1) / vertex.len()), min-radius, debug) +} + +#let draw-cycle-center-arc(ctx, name, center-arc) = { + import cetz.draw: * + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius, debug) = cycle-center-radius(ctx, cetz-ctx, vertex) + debug + if name != none { + anchor(name, center) + } + if center-arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= center-arc.at("radius", default: 0.7) + } else { + min-radius *= center-arc.at("radius", default: 0.5) + } + let start = center-arc.at("start", default: 0deg) + let end = center-arc.at("end", default: 360deg) + let delta = center-arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + arc( + center, + ..center-arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) +} + +#let draw-cycle(cycle, ctx, draw-fragments-and-link) = { + let cycle-step-angle = 360deg / cycle.faces + let angle = angles.angle-from-ctx(ctx, cycle.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ( + ctx.relative-angle == 0deg + and ctx.angle == 0deg + and not cycle.args.at( + "align", + default: false, + ) + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-fragment = none + if ctx.last-anchor.type == "fragment" { + first-fragment = ctx.last-anchor.name + if first-fragment not in ctx.hooks { + ctx.hooks.insert(first-fragment, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in cycle.args { + name = cycle.args.at("name") + record-vertex = true + } else if "arc" in cycle.args { + record-vertex = true + } + let (cycle-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: cycle.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-fragment: first-fragment, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + cycle.body, + ) + ctx = context_.update-parent-context(ctx, cycle-ctx) + if record-vertex { + drawing += draw-cycle-center-arc(cycle-ctx, name, cycle.args.at("arc", default: none)) + } + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.1.10/src/drawer/fragment.typ b/packages/preview/alchemist/0.1.10/src/drawer/fragment.typ new file mode 100644 index 0000000000..435238471d --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/drawer/fragment.typ @@ -0,0 +1,161 @@ +#import "../utils/context.typ": * +#import "../utils/anchors.typ": * +#import "@preview/cetz:0.4.2" + +#let draw-fragment-text(ctx, mol, pos) = { + import cetz.draw: * + let id = 0 + let no-anchor-id = 0 + let last-anchor-name = none + for (eq, anchored) in mol.atoms { + let name = str(id) + if not anchored { + name += str(no-anchor-id) + no-anchor-id += 1 + } + + let color = if mol.colors != none { + if type(mol.colors) == color { + mol.colors + } else { + mol.colors.at(calc.min(id, mol.colors.len() - 1)) + } + } else { + ctx.config.fragment-color + } + let fragment-font = ctx.config.fragment-font + + // draw atoms of the group one after the other from left to right + content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "mid-west" + }, + ( + if last-anchor-name == none { + pos + } else if mol.vertical { + (to: last-anchor-name + ".south", rel: (0, -.2em)) + } else { + last-anchor-name + ".mid-east" + } + ), + auto-scale: false, + { + show math.equation: math.upright + set text(fill: color) if color != none + set text(font: fragment-font) if fragment-font != none + eq + }, + ) + if anchored { + id += 1 + } + last-anchor-name = name + } +} + +#let draw-fragment-lewis(ctx, group-name, count, lewis) = { + if lewis.len() == 0 { + return () + } + import cetz.draw: * + get-ctx(cetz-ctx => { + for (id, (angle: lewis-angle, radius, draw)) in lewis.enumerate() { + if (lewis-angle == none) { + lewis-angle = ctx.config.lewis.angle + } + if (radius == none) { + radius = ctx.config.lewis.radius + } + let lewis-angle = angles.angle-correction(lewis-angle) + let mol-id = if angles.angle-in-range-inclusive(lewis-angle, 90deg, 270deg) { + 0 + } else { + count - 1 + } + let anchor = fragment-anchor( + ctx, + cetz-ctx, + lewis-angle, + group-name, + str(mol-id), + margin: radius, + ) + scope({ + set-origin(anchor) + rotate(lewis-angle) + draw(ctx, cetz-ctx) + }) + } + }) +} + +#let draw-fragment-elements(mol, ctx) = { + let name = mol.name + if name in ctx.hooks { + panic("Molecule fragment with name " + name + " already exists : " + ctx.hooks.keys().join(", ")) + } + ctx.hooks.insert(name, mol) + + let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("west", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-fragment-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let group-anchor = link-fragment-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (group-anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule fragment must be linked to a coord or a link") + } + ctx = context_.set-last-anchor( + ctx, + (type: "fragment", name: name, count: mol.at("count"), vertical: mol.vertical), + ) + if (side) { + ctx.id += 1 + } + ( + ctx, + { + import cetz.draw: * + group( + anchor: if side { + group-anchor + } else { + "from" + str(ctx.id) + }, + name: name, + { + anchor("default", coord) + draw-fragment-text(ctx, mol, coord) + if not side { + anchor("from" + str(ctx.id), group-anchor) + } + }, + ) + draw-fragment-lewis(ctx, name, mol.count, mol.at("lewis")) + }, + ) +} + +#let draw-fragment(element, ctx) = { + if ctx.first-branch { + panic("A molecule fragment can not be the first element in a cycle") + } + let (ctx, drawing) = draw-fragment-elements(element, ctx) + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.10/src/drawer/hide.typ b/packages/preview/alchemist/0.1.10/src/drawer/hide.typ new file mode 100644 index 0000000000..b4e940f2d4 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/drawer/hide.typ @@ -0,0 +1,12 @@ +#import "@preview/cetz:0.4.2" + +#let draw-hide(hide, ctx, draw-molecules-and-link) = { + ctx.hide = true + let (hide-ctx, drawing, cetz-rec) = draw-molecules-and-link( + ctx, + hide.body, + ) + hide-ctx.hide = false + drawing = cetz.draw.hide(drawing, bounds: hide.bounds) + (hide-ctx, drawing, cetz-rec) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.10/src/drawer/hook.typ b/packages/preview/alchemist/0.1.10/src/drawer/hook.typ new file mode 100644 index 0000000000..1c5dafb9d1 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/drawer/hook.typ @@ -0,0 +1,14 @@ + +#let draw-hook(hook, ctx) = { + if hook.name in ctx.hooks { + panic("Hook " + hook.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.name + "-end-anchor")) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + ctx +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.10/src/drawer/link.typ b/packages/preview/alchemist/0.1.10/src/drawer/link.typ new file mode 100644 index 0000000000..f6385b7546 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/drawer/link.typ @@ -0,0 +1,104 @@ +#import "../utils/angles.typ" +#import "../utils/anchors.typ": * + +#let create-link-decorations-anchors(link, ctx) = { + let link-angle = 0deg + let to-name = none + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-fragment != none { + to-name = ctx.first-fragment + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angles.angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = angles.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angles.angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let from-pos = if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "fragment" { + from-connection = link-fragment-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + fragment-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name") + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = context_.set-last-anchor( + ctx, + ( + type: "link", + hide: ctx.hide, + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: to-name, + to: to-connection, + angle: link-angle, + over: link.at("over", default: none), + draw: link.draw, + ), + ) + ( + ctx, + { + hide({ + circle(name: link-name + "-start-anchor", from-pos, radius: .25em) + }, bounds: ctx.first-draw and not ctx.first-molecule) + let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + line(from-pos, end-anchor, stroke: blue + .1em) + } + hide( + { + circle(name: link-name + "-end-anchor", end-anchor, radius: .25em) + }, + bounds: true, + ) + }, + ) +} + +#let draw-link(link, ctx) = { + ctx.first-branch = false + let drawing + (ctx, drawing) = create-link-decorations-anchors(link, ctx) + ctx.faces-count += 1 + if link.links.len() != 0 { + ctx.hooks-links.push((link.links, ctx.last-anchor.name, false)) + } + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.10/src/drawer/operator.typ b/packages/preview/alchemist/0.1.10/src/drawer/operator.typ new file mode 100644 index 0000000000..edfc0d0155 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/drawer/operator.typ @@ -0,0 +1,36 @@ +#import "@preview/cetz:0.4.2": draw, process, util + +#let draw-operator(operator, ctx) = { + import draw: * + + let op-name = operator.name + ctx.last-anchor = (type: "coord", anchor: (rel: (operator.margin, 0), to: (name: op-name, anchor: "east"))) + + ( + ctx, + get-ctx(cetz-ctx => { + + let west-previous-mol-anchor = (name: ctx.last-name, anchor: "east") + + let east-op-anchor = (rel: (operator.margin, 0), to: west-previous-mol-anchor) + + let op = if (operator.op == none) { + "" + } else { + operator.op + } + + content( + name: op-name, + anchor: "west", + east-op-anchor, + op, + ) + + if (ctx.config.debug) { + circle(west-previous-mol-anchor, radius: 0.05, fill: yellow, stroke: none) + circle(ctx.last-anchor.anchor, radius: 0.05, fill: yellow, stroke: none) + } + }), + ) +} diff --git a/packages/preview/alchemist/0.1.10/src/drawer/parenthesis.typ b/packages/preview/alchemist/0.1.10/src/drawer/parenthesis.typ new file mode 100644 index 0000000000..cb751785ab --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/drawer/parenthesis.typ @@ -0,0 +1,215 @@ +#import "../utils/utils.typ" +#import "@preview/cetz:0.4.2" + +#let left-parenthesis-anchor(parenthesis, ctx) = { + let anchor = if parenthesis.body.at(0).type == "fragment" { + let name = parenthesis.body.at(0).name + parenthesis.body.at(0).name = name + (name: name, anchor: "west") + } else if parenthesis.body.at(0).type == "link" { + let name = parenthesis.body.at(0).at("name") + parenthesis.body.at(0).name = name + (name + "-start-anchor", 45%, name + "-end-anchor") + } else if not left { } else { + panic("The first element of a parenthesis must be a molecule fragment or a link") + } + (ctx, parenthesis, anchor) +} + +#let right-parenthesis-anchor(parenthesis, ctx) = { + let right-name = parenthesis.at("right") + let right-type = "" + if right-name != none { + right-type = utils.get-element-type(parenthesis.body, right-name) + if right-type == none { + panic("The right element of the parenthesis does not exist") + } + } else { + right-type = parenthesis.body.at(-1).type + right-name = parenthesis.body.at(-1).at("name", default: none) + } + + let anchor = if right-type == "fragment" { + (name: right-name, anchor: "east") + } else if right-type == "link" { + (right-name + "-start-anchor", 55%, right-name + "-end-anchor") + } else { + panic("The last element of a parenthesis must be a molecule fragment or a link but got " + right-type) + } + (ctx, parenthesis, anchor) +} + +#let parenthesis-content(parenthesis, height, cetz-ctx) = { + let block = block(height: height * cetz-ctx.length * 1.2, width: 0pt) + let left-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($parenthesis.l block$, size: 100%) + } + let right-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($block parenthesis.r$, size: 100%) + } + + let right-parenthesis-with-attach = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.attach(right-parenthesis, br: parenthesis.br, tr: parenthesis.tr) + } + + (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) +} + +#let draw-parenthesis(parenthesis, ctx, draw-molecules-and-link) = { + let (parenthesis-ctx, drawing, cetz-rec) = draw-molecules-and-link( + ctx, + parenthesis.body, + ) + ctx = parenthesis-ctx + + + drawing += { + import cetz.draw: * + get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + { + drawing + }, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + + let sub-height = utils.bounding-box-height(sub-bounds) + let sub-v-mid = sub-bounds.low.at(1) - sub-height / 2 + + let sub-width = utils.bounding-box-width(sub-bounds) + + let (ctx, parenthesis, left-anchor) = left-parenthesis-anchor(parenthesis, ctx) + + let (ctx, parenthesis, right-anchor) = right-parenthesis-anchor(parenthesis, ctx) + + let height = parenthesis.at("height") + if height == none { + if not parenthesis.align { + panic("You must specify the height of the parenthesis if they are not aligned") + } + height = sub-height + } else { + height = utils.convert-length(cetz-ctx, height) + } + + let (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) = parenthesis-content( + parenthesis, + height, + cetz-ctx, + ) + + let (_, (lx, ly, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, left-anchor) + let (_, (rx, ry, _)) = cetz.coordinate.resolve(cetz-ctx, update: false, right-anchor) + + if ctx.config.debug { + circle((lx, ly), radius: 1pt, fill: orange, stroke: orange) + circle((rx, ry), radius: 1pt, fill: orange, stroke: orange) + rect((lx, sub-bounds.low.at(1)), (rx, sub-bounds.high.at(1)), stroke: green) + line((lx, sub-v-mid), (rx, sub-v-mid), stroke: green) + } + + let hoffset = calc.abs(sub-width - calc.abs(rx - lx)) + + if parenthesis.align { + ly -= ly - sub-v-mid + ry = ly + } else if type(parenthesis.yoffset) == array { + if parenthesis.yoffset.len() != 2 { + panic("The parenthesis yoffset must be a list of length 2 or a number") + } + ly += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(0)) + ry += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(1)) + } else if parenthesis.yoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.yoffset) + ly += offset + ry += offset + } + + if type(parenthesis.xoffset) == array { + if parenthesis.xoffset.len() != 2 { + panic("The parenthesis xoffset must be a list of length 2 or a number") + } + lx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(0)) + rx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(1)) + } else if parenthesis.xoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.xoffset) + lx -= offset + rx += offset + } + + let right-bounds = cetz.process.many(cetz-ctx, content((0, 0), right-parenthesis, auto-scale: false)).bounds + let right-with-attach-bounds = cetz + .process + .many(cetz-ctx, content((0, 0), right-parenthesis-with-attach, auto-scale: false)) + .bounds + let right-voffset = calc.abs(right-bounds.low.at(1) - right-with-attach-bounds.low.at(1)) + if parenthesis.tr != none and parenthesis.br != none { + right-voffset /= 2 + } else if (parenthesis.tr != none) { + right-voffset *= -1 + } + + ( + _ => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + content((lx, ly), anchor: "mid-east", left-parenthesis, auto-scale: false) + content((rx, ry - right-voffset), anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }) + } + + (ctx, drawing, cetz-rec) +} + + +#let draw-resonance-parenthesis(parenthesis, draw-function, ctx) = { + import cetz.draw: * + let left-name = "parenthesis-" + str(ctx.id) + let right-name = "parenthesis-" + str(ctx.id + 1) + ctx.id += 2 + let last-anchor = ctx.last-anchor + let (drawing, ctx) = draw-function(ctx, parenthesis.body, after-operator: true) + + let drawing = get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + drawing, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + let height = parenthesis.height + if height == none { + height = utils.bounding-box-height(sub-bounds) + } else { + height = utils.convert-length(cetz-ctx, height) + } + let width = utils.bounding-box-width(sub-bounds) + + let (left-parenthesis, _, right-parenthesis-with-attach) = parenthesis-content(parenthesis, height, cetz-ctx) + + let right-anchor = (rel: (width, 0), to: (name: left-name, anchor: "east")) + on-layer( + 1, + { + content(last-anchor.anchor, name: left-name, anchor: "mid-east", left-parenthesis, auto-scale: false) + content(right-anchor, name: right-name, anchor: "mid-west", right-parenthesis-with-attach, auto-scale: false) + }, + ) + ( + ctx => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + }) + + ctx.last-anchor = (type: "coord", anchor: (name: right-name, anchor: "mid-east")) + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.1.10/src/elements/fragment.typ b/packages/preview/alchemist/0.1.10/src/elements/fragment.typ new file mode 100644 index 0000000000..be7f1e34c6 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/elements/fragment.typ @@ -0,0 +1,106 @@ +#let split-equation(mol, equation: false, split-charge: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + let count = 0 + for m in mol { + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + if last-number { + result.at(-1) = (result.at(-1).at(0) + m, result.at(-1).at(1)) + } else { + result.push((m, true)) + } + last-number = false + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in fragment fragment") + } + last-number = true + result.push((m, true)) + } else { + panic("Invalid fragment fragment content") + } + } else if m.func() == math.lr { + result.push((m, true)) + last-number = false + } else if m.func() == math.attach { + if split-charge { + result.push((math.attach("", bl: m.at("bl", default: none), tl: m.at("tl", default: none)), false)) + result.push((math.attach(m.base, b: m.at("b", default: none), t: m.at("t", default: none)), true)) + result.push((math.attach("", br: m.at("br", default: none), tr: m.at("tr", default: none)), false)) + } else { + result.push((m, true)) + } + last-number = false + } else if m == [ ] { + continue + } else { + panic("Invalid fragment fragment content") + } + count += 1 + } + (result, count) +} + +#let fragment-cor-regex = "[0-9]*[A-Z][a-z]*'*" +#let exponent-regex = "((?:[0-9]+(?:\\+|\\-)?)|[A-Z]|\\+|\\-)" +#let exponent-base-regex = "(?:(\\^|_)" + exponent-regex + ")?(?:(\\^|_)" + exponent-regex + ")?" +#let fragment-regex = regex("^ *(" + fragment-cor-regex + ")" + exponent-base-regex) + +#let make-fragment-text(text, indexed) = (math.equation(eval(text, mode: "math")), indexed) + +#let split-fragment-string(mol, split-charge: false) = { + let aux(str) = { + let match = str.match(fragment-regex) + if match == none { + panic(str + " is not a valid fragment") + } else if match.captures.at(1) == match.captures.at(3) and match.captures.at(1) != none { + panic("You cannot use an exponent and a subscript twice") + } + let eq = "\"" + match.captures.at(0) + "\"" + + let has-exponent = match.captures.at(2) != none + let has-subscript = match.captures.at(4) != none + + let charge = "" + if has-exponent { + charge += match.captures.at(1) + "(" + match.captures.at(2) + ")" + } + if match.captures.at(3) != none { + charge += match.captures.at(3) + "(" + match.captures.at(4) + ")" + } + if charge != "" { + if split-charge { + charge = "\"\"" + charge + eq = (make-fragment-text(eq, true), make-fragment-text(charge, false)) + } else { + eq += charge + eq = (make-fragment-text(eq, true),) + } + } else { + eq = (make-fragment-text(eq, true),) + } + + (eq, match.end) + } + + let count = 0 + let fragments = while not mol.len() == 0 { + count += 1 + let (eq, end) = aux(mol) + mol = mol.slice(end) + eq + } + (fragments, count) +} diff --git a/packages/preview/alchemist/0.1.10/src/elements/lewis.typ b/packages/preview/alchemist/0.1.10/src/elements/lewis.typ new file mode 100644 index 0000000000..7b90c19c03 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/elements/lewis.typ @@ -0,0 +1,135 @@ +#import "@preview/cetz:0.4.2" + +/// Create a lewis function that is then used to draw a lewis +/// formulae element around the fragment +/// +/// - draw-function (function): The function that will be used to draw the lewis element. It should takes three arguments: the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-lewis(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Lewis function takes no positional arguments") + } + let args = args.named() + let angle = args.at("angle", default: none) + let radius = args.at("radius", default: none) + ( + angle: angle, + radius: radius, + draw: (ctx, cetz-ctx) => draw-function(ctx, cetz-ctx, args) + ) + } +} + +/// draw a sigle electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// +/// The position of the electron is set by the `offset` argument. Available values are: +/// - "top": the electron is placed above the fragment center line +/// - "bottom": the electron is placed below the fragment center line +/// - "center": the electron is placed at the fragment center line +/// +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-single(offset: "top"), +/// )) +/// single(angle:-2) +/// fragment("B", lewis:( +/// lewis-single(offset: "bottom"), +/// )) +/// single(angle:-2) +/// fragment("C", lewis:( +/// lewis-single(offset: "center"), +/// )) +/// }) +/// ```) +#let lewis-single = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-single.radius) + let gap = args.at("gap", default: ctx.config.lewis-single.gap) + let offset = args.at("offset", default: ctx.config.lewis-single.offset) + let gap = if offset == "top" { + gap + } else if offset == "bottom" { + -gap + } else if offset == "center" { + 0 + } else { + panic("Invalid position, expected 'top', 'bottom' or 'center'") + } + let fill = args.at("fill", default: ctx.config.lewis-single.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-single.stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-double(), +/// lewis-double(angle: 90deg), +/// lewis-double(angle: 180deg), +/// lewis-double(angle: -90deg) +/// )) +/// }) +/// ```) +#let lewis-double = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-double.radius) + let gap = args.at("gap", default: ctx.config.lewis-double.gap) + let fill = args.at("fill", default: ctx.config.lewis-double.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-double.stroke) + circle((0, -gap), radius: radius, fill: fill, stroke: stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron liked by a single line +/// +/// It is possible to change the length of the line with the `lenght` argument. +/// It is also possible to change the `stroke` agument +/// #example(``` +/// #skeletize({ +/// fragment("B", lewis:( +/// lewis-line(angle: 45deg), +/// lewis-line(angle: 135deg), +/// lewis-line(angle: -45deg), +/// lewis-line(angle: -135deg) +/// )) +/// }) +/// ```) +#let lewis-line = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let length = args.at("length", default: ctx.config.lewis-line.length) + let stroke = args.at("stroke", default: ctx.config.lewis-line.stroke) + line((0, -length / 2), (0, length / 2), stroke: stroke) +}) + + +/// Draw a rectangle to denote a lone pair of electrons +/// +/// It is possible to change the height and width of the rectangle with the `height` and `width` arguments. +/// It is also possible to change the `fill` and `stroke` arguments +/// #example(``` +/// #skeletize({ +/// fragment("C", lewis:( +/// lewis-rectangle(), +/// lewis-rectangle(angle: 180deg) +/// )) +/// }) +/// ```) +#let lewis-rectangle = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let height = args.at("height", default: ctx.config.lewis-rectangle.height) + let width = args.at("width", default: ctx.config.lewis-rectangle.width) + let fill = args.at("fill", default: ctx.config.lewis-rectangle.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-rectangle.stroke) + rect((-width / 2, -height / 2), (width / 2, height / 2), fill: fill, stroke: stroke) +}) diff --git a/packages/preview/alchemist/0.1.10/src/elements/links.typ b/packages/preview/alchemist/0.1.10/src/elements/links.typ new file mode 100644 index 0000000000..6e78d836e2 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/elements/links.typ @@ -0,0 +1,314 @@ +#import "@preview/cetz:0.4.2" +#import "../drawer/cram.typ": * +#import "../utils/utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes four arguments: the length of the link, the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, cetz-ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, cetz-ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single(stroke: red + 5pt) +/// fragment("B") +/// }) +///```) +#let single = build-link((length, ctx, _, args) => { + import cetz.draw: * + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.single.stroke)) +}) + +/// Draw a double line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// fragment("B") +/// }) +///```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double(offset: "right") +/// fragment("B") +/// double(offset: "left") +/// fragment("C") +/// double(offset: "center") +/// fragment("D") +/// }) +///```) +#let double = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.double.gap)) / 2 + let offset = args.at("offset", default: ctx.config.double.offset) + let coeff = args.at("offset-coeff", default: ctx.config.double.offset-coeff) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + let gap-offset = if offset == "right" { + -gap + } else if offset == "left" { + gap + } else if offset == "center" { + 0 + } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, -gap + gap-offset), (x + length * coeff, -gap + gap-offset)) + } else { + ((0, -gap + gap-offset), (length, -gap + gap-offset)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, gap + gap-offset), (x + length * coeff, gap + gap-offset)) + } else { + ((0, gap + gap-offset), (length, gap + gap-offset)) + }, + stroke: args.at("stroke", default: ctx.config.double.stroke), + ) +}) + +/// Draw a triple line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// fragment("B") +/// }) +///```) +#let triple = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length(cetz-ctx, args.at("gap", default: ctx.config.triple.gap)) + line((0, 0), (length, 0), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + line((0, -gap), (length, -gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) + line((0, gap), (length, gap), stroke: args.at("stroke", default: ctx.config.triple.stroke)) +}) + +/// Draw a filled cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, cetz-ctx, args) => cram( + (0, 0), + (length, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a filled cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, cetz-ctx, args) => cram( + (length, 0), + (0, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a hollow cram between two fragments with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((0, 0), (length, 0), ctx, cetz-ctx, args) +}) + +/// Draw a hollow cram between two fragments with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((length, 0), (0, 0), ctx, cetz-ctx, args) +}) + +/// Draw a dashed cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the arrow base length with the `base-length` argument and the tip length with the `tip-length` argument. +// You can also change distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// tip-length: 1em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-right = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + cetz-ctx, + args, +)) + +/// Draw a dashed cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + cetz-ctx, + args, +)) + + +/// Draw a plus sign between two fragments +/// #example(```` +/// #skeletize({ +/// fragment("A") +/// plus() +/// fragment("B") +/// }) +/// ````) +/// You can change the filling, size and stroke of the glyph with the `fill`, `size` and `stoke` arguments. The default values are the parent `text` parameters. +#let plus = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + content( + anchor: "mid", + (length / 2, 0), + { + set text(fill: args.fill) if "fill" in args + set text(stroke: args.stroke) if "stroke" in args + set text(size: args.size) if "size" in args + text($+$) + } + ) +}) \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.10/src/utils/anchors.typ b/packages/preview/alchemist/0.1.10/src/utils/anchors.typ new file mode 100644 index 0000000000..12ce5afe56 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/utils/anchors.typ @@ -0,0 +1,296 @@ +#import "angles.typ" +#import "context.typ" as context_ +#import "utils.typ": * +#import "@preview/cetz:0.4.2" +#import cetz.draw: * + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +/// Calculate an anchor position around a fragment using an ellipse +/// at a given angle +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - cetz-ctx (cetz-ctx): the cetz context +/// - angle (float, int, angle): the angle of the anchor +/// - fragment (string): the fragment name +/// - id (string): the fragment subpart id +/// - margin (length, none): the margin around the fragment +/// -> anchor: the anchor position around the fragment +#let fragment-anchor(ctx, cetz-ctx, angle, fragment, id, margin: none) = { + let angle = angles.angle-correction(angle) + let fragment-margin = if margin == none { + ctx.config.fragment-margin + } else { + margin + } + fragment-margin = convert-length(cetz-ctx, fragment-margin) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "mid")), + ) + let (a, b) = if angles.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, fragment-margin, fragment, id) + } else { + anchor-south-east(cetz-ctx, center, fragment-margin, fragment, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if angles.angle-in-range-inclusive(angle, 0deg, 90deg) or angles.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +/// Return the index to choose if the link connection is not overridden +#let link-fragment-index(angle, end, count, vertical) = { + if not end { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let fragment-link-anchor(name, id, count) = { + if count <= id { + panic("The last fragment only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "mid")) +} + +#let link-fragment-anchor(name: none, id, count) = { + if id >= count { + panic("This fragment only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the fragment to link to must be defined") + } + if name == none { + (name: str(id), anchor: "mid") + } else { + (name: name, anchor: (str(id), "mid")) + } +} + + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "mid") + if link.to == none or link.from == none { + let angle = link.at( + "angle", + default: angles.angle-between(cetz-ctx, link.from-pos, to-pos), + ) + link.angle = angle + if link.from == none { + link.from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + if link.from == -1 { + link.from = 0 + } + if link.to == -1 { + link.to = ctx.hooks.at(link.to-name).count - 1 + } + let start = fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)) + let end = fragment-anchor(ctx, cetz-ctx, link.angle + 180deg, link.to-name, str(link.to)) + ((start, end), angles.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "mid"), + ), + ) + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "mid")), + ), + ) + } + let end-anchor = fragment-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + angles.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + fragment-anchor(ctx, cetz-ctx, link.angle, link.from-name, str(link.from)), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = angles.angle-correction(angles.angle-between(cetz-ctx, link.from-pos, hook.hook)) + let from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = fragment-anchor(ctx, cetz-ctx, angle, link.from-name, str(from)) + ( + ( + start-anchor, + hook.hook, + ), + angles.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + angles.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + diff --git a/packages/preview/alchemist/0.1.10/src/utils/angles.typ b/packages/preview/alchemist/0.1.10/src/utils/angles.typ new file mode 100644 index 0000000000..60e111292f --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/utils/angles.typ @@ -0,0 +1,98 @@ +#import "@preview/cetz:0.4.2" + +/// Convert any angle to an angle between 0deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a += 360 + } + + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +/// Check if the angle is in the range ]from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +/// Check if the angle is in the range [from, to] +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +/// +/// - ctx (cetz-ctx): The cetz context +/// - from (anchor): The first anchor +/// - to (anchor): The second anchor +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// Calculate the angle from an object with "relative", "absolute" or "angle" key +/// according to the alchemist context (see the manual to see how angles are calculated) +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - object (dict): the object to get the angle from +/// - default (angle): the default angle +/// -> the calculated angle +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +/// Overwrite the offset based on the angle +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} diff --git a/packages/preview/alchemist/0.1.10/src/utils/context.typ b/packages/preview/alchemist/0.1.10/src/utils/context.typ new file mode 100644 index 0000000000..4021bb5177 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/utils/context.typ @@ -0,0 +1,33 @@ +#let update-parent-context(parent-ctx, ctx) = { + let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { + ( + ..parent-ctx.last-anchor, + drew: true, + ) + } else { + parent-ctx.last-anchor + } + ( + ..parent-ctx, + last-anchor: last-anchor, + hooks: ctx.hooks, + hooks-links: ctx.hooks-links, + links: ctx.links, + ) +} + +/// Set the last anchor in the context to the given anchor and save it if needed +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" { + let drew = ctx.last-anchor.at("drew", default: false) + if drew and anchor.type == "link" and anchor.name == ctx.last-anchor.name { + drew = false + let _ = ctx.links.pop() + } + if not drew { + ctx.links.push(ctx.last-anchor) + } + ctx.last-anchor.drew = true + } + (..ctx, last-anchor: anchor) +} diff --git a/packages/preview/alchemist/0.1.10/src/utils/utils.typ b/packages/preview/alchemist/0.1.10/src/utils/utils.typ new file mode 100644 index 0000000000..72220a4987 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/src/utils/utils.typ @@ -0,0 +1,75 @@ +#import "@preview/cetz:0.4.2" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + return if type(num) == length { + float(num.to-absolute() / ctx.length) + } else if type(num) == ratio { + num + } else { + float(num) + } +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} + +/// merge two imbricated dictionaries together +/// The second dictionary is the default value if the key is not present in the first dictionary +#let merge-dictionaries(dict1, default) = { + let result = default + for (key, value) in dict1 { + if type(value) == dictionary { + result.insert(key, merge-dictionaries(value, default.at(key))) + } else { + result.insert(key, value) + } + } + result +} + + +/// get the type of an element by its name +/// +/// - body (drawable): the chemfig body of a molecule +/// - name (string): the name of the element to get the type +/// -> string +#let get-element-type(body, name) = { + for element in body { + if type(element) != dictionary { + continue + } + if "name" in element and element.name == name { + return element.type + } + if element.type == "branch" or element.type == "cycle" or element.type == "parenthesis" { + let type = get-element-type(element.body, name) + if type != none { + return type + } + } + } + none +} + +/// Calculate the height of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-height(bounds) = { + calc.abs(bounds.high.at(1) - bounds.low.at(1)) +} + +/// Calculate the width of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-width(bounds) = { + calc.abs(bounds.high.at(0) - bounds.low.at(0)) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.1.10/typst.toml b/packages/preview/alchemist/0.1.10/typst.toml new file mode 100644 index 0000000000..640aa80635 --- /dev/null +++ b/packages/preview/alchemist/0.1.10/typst.toml @@ -0,0 +1,13 @@ +[package] +name = "alchemist" +version = "0.1.10" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.13.1" +repository = "https://github.com/Typsium/alchemist" +description = "A package to render skeletal formulas using CeTZ" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization", "paper"] +disciplines = ["education", "chemistry", "biology"] +keywords = ["chemistry", "chemical", "formula", "molecule", "chemiviz", "structure", "organic", "lewis", "bond", "aromatic"] From ee44eecf0e3bd2b3e1e4ab5f717cd9e5de89fea6 Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Mon, 25 May 2026 22:34:44 +0200 Subject: [PATCH 23/24] fix readme --- packages/preview/alchemist/0.1.10/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/preview/alchemist/0.1.10/README.md b/packages/preview/alchemist/0.1.10/README.md index 10da6222b4..c6d69a8973 100644 --- a/packages/preview/alchemist/0.1.10/README.md +++ b/packages/preview/alchemist/0.1.10/README.md @@ -1,5 +1,5 @@ [![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FTypsium%2Falchemist%2Fmaster%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://github.com/Typsium/alchemist) -[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/alchemist/blob/master/LICENSE) +[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/alchemist/0.1.10/LICENSE) [![User Manual](https://img.shields.io/badge/manual-.pdf-purple)](https://raw.githubusercontent.com/Typsium/alchemist/0.1.10/doc/manual.pdf) # alchemist @@ -45,7 +45,7 @@ Alchemist is a typst package to draw skeletal formulae. It is based on the [chem ) }) ```` -![links](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic1/ref/1.png) +![links](https://raw.githubusercontent.com/Typsium/alchemist/0.1.10/tests/README-graphic1/ref/1.png) Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: @@ -71,7 +71,7 @@ Alchemist uses cetz to draw the molecules. This means that you can draw cetz sha ) }) ```` -![cetz](https://raw.githubusercontent.com/Typsium/alchemist/master/tests/README-graphic2/ref/1.png) +![cetz](https://raw.githubusercontent.com/Typsium/alchemist/0.1.10/tests/README-graphic2/ref/1.png) ## Usage From 00336fb3e10aba9bb42b7b4d78e9ec6cb5b4aa62 Mon Sep 17 00:00:00 2001 From: Robotechnic Date: Tue, 9 Jun 2026 17:16:47 +0200 Subject: [PATCH 24/24] Upload alchemist v0.2.0 --- packages/preview/alchemist/0.2.0/LICENSE | 22 + packages/preview/alchemist/0.2.0/README.md | 163 ++++++ packages/preview/alchemist/0.2.0/lib.typ | 284 ++++++++++ .../alchemist/0.2.0/src/cetz/process.typ | 96 ++++ .../preview/alchemist/0.2.0/src/default.typ | 61 +++ .../preview/alchemist/0.2.0/src/drawer.typ | 510 ++++++++++++++++++ .../alchemist/0.2.0/src/drawer/branch.typ | 34 ++ .../alchemist/0.2.0/src/drawer/cram.typ | 68 +++ .../alchemist/0.2.0/src/drawer/cycle.typ | 169 ++++++ .../alchemist/0.2.0/src/drawer/fragment.typ | 173 ++++++ .../alchemist/0.2.0/src/drawer/hide.typ | 14 + .../alchemist/0.2.0/src/drawer/hook.typ | 17 + .../alchemist/0.2.0/src/drawer/link.typ | 107 ++++ .../alchemist/0.2.0/src/drawer/operator.typ | 47 ++ .../0.2.0/src/drawer/parenthesis.typ | 274 ++++++++++ .../alchemist/0.2.0/src/elements/fragment.typ | 132 +++++ .../alchemist/0.2.0/src/elements/lewis.typ | 140 +++++ .../alchemist/0.2.0/src/elements/links.typ | 370 +++++++++++++ .../alchemist/0.2.0/src/utils/anchors.typ | 326 +++++++++++ .../alchemist/0.2.0/src/utils/angles.typ | 98 ++++ .../alchemist/0.2.0/src/utils/context.typ | 33 ++ .../alchemist/0.2.0/src/utils/utils.typ | 75 +++ packages/preview/alchemist/0.2.0/typst.toml | 13 + 23 files changed, 3226 insertions(+) create mode 100644 packages/preview/alchemist/0.2.0/LICENSE create mode 100644 packages/preview/alchemist/0.2.0/README.md create mode 100644 packages/preview/alchemist/0.2.0/lib.typ create mode 100644 packages/preview/alchemist/0.2.0/src/cetz/process.typ create mode 100644 packages/preview/alchemist/0.2.0/src/default.typ create mode 100644 packages/preview/alchemist/0.2.0/src/drawer.typ create mode 100644 packages/preview/alchemist/0.2.0/src/drawer/branch.typ create mode 100644 packages/preview/alchemist/0.2.0/src/drawer/cram.typ create mode 100644 packages/preview/alchemist/0.2.0/src/drawer/cycle.typ create mode 100644 packages/preview/alchemist/0.2.0/src/drawer/fragment.typ create mode 100644 packages/preview/alchemist/0.2.0/src/drawer/hide.typ create mode 100644 packages/preview/alchemist/0.2.0/src/drawer/hook.typ create mode 100644 packages/preview/alchemist/0.2.0/src/drawer/link.typ create mode 100644 packages/preview/alchemist/0.2.0/src/drawer/operator.typ create mode 100644 packages/preview/alchemist/0.2.0/src/drawer/parenthesis.typ create mode 100644 packages/preview/alchemist/0.2.0/src/elements/fragment.typ create mode 100644 packages/preview/alchemist/0.2.0/src/elements/lewis.typ create mode 100644 packages/preview/alchemist/0.2.0/src/elements/links.typ create mode 100644 packages/preview/alchemist/0.2.0/src/utils/anchors.typ create mode 100644 packages/preview/alchemist/0.2.0/src/utils/angles.typ create mode 100644 packages/preview/alchemist/0.2.0/src/utils/context.typ create mode 100644 packages/preview/alchemist/0.2.0/src/utils/utils.typ create mode 100644 packages/preview/alchemist/0.2.0/typst.toml diff --git a/packages/preview/alchemist/0.2.0/LICENSE b/packages/preview/alchemist/0.2.0/LICENSE new file mode 100644 index 0000000000..a85204e590 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/alchemist/0.2.0/README.md b/packages/preview/alchemist/0.2.0/README.md new file mode 100644 index 0000000000..5b1043f009 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/README.md @@ -0,0 +1,163 @@ +[![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FTypsium%2Falchemist%2Fmaster%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://github.com/Typsium/alchemist) +[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/alchemist/blob/0.2.0/LICENSE) +[![User Manual](https://img.shields.io/badge/manual-.pdf-purple)](https://raw.githubusercontent.com/Typsium/alchemist/0.2.0/doc/manual.pdf) + +# alchemist + +Alchemist is a typst package to draw skeletal formulae. It is based on the [chemfig](https://ctan.org/pkg/chemfig) package. The main goal of alchemist is not to reproduce one-to-one chemfig. Instead, it aims to provide an interface to achieve the same results in Typst. + + +````typ +#skeletize({ + fragment(name: "A", "A") + single() + fragment("B") + branch({ + single(angle: 1) + fragment( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + fragment(name: "X", "X") + }) + branch({ + single(angle: -1) + fragment("Y") + single() + fragment( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + fragment( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +```` +![links](https://raw.githubusercontent.com/Typsium/alchemist/0.2.0/tests/README-graphic1/ref/1.png) + +Alchemist uses cetz to draw the molecules. This means that you can draw cetz shapes in the same canvas as the molecules. Like this: + + +````typ +#skeletize({ + import cetz.draw: * + double(absolute: 30deg, name: "l1") + single(absolute: -30deg, name: "l2") + fragment("X", name: "X") + hobby( + "l1.50%", + ("l1.start", 0.5, 90deg, "l1.end"), + "l1.start", + stroke: (paint: red, dash: "dashed"), + mark: (end: ">"), + ) + hobby( + (to: "X.north", rel: (0, 1pt)), + ("l2.end", 0.4, -90deg, "l2.start"), + "l2.50%", + mark: (end: ">"), + ) +}) +```` +![cetz](https://raw.githubusercontent.com/Typsium/alchemist/0.2.0/tests/README-graphic2/ref/1.png) + +## Usage + +To start using alchemist, just use the following code: + +```typ +#import "@preview/alchemist:0.2.0": * + +#skeletize({ + // Your molecule here +}) +``` + +For more information, check the [manual](https://raw.githubusercontent.com/Typsium/alchemist/0.2.0/doc/manual.pdf). + +## Tests + +The test suite is managed with [tytanic](https://github.com/typst-community/tytanic). + +## Changelog + +### 0.2.0 + +- Updated cetz to version 0.5.2 +- Updated to compiler 0.14.0 +- Rename `plus` link to `plus-link` to avoid confusion with the `plus` typst symbol +- Adds a way to specify stoke of individual lines in double and triple links [#31](https://github.com/Typsium/alchemist/pull/31) +- Fix bugs with touying bindings and `hide` function + +### 0.1.10 + +- Added touying bindings in lib.typ to support automatic integration +- Added a way to ignore charges in the links connection of a fragment with the `ignore-charge` parameter +- Fix [#27](https://github.com/Typsium/alchemist/issues/27) +- Fix [#23](https://github.com/Typsium/alchemist/issues/23) + +### 0.1.9 + +- Updated cetz to version 0.4.2 +- Exposed name in lib.typ to be used by other packages +- Adds a touying support + +### 0.1.8 + +- Fixed bugs introduced in 0.1.7 + +### 0.1.7 + +- Updated cetz to version 0.4.1 +- Added default values for `color` and `font` for `fragment` elements +- Added a `skeletize-config` function to create a `skeletize` function with a specific configuration +- Fixed a bug with cetz anchors not being correctly translated +- Added an `over` argument to links to allow hiding overlapped links + +### 0.1.6 + +- Fixed the parenthesis height to work with the new typst version +- Renamed `molecule` into `fragment` +- Added support for charges and apostrophes in the string of a molecule +- Fixed the parenthesis auto positioning and alignment +- Added a new operator element and a new parameter in parenthesis to write resonance formulae + +### 0.1.5 + +- Update to compiler 0.13.1 and Cetz 0.3.4 + +### 0.1.4 + +- Added the possibility to create Lewis formulae +- Added parenthesis element to create groups and polymer + +### 0.1.3 + +- Added the possibility to add exponent in the string of a molecule. + +### 0.1.2 + +- Added default values for link style properties. +- Updated `cetz` to version 0.3.1. +- Added a `tip-length` argument to dashed cram links. + +### 0.1.1 + +- Exposed the `draw-skeleton` function. This allows to draw in a cetz canvas directly. +- Fixed multiples bugs that causes overdraws of links. + +### 0.1.0 + +- Initial release diff --git a/packages/preview/alchemist/0.2.0/lib.typ b/packages/preview/alchemist/0.2.0/lib.typ new file mode 100644 index 0000000000..8fc37b5305 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/lib.typ @@ -0,0 +1,284 @@ +#import "@preview/cetz:0.5.2" +#import "src/default.typ": default +#import "src/utils/utils.typ" +#import "src/drawer.typ" +#import "src/drawer.typ": skeletize, draw-skeleton, skeletize-config, draw-skeleton-config, hide-drawables +#import "src/elements/links.typ": * +#import "src/elements/fragment.typ": * +#import "src/elements/lewis.typ": * + +#let transparent = color.rgb(100%, 0, 0, 0) +#let name = "alchemist" + +#let touying-reducer-bindings = ( + "reduce": ("skeletize",), + "cover": ("hide",) +) + +/// === Fragment function +/// Build a fragment group based on mol +/// Each fragment is represented as an optional count followed by a fragment name +/// starting by a capital letter followed by an optional exponent followed by an optional indice. +/// #example(``` +/// #skeletize({ +/// fragment("H_2O") +/// }) +///```) +/// #example(``` +/// #skeletize({ +/// fragment("H^A_EF^5_4") +/// }) +/// ```) +/// It is possible to use an equation as a fragment. In this case, the splitting of the equation uses the same rules as in the string case. However, you get some advantages over the string version: +/// - You can use parenthesis to group elements together. +/// - You have no restriction about what you can put in exponent or indice. +/// #example(``` +/// #skeletize({ +/// fragment($C(C H_3)_3$) +/// }) +///```) +/// - name (content): The name of the fragment. It is used as the cetz name of the fragment and to link other fragments to it. +/// - links (dictionary): The links between this fragment and previous fragments or hooks. The key is the name of the fragment or hook and the value is the link function. See @links. +/// +/// Note that the atom-sep and angle arguments are ignored +/// - lewis (list): The list of lewis structures to draw around the fragments. See @lewis +/// - mol (string, equation): The string representing the fragment or an equation of the fragment +/// - vertical (boolean): If true, the fragment is drawn vertically +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", vertical: true) +/// }) +///```) +/// - ignore-charge (boolean): If true, charge of the fragment are excluded from the links connections. This is useful when you want to have the center of your connection to the text without having the charge in the way. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single(relative: 90deg) +/// fragment("B^-", ignore-charge: true) +/// }) +/// ```) +/// - colors (color, list): The color of the fragment. If a list is provided, it colors each group of the fragment with the corresponding color from right to left. If the number of colors is less than the number of groups, the last color is used for the remaining groups. If the number of colors is greater than the number of groups, the extra colors are ignored. +/// #example(``` +/// #skeletize({ +/// fragment("ABCD", colors: (red, green, blue)) +/// single() +/// fragment("EFGH", colors: (orange)) +/// }) +/// ```) +/// -> drawable +#let fragment(name: none, links: (:), lewis: (), vertical: false, ignore-charge: false, colors: none, mol) = { + let (atoms, count) = if type(mol) == str { + split-fragment-string(mol, split-charge: ignore-charge) + } else if mol.func() == math.equation { + split-equation(mol, equation: true, split-charge: ignore-charge) + } else { + panic("Invalid fragment content") + } + + if type(lewis) != array { + panic("Lewis formulae elements must be in a list") + } + + ( + ( + type: "fragment", + name: name, + atoms: atoms, + colors: colors, + links: links, + lewis: lewis, + vertical: vertical, + count: count + ), + ) +} +#let molecule(name: none, links: (:), lewis: (), vertical: false, mol) = fragment(name: name, links: links, lewis: lewis, vertical: vertical, mol) + +/// === Hooks +/// Create a hook in the fragment. It allows to connect links to the place where the hook is. +/// Hooks are placed at the end of links or at the beginning of the fragment. +/// - name (string): The name of the hook +/// -> drawable +#let hook(name) = { + ( + ( + type: "hook", + name: name, + ), + ) +} + +/// === Branch and cycles +/// Create a branch from the current fragment, the first element +/// of the branch has to be a link. +/// +/// You can specify an angle argument like for links. This angle will be then +/// used as the `base-angle` for the branch. +/// +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// branch({ +/// single(angle:1) +/// fragment("B") +/// }) +/// branch({ +/// double(angle: -1) +/// fragment("D") +/// }) +/// single() +/// double() +/// single() +/// fragment("C") +/// }) +///```) +/// - body (drawable): the body of the branch. It must start with a link. +/// -> drawable +#let branch(body, ..args) = { + if args.pos().len() != 0 { + panic("Branch takes one positional argument: the body of the branch") + } + ((type: "branch", body: body, args: args.named()),) +} + +/// Create a regular cycle of fragments +/// You can specify an angle argument like for links. This angle will be then +/// the angle of the first link of the cycle. +/// +/// The argument `align` can be used to force align the cycle according to the +/// relative angle of the previous link. +/// +/// #example(``` +/// #skeletize({ +/// cycle(5, { +/// single() +/// double() +/// single() +/// double() +/// single() +/// }) +/// }) +///```) +/// - faces (int): the number of faces of the cycle +/// - body (drawable): the body of the cycle. It must start and end with a fragment or a link. +/// -> drawable +#let cycle(faces, body, ..args) = { + if args.pos().len() != 0 { + panic("Cycle takes two positional arguments: number of faces and body") + } + if faces < 3 { + panic("A cycle must have at least 3 faces") + } + ( + ( + type: "cycle", + faces: faces, + body: body, + args: args.named(), + ), + ) +} + +/// === Parenthesis +/// Encapsulate a drawable between two parenthesis. The left parenthesis is placed at the left of the first element of the body and by default the right parenthesis is placed at the right of the last element of the body. +/// +/// #example(``` +/// #skeletize( +/// config: ( +/// angle-increment: 30deg +/// ), { +/// parenthesis( +/// l:"[", r:"]", +/// br: $n$, { +/// single(angle: 1) +/// single(angle: -1) +/// single(angle: 1) +/// }) +/// }) +/// ```) +/// For more examples, see @examples +/// +/// - body (drawable): the body of the parenthesis. It must start and end with a fragment or a link. +/// - l (string): the left parenthesis +/// - r (string): the right parenthesis +/// - align (true): if true, the parenthesis will have the same y position. They will also get sized and aligned according to the body height. If false, they are not aligned and the height argument must be specified. +/// - resonance (boolean): if true, the parenthesis will be drawn in resonance mode. This means that the left and right parenthesis will be placed outside the molecule. Also, the parenthesis will be separated from the previous and next molecule. This can be true only if the parenthesis is the first element of the skeletal formula or if the previous element is an operator. See @resonance for more details. +/// - height (float, length): the height of the parenthesis. If align is true, this argument is optional. +/// - yoffset (float, length, list): the vertical offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - xoffset (float, length, list): the horizontal offset of parenthesis. You can also provide a tuple for left and right parenthesis +/// - right (string): Sometime, it is not convenient to place the right parenthesis at the end of the body. In this case, you can specify the name of the fragment or link where the right parenthesis should be placed. It is especially useful when the body end by a cycle. See @polySulfide +/// - tr (content): the exponent content of the right parenthesis +/// - br (content): the indice content of the right parenthesis +/// -> drawable +#let parenthesis(body, l: "(", r: ")", align: true, resonance: false, height: none, yoffset: none, xoffset: none, right: none, tr: none, br: none) = { + if l.len() > 2 { + panic("Left can be at most 2 characters") + } + let l = eval(l, mode: "math") + if r.len() > 2 { + panic("Right can be at most 2 characters") + } + let r = eval(r, mode: "math") + ( + ( + type: "parenthesis", + body: body, + calc: calc, + l: l, + r: r, + align: align, + tr: tr, + br: br, + height: height, + xoffset: xoffset, + yoffset: yoffset, + right: right, + resonance: resonance, + ), + ) +} + + +/// === Operator +/// Create an operator between two fragments. Creating an operator "reset" the placement of the next fragment. +/// This allow to add multiple molecules in the same skeletal formula. Without this, the next fragment would be placed at the end of the previous one. +/// An important point is that you can't use previous hooks to link two molecules separate by an operator. +/// This element is used in resonance structures (@resonance) and in some cases to put multiples molecules in the same skeletal formula (as you can set op to none). +/// +/// - op (content, string, none): The operator content. It can be a string or a content. A none value won't display anything. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// operator($->$, margin: 1em) +/// fragment("B") +/// }) +/// ```) +/// See @resonance for more examples. +/// - name (string): The name of the operator. +/// - margin (float, length): The margin between the operator and previous / next molecule. +/// -> drawable +#let operator(name: none, margin: 1em, op) = { + ( + ( + type: "operator", + name: name, + op: op, + margin: margin, + ), + ) +} + +/// === Hiding part of the molecule +/// This element allows to hide part of the molecule. It can be used to hide the part of the molecule that is not relevant for the current discussion. It can also be used to create some animation effects by hiding and showing different parts of the molecule. Note that the hidden part is still present in the drawing and can be linked to other fragments. This means that you can hide a part of the molecule and still link it to other fragments or hooks. The hidden part is also still present in the cetz record, which means that you can still use it in the cetz drawing. The only thing that is hidden is the drawing of the hidden part. +/// - bounds (boolean): If true, the hidden part keep the same bounding box as if it was not hidden. If false, the hidden part doesn't take any space in the drawing. +/// - body (drawable): The body to hide. It can be any drawable. +/// -> drawable +#let hide(bounds: true, body) = { + ( + ( + type: "hide", + bounds: bounds, + body: body, + ), + ) +} diff --git a/packages/preview/alchemist/0.2.0/src/cetz/process.typ b/packages/preview/alchemist/0.2.0/src/cetz/process.typ new file mode 100644 index 0000000000..29611e23bc --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/cetz/process.typ @@ -0,0 +1,96 @@ +// Temporary custom process.typ file to override default behavior + +#import "@preview/cetz:0.5.2": util, path-util, vector, drawable, process.aabb + + +/// Processes an element's function to get its drawables and bounds. Returns a {{dictionary}} with the key-values: `ctx` The modified context object, `bounds` The {{aabb}} of the element's drawables, `drawables` An {{array}} of the element's {{drawable}}s. +/// +/// - ctx (ctx): The current context object. +/// - element-func (function): A function that when passed {{ctx}}, it should return an element dictionary. +#let element(ctx, element-func) = { + let bounds = none + let element + let anchors = () + + (ctx, ..element,) = element-func(ctx) + if "drawables" in element { + if type(element.drawables) == dictionary { + element.drawables = (element.drawables,) + } + for drawable in drawable.filter-tagged(element.drawables, drawable.TAG.no-bounds) { + bounds = aabb.aabb( + if drawable.type == "path" { + path-util.bounds(drawable.segments) + } else if drawable.type == "content" { + let (x, y, _, w, h,) = drawable.pos + (drawable.width, drawable.height) + ((x + w / 2, y - h / 2, 0.0), (x - w / 2, y + h / 2, 0.0)) + }, + init: bounds + ) + } + } + + let name = element.at("name", default: none) + element.name = name + if name != none { + assert.eq(type(name), str, + message: "Element name must be a string") + assert(not name.contains("."), + message: "Invalid name for element '" + element.name + "'; name must not contain '.'") + + if "anchors" in element { + anchors.push(name) + ctx.nodes.insert(name, element) + if ctx.groups.len() > 0 { + ctx.groups.last().push(name) + } + } + } + + // Draw a debug bounding box. + if ctx.debug and bounds != none { + element.drawables.push(drawable.line-strip( + (bounds.low, + (bounds.high.at(0), bounds.low.at(1), 0.0), + bounds.high, + (bounds.low.at(0), bounds.high.at(1), 0.0) + ), + close: true, + stroke: red, + tags: (drawable.TAG.debug,))) + } + + return ( + ctx: ctx, + bounds: bounds, + drawables: element.at("drawables", default: ()), + element: element, + anchors: anchors + ) +} + +/// Runs the `element` function for a list of element functions and aggregates the results. +/// - ctx (ctx): The current context object. +/// - body (array): The array of element functions to process. +/// -> dictionary +#let many(ctx, body) = { + let drawables = () + let bounds = none + let elements = () + let anchors = () + + for el in body { + let r = element(ctx, el) + if r != none { + if r.bounds != none { + let pts = (r.bounds.low, r.bounds.high,) + bounds = aabb.aabb(pts, init: bounds) + } + ctx = r.ctx + drawables += r.drawables + anchors += r.anchors + } + elements.push(r.element) + } + return (ctx: ctx, bounds: bounds, drawables: drawables, elements: elements, anchors: anchors) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.2.0/src/default.typ b/packages/preview/alchemist/0.2.0/src/default.typ new file mode 100644 index 0000000000..369a85c890 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/default.typ @@ -0,0 +1,61 @@ +#let default = ( + atom-sep: 3em, + fragment-margin: 0.2em, + fragment-font: none, + fragment-color: none, + link-over-radius: .2, + angle-increment: 45deg, + base-angle: 0deg, + debug: false, + single: ( + stroke: black, + ), + double: ( + gap: .25em, + offset: "center", + offset-coeff: 0.85, + stroke: black, + ), + triple: ( + gap: .25em, + stroke: black, + ), + filled-cram: ( + stroke: none, + fill: black, + base-length: .8em, + ), + dashed-cram: ( + stroke: black + .05em, + dash-gap: .3em, + base-length: .8em, + tip-length: .1em, + ), + lewis: ( + angle: 0deg, + radius: 0.2em, + ), + lewis-single: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + offset: "top", + ), + lewis-double: ( + stroke: black, + fill: black, + radius: .1em, + gap: .25em, + ), + lewis-line: ( + stroke: black, + length: .7em, + ), + lewis-rectangle: ( + stroke: .08em + black, + fill: white, + height: .7em, + width: .3em, + ), +) \ No newline at end of file diff --git a/packages/preview/alchemist/0.2.0/src/drawer.typ b/packages/preview/alchemist/0.2.0/src/drawer.typ new file mode 100644 index 0000000000..13715c064e --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/drawer.typ @@ -0,0 +1,510 @@ +#import "default.typ": default +#import "@preview/cetz:0.5.2" +#import "cetz/process.typ" as custom-process +#import "utils/utils.typ": * +#import "utils/anchors.typ": * +#import "utils/utils.typ" as utils +#import "drawer/fragment.typ" as fragment +#import "drawer/link.typ" as link +#import "drawer/branch.typ" as branch +#import "drawer/cycle.typ" as cycle +#import "drawer/parenthesis.typ" as parenthesis +#import "drawer/hook.typ" as hook +#import "drawer/operator.typ" as operator +#import "drawer/hide.typ": draw-hide + +#import cetz.draw: * + +#let default-anchor = (type: "coord", anchor: (0, 0)) + +#let default-ctx = ( + // general + last-anchor: default-anchor, // keep trace of the place to draw + last-name: "", // last name used to draw + links: (), // list of links to draw + hooks: (:), // list of hooks + hooks-links: (), // list of links to hooks + relative-angle: 0deg, // current global relative angle + angle: 0deg, // current global angle + id: 0, // an id used to name things with an unique name + first-molecule: true, // true if we are drawing the first molecule of the current equation + first-draw: true, // true if no element of the current molecule has been drawn yet + // branch + first-branch: false, // true if the next element is the first in a branch + // cycle + first-fragment: none, // name of the first fragment in the cycle + in-cycle: false, // true if we are in a cycle + cycle-faces: 0, // number of faces in the current cycle + faces-count: 0, // number of faces already drawn + cycle-step-angle: 0deg, // angle between two faces in the cycle + record-vertex: false, // true if the cycle should keep track of its vertices + vertex-anchors: (), // list of the cycle vertices + hide: false, // true if the current elements should be hidden +) + +#let draw-hooks-links(links, mol-name, ctx, from-mol) = { + let hook-id = 0 + for (to-name, (link,)) in links { + if link.at(mol-name, default: none) == none { + link.name = mol-name + "-hook-" + str(hook-id) + hook-id += 1 + } + if to-name not in ctx.hooks { + panic("Molecule " + to-name + " does not exist") + } + let to-hook = ctx.hooks.at(to-name) + if to-hook.type == "fragment" { + ctx.links.push(( + type: "link", + hide: ctx.hide, + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + from: none, + to: none, + over: link.at("over", default: none), + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + )) + } else if to-hook.type == "hook" { + ctx.links.push(( + type: if from-mol { + "mol-hook-link" + } else { + "link-hook-link" + }, + hide: ctx.hide, + name: link.at("name"), + from-pos: if from-mol { + (name: mol-name, anchor: "mid") + } else { + mol-name + "-end-anchor" + }, + from-name: if from-mol { + mol-name + }, + to-name: to-name, + to-hook: to-hook.hook, + override: angles.angle-override(ctx.angle, ctx), + draw: link.draw, + over: link.at("over", default: none), + )) + } else { + panic("Unknown hook type " + ctx.hook.at(to-name).type) + } + } + ctx +} + +#let draw-fragments-and-link(ctx, body) = { + let fragment-drawing = () + let cetz-drawing = () + ctx.first-draw = true + for element in body { + if ctx.in-cycle and ctx.faces-count >= ctx.cycle-faces { + continue + } + let drawing = () + let cetz-rec = () + if type(element) == function { + cetz-drawing.push(element) + } else if type(element) == dictionary { + if "type" not in element { + panic("Element " + repr(element) + " has no type") + } else if element.type == "fragment" { + (ctx, drawing) = fragment.draw-fragment(element, ctx) + } else if element.type == "link" { + (ctx, drawing) = link.draw-link(element, ctx) + } else if element.type == "branch" { + (ctx, drawing, cetz-rec) = branch.draw-branch( + element, + ctx, + draw-fragments-and-link, + ) + } else if element.type == "cycle" { + (ctx, drawing, cetz-rec) = cycle.draw-cycle( + element, + ctx, + draw-fragments-and-link, + ) + } else if element.type == "hook" { + ctx = hook.draw-hook(element, ctx) + } else if element.type == "parenthesis" { + (ctx, drawing, cetz-rec) = parenthesis.draw-parenthesis( + element, + ctx, + draw-fragments-and-link, + ) + } else if element.type == "operator" { + (ctx, drawing) = operator.draw-operator(element, fragment-drawing, ctx) + } else if element.type == "hide" { + (ctx, drawing, cetz-rec) = draw-hide( + element, + ctx, + draw-fragments-and-link, + ) + } else { + panic("Unknown element type " + element.type) + } + } else if type(element) == type([]) { + if element.func() == metadata { + // ignore metadata + } else { + panic("Unexpected content element: " + repr(element)) + } + } else { + panic("Unexpected element type: " + str( + type(element), + ) + " with value " + repr(element)) + } + fragment-drawing += drawing + cetz-drawing += cetz-rec + ctx.first-draw = false + } + if ctx.last-anchor.type == "link" and not ctx.last-anchor.at( + "drew", + default: false, + ) { + ctx.links.push(ctx.last-anchor) + ctx.last-anchor.drew = true + } + ( + ctx, + fragment-drawing, + cetz-drawing, + ) +} + +#let draw-link-over(ctx, link, over, angle) = { + let name = link.name + "-over" + let link-over-radius = ctx.config.link-over-radius + let (over, length, radius) = if type(over) == str { + (over, link-over-radius, link-over-radius) + } else if type(over) == dictionary { + let name = over.at("name", default: none) + if name == none { + panic("Over argument must have a name") + } + ( + name, + over.at("length", default: link-over-radius), + over.at("radius", default: link-over-radius), + ) + } else { + panic("Over must be a string or a dictionary, got " + type(link.at("over"))) + } + intersections(name, over, link.name) + let color = if ctx.config.debug { + red + } else { + white + } + scope({ + rotate(angle) + rect( + anchor: "center", + (to: name + ".0", rel: (-length / 2, -radius / 2)), + (rel: (length, radius)), + fill: color, + stroke: color, + ) + }) +} + +#let draw-link-decoration(ctx) = { + ( + ctx, + get-ctx(cetz-ctx => { + for link in ctx.links { + let drawing = { + let ((from, to), angle) = calculate-link-anchors(ctx, cetz-ctx, link) + if ctx.config.debug { + circle(from, radius: .1em, fill: red, stroke: red) + circle(to, radius: .1em, fill: red, stroke: red) + } + let length = distance-between(cetz-ctx, from, to) + hide(line(from, to, name: link.name)) + if link.at("over") != none { + if type(link.at("over")) == array { + for over in link.at("over") { + draw-link-over(ctx, link, over, angle) + } + } else { + draw-link-over(ctx, link, link.at("over"), angle) + } + } + + scope({ + set-origin(from) + rotate(angle) + (link.draw)(length, ctx, cetz-ctx, override: link.override) + }) + } + if link.hide { + hide(drawing) + } else { + drawing + } + } + }), + ) +} + +/// set elements names and split the molecule into sub-groups +#let preprocessing( + body, + group-id: 0, + link-id: 0, + operator-id: 0, + top-level: true, +) = { + let result = ((),) + let has_element = false + for element in body { + if type(element) == dictionary { + has_element = true + if element.at("name", default: none) == none { + if element.type == "fragment" { + element.name = "fragment-" + str(group-id) + group-id += 1 + } else if element.type == "link" { + element.name = "link-" + str(link-id) + link-id += 1 + } else if element.type == "operator" { + element.name = "operator-" + str(operator-id) + operator-id += 1 + } + } + if element.at("body", default: none) != none { + let child-body + (child-body, group-id, link-id, operator-id) = preprocessing( + element.body, + group-id: group-id, + link-id: link-id, + operator-id: operator-id, + top-level: false, + ) + if element.type == "parenthesis" and element.resonance { + element.body = child-body + } else { + element.body = child-body.at(0) + } + } + if element.type == "operator" or ( + element.type == "parenthesis" and element.resonance + ) { + result.push(element) + result.push(()) + } else { + result.at(-1).push(element) + } + } else if type(element) == function { + result.at(-1).push(element) + } else if element == none { + // ignore empty elements + } else { + panic("Unexpected element type: " + str( + type(element), + ) + " with value " + repr(element)) + } + } + if top-level and not has_element { + panic("The skeletize body must contain at least one element", body) + } + (result, group-id, link-id, operator-id) +} + +#let operator-group-name = id => { + "operator-group-" + str(id) +} + +#let draw-groups(ctx, bodies, after-operator: false) = { + ctx.last-name = "" + + let drawables = for (i, body) in bodies.enumerate() { + if type(body) == array { + if body.len() > 0 { + ctx.last-name = operator-group-name(i) + get-ctx(cetz-ctx => { + let ctx = ctx + if ctx.last-anchor.type == "coord" { + (cetz-ctx, ctx.last-anchor.anchor) = cetz.coordinate.resolve( + cetz-ctx, + ctx.last-anchor.anchor, + ) + } + let last-anchor = ctx.last-anchor + let (ctx, atoms, cetz-drawing) = draw-fragments-and-link(ctx, body) + for (links, name, from-mol) in ctx.hooks-links { + ctx = draw-hooks-links(links, name, ctx, from-mol) + } + + let molecule = { + atoms + } + + let ( + ctx: cetz-ctx, + drawables, + bounds: molecule-bounds, + anchors, + ) = custom-process.many(cetz-ctx, molecule) + molecule-bounds = cetz.util.revert-transform( + cetz-ctx.transform, + molecule-bounds, + ) + + let (translate-x, translate-y) = if after-operator { + let (_, origin-anchor) = cetz.coordinate.resolve( + cetz-ctx, + last-anchor.anchor, + ) + ( + origin-anchor.at(0) - molecule-bounds.low.at(0), + origin-anchor.at(1) - ( + molecule-bounds.low.at(1) + molecule-bounds.high.at(1) + ) / 2, + ) + } else { + (0, 0) + } + + + let transform-matrix = cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ) + // panic(anchors) + for name in anchors { + let hold-anchors = cetz-ctx.nodes.at(name).anchors + cetz-ctx.nodes.at(name).anchors = name => { + if name != () { + cetz.matrix.mul4x4-vec3(transform-matrix, hold-anchors(name)) + } else { + hold-anchors(name) + } + } + } + ( + _ => ( + ctx: cetz-ctx, + drawables: cetz.drawable.apply-transform( + cetz.matrix.transform-translate( + translate-x, + translate-y, + 0, + ), + drawables, + ), + ), + ) + scope({ + translate(x: translate-x, y: translate-y) + draw-link-decoration(ctx).at(1) + on-layer(2, cetz-drawing) + let bound-rect = cetz.draw.rect( + molecule-bounds.low, + molecule-bounds.high, + name: ctx.last-name, + stroke: purple, + ) + if ctx.config.debug { + bound-rect + } else { + cetz.draw.hide(bound-rect) + } + }) + }) + } + } else if body.type == "operator" { + let (op-ctx, drawable) = operator.draw-operator(body, ctx) + after-operator = true + ctx = op-ctx + drawable + } else if body.type == "parenthesis" { + let (parenthesis-ctx, drawable) = parenthesis.draw-resonance-parenthesis( + body, + draw-groups, + ctx, + ) + ctx = parenthesis-ctx + drawable + } else { + panic("Unexpected element type: " + body.type) + } + ctx.first-molecule = false + } + (drawables, ctx) +} + +#let draw-skeleton(config: default, name: none, mol-anchor: none, body) = { + let config = merge-dictionaries(config, default) + let ctx = default-ctx + ctx.angle = config.base-angle + ctx.config = config + + let bodies = preprocessing(body).at(0) + let (final-drawing, ctx) = draw-groups(ctx, bodies) + + if name == none { + final-drawing + } else { + group( + name: name, + anchor: mol-anchor, + { + anchor("default", (0, 0)) + final-drawing + }, + ) + } +} + +/// setup a molecule skeleton drawer +#let skeletize(debug: false, background: none, config: (:), body) = { + if "debug" not in config { + config.insert("debug", debug) + } + cetz.canvas( + debug: debug, + background: background, + draw-skeleton(config: config, body), + ) +} + +#let skeletize-config(default-config) = { + let config-function(debug: false, background: none, config: (:), body) = { + skeletize( + debug: debug, + background: background, + config: merge-dictionaries(config, default-config), + body, + ) + } + config-function +} + +#let draw-skeleton-config(default-config) = { + let config-function(config: (:), name: none, mol-anchor: none, body) = { + draw-skeleton( + config: merge-dictionaries(config, default-config), + name: name, + mol-anchor: mol-anchor, + body, + ) + } + config-function +} + + +#let hide-drawables(elements) = { + return elements +} diff --git a/packages/preview/alchemist/0.2.0/src/drawer/branch.typ b/packages/preview/alchemist/0.2.0/src/drawer/branch.typ new file mode 100644 index 0000000000..0e488f3dba --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/drawer/branch.typ @@ -0,0 +1,34 @@ +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +/// Compute the angle the branch should take in order to be "in the center" +/// of the cycle angle +#let cycle-angle(ctx) = { + if ctx.in-cycle { + if ctx.faces-count == 0 { + ctx.relative-angle - ctx.cycle-step-angle - ( + 180deg - ctx.cycle-step-angle + ) / 2 + } else { + ctx.relative-angle - (180deg - ctx.cycle-step-angle) / 2 + } + } else { + ctx.angle + } +} + +#let draw-branch(branch, ctx, draw-fragments-and-link) = { + let angle = angles.angle-from-ctx(ctx, branch.args, cycle-angle(ctx)) + let (branch-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: false, + first-branch: true, + cycle-step-angle: 0, + angle: angle, + ), + branch.body, + ) + ctx = context_.update-parent-context(ctx, branch-ctx) + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.2.0/src/drawer/cram.typ b/packages/preview/alchemist/0.2.0/src/drawer/cram.typ new file mode 100644 index 0000000000..11c7b6cd94 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/drawer/cram.typ @@ -0,0 +1,68 @@ +#import "@preview/cetz:0.5.2" +#import "../utils/utils.typ" + +/// Draw a triangle between two fragments +#let cram(from, to, ctx, cetz-ctx, args) = { + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.filled-cram.base-length), + ) + cetz.draw.line( + (from-x, from-y - base-length / 2), + (from-x, from-y + base-length / 2), + (to-x, to-y), + close: true, + stroke: args.at("stroke", default: ctx.config.filled-cram.stroke), + fill: args.at("fill", default: ctx.config.filled-cram.fill), + ) +} + +/// Draw a dashed triangle between two molecules fragments +#let dashed-cram(from, to, length, ctx, cetz-ctx, args) = { + import cetz.draw: * + let (cetz-ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(cetz-ctx, from) + let (cetz-ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(cetz-ctx, to) + let base-length = utils.convert-length( + cetz-ctx, + args.at("base-length", default: ctx.config.dashed-cram.base-length), + ) + let tip-length = utils.convert-length( + cetz-ctx, + args.at("tip-length", default: ctx.config.dashed-cram.tip-length), + ) + hide({ + line( + name: "top", + (from-x, from-y - base-length / 2), + (to-x, to-y - tip-length / 2), + ) + line( + name: "bottom", + (from-x, from-y + base-length / 2), + (to-x, to-y + tip-length / 2), + ) + }) + let stroke = args.at("stroke", default: ctx.config.dashed-cram.stroke) + let dash-gap = utils.convert-length( + cetz-ctx, + args.at("dash-gap", default: ctx.config.dashed-cram.dash-gap), + ) + let dash-width = stroke.thickness + let converted-dash-width = utils.convert-length(cetz-ctx, dash-width) + let length = utils.convert-length(cetz-ctx, length) + + let dash-count = int(calc.ceil(length / (dash-gap + converted-dash-width))) + let incr = 100% / dash-count + + let percentage = 0% + while percentage <= 100% { + line( + (name: "top", anchor: percentage), + (name: "bottom", anchor: percentage), + stroke: stroke, + ) + percentage += incr + } +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.2.0/src/drawer/cycle.typ b/packages/preview/alchemist/0.2.0/src/drawer/cycle.typ new file mode 100644 index 0000000000..4e060163b2 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/drawer/cycle.typ @@ -0,0 +1,169 @@ +#import "@preview/cetz:0.5.2" +#import "../utils/utils.typ" +#import "../utils/angles.typ" +#import "../utils/context.typ" as context_ + +#let max-int = 9223372036854775807 + +/// Insert missing vertices in the cycle +#let missing-vertices(ctx, vertex, cetz-ctx) = { + let atom-sep = utils.convert-length(cetz-ctx, ctx.config.atom-sep) + for i in range(ctx.cycle-faces - vertex.len()) { + let (x, y, _) = vertex.last() + vertex.push(( + x + atom-sep * calc.cos(ctx.relative-angle + ctx.cycle-step-angle * ( + i + 1 + )), + y + atom-sep * calc.sin(ctx.relative-angle + ctx.cycle-step-angle * ( + i + 1 + )), + 0, + )) + } + vertex +} + +/// extrapolate the center and the radius of the cycle +#let cycle-center-radius(ctx, cetz-ctx, vertex) = { + let min-radius = max-int + let center = (0, 0) + let faces = ctx.cycle-faces + let odd = calc.rem(faces, 2) == 1 + let debug = () + for (i, v) in vertex.enumerate() { + if (ctx.config.debug) { + debug += cetz.draw.circle(v, radius: .1em, fill: blue, stroke: blue) + } + let (x, y, _) = v + center = (center.at(0) + x, center.at(1) + y) + if odd { + let opposite1 = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let opposite2 = calc.rem-euclid(i + calc.div-euclid(faces, 2) + 1, faces) + let (ox1, oy1, _) = vertex.at(opposite1) + let (ox2, oy2, _) = vertex.at(opposite2) + let radius = utils.distance-between( + cetz-ctx, + (x, y), + ((ox1 + ox2) / 2, (oy1 + oy2) / 2), + ) / 2 + if radius < min-radius { + min-radius = radius + } + } else { + let opposite = calc.rem-euclid(i + calc.div-euclid(faces, 2), faces) + let (ox, oy, _) = vertex.at(opposite) + let radius = utils.distance-between(cetz-ctx, (x, y), (ox, oy)) / 2 + if radius < min-radius { + min-radius = radius + } + } + } + ( + (center.at(0) / vertex.len(), center.at(1) / vertex.len()), + min-radius, + debug, + ) +} + +#let draw-cycle-center-arc(ctx, name, center-arc) = { + import cetz.draw: * + let faces = ctx.cycle-faces + let vertex = ctx.vertex-anchors + get-ctx(cetz-ctx => { + let (cetz-ctx, ..vertex) = cetz.coordinate.resolve(cetz-ctx, ..vertex) + if vertex.len() < faces { + vertex = missing-vertices(ctx, cetz-ctx) + } + let (center, min-radius, debug) = cycle-center-radius(ctx, cetz-ctx, vertex) + debug + if name != none { + anchor(name, center) + } + if center-arc != none { + if min-radius == max-int { + panic("The cycle has no opposite vertices") + } + if ctx.cycle-faces > 4 { + min-radius *= center-arc.at("radius", default: 0.7) + } else { + min-radius *= center-arc.at("radius", default: 0.5) + } + let start = center-arc.at("start", default: 0deg) + let end = center-arc.at("end", default: 360deg) + let delta = center-arc.at("delta", default: end - start) + center = ( + center.at(0) + min-radius * calc.cos(start), + center.at(1) + min-radius * calc.sin(start), + ) + arc( + center, + ..center-arc, + radius: min-radius, + start: start, + delta: delta, + ) + } + }) +} + +#let draw-cycle(cycle, ctx, draw-fragments-and-link) = { + let cycle-step-angle = 360deg / cycle.faces + let angle = angles.angle-from-ctx(ctx, cycle.args, none) + if angle == none { + if ctx.in-cycle { + angle = ctx.relative-angle - (180deg - cycle-step-angle) + if ctx.faces-count != 0 { + angle += ctx.cycle-step-angle + } + } else if ( + ctx.relative-angle == 0deg and ctx.angle == 0deg and not cycle.args.at( + "align", + default: false, + ) + ) { + angle = cycle-step-angle - 90deg + } else { + angle = ctx.relative-angle - (180deg - cycle-step-angle) / 2 + } + } + let first-fragment = none + if ctx.last-anchor.type == "fragment" { + first-fragment = ctx.last-anchor.name + if first-fragment not in ctx.hooks { + ctx.hooks.insert(first-fragment, ctx.last-anchor) + } + } + let name = none + let record-vertex = false + if "name" in cycle.args { + name = cycle.args.at("name") + record-vertex = true + } else if "arc" in cycle.args { + record-vertex = true + } + let (cycle-ctx, drawing, cetz-rec) = draw-fragments-and-link( + ( + ..ctx, + in-cycle: true, + cycle-faces: cycle.faces, + faces-count: 0, + first-branch: true, + cycle-step-angle: cycle-step-angle, + relative-angle: angle, + first-fragment: first-fragment, + angle: angle, + record-vertex: record-vertex, + vertex-anchors: (), + ), + cycle.body, + ) + ctx = context_.update-parent-context(ctx, cycle-ctx) + if record-vertex { + drawing += draw-cycle-center-arc( + cycle-ctx, + name, + cycle.args.at("arc", default: none), + ) + } + (ctx, drawing, cetz-rec) +} diff --git a/packages/preview/alchemist/0.2.0/src/drawer/fragment.typ b/packages/preview/alchemist/0.2.0/src/drawer/fragment.typ new file mode 100644 index 0000000000..50eb3c9a8c --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/drawer/fragment.typ @@ -0,0 +1,173 @@ +#import "../utils/context.typ": * +#import "../utils/anchors.typ": * +#import "@preview/cetz:0.5.2" + +#let draw-fragment-text(ctx, mol, pos) = { + import cetz.draw: * + let id = 0 + let no-anchor-id = 0 + let last-anchor-name = none + for (eq, anchored) in mol.atoms { + let name = str(id) + if not anchored { + name += str(no-anchor-id) + no-anchor-id += 1 + } + + let color = if mol.colors != none { + if type(mol.colors) == color { + mol.colors + } else { + mol.colors.at(calc.min(id, mol.colors.len() - 1)) + } + } else { + ctx.config.fragment-color + } + let fragment-font = ctx.config.fragment-font + + // draw atoms of the group one after the other from left to right + content( + name: name, + anchor: if mol.vertical { + "north" + } else { + "mid-west" + }, + ( + if last-anchor-name == none { + pos + } else if mol.vertical { + (to: last-anchor-name + ".south", rel: (0, -.2em)) + } else { + last-anchor-name + ".mid-east" + } + ), + auto-scale: false, + { + show math.equation: math.upright + set text(fill: color) if color != none + set text(font: fragment-font) if fragment-font != none + eq + }, + ) + if anchored { + id += 1 + } + last-anchor-name = name + } +} + +#let draw-fragment-lewis(ctx, group-name, count, lewis) = { + if lewis.len() == 0 { + return () + } + import cetz.draw: * + get-ctx(cetz-ctx => { + for (id, (angle: lewis-angle, radius, draw)) in lewis.enumerate() { + if (lewis-angle == none) { + lewis-angle = ctx.config.lewis.angle + } + if (radius == none) { + radius = ctx.config.lewis.radius + } + let lewis-angle = angles.angle-correction(lewis-angle) + let mol-id = if angles.angle-in-range-inclusive( + lewis-angle, + 90deg, + 270deg, + ) { + 0 + } else { + count - 1 + } + let anchor = fragment-anchor( + ctx, + cetz-ctx, + lewis-angle, + group-name, + str(mol-id), + margin: radius, + ) + scope({ + set-origin(anchor) + rotate(lewis-angle) + draw(ctx, cetz-ctx) + }) + } + }) +} + +#let draw-fragment-elements(mol, ctx) = { + let name = mol.name + if name in ctx.hooks { + panic("Molecule fragment with name " + name + " already exists : " + ctx + .hooks + .keys() + .join(", ")) + } + ctx.hooks.insert(name, mol) + + let (group-anchor, side, coord) = if ctx.last-anchor.type == "coord" { + ("west", true, ctx.last-anchor.anchor) + } else if ctx.last-anchor.type == "link" { + if ctx.last-anchor.to == none { + ctx.last-anchor.to = link-fragment-index( + ctx.last-anchor.angle, + true, + mol.count - 1, + mol.vertical, + ) + } + let group-anchor = link-fragment-anchor(ctx.last-anchor.to, mol.count) + ctx.last-anchor.to-name = name + (group-anchor, false, ctx.last-anchor.name + "-end-anchor") + } else { + panic("A molecule fragment must be linked to a coord or a link") + } + ctx = context_.set-last-anchor( + ctx, + ( + type: "fragment", + name: name, + count: mol.at("count"), + vertical: mol.vertical, + ), + ) + if (side) { + ctx.id += 1 + } + ( + ctx, + { + import cetz.draw: * + group( + anchor: if side { + group-anchor + } else { + "from" + str(ctx.id) + }, + name: name, + { + anchor("default", coord) + draw-fragment-text(ctx, mol, coord) + if not side { + anchor("from" + str(ctx.id), group-anchor) + } + }, + ) + draw-fragment-lewis(ctx, name, mol.count, mol.at("lewis")) + }, + ) +} + +#let draw-fragment(element, ctx) = { + if ctx.first-branch { + panic("A molecule fragment can not be the first element in a cycle") + } + let (ctx, drawing) = draw-fragment-elements(element, ctx) + if element.links.len() != 0 { + ctx.hooks.insert(ctx.last-anchor.name, element) + ctx.hooks-links.push((element.links, ctx.last-anchor.name, true)) + } + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.2.0/src/drawer/hide.typ b/packages/preview/alchemist/0.2.0/src/drawer/hide.typ new file mode 100644 index 0000000000..3fb6140f9f --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/drawer/hide.typ @@ -0,0 +1,14 @@ +#import "@preview/cetz:0.5.2" + +#let draw-hide(hide, ctx, draw-fragment-and-link) = { + let hold-hide = ctx.hide + ctx.hide = true + let (hide-ctx, drawing, cetz-rec) = draw-fragment-and-link( + ctx, + hide.body, + ) + hide-ctx.hide = hold-hide + drawing = cetz.draw.hide(drawing, bounds: hide.bounds) + cetz-rec = cetz.draw.hide(cetz-rec, bounds: hide.bounds) + (hide-ctx, drawing, cetz-rec) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.2.0/src/drawer/hook.typ b/packages/preview/alchemist/0.2.0/src/drawer/hook.typ new file mode 100644 index 0000000000..158ed80eef --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/drawer/hook.typ @@ -0,0 +1,17 @@ + +#let draw-hook(hook, ctx) = { + if hook.name in ctx.hooks { + panic("Hook " + hook.name + " already exists") + } + if ctx.last-anchor.type == "link" { + ctx.hooks.insert( + hook.name, + (type: "hook", hook: ctx.last-anchor.name + "-end-anchor"), + ) + } else if ctx.last-anchor.type == "coord" { + ctx.hooks.insert(hook.name, (type: "hook", hook: ctx.last-anchor.anchor)) + } else { + panic("A hook must placed after a link or at the beginning of the skeleton") + } + ctx +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.2.0/src/drawer/link.typ b/packages/preview/alchemist/0.2.0/src/drawer/link.typ new file mode 100644 index 0000000000..fd3be53366 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/drawer/link.typ @@ -0,0 +1,107 @@ +#import "../utils/angles.typ" +#import "../utils/anchors.typ": * + +#let create-link-decorations-anchors(link, ctx) = { + let link-angle = 0deg + let to-name = none + if ctx.in-cycle { + if ctx.faces-count == ctx.cycle-faces - 1 and ctx.first-fragment != none { + to-name = ctx.first-fragment + } + if ctx.faces-count == 0 { + link-angle = ctx.relative-angle + } else { + link-angle = ctx.relative-angle + ctx.cycle-step-angle + } + } else { + link-angle = angles.angle-from-ctx(ctx, link, ctx.angle) + } + link-angle = angles.angle-correction(link-angle) + ctx.relative-angle = link-angle + let override = angles.angle-override(link-angle, ctx) + + let to-connection = link.at("to", default: none) + let from-connection = none + let from-name = none + + let from-pos = if ctx.last-anchor.type == "coord" { + ctx.last-anchor.anchor + } else if ctx.last-anchor.type == "fragment" { + from-connection = link-fragment-index( + link-angle, + false, + ctx.last-anchor.count - 1, + ctx.last-anchor.vertical, + ) + from-connection = link.at("from", default: from-connection) + from-name = ctx.last-anchor.name + fragment-link-anchor( + ctx.last-anchor.name, + from-connection, + ctx.last-anchor.count, + ) + } else if ctx.last-anchor.type == "link" { + ctx.last-anchor.name + "-end-anchor" + } else { + panic("Unknown anchor type " + ctx.last-anchor.type) + } + let length = link.at("atom-sep", default: ctx.config.atom-sep) + let link-name = link.at("name") + if ctx.record-vertex { + if ctx.faces-count == 0 { + ctx.vertex-anchors.push(from-pos) + } + if ctx.faces-count < ctx.cycle-faces - 1 { + ctx.vertex-anchors.push(link-name + "-end-anchor") + } + } + ctx = context_.set-last-anchor( + ctx, + ( + type: "link", + hide: ctx.hide, + name: link-name, + override: override, + from-pos: from-pos, + from-name: from-name, + from: from-connection, + to-name: to-name, + to: to-connection, + angle: link-angle, + over: link.at("over", default: none), + draw: link.draw, + ), + ) + ( + ctx, + { + hide( + { + circle(name: link-name + "-start-anchor", from-pos, radius: .25em) + }, + bounds: ctx.first-draw and not ctx.first-molecule, + ) + let end-anchor = (to: from-pos, rel: (angle: link-angle, radius: length)) + if ctx.config.debug { + line(from-pos, end-anchor, stroke: blue + .1em) + } + hide( + { + circle(name: link-name + "-end-anchor", end-anchor, radius: .25em) + }, + bounds: true, + ) + }, + ) +} + +#let draw-link(link, ctx) = { + ctx.first-branch = false + let drawing + (ctx, drawing) = create-link-decorations-anchors(link, ctx) + ctx.faces-count += 1 + if link.links.len() != 0 { + ctx.hooks-links.push((link.links, ctx.last-anchor.name, false)) + } + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.2.0/src/drawer/operator.typ b/packages/preview/alchemist/0.2.0/src/drawer/operator.typ new file mode 100644 index 0000000000..ab31054100 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/drawer/operator.typ @@ -0,0 +1,47 @@ +#import "@preview/cetz:0.5.2": draw, process, util + +#let draw-operator(operator, ctx) = { + import draw: * + + let op-name = operator.name + ctx.last-anchor = ( + type: "coord", + anchor: (rel: (operator.margin, 0), to: (name: op-name, anchor: "east")), + ) + + ( + ctx, + get-ctx(cetz-ctx => { + + let west-previous-mol-anchor = (name: ctx.last-name, anchor: "east") + + let east-op-anchor = ( + rel: (operator.margin, 0), + to: west-previous-mol-anchor, + ) + + let op = if (operator.op == none) { + "" + } else { + operator.op + } + + content( + name: op-name, + anchor: "west", + east-op-anchor, + op, + ) + + if (ctx.config.debug) { + circle( + west-previous-mol-anchor, + radius: 0.05, + fill: yellow, + stroke: none, + ) + circle(ctx.last-anchor.anchor, radius: 0.05, fill: yellow, stroke: none) + } + }), + ) +} diff --git a/packages/preview/alchemist/0.2.0/src/drawer/parenthesis.typ b/packages/preview/alchemist/0.2.0/src/drawer/parenthesis.typ new file mode 100644 index 0000000000..d6c30a6d6a --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/drawer/parenthesis.typ @@ -0,0 +1,274 @@ +#import "../utils/utils.typ" +#import "@preview/cetz:0.5.2" + +#let left-parenthesis-anchor(parenthesis, ctx) = { + let anchor = if parenthesis.body.at(0).type == "fragment" { + let name = parenthesis.body.at(0).name + parenthesis.body.at(0).name = name + (name: name, anchor: "west") + } else if parenthesis.body.at(0).type == "link" { + let name = parenthesis.body.at(0).at("name") + parenthesis.body.at(0).name = name + (name + "-start-anchor", 45%, name + "-end-anchor") + } else if not left { } else { + panic("The first element of a parenthesis must be a molecule fragment or a link") + } + (ctx, parenthesis, anchor) +} + +#let right-parenthesis-anchor(parenthesis, ctx) = { + let right-name = parenthesis.at("right") + let right-type = "" + if right-name != none { + right-type = utils.get-element-type(parenthesis.body, right-name) + if right-type == none { + panic("The right element of the parenthesis does not exist") + } + } else { + right-type = parenthesis.body.at(-1).type + right-name = parenthesis.body.at(-1).at("name", default: none) + } + + let anchor = if right-type == "fragment" { + (name: right-name, anchor: "east") + } else if right-type == "link" { + (right-name + "-start-anchor", 55%, right-name + "-end-anchor") + } else { + panic("The last element of a parenthesis must be a molecule fragment or a link but got " + right-type) + } + (ctx, parenthesis, anchor) +} + +#let parenthesis-content(parenthesis, height, cetz-ctx) = { + let block = block(height: height * cetz-ctx.length * 1.2, width: 0pt) + let left-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($parenthesis.l block$, size: 100%) + } + let right-parenthesis = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.lr($block parenthesis.r$, size: 100%) + } + + let right-parenthesis-with-attach = { + set text(top-edge: "bounds", bottom-edge: "bounds") + math.attach(right-parenthesis, br: parenthesis.br, tr: parenthesis.tr) + } + + (left-parenthesis, right-parenthesis, right-parenthesis-with-attach) +} + +#let draw-parenthesis(parenthesis, ctx, draw-molecules-and-link) = { + let (parenthesis-ctx, drawing, cetz-rec) = draw-molecules-and-link( + ctx, + parenthesis.body, + ) + ctx = parenthesis-ctx + + + drawing += { + import cetz.draw: * + get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + { + drawing + }, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + + let sub-height = utils.bounding-box-height(sub-bounds) + let sub-v-mid = sub-bounds.low.at(1) + sub-height / 2 + + let sub-width = utils.bounding-box-width(sub-bounds) + + let (ctx, parenthesis, left-anchor) = left-parenthesis-anchor( + parenthesis, + ctx, + ) + + let (ctx, parenthesis, right-anchor) = right-parenthesis-anchor( + parenthesis, + ctx, + ) + + let height = parenthesis.at("height") + if height == none { + if not parenthesis.align { + panic("You must specify the height of the parenthesis if they are not aligned") + } + height = sub-height + } else { + height = utils.convert-length(cetz-ctx, height) + } + + let ( + left-parenthesis, + right-parenthesis, + right-parenthesis-with-attach, + ) = parenthesis-content( + parenthesis, + height, + cetz-ctx, + ) + + let (_, (lx, ly, _)) = cetz.coordinate.resolve( + cetz-ctx, + update: false, + left-anchor, + ) + let (_, (rx, ry, _)) = cetz.coordinate.resolve( + cetz-ctx, + update: false, + right-anchor, + ) + + if ctx.config.debug { + circle((lx, ly), radius: 1pt, fill: orange, stroke: orange) + circle((rx, ry), radius: 1pt, fill: orange, stroke: orange) + rect( + (lx, sub-bounds.low.at(1)), + (rx, sub-bounds.high.at(1)), + stroke: green, + ) + line((lx, sub-v-mid), (rx, sub-v-mid), stroke: green) + } + + let hoffset = calc.abs(sub-width - calc.abs(rx - lx)) + + if parenthesis.align { + ly -= ly - sub-v-mid + ry = ly + } else if type(parenthesis.yoffset) == array { + if parenthesis.yoffset.len() != 2 { + panic("The parenthesis yoffset must be a list of length 2 or a number") + } + ly += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(0)) + ry += utils.convert-length(cetz-ctx, parenthesis.yoffset.at(1)) + } else if parenthesis.yoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.yoffset) + ly += offset + ry += offset + } + + if type(parenthesis.xoffset) == array { + if parenthesis.xoffset.len() != 2 { + panic("The parenthesis xoffset must be a list of length 2 or a number") + } + lx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(0)) + rx += utils.convert-length(cetz-ctx, parenthesis.xoffset.at(1)) + } else if parenthesis.xoffset != none { + let offset = utils.convert-length(cetz-ctx, parenthesis.xoffset) + lx -= offset + rx += offset + } + + let right-bounds = cetz + .process + .many(cetz-ctx, content((0, 0), right-parenthesis, auto-scale: false)) + .bounds + let right-with-attach-bounds = cetz + .process + .many(cetz-ctx, content( + (0, 0), + right-parenthesis-with-attach, + auto-scale: false, + )) + .bounds + let right-voffset = calc.abs(right-bounds + .low + .at(1) - right-with-attach-bounds.low.at(1)) + if parenthesis.tr != none and parenthesis.br != none { + right-voffset /= 2 + } else if (parenthesis.tr != none) { + right-voffset *= -1 + } + + ( + _ => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + content((lx, ly), anchor: "mid-east", left-parenthesis, auto-scale: false) + content( + (rx, ry - right-voffset), + anchor: "mid-west", + right-parenthesis-with-attach, + auto-scale: false, + ) + }) + } + + (ctx, drawing, cetz-rec) +} + + +#let draw-resonance-parenthesis(parenthesis, draw-function, ctx) = { + import cetz.draw: * + let left-name = "parenthesis-" + str(ctx.id) + let right-name = "parenthesis-" + str(ctx.id + 1) + ctx.id += 2 + let last-anchor = ctx.last-anchor + let (drawing, ctx) = draw-function( + ctx, + parenthesis.body, + after-operator: true, + ) + + let drawing = get-ctx(cetz-ctx => { + let (ctx: cetz-ctx, bounds, drawables) = cetz.process.many( + cetz-ctx, + drawing, + ) + let sub-bounds = cetz.util.revert-transform(cetz-ctx.transform, bounds) + let height = parenthesis.height + if height == none { + height = utils.bounding-box-height(sub-bounds) + } else { + height = utils.convert-length(cetz-ctx, height) + } + let width = utils.bounding-box-width(sub-bounds) + + let ( + left-parenthesis, + _, + right-parenthesis-with-attach, + ) = parenthesis-content(parenthesis, height, cetz-ctx) + + let right-anchor = (rel: (width, 0), to: (name: left-name, anchor: "east")) + on-layer( + 1, + { + content( + last-anchor.anchor, + name: left-name, + anchor: "mid-east", + left-parenthesis, + auto-scale: false, + ) + content( + right-anchor, + name: right-name, + anchor: "mid-west", + right-parenthesis-with-attach, + auto-scale: false, + ) + }, + ) + ( + ctx => ( + ctx: cetz-ctx, + drawables: drawables, + bounds: bounds, + ), + ) + }) + + ctx.last-anchor = ( + type: "coord", + anchor: (name: right-name, anchor: "mid-east"), + ) + (ctx, drawing) +} diff --git a/packages/preview/alchemist/0.2.0/src/elements/fragment.typ b/packages/preview/alchemist/0.2.0/src/elements/fragment.typ new file mode 100644 index 0000000000..fffd3f9c19 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/elements/fragment.typ @@ -0,0 +1,132 @@ +#let split-equation(mol, equation: false, split-charge: false) = { + if equation { + mol = mol.body + if mol.has("children") { + mol = mol.children + } else { + mol = (mol,) + } + } + + + let result = () + let last-number = false + let count = 0 + for m in mol { + if m.has("text") { + let text = m.text + if str.match(text, regex("^[A-Z][a-z]*$")) != none { + if last-number { + result.at(-1) = (result.at(-1).at(0) + m, result.at(-1).at(1)) + } else { + result.push((m, true)) + } + last-number = false + } else if str.match(text, regex("^[0-9]+$")) != none { + if last-number { + panic("Consecutive numbers in fragment fragment") + } + last-number = true + result.push((m, true)) + } else { + panic("Invalid fragment fragment content") + } + } else if m.func() == math.lr { + result.push((m, true)) + last-number = false + } else if m.func() == math.attach { + if split-charge { + result.push(( + math.attach( + "", + bl: m.at("bl", default: none), + tl: m.at("tl", default: none), + ), + false, + )) + result.push(( + math.attach( + m.base, + b: m.at("b", default: none), + t: m.at("t", default: none), + ), + true, + )) + result.push(( + math.attach( + "", + br: m.at("br", default: none), + tr: m.at("tr", default: none), + ), + false, + )) + } else { + result.push((m, true)) + } + last-number = false + } else if m == [ ] { + continue + } else { + panic("Invalid fragment fragment content") + } + count += 1 + } + (result, count) +} + +#let fragment-cor-regex = "[0-9]*[A-Z][a-z]*'*" +#let exponent-regex = "((?:[0-9]+(?:\\+|\\-)?)|[A-Z]|\\+|\\-)" +#let exponent-base-regex = "(?:(\\^|_)" + exponent-regex + ")?(?:(\\^|_)" + exponent-regex + ")?" +#let fragment-regex = regex("^ *(" + fragment-cor-regex + ")" + exponent-base-regex) + +#let make-fragment-text(text, indexed) = ( + math.equation(eval(text, mode: "math")), + indexed, +) + +#let split-fragment-string(mol, split-charge: false) = { + let aux(str) = { + let match = str.match(fragment-regex) + if match == none { + panic(str + " is not a valid fragment") + } else if match.captures.at(1) == match.captures.at(3) and match + .captures + .at(1) != none { + panic("You cannot use an exponent and a subscript twice") + } + let eq = "\"" + match.captures.at(0) + "\"" + + let has-exponent = match.captures.at(2) != none + let has-subscript = match.captures.at(4) != none + + let charge = "" + if has-exponent { + charge += match.captures.at(1) + "(" + match.captures.at(2) + ")" + } + if match.captures.at(3) != none { + charge += match.captures.at(3) + "(" + match.captures.at(4) + ")" + } + if charge != "" { + if split-charge { + charge = "\"\"" + charge + eq = (make-fragment-text(eq, true), make-fragment-text(charge, false)) + } else { + eq += charge + eq = (make-fragment-text(eq, true),) + } + } else { + eq = (make-fragment-text(eq, true),) + } + + (eq, match.end) + } + + let count = 0 + let fragments = while not mol.len() == 0 { + count += 1 + let (eq, end) = aux(mol) + mol = mol.slice(end) + eq + } + (fragments, count) +} diff --git a/packages/preview/alchemist/0.2.0/src/elements/lewis.typ b/packages/preview/alchemist/0.2.0/src/elements/lewis.typ new file mode 100644 index 0000000000..48aa726665 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/elements/lewis.typ @@ -0,0 +1,140 @@ +#import "@preview/cetz:0.5.2" + +/// Create a lewis function that is then used to draw a lewis +/// formulae element around the fragment +/// +/// - draw-function (function): The function that will be used to draw the lewis element. It should takes three arguments: the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-lewis(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Lewis function takes no positional arguments") + } + let args = args.named() + let angle = args.at("angle", default: none) + let radius = args.at("radius", default: none) + ( + angle: angle, + radius: radius, + draw: (ctx, cetz-ctx) => draw-function(ctx, cetz-ctx, args), + ) + } +} + +/// draw a sigle electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// +/// The position of the electron is set by the `offset` argument. Available values are: +/// - "top": the electron is placed above the fragment center line +/// - "bottom": the electron is placed below the fragment center line +/// - "center": the electron is placed at the fragment center line +/// +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-single(offset: "top"), +/// )) +/// single(angle:-2) +/// fragment("B", lewis:( +/// lewis-single(offset: "bottom"), +/// )) +/// single(angle:-2) +/// fragment("C", lewis:( +/// lewis-single(offset: "center"), +/// )) +/// }) +/// ```) +#let lewis-single = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-single.radius) + let gap = args.at("gap", default: ctx.config.lewis-single.gap) + let offset = args.at("offset", default: ctx.config.lewis-single.offset) + let gap = if offset == "top" { + gap + } else if offset == "bottom" { + -gap + } else if offset == "center" { + 0 + } else { + panic("Invalid position, expected 'top', 'bottom' or 'center'") + } + let fill = args.at("fill", default: ctx.config.lewis-single.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-single.stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron around the fragment +/// +/// It is possible to change the distance from the center of +/// the electron with the `gap` argument. +/// It is also possible to change the `radius`, `stroke` and `fill` arguments +/// #example(``` +/// #skeletize({ +/// fragment("A", lewis:( +/// lewis-double(), +/// lewis-double(angle: 90deg), +/// lewis-double(angle: 180deg), +/// lewis-double(angle: -90deg) +/// )) +/// }) +/// ```) +#let lewis-double = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let radius = args.at("radius", default: ctx.config.lewis-double.radius) + let gap = args.at("gap", default: ctx.config.lewis-double.gap) + let fill = args.at("fill", default: ctx.config.lewis-double.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-double.stroke) + circle((0, -gap), radius: radius, fill: fill, stroke: stroke) + circle((0, gap), radius: radius, fill: fill, stroke: stroke) +}) + +/// Draw a pair of electron liked by a single line +/// +/// It is possible to change the length of the line with the `lenght` argument. +/// It is also possible to change the `stroke` agument +/// #example(``` +/// #skeletize({ +/// fragment("B", lewis:( +/// lewis-line(angle: 45deg), +/// lewis-line(angle: 135deg), +/// lewis-line(angle: -45deg), +/// lewis-line(angle: -135deg) +/// )) +/// }) +/// ```) +#let lewis-line = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let length = args.at("length", default: ctx.config.lewis-line.length) + let stroke = args.at("stroke", default: ctx.config.lewis-line.stroke) + line((0, -length / 2), (0, length / 2), stroke: stroke) +}) + + +/// Draw a rectangle to denote a lone pair of electrons +/// +/// It is possible to change the height and width of the rectangle with the `height` and `width` arguments. +/// It is also possible to change the `fill` and `stroke` arguments +/// #example(``` +/// #skeletize({ +/// fragment("C", lewis:( +/// lewis-rectangle(), +/// lewis-rectangle(angle: 180deg) +/// )) +/// }) +/// ```) +#let lewis-rectangle = build-lewis((ctx, cetz-ctx, args) => { + import cetz.draw: * + let height = args.at("height", default: ctx.config.lewis-rectangle.height) + let width = args.at("width", default: ctx.config.lewis-rectangle.width) + let fill = args.at("fill", default: ctx.config.lewis-rectangle.fill) + let stroke = args.at("stroke", default: ctx.config.lewis-rectangle.stroke) + rect( + (-width / 2, -height / 2), + (width / 2, height / 2), + fill: fill, + stroke: stroke, + ) +}) diff --git a/packages/preview/alchemist/0.2.0/src/elements/links.typ b/packages/preview/alchemist/0.2.0/src/elements/links.typ new file mode 100644 index 0000000000..cc50c585bd --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/elements/links.typ @@ -0,0 +1,370 @@ +#import "@preview/cetz:0.5.2" +#import "../drawer/cram.typ": * +#import "../utils/utils.typ" + + +/// Create a link function that is then used to draw a link between two points +/// +/// - draw-function (function): The function that will be used to draw the link. It should takes four arguments: the length of the link, the alchemist context, the cetz context, and a dictionary of named arguments that can be used to configure the links +/// -> function +#let build-link(draw-function) = { + (..args) => { + if args.pos().len() != 0 { + panic("Links takes no positional arguments") + } + let args = args.named() + ( + ( + type: "link", + draw: (length, ctx, cetz-ctx, override: (:)) => { + let args = args + for (key, val) in override { + args.insert(key, val) + } + draw-function(length, ctx, cetz-ctx, args) + }, + links: (:), + ..args, + ), + ) + } +} + +/// Draw a single line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// single(stroke: red + 5pt) +/// fragment("B") +/// }) +///```) +#let single = build-link((length, ctx, _, args) => { + import cetz.draw: * + line( + (0, 0), + (length, 0), + stroke: args.at("stroke", default: ctx.config.single.stroke), + ) +}) + +/// Draw a double line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the two lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double( +/// stroke: orange + 2pt, +/// gap: .8em +/// ) +/// fragment("B") +/// }) +///```) +/// It is also possible to only change the color and width of the +/// lines seperately with the `stroke-left` and `stroke-right` arguments. +/// #example(``` +/// #skeletize({ +/// double(stroke: 2pt, stroke-right: red, stroke-left: (dash: "dashed")) +/// }) +/// ```) +/// This link also supports an `offset` argument that can be set to `left`, `right` or `center`. +///It allows to make either the let side, right side or the center of the double line to be aligned with the link point. +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// double(offset: "right") +/// fragment("B") +/// double(offset: "left") +/// fragment("C") +/// double(offset: "center") +/// fragment("D") +/// }) +///```) +#let double = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length( + cetz-ctx, + args.at("gap", default: ctx.config.double.gap), + ) / 2 + let offset = args.at("offset", default: ctx.config.double.offset) + let coeff = args.at("offset-coeff", default: ctx.config.double.offset-coeff) + if coeff < 0 or coeff > 1 { + panic("Invalid offset-coeff value: must be between 0 and 1") + } + let gap-offset = if offset == "right" { + -gap + } else if offset == "left" { + gap + } else if offset == "center" { + 0 + } else { + panic("Invalid offset value: must be \"left\", \"right\" or \"center\"") + } + + line( + ..if offset == "right" { + let x = length * (1 - coeff) / 2 + ((x, -gap + gap-offset), (x + length * coeff, -gap + gap-offset)) + } else { + ((0, -gap + gap-offset), (length, -gap + gap-offset)) + }, + stroke: args.at( + "stroke-right", + default: args.at("stroke", default: ctx.config.double.stroke), + ), + ) + line( + ..if offset == "left" { + let x = length * (1 - coeff) / 2 + ((x, gap + gap-offset), (x + length * coeff, gap + gap-offset)) + } else { + ((0, gap + gap-offset), (length, gap + gap-offset)) + }, + stroke: args.at( + "stroke-left", + default: args.at("stroke", default: ctx.config.double.stroke), + ), + ) +}) + +/// Draw a triple line between two fragments +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple() +/// fragment("B") +/// }) +///```) +/// It is possible to change the color and width of the line +/// with the `stroke` argument and the gap between the three lines +/// with the `gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// triple( +/// stroke: blue + .5pt, +/// gap: .15em +/// ) +/// fragment("B") +/// }) +///```) +/// It is also possible to only change the color and width of the +/// lines seperately with the `stroke-left`, `stroke-center` and `stroke-right` arguments. +/// #example(``` +/// #skeletize({ +/// triple(stroke: 2pt, stroke-left: red, stroke-center: green, stroke-right: blue) +/// }) +/// ```) +#let triple = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + let gap = utils.convert-length( + cetz-ctx, + args.at("gap", default: ctx.config.triple.gap), + ) + line( + (0, 0), + (length, 0), + stroke: args.at( + "stroke-left", + default: args.at("stroke", default: ctx.config.triple.stroke), + ), + ) + line( + (0, -gap), + (length, -gap), + stroke: args.at( + "stroke-center", + default: args.at("stroke", default: ctx.config.triple.stroke), + ), + ) + line( + (0, gap), + (length, gap), + stroke: args.at( + "stroke-right", + default: args.at("stroke", default: ctx.config.triple.stroke), + ), + ) +}) + +/// Draw a filled cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-right( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-right = build-link((length, ctx, cetz-ctx, args) => cram( + (0, 0), + (length, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a filled cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke and fill color of the arrow +/// with the `stroke` and `fill` arguments. You can also change the base length of the arrow with the `base-length` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-filled-left( +/// stroke: red + 2pt, +/// fill: green, +/// base-length: 2em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-filled-left = build-link((length, ctx, cetz-ctx, args) => cram( + (length, 0), + (0, 0), + ctx, + cetz-ctx, + args, +)) + +/// Draw a hollow cram between two fragments with the arrow pointing to the right +/// It is a shorthand for `cram-filled-right(fill: none)` +#let cram-hollow-right = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((0, 0), (length, 0), ctx, cetz-ctx, args) +}) + +/// Draw a hollow cram between two fragments with the arrow pointing to the left +/// It is a shorthand for `cram-filled-left(fill: none)` +#let cram-hollow-left = build-link((length, ctx, cetz-ctx, args) => { + args.fill = none + args.stroke = args.at("stroke", default: black) + cram((length, 0), (0, 0), ctx, cetz-ctx, args) +}) + +/// Draw a dashed cram between two fragments with the arrow pointing to the right +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the arrow base length with the `base-length` argument and the tip length with the `tip-length` argument. +// You can also change distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-right( +/// stroke: red + 2pt, +/// base-length: 2em, +/// tip-length: 1em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-right = build-link(( + length, + ctx, + cetz-ctx, + args, +) => dashed-cram( + (0, 0), + (length, 0), + length, + ctx, + cetz-ctx, + args, +)) + +/// Draw a dashed cram between two fragments with the arrow pointing to the left +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left() +/// fragment("B") +/// }) +///```) +/// It is possible to change the stroke of the lines in the arrow +/// with the `stroke` argument. You can also change the base length of the arrow with the `base-length` argument and distance between the dashes with the `dash-gap` argument +/// #example(``` +/// #skeletize({ +/// fragment("A") +/// cram-dashed-left( +/// stroke: red + 2pt, +/// base-length: 2em, +/// dash-gap: .5em +/// ) +/// fragment("B") +/// }) +///```) +#let cram-dashed-left = build-link((length, ctx, cetz-ctx, args) => dashed-cram( + (length, 0), + (0, 0), + length, + ctx, + cetz-ctx, + args, +)) + + +/// Draw a plus sign between two fragments +/// #example(```` +/// #skeletize({ +/// fragment("A") +/// plus-link() +/// fragment("B") +/// }) +/// ````) +/// You can change the filling, size and stroke of the glyph with the `fill`, `size` and `stoke` arguments. The default values are the parent `text` parameters. +#let plus-link = build-link((length, ctx, cetz-ctx, args) => { + import cetz.draw: * + content( + anchor: "mid", + (length / 2, 0), + { + set text(fill: args.fill) if "fill" in args + set text(stroke: args.stroke) if "stroke" in args + set text(size: args.size) if "size" in args + text($+$) + }, + ) +}) \ No newline at end of file diff --git a/packages/preview/alchemist/0.2.0/src/utils/anchors.typ b/packages/preview/alchemist/0.2.0/src/utils/anchors.typ new file mode 100644 index 0000000000..5c83a19cfb --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/utils/anchors.typ @@ -0,0 +1,326 @@ +#import "angles.typ" +#import "context.typ" as context_ +#import "utils.typ": * +#import "@preview/cetz:0.5.2" +#import cetz.draw: * + +#let anchor-north-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + + let a = (a - x) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-north-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "north")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (b - y) + delta + (a, b) +} + +#let anchor-south-west(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "west")), + ) + let a = (x - a) + delta + let b = (y - b) + delta + (a, b) +} + +#let anchor-south-east(cetz-ctx, (x, y, _), delta, fragment, id) = { + let (cetz-ctx, (_, b, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "south")), + ) + let (cetz-ctx, (a, _, _)) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "east")), + ) + let a = (a - x) + delta + let b = (y - b) + delta + (a, b) +} + +/// Calculate an anchor position around a fragment using an ellipse +/// at a given angle +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - cetz-ctx (cetz-ctx): the cetz context +/// - angle (float, int, angle): the angle of the anchor +/// - fragment (string): the fragment name +/// - id (string): the fragment subpart id +/// - margin (length, none): the margin around the fragment +/// -> anchor: the anchor position around the fragment +#let fragment-anchor(ctx, cetz-ctx, angle, fragment, id, margin: none) = { + let angle = angles.angle-correction(angle) + let fragment-margin = if margin == none { + ctx.config.fragment-margin + } else { + margin + } + fragment-margin = convert-length(cetz-ctx, fragment-margin) + let (cetz-ctx, center) = cetz.coordinate.resolve( + cetz-ctx, + (name: fragment, anchor: (id, "mid")), + ) + let (a, b) = if angles.angle-in-range(angle, 0deg, 90deg) { + anchor-north-east(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 90deg, 180deg) { + anchor-north-west(cetz-ctx, center, fragment-margin, fragment, id) + } else if angles.angle-in-range(angle, 180deg, 270deg) { + anchor-south-west(cetz-ctx, center, fragment-margin, fragment, id) + } else { + anchor-south-east(cetz-ctx, center, fragment-margin, fragment, id) + } + + // https://www.petercollingridge.co.uk/tutorials/computational-geometry/finding-angle-around-ellipse/ + let angle = if angles.angle-in-range-inclusive( + angle, + 0deg, + 90deg, + ) or angles.angle-in-range-strict( + angle, + 270deg, + 360deg, + ) { + calc.atan(calc.tan(angle) * a / b) + } else { + calc.atan(calc.tan(angle) * a / b) - 180deg + } + + + if a == 0 or b == 0 { + panic("Ellipse " + ellipse + " has no width or height") + } + (center.at(0) + a * calc.cos(angle), center.at(1) + b * calc.sin(angle)) +} + +/// Return the index to choose if the link connection is not overridden +#let link-fragment-index(angle, end, count, vertical) = { + if not end { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + 0 + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + 0 + } else { + count + } + } else { + if vertical and angles.angle-in-range-strict(angle, 0deg, 180deg) { + count + } else if angles.angle-in-range-strict(angle, 90deg, 270deg) { + count + } else { + 0 + } + } +} + +#let fragment-link-anchor(name, id, count) = { + if count <= id { + panic("The last fragment only has " + str(count) + " connections") + } + if id == -1 { + id = count - 1 + } + (name: name, anchor: (str(id), "mid")) +} + +#let link-fragment-anchor(name: none, id, count) = { + if id >= count { + panic("This fragment only has " + str(count) + " anchors") + } + if id == -1 { + panic("The index of the fragment to link to must be defined") + } + if name == none { + (name: str(id), anchor: "mid") + } else { + (name: name, anchor: (str(id), "mid")) + } +} + + +#let calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) = { + let to-pos = (name: link.to-name, anchor: "mid") + if link.to == none or link.from == none { + let angle = link.at( + "angle", + default: angles.angle-between(cetz-ctx, link.from-pos, to-pos), + ) + link.angle = angle + if link.from == none { + link.from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + } + if link.to == none { + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + } + } + if link.from == -1 { + link.from = 0 + } + if link.to == -1 { + link.to = ctx.hooks.at(link.to-name).count - 1 + } + let start = fragment-anchor( + ctx, + cetz-ctx, + link.angle, + link.from-name, + str(link.from), + ) + let end = fragment-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ((start, end), angles.angle-between(cetz-ctx, start, end)) +} + +#let calculate-link-mol-anchors(ctx, cetz-ctx, link) = { + if link.to == none { + let angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: "mid"), + ), + ) + link.to = link-fragment-index( + angle, + true, + ctx.hooks.at(link.to-name).count - 1, + ctx.hooks.at(link.to-name).vertical, + ) + link.angle = angle + } else if "angle" not in link { + link.angle = angles.angle-correction( + angles.angle-between( + cetz-ctx, + link.from-pos, + (name: link.to-name, anchor: (str(link.to), "mid")), + ), + ) + } + let end-anchor = fragment-anchor( + ctx, + cetz-ctx, + link.angle + 180deg, + link.to-name, + str(link.to), + ) + ( + ( + link.from-pos, + end-anchor, + ), + angles.angle-between(cetz-ctx, link.from-pos, end-anchor), + ) +} + +#let calculate-mol-link-anchors(ctx, cetz-ctx, link) = { + ( + ( + fragment-anchor( + ctx, + cetz-ctx, + link.angle, + link.from-name, + str(link.from), + ), + link.name + "-end-anchor", + ), + link.angle, + ) +} + +#let calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + let angle = angles.angle-correction( + angles.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) + let from = link-fragment-index( + angle, + false, + ctx.hooks.at(link.from-name).count - 1, + ctx.hooks.at(link.from-name).vertical, + ) + let start-anchor = fragment-anchor( + ctx, + cetz-ctx, + angle, + link.from-name, + str(from), + ) + ( + ( + start-anchor, + hook.hook, + ), + angles.angle-between(cetz-ctx, start-anchor, hook.hook), + ) +} + +#let calculate-link-hook-link-anchors(ctx, cetz-ctx, link) = { + let hook = ctx.hooks.at(link.to-name) + ( + ( + link.from-pos, + hook.hook, + ), + angles.angle-between(cetz-ctx, link.from-pos, hook.hook), + ) +} + +#let calculate-link-link-anchors(link) = { + ((link.from-pos, link.name + "-end-anchor"), link.angle) +} + +#let calculate-link-anchors(ctx, cetz-ctx, link) = { + if link.type == "mol-hook-link" { + calculate-mol-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.type == "link-hook-link" { + calculate-link-hook-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none and link.from-name != none { + calculate-mol-mol-link-anchors(ctx, cetz-ctx, link) + } else if link.to-name != none { + calculate-link-mol-anchors(ctx, cetz-ctx, link) + } else if link.from-name != none { + calculate-mol-link-anchors(ctx, cetz-ctx, link) + } else { + calculate-link-link-anchors(link) + } +} + diff --git a/packages/preview/alchemist/0.2.0/src/utils/angles.typ b/packages/preview/alchemist/0.2.0/src/utils/angles.typ new file mode 100644 index 0000000000..b8faca0860 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/utils/angles.typ @@ -0,0 +1,98 @@ +#import "@preview/cetz:0.5.2" + +/// Convert any angle to an angle between 0deg and 360deg +#let angle-correction(a) = { + if type(a) == angle { + a = a.deg() + } + if type(a) != float and type(a) != int { + panic("angle-correction: The angle must be a number or an angle") + } + while a < 0 { + a += 360 + } + + calc.rem(a, 360) * 1deg +} + +/// Check if the angle is in the range [from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle < to +} + +/// Check if the angle is in the range ]from, to[ +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-strict(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle > from and angle < to +} + +/// Check if the angle is in the range [from, to] +/// +/// - angle (float, int, angle): The angle to check +/// - from (float): The start of the range +/// - to (float): The end of the range +/// -> true if the angle is in the range +#let angle-in-range-inclusive(angle, from, to) = { + if to < from { + panic("angle-in-range: The 'to' angle must be greater than the 'from' angle") + } + angle = angle-correction(angle) + angle >= from and angle <= to +} + +/// get the angle between two anchors +/// +/// - ctx (cetz-ctx): The cetz context +/// - from (anchor): The first anchor +/// - to (anchor): The second anchor +#let angle-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let angle = calc.atan2(to-x - from-x, to-y - from-y) + angle +} + +/// Calculate the angle from an object with "relative", "absolute" or "angle" key +/// according to the alchemist context (see the manual to see how angles are calculated) +/// +/// - ctx (alchemist-ctx): the alchemist context +/// - object (dict): the object to get the angle from +/// - default (angle): the default angle +/// -> the calculated angle +#let angle-from-ctx(ctx, object, default) = { + if "relative" in object { + object.at("relative") + ctx.relative-angle + } else if "absolute" in object { + object.at("absolute") + } else if "angle" in object { + object.at("angle") * ctx.config.angle-increment + } else { + default + } +} + +/// Overwrite the offset based on the angle +#let angle-override(angle, ctx) = { + if ctx.in-cycle { + ("offset": "left") + } else { + (:) + } +} diff --git a/packages/preview/alchemist/0.2.0/src/utils/context.typ b/packages/preview/alchemist/0.2.0/src/utils/context.typ new file mode 100644 index 0000000000..89d4548d61 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/utils/context.typ @@ -0,0 +1,33 @@ +#let update-parent-context(parent-ctx, ctx) = { + let last-anchor = if parent-ctx.last-anchor != ctx.last-anchor { + ( + ..parent-ctx.last-anchor, + drew: true, + ) + } else { + parent-ctx.last-anchor + } + ( + ..parent-ctx, + last-anchor: last-anchor, + hooks: ctx.hooks, + hooks-links: ctx.hooks-links, + links: ctx.links, + ) +} + +/// Set the last anchor in the context to the given anchor and save it if needed +#let set-last-anchor(ctx, anchor) = { + if ctx.last-anchor.type == "link" { + let drew = ctx.last-anchor.at("drew", default: false) + if drew and anchor.type == "link" and anchor.name == ctx.last-anchor.name { + drew = false + let _ = ctx.links.pop() + } + if not drew { + ctx.links.push(ctx.last-anchor) + } + ctx.last-anchor.drew = true + } + (..ctx, last-anchor: anchor) +} diff --git a/packages/preview/alchemist/0.2.0/src/utils/utils.typ b/packages/preview/alchemist/0.2.0/src/utils/utils.typ new file mode 100644 index 0000000000..86de745036 --- /dev/null +++ b/packages/preview/alchemist/0.2.0/src/utils/utils.typ @@ -0,0 +1,75 @@ +#import "@preview/cetz:0.5.2" + +#let convert-length(ctx, num) = { + // This function come from the cetz module + return if type(num) == length { + float(num.to-absolute() / ctx.length) + } else if type(num) == ratio { + num + } else { + float(num) + } +} + +/// get the distance between two anchors +#let distance-between(ctx, from, to) = { + let (ctx, (from-x, from-y, _)) = cetz.coordinate.resolve(ctx, from) + let (ctx, (to-x, to-y, _)) = cetz.coordinate.resolve(ctx, to) + let distance = calc.sqrt(calc.pow(to-x - from-x, 2) + calc.pow( + to-y - from-y, + 2, + )) + distance +} + +/// merge two imbricated dictionaries together +/// The second dictionary is the default value if the key is not present in the first dictionary +#let merge-dictionaries(dict1, default) = { + let result = default + for (key, value) in dict1 { + if type(value) == dictionary { + result.insert(key, merge-dictionaries(value, default.at(key))) + } else { + result.insert(key, value) + } + } + result +} + + +/// get the type of an element by its name +/// +/// - body (drawable): the chemfig body of a molecule +/// - name (string): the name of the element to get the type +/// -> string +#let get-element-type(body, name) = { + for element in body { + if type(element) != dictionary { + continue + } + if "name" in element and element.name == name { + return element.type + } + if element.type == "branch" or element.type == "cycle" or element.type == "parenthesis" { + let type = get-element-type(element.body, name) + if type != none { + return type + } + } + } + none +} + +/// Calculate the height of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-height(bounds) = { + calc.abs(bounds.high.at(1) - bounds.low.at(1)) +} + +/// Calculate the width of a bounding box +/// - bounds (dictionary): the bounding box +/// -> float +#let bounding-box-width(bounds) = { + calc.abs(bounds.high.at(0) - bounds.low.at(0)) +} \ No newline at end of file diff --git a/packages/preview/alchemist/0.2.0/typst.toml b/packages/preview/alchemist/0.2.0/typst.toml new file mode 100644 index 0000000000..18d8691e4e --- /dev/null +++ b/packages/preview/alchemist/0.2.0/typst.toml @@ -0,0 +1,13 @@ +[package] +name = "alchemist" +version = "0.2.0" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>"] +license = "MIT" +compiler = "0.14.0" +repository = "https://github.com/Typsium/alchemist" +description = "A package to render skeletal formulas using CeTZ" +exclude = ["generate_images.sh", "generate_images.awk", ".gitignore","images"] +categories = ["visualization", "paper"] +disciplines = ["education", "chemistry", "biology"] +keywords = ["chemistry", "chemical", "formula", "molecule", "chemiviz", "structure", "organic", "lewis", "bond", "aromatic"]