mittens-engine 0.7.0

A Vulkan and OpenXR scene engine with ECS, reactive signals, and Meow Meow scripting
use crate::engine::ecs::component::{
    OpacityComponent, SelectableComponent, SerializeComponent, TransformComponent,
};
use crate::engine::ecs::system::paint_placement::{PlacementPose, SurfacePlacementFrame};
use crate::engine::ecs::{ComponentId, IntentValue, SignalEmitter, World};

pub const PLACEMENT_PREVIEW_OPACITY: f32 = 0.45;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlacementKind {
    PaintAsset,
    Grid,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PlacementPreviewStyle {
    pub opacity: f32,
}

impl Default for PlacementPreviewStyle {
    fn default() -> Self {
        Self {
            opacity: PLACEMENT_PREVIEW_OPACITY,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PlacementPreviewSession {
    pub active_editor: ComponentId,
    pub placement_kind: PlacementKind,
    pub preview_root_component_id: ComponentId,
    pub target_renderable: Option<ComponentId>,
    pub last_valid_placement_frame: Option<SurfacePlacementFrame>,
    pub local_min_z: f32,
}

pub fn create_preview_shell(
    world: &mut World,
    preview_root: ComponentId,
    emit: &mut dyn SignalEmitter,
    style: PlacementPreviewStyle,
) {
    let selectable = world.add_component_boxed_named(
        "placement_preview_selectable",
        Box::new(SelectableComponent::off()),
    );
    let serialize = world.add_component_boxed_named(
        "placement_preview_serialize",
        Box::new(SerializeComponent::off()),
    );
    let opacity = world.add_component_boxed_named(
        "placement_preview_opacity",
        Box::new(OpacityComponent::new().with_opacity(style.opacity)),
    );
    let _ = world.add_child(preview_root, selectable);
    let _ = world.add_child(preview_root, serialize);
    let _ = world.add_child(preview_root, opacity);
    world.init_component_tree(preview_root, emit);
}

pub fn update_preview_pose(
    world: &mut World,
    emit: &mut dyn SignalEmitter,
    preview_root: ComponentId,
    pose: PlacementPose,
) {
    let Some(transform) = world.get_component_by_id_as_mut::<TransformComponent>(preview_root)
    else {
        return;
    };
    transform.transform.translation = pose.translation;
    transform.transform.rotation = pose.rotation;
    transform.transform.recompute_model();
    emit.push_intent_now(
        preview_root,
        IntentValue::UpdateTransform {
            component_id: preview_root,
            translation: pose.translation,
            rotation_quat_xyzw: pose.rotation,
            scale: transform.transform.scale,
        },
    );
}

pub fn commit_preview(world: &mut World, preview_root: ComponentId) {
    remove_preview_markers(world, preview_root);
}

pub fn cancel_preview(world: &mut World, preview_root: ComponentId) {
    let _ = world.remove_component_subtree(preview_root);
}

fn remove_preview_markers(world: &mut World, preview_root: ComponentId) {
    let mut stack = vec![preview_root];
    let mut preview_nodes = Vec::new();
    while let Some(node) = stack.pop() {
        if matches!(
            world.component_label(node),
            Some("placement_preview_selectable")
                | Some("placement_preview_serialize")
                | Some("placement_preview_world_panel")
                | Some("placement_preview_opacity")
        ) {
            preview_nodes.push(node);
            continue;
        }
        for &child in world.children_of(node) {
            stack.push(child);
        }
    }
    for child in preview_nodes {
        let _ = world.remove_component_leaf(child);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn commit_preview_removes_preview_markers_recursively() {
        let mut world = World::default();
        let preview_root = world.add_component(TransformComponent::new());
        let child = world.add_component(TransformComponent::new());
        let grandchild = world.add_component(TransformComponent::new());
        let opacity = world.add_component_boxed_named(
            "placement_preview_opacity",
            Box::new(OpacityComponent::new().with_opacity(PLACEMENT_PREVIEW_OPACITY)),
        );
        let selectable = world.add_component_boxed_named(
            "placement_preview_selectable",
            Box::new(SelectableComponent::off()),
        );

        let _ = world.add_child(preview_root, child);
        let _ = world.add_child(child, grandchild);
        let _ = world.add_child(grandchild, opacity);
        let _ = world.add_child(preview_root, selectable);

        commit_preview(&mut world, preview_root);

        assert!(world.get_component_record(opacity).is_none());
        assert!(world.get_component_record(selectable).is_none());
        assert!(world.get_component_record(child).is_some());
        assert!(world.get_component_record(grandchild).is_some());
    }
}