From c5b30bb8b9f2bd7accf2af9d49c6e45a280dfeb7 Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Wed, 26 Jun 2024 08:35:27 -0700 Subject: [PATCH] Implement (barely) and test block inventory rendering. `inv::InvInBlock` is now public, but doesn't have accessors yet so it can't be customized. --- all-is-cubes-content/src/city/exhibits.rs | 47 +++++++- all-is-cubes/src/block.rs | 22 ++++ all-is-cubes/src/block/modifier/composite.rs | 81 ++++++++----- all-is-cubes/src/inv.rs | 2 + all-is-cubes/src/inv/inv_in_block.rs | 114 +++++++++++++++++++ 5 files changed, 238 insertions(+), 28 deletions(-) create mode 100644 all-is-cubes/src/inv/inv_in_block.rs diff --git a/all-is-cubes-content/src/city/exhibits.rs b/all-is-cubes-content/src/city/exhibits.rs index 5909c85e0..823732a39 100644 --- a/all-is-cubes-content/src/city/exhibits.rs +++ b/all-is-cubes-content/src/city/exhibits.rs @@ -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::{ @@ -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, @@ -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, @@ -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::::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", @@ -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::::using(ctx.universe)?; + let icons = BlockProvider::::using(ctx.universe)?; let icons = icons.iter().map(|(_, block)| block.clone()); let widget_blocks = BlockProvider::::using(ctx.universe)?; diff --git a/all-is-cubes/src/block.rs b/all-is-cubes/src/block.rs index 201994bce..53848c601 100644 --- a/all-is-cubes/src/block.rs +++ b/all-is-cubes/src/block.rs @@ -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}; @@ -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) -> 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::>(), + ); + self.with_modifier(Modifier::Inventory(inventory)) + } + /// Rotates this block by the specified rotation. /// /// Compared to direct use of [`Modifier::Rotate`], this will: diff --git a/all-is-cubes/src/block/modifier/composite.rs b/all-is-cubes/src/block/modifier/composite.rs index 973a668ea..0ba05e94a 100644 --- a/all-is-cubes/src/block/modifier/composite.rs +++ b/all-is-cubes/src/block/modifier/composite.rs @@ -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; @@ -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 { - // 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)] diff --git a/all-is-cubes/src/inv.rs b/all-is-cubes/src/inv.rs index 766f03a75..6f89796da 100644 --- a/all-is-cubes/src/inv.rs +++ b/all-is-cubes/src/inv.rs @@ -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::*; diff --git a/all-is-cubes/src/inv/inv_in_block.rs b/all-is-cubes/src/inv/inv_in_block.rs new file mode 100644 index 000000000..792f2729d --- /dev/null +++ b/all-is-cubes/src/inv/inv_in_block.rs @@ -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, +} + +#[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 + '_ { + 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![ + (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)), + ] + ); + } +}