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 = App::new();
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(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 || concinnity_host::store::paths::data_dir().is_some_and(|d| d.join("0").exists());
183
184 if world_exists && !blobs_present() {
186 crate::build_world_to_disk(world_path)?;
187 }
188
189 if blobs_present() {
191 app.load_blob().map_err(|e| {
192 std::io::Error::new(
193 std::io::ErrorKind::InvalidData,
194 format!("failed to load compiled world data: {e:?}"),
195 )
196 })?;
197 match crate::authoring::name_table::prime_from_lock_file() {
202 Ok(n) if n > 0 => tracing::info!("editor: primed {n} asset names from the build lock"),
203 Ok(_) => {}
204 Err(e) => tracing::warn!("editor: could not prime asset names: {e}"),
205 }
206 match crate::authoring::reload_sources::install_from_lock(app.world_mut(), entries) {
211 Ok(n) if n > 0 => {
212 tracing::info!("editor: recovered {n} hot-reload source(s) from the build lock");
213 }
214 Ok(_) => {}
215 Err(e) => tracing::warn!("editor: could not recover hot-reload sources: {e}"),
216 }
217 }
218
219 if !concinnity_engine::ecs::renders(app.world()) {
222 let base = if world_exists {
223 std::fs::read_to_string(world_path)?
224 } else {
225 String::new()
226 };
227 let world = crate::build_world_from_str(&seeded_content(&base))?;
228 app.load_world(world);
229 }
230
231 Ok(())
232}
233
234fn seeded_content(base: &str) -> String {
238 if base.trim().is_empty() {
239 SEED_GRAPHICS_CONFIG.to_string()
240 } else {
241 format!("{base}\n{SEED_GRAPHICS_CONFIG}")
242 }
243}
244
245struct MultiHook {
248 hooks: Vec<Box<dyn DebugHook>>,
249}
250
251impl MultiHook {
252 fn boxed(hooks: Vec<Box<dyn DebugHook>>) -> Box<dyn DebugHook> {
253 Box::new(Self { hooks })
254 }
255}
256
257impl DebugHook for MultiHook {
258 fn tick(&mut self, world: &mut World) {
259 for hook in &mut self.hooks {
260 hook.tick(world);
261 }
262 }
263
264 fn apply_world_swap(&mut self, app: &mut crate::app::state::App) {
265 for hook in &mut self.hooks {
266 hook.apply_world_swap(app);
267 }
268 }
269
270 fn attach_shutdown(&mut self, shutdown: ShutdownToken) {
271 for hook in &mut self.hooks {
272 hook.attach_shutdown(shutdown.clone());
273 }
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
284 fn seeded_content_of_empty_is_the_render_marker() {
285 assert_eq!(seeded_content(""), SEED_GRAPHICS_CONFIG);
286 assert_eq!(seeded_content(" \n"), SEED_GRAPHICS_CONFIG);
287 }
288
289 #[test]
292 fn seeded_content_appends_marker_to_authored_content() {
293 let base = "{\"name\":\"phys\",\"type\":\"PhysicsConfig\",\"args\":{}}";
294 let seeded = seeded_content(base);
295 let parsed = crate::world::parse_world_jsonl(&seeded).unwrap();
296 assert_eq!(parsed.len(), 2, "authored entry plus the seed marker");
297 assert_eq!(parsed[0]["name"], "phys");
298 assert_eq!(parsed[1]["type"], "GraphicsConfig");
299 }
300
301 #[test]
303 fn seed_marker_is_a_graphics_config() {
304 let parsed = crate::world::parse_world_jsonl(SEED_GRAPHICS_CONFIG).unwrap();
305 assert_eq!(parsed.len(), 1);
306 assert_eq!(parsed[0]["type"], "GraphicsConfig");
307 }
308
309 #[test]
312 fn resolve_edit_target_honors_an_explicit_path() {
313 let (path, exists) = resolve_edit_target(Some("/no/such/cn-editor-world.jsonl"));
314 assert_eq!(path, "/no/such/cn-editor-world.jsonl");
315 assert!(
316 !exists,
317 "a missing explicit path is reported absent, not an error"
318 );
319 }
320}