From 19a959a4b04b07a568e7879daf6a5d0a93090a6e Mon Sep 17 00:00:00 2001 From: b-guild <148972828+b-guild@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:56:23 -0700 Subject: [PATCH] Correct source EOL from CRLF to LF, no other changes. (#851) --- .gitattributes | 16 + editor/src/plugins/tilemap/autotile.rs | 2046 ++++++------- editor/src/plugins/tilemap/brush_macro.rs | 2066 ++++++------- editor/src/plugins/tilemap/collider_editor.rs | 890 +++--- editor/src/plugins/tilemap/colliders_tab.rs | 918 +++--- editor/src/plugins/tilemap/handle_editor.rs | 662 ++-- editor/src/plugins/tilemap/handle_field.rs | 426 +-- .../src/plugins/tilemap/interaction_mode.rs | 1806 +++++------ editor/src/plugins/tilemap/macro_inspector.rs | 866 +++--- editor/src/plugins/tilemap/macro_tab.rs | 1122 +++---- editor/src/plugins/tilemap/panel_preview.rs | 584 ++-- editor/src/plugins/tilemap/properties_tab.rs | 1942 ++++++------ .../src/plugins/tilemap/tile_bounds_editor.rs | 618 ++-- editor/src/plugins/tilemap/tile_editor.rs | 1300 ++++---- editor/src/plugins/tilemap/tile_inspector.rs | 2696 ++++++++--------- .../src/plugins/tilemap/tile_prop_editor.rs | 1730 +++++------ editor/src/plugins/tilemap/wfc.rs | 2464 +++++++-------- fyrox-animation/src/lib.rs | 2404 +++++++-------- fyrox-autotile/src/auto.rs | 1224 ++++---- fyrox-autotile/src/wave.rs | 1620 +++++----- .../src/resource/gltf/gltf_standard.shader | 1232 ++++---- fyrox-impl/src/resource/gltf/simplify.rs | 458 +-- .../scene/terrain/brushstroke/brushraster.rs | 950 +++--- .../scene/terrain/brushstroke/strokechunks.rs | 740 ++--- fyrox-impl/src/scene/tilemap/autotile.rs | 1030 +++---- fyrox-impl/src/scene/tilemap/data.rs | 858 +++--- fyrox-impl/src/scene/tilemap/effect.rs | 536 ++-- fyrox-impl/src/scene/tilemap/property.rs | 1456 ++++----- fyrox-impl/src/scene/tilemap/tile_collider.rs | 1076 +++---- fyrox-impl/src/scene/tilemap/tile_rect.rs | 1370 ++++----- fyrox-impl/src/scene/tilemap/tile_source.rs | 1202 ++++---- fyrox-impl/src/scene/tilemap/transform.rs | 1464 ++++----- fyrox-impl/src/scene/tilemap/update.rs | 1970 ++++++------ fyrox-math/src/segment.rs | 526 ++-- fyrox-ui/src/formatted_text/run.rs | 470 +-- fyrox-ui/src/formatted_text/textwrapper.rs | 522 ++-- .../src/inspector/editors/immutable_string.rs | 186 +- 37 files changed, 21731 insertions(+), 21715 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..c77edf37b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +# Rust files are text files with Unix-style newlines. +*.rs text eol=lf +# Other text files have Unix-style newlines. +*.shader text eol=lf +*.toml text eol=lf +*.md text eol=lf +*.html text eol=lf +*.js text eol=lf +*.css text eol=lf +*.glsl text eol=lf +LICENSE text eol=lf +# Image files are binary +*.png binary +*.jpg binary +*.ttf binary +*.bin binary \ No newline at end of file diff --git a/editor/src/plugins/tilemap/autotile.rs b/editor/src/plugins/tilemap/autotile.rs index 9b249ddd0..274459973 100644 --- a/editor/src/plugins/tilemap/autotile.rs +++ b/editor/src/plugins/tilemap/autotile.rs @@ -1,1023 +1,1023 @@ -// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -use fyrox::{ - asset::{untyped::UntypedResource, Resource, ResourceData}, - autotile::{ConstraintFillRules, NeededTerrain, TerrainSource}, - core::{log::MessageKind, swap_hash_map_entry}, - fxhash::FxHashMap, - gui::{ - check_box::{CheckBoxBuilder, CheckBoxMessage}, - dropdown_list::{DropdownListBuilder, DropdownListMessage}, - formatted_text::WrapMode, - stack_panel::StackPanelBuilder, - }, - rand::thread_rng, - scene::tilemap::{ - brush::TileMapBrushResource, - tileset::{ - TileSetPropertyF32, TileSetPropertyId, TileSetPropertyNine, TileSetPropertyType, - TileSetPropertyValueElement, - }, - MacroTilesUpdate, TileSetAutoTileConstraint, TileSetAutoTileContext, TileSetAutoTiler, - TileSetConstraintMap, TileSetPatternSource, TileTerrainId, - }, -}; - -use crate::{ - command::{Command, CommandContext, CommandGroup, CommandTrait}, - send_sync_message, -}; - -use super::*; - -const PATTERN_PROP_DESC: &str = concat!("Choose a nine-slice property from the tile set. ", - "This property will provide the pattern that the autotiler uses to know whether two tiles match along each edge. ", - "The central value of the nine is used to determine which tiles are permited in particular cell and how tiles are prioritized."); - -const FREQUENCY_PROP_DESC: &str = concat!("Choose a float property from the tile set. ", - "This property will provide the frequency that the autotiler uses to know know often to choose a tile when there is more than one ", - "tile with the same pattern."); - -const PROPERTY_LABEL_WIDTH: f32 = 150.0; - -fn log_kind_to_index(log_kind: Option) -> usize { - match log_kind { - None => 0, - Some(MessageKind::Information) => 1, - Some(MessageKind::Warning) => 2, - Some(MessageKind::Error) => 3, - } -} - -fn index_to_log_kind(index: usize) -> Option { - match index { - 0 => None, - 1 => Some(MessageKind::Information), - 2 => Some(MessageKind::Warning), - 3 => Some(MessageKind::Error), - _ => None, - } -} - -fn make_failure_log_list( - widget_builder: WidgetBuilder, - log_kind: Option, - ctx: &mut BuildContext, -) -> Handle { - let items = vec![ - make_list_item("None", ctx), - make_list_item("Info", ctx), - make_list_item("Warn", ctx), - make_list_item("Error", ctx), - ]; - DropdownListBuilder::new(widget_builder) - .with_selected(log_kind_to_index(log_kind)) - .with_items(items) - .build(ctx) -} - -fn make_list_item(text: &str, ctx: &mut BuildContext) -> Handle { - let content = TextBuilder::new(WidgetBuilder::new().on_column(1)) - .with_vertical_text_alignment(VerticalAlignment::Center) - .with_horizontal_text_alignment(HorizontalAlignment::Left) - .with_text(text) - .build(ctx); - DecoratorBuilder::new( - BorderBuilder::new(WidgetBuilder::new().with_child(content)) - .with_corner_radius(4.0.into()) - .with_pad_by_corner_radius(false), - ) - .build(ctx) -} - -#[derive(Default)] -pub struct AutoTileMacro { - pattern_list: MacroPropertyField, - frequency_list: MacroPropertyField, - failure_log_list: Handle, - context: TileSetAutoTileContext, - constraints: TileSetConstraintMap, - autotiler: TileSetAutoTiler, -} - -#[derive(Default, Debug, Clone, Reflect)] -struct CellData { - terrain_id: TileTerrainId, - #[reflect(hidden)] - fill: ConstraintFillRules, -} - -impl Visit for CellData { - fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult { - let mut region = visitor.enter_region(name)?; - self.terrain_id.visit("TerrainId", &mut region)?; - let mut region = region.enter_region("Fill")?; - self.fill - .include_adjacent - .visit("IncludeAdjacent", &mut region)?; - self.fill - .include_diagonal - .visit("IncludeDiagonal", &mut region) - } -} - -#[derive(Debug, Default, Clone, Visit, Reflect, TypeUuidProvider)] -#[type_uuid(id = "b320543d-3df0-43fd-b0d9-60a398f49853")] -pub(super) struct AutoTileInstance { - frequency_property: Option, - pattern_property: Option, - cells: FxHashMap, - #[visit(optional)] - failure_log_kind: Option, - #[reflect(hidden)] - #[visit(skip)] - widgets: InstanceCellWidgets, -} - -#[derive(Debug, Default, Clone)] -struct InstanceCellWidgets { - handle: Handle, - value_field: MacroPropertyValueField, - adjacent_toggle: Handle, - diagonal_toggle: Handle, -} - -impl ResourceData for AutoTileInstance { - fn type_uuid(&self) -> Uuid { - ::type_uuid() - } - - fn save(&mut self, _path: &std::path::Path) -> Result<(), Box> { - Err("Saving is not supported!".to_string().into()) - } - - fn can_be_saved(&self) -> bool { - false - } - - fn try_clone_box(&self) -> Option> { - Some(Box::new(self.clone())) - } -} - -pub struct TileSetTerrainSource<'a, 'b> { - update: &'a MacroTilesUpdate, - instance: &'b AutoTileInstance, -} - -impl TerrainSource for TileSetTerrainSource<'_, '_> { - type Position = Vector2; - type Terrain = TileTerrainId; - - fn iter(&self) -> impl Iterator, TileTerrainId>> + '_ { - self.update.iter().filter_map(|(p, v)| { - let cell_data = self.instance.cells.get(&v.as_ref()?.source?.handle()?)?; - Some(NeededTerrain { - position: *p, - terrain: cell_data.terrain_id, - fill: cell_data.fill, - }) - }) - } - - fn contains_position(&self, position: &Self::Position) -> bool { - let Some(el) = self.update.get(position).cloned().flatten() else { - return false; - }; - let Some(brush_cell) = el.source.and_then(|s| s.handle()) else { - return false; - }; - self.instance.cells.contains_key(&brush_cell) - } -} - -impl BrushMacro for AutoTileMacro { - fn uuid(&self) -> &Uuid { - &uuid!("ab05f992-2591-4729-8164-7b5cc1141d72") - } - - fn name(&self) -> &'static str { - "Autotiler" - } - - fn on_instance_ui_message( - &mut self, - context: &BrushMacroInstance, - message: &UiMessage, - editor: &mut Editor, - ) { - let ui = editor.engine.user_interfaces.first_mut(); - let Some(tile_set) = context.tile_set() else { - return; - }; - if let Some(TileSetPropertyMessage(uuid)) = message.data() { - if message.destination() == self.pattern_list.handle() { - editor.message_sender.do_command(SetPatternPropCommand { - brush: context.brush.clone(), - instance: context.settings().unwrap(), - data: uuid.map(TileSetPropertyNine), - }); - } else if message.destination() == self.frequency_list.handle() { - editor.message_sender.do_command(SetFrequencyPropCommand { - brush: context.brush.clone(), - instance: context.settings().unwrap(), - data: uuid.map(TileSetPropertyF32), - }); - } - } else { - if let Some(DropdownListMessage::SelectionChanged(Some(index))) = message.data() { - if message.destination() == self.failure_log_list - && message.direction() == MessageDirection::FromWidget - { - editor.message_sender.do_command(SetFailureKindCommand { - brush: context.brush.clone(), - instance: context.settings().unwrap(), - data: index_to_log_kind(*index), - }); - } - } - self.pattern_list - .on_ui_message(&tile_set.data_ref(), message, ui); - self.frequency_list - .on_ui_message(&tile_set.data_ref(), message, ui); - } - } - - fn on_cell_ui_message( - &mut self, - context: &MacroMessageContext, - message: &UiMessage, - editor: &mut Editor, - ) { - let Some(cell) = context.cell else { - return; - }; - let ui = editor.engine.user_interfaces.first_mut(); - for r in context.instances_with_uuid(*self.uuid()) { - let instance = r.try_cast::().unwrap(); - let mut settings = instance.data_ref(); - let prop_id = settings - .pattern_property - .as_ref() - .map(|prop| prop.property_uuid()); - let tile_set = context.tile_set(); - let tile_set = tile_set.as_ref().map(|t| t.data_ref()); - let prop = - prop_id.and_then(|id| tile_set.as_ref().and_then(|set| set.find_property(*id))); - settings - .widgets - .value_field - .on_ui_message(prop, message, ui); - let cell_data = settings.cells.get(&cell).cloned().unwrap_or_default(); - if let Some(&TileSetPropertyValueMessage(TileSetPropertyValueElement::I8(v))) = - message.data() - { - if message.destination() == settings.widgets.value_field.handle() { - drop(settings); - editor.message_sender.do_command(SetCellCommand { - brush: context.brush.clone(), - instance, - cell, - data: Some(CellData { - terrain_id: v, - ..cell_data - }), - }); - } - } else if let Some(&CheckBoxMessage::Check(Some(checked))) = message.data() { - if message.destination() == settings.widgets.adjacent_toggle { - drop(settings); - editor.message_sender.do_command(SetCellCommand { - brush: context.brush.clone(), - instance, - cell, - data: Some(CellData { - fill: ConstraintFillRules { - include_adjacent: checked, - ..cell_data.fill - }, - ..cell_data - }), - }); - } else if message.destination() == settings.widgets.diagonal_toggle { - drop(settings); - editor.message_sender.do_command(SetCellCommand { - brush: context.brush.clone(), - instance, - cell, - data: Some(CellData { - fill: ConstraintFillRules { - include_diagonal: checked, - ..cell_data.fill - }, - ..cell_data - }), - }); - } - } - } - } - - fn create_instance(&self, _brush: &TileMapBrushResource) -> Option { - Some(UntypedResource::new_embedded(AutoTileInstance::default())) - } - - fn can_create_cell(&self) -> bool { - true - } - - fn fill_cell_set( - &self, - context: &BrushMacroInstance, - cell_set: &mut FxHashSet, - ) { - let Some(data) = context - .settings - .as_ref() - .and_then(|r| r.try_cast::()) - else { - return; - }; - let data = data.data_ref(); - cell_set.extend(data.cells.keys()); - } - - fn create_cell(&self, context: &BrushMacroCellContext) -> Option { - let instance = context.settings()?; - let cell = context.cell?; - Some(Command::new(SetCellCommand { - brush: context.brush.clone(), - cell, - instance, - data: Some(CellData::default()), - })) - } - - fn move_cells( - &self, - from: Box<[TileDefinitionHandle]>, - to: Box<[TileDefinitionHandle]>, - context: &BrushMacroInstance, - ) -> Option { - let instance = context.settings::()?; - Some(Command::new(MoveCellsCommand::new( - context.brush.clone(), - from, - to, - instance, - ))) - } - - fn move_pages( - &self, - from: Box<[Vector2]>, - to: Box<[Vector2]>, - context: &BrushMacroInstance, - ) -> Option { - let instance = context.settings::()?; - Some(Command::new(MovePagesCommand::new( - context.brush.clone(), - from, - to, - instance, - ))) - } - - fn copy_cell( - &self, - source: Option, - destination: TileDefinitionHandle, - context: &BrushMacroInstance, - ) -> Option { - let instance = context.settings::()?; - let guard = instance.data_ref(); - let data = if let Some(source) = source { - guard.cells.get(&source).cloned() - } else { - None - }; - Some(Command::new(SetCellCommand { - brush: context.brush.clone(), - cell: destination, - instance: instance.clone(), - data, - })) - } - - fn copy_page( - &self, - source: Option>, - destination: Vector2, - context: &BrushMacroInstance, - ) -> Option { - let instance = context.settings::()?; - let guard = instance.data_ref(); - let used = guard - .cells - .keys() - .filter(|h| Some(h.page()) == source) - .map(|h| h.tile()) - .collect::>(); - let commands = guard - .cells - .iter() - .filter_map(|(handle, data)| { - if Some(handle.page()) == source { - let cell = TileDefinitionHandle::try_new(destination, handle.tile())?; - Some(Command::new(SetCellCommand { - brush: context.brush.clone(), - cell, - instance: instance.clone(), - data: Some(data.clone()), - })) - } else if handle.page() == destination && !used.contains(&handle.tile()) { - Some(Command::new(SetCellCommand { - brush: context.brush.clone(), - cell: *handle, - instance: instance.clone(), - data: None, - })) - } else { - None - } - }) - .collect::>(); - (!commands.is_empty()) - .then(|| Command::new(CommandGroup::from(commands).with_custom_name("Set Page Macros"))) - } - - fn build_instance_editor( - &mut self, - context: &BrushMacroInstance, - ctx: &mut BuildContext, - ) -> Option> { - let instance = context.settings::().unwrap(); - let instance = instance.data_ref(); - let pattern_id = instance - .pattern_property - .as_ref() - .map(|p| p.property_uuid()); - let frequency_id = instance - .frequency_property - .as_ref() - .map(|p| p.property_uuid()); - let tile_set = context.tile_set(); - let tile_set = tile_set.as_ref().map(|t| t.data_ref()); - let tile_set = tile_set.as_deref(); - self.pattern_list = MacroPropertyField::new( - WidgetBuilder::new().with_margin(Thickness::uniform(5.0)), - "Pattern Property".into(), - TileSetPropertyType::NineSlice, - pattern_id, - tile_set, - ctx, - ); - self.frequency_list = MacroPropertyField::new( - WidgetBuilder::new().with_margin(Thickness::uniform(5.0)), - "Frequency Property".into(), - TileSetPropertyType::F32, - frequency_id, - tile_set, - ctx, - ); - let failure_log_label = TextBuilder::new(WidgetBuilder::new()) - .with_text("Failure Log Level") - .build(ctx); - self.failure_log_list = make_failure_log_list( - WidgetBuilder::new().on_column(1), - instance.failure_log_kind, - ctx, - ); - let failure_log_field = GridBuilder::new( - WidgetBuilder::new() - .with_margin(Thickness::uniform(5.0)) - .with_child(failure_log_label) - .with_child(self.failure_log_list), - ) - .add_row(Column::auto()) - .add_column(Row::strict(PROPERTY_LABEL_WIDTH)) - .add_column(Row::stretch()) - .build(ctx); - let pattern_prop_help_text = - TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(5.0))) - .with_wrap(WrapMode::Word) - .with_text(PATTERN_PROP_DESC) - .build(ctx); - let freq_prop_help_text = - TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(5.0))) - .with_wrap(WrapMode::Word) - .with_text(FREQUENCY_PROP_DESC) - .build(ctx); - let handle = StackPanelBuilder::new( - WidgetBuilder::new() - .with_margin(Thickness::uniform(5.0)) - .with_child(pattern_prop_help_text) - .with_child(self.pattern_list.handle()) - .with_child(freq_prop_help_text) - .with_child(self.frequency_list.handle()) - .with_child(failure_log_field), - ) - .build(ctx); - Some(handle) - } - - fn build_cell_editor( - &mut self, - context: &BrushMacroCellContext, - ctx: &mut BuildContext, - ) -> Option> { - let instance = context.settings::().unwrap(); - let mut instance = instance.data_ref(); - let prop_id = instance - .pattern_property - .as_ref() - .map(|p| p.property_uuid()); - let value = context.cell.and_then(|c| instance.cells.get(&c)); - let tile_set = context.tile_set(); - let tile_set = tile_set.as_ref().map(|set| set.data_ref()); - let prop = if let (Some(tile_set), Some(uuid)) = (tile_set.as_ref(), prop_id) { - tile_set.find_property(*uuid) - } else { - None - }; - let terrain_id = value.map(|d| d.terrain_id).unwrap_or_default(); - let adjacent = value.map(|d| d.fill.include_adjacent).unwrap_or_default(); - let diagonal = value.map(|d| d.fill.include_diagonal).unwrap_or_default(); - let value_field = MacroPropertyValueField::new( - WidgetBuilder::new(), - "Terrain".into(), - TileSetPropertyValueElement::I8(terrain_id), - prop, - ctx, - ); - let adjacent_toggle = CheckBoxBuilder::new(WidgetBuilder::new()) - .checked(Some(adjacent)) - .build(ctx); - let diagonal_toggle = CheckBoxBuilder::new(WidgetBuilder::new()) - .checked(Some(diagonal)) - .build(ctx); - let adjacent_field = GridBuilder::new( - WidgetBuilder::new() - .with_child( - TextBuilder::new(WidgetBuilder::new().on_column(1)) - .with_text("Adjacent") - .build(ctx), - ) - .with_child(adjacent_toggle), - ) - .add_row(Row::auto()) - .add_column(Column::strict(20.0)) - .add_column(Column::stretch()) - .build(ctx); - let diagonal_field = GridBuilder::new( - WidgetBuilder::new() - .with_child( - TextBuilder::new(WidgetBuilder::new().on_column(1)) - .with_text("Diagonal") - .build(ctx), - ) - .with_child(diagonal_toggle), - ) - .add_row(Row::auto()) - .add_column(Column::strict(20.0)) - .add_column(Column::stretch()) - .build(ctx); - let handle = StackPanelBuilder::new( - WidgetBuilder::new() - .with_visibility(value.is_some()) - .with_child(value_field.handle()) - .with_child(adjacent_field) - .with_child(diagonal_field), - ) - .build(ctx); - instance.widgets.handle = handle; - instance.widgets.value_field = value_field; - instance.widgets.adjacent_toggle = adjacent_toggle; - instance.widgets.diagonal_toggle = diagonal_toggle; - Some(handle) - } - - fn sync_instance_editor(&mut self, context: &BrushMacroInstance, ui: &mut UserInterface) { - let Some(instance) = context.settings::() else { - return; - }; - let instance = instance.data_ref(); - let pattern_id = instance - .pattern_property - .as_ref() - .map(|p| p.property_uuid()); - let frequency_id = instance - .frequency_property - .as_ref() - .map(|p| p.property_uuid()); - let tile_set = context.tile_set(); - let tile_set = tile_set.as_ref().map(|t| t.data_ref()); - let tile_set = tile_set.as_deref(); - self.pattern_list.sync(pattern_id, tile_set, ui); - self.frequency_list.sync(frequency_id, tile_set, ui); - send_sync_message( - ui, - DropdownListMessage::selection( - self.failure_log_list, - MessageDirection::ToWidget, - Some(log_kind_to_index(instance.failure_log_kind)), - ), - ); - } - - fn sync_cell_editors(&mut self, context: &MacroMessageContext, ui: &mut UserInterface) { - for r in context.instances_with_uuid(*self.uuid()) { - let settings = r.try_cast::().unwrap(); - let settings = settings.data_ref(); - let prop_id = settings - .pattern_property - .as_ref() - .map(|prop| prop.property_uuid()); - let tile_set = context.tile_set(); - let tile_set = tile_set.as_ref().map(|t| t.data_ref()); - let prop = - prop_id.and_then(|id| tile_set.as_ref().and_then(|set| set.find_property(*id))); - let cell_data = context - .cell - .as_ref() - .and_then(|cell| settings.cells.get(cell)); - let value = cell_data.map(|cd| cd.terrain_id).unwrap_or_default(); - let value = TileSetPropertyValueElement::I8(value); - let adjacent = cell_data.map(|d| d.fill.include_adjacent); - let diagonal = cell_data.map(|d| d.fill.include_diagonal); - settings.widgets.value_field.sync(value, prop, ui); - send_sync_message( - ui, - CheckBoxMessage::checked( - settings.widgets.adjacent_toggle, - MessageDirection::ToWidget, - adjacent, - ), - ); - send_sync_message( - ui, - CheckBoxMessage::checked( - settings.widgets.diagonal_toggle, - MessageDirection::ToWidget, - diagonal, - ), - ); - ui.send_message(WidgetMessage::visibility( - settings.widgets.handle, - MessageDirection::ToWidget, - cell_data.is_some(), - )); - } - } - - fn begin_update( - &mut self, - context: &BrushMacroInstance, - stamp: &Stamp, - tile_map: &TileMapContext, - ) { - let Some(tile_set) = tile_map.tile_set() else { - self.context.clear(); - return; - }; - if context.tile_set().as_deref() != Some(tile_set) { - self.context.clear(); - return; - } - let instance = context.settings::().unwrap(); - let instance = instance.data_ref(); - let Some(pattern_property) = instance.pattern_property else { - self.context.clear(); - return; - }; - let frequency_property = instance.frequency_property; - // Check that at least one cell in the stamp is part of this instance. - if !stamp.values().any(|StampElement { source, .. }| { - source - .and_then(|s| s.handle()) - .is_some_and(|handle| instance.cells.contains_key(&handle)) - }) { - self.context.clear(); - return; - } - Log::verify(self.context.fill_pattern_map( - &tile_set.data_ref(), - pattern_property, - frequency_property, - )); - } - - fn amend_update( - &mut self, - context: &BrushMacroInstance, - update: &mut MacroTilesUpdate, - tile_map: &TileMap, - ) { - if self.context.is_empty() { - return; - } - let Some(instance) = &context.settings() else { - return; - }; - let instance = instance.data_ref(); - let instance = &instance; - let terrains = TileSetTerrainSource { update, instance }; - let Some(tile_set) = tile_map.tile_set() else { - return; - }; - let tile_set = tile_set.data_ref(); - let tile_set = &tile_set; - let Some(property_id) = instance.pattern_property else { - return; - }; - let patterns = TileSetPatternSource { - tile_map, - tile_set, - update, - property_id, - }; - self.constraints.fill_from(&terrains, &patterns); - self.autotiler.clear(); - self.autotiler.autotile(&TileSetAutoTileConstraint { - position_constraints: &self.constraints, - pattern_constraints: &self.context.patterns, - }); - self.autotiler - .apply_autotile_to_update(&mut thread_rng(), &self.context.values, update); - if let Some(kind) = instance.failure_log_kind { - for pos in self.constraints.all_positions() { - if !self.autotiler.contains_key(pos) { - Log::writeln(kind, "Autotile failed to find a tile."); - break; - } - } - } - } - - fn create_command( - &mut self, - _context: &BrushMacroInstance, - _update: &mut MacroTilesUpdate, - _tile_map: &TileMapContext, - ) -> Option { - None - } -} - -#[derive(Debug)] -struct SetCellCommand { - pub brush: TileMapBrushResource, - pub instance: Resource, - pub cell: TileDefinitionHandle, - pub data: Option, -} - -impl SetCellCommand { - fn swap(&mut self) { - let mut instance = self.instance.data_ref(); - swap_hash_map_entry(instance.cells.entry(self.cell), &mut self.data); - self.brush.data_ref().change_flag.set(); - } -} - -impl CommandTrait for SetCellCommand { - fn name(&mut self, _context: &dyn CommandContext) -> String { - "Update Autotile Cell".into() - } - - fn execute(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } - - fn revert(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } -} - -#[derive(Debug)] -struct MoveCellsCommand { - pub brush: TileMapBrushResource, - pub from: Box<[TileDefinitionHandle]>, - pub to: Box<[TileDefinitionHandle]>, - pub instance: Resource, - data: Box<[Option]>, -} - -impl MoveCellsCommand { - fn new( - brush: TileMapBrushResource, - from: Box<[TileDefinitionHandle]>, - to: Box<[TileDefinitionHandle]>, - instance: Resource, - ) -> Self { - Self { - data: vec![None; from.len()].into_boxed_slice(), - brush, - from, - to, - instance, - } - } - fn swap(&mut self) { - let mut instance = self.instance.data_ref(); - for (&from, data) in self.from.iter().zip(self.data.iter_mut()) { - swap_hash_map_entry(instance.cells.entry(from), data); - } - for (&to, data) in self.to.iter().zip(self.data.iter_mut()) { - swap_hash_map_entry(instance.cells.entry(to), data); - } - std::mem::swap(&mut self.from, &mut self.to); - self.brush.data_ref().change_flag.set(); - } -} - -impl CommandTrait for MoveCellsCommand { - fn name(&mut self, _context: &dyn CommandContext) -> String { - "Update Autotile Cell".into() - } - - fn execute(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } - - fn revert(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } -} - -#[derive(Debug)] -struct MovePagesCommand { - pub brush: TileMapBrushResource, - pub from: Box<[Vector2]>, - pub to: Box<[Vector2]>, - pub instance: Resource, - data: Box<[FxHashMap, CellData>]>, -} - -impl MovePagesCommand { - fn new( - brush: TileMapBrushResource, - from: Box<[Vector2]>, - to: Box<[Vector2]>, - instance: Resource, - ) -> Self { - Self { - data: vec![FxHashMap::default(); from.len()].into_boxed_slice(), - brush, - from, - to, - instance, - } - } - fn swap(&mut self) { - let mut instance = self.instance.data_ref(); - for (&from, data) in self.from.iter().zip(self.data.iter_mut()) { - swap_page(&mut instance.cells, from, data); - } - for (&to, data) in self.to.iter().zip(self.data.iter_mut()) { - swap_page(&mut instance.cells, to, data); - } - std::mem::swap(&mut self.from, &mut self.to); - self.brush.data_ref().change_flag.set(); - } -} - -fn swap_page( - map: &mut FxHashMap, - position: Vector2, - data: &mut FxHashMap, T>, -) { - let mut new_data = FxHashMap::default(); - map.retain(|h, d| { - if h.page() != position { - true - } else { - new_data.insert(h.tile(), std::mem::take(d)); - false - } - }); - map.extend( - data.drain() - .filter_map(|(p, d)| Some((TileDefinitionHandle::try_new(position, p)?, d))), - ); - *data = new_data; -} - -impl CommandTrait for MovePagesCommand { - fn name(&mut self, _context: &dyn CommandContext) -> String { - "Update Autotile Cell".into() - } - - fn execute(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } - - fn revert(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } -} - -#[derive(Debug)] -struct SetPatternPropCommand { - pub brush: TileMapBrushResource, - pub instance: Resource, - pub data: Option, -} - -impl SetPatternPropCommand { - fn swap(&mut self) { - let mut instance = self.instance.data_ref(); - std::mem::swap(&mut instance.pattern_property, &mut self.data); - self.brush.data_ref().change_flag.set(); - } -} - -impl CommandTrait for SetPatternPropCommand { - fn name(&mut self, _context: &dyn CommandContext) -> String { - "Update Autotile Property".into() - } - - fn execute(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } - - fn revert(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } -} - -#[derive(Debug)] -struct SetFrequencyPropCommand { - pub brush: TileMapBrushResource, - pub instance: Resource, - pub data: Option, -} - -impl SetFrequencyPropCommand { - fn swap(&mut self) { - let mut instance = self.instance.data_ref(); - std::mem::swap(&mut instance.frequency_property, &mut self.data); - self.brush.data_ref().change_flag.set(); - } -} - -impl CommandTrait for SetFrequencyPropCommand { - fn name(&mut self, _context: &dyn CommandContext) -> String { - "Update Autotile Property".into() - } - - fn execute(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } - - fn revert(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } -} - -#[derive(Debug)] -struct SetFailureKindCommand { - pub brush: TileMapBrushResource, - pub instance: Resource, - pub data: Option, -} - -impl SetFailureKindCommand { - fn swap(&mut self) { - let mut instance = self.instance.data_ref(); - std::mem::swap(&mut instance.failure_log_kind, &mut self.data); - self.brush.data_ref().change_flag.set(); - } -} - -impl CommandTrait for SetFailureKindCommand { - fn name(&mut self, _context: &dyn CommandContext) -> String { - "Update Autotile Failure Level".into() - } - - fn execute(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } - - fn revert(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } -} +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use fyrox::{ + asset::{untyped::UntypedResource, Resource, ResourceData}, + autotile::{ConstraintFillRules, NeededTerrain, TerrainSource}, + core::{log::MessageKind, swap_hash_map_entry}, + fxhash::FxHashMap, + gui::{ + check_box::{CheckBoxBuilder, CheckBoxMessage}, + dropdown_list::{DropdownListBuilder, DropdownListMessage}, + formatted_text::WrapMode, + stack_panel::StackPanelBuilder, + }, + rand::thread_rng, + scene::tilemap::{ + brush::TileMapBrushResource, + tileset::{ + TileSetPropertyF32, TileSetPropertyId, TileSetPropertyNine, TileSetPropertyType, + TileSetPropertyValueElement, + }, + MacroTilesUpdate, TileSetAutoTileConstraint, TileSetAutoTileContext, TileSetAutoTiler, + TileSetConstraintMap, TileSetPatternSource, TileTerrainId, + }, +}; + +use crate::{ + command::{Command, CommandContext, CommandGroup, CommandTrait}, + send_sync_message, +}; + +use super::*; + +const PATTERN_PROP_DESC: &str = concat!("Choose a nine-slice property from the tile set. ", + "This property will provide the pattern that the autotiler uses to know whether two tiles match along each edge. ", + "The central value of the nine is used to determine which tiles are permited in particular cell and how tiles are prioritized."); + +const FREQUENCY_PROP_DESC: &str = concat!("Choose a float property from the tile set. ", + "This property will provide the frequency that the autotiler uses to know know often to choose a tile when there is more than one ", + "tile with the same pattern."); + +const PROPERTY_LABEL_WIDTH: f32 = 150.0; + +fn log_kind_to_index(log_kind: Option) -> usize { + match log_kind { + None => 0, + Some(MessageKind::Information) => 1, + Some(MessageKind::Warning) => 2, + Some(MessageKind::Error) => 3, + } +} + +fn index_to_log_kind(index: usize) -> Option { + match index { + 0 => None, + 1 => Some(MessageKind::Information), + 2 => Some(MessageKind::Warning), + 3 => Some(MessageKind::Error), + _ => None, + } +} + +fn make_failure_log_list( + widget_builder: WidgetBuilder, + log_kind: Option, + ctx: &mut BuildContext, +) -> Handle { + let items = vec![ + make_list_item("None", ctx), + make_list_item("Info", ctx), + make_list_item("Warn", ctx), + make_list_item("Error", ctx), + ]; + DropdownListBuilder::new(widget_builder) + .with_selected(log_kind_to_index(log_kind)) + .with_items(items) + .build(ctx) +} + +fn make_list_item(text: &str, ctx: &mut BuildContext) -> Handle { + let content = TextBuilder::new(WidgetBuilder::new().on_column(1)) + .with_vertical_text_alignment(VerticalAlignment::Center) + .with_horizontal_text_alignment(HorizontalAlignment::Left) + .with_text(text) + .build(ctx); + DecoratorBuilder::new( + BorderBuilder::new(WidgetBuilder::new().with_child(content)) + .with_corner_radius(4.0.into()) + .with_pad_by_corner_radius(false), + ) + .build(ctx) +} + +#[derive(Default)] +pub struct AutoTileMacro { + pattern_list: MacroPropertyField, + frequency_list: MacroPropertyField, + failure_log_list: Handle, + context: TileSetAutoTileContext, + constraints: TileSetConstraintMap, + autotiler: TileSetAutoTiler, +} + +#[derive(Default, Debug, Clone, Reflect)] +struct CellData { + terrain_id: TileTerrainId, + #[reflect(hidden)] + fill: ConstraintFillRules, +} + +impl Visit for CellData { + fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult { + let mut region = visitor.enter_region(name)?; + self.terrain_id.visit("TerrainId", &mut region)?; + let mut region = region.enter_region("Fill")?; + self.fill + .include_adjacent + .visit("IncludeAdjacent", &mut region)?; + self.fill + .include_diagonal + .visit("IncludeDiagonal", &mut region) + } +} + +#[derive(Debug, Default, Clone, Visit, Reflect, TypeUuidProvider)] +#[type_uuid(id = "b320543d-3df0-43fd-b0d9-60a398f49853")] +pub(super) struct AutoTileInstance { + frequency_property: Option, + pattern_property: Option, + cells: FxHashMap, + #[visit(optional)] + failure_log_kind: Option, + #[reflect(hidden)] + #[visit(skip)] + widgets: InstanceCellWidgets, +} + +#[derive(Debug, Default, Clone)] +struct InstanceCellWidgets { + handle: Handle, + value_field: MacroPropertyValueField, + adjacent_toggle: Handle, + diagonal_toggle: Handle, +} + +impl ResourceData for AutoTileInstance { + fn type_uuid(&self) -> Uuid { + ::type_uuid() + } + + fn save(&mut self, _path: &std::path::Path) -> Result<(), Box> { + Err("Saving is not supported!".to_string().into()) + } + + fn can_be_saved(&self) -> bool { + false + } + + fn try_clone_box(&self) -> Option> { + Some(Box::new(self.clone())) + } +} + +pub struct TileSetTerrainSource<'a, 'b> { + update: &'a MacroTilesUpdate, + instance: &'b AutoTileInstance, +} + +impl TerrainSource for TileSetTerrainSource<'_, '_> { + type Position = Vector2; + type Terrain = TileTerrainId; + + fn iter(&self) -> impl Iterator, TileTerrainId>> + '_ { + self.update.iter().filter_map(|(p, v)| { + let cell_data = self.instance.cells.get(&v.as_ref()?.source?.handle()?)?; + Some(NeededTerrain { + position: *p, + terrain: cell_data.terrain_id, + fill: cell_data.fill, + }) + }) + } + + fn contains_position(&self, position: &Self::Position) -> bool { + let Some(el) = self.update.get(position).cloned().flatten() else { + return false; + }; + let Some(brush_cell) = el.source.and_then(|s| s.handle()) else { + return false; + }; + self.instance.cells.contains_key(&brush_cell) + } +} + +impl BrushMacro for AutoTileMacro { + fn uuid(&self) -> &Uuid { + &uuid!("ab05f992-2591-4729-8164-7b5cc1141d72") + } + + fn name(&self) -> &'static str { + "Autotiler" + } + + fn on_instance_ui_message( + &mut self, + context: &BrushMacroInstance, + message: &UiMessage, + editor: &mut Editor, + ) { + let ui = editor.engine.user_interfaces.first_mut(); + let Some(tile_set) = context.tile_set() else { + return; + }; + if let Some(TileSetPropertyMessage(uuid)) = message.data() { + if message.destination() == self.pattern_list.handle() { + editor.message_sender.do_command(SetPatternPropCommand { + brush: context.brush.clone(), + instance: context.settings().unwrap(), + data: uuid.map(TileSetPropertyNine), + }); + } else if message.destination() == self.frequency_list.handle() { + editor.message_sender.do_command(SetFrequencyPropCommand { + brush: context.brush.clone(), + instance: context.settings().unwrap(), + data: uuid.map(TileSetPropertyF32), + }); + } + } else { + if let Some(DropdownListMessage::SelectionChanged(Some(index))) = message.data() { + if message.destination() == self.failure_log_list + && message.direction() == MessageDirection::FromWidget + { + editor.message_sender.do_command(SetFailureKindCommand { + brush: context.brush.clone(), + instance: context.settings().unwrap(), + data: index_to_log_kind(*index), + }); + } + } + self.pattern_list + .on_ui_message(&tile_set.data_ref(), message, ui); + self.frequency_list + .on_ui_message(&tile_set.data_ref(), message, ui); + } + } + + fn on_cell_ui_message( + &mut self, + context: &MacroMessageContext, + message: &UiMessage, + editor: &mut Editor, + ) { + let Some(cell) = context.cell else { + return; + }; + let ui = editor.engine.user_interfaces.first_mut(); + for r in context.instances_with_uuid(*self.uuid()) { + let instance = r.try_cast::().unwrap(); + let mut settings = instance.data_ref(); + let prop_id = settings + .pattern_property + .as_ref() + .map(|prop| prop.property_uuid()); + let tile_set = context.tile_set(); + let tile_set = tile_set.as_ref().map(|t| t.data_ref()); + let prop = + prop_id.and_then(|id| tile_set.as_ref().and_then(|set| set.find_property(*id))); + settings + .widgets + .value_field + .on_ui_message(prop, message, ui); + let cell_data = settings.cells.get(&cell).cloned().unwrap_or_default(); + if let Some(&TileSetPropertyValueMessage(TileSetPropertyValueElement::I8(v))) = + message.data() + { + if message.destination() == settings.widgets.value_field.handle() { + drop(settings); + editor.message_sender.do_command(SetCellCommand { + brush: context.brush.clone(), + instance, + cell, + data: Some(CellData { + terrain_id: v, + ..cell_data + }), + }); + } + } else if let Some(&CheckBoxMessage::Check(Some(checked))) = message.data() { + if message.destination() == settings.widgets.adjacent_toggle { + drop(settings); + editor.message_sender.do_command(SetCellCommand { + brush: context.brush.clone(), + instance, + cell, + data: Some(CellData { + fill: ConstraintFillRules { + include_adjacent: checked, + ..cell_data.fill + }, + ..cell_data + }), + }); + } else if message.destination() == settings.widgets.diagonal_toggle { + drop(settings); + editor.message_sender.do_command(SetCellCommand { + brush: context.brush.clone(), + instance, + cell, + data: Some(CellData { + fill: ConstraintFillRules { + include_diagonal: checked, + ..cell_data.fill + }, + ..cell_data + }), + }); + } + } + } + } + + fn create_instance(&self, _brush: &TileMapBrushResource) -> Option { + Some(UntypedResource::new_embedded(AutoTileInstance::default())) + } + + fn can_create_cell(&self) -> bool { + true + } + + fn fill_cell_set( + &self, + context: &BrushMacroInstance, + cell_set: &mut FxHashSet, + ) { + let Some(data) = context + .settings + .as_ref() + .and_then(|r| r.try_cast::()) + else { + return; + }; + let data = data.data_ref(); + cell_set.extend(data.cells.keys()); + } + + fn create_cell(&self, context: &BrushMacroCellContext) -> Option { + let instance = context.settings()?; + let cell = context.cell?; + Some(Command::new(SetCellCommand { + brush: context.brush.clone(), + cell, + instance, + data: Some(CellData::default()), + })) + } + + fn move_cells( + &self, + from: Box<[TileDefinitionHandle]>, + to: Box<[TileDefinitionHandle]>, + context: &BrushMacroInstance, + ) -> Option { + let instance = context.settings::()?; + Some(Command::new(MoveCellsCommand::new( + context.brush.clone(), + from, + to, + instance, + ))) + } + + fn move_pages( + &self, + from: Box<[Vector2]>, + to: Box<[Vector2]>, + context: &BrushMacroInstance, + ) -> Option { + let instance = context.settings::()?; + Some(Command::new(MovePagesCommand::new( + context.brush.clone(), + from, + to, + instance, + ))) + } + + fn copy_cell( + &self, + source: Option, + destination: TileDefinitionHandle, + context: &BrushMacroInstance, + ) -> Option { + let instance = context.settings::()?; + let guard = instance.data_ref(); + let data = if let Some(source) = source { + guard.cells.get(&source).cloned() + } else { + None + }; + Some(Command::new(SetCellCommand { + brush: context.brush.clone(), + cell: destination, + instance: instance.clone(), + data, + })) + } + + fn copy_page( + &self, + source: Option>, + destination: Vector2, + context: &BrushMacroInstance, + ) -> Option { + let instance = context.settings::()?; + let guard = instance.data_ref(); + let used = guard + .cells + .keys() + .filter(|h| Some(h.page()) == source) + .map(|h| h.tile()) + .collect::>(); + let commands = guard + .cells + .iter() + .filter_map(|(handle, data)| { + if Some(handle.page()) == source { + let cell = TileDefinitionHandle::try_new(destination, handle.tile())?; + Some(Command::new(SetCellCommand { + brush: context.brush.clone(), + cell, + instance: instance.clone(), + data: Some(data.clone()), + })) + } else if handle.page() == destination && !used.contains(&handle.tile()) { + Some(Command::new(SetCellCommand { + brush: context.brush.clone(), + cell: *handle, + instance: instance.clone(), + data: None, + })) + } else { + None + } + }) + .collect::>(); + (!commands.is_empty()) + .then(|| Command::new(CommandGroup::from(commands).with_custom_name("Set Page Macros"))) + } + + fn build_instance_editor( + &mut self, + context: &BrushMacroInstance, + ctx: &mut BuildContext, + ) -> Option> { + let instance = context.settings::().unwrap(); + let instance = instance.data_ref(); + let pattern_id = instance + .pattern_property + .as_ref() + .map(|p| p.property_uuid()); + let frequency_id = instance + .frequency_property + .as_ref() + .map(|p| p.property_uuid()); + let tile_set = context.tile_set(); + let tile_set = tile_set.as_ref().map(|t| t.data_ref()); + let tile_set = tile_set.as_deref(); + self.pattern_list = MacroPropertyField::new( + WidgetBuilder::new().with_margin(Thickness::uniform(5.0)), + "Pattern Property".into(), + TileSetPropertyType::NineSlice, + pattern_id, + tile_set, + ctx, + ); + self.frequency_list = MacroPropertyField::new( + WidgetBuilder::new().with_margin(Thickness::uniform(5.0)), + "Frequency Property".into(), + TileSetPropertyType::F32, + frequency_id, + tile_set, + ctx, + ); + let failure_log_label = TextBuilder::new(WidgetBuilder::new()) + .with_text("Failure Log Level") + .build(ctx); + self.failure_log_list = make_failure_log_list( + WidgetBuilder::new().on_column(1), + instance.failure_log_kind, + ctx, + ); + let failure_log_field = GridBuilder::new( + WidgetBuilder::new() + .with_margin(Thickness::uniform(5.0)) + .with_child(failure_log_label) + .with_child(self.failure_log_list), + ) + .add_row(Column::auto()) + .add_column(Row::strict(PROPERTY_LABEL_WIDTH)) + .add_column(Row::stretch()) + .build(ctx); + let pattern_prop_help_text = + TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(5.0))) + .with_wrap(WrapMode::Word) + .with_text(PATTERN_PROP_DESC) + .build(ctx); + let freq_prop_help_text = + TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(5.0))) + .with_wrap(WrapMode::Word) + .with_text(FREQUENCY_PROP_DESC) + .build(ctx); + let handle = StackPanelBuilder::new( + WidgetBuilder::new() + .with_margin(Thickness::uniform(5.0)) + .with_child(pattern_prop_help_text) + .with_child(self.pattern_list.handle()) + .with_child(freq_prop_help_text) + .with_child(self.frequency_list.handle()) + .with_child(failure_log_field), + ) + .build(ctx); + Some(handle) + } + + fn build_cell_editor( + &mut self, + context: &BrushMacroCellContext, + ctx: &mut BuildContext, + ) -> Option> { + let instance = context.settings::().unwrap(); + let mut instance = instance.data_ref(); + let prop_id = instance + .pattern_property + .as_ref() + .map(|p| p.property_uuid()); + let value = context.cell.and_then(|c| instance.cells.get(&c)); + let tile_set = context.tile_set(); + let tile_set = tile_set.as_ref().map(|set| set.data_ref()); + let prop = if let (Some(tile_set), Some(uuid)) = (tile_set.as_ref(), prop_id) { + tile_set.find_property(*uuid) + } else { + None + }; + let terrain_id = value.map(|d| d.terrain_id).unwrap_or_default(); + let adjacent = value.map(|d| d.fill.include_adjacent).unwrap_or_default(); + let diagonal = value.map(|d| d.fill.include_diagonal).unwrap_or_default(); + let value_field = MacroPropertyValueField::new( + WidgetBuilder::new(), + "Terrain".into(), + TileSetPropertyValueElement::I8(terrain_id), + prop, + ctx, + ); + let adjacent_toggle = CheckBoxBuilder::new(WidgetBuilder::new()) + .checked(Some(adjacent)) + .build(ctx); + let diagonal_toggle = CheckBoxBuilder::new(WidgetBuilder::new()) + .checked(Some(diagonal)) + .build(ctx); + let adjacent_field = GridBuilder::new( + WidgetBuilder::new() + .with_child( + TextBuilder::new(WidgetBuilder::new().on_column(1)) + .with_text("Adjacent") + .build(ctx), + ) + .with_child(adjacent_toggle), + ) + .add_row(Row::auto()) + .add_column(Column::strict(20.0)) + .add_column(Column::stretch()) + .build(ctx); + let diagonal_field = GridBuilder::new( + WidgetBuilder::new() + .with_child( + TextBuilder::new(WidgetBuilder::new().on_column(1)) + .with_text("Diagonal") + .build(ctx), + ) + .with_child(diagonal_toggle), + ) + .add_row(Row::auto()) + .add_column(Column::strict(20.0)) + .add_column(Column::stretch()) + .build(ctx); + let handle = StackPanelBuilder::new( + WidgetBuilder::new() + .with_visibility(value.is_some()) + .with_child(value_field.handle()) + .with_child(adjacent_field) + .with_child(diagonal_field), + ) + .build(ctx); + instance.widgets.handle = handle; + instance.widgets.value_field = value_field; + instance.widgets.adjacent_toggle = adjacent_toggle; + instance.widgets.diagonal_toggle = diagonal_toggle; + Some(handle) + } + + fn sync_instance_editor(&mut self, context: &BrushMacroInstance, ui: &mut UserInterface) { + let Some(instance) = context.settings::() else { + return; + }; + let instance = instance.data_ref(); + let pattern_id = instance + .pattern_property + .as_ref() + .map(|p| p.property_uuid()); + let frequency_id = instance + .frequency_property + .as_ref() + .map(|p| p.property_uuid()); + let tile_set = context.tile_set(); + let tile_set = tile_set.as_ref().map(|t| t.data_ref()); + let tile_set = tile_set.as_deref(); + self.pattern_list.sync(pattern_id, tile_set, ui); + self.frequency_list.sync(frequency_id, tile_set, ui); + send_sync_message( + ui, + DropdownListMessage::selection( + self.failure_log_list, + MessageDirection::ToWidget, + Some(log_kind_to_index(instance.failure_log_kind)), + ), + ); + } + + fn sync_cell_editors(&mut self, context: &MacroMessageContext, ui: &mut UserInterface) { + for r in context.instances_with_uuid(*self.uuid()) { + let settings = r.try_cast::().unwrap(); + let settings = settings.data_ref(); + let prop_id = settings + .pattern_property + .as_ref() + .map(|prop| prop.property_uuid()); + let tile_set = context.tile_set(); + let tile_set = tile_set.as_ref().map(|t| t.data_ref()); + let prop = + prop_id.and_then(|id| tile_set.as_ref().and_then(|set| set.find_property(*id))); + let cell_data = context + .cell + .as_ref() + .and_then(|cell| settings.cells.get(cell)); + let value = cell_data.map(|cd| cd.terrain_id).unwrap_or_default(); + let value = TileSetPropertyValueElement::I8(value); + let adjacent = cell_data.map(|d| d.fill.include_adjacent); + let diagonal = cell_data.map(|d| d.fill.include_diagonal); + settings.widgets.value_field.sync(value, prop, ui); + send_sync_message( + ui, + CheckBoxMessage::checked( + settings.widgets.adjacent_toggle, + MessageDirection::ToWidget, + adjacent, + ), + ); + send_sync_message( + ui, + CheckBoxMessage::checked( + settings.widgets.diagonal_toggle, + MessageDirection::ToWidget, + diagonal, + ), + ); + ui.send_message(WidgetMessage::visibility( + settings.widgets.handle, + MessageDirection::ToWidget, + cell_data.is_some(), + )); + } + } + + fn begin_update( + &mut self, + context: &BrushMacroInstance, + stamp: &Stamp, + tile_map: &TileMapContext, + ) { + let Some(tile_set) = tile_map.tile_set() else { + self.context.clear(); + return; + }; + if context.tile_set().as_deref() != Some(tile_set) { + self.context.clear(); + return; + } + let instance = context.settings::().unwrap(); + let instance = instance.data_ref(); + let Some(pattern_property) = instance.pattern_property else { + self.context.clear(); + return; + }; + let frequency_property = instance.frequency_property; + // Check that at least one cell in the stamp is part of this instance. + if !stamp.values().any(|StampElement { source, .. }| { + source + .and_then(|s| s.handle()) + .is_some_and(|handle| instance.cells.contains_key(&handle)) + }) { + self.context.clear(); + return; + } + Log::verify(self.context.fill_pattern_map( + &tile_set.data_ref(), + pattern_property, + frequency_property, + )); + } + + fn amend_update( + &mut self, + context: &BrushMacroInstance, + update: &mut MacroTilesUpdate, + tile_map: &TileMap, + ) { + if self.context.is_empty() { + return; + } + let Some(instance) = &context.settings() else { + return; + }; + let instance = instance.data_ref(); + let instance = &instance; + let terrains = TileSetTerrainSource { update, instance }; + let Some(tile_set) = tile_map.tile_set() else { + return; + }; + let tile_set = tile_set.data_ref(); + let tile_set = &tile_set; + let Some(property_id) = instance.pattern_property else { + return; + }; + let patterns = TileSetPatternSource { + tile_map, + tile_set, + update, + property_id, + }; + self.constraints.fill_from(&terrains, &patterns); + self.autotiler.clear(); + self.autotiler.autotile(&TileSetAutoTileConstraint { + position_constraints: &self.constraints, + pattern_constraints: &self.context.patterns, + }); + self.autotiler + .apply_autotile_to_update(&mut thread_rng(), &self.context.values, update); + if let Some(kind) = instance.failure_log_kind { + for pos in self.constraints.all_positions() { + if !self.autotiler.contains_key(pos) { + Log::writeln(kind, "Autotile failed to find a tile."); + break; + } + } + } + } + + fn create_command( + &mut self, + _context: &BrushMacroInstance, + _update: &mut MacroTilesUpdate, + _tile_map: &TileMapContext, + ) -> Option { + None + } +} + +#[derive(Debug)] +struct SetCellCommand { + pub brush: TileMapBrushResource, + pub instance: Resource, + pub cell: TileDefinitionHandle, + pub data: Option, +} + +impl SetCellCommand { + fn swap(&mut self) { + let mut instance = self.instance.data_ref(); + swap_hash_map_entry(instance.cells.entry(self.cell), &mut self.data); + self.brush.data_ref().change_flag.set(); + } +} + +impl CommandTrait for SetCellCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Update Autotile Cell".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } +} + +#[derive(Debug)] +struct MoveCellsCommand { + pub brush: TileMapBrushResource, + pub from: Box<[TileDefinitionHandle]>, + pub to: Box<[TileDefinitionHandle]>, + pub instance: Resource, + data: Box<[Option]>, +} + +impl MoveCellsCommand { + fn new( + brush: TileMapBrushResource, + from: Box<[TileDefinitionHandle]>, + to: Box<[TileDefinitionHandle]>, + instance: Resource, + ) -> Self { + Self { + data: vec![None; from.len()].into_boxed_slice(), + brush, + from, + to, + instance, + } + } + fn swap(&mut self) { + let mut instance = self.instance.data_ref(); + for (&from, data) in self.from.iter().zip(self.data.iter_mut()) { + swap_hash_map_entry(instance.cells.entry(from), data); + } + for (&to, data) in self.to.iter().zip(self.data.iter_mut()) { + swap_hash_map_entry(instance.cells.entry(to), data); + } + std::mem::swap(&mut self.from, &mut self.to); + self.brush.data_ref().change_flag.set(); + } +} + +impl CommandTrait for MoveCellsCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Update Autotile Cell".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } +} + +#[derive(Debug)] +struct MovePagesCommand { + pub brush: TileMapBrushResource, + pub from: Box<[Vector2]>, + pub to: Box<[Vector2]>, + pub instance: Resource, + data: Box<[FxHashMap, CellData>]>, +} + +impl MovePagesCommand { + fn new( + brush: TileMapBrushResource, + from: Box<[Vector2]>, + to: Box<[Vector2]>, + instance: Resource, + ) -> Self { + Self { + data: vec![FxHashMap::default(); from.len()].into_boxed_slice(), + brush, + from, + to, + instance, + } + } + fn swap(&mut self) { + let mut instance = self.instance.data_ref(); + for (&from, data) in self.from.iter().zip(self.data.iter_mut()) { + swap_page(&mut instance.cells, from, data); + } + for (&to, data) in self.to.iter().zip(self.data.iter_mut()) { + swap_page(&mut instance.cells, to, data); + } + std::mem::swap(&mut self.from, &mut self.to); + self.brush.data_ref().change_flag.set(); + } +} + +fn swap_page( + map: &mut FxHashMap, + position: Vector2, + data: &mut FxHashMap, T>, +) { + let mut new_data = FxHashMap::default(); + map.retain(|h, d| { + if h.page() != position { + true + } else { + new_data.insert(h.tile(), std::mem::take(d)); + false + } + }); + map.extend( + data.drain() + .filter_map(|(p, d)| Some((TileDefinitionHandle::try_new(position, p)?, d))), + ); + *data = new_data; +} + +impl CommandTrait for MovePagesCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Update Autotile Cell".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } +} + +#[derive(Debug)] +struct SetPatternPropCommand { + pub brush: TileMapBrushResource, + pub instance: Resource, + pub data: Option, +} + +impl SetPatternPropCommand { + fn swap(&mut self) { + let mut instance = self.instance.data_ref(); + std::mem::swap(&mut instance.pattern_property, &mut self.data); + self.brush.data_ref().change_flag.set(); + } +} + +impl CommandTrait for SetPatternPropCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Update Autotile Property".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } +} + +#[derive(Debug)] +struct SetFrequencyPropCommand { + pub brush: TileMapBrushResource, + pub instance: Resource, + pub data: Option, +} + +impl SetFrequencyPropCommand { + fn swap(&mut self) { + let mut instance = self.instance.data_ref(); + std::mem::swap(&mut instance.frequency_property, &mut self.data); + self.brush.data_ref().change_flag.set(); + } +} + +impl CommandTrait for SetFrequencyPropCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Update Autotile Property".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } +} + +#[derive(Debug)] +struct SetFailureKindCommand { + pub brush: TileMapBrushResource, + pub instance: Resource, + pub data: Option, +} + +impl SetFailureKindCommand { + fn swap(&mut self) { + let mut instance = self.instance.data_ref(); + std::mem::swap(&mut instance.failure_log_kind, &mut self.data); + self.brush.data_ref().change_flag.set(); + } +} + +impl CommandTrait for SetFailureKindCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Update Autotile Failure Level".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } +} diff --git a/editor/src/plugins/tilemap/brush_macro.rs b/editor/src/plugins/tilemap/brush_macro.rs index 52a84d3fc..0bad73673 100644 --- a/editor/src/plugins/tilemap/brush_macro.rs +++ b/editor/src/plugins/tilemap/brush_macro.rs @@ -1,1033 +1,1033 @@ -// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -use fyrox::{ - asset::{untyped::UntypedResource, Resource, ResourceData, ResourceDataRef}, - core::{ - futures::executor::block_on, - log::Log, - parking_lot::{lock_api::MutexGuard, RawMutex}, - }, - gui::{ - dropdown_list::{DropdownListBuilder, DropdownListMessage}, - grid::{Column, GridBuilder, Row}, - numeric::{NumericUpDownBuilder, NumericUpDownMessage}, - text::{TextBuilder, TextMessage}, - text_box::TextBoxBuilder, - }, - scene::tilemap::{ - brush::{BrushMacroData, TileMapBrush, TileMapBrushResource}, - tileset::{ - NamableValue, TileSetPropertyLayer, TileSetPropertyType, TileSetPropertyValueElement, - }, - MacroTilesUpdate, - }, -}; - -use crate::{ - command::{Command, CommandContext, CommandTrait}, - send_sync_message, -}; - -use super::*; - -const PROPERTY_LABEL_WIDTH: f32 = 150.0; -const UNKNOWN_PROPERTY: &str = "UNKNOWN PROPERTY"; - -/// An Arc Mutex reference to a [`BrushMacroList`] that allows multiple objects -/// to share access to a common macro list. Among the things that need to share -/// the list are the tile map interaction mode and the [`TileSetEditor`]. -#[derive(Default, Clone)] -pub struct BrushMacroListRef(Arc>); - -impl BrushMacroListRef { - /// Create a new reference and assign it to point to the given macro list. - pub fn new(list: BrushMacroList) -> Self { - Self(Arc::new(Mutex::new(list))) - } - /// Access the macro list. - pub fn lock(&self) -> MutexGuard { - self.0 - .try_lock() - .expect("BrushMacroList lock should not fail") - } -} - -/// An Arc Mutex reference to a [`MacroCellSetList`] which allows -/// shared access to the positions of cells which are currently involved -/// in some macro. This is only used in the [`TileSetEditor`], but it needs -/// to be shared with the [`PaletteWidget`] so that the widget can -/// render a highlight at those positions. -#[derive(Default, Clone)] -pub struct MacroCellSetListRef(Arc>); - -impl MacroCellSetListRef { - /// Create a new reference and assign it to point to the given set of cells. - pub fn new(list: MacroCellSetList) -> Self { - Self(Arc::new(Mutex::new(list))) - } - /// Access the set of cells. - pub fn lock(&self) -> MutexGuard { - self.0.try_lock().expect("MacroCellSetList already in use") - } -} - -/// A context for calling some [`BrushMacro`] methods when using the macro -/// to modify a tile map. It gives the macro extensive access to whatever it -/// might need for a wide variety of automatic actions that may be desired -/// along with tile map editing. -pub struct TileMapContext<'a> { - /// The handle for the tile map that is being edited. - pub node: Handle, - /// The handle for the scene that contains the tile map. - pub scene: Handle, - /// The engine that contains the scene. - pub engine: &'a mut Engine, -} - -impl TileMapContext<'_> { - /// The tile map that is being edited. - pub fn tile_map(&self) -> &TileMap { - self.engine.scenes[self.scene].graph[self.node] - .cast() - .unwrap() - } - /// The tile set resource from within the tile map, if the tile map has one. - pub fn tile_set(&self) -> Option<&TileSetResource> { - self.tile_map().tile_set() - } -} - -/// Calling some methods of [`BrushMacro`] may require access to the brush resource -/// and to the [`UntypedResource`] that the brush holds to represent the configuration -/// settings for an instance of the macro. -#[derive(Debug, Clone)] -pub struct BrushMacroInstance { - /// The brush that has an instance of the macro. - pub brush: TileMapBrushResource, - /// The configuration for an instance of the macro. - /// Some macros may not require any configuration, so this resource is optional. - pub settings: Option, -} - -impl BrushMacroInstance { - /// A typed reference to the configuration resource. - pub fn settings(&self) -> Option> - where - T: ResourceData + Default + TypeUuidProvider, - { - self.settings.as_ref()?.try_cast() - } - /// A data ref to the brush resource. - pub fn brush(&self) -> ResourceDataRef { - self.brush.data_ref() - } - /// The tile set resource taken from the brush. - pub fn tile_set(&self) -> Option { - TileSetFromBrush::try_new(&self.brush) - } -} - -/// The context required for handling UI messages for some particular selected -/// brush cell. This is used for both received [`UiMessage`] and when the brush -/// cell settings need to sync to the brush resource. -#[derive(Debug, Clone)] -pub struct MacroMessageContext { - /// The brush that has an instance of the macro. - pub brush: TileMapBrushResource, - /// The currently selected brush cell, or None if no cell is selected. - pub cell: Option, -} - -impl From for MacroMessageContext { - fn from(value: BrushMacroCellContext) -> Self { - Self { - brush: value.brush, - cell: value.cell, - } - } -} - -impl MacroMessageContext { - /// A reference to the brush's data. - pub fn brush(&self) -> ResourceDataRef { - self.brush.data_ref() - } - /// The tile set taken from the brush. - pub fn tile_set(&self) -> Option { - TileSetFromBrush::try_new(&self.brush) - } - /// A copy of the list of configuration resources for the macro instances - /// that match the given UUID. This is copied instead of being borrowed - /// to avoid holding a lock on the brush resource while iterating through - /// the macro instances. - pub fn instances_with_uuid(&self, uuid: Uuid) -> Vec { - self.brush - .data_ref() - .macros - .instances_with_uuid(uuid) - .cloned() - .collect() - } -} - -/// Context for methods of [`BrushMacro`] that are specific to a single brush cell, -/// such as adding a new cell to the macro, removing a cell, or building the widgets -/// for editing the settings of a cell. -#[derive(Debug, Clone)] -pub struct BrushMacroCellContext { - /// The brush that has an instance of the macro. - pub brush: TileMapBrushResource, - /// The configuration for an instance of the macro. - pub settings: Option, - /// The currently selected brush cell, or None if no cell is selected. - pub cell: Option, -} - -impl From for BrushMacroInstance { - fn from(value: BrushMacroCellContext) -> Self { - Self { - brush: value.brush, - settings: value.settings, - } - } -} - -impl BrushMacroCellContext { - /// A typed reference to the configuration resource. - pub fn settings(&self) -> Option> - where - T: ResourceData + Default + TypeUuidProvider, - { - self.settings.as_ref()?.try_cast() - } - /// A reference to the brush's data. - pub fn brush(&self) -> ResourceDataRef { - self.brush.data_ref() - } - /// The tile set resource taken from the brush. - pub fn tile_set(&self) -> Option { - TileSetFromBrush::try_new(&self.brush) - } -} - -/// Access to the tile set of a brush through a lock on the brush's data. -pub struct TileSetFromBrush<'a>(ResourceDataRef<'a, TileMapBrush>); - -impl Deref for TileSetFromBrush<'_> { - type Target = Resource; - - fn deref(&self) -> &Self::Target { - self.0.tile_set.as_ref().unwrap() - } -} - -impl<'a> TileSetFromBrush<'a> { - /// Construct access to the tile set of the given brush after confirming that - /// the brush is loaded, the brush has a tile set, and the tile set is loaded. - pub fn try_new(brush: &'a TileMapBrushResource) -> Option { - Log::verify(block_on(brush.clone())); - if !brush.is_ok() { - return None; - } - let brush_guard = brush.data_ref(); - let tile_set = brush_guard.tile_set.clone(); - drop(brush_guard); - if let Some(tile_set) = tile_set { - Log::verify(block_on(tile_set.clone())); - if !tile_set.is_ok() { - return None; - } - } - Some(Self(brush.data_ref())) - } -} - -/// The trait that defines the operation necessary for implementing a macro for a tile map brush. -pub trait BrushMacro: 'static + Send + Sync { - /// The UUID that identifies an instance of a macro as belonging to this macro. - /// One macro may have several instances, and all of them will share this UUID. - fn uuid(&self) -> &Uuid; - /// The name of the macro that will appear in the brush editor. - fn name(&self) -> &'static str; - /// Handle a UI message that was received while the brush editor was displaying the settings - /// for an instance of this macro in the Macros tab. - fn on_instance_ui_message( - &mut self, - context: &BrushMacroInstance, - message: &UiMessage, - editor: &mut Editor, - ); - /// Handle a UI message that was received while a particular brush cell was selected in the editor, - /// or when no brush cell is selected. Each macro needs to keep track of the widgets that it uses to - /// display the settings for cells in the tile inspector, and this method is where messages from those - /// widgets should be handled. - fn on_cell_ui_message( - &mut self, - context: &MacroMessageContext, - message: &UiMessage, - editor: &mut Editor, - ); - /// Create a resource to store the settings of this macro. - /// A return value of None indicates that his macro needs no settings resource. - /// An instance will still be created without the resource. - fn create_instance(&self, brush: &TileMapBrushResource) -> Option; - /// True if instances of this macro can contain particular brush cells. - fn can_create_cell(&self) -> bool; - /// Create a command to modify the given instance's data to include the given cell. - /// None is returned if no command is necessary, such as if the cell is already included - /// or no cell is selected. Adding the currently selected cell will naturally require - /// the widgets that edit the data to change, but that will wait until - /// [`BrushMacro::sync_cell_editors`] is called. - fn create_cell(&self, context: &BrushMacroCellContext) -> Option; - /// Create a command to move cell data from the given cells to the given cells. - /// The two lists of cells should always be the same length. - /// This method works on multiple cells at once because `from` and `to` may - /// share some cells in common, in which case correctly moving the cells - /// requires first removing all the cells from `from`, then inserting all - /// the cells into `to`. - fn move_cells( - &self, - from: Box<[TileDefinitionHandle]>, - to: Box<[TileDefinitionHandle]>, - context: &BrushMacroInstance, - ) -> Option; - /// Create a command to move cell data from the given pages to the given pages. - /// The two lists of pages should always be the same length. - /// This method works on multiple pages at once because `from` and `to` may - /// share some pages in common, in which case correctly moving the pages - /// requires first removing all the pages from `from`, then inserting all - /// the pages into `to`. - fn move_pages( - &self, - from: Box<[Vector2]>, - to: Box<[Vector2]>, - context: &BrushMacroInstance, - ) -> Option; - /// Create a command to modify the given instances's data to copy the given cell. - /// None is returned if no command is necessary, such as if the macro has no cell data. - fn copy_cell( - &self, - source: Option, - destination: TileDefinitionHandle, - context: &BrushMacroInstance, - ) -> Option; - /// Create a command to modify the given instances's data to copy the given page. - /// None is returned if no command is necessary, such as if the macro has no cell data. - fn copy_page( - &self, - source: Option>, - destination: Vector2, - context: &BrushMacroInstance, - ) -> Option; - /// Modify the given `cell_set` to include the handles of all the cells that are part of - /// the given instance of this macro. This is necessary for the brush editor to accurately - /// update itself. - fn fill_cell_set( - &self, - context: &BrushMacroInstance, - cell_set: &mut FxHashSet, - ); - /// Build the widgets that will edit the configuration data for an instance of the - /// macro. The macro is modified to remember the handles of the widgets so that it - /// can respond correctly to UI messages. None is returned if this macro needs no - /// widgets because it cannot be edited. - fn build_instance_editor( - &mut self, - context: &BrushMacroInstance, - ctx: &mut BuildContext, - ) -> Option>; - /// Build the widgets that will edit the configuration data for an instance of the - /// macro and for a particular cell of the brush. - /// The macro is modified to remember the handles of the widgets so that it - /// can respond correctly to UI messages. Each instance may have it own separate widgets, - /// so the handles must be remembered for every instance. - /// None is returned if this macro needs no widgets because cell data cannot be edited. - /// If no cell is selected or if the cell is not part of the macro, then the widgets - /// should stil be created but they should be invisible. They can be made visible - /// when [`BrushMacro::sync_cell_editors`] is called. - fn build_cell_editor( - &mut self, - context: &BrushMacroCellContext, - ctx: &mut BuildContext, - ) -> Option>; - /// Send the necessary messages to update the cell editor widgets to edit the data for - /// the given instance. - fn sync_instance_editor(&mut self, context: &BrushMacroInstance, ui: &mut UserInterface); - /// Send the necessary messages to update the cell editor widgets to edit the data for - /// the given cell, or make those widgets invisible if there is no selected cell or the - /// cell has no data. - fn sync_cell_editors(&mut self, context: &MacroMessageContext, ui: &mut UserInterface); - /// This is called when the user begins drawing. This is called for every macro instance - /// stored in the current brush when the user presses the mouse button down to begin a stroke - /// and gives the macros a chance to prepare themselves for the series of [`amend_update`](BrushMacro::amend_update) - /// calls that will follow. - fn begin_update( - &mut self, - context: &BrushMacroInstance, - stamp: &Stamp, - tile_map: &TileMapContext, - ); - /// This is called for every macro instance whenever the user uses a brush to update a tile map. - /// - `context`: The macro instance. - /// - `update`: The change that the user is attempting to make to the tile map. Modify this to - /// amend the change as appropriate. - /// - `tile_map`: The tile map that is being edited. - fn amend_update( - &mut self, - context: &BrushMacroInstance, - update: &mut MacroTilesUpdate, - tile_map: &TileMap, - ); - /// This is called just before a command is submitted to actually modify the tile map. - /// Each macro instance receives this call, and each instance may make any final modifications - /// to the `update`, and may return an optional [`Command`] that will be collected with commands - /// from the other macros along with the command to apply `update` to the tile map to construct - /// the command that is finally sent to the editor. - fn create_command( - &mut self, - context: &BrushMacroInstance, - update: &mut MacroTilesUpdate, - tile_map: &TileMapContext, - ) -> Option; -} - -/// List of [`BrushMacro`] implementations that the [`TileMapEditorPlugin`] keeps in order to allow -/// macro instances to be added to a [`TileMapBrush`]. -pub struct BrushMacroList { - content: Vec>, -} - -impl Default for BrushMacroList { - fn default() -> Self { - let mut result = Self { - content: Default::default(), - }; - result.add(AutoTileMacro::default()); - result.add(WfcMacro::default()); - result - } -} - -impl BrushMacroList { - /// Add the given macro to the list. - #[inline] - pub fn add(&mut self, brush_macro: T) { - self.content - .push(Box::new(brush_macro) as Box); - } - /// True if the list is empty. - #[inline] - pub fn is_empty(&self) -> bool { - self.content.is_empty() - } - /// The number of macros in the list. - #[inline] - pub fn len(&self) -> usize { - self.content.len() - } - /// Iterate through the macros. - #[inline] - pub fn iter(&self) -> impl Iterator { - self.content.iter().map(|b| b.as_ref()) - } - /// Iterate through the macros. - #[inline] - pub fn iter_mut(&mut self) -> impl Iterator { - self.content.iter_mut().map(|b| b.as_mut()) - } - /// Access the macro at the given index. - #[inline] - pub fn get_by_index(&self, index: usize) -> Option<&dyn BrushMacro> { - self.content.get(index).map(|b| b.as_ref()) - } - /// Access the macro at the given index. - #[inline] - pub fn get_by_index_mut(&mut self, index: usize) -> Option<&mut dyn BrushMacro> { - self.content.get_mut(index).map(|b| b.as_mut()) - } - /// Find the macro with the given UUID. - pub fn get_by_uuid(&self, uuid: &Uuid) -> Option<&dyn BrushMacro> { - for brush_macro in self.content.iter() { - if brush_macro.uuid() == uuid { - return Some(brush_macro.as_ref()); - } - } - None - } - /// Find the macro with the given UUID. - pub fn get_by_uuid_mut(&mut self, uuid: &Uuid) -> Option<&mut dyn BrushMacro> { - for brush_macro in self.content.iter_mut() { - if brush_macro.uuid() == uuid { - return Some(brush_macro.as_mut()); - } - } - None - } - /// Remove all macros from the list. - #[inline] - pub fn clear(&mut self) { - self.content.clear(); - } -} - -/// Command for changing the position of a macro instance within the instance -/// list of a [`TileMapBrush`]. This instance is swapped with whichever instance -/// already occupies the new position. -#[derive(Debug)] -pub struct MoveMacroCommand { - /// The brush resource to modify - pub brush: TileMapBrushResource, - /// The starting position of the instance - pub start: usize, - /// The resulting position of the instance - pub end: usize, -} - -impl CommandTrait for MoveMacroCommand { - fn name(&mut self, _context: &dyn CommandContext) -> String { - "Move Brush Macro".into() - } - - fn execute(&mut self, _context: &mut dyn CommandContext) { - let mut brush = self.brush.data_ref(); - brush.macros.swap(self.start, self.end); - brush.change_flag.set(); - } - - fn revert(&mut self, _context: &mut dyn CommandContext) { - let mut brush = self.brush.data_ref(); - brush.macros.swap(self.start, self.end); - brush.change_flag.set(); - } -} - -/// Command for removing a macro instance from the instance list of a [`TileMapBrush`]. -#[derive(Debug)] -pub struct RemoveMacroCommand { - /// The brush resource to modify - pub brush: TileMapBrushResource, - /// The index of the instance to remove. - pub index: usize, - /// The data that was removed, after the operation is finished. It should be initially None. - pub data: Option, -} - -impl CommandTrait for RemoveMacroCommand { - fn name(&mut self, _context: &dyn CommandContext) -> String { - "Remove Brush Macro".into() - } - - fn execute(&mut self, _context: &mut dyn CommandContext) { - let mut brush = self.brush.data_ref(); - self.data = Some(brush.macros.remove(self.index)); - brush.change_flag.set(); - } - - fn revert(&mut self, _context: &mut dyn CommandContext) { - let mut brush = self.brush.data_ref(); - if let Some(data) = self.data.take() { - brush.macros.insert(self.index, data); - brush.change_flag.set(); - } - } -} - -/// Command for inserting a macro instance into the instance list of a [`TileMapBrush`]. -#[derive(Debug)] -pub struct AddMacroCommand { - /// The brush resource to modify - pub brush: TileMapBrushResource, - /// The index where the new instance will be inserted - pub index: usize, - /// The instance data to insert - pub data: Option, -} - -impl CommandTrait for AddMacroCommand { - fn name(&mut self, _context: &dyn CommandContext) -> String { - "Add Brush Macro".into() - } - - fn execute(&mut self, _context: &mut dyn CommandContext) { - let mut brush = self.brush.data_ref(); - if let Some(data) = self.data.take() { - brush.macros.insert(self.index, data); - brush.change_flag.set(); - } - } - - fn revert(&mut self, _context: &mut dyn CommandContext) { - let mut brush = self.brush.data_ref(); - self.data = Some(brush.macros.remove(self.index)); - brush.change_flag.set(); - } -} - -/// Command for changing the name of a marco instances within a tile map brush. -#[derive(Debug)] -pub struct SetMacroNameCommand { - /// The brush resouce to modify - pub brush: TileMapBrushResource, - /// The index of the instance that will be renamed - pub index: usize, - /// The new name for the instance - pub name: String, -} - -impl SetMacroNameCommand { - fn swap(&mut self) { - let mut brush = self.brush.data_ref(); - let brush_macro = brush.macros.get_mut(self.index).unwrap(); - std::mem::swap(&mut brush_macro.name, &mut self.name); - brush.change_flag.set(); - } -} - -impl CommandTrait for SetMacroNameCommand { - fn name(&mut self, _context: &dyn CommandContext) -> String { - "Rename Brush Macro".into() - } - - fn execute(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } - - fn revert(&mut self, _context: &mut dyn CommandContext) { - self.swap(); - } -} - -/// Message sent from a [`MacroPropertyValueField`] when the value changes. -#[derive(Clone, Debug, PartialEq)] -pub struct TileSetPropertyValueMessage(pub TileSetPropertyValueElement); - -impl TileSetPropertyValueMessage { - /// Construct a message to indicate a change in the value of a [`MacroPropertyValueField`]. - pub fn value( - destination: Handle, - direction: MessageDirection, - value: TileSetPropertyValueElement, - ) -> UiMessage { - UiMessage::with_data(TileSetPropertyValueMessage(value)) - .with_destination(destination) - .with_direction(direction) - } -} - -/// Message sent from a [`MacroPropertyField`] when the value changes. -#[derive(Clone, Debug, PartialEq)] -pub struct TileSetPropertyMessage(pub Option); - -impl TileSetPropertyMessage { - /// Construct a message to indicate a change in the value of a [`MacroPropertyField`]. - pub fn property_id( - destination: Handle, - direction: MessageDirection, - value: Option, - ) -> UiMessage { - UiMessage::with_data(TileSetPropertyMessage(value)) - .with_destination(destination) - .with_direction(direction) - } -} - -/// A field that allows the user to choose a property value from a [`TileSet`]. -/// The property is represented as a [`TileSetPropertyValueElement`] internally, but the user -/// is given a dropdown list of value names if possible. -#[derive(Debug, Default, Clone)] -pub struct MacroPropertyValueField { - handle: Handle, - textbox: Handle, - list: Handle, -} - -fn make_index_and_value_list( - prop: Option<&TileSetPropertyLayer>, - value: NamableValue, - ctx: &mut BuildContext, -) -> (usize, Vec>) { - let index; - let items; - if let Some(prop) = prop { - index = prop - .find_value_index(value) - .map(|i| i + 1) - .unwrap_or_default(); - items = make_named_value_list_items(prop, ctx); - } else { - index = 0; - items = vec![make_named_value_list_option( - ctx, - ELEMENT_MATCH_HIGHLIGHT_COLOR.to_opaque(), - "Custom", - )]; - } - (index, items) -} - -fn find_list_index(prop: Option<&TileSetPropertyLayer>, value: NamableValue) -> usize { - if let Some(prop) = prop { - prop.find_value_index(value) - .map(|i| i + 1) - .unwrap_or_default() - } else { - 0 - } -} - -impl MacroPropertyValueField { - /// -`label`: The string that names this field on the left side. - /// -`value`: The current value of the property. This determines the type of the field, - /// and cannot be changed later. - /// -`prop`: The property of the tile set. This is used to build a dropdown list of - /// named values. - pub fn new( - widget_builder: WidgetBuilder, - label: String, - value: TileSetPropertyValueElement, - prop: Option<&TileSetPropertyLayer>, - ctx: &mut BuildContext, - ) -> Self { - use TileSetPropertyValueElement as Element; - let label = TextBuilder::new(WidgetBuilder::new()) - .with_text(label) - .build(ctx); - let wb = WidgetBuilder::new().on_column(1); - let textbox = match &value { - Element::I32(v) => NumericUpDownBuilder::::new(wb) - .with_value(*v) - .build(ctx), - Element::F32(v) => NumericUpDownBuilder::::new(wb) - .with_value(*v) - .build(ctx), - Element::I8(v) => NumericUpDownBuilder::::new(wb) - .with_value(*v) - .build(ctx), - Element::String(v) => TextBoxBuilder::new(wb).with_text(v).build(ctx), - }; - let list = if let Ok(value) = value.try_into() { - let (index, items) = make_index_and_value_list(prop, value, ctx); - DropdownListBuilder::new(WidgetBuilder::new().on_column(1).on_row(1)) - .with_items(items) - .with_selected(index) - .build(ctx) - } else { - Handle::NONE - }; - let handle = GridBuilder::new( - widget_builder - .with_child(label) - .with_child(textbox) - .with_child(list), - ) - .add_column(Column::strict(PROPERTY_LABEL_WIDTH)) - .add_column(Column::stretch()) - .add_row(Row::auto()) - .add_row(Row::auto()) - .build(ctx); - Self { - handle, - textbox, - list, - } - } - /// The handle of the widget that holds the overall field. - pub fn handle(&self) -> Handle { - self.handle - } - /// Update the field's widgets to reflect any changes in the tile set. - /// -`value`: The UUID of the currently selected property. This will be ignored - /// if its type does not match the type of the field. - /// -`prop`: The property of the tile set. This is used to build a dropdown list of - /// named values. - pub fn sync( - &self, - value: TileSetPropertyValueElement, - prop: Option<&TileSetPropertyLayer>, - ui: &mut UserInterface, - ) { - use TileSetPropertyValueElement as Element; - let msg = match &value { - Element::I32(v) => { - NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, *v) - } - Element::F32(v) => { - NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, *v) - } - Element::String(v) => { - TextMessage::text(self.textbox, MessageDirection::ToWidget, v.to_string()) - } - Element::I8(v) => { - NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, *v) - } - }; - send_sync_message(ui, msg); - if let Ok(value) = value.try_into() { - let (index, items) = make_index_and_value_list(prop, value, &mut ui.build_ctx()); - ui.send_message(DropdownListMessage::items( - self.list, - MessageDirection::ToWidget, - items, - )); - send_sync_message( - ui, - DropdownListMessage::selection(self.list, MessageDirection::ToWidget, Some(index)), - ); - } - } - fn on_numeric_message( - &self, - prop: Option<&TileSetPropertyLayer>, - element: TileSetPropertyValueElement, - value: NamableValue, - ui: &mut UserInterface, - ) { - ui.send_message(TileSetPropertyValueMessage::value( - self.handle, - MessageDirection::FromWidget, - element, - )); - send_sync_message( - ui, - DropdownListMessage::selection( - self.list, - MessageDirection::ToWidget, - Some(find_list_index(prop, value)), - ), - ); - } - /// Handle the given message, which might be relevant to some widget in the field. - pub fn on_ui_message( - &mut self, - prop: Option<&TileSetPropertyLayer>, - message: &UiMessage, - ui: &mut UserInterface, - ) { - if let Some(TextMessage::Text(text)) = message.data() { - if message.destination() == self.textbox - && message.direction() == MessageDirection::FromWidget - { - ui.send_message(TileSetPropertyValueMessage::value( - self.handle, - MessageDirection::FromWidget, - TileSetPropertyValueElement::String(text.into()), - )); - } - } else if let Some(NumericUpDownMessage::::Value(v)) = message.data() { - if message.destination() == self.textbox - && message.direction() == MessageDirection::FromWidget - { - self.on_numeric_message( - prop, - TileSetPropertyValueElement::I8(*v), - NamableValue::I8(*v), - ui, - ); - } - } else if let Some(NumericUpDownMessage::::Value(v)) = message.data() { - if message.destination() == self.textbox - && message.direction() == MessageDirection::FromWidget - { - self.on_numeric_message( - prop, - TileSetPropertyValueElement::I32(*v), - NamableValue::I32(*v), - ui, - ); - } - } else if let Some(NumericUpDownMessage::::Value(v)) = message.data() { - if message.destination() == self.textbox - && message.direction() == MessageDirection::FromWidget - { - self.on_numeric_message( - prop, - TileSetPropertyValueElement::F32(*v), - NamableValue::F32(*v), - ui, - ); - } - } else if let Some(DropdownListMessage::SelectionChanged(Some(index))) = message.data() { - if message.destination() == self.list - && message.direction() == MessageDirection::FromWidget - && *index > 0 - { - if let Some(v) = prop.and_then(|p| p.named_values.get(index - 1)) { - ui.send_message(TileSetPropertyValueMessage::value( - self.handle, - MessageDirection::FromWidget, - v.value.into(), - )); - let msg = match v.value { - NamableValue::I32(v) => { - NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, v) - } - NamableValue::F32(v) => { - NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, v) - } - NamableValue::I8(v) => { - NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, v) - } - }; - send_sync_message(ui, msg); - } - } - } - } -} - -fn make_item(text: &str, ctx: &mut BuildContext) -> Handle { - DecoratorBuilder::new(BorderBuilder::new( - WidgetBuilder::new().with_child( - TextBuilder::new(WidgetBuilder::new()) - .with_text(text) - .build(ctx), - ), - )) - .build(ctx) -} - -/// A field that allows the user to choose a property from a [`TileSet`]. -/// The property is represented as UUID internally, but the user -/// is given a dropdown list of property names. -#[derive(Debug, Default)] -pub struct MacroPropertyField { - prop_type: TileSetPropertyType, - handle: Handle, - list: Handle, -} - -fn make_index_and_items( - prop_type: TileSetPropertyType, - value: Option<&Uuid>, - tile_set: Option<&TileSet>, - ctx: &mut BuildContext, -) -> (usize, Vec>) { - let mut items = vec![make_item("None", ctx)]; - let mut index = value.is_none().then_some(0); - if let Some(tile_set) = tile_set { - for (i, prop) in tile_set - .properties - .iter() - .filter(|p| p.prop_type == prop_type) - .enumerate() - { - if Some(&prop.uuid) == value { - index = Some(i + 1); - } - items.push(make_item(&prop.name, ctx)); - } - } - let index = if let Some(index) = index { - index - } else { - let index = items.len(); - items.push(make_item(UNKNOWN_PROPERTY, ctx)); - index - }; - (index, items) -} - -impl MacroPropertyField { - /// -`label`: The string that names this field on the left side. - /// -`prop_type`: The type of value that the property is expected to hold. - /// Only properties that match this type will be listed. - /// -`value`: The UUID of the currently selected property, or None. - /// -`tile_set`: The tile set to search for properties. - pub fn new( - widget_builder: WidgetBuilder, - label: String, - prop_type: TileSetPropertyType, - value: Option<&Uuid>, - tile_set: Option<&TileSet>, - ctx: &mut BuildContext, - ) -> Self { - let label = TextBuilder::new(WidgetBuilder::new()) - .with_text(label) - .build(ctx); - let (index, items) = make_index_and_items(prop_type, value, tile_set, ctx); - let list = DropdownListBuilder::new(WidgetBuilder::new().on_column(1)) - .with_items(items) - .with_selected(index) - .build(ctx); - let handle = GridBuilder::new(widget_builder.with_child(label).with_child(list)) - .add_column(Column::strict(PROPERTY_LABEL_WIDTH)) - .add_column(Column::stretch()) - .add_row(Row::auto()) - .build(ctx); - Self { - prop_type, - handle, - list, - } - } - /// The handle of the widget that holds the overall field. - pub fn handle(&self) -> Handle { - self.handle - } - /// Update the field's widgets to reflect any changes in the tile set. - /// -`value`: The UUID of the currently selected property. - pub fn sync(&self, value: Option<&Uuid>, tile_set: Option<&TileSet>, ui: &mut UserInterface) { - let (index, items) = - make_index_and_items(self.prop_type, value, tile_set, &mut ui.build_ctx()); - ui.send_message(DropdownListMessage::items( - self.list, - MessageDirection::ToWidget, - items, - )); - send_sync_message( - ui, - DropdownListMessage::selection(self.list, MessageDirection::ToWidget, Some(index)), - ); - } - /// Handle the given message, which might be relevant to some widget in the field. - pub fn on_ui_message( - &mut self, - tile_set: &TileSet, - message: &UiMessage, - ui: &mut UserInterface, - ) { - if let Some(DropdownListMessage::SelectionChanged(index)) = message.data() { - if message.destination() == self.list - && message.direction() == MessageDirection::FromWidget - { - let id = if let Some(index) = *index { - if index > 0 { - tile_set - .properties - .iter() - .filter(|p| p.prop_type == self.prop_type) - .nth(index - 1) - .map(|p| p.uuid) - } else { - None - } - } else { - None - }; - ui.send_message(TileSetPropertyMessage::property_id( - self.handle, - MessageDirection::FromWidget, - id, - )); - } - } - } -} +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use fyrox::{ + asset::{untyped::UntypedResource, Resource, ResourceData, ResourceDataRef}, + core::{ + futures::executor::block_on, + log::Log, + parking_lot::{lock_api::MutexGuard, RawMutex}, + }, + gui::{ + dropdown_list::{DropdownListBuilder, DropdownListMessage}, + grid::{Column, GridBuilder, Row}, + numeric::{NumericUpDownBuilder, NumericUpDownMessage}, + text::{TextBuilder, TextMessage}, + text_box::TextBoxBuilder, + }, + scene::tilemap::{ + brush::{BrushMacroData, TileMapBrush, TileMapBrushResource}, + tileset::{ + NamableValue, TileSetPropertyLayer, TileSetPropertyType, TileSetPropertyValueElement, + }, + MacroTilesUpdate, + }, +}; + +use crate::{ + command::{Command, CommandContext, CommandTrait}, + send_sync_message, +}; + +use super::*; + +const PROPERTY_LABEL_WIDTH: f32 = 150.0; +const UNKNOWN_PROPERTY: &str = "UNKNOWN PROPERTY"; + +/// An Arc Mutex reference to a [`BrushMacroList`] that allows multiple objects +/// to share access to a common macro list. Among the things that need to share +/// the list are the tile map interaction mode and the [`TileSetEditor`]. +#[derive(Default, Clone)] +pub struct BrushMacroListRef(Arc>); + +impl BrushMacroListRef { + /// Create a new reference and assign it to point to the given macro list. + pub fn new(list: BrushMacroList) -> Self { + Self(Arc::new(Mutex::new(list))) + } + /// Access the macro list. + pub fn lock(&self) -> MutexGuard { + self.0 + .try_lock() + .expect("BrushMacroList lock should not fail") + } +} + +/// An Arc Mutex reference to a [`MacroCellSetList`] which allows +/// shared access to the positions of cells which are currently involved +/// in some macro. This is only used in the [`TileSetEditor`], but it needs +/// to be shared with the [`PaletteWidget`] so that the widget can +/// render a highlight at those positions. +#[derive(Default, Clone)] +pub struct MacroCellSetListRef(Arc>); + +impl MacroCellSetListRef { + /// Create a new reference and assign it to point to the given set of cells. + pub fn new(list: MacroCellSetList) -> Self { + Self(Arc::new(Mutex::new(list))) + } + /// Access the set of cells. + pub fn lock(&self) -> MutexGuard { + self.0.try_lock().expect("MacroCellSetList already in use") + } +} + +/// A context for calling some [`BrushMacro`] methods when using the macro +/// to modify a tile map. It gives the macro extensive access to whatever it +/// might need for a wide variety of automatic actions that may be desired +/// along with tile map editing. +pub struct TileMapContext<'a> { + /// The handle for the tile map that is being edited. + pub node: Handle, + /// The handle for the scene that contains the tile map. + pub scene: Handle, + /// The engine that contains the scene. + pub engine: &'a mut Engine, +} + +impl TileMapContext<'_> { + /// The tile map that is being edited. + pub fn tile_map(&self) -> &TileMap { + self.engine.scenes[self.scene].graph[self.node] + .cast() + .unwrap() + } + /// The tile set resource from within the tile map, if the tile map has one. + pub fn tile_set(&self) -> Option<&TileSetResource> { + self.tile_map().tile_set() + } +} + +/// Calling some methods of [`BrushMacro`] may require access to the brush resource +/// and to the [`UntypedResource`] that the brush holds to represent the configuration +/// settings for an instance of the macro. +#[derive(Debug, Clone)] +pub struct BrushMacroInstance { + /// The brush that has an instance of the macro. + pub brush: TileMapBrushResource, + /// The configuration for an instance of the macro. + /// Some macros may not require any configuration, so this resource is optional. + pub settings: Option, +} + +impl BrushMacroInstance { + /// A typed reference to the configuration resource. + pub fn settings(&self) -> Option> + where + T: ResourceData + Default + TypeUuidProvider, + { + self.settings.as_ref()?.try_cast() + } + /// A data ref to the brush resource. + pub fn brush(&self) -> ResourceDataRef { + self.brush.data_ref() + } + /// The tile set resource taken from the brush. + pub fn tile_set(&self) -> Option { + TileSetFromBrush::try_new(&self.brush) + } +} + +/// The context required for handling UI messages for some particular selected +/// brush cell. This is used for both received [`UiMessage`] and when the brush +/// cell settings need to sync to the brush resource. +#[derive(Debug, Clone)] +pub struct MacroMessageContext { + /// The brush that has an instance of the macro. + pub brush: TileMapBrushResource, + /// The currently selected brush cell, or None if no cell is selected. + pub cell: Option, +} + +impl From for MacroMessageContext { + fn from(value: BrushMacroCellContext) -> Self { + Self { + brush: value.brush, + cell: value.cell, + } + } +} + +impl MacroMessageContext { + /// A reference to the brush's data. + pub fn brush(&self) -> ResourceDataRef { + self.brush.data_ref() + } + /// The tile set taken from the brush. + pub fn tile_set(&self) -> Option { + TileSetFromBrush::try_new(&self.brush) + } + /// A copy of the list of configuration resources for the macro instances + /// that match the given UUID. This is copied instead of being borrowed + /// to avoid holding a lock on the brush resource while iterating through + /// the macro instances. + pub fn instances_with_uuid(&self, uuid: Uuid) -> Vec { + self.brush + .data_ref() + .macros + .instances_with_uuid(uuid) + .cloned() + .collect() + } +} + +/// Context for methods of [`BrushMacro`] that are specific to a single brush cell, +/// such as adding a new cell to the macro, removing a cell, or building the widgets +/// for editing the settings of a cell. +#[derive(Debug, Clone)] +pub struct BrushMacroCellContext { + /// The brush that has an instance of the macro. + pub brush: TileMapBrushResource, + /// The configuration for an instance of the macro. + pub settings: Option, + /// The currently selected brush cell, or None if no cell is selected. + pub cell: Option, +} + +impl From for BrushMacroInstance { + fn from(value: BrushMacroCellContext) -> Self { + Self { + brush: value.brush, + settings: value.settings, + } + } +} + +impl BrushMacroCellContext { + /// A typed reference to the configuration resource. + pub fn settings(&self) -> Option> + where + T: ResourceData + Default + TypeUuidProvider, + { + self.settings.as_ref()?.try_cast() + } + /// A reference to the brush's data. + pub fn brush(&self) -> ResourceDataRef { + self.brush.data_ref() + } + /// The tile set resource taken from the brush. + pub fn tile_set(&self) -> Option { + TileSetFromBrush::try_new(&self.brush) + } +} + +/// Access to the tile set of a brush through a lock on the brush's data. +pub struct TileSetFromBrush<'a>(ResourceDataRef<'a, TileMapBrush>); + +impl Deref for TileSetFromBrush<'_> { + type Target = Resource; + + fn deref(&self) -> &Self::Target { + self.0.tile_set.as_ref().unwrap() + } +} + +impl<'a> TileSetFromBrush<'a> { + /// Construct access to the tile set of the given brush after confirming that + /// the brush is loaded, the brush has a tile set, and the tile set is loaded. + pub fn try_new(brush: &'a TileMapBrushResource) -> Option { + Log::verify(block_on(brush.clone())); + if !brush.is_ok() { + return None; + } + let brush_guard = brush.data_ref(); + let tile_set = brush_guard.tile_set.clone(); + drop(brush_guard); + if let Some(tile_set) = tile_set { + Log::verify(block_on(tile_set.clone())); + if !tile_set.is_ok() { + return None; + } + } + Some(Self(brush.data_ref())) + } +} + +/// The trait that defines the operation necessary for implementing a macro for a tile map brush. +pub trait BrushMacro: 'static + Send + Sync { + /// The UUID that identifies an instance of a macro as belonging to this macro. + /// One macro may have several instances, and all of them will share this UUID. + fn uuid(&self) -> &Uuid; + /// The name of the macro that will appear in the brush editor. + fn name(&self) -> &'static str; + /// Handle a UI message that was received while the brush editor was displaying the settings + /// for an instance of this macro in the Macros tab. + fn on_instance_ui_message( + &mut self, + context: &BrushMacroInstance, + message: &UiMessage, + editor: &mut Editor, + ); + /// Handle a UI message that was received while a particular brush cell was selected in the editor, + /// or when no brush cell is selected. Each macro needs to keep track of the widgets that it uses to + /// display the settings for cells in the tile inspector, and this method is where messages from those + /// widgets should be handled. + fn on_cell_ui_message( + &mut self, + context: &MacroMessageContext, + message: &UiMessage, + editor: &mut Editor, + ); + /// Create a resource to store the settings of this macro. + /// A return value of None indicates that his macro needs no settings resource. + /// An instance will still be created without the resource. + fn create_instance(&self, brush: &TileMapBrushResource) -> Option; + /// True if instances of this macro can contain particular brush cells. + fn can_create_cell(&self) -> bool; + /// Create a command to modify the given instance's data to include the given cell. + /// None is returned if no command is necessary, such as if the cell is already included + /// or no cell is selected. Adding the currently selected cell will naturally require + /// the widgets that edit the data to change, but that will wait until + /// [`BrushMacro::sync_cell_editors`] is called. + fn create_cell(&self, context: &BrushMacroCellContext) -> Option; + /// Create a command to move cell data from the given cells to the given cells. + /// The two lists of cells should always be the same length. + /// This method works on multiple cells at once because `from` and `to` may + /// share some cells in common, in which case correctly moving the cells + /// requires first removing all the cells from `from`, then inserting all + /// the cells into `to`. + fn move_cells( + &self, + from: Box<[TileDefinitionHandle]>, + to: Box<[TileDefinitionHandle]>, + context: &BrushMacroInstance, + ) -> Option; + /// Create a command to move cell data from the given pages to the given pages. + /// The two lists of pages should always be the same length. + /// This method works on multiple pages at once because `from` and `to` may + /// share some pages in common, in which case correctly moving the pages + /// requires first removing all the pages from `from`, then inserting all + /// the pages into `to`. + fn move_pages( + &self, + from: Box<[Vector2]>, + to: Box<[Vector2]>, + context: &BrushMacroInstance, + ) -> Option; + /// Create a command to modify the given instances's data to copy the given cell. + /// None is returned if no command is necessary, such as if the macro has no cell data. + fn copy_cell( + &self, + source: Option, + destination: TileDefinitionHandle, + context: &BrushMacroInstance, + ) -> Option; + /// Create a command to modify the given instances's data to copy the given page. + /// None is returned if no command is necessary, such as if the macro has no cell data. + fn copy_page( + &self, + source: Option>, + destination: Vector2, + context: &BrushMacroInstance, + ) -> Option; + /// Modify the given `cell_set` to include the handles of all the cells that are part of + /// the given instance of this macro. This is necessary for the brush editor to accurately + /// update itself. + fn fill_cell_set( + &self, + context: &BrushMacroInstance, + cell_set: &mut FxHashSet, + ); + /// Build the widgets that will edit the configuration data for an instance of the + /// macro. The macro is modified to remember the handles of the widgets so that it + /// can respond correctly to UI messages. None is returned if this macro needs no + /// widgets because it cannot be edited. + fn build_instance_editor( + &mut self, + context: &BrushMacroInstance, + ctx: &mut BuildContext, + ) -> Option>; + /// Build the widgets that will edit the configuration data for an instance of the + /// macro and for a particular cell of the brush. + /// The macro is modified to remember the handles of the widgets so that it + /// can respond correctly to UI messages. Each instance may have it own separate widgets, + /// so the handles must be remembered for every instance. + /// None is returned if this macro needs no widgets because cell data cannot be edited. + /// If no cell is selected or if the cell is not part of the macro, then the widgets + /// should stil be created but they should be invisible. They can be made visible + /// when [`BrushMacro::sync_cell_editors`] is called. + fn build_cell_editor( + &mut self, + context: &BrushMacroCellContext, + ctx: &mut BuildContext, + ) -> Option>; + /// Send the necessary messages to update the cell editor widgets to edit the data for + /// the given instance. + fn sync_instance_editor(&mut self, context: &BrushMacroInstance, ui: &mut UserInterface); + /// Send the necessary messages to update the cell editor widgets to edit the data for + /// the given cell, or make those widgets invisible if there is no selected cell or the + /// cell has no data. + fn sync_cell_editors(&mut self, context: &MacroMessageContext, ui: &mut UserInterface); + /// This is called when the user begins drawing. This is called for every macro instance + /// stored in the current brush when the user presses the mouse button down to begin a stroke + /// and gives the macros a chance to prepare themselves for the series of [`amend_update`](BrushMacro::amend_update) + /// calls that will follow. + fn begin_update( + &mut self, + context: &BrushMacroInstance, + stamp: &Stamp, + tile_map: &TileMapContext, + ); + /// This is called for every macro instance whenever the user uses a brush to update a tile map. + /// - `context`: The macro instance. + /// - `update`: The change that the user is attempting to make to the tile map. Modify this to + /// amend the change as appropriate. + /// - `tile_map`: The tile map that is being edited. + fn amend_update( + &mut self, + context: &BrushMacroInstance, + update: &mut MacroTilesUpdate, + tile_map: &TileMap, + ); + /// This is called just before a command is submitted to actually modify the tile map. + /// Each macro instance receives this call, and each instance may make any final modifications + /// to the `update`, and may return an optional [`Command`] that will be collected with commands + /// from the other macros along with the command to apply `update` to the tile map to construct + /// the command that is finally sent to the editor. + fn create_command( + &mut self, + context: &BrushMacroInstance, + update: &mut MacroTilesUpdate, + tile_map: &TileMapContext, + ) -> Option; +} + +/// List of [`BrushMacro`] implementations that the [`TileMapEditorPlugin`] keeps in order to allow +/// macro instances to be added to a [`TileMapBrush`]. +pub struct BrushMacroList { + content: Vec>, +} + +impl Default for BrushMacroList { + fn default() -> Self { + let mut result = Self { + content: Default::default(), + }; + result.add(AutoTileMacro::default()); + result.add(WfcMacro::default()); + result + } +} + +impl BrushMacroList { + /// Add the given macro to the list. + #[inline] + pub fn add(&mut self, brush_macro: T) { + self.content + .push(Box::new(brush_macro) as Box); + } + /// True if the list is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.content.is_empty() + } + /// The number of macros in the list. + #[inline] + pub fn len(&self) -> usize { + self.content.len() + } + /// Iterate through the macros. + #[inline] + pub fn iter(&self) -> impl Iterator { + self.content.iter().map(|b| b.as_ref()) + } + /// Iterate through the macros. + #[inline] + pub fn iter_mut(&mut self) -> impl Iterator { + self.content.iter_mut().map(|b| b.as_mut()) + } + /// Access the macro at the given index. + #[inline] + pub fn get_by_index(&self, index: usize) -> Option<&dyn BrushMacro> { + self.content.get(index).map(|b| b.as_ref()) + } + /// Access the macro at the given index. + #[inline] + pub fn get_by_index_mut(&mut self, index: usize) -> Option<&mut dyn BrushMacro> { + self.content.get_mut(index).map(|b| b.as_mut()) + } + /// Find the macro with the given UUID. + pub fn get_by_uuid(&self, uuid: &Uuid) -> Option<&dyn BrushMacro> { + for brush_macro in self.content.iter() { + if brush_macro.uuid() == uuid { + return Some(brush_macro.as_ref()); + } + } + None + } + /// Find the macro with the given UUID. + pub fn get_by_uuid_mut(&mut self, uuid: &Uuid) -> Option<&mut dyn BrushMacro> { + for brush_macro in self.content.iter_mut() { + if brush_macro.uuid() == uuid { + return Some(brush_macro.as_mut()); + } + } + None + } + /// Remove all macros from the list. + #[inline] + pub fn clear(&mut self) { + self.content.clear(); + } +} + +/// Command for changing the position of a macro instance within the instance +/// list of a [`TileMapBrush`]. This instance is swapped with whichever instance +/// already occupies the new position. +#[derive(Debug)] +pub struct MoveMacroCommand { + /// The brush resource to modify + pub brush: TileMapBrushResource, + /// The starting position of the instance + pub start: usize, + /// The resulting position of the instance + pub end: usize, +} + +impl CommandTrait for MoveMacroCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Move Brush Macro".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + brush.macros.swap(self.start, self.end); + brush.change_flag.set(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + brush.macros.swap(self.start, self.end); + brush.change_flag.set(); + } +} + +/// Command for removing a macro instance from the instance list of a [`TileMapBrush`]. +#[derive(Debug)] +pub struct RemoveMacroCommand { + /// The brush resource to modify + pub brush: TileMapBrushResource, + /// The index of the instance to remove. + pub index: usize, + /// The data that was removed, after the operation is finished. It should be initially None. + pub data: Option, +} + +impl CommandTrait for RemoveMacroCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Remove Brush Macro".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + self.data = Some(brush.macros.remove(self.index)); + brush.change_flag.set(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + if let Some(data) = self.data.take() { + brush.macros.insert(self.index, data); + brush.change_flag.set(); + } + } +} + +/// Command for inserting a macro instance into the instance list of a [`TileMapBrush`]. +#[derive(Debug)] +pub struct AddMacroCommand { + /// The brush resource to modify + pub brush: TileMapBrushResource, + /// The index where the new instance will be inserted + pub index: usize, + /// The instance data to insert + pub data: Option, +} + +impl CommandTrait for AddMacroCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Add Brush Macro".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + if let Some(data) = self.data.take() { + brush.macros.insert(self.index, data); + brush.change_flag.set(); + } + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + self.data = Some(brush.macros.remove(self.index)); + brush.change_flag.set(); + } +} + +/// Command for changing the name of a marco instances within a tile map brush. +#[derive(Debug)] +pub struct SetMacroNameCommand { + /// The brush resouce to modify + pub brush: TileMapBrushResource, + /// The index of the instance that will be renamed + pub index: usize, + /// The new name for the instance + pub name: String, +} + +impl SetMacroNameCommand { + fn swap(&mut self) { + let mut brush = self.brush.data_ref(); + let brush_macro = brush.macros.get_mut(self.index).unwrap(); + std::mem::swap(&mut brush_macro.name, &mut self.name); + brush.change_flag.set(); + } +} + +impl CommandTrait for SetMacroNameCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Rename Brush Macro".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } +} + +/// Message sent from a [`MacroPropertyValueField`] when the value changes. +#[derive(Clone, Debug, PartialEq)] +pub struct TileSetPropertyValueMessage(pub TileSetPropertyValueElement); + +impl TileSetPropertyValueMessage { + /// Construct a message to indicate a change in the value of a [`MacroPropertyValueField`]. + pub fn value( + destination: Handle, + direction: MessageDirection, + value: TileSetPropertyValueElement, + ) -> UiMessage { + UiMessage::with_data(TileSetPropertyValueMessage(value)) + .with_destination(destination) + .with_direction(direction) + } +} + +/// Message sent from a [`MacroPropertyField`] when the value changes. +#[derive(Clone, Debug, PartialEq)] +pub struct TileSetPropertyMessage(pub Option); + +impl TileSetPropertyMessage { + /// Construct a message to indicate a change in the value of a [`MacroPropertyField`]. + pub fn property_id( + destination: Handle, + direction: MessageDirection, + value: Option, + ) -> UiMessage { + UiMessage::with_data(TileSetPropertyMessage(value)) + .with_destination(destination) + .with_direction(direction) + } +} + +/// A field that allows the user to choose a property value from a [`TileSet`]. +/// The property is represented as a [`TileSetPropertyValueElement`] internally, but the user +/// is given a dropdown list of value names if possible. +#[derive(Debug, Default, Clone)] +pub struct MacroPropertyValueField { + handle: Handle, + textbox: Handle, + list: Handle, +} + +fn make_index_and_value_list( + prop: Option<&TileSetPropertyLayer>, + value: NamableValue, + ctx: &mut BuildContext, +) -> (usize, Vec>) { + let index; + let items; + if let Some(prop) = prop { + index = prop + .find_value_index(value) + .map(|i| i + 1) + .unwrap_or_default(); + items = make_named_value_list_items(prop, ctx); + } else { + index = 0; + items = vec![make_named_value_list_option( + ctx, + ELEMENT_MATCH_HIGHLIGHT_COLOR.to_opaque(), + "Custom", + )]; + } + (index, items) +} + +fn find_list_index(prop: Option<&TileSetPropertyLayer>, value: NamableValue) -> usize { + if let Some(prop) = prop { + prop.find_value_index(value) + .map(|i| i + 1) + .unwrap_or_default() + } else { + 0 + } +} + +impl MacroPropertyValueField { + /// -`label`: The string that names this field on the left side. + /// -`value`: The current value of the property. This determines the type of the field, + /// and cannot be changed later. + /// -`prop`: The property of the tile set. This is used to build a dropdown list of + /// named values. + pub fn new( + widget_builder: WidgetBuilder, + label: String, + value: TileSetPropertyValueElement, + prop: Option<&TileSetPropertyLayer>, + ctx: &mut BuildContext, + ) -> Self { + use TileSetPropertyValueElement as Element; + let label = TextBuilder::new(WidgetBuilder::new()) + .with_text(label) + .build(ctx); + let wb = WidgetBuilder::new().on_column(1); + let textbox = match &value { + Element::I32(v) => NumericUpDownBuilder::::new(wb) + .with_value(*v) + .build(ctx), + Element::F32(v) => NumericUpDownBuilder::::new(wb) + .with_value(*v) + .build(ctx), + Element::I8(v) => NumericUpDownBuilder::::new(wb) + .with_value(*v) + .build(ctx), + Element::String(v) => TextBoxBuilder::new(wb).with_text(v).build(ctx), + }; + let list = if let Ok(value) = value.try_into() { + let (index, items) = make_index_and_value_list(prop, value, ctx); + DropdownListBuilder::new(WidgetBuilder::new().on_column(1).on_row(1)) + .with_items(items) + .with_selected(index) + .build(ctx) + } else { + Handle::NONE + }; + let handle = GridBuilder::new( + widget_builder + .with_child(label) + .with_child(textbox) + .with_child(list), + ) + .add_column(Column::strict(PROPERTY_LABEL_WIDTH)) + .add_column(Column::stretch()) + .add_row(Row::auto()) + .add_row(Row::auto()) + .build(ctx); + Self { + handle, + textbox, + list, + } + } + /// The handle of the widget that holds the overall field. + pub fn handle(&self) -> Handle { + self.handle + } + /// Update the field's widgets to reflect any changes in the tile set. + /// -`value`: The UUID of the currently selected property. This will be ignored + /// if its type does not match the type of the field. + /// -`prop`: The property of the tile set. This is used to build a dropdown list of + /// named values. + pub fn sync( + &self, + value: TileSetPropertyValueElement, + prop: Option<&TileSetPropertyLayer>, + ui: &mut UserInterface, + ) { + use TileSetPropertyValueElement as Element; + let msg = match &value { + Element::I32(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, *v) + } + Element::F32(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, *v) + } + Element::String(v) => { + TextMessage::text(self.textbox, MessageDirection::ToWidget, v.to_string()) + } + Element::I8(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, *v) + } + }; + send_sync_message(ui, msg); + if let Ok(value) = value.try_into() { + let (index, items) = make_index_and_value_list(prop, value, &mut ui.build_ctx()); + ui.send_message(DropdownListMessage::items( + self.list, + MessageDirection::ToWidget, + items, + )); + send_sync_message( + ui, + DropdownListMessage::selection(self.list, MessageDirection::ToWidget, Some(index)), + ); + } + } + fn on_numeric_message( + &self, + prop: Option<&TileSetPropertyLayer>, + element: TileSetPropertyValueElement, + value: NamableValue, + ui: &mut UserInterface, + ) { + ui.send_message(TileSetPropertyValueMessage::value( + self.handle, + MessageDirection::FromWidget, + element, + )); + send_sync_message( + ui, + DropdownListMessage::selection( + self.list, + MessageDirection::ToWidget, + Some(find_list_index(prop, value)), + ), + ); + } + /// Handle the given message, which might be relevant to some widget in the field. + pub fn on_ui_message( + &mut self, + prop: Option<&TileSetPropertyLayer>, + message: &UiMessage, + ui: &mut UserInterface, + ) { + if let Some(TextMessage::Text(text)) = message.data() { + if message.destination() == self.textbox + && message.direction() == MessageDirection::FromWidget + { + ui.send_message(TileSetPropertyValueMessage::value( + self.handle, + MessageDirection::FromWidget, + TileSetPropertyValueElement::String(text.into()), + )); + } + } else if let Some(NumericUpDownMessage::::Value(v)) = message.data() { + if message.destination() == self.textbox + && message.direction() == MessageDirection::FromWidget + { + self.on_numeric_message( + prop, + TileSetPropertyValueElement::I8(*v), + NamableValue::I8(*v), + ui, + ); + } + } else if let Some(NumericUpDownMessage::::Value(v)) = message.data() { + if message.destination() == self.textbox + && message.direction() == MessageDirection::FromWidget + { + self.on_numeric_message( + prop, + TileSetPropertyValueElement::I32(*v), + NamableValue::I32(*v), + ui, + ); + } + } else if let Some(NumericUpDownMessage::::Value(v)) = message.data() { + if message.destination() == self.textbox + && message.direction() == MessageDirection::FromWidget + { + self.on_numeric_message( + prop, + TileSetPropertyValueElement::F32(*v), + NamableValue::F32(*v), + ui, + ); + } + } else if let Some(DropdownListMessage::SelectionChanged(Some(index))) = message.data() { + if message.destination() == self.list + && message.direction() == MessageDirection::FromWidget + && *index > 0 + { + if let Some(v) = prop.and_then(|p| p.named_values.get(index - 1)) { + ui.send_message(TileSetPropertyValueMessage::value( + self.handle, + MessageDirection::FromWidget, + v.value.into(), + )); + let msg = match v.value { + NamableValue::I32(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, v) + } + NamableValue::F32(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, v) + } + NamableValue::I8(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, v) + } + }; + send_sync_message(ui, msg); + } + } + } + } +} + +fn make_item(text: &str, ctx: &mut BuildContext) -> Handle { + DecoratorBuilder::new(BorderBuilder::new( + WidgetBuilder::new().with_child( + TextBuilder::new(WidgetBuilder::new()) + .with_text(text) + .build(ctx), + ), + )) + .build(ctx) +} + +/// A field that allows the user to choose a property from a [`TileSet`]. +/// The property is represented as UUID internally, but the user +/// is given a dropdown list of property names. +#[derive(Debug, Default)] +pub struct MacroPropertyField { + prop_type: TileSetPropertyType, + handle: Handle, + list: Handle, +} + +fn make_index_and_items( + prop_type: TileSetPropertyType, + value: Option<&Uuid>, + tile_set: Option<&TileSet>, + ctx: &mut BuildContext, +) -> (usize, Vec>) { + let mut items = vec![make_item("None", ctx)]; + let mut index = value.is_none().then_some(0); + if let Some(tile_set) = tile_set { + for (i, prop) in tile_set + .properties + .iter() + .filter(|p| p.prop_type == prop_type) + .enumerate() + { + if Some(&prop.uuid) == value { + index = Some(i + 1); + } + items.push(make_item(&prop.name, ctx)); + } + } + let index = if let Some(index) = index { + index + } else { + let index = items.len(); + items.push(make_item(UNKNOWN_PROPERTY, ctx)); + index + }; + (index, items) +} + +impl MacroPropertyField { + /// -`label`: The string that names this field on the left side. + /// -`prop_type`: The type of value that the property is expected to hold. + /// Only properties that match this type will be listed. + /// -`value`: The UUID of the currently selected property, or None. + /// -`tile_set`: The tile set to search for properties. + pub fn new( + widget_builder: WidgetBuilder, + label: String, + prop_type: TileSetPropertyType, + value: Option<&Uuid>, + tile_set: Option<&TileSet>, + ctx: &mut BuildContext, + ) -> Self { + let label = TextBuilder::new(WidgetBuilder::new()) + .with_text(label) + .build(ctx); + let (index, items) = make_index_and_items(prop_type, value, tile_set, ctx); + let list = DropdownListBuilder::new(WidgetBuilder::new().on_column(1)) + .with_items(items) + .with_selected(index) + .build(ctx); + let handle = GridBuilder::new(widget_builder.with_child(label).with_child(list)) + .add_column(Column::strict(PROPERTY_LABEL_WIDTH)) + .add_column(Column::stretch()) + .add_row(Row::auto()) + .build(ctx); + Self { + prop_type, + handle, + list, + } + } + /// The handle of the widget that holds the overall field. + pub fn handle(&self) -> Handle { + self.handle + } + /// Update the field's widgets to reflect any changes in the tile set. + /// -`value`: The UUID of the currently selected property. + pub fn sync(&self, value: Option<&Uuid>, tile_set: Option<&TileSet>, ui: &mut UserInterface) { + let (index, items) = + make_index_and_items(self.prop_type, value, tile_set, &mut ui.build_ctx()); + ui.send_message(DropdownListMessage::items( + self.list, + MessageDirection::ToWidget, + items, + )); + send_sync_message( + ui, + DropdownListMessage::selection(self.list, MessageDirection::ToWidget, Some(index)), + ); + } + /// Handle the given message, which might be relevant to some widget in the field. + pub fn on_ui_message( + &mut self, + tile_set: &TileSet, + message: &UiMessage, + ui: &mut UserInterface, + ) { + if let Some(DropdownListMessage::SelectionChanged(index)) = message.data() { + if message.destination() == self.list + && message.direction() == MessageDirection::FromWidget + { + let id = if let Some(index) = *index { + if index > 0 { + tile_set + .properties + .iter() + .filter(|p| p.prop_type == self.prop_type) + .nth(index - 1) + .map(|p| p.uuid) + } else { + None + } + } else { + None + }; + ui.send_message(TileSetPropertyMessage::property_id( + self.handle, + MessageDirection::FromWidget, + id, + )); + } + } + } +} diff --git a/editor/src/plugins/tilemap/collider_editor.rs b/editor/src/plugins/tilemap/collider_editor.rs index 667012e3f..df537d5d5 100644 --- a/editor/src/plugins/tilemap/collider_editor.rs +++ b/editor/src/plugins/tilemap/collider_editor.rs @@ -1,445 +1,445 @@ -// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -//! The [`TileEditor`] for a tile's collider layer. This allows the tile set editor -//! to display a dropdown list for the various [`TileCollider`] options, and displays -//! a text box for editing custom colliders when appropriate. -//! See [`TileColliderEditor`] for more information. - -use commands::SetTileSetTilesCommand; -use fyrox::{ - asset::Resource, - core::{algebra::Vector2, color::Color, pool::Handle, type_traits::prelude::*}, - gui::{ - border::BorderBuilder, - brush::Brush, - button::{Button, ButtonBuilder, ButtonMessage}, - decorator::{DecoratorBuilder, DecoratorMessage}, - dropdown_list::{DropdownListBuilder, DropdownListMessage}, - formatted_text::WrapMode, - grid::{Column, GridBuilder, Row}, - image::ImageBuilder, - message::UiMessage, - stack_panel::StackPanelBuilder, - text::{TextBuilder, TextMessage}, - text_box::{TextBoxBuilder, TextCommitMode}, - utils::make_simple_tooltip, - widget::WidgetBuilder, - BuildContext, HorizontalAlignment, Thickness, UiNode, UserInterface, VerticalAlignment, - }, - scene::tilemap::{tileset::*, CustomTileCollider, CustomTileColliderResource, TileSetUpdate}, -}; -use std::str::FromStr; - -use crate::{send_sync_message, MSG_SYNC_FLAG}; - -use super::*; - -const COLLIDER_NAMES: &[&str] = &["None", "Full", "Custom"]; - -fn collider_to_index(tile_collider: &TileCollider) -> Option { - match tile_collider { - TileCollider::None => Some(0), - TileCollider::Rectangle => Some(1), - TileCollider::Custom(_) => Some(2), - TileCollider::Mesh => None, - } -} - -fn send_visibility(ui: &UserInterface, destination: Handle, visible: bool) { - ui.send_message(WidgetMessage::visibility( - destination, - MessageDirection::ToWidget, - visible, - )); -} - -fn highlight_tool_button(button: Handle, highlight: bool, ui: &UserInterface) { - let decorator = *ui.try_get_of_type::