use concinnity_cook::authoring::registry::RegisteredType;
use concinnity_cook::authoring::world::find_entry;
use concinnity_core::components::{Camera3D, FrameInput, Transform};
use concinnity_core::ecs::PickIndex;
use concinnity_core::ecs::World;
use crate::editor::asset_handle::AssetHandle;
use crate::editor::hook::pick;
use crate::editor::hook::{EditorHook, entry_type};
use crate::editor::panels::content_panel;
use crate::editor::panels::registry::PanelKey;
use crate::editor::theme;
use crate::editor::viewport::billboards;
use crate::editor::viewport::snap;
use crate::editor::widget;
const DRAG_START_PX: f32 = 4.0;
const FREE_DROP_DISTANCE: f32 = 10.0;
const GHOST_HALF: [f32; 3] = [0.5, 0.5, 0.5];
pub(in crate::editor::hook) struct ContentDrag {
pub name: String,
pub asset_type: RegisteredType,
anchor: [f32; 2],
moved: bool,
pose: Option<DropPose>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(in crate::editor::hook) struct DropPose {
pub position: [f32; 3],
pub normal: [f32; 3],
pub rotation_deg: [f32; 3],
}
pub(in crate::editor::hook) fn align_rotation(axis: usize, sign: f32) -> [f32; 3] {
match (axis, sign > 0.0) {
(0, true) => [0.0, 0.0, -90.0],
(0, false) => [0.0, 0.0, 90.0],
(1, true) => [0.0, 0.0, 0.0],
(1, false) => [180.0, 0.0, 0.0],
(2, true) => [90.0, 0.0, 0.0],
_ => [-90.0, 0.0, 0.0],
}
}
pub(in crate::editor::hook) fn placement_args(
asset_type: RegisteredType,
name: &str,
pos: [f32; 3],
) -> Option<serde_json::Value> {
let field = match asset_type {
RegisteredType::Mesh | RegisteredType::ProceduralMesh => "mesh",
RegisteredType::Model => "model",
RegisteredType::Prefab => "prefab",
_ => return None,
};
Some(serde_json::json!({ field: name, "position": pos }))
}
pub(in crate::editor::hook) fn drag_has_effect(asset_type: RegisteredType) -> bool {
placement_args(asset_type, "", [0.0; 3]).is_some() || asset_type == RegisteredType::Material
}
impl EditorHook {
pub(in crate::editor::hook) fn arm_content_drag(
&mut self,
name: String,
asset_type: RegisteredType,
at: [f32; 2],
) {
if !drag_has_effect(asset_type) {
return;
}
self.content_drag = Some(ContentDrag {
name,
asset_type,
anchor: at,
moved: false,
pose: None,
});
}
pub(in crate::editor::hook) fn drive_content_drag(
&mut self,
input: &FrameInput,
vp: [f32; 2],
world: &mut World,
) {
if input.escape {
self.content_drag = None;
return;
}
let mouse = [input.mouse_x, input.mouse_y];
if input.left_button_down {
let over_panel = self.cursor_over_content_panel(mouse, vp);
let moved = match &mut self.content_drag {
Some(drag) => {
let (dx, dy) = (mouse[0] - drag.anchor[0], mouse[1] - drag.anchor[1]);
drag.moved |= (dx * dx + dy * dy).sqrt() >= DRAG_START_PX;
drag.moved
}
None => return,
};
let pose = (moved && !over_panel)
.then(|| self.drop_pose(world, vp, mouse, input.ctrl))
.flatten();
if let Some(drag) = &mut self.content_drag {
drag.pose = pose;
}
return;
}
let Some(drag) = self.content_drag.take() else {
return;
};
let Some(pose) = drag.pose else {
return;
};
if drag.asset_type == RegisteredType::Material {
self.assign_material_under_cursor(world, vp, mouse, &drag.name);
return;
}
let Some(mut args) = placement_args(
drag.asset_type,
&drag.name,
pose.position.map(super::gizmo::round3),
) else {
return;
};
if pose.rotation_deg != [0.0; 3]
&& let Some(obj) = args.as_object_mut()
{
obj.insert(
"rotation_deg".to_string(),
serde_json::json!(pose.rotation_deg),
);
}
let key = self
.entries
.push(serde_json::json!({ "type": "Prop", "args": args }));
self.mark_changed();
self.selection.replace(AssetHandle::Entry(key));
}
pub(in crate::editor::hook) fn drop_point(
&self,
world: &World,
vp: [f32; 2],
mouse: [f32; 2],
ctrl: bool,
) -> Option<[f32; 3]> {
self.drop_pose(world, vp, mouse, ctrl).map(|p| p.position)
}
fn drop_pose(
&self,
world: &World,
vp: [f32; 2],
mouse: [f32; 2],
ctrl: bool,
) -> Option<DropPose> {
let ray = pick::camera_ray(world, vp, mouse)?;
let hit = world
.resource::<PickIndex>()
.into_iter()
.flat_map(|index| index.entries.iter())
.filter_map(|e| concinnity_core::math::pick::ray_aabb_face(&ray, e.bb_min, e.bb_max))
.min_by(|a, b| a.t.total_cmp(&b.t));
let t = hit.map(|f| f.t).unwrap_or_else(|| {
if ray.dir[1] < -1e-4 && ray.origin[1] > 0.0 {
-ray.origin[1] / ray.dir[1]
} else {
FREE_DROP_DISTANCE
}
});
let mut position = [
ray.origin[0] + ray.dir[0] * t,
ray.origin[1] + ray.dir[1] * t,
ray.origin[2] + ray.dir[2] * t,
];
if let Some(step) = self.snap.translate.active_step(ctrl) {
position = position.map(|v| snap::snap_step(v, step));
}
let mut normal = [0.0, 1.0, 0.0];
let mut rotation_deg = [0.0; 3];
if let Some(face) = hit.filter(|f| f.sign != 0.0) {
normal = [0.0; 3];
normal[face.axis] = face.sign;
if self.align_to_surface {
rotation_deg = align_rotation(face.axis, face.sign);
}
}
Some(DropPose {
position,
normal,
rotation_deg,
})
}
fn assign_material_under_cursor(
&mut self,
world: &mut World,
vp: [f32; 2],
mouse: [f32; 2],
material: &str,
) {
let Some(ray) = pick::camera_ray(world, vp, mouse) else {
return;
};
let hit = self
.ray_hit_names(world, &ray)
.into_iter()
.find_map(|name| {
let idx = find_entry(&self.entries, &name)?;
(entry_type(&self.entries[idx]) == Some("Prop")).then_some(idx)
});
let Some(idx) = hit else {
return;
};
if let Some(args) = self.entries[idx]
.as_object_mut()
.and_then(|o| o.get_mut("args"))
.and_then(|a| a.as_object_mut())
{
args.insert(
"material".to_string(),
serde_json::Value::String(material.to_string()),
);
self.mark_changed();
}
}
fn ray_hit_names(
&self,
world: &World,
ray: &concinnity_core::math::pick::PickRay,
) -> Vec<String> {
let Some(index) = world.resource::<PickIndex>() else {
return Vec::new();
};
let mut hits: Vec<(f32, String)> = index
.entries
.iter()
.filter_map(|e| {
let t = concinnity_core::math::pick::ray_aabb(ray, e.bb_min, e.bb_max)?;
Some((t, pick::resolve_name(e.asset_id)?))
})
.collect();
hits.sort_by(|a, b| a.0.total_cmp(&b.0));
hits.into_iter().map(|(_, n)| n).collect()
}
fn cursor_over_content_panel(&self, mouse: [f32; 2], vp: [f32; 2]) -> bool {
let o = self.origin(PanelKey::Content, vp);
widget::point_in(mouse[0], mouse[1], content_panel::panel_rect(o))
}
pub(in crate::editor::hook) fn content_ghost_pose(&self) -> Option<DropPose> {
self.content_drag.as_ref().and_then(|d| d.pose)
}
pub(in crate::editor::hook) fn drive_content_ghost(&self, world: &mut World, vp: [f32; 2]) {
let Some(pose) = self.content_ghost_pose() else {
return;
};
let Some(cam) = world.query::<Camera3D>().next() else {
return;
};
let (view, fov) = (cam.view_matrix, cam.fov_y_degrees.to_radians());
let transform = Transform {
position: [
pose.position[0] + pose.normal[0] * GHOST_HALF[0],
pose.position[1] + pose.normal[1] * GHOST_HALF[1],
pose.position[2] + pose.normal[2] * GHOST_HALF[2],
],
rotation_deg: pose.rotation_deg,
scale: [1.0; 3],
};
let tint = theme::ACCENT_TINT;
if let Some(centers) =
billboards::box_outline(&view, fov, vp, &transform.model_matrix(), GHOST_HALF)
{
billboards::place_box_outline(world, ¢ers, tint);
}
}
}