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"]