1use alloc::borrow::Cow;
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6
7use crate::block::{
8 self, AIR, Block, BlockAttributes, BlockCollision, BlockParts, BlockPtr, Modifier, Primitive,
9 Resolution,
10};
11use crate::math::{Cube, GridAab, GridPoint, Rgb, Rgb01, Rgba};
12use crate::space::{SetCubeError, Space};
13use crate::transaction::{self, Merge, Transaction};
14use crate::universe::{Handle, Name, ReadTicket, Universe, UniverseTransaction};
15
16#[cfg(doc)]
17use crate::space;
18
19#[derive(Clone, Debug, Eq, PartialEq)]
49#[must_use]
50pub struct Builder<'u, P, Txn> {
51 read_ticket: ReadTicket<'u>,
52
53 pub(in crate::block) attributes: BlockAttributes,
55
56 primitive_builder: P,
57
58 modifiers: Vec<Modifier>,
59
60 transaction: Txn,
63}
64
65impl Default for Builder<'_, NeedsPrimitive, ()> {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl Builder<'_, NeedsPrimitive, ()> {
72 pub(super) const fn new() -> Self {
74 Builder {
75 read_ticket: ReadTicket::stub(),
76 attributes: BlockAttributes::default(),
77 primitive_builder: NeedsPrimitive,
78 modifiers: Vec::new(),
79 transaction: (),
80 }
81 }
82
83 #[track_caller]
87 pub fn build_attributes(self) -> BlockAttributes {
88 let Self {
89 read_ticket: _,
90 attributes,
91 primitive_builder: NeedsPrimitive,
92 modifiers,
93 transaction: (),
94 } = self;
95 assert_eq!(modifiers, []);
96 attributes
97 }
98}
99
100impl<'u, P, Txn> Builder<'u, P, Txn> {
101 pub fn attributes(mut self, value: BlockAttributes) -> Self {
104 self.attributes = value;
105 self
106 }
107
108 pub fn modifier(mut self, modifier: Modifier) -> Self {
111 self.modifiers.push(modifier);
113 self
114 }
115
116 pub fn color(self, color: impl Into<Rgba>) -> Builder<'u, Atom, ()> {
120 let Self {
121 read_ticket,
122 attributes,
123 primitive_builder: _,
124 modifiers,
125 transaction: _,
126 } = self;
127 Builder {
128 read_ticket,
129 attributes,
130 primitive_builder: Atom {
131 color: color.into(),
132 emission: Rgb::ZERO,
133 collision: BlockCollision::Hard,
134 },
135 modifiers,
136 transaction: (),
140 }
141 }
142
143 pub fn voxels_handle(
147 self,
148 resolution: Resolution,
149 space: Handle<Space>,
150 ) -> Builder<'u, Voxels, ()> {
151 let Self {
152 read_ticket,
153 attributes,
154 primitive_builder: _,
155 modifiers,
156 transaction: _,
157 } = self;
158 Builder {
159 read_ticket,
160 attributes,
161 primitive_builder: Voxels {
162 space,
163 resolution,
164 offset: GridPoint::origin(),
165 },
166 modifiers,
167 transaction: (),
171 }
172 }
173
174 pub(crate) fn voxels_space(
178 self,
179 resolution: Resolution,
180 space: Space,
181 ) -> Builder<'u, Voxels, UniverseTransaction> {
182 let (space_handle, transaction) = UniverseTransaction::insert(Name::Pending, space);
183
184 let Self {
185 read_ticket,
186 attributes,
187 primitive_builder: _,
188 modifiers,
189 transaction: _,
190 } = self;
191 Builder {
192 read_ticket,
193 attributes,
194 primitive_builder: Voxels {
195 space: space_handle,
196 resolution,
197 offset: GridPoint::origin(),
198 },
199 modifiers,
200 transaction,
204 }
205 }
206
207 pub fn voxels_fn<'a, F, B>(
225 self,
226 resolution: Resolution,
228 mut function: F,
229 ) -> Result<Builder<'u, Voxels, UniverseTransaction>, SetCubeError>
230 where
231 F: FnMut(Cube) -> B,
232 B: Into<Cow<'a, Block>>,
233 {
234 #[inline(never)]
238 fn voxels_fn_impl<'a, 'u>(
239 read_ticket: ReadTicket<'u>,
240 attributes: BlockAttributes,
241 modifiers: Vec<Modifier>,
242 resolution: Resolution,
243 function: &mut dyn FnMut(Cube) -> Cow<'a, Block>,
244 ) -> Result<Builder<'u, Voxels, UniverseTransaction>, SetCubeError> {
245 let mut not_air_bounds: Option<GridAab> = None;
246
247 let mut space = Space::for_block(resolution).build();
248 space.mutate(read_ticket, |m| {
250 m.fill_all(|cube| {
251 let block = function(cube);
252
253 if block.as_ref() != &AIR {
255 let cube_bb = cube.grid_aab();
256 not_air_bounds = Some(if let Some(bounds) = not_air_bounds {
257 bounds.union_box(cube_bb)
258 } else {
259 cube_bb
260 });
261 }
262
263 Some(block)
264 })
265 })?;
266
267 let not_air_bounds = not_air_bounds.unwrap_or(GridAab::ORIGIN_EMPTY);
271 if space.bounds() != not_air_bounds {
272 let mut shrunk =
275 Space::builder(not_air_bounds).physics(space.physics().clone()).build();
276 shrunk.mutate(read_ticket, |m| {
277 m.fill(not_air_bounds, |cube| Some(&space[cube]))
278 })?;
279 space = shrunk;
280 }
281
282 let (space_handle, transaction) = UniverseTransaction::insert(Name::Pending, space);
283
284 Ok(Builder {
285 read_ticket,
286 attributes,
287 primitive_builder: Voxels {
288 space: space_handle,
289 resolution,
290 offset: GridPoint::origin(),
291 },
292 modifiers,
293 transaction,
294 })
295 }
296
297 let Self {
298 read_ticket,
299 attributes,
300 primitive_builder: _,
301 modifiers,
302 transaction: _,
303 } = self;
304 voxels_fn_impl(
305 read_ticket,
306 attributes,
307 modifiers,
308 resolution,
309 &mut |cube| function(cube).into(),
310 )
311 }
312
313 #[expect(clippy::elidable_lifetime_names, reason = "names for clarity")]
321 pub fn read_ticket<'u2>(self, read_ticket: ReadTicket<'u2>) -> Builder<'u2, P, Txn> {
322 Builder {
323 read_ticket,
324 attributes: self.attributes,
325 primitive_builder: self.primitive_builder,
326 modifiers: self.modifiers,
327 transaction: self.transaction,
328 }
329 }
330
331 fn build_block_and_txn_internal(self) -> (Block, Txn)
332 where
333 P: BuildPrimitive,
334 {
335 let Self {
336 read_ticket: _,
337 attributes,
338 primitive_builder,
339 mut modifiers,
340 transaction,
341 } = self;
342 let primitive = primitive_builder.build_primitive();
343
344 if attributes != BlockAttributes::default() {
345 modifiers.insert(0, Modifier::Attributes(Arc::new(attributes)));
346 }
347
348 let block = if matches!(primitive, Primitive::Air) && modifiers.is_empty() {
349 AIR
351 } else {
352 Block(BlockPtr::Owned(Arc::new(BlockParts {
353 primitive,
354 modifiers,
355 })))
356 };
357
358 (block, transaction)
359 }
360}
361
362impl<P: BuildPrimitive> Builder<'_, P, ()> {
363 pub fn build(self) -> Block {
368 let (block, ()) = self.build_block_and_txn_internal();
369 block
370 }
371}
372
373impl<P: BuildPrimitive> Builder<'_, P, UniverseTransaction> {
374 pub fn build_into(self, universe: &mut Universe) -> Block {
379 let (block, transaction) = self.build_block_and_txn_internal();
380
381 transaction.execute(universe, (), &mut transaction::no_outputs).unwrap();
383
384 block
385 }
386
387 pub fn build_txn(self, transaction: &mut UniverseTransaction) -> Block {
390 let (block, txn) = self.build_block_and_txn_internal();
391 transaction.merge_from(txn).unwrap();
392 block
393 }
394}
395
396impl<Txn> Builder<'_, Atom, Txn> {
398 pub const fn collision(mut self, collision: BlockCollision) -> Self {
400 self.primitive_builder.collision = collision;
401 self
402 }
403
404 pub fn light_emission(mut self, value: impl Into<Rgb>) -> Self {
408 self.primitive_builder.emission = value.into();
409 self
410 }
411}
412
413impl<Txn> Builder<'_, Voxels, Txn> {
415 pub fn offset(mut self, offset: GridPoint) -> Self {
419 self.primitive_builder.offset = offset;
420 self
421 }
422
423 }
426
427impl<C: BuildPrimitive> From<Builder<'_, C, ()>> for Block {
429 fn from(builder: Builder<'_, C, ()>) -> Self {
430 builder.build()
431 }
432}
433impl From<Rgba> for Builder<'_, Atom, ()> {
435 fn from(color: Rgba) -> Self {
436 Block::builder().color(color)
437 }
438}
439impl From<Rgb01> for Builder<'_, Atom, ()> {
441 fn from(color: Rgb01) -> Self {
442 Block::builder().color(color.with_alpha_one())
443 }
444}
445
446#[expect(clippy::exhaustive_structs)]
449#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)]
450pub struct NeedsPrimitive;
451
452#[doc(hidden)]
457pub trait BuildPrimitive {
458 fn build_primitive(self) -> Primitive;
459}
460
461#[derive(Clone, Debug, Eq, Hash, PartialEq)]
465pub struct Atom {
466 color: Rgba,
467 emission: Rgb,
468 collision: BlockCollision,
469}
470impl BuildPrimitive for Atom {
471 fn build_primitive(self) -> Primitive {
472 Primitive::Atom(block::Atom {
473 color: self.color,
474 emission: self.emission,
475 collision: self.collision,
476 })
477 }
478}
479
480#[derive(Clone, Debug, Eq, Hash, PartialEq)]
482pub struct Voxels {
483 space: Handle<Space>,
484 resolution: Resolution,
485 offset: GridPoint,
486}
487impl BuildPrimitive for Voxels {
488 fn build_primitive(self) -> Primitive {
489 Primitive::Recur {
490 offset: self.offset,
491 resolution: self.resolution,
492 space: self.space,
493 }
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use alloc::boxed::Box;
500 use euclid::{point3, vec3};
501
502 use crate::block::{self, Resolution::*, TickAction};
503 use crate::content::palette;
504 use crate::inv;
505 use crate::math::{Face6, GridRotation, Vol, ps32};
506 use crate::op::Operation;
507 use crate::sound;
508 use crate::space::SpacePhysics;
509 use crate::transaction::Transactional as _;
510
511 use super::*;
512
513 #[test]
514 fn defaults() {
515 let color = Rgba::new(0.1, 0.2, 0.3, 0.4);
516 assert_eq!(
517 Block::builder().color(color).build(),
518 Block::from(block::Atom {
519 color,
520 emission: Rgb::ZERO,
521 collision: BlockCollision::Hard,
522 }),
523 );
524 }
525
526 #[test]
527 fn default_equivalent() {
528 assert_eq!(
529 Builder::new(),
530 <Builder<'_, NeedsPrimitive, ()> as Default>::default()
531 );
532 }
533
534 #[test]
535 fn every_field_nondefault() {
536 let color = Rgba::new(0.1, 0.2, 0.3, 0.4);
537 let emission = Rgb::new(0.1, 3.0, 0.1);
538 let inventory = inv::InvInBlock::new(
539 9,
540 R4,
541 R16,
542 vec![
543 inv::IconRow::new(0..3, point3(1, 1, 1), vec3(5, 0, 0)),
544 inv::IconRow::new(3..6, point3(1, 1, 6), vec3(5, 0, 0)),
545 inv::IconRow::new(6..9, point3(1, 1, 11), vec3(5, 0, 0)),
546 ],
547 );
548 let ambient_sound = {
549 let mut s = sound::Ambient::SILENT;
550 s.noise_bands[sound::Band::MIN] = ps32(0.5);
551 s
552 };
553 let rotation_rule = block::RotationPlacementRule::Attach { by: Face6::NZ };
554 let placement_action = Some(block::PlacementAction {
555 operation: Operation::Become(block::from_color!(1.0, 0.0, 1.0)),
556 in_front: false,
557 });
558 let tick_action = Some(TickAction::from(Operation::Become(AIR)));
559 let activation_action = Some(Operation::Become(block::from_color!(1.0, 1.0, 1.0)));
560 assert_eq!(
561 Block::builder()
562 .color(color)
563 .display_name("hello world")
564 .inventory_config(inventory.clone())
565 .collision(BlockCollision::None)
566 .rotation_rule(rotation_rule)
567 .selectable(false)
568 .light_emission(emission)
569 .ambient_sound(ambient_sound.clone())
570 .placement_action(placement_action.clone())
571 .tick_action(tick_action.clone())
572 .activation_action(activation_action.clone())
573 .animation_hint(block::AnimationHint::replacement(
574 block::AnimationChange::Shape
575 ))
576 .modifier(Modifier::Rotate(Face6::PY.clockwise()))
577 .build(),
578 Block::from(block::Atom {
579 color,
580 emission,
581 collision: BlockCollision::None,
582 })
583 .with_modifier(BlockAttributes {
584 display_name: "hello world".into(),
585 selectable: false,
586 placement_action,
587 inventory,
588 ambient_sound,
589 rotation_rule,
590 tick_action,
591 activation_action,
592 animation_hint: block::AnimationHint::replacement(block::AnimationChange::Shape),
593 })
594 .with_modifier(Modifier::Rotate(Face6::PY.clockwise()))
595 );
596 }
597
598 #[test]
599 fn voxels_from_space() {
600 let mut universe = Universe::new();
601 let space_handle = universe.insert_anonymous(Space::empty_positive(1, 1, 1));
602
603 assert_eq!(
604 Block::builder()
605 .display_name("hello world")
606 .voxels_handle(R2, space_handle.clone())
607 .build(),
608 Block::from_primitive(Primitive::Recur {
609 offset: GridPoint::origin(),
610 resolution: R2, space: space_handle
612 })
613 .with_modifier(Modifier::Attributes(Arc::new(BlockAttributes {
614 display_name: "hello world".into(),
615 ..BlockAttributes::default()
616 }))),
617 );
618 }
619
620 #[test]
621 fn voxels_from_fn_basic() {
622 let mut universe = Universe::new();
623
624 let resolution = R4;
625 let expected_bounds = GridAab::for_block(resolution);
626 let atom = block::from_color!(palette::DIRT);
627 let block = Block::builder()
628 .display_name("hello world")
629 .voxels_fn(resolution, |_cube| &atom)
630 .unwrap()
631 .build_into(&mut universe);
632
633 let space_handle = if let Primitive::Recur { space, .. } = block.primitive() {
635 space.clone()
636 } else {
637 panic!("expected Recur, found {block:?}");
638 };
639
640 assert_eq!(
641 block,
642 Block::from_primitive(Primitive::Recur {
643 offset: GridPoint::origin(),
644 resolution,
645 space: space_handle.clone()
646 })
647 .with_modifier(BlockAttributes {
648 display_name: "hello world".into(),
649 ..BlockAttributes::default()
650 }),
651 );
652
653 let space = space_handle.read(universe.read_ticket()).unwrap();
655 assert_eq!(space.bounds(), expected_bounds);
656 assert_eq!(space.physics(), &SpacePhysics::DEFAULT_FOR_BLOCK);
657 assert_eq!(
658 space.extract(expected_bounds, |e| e.block_data().block()),
659 Vol::<Box<[&Block]>>::from_fn(expected_bounds, |_| &atom)
660 );
661 }
662
663 #[test]
665 fn voxels_from_fn_shrinkwrap() {
666 let mut universe = Universe::new();
667
668 let resolution = R4;
669 let expected_bounds = GridAab::from_lower_upper([0, 0, 0], [2, 4, 4]);
670 let atom = block::from_color!(palette::DIRT);
671 let block = Block::builder()
672 .display_name("hello world")
673 .voxels_fn(resolution, |cube| {
674 if expected_bounds.contains_cube(cube) {
675 &atom
676 } else {
677 &AIR
678 }
679 })
680 .unwrap()
681 .build_into(&mut universe);
682
683 let space_handle = if let Primitive::Recur { space, .. } = block.primitive() {
685 space.clone()
686 } else {
687 panic!("expected Recur, found {block:?}");
688 };
689
690 let space = space_handle.read(universe.read_ticket()).unwrap();
693 assert_eq!(space.bounds(), expected_bounds);
694 assert_eq!(space.physics(), &SpacePhysics::DEFAULT_FOR_BLOCK);
695 assert_eq!(
696 space.extract(expected_bounds, |e| e.block_data().block()),
697 Vol::<Box<[&Block]>>::from_fn(expected_bounds, |_| &atom)
698 );
699 }
700
701 #[test]
702 fn explicit_txn() {
703 let resolution = R8;
704 let mut universe = Universe::new();
705 let _block = universe
706 .transact(|txn, _| {
707 Ok(Block::builder()
708 .display_name("hello world")
709 .voxels_fn(resolution, |_cube| &AIR)
710 .unwrap()
711 .build_txn(txn))
712 })
713 .unwrap();
714
715 assert_eq!(universe.iter_by_type::<Space>().count(), 1);
716 }
717
718 #[test]
719 fn modifier_before_color_is_equivalent() {
720 assert_eq!(
721 Block::builder()
722 .modifier(Modifier::Rotate(GridRotation::RXYz))
723 .color(Rgba::WHITE),
724 Block::builder()
725 .color(Rgba::WHITE)
726 .modifier(Modifier::Rotate(GridRotation::RXYz))
727 );
728 }
729
730 #[test]
731 fn modifier_before_voxels_is_equivalent() {
732 let h = Handle::new_gone(Name::Pending);
733 assert_eq!(
734 Block::builder()
735 .modifier(Modifier::Rotate(GridRotation::RXYz))
736 .voxels_handle(R8, h.clone()),
737 Block::builder()
738 .voxels_handle(R8, h)
739 .modifier(Modifier::Rotate(GridRotation::RXYz))
740 );
741 }
742}