Bad stack layouter 🚑

This commit is contained in:
Laurenz 2019-12-09 13:29:04 +01:00
parent 64f938b449
commit 7e98022435
15 changed files with 385 additions and 99 deletions

View File

@ -261,7 +261,7 @@ impl<'d, W: Write> ExportProcess<'d, W> {
text.tj(self.fonts[active_font.0].encode_text(&string)?);
},
LayoutAction::DebugBox(_, _) => {}
LayoutAction::DebugBox(_) => {}
}
}

View File

@ -15,8 +15,7 @@ pub enum LayoutAction {
/// Write text starting at the current position.
WriteText(String),
/// Visualize a box for debugging purposes.
/// The arguments are position and size.
DebugBox(Size2D, Size2D),
DebugBox(Size2D),
}
impl Serialize for LayoutAction {
@ -25,14 +24,7 @@ impl Serialize for LayoutAction {
MoveAbsolute(s) => write!(f, "m {:.4} {:.4}", s.x.to_pt(), s.y.to_pt()),
SetFont(i, s) => write!(f, "f {} {}", i, s.to_pt()),
WriteText(s) => write!(f, "w {}", s),
DebugBox(p, s) => write!(
f,
"b {} {} {} {}",
p.x.to_pt(),
p.y.to_pt(),
s.x.to_pt(),
s.y.to_pt()
),
DebugBox(s) => write!(f, "b {} {}", s.x.to_pt(), s.y.to_pt()),
}
}
}
@ -44,7 +36,7 @@ impl Display for LayoutAction {
MoveAbsolute(s) => write!(f, "move {} {}", s.x, s.y),
SetFont(i, s) => write!(f, "font {} {}", i, s),
WriteText(s) => write!(f, "write \"{}\"", s),
DebugBox(p, s) => write!(f, "box {} {}", p, s),
DebugBox(s) => write!(f, "box {}", s),
}
}
}
@ -87,8 +79,6 @@ impl LayoutActions {
pub fn add(&mut self, action: LayoutAction) {
match action {
MoveAbsolute(pos) => self.next_pos = Some(self.origin + pos),
DebugBox(pos, size) => self.actions.push(DebugBox(self.origin + pos, size)),
SetFont(index, size) => {
self.next_font = Some((index, size));
}

View File

@ -248,6 +248,16 @@ impl Axis {
}
}
/// The inverse axis.
pub fn inv(&self) -> Axis {
match self {
Axis::LeftToRight => Axis::RightToLeft,
Axis::RightToLeft => Axis::LeftToRight,
Axis::TopToBottom => Axis::BottomToTop,
Axis::BottomToTop => Axis::TopToBottom,
}
}
/// The direction factor for this axis.
///
/// - 1 if the axis is positive.
@ -363,7 +373,7 @@ enum LastSpacing {
}
impl LastSpacing {
#[allow(dead_code)]
/// The size of the soft space if this is a soft space or zero otherwise.
fn soft_or_zero(&self) -> Size {
match self {
LastSpacing::Soft(space, _) => *space,
@ -372,18 +382,6 @@ impl LastSpacing {
}
}
/// The specialized anchor position for an item with the given alignment in a
/// container with a given size along the given axis.
#[allow(dead_code)]
fn anchor(axis: Axis, size: Size, alignment: Alignment) -> Size {
use Alignment::*;
match (axis.is_positive(), alignment) {
(true, Origin) | (false, End) => Size::zero(),
(_, Center) => size / 2,
(true, End) | (false, Origin) => size,
}
}
/// Layout components that can be serialized.
pub trait Serialize {
/// Serialize the data structure into an output writable.

View File

@ -37,7 +37,7 @@ struct Space {
/// Whether to add the layout for this space even if it would be empty.
hard: bool,
/// The so-far accumulated subspaces.
spaces: Vec<Subspace>,
subs: Vec<Subspace>,
}
/// A part of a space with fixed axes and secondary alignment.
@ -88,8 +88,7 @@ impl StackLayouter {
/// Add a layout to the stack.
pub fn add(&mut self, layout: Layout) -> LayoutResult<()> {
if layout.alignment.secondary != self.sub.alignment {
// self.finish_subspace();
// finish sub and start new with layout's alignment
self.finish_subspace(layout.alignment.secondary);
}
// Add a cached soft space if there is one.
@ -174,13 +173,13 @@ impl StackLayouter {
}
}
/// Change the layouting axis used by this layouter.
/// Change the layouting axes used by this layouter.
///
/// This starts a new subspace (if the axes are actually different from the
/// current ones).
pub fn set_axes(&mut self, axes: LayoutAxes) {
if axes != self.ctx.axes {
self.finish_subspace();
self.finish_subspace(Alignment::Origin);
let (origin, usable) = self.remaining_subspace();
self.sub = Subspace::new(axes, Alignment::Origin, origin, usable);
@ -226,9 +225,7 @@ impl StackLayouter {
/// Whether the current layout space (not subspace) is empty.
pub fn space_is_empty(&self) -> bool {
self.sub.layouts.is_empty()
&& self.sub.size == Size2D::zero()
&& self.space.spaces.is_empty()
self.subspace_is_empty() && self.space.subs.is_empty()
}
/// Whether the current layout space is the last is the followup list.
@ -244,12 +241,127 @@ impl StackLayouter {
self.layouts
}
pub fn finish_space(&mut self, _hard: bool) {
unimplemented!()
}
/// Finish the current space and start a new one.
pub fn finish_space(&mut self, hard: bool) {
self.finish_subspace(Alignment::Origin);
fn finish_subspace(&mut self) {
unimplemented!()
println!();
println!("FINISHING SPACE:");
println!();
let space = self.ctx.spaces[self.space.index];
let mut subs = std::mem::replace(&mut self.space.subs, vec![]);
// ---------------------------------------------------------------------
// Compute the size of the whole space.
let usable = space.usable();
let mut max = Size2D {
x: if space.expand.0 { usable.x } else { Size::zero() },
y: if space.expand.1 { usable.y } else { Size::zero() },
};
// The total size is determined by the maximum position + extent of one
// of the boxes.
for sub in &subs {
max.max_eq(sub.origin + sub.axes.specialize(sub.size));
}
let dimensions = max.padded(space.padding);
println!("WITH DIMENSIONS: {}", dimensions);
println!("SUBS: {:#?}", subs);
// ---------------------------------------------------------------------
// Justify the boxes according to their alignment and give each box
// the appropriate origin and usable space.
// use Alignment::*;
for sub in &mut subs {
// The usable width should not exceed the total usable width
// (previous value) or the maximum width of the layout as a whole.
sub.usable.x = crate::size::min(
sub.usable.x,
sub.axes.specialize(max - sub.origin).x,
);
sub.usable.y = sub.size.y;
}
// if space.expand.1 {
// let height = subs.iter().map(|sub| sub.size.y).sum();
// let centers = subs.iter()
// .filter(|sub| sub.alignment == Alignment::Center)
// .count()
// .max(1);
// let grow = max.y - height;
// let center_grow = grow / (centers as i32);
// println!("center grow = {}", center_grow);
// let mut offset = Size::zero();
// for sub in &mut subs {
// sub.origin.y += offset;
// if sub.alignment == Center {
// sub.usable.y += center_grow;
// offset += center_grow;
// }
// }
// if let Some(last) = subs.last_mut() {
// last.usable.y += grow - offset;
// }
// }
// ---------------------------------------------------------------------
// Do the thing
// Add a debug box with this boxes size.
let mut actions = LayoutActions::new();
actions.add(LayoutAction::DebugBox(dimensions));
for sub in subs {
let LayoutAxes { primary, secondary } = sub.axes;
// The factor is +1 if the axis is positive and -1 otherwise.
let factor = sub.axes.secondary.factor();
// The anchor is the position of the origin-most point of the
// layout.
let anchor =
sub.usable.y.anchor(sub.alignment, secondary.is_positive())
- factor * sub.size.y.anchor(sub.alignment, true);
for entry in sub.layouts {
let layout = entry.layout;
let alignment = layout.alignment.primary;
let size = sub.axes.generalize(layout.dimensions);
let x =
sub.usable.x.anchor(alignment, primary.is_positive())
- size.x.anchor(alignment, primary.is_positive());
let y = anchor
+ factor * entry.offset
- size.y.anchor(Alignment::Origin, secondary.is_positive());
let pos = sub.origin + sub.axes.specialize(Size2D::new(x, y));
actions.add_layout(pos, layout);
}
}
// ---------------------------------------------------------------------
self.layouts.push(Layout {
dimensions,
baseline: None,
alignment: self.ctx.alignment,
actions: actions.to_vec(),
});
self.start_space(self.next_space(), hard);
}
/// Start a new space with the given index.
@ -263,14 +375,46 @@ impl StackLayouter {
self.sub = Subspace::new(axes, Alignment::Origin, space.start(), space.usable());
}
/// The remaining sub
fn remaining_subspace(&self) -> (Size2D, Size2D) {
unimplemented!()
}
/// The index of the next space.
fn next_space(&self) -> usize {
(self.space.index + 1).min(self.ctx.spaces.len() - 1)
}
/// Finish the current subspace.
fn finish_subspace(&mut self, new_alignment: Alignment) {
let empty = self.subspace_is_empty();
let axes = self.ctx.axes;
let (origin, usable) = self.remaining_subspace();
let new_sub = Subspace::new(axes, new_alignment, origin, usable);
let sub = std::mem::replace(&mut self.sub, new_sub);
if !empty {
self.space.subs.push(sub);
}
}
/// The remaining sub
fn remaining_subspace(&self) -> (Size2D, Size2D) {
let offset = self.sub.size.y + self.sub.last_spacing.soft_or_zero();
let new_origin = self.sub.origin + match self.ctx.axes.secondary.is_positive() {
true => self.ctx.axes.specialize(Size2D::with_y(offset)),
false => Size2D::zero(),
};
let new_usable = self.ctx.axes.specialize(Size2D {
x: self.sub.usable.x,
y: self.sub.usable.y - offset,
});
(new_origin, new_usable)
}
/// Whether the current layout space (not subspace) is empty.
fn subspace_is_empty(&self) -> bool {
self.sub.layouts.is_empty() && self.sub.size == Size2D::zero()
}
}
impl Space {
@ -278,7 +422,7 @@ impl Space {
Space {
index,
hard,
spaces: vec![],
subs: vec![],
}
}
}

View File

@ -11,7 +11,7 @@ pub fn layout_tree(tree: &SyntaxTree, ctx: LayoutContext) -> LayoutResult<MultiL
#[derive(Debug, Clone)]
struct TreeLayouter<'a, 'p> {
ctx: LayoutContext<'a, 'p>,
flex: FlexLayouter,
stack: StackLayouter,
style: LayoutStyle,
}
@ -19,11 +19,10 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
/// Create a new syntax tree layouter.
fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> {
TreeLayouter {
flex: FlexLayouter::new(FlexContext {
stack: StackLayouter::new(StackContext {
spaces: ctx.spaces.clone(),
axes: ctx.axes,
alignment: ctx.alignment,
flex_spacing: flex_spacing(&ctx.style.text),
}),
style: ctx.style.clone(),
ctx,
@ -56,25 +55,22 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
alignment: self.ctx.alignment,
})?;
Ok(self.flex.add(layout))
self.stack.add(layout)
}
fn layout_space(&mut self) {
self.flex.add_primary_space(
word_spacing(&self.style.text),
SPACE_KIND,
);
}
fn layout_paragraph(&mut self) -> LayoutResult<()> {
self.flex.add_secondary_space(
Ok(self.stack.add_spacing(
paragraph_spacing(&self.style.text),
PARAGRAPH_KIND,
)
))
}
fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> {
let spaces = self.flex.remaining();
let spaces = self.stack.remaining();
let commands = func.0.layout(LayoutContext {
loader: self.ctx.loader,
@ -97,16 +93,16 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
match command {
LayoutTree(tree) => self.layout(tree)?,
Add(layout) => self.flex.add(layout),
AddMultiple(layouts) => self.flex.add_multiple(layouts),
Add(layout) => self.stack.add(layout)?,
AddMultiple(layouts) => self.stack.add_multiple(layouts)?,
AddSpacing(space, kind, axis) => match axis {
GenericAxisKind::Primary => self.flex.add_primary_space(space, kind),
GenericAxisKind::Secondary => self.flex.add_secondary_space(space, kind)?,
GenericAxisKind::Primary => {},
GenericAxisKind::Secondary => self.stack.add_spacing(space, kind),
}
FinishLine => self.flex.add_break(),
FinishRun => { self.flex.finish_run()?; },
FinishSpace => self.flex.finish_space(true)?,
FinishLine => {},
FinishRun => {},
FinishSpace => self.stack.finish_space(true),
BreakParagraph => self.layout_paragraph()?,
SetTextStyle(style) => self.style.text = style,
@ -116,7 +112,7 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
}
self.style.page = style;
self.flex.set_spaces(smallvec![
self.stack.set_spaces(smallvec![
LayoutSpace {
dimensions: style.dimensions,
padding: style.margins,
@ -126,7 +122,7 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
}
SetAlignment(alignment) => self.ctx.alignment = alignment,
SetAxes(axes) => {
self.flex.set_axes(axes);
self.stack.set_axes(axes);
self.ctx.axes = axes;
}
}
@ -135,7 +131,7 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
}
fn finish(self) -> LayoutResult<MultiLayout> {
self.flex.finish()
Ok(self.stack.finish())
}
}

View File

@ -14,6 +14,8 @@
//! format. Submodules for these formats are located in the [export](crate::export)
//! module. Currently, the only supported output format is _PDF_.
#![allow(unused)]
pub extern crate toddle;
use std::cell::RefCell;

View File

@ -6,7 +6,7 @@ function! {
#[derive(Debug, PartialEq)]
pub struct Boxed {
body: SyntaxTree,
map: ExtentMap,
map: ExtentMap<PSize>,
}
parse(args, body, ctx) {
@ -17,7 +17,18 @@ function! {
}
layout(self, mut ctx) {
self.map.apply(ctx.axes, &mut ctx.spaces[0].dimensions)?;
use SpecificAxisKind::*;
let space = &mut ctx.spaces[0];
self.map.apply_with(ctx.axes, |axis, p| {
let entity = match axis {
Horizontal => { space.expand.0 = true; &mut space.dimensions.x },
Vertical => { space.expand.1 = true; &mut space.dimensions.y },
};
*entity = p.concretize(*entity)
});
vec![AddMultiple(layout_tree(&self.body, ctx)?)]
}
}

60
src/library/direction.rs Normal file
View File

@ -0,0 +1,60 @@
use crate::func::prelude::*;
use super::maps::ConsistentMap;
use super::keys::AxisKey;
function! {
/// `direction`: Sets the directions for the layouting axes.
#[derive(Debug, PartialEq)]
pub struct Direction {
body: Option<SyntaxTree>,
map: ConsistentMap<AxisKey, Axis>,
}
parse(args, body, ctx) {
let mut map = ConsistentMap::new();
map.add_opt_span(AxisKey::Primary, args.get_pos_opt::<Axis>()?)?;
map.add_opt_span(AxisKey::Secondary, args.get_pos_opt::<Axis>()?)?;
for arg in args.keys() {
let axis = AxisKey::from_ident(&arg.v.key)?;
let value = Axis::from_expr(arg.v.value)?;
map.add(axis, value)?;
}
Direction {
body: parse!(optional: body, ctx),
map,
}
}
layout(self, mut ctx) {
let axes = ctx.axes;
let map = self.map.dedup(|key, &direction| {
Ok((match key {
AxisKey::Primary => GenericAxisKind::Primary,
AxisKey::Secondary => GenericAxisKind::Secondary,
AxisKey::Horizontal => axes.horizontal(),
AxisKey::Vertical => axes.vertical(),
}, direction))
})?;
map.with(GenericAxisKind::Primary, |&val| ctx.axes.primary = val);
map.with(GenericAxisKind::Secondary, |&val| ctx.axes.secondary = val);
if ctx.axes.primary.is_horizontal() == ctx.axes.secondary.is_horizontal() {
error!(
"aligned primary and secondary axes: `{}`, `{}`",
format!("{:?}", ctx.axes.primary).to_lowercase(),
format!("{:?}", ctx.axes.secondary).to_lowercase(),
);
}
match &self.body {
Some(body) => vec![AddMultiple(layout_tree(&body, ctx)?)],
None => vec![Command::SetAxes(ctx.axes)],
}
}
}

View File

@ -66,7 +66,7 @@ kind!(AxisKey, "axis",
"secondary" => AxisKey::Secondary,
);
/// An argument key which identifies a target alignment.
/// An argument key which describes a target alignment.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum AlignmentKey {
Left,
@ -172,3 +172,10 @@ kind!(PaddingKey<AxisKey>, "axis or side",
"vertical-origin" => PaddingKey::AxisAligned(AxisKey::Vertical, AlignmentKey::Origin),
"vertical-end" => PaddingKey::AxisAligned(AxisKey::Vertical, AlignmentKey::End),
);
kind!(Axis, "direction",
"ltr" => Axis::LeftToRight,
"rtl" => Axis::RightToLeft,
"ttb" => Axis::TopToBottom,
"btt" => Axis::BottomToTop,
);

View File

@ -72,14 +72,14 @@ impl<K, V> ConsistentMap<K, V> where K: Hash + Eq {
/// A map for storing extents along axes.
#[derive(Debug, Clone, PartialEq)]
pub struct ExtentMap(ConsistentMap<AxisKey, Size>);
pub struct ExtentMap<E: ExpressionKind + Copy>(ConsistentMap<AxisKey, E>);
impl ExtentMap {
impl<E: ExpressionKind + Copy> ExtentMap<E> {
/// Parse an extent map from the function args.
///
/// If `enforce` is true other arguments will create an error, otherwise
/// they are left intact.
pub fn new(args: &mut FuncArgs, enforce: bool) -> ParseResult<ExtentMap> {
pub fn new(args: &mut FuncArgs, enforce: bool) -> ParseResult<ExtentMap<E>> {
let mut map = ConsistentMap::new();
for arg in args.keys() {
@ -96,22 +96,38 @@ impl ExtentMap {
}
};
let size = Size::from_expr(arg.v.value)?;
map.add(key, size)?;
let e = E::from_expr(arg.v.value)?;
map.add(key, e)?;
}
Ok(ExtentMap(map))
}
/// Map from any axis key to the specific axis kind.
pub fn apply(&self, axes: LayoutAxes, dimensions: &mut Size2D) -> LayoutResult<()> {
let map = self.0.dedup(|key, &val| Ok((key.specific(axes), val)))?;
map.with(SpecificAxisKind::Horizontal, |&val| dimensions.x = val);
map.with(SpecificAxisKind::Vertical, |&val| dimensions.y = val);
/// Apply the extents on the dimensions.
pub fn apply<F>(
&self,
axes: LayoutAxes,
dimensions: &mut Size2D,
size: F
) -> LayoutResult<()> where F: Fn(&E) -> Size {
let map = self.dedup(axes)?;
map.with(SpecificAxisKind::Horizontal, |val| dimensions.x = size(val));
map.with(SpecificAxisKind::Vertical, |val| dimensions.y = size(val));
Ok(())
}
/// Map from any axis key to the specific axis kind.
pub fn apply_with<F>(&self, axes: LayoutAxes, mut f: F) -> LayoutResult<()>
where F: FnMut(SpecificAxisKind, &E) {
for (&key, value) in self.dedup(axes)?.iter() {
f(key, value);
}
Ok(())
}
fn dedup(&self, axes: LayoutAxes) -> LayoutResult<ConsistentMap<SpecificAxisKind, E>> {
self.0.dedup(|key, &val| Ok((key.specific(axes), val)))
}
}
/// A map for storing padding at sides.

View File

@ -8,6 +8,7 @@ use maps::*;
pub_use_mod!(align);
pub_use_mod!(boxed);
pub_use_mod!(direction);
pub mod maps;
pub mod keys;
@ -19,6 +20,7 @@ pub fn std() -> Scope {
std.add::<Align>("align");
std.add::<Boxed>("box");
std.add::<Direction>("direction");
std.add::<PageSize>("page.size");
std.add::<PageMargins>("page.margins");
@ -78,7 +80,7 @@ function! {
/// `page.size`: Set the size of pages.
#[derive(Debug, PartialEq)]
pub struct PageSize {
map: ExtentMap,
map: ExtentMap<Size>,
}
parse(args, body) {
@ -90,7 +92,7 @@ function! {
layout(self, ctx) {
let mut style = ctx.style.page;
self.map.apply(ctx.axes, &mut style.dimensions)?;
self.map.apply(ctx.axes, &mut style.dimensions, |&s| s)?;
vec![SetPageStyle(style)]
}
}
@ -150,11 +152,7 @@ function! {
layout(self, ctx) {
let axis = self.axis.generic(ctx.axes);
let spacing = match self.spacing {
FSize::Absolute(size) => size,
FSize::Scaled(scale) => scale * ctx.style.text.font_size,
};
let spacing = self.spacing.concretize(ctx.style.text.font_size);
vec![AddSpacing(spacing, SpacingKind::Hard, axis)]
}
}

View File

@ -6,6 +6,8 @@ use std::iter::Sum;
use std::ops::*;
use std::str::FromStr;
use crate::layout::Alignment;
/// A general space type.
#[derive(Copy, Clone, PartialEq)]
pub struct Size {
@ -80,6 +82,18 @@ impl Size {
pub fn max_eq(&mut self, other: Size) {
*self = max(*self, other);
}
/// The specialized anchor position for an item with the given alignment in a
/// container with a given size along the given axis.
pub fn anchor(&self, alignment: Alignment, positive: bool) -> Size {
use Alignment::*;
match (positive, alignment) {
(true, Origin) | (false, End) => Size::zero(),
(_, Center) => *self / 2,
(true, End) | (false, Origin) => *self,
}
}
}
impl Size2D {
@ -178,6 +192,16 @@ impl SizeBox {
}
}
impl ScaleSize {
/// Use the absolute value or scale the entity.
pub fn concretize(&self, entity: Size) -> Size {
match self {
ScaleSize::Absolute(s) => *s,
ScaleSize::Scaled(s) => *s * entity,
}
}
}
/// The maximum of two sizes.
pub fn max(a: Size, b: Size) -> Size {
if a >= b { a } else { b }
@ -192,7 +216,7 @@ pub fn min(a: Size, b: Size) -> Size {
impl Display for Size {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}pt", self.points)
write!(f, "{}cm", self.to_cm())
}
}

View File

@ -23,10 +23,7 @@ fn main() -> Result<()> {
create_dir_all("tests/cache/pdf")?;
let tests: Vec<_> = read_dir("tests/layouts/")?.collect();
let len = tests.len();
println!();
println!("Running {} test{}", len, if len > 1 { "s" } else { "" });
let mut filtered = Vec::new();
for entry in tests {
let path = entry?.path();
@ -36,14 +33,23 @@ fn main() -> Result<()> {
let name = path
.file_stem().ok_or("expected file stem")?
.to_string_lossy();
.to_string_lossy()
.to_string();
if opts.matches(&name) {
let src = read_to_string(&path)?;
panic::catch_unwind(|| test(&name, &src)).ok();
filtered.push((name, src));
}
}
let len = filtered.len();
println!();
println!("Running {} test{}", len, if len > 1 { "s" } else { "" });
for (name, src) in filtered {
panic::catch_unwind(|| test(&name, &src)).ok();
}
println!();
Ok(())
@ -79,6 +85,7 @@ fn test(name: &str, src: &str) -> Result<()> {
});
}
}
drop(loader);
// Write the serialized layout file.
let path = format!("tests/cache/serial/{}", name);

31
tests/layouts/test.typ Normal file
View File

@ -0,0 +1,31 @@
[page.size: w=5cm, h=5cm]
[page.margins: 0cm]
// [box: w=4cm, h=3cm][1]
// //
// [direction: ttb, ltr]
// [box: w=2cm, h=1][2]
// //
// [direction: btt, rtl]
// [align: bottom, right]
// [box: w=3cm, h=1][3]
// //
// [direction: ltr, ttb]
// [align: center, center]
// [box: w=2cm, h=2cm][4]
[align: center]
//[direction: primary=btt, secondary=rtl]
//[align: primary=bottom, secondary=right]
//[box][Hi]
[box][
//[align: primary=center, secondary=bottom]
[direction: secondary=btt]
Blabla
[v: 0.5cm]
[align: vertical=end] Origin 2]
//[align: vertical=center] Center
//[align: vertical=center] Center
//[align: vertical=end] End End End

View File

@ -16,7 +16,7 @@ def main():
assert len(sys.argv) == 2, 'usage: python render.py <name>'
name = sys.argv[1]
filename = os.path.join(SERIAL, f'{name}.tld')
filename = os.path.join(SERIAL, name)
with open(filename, encoding='utf-8') as file:
lines = [line[:-1] for line in file.readlines()]
@ -25,7 +25,7 @@ def main():
image = renderer.export()
pathlib.Path(RENDER).mkdir(parents=True, exist_ok=True)
image.save(os.path.join(RENDER, f'{name}.png')
image.save(os.path.join(RENDER, f'{name}.png'))
class MultiboxRenderer:
@ -56,6 +56,7 @@ class MultiboxRenderer:
renderer = BoxRenderer(self.fonts, width, height)
for i in range(action_count):
if i == 0: continue
command = self.content[start + i]
renderer.execute(command)
@ -145,7 +146,8 @@ class BoxRenderer:
self.draw.text(self.cursor, text, (0, 0, 0, 255), font=self.font)
elif cmd == 'b':
x, y, w, h = (pix(float(s)) for s in parts)
x, y = self.cursor
w, h = (pix(float(s)) for s in parts)
rect = [x, y, x+w-1, y+h-1]
forbidden_colors = set()