concinnity_dev/editor/
mod.rs1mod asset_list;
10mod asset_tree;
11mod axes;
12mod behavior;
13mod behavior_chart;
14mod behavior_panel;
15mod billboards;
16mod character_shape;
17mod character_shape_panel;
18mod console;
19mod console_panel;
20mod content_panel;
21mod create_menu;
22mod cursor;
23mod file_dialog;
24mod filter;
25mod form;
26mod form_panel;
27mod framing;
28mod gizmo;
29mod gltf_export;
30mod group_transform;
31mod health;
32mod health_panel;
33mod highlight;
34mod history;
35mod hook;
36mod hud;
37mod import_panel;
38mod inject;
39mod lighting;
40mod lighting_panel;
41mod list_panel;
42mod live;
43mod marquee;
44pub(crate) mod notify;
45mod orbit;
46mod outlines;
47mod overrides;
48mod palette;
49mod palette_panel;
50mod panel;
51mod preview;
52mod registry;
53mod resize;
54mod select_related;
55mod selection;
56mod session_store;
57mod sim;
58mod snap;
59mod story;
60mod story_panel;
61mod template_panel;
62mod templates;
63mod theme;
64mod thumbs;
65mod toast_overlay;
66mod variables;
67mod variables_panel;
68mod view;
69mod view_menu;
70mod visibility;
71mod widget;
72mod widget_slider;
73
74use crate::app::state::App;
75use crate::debug_hook::DebugHook;
76use crate::ecs::World;
77use crate::world::{WORLD_JSONL, find_world_jsonl};
78use concinnity_engine::shutdown::ShutdownToken;
79use hook::EditorHook;
80
81const SEED_GRAPHICS_CONFIG: &str =
89 "{\"name\":\"editor_default_gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}";
90
91pub fn run_editor(json_path: Option<&str>, debug_port: Option<u16>) -> std::io::Result<()> {
96 let console_sink = console::ConsoleSink::default();
100 console::install_tracing(console_sink.clone());
101
102 let (world_path, world_exists) = resolve_edit_target(json_path);
106
107 concinnity_engine::app::dev_flags::set_world_jsonl_path(Some(world_path.clone()));
111
112 let entries = if world_exists {
115 let content = std::fs::read_to_string(&world_path)?;
116 crate::world::parse_world_jsonl(&content)
117 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?
118 } else {
119 Vec::new()
120 };
121
122 let mut app = crate::project::app();
125 boot_world(&mut app, &world_path, world_exists, &entries)?;
126
127 inject::editor_hud(app.world_mut());
131
132 let editor_hook = EditorHook::new(world_path, entries).with_console_sink(console_sink);
138 let hook: Box<dyn DebugHook> = match debug_port {
139 Some(port) => {
140 let server =
141 crate::debug::DebugServer::start(port)?.with_notifier(editor_hook.notifier());
142 MultiHook::boxed(vec![Box::new(editor_hook), Box::new(server)])
143 }
144 None => {
145 let reload = crate::debug::hot_reload::HotReloadDriver::new()
146 .with_notifier(editor_hook.notifier());
147 MultiHook::boxed(vec![Box::new(editor_hook), Box::new(reload)])
148 }
149 };
150
151 crate::run::start_app(app, Some(hook))
152}
153
154fn resolve_edit_target(json_path: Option<&str>) -> (String, bool) {
159 match json_path {
160 Some(p) => (p.to_string(), std::path::Path::new(p).exists()),
161 None => match find_world_jsonl(crate::project::worlds_dir().as_deref(), None) {
162 Ok(p) => (p, true),
163 Err(_) => (WORLD_JSONL.to_string(), false),
164 },
165 }
166}
167
168fn boot_world(
176 app: &mut App,
177 world_path: &str,
178 world_exists: bool,
179 entries: &[serde_json::Value],
180) -> std::io::Result<()> {
181 let blobs_present = || {
182 crate::project::data_dir()
183 .is_some_and(|d| concinnity_host::store::blob::primary_in(&d).exists())
184 };
185
186 if world_exists && !blobs_present() {
188 crate::build_world_to_disk(world_path)?;
189 }
190
191 if blobs_present() {
193 app.load_blob().map_err(|e| {
194 std::io::Error::new(
195 std::io::ErrorKind::InvalidData,
196 format!("failed to load compiled world data: {e:?}"),
197 )
198 })?;
199 match crate::authoring::name_table::prime_from_lock_file() {
204 Ok(n) if n > 0 => tracing::info!("editor: primed {n} asset names from the build lock"),
205 Ok(_) => {}
206 Err(e) => tracing::warn!("editor: could not prime asset names: {e}"),
207 }
208 match crate::authoring::reload_sources::install_from_lock(app.world_mut(), entries) {
213 Ok(n) if n > 0 => {
214 tracing::info!("editor: recovered {n} hot-reload source(s) from the build lock");
215 }
216 Ok(_) => {}
217 Err(e) => tracing::warn!("editor: could not recover hot-reload sources: {e}"),
218 }
219 }
220
221 if !concinnity_engine::ecs::renders(app.world()) {
224 let base = if world_exists {
225 std::fs::read_to_string(world_path)?
226 } else {
227 String::new()
228 };
229 let world = crate::build_world_from_str(&seeded_content(&base))?;
230 app.load_world(world);
231 }
232
233 Ok(())
234}
235
236fn seeded_content(base: &str) -> String {
240 if base.trim().is_empty() {
241 SEED_GRAPHICS_CONFIG.to_string()
242 } else {
243 format!("{base}\n{SEED_GRAPHICS_CONFIG}")
244 }
245}
246
247struct MultiHook {
250 hooks: Vec<Box<dyn DebugHook>>,
251}
252
253impl MultiHook {
254 fn boxed(hooks: Vec<Box<dyn DebugHook>>) -> Box<dyn DebugHook> {
255 Box::new(Self { hooks })
256 }
257}
258
259impl DebugHook for MultiHook {
260 fn tick(&mut self, world: &mut World) {
261 for hook in &mut self.hooks {
262 hook.tick(world);
263 }
264 }
265
266 fn apply_world_swap(&mut self, app: &mut crate::app::state::App) {
267 for hook in &mut self.hooks {
268 hook.apply_world_swap(app);
269 }
270 }
271
272 fn attach_shutdown(&mut self, shutdown: ShutdownToken) {
273 for hook in &mut self.hooks {
274 hook.attach_shutdown(shutdown.clone());
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
286 fn seeded_content_of_empty_is_the_render_marker() {
287 assert_eq!(seeded_content(""), SEED_GRAPHICS_CONFIG);
288 assert_eq!(seeded_content(" \n"), SEED_GRAPHICS_CONFIG);
289 }
290
291 #[test]
294 fn seeded_content_appends_marker_to_authored_content() {
295 let base = "{\"name\":\"phys\",\"type\":\"PhysicsConfig\",\"args\":{}}";
296 let seeded = seeded_content(base);
297 let parsed = crate::world::parse_world_jsonl(&seeded).unwrap();
298 assert_eq!(parsed.len(), 2, "authored entry plus the seed marker");
299 assert_eq!(parsed[0]["name"], "phys");
300 assert_eq!(parsed[1]["type"], "GraphicsConfig");
301 }
302
303 #[test]
305 fn seed_marker_is_a_graphics_config() {
306 let parsed = crate::world::parse_world_jsonl(SEED_GRAPHICS_CONFIG).unwrap();
307 assert_eq!(parsed.len(), 1);
308 assert_eq!(parsed[0]["type"], "GraphicsConfig");
309 }
310
311 #[test]
314 fn resolve_edit_target_honors_an_explicit_path() {
315 let (path, exists) = resolve_edit_target(Some("/no/such/cn-editor-world.jsonl"));
316 assert_eq!(path, "/no/such/cn-editor-world.jsonl");
317 assert!(
318 !exists,
319 "a missing explicit path is reported absent, not an error"
320 );
321 }
322}