Introducing tile map brush macros and autotile

This commit is contained in:
b-guild
2025-02-20 22:18:26 -08:00
parent 9edcba619e
commit 3b0e31dd34
33 changed files with 7342 additions and 292 deletions
+2 -1
View File
@@ -19,7 +19,8 @@ members = [
"project-manager",
"fyrox-graphics",
"fyrox-build-tools",
"fyrox-texture"]
"fyrox-texture",
"fyrox-autotile"]
resolver = "2"
[profile.dev]
+657
View File
@@ -0,0 +1,657 @@
// 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::{ResourceKind, UntypedResource},
Resource, ResourceData,
},
autotile::{ConstraintFillRules, NeededTerrain, TerrainSource},
core::swap_hash_map_entry,
fxhash::FxHashMap,
gui::{
check_box::{CheckBoxBuilder, CheckBoxMessage},
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, 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.");
#[derive(Default)]
pub struct AutoTileMacro {
pattern_list: MacroPropertyField,
frequency_list: MacroPropertyField,
context: TileSetAutoTileContext,
constraints: TileSetConstraintMap,
autotiler: TileSetAutoTiler,
}
#[derive(Default, Debug, Clone, Visit, Reflect)]
struct CellData {
terrain_id: TileTerrainId,
fill: ConstraintFillRules,
}
#[derive(Debug, Default, Clone, Visit, Reflect, TypeUuidProvider)]
#[type_uuid(id = "b320543d-3df0-43fd-b0d9-60a398f49853")]
pub(super) struct AutoTileInstance {
frequency_property: Option<TileSetPropertyF32>,
pattern_property: Option<TileSetPropertyNine>,
cells: FxHashMap<TileDefinitionHandle, CellData>,
#[reflect(hidden)]
#[visit(skip)]
widgets: InstanceCellWidgets,
}
#[derive(Debug, Default, Clone)]
struct InstanceCellWidgets {
handle: Handle<UiNode>,
value_field: MacroPropertyValueField,
adjacent_toggle: Handle<UiNode>,
diagonal_toggle: Handle<UiNode>,
}
impl ResourceData for AutoTileInstance {
fn type_uuid(&self) -> Uuid {
<Self as TypeUuidProvider>::type_uuid()
}
fn save(&mut self, _path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
Err("Saving is not supported!".to_string().into())
}
fn can_be_saved(&self) -> bool {
false
}
}
pub struct TileSetTerrainSource<'a, 'b> {
update: &'a MacroTilesUpdate,
instance: &'b AutoTileInstance,
}
impl TerrainSource for TileSetTerrainSource<'_, '_> {
type Position = Vector2<i32>;
type Terrain = TileTerrainId;
fn iter(&self) -> impl Iterator<Item = NeededTerrain<Vector2<i32>, TileTerrainId>> + '_ {
self.update.iter().filter_map(|(p, v)| {
let cell_data = self.instance.cells.get(v.as_ref()?.brush_cell.as_ref()?)?;
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.brush_cell 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 {
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 ui = editor.engine.user_interfaces.first_mut();
for r in context.instances_with_uuid(*self.uuid()) {
let instance = r.try_cast::<AutoTileInstance>().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 = context.cell.unwrap();
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()
&& context.cell.is_some()
{
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<UntypedResource> {
Some(UntypedResource::new_ok(
ResourceKind::Embedded,
AutoTileInstance::default(),
))
}
fn can_create_cell(&self) -> bool {
true
}
fn fill_cell_set(
&self,
context: &BrushMacroInstance,
cell_set: &mut FxHashSet<TileDefinitionHandle>,
) {
let Some(data) = context
.settings
.as_ref()
.and_then(|r| r.try_cast::<AutoTileInstance>())
else {
return;
};
let data = data.data_ref();
cell_set.extend(data.cells.keys());
}
fn create_cell(&self, context: &BrushMacroCell) -> Option<Command> {
let instance = context.settings()?;
let cell = context.cell?;
Some(Command::new(SetCellCommand {
brush: context.brush.clone(),
cell,
instance,
data: Some(CellData::default()),
}))
}
fn remove_cell(&self, context: &BrushMacroCell) -> Option<Command> {
let instance = context.settings()?;
let cell = context.cell?;
Some(Command::new(SetCellCommand {
brush: context.brush.clone(),
cell,
instance,
data: None,
}))
}
fn build_instance_editor(
&mut self,
context: &BrushMacroInstance,
ctx: &mut BuildContext,
) -> Option<Handle<UiNode>> {
let instance = context.settings::<AutoTileInstance>().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 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()),
)
.build(ctx);
Some(handle)
}
fn build_cell_editor(
&mut self,
context: &BrushMacroCell,
ctx: &mut BuildContext,
) -> Option<Handle<UiNode>> {
let instance = context.settings::<AutoTileInstance>().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::<AutoTileInstance>() 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);
}
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::<AutoTileInstance>().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, 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::<AutoTileInstance>().unwrap();
let instance = instance.data_ref();
let Some(pattern_property) = instance.pattern_property else {
self.context.clear();
return;
};
let frequency_property = instance.frequency_property;
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,
) {
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);
}
fn create_command(
&mut self,
_context: &BrushMacroInstance,
_update: &mut MacroTilesUpdate,
_tile_map: &TileMapContext,
) -> Option<Command> {
None
}
}
#[derive(Debug)]
struct SetCellCommand {
pub brush: TileMapBrushResource,
pub instance: Resource<AutoTileInstance>,
pub cell: TileDefinitionHandle,
pub data: Option<CellData>,
}
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 SetPatternPropCommand {
pub brush: TileMapBrushResource,
pub instance: Resource<AutoTileInstance>,
pub data: Option<TileSetPropertyNine>,
}
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<AutoTileInstance>,
pub data: Option<TileSetPropertyF32>,
}
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();
}
}
+857
View File
@@ -0,0 +1,857 @@
// 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";
#[derive(Default, Clone)]
pub struct BrushMacroListRef(Arc<Mutex<BrushMacroList>>);
impl BrushMacroListRef {
pub fn new(list: BrushMacroList) -> Self {
Self(Arc::new(Mutex::new(list)))
}
pub fn lock(&self) -> MutexGuard<RawMutex, BrushMacroList> {
self.0
.try_lock()
.expect("BrushMacroList lock should not fail")
}
}
#[derive(Default, Clone)]
pub struct MacroCellSetListRef(Arc<Mutex<MacroCellSetList>>);
impl MacroCellSetListRef {
pub fn new(list: MacroCellSetList) -> Self {
Self(Arc::new(Mutex::new(list)))
}
pub fn lock(&self) -> MutexGuard<RawMutex, MacroCellSetList> {
self.0.try_lock().expect("MacroCellSetList already in use")
}
}
pub struct TileMapContext<'a> {
pub node: Handle<Node>,
pub scene: Handle<Scene>,
pub engine: &'a mut Engine,
}
impl TileMapContext<'_> {
pub fn tile_map(&self) -> &TileMap {
self.engine.scenes[self.scene].graph[self.node]
.cast()
.unwrap()
}
pub fn tile_set(&self) -> Option<&TileSetResource> {
self.tile_map().tile_set()
}
}
#[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.
pub settings: Option<UntypedResource>,
}
impl BrushMacroInstance {
pub fn settings<T>(&self) -> Option<Resource<T>>
where
T: ResourceData + Default + TypeUuidProvider,
{
self.settings.as_ref()?.try_cast()
}
pub fn brush(&self) -> ResourceDataRef<TileMapBrush> {
self.brush.data_ref()
}
pub fn tile_set(&self) -> Option<TileSetFromBrush> {
TileSetFromBrush::try_new(&self.brush)
}
}
#[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<TileDefinitionHandle>,
}
impl From<BrushMacroCell> for MacroMessageContext {
fn from(value: BrushMacroCell) -> Self {
Self {
brush: value.brush,
cell: value.cell,
}
}
}
impl MacroMessageContext {
pub fn brush(&self) -> ResourceDataRef<TileMapBrush> {
self.brush.data_ref()
}
pub fn tile_set(&self) -> Option<TileSetFromBrush> {
TileSetFromBrush::try_new(&self.brush)
}
pub fn instances_with_uuid(&self, uuid: Uuid) -> Vec<UntypedResource> {
self.brush
.data_ref()
.macros
.instances_with_uuid(uuid)
.cloned()
.collect()
}
}
#[derive(Debug, Clone)]
pub struct BrushMacroCell {
/// The brush that has an instance of the macro.
pub brush: TileMapBrushResource,
/// The configuration for an instance of the macro.
pub settings: Option<UntypedResource>,
/// The currently selected brush cell, or None if no cell is selected.
pub cell: Option<TileDefinitionHandle>,
}
impl From<BrushMacroCell> for BrushMacroInstance {
fn from(value: BrushMacroCell) -> Self {
Self {
brush: value.brush,
settings: value.settings,
}
}
}
impl BrushMacroCell {
pub fn settings<T>(&self) -> Option<Resource<T>>
where
T: ResourceData + Default + TypeUuidProvider,
{
self.settings.as_ref()?.try_cast()
}
pub fn brush(&self) -> ResourceDataRef<TileMapBrush> {
self.brush.data_ref()
}
pub fn tile_set(&self) -> Option<TileSetFromBrush> {
TileSetFromBrush::try_new(&self.brush)
}
}
pub struct TileSetFromBrush<'a>(ResourceDataRef<'a, TileMapBrush>);
impl Deref for TileSetFromBrush<'_> {
type Target = Resource<TileSet>;
fn deref(&self) -> &Self::Target {
self.0.tile_set.as_ref().unwrap()
}
}
impl<'a> TileSetFromBrush<'a> {
pub fn try_new(brush: &'a TileMapBrushResource) -> Option<Self> {
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()))
}
}
pub trait BrushMacro: 'static + Send + Sync {
fn uuid(&self) -> &Uuid;
fn name(&self) -> &'static str;
fn on_instance_ui_message(
&mut self,
context: &BrushMacroInstance,
message: &UiMessage,
editor: &mut Editor,
);
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<UntypedResource>;
/// 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_editor`] is called.
fn create_cell(&self, context: &BrushMacroCell) -> Option<Command>;
/// Create a command to modify the given instances's data to remove the given cell.
/// None is returned if no command is necessary, such as if the cell is already excluded
/// or no cell is selected.
fn remove_cell(&self, context: &BrushMacroCell) -> Option<Command>;
fn fill_cell_set(
&self,
context: &BrushMacroInstance,
cell_set: &mut FxHashSet<TileDefinitionHandle>,
);
/// 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<Handle<UiNode>>;
/// 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_editor`] is called.
fn build_cell_editor(
&mut self,
context: &BrushMacroCell,
ctx: &mut BuildContext,
) -> Option<Handle<UiNode>>;
/// 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);
fn begin_update(&mut self, context: &BrushMacroInstance, tile_map: &TileMapContext);
fn amend_update(
&mut self,
context: &BrushMacroInstance,
update: &mut MacroTilesUpdate,
tile_map: &TileMap,
);
fn create_command(
&mut self,
context: &BrushMacroInstance,
update: &mut MacroTilesUpdate,
tile_map: &TileMapContext,
) -> Option<Command>;
}
pub struct BrushMacroList {
content: Vec<Box<dyn BrushMacro>>,
}
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 {
#[inline]
pub fn add<T: BrushMacro>(&mut self, brush_macro: T) {
self.content
.push(Box::new(brush_macro) as Box<dyn BrushMacro>);
}
#[inline]
pub fn is_empty(&self) -> bool {
self.content.is_empty()
}
#[inline]
pub fn len(&self) -> usize {
self.content.len()
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &dyn BrushMacro> {
self.content.iter().map(|b| b.as_ref())
}
#[inline]
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut dyn BrushMacro> {
self.content.iter_mut().map(|b| b.as_mut())
}
#[inline]
pub fn get_by_index(&self, index: usize) -> Option<&dyn BrushMacro> {
self.content.get(index).map(|b| b.as_ref())
}
#[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())
}
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
}
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
}
#[inline]
pub fn clear(&mut self) {
self.content.clear();
}
}
#[derive(Debug)]
pub struct MoveMacroCommand {
pub brush: TileMapBrushResource,
pub start: usize,
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();
}
}
#[derive(Debug)]
pub struct RemoveMacroCommand {
pub brush: TileMapBrushResource,
pub index: usize,
pub data: Option<BrushMacroData>,
}
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();
}
}
}
#[derive(Debug)]
pub struct AddMacroCommand {
pub brush: TileMapBrushResource,
pub index: usize,
pub data: Option<BrushMacroData>,
}
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();
}
}
#[derive(Debug)]
pub struct SetMacroNameCommand {
pub brush: TileMapBrushResource,
pub index: usize,
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();
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TileSetPropertyValueMessage(pub TileSetPropertyValueElement);
impl TileSetPropertyValueMessage {
pub fn value(
destination: Handle<UiNode>,
direction: MessageDirection,
value: TileSetPropertyValueElement,
) -> UiMessage {
UiMessage::with_data(TileSetPropertyValueMessage(value))
.with_destination(destination)
.with_direction(direction)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TileSetPropertyMessage(pub Option<Uuid>);
impl TileSetPropertyMessage {
pub fn property_id(
destination: Handle<UiNode>,
direction: MessageDirection,
value: Option<Uuid>,
) -> UiMessage {
UiMessage::with_data(TileSetPropertyMessage(value))
.with_destination(destination)
.with_direction(direction)
}
}
#[derive(Debug, Default, Clone)]
pub struct MacroPropertyValueField {
handle: Handle<UiNode>,
textbox: Handle<UiNode>,
list: Handle<UiNode>,
}
fn make_index_and_value_list(
prop: Option<&TileSetPropertyLayer>,
value: NamableValue,
ctx: &mut BuildContext,
) -> (usize, Vec<Handle<UiNode>>) {
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 {
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::<i32>::new(wb)
.with_value(*v)
.build(ctx),
Element::F32(v) => NumericUpDownBuilder::<f32>::new(wb)
.with_value(*v)
.build(ctx),
Element::I8(v) => NumericUpDownBuilder::<i8>::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,
}
}
pub fn handle(&self) -> Handle<UiNode> {
self.handle
}
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)),
);
}
}
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::<i8>::Value(v)) = message.data() {
if message.destination() == self.textbox
&& message.direction() == MessageDirection::FromWidget
{
ui.send_message(TileSetPropertyValueMessage::value(
self.handle,
MessageDirection::FromWidget,
TileSetPropertyValueElement::I8(*v),
));
send_sync_message(
ui,
DropdownListMessage::selection(
self.list,
MessageDirection::ToWidget,
Some(find_list_index(prop, NamableValue::I8(*v))),
),
);
}
} else if let Some(NumericUpDownMessage::<i32>::Value(v)) = message.data() {
if message.destination() == self.textbox
&& message.direction() == MessageDirection::FromWidget
{
ui.send_message(TileSetPropertyValueMessage::value(
self.handle,
MessageDirection::FromWidget,
TileSetPropertyValueElement::I32(*v),
));
send_sync_message(
ui,
DropdownListMessage::selection(
self.list,
MessageDirection::ToWidget,
Some(find_list_index(prop, NamableValue::I32(*v))),
),
);
}
} else if let Some(NumericUpDownMessage::<f32>::Value(v)) = message.data() {
if message.destination() == self.textbox
&& message.direction() == MessageDirection::FromWidget
{
ui.send_message(TileSetPropertyValueMessage::value(
self.handle,
MessageDirection::FromWidget,
TileSetPropertyValueElement::F32(*v),
));
send_sync_message(
ui,
DropdownListMessage::selection(
self.list,
MessageDirection::ToWidget,
Some(find_list_index(prop, NamableValue::F32(*v))),
),
);
}
} 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<UiNode> {
DecoratorBuilder::new(BorderBuilder::new(
WidgetBuilder::new().with_child(
TextBuilder::new(WidgetBuilder::new())
.with_text(text)
.build(ctx),
),
))
.build(ctx)
}
#[derive(Debug, Default)]
pub struct MacroPropertyField {
prop_type: TileSetPropertyType,
handle: Handle<UiNode>,
list: Handle<UiNode>,
}
fn make_index_and_items(
prop_type: TileSetPropertyType,
value: Option<&Uuid>,
tile_set: Option<&TileSet>,
ctx: &mut BuildContext,
) -> (usize, Vec<Handle<UiNode>>) {
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 {
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,
}
}
pub fn handle(&self) -> Handle<UiNode> {
self.handle
}
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)),
);
}
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,
));
}
}
}
}
+38 -38
View File
@@ -61,7 +61,7 @@ impl SetColliderLayerNameCommand {
return;
};
std::mem::swap(&mut collider.name, &mut self.name);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -93,7 +93,7 @@ impl SetPropertyLayerNameCommand {
return;
};
std::mem::swap(&mut property.name, &mut self.name);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -125,7 +125,7 @@ impl SetColliderLayerColorCommand {
return;
};
std::mem::swap(&mut collider.color, &mut self.color);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -161,7 +161,7 @@ impl SetPropertyValueColorCommand {
return;
};
std::mem::swap(&mut named_value.color, &mut self.color);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -197,7 +197,7 @@ impl SetPropertyValueNameCommand {
return;
};
std::mem::swap(&mut named_value.name, &mut self.name);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -233,7 +233,7 @@ impl SetPropertyValueCommand {
return;
};
std::mem::swap(&mut named_value.value, &mut self.value);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -282,7 +282,7 @@ impl CommandTrait for AddPropertyValueCommand {
..NamedValue::default()
},
);
tile_set.change_count.set();
tile_set.change_flag.set();
}
fn revert(&mut self, _context: &mut dyn CommandContext) {
@@ -291,7 +291,7 @@ impl CommandTrait for AddPropertyValueCommand {
return;
};
property.named_values.remove(self.index);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -314,7 +314,7 @@ impl CommandTrait for RemovePropertyValueCommand {
return;
};
self.value = Some(property.named_values.remove(self.index));
tile_set.change_count.set();
tile_set.change_flag.set();
}
fn revert(&mut self, _context: &mut dyn CommandContext) {
@@ -325,7 +325,7 @@ impl CommandTrait for RemovePropertyValueCommand {
property
.named_values
.insert(self.index, self.value.take().unwrap());
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -350,13 +350,13 @@ impl CommandTrait for AddColliderLayerCommand {
..TileSetColliderLayer::default()
},
);
tile_set.change_count.set();
tile_set.change_flag.set();
}
fn revert(&mut self, _context: &mut dyn CommandContext) {
let mut tile_set = self.tile_set.data_ref();
tile_set.colliders.remove(self.index);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -383,13 +383,13 @@ impl CommandTrait for AddPropertyLayerCommand {
..TileSetPropertyLayer::default()
},
);
tile_set.change_count.set();
tile_set.change_flag.set();
}
fn revert(&mut self, _context: &mut dyn CommandContext) {
let mut tile_set = self.tile_set.data_ref();
tile_set.properties.remove(self.index);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -408,13 +408,13 @@ impl CommandTrait for MovePropertyLayerCommand {
fn execute(&mut self, _context: &mut dyn CommandContext) {
let mut tile_set = self.tile_set.data_ref();
tile_set.properties.swap(self.start, self.end);
tile_set.change_count.set();
tile_set.change_flag.set();
}
fn revert(&mut self, _context: &mut dyn CommandContext) {
let mut tile_set = self.tile_set.data_ref();
tile_set.properties.swap(self.start, self.end);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -433,7 +433,7 @@ impl MovePropertyValueCommand {
return;
};
property.named_values.swap(self.start, self.end);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -466,13 +466,13 @@ impl CommandTrait for MoveColliderLayerCommand {
fn execute(&mut self, _context: &mut dyn CommandContext) {
let mut tile_set = self.tile_set.data_ref();
tile_set.colliders.swap(self.start, self.end);
tile_set.change_count.set();
tile_set.change_flag.set();
}
fn revert(&mut self, _context: &mut dyn CommandContext) {
let mut tile_set = self.tile_set.data_ref();
tile_set.colliders.swap(self.start, self.end);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -495,7 +495,7 @@ impl CommandTrait for RemoveColliderLayerCommand {
let uuid = layer.uuid;
self.layer = Some(layer);
tile_set.swap_all_values_for_collider(uuid, &mut self.values);
tile_set.change_count.set();
tile_set.change_flag.set();
}
fn revert(&mut self, _context: &mut dyn CommandContext) {
@@ -504,7 +504,7 @@ impl CommandTrait for RemoveColliderLayerCommand {
let uuid = layer.uuid;
tile_set.colliders.insert(self.index, layer);
tile_set.swap_all_values_for_collider(uuid, &mut self.values);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -527,7 +527,7 @@ impl CommandTrait for RemovePropertyLayerCommand {
let uuid = layer.uuid;
self.layer = Some(layer);
tile_set.swap_all_values_for_property(uuid, &mut self.values);
tile_set.change_count.set();
tile_set.change_flag.set();
}
fn revert(&mut self, _context: &mut dyn CommandContext) {
@@ -536,7 +536,7 @@ impl CommandTrait for RemovePropertyLayerCommand {
let uuid = layer.uuid;
tile_set.properties.insert(self.index, layer);
tile_set.swap_all_values_for_property(uuid, &mut self.values);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -551,7 +551,7 @@ impl SetBrushPageCommand {
fn swap(&mut self) {
let mut brush = self.brush.data_ref();
swap_hash_map_entry(brush.pages.entry(self.position), &mut self.page);
brush.change_count.set();
brush.change_flag.set();
}
}
@@ -582,7 +582,7 @@ impl SetTileSetPageCommand {
swap_hash_map_entry(tile_set.pages.entry(self.position), &mut self.page);
tile_set.rebuild_transform_sets();
tile_set.rebuild_animations();
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -636,7 +636,7 @@ impl MoveTileSetPageCommand {
std::mem::swap(&mut self.start_offset, &mut self.end_offset);
tile_set.rebuild_transform_sets();
tile_set.rebuild_animations();
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -686,7 +686,7 @@ impl MoveBrushPageCommand {
swap_hash_map_entry(brush.pages.entry(*p + self.end_offset), &mut self.data[i]);
}
std::mem::swap(&mut self.start_offset, &mut self.end_offset);
brush.change_count.set();
brush.change_flag.set();
}
}
@@ -743,7 +743,7 @@ impl MoveTileSetTileCommand {
std::mem::swap(&mut self.start_offset, &mut self.end_offset);
tile_set.rebuild_transform_sets();
tile_set.rebuild_animations();
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -800,7 +800,7 @@ impl MoveBrushTileCommand {
swap_hash_map_entry(page.tiles.entry(*p + self.end_offset), &mut self.data[i]);
}
std::mem::swap(&mut self.start_offset, &mut self.end_offset);
brush.change_count.set();
brush.change_flag.set();
}
}
@@ -904,7 +904,7 @@ impl TransformTilesCommand {
.transformed(self.transformation);
}
self.transformation = self.transformation.inverted();
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -935,7 +935,7 @@ impl SetBrushTilesCommand {
if let Some(page) = brush.pages.get_mut(&self.page) {
page.tiles.swap_tiles(&mut self.tiles);
}
brush.change_count.set();
brush.change_flag.set();
}
}
@@ -963,7 +963,7 @@ impl SetTileSetTilesCommand {
fn swap(&mut self) {
let mut tile_set = self.tile_set.data_ref();
tile_set.swap(&mut self.tiles);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -1034,7 +1034,7 @@ impl ModifyAnimationSpeedCommand {
return;
};
std::mem::swap(&mut self.frame_rate, &mut anim.frame_rate);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -1069,7 +1069,7 @@ impl ModifyPageTileSizeCommand {
return;
};
std::mem::swap(&mut self.size, &mut mat.tile_size);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -1104,7 +1104,7 @@ impl ModifyPageMaterialCommand {
return;
};
std::mem::swap(&mut self.material, &mut mat.material);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -1137,7 +1137,7 @@ impl ModifyPageIconCommand {
return;
};
std::mem::swap(&mut self.icon, &mut page.icon);
tile_set.change_count.set();
tile_set.change_flag.set();
}
}
@@ -1170,7 +1170,7 @@ impl ModifyBrushPageIconCommand {
return;
};
std::mem::swap(&mut self.icon, &mut page.icon);
brush.change_count.set();
brush.change_flag.set();
}
}
@@ -1198,7 +1198,7 @@ impl ModifyBrushTileSetCommand {
fn swap(&mut self) {
let mut brush = self.brush.data_ref();
std::mem::swap(&mut self.tile_set, &mut brush.tile_set);
brush.change_count.set();
brush.change_flag.set();
}
}
+223 -27
View File
@@ -22,15 +22,21 @@
use commands::{MoveMapTileCommand, SetMapTilesCommand};
use fyrox::{
asset::untyped::UntypedResource,
fxhash::FxHashMap,
scene::tilemap::{
tileset::TileSetRef, OptionTileRect, TileCursorEffect, TileEraseEffect, TileMapData,
brush::TileMapBrushResource,
tileset::{OptionTileSet, TileSetRef},
MacroTilesUpdate, OptionTileRect, TileCursorEffect, TileEraseEffect, TileMapData,
TileOverlayEffect, TileSelectionEffect, TileSource, TileUpdateEffect, TilesUpdate,
TransTilesUpdate,
},
};
use crate::make_color_material;
use crate::{
command::{Command, CommandGroup},
make_color_material,
};
use super::*;
@@ -60,6 +66,13 @@ pub struct TileMapInteractionMode {
/// tile map control panel, allowing this object to be aware of the chosen tool
/// and the selected stamp.
state: TileDrawStateRef,
/// List if tools that can be added to a tile map brush to assist with tile map editing.
brush_macro_list: BrushMacroListRef,
/// Temporary space to store tiles that are in the process of being amended by macros.
macro_update: MacroTilesUpdate,
/// A copy of the list of macro instance resources taken from the current brush.
/// This is temporary storage that is used to avoid repeated allocation.
macro_instance_list: Vec<(Uuid, Option<UntypedResource>)>,
/// A copy of the current drawing mode that is made whenever the user
/// presses the mouse button. While the actual drawing mode may change
/// during a mouse stroke, this value never will, so nothing breaks by changing
@@ -92,13 +105,21 @@ pub struct TileMapInteractionMode {
}
impl TileMapInteractionMode {
pub fn new(tile_map: Handle<Node>, state: TileDrawStateRef, sender: MessageSender) -> Self {
pub fn new(
tile_map: Handle<Node>,
state: TileDrawStateRef,
brush_macro_list: BrushMacroListRef,
sender: MessageSender,
) -> Self {
let cursor_material = make_color_material(CURSOR_COLOR);
let select_material = make_color_material(SELECT_COLOR);
let erase_material = make_color_material(ERASE_COLOR);
Self {
tile_map,
state,
brush_macro_list,
macro_update: MacroTilesUpdate::default(),
macro_instance_list: Vec::default(),
current_tool: DrawingMode::Pick,
click_grid_position: None,
current_grid_position: None,
@@ -185,7 +206,7 @@ impl TileMapInteractionMode {
let tile_set = state.tile_set.as_ref().map(TileSetRef::new);
if let Some(mut tile_set) = tile_set {
let tile_set = tile_set.as_loaded();
for (pos, handle) in stamp.iter() {
for (pos, StampElement { handle, .. }) in stamp.iter() {
let handle = tile_set
.get_transformed_version(stamp.transformation(), *handle)
.unwrap_or(*handle);
@@ -257,7 +278,138 @@ fn update_select(
let Some(tiles) = tiles.as_loaded_ref() else {
return;
};
state.update_stamp(tile_map.tile_set().cloned(), |p| tiles.get(p));
state.update_stamp(None, tile_map.tile_set().cloned(), |p| {
tiles.get(p).map(|t| t.into())
});
}
fn macro_begin(
tile_map_context: &TileMapContext,
update: &mut TransTilesUpdate,
macro_update: &mut MacroTilesUpdate,
brush: &TileMapBrushResource,
macros: &mut BrushMacroList,
macro_instances: &mut Vec<(Uuid, Option<UntypedResource>)>,
) {
let tile_map_handle = tile_map_context.node;
let Some(tile_map) = tile_map_context.engine.scenes[tile_map_context.scene].graph
[tile_map_handle]
.cast::<TileMap>()
else {
return;
};
let brush_guard = brush.data_ref();
macro_instances.clear();
macro_instances.extend(
brush_guard
.macros
.iter()
.map(|m| (m.macro_id, m.settings.clone())),
);
if macro_instances.is_empty() {
return;
}
let tile_set = brush_guard.tile_set();
let mut tile_set_guard = tile_set.as_ref().map(|ts| ts.data_ref());
let tile_set = OptionTileSet(tile_set_guard.as_deref_mut());
update.fill_macro_tiles_update(&tile_set, macro_update);
drop(tile_set_guard);
drop(brush_guard);
let mut context = BrushMacroInstance {
brush: brush.clone(),
settings: None,
};
for (id, settings) in macro_instances.drain(..) {
let Some(brush_macro) = macros.get_by_uuid_mut(&id) else {
continue;
};
context.settings = settings;
brush_macro.begin_update(&context, tile_map_context);
brush_macro.amend_update(&context, macro_update, tile_map);
}
macro_update.fill_trans_tiles_update(update);
}
fn macro_amend_update(
tile_map: &TileMap,
update: &mut TransTilesUpdate,
macro_update: &mut MacroTilesUpdate,
brush: &TileMapBrushResource,
macros: &mut BrushMacroList,
macro_instances: &mut Vec<(Uuid, Option<UntypedResource>)>,
) {
let brush_guard = brush.data_ref();
macro_instances.clear();
macro_instances.extend(
brush_guard
.macros
.iter()
.map(|m| (m.macro_id, m.settings.clone())),
);
if macro_instances.is_empty() {
return;
}
let tile_set = brush_guard.tile_set();
let mut tile_set_guard = tile_set.as_ref().map(|ts| ts.data_ref());
let tile_set = OptionTileSet(tile_set_guard.as_deref_mut());
update.fill_macro_tiles_update(&tile_set, macro_update);
drop(tile_set_guard);
drop(brush_guard);
let mut context = BrushMacroInstance {
brush: brush.clone(),
settings: None,
};
for (id, settings) in macro_instances.drain(..) {
let Some(brush_macro) = macros.get_by_uuid_mut(&id) else {
continue;
};
context.settings = settings;
brush_macro.amend_update(&context, macro_update, tile_map);
}
macro_update.fill_trans_tiles_update(update);
}
fn macro_command_list(
tile_map: &TileMapContext,
update: &mut TransTilesUpdate,
macro_update: &mut MacroTilesUpdate,
brush: &TileMapBrushResource,
macros: &mut BrushMacroList,
macro_instances: &mut Vec<(Uuid, Option<UntypedResource>)>,
) -> Vec<Command> {
let brush_guard = brush.data_ref();
macro_instances.clear();
macro_instances.extend(
brush_guard
.macros
.iter()
.map(|m| (m.macro_id, m.settings.clone())),
);
if macro_instances.is_empty() {
return Vec::default();
}
let tile_set = brush_guard.tile_set();
let mut tile_set_guard = tile_set.as_ref().map(|ts| ts.data_ref());
let tile_set = OptionTileSet(tile_set_guard.as_deref_mut());
update.fill_macro_tiles_update(&tile_set, macro_update);
drop(tile_set_guard);
drop(brush_guard);
let mut context = BrushMacroInstance {
brush: brush.clone(),
settings: None,
};
let mut commands = Vec::default();
for (id, settings) in macro_instances.drain(..) {
let Some(brush_macro) = macros.get_by_uuid_mut(&id) else {
continue;
};
context.settings = settings;
if let Some(command) = brush_macro.create_command(&context, macro_update, tile_map) {
commands.push(command);
}
}
macro_update.fill_trans_tiles_update(update);
commands
}
fn draw(
@@ -327,7 +479,8 @@ impl InteractionMode for TileMapInteractionMode {
let Some(game_scene) = controller.downcast_mut::<GameScene>() else {
return;
};
let scene = &mut engine.scenes[game_scene.scene];
let scene_handle = game_scene.scene;
let scene = &mut engine.scenes[scene_handle];
let mods = engine.user_interfaces.first().keyboard_modifiers();
let state = self.state.lock();
self.current_tool = state.drawing_mode;
@@ -389,14 +542,24 @@ impl InteractionMode for TileMapInteractionMode {
}
mode => {
self.mouse_mode = MouseMode::Drawing;
draw(
&mut self.update_effect.lock().update,
tiles,
mode,
&state,
grid_coord,
grid_coord,
);
let update = &mut self.update_effect.lock().update;
draw(update, tiles, mode, &state, grid_coord, grid_coord);
drop(tiles_guard);
if let Some(brush) = state.stamp.brush() {
let tile_map = TileMapContext {
node: self.tile_map,
scene: scene_handle,
engine,
};
macro_begin(
&tile_map,
update,
&mut self.macro_update,
brush,
&mut self.brush_macro_list.lock(),
&mut self.macro_instance_list,
);
}
}
}
}
@@ -414,7 +577,8 @@ impl InteractionMode for TileMapInteractionMode {
let Some(game_scene) = controller.downcast_mut::<GameScene>() else {
return;
};
let scene = &mut engine.scenes[game_scene.scene];
let scene_handle = game_scene.scene;
let scene = &mut engine.scenes[scene_handle];
let Some(tile_map) = scene.graph.try_get_mut_of_type::<TileMap>(self.tile_map) else {
return;
};
@@ -454,14 +618,41 @@ impl InteractionMode for TileMapInteractionMode {
if let DrawingMode::Pick = self.current_tool {
self.selecting
.clone_from(&self.select_effect.lock().positions);
} else if let Some(tile_set) = state.tile_set.as_ref().or(tile_map.tile_set()) {
} else if let Some(tile_set) =
state.tile_set.as_ref().or(tile_map.tile_set()).cloned()
{
let update_source = &mut self.update_effect.lock().update;
let tile_map_context = TileMapContext {
node: tile_map_handle,
scene: scene_handle,
engine,
};
let mut commands = if let Some(brush) = state.stamp.brush() {
macro_command_list(
&tile_map_context,
update_source,
&mut self.macro_update,
brush,
&mut self.brush_macro_list.lock(),
&mut self.macro_instance_list,
)
} else {
Vec::default()
};
let update =
update_source.build_tiles_update(&TileSetRef::new(tile_set).as_loaded());
self.sender.do_command(SetMapTilesCommand {
update_source.build_tiles_update(&TileSetRef::new(&tile_set).as_loaded());
let command = SetMapTilesCommand {
tile_map: tile_map_handle,
tiles: update,
});
};
if !commands.is_empty() {
commands.push(Command::new(command));
self.sender.do_command(
CommandGroup::from(commands).with_custom_name("Draw Tiles"),
);
} else {
self.sender.do_command(command);
}
update_source.clear();
}
}
@@ -544,14 +735,19 @@ impl InteractionMode for TileMapInteractionMode {
end,
);
} else {
draw(
&mut self.update_effect.lock().update,
tiles,
self.current_tool,
&state,
start,
end,
);
let update = &mut self.update_effect.lock().update;
draw(update, tiles, self.current_tool, &state, start, end);
drop(tiles_guard);
if let Some(brush) = state.stamp.brush() {
macro_amend_update(
tile_map,
update,
&mut self.macro_update,
brush,
&mut self.brush_macro_list.lock(),
&mut self.macro_instance_list,
);
}
}
}
}
@@ -0,0 +1,435 @@
// 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,
gui::{
button::{Button, ButtonMessage},
grid::{Column, GridBuilder, Row},
stack_panel::StackPanelBuilder,
text::{TextBuilder, TextMessage},
VerticalAlignment,
},
scene::tilemap::brush::TileMapBrushResource,
};
use super::*;
const ADD_BUTTON_LABEL: &str = "+";
const REMOVE_BUTTON_LABEL: &str = "-";
const ADD_TOOLTIP: &str = "Add the selected cell to this macro.";
const REMOVE_TOOLTIP: &str = "Remove the selected cell from this macro.";
const CELL_WITH_MACRO_COLOR: Color = Color::DARK_SLATE_BLUE;
const CELL_WITHOUT_MACRO_COLOR: Color = Color::opaque(50, 50, 50);
#[derive(Visit, Reflect)]
pub struct MacroInspector {
handle: Handle<UiNode>,
content: Handle<UiNode>,
#[visit(skip)]
#[reflect(hidden)]
instance_list: Vec<Option<UntypedResource>>,
#[visit(skip)]
#[reflect(hidden)]
macro_list: BrushMacroListRef,
#[visit(skip)]
#[reflect(hidden)]
cell_sets: MacroCellSetListRef,
#[visit(skip)]
#[reflect(hidden)]
items: Vec<Item>,
}
impl Debug for MacroInspector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MacroInspector")
.field("handle", &self.handle)
.field("content", &self.content)
.finish()
}
}
struct Item {
macro_id: Uuid,
editor: Option<ItemEditor>,
}
struct ItemEditor {
handle: Handle<UiNode>,
header: ItemHeader,
}
struct ItemHeader {
handle: Handle<UiNode>,
label: Handle<UiNode>,
button: Handle<UiNode>,
}
impl ItemEditor {
fn new(header: ItemHeader, content: Handle<UiNode>, ctx: &mut BuildContext) -> Self {
let handle = if content.is_some() {
StackPanelBuilder::new(
WidgetBuilder::new()
.with_child(header.handle)
.with_child(content),
)
.build(ctx)
} else {
header.handle
};
Self { handle, header }
}
fn button(&self) -> Handle<UiNode> {
self.header.button
}
}
fn make_button(title: &str, tooltip: &str, ctx: &mut BuildContext) -> Handle<UiNode> {
ButtonBuilder::new(
WidgetBuilder::new()
.on_column(1)
.with_height(24.0)
.with_margin(Thickness::uniform(1.0))
.with_tooltip(make_simple_tooltip(ctx, tooltip)),
)
.with_text(title)
.build(ctx)
}
fn make_add_button(ctx: &mut BuildContext) -> Handle<UiNode> {
make_button(ADD_BUTTON_LABEL, ADD_TOOLTIP, ctx)
}
fn make_remove_button(ctx: &mut BuildContext) -> Handle<UiNode> {
make_button(REMOVE_BUTTON_LABEL, REMOVE_TOOLTIP, ctx)
}
impl ItemHeader {
fn new(name: String, has_cell: bool, ctx: &mut BuildContext) -> Self {
let label = TextBuilder::new(
WidgetBuilder::new().with_vertical_alignment(VerticalAlignment::Center),
)
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_text(name)
.build(ctx);
let button = if has_cell {
make_remove_button(ctx)
} else {
make_add_button(ctx)
};
let grid = GridBuilder::new(
WidgetBuilder::new()
.with_child(label)
.with_child(button)
.with_margin(Thickness::uniform(2.0)),
)
.add_row(Row::auto())
.add_column(Column::stretch())
.add_column(Column::strict(20.0))
.build(ctx);
let back = Brush::Solid(if has_cell {
CELL_WITH_MACRO_COLOR
} else {
CELL_WITHOUT_MACRO_COLOR
});
Self {
handle: BorderBuilder::new(
WidgetBuilder::new()
.with_background(back.into())
.with_foreground(Brush::Solid(Color::BLACK).into())
.with_child(grid),
)
.build(ctx),
label,
button,
}
}
fn sync(&self, name: String, has_cell: bool, ui: &mut UserInterface) {
ui.send_message(TextMessage::text(
self.label,
MessageDirection::ToWidget,
name,
));
let button_text = if has_cell {
REMOVE_BUTTON_LABEL
} else {
ADD_BUTTON_LABEL
};
let button = ui.node(self.button).cast::<Button>().unwrap();
ui.send_message(TextMessage::text(
*button.content,
MessageDirection::ToWidget,
button_text.into(),
));
let tooltip = if has_cell {
REMOVE_TOOLTIP
} else {
ADD_TOOLTIP
};
let tooltip = make_simple_tooltip(&mut ui.build_ctx(), tooltip);
ui.send_message(WidgetMessage::tooltip(
self.button,
MessageDirection::ToWidget,
Some(tooltip),
));
let color = if has_cell {
CELL_WITH_MACRO_COLOR
} else {
CELL_WITHOUT_MACRO_COLOR
};
ui.send_message(WidgetMessage::background(
self.handle,
MessageDirection::ToWidget,
Brush::Solid(color).into(),
));
}
}
fn make_items(
macro_list: &mut BrushMacroList,
cell_sets: &MacroCellSetList,
brush: Option<TileMapBrushResource>,
cell: Option<TileDefinitionHandle>,
ctx: &mut BuildContext,
items: &mut Vec<Item>,
) {
items.clear();
let Some(brush) = brush else {
return;
};
let mut brush_macro_list = brush.data_ref().macros.clone();
for (cell_set, instance) in cell_sets.iter().zip(brush_macro_list.iter_mut()) {
let macro_id = instance.macro_id;
let Some(m) = macro_list.get_by_uuid_mut(&macro_id) else {
items.push(Item {
macro_id,
editor: None,
});
continue;
};
if !m.can_create_cell() {
items.push(Item {
macro_id,
editor: None,
});
continue;
}
let brush_macro_cell = BrushMacroCell {
brush: brush.clone(),
settings: instance.settings.clone(),
cell,
};
let has_cell = cell
.as_ref()
.map(|c| cell_set.contains(c))
.unwrap_or_default();
let header = ItemHeader::new(std::mem::take(&mut instance.name), has_cell, ctx);
let content = m
.build_cell_editor(&brush_macro_cell, ctx)
.unwrap_or_default();
items.push(Item {
macro_id,
editor: Some(ItemEditor::new(header, content, ctx)),
});
}
}
fn fill_instance_list(
brush: Option<&TileMapBrushResource>,
instance_list: &mut Vec<Option<UntypedResource>>,
) {
instance_list.clear();
let Some(brush) = brush else {
return;
};
instance_list.extend(
brush
.data_ref()
.macros
.iter()
.map(|inst| inst.settings.clone()),
);
}
impl MacroInspector {
pub fn new(
macro_list: BrushMacroListRef,
cell_sets: MacroCellSetListRef,
brush: Option<TileMapBrushResource>,
cell: Option<TileDefinitionHandle>,
ctx: &mut BuildContext,
) -> Self {
let mut instance_list = Vec::default();
fill_instance_list(brush.as_ref(), &mut instance_list);
let mut items = Vec::default();
make_items(
&mut macro_list.lock(),
&cell_sets.lock(),
brush,
cell,
ctx,
&mut items,
);
let content = StackPanelBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(2.0))
.with_children(
items
.iter()
.filter_map(|item| item.editor.as_ref())
.map(|editor| editor.handle),
),
)
.build(ctx);
Self {
handle: BorderBuilder::new(WidgetBuilder::new().with_child(content)).build(ctx),
content,
macro_list,
cell_sets,
items,
instance_list,
}
}
pub fn handle(&self) -> Handle<UiNode> {
self.handle
}
pub fn sync_to_cell(
&mut self,
brush: Option<TileMapBrushResource>,
cell: Option<TileDefinitionHandle>,
ui: &mut UserInterface,
) {
let Some(brush) = brush else {
return;
};
let brush_guard = brush.data_ref();
let brush_instances = brush_guard.macros.iter().map(|m| m.settings.as_ref());
let inspector_instances = self.instance_list.iter().map(|inst| inst.as_ref());
let needs_rebuild = !brush_instances.eq(inspector_instances);
drop(brush_guard);
if !needs_rebuild {
self.sync_to_cell_inner(brush, cell, ui);
} else {
fill_instance_list(Some(&brush), &mut self.instance_list);
make_items(
&mut self.macro_list.lock(),
&self.cell_sets.lock(),
Some(brush),
cell,
&mut ui.build_ctx(),
&mut self.items,
);
ui.send_message(WidgetMessage::replace_children(
self.content,
MessageDirection::ToWidget,
self.items
.iter()
.filter_map(|item| item.editor.as_ref())
.map(|e| e.handle)
.collect(),
));
}
}
fn sync_to_cell_inner(
&self,
brush: TileMapBrushResource,
cell: Option<TileDefinitionHandle>,
ui: &mut UserInterface,
) {
let context = MacroMessageContext {
brush: brush.clone(),
cell,
};
for m in self.macro_list.lock().iter_mut() {
m.sync_cell_editors(&context, ui);
}
let cell_sets = self.cell_sets.lock();
let brush_guard = brush.data_ref();
for (i, item) in self.items.iter().enumerate() {
let Some(instance) = brush_guard.macros.get(i) else {
continue;
};
let Some(editor) = item.editor.as_ref() else {
continue;
};
let has_cell = cell
.map(|c| cell_sets.cell_has_macro(c, i))
.unwrap_or_default();
editor.header.sync(instance.name.clone(), has_cell, ui);
}
}
pub fn handle_ui_message(
&mut self,
brush: TileMapBrushResource,
cell: Option<TileDefinitionHandle>,
message: &UiMessage,
editor: &mut Editor,
) {
let context = MacroMessageContext {
brush: brush.clone(),
cell,
};
let mut macro_list = self.macro_list.lock();
for brush_macro in macro_list.iter_mut() {
brush_macro.on_cell_ui_message(&context, message, editor);
}
let Some(cell) = cell else {
return;
};
if let Some(ButtonMessage::Click) = message.data() {
let cell_sets = self.cell_sets.lock();
for (i, item) in self.items.iter().enumerate() {
let Some(cell_editor) = item.editor.as_ref() else {
continue;
};
if cell_editor.button() == message.destination() {
let Some(brush_macro) = macro_list.get_by_uuid_mut(&item.macro_id) else {
continue;
};
let brush_guard = brush.data_ref();
let Some(instance) = brush_guard.macros.get(i) else {
continue;
};
let settings = instance.settings.clone();
drop(brush_guard);
let command = if cell_sets.cell_has_macro(cell, i) {
let cell = Some(cell);
brush_macro.remove_cell(&BrushMacroCell {
brush,
settings,
cell,
})
} else {
let cell = Some(cell);
brush_macro.create_cell(&BrushMacroCell {
brush,
settings,
cell,
})
};
if let Some(command) = command {
editor.message_sender.send(Message::DoCommand(command));
}
break;
}
}
}
}
}
+561
View File
@@ -0,0 +1,561 @@
// 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.
//! This is the tab of the tile set editor that allows the user to modify the collider
//! layers stored within the tile set. Layers can be created, deleted, renamed
//! and their colors can be modified.
use core::f32;
use brush::{BrushMacroData, TileMapBrushResource};
use fyrox::gui::{
button::ButtonMessage,
grid::*,
list_view::{ListView, ListViewBuilder, ListViewMessage},
scroll_viewer::ScrollViewerBuilder,
stack_panel::StackPanelBuilder,
text::{TextBuilder, TextMessage},
text_box::{TextBoxBuilder, TextCommitMode},
utils::{make_arrow, ArrowDirection},
HorizontalAlignment, VerticalAlignment,
};
use fyrox::scene::tilemap::*;
use crate::{send_sync_message, MSG_SYNC_FLAG};
use super::*;
const MISSING_MACRO: &str = "UNKNOWN MACRO";
/// This is the tab of the tile set editor that allows the user to modify the macros
/// stored within the brush. Macro instances can be created, deleted, renamed
/// and their settings can be modified.
pub struct MacroTab {
handle: Handle<UiNode>,
macros: BrushMacroListRef,
current_macro_id: Option<Uuid>,
macro_panel: Handle<UiNode>,
list: Handle<UiNode>,
up_button: Handle<UiNode>,
down_button: Handle<UiNode>,
remove_button: Handle<UiNode>,
add_buttons: Box<[Handle<UiNode>]>,
data_panel: Handle<UiNode>,
name_field: Handle<UiNode>,
}
fn make_arrow_button(
ctx: &mut BuildContext,
dir: ArrowDirection,
column: usize,
row: usize,
) -> Handle<UiNode> {
let arrow = make_arrow(ctx, dir, 16.0);
ButtonBuilder::new(
WidgetBuilder::new()
.on_column(column)
.on_row(row)
.with_height(24.0)
.with_margin(Thickness::uniform(1.0)),
)
.with_content(arrow)
.build(ctx)
}
fn make_button(
title: &str,
tooltip: &str,
ctx: &mut BuildContext,
column: usize,
row: usize,
) -> Handle<UiNode> {
ButtonBuilder::new(
WidgetBuilder::new()
.on_column(column)
.on_row(row)
.with_height(24.0)
.with_margin(Thickness::uniform(1.0))
.with_tooltip(make_simple_tooltip(ctx, tooltip)),
)
.with_text(title)
.build(ctx)
}
pub fn make_list_item(
ctx: &mut BuildContext,
instance_name: &str,
macro_name: Option<&str>,
) -> Handle<UiNode> {
let macro_name = macro_name.unwrap_or(MISSING_MACRO);
let content = GridBuilder::new(
WidgetBuilder::new()
.with_child(
TextBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::right(5.0))
.on_column(0),
)
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_horizontal_text_alignment(HorizontalAlignment::Right)
.with_text(macro_name)
.build(ctx),
)
.with_child(
TextBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::left(5.0))
.on_column(1),
)
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_horizontal_text_alignment(HorizontalAlignment::Left)
.with_text(instance_name)
.build(ctx),
),
)
.add_row(Row::auto())
.add_column(Column::stretch())
.add_column(Column::stretch())
.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)
}
fn make_instance_items(
ctx: &mut BuildContext,
brush: TileMapBrushResource,
macros: &BrushMacroList,
) -> Vec<Handle<UiNode>> {
brush
.data_ref()
.macros
.iter()
.map(|d| {
make_list_item(
ctx,
d.name.as_str(),
macros.get_by_uuid(&d.macro_id).map(|m| m.name()),
)
})
.collect()
}
fn make_add_button(title: &str, ctx: &mut BuildContext) -> Handle<UiNode> {
ButtonBuilder::new(
WidgetBuilder::new()
.with_height(24.0)
.with_margin(Thickness::uniform(1.0)),
)
.with_text(title)
.build(ctx)
}
fn make_add_buttons(ctx: &mut BuildContext, macros: &BrushMacroList) -> Box<[Handle<UiNode>]> {
macros
.iter()
.map(|m| make_add_button(m.name(), ctx))
.collect()
}
fn make_macro_instance_editor(
ctx: &mut BuildContext,
brush: TileMapBrushResource,
macros: &mut BrushMacroList,
instance: Option<BrushMacroData>,
) -> Option<Handle<UiNode>> {
let instance = instance?;
let brush_macro = macros.get_by_uuid_mut(&instance.macro_id)?;
brush_macro.build_instance_editor(
&BrushMacroInstance {
brush,
settings: instance.settings.clone(),
},
ctx,
)
}
impl MacroTab {
pub fn new(macros: BrushMacroListRef, tile_book: TileBook, ctx: &mut BuildContext) -> Self {
let macros_guard = macros.lock();
let items;
if let TileBook::Brush(brush) = &tile_book {
items = make_instance_items(ctx, brush.clone(), &macros_guard);
} else {
items = Vec::default();
}
let add_buttons = make_add_buttons(ctx, &macros_guard);
let properties_scroll = ScrollViewerBuilder::new(WidgetBuilder::new()).build(ctx);
let list = ListViewBuilder::new(WidgetBuilder::new().on_row(1))
.with_items(items)
.with_scroll_viewer(properties_scroll)
.build(ctx);
let up_button = make_arrow_button(ctx, ArrowDirection::Top, 0, 0);
let down_button = make_arrow_button(ctx, ArrowDirection::Bottom, 1, 0);
let up_down = GridBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(2.0))
.on_row(2)
.with_child(up_button)
.with_child(down_button),
)
.add_row(Row::auto())
.add_column(Column::stretch())
.add_column(Column::stretch())
.build(ctx);
let add_button_panel = ScrollViewerBuilder::new(
WidgetBuilder::new()
.with_max_size(Vector2::new(f32::INFINITY, 100.0))
.with_margin(Thickness::uniform(2.0))
.on_row(3),
)
.with_horizontal_scroll_allowed(false)
.with_content(
StackPanelBuilder::new(WidgetBuilder::new().with_children(add_buttons.iter().copied()))
.build(ctx),
)
.build(ctx);
let left_label = TextBuilder::new(
WidgetBuilder::new()
.with_horizontal_alignment(HorizontalAlignment::Center)
.with_margin(Thickness::uniform(2.0)),
)
.with_text("Macros:")
.build(ctx);
let left_side = GridBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(2.0))
.with_child(left_label)
.with_child(list)
.with_child(up_down)
.with_child(add_button_panel),
)
.add_row(Row::auto())
.add_row(Row::stretch())
.add_row(Row::strict(30.0))
.add_row(Row::auto())
.add_column(Column::stretch())
.build(ctx);
let name_text = TextBuilder::new(
WidgetBuilder::new()
.with_vertical_alignment(VerticalAlignment::Center)
.with_margin(Thickness::right(4.0)),
)
.with_text("Name:")
.build(ctx);
let name_field = TextBoxBuilder::new(WidgetBuilder::new().with_height(20.0).on_column(1))
.with_text_commit_mode(TextCommitMode::Changed)
.with_vertical_text_alignment(VerticalAlignment::Center)
.build(ctx);
let remove_button = make_button(
"Delete",
"Delete this collider from every tile in the tile set.",
ctx,
2,
0,
);
let header = GridBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(2.0))
.with_child(name_text)
.with_child(name_field)
.with_child(remove_button),
)
.add_column(Column::auto())
.add_column(Column::stretch())
.add_column(Column::strict(100.0))
.add_row(Row::auto())
.build(ctx);
let macro_panel =
BorderBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(2.0)))
.with_stroke_thickness(Thickness::uniform(1.0).into())
.build(ctx);
let data_panel = GridBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(2.0))
.on_column(1)
.with_child(header)
.with_child(
ScrollViewerBuilder::new(WidgetBuilder::new().on_row(1))
.with_content(macro_panel)
.with_horizontal_scroll_allowed(false)
.build(ctx),
),
)
.add_row(Row::auto())
.add_row(Row::stretch())
.add_column(Column::stretch())
.build(ctx);
drop(macros_guard);
Self {
handle: GridBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(2.0))
.with_child(left_side)
.with_child(data_panel),
)
.add_row(Row::stretch())
.add_column(Column::stretch())
.add_column(Column::stretch())
.build(ctx),
macros,
current_macro_id: None,
macro_panel,
list,
add_buttons,
up_button,
down_button,
remove_button,
data_panel,
name_field,
}
}
pub fn handle(&self) -> Handle<UiNode> {
self.handle
}
pub fn sync_to_model(&mut self, brush: TileMapBrushResource, ui: &mut UserInterface) {
let items = make_instance_items(&mut ui.build_ctx(), brush.clone(), &self.macros.lock());
ui.send_message(ListViewMessage::items(
self.list,
MessageDirection::ToWidget,
items,
));
self.sync_data(brush, ui);
}
fn sync_data(&mut self, brush: TileMapBrushResource, ui: &mut UserInterface) {
let sel_index = self.selection_index(ui);
let brush_guard = brush.data_ref();
let brush_macro = sel_index.and_then(|i| brush_guard.macros.get(i));
let name = brush_macro.map(|m| m.name.clone()).unwrap_or_default();
let macro_id = brush_macro.map(|m| m.macro_id);
ui.send_message(WidgetMessage::enabled(
self.data_panel,
MessageDirection::ToWidget,
brush_macro.is_some(),
));
send_sync_message(
ui,
TextMessage::text(self.name_field, MessageDirection::ToWidget, name),
);
if macro_id == self.current_macro_id {
if let Some(brush_macro) = brush_macro {
let macro_id = brush_macro.macro_id;
let settings = brush_macro.settings.clone();
drop(brush_guard);
let mut macro_list = self.macros.lock();
if let Some(m) = macro_list.get_by_uuid_mut(&macro_id) {
m.sync_instance_editor(
&BrushMacroInstance {
brush: brush.clone(),
settings,
},
ui,
);
}
}
} else {
let instance = brush_macro.cloned();
drop(brush_guard);
self.current_macro_id = macro_id;
let mut macro_list = self.macros.lock();
let editor = make_macro_instance_editor(
&mut ui.build_ctx(),
brush.clone(),
&mut macro_list,
instance,
);
ui.send_message(WidgetMessage::replace_children(
self.macro_panel,
MessageDirection::ToWidget,
editor.into_iter().collect(),
));
}
}
pub fn handle_ui_message(
&mut self,
brush: &TileMapBrushResource,
message: &UiMessage,
editor: &mut Editor,
) {
if message.direction() == MessageDirection::ToWidget || message.flags == MSG_SYNC_FLAG {
return;
}
if let Some(sel_index) = self.selection_index(editor.engine.user_interfaces.first_mut()) {
let brush_guard = brush.data_ref();
let instance = brush_guard.macros.get(sel_index);
if let Some(instance) = instance {
let mut macro_list = self.macros.lock();
if let Some(m) = macro_list.get_by_uuid_mut(&instance.macro_id) {
let settings = instance.settings.clone();
drop(brush_guard);
m.on_instance_ui_message(
&BrushMacroInstance {
brush: brush.clone(),
settings,
},
message,
editor,
);
}
}
}
let ui = editor.engine.user_interfaces.first_mut();
let sender = &editor.message_sender;
if let Some(ListViewMessage::SelectionChanged(_)) = message.data() {
if message.destination() == self.list {
self.sync_data(brush.clone(), ui);
}
} else if let Some(TextMessage::Text(value)) = message.data() {
if message.destination() == self.name_field {
self.update_name(brush, value, ui, sender);
}
} else if let Some(ButtonMessage::Click) = message.data() {
if message.destination() == self.up_button {
self.move_instance(brush, -1, ui, sender);
} else if message.destination() == self.down_button {
self.move_instance(brush, 1, ui, sender);
} else if message.destination() == self.remove_button {
self.remove_instance(brush, ui, sender);
} else if let Some(index) = self
.add_buttons
.iter()
.position(|&h| h == message.destination())
{
self.add_instance(brush, index, ui, sender);
}
}
}
fn selection_index(&self, ui: &UserInterface) -> Option<usize> {
ui.node(self.list)
.cast::<ListView>()?
.selection
.last()
.copied()
}
fn update_name(
&self,
resource: &TileMapBrushResource,
name: &str,
ui: &UserInterface,
sender: &MessageSender,
) {
let brush = resource.data_ref();
let macros = &brush.macros;
let Some(sel_index) = self
.selection_index(ui)
.map(|i| i.clamp(0, macros.len().saturating_sub(1)))
else {
return;
};
ui.send_message(WidgetMessage::focus(
self.name_field,
MessageDirection::ToWidget,
));
sender.do_command(SetMacroNameCommand {
brush: resource.clone(),
index: sel_index,
name: name.into(),
});
}
fn move_instance(
&self,
resource: &TileMapBrushResource,
amount: isize,
ui: &UserInterface,
sender: &MessageSender,
) {
let brush = resource.data_ref();
let Some(sel_index) = self.selection_index(ui) else {
return;
};
let new_index = sel_index
.saturating_add_signed(amount)
.clamp(0, brush.macros.len().saturating_sub(1));
if sel_index == new_index {
return;
}
ui.send_message(ListViewMessage::selection(
self.list,
MessageDirection::ToWidget,
vec![new_index],
));
sender.do_command(MoveMacroCommand {
brush: resource.clone(),
start: sel_index,
end: new_index,
});
}
fn add_instance(
&self,
resource: &TileMapBrushResource,
macro_index: usize,
ui: &UserInterface,
sender: &MessageSender,
) {
let brush = resource.data_ref();
let index = self
.selection_index(ui)
.map(|i| i + 1)
.unwrap_or(0)
.clamp(0, brush.macros.len());
let macros = self.macros.lock();
let Some(brush_macro) = macros.get_by_index(macro_index) else {
return;
};
let data = brush_macro.create_instance(resource);
let data = Some(BrushMacroData {
macro_id: *brush_macro.uuid(),
name: String::default(),
settings: data,
});
ui.send_message(ListViewMessage::selection(
self.list,
MessageDirection::ToWidget,
vec![index],
));
sender.do_command(AddMacroCommand {
brush: resource.clone(),
index,
data,
});
}
fn remove_instance(
&self,
resource: &TileMapBrushResource,
ui: &UserInterface,
sender: &MessageSender,
) {
let brush = resource.data_ref();
let Some(index) = self
.selection_index(ui)
.map(|i| i.clamp(0, brush.macros.len().saturating_sub(1)))
else {
return;
};
sender.do_command(RemoveMacroCommand {
brush: resource.clone(),
index,
data: None,
});
}
}
+151 -16
View File
@@ -22,12 +22,16 @@
#![allow(clippy::collapsible_match)] // STFU
mod autotile;
mod brush_macro;
mod collider_editor;
mod colliders_tab;
mod commands;
mod handle_editor;
mod handle_field;
mod interaction_mode;
mod macro_inspector;
mod macro_tab;
mod misc;
pub mod palette;
pub mod panel;
@@ -39,16 +43,28 @@ mod tile_editor;
mod tile_inspector;
mod tile_prop_editor;
pub mod tileset;
mod wfc;
use autotile::*;
pub use brush_macro::*;
use collider_editor::*;
use colliders_tab::*;
use fyrox::core::futures::executor::block_on;
use fyrox::core::log::Log;
use fyrox::fxhash::FxHashMap;
use fyrox::gui::grid::{Column, GridBuilder, Row};
use fyrox::gui::style::resource::StyleResourceExt;
use fyrox::gui::style::Style;
use fyrox::gui::text::TextBuilder;
use fyrox::gui::{message::KeyCode, texture::TextureResource};
use fyrox::scene::tilemap::TileMapEffectRef;
use fyrox::gui::{HorizontalAlignment, VerticalAlignment};
use fyrox::scene::tilemap::brush::TileMapBrushResource;
use fyrox::scene::tilemap::tileset::{TileSetPropertyLayer, ELEMENT_MATCH_HIGHLIGHT_COLOR};
use fyrox::scene::tilemap::{StampElement, TileMapEffectRef};
pub use handle_editor::*;
use handle_field::*;
use interaction_mode::*;
use macro_inspector::*;
use palette::PaletteWidget;
use panel::TileMapPanel;
use panel_preview::*;
@@ -57,6 +73,7 @@ use tile_bounds_editor::*;
use tile_editor::*;
use tile_inspector::*;
use tile_prop_editor::*;
use wfc::*;
use crate::fyrox::{
core::{
@@ -135,6 +152,57 @@ lazy_static! {
load_image!("../../../resources/palette.png");
}
#[derive(Default, Debug, Clone)]
pub struct MacroCellSetList {
content: Vec<FxHashSet<TileDefinitionHandle>>,
cells_by_page: FxHashMap<Vector2<i32>, FxHashSet<Vector2<i32>>>,
}
impl Deref for MacroCellSetList {
type Target = Vec<FxHashSet<TileDefinitionHandle>>;
fn deref(&self) -> &Self::Target {
&self.content
}
}
impl DerefMut for MacroCellSetList {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.content
}
}
impl MacroCellSetList {
pub fn clear(&mut self) {
self.content.clear();
self.cells_by_page.clear();
}
pub fn cell_has_any_macro(&self, handle: TileDefinitionHandle) -> bool {
self.content.iter().any(|s| s.contains(&handle))
}
pub fn cell_has_macro(&self, handle: TileDefinitionHandle, index: usize) -> bool {
self.content
.get(index)
.map(|s| s.contains(&handle))
.unwrap_or_default()
}
pub fn cells_on_page(&self, page: Vector2<i32>) -> Option<&FxHashSet<Vector2<i32>>> {
self.cells_by_page.get(&page)
}
pub fn finalize(&mut self) {
self.cells_by_page.clear();
for set in self.content.iter() {
for handle in set.iter() {
_ = self
.cells_by_page
.entry(handle.page())
.or_default()
.insert(handle.tile());
}
}
}
}
fn make_drawing_mode_button(
ctx: &mut BuildContext,
width: f32,
@@ -239,6 +307,8 @@ pub struct TileMapEditorPlugin {
/// The state that is shared to allow this plugin to coordinate with the tile map
/// interaction mode, the tile map control panel, and the tile set editor.
state: TileDrawStateRef,
/// List if tools that can be added to a tile map brush to assist with tile map editing.
brush_macro_list: BrushMacroListRef,
/// The tile set editor, if it is open.
tile_set_editor: Option<TileSetEditor>,
/// The tile map control panel, if it is open. The control panel allows the user
@@ -462,12 +532,17 @@ impl TileDrawState {
/// and tile set. The given `tile_handle` function is used to determine the handles
/// for each selection position.
#[inline]
pub fn update_stamp<F>(&mut self, tile_set: Option<TileSetResource>, tile_handle: F)
where
F: Fn(Vector2<i32>) -> Option<TileDefinitionHandle>,
pub fn update_stamp<F>(
&mut self,
brush: Option<TileMapBrushResource>,
tile_set: Option<TileSetResource>,
tile_handle: F,
) where
F: Fn(Vector2<i32>) -> Option<StampElement>,
{
self.tile_set = tile_set;
self.stamp.build(
brush,
self.selection
.positions
.iter()
@@ -587,7 +662,12 @@ impl TileMapEditorPlugin {
let Some(tile_map) = self.get_tile_map_mut(editor) else {
return;
};
let mut interaction_mode = TileMapInteractionMode::new(handle, self.state.clone(), sender);
let mut interaction_mode = TileMapInteractionMode::new(
handle,
self.state.clone(),
self.brush_macro_list.clone(),
sender,
);
interaction_mode.on_tile_map_selected(tile_map);
// Prepare the tile map interaction mode.
let Some(entry) = editor.scenes.current_scene_entry_mut() else {
@@ -608,6 +688,9 @@ impl EditorPlugin for TileMapEditorPlugin {
.asset_browser
.preview_generators
.add(TileSet::type_uuid(), TileSetPreview);
let state = editor.engine.resource_manager.state();
state.constructors_container.add::<AutoTileInstance>();
state.constructors_container.add::<WfcInstance>();
}
fn on_exit(&mut self, _editor: &mut Editor) {
@@ -649,18 +732,12 @@ impl EditorPlugin for TileMapEditorPlugin {
return;
}
let ui = editor.engine.user_interfaces.first_mut();
if let Some(tile_set_editor) = self.tile_set_editor.take() {
self.tile_set_editor = tile_set_editor.handle_ui_message(
message,
ui,
&editor.engine.resource_manager,
&editor.message_sender,
editor.engine.serialization_context.clone(),
);
self.tile_set_editor = tile_set_editor.handle_ui_message(message, editor);
}
let ui = editor.engine.user_interfaces.first_mut();
if let Some(OpenTilePanelMessage { resource, center }) = message.data() {
self.open_panel_for_tile_set(resource.clone(), *center, ui, &editor.message_sender);
} else if let Some(&TileDefinitionHandleEditorMessage::Goto(handle)) = message.data() {
@@ -718,9 +795,19 @@ impl EditorPlugin for TileMapEditorPlugin {
let ui = editor.engine.user_interfaces.first_mut();
let tile_book: Option<TileBook> = if let Message::OpenTileSetEditor(tile_set) = message {
Some(TileBook::TileSet(tile_set.clone()))
Log::verify(block_on(tile_set.clone()));
tile_set
.is_ok()
.then(|| TileBook::TileSet(tile_set.clone()))
} else if let Message::OpenTileMapBrushEditor(brush) = message {
Some(TileBook::Brush(brush.clone()))
let brush = brush.clone();
Log::verify(block_on(brush.clone()));
if brush.is_ok() {
let is_loaded = brush.data_ref().block_until_tile_set_is_loaded();
is_loaded.then_some(TileBook::Brush(brush))
} else {
None
}
} else {
None
};
@@ -730,6 +817,7 @@ impl EditorPlugin for TileMapEditorPlugin {
let mut tile_set_editor = TileSetEditor::new(
tile_book.clone(),
self.state.clone(),
self.brush_macro_list.clone(),
editor.message_sender.clone(),
editor.engine.resource_manager.clone(),
&mut ui.build_ctx(),
@@ -797,3 +885,50 @@ impl EditorPlugin for TileMapEditorPlugin {
}
}
}
/// Create one item for the dropdown list.
pub fn make_named_value_list_option(
ctx: &mut BuildContext,
color: Color,
name: &str,
) -> Handle<UiNode> {
let icon = BorderBuilder::new(
WidgetBuilder::new()
.on_column(0)
.with_background(Brush::Solid(color).into()),
)
.build(ctx);
let text = TextBuilder::new(WidgetBuilder::new().on_column(1))
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_horizontal_text_alignment(HorizontalAlignment::Left)
.with_text(name)
.build(ctx);
let grid = GridBuilder::new(WidgetBuilder::new().with_child(icon).with_child(text))
.add_column(Column::strict(20.0))
.add_column(Column::stretch())
.add_row(Row::auto())
.build(ctx);
DecoratorBuilder::new(
BorderBuilder::new(WidgetBuilder::new().with_child(grid))
.with_corner_radius((4.0).into())
.with_pad_by_corner_radius(false),
)
.build(ctx)
}
/// Create the items for the dropdown list list that lets the user select a named value.
pub fn make_named_value_list_items(
layer: &TileSetPropertyLayer,
ctx: &mut BuildContext,
) -> Vec<Handle<UiNode>> {
let custom =
make_named_value_list_option(ctx, ELEMENT_MATCH_HIGHLIGHT_COLOR.to_opaque(), "Custom");
std::iter::once(custom)
.chain(
layer
.named_values
.iter()
.map(|v| make_named_value_list_option(ctx, v.color.to_opaque(), &v.name)),
)
.collect()
}
+62 -35
View File
@@ -65,6 +65,7 @@ use super::{commands::*, *};
pub const DEFAULT_MATERIAL_COLOR: Color = Color::from_rgba(255, 255, 255, 125);
pub const CURSOR_HIGHLIGHT_COLOR: Color = Color::from_rgba(255, 255, 255, 50);
pub const MACRO_CELL_HIGHLIGHT_COLOR: Color = Color::DARK_SLATE_BLUE;
const MOUSE_CLICK_DELAY_FRAMES: usize = 1;
const NO_PAGE_COLOR: Color = Color::from_rgba(20, 5, 5, 255);
@@ -166,7 +167,7 @@ fn calc_slice_coord(position: f32, step: f32) -> usize {
/// Displays a scrollable grid of till cells, with options to allow the tiles
/// to be selected, dragged, and edits in various ways.
#[derive(Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
#[derive(Clone, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
#[type_uuid(id = "5356a864-c026-4bd7-a4b1-30bacf77d8fa")]
pub struct PaletteWidget {
widget: Widget,
@@ -220,6 +221,10 @@ pub struct PaletteWidget {
#[visit(skip)]
#[reflect(hidden)]
highlight: FxHashMap<Subposition, Color>,
/// The cells which should be marked as being included in some macro.
#[visit(skip)]
#[reflect(hidden)]
macro_cells: Option<MacroCellSetListRef>,
#[visit(skip)]
#[reflect(hidden)]
colliders: Vec<ColliderHighlight>,
@@ -255,6 +260,20 @@ pub struct PaletteWidget {
define_widget_deref!(PaletteWidget);
impl Debug for PaletteWidget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PaletteWidget")
.field("widget", &self.widget)
.field("content", &self.content)
.field("page", &self.page)
.field("kind", &self.kind)
.field("editable", &self.editable)
.field("material_color", &self.material_color)
.field("tile_size", &self.tile_size)
.finish()
}
}
type PaletteTriangleData = (Vec<Point2<f32>>, Vec<[u32; 3]>);
#[derive(Debug, Clone)]
@@ -301,7 +320,7 @@ impl PaletteOverlay {
pub fn set_to_stamp(&mut self, stamp: &Stamp, tile_set: &OptionTileSet) {
self.movable_tiles.clear();
self.erased_tiles.clear();
for (pos, handle) in stamp.iter() {
for (pos, StampElement { handle, .. }) in stamp.iter() {
let data = tile_set
.get_transformed_render_data(stamp.transformation(), *handle)
.unwrap_or_else(TileRenderData::missing_data);
@@ -486,7 +505,7 @@ impl PaletteWidget {
TileBook::Brush(resource) => {
if let Some(source_set) = source_set
.cloned()
.or_else(|| resource.state().data()?.tile_set.clone())
.or_else(|| resource.state().data()?.tile_set())
{
let mut source_set = TileSetRef::new(&source_set);
self.sender.do_command(SetBrushTilesCommand {
@@ -584,16 +603,19 @@ impl PaletteWidget {
DrawingMode::Pick
}
}
pub fn sync_selection_to_model(&mut self) {
fn update_stamp(&self, state: &mut TileDrawStateGuardMut) {
let page = self.page.unwrap_or_default();
let brush = self.content.brush_ref().cloned();
state.update_stamp(brush, self.content.get_tile_set(), |p| {
self.content
.get_stamp_element(ResourceTilePosition::new(self.stage(), page, p))
});
}
pub fn sync_selection_to_model(&mut self) {
let mut state = self.state.lock_mut("sync_selection_to_model");
state.tile_set = self.content.get_tile_set();
self.selecting_tiles.clear();
let stamp_trans = state.stamp.transformation();
state.update_stamp(self.content.get_tile_set(), |p| {
self.content
.get_tile_handle(ResourceTilePosition::new(self.stage(), page, p))
});
self.update_stamp(&mut state);
state.stamp.transform(stamp_trans);
}
fn update_selection(&mut self) {
@@ -604,9 +626,7 @@ impl PaletteWidget {
if self.kind == TilePaletteStage::Tiles && self.page.is_none() {
return;
}
let page = self.page.unwrap_or_default();
let mut state = self.state.lock_mut("update_selection");
state.tile_set = self.content.get_tile_set();
state.set_palette(self.handle);
let positions = state.selection_positions_mut();
positions.clone_from(&self.selecting_tiles);
@@ -616,10 +636,7 @@ impl PaletteWidget {
} else {
positions.extend(rect.iter());
}
state.update_stamp(self.content.get_tile_set(), |p| {
self.content
.get_tile_handle(ResourceTilePosition::new(self.stage(), page, p))
});
self.update_stamp(&mut state);
}
fn finalize_selection(&mut self, ui: &mut UserInterface) {
let MouseMode::Drawing { end, .. } = self.mode.clone() else {
@@ -634,16 +651,12 @@ impl PaletteWidget {
}
TilePaletteStage::Pages => self.send_new_page(end_tile, ui),
}
let page = self.page.unwrap_or_default();
let mut state = self.state.lock_mut("finalize_selection");
state.tile_set = self.content.get_tile_set();
state.set_palette(self.handle);
let positions = state.selection_positions();
self.selecting_tiles.clone_from(positions);
state.update_stamp(self.content.get_tile_set(), |p| {
self.content
.get_tile_handle(ResourceTilePosition::new(self.stage(), page, p))
});
self.update_stamp(&mut state);
}
fn select_all(&mut self) {
let Some(page) = self.page else {
@@ -659,25 +672,19 @@ impl PaletteWidget {
let sel = state.selection_positions_mut();
sel.clear();
sel.extend(results.iter().copied());
state.update_stamp(self.content.get_tile_set(), |p| {
self.content
.get_tile_handle(ResourceTilePosition::new(self.stage(), page, p))
});
self.update_stamp(&mut state);
}
fn select_one(&mut self, position: Vector2<i32>) {
let Some(page) = self.page else {
if self.page.is_none() {
return;
};
}
let mut state = self.state.lock_mut("select_one");
state.tile_set = self.content.get_tile_set();
state.set_palette(self.handle);
let sel = state.selection_positions_mut();
sel.clear();
sel.insert(position);
state.update_stamp(self.content.get_tile_set(), |p| {
self.content
.get_tile_handle(ResourceTilePosition::new(self.stage(), page, p))
});
self.update_stamp(&mut state);
}
fn begin_motion(&mut self, mode: DrawingMode, pos: MousePos, ui: &mut UserInterface) {
match mode {
@@ -1040,17 +1047,17 @@ impl PaletteWidget {
let tile_set = tile_set.data_ref();
for uuid in self.state.lock().visible_colliders.iter() {
for (pos, handle) in self.update.iter() {
let Some((trans, handle)) = handle else {
let Some((trans, handle)) = handle.as_ref().map(|h| h.pair()) else {
continue;
};
let Some(color) = tile_set.collider_color(*uuid) else {
continue;
};
let tile_collider = tile_set.tile_collider(*handle, *uuid);
let tile_collider = tile_set.tile_collider(handle, *uuid);
if tile_collider.is_none() {
continue;
}
self.push_tile_collider(*pos, *trans, tile_collider, ctx);
self.push_tile_collider(*pos, trans, tile_collider, ctx);
self.commit_color(color, ctx);
}
}
@@ -1345,10 +1352,10 @@ impl Control for PaletteWidget {
let mut tile_set = tile_set.state();
if let Some(tile_set) = tile_set.data() {
for (pos, v) in self.update.iter() {
let Some((t, h)) = v else {
let Some((t, h)) = v.as_ref().map(|v| v.pair()) else {
continue;
};
let Some(data) = tile_set.get_transformed_render_data(*t, *h) else {
let Some(data) = tile_set.get_transformed_render_data(t, h) else {
continue;
};
let t = self.tile_size;
@@ -1456,6 +1463,18 @@ impl Control for PaletteWidget {
self.commit_color(Color::GREEN_YELLOW, ctx);
}
}
if stage == TilePaletteStage::Tiles {
if let Some(page) = self.page {
if let Some(cell_set_list) = &self.macro_cells {
if let Some(cells_on_page) = cell_set_list.lock().cells_on_page(page) {
for position in cells_on_page.iter() {
self.push_cell_rect(*position, line_thickness * 6.0, ctx);
}
self.commit_color(MACRO_CELL_HIGHLIGHT_COLOR, ctx);
}
}
}
}
let state = self.state.lock();
if state.selection_palette() == self.handle {
for sel in state.selection_positions() {
@@ -1562,6 +1581,7 @@ pub struct PaletteWidgetBuilder {
page: Option<Vector2<i32>>,
sender: MessageSender,
state: TileDrawStateRef,
macro_cells: Option<MacroCellSetListRef>,
kind: TilePaletteStage,
editable: bool,
}
@@ -1577,6 +1597,7 @@ impl PaletteWidgetBuilder {
tile_book: TileBook::Empty,
sender,
state,
macro_cells: None,
kind: TilePaletteStage::default(),
editable: false,
page: None,
@@ -1603,6 +1624,11 @@ impl PaletteWidgetBuilder {
self
}
pub fn with_macro_cells(mut self, macro_cells: MacroCellSetListRef) -> Self {
self.macro_cells = Some(macro_cells);
self
}
pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
ctx.add_node(UiNode::new(PaletteWidget {
widget: self
@@ -1612,6 +1638,7 @@ impl PaletteWidgetBuilder {
.build(ctx),
sender: self.sender,
state: self.state,
macro_cells: self.macro_cells,
overlay: PaletteOverlay::default(),
content: self.tile_book,
kind: self.kind,
+1 -1
View File
@@ -512,7 +512,7 @@ impl TileMapPanel {
return;
};
let brush = brush.data_ref();
let Some(tile_set) = brush.tile_set.clone() else {
let Some(tile_set) = brush.tile_set() else {
return;
};
self.tile_book = TileBook::TileSet(tile_set);
+1 -1
View File
@@ -196,7 +196,7 @@ impl Control for PanelPreview {
let time = ctx.elapsed_time;
for (pos, &handle) in stamp.iter() {
for (pos, &StampElement { handle, .. }) in stamp.iter() {
let handle = tile_set
.get_animated_version(time, handle)
.unwrap_or(handle);
+46 -10
View File
@@ -32,7 +32,7 @@ use crate::{
send_sync_message, MSG_SYNC_FLAG,
};
use fyrox::{
asset::{manager::ResourceManager, ResourceDataRef},
asset::ResourceDataRef,
core::{
algebra::Vector2, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
visitor::prelude::*,
@@ -359,6 +359,16 @@ impl<'a> TileEditorState<'a> {
.copied()
.filter_map(move |p| TileDefinitionHandle::try_new(page?, p))
}
/// If exactly one brush cell is selected, return the position of that cell within the brush.
pub fn selected_brush_cell(&self) -> Option<TileDefinitionHandle> {
if self.is_brush() {
let mut iter = self.tile_handles();
let result = iter.next()?;
iter.next().is_none().then_some(result)
} else {
None
}
}
/// Iterate the selected positions in the form of `TileDefinitionHandle` using the current page for page coordinates.
/// and skip any position that already contains a tile.
pub fn empty_tiles(&self) -> impl Iterator<Item = TileDefinitionHandle> + '_ {
@@ -716,6 +726,8 @@ pub struct TileInspector {
#[visit(skip)]
#[reflect(hidden)]
state: TileDrawStateRef,
/// Widget for editing macro cell data.
macro_inspector: MacroInspector,
/// The tile set editor palette widget that allows the user to select a page.
/// This is *not* a widget within the TileInspector, but the TileInspector needs to have
/// the handle in order to determine where the user is selecting.
@@ -773,10 +785,11 @@ impl Debug for TileInspector {
impl TileInspector {
pub fn new(
state: TileDrawStateRef,
macro_list: BrushMacroListRef,
cell_sets: MacroCellSetListRef,
pages_palette: Handle<UiNode>,
tiles_palette: Handle<UiNode>,
tile_book: TileBook,
_resource_manager: ResourceManager,
sender: MessageSender,
ctx: &mut BuildContext,
) -> Self {
@@ -853,6 +866,13 @@ impl TileInspector {
let page_icon_field = TileHandleFieldBuilder::new(WidgetBuilder::new())
.with_label("Page Icon")
.build(ctx);
let macro_inspector = MacroInspector::new(
macro_list,
cell_sets,
tile_book.brush_ref().cloned(),
None,
ctx,
);
let tile_editor_state = TileEditorStateRef {
page: None,
state: state.clone(),
@@ -874,12 +894,14 @@ impl TileInspector {
.with_child(create_tile)
.with_children(tile_editors.iter().map(|e| e.lock().handle()))
.with_child(property_editors.handle)
.with_child(collider_editors.handle),
.with_child(collider_editors.handle)
.with_child(macro_inspector.handle()),
)
.build(ctx);
Self {
handle,
state,
macro_inspector,
pages_palette,
tiles_palette,
tile_book,
@@ -943,6 +965,15 @@ impl TileInspector {
let tile_data_selected = state.tile_data().next().is_some();
let mat_page_selected = state.material_page().is_some();
let anim_page_selected = state.animation_page().is_some();
let brush_tile = state.selected_brush_cell();
drop(state);
self.macro_inspector.sync_to_cell(
tile_editor_state.tile_book.brush_ref().cloned(),
brush_tile,
ui,
);
let state = tile_editor_state.lock();
send_visibility(ui, self.macro_inspector.handle(), brush_tile.is_some());
send_visibility(ui, self.tile_set_page_creator, tile_set_empty_pages);
send_visibility(ui, self.brush_page_creator, brush_empty_pages);
send_visibility(ui, self.create_tile, empty_tiles);
@@ -977,7 +1008,7 @@ impl TileInspector {
.chain(self.property_editors.iter())
.chain(self.collider_editors.iter());
for editor_ref in iter {
let mut editor = editor_ref.lock();
let mut editor = editor_ref.try_lock().expect("Failed to lock editor_ref");
editor.sync_to_state(&state, ui);
let draw_button = editor.draw_button();
drop(editor);
@@ -1038,20 +1069,25 @@ impl TileInspector {
);
}
}
pub fn handle_ui_message(
&self,
message: &UiMessage,
ui: &mut UserInterface,
sender: &MessageSender,
) {
pub fn handle_ui_message(&mut self, message: &UiMessage, editor: &mut Editor) {
let ui = editor.engine.user_interfaces.first_mut();
if message.flags == MSG_SYNC_FLAG || message.direction() == MessageDirection::ToWidget {
return;
}
if !ui.is_node_child_of(message.destination(), self.handle()) {
return;
}
if let Some(brush) = self.tile_book.brush_ref() {
let tile_editor_state = self.tile_editor_state(ui);
let cell = tile_editor_state.lock().selected_brush_cell();
drop(tile_editor_state);
self.macro_inspector
.handle_ui_message(brush.clone(), cell, message, editor);
}
let ui = editor.engine.user_interfaces.first_mut();
let tile_editor_state = self.tile_editor_state(ui);
let mut tile_editor_state = tile_editor_state.lock();
let sender = &editor.message_sender;
let iter = self
.tile_editors
.iter()
+3 -47
View File
@@ -46,7 +46,7 @@ use fyrox::{
text_box::TextBoxBuilder,
utils::make_simple_tooltip,
widget::WidgetBuilder,
BuildContext, HorizontalAlignment, Thickness, UiNode, UserInterface, VerticalAlignment,
BuildContext, Thickness, UiNode, UserInterface,
},
scene::tilemap::{tileset::*, TileDataUpdate, TileSetUpdate},
};
@@ -143,7 +143,7 @@ impl TilePropertyEditor {
.on_column(1)
.with_visibility(!prop_layer.named_values.is_empty()),
)
.with_items(build_list(prop_layer, ctx))
.with_items(make_named_value_list_items(prop_layer, ctx))
.with_selected(index)
.build(ctx);
let grid = GridBuilder::new(
@@ -208,7 +208,7 @@ impl TilePropertyEditor {
MessageDirection::ToWidget,
layer.name.to_string(),
));
let list = build_list(layer, &mut ui.build_ctx());
let list = make_named_value_list_items(layer, &mut ui.build_ctx());
ui.send_message(DropdownListMessage::items(
self.list,
MessageDirection::ToWidget,
@@ -646,50 +646,6 @@ impl TileEditor for TilePropertyEditor {
}
}
/// Create one item for the dropdown list.
pub fn make_named_value_list_option(
ctx: &mut BuildContext,
color: Color,
name: &str,
) -> Handle<UiNode> {
let icon = BorderBuilder::new(
WidgetBuilder::new()
.on_column(0)
.with_background(Brush::Solid(color).into()),
)
.build(ctx);
let text = TextBuilder::new(WidgetBuilder::new().on_column(1))
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_horizontal_text_alignment(HorizontalAlignment::Left)
.with_text(name)
.build(ctx);
let grid = GridBuilder::new(WidgetBuilder::new().with_child(icon).with_child(text))
.add_column(Column::strict(20.0))
.add_column(Column::stretch())
.add_row(Row::auto())
.build(ctx);
DecoratorBuilder::new(
BorderBuilder::new(WidgetBuilder::new().with_child(grid))
.with_corner_radius((4.0).into())
.with_pad_by_corner_radius(false),
)
.build(ctx)
}
/// Create the list items for the dropdown list.
fn build_list(layer: &TileSetPropertyLayer, ctx: &mut BuildContext) -> Vec<Handle<UiNode>> {
let custom =
make_named_value_list_option(ctx, ELEMENT_MATCH_HIGHLIGHT_COLOR.to_opaque(), "Custom");
std::iter::once(custom)
.chain(
layer
.named_values
.iter()
.map(|v| make_named_value_list_option(ctx, v.color.to_opaque(), &v.name)),
)
.collect()
}
/// Each of the nine buttons has a label and a color to indicate
/// which number is stored in the corresponding element of the value.
/// When the number matches one of the pre-defined values, then the
+92 -19
View File
@@ -22,13 +22,12 @@
//! It serves to edit both tile sets and brushes, since they are conceptually similiar,
//! with only minor modifications needed when switching between the two modes.
use super::*;
use super::{commands::*, *};
use crate::{
command::{Command, CommandGroup},
fyrox::{
asset::manager::ResourceManager,
core::{algebra::Vector2, color::Color, log::Log, pool::Handle},
engine::SerializationContext,
gui::{
border::BorderBuilder,
brush::Brush,
@@ -53,9 +52,9 @@ use crate::{
message::MessageSender,
plugins::inspector::editors::resource::{ResourceFieldBuilder, ResourceFieldMessage},
};
use commands::*;
use fyrox::scene::tilemap::brush::TileMapBrushResource;
use macro_tab::MacroTab;
use palette::{PaletteWidgetBuilder, DEFAULT_MATERIAL_COLOR};
use std::sync::Arc;
const TAB_MARGIN: Thickness = Thickness {
left: 10.0,
@@ -73,6 +72,7 @@ pub struct TileSetEditor {
/// the tile map control panel that allows the user to switch
/// tools and select stamps, and others.
state: TileDrawStateRef,
macro_list: BrushMacroListRef,
/// The resource to be edited. It can either be a tile set or a brush.
tile_book: TileBook,
/// The field that controls the tint of the background material on tile atlas pages.
@@ -109,6 +109,13 @@ pub struct TileSetEditor {
properties_tab: PropertiesTab,
/// The tab that allows users to add, remove, and edit collider layers.
colliders_tab: CollidersTab,
/// The tab that allows users to add, remove, and edit brush macros.
macros_tab: MacroTab,
/// The set of cells associated with each macro instance in order.
/// The length of this Vec matches the length of the brush's [`TileMapBrush::macros`]
/// list, and each element of this Vec is the set of cells for the macro
/// at the same index in the brush's macro list.
brush_macro_cell_sets: MacroCellSetListRef,
}
fn make_tab(name: &str, content: Handle<UiNode>, ctx: &mut BuildContext) -> TabDefinition {
@@ -155,14 +162,48 @@ fn tile_set_to_title(tile_book: &TileBook) -> String {
}
}
fn build_brush_macro_cell_sets(
macro_list: &BrushMacroList,
brush: TileMapBrushResource,
brush_macro_cell_sets: &mut MacroCellSetList,
) {
let brush_guard = brush.data_ref();
let instances = &brush_guard.macros;
brush_macro_cell_sets.resize_with(instances.len(), FxHashSet::default);
for (instance, set) in instances.iter().zip(brush_macro_cell_sets.iter_mut()) {
set.clear();
let Some(m) = macro_list.get_by_uuid(&instance.macro_id) else {
continue;
};
m.fill_cell_set(
&BrushMacroInstance {
brush: brush.clone(),
settings: instance.settings.clone(),
},
set,
);
}
brush_macro_cell_sets.finalize();
}
impl TileSetEditor {
pub fn new(
tile_book: TileBook,
state: TileDrawStateRef,
macro_list: BrushMacroListRef,
sender: MessageSender,
resource_manager: ResourceManager,
ctx: &mut BuildContext,
) -> Self {
let mut brush_macro_cell_sets = MacroCellSetList::default();
if let Some(brush) = tile_book.brush_ref() {
build_brush_macro_cell_sets(
&macro_list.lock(),
brush.clone(),
&mut brush_macro_cell_sets,
);
}
let brush_macro_cell_sets = MacroCellSetListRef::new(brush_macro_cell_sets);
let tile_set_field =
ResourceFieldBuilder::<TileSet>::new(WidgetBuilder::new().on_column(1), sender.clone())
.with_resource(tile_book.get_tile_set())
@@ -259,14 +300,16 @@ impl TileSetEditor {
.with_resource(tile_book.clone())
.with_kind(TilePaletteStage::Tiles)
.with_editable(true)
.with_macro_cells(brush_macro_cell_sets.clone())
.build(ctx);
let tile_inspector = TileInspector::new(
state.clone(),
macro_list.clone(),
brush_macro_cell_sets.clone(),
pages_palette,
tiles_palette,
tile_book.clone(),
resource_manager,
sender,
ctx,
);
@@ -351,10 +394,12 @@ impl TileSetEditor {
let properties_tab = PropertiesTab::new(tile_book.clone(), ctx);
let colliders_tab = CollidersTab::new(tile_book.clone(), ctx);
let macros_tab = MacroTab::new(macro_list.clone(), tile_book.clone(), ctx);
let tab_control = TabControlBuilder::new(WidgetBuilder::new())
.with_tab(make_tab("Tiles", tile_tab, ctx))
.with_tab(make_tab("Properties", properties_tab.handle(), ctx))
.with_tab(make_tab("Collision", colliders_tab.handle(), ctx))
.with_tab(make_tab("Macros", macros_tab.handle(), ctx))
.build(ctx);
let window = WindowBuilder::new(WidgetBuilder::new().with_width(800.0).with_height(600.0))
@@ -376,7 +421,9 @@ impl TileSetEditor {
window,
properties_tab,
colliders_tab,
macros_tab,
state,
macro_list,
tab_control,
color_field,
cell_position,
@@ -391,6 +438,7 @@ impl TileSetEditor {
tile_inspector,
tile_set_selector,
tile_set_field,
brush_macro_cell_sets,
};
editor.sync_to_model(ctx.inner_mut());
@@ -401,6 +449,15 @@ impl TileSetEditor {
pub fn set_tile_resource(&mut self, tile_book: TileBook, ui: &mut UserInterface) {
self.try_save();
self.tile_book = tile_book.clone();
if let Some(brush) = tile_book.brush_ref() {
build_brush_macro_cell_sets(
&self.macro_list.lock(),
brush.clone(),
&mut self.brush_macro_cell_sets.lock(),
);
} else {
self.brush_macro_cell_sets.lock().clear();
}
self.tile_inspector.set_tile_resource(tile_book.clone(), ui);
ui.send_message(WindowMessage::title(
self.window,
@@ -418,7 +475,7 @@ impl TileSetEditor {
ui.send_message(ResourceFieldMessage::value(
self.tile_set_field,
MessageDirection::ToWidget,
brush.data_ref().tile_set.clone(),
brush.data_ref().tile_set(),
));
}
self.send_tabs_visible(tile_book.is_tile_set(), ui);
@@ -440,15 +497,24 @@ impl TileSetEditor {
Some(Vector2::new(0, 0)),
));
}
self.sync_to_model(ui);
}
fn send_tabs_visible(&self, visibility: bool, ui: &mut UserInterface) {
fn send_tabs_visible(&self, is_tile_set: bool, ui: &mut UserInterface) {
let tab_control = ui.node(self.tab_control).cast::<TabControl>().unwrap();
let tabs = tab_control.headers_container;
let children = ui.node(tabs).children();
for &tab in &children[1..3] {
ui.send_message(WidgetMessage::visibility(
tab,
MessageDirection::ToWidget,
is_tile_set,
));
}
ui.send_message(WidgetMessage::visibility(
tabs,
children[3],
MessageDirection::ToWidget,
visibility,
!is_tile_set,
));
}
@@ -532,11 +598,19 @@ impl TileSetEditor {
let tile_set = tile_set.as_loaded();
self.properties_tab.sync_to_model(&tile_set, ui);
self.colliders_tab.sync_to_model(&tile_set, ui);
self.brush_macro_cell_sets.lock().clear();
} else if let Some(brush) = self.tile_book.brush_ref() {
build_brush_macro_cell_sets(
&self.macro_list.lock(),
brush.clone(),
&mut self.brush_macro_cell_sets.lock(),
);
self.macros_tab.sync_to_model(brush.clone(), ui);
}
self.tile_inspector.sync_to_model(ui);
if let TileBook::Brush(brush) = &self.tile_book {
let brush = brush.data_ref();
let tile_set = brush.tile_set.clone();
let tile_set = brush.tile_set();
ui.send_message(ResourceFieldMessage::value(
self.tile_set_field,
MessageDirection::ToWidget,
@@ -549,21 +623,20 @@ impl TileSetEditor {
Log::verify(self.tile_book.save());
}
pub fn handle_ui_message(
mut self,
message: &UiMessage,
ui: &mut UserInterface,
_resource_manager: &ResourceManager,
sender: &MessageSender,
_serialization_context: Arc<SerializationContext>,
) -> Option<Self> {
self.tile_inspector.handle_ui_message(message, ui, sender);
pub fn handle_ui_message(mut self, message: &UiMessage, editor: &mut Editor) -> Option<Self> {
self.tile_inspector.handle_ui_message(message, editor);
if let Some(r) = self.tile_book.tile_set_ref() {
let ui = editor.engine.user_interfaces.first_mut();
let sender = &editor.message_sender;
self.properties_tab
.handle_ui_message(r.clone(), message, ui, sender);
self.colliders_tab
.handle_ui_message(r.clone(), message, ui, sender);
} else if let Some(r) = self.tile_book.brush_ref() {
self.macros_tab.handle_ui_message(r, message, editor);
}
let ui = editor.engine.user_interfaces.first_mut();
let sender = &editor.message_sender;
if let Some(WindowMessage::Close) = message.data() {
if message.destination() == self.window {
self.try_save();
+887
View File
@@ -0,0 +1,887 @@
// 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::{ResourceKind, UntypedResource},
Resource, ResourceData,
},
core::swap_hash_map_entry,
fxhash::FxHashMap,
gui::{
button::ButtonMessage,
check_box::{CheckBoxBuilder, CheckBoxMessage},
formatted_text::WrapMode,
numeric::{NumericUpDownBuilder, NumericUpDownMessage},
stack_panel::StackPanelBuilder,
},
rand::thread_rng,
scene::tilemap::{
brush::TileMapBrushResource,
tileset::{
NamableValue, TileSetPropertyF32, TileSetPropertyId, TileSetPropertyNine,
TileSetPropertyType, TileSetPropertyValueElement,
},
MacroTilesUpdate, TileSetWfcConstraint, TileSetWfcPropagator, TileTerrainId,
},
};
use crate::{
command::{Command, CommandContext, CommandTrait},
send_sync_message,
};
use super::*;
const DEFAULT_MAX_ATTEMPTS: u32 = 10;
const DEFAULT_CONSTRAIN_EDGES: bool = true;
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. ");
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.");
#[derive(Default)]
pub struct WfcMacro {
pattern_list: MacroPropertyField,
frequency_list: MacroPropertyField,
edges_toggle: Handle<UiNode>,
attempts_field: Handle<UiNode>,
terrain_list: Vec<TerrainWidgets>,
value_field: MacroPropertyValueField,
add_button: Handle<UiNode>,
terrain_stack: Handle<UiNode>,
current_terrain: TileTerrainId,
constraint: TileSetWfcConstraint,
propagator: TileSetWfcPropagator,
}
#[derive(Debug, Clone, Visit, Reflect, TypeUuidProvider)]
#[type_uuid(id = "24f9947e-f58b-4623-ad14-cb21cd09297e")]
pub(super) struct WfcInstance {
frequency_property: Option<TileSetPropertyF32>,
pattern_property: Option<TileSetPropertyNine>,
#[reflect(hidden)]
terrain_freq: FxHashMap<TileTerrainId, f32>,
max_attempts: u32,
constrain_edges: bool,
#[reflect(hidden)]
cells: FxHashSet<TileDefinitionHandle>,
}
impl Default for WfcInstance {
fn default() -> Self {
Self {
frequency_property: None,
pattern_property: None,
terrain_freq: FxHashMap::default(),
max_attempts: DEFAULT_MAX_ATTEMPTS,
constrain_edges: DEFAULT_CONSTRAIN_EDGES,
cells: FxHashSet::default(),
}
}
}
#[derive(Debug, Default, Clone)]
struct TerrainWidgets {
terrain: TileTerrainId,
color: Color,
name: String,
frequency_field: Handle<UiNode>,
delete_button: Handle<UiNode>,
}
fn terrain_list_needs_rebuild(
terrain_freq: &[(TileTerrainId, f32)],
layer: Option<&TileSetPropertyLayer>,
list: &[TerrainWidgets],
) -> bool {
let new_iter = terrain_freq.iter().map(|&(id, _)| {
let color;
let name;
if let Some(layer) = layer {
color = layer
.value_to_color(NamableValue::I8(id))
.unwrap_or(ELEMENT_MATCH_HIGHLIGHT_COLOR);
name = layer.value_to_name(NamableValue::I8(id));
} else {
color = ELEMENT_MATCH_HIGHLIGHT_COLOR;
name = "".into();
}
(id, color, name)
});
let old_iter = list.iter().map(|w| (w.terrain, w.color, w.name.clone()));
!new_iter.eq(old_iter)
}
fn sync_terrain_list(
terrain_freq: &[(TileTerrainId, f32)],
list: &[TerrainWidgets],
ui: &mut UserInterface,
) {
let freq_iter = terrain_freq.iter().map(|&(_, freq)| freq);
let handle_iter = list.iter().map(|w| w.frequency_field);
for (handle, freq) in handle_iter.zip(freq_iter) {
send_sync_message(
ui,
NumericUpDownMessage::value(handle, MessageDirection::ToWidget, freq),
);
}
}
fn make_terrain_list(
terrain_freq: &[(TileTerrainId, f32)],
layer: Option<&TileSetPropertyLayer>,
list: &mut Vec<TerrainWidgets>,
ctx: &mut BuildContext,
) -> Vec<Handle<UiNode>> {
list.clear();
let mut result = Vec::default();
for &(terrain, frequency) in terrain_freq {
let (handle, widgets) = make_terrain_list_element(terrain, frequency, layer, ctx);
list.push(widgets);
result.push(handle);
}
result
}
fn make_terrain_list_element(
terrain: TileTerrainId,
frequency: f32,
layer: Option<&TileSetPropertyLayer>,
ctx: &mut BuildContext,
) -> (Handle<UiNode>, TerrainWidgets) {
let number = TextBuilder::new(WidgetBuilder::new())
.with_text(terrain.to_string())
.with_horizontal_text_alignment(HorizontalAlignment::Right)
.build(ctx);
let color;
let name;
if let Some(layer) = layer {
color = layer
.value_to_color(NamableValue::I8(terrain))
.unwrap_or(ELEMENT_MATCH_HIGHLIGHT_COLOR);
name = layer.value_to_name(NamableValue::I8(terrain));
} else {
color = ELEMENT_MATCH_HIGHLIGHT_COLOR;
name = "".into();
}
let icon = BorderBuilder::new(
WidgetBuilder::new()
.on_column(1)
.with_background(Brush::Solid(color).into()),
)
.build(ctx);
let name_text = TextBuilder::new(WidgetBuilder::new().on_column(2))
.with_text(name.clone())
.build(ctx);
let frequency_field = NumericUpDownBuilder::new(
WidgetBuilder::new()
.on_column(3)
.with_margin(Thickness::left_right(5.0)),
)
.with_value(frequency)
.with_min_value(0.0)
.build(ctx);
let delete_button = ButtonBuilder::new(
WidgetBuilder::new()
.on_column(4)
.with_margin(Thickness::uniform(2.0)),
)
.with_text("Delete")
.build(ctx);
let handle = GridBuilder::new(
WidgetBuilder::new()
.with_child(number)
.with_child(icon)
.with_child(name_text)
.with_child(frequency_field)
.with_child(delete_button)
.with_margin(Thickness::uniform(2.0)),
)
.add_row(Row::auto())
.add_column(Column::strict(50.0))
.add_column(Column::strict(20.0))
.add_column(Column::strict(100.0))
.add_column(Column::stretch())
.add_column(Column::strict(50.0))
.build(ctx);
let widgets = TerrainWidgets {
terrain,
color,
name,
frequency_field,
delete_button,
};
(handle, widgets)
}
impl WfcInstance {
fn sorted_terrain_list(&self) -> Vec<(TileTerrainId, f32)> {
let mut result = Vec::default();
result.extend(self.terrain_freq.iter().map(|(&id, &f)| (id, f)));
result.sort_by_key(|&(id, _)| id);
result
}
}
impl ResourceData for WfcInstance {
fn type_uuid(&self) -> Uuid {
<Self as TypeUuidProvider>::type_uuid()
}
fn save(&mut self, _path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
Err("Saving is not supported!".to_string().into())
}
fn can_be_saved(&self) -> bool {
false
}
}
impl BrushMacro for WfcMacro {
fn uuid(&self) -> &Uuid {
&uuid!("2d14ef6a-6422-4b97-a9c9-ae5bcdfecd7e")
}
fn name(&self) -> &'static str {
"Wave Function Collapse"
}
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(&CheckBoxMessage::Check(Some(checked))) = message.data() {
if message.destination() == self.edges_toggle {
editor.message_sender.do_command(SetConstrainEdgesCommand {
brush: context.brush.clone(),
instance: context.settings().unwrap(),
data: checked,
});
}
} else if let Some(&NumericUpDownMessage::<u32>::Value(value)) = message.data() {
if message.destination() == self.attempts_field {
editor.message_sender.do_command(SetMaxAttemptsCommand {
brush: context.brush.clone(),
instance: context.settings().unwrap(),
data: value,
});
}
} else if let Some(ButtonMessage::Click) = message.data() {
if message.destination() == self.add_button {
editor
.message_sender
.do_command(SetTerrainFrequencyCommand {
brush: context.brush.clone(),
instance: context.settings().unwrap(),
terrain_id: self.current_terrain,
data: Some(1.0),
});
} else {
for w in self.terrain_list.iter() {
if message.destination() == w.delete_button {
editor
.message_sender
.do_command(SetTerrainFrequencyCommand {
brush: context.brush.clone(),
instance: context.settings().unwrap(),
terrain_id: w.terrain,
data: None,
});
}
}
}
} else if let Some(&TileSetPropertyValueMessage(TileSetPropertyValueElement::I8(id))) =
message.data()
{
if message.destination() == self.value_field.handle() {
self.current_terrain = id;
}
} else if let Some(&NumericUpDownMessage::<f32>::Value(frequency)) = message.data() {
for w in self.terrain_list.iter() {
if message.destination() == w.frequency_field {
editor
.message_sender
.do_command(SetTerrainFrequencyCommand {
brush: context.brush.clone(),
instance: context.settings().unwrap(),
terrain_id: w.terrain,
data: Some(frequency),
});
}
}
} else {
let tile_set = tile_set.data_ref();
self.pattern_list.on_ui_message(&tile_set, message, ui);
self.frequency_list.on_ui_message(&tile_set, message, ui);
let instance = context.settings::<WfcInstance>().unwrap();
let instance = instance.data_ref();
let pattern_id = instance
.pattern_property
.as_ref()
.map(|p| p.property_uuid());
let terrain_layer = pattern_id.and_then(|id| tile_set.find_property(*id));
self.value_field.on_ui_message(terrain_layer, message, ui);
}
}
fn on_cell_ui_message(
&mut self,
_context: &MacroMessageContext,
_message: &UiMessage,
_editor: &mut Editor,
) {
}
fn create_instance(&self, _brush: &TileMapBrushResource) -> Option<UntypedResource> {
Some(UntypedResource::new_ok(
ResourceKind::Embedded,
WfcInstance::default(),
))
}
fn can_create_cell(&self) -> bool {
true
}
fn fill_cell_set(
&self,
context: &BrushMacroInstance,
cell_set: &mut FxHashSet<TileDefinitionHandle>,
) {
let Some(data) = context
.settings
.as_ref()
.and_then(|r| r.try_cast::<WfcInstance>())
else {
return;
};
let data = data.data_ref();
cell_set.extend(data.cells.iter());
}
fn create_cell(&self, context: &BrushMacroCell) -> Option<Command> {
let instance = context.settings()?;
let cell = context.cell?;
Some(Command::new(SetCellCommand {
brush: context.brush.clone(),
cell,
instance,
included: true,
}))
}
fn remove_cell(&self, context: &BrushMacroCell) -> Option<Command> {
let instance = context.settings()?;
let cell = context.cell?;
Some(Command::new(SetCellCommand {
brush: context.brush.clone(),
cell,
instance,
included: false,
}))
}
fn build_instance_editor(
&mut self,
context: &BrushMacroInstance,
ctx: &mut BuildContext,
) -> Option<Handle<UiNode>> {
let instance = context.settings::<WfcInstance>().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 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 constrain_edges = instance.constrain_edges;
let attempts = instance.max_attempts;
self.attempts_field = NumericUpDownBuilder::new(WidgetBuilder::new().on_column(1))
.with_value(attempts)
.build(ctx);
self.edges_toggle = CheckBoxBuilder::new(WidgetBuilder::new())
.checked(Some(constrain_edges))
.build(ctx);
let edges_field = GridBuilder::new(
WidgetBuilder::new()
.with_child(
TextBuilder::new(WidgetBuilder::new().on_column(1))
.with_text("Constrain Edges")
.build(ctx),
)
.with_child(self.edges_toggle),
)
.add_row(Row::auto())
.add_column(Column::strict(20.0))
.add_column(Column::stretch())
.build(ctx);
let attempts_field = GridBuilder::new(
WidgetBuilder::new()
.with_child(
TextBuilder::new(WidgetBuilder::new())
.with_text("Max Attempts")
.build(ctx),
)
.with_child(self.attempts_field),
)
.add_row(Row::auto())
.add_column(Column::strict(150.0))
.add_column(Column::stretch())
.build(ctx);
let terrain_layer =
tile_set.and_then(|tile_set| pattern_id.and_then(|id| tile_set.find_property(*id)));
self.value_field = MacroPropertyValueField::new(
WidgetBuilder::new(),
"Terrain".into(),
TileSetPropertyValueElement::I8(self.current_terrain),
terrain_layer,
ctx,
);
self.add_button = ButtonBuilder::new(
WidgetBuilder::new()
.on_column(1)
.with_margin(Thickness::uniform(1.0)),
)
.with_text("Add")
.build(ctx);
let add_row_field = GridBuilder::new(
WidgetBuilder::new()
.with_child(self.value_field.handle())
.with_child(self.add_button),
)
.add_row(Row::auto())
.add_column(Column::stretch())
.add_column(Column::strict(50.0))
.build(ctx);
self.terrain_stack =
StackPanelBuilder::new(WidgetBuilder::new().with_children(make_terrain_list(
&instance.sorted_terrain_list(),
terrain_layer,
&mut self.terrain_list,
ctx,
)))
.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(edges_field)
.with_child(attempts_field)
.with_child(add_row_field)
.with_child(self.terrain_stack),
)
.build(ctx);
Some(handle)
}
fn build_cell_editor(
&mut self,
_context: &BrushMacroCell,
_ctx: &mut BuildContext,
) -> Option<Handle<UiNode>> {
None
}
fn sync_instance_editor(&mut self, context: &BrushMacroInstance, ui: &mut UserInterface) {
let Some(instance) = context.settings::<WfcInstance>() 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,
CheckBoxMessage::checked(
self.edges_toggle,
MessageDirection::ToWidget,
Some(instance.constrain_edges),
),
);
send_sync_message(
ui,
NumericUpDownMessage::<u32>::value(
self.attempts_field,
MessageDirection::ToWidget,
instance.max_attempts,
),
);
let layer =
tile_set.and_then(|tile_set| pattern_id.and_then(|id| tile_set.find_property(*id)));
self.value_field.sync(
TileSetPropertyValueElement::I8(self.current_terrain),
layer,
ui,
);
let terrain_freq = instance.sorted_terrain_list();
if terrain_list_needs_rebuild(&terrain_freq, layer, &self.terrain_list) {
let list = make_terrain_list(
&terrain_freq,
layer,
&mut self.terrain_list,
&mut ui.build_ctx(),
);
ui.send_message(WidgetMessage::replace_children(
self.terrain_stack,
MessageDirection::ToWidget,
list,
));
} else {
sync_terrain_list(&terrain_freq, &self.terrain_list, ui);
}
}
fn sync_cell_editors(&mut self, _context: &MacroMessageContext, _ui: &mut UserInterface) {}
fn begin_update(&mut self, _context: &BrushMacroInstance, _tile_map: &TileMapContext) {}
fn amend_update(
&mut self,
_context: &BrushMacroInstance,
_update: &mut MacroTilesUpdate,
_tile_map: &TileMap,
) {
}
fn create_command(
&mut self,
context: &BrushMacroInstance,
update: &mut MacroTilesUpdate,
tile_map: &TileMapContext,
) -> Option<Command> {
let Some(tile_set) = tile_map.tile_set() else {
self.constraint.clear();
return None;
};
if context.tile_set().as_deref() != Some(tile_set) {
self.constraint.clear();
return None;
}
let instance = context.settings::<WfcInstance>().unwrap();
let instance = instance.data_ref();
let Some(pattern_property) = instance.pattern_property else {
self.constraint.clear();
return None;
};
let frequency_property = instance.frequency_property;
Log::verify(self.constraint.fill_pattern_map(
&tile_set.data_ref(),
pattern_property,
frequency_property,
&instance.terrain_freq,
));
let mut rng = thread_rng();
for _ in 0..instance.max_attempts {
self.propagator.fill_from(self.constraint.deref());
for (&p, v) in update.iter() {
if let Some(StampElement {
brush_cell: Some(cell),
..
}) = v
{
if instance.cells.contains(cell) {
self.propagator.add_cell(p);
}
}
}
if instance.constrain_edges
&& self
.propagator
.constrain_edges(
&tile_set.data_ref(),
pattern_property,
tile_map.tile_map(),
update,
self.constraint.deref(),
)
.is_err()
{
return None;
}
if let Ok(()) = self
.propagator
.observe_all(&mut rng, self.constraint.deref())
{
self.propagator
.apply_autotile_to_update(&mut rng, &self.constraint, update);
return None;
}
}
Log::err(format!(
"WFC failed after {} attempts",
instance.max_attempts
));
self.propagator
.apply_autotile_to_update(&mut rng, &self.constraint, update);
None
}
}
#[derive(Debug)]
struct SetCellCommand {
pub brush: TileMapBrushResource,
pub instance: Resource<WfcInstance>,
pub cell: TileDefinitionHandle,
pub included: bool,
}
impl SetCellCommand {
fn swap(&mut self) {
let mut instance = self.instance.data_ref();
let contains = instance.cells.contains(&self.cell);
if contains != self.included {
if self.included {
_ = instance.cells.insert(self.cell);
} else {
_ = instance.cells.remove(&self.cell);
}
self.included = contains;
self.brush.data_ref().change_flag.set();
}
}
}
impl CommandTrait for SetCellCommand {
fn name(&mut self, _context: &dyn CommandContext) -> String {
"Update Wave Function Collapse 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<WfcInstance>,
pub data: Option<TileSetPropertyNine>,
}
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<WfcInstance>,
pub data: Option<TileSetPropertyF32>,
}
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 SetTerrainFrequencyCommand {
pub brush: TileMapBrushResource,
pub instance: Resource<WfcInstance>,
pub terrain_id: TileTerrainId,
pub data: Option<f32>,
}
impl SetTerrainFrequencyCommand {
fn swap(&mut self) {
let mut instance = self.instance.data_ref();
swap_hash_map_entry(instance.terrain_freq.entry(self.terrain_id), &mut self.data);
self.brush.data_ref().change_flag.set();
}
}
impl CommandTrait for SetTerrainFrequencyCommand {
fn name(&mut self, _context: &dyn CommandContext) -> String {
"Update Terrain Frequency".into()
}
fn execute(&mut self, _context: &mut dyn CommandContext) {
self.swap();
}
fn revert(&mut self, _context: &mut dyn CommandContext) {
self.swap();
}
}
#[derive(Debug)]
struct SetConstrainEdgesCommand {
pub brush: TileMapBrushResource,
pub instance: Resource<WfcInstance>,
pub data: bool,
}
impl SetConstrainEdgesCommand {
fn swap(&mut self) {
let mut instance = self.instance.data_ref();
std::mem::swap(&mut instance.constrain_edges, &mut self.data);
self.brush.data_ref().change_flag.set();
}
}
impl CommandTrait for SetConstrainEdgesCommand {
fn name(&mut self, _context: &dyn CommandContext) -> String {
"Update Constrain Edges".into()
}
fn execute(&mut self, _context: &mut dyn CommandContext) {
self.swap();
}
fn revert(&mut self, _context: &mut dyn CommandContext) {
self.swap();
}
}
#[derive(Debug)]
struct SetMaxAttemptsCommand {
pub brush: TileMapBrushResource,
pub instance: Resource<WfcInstance>,
pub data: u32,
}
impl SetMaxAttemptsCommand {
fn swap(&mut self) {
let mut instance = self.instance.data_ref();
std::mem::swap(&mut instance.max_attempts, &mut self.data);
self.brush.data_ref().change_flag.set();
}
}
impl CommandTrait for SetMaxAttemptsCommand {
fn name(&mut self, _context: &dyn CommandContext) -> String {
"Update Max Attempts".into()
}
fn execute(&mut self, _context: &mut dyn CommandContext) {
self.swap();
}
fn revert(&mut self, _context: &mut dyn CommandContext) {
self.swap();
}
}
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "fyrox-autotile"
license = "MIT"
version = "0.36.0"
authors = ["Dmitry Stepanov <[email protected]>"]
edition = "2021"
rust-version = "1.80"
description = "An autotiler for Fyrox game engine"
homepage = "https://github.com/FyroxEngine/Fyrox"
keywords = ["tilemap", "autotile", "wave function collapse"]
categories = ["game-development"]
repository = "https://github.com/FyroxEngine/Fyrox"
readme = "README.md"
include = ["/src/**/*", "/Cargo.toml", "/LICENSE", "/README.md"]
[dependencies]
fyrox-core = { path = "../fyrox-core", version = "0.36.0" }
fxhash = "0.2.1"
rand = "0.8.5"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Dmitry Stepanov
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.
+13
View File
@@ -0,0 +1,13 @@
# fyrox-autotile
An implementation of autotiling algorithms for use with regular tile grids. This includes:
- Interactive terrain-based autotiling for tile map editing. The user picks a broad category of tile for each cell. These categories are called *terrains* and each terrain may have many tiles. The algorithm picks specific tiles for each cell from its terrain so that the tiles fit naturally together, thus sparing the user from having to manually select matching tiles.
The algorithm is based upon a Godot library for the same purpose: https://github.com/dandeliondino/terrain-autotiler
- Randomized tile generation using Wave Function Collapse. Unlike terrain-based autotiling where the purpose is to automate tedious work for an artist, the purpose of Wave Function Collapse is to replace the artist entirely by generating creative tile layouts through randomization.
The algorithm is based upon fast-wfc, a C++ library for the same purpose: https://github.com/math-fehr/fast-wfc
These algorithms are designed to be generic in a way that will work with *any* regular grid. They should work just as well with square cells and hexagon cells and 3D grids. The user specifies the coordinate system that is used to identify cells and which cells are neighbors to which other cells.
+608
View File
@@ -0,0 +1,608 @@
// 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 std::{fmt::Debug, ops::Deref};
use super::*;
/// The contraints that control how the autotiler searches for tiles at each position.
pub trait AutoConstrain {
/// The type of a tile's position.
type Position: OffsetPosition;
/// The type of tile terrains. Each terrain represents a set of possible patterns.
type Terrain: TileTerrain;
/// An iterator for all the positions that the autotiler should consider when
/// choosing tiles. The autotiler will select tiles for position in the order
/// they are delivered by this iterator.
fn all_positions(&self) -> impl Iterator<Item = &Self::Position>;
/// The contraint for the given position. This allows the autotiler to examine
/// adjacent cells when deciding if a particular pattern is a valid choice
/// for the cell under consideration.
fn constraint_at(
&self,
position: &Self::Position,
) -> TileConstraint<Self::Terrain, <Self::Terrain as TileTerrain>::Pattern>;
/// True if the given patterns may be placed adjacent to each other with the
/// given offset. For example, if `offset` is up, then return true if
/// `to` may be legally placed above `from`.
fn is_legal(
&self,
from: &<Self::Terrain as TileTerrain>::Pattern,
offset: &<Self::Position as OffsetPosition>::Offset,
to: &<Self::Terrain as TileTerrain>::Pattern,
) -> bool;
/// True if the given patterns may be placed adjacent to each other with the
/// given offset. For example, if `offset` is up, then return true if
/// `to` may be legally placed above `from`.
fn is_legal_diagonal(
&self,
from: &<Self::Terrain as TileTerrain>::Pattern,
diagonal: &<Self::Position as OffsetPosition>::Diagonal,
to: &<Self::Terrain as TileTerrain>::Pattern,
) -> bool;
}
/// For autotiling, tiles are grouped into patterns, and patterns are grouped
/// into terrains. Each cell that is to be autotiled is given a terrain, and
/// the autotiler must choose a pattern from within that terrain.
pub trait TileTerrain {
/// The type of the patterns within this terrain.
type Pattern;
/// Iterate through the patterns within the terrain in the order of priority.
/// The autotiler will proceed through this iterator until it finds a pattern
/// that is legal according to the constraints, then ignore any remaining patterns.
fn all_patterns(&self) -> impl Iterator<Item = &Self::Pattern>;
}
/// Trait for objects that represent a particular autotiling problem to be solved,
/// with a set of positions that need tiles and a terrain for each position to control
/// which tiles are valid.
pub trait TerrainSource {
/// The type of positions that need tiles.
type Position;
/// The type of the terrains that control which patterns are allowed at each position.
type Terrain;
/// Iterate over the positions that need tiles and the terrains at each position.
fn iter(&self) -> impl Iterator<Item = NeededTerrain<Self::Position, Self::Terrain>> + '_;
/// True if the given position needs to be tiled.
fn contains_position(&self, position: &Self::Position) -> bool;
}
/// This represents a cell that needs to be autotiled, including its position,
/// its terrain, and the rules for what to do with the surrounding tiles.
pub struct NeededTerrain<Pos, Ter> {
/// The position of the cell to be autotiled.
pub position: Pos,
/// The terrain which specifies the valid patterns for the cell.
pub terrain: Ter,
/// The rules for whether surrounding tiles may be changed while
/// autitiling this cell.
pub fill: ConstraintFillRules,
}
/// Trait for objects that represent a constraining environment for the autotiler.
/// Given any position, a `PatternSource` object can supply a [`TileConstraint`] for that position.
/// While a [`TerrainSource`] object alone may be enough to tell the autotiler what positions it needs
/// to fill with tiles, a `PatternSource` can provide information about the neighboring tiles, and thus
/// give the autotiler a complete picture of the environment it is tiling.
pub trait PatternSource {
/// The type of positions that need tiles.
type Position;
/// The type of the terrains that control which patterns are allowed at each position.
type Terrain;
/// The type of the patterns that the autotiler must choose for each position.
type Pattern;
/// The contraint for the cell at the given position.
fn get(&self, position: &Self::Position) -> TileConstraint<Self::Terrain, Self::Pattern>;
/// The terrain of the pattern at the given position.
fn get_terrain(&self, position: &Self::Position) -> Option<Self::Terrain>;
}
/// When a cell is autotiled it may be desired that adjacent cells should be modified
/// to fit with the new terrain of this cell. This object specifies whether that should
/// be done. Cells that are added this way keep their current terrain, but their pattern
/// may change.
#[derive(Debug, Default, Clone, Copy, Visit, Reflect)]
pub struct ConstraintFillRules {
/// True if adjacent cells should be automatically added to the autotiling constraint
/// so that they may be modified.
pub include_adjacent: bool,
/// True if diagonal cells should be automatically added to the autotiling constraint
/// so that they may be modified.
pub include_diagonal: bool,
}
/// A pattern source that is stored in a hash map.
#[derive(Clone)]
pub struct HashConstraintMap<Pos, Ter, Pat> {
constraints: FxHashMap<Pos, TileConstraint<Ter, Pat>>,
terrain_cells: Vec<Pos>,
}
impl<Ter: Debug, Pat: Debug> Debug for HashConstraintMap<Vector2<i32>, Ter, Pat> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("HashConstraintMap:\n")?;
for (p, v) in self.constraints.iter() {
writeln!(f, " ({:2},{:2})->{v:?}", p.x, p.y)?;
}
write!(f, "terrain_cells: {:?}", self.terrain_cells)
}
}
impl<Pos, Ter, Pat> HashConstraintMap<Pos, Ter, Pat>
where
Pos: Hash + Eq + OffsetPosition,
Ter: Clone,
Pat: Clone,
{
/// Replace the content of this map using the given `PatternSource` and `TerrainSource`.
/// The `TerrainSource` specifies certain cells that need to be given tiles by specifying
/// the required terrain for those tiles. The terrains and patterns for the surrounding
/// cells are taken from the `PatternSource`.
///
/// The immediately adjacent cells are added to the front of the list of cells to be
/// assigned tiles and their terrain is taken from the `PatternSource`. The cells that
/// are one further away are make immutable by constraining them to be exactly their
/// current pattern from `PatternSource`.
pub fn fill_from<T, P>(&mut self, terrains: &T, patterns: &P)
where
T: TerrainSource<Position = Pos, Terrain = Ter>,
P: PatternSource<Position = Pos, Terrain = Ter, Pattern = Pat>,
{
self.clear();
for ter in terrains.iter() {
if ter.fill.include_diagonal {
for offset in Pos::all_diagonals() {
let p = ter.position.clone() + offset;
if !self.terrain_cells.contains(&p) && !terrains.contains_position(&p) {
if let Some(terrain) = patterns.get_terrain(&p) {
self.insert(p.clone(), TileConstraint::Terrain(terrain));
}
}
}
}
if ter.fill.include_adjacent {
for offset in Pos::all_offsets() {
let p = ter.position.clone() + offset;
if !self.terrain_cells.contains(&p) && !terrains.contains_position(&p) {
if let Some(terrain) = patterns.get_terrain(&p) {
self.insert(p.clone(), TileConstraint::Terrain(terrain));
}
}
}
}
}
for ter in terrains.iter() {
self.insert(ter.position, TileConstraint::Terrain(ter.terrain));
}
for pos in self.terrain_cells.clone().iter() {
for offset in Pos::all_offsets() {
let p = pos.clone() + offset;
if !self.terrain_cells.contains(&p) {
self.insert(p.clone(), patterns.get(&p));
}
}
}
}
}
impl<Pos, Ter, Pat> HashConstraintMap<Pos, Ter, Pat>
where
Pos: Hash + Eq + Clone,
{
/// Makes this map empty, so all cells have [`TileConstraint::None`].
pub fn clear(&mut self) {
self.constraints.clear();
self.terrain_cells.clear();
}
/// Add the given constraint to this map.
pub fn insert(&mut self, pos: Pos, constraint: TileConstraint<Ter, Pat>) {
if constraint.is_terrain() {
self.terrain_cells.push(pos.clone());
}
_ = self.constraints.insert(pos, constraint);
}
/// Get the constraint for the given position.
pub fn get(&self, pos: &Pos) -> &TileConstraint<Ter, Pat> {
self.constraints.get(pos).unwrap_or_default()
}
/// Iterate over all the terrain cells that are to be tiled in the order
/// that they should be tiled.
pub fn all_positions(&self) -> impl Iterator<Item = &Pos> {
self.terrain_cells.iter()
}
}
impl<Pos, Ter, Pat> Default for HashConstraintMap<Pos, Ter, Pat> {
fn default() -> Self {
Self {
constraints: FxHashMap::default(),
terrain_cells: Vec::default(),
}
}
}
impl<Pos, Ter, Pat> Deref for HashConstraintMap<Pos, Ter, Pat> {
type Target = FxHashMap<Pos, TileConstraint<Ter, Pat>>;
fn deref(&self) -> &Self::Target {
&self.constraints
}
}
/// The ways in which a cell's choice of tile can be constrained.
#[derive(Debug, Clone)]
pub enum TileConstraint<T, P> {
/// No constraint. This means that the cell is outside of the area of consideration
/// for the autotiler, such as beyond the edge of the world. Cells with this constraint
/// put no limits on what may be adjacent in any direction.
None,
/// A terrain is a set of possible patterns. Any pattern within the terrain may be chosen
/// to be the pattern for this cell.
Terrain(T),
/// A pattern constraint means that the pattern for this cell has already been chosen and
/// may not change.
Pattern(P),
}
impl<T, P> Default for TileConstraint<T, P> {
fn default() -> Self {
Self::None
}
}
impl<T, P> Default for &TileConstraint<T, P> {
fn default() -> Self {
&TileConstraint::None
}
}
impl<T, P> TileConstraint<T, P> {
/// True if this is the None constraint that does not restrict which pattern
/// may be chosen.
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
/// True if this constraint is not None, meaning it is either a pattern or a terrain.
pub fn is_some(&self) -> bool {
!self.is_none()
}
/// True if this constraint is a terrain, meaning there are zero or more permitted patterns.
pub fn is_terrain(&self) -> bool {
matches!(self, Self::Terrain(_))
}
/// Tue if this contraint is a pattern, meaning that only that exact pattern is permitted.
pub fn is_pattern(&self) -> bool {
matches!(self, Self::Pattern(_))
}
/// An iterator over all the patterns explicitly permitted by this contraint,
/// or an empty iterator if the constraint is None.
/// The None constraint permits any pattern, but it is not possible to create
/// an interator over all patterns here, so `all_patterns` should usually only
/// be called if [`is_some`](Self::is_some) returns true.
pub fn all_patterns(&self) -> TileConstraint<impl Iterator<Item = &P>, &P>
where
T: TileTerrain<Pattern = P>,
{
match self {
Self::None => TileConstraint::None,
Self::Terrain(t) => TileConstraint::Terrain(t.all_patterns()),
Self::Pattern(p) => TileConstraint::Pattern(p),
}
}
}
impl<T, P> Iterator for TileConstraint<T, P>
where
T: Iterator<Item = P>,
P: Clone,
{
type Item = P;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::None => None,
Self::Terrain(t) => t.next(),
Self::Pattern(p) => {
let p = p.clone();
*self = Self::None;
Some(p)
}
}
}
}
/// The object responsible for autotiling and owning a hash map
/// were the result is stored.
#[derive(Debug, Default, Clone)]
pub struct AutoTiler<Pos, Pat> {
patterns: FxHashMap<Pos, Pat>,
}
impl<Pos, Pat> std::ops::Deref for AutoTiler<Pos, Pat> {
type Target = FxHashMap<Pos, Pat>;
fn deref(&self) -> &Self::Target {
&self.patterns
}
}
impl<Pos, Pat> std::ops::DerefMut for AutoTiler<Pos, Pat> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.patterns
}
}
impl<Pos: OffsetPosition, Pat: Clone + Debug> AutoTiler<Pos, Pat> {
/// Fill the autotile map with patterns according to the given constraint.
/// The hash map is not automatically cleared by this method, so it may be
/// called multiple times with different constraints to build up a single
/// autotile solution.
pub fn autotile<C, T>(&mut self, constraint: &C)
where
C: AutoConstrain<Position = Pos, Terrain = T>,
T: TileTerrain<Pattern = Pat> + Debug,
Pos: Debug,
<Pos as OffsetPosition>::Offset: Debug,
{
for pos in constraint.all_positions() {
if !self.patterns.contains_key(pos) {
if let Some(pat) = self.find_pattern(pos, constraint) {
_ = self.patterns.insert(pos.clone(), pat);
}
}
}
}
fn find_pattern<C, T>(&self, position: &Pos, constraint: &C) -> Option<Pat>
where
C: AutoConstrain<Position = Pos, Terrain = T>,
T: TileTerrain<Pattern = Pat> + Debug,
Pos: Debug,
<Pos as OffsetPosition>::Offset: Debug,
{
for pat in constraint.constraint_at(position).all_patterns() {
if self.is_pattern_legal(position, pat, constraint) {
return Some(pat.clone());
}
}
None
}
fn is_pattern_legal<C, T>(&self, position: &Pos, pattern: &Pat, constraint: &C) -> bool
where
C: AutoConstrain<Position = Pos, Terrain = T>,
T: TileTerrain<Pattern = Pat>,
<Pos as OffsetPosition>::Offset: Debug,
{
for diagonal in Pos::all_diagonals() {
let p = position.clone() + diagonal.clone();
if let Some(pat) = self.patterns.get(&p) {
if !constraint.is_legal_diagonal(pattern, &diagonal, pat) {
return false;
}
} else {
let cell_constraint = constraint.constraint_at(&p);
if cell_constraint.is_some()
&& !cell_constraint
.all_patterns()
.any(|pat| constraint.is_legal_diagonal(pattern, &diagonal, pat))
{
return false;
}
}
}
for offset in Pos::all_offsets() {
let p = position.clone() + offset.clone();
if let Some(pat) = self.patterns.get(&p) {
if !constraint.is_legal(pattern, &offset, pat) {
return false;
}
} else {
let cell_constraint = constraint.constraint_at(&p);
if cell_constraint.is_some()
&& !cell_constraint
.all_patterns()
.any(|pat| constraint.is_legal(pattern, &offset, pat))
{
return false;
}
}
}
true
}
}
/// This contains the background information required for autotiling
/// independent of the particular cells to be filled, so it can be used
/// for multiple autotiling tasks. It contains a hash map from terrain
/// values to lists of patterns, representing both the patterns contained
/// within each terrain and the order in which the patterns should be examined.
/// It also contains and a hash map from patterns to a [`ProbabilitySet`] of tiles,
/// representing how a tile should be randomly selected once a pattern is chosen.
#[derive(Clone)]
pub struct AutoTileContext<Ter, Pat, Tile> {
pattern_list_pool: Vec<Vec<Pat>>,
probability_set_pool: Vec<ProbabilitySet<Tile>>,
/// Hash map from terrain values to the list of pattern values that each terrain represents.
/// The pattern values should be sorted into the order in which the autotiler will try to insert
/// the patterns into the cells.
pub patterns: AutoTerrainPatternMap<Ter, Pat>,
/// Hash map from pattern values to [`ProbabilitySet`] of tile values, giving each tile the probability
/// it should have when randomly selecting a tile for a given pattern.
pub values: AutoPatternValueMap<Pat, Tile>,
}
impl<Ter: Debug, Pat: Debug, Tile: Debug> Debug for AutoTileContext<Ter, Pat, Tile> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("AutoTileContext\n")?;
f.write_str("patterns:\n")?;
for (p, v) in self.patterns.iter() {
writeln!(f, "{p:?} -> {v:?}")?;
}
f.write_str("values:\n")?;
for (p, v) in self.values.iter() {
writeln!(f, "{p:?} -> {v:?}")?;
}
Ok(())
}
}
impl<Ter, Pat, Tile> Default for AutoTileContext<Ter, Pat, Tile> {
fn default() -> Self {
Self {
pattern_list_pool: Default::default(),
probability_set_pool: Default::default(),
patterns: Default::default(),
values: Default::default(),
}
}
}
impl<Ter: Eq + Hash, Pat: Eq + Hash + Clone, Tile> AutoTileContext<Ter, Pat, Tile> {
/// Make the context empty in preparation for building a new context.
pub fn clear(&mut self) {
for (_, mut list) in self.patterns.drain() {
list.clear();
self.pattern_list_pool.push(list);
}
for (_, mut set) in self.values.drain() {
set.clear();
self.probability_set_pool.push(set);
}
}
/// Add a particular tile to the context and specify that tile's terrain, pattern,
/// and frequency. The higher the frequency, the more likely this tile will be chosen
/// if it shares the same pattern as another tile.
pub fn add(&mut self, key: Ter, pattern: Pat, frequency: f32, value: Tile) {
self.patterns
.entry(key)
.or_insert_with(|| self.pattern_list_pool.pop().unwrap_or_default())
.push(pattern.clone());
self.values
.entry(pattern)
.or_insert_with(|| self.probability_set_pool.pop().unwrap_or_default())
.add(frequency, value);
}
/// Sort the pattern values according to their natural order.
pub fn sort(&mut self)
where
Pat: Ord,
{
for pattern_list in self.patterns.values_mut() {
pattern_list.sort();
pattern_list.dedup();
}
}
/// Get a random tile that has the given pattern, if possible.
pub fn get_random_value<R: Rng + ?Sized>(&self, rng: &mut R, pattern: &Pat) -> Option<&Tile> {
self.values.get(pattern).and_then(|vs| vs.get_random(rng))
}
}
/// A hash map from terrain values to lists of patterns.
/// Each pattern has a terrain, and this map is used to list all of
/// the patterns for a given terrain id value in the order of priority.
/// If multiple patterns in the list may be chosen for some cell,
/// the pattern nearest the front of the list should be chosen.
///
/// * Ter: The type of the terrain ids that are to be associated with lists of patterns.
/// * Pat: The type of a pattern.
///
/// See [`AutoTileContext`] for a way to construct an `AutoTerrainPatternMap`.
/// Once an `AutoTerrainPatternMap` has been constructed, it may be
/// used as part of an [`AutoPatternConstraint`].
pub type AutoTerrainPatternMap<Ter, Pat> = FxHashMap<Ter, Vec<Pat>>;
/// A hash map from patterns to tiles. It is possible for multiple tiles to share
/// the same pattern, and in that case there is no way for the autotiler to deterministically
/// choose a tile. This map gives each pattern a [`ProbabilitySet`] that can be used
/// to randomly select a tile based upon the relative frequency of each tile.
///
/// * Pat: The type of a tile pattern.
/// * Tile: The type of a tile.
///
/// See [`AutoTileContext`] for a way to construct an `AutoPatternValueMap`.
pub type AutoPatternValueMap<Pat, Tile> = FxHashMap<Pat, ProbabilitySet<Tile>>;
/// A pair of an [`HashConstraintMap`] and an [`AutoTerrainPatternMap`].
/// - The constraint map tells the auto-tiler which cells have undecided patterns,
/// which cells have fixed patterns, the terrain type of the undecided cells, and
/// the order in which to fill them.
/// - The terrain map tells the auto-tiler which patterns are available for each
/// terrain type, and the order in which the patterns should be tried.
///
/// See [`AutoTileContext`] for a way to construct an [`AutoTerrainPatternMap`].
pub struct AutoPatternConstraint<'a, 'b, Pos, Ter, Pat> {
/// The position constraints define the specific problem for the autotiler to solve
/// by giving it the constraints for each cell, including the cells whose tiles are already
/// determined and the cells that are yet to be determined.
pub position_constraints: &'a HashConstraintMap<Pos, Ter, Pat>,
/// The pattern contraints define the patterns that are available for each terrain.
/// This will usually remain fixed across many calls to the autotiler, and so it may be reused
/// so long as the set of tiles does not change.
/// See [`AutoTileContext`] for a way to construct an [`AutoTerrainPatternMap`].
pub pattern_constraints: &'b AutoTerrainPatternMap<Ter, Pat>,
}
impl<'b, Pos, Ter, Pat> AutoConstrain for AutoPatternConstraint<'_, 'b, Pos, Ter, Pat>
where
Pos: OffsetPosition,
Ter: Hash + Eq,
Pat: TilePattern<Offset = Pos::Offset, Diagonal = Pos::Diagonal> + Clone,
Pat: Debug,
Pos::Offset: Debug,
{
type Position = Pos;
type Terrain = ListTerrain<'b, Pat>;
fn all_positions(&self) -> impl Iterator<Item = &Self::Position> {
self.position_constraints.all_positions()
}
fn constraint_at(&self, position: &Pos) -> TileConstraint<ListTerrain<'b, Pat>, Pat> {
match self.position_constraints.get(position) {
TileConstraint::Terrain(ter) => match self.pattern_constraints.get(ter) {
Some(pat_list) => TileConstraint::Terrain(ListTerrain(pat_list)),
None => TileConstraint::None,
},
TileConstraint::Pattern(pat) => TileConstraint::Pattern(pat.clone()),
TileConstraint::None => TileConstraint::None,
}
}
fn is_legal(&self, from: &Pat, offset: &Pos::Offset, to: &Pat) -> bool {
from.is_legal(offset, to)
}
fn is_legal_diagonal(&self, from: &Pat, diagonal: &Pos::Diagonal, to: &Pat) -> bool {
from.is_legal_diagonal(diagonal, to)
}
}
/// An implementation of [`TileTerrain`] based upon a slice of patterns.
#[derive(Debug, Clone)]
pub struct ListTerrain<'a, P>(pub &'a [P]);
impl<P> TileTerrain for ListTerrain<'_, P> {
type Pattern = P;
fn all_patterns(&self) -> impl Iterator<Item = &P> {
self.0.iter()
}
}
+707
View File
@@ -0,0 +1,707 @@
// 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.
//! Autotiling allows you to fill the content of a grid according to pre-defined rules.
//! Tiles are assigned frequencies and adjacency rules, and then an algorithm is used
//! to attempt to fill a given area with tiles in accordance with the rules and frequencies.
//!
//! This library supports both deterministic autotiling and wave function collapse.
//! Deterministic autotiling tries to find the particular tile that the user wants
//! based upon giving specification for the purpose of streamlining creating tile-based art.
//! Wave function collapse is a process of automatically generating random content where
//! the algorithm is giving freedom to choose whatever tiles it likes within the limits
//! of the adjacency rules.
//!
//! The autotiling algorithm is based upon [Terrain Autotiler](https://github.com/dandeliondino/terrain-autotiler).
//!
//! The wave function collapse algorith is based upon [fast-wfc](https://github.com/math-fehr/fast-wfc).
#![warn(missing_docs)]
use std::{collections::hash_map::Entry, fmt::Debug, hash::Hash};
use fxhash::FxHashMap;
use fyrox_core::algebra::{Vector2, Vector3};
use fyrox_core::{reflect::prelude::*, visitor::prelude::*};
use rand::Rng;
mod auto;
mod wave;
pub use auto::*;
pub use wave::*;
/// Position types for tiles that provides abstract access to all of the position's
/// adjacent positions.
pub trait OffsetPosition:
Eq
+ Hash
+ Clone
+ std::ops::Add<Self::Offset, Output = Self>
+ std::ops::Add<Self::Diagonal, Output = Self>
{
/// An offset from a position to one of its adjacent positions.
/// Since `OffsetPosition` implements `Add<Offset, Ouput=Self>`, an offset can be added to a position
/// to get the adjacent position.
type Offset: std::ops::Neg<Output = Self::Offset> + Clone;
/// A diagonal offset from a position to one of the positions that are nearby but not adjacent.
/// Since `OffsetPosition` implements `Add<Diagonal, Ouput=Self>`, a diagonal can be added to a position
/// to get the diagonal position.
type Diagonal: std::ops::Neg<Output = Self::Diagonal> + Clone;
/// An iterator over all offsets, giving access to all adjacent positions from any position by using
/// position arithmetic: position + offset == adjacent position.
fn all_offsets() -> impl Iterator<Item = Self::Offset>;
/// An iterator over all diagonals, giving access to nearby non-adjacent positions by using position
/// arithmetic: position + diagonal == nearby position.
fn all_diagonals() -> impl Iterator<Item = Self::Diagonal>;
}
impl OffsetPosition for Vector2<i32> {
type Offset = Vector2Offset;
type Diagonal = Vector2Diagonal;
fn all_offsets() -> impl Iterator<Item = Self::Offset> {
(0..4).map(Vector2Offset)
}
fn all_diagonals() -> impl Iterator<Item = Self::Diagonal> {
(0..4).map(Vector2Diagonal)
}
}
/// An offset to move from a `Vector2<i32>` position to an diagonal `Vector2<i32>`.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Vector2Diagonal(usize);
impl Vector2Diagonal {
/// Diagonal (-1, -1)
pub const LEFT_DOWN: Vector2Diagonal = Vector2Diagonal(0);
/// Diagonal (1, -1)
pub const RIGHT_DOWN: Vector2Diagonal = Vector2Diagonal(1);
/// Diagonal (-1, 1)
pub const LEFT_UP: Vector2Diagonal = Vector2Diagonal(2);
/// Diagonal (1, 1)
pub const RIGHT_UP: Vector2Diagonal = Vector2Diagonal(3);
/// The bit in a `PatternBits` object that corresponds to this diagonal.
/// The returned bit is the peer that must match against the diagonal pattern
/// in this direction.
fn peering_bit(&self) -> usize {
DIAGONAL_PEERING_BITS[self.0]
}
/// The x difference of the offset, so position.x + dx == adjacent.x
pub fn dx(&self) -> i32 {
DIAG2[self.0].x
}
/// The y difference of the offset, so position.y + dy == adjacent.y
pub fn dy(&self) -> i32 {
DIAG2[self.0].y
}
}
impl Debug for Vector2Diagonal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Vector2Diagonal")
.field("dx", &self.dx())
.field("dy", &self.dy())
.finish()
}
}
impl std::ops::Add<Vector2Diagonal> for Vector2<i32> {
type Output = Vector2<i32>;
fn add(self, rhs: Vector2Diagonal) -> Self::Output {
self + DIAG2[rhs.0]
}
}
impl std::ops::Neg for Vector2Diagonal {
type Output = Self;
fn neg(self) -> Self::Output {
Vector2Diagonal(3 - self.0)
}
}
/// An offset to move from a `Vector2<i32>` position to an adjacent `Vector2<i32>`
/// in one of the four cardinal directions.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Vector2Offset(usize);
const OFFSETS2: [Vector2<i32>; 4] = [
Vector2::new(-1, 0),
Vector2::new(0, -1),
Vector2::new(0, 1),
Vector2::new(1, 0),
];
const DIAG2: [Vector2<i32>; 4] = [
Vector2::new(-1, -1),
Vector2::new(1, -1),
Vector2::new(-1, 1),
Vector2::new(1, 1),
];
const fn bit_pos(x: usize, y: usize) -> usize {
x + y * 3
}
const OFFSET_PEERING_BITS: [[usize; 3]; 4] = [
[bit_pos(0, 0), bit_pos(0, 1), bit_pos(0, 2)],
[bit_pos(0, 0), bit_pos(1, 0), bit_pos(2, 0)],
[bit_pos(0, 2), bit_pos(1, 2), bit_pos(2, 2)],
[bit_pos(2, 0), bit_pos(2, 1), bit_pos(2, 2)],
];
const DIAGONAL_PEERING_BITS: [usize; 4] =
[bit_pos(0, 0), bit_pos(2, 0), bit_pos(0, 2), bit_pos(2, 2)];
impl Vector2Offset {
/// Offset (0, 1)
pub const UP: Vector2Offset = Vector2Offset(2);
/// Offset (0, -1)
pub const DOWN: Vector2Offset = Vector2Offset(1);
/// Offset (-1, 0)
pub const LEFT: Vector2Offset = Vector2Offset(0);
/// Offset (1, 0)
pub const RIGHT: Vector2Offset = Vector2Offset(3);
/// Iterator over the three peering bits that are along the edge
/// of a 3x3 pattern grid in the direction of this offset.
fn peering_bits(&self) -> impl Iterator<Item = usize> {
OFFSET_PEERING_BITS[self.0].iter().copied()
}
/// The x difference of the offset, so position.x + dx == adjacent.x
pub fn dx(&self) -> i32 {
OFFSETS2[self.0].x
}
/// The y difference of the offset, so position.y + dy == adjacent.y
pub fn dy(&self) -> i32 {
OFFSETS2[self.0].y
}
}
impl Debug for Vector2Offset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Vector2Offset")
.field("dx", &self.dx())
.field("dy", &self.dy())
.finish()
}
}
impl From<Vector2Offset> for Vector2<i32> {
fn from(value: Vector2Offset) -> Self {
OFFSETS2[value.0]
}
}
impl std::ops::Add<Vector2Offset> for Vector2<i32> {
type Output = Vector2<i32>;
fn add(self, rhs: Vector2Offset) -> Self::Output {
self + OFFSETS2[rhs.0]
}
}
impl std::ops::Neg for Vector2Offset {
type Output = Self;
fn neg(self) -> Self::Output {
Vector2Offset(3 - self.0)
}
}
impl OffsetPosition for Vector3<i32> {
type Offset = Vector3Offset;
type Diagonal = Vector3Diagonal;
fn all_offsets() -> impl Iterator<Item = Self::Offset> {
(0..6).map(Vector3Offset)
}
fn all_diagonals() -> impl Iterator<Item = Self::Diagonal> {
(0..DIAG3.len()).map(|i| DIAG3[i]).map(Vector3Diagonal)
}
}
/// An offset to move from a `Vector2<i32>` position to an diagonal `Vector2<i32>`.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Vector3Diagonal(Vector3<i32>);
const DIAG3: [Vector3<i32>; 20] = [
Vector3::new(-1, -1, -1),
Vector3::new(1, -1, -1),
Vector3::new(-1, 1, -1),
Vector3::new(1, 1, -1),
Vector3::new(-1, -1, 0),
Vector3::new(1, -1, 0),
Vector3::new(-1, 1, 0),
Vector3::new(1, 1, 0),
Vector3::new(-1, -1, 1),
Vector3::new(1, -1, 1),
Vector3::new(-1, 1, 1),
Vector3::new(1, 1, 1),
Vector3::new(-1, 0, -1),
Vector3::new(0, -1, -1),
Vector3::new(1, 0, -1),
Vector3::new(0, 1, -1),
Vector3::new(-1, 0, 1),
Vector3::new(0, -1, 1),
Vector3::new(1, 0, 1),
Vector3::new(0, 1, 1),
];
impl std::ops::Add<Vector3Diagonal> for Vector3<i32> {
type Output = Vector3<i32>;
fn add(self, rhs: Vector3Diagonal) -> Self::Output {
self + rhs.0
}
}
impl std::ops::Neg for Vector3Diagonal {
type Output = Self;
fn neg(self) -> Self::Output {
Vector3Diagonal(-self.0)
}
}
/// An offset to move from a `Vector3<i32>` position to an adjacent `Vector3<i32>`
/// in one of the six cardinal directions.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Vector3Offset(usize);
impl Vector3Offset {
/// The x difference of the offset, so position.x + dx == adjacent.x
pub fn dx(&self) -> i32 {
OFFSETS3[self.0].x
}
/// The y difference of the offset, so position.y + dy == adjacent.y
pub fn dy(&self) -> i32 {
OFFSETS3[self.0].y
}
/// The z difference of the offset, so position.z + dz == adjacent.z
pub fn dz(&self) -> i32 {
OFFSETS3[self.0].z
}
}
const OFFSETS3: [Vector3<i32>; 6] = [
Vector3::new(-1, 0, 0),
Vector3::new(0, -1, 0),
Vector3::new(0, 0, -1),
Vector3::new(0, 0, 1),
Vector3::new(0, 1, 0),
Vector3::new(1, 0, 0),
];
impl From<Vector3Offset> for Vector3<i32> {
fn from(value: Vector3Offset) -> Self {
OFFSETS3[value.0]
}
}
impl std::ops::Add<Vector3Offset> for Vector3<i32> {
type Output = Vector3<i32>;
fn add(self, rhs: Vector3Offset) -> Self::Output {
self + OFFSETS3[rhs.0]
}
}
impl std::ops::Neg for Vector3Offset {
type Output = Self;
fn neg(self) -> Self::Output {
Vector3Offset(5 - self.0)
}
}
/// A set of values of type `V` where each value is associated with a frequency
/// so that a value can be chosen from the set at random with the probability of each
/// value weighted by its frequency.
#[derive(Debug, Clone)]
pub struct ProbabilitySet<V> {
total: f32,
content: Vec<(f32, V)>,
}
impl<V> Default for ProbabilitySet<V> {
fn default() -> Self {
Self {
total: 0.0,
content: Vec::default(),
}
}
}
impl<V> ProbabilitySet<V> {
/// Iterate through all the items in the set and their frequencies.
pub fn iter(&self) -> impl Iterator<Item = (f32, &V)> {
self.content.iter().map(|(f, v)| (*f, v))
}
/// Remove the content of the set.
pub fn clear(&mut self) {
self.total = 0.0;
self.content.clear();
}
/// Add a value and give it a frequency.
pub fn add(&mut self, frequency: f32, value: V) {
if frequency > 0.0 {
self.total += frequency;
self.content.push((frequency, value));
}
}
/// The sum of the frequencies of all the elements of the set.
/// The probability of an element being chosen is the frequency of
/// that element divided by this total.
pub fn total_frequency(&self) -> f32 {
self.total
}
/// Choose a value from the set using the given random number generator.
/// The probability of each element of the set being chosen is the frequency
/// of that element divided by the sum of the frequencies of all elements.
pub fn get_random<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<&V> {
if self.total <= 0.0 {
return None;
}
let mut p = rng.gen_range(0.0..self.total);
for (f, v) in self.iter() {
if p < f {
return Some(v);
}
p -= f;
}
self.iter().next().map(|v| v.1)
}
}
/// For the purposes of autotiling, tiles are represented by patterns that
/// hold the data which determines whether two tiles are permitted to appear
/// adjacent to each other. Therefore autotile algorithms place patterns
/// rather than tiles, and translating those patterns into actual tiles
/// is a later step.
pub trait TilePattern {
/// A pattern is capable of being adjacent to other patterns in several directions.
/// The pattern's offset type is the type of the offset between a pattern's position
/// and the position of an adjacent pattern.
type Offset;
/// A pattern may connect to another pattern by touching their corners.
/// This creates a diagonal connection where the patterns are touching
/// but not adjacent, and in some contexts there may be rules regarding
/// which patterns may be diagonally connected to which other patterns.
type Diagonal;
/// True if this pattern may be adjacent the given other pattern of this type
/// when that other pattern is positioned at `offset` relative to this pattern.
fn is_legal(&self, offset: &Self::Offset, to: &Self) -> bool;
/// True if this pattern may be diagonal to the given other pattern when
/// that other pattern is positioned at `offset` relative to this pattern.
fn is_legal_diagonal(&self, offset: &Self::Diagonal, to: &Self) -> bool;
}
/// `PatternBits` represents a tile's pattern as a 3x3 grid of `i8` values
/// for doing autotiling in 2D with `Vector2<i32>` positions.
/// The center of the 3x3 grid is called the pattern's *terrain.*
/// The 8 values around the outside of the grid are called the pattern's
/// *peering bits.*
///
/// The peering bits of this pattern determine whether it is permitted to use this
/// pattern adjacent to other patterns. A pattern is only legal if all three peering
/// bits along each edge match the bits on the nearest edge of the adjacent pattern.
///
/// In the case of diagonal connections, only only bit must match in each pattern:
/// the corner bit that is nearest to the other pattern.
///
/// For the details of how peering bits constrain where this pattern may be used,
/// see the implementation of [`TilePattern`].
#[derive(Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PatternBits(pub [i8; 9]);
impl PatternBits {
/// The pattern's terrain ID that can be used to categorize this pattern,
/// and to determine which peering bits are the most important for pattern sorting.
/// Patterns that have more peering bits that match its terrain are given higher priority
/// when sorting and appear earlier in the list.
pub fn center(&self) -> i8 {
self.0[bit_pos(1, 1)]
}
/// The number of bits in the pattern that match the terrain ID. This count is at most 9
/// and at least 1 because the center bit always matches itself.
pub fn center_terrain_count(&self) -> usize {
let center = self.center();
self.0.iter().filter(|t| **t == center).count()
}
/// The number of distinct distinct values in this pattern. This count is at most 9 and
/// at least 1, for the case where every bit is the same.
pub fn unique_terrain_count(&self) -> usize {
let mut count = 1;
'outer: for i in 1..9 {
let current = self.0[i];
for j in 0..i {
if self.0[j] == current {
continue 'outer;
}
}
count += 1;
}
count
}
}
impl Debug for PatternBits {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn w(f: &mut std::fmt::Formatter<'_>, bs: &[i8]) -> std::fmt::Result {
write!(f, "{0},{1},{2}", bs[0], bs[1], bs[2])
}
let bs = &self.0;
f.write_str("[")?;
w(f, &bs[0..3])?;
f.write_str("|")?;
w(f, &bs[3..6])?;
f.write_str("|")?;
w(f, &bs[6..9])?;
f.write_str("]")
}
}
impl PartialOrd for PatternBits {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PatternBits {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.center_terrain_count()
.cmp(&other.center_terrain_count())
.reverse()
.then_with(|| {
self.unique_terrain_count()
.cmp(&other.unique_terrain_count())
})
.then_with(|| self.0.cmp(&other.0))
}
}
/// Converts an x,y position into index in 0..9. Both x and y must be within 0..3.
#[inline]
fn nine_position_to_index(position: Vector2<usize>) -> usize {
if position.y > 2 || position.x > 2 {
panic!("Illegal terrain bit position: {:?}", position);
}
bit_pos(position.x, position.y)
}
impl std::ops::Index<Vector2<usize>> for PatternBits {
type Output = i8;
fn index(&self, index: Vector2<usize>) -> &Self::Output {
&self.0[nine_position_to_index(index)]
}
}
impl std::ops::IndexMut<Vector2<usize>> for PatternBits {
fn index_mut(&mut self, index: Vector2<usize>) -> &mut Self::Output {
&mut self.0[nine_position_to_index(index)]
}
}
impl TilePattern for PatternBits {
type Offset = Vector2Offset;
type Diagonal = Vector2Diagonal;
fn is_legal(&self, offset: &Vector2Offset, to: &Self) -> bool {
offset
.peering_bits()
.zip((-*offset).peering_bits())
.all(|(a, b)| self.0[a] == to.0[b])
}
fn is_legal_diagonal(&self, diagonal: &Vector2Diagonal, to: &Self) -> bool {
self.0[diagonal.peering_bit()] == to.0[(-*diagonal).peering_bit()]
}
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use rand::SeedableRng;
use PatternBits as Bits;
use super::*;
fn make_rng(seed: u64) -> rand::rngs::StdRng {
rand::rngs::StdRng::seed_from_u64(seed)
}
#[test]
fn probability_set_0() {
let set = ProbabilitySet::<u32>::default();
assert_eq!(set.get_random(&mut make_rng(0)), None);
}
#[test]
fn probability_set_1() {
let mut set = ProbabilitySet::<u32>::default();
set.add(0.65, 27);
let mut rng = make_rng(0);
assert_eq!(set.get_random(&mut rng), Some(&27));
assert_eq!(set.get_random(&mut rng), Some(&27));
assert_eq!(set.get_random(&mut rng), Some(&27));
}
#[test]
fn probability_set_2() {
let mut set = ProbabilitySet::<u32>::default();
set.add(0.5, 1);
set.add(1.0, 2);
let mut rng = make_rng(0);
let mut results = FxHashMap::<Option<u32>, usize>::default();
for _ in 0..75 {
*results
.entry(set.get_random(&mut rng).cloned())
.or_default() += 1;
}
let result_1 = results.get(&Some(1)).copied().unwrap_or_default();
let result_2 = results.get(&Some(2)).copied().unwrap_or_default();
assert_eq!(results.get(&None).copied().unwrap_or_default(), 0, "None");
assert!((21..27).contains(&result_1), "1: {}", result_1);
assert!((45..55).contains(&result_2), "2: {}", result_2);
assert_eq!(results.values().sum::<usize>(), 75, "Sum");
}
#[test]
fn probability_set_3() {
let mut set = ProbabilitySet::<u32>::default();
set.add(2.0, 1);
set.add(2.0, 2);
set.add(2.0, 3);
let mut rng = make_rng(0);
let mut results = FxHashMap::<Option<u32>, usize>::default();
for _ in 0..750 {
*results
.entry(set.get_random(&mut rng).cloned())
.or_default() += 1;
}
let result_1 = results.get(&Some(1)).copied().unwrap_or_default();
let result_2 = results.get(&Some(2)).copied().unwrap_or_default();
let result_3 = results.get(&Some(2)).copied().unwrap_or_default();
assert_eq!(results.get(&None).copied().unwrap_or_default(), 0, "None");
assert!((210..270).contains(&result_1), "1: {}", result_1);
assert!((210..270).contains(&result_2), "2: {}", result_2);
assert!((210..270).contains(&result_3), "3: {}", result_3);
assert_eq!(results.values().sum::<usize>(), 750, "Sum");
}
#[test]
fn terrain_ord_1() {
let a = Bits([0, 0, 0, 0, 0, 0, 0, 0, 0]);
let b = Bits([1, 0, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(a.cmp(&b), Ordering::Less);
}
#[test]
fn terrain_ord_2() {
let a = Bits([0, 1, 0, 0, 0, 0, 0, 0, 0]);
let b = Bits([0, 1, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(a.cmp(&b), Ordering::Equal);
}
#[test]
fn terrain_ord_3() {
let a = Bits([0, 1, 0, 0, 0, 0, 0, 0, 0]);
let b = Bits([1, 0, 1, 1, 0, 0, 0, 0, 0]);
assert_eq!(a.cmp(&b), Ordering::Less);
}
#[test]
fn terrain_ord_4() {
let a = Bits([0, 1, 0, 1, 0, 1, 2, 0, 0]);
let b = Bits([1, 0, 1, 1, 0, 0, 0, 0, 0]);
assert_eq!(a.cmp(&b), Ordering::Greater);
}
#[test]
fn terrain_ord_ne() {
let a = Bits([0, 1, 0, 1, 0, 1, 0, 0, 0]);
let b = Bits([1, 0, 1, 1, 0, 0, 0, 0, 0]);
assert_ne!(a.cmp(&b), Ordering::Equal);
}
#[test]
fn tile_pattern_up() {
let a = Bits([-1, -2, -3, -4, -5, -6, 1, 2, 3]);
let b = Bits([1, 2, 3, 4, 5, 6, 7, 8, 9]);
let offset = Vector2Offset::DOWN;
assert!(b.is_legal(&offset, &a), "{b:?} {offset:?} {a:?}");
}
#[test]
fn tile_pattern_down() {
let a = Bits([-1, -2, -3, -4, -5, -6, 1, 2, 3]);
let b = Bits([1, 2, 3, 4, 5, 6, 7, 8, 9]);
let offset = Vector2Offset::UP;
assert!(a.is_legal(&offset, &b), "{b:?} {offset:?} {a:?}");
}
#[test]
fn tile_pattern_right() {
let a = Bits([1, 2, 1, 4, 5, 2, 7, 8, 3]);
let b = Bits([1, -2, -3, 2, -5, -6, 3, -2, -3]);
let offset = Vector2Offset::RIGHT;
assert!(a.is_legal(&offset, &b), "{a:?} {offset:?} {b:?}");
}
#[test]
fn tile_pattern_right2() {
let a = Bits([3, 3, 0, 3, 3, 0, 3, 3, 0]);
let b = Bits([0, 0, 0, 0, 0, 0, 0, 0, 0]);
let offset = Vector2Offset::RIGHT;
assert!(a.is_legal(&offset, &b), "{a:?} {offset:?} {b:?}");
}
#[test]
fn tile_pattern_left() {
let a = Bits([1, 2, 3, 2, 3, 4, 3, 8, 5]);
let b = Bits([-1, -2, 1, -2, -5, 2, -3, -2, 3]);
let offset = Vector2Offset::LEFT;
assert!(a.is_legal(&offset, &b), "{a:?} {offset:?} {b:?}");
}
#[test]
fn tile_pattern_left2() {
let a = Bits([0, 3, 3, 0, 3, 3, 0, 3, 3]);
let b = Bits([0, 0, 0, 0, 0, 0, 0, 0, 0]);
let offset = Vector2Offset::LEFT;
assert!(a.is_legal(&offset, &b), "{a:?} {offset:?} {b:?}");
}
#[test]
fn tile_pattern_left_up() {
let a = Bits([1, 2, 1, 2, 1, 4, 3, 8, 5]);
let b = Bits([-1, -2, 3, -2, -5, -2, -3, -2, -1]);
let offset = Vector2Diagonal::LEFT_UP;
assert!(a.is_legal_diagonal(&offset, &b), "{a:?} {offset:?} {b:?}");
}
#[test]
fn tile_pattern_right_up() {
let a = Bits([1, 2, 1, 2, 1, 4, 1, 8, 3]);
let b = Bits([3, -2, -1, -2, -5, -2, -3, -2, -1]);
let offset = Vector2Diagonal::RIGHT_UP;
assert!(a.is_legal_diagonal(&offset, &b), "{a:?} {offset:?} {b:?}");
}
#[test]
fn tile_pattern_left_down() {
let a = Bits([3, 2, 1, 2, 1, 4, 1, 8, 5]);
let b = Bits([-1, -2, -3, -2, -5, -2, -3, -2, 3]);
let offset = Vector2Diagonal::LEFT_DOWN;
assert!(a.is_legal_diagonal(&offset, &b), "{a:?} {offset:?} {b:?}");
}
#[test]
fn tile_pattern_right_down() {
let a = Bits([1, 2, 3, 2, 1, 4, 1, 8, 1]);
let b = Bits([-3, -2, -1, -2, -5, -2, 3, -2, -1]);
let offset = Vector2Diagonal::RIGHT_DOWN;
assert!(a.is_legal_diagonal(&offset, &b), "{a:?} {offset:?} {b:?}");
}
}
+748
View File
@@ -0,0 +1,748 @@
// 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.
//! Wave function collapse algorithm based upon [fast-wfc](https://github.com/math-fehr/fast-wfc).
use std::{
error::Error,
fmt::Display,
ops::{Deref, DerefMut},
};
use super::*;
type Entropy = f64;
/// A `WavePosition` is like an `OffsetPosition` but it also has a counter type
/// that the wave propagator can use to keep count of how many possible matching
/// patterns are available from each `Offset`.
pub trait WavePosition: OffsetPosition {
/// The type that holds count of patterns in each direction.
type Counter: WaveOffsetCounter<Offset = Self::Offset>;
}
impl WavePosition for Vector2<i32> {
type Counter = Vector2OffsetCounter;
}
/// A trait for counting types that store a `usize` for each of the offsets from
/// some `OffsetPosition` type. During the wave function collapse process, these
/// counts can be reduced as adjacent patterns possibilities are removed so
/// that the algorithm can keep track of how many ways a pattern is possible.
/// For a pattern to be possible, it must match some possible pattern in all
/// offsets, so if any of these counts become 0 the algorithm will know to
/// remove the corresponding pattern.
pub trait WaveOffsetCounter: Default + Clone {
/// The type that will be the index into the array of `usize` counts.
type Offset;
/// The count for the given offset.
fn count(&self, offset: &Self::Offset) -> usize;
/// The count for the given offset.
fn count_mut(&mut self, offset: &Self::Offset) -> &mut usize;
}
/// A [`WaveOffsetCounter`] for `Vector3` offsets, containing 4 `usize` values,
/// one for each adjacent cell of a pattern in 2D.
#[derive(Debug, Clone, Default)]
pub struct Vector2OffsetCounter([usize; 4]);
impl WaveOffsetCounter for Vector2OffsetCounter {
type Offset = Vector2Offset;
fn count(&self, offset: &Vector2Offset) -> usize {
self.0[offset.0]
}
fn count_mut(&mut self, offset: &Vector2Offset) -> &mut usize {
&mut self.0[offset.0]
}
}
impl WavePosition for Vector3<i32> {
type Counter = Vector3OffsetCounter;
}
/// A [`WaveOffsetCounter`] for `Vector3` offsets, containing 6 `usize` values,
/// one for each adjacent cell of a pattern in 3D.
#[derive(Debug, Clone, Default)]
pub struct Vector3OffsetCounter([usize; 6]);
impl WaveOffsetCounter for Vector3OffsetCounter {
type Offset = Vector3Offset;
fn count(&self, offset: &Vector3Offset) -> usize {
self.0[offset.0]
}
fn count_mut(&mut self, offset: &Vector3Offset) -> &mut usize {
&mut self.0[offset.0]
}
}
/// Trait for an object that specifies the rules by which wave function collapse
/// tiles are chosen.
pub trait WfcConstrain {
/// The offset type that is used to specify the ways in which one pattern
/// may be adjacent to another pattern.
type Offset;
/// The type of patterns that wave function collapse will choose.
type Pattern: Eq + Hash + Clone;
/// Iterator for all the patterns that wave function collapse is allowed to choose.
fn all_patterns(&self) -> impl Iterator<Item = &Self::Pattern>;
/// The probability of choosing a given pattern when there are no restrictions on patterns
/// in any of the adjacent cells. This should be between 0.0 and 1.0.
fn probability_of(&self, pattern: &Self::Pattern) -> f32;
/// True if the `from` pattern may be chosen when the `to` pattern has already been chosen
/// for the cell at position `offset` relative to `from`.
fn is_legal(&self, from: &Self::Pattern, offset: &Self::Offset, to: &Self::Pattern) -> bool;
}
/// An implementation for `WfcConstrain` that stores the information for each pattern
/// in a hash table and also keeps track of a which values each pattern may represent,
/// so that patterns may be translated into tiles after wave function collapse is complete.
pub struct HashWfcConstraint<Pat, V> {
pattern_map: FxHashMap<Pat, WfcPatternConstraint<V>>,
}
/// The data for a pattern, with probability of the pattern
/// and probability of each tile that matches the pattern.
struct WfcPatternConstraint<V> {
/// The probability of the pattern being chosen, between 0.0 and 1.0.
probability: f32,
/// The set of values that may be randomly chosen to represent the pattern
/// when patterns are converted into tiles.
value_set: ProbabilitySet<V>,
}
impl<V> Default for WfcPatternConstraint<V> {
fn default() -> Self {
Self {
probability: 0.0,
value_set: ProbabilitySet::default(),
}
}
}
impl<Pat, V> Default for HashWfcConstraint<Pat, V> {
fn default() -> Self {
Self {
pattern_map: FxHashMap::default(),
}
}
}
impl<Pat, V> HashWfcConstraint<Pat, V>
where
Pat: Eq + Hash,
{
/// Remove all data, reseting this object to empty so it is ready
/// to be reused with new pattern data.
pub fn clear(&mut self) {
self.pattern_map.clear();
}
/// Add a new value to the data with the given pattern and frequency.
/// The frequency does not need to be between 0.0 and 1.0.
/// Frequencies of 0.0 or less will be silently ignored, and
/// frequencies will be automatically normalized into probabilities
/// when [`finalize`](Self::finalize) is called.
pub fn add(&mut self, pattern: Pat, frequency: f32, value: V)
where
Pat: Debug,
V: Debug,
{
if frequency <= 0.0 {
return;
}
self.pattern_map
.entry(pattern)
.or_default()
.value_set
.add(frequency, value);
}
/// Calculate the probability of each pattern based on the frequencies
/// of all values that were added with [`add`](Self::add).
/// The sum of the frequencies is calculated and then each frequency is
/// divided by the sum to normalize the frequencies into probabilities between
/// 0.0 and 1.0.
pub fn finalize(&mut self) {
let sum: f32 = self
.pattern_map
.values()
.map(|v| v.value_set.total_frequency())
.sum();
for v in self.pattern_map.values_mut() {
v.probability = v.value_set.total_frequency() / sum;
}
}
/// A random value that matches the given pattern, based upon previous calls to
/// [`add`](Self::add). Often there may be more than one tile that matches a particular
/// pattern, and so this method helps with the final step after wave function collapse:
/// converting the chosen patterns into actual tiles.
pub fn get_random<R: Rng + ?Sized>(&self, rng: &mut R, pattern: &Pat) -> Option<&V> {
self.pattern_map.get(pattern)?.value_set.get_random(rng)
}
}
impl<Pat, V> WfcConstrain for HashWfcConstraint<Pat, V>
where
Pat: Eq + Hash + Clone + TilePattern,
{
type Offset = Pat::Offset;
type Pattern = Pat;
fn all_patterns(&self) -> impl Iterator<Item = &Self::Pattern> {
self.pattern_map.keys()
}
fn probability_of(&self, pattern: &Self::Pattern) -> f32 {
self.pattern_map
.get(pattern)
.map(|s| s.probability)
.unwrap_or_default()
}
fn is_legal(&self, from: &Self::Pattern, offset: &Self::Offset, to: &Self::Pattern) -> bool {
from.is_legal(offset, to)
}
}
#[derive(Debug, Clone)]
struct Wave<Pos: WavePosition, Pat>(FxHashMap<Pos, WaveCell<Pos::Counter, Pat>>);
impl<K: WavePosition, P> Default for Wave<K, P> {
fn default() -> Self {
Self(FxHashMap::default())
}
}
impl<Pos: WavePosition, Pat> Deref for Wave<Pos, Pat> {
type Target = FxHashMap<Pos, WaveCell<Pos::Counter, Pat>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<Pos: WavePosition, Pat> DerefMut for Wave<Pos, Pat> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Debug, Clone)]
struct WaveCell<Count, Pat> {
/// The possible patterns, each with a count of the possible patterns in other
/// cells that make this pattern possible.
pattern_possibilities: FxHashMap<Pat, Count>,
/// The sum of p(P) * log(p(P)) for all possible patterns P in the cell.
plogp_sum: Entropy,
/// The sum of p(P) for all possible patterns P in the cell.
sum: Entropy,
/// The log of `sum`.
log_sum: Entropy,
/// The entropy of the cell
entropy: Entropy,
}
impl<Count, Pat> Default for WaveCell<Count, Pat> {
fn default() -> Self {
Self {
pattern_possibilities: FxHashMap::default(),
plogp_sum: 0.0,
sum: 0.0,
log_sum: 0.0,
entropy: 0.0,
}
}
}
impl<Count, Pat> WaveCell<Count, Pat> {
pub fn single_pattern(&self) -> Option<&Pat> {
let mut keys = self.pattern_possibilities.keys();
let pat = keys.next();
if keys.next().is_none() {
pat
} else {
None
}
}
}
/// Statistical information derived from a [`WfcConstrain`] object.
/// A `WaveLimits` object is only valid for a particular `WfcConstrain` object
/// so long as the output of the `WfcConstrain` object's methods do not change.
#[derive(Debug, Clone)]
struct WaveLimits<Count, Pat> {
max_cell: WaveCell<Count, Pat>,
plogp_map: FxHashMap<Pat, Entropy>,
maximum_noise: Entropy,
}
impl<Count, Pat> Default for WaveLimits<Count, Pat> {
fn default() -> Self {
Self {
max_cell: WaveCell::default(),
plogp_map: FxHashMap::default(),
maximum_noise: 0.0,
}
}
}
impl<Count: WaveOffsetCounter, Pat: Clone + Hash + Eq> WaveLimits<Count, Pat> {
/// Use the data in a [`WfcConstrain`] object to initialize the limits
/// for a new wave function collapse using the given constraint.
pub fn fill_from<
Pos: WavePosition<Counter = Count, Offset = Count::Offset>,
Con: WfcConstrain<Pattern = Pat, Offset = Count::Offset>,
>(
&mut self,
constraint: &Con,
) {
self.plogp_map.clear();
let mut plogp_sum = 0.0;
let mut sum = 0.0;
let mut min_abs_plogp = Entropy::INFINITY;
self.max_cell.pattern_possibilities.clear();
for pattern in constraint.all_patterns() {
let p = constraint.probability_of(pattern) as Entropy;
let plogp = p * p.ln();
self.plogp_map.insert(pattern.clone(), plogp);
let abs_plogp = plogp.abs();
if abs_plogp < min_abs_plogp {
min_abs_plogp = abs_plogp;
}
plogp_sum += plogp;
sum += p;
let mut count = Count::default();
for offset in Pos::all_offsets() {
*count.count_mut(&offset) = constraint
.all_patterns()
.filter(|p| constraint.is_legal(p, &offset, pattern))
.count();
}
self.max_cell
.pattern_possibilities
.insert(pattern.clone(), count);
}
let log_sum = sum.log10();
self.max_cell.plogp_sum = plogp_sum;
self.max_cell.sum = sum;
self.max_cell.log_sum = log_sum;
self.max_cell.entropy = log_sum - plogp_sum / sum;
self.maximum_noise = min_abs_plogp / 2.0;
}
}
/// Wave function collapse is a multi-step that may require a significant amount of time,
/// depending on how may cells need to be filled. `WfcControlFlow` allows wave propagation
/// methods to specify in their return value whether they have completed their task or whether
/// further work is required. This allows the user to spread the wave function collapse
/// across multiple method calls so it can be mixed with other work or even aborted.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum WfcControlFlow {
/// More work is required.
Continue,
/// The method has finished whatever it was trying to do.
Finish,
}
/// Wave function collapse failed to find a solution. Due to the randomization of the algorithm,
/// a failed attempt does not necessarily indicate a problem, but may instead be due to chance.
/// Another attempt my succeed where a previous one failed.
#[derive(Debug)]
pub struct WfcFailure;
impl Error for WfcFailure {}
impl Display for WfcFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Wave function collapse failed to find a solution.")
}
}
/// The propagator is responsible for the wave function collapse algorithm and
/// it contains all the necessary data except for the [`WfcConstrain`] object
/// that contains the rules for which patterns may be chosen.
///
/// The contraint object is not owned by the propagator because the wave function
/// collapse algorithm may be spread out across many steps and it may be necessary
/// for the constraint object to not live as long as the propagator, and be
/// reconstructed at each step.
///
/// Every constraint object at each step should always fully agree with the constraint
/// objects at all previous steps, or else the algorithm may produce undesired results.
#[derive(Debug, Default, Clone)]
pub struct WfcPropagator<Pos: WavePosition, Pat> {
limits: WaveLimits<Pos::Counter, Pat>,
wave: Wave<Pos, Pat>,
propagating: Vec<(Pos, Pat)>,
pending: Vec<(Pos, Pat)>,
backtrack_cells: Vec<WaveCell<Pos::Counter, Pat>>,
backtrack_map: FxHashMap<Pos, WaveCell<Pos::Counter, Pat>>,
}
impl<Pos: WavePosition + Debug, Pat: Clone + Hash + Eq + Debug> WfcPropagator<Pos, Pat> {
/// An iterator over the positions of every cell of the wave.
pub fn positions(&self) -> impl Iterator<Item = &Pos> {
self.wave.keys()
}
/// True if the wave has a cell at the given position.
pub fn contains_cell(&self, position: &Pos) -> bool {
self.wave.contains_key(position)
}
/// Iterator over the cells that have been given a pattern by the wave function collapse.
pub fn assigned_patterns(&self) -> impl Iterator<Item = (&Pos, &Pat)> {
self.wave
.iter()
.filter_map(|(p, c)| Some((p, c.single_pattern()?)))
}
/// Use the data in a [`WfcConstrain`] object to initialize the propagator
/// for a new wave function collapse using the given constraint.
/// After calling this method, the next step is to call [`add_cell`](Self::add_cell) for each
/// cell that needs to be filled by wave function collapse.
pub fn fill_from<Con: WfcConstrain<Pattern = Pat, Offset = <Pos as OffsetPosition>::Offset>>(
&mut self,
constraint: &Con,
) {
self.limits.fill_from::<Pos, Con>(constraint);
self.wave.clear();
self.propagating.clear();
}
/// Create a new cell at the given position, assuming that wave function collapse has not yet begun
/// and all the surrounding cells have no restrictions.
/// Calling this after [`restrict_edge`](Self::restrict_edge), [`observe_random_cell`](Self::observe_random_cell),
/// or [`observe_all`](Self::observe_all) may produce undesirable consequences.
/// After all the cells have been added, the next step is to optionally call `restrict_edge` to constrain which patterns
/// the may be put into the border cells.
pub fn add_cell(&mut self, position: Pos) {
let _ = self.wave.insert(position, self.limits.max_cell.clone());
}
fn save_cell(&mut self, position: Pos) {
if let Entry::Vacant(entry) = self.backtrack_map.entry(position.clone()) {
let Some(cell) = self.wave.get(&position) else {
return;
};
let mut backtrack_cell = self.backtrack_cells.pop().unwrap_or_default();
backtrack_cell.clone_from(cell);
entry.insert(backtrack_cell);
}
}
fn clear_backtrack(&mut self) {
for (_, backtrack_cell) in self.backtrack_map.drain() {
self.backtrack_cells.push(backtrack_cell);
}
}
fn backtrack(&mut self) {
for (pos, backtrack_cell) in self.backtrack_map.drain() {
let Some(cell) = self.wave.get_mut(&pos) else {
continue;
};
cell.clone_from(&backtrack_cell);
self.backtrack_cells.push(backtrack_cell);
}
}
/// Randomly choose a low-entropy cell. This is used by [`observe_random_cell`](Self::observe_random_cell)
/// to decide which cell will be observed.
pub fn find_min_entropy<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<Pos>
where
Pos::Counter: Debug,
{
let mut min_pos = None;
let mut min_entropy = Entropy::INFINITY;
for (position, cell) in self.wave.iter() {
if cell.pattern_possibilities.len() <= 1 || cell.entropy >= min_entropy {
continue;
}
let noise = rng.gen_range(0.0..=self.limits.maximum_noise);
let entropy = cell.entropy + noise;
if entropy < min_entropy {
min_entropy = entropy;
min_pos = Some(position.clone());
}
}
min_pos
}
/// Choose a random pattern from among the potential patterns for the given cell based
/// on probabilities given in `constraint`. This is called by
/// [`observe_random_cell`](Self::observe_random_cell) to decide which pattern to assign.
pub fn choose_random_pattern<R, Con>(
&self,
position: &Pos,
rng: &mut R,
constraint: &Con,
) -> Option<Pat>
where
R: Rng + ?Sized,
Con: WfcConstrain<Pattern = Pat, Offset = Pos::Offset>,
{
let cell = self.wave.get(position)?;
let mut target = rng.gen_range(0.0..cell.sum);
for pattern in cell.pattern_possibilities.keys() {
let p = constraint.probability_of(pattern) as Entropy;
target -= p;
if target <= 0.0 {
return Some(pattern.clone());
}
}
cell.pattern_possibilities.keys().next().cloned()
}
/// Constrain the cells around the given position based on the assumption that the given
/// position has the given pattern. Calling this method twice on the same position may put
/// the surrounding cells into an invalid state, even if both calls have the same pattern.
/// This should be called after all cells have been added using [`add_cell`](Self::add_cell),
/// and there should be no cell at the given position.
///
/// The next step is to call [`observe_random_cell`](Self::observe_random_cell) or
/// [`observe_all`](Self::observe_all) to begin collapsing the wave function.
pub fn restrict_edge<Con>(
&mut self,
position: &Pos,
pattern: &Pat,
constraint: &Con,
) -> Result<(), WfcFailure>
where
Con: WfcConstrain<Pattern = Pat, Offset = Pos::Offset>,
Pos: Debug,
{
for p in constraint.all_patterns() {
if pattern != p {
self.after_restrict(position, p, constraint)?;
}
}
Ok(())
}
fn set_cell<Con>(
&mut self,
position: &Pos,
pattern: &Pat,
constraint: &Con,
) -> Result<(), WfcFailure>
where
Con: WfcConstrain<Pattern = Pat, Offset = Pos::Offset>,
{
let cell = self.wave.get_mut(position).expect("Missing wave cell");
let mut possibilities = std::mem::take(&mut cell.pattern_possibilities);
let (pattern, count) = possibilities
.remove_entry(pattern)
.expect("Missing pattern");
for (p, _) in possibilities.drain() {
self.after_restrict(position, &p, constraint)?;
}
possibilities.insert(pattern, count);
let cell = self.wave.get_mut(position).unwrap();
cell.pattern_possibilities = possibilities;
self.verify(position, constraint)
}
/// Completely collapse the wave function. This method repeatedly observes a random cell and
/// propagates each observation until all cells have been observed or the wave function collapse
/// fails due to not being able to find a valid pattern for some cell.
///
/// If the number of cells is large, this method may be slow. Use [`observe_random_cell`](Self::observe_random_cell)
/// to progress the collapse one cell at a time and thereby have more control over the process
/// and allow the possibility of aborting.
pub fn observe_all<R, Con>(&mut self, rng: &mut R, constraint: &Con) -> Result<(), WfcFailure>
where
R: Rng + ?Sized,
Con: WfcConstrain<Pattern = Pat, Offset = Pos::Offset>,
Pos: Debug,
Pos::Counter: Debug,
{
while self.observe_random_cell(rng, constraint)? == WfcControlFlow::Continue {}
Ok(())
}
/// Observing a cell means choosing a random pattern for that cell from all the potential patterns
/// for that particular cell. Each cell keeps its own independent list of possible patterns,
/// and after a cell has been observed the possibilities for the surrounding cells may need to be
/// restricted. The restriction of the surrounding cells is called *propagation* and it should be
/// performed after each observation by calling [`propagate`](Self::propagate) or
/// [`propagate_until_finished`](Self::propagate_until_finished).
///
/// If propagation is not complete when this method is called, then `propagate_until_finished` is
/// automatically called before the cell is observed.
///
/// [`WfcControlFlow::Continue`] is returned if a cell was successfully observed, meaning that propagation
/// and more observations may be required to complete the collapse.
/// [`WfcControlFlow::Finish`] is returned if no cell could be observed because the pattern for all
/// cells has already been determined and the wave function collapse is complete.
pub fn observe_random_cell<R, Con>(
&mut self,
rng: &mut R,
constraint: &Con,
) -> Result<WfcControlFlow, WfcFailure>
where
R: Rng + ?Sized,
Con: WfcConstrain<Pattern = Pat, Offset = Pos::Offset>,
Pos: Debug,
Pos::Counter: Debug,
{
self.propagate_until_finished(constraint)?;
let Some(position) = self.find_min_entropy(rng) else {
return Ok(WfcControlFlow::Finish);
};
let pattern = self
.choose_random_pattern(&position, rng, constraint)
.unwrap();
self.save_cell(position.clone());
for offset in Pos::all_offsets() {
let p = position.clone() + offset;
self.save_cell(p);
}
match self.set_cell(&position, &pattern, constraint) {
Ok(()) => {
self.clear_backtrack();
self.propagating.append(&mut self.pending);
}
Err(_) => {
self.backtrack();
self.pending.clear();
self.restrict(&position, &pattern, constraint)?;
self.propagating.append(&mut self.pending);
}
}
Ok(WfcControlFlow::Continue)
}
/// Repeatedly call [`propagate`](Self::propagate) until it returns [`WfcControlFlow::Finish`],
/// thus ensuring that all the cells are prepared for the next observation. This is called
/// automatically by [`observe_random_cell`](Self::observe_random_cell).
pub fn propagate_until_finished<Con>(&mut self, constraint: &Con) -> Result<(), WfcFailure>
where
Con: WfcConstrain<Pattern = Pat, Offset = Pos::Offset>,
{
while self.propagate(constraint)? == WfcControlFlow::Continue {}
Ok(())
}
/// Propagate the restrictions from the most recent calls to [`observe_random_cell`](Self::observe_random_cell)
/// or [`restrict_edge`](Self::restrict_edge) by one step, so that appropriate restrictions are spread across
/// the cells of the wave. This method should be repeatedly called until it returns [`WfcControlFlow::Finish`]
/// before another cell is observed so that the observation is based upon an accurate list of possible patterns
/// for the cell.
pub fn propagate<Con>(&mut self, constraint: &Con) -> Result<WfcControlFlow, WfcFailure>
where
Con: WfcConstrain<Pattern = Pat, Offset = Pos::Offset>,
{
let Some((position, pattern)) = self.propagating.pop() else {
return Ok(WfcControlFlow::Finish);
};
self.restrict(&position, &pattern, constraint)?;
self.propagating.append(&mut self.pending);
Ok(WfcControlFlow::Continue)
}
/// The given pattern is no longer valid at the given position. Update the surrouding cells
/// to reflect this restriction and if any of the surrounding cells need to be restricted,
/// add that restriction to the propagation list for future consideration.
/// Return false if the restriction was impossible due to leaving the cell with no remaining
/// possibilities.
fn restrict<Con>(
&mut self,
position: &Pos,
pattern: &Pat,
constraint: &Con,
) -> Result<(), WfcFailure>
where
Con: WfcConstrain<Pattern = Pat, Offset = Pos::Offset>,
{
let Some(cell) = self.wave.get_mut(position) else {
return Ok(());
};
match cell.pattern_possibilities.entry(pattern.clone()) {
Entry::Occupied(entry) => drop(entry.remove()),
Entry::Vacant(_) => return Ok(()),
}
match cell.pattern_possibilities.len() {
0 => return Err(WfcFailure),
1 => self.verify(position, constraint)?,
_ => (),
}
let Some(cell) = self.wave.get_mut(position) else {
return Ok(());
};
if let Some(plogp) = self.limits.plogp_map.get(pattern) {
let p = constraint.probability_of(pattern) as Entropy;
cell.plogp_sum -= plogp;
cell.sum -= p;
cell.log_sum = cell.sum.ln();
cell.entropy = cell.log_sum - cell.plogp_sum / cell.sum;
self.after_restrict(position, pattern, constraint)
} else {
Ok(())
}
}
fn after_restrict<Con>(
&mut self,
position: &Pos,
pattern: &Pat,
constraint: &Con,
) -> Result<(), WfcFailure>
where
Con: WfcConstrain<Pattern = Pat, Offset = Pos::Offset>,
Pos: Debug,
{
for offset in Pos::all_offsets() {
let other_pos = position.clone() + offset.clone();
let Some(other_cell) = self.wave.get_mut(&other_pos) else {
continue;
};
other_cell
.pattern_possibilities
.retain(|other_pattern, counter| {
if constraint.is_legal(pattern, &offset, other_pattern) {
let c = counter.count_mut(&offset);
*c -= 1;
if *c == 0 {
self.pending
.push((other_pos.clone(), other_pattern.clone()));
}
*c > 0
} else {
true
}
});
if other_cell.pattern_possibilities.is_empty() {
return Err(WfcFailure);
}
}
Ok(())
}
fn verify<Con>(&self, position: &Pos, constraint: &Con) -> Result<(), WfcFailure>
where
Con: WfcConstrain<Pattern = Pat, Offset = Pos::Offset>,
{
let Some(cell) = self.wave.get(position) else {
return Ok(());
};
let Some(pat) = cell.single_pattern() else {
return Ok(());
};
for offset in Pos::all_offsets() {
let other_pos = position.clone() + offset.clone();
let Some(other_cell) = self.wave.get(&other_pos) else {
continue;
};
if !other_cell
.pattern_possibilities
.keys()
.any(|p| constraint.is_legal(pat, &offset, p))
{
return Err(WfcFailure);
}
}
Ok(())
}
}
+1
View File
@@ -24,6 +24,7 @@ fyrox-animation = { path = "../fyrox-animation", version = "0.36.0" }
fyrox-graph = { path = "../fyrox-graph", version = "0.36.0" }
fyrox-graphics = { path = "../fyrox-graphics", version = "0.36.0" }
fyrox-texture = { path = "../fyrox-texture", version = "0.36.0" }
fyrox-autotile = { path = "../fyrox-autotile", version = "0.36.0" }
rapier2d = { version = "0.23", features = ["debug-render"] }
rapier3d = { version = "0.23", features = ["debug-render"] }
image = { version = "0.25.1", default-features = false, features = ["gif", "jpeg", "png", "tga", "tiff", "bmp"] }
+3
View File
@@ -63,6 +63,9 @@ pub use fyrox_resource as asset;
#[doc(inline)]
pub use fyrox_ui as gui;
#[doc(inline)]
pub use fyrox_autotile as autotile;
/// Defines a builder's `with_xxx` method.
#[macro_export]
macro_rules! define_with {
+508
View File
@@ -0,0 +1,508 @@
// 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 std::fmt::Debug;
use super::*;
use fxhash::FxHashMap;
use fyrox_autotile::{
AutoPatternConstraint, AutoPatternValueMap, AutoTerrainPatternMap, AutoTileContext, AutoTiler,
HashConstraintMap, HashWfcConstraint, OffsetPosition, PatternSource, TileConstraint,
Vector2Offset, WfcConstrain, WfcFailure, WfcPropagator,
};
use fyrox_core::log::Log;
use crate::core::rand::Rng;
pub use fyrox_autotile::PatternBits;
impl From<NineI8> for PatternBits {
fn from(value: NineI8) -> Self {
Self(value.0)
}
}
/// A value that specifies the terrain of a tile map cell for auto-tiling.
/// This value comes from the center of a nine-slice tile set property,
/// so it can constrain which tiles are permitted in a particular cell.
pub type TileTerrainId = i8;
/// Autotiler for the tiles of a [`TileSet`] that uses the nine-slice
/// values of one of the tile set's properties as the auto-tiler's pattern.
#[derive(Debug, Default, Clone)]
pub struct TileSetAutoTiler(AutoTiler<Vector2<i32>, PatternBits>);
/// This is a specialization of [`AutoTileContext`] for use with a [`TileSet`].
/// The pattern of each tile is taken from the values of a nine-slice property,
/// and [`TileDefinitionHandle`] is used to represent individual tiles.
///
/// See [`TileSetAutoTileContext::fill_pattern_map`] for details on how to fill a
/// `TileSetAutoTileContext` with data from a tile set in preparation for auto-tiling.
#[derive(Default, Clone)]
pub struct TileSetAutoTileContext(
AutoTileContext<TileTerrainId, PatternBits, TileDefinitionHandle>,
);
impl Debug for TileSetAutoTileContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
/// A map from `Vector2<i32>` positions to auto-tile cell constriants.
/// Each cell contraint is either an [`TileTerrainId`] which represents the center of the
/// permitted patterns for this cell, or else it is a [`PatternBits`] which
/// represents the only permitted pattern for this cell and thereby constrains
/// the neighboring cells.
pub type TileSetConstraintMap = HashConstraintMap<Vector2<i32>, TileTerrainId, PatternBits>;
/// A terrain pattern map for tile map auto-tiler, using the fields of a
/// nine-slice tile set property for the terrain types and the patterns.
pub type TileSetTerrainPatternMap = AutoTerrainPatternMap<TileTerrainId, PatternBits>;
/// A mapping from auto-tiler patterns to tile handles. Multiple tiles may share the
/// same pattern, and each tile is given a frequency value that controls the probability
/// of randomly selecting that tile when its pattern is chosen.
pub type TileSetAutoValueMap = AutoPatternValueMap<PatternBits, TileDefinitionHandle>;
/// A pair of a [`TileSetConstraintMap`] and a [`TileSetTerrainPatternMap`].
/// - The constraint map tells the auto-tiler which cells have undecided patterns,
/// which cells have fixed patterns, and the terrain type of the undecided cells.
/// - The terrain map tells the auto-tiler which patterns are available for each
/// terrain type, and the order in which the patterns should be tried.
pub type TileSetAutoTileConstraint<'a, 'b> =
AutoPatternConstraint<'a, 'b, Vector2<i32>, TileTerrainId, PatternBits>;
/// A hash table based wave function collapse constraint that maps [`PatternBits`]
/// objects to sets of [`TileDefinitionHandle`]. Each handle refers to a tile,
/// and each tile is assigned a pattern and a frequency. The sum of the frequencies
/// of all a pattern's tiles becomes the frequency of the pattern, which is then
/// normalized into a probability between 0.0 and 1.0 for use as a constraint
/// for a [`TileSetWfcPropagator`] which will perform the wave function collapse.
#[derive(Default)]
pub struct TileSetWfcConstraint(HashWfcConstraint<PatternBits, TileDefinitionHandle>);
impl Deref for TileSetWfcConstraint {
type Target = HashWfcConstraint<PatternBits, TileDefinitionHandle>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for TileSetWfcConstraint {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TileSetWfcConstraint {
/// Clear the data from this constraint and replace it with new data
/// based upon the given tile set and property UUIDs. All of the tiles
/// of the tile set are searched and each one's pattern property is checked
/// for a center value that matches one of the keys of `terrain_freq`.
/// When such a tile is found, it is inserted into the map using the pattern property
/// value as the pattern and the frequency property value as the frequency.
/// - `tile_set`: The tile set to iterate through.
/// - `pattern_property`: The UUID of a nine-slice property in `tile_set` that will
/// be used for the pattern of each tile. The center value of the nine-slice is
/// the terrain, unless the center value is 0, in which case the tile is ignored.
/// - `frequency_property`: The UUID of a float property in `tile_set` that will be
/// used for the frequency of each tile. If None, then every tile has frequency 1.0.
/// - `terrain_freq`: A hash map of the terrains that will be used in wave function collapse.
/// Tiles whose center value are not keys in this hash map will be ignored.
/// Tiles whose center value are keys in this hash map will have their frequency
/// multiplied by the corresponding value in the hash map to calculate the final
/// frequency of the tile. This allows terrains to have their frequency weighted,
/// and allows unwanted terrains to be excluded.
///
/// *Note:* Terrain 0 is treated specially. It has one pattern, the all-zeros default
/// [`PatternBits`], and it corresponds to the empty tile. Tiles whose pattern value
/// have 0 in the center are presumed to be not intended to be part of the wave
/// function collapse, since 0 is the default value. The frequency of choosing empty
/// tiles in wave function collapse can be controlled by setting `terrain_freq[0]` to
/// some value.
pub fn fill_pattern_map(
&mut self,
tile_set: &TileSet,
pattern_property: TileSetPropertyNine,
frequency_property: Option<TileSetPropertyF32>,
terrain_freq: &FxHashMap<TileTerrainId, f32>,
) -> Result<(), FillPatternMapError> {
self.clear();
if let Some(&frequency) = terrain_freq.get(&0) {
self.add(
PatternBits::default(),
frequency,
TileDefinitionHandle::EMPTY,
);
}
if tile_set
.find_property(*pattern_property.property_uuid())
.is_none()
{
return Err(FillPatternMapError::PatternInvalidId);
}
if let Some(id) = frequency_property {
if tile_set.find_property(*id.property_uuid()).is_none() {
return Err(FillPatternMapError::FrequencyInvalidId);
}
}
for handle in tile_set.all_tiles() {
let frequency = if let Some(id) = frequency_property {
id.get_from_tile_set(tile_set, handle)
.map_err(|_| FillPatternMapError::FrequencyWrongType)?
} else {
1.0
};
if frequency <= 0.0 {
continue;
}
let pattern: NineI8 = pattern_property
.get_from_tile_set(tile_set, handle)
.map_err(|_| FillPatternMapError::PatternWrongType)?;
let pattern: PatternBits = pattern.into();
let center = pattern.center();
if center != 0 {
if let Some(terrain_frequency) = terrain_freq.get(&center) {
self.add(pattern, frequency * terrain_frequency, handle);
}
}
}
self.finalize();
Ok(())
}
}
/// An error that might occur while filling a pattern map from a tile set
/// using [`TileSetAutoTileContext::fill_pattern_map`].
#[derive(Debug, PartialEq, Eq)]
pub enum FillPatternMapError {
/// The UUID for the frequency property was not found in the tile set.
FrequencyInvalidId,
/// The frequency property was not f32.
FrequencyWrongType,
/// The UUID for the terrain property was not found in the tile set.
PatternInvalidId,
/// The terrain property was not a nine-slice.
PatternWrongType,
}
impl Error for FillPatternMapError {}
impl Display for FillPatternMapError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FillPatternMapError::FrequencyInvalidId => write!(
f,
"The property UUID for the frequency does not match any property in the tile set."
),
FillPatternMapError::FrequencyWrongType => {
write!(f, "The frequency property should be an f32.")
}
FillPatternMapError::PatternInvalidId => write!(
f,
"The property UUID for the pattern does not match any property in the tile set."
),
FillPatternMapError::PatternWrongType => {
write!(f, "The pattern property should be a nine-slice property.")
}
}
}
}
impl Deref for TileSetAutoTileContext {
type Target = AutoTileContext<TileTerrainId, PatternBits, TileDefinitionHandle>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for TileSetAutoTileContext {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TileSetAutoTileContext {
/// Prepare for auto-tiling by extracting tile patterns from the given tile set.
/// Each tile has an associated pattern through the value of the property
/// with the UUID given by `pattern_property`. Each pattern contains nine i8 values
/// arranged in a 3x3 grid. The outer eight values are used to determine which tiles
/// are permitted to be adjacent to which other tiles, while the central i8 controls
/// which cells may have this tile. The central value of 0 is reserved for cells
/// that should be empty, and any tiles which have a central value of 0 are ignored
/// by the auto-tiler.
///
/// When more than one tile has the same pattern, the auto-tiler must choose between
/// the tiles randomly. The `frequency_property` is used to weigh the probabilities
/// of choosing each tile, so tiles with higher values are more likely to be chosen.
/// If no frequency property is given, then all tiles are equally likely.
pub fn fill_pattern_map(
&mut self,
tile_set: &TileSet,
pattern_property: TileSetPropertyNine,
frequency_property: Option<TileSetPropertyF32>,
) -> Result<(), FillPatternMapError> {
self.clear();
self.add(0, PatternBits::default(), 1.0, TileDefinitionHandle::EMPTY);
if tile_set
.find_property(*pattern_property.property_uuid())
.is_none()
{
return Err(FillPatternMapError::PatternInvalidId);
}
if let Some(id) = frequency_property {
if tile_set.find_property(*id.property_uuid()).is_none() {
return Err(FillPatternMapError::FrequencyInvalidId);
}
}
for handle in tile_set.all_tiles() {
let frequency = if let Some(id) = frequency_property {
id.get_from_tile_set(tile_set, handle)
.map_err(|_| FillPatternMapError::FrequencyWrongType)?
} else {
1.0
};
if frequency <= 0.0 {
continue;
}
let pattern: NineI8 = pattern_property
.get_from_tile_set(tile_set, handle)
.map_err(|_| FillPatternMapError::PatternWrongType)?;
let pattern: PatternBits = pattern.into();
let center = pattern.center();
if center != 0 {
self.add(center, pattern, frequency, handle);
}
}
self.sort();
Ok(())
}
}
impl Deref for TileSetAutoTiler {
type Target = AutoTiler<Vector2<i32>, PatternBits>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for TileSetAutoTiler {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TileSetAutoTiler {
/// Modify the given tile map update based on the result of the
/// auto-tiler.
pub fn apply_autotile_to_update<R: Rng + ?Sized>(
&self,
rng: &mut R,
value_map: &TileSetAutoValueMap,
update: &mut MacroTilesUpdate,
) {
for (pos, pat) in self.iter() {
let Some(handle) = value_map
.get(pat)
.and_then(|ps| ps.get_random(rng))
.cloned()
else {
continue;
};
let brush_cell = update
.get(pos)
.cloned()
.flatten()
.and_then(|el| el.brush_cell);
let handle = if handle.is_empty() {
None
} else {
Some(StampElement { handle, brush_cell })
};
_ = update.insert(*pos, handle);
}
}
}
/// An object that wraps a tile map, a tile set, an update, and the UUID of the property
/// that is used to store the pattern of each tile, and then provides access to the pattern
/// at any cell. It looks for the cell's handle in the update, then in the tile map, and
/// then uses the tile set to find the pattern.
pub struct TileSetPatternSource<'a, 'b, 'c> {
/// The tile map to get tile handles from.
pub tile_map: &'a TileMap,
/// The tile set to get property values from.
pub tile_set: &'b TileSet,
/// The current set of modifications to the tile map.
/// When a cell has been modified, the updated handle will be used
/// instead of the handle stored in `tile_map`.
pub update: &'c MacroTilesUpdate,
/// The UUID of the property to use as the tile pattern.
pub property_id: TileSetPropertyNine,
}
impl TileSetPatternSource<'_, '_, '_> {
fn pattern_at(&self, position: &Vector2<i32>) -> Result<PatternBits, TilePropertyError> {
let Some(element) = self
.update
.get(position)
.cloned()
.unwrap_or_else(|| self.tile_map.tile_handle(*position).map(|h| h.into()))
else {
return Ok(PatternBits::default());
};
self.property_id
.get_from_tile_set(self.tile_set, element.handle)
.map(|v| v.into())
}
}
impl PatternSource for TileSetPatternSource<'_, '_, '_> {
type Position = Vector2<i32>;
type Terrain = TileTerrainId;
type Pattern = PatternBits;
fn get_terrain(&self, position: &Vector2<i32>) -> Option<TileTerrainId> {
match self.pattern_at(position) {
Ok(pattern) => Some(pattern.center()),
Err(err) => {
Log::err(err.to_string());
None
}
}
}
/// The constraint for the cell at the given position.
fn get(&self, position: &Vector2<i32>) -> TileConstraint<TileTerrainId, PatternBits> {
match self.pattern_at(position) {
Ok(pattern) => TileConstraint::Pattern(pattern),
Err(err) => {
Log::err(err.to_string());
TileConstraint::None
}
}
}
}
/// Wave function collapse propagator for the tiles of a [`TileSet`] that uses
/// the nine-slice values of one of the tile set's properties as the wave function's
/// pattern.
#[derive(Debug, Default, Clone)]
pub struct TileSetWfcPropagator {
propagator: WfcPropagator<Vector2<i32>, PatternBits>,
edge_restrictions: FxHashMap<Vector2<i32>, PatternBits>,
}
impl Deref for TileSetWfcPropagator {
type Target = WfcPropagator<Vector2<i32>, PatternBits>;
fn deref(&self) -> &Self::Target {
&self.propagator
}
}
impl DerefMut for TileSetWfcPropagator {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.propagator
}
}
impl TileSetWfcPropagator {
/// After all the wave cells have been added using [`WfcPropagator::add_cell`],
/// this method may be used to automatically restrict the edges of the cells by
/// using the given tile map to find the patterns of surrounding tiles and
/// calling [`WfcPropagator::restrict_edge`] as appropriate.
///
/// This forces the wave function collapse to fit smoothly with the existing
/// tiles of the tile map, though such restrictions may make failure more likely.
/// [`WfcFailure`] is returned if wave function collapse is made impossible by the
/// restrictions.
pub fn constrain_edges<Con>(
&mut self,
tile_set: &TileSet,
pattern_property: TileSetPropertyNine,
tile_map: &TileMap,
update: &MacroTilesUpdate,
constraint: &Con,
) -> Result<(), WfcFailure>
where
Con: WfcConstrain<Pattern = PatternBits, Offset = Vector2Offset>,
{
let tiles = tile_map.tiles();
let tiles = tiles.map(|r| r.data_ref());
let mut edge_restrictions = std::mem::take(&mut self.edge_restrictions);
edge_restrictions.clear();
for &p in self.positions() {
for offset in Vector2::all_offsets() {
let p = p + offset;
if self.edge_restrictions.contains_key(&p) {
continue;
}
if self.contains_cell(&p) {
continue;
}
let handle = update.get(&p).map(|el| {
el.as_ref()
.map(|el| el.handle)
.unwrap_or(TileDefinitionHandle::EMPTY)
});
let handle = if let Some(handle) = handle {
handle
} else if let Some(tiles) = tiles.as_ref() {
tiles.get(p).unwrap_or(TileDefinitionHandle::EMPTY)
} else {
TileDefinitionHandle::EMPTY
};
let pattern = pattern_property
.get_from_tile_set(tile_set, handle)
.unwrap_or_default();
edge_restrictions.insert(p, PatternBits(pattern.into()));
}
}
for (p, pattern) in edge_restrictions.drain() {
self.restrict_edge(&p, &pattern, constraint)?;
}
self.edge_restrictions = edge_restrictions;
Ok(())
}
/// Modify the given tile map update based on the result of the
/// auto-tiler.
pub fn apply_autotile_to_update<R: Rng + ?Sized>(
&self,
rng: &mut R,
value_map: &TileSetWfcConstraint,
update: &mut MacroTilesUpdate,
) {
for (pos, pat) in self.assigned_patterns() {
let Some(&handle) = value_map.get_random(rng, pat) else {
continue;
};
let brush_cell = update
.get(pos)
.cloned()
.flatten()
.and_then(|el| el.brush_cell);
let handle = if handle.is_empty() {
None
} else {
Some(StampElement { handle, brush_cell })
};
_ = update.insert(*pos, handle);
}
}
}
+148 -17
View File
@@ -27,12 +27,15 @@
//! In contrast, a tile set is a directory for finding tile data according to a specific position on
//! a specific page. Tiles can be moved in a tile set, but doing so will affect the lookup of tile data.
use fyrox_core::{futures::executor::block_on, log::Log};
use crate::{
asset::{
io::ResourceIo,
loader::{BoxedLoaderFuture, LoaderPayload, ResourceLoader},
manager::ResourceManager,
state::LoadError,
untyped::UntypedResource,
Resource, ResourceData,
},
core::{
@@ -92,6 +95,86 @@ impl From<VisitError> for TileMapBrushResourceError {
}
}
/// Collection of additional data that can be stored in a brush for the purposes
/// of tile map macros. The meaning of the data is determined by matching the UUID
/// with the UUID of some macro or other user, and it is up to the user to determine
/// the type of the resource.
#[derive(Debug, Clone, Default, Reflect)]
pub struct BrushMacroInstanceList(Vec<BrushMacroData>);
impl BrushMacroInstanceList {
/// Iterate through the macro instance resources associated with the given UUID.
pub fn instances_with_uuid(&self, uuid: Uuid) -> impl Iterator<Item = &UntypedResource> {
self.0
.iter()
.filter(move |d| d.macro_id == uuid)
.filter_map(|d| d.settings.as_ref())
}
}
impl Deref for BrushMacroInstanceList {
type Target = Vec<BrushMacroData>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BrushMacroInstanceList {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// Custom Visit implementation to prevent the visit from completely failing if
/// it encounters data that cannot be visited.
impl Visit for BrushMacroInstanceList {
fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
let mut region = visitor.enter_region(name)?;
let mut count = self.0.len() as u32;
count.visit("Count", &mut region)?;
if region.is_reading() {
self.clear();
for i in 0..(count as usize) {
let name = i.to_string();
// If one value fails to read, skip it and read the remaining data.
let mut value = BrushMacroData::default();
match value.visit(&name, &mut region) {
Ok(()) => self.0.push(value),
Err(err) => Log::err(format!("Failed to load brush tool data due to: {err}")),
}
}
} else {
for (i, value) in self.0.iter_mut().enumerate() {
let name = i.to_string();
value.visit(&name, &mut region)?;
}
}
Ok(())
}
}
/// A brush can have zero or more instances of a macro, and each instance
/// has its own configuration data.
#[derive(Debug, Default, Clone, Visit, Reflect)]
pub struct BrushMacroData {
/// The UUID of the macro that owns this instance data.
/// This is used to identify the macro that knows the type of this data.
/// This same UUID will be shared by every data that is an instance
/// of the same macro.
pub macro_id: Uuid,
/// The human-readable name of the instance.
pub name: String,
/// The configuration of the macro, allowing its behaviour to be
/// controlled. The macro is responsible for determining the type of this
/// resource and its content. The macro may also choose to have no
/// configuration settings.
pub settings: Option<UntypedResource>,
}
/// A page of tiles within a brush. Having multiple pages allows a brush to be optimized
/// for use in multiple contexts.
#[derive(Default, Debug, Clone, Visit, Reflect)]
@@ -103,15 +186,6 @@ pub struct TileMapBrushPage {
pub tiles: Tiles,
}
impl TileSource for TileMapBrushPage {
fn transformation(&self) -> OrthoTransformation {
OrthoTransformation::default()
}
fn get_at(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle> {
self.tiles.get(&position).copied()
}
}
impl TileMapBrushPage {
/// The smallest Rect that contains all the tiles on this page.
pub fn bounding_rect(&self) -> OptionTileRect {
@@ -178,15 +252,54 @@ pub struct TileMapBrush {
/// users to customize the organization of pages.
#[reflect(hidden)]
pub pages: TileGridMap<TileMapBrushPage>,
/// A count of changes since last save. New changes add +1. Reverting to previous
/// states add -1. Reverting to a state before the last save can result in negative
/// values. Saving is unnecessary whenever this value is 0.
/// Untyped data stored in this brush for use by some macro.
#[visit(optional)]
pub macros: BrushMacroInstanceList,
/// A record of whether the brush has changed since last time it was saved.
#[reflect(hidden)]
#[visit(skip)]
pub change_count: ChangeFlag,
pub change_flag: ChangeFlag,
}
impl TileMapBrush {
/// Return true after blocking to wait for the brush's tile set to load,
/// if the tile set loads successfully, or if the tile set has no brush.
/// Return false if any error occurs while trying to load the tile set.
pub fn block_until_tile_set_is_loaded(&self) -> bool {
let Some(tile_set) = self.tile_set.as_ref() else {
return true;
};
let tile_set = match block_on(tile_set.clone()) {
Ok(tile_set) => tile_set,
Err(e) => {
Log::err(format!("Tile set load failed! Reason: {e:?}"));
return false;
}
};
if tile_set.is_ok() {
true
} else {
Log::err("Tile set load failed!");
false
}
}
/// Return the tile set for this brush, blocking if the tile set is not yet
/// loaded. None is returned if this brush has no tile set or the tile set fails to load.
pub fn tile_set(&self) -> Option<TileSetResource> {
let tile_set = self.tile_set.as_ref()?;
let tile_set = match block_on(tile_set.clone()) {
Ok(tile_set) => tile_set,
Err(e) => {
Log::err(format!("Tile set load failed! Reason: {e:?}"));
return None;
}
};
if !tile_set.is_ok() {
Log::err("Tile set load failed!");
return None;
}
Some(tile_set)
}
/// True if there is a tile at the given position.
pub fn has_tile_at(&self, page: Vector2<i32>, tile: Vector2<i32>) -> bool {
let Some(page) = self.pages.get(&page) else {
@@ -270,7 +383,7 @@ impl TileMapBrush {
/// Return true if this brush has no tile set.
pub fn is_missing_tile_set(&self) -> bool {
self.tile_set.is_none()
self.tile_set().is_none()
}
fn palette_render_loop_without_tile_set<F>(
@@ -304,7 +417,7 @@ impl TileMapBrush {
where
F: FnMut(Vector2<i32>, TileRenderData),
{
let Some(tile_set) = self.tile_set.as_ref() else {
let Some(tile_set) = self.tile_set() else {
self.palette_render_loop_without_tile_set(stage, page, func);
return;
};
@@ -342,7 +455,8 @@ impl TileMapBrush {
/// then the rendering data for an error tile is returned using `TileRenderData::missing_tile()`.
pub fn get_tile_render_data(&self, position: ResourceTilePosition) -> Option<TileRenderData> {
let handle = self.redirect_handle(position)?;
let mut tile_set = self.tile_set.as_ref()?.state();
let tile_set = self.tile_set()?;
let mut tile_set = tile_set.state();
let data = tile_set
.data()?
.get_tile_render_data(handle.into())
@@ -358,7 +472,7 @@ impl TileMapBrush {
match position.stage() {
TilePaletteStage::Tiles => {
let page = self.pages.get(&position.page())?;
page.tiles.get_at(position.stage_position())
page.tiles.get(&position.stage_position()).copied()
}
TilePaletteStage::Pages => self
.pages
@@ -367,6 +481,23 @@ impl TileMapBrush {
}
}
/// The stamp element for the given position, if the tile in that cell is used
/// to create a stamp. The [`StampElement::handle`] refers to the location of the tile within the
/// tile set, while the [`StampElement::brush_cell`] refers to the location of the tile within
/// the brush.
pub fn stamp_element(&self, position: ResourceTilePosition) -> Option<StampElement> {
match position.stage() {
TilePaletteStage::Pages => self.redirect_handle(position).map(|h| h.into()),
TilePaletteStage::Tiles => {
let page = self.pages.get(&position.page())?;
Some(StampElement {
handle: *page.tiles.get(&position.stage_position())?,
brush_cell: position.handle(),
})
}
}
}
/// The `TileMaterialBounds` taken from the tile set for the tile in the brush at the given position.
pub fn get_tile_bounds(&self, position: ResourceTilePosition) -> Option<TileMaterialBounds> {
let handle = self.redirect_handle(position)?;
+5 -2
View File
@@ -210,12 +210,15 @@ impl ResourceData for TileMapData {
}
impl TileSource for TileMapData {
fn brush(&self) -> Option<&TileMapBrushResource> {
None
}
fn transformation(&self) -> OrthoTransformation {
OrthoTransformation::default()
}
fn get_at(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle> {
self.get(position)
fn get_at(&self, position: Vector2<i32>) -> Option<StampElement> {
self.get(position).map(|h| h.into())
}
}
+1 -1
View File
@@ -154,7 +154,7 @@ impl TileMapEffect for TileUpdateEffect {
for (&position, value) in self.update.iter() {
if context.is_tile_visible(position) {
context.set_tile_visible(position, false);
if let Some((transform, handle)) = *value {
if let Some((transform, handle)) = value.as_ref().map(|v| v.pair()) {
let handle = context
.tile_set
.get_transformed_version(transform, handle)
+35 -13
View File
@@ -21,6 +21,7 @@
//! Tile map is a 2D "image", made out of a small blocks called tiles. Tile maps used in 2D games to
//! build game worlds quickly and easily. See [`TileMap`] docs for more info and usage examples.
mod autotile;
pub mod brush;
mod data;
mod effect;
@@ -32,6 +33,7 @@ pub mod tileset;
mod transform;
mod update;
pub use autotile::*;
use brush::*;
pub use data::*;
pub use effect::*;
@@ -549,23 +551,23 @@ impl TileBook {
match self {
TileBook::Empty => false,
TileBook::TileSet(r) => {
r.header().kind.is_external() && r.data_ref().change_count.needs_save()
r.header().kind.is_external() && r.data_ref().change_flag.needs_save()
}
TileBook::Brush(r) => {
r.header().kind.is_external() && r.data_ref().change_count.needs_save()
r.header().kind.is_external() && r.data_ref().change_flag.needs_save()
}
}
}
/// Attempt to save the resource to its file, if it has one and if `change_count` not zero.
/// Attempt to save the resource to its file, if it has one and if `change_flag` is set.
/// Otherwise do nothing and return Ok to indicate success.
pub fn save(&self) -> Result<(), Box<dyn Error>> {
match self {
TileBook::Empty => Ok(()),
TileBook::TileSet(r) => {
if r.header().kind.is_external() && r.data_ref().change_count.needs_save() {
if r.header().kind.is_external() && r.data_ref().change_flag.needs_save() {
let result = r.save_back();
if result.is_ok() {
r.data_ref().change_count.reset();
r.data_ref().change_flag.reset();
}
result
} else {
@@ -573,10 +575,10 @@ impl TileBook {
}
}
TileBook::Brush(r) => {
if r.header().kind.is_external() && r.data_ref().change_count.needs_save() {
if r.header().kind.is_external() && r.data_ref().change_flag.needs_save() {
let result = r.save_back();
if result.is_ok() {
r.data_ref().change_count.reset();
r.data_ref().change_flag.reset();
}
result
} else {
@@ -592,6 +594,13 @@ impl TileBook {
_ => None,
}
}
/// A reference to the TileMapBrushResource, if this is a TileMapBrushResource.
pub fn brush_ref(&self) -> Option<&TileMapBrushResource> {
match self {
TileBook::Brush(r) => Some(r),
_ => None,
}
}
/// Returns the tile set associated with this resource.
/// If the resource is a tile set, the return that tile set.
/// If the resource is a brush, then return the tile set used by that brush.
@@ -599,7 +608,7 @@ impl TileBook {
match self {
TileBook::Empty => None,
TileBook::TileSet(r) => Some(r.clone()),
TileBook::Brush(r) => r.state().data()?.tile_set.clone(),
TileBook::Brush(r) => r.state().data()?.tile_set(),
}
}
/// Build a list of the positions of all tiles on the given page.
@@ -710,6 +719,22 @@ impl TileBook {
TileBook::Brush(r) => r.state().data()?.redirect_handle(position),
}
}
/// The StampElement for the given position in this resource. For tile sets the element's
/// [`StampElement::brush_cell`] is None.
/// For brushes, the [`StampElement::handle`] refers to the location of the tile within the
/// tile set, while the [`StampElement::brush_cell`] refers to the location of the tile within
/// the brush.
pub fn get_stamp_element(&self, position: ResourceTilePosition) -> Option<StampElement> {
match self {
TileBook::Empty => None,
TileBook::TileSet(r) => r
.state()
.data()?
.redirect_handle(position)
.map(|h| h.into()),
TileBook::Brush(r) => r.state().data()?.stamp_element(position),
}
}
/// Returns an iterator over `(Vector2<i32>, TileDefinitionHandle)` where the first
/// member of the pair is the position of the tile on the page as provided by `positions`
/// and the second member is the handle that would be returned from [`get_tile_handle`](Self::get_tile_handle).
@@ -958,7 +983,7 @@ impl Deref for TileMapDataRef<'_> {
/// An error in finding a property for a tile.
#[derive(Debug)]
pub enum TilePropertyError {
/// The tile map has no tile set, so not tile data is available.
/// The tile map has no tile set, so no tile data is available.
MissingTileSet,
/// The tile map has a tile set, but it is not yet loaded.
TileSetNotLoaded,
@@ -1031,10 +1056,7 @@ impl TileMap {
let tile_set = tile_set
.as_loaded_ref()
.ok_or(TilePropertyError::TileSetNotLoaded)?;
tile_set
.property_value(handle, property_id)
.map(T::try_from)
.unwrap_or_else(|| Ok(T::default()))
tile_set.tile_property_value(handle, property_id)
}
/// The property value for the property of the given name for the tile at the given position in this tile map.
/// This requires that the tile map has a loaded tile set and the tile set contains a property with the given name.
+110 -1
View File
@@ -43,6 +43,71 @@ use std::fmt::{Debug, Display, Formatter};
use super::*;
use tileset::*;
/// Trait for objects that identify a tile set property of a particular type.
pub trait TileSetPropertyId {
/// The type of the values of the property.
type Property: TryFrom<TileSetPropertyValue, Error = TilePropertyError> + Default;
/// The UUID of the property.
fn property_uuid(&self) -> &Uuid;
/// The value of the property at the given cell of the given tile map.
fn get_from_tile_map(
&self,
tile_map: &TileMap,
position: Vector2<i32>,
) -> Result<Self::Property, TilePropertyError> {
tile_map.tile_property_value(position, *self.property_uuid())
}
/// The value of the property at the given handle in the given tile set.
fn get_from_tile_set(
&self,
tile_set: &TileSet,
handle: TileDefinitionHandle,
) -> Result<Self::Property, TilePropertyError> {
tile_set.tile_property_value(handle, *self.property_uuid())
}
}
/// UUID for a property with values of type i32.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Visit, Reflect)]
pub struct TileSetPropertyI32(pub Uuid);
/// UUID for a property with values of type f32.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Visit, Reflect)]
pub struct TileSetPropertyF32(pub Uuid);
/// UUID for a property with values of type ImmutableString.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Visit, Reflect)]
pub struct TileSetPropertyString(pub Uuid);
/// UUID for a property with values of type [`NineI8`].
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Visit, Reflect)]
pub struct TileSetPropertyNine(pub Uuid);
impl TileSetPropertyId for TileSetPropertyI32 {
type Property = i32;
fn property_uuid(&self) -> &Uuid {
&self.0
}
}
impl TileSetPropertyId for TileSetPropertyF32 {
type Property = f32;
fn property_uuid(&self) -> &Uuid {
&self.0
}
}
impl TileSetPropertyId for TileSetPropertyString {
type Property = ImmutableString;
fn property_uuid(&self) -> &Uuid {
&self.0
}
}
impl TileSetPropertyId for TileSetPropertyNine {
type Property = NineI8;
fn property_uuid(&self) -> &Uuid {
&self.0
}
}
/// Since a tile map may require multiple colliders to represent the diverse ways that physics objects may interact with the tiles,
/// tile set data must allow each tile to include multiple values for its collider information.
/// These multiple collider values are associated with their collider objects by a UUID and a name.
@@ -112,6 +177,29 @@ impl Default for NamableValue {
}
}
impl From<NamableValue> for TileSetPropertyValueElement {
fn from(value: NamableValue) -> Self {
match value {
NamableValue::I8(v) => Self::I8(v),
NamableValue::I32(v) => Self::I32(v),
NamableValue::F32(v) => Self::F32(v),
}
}
}
impl TryFrom<TileSetPropertyValueElement> for NamableValue {
type Error = ();
fn try_from(value: TileSetPropertyValueElement) -> Result<Self, ()> {
match value {
TileSetPropertyValueElement::I32(v) => Ok(Self::I32(v)),
TileSetPropertyValueElement::F32(v) => Ok(Self::F32(v)),
TileSetPropertyValueElement::I8(v) => Ok(Self::I8(v)),
TileSetPropertyValueElement::String(_) => Err(()),
}
}
}
impl NamableValue {
/// True if this value corresponds to the given property value.
pub fn matches(&self, other: &TileSetPropertyOptionValue) -> bool {
@@ -337,7 +425,7 @@ impl NineI8 {
/// An element of data stored within a tile's property.
/// For most value types, the element is the whole of the value,
/// but a nine-slice value contains nine elements.
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Visit, Reflect)]
pub enum TileSetPropertyValueElement {
/// Integer property data.
I32(i32),
@@ -361,6 +449,18 @@ impl Default for TileSetPropertyValueElement {
}
}
impl TileSetPropertyValueElement {
/// The type of the data in this element.
pub fn prop_type(&self) -> TileSetPropertyType {
match self {
TileSetPropertyValueElement::I32(_) => TileSetPropertyType::I32,
TileSetPropertyValueElement::F32(_) => TileSetPropertyType::F32,
TileSetPropertyValueElement::String(_) => TileSetPropertyType::String,
TileSetPropertyValueElement::I8(_) => TileSetPropertyType::NineSlice,
}
}
}
impl OrthoTransform for TileSetPropertyValue {
fn x_flipped(self) -> Self {
if let Self::NineSlice(mut v) = self {
@@ -427,6 +527,15 @@ impl TileSetPropertyValue {
}
}
}
/// The type of the data in this value.
pub fn prop_type(&self) -> TileSetPropertyType {
match self {
TileSetPropertyValue::I32(_) => TileSetPropertyType::I32,
TileSetPropertyValue::F32(_) => TileSetPropertyType::F32,
TileSetPropertyValue::String(_) => TileSetPropertyType::String,
TileSetPropertyValue::NineSlice(_) => TileSetPropertyType::NineSlice,
}
}
/// Converts an x,y position into index in 0..9. Both x and y must be within 0..3.
#[inline]
pub fn nine_position_to_index(position: Vector2<usize>) -> usize {
+80 -33
View File
@@ -266,12 +266,14 @@ impl TileRegion {
/// A trait for types that can produce a TileDefinitionHandle upon demand,
/// for use with drawing on tilemaps.
pub trait TileSource {
/// The brush where these tiles were originally taken from.
fn brush(&self) -> Option<&TileMapBrushResource>;
/// The transformation that should be applied to the tiles before they are written.
fn transformation(&self) -> OrthoTransformation;
/// Produce a tile definition handle for the given position. If an area of multiple
/// tiles is being filled, then the given position represents where the tile
/// will go within the area.
fn get_at(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle>;
fn get_at(&self, position: Vector2<i32>) -> Option<StampElement>;
}
/// A trait for types that can produce a TileDefinitionHandle upon demand,
@@ -283,14 +285,17 @@ pub trait BoundedTileSource: TileSource {
/// A tile source that always produces the same tile.
#[derive(Clone, Debug)]
pub struct SingleTileSource(pub OrthoTransformation, pub TileDefinitionHandle);
pub struct SingleTileSource(pub OrthoTransformation, pub StampElement);
impl TileSource for SingleTileSource {
fn brush(&self) -> Option<&TileMapBrushResource> {
None
}
fn transformation(&self) -> OrthoTransformation {
self.0
}
fn get_at(&self, _position: Vector2<i32>) -> Option<TileDefinitionHandle> {
Some(self.1)
fn get_at(&self, _position: Vector2<i32>) -> Option<StampElement> {
Some(self.1.clone())
}
}
@@ -298,11 +303,14 @@ impl TileSource for SingleTileSource {
pub struct RandomTileSource<'a>(pub &'a Stamp);
impl TileSource for RandomTileSource<'_> {
fn brush(&self) -> Option<&TileMapBrushResource> {
self.0.brush()
}
fn transformation(&self) -> OrthoTransformation {
self.0.transformation()
}
fn get_at(&self, _position: Vector2<i32>) -> Option<TileDefinitionHandle> {
self.0.values().choose(&mut thread_rng()).copied()
fn get_at(&self, _position: Vector2<i32>) -> Option<StampElement> {
self.0.values().choose(&mut thread_rng()).cloned()
}
}
@@ -310,10 +318,13 @@ impl TileSource for RandomTileSource<'_> {
pub struct PartialRandomTileSource<'a>(pub &'a Stamp, pub OptionTileRect);
impl TileSource for PartialRandomTileSource<'_> {
fn brush(&self) -> Option<&TileMapBrushResource> {
self.0.brush()
}
fn transformation(&self) -> OrthoTransformation {
self.0.transformation()
}
fn get_at(&self, _position: Vector2<i32>) -> Option<TileDefinitionHandle> {
fn get_at(&self, _position: Vector2<i32>) -> Option<StampElement> {
let pos = self.1.iter().choose(&mut thread_rng())?;
self.0.get_at(pos)
}
@@ -329,10 +340,13 @@ pub struct RepeatTileSource<'a, S> {
}
impl<S: TileSource> TileSource for RepeatTileSource<'_, S> {
fn brush(&self) -> Option<&TileMapBrushResource> {
self.source.brush()
}
fn transformation(&self) -> OrthoTransformation {
self.source.transformation()
}
fn get_at(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle> {
fn get_at(&self, position: Vector2<i32>) -> Option<StampElement> {
let rect = (*self.region.bounds)?;
let rect_pos = rect.position;
let size = rect.size;
@@ -350,14 +364,42 @@ pub struct Tiles(TileGridMap<TileDefinitionHandle>);
/// A set of tiles and a transformation, which represents the tiles that the user has selected
/// to draw with.
#[derive(Clone, Debug, Default, Visit)]
pub struct Stamp(OrthoTransformation, OrthoTransformMap<TileDefinitionHandle>);
pub struct Stamp {
transform: OrthoTransformation,
#[visit(skip)]
elements: OrthoTransformMap<StampElement>,
#[visit(skip)]
brush: Option<TileMapBrushResource>,
}
/// Each cell of a stamp must have a tile handle and it may optionally have
/// the handle of a brush cell where the tile was taken from.
#[derive(Clone, Debug)]
pub struct StampElement {
/// The stamp cell's tile handle
pub handle: TileDefinitionHandle,
/// The brush cell that this stamp element came from.
pub brush_cell: Option<TileDefinitionHandle>,
}
impl From<TileDefinitionHandle> for StampElement {
fn from(handle: TileDefinitionHandle) -> Self {
Self {
handle,
brush_cell: None,
}
}
}
impl TileSource for Tiles {
fn brush(&self) -> Option<&TileMapBrushResource> {
None
}
fn transformation(&self) -> OrthoTransformation {
OrthoTransformation::default()
}
fn get_at(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle> {
self.get(&position).copied()
fn get_at(&self, position: Vector2<i32>) -> Option<StampElement> {
self.get(&position).copied().map(|h| h.into())
}
}
@@ -382,18 +424,21 @@ impl DerefMut for Tiles {
}
impl TileSource for Stamp {
fn transformation(&self) -> OrthoTransformation {
self.0
fn brush(&self) -> Option<&TileMapBrushResource> {
self.brush.as_ref()
}
fn get_at(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle> {
self.1.get(position).copied()
fn transformation(&self) -> OrthoTransformation {
self.transform
}
fn get_at(&self, position: Vector2<i32>) -> Option<StampElement> {
self.elements.get(position).cloned()
}
}
impl Stamp {
/// Iterate over the tile handles of the stamp.
pub fn tile_iter(&self) -> impl Iterator<Item = TileDefinitionHandle> + '_ {
self.1.values().copied()
self.elements.values().map(|s| s.handle)
}
/// Create a repeating tile source from this stamp to repeat from `start` to `end.`
pub fn repeat(&self, start: Vector2<i32>, end: Vector2<i32>) -> RepeatTileSource<Stamp> {
@@ -415,20 +460,22 @@ impl Stamp {
/// True if this stamp contains no tiles.
pub fn is_empty(&self) -> bool {
self.1.is_empty()
self.elements.is_empty()
}
/// Turn this stamp into an empty stamp.
pub fn clear(&mut self) {
self.1.clear();
self.0 = OrthoTransformation::identity();
self.transform = OrthoTransformation::identity();
self.elements.clear();
}
/// Clear this stamp and fill it with the given tiles.
/// The tiles are moved so that their center is (0,0).
/// The transform is set to identity.
pub fn build<I: Iterator<Item = (Vector2<i32>, TileDefinitionHandle)> + Clone>(
pub fn build<I: Iterator<Item = (Vector2<i32>, StampElement)> + Clone>(
&mut self,
brush: Option<TileMapBrushResource>,
source: I,
) {
self.brush = brush;
self.clear();
let mut rect = OptionTileRect::default();
for (p, _) in source.clone() {
@@ -438,42 +485,42 @@ impl Stamp {
return;
};
let center = rect.center();
for (p, h) in source {
self.insert(p - center, h);
for (p, e) in source {
_ = self.insert(p - center, e);
}
}
/// Rotate the stamp by the given number of 90-degree turns.
pub fn rotate(&mut self, amount: i8) {
self.0 = self.0.rotated(amount);
self.1 = std::mem::take(&mut self.1).rotated(amount);
self.transform = self.transform.rotated(amount);
self.elements = std::mem::take(&mut self.elements).rotated(amount);
}
/// Flip along the x axis.
pub fn x_flip(&mut self) {
self.0 = self.0.x_flipped();
self.1 = std::mem::take(&mut self.1).x_flipped();
self.transform = self.transform.x_flipped();
self.elements = std::mem::take(&mut self.elements).x_flipped();
}
/// Flip along the y axis.
pub fn y_flip(&mut self) {
self.0 = self.0.y_flipped();
self.1 = std::mem::take(&mut self.1).y_flipped();
self.transform = self.transform.y_flipped();
self.elements = std::mem::take(&mut self.elements).y_flipped();
}
/// Rotate the stamp by the given number of 90-degree turns.
pub fn transform(&mut self, amount: OrthoTransformation) {
self.0 = self.0.transformed(amount);
self.1 = std::mem::take(&mut self.1).transformed(amount);
self.transform = self.transform.transformed(amount);
self.elements = std::mem::take(&mut self.elements).transformed(amount);
}
}
impl Deref for Stamp {
type Target = OrthoTransformMap<TileDefinitionHandle>;
type Target = OrthoTransformMap<StampElement>;
fn deref(&self) -> &Self::Target {
&self.1
&self.elements
}
}
impl DerefMut for Stamp {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.1
&mut self.elements
}
}
+165 -6
View File
@@ -45,7 +45,7 @@ use crate::{
resource::texture::TextureResource,
};
use std::{
collections::hash_map::{Entry, Keys},
collections::hash_map::{self, Entry, Keys},
error::Error,
fmt::{Display, Formatter},
ops::{Deref, DerefMut},
@@ -748,6 +748,63 @@ impl TileMaterial {
}
}
/// Iterates through the valid handles of a tile set.
#[derive(Default)]
pub struct TileSetHandleIterator<'a> {
pages_iter: Option<hash_map::Iter<'a, Vector2<i32>, TileSetPage>>,
page_position: Vector2<i32>,
handles_iter: TileSetPageHandleIterator<'a>,
}
#[derive(Default)]
enum TileSetPageHandleIterator<'a> {
#[default]
Empty,
Atlas(Keys<'a, Vector2<i32>, TileData>),
Freeform(Keys<'a, Vector2<i32>, TileDefinition>),
}
impl<'a> TileSetHandleIterator<'a> {
fn new(pages_iter: Option<hash_map::Iter<'a, Vector2<i32>, TileSetPage>>) -> Self {
Self {
pages_iter,
page_position: Vector2::default(),
handles_iter: TileSetPageHandleIterator::Empty,
}
}
}
impl Iterator for TileSetPageHandleIterator<'_> {
type Item = Vector2<i32>;
fn next(&mut self) -> Option<Self::Item> {
match self {
TileSetPageHandleIterator::Empty => None,
TileSetPageHandleIterator::Atlas(keys) => keys.next().cloned(),
TileSetPageHandleIterator::Freeform(keys) => keys.next().cloned(),
}
}
}
impl Iterator for TileSetHandleIterator<'_> {
type Item = TileDefinitionHandle;
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(tile) = self.handles_iter.next() {
return TileDefinitionHandle::try_new(self.page_position, tile);
}
let (&page_position, page) = self.pages_iter.as_mut()?.next()?;
self.page_position = page_position;
self.handles_iter = match &page.source {
TileSetPageSource::Atlas(map) => TileSetPageHandleIterator::Atlas(map.keys()),
TileSetPageSource::Freeform(map) => TileSetPageHandleIterator::Freeform(map.keys()),
_ => TileSetPageHandleIterator::Empty,
};
}
}
}
/// Iterates through the positions of the tiles of a single page within a tile set.
pub struct TileSetPaletteIterator<'a> {
keys: PaletteIterator<'a>,
@@ -786,7 +843,7 @@ impl Iterator for TileSetPaletteIterator<'_> {
pub struct TileSetRef<'a>(ResourceDataRef<'a, TileSet>);
/// Maybe a [`TileSet`], maybe not, depending on whether the resource was successfully loaded.
/// If it is not a `TileSet`, its methods pretend it is an empty `TileSet`.
pub struct OptionTileSet<'a>(Option<&'a mut TileSet>);
pub struct OptionTileSet<'a>(pub Option<&'a mut TileSet>);
impl<'a> From<ResourceDataRef<'a, TileSet>> for TileSetRef<'a> {
fn from(value: ResourceDataRef<'a, TileSet>) -> Self {
@@ -807,11 +864,60 @@ impl<'a> TileSetRef<'a> {
}
}
impl<'a> Deref for OptionTileSet<'a> {
type Target = Option<&'a mut TileSet>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> OptionTileSet<'a> {
/// A reference to the underlying `TileSet` if it was successfully loaded.
pub fn as_ref(&'a self) -> Option<&'a TileSet> {
self.0.as_deref()
}
/// Iterate all valid tile handles.
pub fn all_tiles(&self) -> TileSetHandleIterator {
self.as_ref().map(|t| t.all_tiles()).unwrap_or_default()
}
/// The property value for the property of the given name for the tile at the given position in this tile map.
/// If there is no tile data at the given handle or no property at the given UUID,
/// then the default value for the property's value type is returned.
pub fn tile_property_value<T>(
&self,
handle: TileDefinitionHandle,
property_id: Uuid,
) -> Result<T, TilePropertyError>
where
T: TryFrom<TileSetPropertyValue, Error = TilePropertyError> + Default,
{
self.as_ref()
.map(|t| t.tile_property_value(handle, property_id))
.unwrap_or(Err(TilePropertyError::MissingTileSet))
}
/// The property value for the property of the given name for the tile at the given position in this tile map.
/// If there is no tile data at the given handle, then the default value for the property's value type is returned.
pub fn tile_property_value_by_name(
&self,
handle: TileDefinitionHandle,
property_name: &ImmutableString,
) -> Result<TileSetPropertyValue, TilePropertyError> {
self.as_ref()
.map(|t| t.tile_property_value_by_name(handle, property_name))
.unwrap_or(Err(TilePropertyError::MissingTileSet))
}
/// The property value for the property of the given UUID for the tile at the given position in this tile map.
/// If there is no tile data at the given handle, then the default value for the property's value type is returned.
pub fn tile_property_value_by_uuid_untyped(
&self,
handle: TileDefinitionHandle,
property_id: Uuid,
) -> Result<TileSetPropertyValue, TilePropertyError> {
self.as_ref()
.map(|t| t.tile_property_value_by_uuid_untyped(handle, property_id))
.unwrap_or(Err(TilePropertyError::MissingTileSet))
}
/// Slice containing the properties of this tile set, or an empty slice if the tile set is not loaded.
pub fn properties(&self) -> &[TileSetPropertyLayer] {
self.as_ref()
@@ -1235,10 +1341,59 @@ pub struct TileSet {
/// values. Saving is unnecessary whenever this value is 0.
#[reflect(hidden)]
#[visit(skip)]
pub change_count: ChangeFlag,
pub change_flag: ChangeFlag,
}
impl TileSet {
/// Iterate all valid tile handles.
pub fn all_tiles(&self) -> TileSetHandleIterator {
TileSetHandleIterator::new(Some(self.pages.iter()))
}
/// The property value for the property of the given name for the tile at the given position in this tile map.
/// If there is no tile data at the given handle or no property at the given UUID,
/// then the default value for the property's value type is returned.
pub fn tile_property_value<T>(
&self,
handle: TileDefinitionHandle,
property_id: Uuid,
) -> Result<T, TilePropertyError>
where
T: TryFrom<TileSetPropertyValue, Error = TilePropertyError> + Default,
{
self.property_value(handle, property_id)
.map(T::try_from)
.unwrap_or_else(|| Ok(T::default()))
}
/// The property value for the property of the given name for the tile at the given position in this tile map.
/// If there is no tile data at the given handle, then the default value for the property's value type is returned.
pub fn tile_property_value_by_name(
&self,
handle: TileDefinitionHandle,
property_name: &ImmutableString,
) -> Result<TileSetPropertyValue, TilePropertyError> {
let property = self
.find_property_by_name(property_name)
.ok_or_else(|| TilePropertyError::UnrecognizedName(property_name.clone()))?;
Ok(self
.property_value(handle, property.uuid)
.unwrap_or_else(|| property.prop_type.default_value()))
}
/// The property value for the property of the given UUID for the tile at the given position in this tile map.
/// If there is no tile data at the given handle, then the default value for the property's value type is returned.
pub fn tile_property_value_by_uuid_untyped(
&self,
handle: TileDefinitionHandle,
property_id: Uuid,
) -> Result<TileSetPropertyValue, TilePropertyError> {
if let Some(value) = self.property_value(handle, property_id) {
Ok(value)
} else {
let property = self
.find_property(property_id)
.ok_or(TilePropertyError::UnrecognizedUuid(property_id))?;
Ok(property.prop_type.default_value())
}
}
/// The color of the collider layer with the given uuid.
pub fn collider_color(&self, uuid: Uuid) -> Option<Color> {
self.find_collider(uuid).map(|layer| layer.color)
@@ -1669,8 +1824,12 @@ impl TileSet {
pub fn redirect_handle(&self, position: ResourceTilePosition) -> Option<TileDefinitionHandle> {
match position.stage() {
TilePaletteStage::Tiles => match &self.pages.get(&position.page())?.source {
TileSetPageSource::Transform(tiles) => tiles.get_at(position.stage_position()),
TileSetPageSource::Animation(tiles) => tiles.get_at(position.stage_position()),
TileSetPageSource::Transform(tiles) => {
tiles.get(&position.stage_position()).copied()
}
TileSetPageSource::Animation(tiles) => {
tiles.get(&position.stage_position()).copied()
}
page => {
if page.contains_tile_at(position.stage_position()) {
position.handle()
@@ -1692,7 +1851,7 @@ impl TileSet {
handle: TileDefinitionHandle,
) -> Option<TileDefinitionHandle> {
match &self.pages.get(&handle.page())?.source {
TileSetPageSource::Transform(tiles) => tiles.get_at(handle.tile()),
TileSetPageSource::Transform(tiles) => tiles.get(&handle.tile()).copied(),
_ => None,
}
}
+11 -1
View File
@@ -53,7 +53,7 @@ use super::OptionTileRect;
pub struct OrthoTransformation(i8);
/// A map from `Vector2<i32>` to values. It can be transformed to flip and rotate the positions of the values.
#[derive(Default, Clone, Debug, Visit)]
#[derive(Clone, Debug, Visit)]
pub struct OrthoTransformMap<V> {
transform: OrthoTransformation,
map: FxHashMap<Vector2<i32>, V>,
@@ -315,6 +315,16 @@ impl<V> OrthoTransformMap<V> {
self.map.values()
}
}
impl<V> Default for OrthoTransformMap<V> {
fn default() -> Self {
Self {
transform: Default::default(),
map: Default::default(),
}
}
}
impl<V> OrthoTransform for OrthoTransformMap<V> {
fn x_flipped(self) -> Self {
Self {
+143 -23
View File
@@ -44,6 +44,7 @@ use fyrox_core::swap_hash_map_entry;
use std::{
borrow::Cow,
collections::hash_map::Entry,
fmt::Debug,
ops::{Deref, DerefMut},
};
@@ -396,7 +397,11 @@ impl TileSetUpdate {
let Some(handle) = TileDefinitionHandle::try_new(page, *pos) else {
continue;
};
if let Some(def) = value.and_then(|(t, h)| tile_set.get_transformed_definition(t, h)) {
if let Some(def) = value
.as_ref()
.map(|v| v.pair())
.and_then(|(t, h)| tile_set.get_transformed_definition(t, h))
{
self.insert(handle, TileDataUpdate::FreeformTile(def));
} else {
self.insert(handle, TileDataUpdate::Erase);
@@ -413,10 +418,10 @@ impl TileSetUpdate {
let Some(target_handle) = TileDefinitionHandle::try_new(page, *pos) else {
continue;
};
if let Some((trans, handle)) = value {
if let Some((trans, handle)) = value.as_ref().map(|v| v.pair()) {
let handle = tile_set
.get_transformed_version(*trans, *handle)
.unwrap_or(*handle);
.get_transformed_version(trans, handle)
.unwrap_or(handle);
self.insert(target_handle, TileDataUpdate::TransformSet(Some(handle)));
} else {
self.insert(target_handle, TileDataUpdate::TransformSet(None));
@@ -577,7 +582,23 @@ impl TileSetUpdate {
}
}
type RotTileHandle = (OrthoTransformation, TileDefinitionHandle);
/// A stamp element plus the transformation of the [`Stamp`] where the element is taken from,
/// allowing the stamp element itself to be later transformed before it is applied to a tile map,
/// tile set, or brush.
#[derive(Debug, Clone)]
pub struct RotTileHandle {
/// The transformation of the element.
pub transform: OrthoTransformation,
/// The element of the stamp.
pub element: StampElement,
}
impl RotTileHandle {
/// The transformation and the tile handle.
pub fn pair(&self) -> (OrthoTransformation, TileDefinitionHandle) {
(self.transform, self.element.handle)
}
}
/// This is a step in the process of performing an edit to a tile map, brush, or tile set.
/// It provides handles for the tiles to be written and the transformation to apply to those
@@ -585,6 +606,31 @@ type RotTileHandle = (OrthoTransformation, TileDefinitionHandle);
#[derive(Clone, Debug, Default)]
pub struct TransTilesUpdate(TileGridMap<Option<RotTileHandle>>);
/// This is a step in the process of performing an edit to a tile map when macros are involved.
/// It contains the tiles to be written and the positions within the brush that the tiles were
/// taken from. It is constructed from a [`TransTilesUpdate`] by applying the transformation
/// to the tiles. A None indicates that the tile is to be erased.
#[derive(Clone, Default)]
pub struct MacroTilesUpdate(TileGridMap<Option<StampElement>>);
impl Debug for MacroTilesUpdate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("MacroTilesUpdate")?;
for (p, v) in self.0.iter() {
write!(f, " ({:2},{:2})->", p.x, p.y)?;
if let Some(StampElement { handle, brush_cell }) = v {
write!(f, "{handle}")?;
if let Some(cell) = brush_cell {
write!(f, "[{cell}]")?;
}
} else {
f.write_str("Delete")?;
}
}
Ok(())
}
}
/// A set of changes to a set of tiles. A value of None indicates that a tile
/// is being removed from the set. A None indicates that the tile is to be erased.
#[derive(Clone, Debug, Default, PartialEq)]
@@ -604,6 +650,20 @@ impl DerefMut for TilesUpdate {
}
}
impl Deref for MacroTilesUpdate {
type Target = TileGridMap<Option<StampElement>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for MacroTilesUpdate {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Deref for TransTilesUpdate {
type Target = TileGridMap<Option<RotTileHandle>>;
@@ -618,16 +678,40 @@ impl DerefMut for TransTilesUpdate {
}
}
impl MacroTilesUpdate {
/// Construct a TilesUpdate by stripping out the brush cell information.
pub fn build_tiles_update(&self) -> TilesUpdate {
let mut result = TilesUpdate::default();
for (pos, value) in self.iter() {
let handle = value.as_ref().map(|e| e.handle);
result.insert(*pos, handle);
}
result
}
/// Replace the values of a TransTilesUpdate using the identity transformation. This allows an update to be
/// converted back to a `TransTilesUpdates` after macro processing.
pub fn fill_trans_tiles_update(&self, tiles_update: &mut TransTilesUpdate) {
tiles_update.clear();
for (pos, tile) in self.iter() {
let value = tile.as_ref().cloned().map(|element| RotTileHandle {
transform: OrthoTransformation::identity(),
element,
});
tiles_update.insert(*pos, value);
}
}
}
impl TransTilesUpdate {
/// Construct a TilesUpdate by finding the transformed version of each tile
/// in the given tile set.
pub fn build_tiles_update(&self, tile_set: &OptionTileSet) -> TilesUpdate {
let mut result = TilesUpdate::default();
for (pos, value) in self.iter() {
if let Some((trans, handle)) = value {
if let Some((trans, handle)) = value.as_ref().map(|v| v.pair()) {
let handle = tile_set
.get_transformed_version(*trans, *handle)
.unwrap_or(*handle);
.get_transformed_version(trans, handle)
.unwrap_or(handle);
result.insert(*pos, Some(handle));
} else {
result.insert(*pos, None);
@@ -635,6 +719,32 @@ impl TransTilesUpdate {
}
result
}
/// Construct a TilesUpdate by finding the transformed version of each tile
/// in the given tile set.
pub fn fill_macro_tiles_update(
&self,
tile_set: &OptionTileSet,
macro_update: &mut MacroTilesUpdate,
) {
macro_update.clear();
for (pos, value) in self.iter() {
if let Some(RotTileHandle { transform, element }) = value.as_ref() {
let handle = element.handle;
let handle = tile_set
.get_transformed_version(*transform, handle)
.unwrap_or(handle);
macro_update.insert(
*pos,
Some(StampElement {
handle,
brush_cell: element.brush_cell,
}),
);
} else {
macro_update.insert(*pos, None);
}
}
}
/// Fills the given tiles at the given point using tiles from the given source. This method
/// extends tile map when trying to fill at a point that lies outside the bounding rectangle.
/// Keep in mind, that flood fill is only possible either on free cells or on cells with the same
@@ -649,12 +759,15 @@ impl TransTilesUpdate {
let mut bounds = tiles.bounding_rect();
bounds.push(start_point);
let allowed_definition = tiles.get_at(start_point);
let allowed_definition = tiles.get_at(start_point).map(|t| t.handle);
let mut stack = vec![start_point];
while let Some(position) = stack.pop() {
let definition = tiles.get_at(position);
let definition = tiles.get_at(position).map(|t| t.handle);
if definition == allowed_definition && !self.contains_key(&position) {
let value = brush.get_at(position).map(|h| (brush.transformation(), h));
let value = brush.get_at(position).map(|element| RotTileHandle {
transform: brush.transformation(),
element,
});
self.insert(position, value);
// Continue on neighbours.
@@ -674,9 +787,13 @@ impl TransTilesUpdate {
/// Draws the given tiles on the tile map
#[inline]
pub fn draw_tiles(&mut self, origin: Vector2<i32>, brush: &Stamp) {
let trans = brush.transformation();
for (local_position, handle) in brush.iter() {
self.insert(origin + local_position, Some((trans, *handle)));
let transform = brush.transformation();
for (local_position, element) in brush.iter() {
let element = element.clone();
self.insert(
origin + local_position,
Some(RotTileHandle { transform, element }),
);
}
}
/// Erases the tiles under the given brush.
@@ -703,19 +820,19 @@ impl TransTilesUpdate {
}
/// Fills the given rectangle using the given tiles.
fn rect_fill_inner<S: TileSource>(&mut self, region: TileRegion, brush: &S) {
let trans = brush.transformation();
let transform = brush.transformation();
for (target, source) in region.iter() {
if let Some(definition_handle) = brush.get_at(source) {
self.insert(target, Some((trans, definition_handle)));
if let Some(element) = brush.get_at(source) {
self.insert(target, Some(RotTileHandle { transform, element }));
}
}
}
/// Draw a line from a point to point.
pub fn draw_line<S: TileSource>(&mut self, from: Vector2<i32>, to: Vector2<i32>, brush: &S) {
let trans = brush.transformation();
let transform = brush.transformation();
for position in BresenhamLineIter::new(from, to) {
if let Some(random_tile) = brush.get_at(position - from) {
self.insert(position, Some((trans, random_tile)));
if let Some(element) = brush.get_at(position - from) {
self.insert(position, Some(RotTileHandle { transform, element }));
}
}
}
@@ -783,15 +900,18 @@ impl TransTilesUpdate {
let inner_stamp_region = stamp_region.clone().deflate(1, 1);
// Place corners first.
let trans = stamp.transformation();
let transform = stamp.transformation();
for (corner_position, actual_corner_position) in [
(stamp_rect.left_top_corner(), rect.left_top_corner()),
(stamp_rect.right_top_corner(), rect.right_top_corner()),
(stamp_rect.right_bottom_corner(), rect.right_bottom_corner()),
(stamp_rect.left_bottom_corner(), rect.left_bottom_corner()),
] {
if let Some(tile) = stamp.get(corner_position) {
self.insert(actual_corner_position, Some((trans, *tile)));
if let Some(element) = stamp.get(corner_position).cloned() {
self.insert(
actual_corner_position,
Some(RotTileHandle { transform, element }),
);
}
}