use crate::editor::modal;
use concinnity_cook::authoring::registry::RegisteredType;
use concinnity_cook::authoring::world::write_world_jsonl;
use concinnity_core::components::Camera3D;
use concinnity_core::components::FrameInput;
use concinnity_core::components::InputKey;
use concinnity_core::components::KeyEvent;
use concinnity_core::components::TextInput;
use concinnity_core::components::Transform;
use concinnity_core::ecs::Entity;
use concinnity_core::ecs::PickEntry;
use concinnity_core::ecs::PickIndex;
use concinnity_core::ecs::World;
use concinnity_core::ecs::asset_id::AssetId;
use concinnity_host::thread::asset_id;
use crate::editor::behavior;
use crate::editor::behavior::panel::BehaviorAction;
use crate::editor::hook::{EditorHook, FormTarget, declared_id, entry_type};
use crate::frame_hook::FrameHook;
use crate::editor::panels::asset_tree::{self, TreeGroup};
use crate::editor::panels::form_panel;
use crate::editor::panels::assets_panel::{self, PanelAction};
use crate::editor::panels::registry::{self, PanelKey};
use crate::editor::viewport::gizmo;
use crate::editor::viewport::highlight;
use crate::editor::viewport::marquee;
use crate::editor::widget;
pub(in crate::editor::hook) fn hook(entries: Vec<serde_json::Value>) -> EditorHook {
EditorHook::new("unused.jsonl".to_string(), entries)
}
pub(in crate::editor::hook) fn select(h: &mut EditorHook, names: &[&str]) {
let handles = names.iter().map(|n| h.handle_for(n)).collect();
h.selection.set(handles);
}
pub(in crate::editor::hook) fn selected(h: &EditorHook) -> Vec<String> {
h.selection
.iter()
.filter_map(|m| h.handle_name(m))
.collect()
}
pub(in crate::editor::hook) fn active(h: &EditorHook) -> Option<String> {
h.selection.active().and_then(|m| h.handle_name(m))
}
pub(in crate::editor::hook) fn entry_target(h: &EditorHook, idx: usize) -> FormTarget {
FormTarget::Entry(h.entries.key_at(idx).expect("an entry at idx"))
}
pub(in crate::editor::hook) fn title_rect_of(
h: &EditorHook,
key: PanelKey,
vp: [f32; 2],
) -> [f32; 4] {
let o = h.origin(key, vp);
widget::title_rect(o, registry::panel(key).size(h)[0])
}
pub(in crate::editor::hook) fn close_rect_of(
h: &EditorHook,
key: PanelKey,
vp: [f32; 2],
) -> [f32; 4] {
widget::close_rect(title_rect_of(h, key, vp))
}
use crate::test_support::isolate_state_dir;
pub(in crate::editor::hook) fn world_with_input(input: FrameInput) -> World {
let mut world = World::new();
world.add_component(input);
world
}
pub(in crate::editor::hook) fn world_with_fields() -> World {
let mut world = World::new();
for id in assets_panel::ids()
.field_ids()
.chain(form_panel::ids().field_ids())
{
world.push_identified(id, TextInput::default());
}
world
}
pub(in crate::editor::hook) fn set_field(world: &mut World, id: AssetId, text: &str) {
if let Some(t) = world.get_mut_by_id::<TextInput>(id) {
t.content = text.to_string();
}
}
pub(in crate::editor::hook) fn entry(name: &str, ty: &str) -> serde_json::Value {
serde_json::json!({"type": ty, "args": {"$id": name}})
}
pub(in crate::editor::hook) fn entry_with_args(
name: &str,
ty: &str,
args: serde_json::Value,
) -> serde_json::Value {
serde_json::json!({"type": ty, "args": concinnity_cook::authoring::world::args_with_id(args, name)})
}
pub(in crate::editor::hook) fn seed_tree(h: &mut EditorHook, extra: Vec<TreeGroup>) {
let world_group = TreeGroup {
label: asset_tree::WORLD_GROUP.to_string(),
assets: h
.entries
.iter()
.map(|e| asset_tree::TreeAsset {
name: declared_id(e).unwrap_or_default().to_string(),
asset_type: entry_type(e)
.and_then(RegisteredType::parse)
.expect("a registered entry type"),
badge: asset_tree::Badge::Authored,
promote: None,
})
.collect(),
};
h.assets.groups = std::iter::once(world_group).chain(extra).collect();
h.assets.unfolded = (0..h.assets.groups.len()).collect();
h.assets.stale = false;
}
pub(in crate::editor::hook) fn generated_group(
label: &str,
assets: &[(&str, RegisteredType)],
) -> TreeGroup {
TreeGroup {
label: label.to_string(),
assets: assets
.iter()
.map(|(name, ty)| asset_tree::TreeAsset {
name: name.to_string(),
asset_type: *ty,
badge: asset_tree::Badge::Imported,
promote: Some(serde_json::json!({
"type": ty.as_str(), "args": {"$id": name},
})),
})
.collect(),
}
}
pub(in crate::editor::hook) fn row_of(h: &EditorHook, name: &str) -> (usize, usize) {
h.assets
.groups
.iter()
.enumerate()
.find_map(|(gi, g)| {
g.assets
.iter()
.position(|a| a.name == name)
.map(|ai| (gi, ai))
})
.unwrap_or_else(|| panic!("{name} is not in the seeded tree"))
}
pub(in crate::editor::hook) fn click_row(h: &mut EditorHook, name: &str, world: &mut World) {
let (g, i) = row_of(h, name);
h.apply_panel(PanelAction::SelectRow(g, i), world);
}
pub(in crate::editor::hook) fn set_input(world: &mut World, input: FrameInput) {
if let Some(i) = world.query_mut::<FrameInput>().last() {
*i = input;
} else {
world.add_component(input);
}
}
pub(in crate::editor::hook) fn pick_world(
cam_pos: [f32; 3],
picks: Vec<(AssetId, [f32; 3], [f32; 3])>,
) -> World {
let mut world = world_with_fields();
world.add_component(Camera3D {
position: cam_pos,
view_matrix: concinnity_core::gfx::camera::view_matrix(cam_pos, 0.0, 0.0),
fov_y_degrees: 90.0,
near: 0.05,
view_distance: None,
yaw: 0.0,
pitch: 0.0,
desired_move: [0.0; 3],
jump_requested: false,
interact_requested: false,
controller: None,
});
for (id, s) in highlight::outline_sprites() {
world.push_identified(id, s);
}
let (id, rect) = marquee::rect_sprite();
world.push_identified(id, rect);
world.insert_resource(PickIndex {
entries: picks
.into_iter()
.map(|(asset_id, bb_min, bb_max)| PickEntry {
asset_id,
bb_min,
bb_max,
})
.collect(),
});
world
}
pub(in crate::editor::hook) fn click_at(world: &mut World, h: &mut EditorHook, pos: [f32; 2]) {
click_at_mod(world, h, pos, false);
}
pub(in crate::editor::hook) fn click_at_mod(
world: &mut World,
h: &mut EditorHook,
pos: [f32; 2],
shift: bool,
) {
set_input(
world,
FrameInput {
viewport: [1280.0, 720.0],
mouse_x: pos[0],
mouse_y: pos[1],
left_click: true,
left_button_down: true,
shift,
..Default::default()
},
);
h.tick(world);
}
pub(in crate::editor::hook) fn release_at(world: &mut World, h: &mut EditorHook, pos: [f32; 2]) {
set_input(
world,
FrameInput {
viewport: [1280.0, 720.0],
mouse_x: pos[0],
mouse_y: pos[1],
..Default::default()
},
);
h.tick(world);
}
pub(in crate::editor::hook) fn drag_to(world: &mut World, h: &mut EditorHook, pos: [f32; 2]) {
set_input(
world,
FrameInput {
viewport: [1280.0, 720.0],
mouse_x: pos[0],
mouse_y: pos[1],
left_button_down: true,
..Default::default()
},
);
h.tick(world);
}
pub(in crate::editor::hook) fn drag_input(pos: [f32; 2], held: bool) -> FrameInput {
FrameInput {
viewport: [1280.0, 720.0],
mouse_x: pos[0],
mouse_y: pos[1],
left_button_down: held,
..Default::default()
}
}
pub(in crate::editor::hook) fn behavior(name: &str, args: serde_json::Value) -> serde_json::Value {
serde_json::json!({"type": "Behavior", "args": concinnity_cook::authoring::world::args_with_id(args, name)})
}
pub(in crate::editor::hook) fn behavior_session(
entries: Vec<serde_json::Value>,
) -> (EditorHook, World) {
let mut world = World::new();
for id in behavior::panel::ids().field_ids() {
world.push_identified(id, TextInput::default());
}
let mut h = hook(entries);
registry::panel(PanelKey::Behavior).toggle(&mut h, &mut world);
(h, world)
}
pub(in crate::editor::hook) fn select_behavior(h: &mut EditorHook, world: &mut World, label: &str) {
let i = behavior_row(h, label);
h.apply_behavior_action(BehaviorAction::Select(i), world, [0.0, 0.0]);
}
pub(in crate::editor::hook) fn open_args(h: &EditorHook) -> serde_json::Value {
h.behavior_args()
}
pub(in crate::editor::hook) fn press_remove(h: &mut EditorHook, world: &mut World) {
h.apply_behavior_action(BehaviorAction::Remove, world, [0.0, 0.0]);
}
pub(in crate::editor::hook) fn type_name(world: &mut World, text: &str) {
widget::seed_field(world, behavior::panel::NAME_INPUT, text);
}
pub(in crate::editor::hook) fn focus_story(h: &mut EditorHook) {
h.open[PanelKey::Story] = true;
h.story.focus = true;
h.focus_panel(crate::editor::panels::registry::PanelKey::Story);
}
pub(in crate::editor::hook) fn key_input(key: InputKey) -> FrameInput {
FrameInput {
key_events: vec![KeyEvent::press(key)],
viewport: [1280.0, 720.0],
..Default::default()
}
}
pub(in crate::editor::hook) fn two_prop_rig(
s1: [f32; 3],
s2: [f32; 3],
half: f32,
) -> (World, Entity, Entity, EditorHook) {
asset_id::reset_interner();
let a = asset_id::intern("box_a");
let b = asset_id::intern("box_b");
let bb = |s: [f32; 3]| {
(
[s[0] - half, s[1] - half, s[2] - half],
[s[0] + half, s[1] + half, s[2] + half],
)
};
let (min1, max1) = bb(s1);
let (min2, max2) = bb(s2);
let mut world = pick_world([0.0; 3], vec![(a, min1, max1), (b, min2, max2)]);
let transform = |p: [f32; 3]| Transform {
position: p,
rotation_deg: [0.0; 3],
scale: [1.0; 3],
};
let e1 = world.push(transform(s1));
let e2 = world.push(transform(s2));
world.identify(e1, a);
world.identify(e2, b);
for (id, s) in gizmo::sprites() {
world.push_identified(id, s);
}
let entry = |name: &str, p: [f32; 3]| serde_json::json!({ "type": "Prop", "args": { "$id": name, "position": p } });
let h = hook(vec![entry("box_a", s1), entry("box_b", s2)]);
(world, e1, e2, h)
}
pub(in crate::editor::hook) const SIDE_A: [f32; 3] = [-6.11, -3.3, -5.0];
pub(in crate::editor::hook) const SIDE_B: [f32; 3] = [-3.0, -3.3, -5.0];
pub(in crate::editor::hook) fn playing_hook(entries: Vec<serde_json::Value>) -> EditorHook {
let mut h = hook(entries);
h.sim.toggle_play();
h
}
pub(in crate::editor::hook) fn shape_world_entries() -> Vec<serde_json::Value> {
vec![
serde_json::json!({"type": "SkinnedMesh", "args": {
"$id": "body",
"morph_target_names": ["jaw+", "jaw-", "muscle"],
"skeleton": [{"name": "root", "parent": -1}, {"name": "thigh_l", "parent": 0}]
}}),
serde_json::json!({"type": "CharacterShape", "args": {
"$id": "body_shape",
"target": "body", "sliders": [{"name": "muscle", "value": 0.3}]
}}),
]
}
pub(in crate::editor::hook) fn expandable_hook() -> EditorHook {
isolate_state_dir();
let mut h = hook(vec![serde_json::json!({
"type": "GraphicsConfig", "args": {"$id": "gfx"}
})]);
h.open[PanelKey::Assets] = true;
h
}
pub(in crate::editor::hook) fn a_promotable_asset(h: &EditorHook) -> (usize, usize, String) {
h.assets
.groups
.iter()
.enumerate()
.find_map(|(gi, g)| {
g.assets
.iter()
.enumerate()
.find_map(|(ai, a)| a.promote.as_ref().map(|_| (gi, ai, a.name.clone())))
})
.expect("an injected default can be promoted")
}
pub(in crate::editor::hook) fn behavior_escape_input() -> FrameInput {
FrameInput {
escape: true,
viewport: [1280.0, 720.0],
..Default::default()
}
}
pub(in crate::editor::hook) fn press_behavior_key(
h: &mut EditorHook,
world: &mut World,
key: InputKey,
) {
h.behavior_keys(world, &key_input(key));
}
pub(in crate::editor::hook) fn behavior_row(h: &EditorHook, label: &str) -> usize {
h.behavior_rows()
.iter()
.position(|r| r.label == label)
.unwrap_or_else(|| panic!("no `{label}` row"))
}
pub(in crate::editor::hook) fn open_project(dir: &std::path::Path) {
crate::project::open(
concinnity_host::store::paths::StateTree::at(dir).with_cache(
concinnity_testing::shared_cache_dir("concinnity-dev-tests-cache"),
),
);
}
pub(in crate::editor::hook) fn prop_entry(name: &str) -> serde_json::Value {
serde_json::json!({"type": "Prop", "args": {"$id": name}})
}
pub(in crate::editor::hook) fn write_world(
dir: &std::path::Path,
name: &str,
entries: &[serde_json::Value],
at_secs: u64,
) -> std::path::PathBuf {
std::fs::create_dir_all(dir).unwrap();
let path = dir.join(format!("{name}.jsonl"));
std::fs::write(&path, write_world_jsonl(entries).unwrap()).unwrap();
let file = std::fs::File::options().write(true).open(&path).unwrap();
file.set_modified(std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(at_secs))
.unwrap();
path
}
pub(in crate::editor::hook) fn hook_at(
path: &std::path::Path,
entries: Vec<serde_json::Value>,
) -> EditorHook {
let mut h = EditorHook::new(path.to_string_lossy().into_owned(), entries);
h.refresh_worlds();
h
}
pub(in crate::editor::hook) fn world_with_name_field() -> World {
let mut world = World::new();
for id in modal::ids().field_ids() {
world.push_identified(id, TextInput::default());
}
world
}
pub(in crate::editor::hook) fn set_world_name(world: &mut World, text: &str) {
widget::seed_field(world, modal::NAME_INPUT, text);
}
pub(in crate::editor::hook) fn world_row_index(h: &EditorHook, name: &str) -> usize {
h.worlds
.rows
.iter()
.position(|r| r.name == name)
.unwrap_or_else(|| panic!("{name} is not listed"))
}
pub(in crate::editor::hook) fn world_names(h: &EditorHook) -> Vec<String> {
h.worlds.rows.iter().map(|r| r.name.clone()).collect()
}
pub(in crate::editor::hook) fn press_modal(h: &mut EditorHook, world: &mut World, label: &str) {
let i = button_index(h, label);
let state = h.modal.as_ref().unwrap();
let (count, c) = (state.buttons.len(), state.controls());
let r = modal::button_rect(modal::panel_rect(VP, c), count, i);
click_modal_at(h, world, [r[0] + 2.0, r[1] + 2.0]);
}
pub(in crate::editor::hook) fn press_modal_check(h: &mut EditorHook, world: &mut World) {
let c = h.modal.as_ref().expect("a dialog is open").controls();
let p = modal::panel_rect(VP, c);
let at = (0..p[3] as usize)
.map(|dy| [p[0] + 20.0, p[1] + dy as f32])
.find(|m| modal::hit_check(m[0], m[1], VP, c))
.expect("the dialog has a checkbox");
click_modal_at(h, world, at);
}
fn click_modal_at(h: &mut EditorHook, world: &mut World, at: [f32; 2]) {
let input = FrameInput {
left_click: true,
mouse_x: at[0],
mouse_y: at[1],
viewport: VP,
..Default::default()
};
assert!(h.route_modal_click(&input, VP, world));
}
pub(in crate::editor::hook) fn button_index(h: &EditorHook, label: &str) -> usize {
let buttons = &h.modal.as_ref().expect("a dialog is open").buttons;
buttons
.iter()
.position(|b| b.label == label)
.unwrap_or_else(|| panic!("no '{label}' button"))
}
pub(in crate::editor::hook) const VP: [f32; 2] = [1280.0, 720.0];