diff --git a/packages/preview/diagraph/0.3.1/LICENSE b/packages/preview/diagraph/0.3.1/LICENSE new file mode 100644 index 0000000000..a85204e590 --- /dev/null +++ b/packages/preview/diagraph/0.3.1/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/diagraph/0.3.1/README.md b/packages/preview/diagraph/0.3.1/README.md new file mode 100644 index 0000000000..19743093aa --- /dev/null +++ b/packages/preview/diagraph/0.3.1/README.md @@ -0,0 +1,100 @@ +# diagraph + +A simple Graphviz binding for Typst using the WebAssembly plugin system. + +## Usage + +### Basic usage + + +You can render a Graphviz Dot string to a SVG image using the `render` function: + +```typ +#render("digraph { a -> b }") +``` + +Alternatively, you can use `raw-render` to pass a `raw` instead of a string: + +<!--EXAMPLE(raw-render)--> +````typ +#raw-render( + ```dot + digraph { + a -> b + } + ``` +) +```` + + +For more information about the Graphviz Dot language, you can check the [official documentation](https://graphviz.org/documentation/). + +### Advanced usage + +Check the [manual](https://raw.githubusercontent.com/Robotechnic/diagraph/main/doc/manual.pdf) for more information about the plugin. + + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details + +## Changelog + +### 0.3.1 + +- Updated graphviz version to 12.2.1 +- Fixed a bug with the font being incorrectly set +- Added adjacency lists to the graph rendering possibilities + +### 0.3.0 + +- Added support for edge labels +- Added a manual generated with Typst +- Updated graphviz version +- Fix an error in math mode detection + +### 0.2.5 + +- If the shape is point, the label isn't displayed +- Now a minimum size is not enforced if the node label is empty +- Added support for font alternatives + +### 0.2.4 + +- Added support for xlabels which are now rendered by Typst +- Added support for cluster labels which are now rendered by Typst +- Fix a margin problem with the clusters + +### 0.2.3 + +- Updated to typst 0.11.0 +- Added support for `fontcolor`, `fontsize` and `fontname` nodes attributes +- Diagraph now uses a protocol generator to generate the wasm interface + +### 0.2.2 + +- Fix an alignment issue +- Added a better mathematic formula recognition for node labels + +### 0.2.1 + +- Added support for relative lenghts in the `width` and `height` arguments +- Fix various bugs + +### 0.2.0 + +- Node labels are now handled by Typst + +### 0.1.2 + +- Graphs are now scaled to make the graph text size match the document text size + +### 0.1.1 + +- Remove the `raw-render-rule` show rule because it doesn't allow use of custom font and the `render` / `raw-render` functions are more flexible +- Add the `background` parameter to the `render` and `raw-render` typst functions and default it to `transparent` instead of `white` +- Add center attribute to draw graph in the center of the svg in the `render` c function + +### 0.1.0 + +Initial working version diff --git a/packages/preview/diagraph/0.3.1/adjacency.typ b/packages/preview/diagraph/0.3.1/adjacency.typ new file mode 100644 index 0000000000..0ce65b8384 --- /dev/null +++ b/packages/preview/diagraph/0.3.1/adjacency.typ @@ -0,0 +1,191 @@ +#import "internals.typ": render + +#let inches = ( + "width", + "height", + "len", + "lheight", + "lwidth", + "margin", + "nodesep", + "page", + "size", + "vertices", +) + +#let value-to-str(key, value) = { + if type(value) == length { + if key in inches { + str(value.to-absolute().inches()) + } else { + str(value.to-absolute().pt()) + } + } else if type(value) == color { + "\"" + value.to-hex() + "\"" + } else if value == none { + "none" + } else if value == true { + "true" + } else if value == false { + "false" + } else { + "\"" + str(value) + "\"" + } +} + +#let dict-to-graph-args(args, sep: ";", parent: "") = { + for (key, value) in args { + if type(value) == dictionary { + if sep == "," { + panic("Invalid argument for " + parent + "[" + key + "=...]") + } + key + "[" + dict-to-graph-args(value, sep: ",", parent: key) + "]" + } else { + key + "=" + if type(value) == array { + "\"(" + value.map(value-to-str.with(key)).join(",") + ")\"" + } else { + value-to-str(key, value) + } + } + sep + } +} + +#let create-nodes(min, max) = { + range(min, max).map(str).join(";") +} + +#let create-attributes(labels) = { + labels + .enumerate() + .map(label => { + if type(label.at(1)) == dictionary { + let _ = label.at(1).remove("label", default: "") + _ = label.at(1).remove("xlabel", default: "") + str(label.at(0)) + "[" + dict-to-graph-args(label.at(1), sep: ",") + "]" + } + }) + .join("") +} + +#let create-clusters(clusters) = { + clusters + .enumerate() + .map(cluster => { + let id = str(cluster.at(0)) + let nodes = cluster.at(1) + "subgraph cluster_" + id + "{" + if type(nodes) == dictionary { + let _ = nodes.remove("label", default: "") + let subnodes = nodes.remove("nodes") + dict-to-graph-args(nodes) + subnodes.map(str).join(";") + } else { + nodes.map(str).join(";") + } + "};" + }) + .join("") +} + +#let build-edges(adjacency, directed) = { + adjacency + .enumerate() + .map(edges-list => { + edges-list + .at(1) + .enumerate() + .map(edge => { + if edge.at(1) == none { + "" + } else { + str(edges-list.at(0)) + if directed { + " -> " + } else { + " -- " + } + str(edge.at(0)) + ";" + } + }) + .join("") + }) + .join("") +} + +#let adjacency(..args) = context { + if args.pos().len() != 1 { + panic("adjacency() requires one argument: an adjacency matrix") + } + let adjacency = args.at(0) + let graph-params = args.named() + let vertex-labels = graph-params.remove("vertex-labels", default: ()) + let directed = graph-params.remove("directed", default: true) + let clusters = graph-params.remove("clusters", default: ()) + let debug = graph-params.remove("debug", default: false) + let clip = graph-params.remove("clip", default: true) + + render( + if directed { + "digraph" + } else { + "graph" + } + "{" + dict-to-graph-args(graph-params) + create-nodes( + 0, + adjacency.len(), + ) + ";" + create-clusters(clusters) + create-attributes(vertex-labels) + build-edges(adjacency, directed) + "}", + debug: debug, + clip: clip, + labels: name => { + let id = int(name) + if id < vertex-labels.len() { + let label = vertex-labels.at(id) + if type(label) == dictionary { + label.at("label", default: "") + } else { + label + } + } else { + "" + } + }, + xlabels: name => { + let id = int(name) + if id < vertex-labels.len() { + let label = vertex-labels.at(id) + if type(label) == dictionary { + label.at("xlabel", default: none) + } else { + none + } + } else { + none + } + }, + edges: (name, edges) => { + let id = int(name) + let result = (:) + for edge in edges { + let edge-id = int(edge) + let label = adjacency.at(id).at(edge-id) + if label != none { + result.insert(edge, [#label]) + } + } + result + }, + clusters: (name) => { + let id = int(name.split("_").at(1)) + if id < clusters.len() { + let cluster = clusters.at(id) + if type(cluster) == dictionary { + cluster.at("label", default: none) + } else { + none + } + } else { + none + } + } + ) +} + diff --git a/packages/preview/diagraph/0.3.1/graphviz_interface/diagraph.wasm b/packages/preview/diagraph/0.3.1/graphviz_interface/diagraph.wasm new file mode 100755 index 0000000000..c76b2caf35 Binary files /dev/null and b/packages/preview/diagraph/0.3.1/graphviz_interface/diagraph.wasm differ diff --git a/packages/preview/diagraph/0.3.1/graphviz_interface/protocol.typ b/packages/preview/diagraph/0.3.1/graphviz_interface/protocol.typ new file mode 100644 index 0000000000..16d512bef0 --- /dev/null +++ b/packages/preview/diagraph/0.3.1/graphviz_interface/protocol.typ @@ -0,0 +1,390 @@ +/// Encodes a 32-bytes integer into big-endian bytes. +#let encode-int(value) = { + bytes(( + calc.rem(calc.quo(value, 0x1000000), 0x100), + calc.rem(calc.quo(value, 0x10000), 0x100), + calc.rem(calc.quo(value, 0x100), 0x100), + calc.rem(calc.quo(value, 0x1), 0x100), + )) +} + +/// Decodes a big-endian integer from the given bytes. +#let decode-int(bytes) = { + let result = 0 + for byte in array(bytes.slice(0,4)) { + result = result * 256 + byte + } + (result, 4) +} + +/// Encodes a string into bytes. +#let encode-string(value) = { + bytes(value) + bytes((0x00,)) +} + +/// Decodes a string from the given bytes. +#let decode-string(bytes) = { + let length = 0 + for byte in array(bytes) { + length = length + 1 + if byte == 0x00 { + break + } + } + if length == 0 { + ("", 1) + } else { + (str(bytes.slice(0, length - 1)), length) + } + //(array(bytes.slice(0, length - 1)), length) +} + +/// Encodes a boolean into bytes +#let encode-bool(value) = { + if value { + bytes((0x01,)) + } else { + bytes((0x00,)) + } +} + +/// Decodes a boolean from the given bytes +#let decode-bool(bytes) = { + if bytes.at(0) == 0x00 { + (false, 1) + } else { + (true, 1) + } +} + +/// Encodes a character into bytes +#let encode-char(value) = { + bytes(value) +} + +/// Decodes a character from the given bytes +#let decode-char(bytes) = { + (bytes.at(0), 1) +} + +#let fractional-to-binary(fractional_part, max_dec, zero) = { + let result = 0 + let i = 22 - max_dec + let first_one = 0 + if zero { + while fractional_part < 1 { + fractional_part *= 2 + first_one += 1 + } + fractional_part -= 1 + i = 23 + } + while i > 0 and fractional_part > 0 { + fractional_part *= 2 + if fractional_part >= 1 { + result += calc.pow(2, i - 1) + fractional_part -= 1 + } + i -= 1 + } + (result, first_one) +} + +#let float-to-int(value) = { + if value == 0 { + return 0 + } + let sign = if value < 0.0 { 1 } else { 0 } + let value = calc.abs(value) + let mantissa = calc.trunc(value) + let fractional_part = calc.fract(value) + let exponent = if mantissa == 0 { + 0 + } else { + calc.floor(calc.log(base: 2, mantissa)) - 1 + } + let (fractional_part, first_one) = fractional-to-binary(fractional_part, exponent, mantissa == 0) + mantissa *= calc.pow(2, 22 - exponent) + mantissa += fractional_part + if exponent == 0 { + exponent = -first_one + } + exponent += 127 + return sign * calc.pow(2, 31) + exponent * calc.pow(2, 23) + mantissa +} + +#let mantissa-to-float(mantissa) = { + let result = 1.0 + for i in range(0,23) { + if calc.rem(mantissa, 2) == 1 { + result += 1.0/calc.pow(2, 23 - i) + } + mantissa = calc.quo(mantissa, 2) + } + result +} + +#let int-to-float(value) = { + if value == 0 { + return 0.0 + } + let sign = if value >= calc.pow(2, 31) { + value -= calc.pow(2, 31) + -1 + } else { + 1 + } + let exponent = calc.rem(calc.quo(value, calc.pow(2, 23)), calc.pow(2, 8)) + let mantissa = calc.rem(value, calc.pow(2, 23)) + sign * calc.pow(2, exponent - 127) * mantissa-to-float(mantissa) +} + +/// Encodes a float into bytes +#let encode-float(value) = { + encode-int(float-to-int(value)) +} + +#let encode-point(value) = { + encode-float(value.pt()) +} + +/// Decodes a float from the given bytes +#let decode-float(bytes) = { + let (decoded, size) = decode-int(bytes) + (int-to-float(decoded), size) +} + +#let decode-point(bytes) = { + let (value, size) = decode-float(bytes) + (value * 1pt, size) +} + +/// Encodes a list of elements into bytes +#let encode-list(arr, encoder) = { + let length = encode-int(arr.len()) + let encoded = bytes(arr.map(encoder).map(array).flatten()) + length + encoded +} + +/// Decodes a list of elements from the given bytes +#let decode-list(bytes, decoder) = { + let (length, length_size) = decode-int(bytes) + let result = () + let offset = length_size + for i in range(0, length) { + let (element, size) = decoder(bytes.slice(offset, bytes.len())) + result.push(element) + offset += size + } + (result, offset) +} +#let decode-EdgeLabelInfo(bytes) = { + let offset = 0 + let (f_to, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_index, size) = decode-int(bytes.slice(offset, bytes.len())) + offset += size + let (f_label, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_label_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len())) + offset += size + let (f_xlabel, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_xlabel_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len())) + offset += size + let (f_headlabel, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_headlabel_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len())) + offset += size + let (f_taillabel, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_taillabel_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len())) + offset += size + let (f_color, size) = decode-int(bytes.slice(offset, bytes.len())) + offset += size + let (f_font_name, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_font_size, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + (( + to: f_to, + index: f_index, + label: f_label, + label_math_mode: f_label_math_mode, + xlabel: f_xlabel, + xlabel_math_mode: f_xlabel_math_mode, + headlabel: f_headlabel, + headlabel_math_mode: f_headlabel_math_mode, + taillabel: f_taillabel, + taillabel_math_mode: f_taillabel_math_mode, + color: f_color, + font_name: f_font_name, + font_size: f_font_size, + ), offset) +} +#let decode-NodeLabelInfo(bytes) = { + let offset = 0 + let (f_name, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_label, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len())) + offset += size + let (f_xlabel, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_xlabel_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len())) + offset += size + let (f_color, size) = decode-int(bytes.slice(offset, bytes.len())) + offset += size + let (f_font_name, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_font_size, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_edges_infos, size) = decode-list(bytes.slice(offset, bytes.len()), decode-EdgeLabelInfo) + offset += size + (( + name: f_name, + label: f_label, + math_mode: f_math_mode, + xlabel: f_xlabel, + xlabel_math_mode: f_xlabel_math_mode, + color: f_color, + font_name: f_font_name, + font_size: f_font_size, + edges_infos: f_edges_infos, + ), offset) +} +#let decode-ClusterLabelInfo(bytes) = { + let offset = 0 + let (f_name, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_label, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len())) + offset += size + let (f_color, size) = decode-int(bytes.slice(offset, bytes.len())) + offset += size + let (f_font_name, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + let (f_font_size, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + (( + name: f_name, + label: f_label, + math_mode: f_math_mode, + color: f_color, + font_name: f_font_name, + font_size: f_font_size, + ), offset) +} +#let encode-SizedEdgeLabel(value) = { + encode-bool(value.at("overwrite")) + encode-point(value.at("width")) + encode-point(value.at("height")) + encode-bool(value.at("xoverwrite")) + encode-point(value.at("xwidth")) + encode-point(value.at("xheight")) + encode-bool(value.at("headoverwrite")) + encode-point(value.at("headwidth")) + encode-point(value.at("headheight")) + encode-bool(value.at("tailoverwrite")) + encode-point(value.at("tailwidth")) + encode-point(value.at("tailheight")) +} +#let encode-SizedNodeLabel(value) = { + encode-bool(value.at("overwrite")) + encode-bool(value.at("xoverwrite")) + encode-point(value.at("width")) + encode-point(value.at("height")) + encode-point(value.at("xwidth")) + encode-point(value.at("xheight")) + encode-list(value.at("edges_size"), encode-SizedEdgeLabel) +} +#let encode-SizedClusterLabel(value) = { + encode-point(value.at("width")) + encode-point(value.at("height")) +} +#let decode-EdgeCoordinates(bytes) = { + let offset = 0 + let (f_x, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_y, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_xx, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_xy, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_headx, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_heady, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_tailx, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_taily, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + (( + x: f_x, + y: f_y, + xx: f_xx, + xy: f_xy, + headx: f_headx, + heady: f_heady, + tailx: f_tailx, + taily: f_taily, + ), offset) +} +#let decode-NodeCoordinates(bytes) = { + let offset = 0 + let (f_x, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_y, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_xx, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_xy, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_edges, size) = decode-list(bytes.slice(offset, bytes.len()), decode-EdgeCoordinates) + offset += size + (( + x: f_x, + y: f_y, + xx: f_xx, + xy: f_xy, + edges: f_edges, + ), offset) +} +#let decode-ClusterCoordinates(bytes) = { + let offset = 0 + let (f_x, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + let (f_y, size) = decode-point(bytes.slice(offset, bytes.len())) + offset += size + (( + x: f_x, + y: f_y, + ), offset) +} +#let encode-GetGraphInfo(value) = { + encode-string(value.at("dot")) +} +#let encode-renderGraph(value) = { + encode-point(value.at("font_size")) + encode-string(value.at("dot")) + encode-list(value.at("labels"), encode-SizedNodeLabel) + encode-list(value.at("cluster_labels"), encode-SizedClusterLabel) + encode-string(value.at("engine")) +} +#let decode-Engines(bytes) = { + let offset = 0 + let (f_engines, size) = decode-list(bytes.slice(offset, bytes.len()), decode-string) + offset += size + (( + engines: f_engines, + ), offset) +} +#let decode-graphInfo(bytes) = { + let offset = 0 + let (f_error, size) = decode-bool(bytes.slice(offset, bytes.len())) + offset += size + let (f_labels, size) = decode-list(bytes.slice(offset, bytes.len()), decode-NodeCoordinates) + offset += size + let (f_cluster_labels, size) = decode-list(bytes.slice(offset, bytes.len()), decode-ClusterCoordinates) + offset += size + let (f_svg, size) = decode-string(bytes.slice(offset, bytes.len())) + offset += size + (( + error: f_error, + labels: f_labels, + cluster_labels: f_cluster_labels, + svg: f_svg, + ), offset) +} +#let decode-GraphInfo(bytes) = { + let offset = 0 + let (f_labels, size) = decode-list(bytes.slice(offset, bytes.len()), decode-NodeLabelInfo) + offset += size + let (f_cluster_labels, size) = decode-list(bytes.slice(offset, bytes.len()), decode-ClusterLabelInfo) + offset += size + (( + labels: f_labels, + cluster_labels: f_cluster_labels, + ), offset) +} diff --git a/packages/preview/diagraph/0.3.1/internals.typ b/packages/preview/diagraph/0.3.1/internals.typ new file mode 100644 index 0000000000..18c34d302b --- /dev/null +++ b/packages/preview/diagraph/0.3.1/internals.typ @@ -0,0 +1,561 @@ +#import "graphviz_interface/protocol.typ": * +#let plugin = plugin("graphviz_interface/diagraph.wasm") + + +/// Converts a string containing escape sequences to content. +#let parse-string(s) = { + let result = [] + let row = "" + let is-escaped = false + + for cluster in s { + if is-escaped { + is-escaped = false + + if cluster == "l" { + result += align(left, row) + row = "" + } else if cluster == "n" { + result += align(center, row) + row = "" + } else if cluster == "r" { + result += align(right, row) + row = "" + } else { + row += cluster + } + } else if cluster == "\\" { + is-escaped = true + } else { + row += cluster + } + } + + set block(spacing: 0.65em) + result + align(center, row) +} + +/// Convert a number to a string with a fixed number of digits. +/// The number is padded with zeros on the left if necessary. +#let int-to-string(n, digits, base: 10) = { + let n-str = str(n, base: base) + let n-len = n-str.len() + let zeros = "0" * (digits - n-len) + zeros + n-str +} + +/// Return a buffer in readable format. +#let buffer-repr(buffer) = [ + #repr(array(buffer).map(x => "0x" + int-to-string(x, 2, base: 16)).join(", ")) +] + +/// COnvert a string to math or text mode +#let convert-label(label, math-mode) = { + if label == "" { + return "" + } + if math-mode { + math.equation(eval(mode: "math", label)) + } else { + parse-string(label) + } +} + +/// Return a formatted label based on its color, font and content. +#let label-format(color, font, fontsize, label) = { + if label == "" { + return "" + } + set text(fill: rgb(int-to-string(color, 8, base: 16)), bottom-edge: "bounds") + set text(size: fontsize) if fontsize.pt() != 0 + set text(font: font) if font != ("",) + text(label) +} +/// Check that all edges in the overwrite dictionary are present in the encoded label edges. +#let check-overwrite(encoded-label, edge-overwrite) = { + for to-node in edge-overwrite.keys() { + if encoded-label.at("edges_infos").find(edge => edge.at("to") == to-node) == none { + panic("Node \"" + encoded-label.at("name") + "\" does not have an edge to node \"" + to-node + "\"") + } + } +} + +#let edge-label-format(edge-overwrite, edge-label, color, font, fontsize, name) = { + let overwrite-list = edge-overwrite.at(edge-label.at("to"), default: none) + if overwrite-list != none { + let index = edge-label.at("index") + if type(overwrite-list) == array and index < overwrite-list.len() { + let overwrite = overwrite-list.at(index) + if type(overwrite) != dictionary { + if name == "label" { + return overwrite + } + } else if name in overwrite { + return overwrite.at(name) + } + } else if index == 0 { + if type(overwrite-list) != dictionary { + if name == "label" { + return overwrite-list + } + } else if name in overwrite-list { + return overwrite-list.at(name) + } + } + } + let label-content = edge-label.at(name) + if label-content == "" { + return "" + } + label-format(color, font, fontsize, convert-label(label-content, edge-label.at(name + "_math_mode"))) +} + +#let format-edge-labels(encoded-label, edge-overwrite) = { + let formatted-edge-labels = () + for edge-label in encoded-label.at("edges_infos") { + let font-size = text.size + if edge-label.at("font_size").pt() != 0 { + font-size = edge-label.at("font_size") + } + let font-name = edge-label.at("font_name").split(",") + let font-color = edge-label.at("color") + + // panic(edge-label, edge-overwrite) + + formatted-edge-labels.push(( + native: "label" in edge-overwrite, + label: edge-label-format(edge-overwrite, edge-label, edge-label.at("color"), font-name, font-size, "label"), + xnative: "xlabel" in edge-overwrite, + xlabel: edge-label-format(edge-overwrite, edge-label, edge-label.at("color"), font-name, font-size, "xlabel"), + tailnative: "taillabel" in edge-overwrite, + taillabel: edge-label-format( + edge-overwrite, + edge-label, + edge-label.at("color"), + font-name, + font-size, + "taillabel", + ), + headnative: "headlabel" in edge-overwrite, + headlabel: edge-label-format( + edge-overwrite, + edge-label, + edge-label.at("color"), + font-name, + font-size, + "headlabel", + ), + )) + } + formatted-edge-labels +} + +/// Return +/// - the label content depending on the overwrite method. +/// - a boolean indicating if the label was overwritten. +#let label-overwrite(label-type, label, overwrite-method, font-name, font-size) = { + let name = label.at("name") + if type(overwrite-method) == dictionary and name in overwrite-method { + return (overwrite-method.at(name), true) + } + + if type(overwrite-method) == function { + let overwrite = overwrite-method(name) + if overwrite != none { + return (overwrite, true) + } + } + + let label-content = label.at(label-type) + if label-content != "" { + label-content = convert-label(label-content, label.at("math_mode")) + label-content = label-format(label.at("color"), font-name, font-size, label-content) + return (label-content, true) + } + + return ("", false) +} + +/// Get an array of evaluated labels from a graph. +#let get-labels(labels, xlabels, clusters, edges, dot) = { + let overridden-labels = ( + "dot": dot, + ) + // panic(buffer-repr(encode-GetGraphInfo(overridden-labels))) + let encoded-labels = plugin.get_labels(encode-GetGraphInfo(overridden-labels)) + let (graph-labels, _) = decode-GraphInfo(encoded-labels) + // panic(graph-labels) + ( + graph-labels.at("labels").map(encoded-label => { + let font-size = text.size + if encoded-label.at("font_size").pt() != 0 { + font-size = encoded-label.at("font_size") + } + let font-name = encoded-label.at("font_name").split(",") + + let (label, overwrite) = label-overwrite("label", encoded-label, labels, font-name, font-size) + + let (xlabel, xoverwrite) = label-overwrite("xlabel", encoded-label, xlabels, font-name, font-size) + + let edges-overwrite = if type(edges) == function { + edges(encoded-label.at("name"), encoded-label.at("edges_infos").map(edge => edge.at("to"))) + } else { + edges.at(encoded-label.at("name"), default: (:)) + } + check-overwrite(encoded-label, edges-overwrite) + let edge-labels = format-edge-labels(encoded-label, edges-overwrite) + ( + overwrite: overwrite, + label: label, + xoverwrite: xoverwrite, + xlabel: xlabel, + edges_infos: edge-labels, + ) + }), + graph-labels.at("cluster_labels").map(encoded-label => { + let font-name = encoded-label.at("font_name").split(",") + let font-size = text.size + if encoded-label.at("font_size").pt() != 0 { + font-size = encoded-label.at("font_size") + } + let (label, overwrite) = label-overwrite("label", encoded-label, clusters, font-name, font-size) + ( + overwrite: overwrite, + label: label, + ) + }), + ) +} + + +#let label-dimensions(color, font, fontsize, label) = { + if label == "" { + ( + width: 0pt, + height: 0pt, + ) + } else { + let label = label-format(color, font, fontsize, label) + measure(label) + } +} + +#let measure-label(edge, name, margin: 0pt) = { + let dim = measure(edge.at(name)) + ( + width: dim.width + margin, + height: dim.height + margin, + ) +} + +/// Encodes the dimensions of labels into bytes. +#let encode-label-dimensions(labels, overridden-labels, overridden-xlabels) = { + let edges-margin = 5pt + labels.map(label => { + let edges-size = label.at("edges_infos").map(edge => { + let label = measure-label(edge, "label", margin: edges-margin) + let xlabel = measure-label(edge, "xlabel", margin: edges-margin) + let taillabel = measure-label(edge, "taillabel", margin: edges-margin) + let headlabel = measure-label(edge, "headlabel", margin: edges-margin) + ( + overwrite: edge.at("label") != "", + width: label.width, + height: label.height, + xoverwrite: edge.at("xlabel") != "", + xwidth: xlabel.width, + xheight: xlabel.height, + tailoverwrite: edge.at("taillabel") != "", + tailwidth: taillabel.width, + tailheight: taillabel.height, + headoverwrite: edge.at("headlabel") != "", + headwidth: headlabel.width, + headheight: headlabel.height, + ) + }) + + let dimensions = if label.at("overwrite") { + measure(label.at("label")) + } else { + (width: 0pt, height: 0pt) + } + let xdimensions = if label.at("xoverwrite") { + measure(label.at("xlabel")) + } else { + (width: 0pt, height: 0pt) + } + ( + overwrite: label.at("label") != "" or label.at("overwrite"), + xoverwrite: label.at("xlabel") != "" or label.at("xoverwrite"), + width: dimensions.width, + height: dimensions.height, + xwidth: xdimensions.width, + xheight: xdimensions.height, + edges_size: edges-size, + ) + + }) +} + +#let encode-cluster-label-dimensions(clusters-labels-infos, clusters) = { + clusters-labels-infos.map(label => { + let dim = measure(label.at("label")) + ( + width: dim.width, + height: dim.height, + ) + }) +} + +/// Converts any relative length to an absolute length. +#let relative-to-absolute(value, container-dimension) = { + if type(value) == relative { + let absolute-part = relative-to-absolute(value.length, container-dimension) + let ratio-part = relative-to-absolute(value.ratio, container-dimension) + return absolute-part + ratio-part + } + if type(value) == length { + return value.to-absolute() + } + if type(value) == ratio { + return value * container-dimension + } + panic("Expected relative length, found " + str(type(value))) +} + +#let debug-rectangle(x, y, width, height) = { + place( + top + left, + dx: x, + dy: y, + rect(height: height, width: width, fill: none, stroke: red), + ) +} + +/// Renders a graph with Graphviz. +#let render( + /// A string containing Dot code. + dot, + /// Nodes whose name appear in this dictionary will have their label + /// overridden with the corresponding content. Defaults to an empty + /// dictionary. + labels: (:), + /// Nodes whose name appear in this dictionary will have their xlabel + /// overridden with the corresponding content. Defaults to an empty + /// dictionary. + xlabels: (:), + /// Nodes whose name appear in this dictionary will have their + /// edge label overridden with the corresponding content. + /// Each vale mut be a list of dictionaries, one for each edge. + /// Each dictionary can have the following keys: + /// - `label`: the content of the label + /// - `xlabel`: the content of the xlabel + /// - `taillabel`: the content of the taillabel + /// - `headlabel`: the content of the headlabel + edges: (:), + /// Cluster names whose name appear in this dictionary will have their + /// label overridden with the corresponding content. Defaults to an empty + /// dictionary. + clusters: (:), + /// The name of the engine to generate the graph with. Defaults to `"dot"`. + engine: "dot", + /// The width of the image to display. If set to `auto` (the default), will be + /// the width of the generated SVG or, if the height is set to a value, it + /// will be scaled to keep the aspect ratio. + width: auto, + /// The height of the image to display. If set to `auto` (the default), will + /// be the height of the generated SVG or if the width is set to a value, it + /// will be scaled to keep the aspect ratio. + height: auto, + /// Whether to hide parts of the graph that extend beyond its frame. Defaults + /// to `true`. + clip: true, + /// Display a red rectangle around each label to help with debugging. + debug: false, + /// A color or gradient to fill the background with. If set to `none` (the + /// default), the background will be transparent. + background: none, +) = { + set math.equation(numbering: none) + if type(dot) != str { + panic("The dot code must be a string") + } + + layout(((width: container-width, height: container-height)) => ( + context { + let (labels-infos, clusters-labels-infos) = get-labels( + labels, + xlabels, + clusters, + edges, + dot, + ) + // return [#repr(labels-infos)] + // return [#repr(clusters-labels-infos)] + let labels-info-count = labels-infos.len() + + let encoded-data = ( + "font_size": text.size.to-absolute(), + "dot": dot, + "labels": encode-label-dimensions(labels-infos, labels, xlabels), + "cluster_labels": encode-cluster-label-dimensions(clusters-labels-infos, clusters), + "engine": engine, + ) + + // return [#repr(encoded-data)] + // return [#buffer-repr(encode-renderGraph(encoded-data))] + + let output = plugin.render(encode-renderGraph(encoded-data)) + + if output.at(0) != 0 { + return { + show: highlight.with(fill: red) + set text(white) + raw(block: true, str(output)) + } + } + + let output = decode-graphInfo(output).at(0) + + // return [#repr(output)] + + // Get SVG dimensions. + let (width: svg-width, height: svg-height) = measure(image.decode(output.at("svg"), format: "svg")) + + let final-width = if width == auto { + svg-width + } else { + relative-to-absolute(width, container-width) + } + let final-height = if height == auto { + svg-height + } else { + relative-to-absolute(height, container-height) + } + + if width == auto and height != auto { + let ratio = final-height / svg-height + final-width = svg-width * ratio + } else if width != auto and height == auto { + let ratio = final-width / svg-width + final-height = svg-height * ratio + } + // Rescale the final image to the desired size. + show: block.with( + width: final-width, + height: final-height, + clip: clip, + breakable: false, + ) + + set align(top + left) + + show: scale.with( + origin: top + left, + x: final-width / svg-width * 100%, + y: final-height / svg-height * 100%, + ) + + // Construct the graph and its labels. + show: block.with(width: svg-width, height: svg-height, fill: background) + + // Display SVG. + image.decode( + output.at("svg"), + format: "svg", + width: svg-width, + height: svg-height, + ) + + let place-label(dx, dy, label) = { + let dimensions = measure(label) + place( + top + left, + dx: dx - dimensions.width / 2, + dy: final-height - dy - dimensions.height / 2 - (final-height - svg-height), + label, + ) + if debug { + debug-rectangle( + dx - dimensions.width / 2, + final-height - dy - dimensions.height / 2 - (final-height - svg-height), + dimensions.width, + dimensions.height, + ) + } + } + + let place-edge-label(dx, dy, name, edge-info) = { + if edge-info.at(name) == "" { + return + } + place-label( + dx, + dy, + edge-info.at(name), + ) + } + + // Place labels. + for (label-info, label-coordinates) in labels-infos.zip(output.at("labels")) { + for (edge-info, edge-coordinates) in label-info.at("edges_infos").zip(label-coordinates.at("edges")) { + place-edge-label( + edge-coordinates.at("x"), + edge-coordinates.at("y"), + "label", + edge-info, + ) + place-edge-label( + edge-coordinates.at("xx"), + edge-coordinates.at("xy"), + "xlabel", + edge-info, + ) + place-edge-label( + edge-coordinates.at("headx"), + edge-coordinates.at("heady"), + "headlabel", + edge-info, + ) + place-edge-label( + edge-coordinates.at("tailx"), + edge-coordinates.at("taily"), + "taillabel", + edge-info, + ) + } + + if label-info.at("overwrite") { + place-label( + label-coordinates.at("x"), + label-coordinates.at("y"), + label-info.at("label"), + ) + } + if label-info.at("xoverwrite") { + place-label( + label-coordinates.at("xx"), + label-coordinates.at("xy"), + label-info.at("xlabel"), + ) + } + } + + + for (clusters-infos, cluster-coordinates) in clusters-labels-infos.zip(output.at("cluster_labels")) { + if clusters-infos.at("overwrite") { + place-label( + cluster-coordinates.at("x"), + cluster-coordinates.at("y"), + clusters-infos.at("label"), + ) + } + } + } + )) +} + +#let engine-list() = { + let list = plugin.engine_list() + let (engines, _) = decode-Engines(plugin.engine_list()) + engines +} \ No newline at end of file diff --git a/packages/preview/diagraph/0.3.1/lib.typ b/packages/preview/diagraph/0.3.1/lib.typ new file mode 100644 index 0000000000..fa69497f22 --- /dev/null +++ b/packages/preview/diagraph/0.3.1/lib.typ @@ -0,0 +1,15 @@ +#import "internals.typ": render, engine-list +#import "adjacency.typ": adjacency + +/// Renders a graph with Graphviz. +/// +/// See `render`'s documentation in `internals.typ` for a list of valid +/// arguments and their descriptions. +#let raw-render( + /// A `raw` element containing Dot code. + raw, + ..args, +) = { + assert(raw.has("text"), message: "`raw-render` expects a `raw` element") + return render(raw.text, ..args) +} diff --git a/packages/preview/diagraph/0.3.1/typst.toml b/packages/preview/diagraph/0.3.1/typst.toml new file mode 100644 index 0000000000..19b6a12297 --- /dev/null +++ b/packages/preview/diagraph/0.3.1/typst.toml @@ -0,0 +1,12 @@ +[package] +name = "diagraph" +version = "0.3.1" +entrypoint = "lib.typ" +authors = ["Robotechnic <@Robotechnic>", "Malo <@MDLC01>"] +license = "MIT" +description = "Draw graphs with Graphviz. Use mathematical formulas as labels." +repository = "https://github.com/Robotechnic/diagraph.git" +keywords = ["graphviz", "graph", "diagram"] +categories = ["components", "visualization", "integration"] +compiler = "0.11.0" +exclude = ["graphviz_interface/src/*", "graphviz_interface/.*", "graphviz_interface/Makefile","graphviz_interface/*.prot", "examples/*", ".gitignore", "Makefile"]