forked from HEL/circuiteria
		
	
		
			
				
	
	
		
			325 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Typst
		
	
	
	
	
	
			
		
		
	
	
			325 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Typst
		
	
	
	
	
	
| #import "/src/cetz.typ": draw, coordinate, matrix, vector
 | |
| #import "ports.typ": add-ports, add-port, get-port-pos, get-port-idx
 | |
| #import "../util.typ"
 | |
| 
 | |
| #let find-port(ports, id) = {
 | |
|   for (side, side-ports) in ports {
 | |
|     for (i, port) in side-ports.enumerate() {
 | |
|       if port.id == id {
 | |
|         return (side, i)
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   panic("Could not find port with id '" + str(id) + "' in ports " + repr(ports))
 | |
| }
 | |
| 
 | |
| #let local-to-global(origin, u, v, points) = {
 | |
|   return points-real = points.map(p => {
 | |
|     let (pu, pv) = p
 | |
|     return vector.add(
 | |
|       origin,
 | |
|       vector.add(
 | |
|         vector.scale(u, pu),
 | |
|         vector.scale(v, pv)
 | |
|       )
 | |
|     )
 | |
|   })
 | |
| }
 | |
| 
 | |
| #let default-draw-shape(elmt, bounds) = {
 | |
|   return ({}, bounds)
 | |
| }
 | |
| 
 | |
| #let default-pre-process(elements, element) = {
 | |
|   return elements
 | |
| }
 | |
| 
 | |
| #let resolve-offset(ctx, offset, from, axis) = {
 | |
|   let (ctx, pos) = coordinate.resolve(
 | |
|     ctx,
 | |
|     (rel: offset, to: from)
 | |
|   )
 | |
|   return pos.at(axis)
 | |
| }
 | |
| 
 | |
| #let resolve-align(ctx, elmt, bounds, align, with, axis) = {
 | |
|   let (align-side, i) = find-port(elmt.ports, align)
 | |
|   let margins = (0%, 0%)
 | |
|   if align-side in elmt.ports-margins {
 | |
|     margins = elmt.ports-margins.at(align-side)
 | |
|   }
 | |
| 
 | |
|   let parallel-sides = (
 | |
|     ("north", "south"),
 | |
|     ("west", "east")
 | |
|   ).at(axis)
 | |
| 
 | |
|   let ortho-sides = (
 | |
|     ("west", "east"),
 | |
|     ("north", "south")
 | |
|   ).at(axis)
 | |
| 
 | |
|   let dl
 | |
|   let start-margin
 | |
|   let len = elmt.size.at(axis)
 | |
|   if align-side in parallel-sides {
 | |
|     let used-pct = 100% - margins.at(0) - margins.at(1)
 | |
|     let used-len = len * used-pct / 100%
 | |
|     start-margin = len * margins.at(0) / 100%
 | |
|     
 | |
|     //dl = used-len * (i + 1) / (elmt.ports.at(align-side).len() + 1)
 | |
|     dl = get-port-pos(elmt, bounds, align-side, align, get-port-idx(elmt, align, side: align-side))
 | |
|     /*if not elmt.auto-ports {
 | |
|       start-margin = 0
 | |
|       dl = elmt.ports-pos.at(align)(len)
 | |
|     }*/
 | |
|   } else if align-side == ortho-sides.first() {
 | |
|     dl = 0
 | |
|     start-margin = 0
 | |
|   } else {
 | |
|     dl = len
 | |
|     start-margin = 0
 | |
|   }
 | |
| 
 | |
|   if axis == 1 {
 | |
|     dl = len - dl
 | |
|   }
 | |
|   
 | |
|   let (ctx, with-pos) = coordinate.resolve(ctx, with)
 | |
|   return with-pos.at(axis) - dl + start-margin
 | |
| }
 | |
| 
 | |
| #let resolve-coordinate(ctx, elmt, bounds, coord, axis) = {
 | |
|   if type(coord) == dictionary {
 | |
|     let offset = coord.at("offset", default: none)
 | |
|     let from = coord.at("from", default: none)
 | |
|     let align = coord.at("align", default: none)
 | |
|     let with = coord.at("with", default: none)
 | |
| 
 | |
|     if none not in (offset, from) {
 | |
|       if type(offset) != array {
 | |
|         let a = (0, 0)
 | |
|         a.at(axis) = offset
 | |
|         offset = a
 | |
|       }
 | |
|       return resolve-offset(ctx, offset, from, axis)
 | |
|       
 | |
|     } else if none not in (align, with) {
 | |
|       return resolve-align(ctx, elmt, bounds, align, with, axis)
 | |
|     } else {
 | |
|       panic("Dictionnary must either provide both 'offset' and 'from', or 'align' and 'with'")
 | |
|     }
 | |
|   }
 | |
|   if type(coord) not in (int, float, length) {
 | |
|     panic("Invalid " + "xy".at(axis) + " coordinate: " + repr(coord))
 | |
|   }
 | |
|   return coord
 | |
| }
 | |
| 
 | |
| #let complete-bounds(elmt, bounds) = {
 | |
|   let b = bounds
 | |
|   bounds += (
 | |
|     center: (
 | |
|       (b.br.at(0) + b.tl.at(0))/2,
 | |
|       (b.br.at(1) + b.tl.at(1))/2
 | |
|     ),
 | |
|     b: (
 | |
|       (b.br.at(0) + b.bl.at(0))/2,
 | |
|       (b.br.at(1) + b.bl.at(1))/2
 | |
|     ),
 | |
|     t: (
 | |
|       (b.tr.at(0) + b.tl.at(0))/2,
 | |
|       (b.tr.at(1) + b.tl.at(1))/2
 | |
|     ),
 | |
|     l: (
 | |
|       (b.bl.at(0) + b.tl.at(0))/2,
 | |
|       (b.bl.at(1) + b.tl.at(1))/2
 | |
|     ),
 | |
|     r: (
 | |
|       (b.br.at(0) + b.tr.at(0))/2,
 | |
|       (b.br.at(1) + b.tr.at(1))/2
 | |
|     ),
 | |
|     sides: (
 | |
|       north: (bounds.tl, bounds.tr),
 | |
|       south: (bounds.bl, bounds.br),
 | |
|       west: (bounds.tl, bounds.bl),
 | |
|       east: (bounds.tr, bounds.br),
 | |
|     ),
 | |
|     lengths: (
 | |
|       north: (bounds.tr.at(0) - bounds.tl.at(0)),
 | |
|       south: (bounds.br.at(0) - bounds.bl.at(0)),
 | |
|       west: (bounds.tl.at(1) - bounds.bl.at(1)),
 | |
|       east: (bounds.tr.at(1) - bounds.br.at(1)),
 | |
|     ),
 | |
|     ports: (:)
 | |
|   )
 | |
|   for (side, props) in bounds.sides.pairs() {
 | |
|     let props2 = props
 | |
|     if side in elmt.ports-margins {
 | |
|       let (pt0, pt1) = props
 | |
|       let margins = elmt.ports-margins.at(side)
 | |
|       let a = util.lerp(pt0, margins.at(0), pt1)
 | |
|       let b = util.lerp(pt0, 100% - margins.at(1), pt1)
 | |
|       props2 = (a, b)
 | |
|     }
 | |
|     bounds.ports.insert(side, props2)
 | |
|   }
 | |
|   return bounds
 | |
| }
 | |
| 
 | |
| #let make-bounds(elmt, x, y, w, h) = {
 | |
|   let w2 = w / 2
 | |
|   let h2 = h / 2
 | |
|   
 | |
|   let bounds = (
 | |
|     bl: (x, y),
 | |
|     tl: (x, y + h),
 | |
|     tr: (x + w, y + h),
 | |
|     br: (x + w, y),
 | |
|   )
 | |
|   return complete-bounds(elmt, bounds)
 | |
| }
 | |
| 
 | |
| #let render(draw-shape, elmt) = draw.group(name: elmt.id, ctx => {
 | |
|   let width = elmt.size.first()
 | |
|   let height = elmt.size.last()
 | |
| 
 | |
|   let x = elmt.pos.first()
 | |
|   let y = elmt.pos.last()
 | |
| 
 | |
|   let bounds = make-bounds(elmt, 0, 0, width, height)
 | |
|   x = resolve-coordinate(ctx, elmt, bounds, x, 0)
 | |
|   y = resolve-coordinate(ctx, elmt, bounds, y, 1)
 | |
|   bounds = make-bounds(elmt, x, y, width, height)
 | |
| 
 | |
|   // Workaround because CeTZ needs to have all draw functions in the body
 | |
|   let func = {}
 | |
|   let res = draw-shape(elmt, bounds)
 | |
|   assert(
 | |
|     type(res) == array and res.len() == 2,
 | |
|     message: "The drawing function of element '" + elmt.id + "' did not return a function and new bounds"
 | |
|   )
 | |
|   (func, bounds) = res
 | |
|   if type(func) == function {
 | |
|     func = (func,)
 | |
|   }
 | |
|   assert(
 | |
|     type(bounds) == dictionary,
 | |
|     message: "The drawing function of element '" + elmt.id + "' did not return the correct bounds dictionary"
 | |
|   )
 | |
|   func
 | |
| 
 | |
|   draw.anchor("north", bounds.t)
 | |
|   draw.anchor("south", bounds.b)
 | |
|   draw.anchor("west", bounds.l)
 | |
|   draw.anchor("east", bounds.r)
 | |
|   draw.anchor("north-west", bounds.tl)
 | |
|   draw.anchor("north-east", bounds.tr)
 | |
|   draw.anchor("south-east", bounds.br)
 | |
|   draw.anchor("south-west", bounds.bl)
 | |
| 
 | |
|   if elmt.name != none {
 | |
|     draw.content(
 | |
|       (name: elmt.id, anchor: elmt.name-anchor),
 | |
|       anchor: if elmt.name-anchor in util.valid-anchors {elmt.name-anchor} else {"center"},
 | |
|       padding: 0.5em,
 | |
|       align(center)[*#elmt.name*]
 | |
|     )
 | |
|   }
 | |
| 
 | |
|   add-ports(elmt, bounds)
 | |
| 
 | |
|   if elmt.debug.bounds {
 | |
|     draw.line(
 | |
|       bounds.tl, bounds.tr, bounds.br, bounds.bl,
 | |
|       stroke: red,
 | |
|       close: true
 | |
|     )
 | |
|   }
 | |
| })
 | |
| 
 | |
| /// Draws an element
 | |
| /// - draw-shape (function): Draw function
 | |
| /// - x (number, dictionary): The x position (bottom-left corner).
 | |
| ///
 | |
| ///   If it is a dictionary, it should be in the format `(rel: number, to: str)`, where `rel` is the offset and `to` the base anchor
 | |
| /// - y (number, dictionary): The y position (bottom-left corner).
 | |
| ///
 | |
| ///   If it is a dictionary, it should be in the format `(from: str, to: str)`, where `from` is the base anchor and `to` is the id of the port to align with the anchor
 | |
| /// - w (number): Width of the element
 | |
| /// - h (number): Height of the element
 | |
| /// - name (none, str): Optional name of the block
 | |
| /// - name-anchor (str): Anchor for the optional name
 | |
| /// - ports (dictionary): Dictionary of ports. The keys are cardinal directions ("north", "east", "south" and/or "west"). The values are arrays of ports (dictionaries) with the following fields:
 | |
| ///   - `id` (`str`): (Required) Port id
 | |
| ///   - `name` (`str`): Optional name displayed *in* the block
 | |
| ///   - `clock` (`bool`): Whether it is a clock port (triangle symbol)
 | |
| ///   - `vertical` (`bool`): Whether the name should be drawn vertically
 | |
| /// - ports-margins (dictionary): Dictionary of ports margins (used with automatic port placement). They keys are cardinal directions ("north", "east", "south", "west"). The values are tuples of (`<start>`, `<end>`) margins (numbers)
 | |
| /// - fill (none, color): Fill color
 | |
| /// - stroke (stroke): Border stroke
 | |
| /// - id (str): The block id (for future reference)
 | |
| /// - auto-ports (bool): Whether to use auto port placements or not. If false, `draw-shape` is responsible for adding the appropiate ports
 | |
| /// - ports-y (dictionary): Dictionary of the ports y offsets (used with `auto-ports: false`)
 | |
| /// - debug (dictionary): Dictionary of debug options.
 | |
| ///
 | |
| ///   Supported fields include:
 | |
| ///     - `ports`: if true, shows dots on all ports of the element
 | |
| #let elmt(
 | |
|   cls: "element",
 | |
|   draw-shape: default-draw-shape,
 | |
|   pre-process: default-pre-process,
 | |
|   pos: (0, 0),
 | |
|   size: (1, 1),
 | |
|   name: none,
 | |
|   name-anchor: "center",
 | |
|   ports: (:),
 | |
|   ports-margins: (:),
 | |
|   fill: none,
 | |
|   stroke: black + 1pt,
 | |
|   id: auto,
 | |
|   ports-pos: auto,
 | |
|   debug: (
 | |
|     bounds: false,
 | |
|     ports: false
 | |
|   ),
 | |
|   extra: (:)
 | |
| ) = {
 | |
|   for (key, side-ports) in ports.pairs() {
 | |
|     if type(side-ports) == str {
 | |
|       side-ports = ((id: side-ports),)
 | |
|     } else if type(side-ports) == dictionary {
 | |
|       side-ports = (side-ports,)
 | |
|     }
 | |
| 
 | |
|     for (i, port) in side-ports.enumerate() {
 | |
|       if type(port) == array {
 | |
|         side-ports.at(i) = (
 | |
|           id: port.at(0, default: ""),
 | |
|           name: port.at(1, default: "")
 | |
|         )
 | |
|       } else if type(port) == str {
 | |
|         side-ports.at(i) = (id: port)
 | |
|       }
 | |
|     }
 | |
|     ports.at(key) = side-ports
 | |
|   }
 | |
| 
 | |
|   return ((
 | |
|     cls: cls,
 | |
|     id: id,
 | |
|     draw: render.with(draw-shape),
 | |
|     pre-process: pre-process,
 | |
|     pos: pos,
 | |
|     size: size,
 | |
|     name: name,
 | |
|     name-anchor: name-anchor,
 | |
|     ports: ports,
 | |
|     ports-margins: ports-margins,
 | |
|     fill: fill,
 | |
|     stroke: stroke,
 | |
|     ports-pos: ports-pos,
 | |
|     debug: debug
 | |
|   ) + extra,)
 | |
| }
 |