use super::asset_tree::{self, TreeGroup, TreeRow};
use super::axes;
use super::behavior_panel::{self, BehaviorAction, BehaviorView, Status, ViewMode};
use super::character_shape_panel;
use super::console::{self, ConsoleSink};
use super::console_panel::{self, ConsoleAction, ConsoleView};
use super::content_panel;
use super::form::{self, FormField};
use super::form_panel::{self, FormAction, FormFocus, FormView};
use super::health::HealthState;
use super::health_panel;
use super::hud::{self, HudAction, HudState};
use super::import_panel::{self, ImportAction, ImportRow, ImportStatus, ImportView};
use super::lighting;
use super::lighting_panel::{self, LightingAction, LightingView};
use super::list_panel::Row;
use super::live;
use super::overrides;
use super::palette;
use super::palette_panel::{self, PaletteHit, PaletteView};
use super::panel::{self, PanelAction, PanelView};
use super::preview::{self, PreviewAction};
use super::registry::{self, PANEL_COUNT, PanelKey};
use super::snap;
use super::story;
use super::story_panel::{self, StoryAction, StoryView};
use super::template_panel::{self, TemplateAction, TemplateView};
use super::templates::{self, TemplatesAction};
use super::variables;
use super::variables_panel::{self, VariablesAction, VariablesView};
use super::view::{self, ViewAction};
use super::view_menu;
use super::widget::{self, point_in};
use super::asset_list;
use super::asset_list::ListRow;
use super::billboards;
use super::create_menu;
use super::cursor;
use super::framing;
use super::gizmo;
use super::group_transform;
use super::highlight;
use super::history::History;
use super::marquee;
use super::notify;
use super::orbit;
use super::outlines;
use super::resize;
use super::seeded_content;
use super::selection::Selection;
use super::session_store;
use super::sim;
use super::toast_overlay;
use super::visibility;
use crate::app::state::App;
use crate::components::FrameInput;
use crate::debug_hook::DebugHook;
use crate::ecs::asset_id::AssetId;
use crate::ecs::{CursorShape, DesiredCursor, HudLayers, MenuOverride, PendingBackend, World};
const TOP_BAR_LAYER: i32 = 1_000;
#[derive(Debug, Clone, Copy)]
struct Drag {
key: PanelKey,
grab: [f32; 2],
}
pub(crate) struct EditorHook {
world_path: String,
entries: Vec<serde_json::Value>,
history: History,
baseline: Vec<serde_json::Value>,
saved: Vec<serde_json::Value>,
dirty: bool,
sim: sim::SimControl,
hud_visible: bool,
axes_visible: bool,
rebuild_preview: bool,
rebuild_required: bool,
world_entries: Vec<serde_json::Value>,
world_shadows: Option<live::ShadowBaselines>,
templates_open: bool,
open_template: Option<usize>,
template_list_scroll: usize,
lighting_open: bool,
lighting_focus: Option<usize>,
lighting_status: Option<String>,
shape_open: bool,
shape_scroll: usize,
shape_rows: usize,
shape_status: Option<String>,
shape_seed: u64,
shape_drag: Option<shape_drag::ShapeDrag>,
story_open: bool,
story_lines: Vec<String>,
story_line: usize,
story_scroll: usize,
story_focus: bool,
story_path: String,
story_status: Option<String>,
story_blur: bool,
import_open: bool,
import_focus: bool,
import_scroll: usize,
import_status: Option<ImportStatus>,
console_open: bool,
console_focus: bool,
console_blur: bool,
console_scroll: usize,
console_pinned: bool,
console_sink: ConsoleSink,
console_build_running: std::sync::Arc<std::sync::atomic::AtomicBool>,
notifier: notify::Notifier,
toasts_hidden: bool,
behavior_open: bool,
behavior_index: usize,
behavior_row: Option<usize>,
behavior_scroll: usize,
behavior_picking: bool,
behavior_pick_scroll: usize,
behavior_pick: usize,
behavior_filter: String,
behavior_focus: bool,
behavior_name_focus: bool,
behavior_remove_armed: bool,
behavior_status: Option<Status>,
behavior_mode: ViewMode,
behavior_pan: [f32; 2],
behavior_pan_drag: Option<[f32; 2]>,
variables_open: bool,
variables_row: Option<usize>,
variables_scroll: usize,
variables_name_focus: bool,
variables_value_focus: bool,
behavior_clip: Option<crate::editor::behavior::clip::Clip>,
behavior_overview_card: Option<usize>,
behavior_pulses: Vec<crate::editor::behavior::pulse::NodePulse>,
behavior_breakpoints: Vec<(String, crate::editor::behavior::path::Path)>,
trace_seen: u64,
live_vars: Vec<(String, String, String)>,
live_locals: Vec<(String, String, String)>,
ctrl_held: bool,
hidden_assets: std::collections::BTreeSet<String>,
locked_assets: std::collections::BTreeSet<String>,
isolate: Option<std::collections::BTreeSet<String>>,
shift_held: bool,
viewport: [f32; 2],
panel_open: bool,
tree_groups: Vec<TreeGroup>,
tree_unfolded: Vec<usize>,
tree_scroll: usize,
tree_stale: bool,
tree_status: Option<String>,
search_focus: bool,
row_menu: Option<String>,
picker_open: bool,
picker_scroll: usize,
content_open: bool,
content_scroll: usize,
content_type: usize,
content_search_focus: bool,
content_drag: Option<content_drag::ContentDrag>,
create_menu: Option<create_menu_drive::CreateMenu>,
palette_open: bool,
palette_blur: bool,
palette_query: String,
palette_items: Vec<palette::PaletteItem>,
palette_matches: Vec<usize>,
palette_pick: usize,
palette_scroll: usize,
palette_recent: Vec<String>,
preview_open: bool,
health_open: bool,
health: HealthState,
view_open: bool,
display_menu_open: bool,
view_mode: view_menu::ViewMode,
show_flags: view_menu::ShowFlags,
show_billboards: bool,
extent_show: outlines::CategorySet,
selected_type: Option<String>,
form_target: FormTarget,
form_fields: Vec<FormField>,
form_scroll: usize,
form_focus: FormFocus,
form_error: Option<String>,
field_dropdown: Option<usize>,
field_dropdown_scroll: usize,
form_args: serde_json::Map<String, serde_json::Value>,
form_template: Option<FormTemplate>,
override_menu: Option<usize>,
entity_menu_open: bool,
template_index: Option<overrides::TemplateIndex>,
vec_expanded: std::collections::HashSet<String>,
selection: Selection,
pick_last: Option<pick::PickLast>,
marquee: Option<marquee_drag::MarqueeDrag>,
orbit: Option<orbit_drive::OrbitDrag>,
gizmo_mode: gizmo::GizmoMode,
gizmo_drag: Option<gizmo_drag::GizmoDrag>,
snap: snap::SnapSettings,
align_to_surface: bool,
fly: bool,
fly_clock: Option<std::time::Instant>,
glide: Option<glide_drive::CameraGlide>,
bookmarks: [Option<framing::CameraPose>; session_store::BOOKMARK_SLOTS],
positions: [Option<[f32; 2]>; PANEL_COUNT],
sizes: [Option<[f32; 2]>; PANEL_COUNT],
drag: Option<Drag>,
resize: Option<resize::Resize>,
panel_order: Vec<PanelKey>,
form_touched: bool,
lighting_touched: bool,
story_touched: bool,
rebuild_op: Option<notify::OpHandle>,
rebuild_countdown: u8,
last_rebuild_secs: f32,
}
#[derive(Debug, Clone)]
pub(crate) struct FormTemplate {
pub name: String,
pub baseline: serde_json::Map<String, serde_json::Value>,
pub generated_by: String,
}
struct PanelData {
rows: Vec<TreeRow>,
picker_options: Option<Vec<String>>,
form_title: String,
form_overrides: Option<FormOverridesData>,
}
struct FormOverridesData {
marks: Vec<overrides::FieldOrigin>,
count: usize,
field_menu: Option<(usize, Vec<String>)>,
entity_menu: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) enum FormTarget {
#[default]
New,
Entry(usize),
Promote(serde_json::Value),
}
impl FormTarget {
fn entry(&self) -> Option<usize> {
match self {
FormTarget::Entry(i) => Some(*i),
_ => None,
}
}
fn is_edit(&self) -> bool {
!matches!(self, FormTarget::New)
}
}
#[derive(Default)]
struct TemplateDetailData {
title: String,
description: String,
rows: Vec<ListRow>,
}
struct LightingData {
rows: Vec<lighting::Row>,
fields: Vec<Option<FormField>>,
}
fn scroll_step(cur: usize, delta: f32, max: usize) -> usize {
if delta > 0.0 {
(cur + 1).min(max)
} else {
cur.saturating_sub(1)
}
}
fn visible_slot(j: usize, scroll: usize, window: usize) -> Option<usize> {
(j >= scroll && j < scroll + window).then(|| j - scroll)
}
fn short_status(e: &str) -> String {
let line = e.lines().next().unwrap_or(e);
let clipped: String = line.chars().take(44).collect();
if clipped.len() < line.len() {
format!("{clipped}...")
} else {
clipped
}
}
fn entry_name(e: &serde_json::Value) -> Option<&str> {
e.get("name").and_then(|v| v.as_str())
}
fn entry_type(e: &serde_json::Value) -> Option<&str> {
e.get("type").and_then(|v| v.as_str())
}
fn names_of_type(entries: &[serde_json::Value], ty: &str) -> Vec<String> {
entries
.iter()
.filter(|e| entry_type(e) == Some(ty))
.filter_map(|e| entry_name(e).map(String::from))
.collect()
}
mod asset_tree_edit;
mod axes_drive;
mod behavior_asset;
mod behavior_edit;
mod behavior_keys;
mod billboard_drive;
mod bookmarks;
mod browse;
mod console_edit;
mod content_drag;
mod content_edit;
mod cook_worker;
mod create_menu_drive;
mod drop_floor;
mod duplicate;
mod editing;
mod edits;
mod export_edit;
mod fly;
mod gizmo_drag;
mod glide_drive;
mod hide;
mod import_edit;
mod layout;
mod marquee_drag;
mod orbit_drive;
mod outline_drive;
mod override_edit;
mod palette_edit;
mod pick;
mod lighting_edit;
mod character_shape_edit;
mod shape_drag;
mod notify_drive;
pub(super) mod panels;
mod routing;
mod select_edit;
mod sim_control;
mod trace_drive;
mod view_menu_drive;
mod story_edit;
#[cfg(test)]
mod camera_tests;
#[cfg(test)]
mod console_tests;
#[cfg(test)]
mod override_tests;
#[cfg(test)]
mod palette_tests;
#[cfg(test)]
mod panel_tests;
#[cfg(test)]
mod select_tests;
#[cfg(test)]
mod tests;
mod variables_edit;
impl EditorHook {
pub(crate) fn new(world_path: String, entries: Vec<serde_json::Value>) -> Self {
let bookmarks = session_store::default_path()
.and_then(|path| {
session_store::load(&path)
.worlds
.get(&session_store::world_key(&world_path))
.map(|w| w.bookmarks)
})
.unwrap_or_default();
Self {
world_path,
history: History::default(),
baseline: entries.clone(),
saved: entries.clone(),
world_entries: entries.clone(),
entries,
dirty: false,
sim: sim::SimControl::default(),
hud_visible: true,
axes_visible: true,
rebuild_preview: false,
rebuild_required: false,
world_shadows: None,
templates_open: false,
open_template: None,
template_list_scroll: 0,
lighting_open: false,
lighting_focus: None,
lighting_status: None,
shape_open: false,
shape_scroll: 0,
shape_rows: 0,
shape_status: None,
shape_seed: 0,
shape_drag: None,
story_open: false,
story_lines: vec![String::new()],
story_line: 0,
story_scroll: 0,
story_focus: false,
story_path: String::new(),
story_status: None,
story_blur: false,
import_open: false,
import_focus: false,
import_scroll: 0,
import_status: None,
console_open: false,
console_focus: false,
console_blur: false,
console_scroll: 0,
console_pinned: true,
console_sink: ConsoleSink::default(),
console_build_running: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
notifier: notify::Notifier::default(),
toasts_hidden: true,
behavior_open: false,
behavior_index: 0,
behavior_row: None,
behavior_scroll: 0,
behavior_picking: false,
behavior_pick_scroll: 0,
behavior_pick: 0,
behavior_filter: String::new(),
behavior_focus: false,
behavior_name_focus: false,
behavior_remove_armed: false,
behavior_status: None,
behavior_mode: ViewMode::default(),
behavior_pan: [0.0, 0.0],
behavior_pan_drag: None,
variables_open: false,
variables_row: None,
variables_scroll: 0,
variables_name_focus: false,
variables_value_focus: false,
behavior_clip: None,
behavior_overview_card: None,
behavior_pulses: Vec::new(),
behavior_breakpoints: Vec::new(),
trace_seen: 0,
live_vars: Vec::new(),
live_locals: Vec::new(),
ctrl_held: false,
hidden_assets: std::collections::BTreeSet::new(),
locked_assets: std::collections::BTreeSet::new(),
isolate: None,
shift_held: false,
viewport: [0.0, 0.0],
panel_open: false,
tree_groups: Vec::new(),
tree_unfolded: Vec::new(),
tree_scroll: 0,
tree_stale: true,
tree_status: None,
search_focus: false,
row_menu: None,
picker_open: false,
picker_scroll: 0,
content_open: false,
content_scroll: 0,
content_type: 0,
content_search_focus: false,
content_drag: None,
create_menu: None,
palette_open: false,
palette_blur: false,
palette_query: String::new(),
palette_items: Vec::new(),
palette_matches: Vec::new(),
palette_pick: 0,
palette_scroll: 0,
palette_recent: Vec::new(),
preview_open: true,
health_open: false,
health: HealthState::new(),
view_open: false,
display_menu_open: false,
view_mode: view_menu::ViewMode::default(),
show_flags: view_menu::ShowFlags::default(),
show_billboards: true,
extent_show: outlines::CategorySet::default(),
selected_type: None,
form_target: FormTarget::New,
form_fields: Vec::new(),
form_scroll: 0,
form_focus: FormFocus::Name,
form_error: None,
field_dropdown: None,
field_dropdown_scroll: 0,
form_args: serde_json::Map::new(),
form_template: None,
override_menu: None,
entity_menu_open: false,
template_index: None,
vec_expanded: std::collections::HashSet::new(),
selection: Selection::default(),
pick_last: None,
marquee: None,
orbit: None,
gizmo_mode: gizmo::GizmoMode::default(),
gizmo_drag: None,
snap: snap::SnapSettings::default(),
align_to_surface: false,
fly: false,
fly_clock: None,
glide: None,
bookmarks,
positions: [None; PANEL_COUNT],
sizes: [None; PANEL_COUNT],
drag: None,
resize: None,
panel_order: PanelKey::ALL.to_vec(),
form_touched: false,
lighting_touched: false,
story_touched: false,
rebuild_op: None,
rebuild_countdown: 0,
last_rebuild_secs: 0.0,
}
}
}
impl EditorHook {
pub(crate) fn with_console_sink(mut self, sink: ConsoleSink) -> Self {
self.console_sink = sink;
self
}
pub(crate) fn notifier(&self) -> notify::Notifier {
self.notifier.clone()
}
fn sample_behavior_filter(&mut self, world: &World) {
let typed = match self.behavior_picking {
true => widget::field_text(world, behavior_panel::FILTER_INPUT),
false => String::new(),
};
if typed != self.behavior_filter {
self.behavior_pick = 0;
self.behavior_pick_scroll = 0;
self.behavior_filter = typed;
}
}
fn frontmost_open_panel(&self) -> Option<PanelKey> {
self.panel_order
.iter()
.rev()
.copied()
.find(|&k| registry::panel(k).is_open(self))
}
fn text_focus_active(&self) -> bool {
self.non_console_text_focus() || self.console_focus
}
fn non_console_text_focus(&self) -> bool {
self.search_focus
|| self.content_search_focus
|| self.picker_open
|| self.selected_type.is_some()
|| self.lighting_focus.is_some()
|| self.story_focus
|| self.import_focus
|| self.behavior_focus
|| self.behavior_name_focus
|| self.behavior_picking
|| self.variables_name_focus
|| self.variables_value_focus
|| self.palette_open
}
}
impl DebugHook for EditorHook {
fn tick(&mut self, world: &mut World) {
self.refresh_tree_if_needed();
self.health.sample(world);
self.seed_billboard_transforms(world);
self.sample_behavior_filter(world);
self.sample_palette_query(world);
self.sample_shape_rows(world);
let input = world.query::<FrameInput>().last().cloned();
if let Some(input) = &input {
self.shift_held = input.shift;
self.ctrl_held = input.ctrl;
self.viewport = input.viewport;
if input.typed_char.is_some() {
match self.frontmost_open_panel() {
Some(PanelKey::Edit) => self.form_touched = true,
Some(PanelKey::Lighting) if self.lighting_focus.is_some() => {
self.lighting_touched = true;
}
Some(PanelKey::Story) if self.story_focus => self.story_touched = true,
_ => {}
}
}
if input.escape {
self.sim.pause();
self.fly = false;
self.fly_clock = None;
self.glide = None;
self.orbit = None;
self.create_menu = None;
self.display_menu_open = false;
self.close_palette();
}
if self.glide.is_some() {
self.drive_glide(input, world);
} else {
self.drive_fly(input, world);
}
if input.hud_toggle {
self.hud_visible = !self.hud_visible;
}
let vp = input.viewport;
if self.hud_visible && vp[0] > 0.0 {
self.drive_drag(input, vp);
self.drive_resize(input, vp);
self.drive_behavior_pan(input);
if self.gizmo_drag.is_some() {
self.drive_gizmo_drag(input, vp, world);
}
if self.shape_drag.is_some() {
self.drive_shape_drag(input, world);
}
if self.marquee.is_some() {
self.drive_marquee(input, vp, world);
}
if self.orbit.is_some() {
self.drive_orbit(input, world);
}
if self.content_drag.is_some() {
self.drive_content_drag(input, vp, world);
}
if input.left_click
&& self.drag.is_none()
&& self.resize.is_none()
&& self.gizmo_drag.is_none()
&& self.shape_drag.is_none()
&& self.marquee.is_none()
&& self.content_drag.is_none()
&& self.orbit.is_none()
{
let claimed = self.route_display_menu_click(input, vp)
|| self.route_create_menu_click(input, vp)
|| self.route_palette_dismiss(input, vp)
|| (input.alt && self.try_begin_orbit(input, vp, world));
if !claimed {
self.route_click(input, vp, world);
}
}
if input.right_click
&& self.drag.is_none()
&& self.resize.is_none()
&& self.gizmo_drag.is_none()
&& self.shape_drag.is_none()
&& self.marquee.is_none()
&& self.content_drag.is_none()
&& self.orbit.is_none()
{
self.open_create_menu(input, vp, world);
}
if !input.ctrl
&& !self.sim.playing()
&& !self.text_focus_active()
&& self.gizmo_drag.is_none()
{
match input.captured_key {
Some(crate::components::InputKey::T) => {
self.gizmo_mode = gizmo::GizmoMode::Translate;
}
Some(crate::components::InputKey::R) => {
self.gizmo_mode = gizmo::GizmoMode::Rotate
}
Some(crate::components::InputKey::S) => {
self.gizmo_mode = gizmo::GizmoMode::Scale
}
Some(crate::components::InputKey::F) => {
if input.shift {
self.toggle_fly();
} else {
self.frame_selection(input.viewport, world);
}
}
Some(crate::components::InputKey::H) => {
if input.shift {
self.toggle_isolate();
} else {
self.hide_selected();
}
}
Some(key) => {
if let Some(slot) = bookmarks::slot_for(key) {
self.recall_bookmark(slot, world);
}
}
None => {}
}
}
self.console_blur = false;
self.drive_console_toggle(input, world);
self.palette_blur = false;
self.drive_palette_toggle(input, world);
if input.ctrl
&& !self.sim.playing()
&& !self.text_focus_active()
&& self.gizmo_drag.is_none()
&& self.shape_drag.is_none()
{
match input.captured_key {
Some(crate::components::InputKey::Z) => self.undo(world),
Some(crate::components::InputKey::Y) => self.redo(world),
Some(crate::components::InputKey::D)
if self.frontmost_open_panel() != Some(PanelKey::Behavior) =>
{
self.duplicate_selection();
}
Some(crate::components::InputKey::Down) => {
self.drop_selection_to_floor(world);
}
Some(crate::components::InputKey::H) => self.unhide_all(),
Some(key) => {
if let Some(slot) = bookmarks::slot_for(key) {
self.save_bookmark(slot, world);
}
}
None => {}
}
}
self.sim_keys(input);
if let Some(key) = self.frontmost_open_panel() {
registry::panel(key).frame_keys(self, world, input);
}
if input.scroll_delta.abs() > 0.5 {
let (mx, my) = (input.mouse_x, input.mouse_y);
let form_shown = registry::panel(PanelKey::Edit).is_open(self);
if form_shown && self.field_dropdown.is_some() {
self.scroll_form(input.scroll_delta, world);
} else {
let front_to_back: Vec<PanelKey> =
self.panel_order.iter().rev().copied().collect();
for key in front_to_back {
let p = registry::panel(key);
if !p.is_open(self) {
continue;
}
let o = self.origin(key, vp);
if p.wheel_over(self, world, mx, my, o) {
p.scroll(self, world, input.scroll_delta);
break;
}
}
}
}
}
}
self.drive_sim(world);
self.drive_trace(world);
world.insert_resource(crate::ecs::FlyCam(self.fly && !self.sim.playing()));
world.insert_resource(crate::ecs::HiddenAssets(self.effective_hidden_ids()));
world.insert_resource(crate::ecs::ViewOverrides {
mode: self.view_mode,
show: self.show_flags,
});
hud::apply_layout(world, self.hud_state());
let vp = input.as_ref().map(|i| i.viewport).unwrap_or([0.0, 0.0]);
let mouse = input
.as_ref()
.map(|i| [i.mouse_x, i.mouse_y])
.unwrap_or([0.0, 0.0]);
let shown = self.hud_visible && vp[0] > 0.0;
let mut lines = world
.remove_resource::<crate::ecs::WorldLines>()
.map(|l| l.0)
.unwrap_or_default();
lines.clear();
if self.show_flags.contains(view_menu::ShowFlags::LINES) {
self.push_axis_lines(world, &mut lines);
self.push_extent_lines(world, vp, &mut lines);
}
world.insert_resource(crate::ecs::WorldLines(lines));
let owns_cursor = vp[0] > 0.0 && !self.sim.playing() && !self.fly;
cursor::set_visible(world, owns_cursor);
let shape = if !owns_cursor || !shown {
CursorShape::Default
} else if let Some(r) = &self.resize {
resize::cursor_shape(r.edges)
} else {
self.hover_cursor(vp, mouse)
};
world.insert_resource(DesiredCursor(shape));
if shown {
self.publish_layers(world);
} else {
world.insert_resource(HudLayers::default());
}
for p in registry::all() {
if shown && p.is_open(self) {
let o = self.origin(p.key(), vp);
p.draw(self, world, o, mouse);
} else {
p.hide(world);
}
}
self.drive_billboards(world, vp, shown && self.show_billboards);
self.drive_highlight(world, vp, shown);
self.drive_gizmo_draw(world, vp, shown);
self.drive_marquee_draw(world, shown);
self.drive_create_menu_draw(world, shown, mouse);
self.drive_display_menu_draw(world, vp, shown, mouse);
self.drive_toasts(world, vp, shown, mouse);
}
fn apply_world_swap(&mut self, app: &mut App) {
if !self.refresh_preview(app.world_mut()) {
return;
}
if self.last_rebuild_secs > SLOW_REBUILD_SECS {
if self.rebuild_op.is_none() {
self.rebuild_op = Some(self.notifier.begin_op("Rebuilding preview"));
self.rebuild_countdown = 2;
}
if self.rebuild_countdown > 0 {
self.rebuild_countdown -= 1;
return;
}
}
self.rebuild_preview = false;
self.rebuild_required = false;
let started = std::time::Instant::now();
self.swap_preview_world(app);
self.last_rebuild_secs = started.elapsed().as_secs_f32();
if let Some(op) = self.rebuild_op.take() {
op.finish();
}
}
}
const SLOW_REBUILD_SECS: f32 = 0.15;
impl EditorHook {
fn refresh_preview(&mut self, world: &mut World) -> bool {
if !self.rebuild_preview {
return false;
}
if self.try_live_apply(world) {
self.rebuild_preview = false;
return false;
}
self.sim.on_edit();
true
}
fn try_live_apply(&mut self, world: &mut World) -> bool {
if self.rebuild_required {
return false;
}
if !live::same_assets(&self.world_entries, &self.entries) || !self.seed_world_shadows() {
return false;
}
let shadows = self.world_shadows.as_ref().expect("seeded above");
let Some(changes) = live::args_changes(&self.world_entries, &self.entries, shadows) else {
return false;
};
let Some(plan) = live::plan(world, &self.entries, &changes) else {
return false;
};
tracing::debug!("editor: {} edit(s) applied live, no rebuild", plan.len());
live::commit(world, plan);
self.world_entries = self.entries.clone();
true
}
fn seed_world_shadows(&mut self) -> bool {
if self.world_shadows.is_some() {
return true;
}
let Ok(loaded) = Self::cook_entries(&self.world_entries) else {
return false;
};
self.world_shadows = Some(
loaded
.shadowed
.into_iter()
.map(|s| (s.name, s.args))
.collect(),
);
true
}
fn swap_preview_world(&mut self, app: &mut App) {
let (world, shadows) = match self.build_preview_world() {
Ok(built) => built,
Err(e) => {
tracing::error!("editor: live preview rebuild failed, keeping current world: {e}");
self.notifier.error_with(
&format!("Preview rebuild failed: {e}"),
notify::Action::OpenConsole,
);
return;
}
};
self.world_entries = self.entries.clone();
self.world_shadows = Some(shadows);
let mut staged = App::new();
staged.load_world(world);
let fields = Self::field_snapshot(app.world());
super::inject::editor_hud(staged.world_mut());
Self::restore_fields(staged.world_mut(), &fields);
staged
.world_mut()
.insert_resource(MenuOverride(Some(!self.sim.playing())));
let Some(backend) = concinnity_engine::ecs::take_render_backend(app.world_mut()) else {
return;
};
staged.world_mut().insert_resource(PendingBackend(backend));
let new_world = std::mem::replace(staged.world_mut(), World::new());
app.load_world(new_world);
if let Err(e) = app.start() {
tracing::error!("editor: live preview start failed: {e:?}");
self.notifier.error_with(
&format!("Preview start failed: {e:?}"),
notify::Action::OpenConsole,
);
}
}
}