Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

High DPI-aware Tarmac #31

Merged
merged 7 commits into from
May 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/03-high-dpi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# High DPI Example
This is an example demonstrating how Tarmac detects high DPI variants of assets and automatically generates code to pick the correct variant.
Binary file added examples/03-high-dpi/assets/hello.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/03-high-dpi/assets/hello@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/03-high-dpi/assets/hello@3x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions examples/03-high-dpi/src/assets.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- This file was @generated by Tarmac. It is not intended for manual editing.
return {
hello = function(dpiScale)
if dpiScale >= 3 then
return "rbxassetid://4967220945"
elseif dpiScale >= 2 then
return "rbxassetid://4967220902"
else
return "rbxassetid://4967166732"
end
end,
}
14 changes: 14 additions & 0 deletions examples/03-high-dpi/tarmac-manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[inputs."assets/hello.png"]
hash = "55ef7fd70ec96da73fa0f09bf530605961cce1a654f02684a1a0b0a6234b06cb"
id = 4967166732
packable = false

[inputs."assets/hello@2x.png"]
hash = "02845c418d2c3abe798d1babeecc2e124dddc4765a840cd03d171f32351be3af"
id = 4967220902
packable = false

[inputs."assets/hello@3x.png"]
hash = "1d075da844e76c9e3ec924c1a7fa8efaedbf35787038063664ef939470d7f57c"
id = 4967220945
packable = false
7 changes: 7 additions & 0 deletions examples/03-high-dpi/tarmac.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name = "03-high-dpi"

[[inputs]]
glob = "assets/**/*.png"
codegen = true
base-path = "assets"
codegen-path = "src/assets.lua"
6 changes: 0 additions & 6 deletions src/asset_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@ impl AssetName {
AssetName(displayed.into())
}

/// Spritesheets don't have any canonical name provided by Tarmac's inputs;
/// when we upload them, we want to give them a simple dummy name
pub fn spritesheet() -> Self {
AssetName("spritesheet.png".into())
}

#[cfg(test)]
pub(crate) fn new<S: AsRef<str>>(inner: S) -> Self {
Self(inner.as_ref().into())
Expand Down
250 changes: 149 additions & 101 deletions src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use fs_err::File;
use crate::{
data::ImageSlice,
data::SyncInput,
lua_ast::{Expression, Statement, Table},
lua_ast::{Block, Expression, Function, IfBlock, Statement, Table},
};

const CODEGEN_HEADER: &str =
Expand All @@ -27,26 +27,41 @@ pub fn perform_codegen(output_path: Option<&Path>, inputs: &[&SyncInput]) -> io:
}
}

/// Tree used to track and group inputs hierarchically, before turning them into
/// Lua tables.
enum GroupedItem<'a> {
Folder {
children_by_name: BTreeMap<String, GroupedItem<'a>>,
},
InputGroup {
inputs_by_dpi_scale: BTreeMap<u32, &'a SyncInput>,
},
}

/// Perform codegen for a group of inputs who have `codegen_path` defined.
///
/// We'll build up a Lua file containing nested tables that match the structure
/// of the input's path with its base path stripped away.
fn codegen_grouped(output_path: &Path, inputs: &[&SyncInput]) -> io::Result<()> {
/// Represents the tree of inputs as we're discovering them.
enum Item<'a> {
Folder(BTreeMap<&'a str, Item<'a>>),
Input(&'a SyncInput),
}

let mut root_folder: BTreeMap<&str, Item<'_>> = BTreeMap::new();
let mut root_folder: BTreeMap<String, GroupedItem<'_>> = BTreeMap::new();

// First, collect all of the inputs and group them together into a tree
// according to their relative paths.
for input in inputs {
for &input in inputs {
// Not all inputs will be marked for codegen. We can eliminate those
// right away.
if !input.config.codegen {
continue;
}

// The extension portion of the path is not useful for code generation.
// By stripping it off, we generate the names that users expect.
let mut path_without_extension = input.path_without_dpi_scale.clone();
path_without_extension.set_extension("");

// If we can't construct a relative path, there isn't a sensible name
// that we can use to refer to this input.
let relative_path = input
.path
let relative_path = path_without_extension
.strip_prefix(&input.config.base_path)
.expect("Input base path was not a base path for input");

Expand All @@ -57,77 +72,96 @@ fn codegen_grouped(output_path: &Path, inputs: &[&SyncInput]) -> io::Result<()>
match component {
path::Component::Prefix(_)
| path::Component::RootDir
| path::Component::Normal(_) => segments.push(Path::new(component.as_os_str())),
| path::Component::Normal(_) => {
segments.push(component.as_os_str().to_str().unwrap())
}
path::Component::CurDir => {}
path::Component::ParentDir => assert!(segments.pop().is_some()),
}
}

// Navigate down the tree, creating any folder entries that don't exist
// yet.
//
// This is basically an in-memory `mkdir -p` followed by `touch`.
let mut current_dir = &mut root_folder;
for (i, segment) in segments.iter().enumerate() {
for (i, &segment) in segments.iter().enumerate() {
if i == segments.len() - 1 {
// We assume that the last segment of a path must be a file.

let name = segment.file_stem().unwrap().to_str().unwrap();
current_dir.insert(name, Item::Input(input));
} else {
let name = segment.to_str().unwrap();
let next_entry = current_dir
.entry(name)
.or_insert_with(|| Item::Folder(BTreeMap::new()));

match next_entry {
Item::Folder(next_dir) => {
current_dir = next_dir;
}
Item::Input(_) => {
log::error!(
"A path tried to traverse through a folder as if it were a file: {}",
input.path.display()
);
log::error!("The path segment '{}' is a file because of previous inputs, not a file.", name);
break;
let input_group = match current_dir.get_mut(segment) {
Some(existing) => existing,
None => {
let input_group = GroupedItem::InputGroup {
inputs_by_dpi_scale: BTreeMap::new(),
};
current_dir.insert(segment.to_owned(), input_group);
current_dir.get_mut(segment).unwrap()
}
};

if let GroupedItem::InputGroup {
inputs_by_dpi_scale,
} = input_group
{
inputs_by_dpi_scale.insert(input.dpi_scale, input);
} else {
unreachable!();
}
} else {
let next_entry =
current_dir
.entry(segment.to_owned())
.or_insert_with(|| GroupedItem::Folder {
children_by_name: BTreeMap::new(),
});

if let GroupedItem::Folder { children_by_name } = next_entry {
current_dir = children_by_name;
} else {
unreachable!();
}
}
}
}

fn build_item(item: &Item<'_>) -> Option<Expression> {
fn build_item(item: &GroupedItem<'_>) -> Option<Expression> {
match item {
Item::Folder(children) => {
let entries = children
GroupedItem::Folder { children_by_name } => {
let entries = children_by_name
.iter()
.filter_map(|(&name, child)| build_item(child).map(|item| (name.into(), item)))
.filter_map(|(name, child)| build_item(child).map(|item| (name.into(), item)))
.collect();

Some(Expression::table(entries))
}
Item::Input(input) => {
if input.config.codegen {
if let Some(id) = input.id {
if let Some(slice) = input.slice {
let template = UrlAndSliceTemplate { id, slice };

return Some(template.to_lua());
} else {
let template = AssetUrlTemplate { id };

return Some(template.to_lua());
}
GroupedItem::InputGroup {
inputs_by_dpi_scale,
} => {
if inputs_by_dpi_scale.len() == 1 {
// If there is exactly one input in this group, we can
// generate code knowing that there are no high DPI variants
// to choose from.

let input = inputs_by_dpi_scale.values().next().unwrap();

match (input.id, input.slice) {
(Some(id), Some(slice)) => Some(codegen_url_and_slice(id, slice)),
(Some(id), None) => Some(codegen_just_asset_url(id)),
_ => None,
}
} else {
// In this case, we have the same asset in multiple
// different DPI scales. We can generate code to pick
// between them at runtime.
Some(codegen_with_high_dpi_options(inputs_by_dpi_scale))
}

None
}
}
}

let root_item = build_item(&Item::Folder(root_folder)).unwrap();
let root_item = build_item(&GroupedItem::Folder {
children_by_name: root_folder,
})
.unwrap();
let ast = Statement::Return(root_item);

let mut file = File::create(output_path)?;
Expand All @@ -141,71 +175,85 @@ fn codegen_grouped(output_path: &Path, inputs: &[&SyncInput]) -> io::Result<()>
/// defined, and so generate individual files.
fn codegen_individual(inputs: &[&SyncInput]) -> io::Result<()> {
for input in inputs {
let maybe_expression = if input.config.codegen {
if let Some(id) = input.id {
if let Some(slice) = input.slice {
let template = UrlAndSliceTemplate { id, slice };

Some(template.to_lua())
} else {
let template = AssetUrlTemplate { id };

Some(template.to_lua())
}
} else {
None
}
} else {
None
let expression = match (input.id, input.slice) {
(Some(id), Some(slice)) => codegen_url_and_slice(id, slice),
(Some(id), None) => codegen_just_asset_url(id),
_ => continue,
};

if let Some(expression) = maybe_expression {
let ast = Statement::Return(expression);
let ast = Statement::Return(expression);

let path = input.path.with_extension("lua");
let path = input.path.with_extension("lua");

let mut file = File::create(path)?;
writeln!(file, "{}", CODEGEN_HEADER)?;
write!(file, "{}", ast)?;
}
let mut file = File::create(path)?;
writeln!(file, "{}", CODEGEN_HEADER)?;
write!(file, "{}", ast)?;
}

Ok(())
}

/// Codegen template for CodegenKind::AssetUrl
pub(crate) struct AssetUrlTemplate {
pub id: u64,
fn codegen_url_and_slice(id: u64, slice: ImageSlice) -> Expression {
let offset = slice.min();
let size = slice.size();

let mut table = Table::new();
table.add_entry("Image", format!("rbxassetid://{}", id));
table.add_entry(
"ImageRectOffset",
Expression::Raw(format!("Vector2.new({}, {})", offset.0, offset.1)),
);

table.add_entry(
"ImageRectSize",
Expression::Raw(format!("Vector2.new({}, {})", size.0, size.1)),
);

Expression::Table(table)
}

impl AssetUrlTemplate {
fn to_lua(&self) -> Expression {
Expression::String(format!("rbxassetid://{}", self.id))
}
fn codegen_just_asset_url(id: u64) -> Expression {
Expression::String(format!("rbxassetid://{}", id))
}

pub(crate) struct UrlAndSliceTemplate {
pub id: u64,
pub slice: ImageSlice,
fn codegen_dpi_option(input: &SyncInput) -> (Expression, Block) {
let condition = Expression::Raw(format!("dpiScale >= {}", input.dpi_scale));

// FIXME: We should probably pull data out of SyncInput at the start of
// codegen so that we can handle invariants like this.
let id = input.id.unwrap();

let value = match input.slice {
Some(slice) => codegen_url_and_slice(id, slice),
None => codegen_just_asset_url(id),
};

let body = Statement::Return(value);

(condition, body.into())
}

impl UrlAndSliceTemplate {
fn to_lua(&self) -> Expression {
let offset = self.slice.min();
let size = self.slice.size();
fn codegen_with_high_dpi_options(inputs: &BTreeMap<u32, &SyncInput>) -> Expression {
let args = "dpiScale".to_owned();

let mut options_high_to_low = inputs.values().rev().peekable();

let mut table = Table::new();
table.add_entry("Image", format!("rbxassetid://{}", self.id));
table.add_entry(
"ImageRectOffset",
Expression::Raw(format!("Vector2.new({}, {})", offset.0, offset.1)),
);
let highest_dpi_option = options_high_to_low.next().unwrap();
let (highest_cond, highest_body) = codegen_dpi_option(highest_dpi_option);

table.add_entry(
"ImageRectSize",
Expression::Raw(format!("Vector2.new({}, {})", size.0, size.1)),
);
let mut if_block = IfBlock::new(highest_cond, highest_body);

Expression::Table(table)
while let Some(dpi_option) = options_high_to_low.next() {
let (cond, body) = codegen_dpi_option(dpi_option);

if options_high_to_low.peek().is_some() {
if_block.else_if_blocks.push((cond, body));
} else {
if_block.else_block = Some(body);
}
}

let statements = vec![Statement::If(if_block)];

Expression::Function(Function::new(args, statements))
}
Loading