Skip to content

Commit

Permalink
Implement (barely) and test block inventory rendering.
Browse files Browse the repository at this point in the history
`inv::InvInBlock` is now public, but doesn't have accessors yet
so it can't be customized.
  • Loading branch information
kpreid committed Jul 15, 2024
1 parent 8a7ab00 commit c5b30bb
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 28 deletions.
47 changes: 45 additions & 2 deletions all-is-cubes-content/src/city/exhibits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use all_is_cubes::drawing::embedded_graphics::{
};
use all_is_cubes::drawing::VoxelBrush;
use all_is_cubes::euclid::{size3, vec3, Point3D, Rotation2D, Size3D, Vector2D, Vector3D};
use all_is_cubes::inv;
use all_is_cubes::linking::{BlockProvider, InGenError};
use all_is_cubes::listen::ListenableSource;
use all_is_cubes::math::{
Expand All @@ -36,7 +37,7 @@ use all_is_cubes::space::{SetCubeError, Space, SpaceBuilder, SpacePhysics, Space
use all_is_cubes::transaction::{self, Transaction as _};
use all_is_cubes::{color_block, include_image};

use crate::alg::{four_walls, voronoi_pattern};
use crate::alg::{self, four_walls, voronoi_pattern};
use crate::city::exhibit::{exhibit, Context, Exhibit, ExhibitTransaction, Placement};
use crate::{
make_slab_txn, make_some_blocks, make_some_voxel_blocks_txn, palette, tree, AnimatedVoxels,
Expand All @@ -48,6 +49,7 @@ use crate::{
/// Ordered by distance from the center.
pub(crate) static DEMO_CITY_EXHIBITS: &[Exhibit] = &[
ELEVATOR,
INVENTORY,
KNOT,
TRANSPARENCY_LARGE,
TRANSPARENCY_SMALL,
Expand Down Expand Up @@ -784,6 +786,47 @@ fn COMPOSITE(ctx: Context<'_>) {
Ok((space, ExhibitTransaction::default()))
}

#[macro_rules_attribute::apply(exhibit!)]
#[exhibit(
name: "Modifier::Inventory",
subtitle: "",
placement: Placement::Surface,
)]
fn INVENTORY(ctx: Context<'_>) {
let mut txn = ExhibitTransaction::default();
let demo_blocks = BlockProvider::<DemoBlocks>::using(ctx.universe)?;
let pedestal = &demo_blocks[DemoBlocks::Pedestal];

let mut space = Space::empty(GridAab::from_lower_size([0, 0, 0], [4, 2, 1]));

let inventory_display_block = Block::builder()
.display_name("Has some inventory")
.voxels_fn(R16, |cube| {
// tray shape
if cube.y == 0 || cube.y == 1 && alg::square_radius(R16, cube)[0] == 8 {
const { &color_block!(palette::STEEL) }
} else {
&AIR
}
})?
.build_txn(&mut txn);

let has_items_block = inventory_display_block.with_inventory(
[
inv::Tool::Block(demo_blocks[DemoBlocks::ExhibitBackground].clone()).into(),
inv::Tool::Block(color_block!(Rgb::UNIFORM_LUMINANCE_RED)).into(),
inv::Tool::Block(color_block!(Rgb::UNIFORM_LUMINANCE_GREEN)).into(),
inv::Tool::Block(color_block!(Rgb::UNIFORM_LUMINANCE_BLUE)).into(),
inv::Tool::Block(demo_blocks[DemoBlocks::Lamp(true)].clone()).into(),
]
.into_iter(),
);

stack(&mut space, [0, 0, 0], [pedestal, &has_items_block])?;

Ok((space, txn))
}

#[macro_rules_attribute::apply(exhibit!)]
#[exhibit(
name: "Modifier::Move",
Expand Down Expand Up @@ -1326,7 +1369,7 @@ fn UI_BLOCKS(ctx: Context<'_>) {
use all_is_cubes_ui::vui::blocks::UiBlocks;
use all_is_cubes_ui::vui::widgets::{ToolbarButtonState, WidgetBlocks};

let icons = BlockProvider::<all_is_cubes::inv::Icons>::using(ctx.universe)?;
let icons = BlockProvider::<inv::Icons>::using(ctx.universe)?;
let icons = icons.iter().map(|(_, block)| block.clone());

let widget_blocks = BlockProvider::<WidgetBlocks>::using(ctx.universe)?;
Expand Down
22 changes: 22 additions & 0 deletions all-is-cubes/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;

use crate::inv::{self, InvInBlock};
use crate::listen::{Listen as _, Listener};
use crate::math::{GridAab, GridCoordinate, GridPoint, GridRotation, GridVector, Rgb, Rgba, Vol};
use crate::space::{SetCubeError, Space, SpaceChange};
Expand Down Expand Up @@ -351,6 +352,27 @@ impl Block {
self
}

/// Given a block that does not yet have an [`Modifier::Inventory`], add it.
///
/// The size of the added inventory is the maximum of the size set by [`InvInBlock`]
/// and the size of `contents`.
///
/// TODO: It is not yet decided what happens when this is called on a block which already
/// has an inventory. Currently, it just attaches another modifier.
#[must_use]
pub fn with_inventory(self, contents: impl Iterator<Item = inv::Slot>) -> Block {
let inventory = inv::Inventory::from_slots(
itertools::Itertools::zip_longest(
contents,
// TODO(inventory): InvInBlock should be part of block attributes
core::iter::repeat(inv::Slot::Empty).take(InvInBlock::default().size),
)
.map(|z| z.into_left())
.collect::<Box<[inv::Slot]>>(),
);
self.with_modifier(Modifier::Inventory(inventory))
}

/// Rotates this block by the specified rotation.
///
/// Compared to direct use of [`Modifier::Rotate`], this will:
Expand Down
81 changes: 55 additions & 26 deletions all-is-cubes/src/block/modifier/composite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ use core::mem;
use alloc::vec;
use ordered_float::NotNan;

use crate::block::{self, Block, BlockCollision, Evoxel, Evoxels, MinEval, Modifier, AIR};
use crate::math::{Cube, GridAab, GridCoordinate, GridRotation, Rgb, Vol};
use crate::block::{
self, Block, BlockCollision, Evoxel, Evoxels, MinEval, Modifier, Resolution::R1, AIR,
};
use crate::math::{Cube, GridAab, GridCoordinate, GridRotation, GridSize, Rgb, Vol};
use crate::op::Operation;
use crate::universe;

Expand Down Expand Up @@ -438,38 +440,65 @@ impl CompositeOperator {

// Inventories are rendered by compositing their icon blocks in.
pub(in crate::block) fn render_inventory(
input: MinEval,
mut input: MinEval,
inventory: &crate::inv::Inventory,
filter: &block::EvalFilter,
) -> Result<MinEval, block::InEvalError> {
// TODO(inventory): Define rules under which the inventory is rendered at all, and if so, where
// each icon should be placed. This should be controlled by the evaluation result *preceding*
// this modifier.
// For now, we never render anything.
if true {
// TODO(inventory): condition should be if filter.skip_eval
if filter.skip_eval {
return Ok(input);
}

// TODO(inventory): icon_only_if_intrinsic is a kludge
let Some(icon) = inventory
.slots
.iter()
.find_map(|slot| slot.icon_only_if_intrinsic())
.cloned()
else {
// no nonempty slot to show
return Ok(input);
};
let icon_evaluated = {
let _recursion_scope = block::Budget::recurse(&filter.budget)?;
icon.evaluate_impl(filter)?
};
// TODO(inventory): InvInBlock should be part of block attributes
let config = crate::inv::InvInBlock::default();
for (slot_index, icon_position) in config.icon_positions() {
let Some(placed_icon_bounds) = GridAab::from_lower_size(
icon_position,
GridSize::splat(
(config.icon_resolution / config.icon_scale)
.unwrap_or(R1)
.into(),
),
)
.intersection_cubes(GridAab::for_block(config.icon_resolution)) else {
// Icon's position doesn't intersect the block's bounds.
continue;
};

// TODO(inventory): icon_only_if_intrinsic is a kludge
let Some(icon): Option<&Block> = inventory
.slots
.get(slot_index)
.and_then(|slot| slot.icon_only_if_intrinsic())
else {
// No slot to render at this position.
continue;
};

// TODO(inventory): scale the icon down and place it in a location determined by previously
// established block attributes.
let mut icon_evaluated = {
let _recursion_scope = block::Budget::recurse(&filter.budget)?;
// this is the wrong cost value but it doesn't matter
icon.evaluate_impl(filter)?
.finish(filter.budget.get().to_cost())
};

// TODO(inventory): We should be downsampling the icons (or more precisely,
// asking evaluation to generate a lower resolution as per `config.icon_resolution`).
// For now, we just always generate the resolution-1 form.
let icon_voxel = Evoxel::from_block(&icon_evaluated);
icon_evaluated.voxels = Evoxels::Many(
config.icon_resolution,
Vol::repeat(placed_icon_bounds, icon_voxel),
);

input = evaluate_composition(
icon_evaluated.into(),
input,
CompositeOperator::Over,
filter,
)?;
}

evaluate_composition(icon_evaluated, input, CompositeOperator::Over, filter)
Ok(input)
}

#[cfg(test)]
Expand Down
2 changes: 2 additions & 0 deletions all-is-cubes/src/inv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ mod icons;
pub use icons::*;
mod inventory;
pub use inventory::*;
mod inv_in_block;
pub use inv_in_block::InvInBlock;
mod tool;
pub use tool::*;

Expand Down
114 changes: 114 additions & 0 deletions all-is-cubes/src/inv/inv_in_block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! Configuration of inventories owned by blocks ([`Modifier::Inventory`]).
use alloc::vec::Vec;

use crate::block::Resolution;
use crate::math::{GridCoordinate, GridPoint, GridVector};

#[cfg(doc)]
use crate::block::Modifier;

/// Defines how a [`Modifier::Inventory`] should be configured and displayed within a block.
///
/// TODO: Needs a better name.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InvInBlock {
/// Number of slots the inventory should have.
pub(crate) size: usize, // TODO: use platform-independent max size

/// Scale factor by which to scale down the inventory icon blocks,
/// relative to the bounds of the block in which they are being displayed.
pub(crate) icon_scale: Resolution,

/// Maximum resolution of inventory icons, and resolution in which the `icon_rows`
/// position coordinatess are expressed.
///
/// [`Modifier::Inventory`] is guaranteed not to increase the block resolution
/// beyond this resolution.
pub(crate) icon_resolution: Resolution,

pub(crate) icon_rows: Vec<IconRow>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IconRow {
pub(crate) first_slot: usize,
pub(crate) count: usize,
pub(crate) origin: GridPoint,
pub(crate) stride: GridVector,
}

impl InvInBlock {
/// Returns which inventory slots should be rendered as icons, and the lower corners
/// of the icons.
pub(crate) fn icon_positions(&self) -> impl Iterator<Item = (usize, GridPoint)> + '_ {
self.icon_rows.iter().flat_map(|row| {
(0..row.count).map_while(move |sub_index| {
let slot_index = row.first_slot.checked_add(sub_index)?;
Some((
slot_index,
// TODO: this should be checked arithmetic
row.origin + row.stride * GridCoordinate::try_from(sub_index).ok()?,
))
})
})
}
}

impl Default for InvInBlock {
fn default() -> Self {
// TODO: placeholder; the real default should be empty (zero slots, invisible).
Self {
size: 1,
icon_scale: Resolution::R4,
icon_resolution: Resolution::R16,
icon_rows: vec![
IconRow {
first_slot: 0,
count: 3,
origin: GridPoint::new(1, 1, 1),
stride: GridVector::new(5, 0, 0),
},
IconRow {
first_slot: 3,
count: 3,
origin: GridPoint::new(1, 1, 6),
stride: GridVector::new(5, 0, 0),
},
IconRow {
first_slot: 6,
count: 3,
origin: GridPoint::new(1, 1, 11),
stride: GridVector::new(5, 0, 0),
},
],
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use euclid::point3;
use pretty_assertions::assert_eq;

// TODO: this test should be revised to create an `InvInBlock` for testing instead of the default.
#[test]
fn default_icon_positions() {
let iib = InvInBlock::default();
assert_eq!(
iib.icon_positions().take(10).collect::<Vec<_>>(),
vec![
(0, point3(1, 1, 1)),
(1, point3(6, 1, 1)),
(2, point3(11, 1, 1)),
(3, point3(1, 1, 6)),
(4, point3(6, 1, 6)),
(5, point3(11, 1, 6)),
(6, point3(1, 1, 11)),
(7, point3(6, 1, 11)),
(8, point3(11, 1, 11)),
]
);
}
}

0 comments on commit c5b30bb

Please sign in to comment.