From 71dc9ade286785e57498153ed93552472072a688 Mon Sep 17 00:00:00 2001 From: bigfoodK <38313680+bigfoodK@users.noreply.github.com> Date: Sat, 28 Sep 2024 15:39:37 +0900 Subject: [PATCH] Impl scene sprite editor (#963) --- .../new-client/src/asset_manage_page.rs | 12 +- .../new-client/src/episode_editor/mod.rs | 102 ++++++--- .../src/episode_editor/properties_panel.rs | 110 ++++++++++ .../src/episode_editor/scene_list.rs | 53 ++++- .../src/episode_editor/scene_preview.rs | 2 +- .../episode_editor/scene_sprite_editor/mod.rs | 172 +++++++++++++-- .../scene_sprite_editor/position_tool.rs | 40 ++-- .../scene_sprite_editor/scene_sprite_list.rs | 58 ++++-- .../scene_sprite_editor/size_tool.rs | 5 +- .../scene_sprite_editor/sprite_select_tool.rs | 195 +++++++++-------- luda-editor/new-client/src/home.rs | 69 +++++- luda-editor/new-client/src/lib.rs | 3 +- .../new-client/src/new_episode_page.rs | 197 ++++++++++++++++++ .../new-client/src/psd_sprite_util/mod.rs | 5 + .../psd_sprite_storage.rs} | 54 ++--- .../src/psd_sprite_util/render_psd_sprite.rs | 30 +++ luda-editor/new-client/src/router.rs | 41 +++- luda-editor/new-server/rpc/src/types/mod.rs | 4 +- luda-editor/psd-sprite/src/psd_sprite.rs | 47 +++++ 19 files changed, 952 insertions(+), 247 deletions(-) create mode 100644 luda-editor/new-client/src/episode_editor/properties_panel.rs create mode 100644 luda-editor/new-client/src/new_episode_page.rs create mode 100644 luda-editor/new-client/src/psd_sprite_util/mod.rs rename luda-editor/new-client/src/{render_psd_sprite.rs => psd_sprite_util/psd_sprite_storage.rs} (52%) create mode 100644 luda-editor/new-client/src/psd_sprite_util/render_psd_sprite.rs diff --git a/luda-editor/new-client/src/asset_manage_page.rs b/luda-editor/new-client/src/asset_manage_page.rs index 2dda73d3c..66c427ce1 100644 --- a/luda-editor/new-client/src/asset_manage_page.rs +++ b/luda-editor/new-client/src/asset_manage_page.rs @@ -1,5 +1,5 @@ use crate::{server_connection, simple_button, toast}; -use luda_rpc::{asset::reserve_team_asset_upload, AssetKind}; +use luda_rpc::{asset::reserve_team_asset_upload, AssetKind, AssetTag}; use namui::*; use namui_prebuilt::table::*; use network::http; @@ -35,6 +35,9 @@ impl Component for AssetManagePage<'_> { asset_name: &name, byte_size: encoded_bytes.len() as u64, asset_kind: &AssetKind::Sprite, + tags: &vec![AssetTag::System { + tag: luda_rpc::AssetSystemTag::SpriteCharacter, + }], }) .await { @@ -116,11 +119,14 @@ async fn upload_asset( headers: Vec<(String, String)>, bytes: Vec, ) -> Result<()> { - let mut builder = http::Request::post(presigned_put_uri); + let mut builder = http::Request::put(presigned_put_uri); for (key, value) in headers { builder = builder.header(key, value); } let response = builder.body(bytes)?.send().await?; - response.ensure_status_code()?; + let status = response.ensure_status_code()?.status(); + if !status.is_success() { + return Err(anyhow!("status code: {}", status)); + } Ok(()) } diff --git a/luda-editor/new-client/src/episode_editor/mod.rs b/luda-editor/new-client/src/episode_editor/mod.rs index 1870b6955..aaba3d238 100644 --- a/luda-editor/new-client/src/episode_editor/mod.rs +++ b/luda-editor/new-client/src/episode_editor/mod.rs @@ -1,3 +1,4 @@ +mod properties_panel; mod scene_list; mod scene_preview; mod scene_sprite_editor; @@ -5,10 +6,14 @@ mod speaker_selector; mod text_editor; use super::*; -use luda_rpc::{EpisodeEditAction, Scene}; +use crate::rpc::asset::get_team_asset_docs; +use crate::rpc::episode_editor::join_episode_editor; +use luda_rpc::{AssetDoc, EpisodeEditAction, Scene}; +use properties_panel::PropertiesPanel; use std::{collections::HashMap, sync::Arc}; pub struct EpisodeEditor<'a> { + pub team_id: &'a String, pub project_id: &'a String, pub episode_id: &'a String, } @@ -16,47 +21,66 @@ pub struct EpisodeEditor<'a> { impl Component for EpisodeEditor<'_> { fn render(self, ctx: &RenderCtx) { let Self { + team_id, project_id, episode_id, } = self; let wh = namui::screen::size().map(|x| x.into_px()); - { - use crate::rpc::episode_editor::join_episode_editor::*; - let result = join_episode_editor( - ctx, - |episode_id| Some((RefRequest { episode_id }, ())), - episode_id, - ); + let join_result = join_episode_editor::join_episode_editor( + ctx, + |episode_id| Some((join_episode_editor::RefRequest { episode_id }, ())), + episode_id, + ); + let asset_result = get_team_asset_docs::get_team_asset_docs( + ctx, + |team_id| Some((get_team_asset_docs::RefRequest { team_id }, ())), + team_id, + ); + let asset_docs = ctx.memo({ + || { + let Some(Ok((get_team_asset_docs::Response { asset_docs }, _))) = + asset_result.as_ref() + else { + return HashMap::new(); + }; + asset_docs + .iter() + .map(|asset_doc| (asset_doc.name.clone(), asset_doc.clone())) + .collect() + } + }); - let Some(result) = result.as_ref() else { + let (Some(join_result), Some(asset_result)) = (join_result.as_ref(), asset_result.as_ref()) + else { + ctx.add(typography::center_text( + wh, + "로딩중...", + Color::RED, + 16.int_px(), + )); + return; + }; + + match (join_result, asset_result) { + (Ok((join_episode_editor::Response { scenes, texts }, _)), Ok(_)) => { + ctx.add(LoadedEpisodeEditor { + project_id, + episode_id, + initial_scenes: scenes, + initial_texts: texts, + asset_docs, + }); + } + (join_result, asset_result) => { + let errors = (join_result.as_ref().err(), asset_result.as_ref().err()); ctx.add(typography::center_text( wh, - "로딩중...", + format!("에러: {:#?}", errors), Color::RED, 16.int_px(), )); - return; - }; - - match result { - Ok((Response { scenes, texts }, _)) => { - ctx.add(LoadedEpisodeEditor { - project_id, - episode_id, - initial_scenes: scenes, - initial_texts: texts, - }); - } - Err(err) => { - ctx.add(typography::center_text( - wh, - format!("에러: {:?}", err), - Color::RED, - 16.int_px(), - )); - } } } } @@ -67,6 +91,7 @@ struct LoadedEpisodeEditor<'a> { episode_id: &'a String, initial_scenes: &'a Vec, initial_texts: &'a HashMap>, + asset_docs: Sig<'a, HashMap>, } impl Component for LoadedEpisodeEditor<'_> { @@ -76,6 +101,7 @@ impl Component for LoadedEpisodeEditor<'_> { episode_id, initial_scenes, initial_texts, + asset_docs, } = self; let (scenes, set_scenes) = ctx.state(|| initial_scenes.clone()); let (texts, set_texts) = ctx.state(|| initial_texts.clone()); @@ -251,12 +277,18 @@ impl Component for LoadedEpisodeEditor<'_> { }); }; + let select_scene = &|scene_id: &str| { + set_selected_scene_id.set(Some(scene_id.to_string())); + }; + let wh = namui::screen::size().map(|x| x.into_px()); let scene_list = table::fixed(160.px(), |wh, ctx| { ctx.add(scene_list::SceneList { wh, scenes: &scenes, + select_scene, + add_new_scene: &add_new_scene, }); }); let scene_editor = table::ratio(1, |wh, ctx| { @@ -291,7 +323,15 @@ impl Component for LoadedEpisodeEditor<'_> { ])(wh, ctx); }); }); - let properties_panel = table::ratio(1, |wh, ctx| {}); + let properties_panel = table::ratio(1, |wh, ctx| { + let Some(scene) = scene else { return }; + ctx.add(PropertiesPanel { + wh, + scene, + edit_episode: &edit_episode, + asset_docs, + }); + }); ctx.compose(|ctx| horizontal([scene_list, scene_editor, properties_panel])(wh, ctx)); } diff --git a/luda-editor/new-client/src/episode_editor/properties_panel.rs b/luda-editor/new-client/src/episode_editor/properties_panel.rs new file mode 100644 index 000000000..559805330 --- /dev/null +++ b/luda-editor/new-client/src/episode_editor/properties_panel.rs @@ -0,0 +1,110 @@ +use super::scene_sprite_editor::SceneSpriteEditor; +use luda_rpc::{AssetDoc, EpisodeEditAction, Scene}; +use namui::*; +use namui_prebuilt::{button, table::*}; +use std::collections::HashMap; + +static PROPERTIES_PANEL_TAB_ATOM: Atom = Atom::uninitialized(); + +pub struct PropertiesPanel<'a> { + pub wh: Wh, + pub scene: &'a Scene, + pub edit_episode: &'a dyn Fn(EpisodeEditAction), + pub asset_docs: Sig<'a, HashMap>, +} +impl Component for PropertiesPanel<'_> { + fn render(self, ctx: &RenderCtx) { + let Self { + wh, + scene, + asset_docs, + edit_episode, + } = self; + + let (properties_panel_tab, set_properties_panel_tab) = + ctx.init_atom(&PROPERTIES_PANEL_TAB_ATOM, || PropertiesPanelTab::Standing); + + let update_scene = &|scene: Scene| { + edit_episode(EpisodeEditAction::UpdateScene { scene }); + }; + + ctx.compose(|ctx| { + vertical([ + fixed( + 48.px(), + horizontal([ + render_tab_button( + "스탠딩", + matches!(*properties_panel_tab, PropertiesPanelTab::Standing), + || { + set_properties_panel_tab.set(PropertiesPanelTab::Standing); + }, + ), + render_tab_button( + "배경", + matches!(*properties_panel_tab, PropertiesPanelTab::Background), + || { + set_properties_panel_tab.set(PropertiesPanelTab::Background); + }, + ), + render_tab_button( + "오디오", + matches!(*properties_panel_tab, PropertiesPanelTab::Audio), + || { + set_properties_panel_tab.set(PropertiesPanelTab::Audio); + }, + ), + ]), + ), + ratio(1, |wh, ctx| match properties_panel_tab.as_ref() { + PropertiesPanelTab::Standing => { + ctx.add(SceneSpriteEditor { + wh, + scene, + update_scene, + asset_docs, + }); + } + PropertiesPanelTab::Background => {} + PropertiesPanelTab::Audio => {} + }), + ])(wh, ctx); + }); + } +} + +pub enum PropertiesPanelTab { + Standing, + Background, + Audio, +} + +fn render_tab_button<'a>( + text: &'a str, + selected: bool, + on_click: impl 'a + FnOnce(), +) -> TableCell<'a> { + TableCell::Some { + unit: Unit::Ratio(1.0), + render: Box::new(move |_direction, wh, ctx| { + let (text_color, fill_color) = match selected { + true => (Color::WHITE, Color::BLUE), + false => (Color::BLUE, Color::TRANSPARENT), + }; + + ctx.add(button::TextButton { + rect: wh.to_rect(), + text: text.to_string(), + text_color, + stroke_color: Color::BLUE, + stroke_width: 1.px(), + fill_color, + mouse_buttons: vec![MouseButton::Left], + on_mouse_up_in: move |_| { + on_click(); + }, + }); + }), + need_clip: true, + } +} diff --git a/luda-editor/new-client/src/episode_editor/scene_list.rs b/luda-editor/new-client/src/episode_editor/scene_list.rs index f802507ff..ae19e0094 100644 --- a/luda-editor/new-client/src/episode_editor/scene_list.rs +++ b/luda-editor/new-client/src/episode_editor/scene_list.rs @@ -1,4 +1,4 @@ -use super::render_psd_sprite::render_psd_sprite; +use super::psd_sprite_util::render_psd_sprite; use luda_rpc::Scene; use namui::*; use namui_prebuilt::*; @@ -6,11 +6,18 @@ use namui_prebuilt::*; pub struct SceneList<'a> { pub wh: Wh, pub scenes: &'a [Scene], + pub select_scene: &'a dyn Fn(&str), + pub add_new_scene: &'a dyn Fn(), } impl Component for SceneList<'_> { fn render(self, ctx: &RenderCtx) { - let Self { wh, scenes } = self; + let Self { + wh, + scenes, + select_scene, + add_new_scene, + } = self; ctx.compose(|ctx| { table::vertical([ table::fixed(40.px(), |wh, ctx| { @@ -21,6 +28,26 @@ impl Component for SceneList<'_> { 16.int_px(), )); }), + table::fixed(40.px(), |wh, ctx| { + // TODO: 임시 버튼. 추후 디자인 필요. + ctx.add( + typography::center_text( + wh, + "Add New Scene Last", + Color::WHITE, + 16.int_px(), + ) + .attach_event(|event| { + let Event::MouseDown { event } = event else { + return; + }; + if !event.is_local_xy_in() { + return; + } + add_new_scene(); + }), + ); + }), table::ratio(1, |wh, ctx| { let item_wh = Wh::new(wh.width, wh.width / 4 * 3); ctx.add(list_view::AutoListView { @@ -34,6 +61,7 @@ impl Component for SceneList<'_> { index, scene, wh: item_wh, + select_scene, }, ) }), @@ -48,11 +76,17 @@ struct SceneListCell<'a> { index: usize, scene: &'a Scene, wh: Wh, + select_scene: &'a dyn Fn(&str), } impl Component for SceneListCell<'_> { fn render(self, ctx: &RenderCtx) { - let Self { index, scene, wh } = self; + let Self { + index, + scene, + wh, + select_scene, + } = self; /* 썸네일을 어떻게 보여줄 것인가? 저장하고 보여줄 것인가, 매번 새로 그릴 것인가? @@ -72,6 +106,17 @@ impl Component for SceneListCell<'_> { render_psd_sprite(ctx, scene_sprite, wh); } - ctx.add(simple_rect(wh, Color::TRANSPARENT, 1.px(), Color::BLACK)); + ctx.add( + simple_rect(wh, Color::TRANSPARENT, 1.px(), Color::BLACK).attach_event(|event| { + let Event::MouseDown { event } = event else { + return; + }; + if !event.is_local_xy_in() { + return; + } + event.stop_propagation(); + select_scene(&scene.id); + }), + ); } } diff --git a/luda-editor/new-client/src/episode_editor/scene_preview.rs b/luda-editor/new-client/src/episode_editor/scene_preview.rs index 0f81f301f..10d86ec12 100644 --- a/luda-editor/new-client/src/episode_editor/scene_preview.rs +++ b/luda-editor/new-client/src/episode_editor/scene_preview.rs @@ -1,4 +1,4 @@ -use super::render_psd_sprite::render_psd_sprite; +use super::psd_sprite_util::render_psd_sprite; use luda_rpc::Scene; use namui::*; use namui_prebuilt::*; diff --git a/luda-editor/new-client/src/episode_editor/scene_sprite_editor/mod.rs b/luda-editor/new-client/src/episode_editor/scene_sprite_editor/mod.rs index 553d229cd..1b37440f6 100644 --- a/luda-editor/new-client/src/episode_editor/scene_sprite_editor/mod.rs +++ b/luda-editor/new-client/src/episode_editor/scene_sprite_editor/mod.rs @@ -3,52 +3,184 @@ mod scene_sprite_list; mod size_tool; mod sprite_select_tool; +use luda_rpc::{AssetDoc, Circumcircle, Scene, SceneSprite}; +use math::num::Zero; use namui::*; use namui_prebuilt::*; +use std::collections::{HashMap, HashSet}; pub struct SceneSpriteEditor<'a> { pub wh: Wh, - _phantom: std::marker::PhantomData<&'a ()>, + pub scene: &'a Scene, + pub update_scene: &'a dyn Fn(Scene), + pub asset_docs: Sig<'a, HashMap>, } impl Component for SceneSpriteEditor<'_> { fn render(self, ctx: &RenderCtx) { - let Self { wh, _phantom } = self; + let Self { + wh, + scene, + update_scene, + asset_docs, + } = self; + + let (selected_scene_sprite_index, set_selected_scene_sprite_index) = ctx.state(|| None); + + let scene_sprites = &scene.scene_sprites; + let selected_scene_sprite = selected_scene_sprite_index + .as_ref() + .and_then(|index: usize| scene_sprites.get(index)); + + let remove_scene_sprite = &|index: usize| { + let mut scene = scene.clone(); + scene.scene_sprites.remove(index); + update_scene(scene); + }; + + let add_new_scene_sprite = &|| { + let mut scene = scene.clone(); + scene.scene_sprites.push(SceneSprite { + sprite_id: None, + circumcircle: Circumcircle { + xy: Xy::single(50.percent()), + radius: 50.percent(), + }, + part_option_selections: HashMap::new(), + }); + update_scene(scene); + }; + + let move_scene_sprite_up_down = &|index: usize, upward: bool| { + let mut scene = scene.clone(); + let target_index = match upward { + true => index.checked_sub(1), + false => index.checked_add(1), + }; + if let Some(target_index) = target_index { + scene.scene_sprites.swap(index, target_index); + update_scene(scene); + } + }; + + let select_scene_sprite_index = &|index: usize| { + set_selected_scene_sprite_index.set(Some(index)); + }; + + let select_sprite = &|sprite_id: &str| { + let Some(index) = *selected_scene_sprite_index else { + return; + }; + let mut scene = scene.clone(); + let Some(scene_sprite) = scene.scene_sprites.get_mut(index) else { + return; + }; + scene_sprite.sprite_id = Some(sprite_id.to_string()); + update_scene(scene); + }; + + let select_part_option = + &|part_name: &str, part_option_name: &str, is_single_select: bool| { + let Some(index) = *selected_scene_sprite_index else { + return; + }; + let mut scene = scene.clone(); + let Some(scene_sprite) = scene.scene_sprites.get_mut(index) else { + return; + }; + let part_option_selection = scene_sprite + .part_option_selections + .entry(part_name.to_string()) + .or_insert(HashSet::new()); + + let already_selected = part_option_selection.contains(part_option_name); + if is_single_select { + part_option_selection.clear(); + part_option_selection.insert(part_option_name.to_string()); + } else if already_selected { + part_option_selection.remove(part_option_name); + if part_option_selection.len().is_zero() { + scene_sprite.part_option_selections.remove(part_name); + } + } else { + part_option_selection.insert(part_option_name.to_string()); + } + + update_scene(scene); + }; + + let on_change_position = &|position: Xy| { + let Some(index) = *selected_scene_sprite_index else { + return; + }; + let mut scene = scene.clone(); + let Some(scene_sprite) = scene.scene_sprites.get_mut(index) else { + return; + }; + scene_sprite.circumcircle.xy = position; + update_scene(scene); + }; + + let on_change_size_radius = &|size_radius: Percent| { + let Some(index) = *selected_scene_sprite_index else { + return; + }; + let mut scene = scene.clone(); + let Some(scene_sprite) = scene.scene_sprites.get_mut(index) else { + return; + }; + scene_sprite.circumcircle.radius = size_radius; + update_scene(scene); + }; + ctx.compose(|ctx| { table::vertical([ table::fixed(320.px(), { |wh, ctx| { ctx.add(scene_sprite_list::SceneSpriteList { wh, - scene_sprites: todo!(), - remove_scene_sprite: todo!(), - add_new_scene_sprite: todo!(), - move_scene_sprite_up_down: todo!(), - select_scene_sprite: todo!(), - selected_scene_sprite_index: todo!(), - sprite_docs: todo!(), + scene_sprites, + asset_docs: &asset_docs, + remove_scene_sprite, + add_new_scene_sprite, + move_scene_sprite_up_down, + select_scene_sprite_index, + selected_scene_sprite_index: *selected_scene_sprite_index, }); } }), table::fixed(320.px(), |wh, ctx| { + if selected_scene_sprite.is_none() { + return; + } ctx.add(sprite_select_tool::SpriteSelectTool { wh, - sprite_docs: todo!(), + asset_docs: asset_docs.clone(), + select_sprite, + select_part_option, }); }), table::fixed(320.px(), |wh, ctx| { - ctx.add(position_tool::PositionTool { - wh, - position: todo!(), - on_change_position: todo!(), - }); + if let Some(position) = + selected_scene_sprite.map(|sprite| sprite.circumcircle.xy) + { + ctx.add(position_tool::PositionTool { + wh, + position, + on_change_position, + }); + } }), table::fixed(320.px(), |wh, ctx| { - ctx.add(size_tool::SizeTool { - wh, - size_radius: todo!(), - on_change_size_radius: todo!(), - }); + if let Some(size_radius) = + selected_scene_sprite.map(|sprite| sprite.circumcircle.radius) + { + ctx.add(size_tool::SizeTool { + wh, + size_radius, + on_change_size_radius, + }); + } }), ])(wh, ctx) }); diff --git a/luda-editor/new-client/src/episode_editor/scene_sprite_editor/position_tool.rs b/luda-editor/new-client/src/episode_editor/scene_sprite_editor/position_tool.rs index 4e67bc6e9..3d0e292a8 100644 --- a/luda-editor/new-client/src/episode_editor/scene_sprite_editor/position_tool.rs +++ b/luda-editor/new-client/src/episode_editor/scene_sprite_editor/position_tool.rs @@ -2,8 +2,8 @@ use crate::*; pub struct PositionTool<'a> { pub wh: Wh, - pub position: Xy, - pub on_change_position: &'a dyn Fn(Xy), + pub position: Xy, + pub on_change_position: &'a dyn Fn(Xy), } impl Component for PositionTool<'_> { @@ -20,6 +20,17 @@ impl Component for PositionTool<'_> { ctx.add(typography::body::left(wh.height, "위치", Color::WHITE)); }), table::ratio(1, |wh, ctx| { + const ROWS: isize = 5; + const COLS: isize = 5; + + let to_nearest_point = |xy: Xy| { + let cols = COLS as f32; + let rows = ROWS as f32; + let x = ((cols * xy.x).floor().min(cols).max(0.0) + 0.5) / cols; + let y = ((rows * xy.y).floor().min(rows).max(0.0) + 0.5) / rows; + Xy::new(x, y) + }; + ctx.add( rect(RectParam { rect: Rect::zero_wh(wh), @@ -41,7 +52,8 @@ impl Component for PositionTool<'_> { return; } - on_change_position(event.local_xy() / wh.as_xy()); + let new_xy = to_nearest_point(event.local_xy() / wh.as_xy()); + on_change_position(new_xy.map(|xy| (100.0 * xy).percent())); }), ); @@ -50,20 +62,22 @@ impl Component for PositionTool<'_> { Xy::new(-radius, -radius), Wh::new(radius * 2, radius * 2), )); - let paint = Paint::new(Color::WHITE).set_style(PaintStyle::Stroke); - let rendering_tree = path(circle, paint); + let default_paint = Paint::new(Color::WHITE).set_style(PaintStyle::Stroke); + let active_paint = Paint::new(Color::BLUE).set_style(PaintStyle::Fill); + let default_rendering_tree = path(circle.clone(), default_paint); + let active_rendering_tree = path(circle, active_paint); - let rows = 5; - let cols = 5; + for row in 0..ROWS { + for col in 0..COLS { + let x = wh.width * ((col as f32 + 0.5) / (COLS) as f32); + let y = wh.height * ((row as f32 + 0.5) / (ROWS) as f32); - for row in 0..rows { - for col in 0..cols { - let x = wh.width * ((col as f32 + 0.5) / (cols - 1) as f32); - let y = wh.height * ((row as f32 + 0.5) / (rows - 1) as f32); - - ctx.translate((x, y)).add(rendering_tree.clone()); + ctx.translate((x, y)).add(default_rendering_tree.clone()); } } + + let active_xy = wh.as_xy() * to_nearest_point(position.map(|x| x.as_f32())); + ctx.translate(active_xy).add(active_rendering_tree); }), ])(wh, ctx) }); diff --git a/luda-editor/new-client/src/episode_editor/scene_sprite_editor/scene_sprite_list.rs b/luda-editor/new-client/src/episode_editor/scene_sprite_editor/scene_sprite_list.rs index 822e11b2e..0100cc91e 100644 --- a/luda-editor/new-client/src/episode_editor/scene_sprite_editor/scene_sprite_list.rs +++ b/luda-editor/new-client/src/episode_editor/scene_sprite_editor/scene_sprite_list.rs @@ -1,17 +1,18 @@ use crate::*; use list_view::AutoListView; use luda_rpc::*; +use psd_sprite_util::render_psd_sprite; use std::collections::HashMap; pub struct SceneSpriteList<'a> { pub wh: Wh, pub scene_sprites: &'a [SceneSprite], - pub sprite_docs: &'a HashMap, + pub asset_docs: &'a HashMap, pub remove_scene_sprite: &'a dyn Fn(usize), pub add_new_scene_sprite: &'a dyn Fn(), /// true for up, false for down pub move_scene_sprite_up_down: &'a dyn Fn(usize, bool), - pub select_scene_sprite: &'a dyn Fn(&str), + pub select_scene_sprite_index: &'a dyn Fn(usize), pub selected_scene_sprite_index: Option, } @@ -20,11 +21,11 @@ impl Component for SceneSpriteList<'_> { let Self { wh, scene_sprites, - sprite_docs, + asset_docs, remove_scene_sprite, add_new_scene_sprite, move_scene_sprite_up_down, - select_scene_sprite, + select_scene_sprite_index, selected_scene_sprite_index, } = self; @@ -75,18 +76,18 @@ impl Component for SceneSpriteList<'_> { scroll_bar_width: 16.px(), height: wh.height, item_wh, - items: scene_sprites.into_iter().enumerate().map( - |(index, scene_sprite)| { + items: scene_sprites + .iter() + .enumerate() + .map(|(index, scene_sprite)| { let sprite_name = scene_sprite .sprite_id .as_ref() - .and_then(|sprite_id| { - Some( - sprite_docs - .get(sprite_id) - .map(|sprite_doc| sprite_doc.sprite.name()) - .unwrap_or("???"), - ) + .map(|sprite_id| { + asset_docs + .get(sprite_id) + .map(|asset_doc| asset_doc.name.as_str()) + .unwrap_or("???") }) .unwrap_or(""); ( @@ -95,11 +96,11 @@ impl Component for SceneSpriteList<'_> { wh: item_wh, sprite_name, scene_sprite, - select_scene_sprite, + select_scene_sprite_index, + index, }, ) - }, - ), + }), }); }), ])(wh, ctx) @@ -111,7 +112,8 @@ struct SceneSpriteCell<'a> { wh: Wh, sprite_name: &'a str, scene_sprite: &'a SceneSprite, - select_scene_sprite: &'a dyn Fn(&str), + select_scene_sprite_index: &'a dyn Fn(usize), + index: usize, } impl Component for SceneSpriteCell<'_> { fn render(self, ctx: &RenderCtx) { @@ -119,18 +121,19 @@ impl Component for SceneSpriteCell<'_> { wh, sprite_name, scene_sprite, - select_scene_sprite, + select_scene_sprite_index, + index, } = self; ctx.add(simple_button(wh, "", |_| { - if let Some(sprite_id) = scene_sprite.sprite_id.as_ref() { - select_scene_sprite(sprite_id); - } + select_scene_sprite_index(index); })); ctx.compose(|ctx| { table::horizontal([ - table::fixed(128.px(), |wh, ctx| todo!("ctx.add(scene_sprite_preview);")), + table::fixed(128.px(), |wh, ctx| { + ctx.add(SceneSpritePreview { wh, scene_sprite }); + }), table::ratio(1, |wh, ctx| { ctx.add(typography::body::left(wh.height, sprite_name, Color::WHITE)); }), @@ -138,3 +141,14 @@ impl Component for SceneSpriteCell<'_> { }); } } + +struct SceneSpritePreview<'a> { + wh: Wh, + scene_sprite: &'a SceneSprite, +} +impl Component for SceneSpritePreview<'_> { + fn render(self, ctx: &RenderCtx) { + let Self { wh, scene_sprite } = self; + render_psd_sprite(ctx, scene_sprite, wh); + } +} diff --git a/luda-editor/new-client/src/episode_editor/scene_sprite_editor/size_tool.rs b/luda-editor/new-client/src/episode_editor/scene_sprite_editor/size_tool.rs index a07a338e9..60f97ca95 100644 --- a/luda-editor/new-client/src/episode_editor/scene_sprite_editor/size_tool.rs +++ b/luda-editor/new-client/src/episode_editor/scene_sprite_editor/size_tool.rs @@ -55,6 +55,9 @@ impl Component for SizeTool<'_> { event } Event::MouseUp { event } => { + if !*is_dragging { + return; + } set_is_dragging.set(false); event } @@ -63,7 +66,7 @@ impl Component for SizeTool<'_> { } }; - let x = mouse_event.local_xy().x / wh.width; + let x = (mouse_event.local_xy().x / wh.width).clamp(0.0, 1.0); on_change_size_radius(100.percent() * x); }), ); diff --git a/luda-editor/new-client/src/episode_editor/scene_sprite_editor/sprite_select_tool.rs b/luda-editor/new-client/src/episode_editor/scene_sprite_editor/sprite_select_tool.rs index b6ce11483..b6deb077a 100644 --- a/luda-editor/new-client/src/episode_editor/scene_sprite_editor/sprite_select_tool.rs +++ b/luda-editor/new-client/src/episode_editor/scene_sprite_editor/sprite_select_tool.rs @@ -1,40 +1,66 @@ use crate::*; use list_view::AutoListView; use luda_rpc::*; -use std::collections::HashSet; +use psd_sprite_util::{get_or_load_psd_sprite, PsdSpriteLoadState}; +use std::{ + collections::{HashMap, HashSet}, + ops::Deref, +}; pub struct SpriteSelectTool<'a> { pub wh: Wh, - pub sprite_docs: Sig<'a, [SpriteDoc]>, + pub asset_docs: Sig<'a, HashMap>, + pub select_sprite: &'a dyn Fn(&str), + pub select_part_option: &'a dyn Fn(&str, &str, bool), } impl Component for SpriteSelectTool<'_> { fn render(self, ctx: &RenderCtx) { - let Self { wh, sprite_docs } = self; + let Self { + wh, + asset_docs, + select_sprite, + select_part_option, + } = self; let (selected_sprite_id, set_selected_sprite_id) = ctx.state::>(|| None); let (selected_part_name, set_selected_part_name) = ctx.state::>(|| None); - let (selected_tags, set_selected_tags) = ctx.state::>(Default::default); + let (selected_tags, set_selected_tags) = + ctx.state::>(Default::default); + let parts = ctx.memo(|| { + let Some(selected_sprite_id) = selected_sprite_id.deref() else { + return Default::default(); + }; + let psd_load_state = get_or_load_psd_sprite(selected_sprite_id.clone()); + let PsdSpriteLoadState::Loaded { psd_sprite, .. } = psd_load_state.as_ref() else { + return Default::default(); + }; + psd_sprite.parts() + }); - let tag_filtered_sprite_docs = ctx.memo(|| { - sprite_docs + let tag_filtered_asset_docs = ctx.memo(|| { + asset_docs .iter() - .filter(|sprite_doc| { - sprite_doc.tags.iter().any(|tag| match tag { - SpriteTag::System { tag } => selected_tags.contains(tag), - SpriteTag::Custom { .. } => false, + .filter(|(_id, asset_tag)| { + if !matches!(asset_tag.asset_kind, AssetKind::Sprite) { + return false; + } + asset_tag.tags.iter().any(|tag| match tag { + AssetTag::System { tag } => selected_tags.contains(tag), + AssetTag::Custom { .. } => false, }) }) - .cloned() - .collect::>() + .map(|(id, sprite)| (id.clone(), sprite.clone())) + .collect::>() }); - let tag_toggle_button = |tag: SystemTag| { - let is_on = selected_tags.contains(&SystemTag::Character); + let tag_toggle_button = |tag: AssetSystemTag| { + let is_on = selected_tags.contains(&tag); let text = match tag { - SystemTag::Character => "인물", - SystemTag::Object => "사물", - SystemTag::Background => "배경", + AssetSystemTag::SpriteCharacter => "인물", + AssetSystemTag::SpriteObject => "사물", + AssetSystemTag::SpriteBackground => "배경", + _ => unreachable!(), }; table::ratio(1, move |wh, ctx| { @@ -50,27 +76,17 @@ impl Component for SpriteSelectTool<'_> { }) }; - let selected_sprite_doc = - selected_sprite_id - .as_ref() - .as_ref() - .and_then(|selected_sprite_id| { - sprite_docs - .iter() - .find(|sprite_doc| &sprite_doc.id == selected_sprite_id) - }); - ctx.compose(|ctx| { table::vertical([ table::fixed( 64.px(), table::horizontal([ table::fixed(64.px(), |_, _| {}), - tag_toggle_button(SystemTag::Character), + tag_toggle_button(AssetSystemTag::SpriteCharacter), table::fixed(16.px(), |_, _| {}), - tag_toggle_button(SystemTag::Object), + tag_toggle_button(AssetSystemTag::SpriteObject), table::fixed(16.px(), |_, _| {}), - tag_toggle_button(SystemTag::Background), + tag_toggle_button(AssetSystemTag::SpriteBackground), table::fixed(64.px(), |_, _| {}), ]), ), @@ -80,60 +96,47 @@ impl Component for SpriteSelectTool<'_> { table::ratio(1, |wh, ctx| { let sprite_column = Column { wh, - items: tag_filtered_sprite_docs.iter().enumerate().map( - |(index, sprite)| { - let preview = |wh: Wh, ctx: &ComposeCtx| todo!(); - let on_select = || todo!(); - ( - sprite.id.as_str(), - preview, - sprite.sprite.name().to_string(), - on_select, - ) - }, - ), + items: tag_filtered_asset_docs.iter().map(|(id, sprite)| { + let on_select = || { + set_selected_sprite_id.set(Some(id.clone())); + select_sprite(id); + }; + (sprite.id.as_str(), sprite.name.to_string(), on_select) + }), }; ctx.add(sprite_column); }), table::ratio(1, |wh, ctx| { - let Some(sprite_doc) = selected_sprite_doc.as_ref() else { - return; - }; - let Sprite::Parts { sprite } = &sprite_doc.sprite else { - return; - }; let part_column = Column { wh, - items: sprite.parts.iter().enumerate().map( - |(index, (name, part))| { - let preview = |wh: Wh, ctx: &ComposeCtx| todo!(); - let on_select = || todo!(); - (index, preview, name.to_string(), on_select) - }, - ), + items: parts.iter().enumerate().map(|(index, (name, _part))| { + let on_select = || { + set_selected_part_name.set(Some(name.clone())); + }; + (index, name.to_string(), on_select) + }), }; ctx.add(part_column); }), table::ratio(1, |wh, ctx| { - let Some(sprite_doc) = selected_sprite_doc.as_ref() else { - return; - }; let Some(selected_part_name) = selected_part_name.as_ref() else { return; }; - let Sprite::Parts { sprite } = &sprite_doc.sprite else { - return; - }; - let Some(part) = sprite.parts.get(selected_part_name) else { + let Some(part) = parts.get(selected_part_name) else { return; }; let part_option_column = Column { wh, - items: part.part_options.iter().enumerate().map( - |(index, part_option)| { - let preview = |wh: Wh, ctx: &ComposeCtx| todo!(); - let on_select = || todo!(); - (index, preview, part_option.name.to_string(), on_select) + items: part.options.iter().enumerate().map( + |(index, option_name)| { + let on_select = move || { + select_part_option( + selected_part_name, + option_name, + part.is_single_select, + ); + }; + (index, option_name.to_string(), on_select) }, ), }; @@ -146,61 +149,53 @@ impl Component for SpriteSelectTool<'_> { } } -struct Column +struct Column where Key: Into, - Preview: Fn(Wh, &ComposeCtx), OnSelect: Fn(), - Items: ExactSizeIterator, + Items: ExactSizeIterator, { wh: Wh, items: Items, } -impl Component for Column +impl Component for Column where Key: Into, - Preview: Fn(Wh, &ComposeCtx), OnSelect: Fn(), - Items: ExactSizeIterator, + Items: ExactSizeIterator, { fn render(self, ctx: &RenderCtx) { let Self { wh, items } = self; - let item_wh = Wh::new(wh.width, 80.px()); + let item_wh = Wh::new(wh.width, 48.px()); ctx.add(AutoListView { height: wh.height, scroll_bar_width: 10.px(), item_wh, items: items.map(|item| { - let (key, preview, text, on_select) = item; + let (key, text, on_select) = item; (key, move |ctx: &RenderCtx| { - ctx.compose(|ctx| { - table::horizontal([ - table::fixed(128.px(), |wh, ctx| { - preview(wh, &ctx); - }), - table::ratio(1, |wh, ctx| { - ctx.add(namui::text(TextParam { - text, - x: 0.px(), - y: wh.height / 2.0, - align: TextAlign::Left, - baseline: TextBaseline::Middle, - font: Font { - name: "NotoSansKR-Regular".to_string(), - size: 16.int_px(), - }, - style: TextStyle { - color: Color::WHITE, - ..Default::default() - }, - max_width: Some(wh.width), - })); - }), - ])(item_wh, ctx) - }); + ctx.add(namui::text(TextParam { + text, + x: 0.px(), + y: item_wh.height / 2.0, + align: TextAlign::Left, + baseline: TextBaseline::Middle, + font: Font { + name: "NotoSansKR-Regular".to_string(), + size: 16.int_px(), + }, + style: TextStyle { + color: Color::WHITE, + ..Default::default() + }, + max_width: Some(wh.width), + })); + ctx.add(simple_button(item_wh, "", move |_| { + on_select(); + })); }) }), }); diff --git a/luda-editor/new-client/src/home.rs b/luda-editor/new-client/src/home.rs index 7e443d267..691346a2f 100644 --- a/luda-editor/new-client/src/home.rs +++ b/luda-editor/new-client/src/home.rs @@ -55,10 +55,31 @@ impl Component for Home<'_> { ratio(1, |wh, ctx| { ctx.add(EpisodeList { wh, + team_id: match selection.as_ref() { + Selection::Team { team_id } => Some(team_id), + Selection::Project { team_id, .. } => Some(team_id), + _ => None, + }, project_id: match selection.as_ref() { Selection::Project { project_id, .. } => Some(project_id), _ => None, }, + on_select_episode: &|episode| { + let team_id = match selection.as_ref() { + Selection::Team { team_id } => team_id.clone(), + Selection::Project { team_id, .. } => team_id.clone(), + _ => unreachable!(), + }; + let project_id = match selection.as_ref() { + Selection::Project { project_id, .. } => project_id.clone(), + _ => unreachable!(), + }; + router::route(router::Route::EpisodeEditor { + team_id, + project_id, + episode_id: episode.id.clone(), + }); + }, }); }), ])(screen_wh, ctx); @@ -283,12 +304,19 @@ impl Component for ProjectList<'_> { struct EpisodeList<'a> { wh: Wh, + team_id: Option<&'a String>, project_id: Option<&'a String>, + on_select_episode: &'a dyn Fn(&Episode), } impl Component for EpisodeList<'_> { fn render(self, ctx: &RenderCtx) { - let Self { wh, project_id } = self; + let Self { + wh, + team_id, + project_id, + on_select_episode, + } = self; let title = || { [fixed(24.px(), |wh, ctx| { @@ -331,16 +359,41 @@ impl Component for EpisodeList<'_> { .into_iter() .chain(episodes.iter().map(|episode| { fixed(24.px(), |wh, ctx| { - ctx.add(typography::center_text( - wh, - &episode.name, - Color::WHITE, - 16.int_px(), - )); + ctx.add( + typography::center_text( + wh, + &episode.name, + Color::WHITE, + 16.int_px(), + ) + .attach_event( + |event| { + if let Event::MouseUp { event } = event { + if event.is_local_xy_in() + && event.button == Some(MouseButton::Left) + { + on_select_episode(episode); + } + } + }, + ), + ); }) })) .chain([fixed(24.px(), |wh, ctx| { - ctx.add(simple_button(wh, "새 에피소드", |_event| {})); + ctx.add(simple_button(wh, "새 에피소드", |_event| { + let (Some(team_id), Some(project_id)) = (team_id, project_id) + else { + toast::negative( + "오류가 발생했습니다. 새로고침 후 다시 시도해주세요", + ); + return; + }; + router::route(router::Route::NewEpisode { + team_id: team_id.clone(), + project_id: project_id.clone(), + }); + })); })]), )(wh, ctx) }); diff --git a/luda-editor/new-client/src/lib.rs b/luda-editor/new-client/src/lib.rs index 6ad124aba..872a2dfd5 100644 --- a/luda-editor/new-client/src/lib.rs +++ b/luda-editor/new-client/src/lib.rs @@ -2,9 +2,10 @@ mod asset_manage_page; mod episode_editor; mod home; mod network; +mod new_episode_page; mod new_project_page; mod new_team_page; -mod render_psd_sprite; +mod psd_sprite_util; mod router; mod rpc; mod simple_button; diff --git a/luda-editor/new-client/src/new_episode_page.rs b/luda-editor/new-client/src/new_episode_page.rs new file mode 100644 index 000000000..7bacb64df --- /dev/null +++ b/luda-editor/new-client/src/new_episode_page.rs @@ -0,0 +1,197 @@ +use super::*; +use rpc::episode::create_new_episode::*; + +pub struct NewEpisodePage<'a> { + pub team_id: &'a str, + pub project_id: &'a str, +} + +impl Component for NewEpisodePage<'_> { + fn render(self, ctx: &RenderCtx) { + let Self { + team_id, + project_id, + } = self; + + let screen_wh = namui::screen::size().map(|x| x.into_px()); + let (episode_name, set_episode_name) = ctx.state(String::new); + let (episode_name_validate_err, set_episode_name_validate_err) = + ctx.state::>(|| None); + let (create_new_episode_err, set_create_new_episode_err) = + ctx.state::>(|| None); + + let (submit, on_progress) = make_create_new_episode_fn( + ctx, + || { + if episode_name.is_empty() { + set_episode_name_validate_err.set(Some( + "에피소드 이름이 비어있습니다. 에피소드 이름을 입력해주세요".to_string(), + )); + return None; + } + + set_episode_name_validate_err.set(None); + Some(( + RefRequest { + project_id, + name: &episode_name, + }, + (team_id, project_id), + )) + }, + move |result| match result { + Ok((_, (team_id, project_id))) => { + toast::positive("에피소드 생성 완료"); + router::route(router::Route::Home { + initial_selection: home::Selection::Project { + team_id, + project_id, + }, + }); + } + Err(err) => { + set_create_new_episode_err.set(Some(err)); + } + }, + ); + + ctx.compose(|ctx| { + vertical([ + fixed(24.px(), |wh, ctx| { + ctx.add(typography::title::left( + wh.height, + "새 에피소드 만들기", + Color::WHITE, + )); + }), + fixed(16.px(), |wh, ctx| { + ctx.add(namui::text(TextParam { + text: "에피소드 이름".to_string(), + x: 0.px(), + y: 12.px(), + align: TextAlign::Left, + baseline: TextBaseline::Middle, + font: Font { + name: "NotoSansKR-Regular".to_string(), + size: 12.int_px(), + }, + style: TextStyle { + color: Color::WHITE, + ..Default::default() + }, + max_width: Some(wh.width), + })); + }), + fixed(24.px(), |wh, ctx| { + ctx.add(TextInput { + rect: Rect::zero_wh(wh), + start_text: episode_name.as_ref(), + text_align: TextAlign::Center, + text_baseline: TextBaseline::Middle, + font: Font { + size: 16.int_px(), + name: "NotoSansKR-Regular".to_string(), + }, + style: Style { + rect: RectStyle { + stroke: Some(RectStroke { + color: Color::WHITE, + width: 1.px(), + border_position: BorderPosition::Middle, + }), + fill: Some(RectFill { + color: Color::grayscale_f01(0.3), + }), + round: Some(RectRound { radius: 4.px() }), + }, + text: TextStyle { + color: Color::WHITE, + ..Default::default() + }, + padding: Ltrb::all(8.px()), + }, + prevent_default_codes: &[Code::Enter], + focus: None, + on_edit_done: &|text| { + set_episode_name.set(text); + }, + }); + }), + if let Some(episode_name_validate_err) = episode_name_validate_err.as_ref() { + fixed(16.px(), |wh, ctx| { + ctx.add(namui::text(TextParam { + text: episode_name_validate_err.to_string(), + x: 0.px(), + y: 12.px(), + align: TextAlign::Left, + baseline: TextBaseline::Middle, + font: Font { + name: "NotoSansKR-Regular".to_string(), + size: 12.int_px(), + }, + style: TextStyle { + color: Color::RED, + ..Default::default() + }, + max_width: Some(wh.width), + })); + }) + } else { + empty() + }, + fixed(24.px(), |wh, ctx| { + ctx.add(simple_button(wh, "만들기", |_event| { + submit(); + })); + }), + if let Some(err) = create_new_episode_err.as_ref() { + let text = match err { + Error::NeedLogin => "로그인이 필요합니다".to_string(), + Error::PermissionDenied => "권한이 없습니다".to_string(), + Error::ProjectNotExist => "프로젝트가 존재하지 않습니다".to_string(), + Error::InternalServerError { err } => format!("서버 오류: {}", err), + }; + fixed(16.px(), |wh, ctx| { + ctx.add(namui::text(TextParam { + text, + x: 0.px(), + y: 12.px(), + align: TextAlign::Left, + baseline: TextBaseline::Middle, + font: Font { + name: "NotoSansKR-Regular".to_string(), + size: 12.int_px(), + }, + style: TextStyle { + color: Color::RED, + ..Default::default() + }, + max_width: Some(wh.width), + })); + }) + } else if on_progress { + fixed(16.px(), |wh, ctx| { + ctx.add(namui::text(TextParam { + text: "진행중...".to_string(), + x: 0.px(), + y: 12.px(), + align: TextAlign::Left, + baseline: TextBaseline::Middle, + font: Font { + name: "NotoSansKR-Regular".to_string(), + size: 12.int_px(), + }, + style: TextStyle { + color: Color::WHITE, + ..Default::default() + }, + max_width: Some(wh.width), + })); + }) + } else { + empty() + }, + ])(screen_wh, ctx); + }); + } +} diff --git a/luda-editor/new-client/src/psd_sprite_util/mod.rs b/luda-editor/new-client/src/psd_sprite_util/mod.rs new file mode 100644 index 000000000..bc6a9e37d --- /dev/null +++ b/luda-editor/new-client/src/psd_sprite_util/mod.rs @@ -0,0 +1,5 @@ +mod psd_sprite_storage; +mod render_psd_sprite; + +pub use psd_sprite_storage::*; +pub use render_psd_sprite::*; diff --git a/luda-editor/new-client/src/render_psd_sprite.rs b/luda-editor/new-client/src/psd_sprite_util/psd_sprite_storage.rs similarity index 52% rename from luda-editor/new-client/src/render_psd_sprite.rs rename to luda-editor/new-client/src/psd_sprite_util/psd_sprite_storage.rs index f3ba6f28d..e4b4c5170 100644 --- a/luda-editor/new-client/src/render_psd_sprite.rs +++ b/luda-editor/new-client/src/psd_sprite_util/psd_sprite_storage.rs @@ -1,7 +1,5 @@ -use super::*; -use luda_rpc::SceneSprite; +use namui::*; use psd_sprite::{decode_psd_sprite_from_bytes, PsdSprite, SpriteLoadedImages}; -use psd_sprite_render::RenderPsdSprite; use std::{ collections::HashMap, error::Error, @@ -12,17 +10,11 @@ lazy_static! { static ref PSD_SPRITE_STORAGE: PsdSpriteStorage = PsdSpriteStorage::new(); } -pub fn render_psd_sprite(ctx: &RenderCtx, scene_sprite: &SceneSprite, screen_wh: Wh) { - let Some(sprite_id) = &scene_sprite.sprite_id else { - return; - }; - - let Some(load_state) = PSD_SPRITE_STORAGE.try_get(sprite_id) else { - let sprite_id = sprite_id.clone(); - PSD_SPRITE_STORAGE.set(sprite_id.clone(), PsdSpriteLoadState::Loading); - ctx.spawn(async move { - namui::log!("Loading PSD sprite: {}", sprite_id); - // TODO: Load PSD sprite from the server and cache. +pub fn get_or_load_psd_sprite(sprite_id: String) -> Arc { + let Some(load_state) = PSD_SPRITE_STORAGE.get(&sprite_id) else { + let loading = PSD_SPRITE_STORAGE.set(sprite_id.clone(), PsdSpriteLoadState::Loading); + spawn(async move { + let sprite_id = sprite_id.clone(); let psd_bytes = namui::file::bundle::read("test.psd").await.unwrap(); let decode_result = decode_psd_sprite_from_bytes(&psd_bytes).await; @@ -33,31 +25,14 @@ pub fn render_psd_sprite(ctx: &RenderCtx, scene_sprite: &SceneSprite, screen_wh: loaded_images: Arc::new(loaded_images), }, ); - PSD_SPRITE_STORAGE.set(sprite_id.clone(), load_state); + PSD_SPRITE_STORAGE.set(sprite_id, load_state); }); - return; - }; - - let PsdSpriteLoadState::Loaded { - psd_sprite, - loaded_images, - } = load_state.as_ref() - else { - return; + return loading; }; - - ctx.add_with_key( - format!("{scene_sprite:?}"), - RenderPsdSprite { - psd_sprite: psd_sprite.clone(), - scene_sprite, - loaded_images: loaded_images.clone(), - screen_wh, - }, - ); + load_state } -enum PsdSpriteLoadState { +pub enum PsdSpriteLoadState { Loading, Loaded { psd_sprite: Arc, @@ -66,7 +41,6 @@ enum PsdSpriteLoadState { #[allow(unused)] Error(Box), } - struct PsdSpriteStorage { storage: RwLock>>, } @@ -76,13 +50,15 @@ impl PsdSpriteStorage { storage: RwLock::new(HashMap::new()), } } - fn try_get(&self, sprite_id: &str) -> Option> { + fn get(&self, sprite_id: &str) -> Option> { self.storage.read().unwrap().get(sprite_id).cloned() } - fn set(&self, sprite_id: String, load_state: PsdSpriteLoadState) { + fn set(&self, sprite_id: String, load_state: PsdSpriteLoadState) -> Arc { + let load_state = Arc::new(load_state); self.storage .write() .unwrap() - .insert(sprite_id, Arc::new(load_state)); + .insert(sprite_id, load_state.clone()); + load_state } } diff --git a/luda-editor/new-client/src/psd_sprite_util/render_psd_sprite.rs b/luda-editor/new-client/src/psd_sprite_util/render_psd_sprite.rs new file mode 100644 index 000000000..293a50889 --- /dev/null +++ b/luda-editor/new-client/src/psd_sprite_util/render_psd_sprite.rs @@ -0,0 +1,30 @@ +use super::*; +use luda_rpc::SceneSprite; +use namui::*; +use psd_sprite_render::RenderPsdSprite; + +pub fn render_psd_sprite(ctx: &RenderCtx, scene_sprite: &SceneSprite, screen_wh: Wh) { + let Some(sprite_id) = &scene_sprite.sprite_id else { + return; + }; + + let load_state = get_or_load_psd_sprite(sprite_id.clone()); + + let PsdSpriteLoadState::Loaded { + psd_sprite, + loaded_images, + } = load_state.as_ref() + else { + return; + }; + + ctx.add_with_key( + format!("{scene_sprite:?}"), + RenderPsdSprite { + psd_sprite: psd_sprite.clone(), + scene_sprite, + loaded_images: loaded_images.clone(), + screen_wh, + }, + ); +} diff --git a/luda-editor/new-client/src/router.rs b/luda-editor/new-client/src/router.rs index 171d4288f..f7ae08f35 100644 --- a/luda-editor/new-client/src/router.rs +++ b/luda-editor/new-client/src/router.rs @@ -3,10 +3,25 @@ use super::*; pub struct Router; pub enum Route { - Home { initial_selection: home::Selection }, + Home { + initial_selection: home::Selection, + }, NewTeam, - NewProject { team_id: String }, - AssetManage { team_id: String }, + NewProject { + team_id: String, + }, + NewEpisode { + team_id: String, + project_id: String, + }, + AssetManage { + team_id: String, + }, + EpisodeEditor { + team_id: String, + project_id: String, + episode_id: String, + }, } static ROUTE_ATOM: Atom = Atom::uninitialized(); @@ -26,9 +41,29 @@ impl Component for Router { Route::NewProject { team_id } => { ctx.add(new_project_page::NewProjectPage { team_id }); } + Route::NewEpisode { + team_id, + project_id, + } => { + ctx.add(new_episode_page::NewEpisodePage { + team_id, + project_id, + }); + } Route::AssetManage { team_id } => { ctx.add(asset_manage_page::AssetManagePage { team_id }); } + Route::EpisodeEditor { + team_id, + project_id, + episode_id, + } => { + ctx.add(episode_editor::EpisodeEditor { + team_id, + project_id, + episode_id, + }); + } } } } diff --git a/luda-editor/new-server/rpc/src/types/mod.rs b/luda-editor/new-server/rpc/src/types/mod.rs index 39fad0a22..4cf11c614 100644 --- a/luda-editor/new-server/rpc/src/types/mod.rs +++ b/luda-editor/new-server/rpc/src/types/mod.rs @@ -52,7 +52,9 @@ pub enum EpisodeEditAction { }, } -pub use migration::schema::{AssetDoc, AssetKind, AssetSystemTag, AssetTag, SceneSprite}; +pub use migration::schema::{ + AssetDoc, AssetKind, AssetSystemTag, AssetTag, Circumcircle, SceneSprite, +}; /// Use this on the client side to get the S3 URL of an asset. pub fn asset_s3_get_key(asset_id: &str, asset_kind: AssetKind) -> String { diff --git a/luda-editor/psd-sprite/src/psd_sprite.rs b/luda-editor/psd-sprite/src/psd_sprite.rs index 96bd09d93..338ba2ce6 100644 --- a/luda-editor/psd-sprite/src/psd_sprite.rs +++ b/luda-editor/psd-sprite/src/psd_sprite.rs @@ -1,5 +1,7 @@ use namui_type::*; use psd::BlendMode; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use std::{collections::HashMap, hash::Hash}; #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct PsdSprite { @@ -34,3 +36,48 @@ pub enum SpriteImageId { Mask { prefix: String }, Layer { prefix: String }, } + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct PsdSpritePart { + pub is_single_select: bool, + pub options: Vec, +} + +impl PsdSprite { + pub fn parts(&self) -> HashMap { + return self.entries.par_iter().flat_map(collect_parts).collect(); + + fn collect_parts(entry: &Entry) -> Vec<(String, PsdSpritePart)> { + match &entry.kind { + EntryKind::Layer { .. } => vec![], + EntryKind::Group { entries } => { + let name = entry.name.clone(); + match name { + name if name.ends_with("_m") => { + vec![( + name, + PsdSpritePart { + is_single_select: false, + options: to_options(entries), + }, + )] + } + name if name.ends_with("_s") => { + vec![( + name, + PsdSpritePart { + is_single_select: true, + options: to_options(entries), + }, + )] + } + _ => entries.par_iter().flat_map(collect_parts).collect(), + } + } + } + } + fn to_options(entries: &[Entry]) -> Vec { + entries.iter().map(|entry| entry.name.clone()).collect() + } + } +}