diff --git a/src/config.typ b/src/config.typ new file mode 100644 index 0000000..5d7c1ff --- /dev/null +++ b/src/config.typ @@ -0,0 +1,78 @@ +#let config( + default-font-family: "Ubuntu Mono", + default-font-size: 1em, + italic-font-family: "Ubuntu Mono", + italic-font-size: 0.8em, + background: white, + text-color: black, + link-color: black, + bit-i-color: black, + border-color: black, + bit-width: 30, + bit-height: 30, + description-margin: 10, + dash-length: 6, + dash-space: 4, + arrow-size: 10, + margins: (20, 20, 20, 20), + arrow-margin: 4, + values-gap: 5, + arrow-label-distance: 5, + force-descs-on-side: false, + left-labels: false, + width: 1200, + height: 800, + full-page: false +) = { + return ( + default-font-family: default-font-family, + default-font-size: default-font-size, + italic-font-family: italic-font-family, + italic-font-size: italic-font-size, + background: background, + text-color: text-color, + link-color: link-color, + bit-i-color: bit-i-color, + border-color: border-color, + bit-width: bit-width, + bit-height: bit-height, + description-margin: description-margin, + dash-length: dash-length, + dash-space: dash-space, + arrow-size: arrow-size, + margins: margins, + arrow-margin: arrow-margin, + values-gap: values-gap, + arrow-label-distance: arrow-label-distance, + force-descs-on-side: force-descs-on-side, + left-labels: left-labels, + width: width, + height: height, + full-page: full-page + ) +} + +#let dark = config.with( + background: rgb(24, 24, 24), + text-color: rgb(216, 216, 216), + link-color: rgb(150, 150, 150), + bit-i-color: rgb(180, 180, 180), + border-color: rgb(180, 180, 180) +) + +#let blueprint = config.with( + background: rgb(53, 77, 158), + text-color: rgb(231, 236, 249), + link-color: rgb(169, 193, 228), + bit-i-color: rgb(214, 223, 244), + border-color: rgb(214, 223, 244) +) + +#let transparent = config.with( + background: rgb(0, 0, 0, 0), + text-color: rgb(128, 128, 128), + link-color: rgb(128, 128, 128), + bit-i-color: rgb(128, 128, 128), + border-color: rgb(128, 128, 128) +) + diff --git a/src/lib.typ b/src/lib.typ new file mode 100644 index 0000000..fa6c0a3 --- /dev/null +++ b/src/lib.typ @@ -0,0 +1,4 @@ +#let version = version((0,0,1)) + +#import "config.typ" +#import "schema.typ" \ No newline at end of file diff --git a/src/range.typ b/src/range.typ new file mode 100644 index 0000000..96a7ce4 --- /dev/null +++ b/src/range.typ @@ -0,0 +1,65 @@ +#import "util.typ" + +#let key(start, end) = { + return str(start) + "->" + str(end) +} + +#let bits(range) = { + return range.end - range.start + 1 +} + +#let parse-span(span) = { + let start-end = span.split("-") + if start-end.len() == 1 { + start-end.push(start-end.first()) + } + let start = int(start-end.last()) + let end = int(start-end.first()) + return (start, end) +} + +#let make( + start, + end, + name, + description: "", + values: none, + depends-on: none +) = { + return ( + start: start, + end: end, + name: name, + description: description, + values: values, + depends-on: depends-on, + last-value-y: -1 + ) +} + +#let load(start, end, data) = { + let values = none + let bits = end - start + 1 + + if "values" in data { + values = (:) + for (val, desc) in data.values { + val = util.z-fill(val, bits) + values.insert(val, desc) + } + } + + let depends-on = data.at("depends-on", default: none) + if depends-on != none { + depends-on = parse-span(str(depends-on)) + } + + return make( + start, + end, + str(data.name), + description: data.at("description", default: ""), + values: values, + depends-on: depends-on + ) +} \ No newline at end of file diff --git a/src/renderer.typ b/src/renderer.typ new file mode 100644 index 0000000..9223138 --- /dev/null +++ b/src/renderer.typ @@ -0,0 +1,467 @@ +#import "@preview/cetz:0.2.2": canvas, draw + +#import "range.typ" as rng +#import "structure.typ" +#import "vec.typ" + +#let draw-rect(color, x, y, width, height, thickness: 0) = { + let fill = none + let stroke = color + thickness * 1pt + if thickness == 0 { + fill = color + stroke = none + } + draw.rect((x, -y), (x + width, -y - height), fill: fill, stroke: stroke) +} + +#let draw-text( + txt, + color, + x, + y, + anchor: "center", + font: none, + italic: false, + size: 1em, + fill: none +) = { + let text-params = (:) + if font != none { + text-params.insert("font", font) + } + if italic { + text-params.insert("style", "italic") + } + + let content-params = (:) + if fill != none { + content-params.insert("fill", fill) + content-params.insert("frame", "rect") + content-params.insert("padding", 4pt) + } + + draw.content( + (x, -y), + text(txt, fill: color, size: size, ..text-params), + anchor: anchor, + stroke: none, + ..content-params + ) +} + +#let draw-line(color, a, b) = { + let (x0, y0) = a + let (x1, y1) = b + draw.line((x0, -y0), (x1, -y1), stroke: color) +} + +#let draw-lines(color, ..pts) = { + let pts = pts.pos().map(pt => (pt.at(0), -pt.at(1))) + draw.line(..pts, stroke: color) +} + +#let draw-poly(color, ..pts, thickness: 0) = { + let pts = pts.pos().map(pt => (pt.at(0), -pt.at(1))) + let params = ( + stroke: (paint: color, thickness: thickness), + fill: none + ) + if thickness == 0 { + params = ( + stroke: none, + fill: color + ) + } + draw.line(..pts, ..params) +} + +#let draw-underbracket(config, start, end, bits-y) = { + let bit-w = config.bit-width + let bit-h = config.bit-height + + let x0 = start + bit-w / 2 + let x1 = end - bit-w / 2 + let y0 = bits-y + bit-h * 1.25 + let y1 = bits-y + bit-h * 1.5 + + let col = config.link-color + draw-lines(col, (x0, y0), (x0, y1), (x1, y1), (x1, y0)) +} + +#let draw-link( + config, + start-x, + start-y, + end-x, + end-y +) = { + let bit-h = config.bit-height + let arrow-margin = config.arrow-margin + + if end-x > start-x { + end-x -= arrow-margin + } else { + end-x += arrow-margin + } + + draw-lines( + config.link-color, + (start-x, start-y + bit-h * 1.5), + (start-x, end-y + bit-h / 2), + (end-x, end-y + bit-h / 2), + ) +} + +#let draw-values(config, values, desc-x, desc-y) = { + let shapes = () + let txt-col = config.text-color + let bit-w = config.bit-height // Why ? I don't remember + let gap = config.values-gap + + for (val, desc) in values.pairs().sorted(key: p => p.first()) { + desc-y += gap + let txt = val + " = " + desc + shapes += draw-text( + txt, txt-col, desc-x + bit-w / 2, desc-y, + anchor: "north-west", + font: config.italic-font-family, + italic: true, + size: config.italic-font-size + ) + desc-y += 8 // TODO: change this + } + + return (shapes, desc-x, desc-y) +} + +#let draw-description( + config, + range_, + start-x, + start-y, + width, + desc-x, + desc-y +) = { + let shapes = () + let bit-w = config.bit-width + let bit-h = config.bit-height + + if config.left-labels { + desc-x = calc.min(desc-x, start-x + width / 2 - bit-w) + } else { + desc-x = calc.max(desc-x, start-x + width / 2 + bit-w) + } + + shapes += draw-underbracket(config, start-x, start-x + width, start-y) + + let mid-x = start-x + width / 2 + shapes += draw-link(config, mid-x, start-y, desc-x, desc-y) + + let txt-anchor = "west" + + if config.left-labels { + txt-anchor -= "east" + } + + shapes += draw-text( + range_.description, + config.text-color, + desc-x, desc-y + bit-h / 2, + anchor: "west" + ) + + // TODO: change this + desc-y += 18 + + if range_.values != none and range_.depends-on == none { + let shapes_ + (shapes_, _, desc-y) = draw-values(config, range_.values, desc-x, desc-y) + shapes += shapes_ + } + + desc-y += config.description-margin + + return (shapes, desc-x, desc-y) +} + +#let draw-arrow(config, start-x, start-y, end-x, end-y, label: "") = { + let shapes = () + let dash-len = config.dash-length + let dash-space = config.dash-space + let arrow-size = config.arrow-size + let link-col = config.link-color + let txt-col = config.text-color + let arrow-label-dist = config.arrow-label-distance + + let start = vec.vec(start-x, start-y) + let end = vec.vec(end-x, end-y) + let start-end = vec.sub(end, start) + let d = vec.normalize(start-end) + + let dashes = int(vec.mag(start-end) / (dash-len + dash-space)) + + for i in range(dashes) { + let a = vec.add( + start, + vec.mul(d, i * (dash-len + dash-space)) + ) + let b = vec.add( + a, + vec.mul(d, dash-len) + ) + + shapes += draw-line(link-col, (a.x, a.y), (b.x, b.y)) + } + + let n = vec.vec(d.y, -d.x) + let width = arrow-size / 1.5 + let p1 = vec.sub( + end, + vec.sub( + vec.mul(d, arrow-size), + vec.mul(n, width) + ) + ) + let p2 = vec.sub( + end, + vec.add( + vec.mul(d, arrow-size), + vec.mul(n, width) + ) + ) + + shapes += draw-poly( + link-col, + (end.x, end.y), + (p1.x, p1.y), + (p2.x, p2.y) + ) + + if label != "" { + shapes += draw-text( + label, + txt-col, + (start.x + end.x) / 2, + (start.y + end.y) / 2 + arrow-label-dist, + anchor: "north" + ) + } + + return shapes +} + +#let draw-dependency( + draw-struct, config, + struct, structures, bits-x, bits-y, range_, desc-x, desc-y +) = { + let shapes = () + + let bit-w = config.bit-width + let bit-h = config.bit-height + let arrow-margin = config.arrow-margin + + let start-i = struct.bits - range_.end - 1 + let start-x = bits-x + start-i * bit-w + let width = rng.bits(range_) * bit-w + + shapes += draw-underbracket(config, start-x, start-x + width, bits-y) + let depend-range = struct.ranges.at(rng.key(..range_.depends-on)) + let prev-range-y = bits-y + bit-h * 1.5 + + let prev-depend-y = if depend-range.last-value-y == -1 { + bits-y + bit-h * 1.5 + } else { + depend-range.last-value-y + } + + let depend-start-i = struct.bits - depend-range.end - 1 + let depend-start-x = bits-x + depend-start-i * bit-w + let depend-width = rng.bits(depend-range) * bit-w + let depend-mid = depend-start-x + depend-width / 2 + shapes += draw-underbracket(config, depend-start-x, depend-start-x + depend-width, bits-y) + + for (val, data) in range_.values.pairs().sorted(key: p => p.first()) { + shapes += draw-arrow(config, depend-mid, prev-depend-y, depend-mid, desc-y - arrow-margin) + + let val-ranges = (:) + for i in range(rng.bits(depend-range)) { + val-ranges.insert( + str(depend-range.end - i), + (name: val.at(i)) + ) + } + + let val-struct = ( + bits: rng.bits(depend-range), + start: depend-range.start, + ranges: val-ranges + ) + val-struct = structure.load("", val-struct) + + let shapes_ + (shapes_, ..) = draw-struct(config, val-struct, structures, ox: depend-start-x, oy: desc-y) + shapes += shapes_ + + let y = desc-y + bit-h * 1.5 + + let x1 + let x2 + + // Arrow from left to right + if depend-range.end > range_.start { + x1 = depend-start-x + depend-width + arrow-margin + x2 = start-x - arrow-margin + + // Arrow from right to left + } else { + x1 = depend-start-x - arrow-margin + x2 = start-x + width + arrow-margin + } + + shapes += draw-arrow(config, x1, y, x2, y, label: data.description) + shapes += draw-arrow(config, + start-x + width - bit-w, + prev-range-y, + start-x + width - bit-w, + desc-y + bit-h - arrow-margin + ) + + prev-depend-y = desc-y + bit-h * 2 + arrow-margin + prev-range-y = prev-depend-y + depend-range.last-value-y = prev-depend-y + + (shapes_, desc-y) = draw-struct(config, structures.at(data.structure), structures, ox: start-x, oy: desc-y) + shapes += shapes_ + } + + return (shapes, desc-x, desc-y) +} + +#let draw-structure(config, struct, structures, ox: 0, oy: 0) = { + let shapes + let bg-col = config.background + let txt-col = config.text-color + let border-col = config.border-color + let bit-w = config.bit-width + let bit-h = config.bit-height + + let (bits-x, bits-y) = (ox, oy + bit-h) + let bits-width = struct.bits * bit-w + let start-bit = struct.start + + // Draw rectangle around structure + shapes += draw-rect(border-col, bits-x, bits-y, bits-width, bit-h, thickness: 2) + + for i in range(struct.bits) { + let bit-x = ox + i * bit-w + shapes += draw-text( + str(struct.bits - i - 1 + start-bit), + txt-col, + bit-x + bit-w / 2, + oy + bit-h / 2 + ) + + // Draw separator + if i != 0 { + shapes += draw-line(border-col, (bit-x, bits-y), (bit-x, bits-y + bit-h)) + } + } + + let ranges = structure.get-sorted-ranges(struct) + if config.left-labels { + ranges = ranges.rev() + } + + let desc-x + if config.force-descs-on-side { + desc-x = config.margins.at(3) + structures.main.bits * bit-w + if config.left-labels { + desc-x = config.width - desc-x + } + } else { + desc-x = ox + if config.left-labels { + desc-x += struct.bits * bit-w + } + } + + let desc-y = bits-y + bit-h * 2 + + // Names + simple descriptions + for range_ in ranges { + let start-i = struct.bits - range_.end + start-bit - 1 + let start-x = bits-x + start-i * bit-w + let width = rng.bits(range_) * bit-w + + let name-x = start-x + width / 2 + let name-y = bits-y + bit-h / 2 + + shapes += draw-text(range_.name, txt-col, name-x, name-y, fill: bg-col) + + if range_.description != "" { + //draw.circle((desc-x, -desc-y), radius: 5, fill: red) + let shapes_ + (shapes_, desc-x, desc-y) = draw-description( + config, range_, start-x, bits-y, width, desc-x, desc-y + ) + shapes += shapes_ + } + } + + // Dependencies + for range_ in ranges { + if range_.values() != none and range_.depends-on != none { + let shapes_ + (shapes_, desc-x, desc-y) = draw-dependency( + draw-structure, config, + struct, structures, bits-x, bits-y, range_, desc-x, desc-y + ) + shapes += shapes_ + } + } + + return (shapes, desc-y) +} + +#let render(config, structures) = { + set text( + font: config.default-font-family, + size: config.default-font-size + ) + + let main = structures.main + let ox = config.margins.at(3) + if config.left-labels { + ox = config.width - ox - main.bits * config.bit-width + } + + let params = if config.full-page { + ( + width: auto, + height: auto, + fill: config.background + ) + } else { + (:) + } + + set page(..params) + + canvas(length: 1pt, background: config.background, { + let (shapes, _) = draw-structure( + config, main, structures, + ox: ox, + oy: config.margins.at(0) + ) + shapes + //draw.circle((300, -3000), fill: red, radius: 2) + }) +} + +#let make(config) = { + return ( + config: config, + render: render.with(config) + ) +} \ No newline at end of file diff --git a/src/schema.typ b/src/schema.typ new file mode 100644 index 0000000..8abd894 --- /dev/null +++ b/src/schema.typ @@ -0,0 +1,50 @@ +#import "config.typ" as conf +#import "renderer.typ" +#import "structure.typ" + +#let valid-extensions = ("yaml", "json", "xml") + +#let parse-xml(path) = { + panic("TODO") +} + +#let parse-file(path) = { + let ext = path.split(".").last() + + if not ext in valid-extensions { + let fmts = valid-extensions.map(fmt => "." + fmt).join(", ") + fmts = "(" + fmts + ")" + panic("." + ext + " files are not supported. Valid formats: " + fmts) + } + + if ext == "yaml" { + return yaml(path) + } else if ext == "json" { + return json(path) + } else if ext == "xml" { + return parse-xml(path) + } +} + +#let load(path-or-schema, config: auto) = { + let schema = if type(path-or-schema) == str { + parse-file(path-or-schema) + } else { + parse-raw(path-or-schema) + } + + let structures = (:) + for (id, data) in schema.structures { + id = str(id) + structures.insert(id, structure.load(id, data)) + } + return structures +} + +#let render(structures, config: auto) = { + if config == auto { + config = conf.config() + } + let renderer_ = renderer.make(config) + (renderer_.render)(structures) +} \ No newline at end of file diff --git a/src/structure.typ b/src/structure.typ new file mode 100644 index 0000000..21b4430 --- /dev/null +++ b/src/structure.typ @@ -0,0 +1,54 @@ +#import "range.typ" +#import "util.typ" + +#let make( + name, + bits, + ranges, + start: 0 +) = { + return ( + name: name, + bits: bits, + ranges: ranges, + start: start + ) +} + +#let load(id, data) = { + let struct = (id: id) + let ranges = (:) + + for (range-span, range-data) in data.ranges { + let (start, end) = range.parse-span(str(range-span)) + ranges.insert( + range.key(start, end), + range.load(start, end, range-data) + ) + } + + for range_ in ranges.values() { + if range_.values != none and range_.depends-on != none { + let depends-key = range.key(..range_.depends-on) + let depends-range = ranges.at(depends-key) + let bits = range.bits(depends-range) + let values = (:) + for (v, d) in range_.values { + v = util.z-fill(str(int(v)), bits) + values.insert(v, d) + } + } + } + + return make( + id, + int(data.bits), + ranges, + start: data.at("start", default: 0) + ) +} + +#let get-sorted-ranges(struct) = { + let ranges = struct.ranges.values() + return ranges.sorted(key: r => r.end) +} \ No newline at end of file diff --git a/src/util.typ b/src/util.typ new file mode 100644 index 0000000..c005085 --- /dev/null +++ b/src/util.typ @@ -0,0 +1,4 @@ +#let z-fill(string, length) = { + let filled = "0" * length + string + return filled.slice(-length) +} \ No newline at end of file diff --git a/src/vec.typ b/src/vec.typ new file mode 100644 index 0000000..be36786 --- /dev/null +++ b/src/vec.typ @@ -0,0 +1,44 @@ +#let vec(x, y) = { + return (x: x, y: y) +} + +#let add(v1, v2) = { + return vec( + v1.x + v2.x, + v1.y + v2.y + ) +} + +#let sub(v1, v2) = { + return vec( + v1.x - v2.x, + v1.y - v2.y + ) +} + +#let mul(v, f) = { + return vec( + v.x * f, + v.y * f + ) +} + +#let div(v, f) = { + return vec( + v.x / f, + v.y / f + ) +} + +#let mag(v) = { + return calc.sqrt(v.x * v.x + v.y * v.y) +} + +#let normalize(v) = { + let m = mag(v) + + if m == 0 { + return (x: 0, y: 0) + } + return div(v, m) +} \ No newline at end of file