typstuff/misc/uml/uml.typ

569 lines
13 KiB
Typst
Raw Normal View History

2024-12-13 23:04:02 +01:00
#import "@preview/cetz:0.2.2": canvas, draw
#let class-counter = counter("uml-class")
#let class-colors = (
rgb("#C02B22"),
rgb("#D4721C"),
rgb("D3BC2F"),
rgb("60CB12"),
rgb("#2ECC40"),
rgb("#39CCA5"),
rgb("#38B4DA"),
rgb("#0074d9"),
rgb("#450DC9"),
rgb("#890DC9"),
)
#let visibility(vis, symbol) = (type: "visibility", visibility: vis, symbol: symbol)
#let public = visibility("public", "+")
#let private = visibility("private", "-")
#let protected = visibility("protected", "#")
#let package = visibility("package", "~")
#let class-family(family, prefix: none) = (
type: "class-family",
family: family,
prefix: prefix
)
#let abstract = class-family("abstract")
#let interface = class-family("interface", prefix: "interface")
#let trait = class-family("trait")
#let enum = class-family("enum")
#let utility = class-family("utility", prefix: "utility")
#let staticity(static) = (type: "staticity", static: static)
#let static = staticity(true)
#let non-static = staticity(false)
#let class(name, ..args, grouped: none) = {
let attributes = ()
let methods = ()
let visibility = none
let family = none
let args = args.pos()
if args.len() != 0 {
for arg in args {
if type(arg) == dictionary {
if arg.type == "visibility" {
visibility = arg
} else if arg.type == "class-family" {
family = arg
}
} else if type(arg) == array {
for elmt in arg {
if type(elmt) == dictionary {
if elmt.type == "visibility" {
visibility = elmt
} else if elmt.type == "attribute" {
attributes.push(elmt)
} else if elmt.type == "method" {
methods.push(elmt)
}
}
}
}
}
}
return ((
type: "class",
name: name,
visibility: visibility,
family: family,
attributes: attributes,
methods: methods,
color: rgb(184, 84, 80),
grouped: grouped
),)
}
#let attr(name, default: none, ..args) = {
let visibility = none
let staticity = none
let val-type = none
for arg in args.pos() {
if type(arg) == dictionary {
if arg.type == "visibility" {
visibility = arg
} else if arg.type == "type" {
val-type = arg
} else if arg.type == "staticity" {
staticity = staticity
}
} else if type(arg) == str {
val-type = arg
}
}
return ((
type: "attribute",
name: name,
visibility: visibility,
staticity: staticity,
val-type: val-type,
default: default
),)
}
#let method(name, ..args) = {
let visibility = none
let staticity = none
let parameters = none
let return-type = none
for arg in args.pos() {
if type(arg) == dictionary {
if "type" in arg {
if arg.type == "visibility" {
visibility = arg
} else if arg.type == "staticity" {
staticity = arg
}
} else {
parameters = arg
}
} else if type(arg) == str {
return-type = arg
} else if type(arg) == array {
parameters = arg
}
}
return ((
type: "method",
name: name,
visibility: visibility,
staticity: staticity,
parameters: parameters,
return-type: return-type
),)
}
#let render-attr(attr, defaults, show-visibility: true) = {
let visibility = if attr.visibility == none {
defaults.attr-visibility
} else {
attr.visibility
}
let staticity = if attr.staticity == none {
defaults.attr-staticity
} else {
attr.staticity
}
let name = attr.name
if staticity.static {
name = underline(name)
}
let txt = name
if show-visibility {
txt = visibility.symbol + txt
}
if attr.val-type != none {
txt += ": " + attr.val-type
}
if attr.default != none {
txt += " = " + str(attr.default)
}
return txt
}
#let render-method(method, defaults, show-visibility: true) = {
let visibility = if method.visibility == none {
defaults.method-visibility
} else {
method.visibility
}
let staticity = if method.staticity == none {
defaults.method-staticity
} else {
method.staticity
}
let params = ()
if type(method.parameters) == array {
params = method.parameters
} else if type(method.parameters) == dictionary {
params = method.parameters
.pairs()
.map(((n, t)) => n + ": " + t)
}
let name = method.name
if staticity.static {
name = underline(name)
}
let txt = name
if show-visibility {
txt = visibility.symbol + txt
}
txt += "(" + params.join(", ") + ")"
if method.return-type != none {
txt += ": " + method.return-type
}
return txt
}
#let get-attributes-by-visibility(class, defaults) = {
let by-vis = (:)
for attr in class.attributes {
let visibility = if attr.visibility == none {
defaults.attr-visibility
} else {
attr.visibility
}
if not visibility.visibility in by-vis {
by-vis.insert(visibility.visibility, ())
}
by-vis.at(visibility.visibility).push(attr)
}
return by-vis
}
#let get-methods-by-visibility(class, defaults) = {
let by-vis = (:)
for method in class.methods {
let visibility = if method.visibility == none {
defaults.method-visibility
} else {
method.visibility
}
if not visibility.visibility in by-vis {
by-vis.insert(visibility.visibility, ())
}
by-vis.at(visibility.visibility).push(method)
}
return by-vis
}
#let render-class-attributes(class, defaults) = {
let grouped = if class.grouped == none {
defaults.class-grouped
} else {
class.grouped
}
let cells = ()
if not grouped {
for attr in class.attributes {
cells.push(render-attr(attr, defaults, show-visibility: true))
}
} else {
let by-vis = get-attributes-by-visibility(class, defaults)
for (vis, attrs) in by-vis {
cells.push(table.cell(vis + ":"))
cells += attrs.map(attr => {
let txt = render-attr(attr, defaults, show-visibility: false)
pad(left: 1em, txt)
})
}
}
return cells
}
#let render-class-methods(class, defaults) = {
let grouped = if class.grouped == none {
defaults.class-grouped
} else {
class.grouped
}
let cells = ()
if not grouped {
for method in class.methods {
cells.push(render-method(method, defaults, show-visibility: true))
}
} else {
let by-vis = get-methods-by-visibility(class, defaults)
for (vis, attrs) in by-vis {
cells.push(table.cell(vis + ":"))
cells += attrs.map(method => {
let txt = render-method(method, defaults, show-visibility: false)
pad(left: 1em, txt)
})
}
}
return cells
}
#let render-class(class, defaults) = context {
let family = if class.family == none {
defaults.class-family
} else {
class.family
}
let cells = ()
cells += render-class-attributes(class, defaults)
cells.push(table.hline())
cells += render-class-methods(class, defaults)
let name = [*#class.name*]
let family = class.family
if family == abstract {
name = emph(name)
}
if family != none and family.prefix != none {
name = [*«#family.prefix»*\ #name]
}
let class-i = class-counter.get().first()
let col = class-colors.at(calc.rem(class-i, class-colors.len()))
set table.hline(stroke: col)
table(
inset: (top: 0.5em, bottom: 0.5em, left: 0.5em, right: 0.5em),
stroke: (x, y) => {
let s = (left: col, right: col)
if y == 0 {
s.insert("top", col)
s.insert("bottom", col)
}
return s
},
align: (x, y) => {
if y == 0 {
return center + horizon
}
return left + horizon
},
fill: (x, y) => if y == 0 {
col.lighten(75%)
} else {
none
},
table.header(name),
..cells,
table.hline()
)
class-counter.step()
}
#let diagram(..args) = {
class-counter.update(0)
let args = args.pos()
let defaults = (
class-family: none,
class-grouped: false,
attr-visibility: public,
attr-staticity: non-static,
method-visibility: public,
method-staticity: non-static,
)
let elements = ()
if args.len() != 0 {
for arg in args {
if type(arg) == dictionary {
defaults += arg
} else if type(arg) == array {
elements = arg
}
}
}
let cells = ()
for element in elements {
if element.type == "class" {
cells.push(render-class(element, defaults))
}
}
box(
stroke: black,
inset: 1em,
grid(
columns: 2,
column-gutter: 1em,
align: center + horizon,
row-gutter: 1em,
..cells
)
)
}
#let render-attr2(pos, id, attr, defaults, show-visibility: true) = {
let visibility = if attr.visibility == none {
defaults.attr-visibility
} else {
attr.visibility
}
let staticity = if attr.staticity == none {
defaults.attr-staticity
} else {
attr.staticity
}
let name = attr.name
if staticity.static {
name = underline(name)
}
let txt = name
if show-visibility {
txt = visibility.symbol + txt
}
if attr.val-type != none {
txt += ": " + attr.val-type
}
if attr.default != none {
txt += " = " + str(attr.default)
}
draw.content(pos, txt, name: id)
}
#let render-method2(method, defaults, show-visibility: true) = {
let visibility = if method.visibility == none {
defaults.method-visibility
} else {
method.visibility
}
let staticity = if method.staticity == none {
defaults.method-staticity
} else {
method.staticity
}
let params = ()
if type(method.parameters) == array {
params = method.parameters
} else if type(method.parameters) == dictionary {
params = method.parameters
.pairs()
.map(((n, t)) => n + ": " + t)
}
let name = method.name
if staticity.static {
name = underline(name)
}
let txt = name
if show-visibility {
txt = visibility.symbol + txt
}
txt += "(" + params.join(", ") + ")"
if method.return-type != none {
txt += ": " + method.return-type
}
draw.content((), txt)
}
#let render-class-attributes2(class, defaults) = {
let grouped = if class.grouped == none {
defaults.class-grouped
} else {
class.grouped
}
let cells = ()
let prev = ()
if not grouped {
for (i, attr) in class.attributes.enumerate() {
let id = "class-"+class.name+"-attr-"+str(i)
let txt = render-attr(attr, defaults, show-visibility: true)
draw.content((rel: (0, -0.4em), to: prev), name: id, anchor: "north-west", txt)
prev = id + ".south-west"
}
} else {
let by-vis = get-attributes-by-visibility(class, defaults)
for (vis, attrs) in by-vis {
cells.push(table.cell(vis + ":"))
let vis-id = "class-"+class.name+"-attrs-"+vis
draw.content((rel: (0, -0.4em), to: prev), name: vis-id, anchor: "north-west", vis + ":")
prev = vis-id + ".south-west"
for (i, attr) in attrs.enumerate() {
let txt = render-attr(attr, defaults, show-visibility: false)
txt = pad(left: 1em, txt)
let id = vis-id + "-" + str(i)
draw.content((rel: (0, -0.4em), to: prev), name: id, anchor: "north-west", txt)
prev = id + ".south-west"
}
}
}
}
#let render-class-methods2(class, defaults) = {
}
#let render-class2(class, defaults) = {
let attr-group = render-class-attributes2(class, defaults)
let method-group = render-class-methods2(class, defaults)
let family = if class.family == none {
defaults.class-family
} else {
class.family
}
let name = [*#class.name*]
let family = class.family
if family == abstract {
name = emph(name)
}
if family != none and family.prefix != none {
name = [*«#family.prefix»*\ #name]
}
let class-i = class-counter.get().first()
let col = class-colors.at(calc.rem(class-i, class-colors.len()))
draw.group(name: "class-"+class.name, {
draw.group(name: "class-"+class.name+"-name", padding: 5pt, {
draw.content((), name)
})
draw.on-layer(-1, {
draw.rect("class-"+class.name+"-name.north-west", "class-"+class.name+"-name.south-east", fill: col.lighten(75%), stroke: col, layer: -1)
})
draw.group(name: "class-"+class.name+"-attrs", padding: 0.2em, {
attr-group
})
method-group
})
draw.rect("class-"+class.name+".north-west", "class-"+class.name+".south-east", stroke: col)
}
#let render-class3(class, defaults) = {
let id = "class-" + class.name
return draw.content((rel: (3, 0), to: ()), render-class(class, defaults), name: id)
}
#let diagram2(..args) = context {
class-counter.update(0)
let args = args.pos()
let defaults = (
class-family: none,
class-grouped: false,
attr-visibility: public,
attr-staticity: non-static,
method-visibility: public,
method-staticity: non-static,
)
let elements = ()
if args.len() != 0 {
for arg in args {
if type(arg) == dictionary {
defaults += arg
} else if type(arg) == array {
elements = arg
}
}
}
let cells = ()
canvas({
for element in elements {
if element.type == "class" {
render-class3(element, defaults)
}
}
draw.line("class-Test", "class-Essai", mark: (end: "diamond", scale: 2))
})
}