feat(docs): add new datasheet generation method (#123)

This commit is contained in:
kris j
2025-10-16 00:11:07 +02:00
committed by htfab
parent 13c23e047d
commit c36badfb60
37 changed files with 2664 additions and 121 deletions
+32
View File
@@ -0,0 +1,32 @@
// callouts
#let WARNING_STRONG = rgb("#ff6b21")
#let WARNING_WEAK = rgb("#fd8448")
#let DANGER_STRONG = rgb("#ff0000")
#let DANGER_WEAK = rgb("#ff4747")
#let INFO_STRONG = rgb("#2766ef")
#let INFO_WEAK = rgb("#4b7be2")
// misc document items
#let LINK_DEFAULT = rgb("#4646cd")
#let TABLE_GREY = rgb("#2b2b2b")
#let ARTWORK_GREY_INFO = rgb("#2b2b2bda")
#let HORIZONTAL_RULE_GREY = rgb("#c4c4c4c4")
// badges
#let BADGE_TEAL = rgb("#009696")
#let BADGE_MUTED_TEAL = rgb("#007474")
#let BADGE_BLUE = rgb("#3e71e7")
#let BADGE_PINK = rgb("#db44b2")
#let BADGE_PURPLE = rgb("#a952ff")
// themes
#let THEME_PLUM = rgb("#9c6fb6")
#let THEME_TT06_PINK = rgb("#ec789d")
#let THEME_SKY_BLUE = rgb("#0c66ab")
// unused colours for safekeeping
#let UNUSED_GOLD = rgb("#ffc936")
#let UNUSED_PURPLE_1 = rgb("#9123ff")
#let UNUSED_PURPLE_2 = rgb("#a041ff")
+622
View File
@@ -0,0 +1,622 @@
#import "@preview/tiaoma:0.3.0"
#import "lib.typ": badge, callout, annotated_qrcode, get_image_by_id
#import "colours.typ"
#import "../chapters/pinout-tables.typ" as pins
#import "../chapters/call-to-action.typ": call_to_action_page
#let _funding_content = state("_funding_content", [])
#let _funding_alt_title = state("_funding_alt_title", [])
#let _flip_footer_ordering = state("_flip_footer_ordering", false)
#let _chip_render_content = state("_chip_render_content", none)
#let _chip_render_sponsor_text = state("_chip_render_sponsor_text", none)
#let _chip_render_sponsor_logo = state("_chip_render_sponsor_logo", none)
#let styling(body) = {
set page(paper: "a4", margin: auto)
set text(font: "Montserrat", size: 12pt)
show raw: set text(font: "Martian Mono", weight: "light")
// style headings
show heading.where(level: 1): this => {
text(size: 28pt, this)
v(0.1cm)
}
show heading.where(level: 2): set text(size: 22pt)
show heading.where(level: 3): set text(size: 16pt)
// indent numbered and bullet point lists
set enum(indent: 1em)
set list(indent: 1em)
// format table so that empty cells show dark grey em dash
show table.cell: this => {
if this.body == [] {
align(center + horizon, text(fill: colours.TABLE_GREY)[$dash.em$])
} else {
this
}
}
// set quote styling
// used for project description
show quote: set align(center)
show quote: set pad(x: 1em)
show quote: set block(above: 1.75em)
body
}
#let styling_set_par(body) = {
// justify text, except for in tables
set par(justify: true)
show table: this => {
set par(justify: false)
this
}
body
}
#let _footer(shuttle, invert_text_colour: false, display_pg_as: "1", flip_ordering: false) = {
// setup
set text(size: 10pt)
set text(white) if invert_text_colour
let logo = box(baseline: 0.25em, {
if invert_text_colour {
image("../resources/logos/tt-logo-white.svg", height: 25%)
} else {
image("../resources/logos/tt-logo-black.svg", height: 25%)
}
})
// get the title of the current chapter
let title = context query(selector(heading.where(level: 1)).before(here())).last().body
let pg_num = context counter(page).display(display_pg_as)
let TITLE_SPACING = 0.5cm
let LOGO_SPACING = 0.25cm
let set_1 = {strong(pg_num); h(TITLE_SPACING); emph(title); h(1fr); emph(shuttle); h(LOGO_SPACING); logo}
let set_2 = {logo; h(LOGO_SPACING); emph(shuttle); h(1fr); emph(title); h(TITLE_SPACING); strong(pg_num)}
// actual styling
context {
// alternate odd-even pages
if calc.even(counter(page).get().first()) {
if flip_ordering {set_2} else {set_1}
} else {
if flip_ordering {set_1} else {set_2}
}
}
}
#let project(
title: "Title",
author: ("Author 1", "Author 2"),
repo_link: "!!! missing repository link !!!",
description: "!!! Missing description !!!",
address: "----",
clock: "No Clock",
type: "HDL",
wokwi_id: none,
danger_level: none,
danger_reason: none,
body
) = {
show: styling;
show: styling_set_par;
// make fake heading - this is the one that gets shown in the table of contents
// which has the nice address marker next to it
let fake_heading = block(
heading(level: 2, outlined: true, bookmarked: false)[
#box(baseline: 0.4em, badge(colours.BADGE_TEAL, strong(raw(address))))
#title
]
)
// move fake heading so it doesn't affect layout
place(top + left, hide(fake_heading))
// make heading thats seen on the project page
heading(level: 2, outlined: false, bookmarked: true, title)
// display author names
// if 1, it is displayed as is
// if 2, it is displayed as "a & b"
// if >2, it is displayed as "a, b & c"
let author_text = []
author_text += [by *#author.at(0)*]
if author.len() == 2 {
author_text += [ & *#author.at(1)*]
} else if author.len() > 2 {
let names = author.slice(1, author.len()-1)
for name in names {
author_text += [, *#name*]
}
author_text += [ & *#author.at(author.len()-1)*]
}
author_text
// makes badges array, add address first
let badges = (badge(colours.BADGE_TEAL, strong(raw(address))),)
if clock != "No Clock" {badges.push(badge(colours.BADGE_BLUE, strong(raw(clock))))}
badges.push(badge(colours.BADGE_PINK, strong(raw(type + " Project"))))
// add asic danger callout if necessary
let danger_callout
if danger_level != none {
if danger_level == "medium" {
badges.push(badge(colours.WARNING_STRONG, strong(raw("Medium Danger"))))
danger_callout = callout(
"warning",
raw("This project can damage the ASIC under certain conditions."),
raw(danger_reason)
)
} else if danger_level == "high" {
badges.push(badge(colours.DANGER_STRONG, strong(raw("High Danger"))))
danger_callout = callout(
"danger",
raw("This project will damage the ASIC."),
raw(danger_reason)
)
} else {
panic([unexpected danger_level value (#danger_level)])
}
}
// add badges to page, unpack badge array
grid(columns: badges.len(), column-gutter: 1em, ..badges)
// hardcode these repo/wokwi links to be black and to not follow link-override-colour
link(repo_link, text(black, raw(repo_link.trim("https://", at: start))))
if wokwi_id != none {
let wokwi_link = "https://wokwi.com/projects/" + wokwi_id
parbreak()
link(wokwi_link, text(black, raw(wokwi_link.trim("https://", at: start))))
}
// add description and danger callout to page
quote(block: true, quotes: true, emph(description))
danger_callout
set heading(offset: 2)
// ensure each figure and footnote is only counted within its project
// format numbering as "<project_address>.<figure_count>"
let project_numbering(count) = {address.trim("-", at: start) + "." + str(count)}
counter(figure.where(kind: image)).update(0)
counter(footnote).update(0)
set figure(numbering: project_numbering)
set footnote(numbering: project_numbering)
body
pagebreak(weak: true)
}
#let splash_chapter_page(
title,
page_colour: white,
invert_text_colour: false,
footer_text: none,
additional_content: none
) = {
page(
fill: page_colour,
footer: context {
_footer(
footer_text,
invert_text_colour: invert_text_colour,
flip_ordering: _flip_footer_ordering.final()
)
},
// page body - center title and add additional content if necessary
{
set text(white) if invert_text_colour
// make headings
align(center + horizon, block(text(size: 100pt, strong(title))))
place(top + left, hide(heading(level: 1, title)))
// make info box if needed
if additional_content != none {
set text(black)
place(center + bottom, block(fill: white, inset: 10pt, additional_content))
}
}
)
}
// make a tiling logo page
#let tiling_logo_page() = {
let pattern = tiling(
size: (3cm, 3cm),
spacing: (1cm, 1cm),
image("../resources/logos/tt-logo-light-grey.svg")
)
page(
background: rect(width: 110%, height: 105%, inset: 0pt, outset: 0pt, stroke: none, fill: pattern),
[] // empty page
)
}
#let datasheet(
shuttle_id: none,
shuttle_name: none,
repo_link: none,
theme: "classic",
projects: none,
show_pinouts: "openframe",
theme_override_colour: none,
date: datetime.today(),
link_disable_colour: false,
link_override_colour: none,
chip_viewer_link: none,
qrcode_follows_theme: false,
body
) = {
set document(
title: [#shuttle_name Datasheet],
author: "Tiny Tapeout & Contributors",
description: [The official Tiny Tapeout datasheet for #shuttle_id. Template by Kristaps Jurkans (bluesky/\@krisj.dev)]
)
show: styling;
let date_str = date.display("[month repr:long] [day padding:none], [year]")
// contains title, repo link and compilation date
let cover_text = align(center)[
#text(size: 32pt, strong[#shuttle_name Datasheet])
#link(
repo_link,
text(
size: 14pt,
font: "Martian Mono",
repo_link.trim("https://", at: start)
)
)
#v(2.5cm)
#text(size: 16pt, weight: "medium", date_str)
]
// configure theme variables
let selected_theme_colour = colours.THEME_PLUM // default fill
if theme_override_colour != none {selected_theme_colour = theme_override_colour}
let qrcode_colour = black
if qrcode_follows_theme and theme == "bold" {qrcode_colour = selected_theme_colour}
// make titlepage
if theme == "classic" {
image("../resources/logos/tt-logo-colourful.png")
cover_text
} else if theme == "bold" {
set page(
background: image("../resources/backgrounds/colourful-background-gds-o10-bw.png", height: 100%),
fill: selected_theme_colour
)
align(
center + horizon,
image("../resources/logos/tt-logo-white.svg", height: 60%)
)
set text(white)
cover_text
} else if theme == "monochrome" {
align(
center + horizon,
image("../resources/logos/tt-logo-black.svg", height: 60%)
)
cover_text
}
// style link colour
// moved after title page since theme-override-colour would cause it to blend in with the background
let link_colour = luma(0%)
if not link_disable_colour {
// if override colour specified, set it to that
// else fall back to default link colour
if link_override_colour != none {
link_colour = link_override_colour
} else {
link_colour = colours.LINK_DEFAULT
}
}
show link: set text(link_colour)
// add tiling logo page (this is pg2 of the datasheet)
tiling_logo_page()
show: styling_set_par;
// setup for table of contents
set page(footer: _footer(shuttle_id, invert_text_colour: false, display_pg_as: "i"))
counter(page).update(1)
// add spacing for each H1, and make H1 bold
show outline.entry.where(level: 1): set block(above: 1em)
show outline.entry.where(level: 1): strong
// make actual table of contents
outline(
depth: 2,
title: heading(level: 1, outlined: false, bookmarked: true, "Table of Contents")
)
pagebreak(weak: true)
// determine whether we need to flip the footer ordering depending on if the table fo contents
// ended on an odd or even page
context {
_flip_footer_ordering.update(calc.even(counter(page).get().last()))
}
set page(footer: context {_footer(
shuttle_id, display_pg_as: "1", flip_ordering: _flip_footer_ordering.final()
)})
// reset page counter so that it starts counting actual content now
counter(page).update(1)
// make chip render pages
context {
let chip_renders = _chip_render_content.final()
if chip_renders != none {
let qr
let qr_grid
if chip_viewer_link != none {
qr_grid = annotated_qrcode(
chip_viewer_link, "Online chip viewer",
tiaoma_args: ("scale": 2.0, "fg-color": qrcode_colour)
)
}
let sponsor_grid
if _chip_render_sponsor_text.final() != none and _chip_render_sponsor_logo.final() != none {
sponsor_grid = grid(
columns: 1,
rows: 3,
align: center + horizon,
row-gutter: 1em,
_chip_render_sponsor_text.final(),
_chip_render_sponsor_logo.final(),
[] // added for padding
)
}
let content
let content_arr = ()
if sponsor_grid != none {
content_arr.push(sponsor_grid)
}
if qr_grid != none {
content_arr.push(qr_grid)
}
if content_arr.len() >= 1 {
content = grid(
columns: content_arr.len(),
rows: 1,
column-gutter: 1em, row-gutter: 1em,
align: center + horizon,
..content_arr
)
}
if theme == "bold" {
splash_chapter_page(
"Chip Renders",
page_colour: selected_theme_colour,
invert_text_colour: true,
footer_text: shuttle_id,
additional_content: content
)
} else {
splash_chapter_page(
"Chip Renders",
invert_text_colour: false,
footer_text: shuttle_id,
additional_content: content
)
}
// display chip renders
chip_renders
}
}
// make project splash page + show all projects
if projects != none {
if theme == "bold" {
splash_chapter_page(
"Projects",
page_colour: selected_theme_colour,
invert_text_colour: true,
footer_text: shuttle_id
)
} else {
splash_chapter_page(
"Projects",
footer_text: shuttle_id
)
}
projects
}
// reset counters back to 0, to be used in the info section of the datasheet
counter(figure.where(kind: image)).update(0)
counter(footnote).update(0)
body
// add informational content
// chip pinout diagram
page(include "../chapters/pinout.typ")
// multiplexer infrastructure explanation
page({
include "../chapters/tt-multiplexer.typ"
if show_pinouts == "caravel" {
pins.caravel
} else if show_pinouts == "openframe" {
pins.openframe
} else {
panic([unknown pinout table referenced (#show_pinouts)])
}
})
// add funding/sponsor page
page(context {
let funding_content = _funding_content.final()
let alt_title = _funding_alt_title.final()
if funding_content != [] {
if alt_title != none {
heading(level: 1, alt_title)
} else {
heading(level: 1, "Funding")
}
}
funding_content
})
page(include "../chapters/team.typ")
page(include "../chapters/using-this-datasheet.typ")
// add call to action page
if theme == "classic" or theme == "monochrome" {
page(call_to_action_page())
} else if theme == "bold" {
page(
fill: selected_theme_colour,
footer: none,
{
set text(white)
call_to_action_page(qrcode_colour: black)
}
)
}
}
#let funding(doc, alt_title: none) = {
context {
_funding_content.update(doc)
_funding_alt_title.update(alt_title)
}
}
#let renders(sponsor_text: none, sponsor_logo: none, doc) = {
context {
_chip_render_content.update(doc)
_chip_render_sponsor_text.update(sponsor_text)
_chip_render_sponsor_logo.update(sponsor_logo)
}
}
#let art(id, rot: 90deg) = {
// all artwork is assumed to be external (hosted in a separate repo)
let art_img = get_image_by_id("artwork", id, height: 100%)
let info = yaml("../resources/external/manifest.yaml").at("artwork").at(id)
let attribution = strong[
#info.title $dash.fig$
#if info.at("designer", default: none) != none [Designed by #info.designer.]
Illustrated by #info.artist.
]
// heading shown in table of contents
let art_heading = [#box(baseline: 0.4em, badge(colours.BADGE_PURPLE, strong(`Artwork`))) #info.title]
set text(fill: white, size: 10pt)
page(
paper: "a4",
margin: 0pt,
footer: none,
// add artwork as background
background: rotate(rot, reflow: true, art_img),
{
// make fake & read heading
// first is shown in ToC, the second in the PDF list
place(top, hide(heading(level: 2, outlined: true, bookmarked: false, art_heading)))
place(top, hide(heading(level: 2, outlined: false, bookmarked: true)[(Artwork) #info.title]))
// determine position for artwork attribution
context {
let attribution_placement = bottom + left
// if even
if calc.even(counter(page).get().first()) {
// but flipped
if _flip_footer_ordering.final() {
attribution_placement = bottom + right
} else {
attribution_placement = bottom + left
}
// if odd
} else {
if _flip_footer_ordering.final() {
attribution_placement = bottom + left
} else {
attribution_placement = bottom + right
}
}
// add artwork attribution
place(
attribution_placement,
block(
fill: colours.ARTWORK_GREY_INFO,
inset: 4pt,
attribution
)
)
}
}
)
}
#let add_render(title, subtitle: none, img) = {
page(
align(center, grid(
columns: 1,
row-gutter: 10pt,
align: center,
heading(level: 2, title),
subtitle,
img
)
)
)
}
+178
View File
@@ -0,0 +1,178 @@
#import "@preview/tiaoma:0.3.0"
#import "colours.typ" as colours
/// Create a badge with a given colour and text
///
/// NOTE: This function will replace any instances of a hyphen ("-")
/// with a coloured zero digit. This is intended, and is used for
/// the address badge.
/// - colour (color): Colour of the badge
/// - doc (content, str): Text to display within the badge
/// -> content
#let badge(colour, body) = {
set text(white)
show "-": text(colours.BADGE_MUTED_TEAL, "0")
rect(fill: colour, body)
}
/// Create a callout to display important information
///
/// - type (): Callout type (warning, danger, info, custom)
/// - title (str): Title of the callout
/// - custom_title_colour (color): Colour to use for the title of the custom callout
/// - custom_body_colour (color): Colour to use for the body of the custom callout
/// - body (content, str): Content to display in the body of the callout
/// -> content
#let callout(
type,
title,
custom_title_colour: none,
custom_body_colour: none,
body
) = {
set par(justify: false)
let title_colour
let body_colour
if type == "warning" {
title_colour = colours.WARNING_STRONG
body_colour = colours.WARNING_WEAK
} else if type == "danger" {
title_colour = colours.DANGER_STRONG
body_colour = colours.DANGER_WEAK
} else if type == "info" {
title_colour = colours.INFO_STRONG
body_colour = colours.INFO_WEAK
} else if type == "custom" {
title_colour = custom_title_colour
body_colour = custom_body_colour
} else {
panic([unknown callout type (#type)])
}
set text(white)
// make body first
block(
fill: body_colour,
width: 100%,
inset: 8pt,
{
// make title
block(fill: title_colour, width: 100%, outset: 8pt, strong(title))
body
}
)
}
/// Create an annotated QR code
///
/// An annotated QR code consists of a title, the QR code, and the link to the website.
/// They are stacked on top of each other.
///
/// - url (str): Link to a website
/// - title (str): Title of the QR code
/// - title_colour (color): Colour of the title of the QR code
/// - url_colour (color): Colour of the link
/// - tiaoma_args (dict): Arguments to pass to the tiaoma package
/// -> grid
#let annotated_qrcode(
url,
title,
title_colour: black,
url_colour: black,
tiaoma_args: (:)
) = {
let qr = tiaoma.qrcode(url, options: tiaoma_args)
grid(
columns: 1,
rows: 3,
align: center + horizon,
row-gutter: 1em,
text(fill: title_colour, title),
qr,
// make url be the width of the QR code
context {
block(
width: measure(qr).width + 1cm,
height: auto,
breakable: false,
link(url, text(url_colour, url.trim("https://", at: start))))
}
)
}
/// Retrieve an image from via an ID
///
/// The artwork repo must be cloned into resources/external, and its `manifest.yaml`
/// must be present for this function to work.
///
/// - type (str): Type of image to fetch (must be a key in one of the manifests)
/// - id (str): ID of image to fetch
/// - args (arguments): Arguments to supply to `image()`
/// -> content
#let get_image_by_id(type, id, ..args) = {
// external manifest is used for artwork repo
// since large photos would bloat the template size
let external_manifest = yaml("../resources/external/manifest.yaml")
let local_manifest = yaml("../resources/manifest.yaml")
let combined_manifest = (:)
// combine the two
for (k, v) in local_manifest.pairs() {
combined_manifest.insert(k, v)
}
for (k, v) in external_manifest.pairs() {
combined_manifest.insert(k, v)
}
if type not in combined_manifest {
panic("type not in either local or external manifest")
}
let root_path = "../resources"
if type in external_manifest {
root_path += "/external"
}
let img_details = combined_manifest.at(type).at(id)
// make image
image(root_path + "/" + type + "/" + img_details.file, ..args)
}
/// Create a horizontal line
///
/// Used for `pandoc` compatability
///
/// -> content
#let horizontalrule() = {
line(
length: 100%,
stroke: 2pt + colours.HORIZONTAL_RULE_GREY
)
}
/// Create a block quote
///
/// Used for `pandoc` compatability
///
/// - body (content): Body of the block quote
/// -> content
#let blockquote(body) = {
quote(
block: true,
body
)
}
+3
View File
@@ -0,0 +1,3 @@
#import "lib.typ" as lib
#import "datasheet.typ" as datasheet
#import "colours.typ" as colours