use crate::editor::asset_handle::AssetHandle;
use crate::editor::entry_list::{EntryId, EntryList};
use crate::editor::hook::pick;
use crate::editor::hook::{EditorHook, declared_id, entry_type};
use crate::editor::panels::form;
use crate::editor::viewport::gizmo::{self, GizmoMode};
use crate::editor::viewport::group_transform;
use crate::editor::viewport::snap;
use concinnity_core::components::{Camera3D, FrameInput, GlobalTransform, Parent, Transform};
use concinnity_core::ecs::Entity;
use concinnity_core::ecs::World;
pub(in crate::editor::hook) fn round3(v: f32) -> f32 {
(v * 1000.0).round() / 1000.0
}
fn round1(v: f32) -> f32 {
(v * 10.0).round() / 10.0
}
const MIN_ROTATE_ARM_PX: f32 = 15.0;
const SCALE_FACTOR_RANGE: (f32, f32) = (0.01, 100.0);
pub(in crate::editor::hook) struct GizmoTarget {
pub(in crate::editor::hook) key: EntryId,
pub(in crate::editor::hook) entity: Entity,
pub(super) has_position: bool,
}
struct MemberStart {
handle: AssetHandle,
position: [f32; 3],
rotation: [f32; 3],
scale: [f32; 3],
has_position: bool,
}
pub(in crate::editor::hook) struct GizmoDrag {
axis: usize,
mode: GizmoMode,
pivot: [f32; 3],
starts: Vec<MemberStart>,
grab_t: f32,
last_angle: f32,
accum_deg: f32,
}
impl EditorHook {
pub(in crate::editor::hook) fn member_target(
&self,
world: &World,
mode: GizmoMode,
handle: &AssetHandle,
) -> Option<GizmoTarget> {
let idx = self.handle_index(handle)?;
let key = self.entries.key_at(idx)?;
let ty = entry_type(self.entries.get(idx)?)?;
let merged = form::working_args(ty, Some(&self.entry_args(idx)));
if !merged.get(mode.arg_key()).is_some_and(|p| p.is_array()) {
return None;
}
let has_position = merged.get("position").is_some_and(|p| p.is_array());
let id = self.handle_asset_id(handle)?;
let entity = world.entity_of(id)?;
if world.get::<Parent>(entity).is_some() {
return None;
}
world.get::<Transform>(entity)?;
Some(GizmoTarget {
key,
entity,
has_position,
})
}
fn gizmo_targets(&self, world: &World, mode: GizmoMode) -> Vec<GizmoTarget> {
self.selection
.iter()
.filter_map(|handle| self.member_target(world, mode, handle))
.collect()
}
fn gizmo_origin(world: &World, entity: Entity) -> Option<[f32; 3]> {
world
.get::<GlobalTransform>(entity)
.map(|g| [g.0[3][0], g.0[3][1], g.0[3][2]])
.or_else(|| world.get::<Transform>(entity).map(|t| t.position))
}
fn gizmo_anchor(&self, world: &World, mode: GizmoMode) -> Option<[f32; 3]> {
let origins: Vec<[f32; 3]> = self
.gizmo_targets(world, mode)
.iter()
.filter_map(|t| Self::gizmo_origin(world, t.entity))
.collect();
(!origins.is_empty()).then(|| group_transform::centroid(&origins))
}
pub(in crate::editor::hook) fn gizmo_layout(
&self,
world: &World,
vp: [f32; 2],
) -> Option<gizmo::Layout> {
let anchor = self.gizmo_anchor(world, self.gizmo_mode)?;
let cam = world.query::<Camera3D>().next()?;
gizmo::layout(&cam.view_matrix, cam.fov_y_degrees.to_radians(), vp, anchor)
}
pub(in crate::editor::hook) fn try_gizmo_press(
&mut self,
input: &FrameInput,
vp: [f32; 2],
world: &mut World,
) -> bool {
let mouse = [input.mouse_x, input.mouse_y];
let mode = self.gizmo_mode;
let Some(layout) = self.gizmo_layout(world, vp) else {
return false;
};
let Some(axis) = gizmo::hit_axis(&layout, mouse) else {
return false;
};
let Some(pivot) = self.gizmo_anchor(world, mode) else {
return false;
};
let starts: Vec<MemberStart> = self
.gizmo_targets(world, mode)
.into_iter()
.filter_map(|t| {
let tr = world.get::<Transform>(t.entity)?;
Some(MemberStart {
handle: AssetHandle::Entry(t.key),
position: tr.position,
rotation: tr.rotation_deg,
scale: tr.scale,
has_position: t.has_position,
})
})
.collect();
if starts.is_empty() {
return false;
}
let mut drag = GizmoDrag {
axis,
mode,
pivot,
starts,
grab_t: 0.0,
last_angle: 0.0,
accum_deg: 0.0,
};
match mode {
GizmoMode::Translate | GizmoMode::Scale => {
let Some(ray) = pick::camera_ray(world, vp, mouse) else {
return false;
};
let Some(t) = gizmo::axis_drag_t(pivot, gizmo::AXES[axis], &ray) else {
return false;
};
if mode == GizmoMode::Scale && t.abs() < 1e-4 {
return false;
}
drag.grab_t = t;
}
GizmoMode::Rotate => {
let (dx, dy) = (mouse[0] - layout.origin[0], mouse[1] - layout.origin[1]);
if (dx * dx + dy * dy).sqrt() < MIN_ROTATE_ARM_PX {
return false;
}
drag.last_angle = dy.atan2(dx);
}
}
self.gizmo_drag = Some(drag);
true
}
pub(in crate::editor::hook) fn drive_gizmo_drag(
&mut self,
input: &FrameInput,
vp: [f32; 2],
world: &mut World,
) {
if input.escape {
let Some(drag) = self.gizmo_drag.take() else {
return;
};
for s in &drag.starts {
if let Some(t) = self.member_target(world, drag.mode, &s.handle)
&& let Some(tr) = world.get_mut::<Transform>(t.entity)
{
tr.position = s.position;
tr.rotation_deg = s.rotation;
tr.scale = s.scale;
}
}
return;
}
if self.gizmo_drag.is_none() {
return;
}
if input.left_button_down {
self.follow_gizmo_drag(input, vp, world);
return;
}
self.commit_gizmo(world);
}
fn follow_gizmo_drag(&mut self, input: &FrameInput, vp: [f32; 2], world: &mut World) {
let mouse = [input.mouse_x, input.mouse_y];
let translate_snap = self.snap.translate.active_step(input.ctrl);
let rotate_snap = self.snap.rotate.active_step(input.ctrl);
let mode = match &self.gizmo_drag {
Some(d) => d.mode,
None => return,
};
let angle = match mode {
GizmoMode::Rotate => {
let Some(layout) = self.gizmo_layout(world, vp) else {
return;
};
let Some(drag) = &mut self.gizmo_drag else {
return;
};
let (dx, dy) = (mouse[0] - layout.origin[0], mouse[1] - layout.origin[1]);
let cur = dy.atan2(dx);
let step = gizmo::wrap_deg((cur - drag.last_angle).to_degrees());
drag.last_angle = cur;
drag.accum_deg += step;
let sign = {
let cam = world.query::<Camera3D>().next();
let fwd = cam
.map(|c| {
[
-c.view_matrix[0][2],
-c.view_matrix[1][2],
-c.view_matrix[2][2],
]
})
.unwrap_or([0.0, 0.0, -1.0]);
let a = gizmo::AXES[drag.axis];
let d = a[0] * fwd[0] + a[1] * fwd[1] + a[2] * fwd[2];
if d >= 0.0 { 1.0 } else { -1.0 }
};
let applied = sign * drag.accum_deg;
match rotate_snap {
Some(step) => snap::snap_step(applied, step),
None => applied,
}
}
_ => 0.0,
};
let Some(drag) = self.gizmo_drag.take() else {
return;
};
let axis_i = drag.axis;
let axis = gizmo::AXES[axis_i];
match mode {
GizmoMode::Translate | GizmoMode::Scale => {
let Some(ray) = pick::camera_ray(world, vp, mouse) else {
self.gizmo_drag = Some(drag);
return;
};
let Some(t) = gizmo::axis_drag_t(drag.pivot, axis, &ray) else {
self.gizmo_drag = Some(drag);
return;
};
let delta = match translate_snap {
Some(step) => snap::snap_step(t - drag.grab_t, step),
None => t - drag.grab_t,
};
for s in &drag.starts {
let Some(target) = self.member_target(world, mode, &s.handle) else {
continue;
};
let Some(tr) = world.get_mut::<Transform>(target.entity) else {
continue;
};
if mode == GizmoMode::Translate {
tr.position = group_transform::translate(s.position, axis, delta);
} else {
let factor =
(t / drag.grab_t).clamp(SCALE_FACTOR_RANGE.0, SCALE_FACTOR_RANGE.1);
let mut scale = s.scale;
scale[axis_i] = s.scale[axis_i] * factor;
tr.scale = scale;
if s.has_position {
tr.position =
group_transform::scale_from(s.position, drag.pivot, axis_i, factor);
}
}
}
}
GizmoMode::Rotate => {
for s in &drag.starts {
let Some(target) = self.member_target(world, mode, &s.handle) else {
continue;
};
let Some(tr) = world.get_mut::<Transform>(target.entity) else {
continue;
};
let mut rot = s.rotation;
rot[axis_i] = s.rotation[axis_i] + angle;
tr.rotation_deg = rot;
if s.has_position {
tr.position = group_transform::orbit(s.position, drag.pivot, axis_i, angle);
}
}
}
}
self.gizmo_drag = Some(drag);
}
fn commit_gizmo(&mut self, world: &mut World) {
let Some(drag) = self.gizmo_drag.take() else {
return;
};
let mut changed: Vec<EntryId> = Vec::new();
for s in &drag.starts {
let Some(target) = self.member_target(world, drag.mode, &s.handle) else {
continue;
};
let Some(tr) = world.get::<Transform>(target.entity).cloned() else {
continue;
};
let (value, start) = match drag.mode {
GizmoMode::Translate => (tr.position.map(round3), s.position.map(round3)),
GizmoMode::Rotate => (tr.rotation_deg.map(round1), s.rotation.map(round1)),
GizmoMode::Scale => (tr.scale.map(round3), s.scale.map(round3)),
};
let mut wrote = false;
if value != start {
Self::write_arg(&mut self.entries, target.key, drag.mode.arg_key(), value);
wrote = true;
}
if drag.mode != GizmoMode::Translate && s.has_position {
let pos = tr.position.map(round3);
if pos != s.position.map(round3) {
Self::write_arg(&mut self.entries, target.key, "position", pos);
wrote = true;
}
}
if wrote {
changed.push(target.key);
}
}
if changed.is_empty() {
return;
}
self.mark_changed();
if let Some(key) = self.form.target.entry()
&& changed.contains(&key)
&& let Some(name) = self
.entries
.by_key(key)
.and_then(declared_id)
.map(String::from)
{
self.open_asset_form(&name, world);
}
}
pub(in crate::editor::hook) fn write_arg(
entries: &mut EntryList,
entry: EntryId,
arg: &str,
value: [f32; 3],
) {
if let Some(obj) = entries.by_key_mut(entry).and_then(|e| e.as_object_mut()) {
let args = obj
.entry("args".to_string())
.or_insert_with(|| serde_json::Value::Object(Default::default()));
if let Some(a) = args.as_object_mut() {
a.insert(arg.to_string(), serde_json::json!(value));
}
}
}
pub(in crate::editor::hook) fn drive_gizmo_draw(
&self,
world: &mut World,
vp: [f32; 2],
shown: bool,
) {
let layout = if shown && !self.sim.playing() {
self.gizmo_layout(world, vp)
} else {
None
};
match layout {
Some(l) => gizmo::place(world, &l, self.gizmo_mode),
None => gizmo::hide(world),
}
}
}