From 25983318ad997967212a61a2c743fb8f924b52e8 Mon Sep 17 00:00:00 2001 From: Tom <45141234+Robotechnic@users.noreply.github.com> Date: Fri, 31 Jan 2025 09:48:18 +0100 Subject: [PATCH] alchemist:0.1.4 (#1648) --- 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 000000000..a85204e59 --- /dev/null +++ b/packages/preview/alchemist/0.1.4/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.4/README.md b/packages/preview/alchemist/0.1.4/README.md new file mode 100644 index 000000000..240ca80b9 --- /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 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-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 000000000..17bc6d31f --- /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 000000000..d0a1d6379 --- /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 000000000..03401dd32 --- /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 000000000..f0420b47e --- /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 000000000..5fc933a60 --- /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 000000000..5c35b4562 --- /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 000000000..1c5dafb9d --- /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 000000000..ef9b56cc4 --- /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 000000000..5e9788b27 --- /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 000000000..baa64cb75 --- /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 000000000..d33067b2a --- /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 000000000..b02a0402e --- /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 000000000..1fc5f788d --- /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 000000000..9eeda41b1 --- /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 000000000..f51218c79 --- /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 000000000..d83a114a8 --- /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 000000000..318550a06 --- /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 000000000..9ed91d51a --- /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", ]