mirror of
https://github.com/FyroxEngine/Fyrox.git
synced 2026-10-11 22:50:08 +00:00
Correct source EOL from CRLF to LF, no other changes. (#851)
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# Rust files are text files with Unix-style newlines.
|
||||
*.rs text eol=lf
|
||||
# Other text files have Unix-style newlines.
|
||||
*.shader text eol=lf
|
||||
*.toml text eol=lf
|
||||
*.md text eol=lf
|
||||
*.html text eol=lf
|
||||
*.js text eol=lf
|
||||
*.css text eol=lf
|
||||
*.glsl text eol=lf
|
||||
LICENSE text eol=lf
|
||||
# Image files are binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ttf binary
|
||||
*.bin binary
|
||||
+1023
-1023
File diff suppressed because it is too large
Load Diff
+1033
-1033
File diff suppressed because it is too large
Load Diff
@@ -1,445 +1,445 @@
|
||||
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//! The [`TileEditor`] for a tile's collider layer. This allows the tile set editor
|
||||
//! to display a dropdown list for the various [`TileCollider`] options, and displays
|
||||
//! a text box for editing custom colliders when appropriate.
|
||||
//! See [`TileColliderEditor`] for more information.
|
||||
|
||||
use commands::SetTileSetTilesCommand;
|
||||
use fyrox::{
|
||||
asset::Resource,
|
||||
core::{algebra::Vector2, color::Color, pool::Handle, type_traits::prelude::*},
|
||||
gui::{
|
||||
border::BorderBuilder,
|
||||
brush::Brush,
|
||||
button::{Button, ButtonBuilder, ButtonMessage},
|
||||
decorator::{DecoratorBuilder, DecoratorMessage},
|
||||
dropdown_list::{DropdownListBuilder, DropdownListMessage},
|
||||
formatted_text::WrapMode,
|
||||
grid::{Column, GridBuilder, Row},
|
||||
image::ImageBuilder,
|
||||
message::UiMessage,
|
||||
stack_panel::StackPanelBuilder,
|
||||
text::{TextBuilder, TextMessage},
|
||||
text_box::{TextBoxBuilder, TextCommitMode},
|
||||
utils::make_simple_tooltip,
|
||||
widget::WidgetBuilder,
|
||||
BuildContext, HorizontalAlignment, Thickness, UiNode, UserInterface, VerticalAlignment,
|
||||
},
|
||||
scene::tilemap::{tileset::*, CustomTileCollider, CustomTileColliderResource, TileSetUpdate},
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{send_sync_message, MSG_SYNC_FLAG};
|
||||
|
||||
use super::*;
|
||||
|
||||
const COLLIDER_NAMES: &[&str] = &["None", "Full", "Custom"];
|
||||
|
||||
fn collider_to_index(tile_collider: &TileCollider) -> Option<usize> {
|
||||
match tile_collider {
|
||||
TileCollider::None => Some(0),
|
||||
TileCollider::Rectangle => Some(1),
|
||||
TileCollider::Custom(_) => Some(2),
|
||||
TileCollider::Mesh => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_visibility(ui: &UserInterface, destination: Handle<UiNode>, visible: bool) {
|
||||
ui.send_message(WidgetMessage::visibility(
|
||||
destination,
|
||||
MessageDirection::ToWidget,
|
||||
visible,
|
||||
));
|
||||
}
|
||||
|
||||
fn highlight_tool_button(button: Handle<UiNode>, highlight: bool, ui: &UserInterface) {
|
||||
let decorator = *ui.try_get_of_type::<Button>(button).unwrap().decorator;
|
||||
ui.send_message(DecoratorMessage::select(
|
||||
decorator,
|
||||
MessageDirection::ToWidget,
|
||||
highlight,
|
||||
));
|
||||
}
|
||||
|
||||
pub struct TileColliderEditor {
|
||||
/// The handle for the overall editor.
|
||||
handle: Handle<UiNode>,
|
||||
/// The UUID of the collider layer that we are editing.
|
||||
collider_id: Uuid,
|
||||
/// The current value being edited.
|
||||
value: TileCollider,
|
||||
/// The button which actives this editor as a draw tool
|
||||
/// and allows the user to apply this value to other tiles.
|
||||
draw_button: Handle<UiNode>,
|
||||
/// The button which toggles the visibility of the collider layer.
|
||||
show_button: Handle<UiNode>,
|
||||
/// The widget that shows the color of the collider layer.
|
||||
color_icon: Handle<UiNode>,
|
||||
/// The widget for the name of the collider layer.
|
||||
name_field: Handle<UiNode>,
|
||||
/// The dropdown list of collider types.
|
||||
list: Handle<UiNode>,
|
||||
/// The textbox for editing a custom collider.
|
||||
custom_field: Handle<UiNode>,
|
||||
/// A text widget for showing an error in the custom collider text.
|
||||
error_field: Handle<UiNode>,
|
||||
/// True if the custom collider text actually has an error.
|
||||
has_error: bool,
|
||||
}
|
||||
|
||||
pub fn make_list_option(ctx: &mut BuildContext, name: &str) -> Handle<UiNode> {
|
||||
let text = TextBuilder::new(WidgetBuilder::new())
|
||||
.with_vertical_text_alignment(VerticalAlignment::Center)
|
||||
.with_horizontal_text_alignment(HorizontalAlignment::Left)
|
||||
.with_text(name)
|
||||
.build(ctx);
|
||||
DecoratorBuilder::new(
|
||||
BorderBuilder::new(WidgetBuilder::new().with_child(text))
|
||||
.with_corner_radius((4.0).into())
|
||||
.with_pad_by_corner_radius(false),
|
||||
)
|
||||
.build(ctx)
|
||||
}
|
||||
|
||||
fn build_list(ctx: &mut BuildContext) -> Vec<Handle<UiNode>> {
|
||||
COLLIDER_NAMES
|
||||
.iter()
|
||||
.map(|name| make_list_option(ctx, name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl TileColliderEditor {
|
||||
pub fn new(
|
||||
collider_layer: &TileSetColliderLayer,
|
||||
value: TileCollider,
|
||||
ctx: &mut BuildContext,
|
||||
) -> Self {
|
||||
let color_icon = BorderBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.on_column(2)
|
||||
.with_background(Brush::Solid(collider_layer.color.to_opaque()).into()),
|
||||
)
|
||||
.build(ctx);
|
||||
let draw_button = make_draw_button(Some(0), ctx);
|
||||
let show_button = make_show_button(Some(1), ctx);
|
||||
let name_field = TextBuilder::new(WidgetBuilder::new().on_column(3))
|
||||
.with_text(collider_layer.name.clone())
|
||||
.build(ctx);
|
||||
let custom_field = TextBoxBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_visibility(value.is_custom())
|
||||
.with_min_size(Vector2::new(0.0, 100.0)),
|
||||
)
|
||||
.with_multiline(true)
|
||||
.with_wrap(WrapMode::Word)
|
||||
.with_text_commit_mode(TextCommitMode::Changed)
|
||||
.build(ctx);
|
||||
let error_field = TextBuilder::new(WidgetBuilder::new().with_visibility(false))
|
||||
.with_wrap(WrapMode::Word)
|
||||
.with_horizontal_text_alignment(HorizontalAlignment::Center)
|
||||
.build(ctx);
|
||||
let index = collider_to_index(&value).unwrap_or_default();
|
||||
let list = DropdownListBuilder::new(WidgetBuilder::new().on_column(4))
|
||||
.with_items(build_list(ctx))
|
||||
.with_selected(index)
|
||||
.build(ctx);
|
||||
let grid = GridBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(draw_button)
|
||||
.with_child(show_button)
|
||||
.with_child(color_icon)
|
||||
.with_child(name_field)
|
||||
.with_child(list),
|
||||
)
|
||||
.add_column(Column::auto())
|
||||
.add_column(Column::auto())
|
||||
.add_column(Column::strict(24.0))
|
||||
.add_column(Column::strict(120.0))
|
||||
.add_column(Column::stretch())
|
||||
.add_row(Row::auto())
|
||||
.build(ctx);
|
||||
let handle = StackPanelBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(grid)
|
||||
.with_child(custom_field)
|
||||
.with_child(error_field),
|
||||
)
|
||||
.build(ctx);
|
||||
Self {
|
||||
handle,
|
||||
collider_id: collider_layer.uuid,
|
||||
value,
|
||||
draw_button,
|
||||
show_button,
|
||||
color_icon,
|
||||
name_field,
|
||||
custom_field,
|
||||
error_field,
|
||||
list,
|
||||
has_error: false,
|
||||
}
|
||||
}
|
||||
fn apply_collider_update(&mut self, state: &TileEditorState, ui: &mut UserInterface) {
|
||||
let layer = state.find_collider(self.collider_id).unwrap();
|
||||
ui.send_message(TextMessage::text(
|
||||
self.name_field,
|
||||
MessageDirection::ToWidget,
|
||||
layer.name.to_string(),
|
||||
));
|
||||
ui.send_message(WidgetMessage::background(
|
||||
self.color_icon,
|
||||
MessageDirection::ToWidget,
|
||||
Brush::Solid(layer.color.to_opaque()).into(),
|
||||
));
|
||||
}
|
||||
fn find_value(&self, state: &TileEditorState) -> Option<TileCollider> {
|
||||
let mut iter = state.tile_data().map(|(_, d)| {
|
||||
d.colliders
|
||||
.get(&self.collider_id)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let value = iter.next()?;
|
||||
if iter.all(|v| v == value) {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn sync_value_to_list(&self, ui: &mut UserInterface) {
|
||||
let index = collider_to_index(&self.value);
|
||||
send_sync_message(
|
||||
ui,
|
||||
DropdownListMessage::selection(self.list, MessageDirection::ToWidget, index),
|
||||
);
|
||||
send_visibility(ui, self.custom_field, self.value.is_custom());
|
||||
send_visibility(
|
||||
ui,
|
||||
self.error_field,
|
||||
self.value.is_custom() && self.has_error,
|
||||
);
|
||||
}
|
||||
fn build_custom_collider(
|
||||
&mut self,
|
||||
source: &str,
|
||||
ui: &mut UserInterface,
|
||||
) -> Option<CustomTileColliderResource> {
|
||||
match CustomTileCollider::from_str(source) {
|
||||
Ok(collider) => {
|
||||
self.has_error = false;
|
||||
send_visibility(ui, self.error_field, false);
|
||||
Some(Resource::new_embedded(collider))
|
||||
}
|
||||
Err(e) => {
|
||||
self.has_error = true;
|
||||
send_visibility(ui, self.error_field, true);
|
||||
ui.send_message(TextMessage::text(
|
||||
self.error_field,
|
||||
MessageDirection::ToWidget,
|
||||
e.to_string(),
|
||||
));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
fn build_empty_collider(&mut self, ui: &mut UserInterface) -> CustomTileColliderResource {
|
||||
send_visibility(ui, self.error_field, false);
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(self.custom_field, MessageDirection::ToWidget, "".into()),
|
||||
);
|
||||
Resource::new_embedded(CustomTileCollider::default())
|
||||
}
|
||||
fn send_value(&self, state: &TileEditorState, sender: &MessageSender, tile_book: &TileBook) {
|
||||
let Some(tile_set) = tile_book.tile_set_ref().cloned() else {
|
||||
return;
|
||||
};
|
||||
let Some(page) = state.page() else {
|
||||
return;
|
||||
};
|
||||
let mut update = TileSetUpdate::default();
|
||||
for position in state.selected_positions() {
|
||||
update.set_collider(
|
||||
page,
|
||||
position,
|
||||
std::iter::once(self.collider_id),
|
||||
&self.value,
|
||||
);
|
||||
}
|
||||
sender.do_command(SetTileSetTilesCommand {
|
||||
tile_set,
|
||||
tiles: update,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl TileEditor for TileColliderEditor {
|
||||
fn handle(&self) -> Handle<UiNode> {
|
||||
self.handle
|
||||
}
|
||||
|
||||
fn draw_button(&self) -> Handle<UiNode> {
|
||||
self.draw_button
|
||||
}
|
||||
|
||||
fn sync_to_model(&mut self, state: &TileEditorState, ui: &mut UserInterface) {
|
||||
self.apply_collider_update(state, ui);
|
||||
}
|
||||
|
||||
fn sync_to_state(&mut self, state: &TileEditorState, ui: &mut UserInterface) {
|
||||
self.has_error = false;
|
||||
self.value = self.find_value(state).unwrap_or_default();
|
||||
self.sync_value_to_list(ui);
|
||||
send_visibility(ui, self.custom_field, self.value.is_custom());
|
||||
send_visibility(ui, self.error_field, false);
|
||||
if let TileCollider::Custom(custom) = &self.value {
|
||||
let text = custom.data_ref().to_string();
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(self.custom_field, MessageDirection::ToWidget, text),
|
||||
);
|
||||
}
|
||||
highlight_tool_button(
|
||||
self.show_button,
|
||||
state.is_visible_collider(self.collider_id),
|
||||
ui,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_tile(
|
||||
&self,
|
||||
handle: TileDefinitionHandle,
|
||||
_subposition: Vector2<usize>,
|
||||
state: &TileDrawState,
|
||||
update: &mut TileSetUpdate,
|
||||
_tile_resource: &TileBook,
|
||||
) {
|
||||
update.set_collider(
|
||||
handle.page(),
|
||||
handle.tile(),
|
||||
state.visible_colliders.iter().copied(),
|
||||
&self.value,
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_ui_message(
|
||||
&mut self,
|
||||
state: &mut TileEditorState,
|
||||
message: &UiMessage,
|
||||
ui: &mut UserInterface,
|
||||
tile_book: &TileBook,
|
||||
sender: &MessageSender,
|
||||
) {
|
||||
if message.flags == MSG_SYNC_FLAG || message.direction() == MessageDirection::ToWidget {
|
||||
return;
|
||||
}
|
||||
if let Some(ButtonMessage::Click) = message.data() {
|
||||
if message.destination() == self.show_button {
|
||||
let visible = state.is_visible_collider(self.collider_id);
|
||||
state.set_visible_collider(self.collider_id, !visible);
|
||||
}
|
||||
} else if let Some(DropdownListMessage::SelectionChanged(Some(index))) = message.data() {
|
||||
if message.destination() == self.list {
|
||||
self.value = match *index {
|
||||
1 => TileCollider::Rectangle,
|
||||
2 => TileCollider::Custom(self.build_empty_collider(ui)),
|
||||
_ => TileCollider::None,
|
||||
};
|
||||
send_visibility(ui, self.custom_field, self.value.is_custom());
|
||||
self.send_value(state, sender, tile_book);
|
||||
}
|
||||
} else if let Some(TextMessage::Text(text)) = message.data() {
|
||||
if message.destination() == self.custom_field {
|
||||
if let Some(value) = self.build_custom_collider(text, ui) {
|
||||
self.value = TileCollider::Custom(value);
|
||||
self.send_value(state, sender, tile_book);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DRAW_BUTTON_WIDTH: f32 = 14.0;
|
||||
const DRAW_BUTTON_HEIGHT: f32 = 14.0;
|
||||
|
||||
fn make_button(
|
||||
tab_index: Option<usize>,
|
||||
column: usize,
|
||||
tooltip: &str,
|
||||
icon: Option<TextureResource>,
|
||||
ctx: &mut BuildContext,
|
||||
) -> Handle<UiNode> {
|
||||
ButtonBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_tab_index(tab_index)
|
||||
.on_column(column)
|
||||
.with_tooltip(make_simple_tooltip(ctx, tooltip))
|
||||
.with_margin(Thickness::uniform(1.0)),
|
||||
)
|
||||
.with_back(
|
||||
DecoratorBuilder::new(
|
||||
BorderBuilder::new(
|
||||
WidgetBuilder::new().with_foreground(ctx.style.property(Style::BRUSH_DARKER)),
|
||||
)
|
||||
.with_pad_by_corner_radius(false)
|
||||
.with_corner_radius((4.0).into())
|
||||
.with_stroke_thickness(Thickness::uniform(1.0).into()),
|
||||
)
|
||||
.with_selected_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
|
||||
.with_normal_brush(ctx.style.property(Style::BRUSH_LIGHT))
|
||||
.with_hover_brush(ctx.style.property(Style::BRUSH_LIGHTER))
|
||||
.with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
|
||||
.build(ctx),
|
||||
)
|
||||
.with_content(
|
||||
ImageBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_background(Brush::Solid(Color::opaque(180, 180, 180)).into())
|
||||
.with_margin(Thickness::uniform(2.0))
|
||||
.with_width(DRAW_BUTTON_WIDTH)
|
||||
.with_height(DRAW_BUTTON_HEIGHT),
|
||||
)
|
||||
.with_opt_texture(icon)
|
||||
.build(ctx),
|
||||
)
|
||||
.build(ctx)
|
||||
}
|
||||
|
||||
fn make_draw_button(tab_index: Option<usize>, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
make_button(
|
||||
tab_index,
|
||||
0,
|
||||
"Apply collider to tiles",
|
||||
BRUSH_IMAGE.clone(),
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
|
||||
fn make_show_button(tab_index: Option<usize>, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
make_button(
|
||||
tab_index,
|
||||
1,
|
||||
"Make collider visible",
|
||||
VISIBLE_IMAGE.clone(),
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//! The [`TileEditor`] for a tile's collider layer. This allows the tile set editor
|
||||
//! to display a dropdown list for the various [`TileCollider`] options, and displays
|
||||
//! a text box for editing custom colliders when appropriate.
|
||||
//! See [`TileColliderEditor`] for more information.
|
||||
|
||||
use commands::SetTileSetTilesCommand;
|
||||
use fyrox::{
|
||||
asset::Resource,
|
||||
core::{algebra::Vector2, color::Color, pool::Handle, type_traits::prelude::*},
|
||||
gui::{
|
||||
border::BorderBuilder,
|
||||
brush::Brush,
|
||||
button::{Button, ButtonBuilder, ButtonMessage},
|
||||
decorator::{DecoratorBuilder, DecoratorMessage},
|
||||
dropdown_list::{DropdownListBuilder, DropdownListMessage},
|
||||
formatted_text::WrapMode,
|
||||
grid::{Column, GridBuilder, Row},
|
||||
image::ImageBuilder,
|
||||
message::UiMessage,
|
||||
stack_panel::StackPanelBuilder,
|
||||
text::{TextBuilder, TextMessage},
|
||||
text_box::{TextBoxBuilder, TextCommitMode},
|
||||
utils::make_simple_tooltip,
|
||||
widget::WidgetBuilder,
|
||||
BuildContext, HorizontalAlignment, Thickness, UiNode, UserInterface, VerticalAlignment,
|
||||
},
|
||||
scene::tilemap::{tileset::*, CustomTileCollider, CustomTileColliderResource, TileSetUpdate},
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{send_sync_message, MSG_SYNC_FLAG};
|
||||
|
||||
use super::*;
|
||||
|
||||
const COLLIDER_NAMES: &[&str] = &["None", "Full", "Custom"];
|
||||
|
||||
fn collider_to_index(tile_collider: &TileCollider) -> Option<usize> {
|
||||
match tile_collider {
|
||||
TileCollider::None => Some(0),
|
||||
TileCollider::Rectangle => Some(1),
|
||||
TileCollider::Custom(_) => Some(2),
|
||||
TileCollider::Mesh => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_visibility(ui: &UserInterface, destination: Handle<UiNode>, visible: bool) {
|
||||
ui.send_message(WidgetMessage::visibility(
|
||||
destination,
|
||||
MessageDirection::ToWidget,
|
||||
visible,
|
||||
));
|
||||
}
|
||||
|
||||
fn highlight_tool_button(button: Handle<UiNode>, highlight: bool, ui: &UserInterface) {
|
||||
let decorator = *ui.try_get_of_type::<Button>(button).unwrap().decorator;
|
||||
ui.send_message(DecoratorMessage::select(
|
||||
decorator,
|
||||
MessageDirection::ToWidget,
|
||||
highlight,
|
||||
));
|
||||
}
|
||||
|
||||
pub struct TileColliderEditor {
|
||||
/// The handle for the overall editor.
|
||||
handle: Handle<UiNode>,
|
||||
/// The UUID of the collider layer that we are editing.
|
||||
collider_id: Uuid,
|
||||
/// The current value being edited.
|
||||
value: TileCollider,
|
||||
/// The button which actives this editor as a draw tool
|
||||
/// and allows the user to apply this value to other tiles.
|
||||
draw_button: Handle<UiNode>,
|
||||
/// The button which toggles the visibility of the collider layer.
|
||||
show_button: Handle<UiNode>,
|
||||
/// The widget that shows the color of the collider layer.
|
||||
color_icon: Handle<UiNode>,
|
||||
/// The widget for the name of the collider layer.
|
||||
name_field: Handle<UiNode>,
|
||||
/// The dropdown list of collider types.
|
||||
list: Handle<UiNode>,
|
||||
/// The textbox for editing a custom collider.
|
||||
custom_field: Handle<UiNode>,
|
||||
/// A text widget for showing an error in the custom collider text.
|
||||
error_field: Handle<UiNode>,
|
||||
/// True if the custom collider text actually has an error.
|
||||
has_error: bool,
|
||||
}
|
||||
|
||||
pub fn make_list_option(ctx: &mut BuildContext, name: &str) -> Handle<UiNode> {
|
||||
let text = TextBuilder::new(WidgetBuilder::new())
|
||||
.with_vertical_text_alignment(VerticalAlignment::Center)
|
||||
.with_horizontal_text_alignment(HorizontalAlignment::Left)
|
||||
.with_text(name)
|
||||
.build(ctx);
|
||||
DecoratorBuilder::new(
|
||||
BorderBuilder::new(WidgetBuilder::new().with_child(text))
|
||||
.with_corner_radius((4.0).into())
|
||||
.with_pad_by_corner_radius(false),
|
||||
)
|
||||
.build(ctx)
|
||||
}
|
||||
|
||||
fn build_list(ctx: &mut BuildContext) -> Vec<Handle<UiNode>> {
|
||||
COLLIDER_NAMES
|
||||
.iter()
|
||||
.map(|name| make_list_option(ctx, name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl TileColliderEditor {
|
||||
pub fn new(
|
||||
collider_layer: &TileSetColliderLayer,
|
||||
value: TileCollider,
|
||||
ctx: &mut BuildContext,
|
||||
) -> Self {
|
||||
let color_icon = BorderBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.on_column(2)
|
||||
.with_background(Brush::Solid(collider_layer.color.to_opaque()).into()),
|
||||
)
|
||||
.build(ctx);
|
||||
let draw_button = make_draw_button(Some(0), ctx);
|
||||
let show_button = make_show_button(Some(1), ctx);
|
||||
let name_field = TextBuilder::new(WidgetBuilder::new().on_column(3))
|
||||
.with_text(collider_layer.name.clone())
|
||||
.build(ctx);
|
||||
let custom_field = TextBoxBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_visibility(value.is_custom())
|
||||
.with_min_size(Vector2::new(0.0, 100.0)),
|
||||
)
|
||||
.with_multiline(true)
|
||||
.with_wrap(WrapMode::Word)
|
||||
.with_text_commit_mode(TextCommitMode::Changed)
|
||||
.build(ctx);
|
||||
let error_field = TextBuilder::new(WidgetBuilder::new().with_visibility(false))
|
||||
.with_wrap(WrapMode::Word)
|
||||
.with_horizontal_text_alignment(HorizontalAlignment::Center)
|
||||
.build(ctx);
|
||||
let index = collider_to_index(&value).unwrap_or_default();
|
||||
let list = DropdownListBuilder::new(WidgetBuilder::new().on_column(4))
|
||||
.with_items(build_list(ctx))
|
||||
.with_selected(index)
|
||||
.build(ctx);
|
||||
let grid = GridBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(draw_button)
|
||||
.with_child(show_button)
|
||||
.with_child(color_icon)
|
||||
.with_child(name_field)
|
||||
.with_child(list),
|
||||
)
|
||||
.add_column(Column::auto())
|
||||
.add_column(Column::auto())
|
||||
.add_column(Column::strict(24.0))
|
||||
.add_column(Column::strict(120.0))
|
||||
.add_column(Column::stretch())
|
||||
.add_row(Row::auto())
|
||||
.build(ctx);
|
||||
let handle = StackPanelBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(grid)
|
||||
.with_child(custom_field)
|
||||
.with_child(error_field),
|
||||
)
|
||||
.build(ctx);
|
||||
Self {
|
||||
handle,
|
||||
collider_id: collider_layer.uuid,
|
||||
value,
|
||||
draw_button,
|
||||
show_button,
|
||||
color_icon,
|
||||
name_field,
|
||||
custom_field,
|
||||
error_field,
|
||||
list,
|
||||
has_error: false,
|
||||
}
|
||||
}
|
||||
fn apply_collider_update(&mut self, state: &TileEditorState, ui: &mut UserInterface) {
|
||||
let layer = state.find_collider(self.collider_id).unwrap();
|
||||
ui.send_message(TextMessage::text(
|
||||
self.name_field,
|
||||
MessageDirection::ToWidget,
|
||||
layer.name.to_string(),
|
||||
));
|
||||
ui.send_message(WidgetMessage::background(
|
||||
self.color_icon,
|
||||
MessageDirection::ToWidget,
|
||||
Brush::Solid(layer.color.to_opaque()).into(),
|
||||
));
|
||||
}
|
||||
fn find_value(&self, state: &TileEditorState) -> Option<TileCollider> {
|
||||
let mut iter = state.tile_data().map(|(_, d)| {
|
||||
d.colliders
|
||||
.get(&self.collider_id)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let value = iter.next()?;
|
||||
if iter.all(|v| v == value) {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn sync_value_to_list(&self, ui: &mut UserInterface) {
|
||||
let index = collider_to_index(&self.value);
|
||||
send_sync_message(
|
||||
ui,
|
||||
DropdownListMessage::selection(self.list, MessageDirection::ToWidget, index),
|
||||
);
|
||||
send_visibility(ui, self.custom_field, self.value.is_custom());
|
||||
send_visibility(
|
||||
ui,
|
||||
self.error_field,
|
||||
self.value.is_custom() && self.has_error,
|
||||
);
|
||||
}
|
||||
fn build_custom_collider(
|
||||
&mut self,
|
||||
source: &str,
|
||||
ui: &mut UserInterface,
|
||||
) -> Option<CustomTileColliderResource> {
|
||||
match CustomTileCollider::from_str(source) {
|
||||
Ok(collider) => {
|
||||
self.has_error = false;
|
||||
send_visibility(ui, self.error_field, false);
|
||||
Some(Resource::new_embedded(collider))
|
||||
}
|
||||
Err(e) => {
|
||||
self.has_error = true;
|
||||
send_visibility(ui, self.error_field, true);
|
||||
ui.send_message(TextMessage::text(
|
||||
self.error_field,
|
||||
MessageDirection::ToWidget,
|
||||
e.to_string(),
|
||||
));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
fn build_empty_collider(&mut self, ui: &mut UserInterface) -> CustomTileColliderResource {
|
||||
send_visibility(ui, self.error_field, false);
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(self.custom_field, MessageDirection::ToWidget, "".into()),
|
||||
);
|
||||
Resource::new_embedded(CustomTileCollider::default())
|
||||
}
|
||||
fn send_value(&self, state: &TileEditorState, sender: &MessageSender, tile_book: &TileBook) {
|
||||
let Some(tile_set) = tile_book.tile_set_ref().cloned() else {
|
||||
return;
|
||||
};
|
||||
let Some(page) = state.page() else {
|
||||
return;
|
||||
};
|
||||
let mut update = TileSetUpdate::default();
|
||||
for position in state.selected_positions() {
|
||||
update.set_collider(
|
||||
page,
|
||||
position,
|
||||
std::iter::once(self.collider_id),
|
||||
&self.value,
|
||||
);
|
||||
}
|
||||
sender.do_command(SetTileSetTilesCommand {
|
||||
tile_set,
|
||||
tiles: update,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl TileEditor for TileColliderEditor {
|
||||
fn handle(&self) -> Handle<UiNode> {
|
||||
self.handle
|
||||
}
|
||||
|
||||
fn draw_button(&self) -> Handle<UiNode> {
|
||||
self.draw_button
|
||||
}
|
||||
|
||||
fn sync_to_model(&mut self, state: &TileEditorState, ui: &mut UserInterface) {
|
||||
self.apply_collider_update(state, ui);
|
||||
}
|
||||
|
||||
fn sync_to_state(&mut self, state: &TileEditorState, ui: &mut UserInterface) {
|
||||
self.has_error = false;
|
||||
self.value = self.find_value(state).unwrap_or_default();
|
||||
self.sync_value_to_list(ui);
|
||||
send_visibility(ui, self.custom_field, self.value.is_custom());
|
||||
send_visibility(ui, self.error_field, false);
|
||||
if let TileCollider::Custom(custom) = &self.value {
|
||||
let text = custom.data_ref().to_string();
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(self.custom_field, MessageDirection::ToWidget, text),
|
||||
);
|
||||
}
|
||||
highlight_tool_button(
|
||||
self.show_button,
|
||||
state.is_visible_collider(self.collider_id),
|
||||
ui,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_tile(
|
||||
&self,
|
||||
handle: TileDefinitionHandle,
|
||||
_subposition: Vector2<usize>,
|
||||
state: &TileDrawState,
|
||||
update: &mut TileSetUpdate,
|
||||
_tile_resource: &TileBook,
|
||||
) {
|
||||
update.set_collider(
|
||||
handle.page(),
|
||||
handle.tile(),
|
||||
state.visible_colliders.iter().copied(),
|
||||
&self.value,
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_ui_message(
|
||||
&mut self,
|
||||
state: &mut TileEditorState,
|
||||
message: &UiMessage,
|
||||
ui: &mut UserInterface,
|
||||
tile_book: &TileBook,
|
||||
sender: &MessageSender,
|
||||
) {
|
||||
if message.flags == MSG_SYNC_FLAG || message.direction() == MessageDirection::ToWidget {
|
||||
return;
|
||||
}
|
||||
if let Some(ButtonMessage::Click) = message.data() {
|
||||
if message.destination() == self.show_button {
|
||||
let visible = state.is_visible_collider(self.collider_id);
|
||||
state.set_visible_collider(self.collider_id, !visible);
|
||||
}
|
||||
} else if let Some(DropdownListMessage::SelectionChanged(Some(index))) = message.data() {
|
||||
if message.destination() == self.list {
|
||||
self.value = match *index {
|
||||
1 => TileCollider::Rectangle,
|
||||
2 => TileCollider::Custom(self.build_empty_collider(ui)),
|
||||
_ => TileCollider::None,
|
||||
};
|
||||
send_visibility(ui, self.custom_field, self.value.is_custom());
|
||||
self.send_value(state, sender, tile_book);
|
||||
}
|
||||
} else if let Some(TextMessage::Text(text)) = message.data() {
|
||||
if message.destination() == self.custom_field {
|
||||
if let Some(value) = self.build_custom_collider(text, ui) {
|
||||
self.value = TileCollider::Custom(value);
|
||||
self.send_value(state, sender, tile_book);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DRAW_BUTTON_WIDTH: f32 = 14.0;
|
||||
const DRAW_BUTTON_HEIGHT: f32 = 14.0;
|
||||
|
||||
fn make_button(
|
||||
tab_index: Option<usize>,
|
||||
column: usize,
|
||||
tooltip: &str,
|
||||
icon: Option<TextureResource>,
|
||||
ctx: &mut BuildContext,
|
||||
) -> Handle<UiNode> {
|
||||
ButtonBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_tab_index(tab_index)
|
||||
.on_column(column)
|
||||
.with_tooltip(make_simple_tooltip(ctx, tooltip))
|
||||
.with_margin(Thickness::uniform(1.0)),
|
||||
)
|
||||
.with_back(
|
||||
DecoratorBuilder::new(
|
||||
BorderBuilder::new(
|
||||
WidgetBuilder::new().with_foreground(ctx.style.property(Style::BRUSH_DARKER)),
|
||||
)
|
||||
.with_pad_by_corner_radius(false)
|
||||
.with_corner_radius((4.0).into())
|
||||
.with_stroke_thickness(Thickness::uniform(1.0).into()),
|
||||
)
|
||||
.with_selected_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
|
||||
.with_normal_brush(ctx.style.property(Style::BRUSH_LIGHT))
|
||||
.with_hover_brush(ctx.style.property(Style::BRUSH_LIGHTER))
|
||||
.with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
|
||||
.build(ctx),
|
||||
)
|
||||
.with_content(
|
||||
ImageBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_background(Brush::Solid(Color::opaque(180, 180, 180)).into())
|
||||
.with_margin(Thickness::uniform(2.0))
|
||||
.with_width(DRAW_BUTTON_WIDTH)
|
||||
.with_height(DRAW_BUTTON_HEIGHT),
|
||||
)
|
||||
.with_opt_texture(icon)
|
||||
.build(ctx),
|
||||
)
|
||||
.build(ctx)
|
||||
}
|
||||
|
||||
fn make_draw_button(tab_index: Option<usize>, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
make_button(
|
||||
tab_index,
|
||||
0,
|
||||
"Apply collider to tiles",
|
||||
BRUSH_IMAGE.clone(),
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
|
||||
fn make_show_button(tab_index: Option<usize>, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
make_button(
|
||||
tab_index,
|
||||
1,
|
||||
"Make collider visible",
|
||||
VISIBLE_IMAGE.clone(),
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,459 +1,459 @@
|
||||
// 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 fyrox::{
|
||||
fxhash::FxHashMap,
|
||||
gui::{
|
||||
button::ButtonMessage,
|
||||
color::{ColorFieldBuilder, ColorFieldMessage},
|
||||
grid::*,
|
||||
list_view::{ListView, ListViewBuilder, ListViewMessage},
|
||||
scroll_viewer::ScrollViewerBuilder,
|
||||
text::{TextBuilder, TextMessage},
|
||||
text_box::{TextBoxBuilder, TextCommitMode},
|
||||
utils::{make_arrow, ArrowDirection},
|
||||
HorizontalAlignment, VerticalAlignment,
|
||||
},
|
||||
};
|
||||
|
||||
use fyrox::scene::tilemap::{tileset::*, *};
|
||||
|
||||
use crate::{send_sync_message, MSG_SYNC_FLAG};
|
||||
|
||||
use super::*;
|
||||
use commands::*;
|
||||
|
||||
/// 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.
|
||||
pub struct CollidersTab {
|
||||
handle: Handle<UiNode>,
|
||||
list: Handle<UiNode>,
|
||||
up_button: Handle<UiNode>,
|
||||
down_button: Handle<UiNode>,
|
||||
remove_button: Handle<UiNode>,
|
||||
add_button: Handle<UiNode>,
|
||||
data_panel: Handle<UiNode>,
|
||||
name_field: Handle<UiNode>,
|
||||
color_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, collider: &TileSetColliderLayer) -> Handle<UiNode> {
|
||||
let content = GridBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(
|
||||
BorderBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_horizontal_alignment(HorizontalAlignment::Center)
|
||||
.with_vertical_alignment(VerticalAlignment::Center)
|
||||
.with_width(16.0)
|
||||
.with_height(16.0)
|
||||
.with_background(Brush::Solid(collider.color).into()),
|
||||
)
|
||||
.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(collider.name.clone())
|
||||
.build(ctx),
|
||||
),
|
||||
)
|
||||
.add_row(Row::auto())
|
||||
.add_column(Column::strict(20.0))
|
||||
.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_items(ctx: &mut BuildContext, tile_set: &OptionTileSet) -> Vec<Handle<UiNode>> {
|
||||
tile_set
|
||||
.colliders()
|
||||
.iter()
|
||||
.map(|c| make_list_item(ctx, c))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl CollidersTab {
|
||||
pub fn new(tile_book: TileBook, ctx: &mut BuildContext) -> Self {
|
||||
let items = if let TileBook::TileSet(t) = &tile_book {
|
||||
make_items(ctx, &TileSetRef::new(t).as_loaded())
|
||||
} else {
|
||||
Vec::default()
|
||||
};
|
||||
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 add_button = make_button("New", "Create a new collider.", ctx, 2, 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)
|
||||
.with_child(add_button),
|
||||
)
|
||||
.add_row(Row::auto())
|
||||
.add_column(Column::stretch())
|
||||
.add_column(Column::stretch())
|
||||
.add_column(Column::stretch())
|
||||
.build(ctx);
|
||||
let left_label = TextBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_horizontal_alignment(HorizontalAlignment::Center)
|
||||
.with_margin(Thickness::uniform(2.0)),
|
||||
)
|
||||
.with_text("Colliders:")
|
||||
.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),
|
||||
)
|
||||
.add_row(Row::auto())
|
||||
.add_row(Row::stretch())
|
||||
.add_row(Row::strict(30.0))
|
||||
.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 color_field = ColorFieldBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_margin(Thickness::uniform(2.0))
|
||||
.on_row(1)
|
||||
.with_height(30.0),
|
||||
)
|
||||
.build(ctx);
|
||||
let data_panel = GridBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_margin(Thickness::uniform(2.0))
|
||||
.on_column(1)
|
||||
.with_child(header)
|
||||
.with_child(color_field),
|
||||
)
|
||||
.add_row(Row::auto())
|
||||
.add_row(Row::auto())
|
||||
.add_column(Column::stretch())
|
||||
.build(ctx);
|
||||
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),
|
||||
list,
|
||||
up_button,
|
||||
down_button,
|
||||
add_button,
|
||||
remove_button,
|
||||
data_panel,
|
||||
name_field,
|
||||
color_field,
|
||||
}
|
||||
}
|
||||
pub fn handle(&self) -> Handle<UiNode> {
|
||||
self.handle
|
||||
}
|
||||
pub fn sync_to_model(&mut self, tile_set: &OptionTileSet, ui: &mut UserInterface) {
|
||||
let items = make_items(&mut ui.build_ctx(), tile_set);
|
||||
ui.send_message(ListViewMessage::items(
|
||||
self.list,
|
||||
MessageDirection::ToWidget,
|
||||
items,
|
||||
));
|
||||
self.sync_data(tile_set, ui);
|
||||
}
|
||||
fn sync_data(&mut self, tile_set: &OptionTileSet, ui: &mut UserInterface) {
|
||||
let sel_index = self.selection_index(ui);
|
||||
let name = match sel_index {
|
||||
Some(index) => tile_set
|
||||
.colliders()
|
||||
.get(index)
|
||||
.map(|c| c.name.to_string())
|
||||
.unwrap_or_default(),
|
||||
None => String::default(),
|
||||
};
|
||||
let color = match sel_index {
|
||||
Some(index) => tile_set
|
||||
.colliders()
|
||||
.get(index)
|
||||
.map(|c| c.color)
|
||||
.unwrap_or(Color::BLACK),
|
||||
None => Color::BLACK,
|
||||
};
|
||||
ui.send_message(WidgetMessage::enabled(
|
||||
self.data_panel,
|
||||
MessageDirection::ToWidget,
|
||||
sel_index.is_some(),
|
||||
));
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(self.name_field, MessageDirection::ToWidget, name),
|
||||
);
|
||||
send_sync_message(
|
||||
ui,
|
||||
ColorFieldMessage::color(self.color_field, MessageDirection::ToWidget, color),
|
||||
);
|
||||
}
|
||||
pub fn handle_ui_message(
|
||||
&mut self,
|
||||
tile_set: TileSetResource,
|
||||
message: &UiMessage,
|
||||
ui: &mut UserInterface,
|
||||
sender: &MessageSender,
|
||||
) {
|
||||
if message.direction() == MessageDirection::ToWidget || message.flags == MSG_SYNC_FLAG {
|
||||
return;
|
||||
}
|
||||
if let Some(ListViewMessage::SelectionChanged(_)) = message.data() {
|
||||
if message.destination() == self.list {
|
||||
self.sync_data(&TileSetRef::new(&tile_set).as_loaded(), ui);
|
||||
}
|
||||
} else if let Some(TextMessage::Text(value)) = message.data() {
|
||||
if message.destination() == self.name_field {
|
||||
self.update_name(tile_set, value, ui, sender);
|
||||
}
|
||||
} else if let Some(&ColorFieldMessage::Color(value)) = message.data() {
|
||||
if message.destination() == self.color_field {
|
||||
self.update_color(tile_set, value, ui, sender);
|
||||
}
|
||||
} else if let Some(ButtonMessage::Click) = message.data() {
|
||||
if message.destination() == self.up_button {
|
||||
self.move_layer(tile_set, -1, ui, sender);
|
||||
} else if message.destination() == self.down_button {
|
||||
self.move_layer(tile_set, 1, ui, sender);
|
||||
} else if message.destination() == self.add_button {
|
||||
self.add_layer(tile_set, ui, sender);
|
||||
} else if message.destination() == self.remove_button {
|
||||
self.remove_layer(tile_set, ui, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn selection_index(&self, ui: &UserInterface) -> Option<usize> {
|
||||
ui.node(self.list)
|
||||
.cast::<ListView>()?
|
||||
.selection
|
||||
.last()
|
||||
.copied()
|
||||
}
|
||||
fn update_name(
|
||||
&self,
|
||||
resource: TileSetResource,
|
||||
name: &str,
|
||||
ui: &UserInterface,
|
||||
sender: &MessageSender,
|
||||
) {
|
||||
let mut tile_set = TileSetRef::new(&resource);
|
||||
let tile_set = tile_set.as_loaded();
|
||||
let colliders = tile_set.colliders();
|
||||
let Some(sel_index) = self
|
||||
.selection_index(ui)
|
||||
.map(|i| i.clamp(0, colliders.len() - 1))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(uuid) = colliders.get(sel_index).map(|l| l.uuid) else {
|
||||
return;
|
||||
};
|
||||
ui.send_message(WidgetMessage::focus(
|
||||
self.name_field,
|
||||
MessageDirection::ToWidget,
|
||||
));
|
||||
sender.do_command(SetColliderLayerNameCommand {
|
||||
tile_set: resource.clone(),
|
||||
uuid,
|
||||
name: name.into(),
|
||||
});
|
||||
}
|
||||
fn update_color(
|
||||
&self,
|
||||
resource: TileSetResource,
|
||||
color: Color,
|
||||
ui: &UserInterface,
|
||||
sender: &MessageSender,
|
||||
) {
|
||||
let tile_set = resource.data_ref();
|
||||
let Some(sel_index) = self
|
||||
.selection_index(ui)
|
||||
.map(|i| i.clamp(0, tile_set.colliders.len() - 1))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(uuid) = tile_set.colliders.get(sel_index).map(|l| l.uuid) else {
|
||||
return;
|
||||
};
|
||||
sender.do_command(SetColliderLayerColorCommand {
|
||||
tile_set: resource.clone(),
|
||||
uuid,
|
||||
color,
|
||||
});
|
||||
}
|
||||
fn move_layer(
|
||||
&self,
|
||||
resource: TileSetResource,
|
||||
amount: isize,
|
||||
ui: &UserInterface,
|
||||
sender: &MessageSender,
|
||||
) {
|
||||
let tile_set = resource.data_ref();
|
||||
let Some(sel_index) = self.selection_index(ui) else {
|
||||
return;
|
||||
};
|
||||
let new_index = sel_index
|
||||
.saturating_add_signed(amount)
|
||||
.clamp(0, tile_set.colliders.len() - 1);
|
||||
if sel_index == new_index {
|
||||
return;
|
||||
}
|
||||
ui.send_message(ListViewMessage::selection(
|
||||
self.list,
|
||||
MessageDirection::ToWidget,
|
||||
vec![new_index],
|
||||
));
|
||||
sender.do_command(MoveColliderLayerCommand {
|
||||
tile_set: resource.clone(),
|
||||
start: sel_index,
|
||||
end: new_index,
|
||||
});
|
||||
}
|
||||
fn add_layer(&self, resource: TileSetResource, ui: &UserInterface, sender: &MessageSender) {
|
||||
let tile_set = resource.data_ref();
|
||||
let index = self
|
||||
.selection_index(ui)
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0)
|
||||
.clamp(0, tile_set.colliders.len());
|
||||
ui.send_message(ListViewMessage::selection(
|
||||
self.list,
|
||||
MessageDirection::ToWidget,
|
||||
vec![index],
|
||||
));
|
||||
sender.do_command(AddColliderLayerCommand {
|
||||
tile_set: resource.clone(),
|
||||
index,
|
||||
uuid: Uuid::new_v4(),
|
||||
});
|
||||
}
|
||||
fn remove_layer(&self, resource: TileSetResource, ui: &UserInterface, sender: &MessageSender) {
|
||||
let tile_set = resource.data_ref();
|
||||
let Some(index) = self
|
||||
.selection_index(ui)
|
||||
.map(|i| i.clamp(0, tile_set.colliders.len() - 1))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
sender.do_command(RemoveColliderLayerCommand {
|
||||
tile_set: resource.clone(),
|
||||
index,
|
||||
layer: None,
|
||||
values: FxHashMap::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
// 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 fyrox::{
|
||||
fxhash::FxHashMap,
|
||||
gui::{
|
||||
button::ButtonMessage,
|
||||
color::{ColorFieldBuilder, ColorFieldMessage},
|
||||
grid::*,
|
||||
list_view::{ListView, ListViewBuilder, ListViewMessage},
|
||||
scroll_viewer::ScrollViewerBuilder,
|
||||
text::{TextBuilder, TextMessage},
|
||||
text_box::{TextBoxBuilder, TextCommitMode},
|
||||
utils::{make_arrow, ArrowDirection},
|
||||
HorizontalAlignment, VerticalAlignment,
|
||||
},
|
||||
};
|
||||
|
||||
use fyrox::scene::tilemap::{tileset::*, *};
|
||||
|
||||
use crate::{send_sync_message, MSG_SYNC_FLAG};
|
||||
|
||||
use super::*;
|
||||
use commands::*;
|
||||
|
||||
/// 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.
|
||||
pub struct CollidersTab {
|
||||
handle: Handle<UiNode>,
|
||||
list: Handle<UiNode>,
|
||||
up_button: Handle<UiNode>,
|
||||
down_button: Handle<UiNode>,
|
||||
remove_button: Handle<UiNode>,
|
||||
add_button: Handle<UiNode>,
|
||||
data_panel: Handle<UiNode>,
|
||||
name_field: Handle<UiNode>,
|
||||
color_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, collider: &TileSetColliderLayer) -> Handle<UiNode> {
|
||||
let content = GridBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(
|
||||
BorderBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_horizontal_alignment(HorizontalAlignment::Center)
|
||||
.with_vertical_alignment(VerticalAlignment::Center)
|
||||
.with_width(16.0)
|
||||
.with_height(16.0)
|
||||
.with_background(Brush::Solid(collider.color).into()),
|
||||
)
|
||||
.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(collider.name.clone())
|
||||
.build(ctx),
|
||||
),
|
||||
)
|
||||
.add_row(Row::auto())
|
||||
.add_column(Column::strict(20.0))
|
||||
.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_items(ctx: &mut BuildContext, tile_set: &OptionTileSet) -> Vec<Handle<UiNode>> {
|
||||
tile_set
|
||||
.colliders()
|
||||
.iter()
|
||||
.map(|c| make_list_item(ctx, c))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl CollidersTab {
|
||||
pub fn new(tile_book: TileBook, ctx: &mut BuildContext) -> Self {
|
||||
let items = if let TileBook::TileSet(t) = &tile_book {
|
||||
make_items(ctx, &TileSetRef::new(t).as_loaded())
|
||||
} else {
|
||||
Vec::default()
|
||||
};
|
||||
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 add_button = make_button("New", "Create a new collider.", ctx, 2, 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)
|
||||
.with_child(add_button),
|
||||
)
|
||||
.add_row(Row::auto())
|
||||
.add_column(Column::stretch())
|
||||
.add_column(Column::stretch())
|
||||
.add_column(Column::stretch())
|
||||
.build(ctx);
|
||||
let left_label = TextBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_horizontal_alignment(HorizontalAlignment::Center)
|
||||
.with_margin(Thickness::uniform(2.0)),
|
||||
)
|
||||
.with_text("Colliders:")
|
||||
.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),
|
||||
)
|
||||
.add_row(Row::auto())
|
||||
.add_row(Row::stretch())
|
||||
.add_row(Row::strict(30.0))
|
||||
.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 color_field = ColorFieldBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_margin(Thickness::uniform(2.0))
|
||||
.on_row(1)
|
||||
.with_height(30.0),
|
||||
)
|
||||
.build(ctx);
|
||||
let data_panel = GridBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_margin(Thickness::uniform(2.0))
|
||||
.on_column(1)
|
||||
.with_child(header)
|
||||
.with_child(color_field),
|
||||
)
|
||||
.add_row(Row::auto())
|
||||
.add_row(Row::auto())
|
||||
.add_column(Column::stretch())
|
||||
.build(ctx);
|
||||
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),
|
||||
list,
|
||||
up_button,
|
||||
down_button,
|
||||
add_button,
|
||||
remove_button,
|
||||
data_panel,
|
||||
name_field,
|
||||
color_field,
|
||||
}
|
||||
}
|
||||
pub fn handle(&self) -> Handle<UiNode> {
|
||||
self.handle
|
||||
}
|
||||
pub fn sync_to_model(&mut self, tile_set: &OptionTileSet, ui: &mut UserInterface) {
|
||||
let items = make_items(&mut ui.build_ctx(), tile_set);
|
||||
ui.send_message(ListViewMessage::items(
|
||||
self.list,
|
||||
MessageDirection::ToWidget,
|
||||
items,
|
||||
));
|
||||
self.sync_data(tile_set, ui);
|
||||
}
|
||||
fn sync_data(&mut self, tile_set: &OptionTileSet, ui: &mut UserInterface) {
|
||||
let sel_index = self.selection_index(ui);
|
||||
let name = match sel_index {
|
||||
Some(index) => tile_set
|
||||
.colliders()
|
||||
.get(index)
|
||||
.map(|c| c.name.to_string())
|
||||
.unwrap_or_default(),
|
||||
None => String::default(),
|
||||
};
|
||||
let color = match sel_index {
|
||||
Some(index) => tile_set
|
||||
.colliders()
|
||||
.get(index)
|
||||
.map(|c| c.color)
|
||||
.unwrap_or(Color::BLACK),
|
||||
None => Color::BLACK,
|
||||
};
|
||||
ui.send_message(WidgetMessage::enabled(
|
||||
self.data_panel,
|
||||
MessageDirection::ToWidget,
|
||||
sel_index.is_some(),
|
||||
));
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(self.name_field, MessageDirection::ToWidget, name),
|
||||
);
|
||||
send_sync_message(
|
||||
ui,
|
||||
ColorFieldMessage::color(self.color_field, MessageDirection::ToWidget, color),
|
||||
);
|
||||
}
|
||||
pub fn handle_ui_message(
|
||||
&mut self,
|
||||
tile_set: TileSetResource,
|
||||
message: &UiMessage,
|
||||
ui: &mut UserInterface,
|
||||
sender: &MessageSender,
|
||||
) {
|
||||
if message.direction() == MessageDirection::ToWidget || message.flags == MSG_SYNC_FLAG {
|
||||
return;
|
||||
}
|
||||
if let Some(ListViewMessage::SelectionChanged(_)) = message.data() {
|
||||
if message.destination() == self.list {
|
||||
self.sync_data(&TileSetRef::new(&tile_set).as_loaded(), ui);
|
||||
}
|
||||
} else if let Some(TextMessage::Text(value)) = message.data() {
|
||||
if message.destination() == self.name_field {
|
||||
self.update_name(tile_set, value, ui, sender);
|
||||
}
|
||||
} else if let Some(&ColorFieldMessage::Color(value)) = message.data() {
|
||||
if message.destination() == self.color_field {
|
||||
self.update_color(tile_set, value, ui, sender);
|
||||
}
|
||||
} else if let Some(ButtonMessage::Click) = message.data() {
|
||||
if message.destination() == self.up_button {
|
||||
self.move_layer(tile_set, -1, ui, sender);
|
||||
} else if message.destination() == self.down_button {
|
||||
self.move_layer(tile_set, 1, ui, sender);
|
||||
} else if message.destination() == self.add_button {
|
||||
self.add_layer(tile_set, ui, sender);
|
||||
} else if message.destination() == self.remove_button {
|
||||
self.remove_layer(tile_set, ui, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn selection_index(&self, ui: &UserInterface) -> Option<usize> {
|
||||
ui.node(self.list)
|
||||
.cast::<ListView>()?
|
||||
.selection
|
||||
.last()
|
||||
.copied()
|
||||
}
|
||||
fn update_name(
|
||||
&self,
|
||||
resource: TileSetResource,
|
||||
name: &str,
|
||||
ui: &UserInterface,
|
||||
sender: &MessageSender,
|
||||
) {
|
||||
let mut tile_set = TileSetRef::new(&resource);
|
||||
let tile_set = tile_set.as_loaded();
|
||||
let colliders = tile_set.colliders();
|
||||
let Some(sel_index) = self
|
||||
.selection_index(ui)
|
||||
.map(|i| i.clamp(0, colliders.len() - 1))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(uuid) = colliders.get(sel_index).map(|l| l.uuid) else {
|
||||
return;
|
||||
};
|
||||
ui.send_message(WidgetMessage::focus(
|
||||
self.name_field,
|
||||
MessageDirection::ToWidget,
|
||||
));
|
||||
sender.do_command(SetColliderLayerNameCommand {
|
||||
tile_set: resource.clone(),
|
||||
uuid,
|
||||
name: name.into(),
|
||||
});
|
||||
}
|
||||
fn update_color(
|
||||
&self,
|
||||
resource: TileSetResource,
|
||||
color: Color,
|
||||
ui: &UserInterface,
|
||||
sender: &MessageSender,
|
||||
) {
|
||||
let tile_set = resource.data_ref();
|
||||
let Some(sel_index) = self
|
||||
.selection_index(ui)
|
||||
.map(|i| i.clamp(0, tile_set.colliders.len() - 1))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(uuid) = tile_set.colliders.get(sel_index).map(|l| l.uuid) else {
|
||||
return;
|
||||
};
|
||||
sender.do_command(SetColliderLayerColorCommand {
|
||||
tile_set: resource.clone(),
|
||||
uuid,
|
||||
color,
|
||||
});
|
||||
}
|
||||
fn move_layer(
|
||||
&self,
|
||||
resource: TileSetResource,
|
||||
amount: isize,
|
||||
ui: &UserInterface,
|
||||
sender: &MessageSender,
|
||||
) {
|
||||
let tile_set = resource.data_ref();
|
||||
let Some(sel_index) = self.selection_index(ui) else {
|
||||
return;
|
||||
};
|
||||
let new_index = sel_index
|
||||
.saturating_add_signed(amount)
|
||||
.clamp(0, tile_set.colliders.len() - 1);
|
||||
if sel_index == new_index {
|
||||
return;
|
||||
}
|
||||
ui.send_message(ListViewMessage::selection(
|
||||
self.list,
|
||||
MessageDirection::ToWidget,
|
||||
vec![new_index],
|
||||
));
|
||||
sender.do_command(MoveColliderLayerCommand {
|
||||
tile_set: resource.clone(),
|
||||
start: sel_index,
|
||||
end: new_index,
|
||||
});
|
||||
}
|
||||
fn add_layer(&self, resource: TileSetResource, ui: &UserInterface, sender: &MessageSender) {
|
||||
let tile_set = resource.data_ref();
|
||||
let index = self
|
||||
.selection_index(ui)
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0)
|
||||
.clamp(0, tile_set.colliders.len());
|
||||
ui.send_message(ListViewMessage::selection(
|
||||
self.list,
|
||||
MessageDirection::ToWidget,
|
||||
vec![index],
|
||||
));
|
||||
sender.do_command(AddColliderLayerCommand {
|
||||
tile_set: resource.clone(),
|
||||
index,
|
||||
uuid: Uuid::new_v4(),
|
||||
});
|
||||
}
|
||||
fn remove_layer(&self, resource: TileSetResource, ui: &UserInterface, sender: &MessageSender) {
|
||||
let tile_set = resource.data_ref();
|
||||
let Some(index) = self
|
||||
.selection_index(ui)
|
||||
.map(|i| i.clamp(0, tile_set.colliders.len() - 1))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
sender.do_command(RemoveColliderLayerCommand {
|
||||
tile_set: resource.clone(),
|
||||
index,
|
||||
layer: None,
|
||||
values: FxHashMap::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,331 +1,331 @@
|
||||
// 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.
|
||||
|
||||
//! Give the Fyrox Inspector the ability to edit [`TileDefinitionHandle`] properties.
|
||||
|
||||
use std::any::TypeId;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::{send_sync_message, MSG_SYNC_FLAG};
|
||||
|
||||
use fyrox::gui::inspector::FieldKind;
|
||||
use fyrox::{
|
||||
core::{
|
||||
color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
|
||||
visitor::prelude::*,
|
||||
},
|
||||
gui::{
|
||||
brush::Brush,
|
||||
button::{ButtonBuilder, ButtonMessage},
|
||||
define_constructor, define_widget_deref,
|
||||
grid::{Column, GridBuilder, Row},
|
||||
image::ImageBuilder,
|
||||
inspector::{
|
||||
editors::{
|
||||
PropertyEditorBuildContext, PropertyEditorDefinition, PropertyEditorInstance,
|
||||
PropertyEditorMessageContext, PropertyEditorTranslationContext,
|
||||
},
|
||||
InspectorError, PropertyChanged,
|
||||
},
|
||||
message::{MessageDirection, UiMessage},
|
||||
text::TextMessage,
|
||||
text_box::{TextBoxBuilder, TextCommitMode},
|
||||
widget::{Widget, WidgetBuilder},
|
||||
BuildContext, Control, Thickness, UiNode, UserInterface,
|
||||
},
|
||||
scene::tilemap::TileDefinitionHandle,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A message for events related to [`TileDefinitionHandleEditor`].
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum TileDefinitionHandleEditorMessage {
|
||||
/// The value of the handle has changed.
|
||||
Value(Option<TileDefinitionHandle>),
|
||||
/// The user has clicked the go-to button beside the handle.
|
||||
Goto(TileDefinitionHandle),
|
||||
}
|
||||
|
||||
impl TileDefinitionHandleEditorMessage {
|
||||
define_constructor!(
|
||||
/// The value of the handle has changed.
|
||||
TileDefinitionHandleEditorMessage:Value => fn value(Option<TileDefinitionHandle>), layout: false);
|
||||
define_constructor!(
|
||||
/// The user has clicked the go-to button beside the handle.
|
||||
TileDefinitionHandleEditorMessage:Goto => fn goto(TileDefinitionHandle), layout: false);
|
||||
}
|
||||
|
||||
/// The widget for editing a [`TileDefinitionHandle`].
|
||||
/// It has a button that can be used to focus the tile map control panel on the tile
|
||||
/// represented by this handle, assuming that the control panel has the correct
|
||||
/// tile set. There is no way to ensure this, since a TileDefinitionHandle includes
|
||||
/// no information about which tile set it refers to.
|
||||
///
|
||||
/// The lack of tile set informaiton also means that this widget cannot show an image
|
||||
/// of the tile that the TileDefinitionHandle refers to, but a potential future
|
||||
/// improvement might be to borrow the tile set from the tile map control panel and
|
||||
/// use that to display a tile for this widget.
|
||||
///
|
||||
/// The value is displayed in a text box in the form "(x,y):(x,y)" where the first
|
||||
/// pair is the page coordinates and the second pair is the tile coordinates.
|
||||
/// When editing the handle, one need merely type four integers. Whatever
|
||||
/// characters separate the integers are ignored, so "1 2 3 4" would be accepted.
|
||||
#[derive(Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
|
||||
#[reflect(derived_type = "UiNode")]
|
||||
#[type_uuid(id = "60146cf0-33e3-4757-8e66-e7196324271f")]
|
||||
pub struct TileDefinitionHandleEditor {
|
||||
widget: Widget,
|
||||
field: Handle<UiNode>,
|
||||
button: Handle<UiNode>,
|
||||
value: Option<TileDefinitionHandle>,
|
||||
allow_none: bool,
|
||||
}
|
||||
|
||||
define_widget_deref!(TileDefinitionHandleEditor);
|
||||
|
||||
fn value_to_string(value: Option<TileDefinitionHandle>) -> String {
|
||||
value
|
||||
.map(|handle| handle.to_string())
|
||||
.unwrap_or_else(|| "None".into())
|
||||
}
|
||||
|
||||
impl TileDefinitionHandleEditor {
|
||||
fn text(&self) -> String {
|
||||
value_to_string(self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Control for TileDefinitionHandleEditor {
|
||||
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
|
||||
if message.flags == MSG_SYNC_FLAG {
|
||||
return;
|
||||
}
|
||||
if let Some(&TileDefinitionHandleEditorMessage::Value(handle)) = message.data() {
|
||||
if message.direction() == MessageDirection::ToWidget
|
||||
&& message.destination() == self.handle()
|
||||
{
|
||||
self.value = handle;
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(self.field, MessageDirection::ToWidget, self.text()),
|
||||
);
|
||||
ui.send_message(message.reverse());
|
||||
}
|
||||
} else if let Some(TextMessage::Text(text)) = message.data() {
|
||||
if message.direction() == MessageDirection::FromWidget
|
||||
&& message.destination() == self.field
|
||||
{
|
||||
let value = TileDefinitionHandle::parse(text);
|
||||
if self.allow_none || value.is_some() {
|
||||
self.value = value;
|
||||
}
|
||||
ui.send_message(TileDefinitionHandleEditorMessage::value(
|
||||
self.handle(),
|
||||
MessageDirection::FromWidget,
|
||||
self.value,
|
||||
));
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(self.field, MessageDirection::ToWidget, self.text()),
|
||||
);
|
||||
}
|
||||
} else if let Some(ButtonMessage::Click) = message.data() {
|
||||
if message.direction() == MessageDirection::FromWidget
|
||||
&& message.destination() == self.button
|
||||
{
|
||||
if let Some(handle) = self.value {
|
||||
ui.send_message(TileDefinitionHandleEditorMessage::goto(
|
||||
self.handle(),
|
||||
MessageDirection::FromWidget,
|
||||
handle,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder for creating instances of [`TileDefinitionHandleEditor`].
|
||||
pub struct TileDefinitionHandleEditorBuilder {
|
||||
widget_builder: WidgetBuilder,
|
||||
value: Option<TileDefinitionHandle>,
|
||||
allow_none: bool,
|
||||
}
|
||||
|
||||
impl TileDefinitionHandleEditorBuilder {
|
||||
/// Begin building a new [`TileDefinitionHandleEditor`] with a widget from the given builder.
|
||||
pub fn new(widget_builder: WidgetBuilder) -> Self {
|
||||
Self {
|
||||
widget_builder,
|
||||
value: None,
|
||||
allow_none: true,
|
||||
}
|
||||
}
|
||||
/// Control whether None is an acceptable value for the handle. This defaults to `true`.
|
||||
pub fn with_allow_none(mut self, allow_none: bool) -> Self {
|
||||
self.allow_none = allow_none;
|
||||
self
|
||||
}
|
||||
/// Set the initial value of the handle.
|
||||
pub fn with_value(mut self, value: Option<TileDefinitionHandle>) -> Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
/// Build the widgets for the [`TileDefinitionHandleEditor`].
|
||||
pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
let text = value_to_string(self.value);
|
||||
let field = TextBoxBuilder::new(WidgetBuilder::new())
|
||||
.with_text(text)
|
||||
.with_text_commit_mode(TextCommitMode::LostFocusPlusEnter)
|
||||
.build(ctx);
|
||||
let button = ButtonBuilder::new(WidgetBuilder::new().on_column(1))
|
||||
.with_content(
|
||||
ImageBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_background(Brush::Solid(Color::opaque(180, 180, 180)).into())
|
||||
.with_margin(Thickness::uniform(1.0))
|
||||
.with_width(16.0)
|
||||
.with_height(16.0),
|
||||
)
|
||||
.with_opt_texture(PALETTE_IMAGE.clone())
|
||||
.build(ctx),
|
||||
)
|
||||
.build(ctx);
|
||||
let grid = GridBuilder::new(WidgetBuilder::new().with_child(field).with_child(button))
|
||||
.add_column(Column::stretch())
|
||||
.add_column(Column::auto())
|
||||
.add_row(Row::auto())
|
||||
.build(ctx);
|
||||
ctx.add_node(UiNode::new(TileDefinitionHandleEditor {
|
||||
widget: self.widget_builder.with_child(grid).build(ctx),
|
||||
field,
|
||||
button,
|
||||
allow_none: self.allow_none,
|
||||
value: self.value,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// [`PropertyEditorDefinition`] for [`TileDefinitionHandleEditor`].
|
||||
#[derive(Debug)]
|
||||
pub struct TileDefinitionHandlePropertyEditorDefinition;
|
||||
|
||||
impl PropertyEditorDefinition for TileDefinitionHandlePropertyEditorDefinition {
|
||||
fn value_type_id(&self) -> TypeId {
|
||||
TypeId::of::<TileDefinitionHandle>()
|
||||
}
|
||||
|
||||
fn create_instance(
|
||||
&self,
|
||||
ctx: PropertyEditorBuildContext,
|
||||
) -> Result<PropertyEditorInstance, InspectorError> {
|
||||
let value = *ctx.property_info.cast_value::<TileDefinitionHandle>()?;
|
||||
Ok(PropertyEditorInstance::Simple {
|
||||
editor: TileDefinitionHandleEditorBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_min_size(Vector2::new(0.0, 17.0))
|
||||
.with_margin(Thickness::uniform(1.0)),
|
||||
)
|
||||
.with_allow_none(false)
|
||||
.with_value(Some(value))
|
||||
.build(ctx.build_context),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_message(
|
||||
&self,
|
||||
ctx: PropertyEditorMessageContext,
|
||||
) -> Result<Option<UiMessage>, InspectorError> {
|
||||
let value = *ctx.property_info.cast_value::<TileDefinitionHandle>()?;
|
||||
Ok(Some(TileDefinitionHandleEditorMessage::value(
|
||||
ctx.instance,
|
||||
MessageDirection::ToWidget,
|
||||
Some(value),
|
||||
)))
|
||||
}
|
||||
|
||||
fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option<PropertyChanged> {
|
||||
if ctx.message.direction() == MessageDirection::FromWidget {
|
||||
if let Some(&TileDefinitionHandleEditorMessage::Value(Some(value))) = ctx.message.data()
|
||||
{
|
||||
return Some(PropertyChanged {
|
||||
name: ctx.name.to_string(),
|
||||
value: FieldKind::object(value),
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// [`PropertyEditorDefinition`] for optional [`TileDefinitionHandleEditor`].
|
||||
#[derive(Debug)]
|
||||
pub struct OptionTileDefinitionHandlePropertyEditorDefinition;
|
||||
|
||||
impl PropertyEditorDefinition for OptionTileDefinitionHandlePropertyEditorDefinition {
|
||||
fn value_type_id(&self) -> TypeId {
|
||||
TypeId::of::<Option<TileDefinitionHandle>>()
|
||||
}
|
||||
|
||||
fn create_instance(
|
||||
&self,
|
||||
ctx: PropertyEditorBuildContext,
|
||||
) -> Result<PropertyEditorInstance, InspectorError> {
|
||||
let value = *ctx
|
||||
.property_info
|
||||
.cast_value::<Option<TileDefinitionHandle>>()?;
|
||||
Ok(PropertyEditorInstance::Simple {
|
||||
editor: TileDefinitionHandleEditorBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_min_size(Vector2::new(0.0, 17.0))
|
||||
.with_margin(Thickness::uniform(1.0)),
|
||||
)
|
||||
.with_value(value)
|
||||
.build(ctx.build_context),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_message(
|
||||
&self,
|
||||
ctx: PropertyEditorMessageContext,
|
||||
) -> Result<Option<UiMessage>, InspectorError> {
|
||||
let value = *ctx
|
||||
.property_info
|
||||
.cast_value::<Option<TileDefinitionHandle>>()?;
|
||||
Ok(Some(TileDefinitionHandleEditorMessage::value(
|
||||
ctx.instance,
|
||||
MessageDirection::ToWidget,
|
||||
value,
|
||||
)))
|
||||
}
|
||||
|
||||
fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option<PropertyChanged> {
|
||||
if ctx.message.direction() == MessageDirection::FromWidget {
|
||||
if let Some(&TileDefinitionHandleEditorMessage::Value(value)) = ctx.message.data() {
|
||||
return Some(PropertyChanged {
|
||||
name: ctx.name.to_string(),
|
||||
value: FieldKind::object(value),
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
|
||||
//! Give the Fyrox Inspector the ability to edit [`TileDefinitionHandle`] properties.
|
||||
|
||||
use std::any::TypeId;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::{send_sync_message, MSG_SYNC_FLAG};
|
||||
|
||||
use fyrox::gui::inspector::FieldKind;
|
||||
use fyrox::{
|
||||
core::{
|
||||
color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
|
||||
visitor::prelude::*,
|
||||
},
|
||||
gui::{
|
||||
brush::Brush,
|
||||
button::{ButtonBuilder, ButtonMessage},
|
||||
define_constructor, define_widget_deref,
|
||||
grid::{Column, GridBuilder, Row},
|
||||
image::ImageBuilder,
|
||||
inspector::{
|
||||
editors::{
|
||||
PropertyEditorBuildContext, PropertyEditorDefinition, PropertyEditorInstance,
|
||||
PropertyEditorMessageContext, PropertyEditorTranslationContext,
|
||||
},
|
||||
InspectorError, PropertyChanged,
|
||||
},
|
||||
message::{MessageDirection, UiMessage},
|
||||
text::TextMessage,
|
||||
text_box::{TextBoxBuilder, TextCommitMode},
|
||||
widget::{Widget, WidgetBuilder},
|
||||
BuildContext, Control, Thickness, UiNode, UserInterface,
|
||||
},
|
||||
scene::tilemap::TileDefinitionHandle,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A message for events related to [`TileDefinitionHandleEditor`].
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum TileDefinitionHandleEditorMessage {
|
||||
/// The value of the handle has changed.
|
||||
Value(Option<TileDefinitionHandle>),
|
||||
/// The user has clicked the go-to button beside the handle.
|
||||
Goto(TileDefinitionHandle),
|
||||
}
|
||||
|
||||
impl TileDefinitionHandleEditorMessage {
|
||||
define_constructor!(
|
||||
/// The value of the handle has changed.
|
||||
TileDefinitionHandleEditorMessage:Value => fn value(Option<TileDefinitionHandle>), layout: false);
|
||||
define_constructor!(
|
||||
/// The user has clicked the go-to button beside the handle.
|
||||
TileDefinitionHandleEditorMessage:Goto => fn goto(TileDefinitionHandle), layout: false);
|
||||
}
|
||||
|
||||
/// The widget for editing a [`TileDefinitionHandle`].
|
||||
/// It has a button that can be used to focus the tile map control panel on the tile
|
||||
/// represented by this handle, assuming that the control panel has the correct
|
||||
/// tile set. There is no way to ensure this, since a TileDefinitionHandle includes
|
||||
/// no information about which tile set it refers to.
|
||||
///
|
||||
/// The lack of tile set informaiton also means that this widget cannot show an image
|
||||
/// of the tile that the TileDefinitionHandle refers to, but a potential future
|
||||
/// improvement might be to borrow the tile set from the tile map control panel and
|
||||
/// use that to display a tile for this widget.
|
||||
///
|
||||
/// The value is displayed in a text box in the form "(x,y):(x,y)" where the first
|
||||
/// pair is the page coordinates and the second pair is the tile coordinates.
|
||||
/// When editing the handle, one need merely type four integers. Whatever
|
||||
/// characters separate the integers are ignored, so "1 2 3 4" would be accepted.
|
||||
#[derive(Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
|
||||
#[reflect(derived_type = "UiNode")]
|
||||
#[type_uuid(id = "60146cf0-33e3-4757-8e66-e7196324271f")]
|
||||
pub struct TileDefinitionHandleEditor {
|
||||
widget: Widget,
|
||||
field: Handle<UiNode>,
|
||||
button: Handle<UiNode>,
|
||||
value: Option<TileDefinitionHandle>,
|
||||
allow_none: bool,
|
||||
}
|
||||
|
||||
define_widget_deref!(TileDefinitionHandleEditor);
|
||||
|
||||
fn value_to_string(value: Option<TileDefinitionHandle>) -> String {
|
||||
value
|
||||
.map(|handle| handle.to_string())
|
||||
.unwrap_or_else(|| "None".into())
|
||||
}
|
||||
|
||||
impl TileDefinitionHandleEditor {
|
||||
fn text(&self) -> String {
|
||||
value_to_string(self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Control for TileDefinitionHandleEditor {
|
||||
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
|
||||
if message.flags == MSG_SYNC_FLAG {
|
||||
return;
|
||||
}
|
||||
if let Some(&TileDefinitionHandleEditorMessage::Value(handle)) = message.data() {
|
||||
if message.direction() == MessageDirection::ToWidget
|
||||
&& message.destination() == self.handle()
|
||||
{
|
||||
self.value = handle;
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(self.field, MessageDirection::ToWidget, self.text()),
|
||||
);
|
||||
ui.send_message(message.reverse());
|
||||
}
|
||||
} else if let Some(TextMessage::Text(text)) = message.data() {
|
||||
if message.direction() == MessageDirection::FromWidget
|
||||
&& message.destination() == self.field
|
||||
{
|
||||
let value = TileDefinitionHandle::parse(text);
|
||||
if self.allow_none || value.is_some() {
|
||||
self.value = value;
|
||||
}
|
||||
ui.send_message(TileDefinitionHandleEditorMessage::value(
|
||||
self.handle(),
|
||||
MessageDirection::FromWidget,
|
||||
self.value,
|
||||
));
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(self.field, MessageDirection::ToWidget, self.text()),
|
||||
);
|
||||
}
|
||||
} else if let Some(ButtonMessage::Click) = message.data() {
|
||||
if message.direction() == MessageDirection::FromWidget
|
||||
&& message.destination() == self.button
|
||||
{
|
||||
if let Some(handle) = self.value {
|
||||
ui.send_message(TileDefinitionHandleEditorMessage::goto(
|
||||
self.handle(),
|
||||
MessageDirection::FromWidget,
|
||||
handle,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder for creating instances of [`TileDefinitionHandleEditor`].
|
||||
pub struct TileDefinitionHandleEditorBuilder {
|
||||
widget_builder: WidgetBuilder,
|
||||
value: Option<TileDefinitionHandle>,
|
||||
allow_none: bool,
|
||||
}
|
||||
|
||||
impl TileDefinitionHandleEditorBuilder {
|
||||
/// Begin building a new [`TileDefinitionHandleEditor`] with a widget from the given builder.
|
||||
pub fn new(widget_builder: WidgetBuilder) -> Self {
|
||||
Self {
|
||||
widget_builder,
|
||||
value: None,
|
||||
allow_none: true,
|
||||
}
|
||||
}
|
||||
/// Control whether None is an acceptable value for the handle. This defaults to `true`.
|
||||
pub fn with_allow_none(mut self, allow_none: bool) -> Self {
|
||||
self.allow_none = allow_none;
|
||||
self
|
||||
}
|
||||
/// Set the initial value of the handle.
|
||||
pub fn with_value(mut self, value: Option<TileDefinitionHandle>) -> Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
/// Build the widgets for the [`TileDefinitionHandleEditor`].
|
||||
pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
let text = value_to_string(self.value);
|
||||
let field = TextBoxBuilder::new(WidgetBuilder::new())
|
||||
.with_text(text)
|
||||
.with_text_commit_mode(TextCommitMode::LostFocusPlusEnter)
|
||||
.build(ctx);
|
||||
let button = ButtonBuilder::new(WidgetBuilder::new().on_column(1))
|
||||
.with_content(
|
||||
ImageBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_background(Brush::Solid(Color::opaque(180, 180, 180)).into())
|
||||
.with_margin(Thickness::uniform(1.0))
|
||||
.with_width(16.0)
|
||||
.with_height(16.0),
|
||||
)
|
||||
.with_opt_texture(PALETTE_IMAGE.clone())
|
||||
.build(ctx),
|
||||
)
|
||||
.build(ctx);
|
||||
let grid = GridBuilder::new(WidgetBuilder::new().with_child(field).with_child(button))
|
||||
.add_column(Column::stretch())
|
||||
.add_column(Column::auto())
|
||||
.add_row(Row::auto())
|
||||
.build(ctx);
|
||||
ctx.add_node(UiNode::new(TileDefinitionHandleEditor {
|
||||
widget: self.widget_builder.with_child(grid).build(ctx),
|
||||
field,
|
||||
button,
|
||||
allow_none: self.allow_none,
|
||||
value: self.value,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// [`PropertyEditorDefinition`] for [`TileDefinitionHandleEditor`].
|
||||
#[derive(Debug)]
|
||||
pub struct TileDefinitionHandlePropertyEditorDefinition;
|
||||
|
||||
impl PropertyEditorDefinition for TileDefinitionHandlePropertyEditorDefinition {
|
||||
fn value_type_id(&self) -> TypeId {
|
||||
TypeId::of::<TileDefinitionHandle>()
|
||||
}
|
||||
|
||||
fn create_instance(
|
||||
&self,
|
||||
ctx: PropertyEditorBuildContext,
|
||||
) -> Result<PropertyEditorInstance, InspectorError> {
|
||||
let value = *ctx.property_info.cast_value::<TileDefinitionHandle>()?;
|
||||
Ok(PropertyEditorInstance::Simple {
|
||||
editor: TileDefinitionHandleEditorBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_min_size(Vector2::new(0.0, 17.0))
|
||||
.with_margin(Thickness::uniform(1.0)),
|
||||
)
|
||||
.with_allow_none(false)
|
||||
.with_value(Some(value))
|
||||
.build(ctx.build_context),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_message(
|
||||
&self,
|
||||
ctx: PropertyEditorMessageContext,
|
||||
) -> Result<Option<UiMessage>, InspectorError> {
|
||||
let value = *ctx.property_info.cast_value::<TileDefinitionHandle>()?;
|
||||
Ok(Some(TileDefinitionHandleEditorMessage::value(
|
||||
ctx.instance,
|
||||
MessageDirection::ToWidget,
|
||||
Some(value),
|
||||
)))
|
||||
}
|
||||
|
||||
fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option<PropertyChanged> {
|
||||
if ctx.message.direction() == MessageDirection::FromWidget {
|
||||
if let Some(&TileDefinitionHandleEditorMessage::Value(Some(value))) = ctx.message.data()
|
||||
{
|
||||
return Some(PropertyChanged {
|
||||
name: ctx.name.to_string(),
|
||||
value: FieldKind::object(value),
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// [`PropertyEditorDefinition`] for optional [`TileDefinitionHandleEditor`].
|
||||
#[derive(Debug)]
|
||||
pub struct OptionTileDefinitionHandlePropertyEditorDefinition;
|
||||
|
||||
impl PropertyEditorDefinition for OptionTileDefinitionHandlePropertyEditorDefinition {
|
||||
fn value_type_id(&self) -> TypeId {
|
||||
TypeId::of::<Option<TileDefinitionHandle>>()
|
||||
}
|
||||
|
||||
fn create_instance(
|
||||
&self,
|
||||
ctx: PropertyEditorBuildContext,
|
||||
) -> Result<PropertyEditorInstance, InspectorError> {
|
||||
let value = *ctx
|
||||
.property_info
|
||||
.cast_value::<Option<TileDefinitionHandle>>()?;
|
||||
Ok(PropertyEditorInstance::Simple {
|
||||
editor: TileDefinitionHandleEditorBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_min_size(Vector2::new(0.0, 17.0))
|
||||
.with_margin(Thickness::uniform(1.0)),
|
||||
)
|
||||
.with_value(value)
|
||||
.build(ctx.build_context),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_message(
|
||||
&self,
|
||||
ctx: PropertyEditorMessageContext,
|
||||
) -> Result<Option<UiMessage>, InspectorError> {
|
||||
let value = *ctx
|
||||
.property_info
|
||||
.cast_value::<Option<TileDefinitionHandle>>()?;
|
||||
Ok(Some(TileDefinitionHandleEditorMessage::value(
|
||||
ctx.instance,
|
||||
MessageDirection::ToWidget,
|
||||
value,
|
||||
)))
|
||||
}
|
||||
|
||||
fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option<PropertyChanged> {
|
||||
if ctx.message.direction() == MessageDirection::FromWidget {
|
||||
if let Some(&TileDefinitionHandleEditorMessage::Value(value)) = ctx.message.data() {
|
||||
return Some(PropertyChanged {
|
||||
name: ctx.name.to_string(),
|
||||
value: FieldKind::object(value),
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,213 +1,213 @@
|
||||
// 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.
|
||||
|
||||
//! A widget for showing handles in the tile set editor.
|
||||
|
||||
use crate::{
|
||||
fyrox::gui::{
|
||||
button::ButtonMessage,
|
||||
define_constructor, define_widget_deref,
|
||||
grid::{Column, GridBuilder, Row},
|
||||
stack_panel::StackPanelBuilder,
|
||||
text::{TextBuilder, TextMessage},
|
||||
text_box::TextBoxBuilder,
|
||||
widget::Widget,
|
||||
Control, Orientation,
|
||||
},
|
||||
send_sync_message,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const BUTTON_SIZE: f32 = 12.0;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TileHandleEditorMessage {
|
||||
Goto(TileDefinitionHandle),
|
||||
OpenPalette(TileDefinitionHandle),
|
||||
Value(Option<TileDefinitionHandle>),
|
||||
}
|
||||
|
||||
impl TileHandleEditorMessage {
|
||||
define_constructor!(TileHandleEditorMessage:Goto => fn goto(TileDefinitionHandle), layout: false);
|
||||
define_constructor!(TileHandleEditorMessage:OpenPalette => fn open_palette(TileDefinitionHandle), layout: false);
|
||||
define_constructor!(TileHandleEditorMessage:Value => fn value(Option<TileDefinitionHandle>), layout: false);
|
||||
}
|
||||
|
||||
/// The widget for editing a [`TileDefinitionHandle`].
|
||||
/// It has a button for focusing the tile map control panel on the tile represented
|
||||
/// by this handle, and another button for focusing the tile set editor on that tile.
|
||||
///
|
||||
/// The value is displayed in a text box in the form "(x,y):(x,y)" where the first
|
||||
/// pair is the page coordinates and the second pair is the tile coordinates.
|
||||
/// When editing the handle, one need merely type four integers. Whatever
|
||||
/// characters separate the integers are ignored, so "1 2 3 4" would be accepted.
|
||||
#[derive(Debug, Default, Clone, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
|
||||
#[type_uuid(id = "86513074-461d-4583-a214-fb84f5aacac1")]
|
||||
#[reflect(derived_type = "UiNode")]
|
||||
pub struct TileHandleField {
|
||||
widget: Widget,
|
||||
value: Option<TileDefinitionHandle>,
|
||||
field: Handle<UiNode>,
|
||||
palette_button: Handle<UiNode>,
|
||||
goto_button: Handle<UiNode>,
|
||||
}
|
||||
|
||||
define_widget_deref!(TileHandleField);
|
||||
|
||||
fn value_to_string(value: Option<TileDefinitionHandle>) -> String {
|
||||
if let Some(v) = value {
|
||||
v.to_string()
|
||||
} else {
|
||||
"?".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_label(name: &str, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
TextBuilder::new(WidgetBuilder::new())
|
||||
.with_text(name)
|
||||
.build(ctx)
|
||||
}
|
||||
|
||||
impl Control for TileHandleField {
|
||||
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
|
||||
self.widget.handle_routed_message(ui, message);
|
||||
if message.direction() == MessageDirection::ToWidget {
|
||||
if let Some(TileHandleEditorMessage::Value(value)) = message.data() {
|
||||
self.value = *value;
|
||||
ui.send_message(TextMessage::text(
|
||||
self.field,
|
||||
MessageDirection::ToWidget,
|
||||
value_to_string(*value),
|
||||
));
|
||||
ui.send_message(message.reverse());
|
||||
}
|
||||
} else if let Some(ButtonMessage::Click) = message.data() {
|
||||
if let Some(value) = self.value {
|
||||
if message.destination() == self.palette_button {
|
||||
ui.send_message(TileHandleEditorMessage::open_palette(
|
||||
self.handle(),
|
||||
MessageDirection::FromWidget,
|
||||
value,
|
||||
));
|
||||
} else if message.destination() == self.goto_button {
|
||||
ui.send_message(TileHandleEditorMessage::goto(
|
||||
self.handle(),
|
||||
MessageDirection::FromWidget,
|
||||
value,
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if let Some(TextMessage::Text(text)) = message.data() {
|
||||
if message.flags == 0 {
|
||||
if let Ok(value) = text.parse() {
|
||||
if self.value != Some(value) {
|
||||
self.value = Some(value);
|
||||
ui.send_message(TileHandleEditorMessage::value(
|
||||
self.handle(),
|
||||
MessageDirection::FromWidget,
|
||||
self.value,
|
||||
));
|
||||
}
|
||||
}
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(
|
||||
self.field,
|
||||
MessageDirection::ToWidget,
|
||||
value_to_string(self.value),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TileHandleFieldBuilder {
|
||||
widget_builder: WidgetBuilder,
|
||||
label: String,
|
||||
value: Option<TileDefinitionHandle>,
|
||||
}
|
||||
|
||||
impl TileHandleFieldBuilder {
|
||||
pub fn new(widget_builder: WidgetBuilder) -> Self {
|
||||
Self {
|
||||
widget_builder,
|
||||
label: String::default(),
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
pub fn with_label(mut self, label: &str) -> Self {
|
||||
self.label = label.into();
|
||||
self
|
||||
}
|
||||
pub fn with_value(mut self, value: Option<TileDefinitionHandle>) -> Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
let field = TextBoxBuilder::new(WidgetBuilder::new().on_column(1))
|
||||
.with_text(value_to_string(self.value))
|
||||
.build(ctx);
|
||||
let goto_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
BUTTON_SIZE,
|
||||
BUTTON_SIZE,
|
||||
PICK_IMAGE.clone(),
|
||||
"Jump to tile",
|
||||
None,
|
||||
);
|
||||
let palette_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
BUTTON_SIZE,
|
||||
BUTTON_SIZE,
|
||||
PALETTE_IMAGE.clone(),
|
||||
"Open in palette window",
|
||||
None,
|
||||
);
|
||||
let buttons = StackPanelBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.on_column(2)
|
||||
.with_child(goto_button)
|
||||
.with_child(palette_button),
|
||||
)
|
||||
.with_orientation(Orientation::Horizontal)
|
||||
.build(ctx);
|
||||
let content = GridBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(make_label(&self.label, ctx))
|
||||
.with_child(field)
|
||||
.with_child(buttons),
|
||||
)
|
||||
.add_row(Row::auto())
|
||||
.add_column(Column::strict(FIELD_LABEL_WIDTH))
|
||||
.add_column(Column::stretch())
|
||||
.add_column(Column::auto())
|
||||
.build(ctx);
|
||||
|
||||
ctx.add_node(UiNode::new(TileHandleField {
|
||||
widget: self.widget_builder.with_child(content).build(ctx),
|
||||
value: self.value,
|
||||
field,
|
||||
goto_button,
|
||||
palette_button,
|
||||
}))
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
|
||||
//! A widget for showing handles in the tile set editor.
|
||||
|
||||
use crate::{
|
||||
fyrox::gui::{
|
||||
button::ButtonMessage,
|
||||
define_constructor, define_widget_deref,
|
||||
grid::{Column, GridBuilder, Row},
|
||||
stack_panel::StackPanelBuilder,
|
||||
text::{TextBuilder, TextMessage},
|
||||
text_box::TextBoxBuilder,
|
||||
widget::Widget,
|
||||
Control, Orientation,
|
||||
},
|
||||
send_sync_message,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const BUTTON_SIZE: f32 = 12.0;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TileHandleEditorMessage {
|
||||
Goto(TileDefinitionHandle),
|
||||
OpenPalette(TileDefinitionHandle),
|
||||
Value(Option<TileDefinitionHandle>),
|
||||
}
|
||||
|
||||
impl TileHandleEditorMessage {
|
||||
define_constructor!(TileHandleEditorMessage:Goto => fn goto(TileDefinitionHandle), layout: false);
|
||||
define_constructor!(TileHandleEditorMessage:OpenPalette => fn open_palette(TileDefinitionHandle), layout: false);
|
||||
define_constructor!(TileHandleEditorMessage:Value => fn value(Option<TileDefinitionHandle>), layout: false);
|
||||
}
|
||||
|
||||
/// The widget for editing a [`TileDefinitionHandle`].
|
||||
/// It has a button for focusing the tile map control panel on the tile represented
|
||||
/// by this handle, and another button for focusing the tile set editor on that tile.
|
||||
///
|
||||
/// The value is displayed in a text box in the form "(x,y):(x,y)" where the first
|
||||
/// pair is the page coordinates and the second pair is the tile coordinates.
|
||||
/// When editing the handle, one need merely type four integers. Whatever
|
||||
/// characters separate the integers are ignored, so "1 2 3 4" would be accepted.
|
||||
#[derive(Debug, Default, Clone, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
|
||||
#[type_uuid(id = "86513074-461d-4583-a214-fb84f5aacac1")]
|
||||
#[reflect(derived_type = "UiNode")]
|
||||
pub struct TileHandleField {
|
||||
widget: Widget,
|
||||
value: Option<TileDefinitionHandle>,
|
||||
field: Handle<UiNode>,
|
||||
palette_button: Handle<UiNode>,
|
||||
goto_button: Handle<UiNode>,
|
||||
}
|
||||
|
||||
define_widget_deref!(TileHandleField);
|
||||
|
||||
fn value_to_string(value: Option<TileDefinitionHandle>) -> String {
|
||||
if let Some(v) = value {
|
||||
v.to_string()
|
||||
} else {
|
||||
"?".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_label(name: &str, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
TextBuilder::new(WidgetBuilder::new())
|
||||
.with_text(name)
|
||||
.build(ctx)
|
||||
}
|
||||
|
||||
impl Control for TileHandleField {
|
||||
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
|
||||
self.widget.handle_routed_message(ui, message);
|
||||
if message.direction() == MessageDirection::ToWidget {
|
||||
if let Some(TileHandleEditorMessage::Value(value)) = message.data() {
|
||||
self.value = *value;
|
||||
ui.send_message(TextMessage::text(
|
||||
self.field,
|
||||
MessageDirection::ToWidget,
|
||||
value_to_string(*value),
|
||||
));
|
||||
ui.send_message(message.reverse());
|
||||
}
|
||||
} else if let Some(ButtonMessage::Click) = message.data() {
|
||||
if let Some(value) = self.value {
|
||||
if message.destination() == self.palette_button {
|
||||
ui.send_message(TileHandleEditorMessage::open_palette(
|
||||
self.handle(),
|
||||
MessageDirection::FromWidget,
|
||||
value,
|
||||
));
|
||||
} else if message.destination() == self.goto_button {
|
||||
ui.send_message(TileHandleEditorMessage::goto(
|
||||
self.handle(),
|
||||
MessageDirection::FromWidget,
|
||||
value,
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if let Some(TextMessage::Text(text)) = message.data() {
|
||||
if message.flags == 0 {
|
||||
if let Ok(value) = text.parse() {
|
||||
if self.value != Some(value) {
|
||||
self.value = Some(value);
|
||||
ui.send_message(TileHandleEditorMessage::value(
|
||||
self.handle(),
|
||||
MessageDirection::FromWidget,
|
||||
self.value,
|
||||
));
|
||||
}
|
||||
}
|
||||
send_sync_message(
|
||||
ui,
|
||||
TextMessage::text(
|
||||
self.field,
|
||||
MessageDirection::ToWidget,
|
||||
value_to_string(self.value),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TileHandleFieldBuilder {
|
||||
widget_builder: WidgetBuilder,
|
||||
label: String,
|
||||
value: Option<TileDefinitionHandle>,
|
||||
}
|
||||
|
||||
impl TileHandleFieldBuilder {
|
||||
pub fn new(widget_builder: WidgetBuilder) -> Self {
|
||||
Self {
|
||||
widget_builder,
|
||||
label: String::default(),
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
pub fn with_label(mut self, label: &str) -> Self {
|
||||
self.label = label.into();
|
||||
self
|
||||
}
|
||||
pub fn with_value(mut self, value: Option<TileDefinitionHandle>) -> Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
let field = TextBoxBuilder::new(WidgetBuilder::new().on_column(1))
|
||||
.with_text(value_to_string(self.value))
|
||||
.build(ctx);
|
||||
let goto_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
BUTTON_SIZE,
|
||||
BUTTON_SIZE,
|
||||
PICK_IMAGE.clone(),
|
||||
"Jump to tile",
|
||||
None,
|
||||
);
|
||||
let palette_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
BUTTON_SIZE,
|
||||
BUTTON_SIZE,
|
||||
PALETTE_IMAGE.clone(),
|
||||
"Open in palette window",
|
||||
None,
|
||||
);
|
||||
let buttons = StackPanelBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.on_column(2)
|
||||
.with_child(goto_button)
|
||||
.with_child(palette_button),
|
||||
)
|
||||
.with_orientation(Orientation::Horizontal)
|
||||
.build(ctx);
|
||||
let content = GridBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(make_label(&self.label, ctx))
|
||||
.with_child(field)
|
||||
.with_child(buttons),
|
||||
)
|
||||
.add_row(Row::auto())
|
||||
.add_column(Column::strict(FIELD_LABEL_WIDTH))
|
||||
.add_column(Column::stretch())
|
||||
.add_column(Column::auto())
|
||||
.build(ctx);
|
||||
|
||||
ctx.add_node(UiNode::new(TileHandleField {
|
||||
widget: self.widget_builder.with_child(content).build(ctx),
|
||||
value: self.value,
|
||||
field,
|
||||
goto_button,
|
||||
palette_button,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,433 +1,433 @@
|
||||
// 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, Clone, 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()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Item {
|
||||
macro_id: Uuid,
|
||||
editor: Option<ItemEditor>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ItemEditor {
|
||||
handle: Handle<UiNode>,
|
||||
header: ItemHeader,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
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(¯o_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 = BrushMacroCellContext {
|
||||
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) {
|
||||
brush_macro.copy_cell(None, cell, &BrushMacroInstance { brush, settings })
|
||||
} else {
|
||||
let cell = Some(cell);
|
||||
brush_macro.create_cell(&BrushMacroCellContext {
|
||||
brush,
|
||||
settings,
|
||||
cell,
|
||||
})
|
||||
};
|
||||
if let Some(command) = command {
|
||||
editor.message_sender.send(Message::DoCommand(command));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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, Clone, 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()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Item {
|
||||
macro_id: Uuid,
|
||||
editor: Option<ItemEditor>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ItemEditor {
|
||||
handle: Handle<UiNode>,
|
||||
header: ItemHeader,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
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(¯o_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 = BrushMacroCellContext {
|
||||
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) {
|
||||
brush_macro.copy_cell(None, cell, &BrushMacroInstance { brush, settings })
|
||||
} else {
|
||||
let cell = Some(cell);
|
||||
brush_macro.create_cell(&BrushMacroCellContext {
|
||||
brush,
|
||||
settings,
|
||||
cell,
|
||||
})
|
||||
};
|
||||
if let Some(command) = command {
|
||||
editor.message_sender.send(Message::DoCommand(command));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,292 +1,292 @@
|
||||
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//! The preview widget of the tile map control panel. This allows the user to see the
|
||||
//! currently selected tile stamp, including whatever transformations have been applied
|
||||
//! to the stamp.
|
||||
|
||||
use super::*;
|
||||
use crate::fyrox::{
|
||||
core::{
|
||||
algebra::{Matrix3, Vector2},
|
||||
math::Rect,
|
||||
pool::Handle,
|
||||
reflect::prelude::*,
|
||||
type_traits::prelude::*,
|
||||
visitor::prelude::*,
|
||||
},
|
||||
gui::formatted_text::{FormattedText, FormattedTextBuilder},
|
||||
gui::{
|
||||
brush::Brush,
|
||||
define_widget_deref,
|
||||
draw::{CommandTexture, Draw, DrawingContext},
|
||||
message::UiMessage,
|
||||
widget::{Widget, WidgetBuilder},
|
||||
BuildContext, Control, UiNode, UserInterface,
|
||||
},
|
||||
resource::texture::TextureKind,
|
||||
scene::tilemap::{TileRenderData, TileSource},
|
||||
};
|
||||
|
||||
use fyrox::material::MaterialResource;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
/// The preview widget of the tile map control panel. This allows the user to see the
|
||||
/// currently selected tile stamp, including whatever transformations have been applied
|
||||
/// to the stamp.
|
||||
#[derive(Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
|
||||
#[type_uuid(id = "5356a864-c026-4bd7-a4b1-30bacf77d8fa")]
|
||||
#[reflect(derived_type = "UiNode")]
|
||||
pub struct PanelPreview {
|
||||
widget: Widget,
|
||||
/// The tile editing state that is shared with palette widgets, the tile map interaction mode,
|
||||
/// the tile map control panel, and others. It allows this widget access to determine
|
||||
/// what stamp to show to the user.
|
||||
#[reflect(hidden)]
|
||||
#[visit(skip)]
|
||||
pub state: TileDrawStateRef,
|
||||
tile_size: Vector2<f32>,
|
||||
transform: Matrix3<f32>,
|
||||
/// This text is used to show the tile handle when a single tile is selected.
|
||||
#[visit(skip)]
|
||||
#[reflect(hidden)]
|
||||
handle_text: FormattedText,
|
||||
#[visit(skip)]
|
||||
#[reflect(hidden)]
|
||||
/// This is the size of the handle text, for the purposes of positioning the text
|
||||
/// within the widget.
|
||||
handle_text_size: Vector2<f32>,
|
||||
}
|
||||
|
||||
define_widget_deref!(PanelPreview);
|
||||
|
||||
fn draw_tile(
|
||||
position: Rect<f32>,
|
||||
clip_bounds: Rect<f32>,
|
||||
tile: &TileRenderData,
|
||||
material: &MaterialResource,
|
||||
drawing_context: &mut DrawingContext,
|
||||
) {
|
||||
if tile.is_empty() {
|
||||
return;
|
||||
}
|
||||
let color = tile.color;
|
||||
if let Some(material_bounds) = &tile.material_bounds {
|
||||
if let Some(texture) = material_bounds
|
||||
.material
|
||||
.state()
|
||||
.data()
|
||||
.and_then(|m| m.texture("diffuseTexture"))
|
||||
{
|
||||
let kind = texture.data_ref().kind();
|
||||
if let TextureKind::Rectangle { width, height } = kind {
|
||||
let size = Vector2::new(width, height);
|
||||
let bounds = &material_bounds.bounds;
|
||||
drawing_context.push_rect_filled(
|
||||
&position,
|
||||
Some(&[
|
||||
bounds.left_bottom_uv(size),
|
||||
bounds.right_bottom_uv(size),
|
||||
bounds.right_top_uv(size),
|
||||
bounds.left_top_uv(size),
|
||||
]),
|
||||
);
|
||||
drawing_context.commit(
|
||||
clip_bounds,
|
||||
Brush::Solid(color),
|
||||
CommandTexture::Texture(texture),
|
||||
material,
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
drawing_context.push_rect_filled(&position, None);
|
||||
drawing_context.commit(
|
||||
clip_bounds,
|
||||
Brush::Solid(color),
|
||||
CommandTexture::None,
|
||||
material,
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
drawing_context.push_rect_filled(&position, None);
|
||||
drawing_context.commit(
|
||||
clip_bounds,
|
||||
Brush::Solid(color),
|
||||
CommandTexture::None,
|
||||
material,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelPreview {
|
||||
fn sync_to_state(&mut self) {
|
||||
self.sync_handle_text();
|
||||
let bounds = self.state.lock().stamp.bounding_rect();
|
||||
let Some(bounds) = *bounds else {
|
||||
self.transform = Matrix3::identity();
|
||||
return;
|
||||
};
|
||||
let t = self.tile_size.cast::<f32>();
|
||||
let view_size = self.actual_local_size.get();
|
||||
let bounds_size = bounds.size.cast::<f32>();
|
||||
let bounds_size = Vector2::new(bounds_size.x * t.x, bounds_size.y * t.y);
|
||||
let bounds_position = bounds.position.cast::<f32>();
|
||||
let bounds_position = Vector2::new(bounds_position.x * t.x, bounds_position.y * t.y);
|
||||
let mid_point = bounds_position + bounds_size * 0.5;
|
||||
let zoom_x = view_size.x / bounds_size.x;
|
||||
let zoom_y = view_size.y / bounds_size.y;
|
||||
let zoom = zoom_x.min(zoom_y);
|
||||
let scale = Vector2::new(zoom, -zoom);
|
||||
let translate =
|
||||
view_size * 0.5 - Vector2::new(mid_point.x * scale.x, mid_point.y * scale.y);
|
||||
self.transform =
|
||||
Matrix3::new_translation(&translate) * Matrix3::new_nonuniform_scaling(&scale);
|
||||
}
|
||||
fn sync_handle_text(&mut self) {
|
||||
let text = self.get_handle_text();
|
||||
self.handle_text_size = self.handle_text.set_text(text).build();
|
||||
}
|
||||
fn get_handle_text(&self) -> String {
|
||||
let state = self.state.lock();
|
||||
let Some(mut tile_set) = state.tile_set.as_ref().map(|t| t.state()) else {
|
||||
return "".into();
|
||||
};
|
||||
let Some(tile_set) = tile_set.data() else {
|
||||
return "".into();
|
||||
};
|
||||
let stamp = &state.stamp;
|
||||
let mut iter = stamp.tile_iter();
|
||||
let Some(handle) = iter.next() else {
|
||||
return "".into();
|
||||
};
|
||||
if iter.next().is_some() {
|
||||
return "".into();
|
||||
}
|
||||
let transform = stamp.transformation();
|
||||
if let Some(handle) = tile_set.get_transformed_version(transform, handle) {
|
||||
handle.to_string()
|
||||
} else {
|
||||
format!("{handle}*")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Control for PanelPreview {
|
||||
fn draw(&self, ctx: &mut DrawingContext) {
|
||||
let bounds = self.bounding_rect();
|
||||
ctx.push_rect_filled(&bounds, None);
|
||||
ctx.commit(
|
||||
self.clip_bounds(),
|
||||
self.widget.background(),
|
||||
CommandTexture::None,
|
||||
&self.material,
|
||||
None,
|
||||
);
|
||||
let state = self.state.lock();
|
||||
let stamp = &state.stamp;
|
||||
let Some(mut tile_set) = state.tile_set.as_ref().map(|t| t.state()) else {
|
||||
return;
|
||||
};
|
||||
let Some(tile_set) = tile_set.data() else {
|
||||
return;
|
||||
};
|
||||
|
||||
ctx.transform_stack
|
||||
.push(self.visual_transform() * self.transform);
|
||||
|
||||
let time = ctx.elapsed_time;
|
||||
|
||||
for (pos, &StampElement { handle, .. }) in stamp.iter() {
|
||||
let handle = tile_set
|
||||
.get_animated_version(time, handle)
|
||||
.unwrap_or(handle);
|
||||
let data = tile_set
|
||||
.get_transformed_render_data(stamp.transformation(), handle)
|
||||
.unwrap_or_else(TileRenderData::missing_data);
|
||||
let t = self.tile_size;
|
||||
let position = Vector2::new(pos.x as f32 * t.x, pos.y as f32 * t.y);
|
||||
let rect = Rect { position, size: t };
|
||||
draw_tile(rect, self.clip_bounds(), &data, &self.material, ctx);
|
||||
}
|
||||
|
||||
ctx.transform_stack.pop();
|
||||
let position = bounds.right_bottom_corner() - self.handle_text_size;
|
||||
let rect = Rect {
|
||||
position,
|
||||
size: self.handle_text_size,
|
||||
};
|
||||
ctx.push_rect_filled(&rect, None);
|
||||
ctx.commit(
|
||||
self.clip_bounds(),
|
||||
Brush::Solid(Color::from_rgba(0, 0, 0, 200)),
|
||||
CommandTexture::None,
|
||||
&self.material,
|
||||
None,
|
||||
);
|
||||
ctx.draw_text(
|
||||
self.clip_bounds(),
|
||||
position,
|
||||
&self.material,
|
||||
&self.handle_text,
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
|
||||
self.widget.handle_routed_message(ui, message);
|
||||
if message.destination() == self.handle()
|
||||
&& message.direction() == MessageDirection::ToWidget
|
||||
{
|
||||
if let Some(PaletteMessage::SyncToState) = message.data::<PaletteMessage>() {
|
||||
self.sync_to_state();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PanelPreviewBuilder {
|
||||
widget_builder: WidgetBuilder,
|
||||
state: TileDrawStateRef,
|
||||
}
|
||||
|
||||
impl PanelPreviewBuilder {
|
||||
pub fn new(widget_builder: WidgetBuilder, state: TileDrawStateRef) -> Self {
|
||||
Self {
|
||||
widget_builder,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
ctx.add_node(UiNode::new(PanelPreview {
|
||||
widget: self.widget_builder.with_clip_to_bounds(false).build(ctx),
|
||||
state: self.state,
|
||||
tile_size: Vector2::repeat(32.0),
|
||||
transform: Matrix3::identity(),
|
||||
handle_text: FormattedTextBuilder::new(ctx.inner().default_font.clone())
|
||||
.with_constraint(Vector2::new(f32::INFINITY, f32::INFINITY))
|
||||
.with_brush(Brush::Solid(Color::WHITE))
|
||||
.build(),
|
||||
handle_text_size: Vector2::default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//! The preview widget of the tile map control panel. This allows the user to see the
|
||||
//! currently selected tile stamp, including whatever transformations have been applied
|
||||
//! to the stamp.
|
||||
|
||||
use super::*;
|
||||
use crate::fyrox::{
|
||||
core::{
|
||||
algebra::{Matrix3, Vector2},
|
||||
math::Rect,
|
||||
pool::Handle,
|
||||
reflect::prelude::*,
|
||||
type_traits::prelude::*,
|
||||
visitor::prelude::*,
|
||||
},
|
||||
gui::formatted_text::{FormattedText, FormattedTextBuilder},
|
||||
gui::{
|
||||
brush::Brush,
|
||||
define_widget_deref,
|
||||
draw::{CommandTexture, Draw, DrawingContext},
|
||||
message::UiMessage,
|
||||
widget::{Widget, WidgetBuilder},
|
||||
BuildContext, Control, UiNode, UserInterface,
|
||||
},
|
||||
resource::texture::TextureKind,
|
||||
scene::tilemap::{TileRenderData, TileSource},
|
||||
};
|
||||
|
||||
use fyrox::material::MaterialResource;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
/// The preview widget of the tile map control panel. This allows the user to see the
|
||||
/// currently selected tile stamp, including whatever transformations have been applied
|
||||
/// to the stamp.
|
||||
#[derive(Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
|
||||
#[type_uuid(id = "5356a864-c026-4bd7-a4b1-30bacf77d8fa")]
|
||||
#[reflect(derived_type = "UiNode")]
|
||||
pub struct PanelPreview {
|
||||
widget: Widget,
|
||||
/// The tile editing state that is shared with palette widgets, the tile map interaction mode,
|
||||
/// the tile map control panel, and others. It allows this widget access to determine
|
||||
/// what stamp to show to the user.
|
||||
#[reflect(hidden)]
|
||||
#[visit(skip)]
|
||||
pub state: TileDrawStateRef,
|
||||
tile_size: Vector2<f32>,
|
||||
transform: Matrix3<f32>,
|
||||
/// This text is used to show the tile handle when a single tile is selected.
|
||||
#[visit(skip)]
|
||||
#[reflect(hidden)]
|
||||
handle_text: FormattedText,
|
||||
#[visit(skip)]
|
||||
#[reflect(hidden)]
|
||||
/// This is the size of the handle text, for the purposes of positioning the text
|
||||
/// within the widget.
|
||||
handle_text_size: Vector2<f32>,
|
||||
}
|
||||
|
||||
define_widget_deref!(PanelPreview);
|
||||
|
||||
fn draw_tile(
|
||||
position: Rect<f32>,
|
||||
clip_bounds: Rect<f32>,
|
||||
tile: &TileRenderData,
|
||||
material: &MaterialResource,
|
||||
drawing_context: &mut DrawingContext,
|
||||
) {
|
||||
if tile.is_empty() {
|
||||
return;
|
||||
}
|
||||
let color = tile.color;
|
||||
if let Some(material_bounds) = &tile.material_bounds {
|
||||
if let Some(texture) = material_bounds
|
||||
.material
|
||||
.state()
|
||||
.data()
|
||||
.and_then(|m| m.texture("diffuseTexture"))
|
||||
{
|
||||
let kind = texture.data_ref().kind();
|
||||
if let TextureKind::Rectangle { width, height } = kind {
|
||||
let size = Vector2::new(width, height);
|
||||
let bounds = &material_bounds.bounds;
|
||||
drawing_context.push_rect_filled(
|
||||
&position,
|
||||
Some(&[
|
||||
bounds.left_bottom_uv(size),
|
||||
bounds.right_bottom_uv(size),
|
||||
bounds.right_top_uv(size),
|
||||
bounds.left_top_uv(size),
|
||||
]),
|
||||
);
|
||||
drawing_context.commit(
|
||||
clip_bounds,
|
||||
Brush::Solid(color),
|
||||
CommandTexture::Texture(texture),
|
||||
material,
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
drawing_context.push_rect_filled(&position, None);
|
||||
drawing_context.commit(
|
||||
clip_bounds,
|
||||
Brush::Solid(color),
|
||||
CommandTexture::None,
|
||||
material,
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
drawing_context.push_rect_filled(&position, None);
|
||||
drawing_context.commit(
|
||||
clip_bounds,
|
||||
Brush::Solid(color),
|
||||
CommandTexture::None,
|
||||
material,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelPreview {
|
||||
fn sync_to_state(&mut self) {
|
||||
self.sync_handle_text();
|
||||
let bounds = self.state.lock().stamp.bounding_rect();
|
||||
let Some(bounds) = *bounds else {
|
||||
self.transform = Matrix3::identity();
|
||||
return;
|
||||
};
|
||||
let t = self.tile_size.cast::<f32>();
|
||||
let view_size = self.actual_local_size.get();
|
||||
let bounds_size = bounds.size.cast::<f32>();
|
||||
let bounds_size = Vector2::new(bounds_size.x * t.x, bounds_size.y * t.y);
|
||||
let bounds_position = bounds.position.cast::<f32>();
|
||||
let bounds_position = Vector2::new(bounds_position.x * t.x, bounds_position.y * t.y);
|
||||
let mid_point = bounds_position + bounds_size * 0.5;
|
||||
let zoom_x = view_size.x / bounds_size.x;
|
||||
let zoom_y = view_size.y / bounds_size.y;
|
||||
let zoom = zoom_x.min(zoom_y);
|
||||
let scale = Vector2::new(zoom, -zoom);
|
||||
let translate =
|
||||
view_size * 0.5 - Vector2::new(mid_point.x * scale.x, mid_point.y * scale.y);
|
||||
self.transform =
|
||||
Matrix3::new_translation(&translate) * Matrix3::new_nonuniform_scaling(&scale);
|
||||
}
|
||||
fn sync_handle_text(&mut self) {
|
||||
let text = self.get_handle_text();
|
||||
self.handle_text_size = self.handle_text.set_text(text).build();
|
||||
}
|
||||
fn get_handle_text(&self) -> String {
|
||||
let state = self.state.lock();
|
||||
let Some(mut tile_set) = state.tile_set.as_ref().map(|t| t.state()) else {
|
||||
return "".into();
|
||||
};
|
||||
let Some(tile_set) = tile_set.data() else {
|
||||
return "".into();
|
||||
};
|
||||
let stamp = &state.stamp;
|
||||
let mut iter = stamp.tile_iter();
|
||||
let Some(handle) = iter.next() else {
|
||||
return "".into();
|
||||
};
|
||||
if iter.next().is_some() {
|
||||
return "".into();
|
||||
}
|
||||
let transform = stamp.transformation();
|
||||
if let Some(handle) = tile_set.get_transformed_version(transform, handle) {
|
||||
handle.to_string()
|
||||
} else {
|
||||
format!("{handle}*")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Control for PanelPreview {
|
||||
fn draw(&self, ctx: &mut DrawingContext) {
|
||||
let bounds = self.bounding_rect();
|
||||
ctx.push_rect_filled(&bounds, None);
|
||||
ctx.commit(
|
||||
self.clip_bounds(),
|
||||
self.widget.background(),
|
||||
CommandTexture::None,
|
||||
&self.material,
|
||||
None,
|
||||
);
|
||||
let state = self.state.lock();
|
||||
let stamp = &state.stamp;
|
||||
let Some(mut tile_set) = state.tile_set.as_ref().map(|t| t.state()) else {
|
||||
return;
|
||||
};
|
||||
let Some(tile_set) = tile_set.data() else {
|
||||
return;
|
||||
};
|
||||
|
||||
ctx.transform_stack
|
||||
.push(self.visual_transform() * self.transform);
|
||||
|
||||
let time = ctx.elapsed_time;
|
||||
|
||||
for (pos, &StampElement { handle, .. }) in stamp.iter() {
|
||||
let handle = tile_set
|
||||
.get_animated_version(time, handle)
|
||||
.unwrap_or(handle);
|
||||
let data = tile_set
|
||||
.get_transformed_render_data(stamp.transformation(), handle)
|
||||
.unwrap_or_else(TileRenderData::missing_data);
|
||||
let t = self.tile_size;
|
||||
let position = Vector2::new(pos.x as f32 * t.x, pos.y as f32 * t.y);
|
||||
let rect = Rect { position, size: t };
|
||||
draw_tile(rect, self.clip_bounds(), &data, &self.material, ctx);
|
||||
}
|
||||
|
||||
ctx.transform_stack.pop();
|
||||
let position = bounds.right_bottom_corner() - self.handle_text_size;
|
||||
let rect = Rect {
|
||||
position,
|
||||
size: self.handle_text_size,
|
||||
};
|
||||
ctx.push_rect_filled(&rect, None);
|
||||
ctx.commit(
|
||||
self.clip_bounds(),
|
||||
Brush::Solid(Color::from_rgba(0, 0, 0, 200)),
|
||||
CommandTexture::None,
|
||||
&self.material,
|
||||
None,
|
||||
);
|
||||
ctx.draw_text(
|
||||
self.clip_bounds(),
|
||||
position,
|
||||
&self.material,
|
||||
&self.handle_text,
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
|
||||
self.widget.handle_routed_message(ui, message);
|
||||
if message.destination() == self.handle()
|
||||
&& message.direction() == MessageDirection::ToWidget
|
||||
{
|
||||
if let Some(PaletteMessage::SyncToState) = message.data::<PaletteMessage>() {
|
||||
self.sync_to_state();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PanelPreviewBuilder {
|
||||
widget_builder: WidgetBuilder,
|
||||
state: TileDrawStateRef,
|
||||
}
|
||||
|
||||
impl PanelPreviewBuilder {
|
||||
pub fn new(widget_builder: WidgetBuilder, state: TileDrawStateRef) -> Self {
|
||||
Self {
|
||||
widget_builder,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
ctx.add_node(UiNode::new(PanelPreview {
|
||||
widget: self.widget_builder.with_clip_to_bounds(false).build(ctx),
|
||||
state: self.state,
|
||||
tile_size: Vector2::repeat(32.0),
|
||||
transform: Matrix3::identity(),
|
||||
handle_text: FormattedTextBuilder::new(ctx.inner().default_font.clone())
|
||||
.with_constraint(Vector2::new(f32::INFINITY, f32::INFINITY))
|
||||
.with_brush(Brush::Solid(Color::WHITE))
|
||||
.build(),
|
||||
handle_text_size: Vector2::default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,309 +1,309 @@
|
||||
// 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 super::*;
|
||||
|
||||
use fyrox::{
|
||||
core::{pool::Handle, reflect::prelude::*, type_traits::prelude::*, visitor::prelude::*},
|
||||
gui::{
|
||||
button::ButtonMessage,
|
||||
define_constructor, define_widget_deref,
|
||||
grid::{Column, GridBuilder, Row},
|
||||
message::{MessageDirection, UiMessage},
|
||||
stack_panel::StackPanelBuilder,
|
||||
vec::{Vec2EditorMessage, VecEditorBuilder},
|
||||
widget::{Widget, WidgetBuilder, WidgetMessage},
|
||||
BuildContext, Control, UiNode, UserInterface,
|
||||
},
|
||||
scene::tilemap::{tileset::TileBounds, OrthoTransform},
|
||||
};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TileBoundsMessage {
|
||||
Value(Option<TileBounds>),
|
||||
Turn(i8),
|
||||
FlipX,
|
||||
FlipY,
|
||||
}
|
||||
|
||||
impl TileBoundsMessage {
|
||||
define_constructor!(TileBoundsMessage:Value => fn value(Option<TileBounds>), layout: false);
|
||||
define_constructor!(TileBoundsMessage:Turn => fn turn(i8), layout: false);
|
||||
define_constructor!(TileBoundsMessage:FlipX => fn flip_x(), layout: false);
|
||||
define_constructor!(TileBoundsMessage:FlipY => fn flip_y(), layout: false);
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
|
||||
#[type_uuid(id = "1e600103-6516-4c5a-a30b-f90f64fc9623")]
|
||||
#[reflect(derived_type = "UiNode")]
|
||||
pub struct TileBoundsEditor {
|
||||
widget: Widget,
|
||||
pub value: Option<TileBounds>,
|
||||
pub value_area: Handle<UiNode>,
|
||||
pub left_top: Handle<UiNode>,
|
||||
pub left_bottom: Handle<UiNode>,
|
||||
pub right_top: Handle<UiNode>,
|
||||
pub right_bottom: Handle<UiNode>,
|
||||
pub button_left: Handle<UiNode>,
|
||||
pub button_right: Handle<UiNode>,
|
||||
pub button_flip_x: Handle<UiNode>,
|
||||
pub button_flip_y: Handle<UiNode>,
|
||||
}
|
||||
|
||||
define_widget_deref!(TileBoundsEditor);
|
||||
|
||||
impl TileBoundsEditor {
|
||||
fn get_field(&self, index: usize) -> Handle<UiNode> {
|
||||
match index {
|
||||
0 => self.left_bottom,
|
||||
1 => self.right_bottom,
|
||||
2 => self.right_top,
|
||||
3 => self.left_top,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
fn turn(&mut self, amount: i8, ui: &mut UserInterface) {
|
||||
if let Some(value) = self.value.clone().map(|v| v.rotated(amount)) {
|
||||
ui.send_message(TileBoundsMessage::value(
|
||||
self.handle,
|
||||
MessageDirection::ToWidget,
|
||||
Some(value),
|
||||
));
|
||||
} else {
|
||||
ui.send_message(TileBoundsMessage::turn(
|
||||
self.handle,
|
||||
MessageDirection::FromWidget,
|
||||
amount,
|
||||
));
|
||||
}
|
||||
}
|
||||
fn flip_x(&mut self, ui: &mut UserInterface) {
|
||||
if let Some(value) = self.value.clone().map(|v| v.x_flipped()) {
|
||||
ui.send_message(TileBoundsMessage::value(
|
||||
self.handle,
|
||||
MessageDirection::ToWidget,
|
||||
Some(value),
|
||||
));
|
||||
} else {
|
||||
ui.send_message(TileBoundsMessage::flip_x(
|
||||
self.handle,
|
||||
MessageDirection::FromWidget,
|
||||
));
|
||||
}
|
||||
}
|
||||
fn flip_y(&mut self, ui: &mut UserInterface) {
|
||||
if let Some(value) = self.value.clone().map(|v| v.y_flipped()) {
|
||||
ui.send_message(TileBoundsMessage::value(
|
||||
self.handle,
|
||||
MessageDirection::ToWidget,
|
||||
Some(value),
|
||||
));
|
||||
} else {
|
||||
ui.send_message(TileBoundsMessage::flip_y(
|
||||
self.handle,
|
||||
MessageDirection::FromWidget,
|
||||
));
|
||||
}
|
||||
}
|
||||
fn set_value(
|
||||
&mut self,
|
||||
new_value: &Option<TileBounds>,
|
||||
message: &UiMessage,
|
||||
ui: &mut UserInterface,
|
||||
) {
|
||||
match (&self.value, new_value) {
|
||||
(None, None) => (),
|
||||
(Some(_), None) => {
|
||||
ui.send_message(WidgetMessage::visibility(
|
||||
self.value_area,
|
||||
MessageDirection::ToWidget,
|
||||
false,
|
||||
));
|
||||
self.value = None;
|
||||
ui.send_message(message.reverse());
|
||||
}
|
||||
(None, Some(v)) => {
|
||||
ui.send_message(WidgetMessage::visibility(
|
||||
self.value_area,
|
||||
MessageDirection::ToWidget,
|
||||
true,
|
||||
));
|
||||
self.value = Some(v.clone());
|
||||
for i in 0..4 {
|
||||
ui.send_message(Vec2EditorMessage::value(
|
||||
self.get_field(i),
|
||||
MessageDirection::ToWidget,
|
||||
v.get(i),
|
||||
));
|
||||
}
|
||||
ui.send_message(message.reverse());
|
||||
}
|
||||
(Some(v0), Some(v1)) => {
|
||||
let mut has_changed = false;
|
||||
for i in 0..4 {
|
||||
if v0.get(i) != v1.get(i) {
|
||||
has_changed = true;
|
||||
ui.send_message(Vec2EditorMessage::value(
|
||||
self.get_field(i),
|
||||
MessageDirection::ToWidget,
|
||||
v1.get(i),
|
||||
));
|
||||
}
|
||||
}
|
||||
self.value = Some(v1.clone());
|
||||
if has_changed {
|
||||
ui.send_message(message.reverse());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Control for TileBoundsEditor {
|
||||
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
|
||||
self.widget.handle_routed_message(ui, message);
|
||||
if let Some(Vec2EditorMessage::<u32>::Value(v)) = message.data() {
|
||||
if message.direction() == MessageDirection::FromWidget {
|
||||
for i in 0..4 {
|
||||
if self.get_field(i) == message.destination() {
|
||||
let mut value = self.value.clone().unwrap_or_default();
|
||||
*value.get_mut(i) = *v;
|
||||
// This does not trigger a Value FromWidget message from the VecEditor
|
||||
// because the values of the fields have not changed.
|
||||
ui.send_message(TileBoundsMessage::value(
|
||||
self.handle,
|
||||
MessageDirection::ToWidget,
|
||||
Some(value),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(TileBoundsMessage::Value(v)) = message.data() {
|
||||
if message.direction() == MessageDirection::ToWidget {
|
||||
self.set_value(v, message, ui);
|
||||
}
|
||||
} else if let Some(ButtonMessage::Click) = message.data() {
|
||||
if message.destination() == self.button_left {
|
||||
self.turn(1, ui)
|
||||
} else if message.destination() == self.button_right {
|
||||
self.turn(-1, ui)
|
||||
} else if message.destination() == self.button_flip_x {
|
||||
self.flip_x(ui)
|
||||
} else if message.destination() == self.button_flip_y {
|
||||
self.flip_y(ui)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TileBoundsEditorBuilder {
|
||||
widget_builder: WidgetBuilder,
|
||||
}
|
||||
|
||||
impl TileBoundsEditorBuilder {
|
||||
pub fn new(widget_builder: WidgetBuilder) -> Self {
|
||||
Self { widget_builder }
|
||||
}
|
||||
pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
let left_top = VecEditorBuilder::<u32, 2>::new(WidgetBuilder::new()).build(ctx);
|
||||
let right_top =
|
||||
VecEditorBuilder::<u32, 2>::new(WidgetBuilder::new().on_column(1)).build(ctx);
|
||||
let left_bottom =
|
||||
VecEditorBuilder::<u32, 2>::new(WidgetBuilder::new().on_row(1)).build(ctx);
|
||||
let right_bottom =
|
||||
VecEditorBuilder::<u32, 2>::new(WidgetBuilder::new().on_row(1).on_column(1)).build(ctx);
|
||||
let value_area = GridBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_visibility(false)
|
||||
.with_child(left_top)
|
||||
.with_child(right_top)
|
||||
.with_child(left_bottom)
|
||||
.with_child(right_bottom),
|
||||
)
|
||||
.add_column(Column::stretch())
|
||||
.add_column(Column::stretch())
|
||||
.add_row(Row::auto())
|
||||
.add_row(Row::auto())
|
||||
.build(ctx);
|
||||
let width = 20.0;
|
||||
let height = 20.0;
|
||||
let left_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
TURN_LEFT_IMAGE.clone(),
|
||||
"Rotate left 90 degrees.",
|
||||
Some(0),
|
||||
);
|
||||
let right_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
TURN_RIGHT_IMAGE.clone(),
|
||||
"Rotate right 90 degrees.",
|
||||
Some(0),
|
||||
);
|
||||
let flip_x_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
FLIP_X_IMAGE.clone(),
|
||||
"Flip along the x-axis.",
|
||||
Some(0),
|
||||
);
|
||||
let flip_y_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
FLIP_Y_IMAGE.clone(),
|
||||
"Flip along the y-axis.",
|
||||
Some(0),
|
||||
);
|
||||
let buttons = StackPanelBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(left_button)
|
||||
.with_child(right_button)
|
||||
.with_child(flip_x_button)
|
||||
.with_child(flip_y_button),
|
||||
)
|
||||
.with_orientation(fyrox::gui::Orientation::Horizontal)
|
||||
.build(ctx);
|
||||
let content = StackPanelBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(value_area)
|
||||
.with_child(buttons),
|
||||
)
|
||||
.build(ctx);
|
||||
ctx.add_node(UiNode::new(TileBoundsEditor {
|
||||
widget: self.widget_builder.with_child(content).build(ctx),
|
||||
value: None,
|
||||
value_area,
|
||||
left_top,
|
||||
right_top,
|
||||
left_bottom,
|
||||
right_bottom,
|
||||
button_left: left_button,
|
||||
button_right: right_button,
|
||||
button_flip_x: flip_x_button,
|
||||
button_flip_y: flip_y_button,
|
||||
}))
|
||||
}
|
||||
}
|
||||
// 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 super::*;
|
||||
|
||||
use fyrox::{
|
||||
core::{pool::Handle, reflect::prelude::*, type_traits::prelude::*, visitor::prelude::*},
|
||||
gui::{
|
||||
button::ButtonMessage,
|
||||
define_constructor, define_widget_deref,
|
||||
grid::{Column, GridBuilder, Row},
|
||||
message::{MessageDirection, UiMessage},
|
||||
stack_panel::StackPanelBuilder,
|
||||
vec::{Vec2EditorMessage, VecEditorBuilder},
|
||||
widget::{Widget, WidgetBuilder, WidgetMessage},
|
||||
BuildContext, Control, UiNode, UserInterface,
|
||||
},
|
||||
scene::tilemap::{tileset::TileBounds, OrthoTransform},
|
||||
};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TileBoundsMessage {
|
||||
Value(Option<TileBounds>),
|
||||
Turn(i8),
|
||||
FlipX,
|
||||
FlipY,
|
||||
}
|
||||
|
||||
impl TileBoundsMessage {
|
||||
define_constructor!(TileBoundsMessage:Value => fn value(Option<TileBounds>), layout: false);
|
||||
define_constructor!(TileBoundsMessage:Turn => fn turn(i8), layout: false);
|
||||
define_constructor!(TileBoundsMessage:FlipX => fn flip_x(), layout: false);
|
||||
define_constructor!(TileBoundsMessage:FlipY => fn flip_y(), layout: false);
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
|
||||
#[type_uuid(id = "1e600103-6516-4c5a-a30b-f90f64fc9623")]
|
||||
#[reflect(derived_type = "UiNode")]
|
||||
pub struct TileBoundsEditor {
|
||||
widget: Widget,
|
||||
pub value: Option<TileBounds>,
|
||||
pub value_area: Handle<UiNode>,
|
||||
pub left_top: Handle<UiNode>,
|
||||
pub left_bottom: Handle<UiNode>,
|
||||
pub right_top: Handle<UiNode>,
|
||||
pub right_bottom: Handle<UiNode>,
|
||||
pub button_left: Handle<UiNode>,
|
||||
pub button_right: Handle<UiNode>,
|
||||
pub button_flip_x: Handle<UiNode>,
|
||||
pub button_flip_y: Handle<UiNode>,
|
||||
}
|
||||
|
||||
define_widget_deref!(TileBoundsEditor);
|
||||
|
||||
impl TileBoundsEditor {
|
||||
fn get_field(&self, index: usize) -> Handle<UiNode> {
|
||||
match index {
|
||||
0 => self.left_bottom,
|
||||
1 => self.right_bottom,
|
||||
2 => self.right_top,
|
||||
3 => self.left_top,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
fn turn(&mut self, amount: i8, ui: &mut UserInterface) {
|
||||
if let Some(value) = self.value.clone().map(|v| v.rotated(amount)) {
|
||||
ui.send_message(TileBoundsMessage::value(
|
||||
self.handle,
|
||||
MessageDirection::ToWidget,
|
||||
Some(value),
|
||||
));
|
||||
} else {
|
||||
ui.send_message(TileBoundsMessage::turn(
|
||||
self.handle,
|
||||
MessageDirection::FromWidget,
|
||||
amount,
|
||||
));
|
||||
}
|
||||
}
|
||||
fn flip_x(&mut self, ui: &mut UserInterface) {
|
||||
if let Some(value) = self.value.clone().map(|v| v.x_flipped()) {
|
||||
ui.send_message(TileBoundsMessage::value(
|
||||
self.handle,
|
||||
MessageDirection::ToWidget,
|
||||
Some(value),
|
||||
));
|
||||
} else {
|
||||
ui.send_message(TileBoundsMessage::flip_x(
|
||||
self.handle,
|
||||
MessageDirection::FromWidget,
|
||||
));
|
||||
}
|
||||
}
|
||||
fn flip_y(&mut self, ui: &mut UserInterface) {
|
||||
if let Some(value) = self.value.clone().map(|v| v.y_flipped()) {
|
||||
ui.send_message(TileBoundsMessage::value(
|
||||
self.handle,
|
||||
MessageDirection::ToWidget,
|
||||
Some(value),
|
||||
));
|
||||
} else {
|
||||
ui.send_message(TileBoundsMessage::flip_y(
|
||||
self.handle,
|
||||
MessageDirection::FromWidget,
|
||||
));
|
||||
}
|
||||
}
|
||||
fn set_value(
|
||||
&mut self,
|
||||
new_value: &Option<TileBounds>,
|
||||
message: &UiMessage,
|
||||
ui: &mut UserInterface,
|
||||
) {
|
||||
match (&self.value, new_value) {
|
||||
(None, None) => (),
|
||||
(Some(_), None) => {
|
||||
ui.send_message(WidgetMessage::visibility(
|
||||
self.value_area,
|
||||
MessageDirection::ToWidget,
|
||||
false,
|
||||
));
|
||||
self.value = None;
|
||||
ui.send_message(message.reverse());
|
||||
}
|
||||
(None, Some(v)) => {
|
||||
ui.send_message(WidgetMessage::visibility(
|
||||
self.value_area,
|
||||
MessageDirection::ToWidget,
|
||||
true,
|
||||
));
|
||||
self.value = Some(v.clone());
|
||||
for i in 0..4 {
|
||||
ui.send_message(Vec2EditorMessage::value(
|
||||
self.get_field(i),
|
||||
MessageDirection::ToWidget,
|
||||
v.get(i),
|
||||
));
|
||||
}
|
||||
ui.send_message(message.reverse());
|
||||
}
|
||||
(Some(v0), Some(v1)) => {
|
||||
let mut has_changed = false;
|
||||
for i in 0..4 {
|
||||
if v0.get(i) != v1.get(i) {
|
||||
has_changed = true;
|
||||
ui.send_message(Vec2EditorMessage::value(
|
||||
self.get_field(i),
|
||||
MessageDirection::ToWidget,
|
||||
v1.get(i),
|
||||
));
|
||||
}
|
||||
}
|
||||
self.value = Some(v1.clone());
|
||||
if has_changed {
|
||||
ui.send_message(message.reverse());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Control for TileBoundsEditor {
|
||||
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
|
||||
self.widget.handle_routed_message(ui, message);
|
||||
if let Some(Vec2EditorMessage::<u32>::Value(v)) = message.data() {
|
||||
if message.direction() == MessageDirection::FromWidget {
|
||||
for i in 0..4 {
|
||||
if self.get_field(i) == message.destination() {
|
||||
let mut value = self.value.clone().unwrap_or_default();
|
||||
*value.get_mut(i) = *v;
|
||||
// This does not trigger a Value FromWidget message from the VecEditor
|
||||
// because the values of the fields have not changed.
|
||||
ui.send_message(TileBoundsMessage::value(
|
||||
self.handle,
|
||||
MessageDirection::ToWidget,
|
||||
Some(value),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(TileBoundsMessage::Value(v)) = message.data() {
|
||||
if message.direction() == MessageDirection::ToWidget {
|
||||
self.set_value(v, message, ui);
|
||||
}
|
||||
} else if let Some(ButtonMessage::Click) = message.data() {
|
||||
if message.destination() == self.button_left {
|
||||
self.turn(1, ui)
|
||||
} else if message.destination() == self.button_right {
|
||||
self.turn(-1, ui)
|
||||
} else if message.destination() == self.button_flip_x {
|
||||
self.flip_x(ui)
|
||||
} else if message.destination() == self.button_flip_y {
|
||||
self.flip_y(ui)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TileBoundsEditorBuilder {
|
||||
widget_builder: WidgetBuilder,
|
||||
}
|
||||
|
||||
impl TileBoundsEditorBuilder {
|
||||
pub fn new(widget_builder: WidgetBuilder) -> Self {
|
||||
Self { widget_builder }
|
||||
}
|
||||
pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
|
||||
let left_top = VecEditorBuilder::<u32, 2>::new(WidgetBuilder::new()).build(ctx);
|
||||
let right_top =
|
||||
VecEditorBuilder::<u32, 2>::new(WidgetBuilder::new().on_column(1)).build(ctx);
|
||||
let left_bottom =
|
||||
VecEditorBuilder::<u32, 2>::new(WidgetBuilder::new().on_row(1)).build(ctx);
|
||||
let right_bottom =
|
||||
VecEditorBuilder::<u32, 2>::new(WidgetBuilder::new().on_row(1).on_column(1)).build(ctx);
|
||||
let value_area = GridBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_visibility(false)
|
||||
.with_child(left_top)
|
||||
.with_child(right_top)
|
||||
.with_child(left_bottom)
|
||||
.with_child(right_bottom),
|
||||
)
|
||||
.add_column(Column::stretch())
|
||||
.add_column(Column::stretch())
|
||||
.add_row(Row::auto())
|
||||
.add_row(Row::auto())
|
||||
.build(ctx);
|
||||
let width = 20.0;
|
||||
let height = 20.0;
|
||||
let left_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
TURN_LEFT_IMAGE.clone(),
|
||||
"Rotate left 90 degrees.",
|
||||
Some(0),
|
||||
);
|
||||
let right_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
TURN_RIGHT_IMAGE.clone(),
|
||||
"Rotate right 90 degrees.",
|
||||
Some(0),
|
||||
);
|
||||
let flip_x_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
FLIP_X_IMAGE.clone(),
|
||||
"Flip along the x-axis.",
|
||||
Some(0),
|
||||
);
|
||||
let flip_y_button = make_drawing_mode_button(
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
FLIP_Y_IMAGE.clone(),
|
||||
"Flip along the y-axis.",
|
||||
Some(0),
|
||||
);
|
||||
let buttons = StackPanelBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(left_button)
|
||||
.with_child(right_button)
|
||||
.with_child(flip_x_button)
|
||||
.with_child(flip_y_button),
|
||||
)
|
||||
.with_orientation(fyrox::gui::Orientation::Horizontal)
|
||||
.build(ctx);
|
||||
let content = StackPanelBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_child(value_area)
|
||||
.with_child(buttons),
|
||||
)
|
||||
.build(ctx);
|
||||
ctx.add_node(UiNode::new(TileBoundsEditor {
|
||||
widget: self.widget_builder.with_child(content).build(ctx),
|
||||
value: None,
|
||||
value_area,
|
||||
left_top,
|
||||
right_top,
|
||||
left_bottom,
|
||||
right_bottom,
|
||||
button_left: left_button,
|
||||
button_right: right_button,
|
||||
button_flip_x: flip_x_button,
|
||||
button_flip_y: flip_y_button,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1232
-1232
File diff suppressed because it is too large
Load Diff
+1202
-1202
File diff suppressed because it is too large
Load Diff
+612
-612
File diff suppressed because it is too large
Load Diff
+810
-810
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,229 +1,229 @@
|
||||
// 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;
|
||||
|
||||
pub trait CurvePoint {
|
||||
fn x(&self) -> f32;
|
||||
fn y(&self) -> f32;
|
||||
}
|
||||
|
||||
pub fn simplify<P: CurvePoint + Clone + Debug>(
|
||||
points: &[P],
|
||||
epsilon: f32,
|
||||
max_step: f32,
|
||||
) -> Vec<P> {
|
||||
find_important_points(points, epsilon, max_step)
|
||||
.into_iter()
|
||||
.map(|i| points[i].clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn find_important_points<P: CurvePoint + Debug>(
|
||||
points: &[P],
|
||||
epsilon: f32,
|
||||
max_step: f32,
|
||||
) -> Vec<usize> {
|
||||
if points.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut keep_flags: Vec<bool> = Vec::new();
|
||||
keep_flags.resize(points.len(), false);
|
||||
let end = keep_flags.len() - 1;
|
||||
keep_flags[0] = true;
|
||||
keep_flags[end] = true;
|
||||
find_points_in_span(points, keep_flags.as_mut_slice(), 0, end, epsilon);
|
||||
if max_step.is_finite() {
|
||||
limit_step_size(points, keep_flags.as_mut_slice(), max_step);
|
||||
}
|
||||
let mut result: Vec<usize> = Vec::new();
|
||||
for (i, k) in keep_flags.into_iter().enumerate() {
|
||||
if k {
|
||||
result.push(i)
|
||||
}
|
||||
}
|
||||
if result.len() == 2 && f32::abs(points[result[0]].y() - points[result[1]].y()) < epsilon {
|
||||
result.pop();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn limit_step_size<P: CurvePoint>(points: &[P], keep_flags: &mut [bool], max_step: f32) {
|
||||
let end = points.len() - 1;
|
||||
let mut i: usize = 1;
|
||||
while i < end {
|
||||
if keep_flags[i] {
|
||||
i += 1;
|
||||
} else {
|
||||
let next = find_step(i - 1, points, keep_flags, max_step);
|
||||
keep_flags[next] = true;
|
||||
i = usize::max(next + 1, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_step<P: CurvePoint>(
|
||||
start: usize,
|
||||
points: &[P],
|
||||
keep_flags: &mut [bool],
|
||||
max_step: f32,
|
||||
) -> usize {
|
||||
let start_y = points[start].y();
|
||||
for i in start + 1..points.len() {
|
||||
let step = f32::abs(points[i].y() - start_y);
|
||||
if step > max_step {
|
||||
return usize::max(i - 1, start + 1);
|
||||
} else if keep_flags[i] {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
points.len() - 1
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
fn find_points_in_span<P: CurvePoint + Debug>(
|
||||
points: &[P],
|
||||
keep_flags: &mut [bool],
|
||||
start: usize,
|
||||
end: usize,
|
||||
epsilon: f32,
|
||||
) {
|
||||
if end <= start + 1 {
|
||||
return;
|
||||
}
|
||||
let x0 = points[start].x();
|
||||
let y0 = points[start].y();
|
||||
let slope = (points[end].y() - y0) / (points[end].x() - x0);
|
||||
let mut far_point_index: usize = 0;
|
||||
let mut far_point_dist: f32 = 0.0;
|
||||
for i in start + 1..end {
|
||||
let (x, y) = (points[i].x(), points[i].y());
|
||||
let y_line: f32 = y0 + slope * (x - x0);
|
||||
let dist: f32 = (y - y_line).abs();
|
||||
if far_point_dist < dist {
|
||||
far_point_dist = dist;
|
||||
far_point_index = i;
|
||||
}
|
||||
}
|
||||
if far_point_index == 0 || far_point_dist < epsilon {
|
||||
return;
|
||||
}
|
||||
keep_flags[far_point_index] = true;
|
||||
find_points_in_span(points, keep_flags, start, far_point_index, epsilon);
|
||||
find_points_in_span(points, keep_flags, far_point_index, end, epsilon);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
type Point = (f32, f32);
|
||||
impl CurvePoint for Point {
|
||||
fn x(&self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
fn y(&self) -> f32 {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn empty() {
|
||||
let points: Vec<Point> = Vec::new();
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result.len(), 0);
|
||||
}
|
||||
#[test]
|
||||
fn size_1() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0]);
|
||||
}
|
||||
#[test]
|
||||
fn size_2() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 1]);
|
||||
}
|
||||
#[test]
|
||||
fn size_3() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 1, 2]);
|
||||
}
|
||||
#[test]
|
||||
fn size_4() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, -1.0), (3.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 1, 2, 3]);
|
||||
}
|
||||
#[test]
|
||||
fn size_5() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, -1.0), (3.0, 0.0), (4.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 1, 2, 3, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn irregular_x() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (4.0, -1.0), (6.0, 0.0), (10.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 1, 2, 3, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn irregular_x_remove_2() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (4.0, 4.0), (6.0, 2.0), (10.0, -2.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 2, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn remove_middle() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 2]);
|
||||
}
|
||||
#[test]
|
||||
fn remove_all_but_one() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 0.00001), (2.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0]);
|
||||
}
|
||||
#[test]
|
||||
fn remove_2() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.0), (4.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 2, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn small_step_size() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.0), (4.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, 0.5);
|
||||
assert_eq!(result, vec![0, 1, 2, 3, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn large_step_size() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.0), (4.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, 2.0);
|
||||
assert_eq!(result, vec![0, 2, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn mid_step_size() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.5), (4.0, 1.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, 1.0);
|
||||
assert_eq!(result, vec![0, 1, 2, 4]);
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
|
||||
pub trait CurvePoint {
|
||||
fn x(&self) -> f32;
|
||||
fn y(&self) -> f32;
|
||||
}
|
||||
|
||||
pub fn simplify<P: CurvePoint + Clone + Debug>(
|
||||
points: &[P],
|
||||
epsilon: f32,
|
||||
max_step: f32,
|
||||
) -> Vec<P> {
|
||||
find_important_points(points, epsilon, max_step)
|
||||
.into_iter()
|
||||
.map(|i| points[i].clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn find_important_points<P: CurvePoint + Debug>(
|
||||
points: &[P],
|
||||
epsilon: f32,
|
||||
max_step: f32,
|
||||
) -> Vec<usize> {
|
||||
if points.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut keep_flags: Vec<bool> = Vec::new();
|
||||
keep_flags.resize(points.len(), false);
|
||||
let end = keep_flags.len() - 1;
|
||||
keep_flags[0] = true;
|
||||
keep_flags[end] = true;
|
||||
find_points_in_span(points, keep_flags.as_mut_slice(), 0, end, epsilon);
|
||||
if max_step.is_finite() {
|
||||
limit_step_size(points, keep_flags.as_mut_slice(), max_step);
|
||||
}
|
||||
let mut result: Vec<usize> = Vec::new();
|
||||
for (i, k) in keep_flags.into_iter().enumerate() {
|
||||
if k {
|
||||
result.push(i)
|
||||
}
|
||||
}
|
||||
if result.len() == 2 && f32::abs(points[result[0]].y() - points[result[1]].y()) < epsilon {
|
||||
result.pop();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn limit_step_size<P: CurvePoint>(points: &[P], keep_flags: &mut [bool], max_step: f32) {
|
||||
let end = points.len() - 1;
|
||||
let mut i: usize = 1;
|
||||
while i < end {
|
||||
if keep_flags[i] {
|
||||
i += 1;
|
||||
} else {
|
||||
let next = find_step(i - 1, points, keep_flags, max_step);
|
||||
keep_flags[next] = true;
|
||||
i = usize::max(next + 1, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_step<P: CurvePoint>(
|
||||
start: usize,
|
||||
points: &[P],
|
||||
keep_flags: &mut [bool],
|
||||
max_step: f32,
|
||||
) -> usize {
|
||||
let start_y = points[start].y();
|
||||
for i in start + 1..points.len() {
|
||||
let step = f32::abs(points[i].y() - start_y);
|
||||
if step > max_step {
|
||||
return usize::max(i - 1, start + 1);
|
||||
} else if keep_flags[i] {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
points.len() - 1
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
fn find_points_in_span<P: CurvePoint + Debug>(
|
||||
points: &[P],
|
||||
keep_flags: &mut [bool],
|
||||
start: usize,
|
||||
end: usize,
|
||||
epsilon: f32,
|
||||
) {
|
||||
if end <= start + 1 {
|
||||
return;
|
||||
}
|
||||
let x0 = points[start].x();
|
||||
let y0 = points[start].y();
|
||||
let slope = (points[end].y() - y0) / (points[end].x() - x0);
|
||||
let mut far_point_index: usize = 0;
|
||||
let mut far_point_dist: f32 = 0.0;
|
||||
for i in start + 1..end {
|
||||
let (x, y) = (points[i].x(), points[i].y());
|
||||
let y_line: f32 = y0 + slope * (x - x0);
|
||||
let dist: f32 = (y - y_line).abs();
|
||||
if far_point_dist < dist {
|
||||
far_point_dist = dist;
|
||||
far_point_index = i;
|
||||
}
|
||||
}
|
||||
if far_point_index == 0 || far_point_dist < epsilon {
|
||||
return;
|
||||
}
|
||||
keep_flags[far_point_index] = true;
|
||||
find_points_in_span(points, keep_flags, start, far_point_index, epsilon);
|
||||
find_points_in_span(points, keep_flags, far_point_index, end, epsilon);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
type Point = (f32, f32);
|
||||
impl CurvePoint for Point {
|
||||
fn x(&self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
fn y(&self) -> f32 {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn empty() {
|
||||
let points: Vec<Point> = Vec::new();
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result.len(), 0);
|
||||
}
|
||||
#[test]
|
||||
fn size_1() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0]);
|
||||
}
|
||||
#[test]
|
||||
fn size_2() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 1]);
|
||||
}
|
||||
#[test]
|
||||
fn size_3() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 1, 2]);
|
||||
}
|
||||
#[test]
|
||||
fn size_4() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, -1.0), (3.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 1, 2, 3]);
|
||||
}
|
||||
#[test]
|
||||
fn size_5() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, -1.0), (3.0, 0.0), (4.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 1, 2, 3, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn irregular_x() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (4.0, -1.0), (6.0, 0.0), (10.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 1, 2, 3, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn irregular_x_remove_2() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (4.0, 4.0), (6.0, 2.0), (10.0, -2.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 2, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn remove_middle() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 2]);
|
||||
}
|
||||
#[test]
|
||||
fn remove_all_but_one() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 0.00001), (2.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0]);
|
||||
}
|
||||
#[test]
|
||||
fn remove_2() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.0), (4.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
|
||||
assert_eq!(result, vec![0, 2, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn small_step_size() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.0), (4.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, 0.5);
|
||||
assert_eq!(result, vec![0, 1, 2, 3, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn large_step_size() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.0), (4.0, 0.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, 2.0);
|
||||
assert_eq!(result, vec![0, 2, 4]);
|
||||
}
|
||||
#[test]
|
||||
fn mid_step_size() {
|
||||
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.5), (4.0, 1.0)];
|
||||
let result = find_important_points(points.as_slice(), 0.001, 1.0);
|
||||
assert_eq!(result, vec![0, 1, 2, 4]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,475 +1,475 @@
|
||||
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//! The brushraster module converts floating-point brush positions into rasterized
|
||||
//! pixels, with each pixel given a strength based on how close it is to the center of the
|
||||
//! brush and the hardness of the brush. Soft brushes gradually decrease in strength
|
||||
//! from the center to the edge. Hard brushes have full strength until very close to the edge.
|
||||
//!
|
||||
//! This rasterization tool is used by the UI to convert mouse events into pixel messages
|
||||
//! that get sent to the brush painting thread.
|
||||
use fyrox_core::math::segment::LineSegment2;
|
||||
|
||||
use crate::core::{
|
||||
algebra::{Matrix2, Vector2},
|
||||
math::{OptionRect, Rect},
|
||||
};
|
||||
|
||||
/// Adjust the strength of a brush pixel based on the hardness of the brush.
|
||||
fn apply_hardness(hardness: f32, strength: f32) -> f32 {
|
||||
if strength == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let h = 1.0 - hardness;
|
||||
if strength < h {
|
||||
strength / h
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for objects that are capable of assigning strengths to pixels.
|
||||
pub trait BrushRaster {
|
||||
/// Calculate the strength of a pixel at the given position,
|
||||
/// as measured relative to the center of the brush, so that point (0.0, 0.0)
|
||||
/// is the exact center of the brush.
|
||||
fn strength_at(&self, point: Vector2<f32>) -> f32;
|
||||
/// An AABB that contains all the pixels of the brush, with (0.0, 0.0) being
|
||||
/// at the center of the brush.
|
||||
fn bounds(&self) -> Rect<f32>;
|
||||
/// An AABB that contains all the pixels of the brush after it has been transformed
|
||||
/// and translated. First the brush is multiplied by `transform` and then it is
|
||||
/// translated to `center`, and then an AABB it calculated for the resulting brush.
|
||||
fn transformed_bounds(&self, center: Vector2<f32>, transform: &Matrix2<f32>) -> Rect<i32> {
|
||||
let mut bounds = OptionRect::<i32>::default();
|
||||
let rect = self.bounds();
|
||||
for p in [
|
||||
rect.left_top_corner(),
|
||||
rect.left_bottom_corner(),
|
||||
rect.right_top_corner(),
|
||||
rect.right_bottom_corner(),
|
||||
] {
|
||||
let p1 = transform * p + center;
|
||||
let ceil = p1.map(|x| x.ceil() as i32);
|
||||
let floor = p1.map(|x| x.floor() as i32);
|
||||
let rect = Rect::new(floor.x, floor.y, ceil.x - floor.x, ceil.y - floor.y);
|
||||
bounds.extend_to_contain(rect);
|
||||
}
|
||||
bounds.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Rasterize a round brush with the given radius.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CircleRaster(pub f32);
|
||||
|
||||
impl BrushRaster for CircleRaster {
|
||||
fn strength_at(&self, point: Vector2<f32>) -> f32 {
|
||||
let radius = self.0;
|
||||
let dist_sqr = point.magnitude_squared();
|
||||
if dist_sqr >= radius * radius {
|
||||
0.0
|
||||
} else {
|
||||
(1.0 - dist_sqr.sqrt() / radius).max(0.0)
|
||||
}
|
||||
}
|
||||
fn bounds(&self) -> Rect<f32> {
|
||||
let radius = self.0;
|
||||
Rect::from_points(Vector2::new(-radius, -radius), Vector2::new(radius, radius))
|
||||
}
|
||||
}
|
||||
|
||||
/// Rasterize a rectangular brush with the given x-radius and y-radius.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RectRaster(pub f32, pub f32);
|
||||
|
||||
impl BrushRaster for RectRaster {
|
||||
fn strength_at(&self, point: Vector2<f32>) -> f32 {
|
||||
let radius = Vector2::new(self.0, self.1);
|
||||
// Flip p so that it is on the positive side of both axes.
|
||||
let p = point.abs();
|
||||
let min = radius.min();
|
||||
let inner = Vector2::new(radius.x - min, radius.y - min);
|
||||
let outer = radius - inner;
|
||||
if outer.min() == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let p = (p - inner).sup(&Vector2::new(0.0, 0.0));
|
||||
if p.x > outer.x || p.y > outer.y {
|
||||
0.0
|
||||
} else {
|
||||
1.0 - (p.x / outer.x).max(p.y / outer.y)
|
||||
}
|
||||
}
|
||||
fn bounds(&self) -> Rect<f32> {
|
||||
let RectRaster(x, y) = self;
|
||||
Rect::from_points(Vector2::new(-x, -y), Vector2::new(*x, *y))
|
||||
}
|
||||
}
|
||||
|
||||
/// A pixel within a brush shape.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct BrushPixel {
|
||||
/// The position of the pixel
|
||||
pub position: Vector2<i32>,
|
||||
/// The strength of the brush at this pixel, with 0.0 indicating the pixel is outside the bounds of the brush,
|
||||
/// and 1.0 indicating the maximum strength of the brush.
|
||||
pub strength: f32,
|
||||
}
|
||||
|
||||
/// An iterator that produces coordinates by scanning an integer Rect.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RectIter {
|
||||
bounds: Rect<i32>,
|
||||
next_pos: Vector2<i32>,
|
||||
}
|
||||
|
||||
impl RectIter {
|
||||
/// Create an iterator that returns coordinates within the given bounds.
|
||||
pub fn new(bounds: Rect<i32>) -> Self {
|
||||
Self {
|
||||
bounds,
|
||||
next_pos: Vector2::default(),
|
||||
}
|
||||
}
|
||||
/// The Rect that this iter is scanning.
|
||||
pub fn bounds(&self) -> Rect<i32> {
|
||||
self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RectIter {
|
||||
type Item = Vector2<i32>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.next_pos.y > self.bounds.size.y {
|
||||
return None;
|
||||
}
|
||||
let result = self.next_pos + self.bounds.position;
|
||||
if self.next_pos.x < self.bounds.size.x {
|
||||
self.next_pos.x += 1;
|
||||
} else {
|
||||
self.next_pos.y += 1;
|
||||
self.next_pos.x = 0;
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the pixels of a [BrushRaster] object.
|
||||
/// For each pixel, it produces a [BrushPixel].
|
||||
/// The pixels produced can include pixels with zero strength.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StampPixels<R> {
|
||||
brush_raster: R,
|
||||
center: Vector2<f32>,
|
||||
hardness: f32,
|
||||
inv_transform: Matrix2<f32>,
|
||||
bounds_iter: RectIter,
|
||||
}
|
||||
|
||||
impl<R> StampPixels<R>
|
||||
where
|
||||
R: BrushRaster,
|
||||
{
|
||||
/// An AABB containing all the pixels that this iterator produces.
|
||||
pub fn bounds(&self) -> Rect<i32> {
|
||||
self.bounds_iter.bounds()
|
||||
}
|
||||
/// Construct a new pixel iterator for a stamp at the given location.
|
||||
pub fn new(
|
||||
brush_raster: R,
|
||||
center: Vector2<f32>,
|
||||
hardness: f32,
|
||||
transform: Matrix2<f32>,
|
||||
) -> Self {
|
||||
let (transform, inv_transform) = transform
|
||||
.try_inverse()
|
||||
.map(|m| (transform, m))
|
||||
.unwrap_or((Matrix2::identity(), Matrix2::identity()));
|
||||
let bounds = brush_raster.transformed_bounds(center, &transform);
|
||||
Self {
|
||||
brush_raster,
|
||||
center,
|
||||
hardness,
|
||||
inv_transform,
|
||||
bounds_iter: RectIter::new(bounds),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Iterator for StampPixels<R>
|
||||
where
|
||||
R: BrushRaster,
|
||||
{
|
||||
type Item = BrushPixel;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let position = self.bounds_iter.next()?;
|
||||
let fx = position.x as f32;
|
||||
let fy = position.y as f32;
|
||||
let p = Vector2::new(fx, fy) - self.center;
|
||||
let p = self.inv_transform * p;
|
||||
let strength = self.brush_raster.strength_at(p);
|
||||
let strength = apply_hardness(self.hardness, strength);
|
||||
Some(BrushPixel { position, strength })
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator of the pixels that are painted when a brush
|
||||
/// is smeared from a start point to an end point.
|
||||
/// It works just like [StampPixels] but across a line segment instead of at a single point.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmearPixels<R> {
|
||||
brush_raster: R,
|
||||
segment: LineSegment2<f32>,
|
||||
aspect_segment: LineSegment2<f32>,
|
||||
hardness: f32,
|
||||
inv_transform: Matrix2<f32>,
|
||||
aspect_transform: Matrix2<f32>,
|
||||
bounds_iter: RectIter,
|
||||
}
|
||||
|
||||
impl<R> SmearPixels<R> {
|
||||
/// The bounding rectangle of the pixels.
|
||||
pub fn bounds(&self) -> Rect<i32> {
|
||||
self.bounds_iter.bounds()
|
||||
}
|
||||
/// Construct a new pixel iterator for a smear with the given start and end points.
|
||||
pub fn new(
|
||||
brush_raster: R,
|
||||
start: Vector2<f32>,
|
||||
end: Vector2<f32>,
|
||||
hardness: f32,
|
||||
transform: Matrix2<f32>,
|
||||
) -> Self
|
||||
where
|
||||
R: BrushRaster,
|
||||
{
|
||||
let mut bounds: OptionRect<i32> = Default::default();
|
||||
let (transform, inv_transform) = transform
|
||||
.try_inverse()
|
||||
.map(|m| (transform, m))
|
||||
.unwrap_or((Matrix2::identity(), Matrix2::identity()));
|
||||
bounds.extend_to_contain(brush_raster.transformed_bounds(start, &transform));
|
||||
bounds.extend_to_contain(brush_raster.transformed_bounds(end, &transform));
|
||||
let segment = LineSegment2::new(&start, &end);
|
||||
let aspect_bounds = brush_raster.bounds();
|
||||
let aspect_transform =
|
||||
Matrix2::new(1.0 / aspect_bounds.w(), 0.0, 0.0, 1.0 / aspect_bounds.h());
|
||||
let aspect_transform = aspect_transform * inv_transform;
|
||||
let aspect_segment = LineSegment2 {
|
||||
start: aspect_transform * segment.start,
|
||||
end: aspect_transform * segment.end,
|
||||
};
|
||||
Self {
|
||||
brush_raster,
|
||||
segment,
|
||||
aspect_segment,
|
||||
hardness,
|
||||
inv_transform,
|
||||
aspect_transform,
|
||||
bounds_iter: RectIter::new(bounds.unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Iterator for SmearPixels<R>
|
||||
where
|
||||
R: BrushRaster,
|
||||
{
|
||||
type Item = BrushPixel;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let position = self.bounds_iter.next()?;
|
||||
let fx = position.x as f32;
|
||||
let fy = position.y as f32;
|
||||
let p = Vector2::new(fx, fy);
|
||||
let aspect_p = self.aspect_transform * p;
|
||||
let t = self.aspect_segment.nearest_t(&aspect_p);
|
||||
let center = self.segment.interpolate_clamped(t);
|
||||
let p = p - center;
|
||||
let p = self.inv_transform * p;
|
||||
let s = self.brush_raster.strength_at(p);
|
||||
let strength = apply_hardness(self.hardness, s);
|
||||
Some(BrushPixel { position, strength })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn rect_extend_to_contain_f64() {
|
||||
let mut rect = Rect::new(0.0, 0.0, 1.0, 1.0);
|
||||
rect.extend_to_contain(Rect::new(-1.0, -1.0, 1.0, 1.0));
|
||||
assert_eq!(rect, Rect::new(-1.0, -1.0, 2.0, 2.0));
|
||||
}
|
||||
#[test]
|
||||
fn rect_extend_to_contain_i32() {
|
||||
let mut rect: Rect<i32> = Rect::new(0, 0, 1, 1);
|
||||
|
||||
rect.extend_to_contain(Rect::new(1, 1, 1, 1));
|
||||
assert_eq!(rect, Rect::<i32>::new(0, 0, 2, 2));
|
||||
|
||||
rect.extend_to_contain(Rect::new(-1, -1, 1, 1));
|
||||
assert_eq!(rect, Rect::<i32>::new(-1, -1, 3, 3));
|
||||
}
|
||||
#[test]
|
||||
fn bounds_iter() {
|
||||
let result: Vec<Vector2<i32>> = RectIter::new(Rect::new(-1, -2, 3, 2)).collect();
|
||||
let expected: Vec<Vector2<i32>> = vec![
|
||||
(-1, -2),
|
||||
(0, -2),
|
||||
(1, -2),
|
||||
(2, -2),
|
||||
(-1, -1),
|
||||
(0, -1),
|
||||
(1, -1),
|
||||
(2, -1),
|
||||
(-1, 0),
|
||||
(0, 0),
|
||||
(1, 0),
|
||||
(2, 0),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(x, y)| Vector2::new(x, y))
|
||||
.collect();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
#[test]
|
||||
fn finite_pixels_circle() {
|
||||
let mut iter = StampPixels::new(
|
||||
CircleRaster(2.0),
|
||||
Vector2::default(),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
for _ in 0..100 {
|
||||
if iter.next().is_none() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("Iter went over 100.");
|
||||
}
|
||||
#[test]
|
||||
fn finite_pixels_rect() {
|
||||
let mut iter = StampPixels::new(
|
||||
RectRaster(2.0, 2.0),
|
||||
Vector2::default(),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
for _ in 0..100 {
|
||||
if iter.next().is_none() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("Iter went over 100.");
|
||||
}
|
||||
#[test]
|
||||
fn pixels_range_circle() {
|
||||
let mut iter = StampPixels::new(
|
||||
CircleRaster(2.0),
|
||||
Vector2::default(),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
let mut rect = Rect::new(0, 0, 0, 0);
|
||||
let mut points: Vec<Vector2<i32>> = Vec::default();
|
||||
for _ in 0..100 {
|
||||
if let Some(BrushPixel { position, .. }) = iter.next() {
|
||||
points.push(Vector2::new(position.x, position.y));
|
||||
rect.extend_to_contain(Rect::new(position.x, position.y, 0, 0));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
rect,
|
||||
Rect::new(-2, -2, 4, 4),
|
||||
"{:?} {:?}",
|
||||
iter.bounds(),
|
||||
points
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn pixels_range_rect() {
|
||||
let mut iter = StampPixels::new(
|
||||
RectRaster(2.0, 2.0),
|
||||
Vector2::default(),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
let mut rect = Rect::new(0, 0, 0, 0);
|
||||
let mut points: Vec<Vector2<i32>> = Vec::default();
|
||||
for _ in 0..100 {
|
||||
if let Some(BrushPixel { position, .. }) = iter.next() {
|
||||
points.push(Vector2::new(position.x, position.y));
|
||||
rect.extend_to_contain(Rect::new(position.x, position.y, 0, 0));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
rect,
|
||||
Rect::new(-2, -2, 4, 4),
|
||||
"{:?} {:?}",
|
||||
iter.bounds(),
|
||||
points
|
||||
);
|
||||
}
|
||||
fn find_strength_at(iter: &StampPixels<RectRaster>, (x, y): (i32, i32)) -> f32 {
|
||||
let p = Vector2::new(x, y);
|
||||
let pix = iter.clone().find(|x| x.position == p);
|
||||
pix.map(|p| p.strength).unwrap_or(0.0)
|
||||
}
|
||||
#[test]
|
||||
fn simple_rect() {
|
||||
let iter = StampPixels::new(
|
||||
RectRaster(1.1, 1.1),
|
||||
Vector2::new(1.0, 1.0),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
assert_eq!(iter.bounds().w(), 4, "w != 4: {:?}", iter.bounds());
|
||||
assert_eq!(iter.bounds().h(), 4, "h != 4: {:?}", iter.bounds());
|
||||
assert_eq!(find_strength_at(&iter, (1, 1)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (0, 1)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (0, 2)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (0, 0)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (-1, 1)), 0.0);
|
||||
}
|
||||
#[test]
|
||||
fn distant_rect() {
|
||||
let iter = StampPixels::new(
|
||||
RectRaster(1.1, 1.1),
|
||||
Vector2::new(1001.0, 2501.0),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
assert_eq!(iter.bounds().w(), 4, "w != 4: {:?}", iter.bounds());
|
||||
assert_eq!(iter.bounds().h(), 4, "h != 4: {:?}", iter.bounds());
|
||||
assert_eq!(find_strength_at(&iter, (1001, 2501)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (1000, 2501)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (1000, 2502)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (1000, 2500)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (999, 2501)), 0.0);
|
||||
}
|
||||
}
|
||||
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//! The brushraster module converts floating-point brush positions into rasterized
|
||||
//! pixels, with each pixel given a strength based on how close it is to the center of the
|
||||
//! brush and the hardness of the brush. Soft brushes gradually decrease in strength
|
||||
//! from the center to the edge. Hard brushes have full strength until very close to the edge.
|
||||
//!
|
||||
//! This rasterization tool is used by the UI to convert mouse events into pixel messages
|
||||
//! that get sent to the brush painting thread.
|
||||
use fyrox_core::math::segment::LineSegment2;
|
||||
|
||||
use crate::core::{
|
||||
algebra::{Matrix2, Vector2},
|
||||
math::{OptionRect, Rect},
|
||||
};
|
||||
|
||||
/// Adjust the strength of a brush pixel based on the hardness of the brush.
|
||||
fn apply_hardness(hardness: f32, strength: f32) -> f32 {
|
||||
if strength == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let h = 1.0 - hardness;
|
||||
if strength < h {
|
||||
strength / h
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for objects that are capable of assigning strengths to pixels.
|
||||
pub trait BrushRaster {
|
||||
/// Calculate the strength of a pixel at the given position,
|
||||
/// as measured relative to the center of the brush, so that point (0.0, 0.0)
|
||||
/// is the exact center of the brush.
|
||||
fn strength_at(&self, point: Vector2<f32>) -> f32;
|
||||
/// An AABB that contains all the pixels of the brush, with (0.0, 0.0) being
|
||||
/// at the center of the brush.
|
||||
fn bounds(&self) -> Rect<f32>;
|
||||
/// An AABB that contains all the pixels of the brush after it has been transformed
|
||||
/// and translated. First the brush is multiplied by `transform` and then it is
|
||||
/// translated to `center`, and then an AABB it calculated for the resulting brush.
|
||||
fn transformed_bounds(&self, center: Vector2<f32>, transform: &Matrix2<f32>) -> Rect<i32> {
|
||||
let mut bounds = OptionRect::<i32>::default();
|
||||
let rect = self.bounds();
|
||||
for p in [
|
||||
rect.left_top_corner(),
|
||||
rect.left_bottom_corner(),
|
||||
rect.right_top_corner(),
|
||||
rect.right_bottom_corner(),
|
||||
] {
|
||||
let p1 = transform * p + center;
|
||||
let ceil = p1.map(|x| x.ceil() as i32);
|
||||
let floor = p1.map(|x| x.floor() as i32);
|
||||
let rect = Rect::new(floor.x, floor.y, ceil.x - floor.x, ceil.y - floor.y);
|
||||
bounds.extend_to_contain(rect);
|
||||
}
|
||||
bounds.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Rasterize a round brush with the given radius.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CircleRaster(pub f32);
|
||||
|
||||
impl BrushRaster for CircleRaster {
|
||||
fn strength_at(&self, point: Vector2<f32>) -> f32 {
|
||||
let radius = self.0;
|
||||
let dist_sqr = point.magnitude_squared();
|
||||
if dist_sqr >= radius * radius {
|
||||
0.0
|
||||
} else {
|
||||
(1.0 - dist_sqr.sqrt() / radius).max(0.0)
|
||||
}
|
||||
}
|
||||
fn bounds(&self) -> Rect<f32> {
|
||||
let radius = self.0;
|
||||
Rect::from_points(Vector2::new(-radius, -radius), Vector2::new(radius, radius))
|
||||
}
|
||||
}
|
||||
|
||||
/// Rasterize a rectangular brush with the given x-radius and y-radius.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RectRaster(pub f32, pub f32);
|
||||
|
||||
impl BrushRaster for RectRaster {
|
||||
fn strength_at(&self, point: Vector2<f32>) -> f32 {
|
||||
let radius = Vector2::new(self.0, self.1);
|
||||
// Flip p so that it is on the positive side of both axes.
|
||||
let p = point.abs();
|
||||
let min = radius.min();
|
||||
let inner = Vector2::new(radius.x - min, radius.y - min);
|
||||
let outer = radius - inner;
|
||||
if outer.min() == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let p = (p - inner).sup(&Vector2::new(0.0, 0.0));
|
||||
if p.x > outer.x || p.y > outer.y {
|
||||
0.0
|
||||
} else {
|
||||
1.0 - (p.x / outer.x).max(p.y / outer.y)
|
||||
}
|
||||
}
|
||||
fn bounds(&self) -> Rect<f32> {
|
||||
let RectRaster(x, y) = self;
|
||||
Rect::from_points(Vector2::new(-x, -y), Vector2::new(*x, *y))
|
||||
}
|
||||
}
|
||||
|
||||
/// A pixel within a brush shape.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct BrushPixel {
|
||||
/// The position of the pixel
|
||||
pub position: Vector2<i32>,
|
||||
/// The strength of the brush at this pixel, with 0.0 indicating the pixel is outside the bounds of the brush,
|
||||
/// and 1.0 indicating the maximum strength of the brush.
|
||||
pub strength: f32,
|
||||
}
|
||||
|
||||
/// An iterator that produces coordinates by scanning an integer Rect.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RectIter {
|
||||
bounds: Rect<i32>,
|
||||
next_pos: Vector2<i32>,
|
||||
}
|
||||
|
||||
impl RectIter {
|
||||
/// Create an iterator that returns coordinates within the given bounds.
|
||||
pub fn new(bounds: Rect<i32>) -> Self {
|
||||
Self {
|
||||
bounds,
|
||||
next_pos: Vector2::default(),
|
||||
}
|
||||
}
|
||||
/// The Rect that this iter is scanning.
|
||||
pub fn bounds(&self) -> Rect<i32> {
|
||||
self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RectIter {
|
||||
type Item = Vector2<i32>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.next_pos.y > self.bounds.size.y {
|
||||
return None;
|
||||
}
|
||||
let result = self.next_pos + self.bounds.position;
|
||||
if self.next_pos.x < self.bounds.size.x {
|
||||
self.next_pos.x += 1;
|
||||
} else {
|
||||
self.next_pos.y += 1;
|
||||
self.next_pos.x = 0;
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the pixels of a [BrushRaster] object.
|
||||
/// For each pixel, it produces a [BrushPixel].
|
||||
/// The pixels produced can include pixels with zero strength.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StampPixels<R> {
|
||||
brush_raster: R,
|
||||
center: Vector2<f32>,
|
||||
hardness: f32,
|
||||
inv_transform: Matrix2<f32>,
|
||||
bounds_iter: RectIter,
|
||||
}
|
||||
|
||||
impl<R> StampPixels<R>
|
||||
where
|
||||
R: BrushRaster,
|
||||
{
|
||||
/// An AABB containing all the pixels that this iterator produces.
|
||||
pub fn bounds(&self) -> Rect<i32> {
|
||||
self.bounds_iter.bounds()
|
||||
}
|
||||
/// Construct a new pixel iterator for a stamp at the given location.
|
||||
pub fn new(
|
||||
brush_raster: R,
|
||||
center: Vector2<f32>,
|
||||
hardness: f32,
|
||||
transform: Matrix2<f32>,
|
||||
) -> Self {
|
||||
let (transform, inv_transform) = transform
|
||||
.try_inverse()
|
||||
.map(|m| (transform, m))
|
||||
.unwrap_or((Matrix2::identity(), Matrix2::identity()));
|
||||
let bounds = brush_raster.transformed_bounds(center, &transform);
|
||||
Self {
|
||||
brush_raster,
|
||||
center,
|
||||
hardness,
|
||||
inv_transform,
|
||||
bounds_iter: RectIter::new(bounds),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Iterator for StampPixels<R>
|
||||
where
|
||||
R: BrushRaster,
|
||||
{
|
||||
type Item = BrushPixel;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let position = self.bounds_iter.next()?;
|
||||
let fx = position.x as f32;
|
||||
let fy = position.y as f32;
|
||||
let p = Vector2::new(fx, fy) - self.center;
|
||||
let p = self.inv_transform * p;
|
||||
let strength = self.brush_raster.strength_at(p);
|
||||
let strength = apply_hardness(self.hardness, strength);
|
||||
Some(BrushPixel { position, strength })
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator of the pixels that are painted when a brush
|
||||
/// is smeared from a start point to an end point.
|
||||
/// It works just like [StampPixels] but across a line segment instead of at a single point.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmearPixels<R> {
|
||||
brush_raster: R,
|
||||
segment: LineSegment2<f32>,
|
||||
aspect_segment: LineSegment2<f32>,
|
||||
hardness: f32,
|
||||
inv_transform: Matrix2<f32>,
|
||||
aspect_transform: Matrix2<f32>,
|
||||
bounds_iter: RectIter,
|
||||
}
|
||||
|
||||
impl<R> SmearPixels<R> {
|
||||
/// The bounding rectangle of the pixels.
|
||||
pub fn bounds(&self) -> Rect<i32> {
|
||||
self.bounds_iter.bounds()
|
||||
}
|
||||
/// Construct a new pixel iterator for a smear with the given start and end points.
|
||||
pub fn new(
|
||||
brush_raster: R,
|
||||
start: Vector2<f32>,
|
||||
end: Vector2<f32>,
|
||||
hardness: f32,
|
||||
transform: Matrix2<f32>,
|
||||
) -> Self
|
||||
where
|
||||
R: BrushRaster,
|
||||
{
|
||||
let mut bounds: OptionRect<i32> = Default::default();
|
||||
let (transform, inv_transform) = transform
|
||||
.try_inverse()
|
||||
.map(|m| (transform, m))
|
||||
.unwrap_or((Matrix2::identity(), Matrix2::identity()));
|
||||
bounds.extend_to_contain(brush_raster.transformed_bounds(start, &transform));
|
||||
bounds.extend_to_contain(brush_raster.transformed_bounds(end, &transform));
|
||||
let segment = LineSegment2::new(&start, &end);
|
||||
let aspect_bounds = brush_raster.bounds();
|
||||
let aspect_transform =
|
||||
Matrix2::new(1.0 / aspect_bounds.w(), 0.0, 0.0, 1.0 / aspect_bounds.h());
|
||||
let aspect_transform = aspect_transform * inv_transform;
|
||||
let aspect_segment = LineSegment2 {
|
||||
start: aspect_transform * segment.start,
|
||||
end: aspect_transform * segment.end,
|
||||
};
|
||||
Self {
|
||||
brush_raster,
|
||||
segment,
|
||||
aspect_segment,
|
||||
hardness,
|
||||
inv_transform,
|
||||
aspect_transform,
|
||||
bounds_iter: RectIter::new(bounds.unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Iterator for SmearPixels<R>
|
||||
where
|
||||
R: BrushRaster,
|
||||
{
|
||||
type Item = BrushPixel;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let position = self.bounds_iter.next()?;
|
||||
let fx = position.x as f32;
|
||||
let fy = position.y as f32;
|
||||
let p = Vector2::new(fx, fy);
|
||||
let aspect_p = self.aspect_transform * p;
|
||||
let t = self.aspect_segment.nearest_t(&aspect_p);
|
||||
let center = self.segment.interpolate_clamped(t);
|
||||
let p = p - center;
|
||||
let p = self.inv_transform * p;
|
||||
let s = self.brush_raster.strength_at(p);
|
||||
let strength = apply_hardness(self.hardness, s);
|
||||
Some(BrushPixel { position, strength })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn rect_extend_to_contain_f64() {
|
||||
let mut rect = Rect::new(0.0, 0.0, 1.0, 1.0);
|
||||
rect.extend_to_contain(Rect::new(-1.0, -1.0, 1.0, 1.0));
|
||||
assert_eq!(rect, Rect::new(-1.0, -1.0, 2.0, 2.0));
|
||||
}
|
||||
#[test]
|
||||
fn rect_extend_to_contain_i32() {
|
||||
let mut rect: Rect<i32> = Rect::new(0, 0, 1, 1);
|
||||
|
||||
rect.extend_to_contain(Rect::new(1, 1, 1, 1));
|
||||
assert_eq!(rect, Rect::<i32>::new(0, 0, 2, 2));
|
||||
|
||||
rect.extend_to_contain(Rect::new(-1, -1, 1, 1));
|
||||
assert_eq!(rect, Rect::<i32>::new(-1, -1, 3, 3));
|
||||
}
|
||||
#[test]
|
||||
fn bounds_iter() {
|
||||
let result: Vec<Vector2<i32>> = RectIter::new(Rect::new(-1, -2, 3, 2)).collect();
|
||||
let expected: Vec<Vector2<i32>> = vec![
|
||||
(-1, -2),
|
||||
(0, -2),
|
||||
(1, -2),
|
||||
(2, -2),
|
||||
(-1, -1),
|
||||
(0, -1),
|
||||
(1, -1),
|
||||
(2, -1),
|
||||
(-1, 0),
|
||||
(0, 0),
|
||||
(1, 0),
|
||||
(2, 0),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(x, y)| Vector2::new(x, y))
|
||||
.collect();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
#[test]
|
||||
fn finite_pixels_circle() {
|
||||
let mut iter = StampPixels::new(
|
||||
CircleRaster(2.0),
|
||||
Vector2::default(),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
for _ in 0..100 {
|
||||
if iter.next().is_none() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("Iter went over 100.");
|
||||
}
|
||||
#[test]
|
||||
fn finite_pixels_rect() {
|
||||
let mut iter = StampPixels::new(
|
||||
RectRaster(2.0, 2.0),
|
||||
Vector2::default(),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
for _ in 0..100 {
|
||||
if iter.next().is_none() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("Iter went over 100.");
|
||||
}
|
||||
#[test]
|
||||
fn pixels_range_circle() {
|
||||
let mut iter = StampPixels::new(
|
||||
CircleRaster(2.0),
|
||||
Vector2::default(),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
let mut rect = Rect::new(0, 0, 0, 0);
|
||||
let mut points: Vec<Vector2<i32>> = Vec::default();
|
||||
for _ in 0..100 {
|
||||
if let Some(BrushPixel { position, .. }) = iter.next() {
|
||||
points.push(Vector2::new(position.x, position.y));
|
||||
rect.extend_to_contain(Rect::new(position.x, position.y, 0, 0));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
rect,
|
||||
Rect::new(-2, -2, 4, 4),
|
||||
"{:?} {:?}",
|
||||
iter.bounds(),
|
||||
points
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn pixels_range_rect() {
|
||||
let mut iter = StampPixels::new(
|
||||
RectRaster(2.0, 2.0),
|
||||
Vector2::default(),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
let mut rect = Rect::new(0, 0, 0, 0);
|
||||
let mut points: Vec<Vector2<i32>> = Vec::default();
|
||||
for _ in 0..100 {
|
||||
if let Some(BrushPixel { position, .. }) = iter.next() {
|
||||
points.push(Vector2::new(position.x, position.y));
|
||||
rect.extend_to_contain(Rect::new(position.x, position.y, 0, 0));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
rect,
|
||||
Rect::new(-2, -2, 4, 4),
|
||||
"{:?} {:?}",
|
||||
iter.bounds(),
|
||||
points
|
||||
);
|
||||
}
|
||||
fn find_strength_at(iter: &StampPixels<RectRaster>, (x, y): (i32, i32)) -> f32 {
|
||||
let p = Vector2::new(x, y);
|
||||
let pix = iter.clone().find(|x| x.position == p);
|
||||
pix.map(|p| p.strength).unwrap_or(0.0)
|
||||
}
|
||||
#[test]
|
||||
fn simple_rect() {
|
||||
let iter = StampPixels::new(
|
||||
RectRaster(1.1, 1.1),
|
||||
Vector2::new(1.0, 1.0),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
assert_eq!(iter.bounds().w(), 4, "w != 4: {:?}", iter.bounds());
|
||||
assert_eq!(iter.bounds().h(), 4, "h != 4: {:?}", iter.bounds());
|
||||
assert_eq!(find_strength_at(&iter, (1, 1)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (0, 1)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (0, 2)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (0, 0)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (-1, 1)), 0.0);
|
||||
}
|
||||
#[test]
|
||||
fn distant_rect() {
|
||||
let iter = StampPixels::new(
|
||||
RectRaster(1.1, 1.1),
|
||||
Vector2::new(1001.0, 2501.0),
|
||||
1.0,
|
||||
Matrix2::identity(),
|
||||
);
|
||||
assert_eq!(iter.bounds().w(), 4, "w != 4: {:?}", iter.bounds());
|
||||
assert_eq!(iter.bounds().h(), 4, "h != 4: {:?}", iter.bounds());
|
||||
assert_eq!(find_strength_at(&iter, (1001, 2501)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (1000, 2501)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (1000, 2502)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (1000, 2500)), 1.0);
|
||||
assert_eq!(find_strength_at(&iter, (999, 2501)), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,370 +1,370 @@
|
||||
// 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 module manages the record of which pixels have been recently edited by a brushstroke.
|
||||
//! It stores the modified chunks and the pixels within each chunk since the last time
|
||||
//! the changes were written to the terrain's textures.
|
||||
use super::{ChunkData, StrokeData, TerrainTextureKind};
|
||||
use crate::core::algebra::Vector2;
|
||||
use crate::fxhash::{FxHashMap, FxHashSet};
|
||||
use crate::resource::texture::TextureResource;
|
||||
use crate::scene::terrain::pixel_position_to_grid_position;
|
||||
|
||||
/// The list of modified pixels in each chunk.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StrokeChunks {
|
||||
/// The size of each chunk as measured by distance from one chunk origin to the next.
|
||||
/// This does not include the overlap pixel around the edges of height textures,
|
||||
/// because that overlap does not contribute to the distance between the origins of the textuers.
|
||||
chunk_size: Vector2<u32>,
|
||||
kind: TerrainTextureKind,
|
||||
/// The position of each written pixel within each chunk.
|
||||
written_pixels: FxHashMap<Vector2<i32>, FxHashSet<Vector2<u32>>>,
|
||||
/// The number of pixels written to this object.
|
||||
count: usize,
|
||||
/// Pixel hash sets that are allocated but not currently in use
|
||||
unused_chunks: Vec<FxHashSet<Vector2<u32>>>,
|
||||
}
|
||||
|
||||
impl StrokeChunks {
|
||||
/// The number of modified pixels that this object is currently tracking
|
||||
#[inline]
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
/// The kind of texture being edited
|
||||
#[inline]
|
||||
pub fn kind(&self) -> TerrainTextureKind {
|
||||
self.kind
|
||||
}
|
||||
/// Erase the currently stored pixel data and prepare for a new set of pixels.
|
||||
pub fn clear(&mut self) {
|
||||
self.count = 0;
|
||||
// Move and clear the no-longer needed chunk pixel sets into the unused list.
|
||||
for mut c in self.written_pixels.drain().map(|(_, v)| v) {
|
||||
c.clear();
|
||||
self.unused_chunks.push(c);
|
||||
}
|
||||
}
|
||||
/// Update the texture kind and the texture size.
|
||||
pub fn set_layout(&mut self, kind: TerrainTextureKind, size: Vector2<u32>) {
|
||||
self.kind = kind;
|
||||
// If the texture is a height texture, then its edges overlap with the neigboring chunks,
|
||||
// so the size we need is three less than the actual size in each dimension.
|
||||
self.chunk_size = match kind {
|
||||
TerrainTextureKind::Height => size.map(|x| x - 3),
|
||||
TerrainTextureKind::Mask => size,
|
||||
};
|
||||
}
|
||||
/// For every chunk that has been written in this object since the last clear, copy the texture data from the given textures
|
||||
/// and store it in the given list of texture data, if the list does not already contain data for those coordinates.
|
||||
///
|
||||
/// The purpose of this is to save a backup copy of chunks that are modified by the current brushstroke so that an undo command
|
||||
/// can be created. This should be called immediately before [StrokeChunks::apply] so that the copied textures are unmodified.
|
||||
/// If saved chunk data already exists for some chunk, nothing is done since it is presumed that existing data is the original
|
||||
/// data that we are trying to preserve.
|
||||
///
|
||||
/// - `textures`: The source of texture data.
|
||||
/// - `saved_chunk_data`: The list of chunk data that may be modified if it does not already contain a copy of chunk data for each
|
||||
/// written chunk coordinates.
|
||||
pub fn copy_texture_data(
|
||||
&self,
|
||||
textures: &FxHashMap<Vector2<i32>, TextureResource>,
|
||||
saved_chunk_data: &mut Vec<ChunkData>,
|
||||
) {
|
||||
for (c, _) in self.written_pixels.iter() {
|
||||
if saved_chunk_data.iter().any(|x| x.grid_position == *c) {
|
||||
continue;
|
||||
}
|
||||
let Some(texture) = textures.get(c) else {
|
||||
continue;
|
||||
};
|
||||
saved_chunk_data.push(ChunkData::from_texture(*c, texture));
|
||||
}
|
||||
}
|
||||
/// Use the pixels stored in this object to modify the given textures with
|
||||
/// pixel data from the given StrokeData.
|
||||
/// Once the textures have been modified using this method [StrokeChunks::clear]
|
||||
/// should be called, since the data in this object has served its purpose.
|
||||
pub fn apply<V>(
|
||||
&self,
|
||||
stroke: &StrokeData<V>,
|
||||
textures: &FxHashMap<Vector2<i32>, TextureResource>,
|
||||
) where
|
||||
V: Clone,
|
||||
{
|
||||
for (c, pxs) in self.written_pixels.iter() {
|
||||
let Some(texture) = textures.get(c) else {
|
||||
continue;
|
||||
};
|
||||
let mut texture_data = texture.data_ref();
|
||||
let mut modify = texture_data.modify();
|
||||
let Some(data) = modify.data_mut_of_type::<V>() else {
|
||||
continue;
|
||||
};
|
||||
let origin = self.chunk_to_origin(*c);
|
||||
let row_size = self.row_size();
|
||||
for p in pxs.iter() {
|
||||
let position = match self.kind {
|
||||
TerrainTextureKind::Mask => origin + p.map(|x| x as i32),
|
||||
TerrainTextureKind::Height => origin + p.map(|x| x as i32 - 1),
|
||||
};
|
||||
let Some(value) = stroke.latest_pixel_value(position) else {
|
||||
continue;
|
||||
};
|
||||
let index = p.x as usize + p.y as usize * row_size;
|
||||
data[index].clone_from(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Calculates which chunk contains the given pixel position.
|
||||
#[inline]
|
||||
pub fn pixel_position_to_grid_position(&self, position: Vector2<i32>) -> Vector2<i32> {
|
||||
pixel_position_to_grid_position(position, self.chunk_size)
|
||||
}
|
||||
/// Calculates the origin pixel position of the given chunk.
|
||||
pub fn chunk_to_origin(&self, grid_position: Vector2<i32>) -> Vector2<i32> {
|
||||
Vector2::new(
|
||||
grid_position.x * self.chunk_size.x as i32,
|
||||
grid_position.y * self.chunk_size.y as i32,
|
||||
)
|
||||
}
|
||||
/// The width of the texture in pixels.
|
||||
pub fn row_size(&self) -> usize {
|
||||
match self.kind {
|
||||
TerrainTextureKind::Height => (self.chunk_size.x + 3) as usize,
|
||||
TerrainTextureKind::Mask => self.chunk_size.x as usize,
|
||||
}
|
||||
}
|
||||
/// Calculate the index of a pixel at the given position within texture data,
|
||||
/// based on the row size. The given position is relative to the origin of the texture
|
||||
/// and must be within the bounds of the texture.
|
||||
pub fn pixel_index(&self, position: Vector2<i32>) -> usize {
|
||||
if !self.is_valid_pixel(position) {
|
||||
panic!(
|
||||
"Invalid pixel position: ({}, {}) within ({}, {})",
|
||||
position.x, position.y, self.chunk_size.x, self.chunk_size.y
|
||||
);
|
||||
}
|
||||
let p = match self.kind {
|
||||
TerrainTextureKind::Height => position.map(|x| (x + 1) as usize),
|
||||
TerrainTextureKind::Mask => position.map(|x| x as usize),
|
||||
};
|
||||
p.x + p.y * self.row_size()
|
||||
}
|
||||
/// True if the given pixel position is within the bounds of a chunk for the current kind of chunk data.
|
||||
/// Due to the margins of the height textures, it is permitted to index height textures to -1 and chunk_size.x + 1.
|
||||
pub fn is_valid_pixel(&self, position: Vector2<i32>) -> bool {
|
||||
let size = self.chunk_size.map(|x| x as i32);
|
||||
match self.kind {
|
||||
TerrainTextureKind::Height => {
|
||||
(-1..=size.x + 1).contains(&position.x) && (-1..=size.y + 1).contains(&position.y)
|
||||
}
|
||||
TerrainTextureKind::Mask => {
|
||||
(0..size.x).contains(&position.x) && (0..size.y).contains(&position.x)
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Insert the the pixel at the given position into this data.
|
||||
/// This method determines which chunks have a pixel at that position
|
||||
/// and marks each of those chunks as being modified.
|
||||
pub fn write(&mut self, position: Vector2<i32>) {
|
||||
let grid_pos = self.pixel_position_to_grid_position(position);
|
||||
self.count += 1;
|
||||
match self.kind {
|
||||
TerrainTextureKind::Height => {
|
||||
for x in grid_pos.x - 1..=grid_pos.x + 1 {
|
||||
for y in grid_pos.y - 1..=grid_pos.y + 1 {
|
||||
self.write_height(Vector2::new(x, y), position);
|
||||
}
|
||||
}
|
||||
}
|
||||
TerrainTextureKind::Mask => {
|
||||
let origin = self.chunk_to_origin(grid_pos);
|
||||
let pos = (position - origin).map(|x| x as u32);
|
||||
self.write_to_chunk(grid_pos, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn write_height(&mut self, grid_pos: Vector2<i32>, position: Vector2<i32>) {
|
||||
let origin = self.chunk_to_origin(grid_pos);
|
||||
let pos = position - origin;
|
||||
let size = self.chunk_size;
|
||||
let (w, h) = (size.x as i32, size.y as i32);
|
||||
if (-1..=w + 1).contains(&pos.x) && (-1..=h + 1).contains(&pos.y) {
|
||||
self.write_to_chunk(grid_pos, pos.map(|x| (x + 1) as u32));
|
||||
}
|
||||
}
|
||||
fn write_to_chunk(&mut self, grid_pos: Vector2<i32>, position: Vector2<u32>) {
|
||||
let mut unused = std::mem::take(&mut self.unused_chunks);
|
||||
self.written_pixels
|
||||
.entry(grid_pos)
|
||||
.or_insert_with(|| unused.pop().unwrap_or_default())
|
||||
.insert(position);
|
||||
self.unused_chunks = unused;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
const RANDOM_POINTS: &[(i32, i32)] = &[
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(-1, -1),
|
||||
(20, -123),
|
||||
(-11, 22),
|
||||
(42, 285),
|
||||
(360, -180),
|
||||
(123, -456),
|
||||
(54, 32),
|
||||
(-2, -3),
|
||||
];
|
||||
#[test]
|
||||
fn chunk_to_origin() {
|
||||
let mut chunks = StrokeChunks::default();
|
||||
chunks.set_layout(TerrainTextureKind::Height, Vector2::new(7, 7));
|
||||
assert_eq!(
|
||||
chunks.chunk_to_origin(Vector2::new(0, 0)),
|
||||
Vector2::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
chunks.chunk_to_origin(Vector2::new(1, 0)),
|
||||
Vector2::new(4, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
chunks.chunk_to_origin(Vector2::new(-2, -1)),
|
||||
Vector2::new(-8, -4)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn pixel_position_to_grid_position() {
|
||||
let mut chunks = StrokeChunks::default();
|
||||
chunks.set_layout(TerrainTextureKind::Height, Vector2::new(7, 7));
|
||||
assert_eq!(
|
||||
chunks.pixel_position_to_grid_position(Vector2::new(0, 0)),
|
||||
Vector2::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
chunks.pixel_position_to_grid_position(Vector2::new(2, 3)),
|
||||
Vector2::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
chunks.pixel_position_to_grid_position(Vector2::new(-1, -1)),
|
||||
Vector2::new(-1, -1)
|
||||
);
|
||||
assert_eq!(
|
||||
chunks.pixel_position_to_grid_position(Vector2::new(4, -4)),
|
||||
Vector2::new(1, -1)
|
||||
);
|
||||
}
|
||||
fn test_points_height(size: Vector2<u32>) {
|
||||
let mut chunks = StrokeChunks::default();
|
||||
chunks.set_layout(TerrainTextureKind::Height, size);
|
||||
for p in RANDOM_POINTS.iter() {
|
||||
let p = Vector2::new(p.0, p.1);
|
||||
let grid_pos = chunks.pixel_position_to_grid_position(p);
|
||||
let origin = chunks.chunk_to_origin(grid_pos);
|
||||
let pixel = p - origin;
|
||||
test_point(p, pixel, size.map(|x| x - 1));
|
||||
}
|
||||
let s = size.map(|x| x as i32);
|
||||
for x in -s.x..=s.x * 2 {
|
||||
for y in -s.y..=s.y * 2 {
|
||||
let p = Vector2::new(x, y);
|
||||
let grid_pos = chunks.pixel_position_to_grid_position(p);
|
||||
let origin = chunks.chunk_to_origin(grid_pos);
|
||||
let pixel = p - origin;
|
||||
test_point(p, pixel, size.map(|x| x - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn test_points_mask(size: Vector2<u32>) {
|
||||
let mut chunks = StrokeChunks::default();
|
||||
chunks.set_layout(TerrainTextureKind::Mask, size);
|
||||
for p in RANDOM_POINTS.iter() {
|
||||
let p = Vector2::new(p.0, p.1);
|
||||
let grid_pos = chunks.pixel_position_to_grid_position(p);
|
||||
let origin = chunks.chunk_to_origin(grid_pos);
|
||||
let pixel = p - origin;
|
||||
test_point(p, pixel, size);
|
||||
}
|
||||
let s = size.map(|x| x as i32);
|
||||
for x in -s.x..=s.x * 2 {
|
||||
for y in -s.y..=s.y * 2 {
|
||||
let p = Vector2::new(x, y);
|
||||
let grid_pos = chunks.pixel_position_to_grid_position(p);
|
||||
let origin = chunks.chunk_to_origin(grid_pos);
|
||||
let pixel = p - origin;
|
||||
test_point(p, pixel, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn test_point(p: Vector2<i32>, pixel: Vector2<i32>, size: Vector2<u32>) {
|
||||
assert!(
|
||||
pixel.x >= 0,
|
||||
"({}, {}) -> ({}, {})",
|
||||
p.x,
|
||||
p.y,
|
||||
pixel.x,
|
||||
pixel.y
|
||||
);
|
||||
assert!(
|
||||
pixel.y >= 0,
|
||||
"({}, {}) -> ({}, {})",
|
||||
p.x,
|
||||
p.y,
|
||||
pixel.x,
|
||||
pixel.y
|
||||
);
|
||||
assert!(
|
||||
pixel.x < size.x as i32,
|
||||
"({}, {}) -> ({}, {})",
|
||||
p.x,
|
||||
p.y,
|
||||
pixel.x,
|
||||
pixel.y
|
||||
);
|
||||
assert!(
|
||||
pixel.y < size.y as i32,
|
||||
"({}, {}) -> ({}, {})",
|
||||
p.x,
|
||||
p.y,
|
||||
pixel.x,
|
||||
pixel.y
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn random_points_5x5() {
|
||||
test_points_height(Vector2::new(5, 5));
|
||||
test_points_mask(Vector2::new(5, 5));
|
||||
}
|
||||
#[test]
|
||||
fn random_points_10x10() {
|
||||
test_points_height(Vector2::new(10, 10));
|
||||
test_points_mask(Vector2::new(10, 10));
|
||||
}
|
||||
#[test]
|
||||
fn random_points_257x257() {
|
||||
test_points_height(Vector2::new(257, 257));
|
||||
test_points_mask(Vector2::new(257, 257));
|
||||
}
|
||||
}
|
||||
// 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 module manages the record of which pixels have been recently edited by a brushstroke.
|
||||
//! It stores the modified chunks and the pixels within each chunk since the last time
|
||||
//! the changes were written to the terrain's textures.
|
||||
use super::{ChunkData, StrokeData, TerrainTextureKind};
|
||||
use crate::core::algebra::Vector2;
|
||||
use crate::fxhash::{FxHashMap, FxHashSet};
|
||||
use crate::resource::texture::TextureResource;
|
||||
use crate::scene::terrain::pixel_position_to_grid_position;
|
||||
|
||||
/// The list of modified pixels in each chunk.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StrokeChunks {
|
||||
/// The size of each chunk as measured by distance from one chunk origin to the next.
|
||||
/// This does not include the overlap pixel around the edges of height textures,
|
||||
/// because that overlap does not contribute to the distance between the origins of the textuers.
|
||||
chunk_size: Vector2<u32>,
|
||||
kind: TerrainTextureKind,
|
||||
/// The position of each written pixel within each chunk.
|
||||
written_pixels: FxHashMap<Vector2<i32>, FxHashSet<Vector2<u32>>>,
|
||||
/// The number of pixels written to this object.
|
||||
count: usize,
|
||||
/// Pixel hash sets that are allocated but not currently in use
|
||||
unused_chunks: Vec<FxHashSet<Vector2<u32>>>,
|
||||
}
|
||||
|
||||
impl StrokeChunks {
|
||||
/// The number of modified pixels that this object is currently tracking
|
||||
#[inline]
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
/// The kind of texture being edited
|
||||
#[inline]
|
||||
pub fn kind(&self) -> TerrainTextureKind {
|
||||
self.kind
|
||||
}
|
||||
/// Erase the currently stored pixel data and prepare for a new set of pixels.
|
||||
pub fn clear(&mut self) {
|
||||
self.count = 0;
|
||||
// Move and clear the no-longer needed chunk pixel sets into the unused list.
|
||||
for mut c in self.written_pixels.drain().map(|(_, v)| v) {
|
||||
c.clear();
|
||||
self.unused_chunks.push(c);
|
||||
}
|
||||
}
|
||||
/// Update the texture kind and the texture size.
|
||||
pub fn set_layout(&mut self, kind: TerrainTextureKind, size: Vector2<u32>) {
|
||||
self.kind = kind;
|
||||
// If the texture is a height texture, then its edges overlap with the neigboring chunks,
|
||||
// so the size we need is three less than the actual size in each dimension.
|
||||
self.chunk_size = match kind {
|
||||
TerrainTextureKind::Height => size.map(|x| x - 3),
|
||||
TerrainTextureKind::Mask => size,
|
||||
};
|
||||
}
|
||||
/// For every chunk that has been written in this object since the last clear, copy the texture data from the given textures
|
||||
/// and store it in the given list of texture data, if the list does not already contain data for those coordinates.
|
||||
///
|
||||
/// The purpose of this is to save a backup copy of chunks that are modified by the current brushstroke so that an undo command
|
||||
/// can be created. This should be called immediately before [StrokeChunks::apply] so that the copied textures are unmodified.
|
||||
/// If saved chunk data already exists for some chunk, nothing is done since it is presumed that existing data is the original
|
||||
/// data that we are trying to preserve.
|
||||
///
|
||||
/// - `textures`: The source of texture data.
|
||||
/// - `saved_chunk_data`: The list of chunk data that may be modified if it does not already contain a copy of chunk data for each
|
||||
/// written chunk coordinates.
|
||||
pub fn copy_texture_data(
|
||||
&self,
|
||||
textures: &FxHashMap<Vector2<i32>, TextureResource>,
|
||||
saved_chunk_data: &mut Vec<ChunkData>,
|
||||
) {
|
||||
for (c, _) in self.written_pixels.iter() {
|
||||
if saved_chunk_data.iter().any(|x| x.grid_position == *c) {
|
||||
continue;
|
||||
}
|
||||
let Some(texture) = textures.get(c) else {
|
||||
continue;
|
||||
};
|
||||
saved_chunk_data.push(ChunkData::from_texture(*c, texture));
|
||||
}
|
||||
}
|
||||
/// Use the pixels stored in this object to modify the given textures with
|
||||
/// pixel data from the given StrokeData.
|
||||
/// Once the textures have been modified using this method [StrokeChunks::clear]
|
||||
/// should be called, since the data in this object has served its purpose.
|
||||
pub fn apply<V>(
|
||||
&self,
|
||||
stroke: &StrokeData<V>,
|
||||
textures: &FxHashMap<Vector2<i32>, TextureResource>,
|
||||
) where
|
||||
V: Clone,
|
||||
{
|
||||
for (c, pxs) in self.written_pixels.iter() {
|
||||
let Some(texture) = textures.get(c) else {
|
||||
continue;
|
||||
};
|
||||
let mut texture_data = texture.data_ref();
|
||||
let mut modify = texture_data.modify();
|
||||
let Some(data) = modify.data_mut_of_type::<V>() else {
|
||||
continue;
|
||||
};
|
||||
let origin = self.chunk_to_origin(*c);
|
||||
let row_size = self.row_size();
|
||||
for p in pxs.iter() {
|
||||
let position = match self.kind {
|
||||
TerrainTextureKind::Mask => origin + p.map(|x| x as i32),
|
||||
TerrainTextureKind::Height => origin + p.map(|x| x as i32 - 1),
|
||||
};
|
||||
let Some(value) = stroke.latest_pixel_value(position) else {
|
||||
continue;
|
||||
};
|
||||
let index = p.x as usize + p.y as usize * row_size;
|
||||
data[index].clone_from(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Calculates which chunk contains the given pixel position.
|
||||
#[inline]
|
||||
pub fn pixel_position_to_grid_position(&self, position: Vector2<i32>) -> Vector2<i32> {
|
||||
pixel_position_to_grid_position(position, self.chunk_size)
|
||||
}
|
||||
/// Calculates the origin pixel position of the given chunk.
|
||||
pub fn chunk_to_origin(&self, grid_position: Vector2<i32>) -> Vector2<i32> {
|
||||
Vector2::new(
|
||||
grid_position.x * self.chunk_size.x as i32,
|
||||
grid_position.y * self.chunk_size.y as i32,
|
||||
)
|
||||
}
|
||||
/// The width of the texture in pixels.
|
||||
pub fn row_size(&self) -> usize {
|
||||
match self.kind {
|
||||
TerrainTextureKind::Height => (self.chunk_size.x + 3) as usize,
|
||||
TerrainTextureKind::Mask => self.chunk_size.x as usize,
|
||||
}
|
||||
}
|
||||
/// Calculate the index of a pixel at the given position within texture data,
|
||||
/// based on the row size. The given position is relative to the origin of the texture
|
||||
/// and must be within the bounds of the texture.
|
||||
pub fn pixel_index(&self, position: Vector2<i32>) -> usize {
|
||||
if !self.is_valid_pixel(position) {
|
||||
panic!(
|
||||
"Invalid pixel position: ({}, {}) within ({}, {})",
|
||||
position.x, position.y, self.chunk_size.x, self.chunk_size.y
|
||||
);
|
||||
}
|
||||
let p = match self.kind {
|
||||
TerrainTextureKind::Height => position.map(|x| (x + 1) as usize),
|
||||
TerrainTextureKind::Mask => position.map(|x| x as usize),
|
||||
};
|
||||
p.x + p.y * self.row_size()
|
||||
}
|
||||
/// True if the given pixel position is within the bounds of a chunk for the current kind of chunk data.
|
||||
/// Due to the margins of the height textures, it is permitted to index height textures to -1 and chunk_size.x + 1.
|
||||
pub fn is_valid_pixel(&self, position: Vector2<i32>) -> bool {
|
||||
let size = self.chunk_size.map(|x| x as i32);
|
||||
match self.kind {
|
||||
TerrainTextureKind::Height => {
|
||||
(-1..=size.x + 1).contains(&position.x) && (-1..=size.y + 1).contains(&position.y)
|
||||
}
|
||||
TerrainTextureKind::Mask => {
|
||||
(0..size.x).contains(&position.x) && (0..size.y).contains(&position.x)
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Insert the the pixel at the given position into this data.
|
||||
/// This method determines which chunks have a pixel at that position
|
||||
/// and marks each of those chunks as being modified.
|
||||
pub fn write(&mut self, position: Vector2<i32>) {
|
||||
let grid_pos = self.pixel_position_to_grid_position(position);
|
||||
self.count += 1;
|
||||
match self.kind {
|
||||
TerrainTextureKind::Height => {
|
||||
for x in grid_pos.x - 1..=grid_pos.x + 1 {
|
||||
for y in grid_pos.y - 1..=grid_pos.y + 1 {
|
||||
self.write_height(Vector2::new(x, y), position);
|
||||
}
|
||||
}
|
||||
}
|
||||
TerrainTextureKind::Mask => {
|
||||
let origin = self.chunk_to_origin(grid_pos);
|
||||
let pos = (position - origin).map(|x| x as u32);
|
||||
self.write_to_chunk(grid_pos, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn write_height(&mut self, grid_pos: Vector2<i32>, position: Vector2<i32>) {
|
||||
let origin = self.chunk_to_origin(grid_pos);
|
||||
let pos = position - origin;
|
||||
let size = self.chunk_size;
|
||||
let (w, h) = (size.x as i32, size.y as i32);
|
||||
if (-1..=w + 1).contains(&pos.x) && (-1..=h + 1).contains(&pos.y) {
|
||||
self.write_to_chunk(grid_pos, pos.map(|x| (x + 1) as u32));
|
||||
}
|
||||
}
|
||||
fn write_to_chunk(&mut self, grid_pos: Vector2<i32>, position: Vector2<u32>) {
|
||||
let mut unused = std::mem::take(&mut self.unused_chunks);
|
||||
self.written_pixels
|
||||
.entry(grid_pos)
|
||||
.or_insert_with(|| unused.pop().unwrap_or_default())
|
||||
.insert(position);
|
||||
self.unused_chunks = unused;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
const RANDOM_POINTS: &[(i32, i32)] = &[
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(-1, -1),
|
||||
(20, -123),
|
||||
(-11, 22),
|
||||
(42, 285),
|
||||
(360, -180),
|
||||
(123, -456),
|
||||
(54, 32),
|
||||
(-2, -3),
|
||||
];
|
||||
#[test]
|
||||
fn chunk_to_origin() {
|
||||
let mut chunks = StrokeChunks::default();
|
||||
chunks.set_layout(TerrainTextureKind::Height, Vector2::new(7, 7));
|
||||
assert_eq!(
|
||||
chunks.chunk_to_origin(Vector2::new(0, 0)),
|
||||
Vector2::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
chunks.chunk_to_origin(Vector2::new(1, 0)),
|
||||
Vector2::new(4, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
chunks.chunk_to_origin(Vector2::new(-2, -1)),
|
||||
Vector2::new(-8, -4)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn pixel_position_to_grid_position() {
|
||||
let mut chunks = StrokeChunks::default();
|
||||
chunks.set_layout(TerrainTextureKind::Height, Vector2::new(7, 7));
|
||||
assert_eq!(
|
||||
chunks.pixel_position_to_grid_position(Vector2::new(0, 0)),
|
||||
Vector2::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
chunks.pixel_position_to_grid_position(Vector2::new(2, 3)),
|
||||
Vector2::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
chunks.pixel_position_to_grid_position(Vector2::new(-1, -1)),
|
||||
Vector2::new(-1, -1)
|
||||
);
|
||||
assert_eq!(
|
||||
chunks.pixel_position_to_grid_position(Vector2::new(4, -4)),
|
||||
Vector2::new(1, -1)
|
||||
);
|
||||
}
|
||||
fn test_points_height(size: Vector2<u32>) {
|
||||
let mut chunks = StrokeChunks::default();
|
||||
chunks.set_layout(TerrainTextureKind::Height, size);
|
||||
for p in RANDOM_POINTS.iter() {
|
||||
let p = Vector2::new(p.0, p.1);
|
||||
let grid_pos = chunks.pixel_position_to_grid_position(p);
|
||||
let origin = chunks.chunk_to_origin(grid_pos);
|
||||
let pixel = p - origin;
|
||||
test_point(p, pixel, size.map(|x| x - 1));
|
||||
}
|
||||
let s = size.map(|x| x as i32);
|
||||
for x in -s.x..=s.x * 2 {
|
||||
for y in -s.y..=s.y * 2 {
|
||||
let p = Vector2::new(x, y);
|
||||
let grid_pos = chunks.pixel_position_to_grid_position(p);
|
||||
let origin = chunks.chunk_to_origin(grid_pos);
|
||||
let pixel = p - origin;
|
||||
test_point(p, pixel, size.map(|x| x - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn test_points_mask(size: Vector2<u32>) {
|
||||
let mut chunks = StrokeChunks::default();
|
||||
chunks.set_layout(TerrainTextureKind::Mask, size);
|
||||
for p in RANDOM_POINTS.iter() {
|
||||
let p = Vector2::new(p.0, p.1);
|
||||
let grid_pos = chunks.pixel_position_to_grid_position(p);
|
||||
let origin = chunks.chunk_to_origin(grid_pos);
|
||||
let pixel = p - origin;
|
||||
test_point(p, pixel, size);
|
||||
}
|
||||
let s = size.map(|x| x as i32);
|
||||
for x in -s.x..=s.x * 2 {
|
||||
for y in -s.y..=s.y * 2 {
|
||||
let p = Vector2::new(x, y);
|
||||
let grid_pos = chunks.pixel_position_to_grid_position(p);
|
||||
let origin = chunks.chunk_to_origin(grid_pos);
|
||||
let pixel = p - origin;
|
||||
test_point(p, pixel, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn test_point(p: Vector2<i32>, pixel: Vector2<i32>, size: Vector2<u32>) {
|
||||
assert!(
|
||||
pixel.x >= 0,
|
||||
"({}, {}) -> ({}, {})",
|
||||
p.x,
|
||||
p.y,
|
||||
pixel.x,
|
||||
pixel.y
|
||||
);
|
||||
assert!(
|
||||
pixel.y >= 0,
|
||||
"({}, {}) -> ({}, {})",
|
||||
p.x,
|
||||
p.y,
|
||||
pixel.x,
|
||||
pixel.y
|
||||
);
|
||||
assert!(
|
||||
pixel.x < size.x as i32,
|
||||
"({}, {}) -> ({}, {})",
|
||||
p.x,
|
||||
p.y,
|
||||
pixel.x,
|
||||
pixel.y
|
||||
);
|
||||
assert!(
|
||||
pixel.y < size.y as i32,
|
||||
"({}, {}) -> ({}, {})",
|
||||
p.x,
|
||||
p.y,
|
||||
pixel.x,
|
||||
pixel.y
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn random_points_5x5() {
|
||||
test_points_height(Vector2::new(5, 5));
|
||||
test_points_mask(Vector2::new(5, 5));
|
||||
}
|
||||
#[test]
|
||||
fn random_points_10x10() {
|
||||
test_points_height(Vector2::new(10, 10));
|
||||
test_points_mask(Vector2::new(10, 10));
|
||||
}
|
||||
#[test]
|
||||
fn random_points_257x257() {
|
||||
test_points_height(Vector2::new(257, 257));
|
||||
test_points_mask(Vector2::new(257, 257));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,429 +1,429 @@
|
||||
// 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::{collections::hash_map, path::Path};
|
||||
|
||||
use crate::asset::{Resource, ResourceData};
|
||||
use fxhash::FxHashMap;
|
||||
use fyrox_core::visitor::BinaryBlob;
|
||||
|
||||
use crate::core::{
|
||||
algebra::Vector2, reflect::prelude::*, type_traits::prelude::*, visitor::prelude::*,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const CHUNK_WIDTH: usize = 16;
|
||||
const CHUNK_HEIGHT: usize = 16;
|
||||
const WIDTH_BITS: i32 = (CHUNK_WIDTH - 1) as i32;
|
||||
const HEIGHT_BITS: i32 = (CHUNK_HEIGHT - 1) as i32;
|
||||
|
||||
/// Resource for storing the tile handles of a tile map.
|
||||
pub type TileMapDataResource = Resource<TileMapData>;
|
||||
|
||||
/// Given a tile position, calculate the position of the chunk containing that tile
|
||||
/// and the position of the tile within that chunk, and return them as a pair:
|
||||
/// (chunk position, tile position within chunk)
|
||||
fn tile_position_to_chunk_position(position: Vector2<i32>) -> (Vector2<i32>, Vector2<i32>) {
|
||||
let x = position.x;
|
||||
let y = position.y;
|
||||
let x_chunk = x & !WIDTH_BITS;
|
||||
let y_chunk = y & !HEIGHT_BITS;
|
||||
(
|
||||
Vector2::new(x_chunk, y_chunk),
|
||||
Vector2::new(x - x_chunk, y - y_chunk),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Reflect)]
|
||||
struct Chunk([TileDefinitionHandle; CHUNK_WIDTH * CHUNK_HEIGHT]);
|
||||
|
||||
impl Visit for Chunk {
|
||||
fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
|
||||
if visitor.is_reading() {
|
||||
let mut data = Vec::default();
|
||||
BinaryBlob { vec: &mut data }.visit(name, visitor)?;
|
||||
if data.len() != CHUNK_WIDTH * CHUNK_HEIGHT {
|
||||
return Err(VisitError::User(
|
||||
"Wrong number of handles in a chunk".into(),
|
||||
));
|
||||
}
|
||||
self.0
|
||||
.clone_from_slice(&data[0..CHUNK_WIDTH * CHUNK_HEIGHT]);
|
||||
Ok(())
|
||||
} else {
|
||||
BinaryBlob {
|
||||
vec: &mut self.0.to_vec(),
|
||||
}
|
||||
.visit(name, visitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Chunk {
|
||||
fn default() -> Self {
|
||||
Self([TileDefinitionHandle::EMPTY; CHUNK_WIDTH * CHUNK_HEIGHT])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<Vector2<i32>> for Chunk {
|
||||
type Output = TileDefinitionHandle;
|
||||
|
||||
fn index(&self, index: Vector2<i32>) -> &Self::Output {
|
||||
let x: usize = index.x.try_into().unwrap();
|
||||
let y: usize = index.y.try_into().unwrap();
|
||||
&self.0[x + y * CHUNK_WIDTH]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<Vector2<i32>> for Chunk {
|
||||
fn index_mut(&mut self, index: Vector2<i32>) -> &mut Self::Output {
|
||||
let x: usize = index.x.try_into().unwrap();
|
||||
let y: usize = index.y.try_into().unwrap();
|
||||
&mut self.0[x + y * CHUNK_WIDTH]
|
||||
}
|
||||
}
|
||||
|
||||
impl Chunk {
|
||||
fn iter(&self, offset: Vector2<i32>) -> ChunkIterator {
|
||||
ChunkIterator {
|
||||
position: Vector2::new(0, 0),
|
||||
chunk: self,
|
||||
offset,
|
||||
}
|
||||
}
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.iter().all(|h| *h == TileDefinitionHandle::EMPTY)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChunkIterator<'a> {
|
||||
position: Vector2<i32>,
|
||||
offset: Vector2<i32>,
|
||||
chunk: &'a Chunk,
|
||||
}
|
||||
|
||||
impl Iterator for ChunkIterator<'_> {
|
||||
type Item = (Vector2<i32>, TileDefinitionHandle);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.position.y >= CHUNK_HEIGHT as i32 {
|
||||
return None;
|
||||
}
|
||||
let result_position = self.position;
|
||||
let result = self.chunk[result_position];
|
||||
if self.position.x < (CHUNK_WIDTH - 1) as i32 {
|
||||
self.position.x += 1;
|
||||
} else {
|
||||
self.position.x = 0;
|
||||
self.position.y += 1;
|
||||
}
|
||||
if !result.is_empty() {
|
||||
return Some((result_position + self.offset, result));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over the tiles of a [`TileMapData`] in the form of (position, handle).
|
||||
pub struct TileMapDataIterator<'a, P: 'a> {
|
||||
predicate: P,
|
||||
map_iter: hash_map::Iter<'a, Vector2<i32>, Chunk>,
|
||||
chunk_iter: Option<ChunkIterator<'a>>,
|
||||
}
|
||||
|
||||
impl<P: FnMut(Vector2<i32>) -> bool> Iterator for TileMapDataIterator<'_, P> {
|
||||
type Item = (Vector2<i32>, TileDefinitionHandle);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let chunk_iter = match &mut self.chunk_iter {
|
||||
Some(iter) => iter,
|
||||
None => self.next_chunk()?,
|
||||
};
|
||||
if let Some(result) = chunk_iter.next() {
|
||||
Some(result)
|
||||
} else {
|
||||
self.next_chunk()?.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P: FnMut(Vector2<i32>) -> bool> TileMapDataIterator<'a, P> {
|
||||
fn next_chunk(&mut self) -> Option<&mut ChunkIterator<'a>> {
|
||||
loop {
|
||||
let (pos, chunk) = self.map_iter.next()?;
|
||||
if (self.predicate)(*pos) {
|
||||
return Some(self.chunk_iter.insert(chunk.iter(*pos)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asset containing the tile handles of a tile map.
|
||||
#[derive(Clone, Default, Debug, Reflect, TypeUuidProvider, ComponentProvider)]
|
||||
#[type_uuid(id = "a8e4b6b4-c1bd-4ed9-a753-0d5a3dfe1729")]
|
||||
pub struct TileMapData {
|
||||
content: FxHashMap<Vector2<i32>, Chunk>,
|
||||
}
|
||||
|
||||
impl Visit for TileMapData {
|
||||
fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
|
||||
if !visitor.is_reading() {
|
||||
self.shrink_to_fit();
|
||||
}
|
||||
self.content.visit(name, visitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceData for TileMapData {
|
||||
fn type_uuid(&self) -> Uuid {
|
||||
<Self as TypeUuidProvider>::type_uuid()
|
||||
}
|
||||
|
||||
fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
|
||||
let mut visitor = Visitor::new();
|
||||
self.visit("TileMapData", &mut visitor)?;
|
||||
visitor.save_binary_to_file(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn can_be_saved(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn try_clone_box(&self) -> Option<Box<dyn ResourceData>> {
|
||||
Some(Box::new(self.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl TileSource for TileMapData {
|
||||
fn brush(&self) -> Option<&TileMapBrushResource> {
|
||||
None
|
||||
}
|
||||
fn transformation(&self) -> OrthoTransformation {
|
||||
OrthoTransformation::default()
|
||||
}
|
||||
|
||||
fn get_at(&self, position: Vector2<i32>) -> Option<StampElement> {
|
||||
self.get(position).map(|h| h.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundedTileSource for TileMapData {
|
||||
fn bounding_rect(&self) -> OptionTileRect {
|
||||
let mut rect = OptionTileRect::default();
|
||||
for (pos, _) in self.iter() {
|
||||
rect.push(pos);
|
||||
}
|
||||
rect
|
||||
}
|
||||
}
|
||||
|
||||
impl TileMapData {
|
||||
/// Iterate over all pairs of (position, handle) in this data.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Vector2<i32>, TileDefinitionHandle)> + '_ {
|
||||
let map_iter = self.content.iter();
|
||||
TileMapDataIterator {
|
||||
predicate: |_| true,
|
||||
map_iter,
|
||||
chunk_iter: None,
|
||||
}
|
||||
}
|
||||
/// Iterate over all pairs of (position, handle) in this data.
|
||||
pub fn bounded_iter(
|
||||
&self,
|
||||
bounds: OptionTileRect,
|
||||
) -> impl Iterator<Item = (Vector2<i32>, TileDefinitionHandle)> + '_ {
|
||||
let map_iter = self.content.iter();
|
||||
TileMapDataIterator {
|
||||
predicate: move |pos: Vector2<i32>| {
|
||||
bounds.intersects(TileRect::new(
|
||||
pos.x,
|
||||
pos.y,
|
||||
CHUNK_WIDTH as i32,
|
||||
CHUNK_HEIGHT as i32,
|
||||
))
|
||||
},
|
||||
map_iter,
|
||||
chunk_iter: None,
|
||||
}
|
||||
}
|
||||
/// Apply the updates specified in the given `TileUpdate` and modify it so that it
|
||||
/// contains the tiles require to undo the change. Calling `swap_tiles` twice with the same
|
||||
/// `TileUpdate` object will do the changes and then undo them, leaving the tiles unchanged in the end.
|
||||
pub fn swap_tiles(&mut self, tiles: &mut TilesUpdate) {
|
||||
for (p, h) in tiles.iter_mut() {
|
||||
*h = self.replace(*p, *h);
|
||||
}
|
||||
}
|
||||
/// Get the handle for the tile at the given position, if one exists.
|
||||
pub fn get(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle> {
|
||||
let (chunk, pos) = tile_position_to_chunk_position(position);
|
||||
let chunk = self.content.get(&chunk)?;
|
||||
let handle = chunk[pos];
|
||||
if handle.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(handle)
|
||||
}
|
||||
}
|
||||
/// Replace the handle at the given position with the given handle and return the original
|
||||
/// handle at that position.
|
||||
pub fn replace(
|
||||
&mut self,
|
||||
position: Vector2<i32>,
|
||||
value: Option<TileDefinitionHandle>,
|
||||
) -> Option<TileDefinitionHandle> {
|
||||
let (chunk, pos) = tile_position_to_chunk_position(position);
|
||||
if let Some(chunk) = self.content.get_mut(&chunk) {
|
||||
let handle = &mut chunk[pos];
|
||||
let result = *handle;
|
||||
*handle = value.unwrap_or(TileDefinitionHandle::EMPTY);
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
} else if let Some(value) = value {
|
||||
let chunk = self.content.entry(chunk).or_default();
|
||||
chunk[pos] = value;
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Set a new handle for the tile at the given position.
|
||||
pub fn set(&mut self, position: Vector2<i32>, value: TileDefinitionHandle) {
|
||||
let (chunk, pos) = tile_position_to_chunk_position(position);
|
||||
let chunk = self.content.entry(chunk).or_default();
|
||||
chunk[pos] = value;
|
||||
}
|
||||
/// Remove the tile at the given position.
|
||||
pub fn remove(&mut self, position: Vector2<i32>) {
|
||||
let (chunk, pos) = tile_position_to_chunk_position(position);
|
||||
if let Some(chunk) = self.content.get_mut(&chunk) {
|
||||
chunk[pos] = TileDefinitionHandle::EMPTY;
|
||||
}
|
||||
}
|
||||
/// Remove all empty chunks.
|
||||
pub fn shrink_to_fit(&mut self) {
|
||||
self.content.retain(|_, v| !v.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn v(x: i32, y: i32) -> Vector2<i32> {
|
||||
Vector2::new(x, y)
|
||||
}
|
||||
|
||||
fn h(a: i16, b: i16, c: i16, d: i16) -> TileDefinitionHandle {
|
||||
TileDefinitionHandle::new(a, b, c, d)
|
||||
}
|
||||
|
||||
fn v_ord(a: &Vector2<i32>, b: &Vector2<i32>) -> Ordering {
|
||||
a.y.cmp(&b.y).reverse().then(a.x.cmp(&b.x))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn position_to_chunk() {
|
||||
assert_eq!(
|
||||
tile_position_to_chunk_position(v(16, 16)),
|
||||
(v(16, 16), v(0, 0))
|
||||
);
|
||||
assert_eq!(tile_position_to_chunk_position(v(0, 0)), (v(0, 0), v(0, 0)));
|
||||
assert_eq!(
|
||||
tile_position_to_chunk_position(v(-5, 5)),
|
||||
(v(-16, 0), v(11, 5))
|
||||
);
|
||||
assert_eq!(
|
||||
tile_position_to_chunk_position(v(-16, 5)),
|
||||
(v(-16, 0), v(0, 5))
|
||||
);
|
||||
assert_eq!(
|
||||
tile_position_to_chunk_position(v(-17, 5)),
|
||||
(v(-32, 0), v(15, 5))
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn create_chunks() {
|
||||
let mut data = TileMapData::default();
|
||||
let coords = vec![
|
||||
(v(0, 0), h(1, 2, 3, 4)),
|
||||
(v(-1, -2), h(1, 2, 3, 0)),
|
||||
(v(16, 16), h(1, 2, 3, 5)),
|
||||
(v(-1, -1), h(1, 2, 3, 6)),
|
||||
(v(-17, 0), h(1, 2, 3, 7)),
|
||||
];
|
||||
for (pos, handle) in coords.iter() {
|
||||
data.set(*pos, *handle);
|
||||
}
|
||||
let mut coords = coords
|
||||
.into_iter()
|
||||
.map(|(p, _)| tile_position_to_chunk_position(p).0)
|
||||
.collect::<Vec<_>>();
|
||||
coords.sort_by(v_ord);
|
||||
coords.dedup();
|
||||
let mut result = data.content.keys().copied().collect::<Vec<_>>();
|
||||
result.sort_by(v_ord);
|
||||
assert_eq!(result, coords);
|
||||
}
|
||||
#[test]
|
||||
fn iter_full_chunk() {
|
||||
let mut data = TileMapData::default();
|
||||
let mut required = FxHashSet::default();
|
||||
let mut extra = Vec::default();
|
||||
for x in 0..CHUNK_WIDTH as i32 {
|
||||
for y in 0..CHUNK_HEIGHT as i32 {
|
||||
data.set(v(x, y), h(0, 0, 0, 0));
|
||||
required.insert(v(x, y));
|
||||
}
|
||||
}
|
||||
for (result, _) in data.iter() {
|
||||
if !required.remove(&result) {
|
||||
extra.push(result);
|
||||
}
|
||||
}
|
||||
let required = required.into_iter().collect::<Vec<_>>();
|
||||
assert_eq!((required, extra), (vec![], vec![]));
|
||||
}
|
||||
#[test]
|
||||
fn iter() {
|
||||
let mut data = TileMapData::default();
|
||||
let mut coords = vec![
|
||||
(v(0, 0), h(1, 2, 3, 4)),
|
||||
(v(-1, -2), h(1, 2, 3, 0)),
|
||||
(v(16, 16), h(1, 2, 3, 5)),
|
||||
(v(-1, -1), h(1, 2, 3, 6)),
|
||||
(v(-17, 0), h(1, 2, 3, 7)),
|
||||
];
|
||||
for (pos, handle) in coords.iter() {
|
||||
data.set(*pos, *handle);
|
||||
}
|
||||
let mut result = data.iter().collect::<Vec<_>>();
|
||||
result.sort_by(|(a, _), (b, _)| v_ord(a, b));
|
||||
coords.sort_by(|(a, _), (b, _)| v_ord(a, b));
|
||||
assert_eq!(result, coords);
|
||||
}
|
||||
}
|
||||
// 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::{collections::hash_map, path::Path};
|
||||
|
||||
use crate::asset::{Resource, ResourceData};
|
||||
use fxhash::FxHashMap;
|
||||
use fyrox_core::visitor::BinaryBlob;
|
||||
|
||||
use crate::core::{
|
||||
algebra::Vector2, reflect::prelude::*, type_traits::prelude::*, visitor::prelude::*,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const CHUNK_WIDTH: usize = 16;
|
||||
const CHUNK_HEIGHT: usize = 16;
|
||||
const WIDTH_BITS: i32 = (CHUNK_WIDTH - 1) as i32;
|
||||
const HEIGHT_BITS: i32 = (CHUNK_HEIGHT - 1) as i32;
|
||||
|
||||
/// Resource for storing the tile handles of a tile map.
|
||||
pub type TileMapDataResource = Resource<TileMapData>;
|
||||
|
||||
/// Given a tile position, calculate the position of the chunk containing that tile
|
||||
/// and the position of the tile within that chunk, and return them as a pair:
|
||||
/// (chunk position, tile position within chunk)
|
||||
fn tile_position_to_chunk_position(position: Vector2<i32>) -> (Vector2<i32>, Vector2<i32>) {
|
||||
let x = position.x;
|
||||
let y = position.y;
|
||||
let x_chunk = x & !WIDTH_BITS;
|
||||
let y_chunk = y & !HEIGHT_BITS;
|
||||
(
|
||||
Vector2::new(x_chunk, y_chunk),
|
||||
Vector2::new(x - x_chunk, y - y_chunk),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Reflect)]
|
||||
struct Chunk([TileDefinitionHandle; CHUNK_WIDTH * CHUNK_HEIGHT]);
|
||||
|
||||
impl Visit for Chunk {
|
||||
fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
|
||||
if visitor.is_reading() {
|
||||
let mut data = Vec::default();
|
||||
BinaryBlob { vec: &mut data }.visit(name, visitor)?;
|
||||
if data.len() != CHUNK_WIDTH * CHUNK_HEIGHT {
|
||||
return Err(VisitError::User(
|
||||
"Wrong number of handles in a chunk".into(),
|
||||
));
|
||||
}
|
||||
self.0
|
||||
.clone_from_slice(&data[0..CHUNK_WIDTH * CHUNK_HEIGHT]);
|
||||
Ok(())
|
||||
} else {
|
||||
BinaryBlob {
|
||||
vec: &mut self.0.to_vec(),
|
||||
}
|
||||
.visit(name, visitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Chunk {
|
||||
fn default() -> Self {
|
||||
Self([TileDefinitionHandle::EMPTY; CHUNK_WIDTH * CHUNK_HEIGHT])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<Vector2<i32>> for Chunk {
|
||||
type Output = TileDefinitionHandle;
|
||||
|
||||
fn index(&self, index: Vector2<i32>) -> &Self::Output {
|
||||
let x: usize = index.x.try_into().unwrap();
|
||||
let y: usize = index.y.try_into().unwrap();
|
||||
&self.0[x + y * CHUNK_WIDTH]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<Vector2<i32>> for Chunk {
|
||||
fn index_mut(&mut self, index: Vector2<i32>) -> &mut Self::Output {
|
||||
let x: usize = index.x.try_into().unwrap();
|
||||
let y: usize = index.y.try_into().unwrap();
|
||||
&mut self.0[x + y * CHUNK_WIDTH]
|
||||
}
|
||||
}
|
||||
|
||||
impl Chunk {
|
||||
fn iter(&self, offset: Vector2<i32>) -> ChunkIterator {
|
||||
ChunkIterator {
|
||||
position: Vector2::new(0, 0),
|
||||
chunk: self,
|
||||
offset,
|
||||
}
|
||||
}
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.iter().all(|h| *h == TileDefinitionHandle::EMPTY)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChunkIterator<'a> {
|
||||
position: Vector2<i32>,
|
||||
offset: Vector2<i32>,
|
||||
chunk: &'a Chunk,
|
||||
}
|
||||
|
||||
impl Iterator for ChunkIterator<'_> {
|
||||
type Item = (Vector2<i32>, TileDefinitionHandle);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.position.y >= CHUNK_HEIGHT as i32 {
|
||||
return None;
|
||||
}
|
||||
let result_position = self.position;
|
||||
let result = self.chunk[result_position];
|
||||
if self.position.x < (CHUNK_WIDTH - 1) as i32 {
|
||||
self.position.x += 1;
|
||||
} else {
|
||||
self.position.x = 0;
|
||||
self.position.y += 1;
|
||||
}
|
||||
if !result.is_empty() {
|
||||
return Some((result_position + self.offset, result));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over the tiles of a [`TileMapData`] in the form of (position, handle).
|
||||
pub struct TileMapDataIterator<'a, P: 'a> {
|
||||
predicate: P,
|
||||
map_iter: hash_map::Iter<'a, Vector2<i32>, Chunk>,
|
||||
chunk_iter: Option<ChunkIterator<'a>>,
|
||||
}
|
||||
|
||||
impl<P: FnMut(Vector2<i32>) -> bool> Iterator for TileMapDataIterator<'_, P> {
|
||||
type Item = (Vector2<i32>, TileDefinitionHandle);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let chunk_iter = match &mut self.chunk_iter {
|
||||
Some(iter) => iter,
|
||||
None => self.next_chunk()?,
|
||||
};
|
||||
if let Some(result) = chunk_iter.next() {
|
||||
Some(result)
|
||||
} else {
|
||||
self.next_chunk()?.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P: FnMut(Vector2<i32>) -> bool> TileMapDataIterator<'a, P> {
|
||||
fn next_chunk(&mut self) -> Option<&mut ChunkIterator<'a>> {
|
||||
loop {
|
||||
let (pos, chunk) = self.map_iter.next()?;
|
||||
if (self.predicate)(*pos) {
|
||||
return Some(self.chunk_iter.insert(chunk.iter(*pos)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asset containing the tile handles of a tile map.
|
||||
#[derive(Clone, Default, Debug, Reflect, TypeUuidProvider, ComponentProvider)]
|
||||
#[type_uuid(id = "a8e4b6b4-c1bd-4ed9-a753-0d5a3dfe1729")]
|
||||
pub struct TileMapData {
|
||||
content: FxHashMap<Vector2<i32>, Chunk>,
|
||||
}
|
||||
|
||||
impl Visit for TileMapData {
|
||||
fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
|
||||
if !visitor.is_reading() {
|
||||
self.shrink_to_fit();
|
||||
}
|
||||
self.content.visit(name, visitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceData for TileMapData {
|
||||
fn type_uuid(&self) -> Uuid {
|
||||
<Self as TypeUuidProvider>::type_uuid()
|
||||
}
|
||||
|
||||
fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
|
||||
let mut visitor = Visitor::new();
|
||||
self.visit("TileMapData", &mut visitor)?;
|
||||
visitor.save_binary_to_file(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn can_be_saved(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn try_clone_box(&self) -> Option<Box<dyn ResourceData>> {
|
||||
Some(Box::new(self.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl TileSource for TileMapData {
|
||||
fn brush(&self) -> Option<&TileMapBrushResource> {
|
||||
None
|
||||
}
|
||||
fn transformation(&self) -> OrthoTransformation {
|
||||
OrthoTransformation::default()
|
||||
}
|
||||
|
||||
fn get_at(&self, position: Vector2<i32>) -> Option<StampElement> {
|
||||
self.get(position).map(|h| h.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundedTileSource for TileMapData {
|
||||
fn bounding_rect(&self) -> OptionTileRect {
|
||||
let mut rect = OptionTileRect::default();
|
||||
for (pos, _) in self.iter() {
|
||||
rect.push(pos);
|
||||
}
|
||||
rect
|
||||
}
|
||||
}
|
||||
|
||||
impl TileMapData {
|
||||
/// Iterate over all pairs of (position, handle) in this data.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Vector2<i32>, TileDefinitionHandle)> + '_ {
|
||||
let map_iter = self.content.iter();
|
||||
TileMapDataIterator {
|
||||
predicate: |_| true,
|
||||
map_iter,
|
||||
chunk_iter: None,
|
||||
}
|
||||
}
|
||||
/// Iterate over all pairs of (position, handle) in this data.
|
||||
pub fn bounded_iter(
|
||||
&self,
|
||||
bounds: OptionTileRect,
|
||||
) -> impl Iterator<Item = (Vector2<i32>, TileDefinitionHandle)> + '_ {
|
||||
let map_iter = self.content.iter();
|
||||
TileMapDataIterator {
|
||||
predicate: move |pos: Vector2<i32>| {
|
||||
bounds.intersects(TileRect::new(
|
||||
pos.x,
|
||||
pos.y,
|
||||
CHUNK_WIDTH as i32,
|
||||
CHUNK_HEIGHT as i32,
|
||||
))
|
||||
},
|
||||
map_iter,
|
||||
chunk_iter: None,
|
||||
}
|
||||
}
|
||||
/// Apply the updates specified in the given `TileUpdate` and modify it so that it
|
||||
/// contains the tiles require to undo the change. Calling `swap_tiles` twice with the same
|
||||
/// `TileUpdate` object will do the changes and then undo them, leaving the tiles unchanged in the end.
|
||||
pub fn swap_tiles(&mut self, tiles: &mut TilesUpdate) {
|
||||
for (p, h) in tiles.iter_mut() {
|
||||
*h = self.replace(*p, *h);
|
||||
}
|
||||
}
|
||||
/// Get the handle for the tile at the given position, if one exists.
|
||||
pub fn get(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle> {
|
||||
let (chunk, pos) = tile_position_to_chunk_position(position);
|
||||
let chunk = self.content.get(&chunk)?;
|
||||
let handle = chunk[pos];
|
||||
if handle.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(handle)
|
||||
}
|
||||
}
|
||||
/// Replace the handle at the given position with the given handle and return the original
|
||||
/// handle at that position.
|
||||
pub fn replace(
|
||||
&mut self,
|
||||
position: Vector2<i32>,
|
||||
value: Option<TileDefinitionHandle>,
|
||||
) -> Option<TileDefinitionHandle> {
|
||||
let (chunk, pos) = tile_position_to_chunk_position(position);
|
||||
if let Some(chunk) = self.content.get_mut(&chunk) {
|
||||
let handle = &mut chunk[pos];
|
||||
let result = *handle;
|
||||
*handle = value.unwrap_or(TileDefinitionHandle::EMPTY);
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
} else if let Some(value) = value {
|
||||
let chunk = self.content.entry(chunk).or_default();
|
||||
chunk[pos] = value;
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Set a new handle for the tile at the given position.
|
||||
pub fn set(&mut self, position: Vector2<i32>, value: TileDefinitionHandle) {
|
||||
let (chunk, pos) = tile_position_to_chunk_position(position);
|
||||
let chunk = self.content.entry(chunk).or_default();
|
||||
chunk[pos] = value;
|
||||
}
|
||||
/// Remove the tile at the given position.
|
||||
pub fn remove(&mut self, position: Vector2<i32>) {
|
||||
let (chunk, pos) = tile_position_to_chunk_position(position);
|
||||
if let Some(chunk) = self.content.get_mut(&chunk) {
|
||||
chunk[pos] = TileDefinitionHandle::EMPTY;
|
||||
}
|
||||
}
|
||||
/// Remove all empty chunks.
|
||||
pub fn shrink_to_fit(&mut self) {
|
||||
self.content.retain(|_, v| !v.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn v(x: i32, y: i32) -> Vector2<i32> {
|
||||
Vector2::new(x, y)
|
||||
}
|
||||
|
||||
fn h(a: i16, b: i16, c: i16, d: i16) -> TileDefinitionHandle {
|
||||
TileDefinitionHandle::new(a, b, c, d)
|
||||
}
|
||||
|
||||
fn v_ord(a: &Vector2<i32>, b: &Vector2<i32>) -> Ordering {
|
||||
a.y.cmp(&b.y).reverse().then(a.x.cmp(&b.x))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn position_to_chunk() {
|
||||
assert_eq!(
|
||||
tile_position_to_chunk_position(v(16, 16)),
|
||||
(v(16, 16), v(0, 0))
|
||||
);
|
||||
assert_eq!(tile_position_to_chunk_position(v(0, 0)), (v(0, 0), v(0, 0)));
|
||||
assert_eq!(
|
||||
tile_position_to_chunk_position(v(-5, 5)),
|
||||
(v(-16, 0), v(11, 5))
|
||||
);
|
||||
assert_eq!(
|
||||
tile_position_to_chunk_position(v(-16, 5)),
|
||||
(v(-16, 0), v(0, 5))
|
||||
);
|
||||
assert_eq!(
|
||||
tile_position_to_chunk_position(v(-17, 5)),
|
||||
(v(-32, 0), v(15, 5))
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn create_chunks() {
|
||||
let mut data = TileMapData::default();
|
||||
let coords = vec![
|
||||
(v(0, 0), h(1, 2, 3, 4)),
|
||||
(v(-1, -2), h(1, 2, 3, 0)),
|
||||
(v(16, 16), h(1, 2, 3, 5)),
|
||||
(v(-1, -1), h(1, 2, 3, 6)),
|
||||
(v(-17, 0), h(1, 2, 3, 7)),
|
||||
];
|
||||
for (pos, handle) in coords.iter() {
|
||||
data.set(*pos, *handle);
|
||||
}
|
||||
let mut coords = coords
|
||||
.into_iter()
|
||||
.map(|(p, _)| tile_position_to_chunk_position(p).0)
|
||||
.collect::<Vec<_>>();
|
||||
coords.sort_by(v_ord);
|
||||
coords.dedup();
|
||||
let mut result = data.content.keys().copied().collect::<Vec<_>>();
|
||||
result.sort_by(v_ord);
|
||||
assert_eq!(result, coords);
|
||||
}
|
||||
#[test]
|
||||
fn iter_full_chunk() {
|
||||
let mut data = TileMapData::default();
|
||||
let mut required = FxHashSet::default();
|
||||
let mut extra = Vec::default();
|
||||
for x in 0..CHUNK_WIDTH as i32 {
|
||||
for y in 0..CHUNK_HEIGHT as i32 {
|
||||
data.set(v(x, y), h(0, 0, 0, 0));
|
||||
required.insert(v(x, y));
|
||||
}
|
||||
}
|
||||
for (result, _) in data.iter() {
|
||||
if !required.remove(&result) {
|
||||
extra.push(result);
|
||||
}
|
||||
}
|
||||
let required = required.into_iter().collect::<Vec<_>>();
|
||||
assert_eq!((required, extra), (vec![], vec![]));
|
||||
}
|
||||
#[test]
|
||||
fn iter() {
|
||||
let mut data = TileMapData::default();
|
||||
let mut coords = vec![
|
||||
(v(0, 0), h(1, 2, 3, 4)),
|
||||
(v(-1, -2), h(1, 2, 3, 0)),
|
||||
(v(16, 16), h(1, 2, 3, 5)),
|
||||
(v(-1, -1), h(1, 2, 3, 6)),
|
||||
(v(-17, 0), h(1, 2, 3, 7)),
|
||||
];
|
||||
for (pos, handle) in coords.iter() {
|
||||
data.set(*pos, *handle);
|
||||
}
|
||||
let mut result = data.iter().collect::<Vec<_>>();
|
||||
result.sort_by(|(a, _), (b, _)| v_ord(a, b));
|
||||
coords.sort_by(|(a, _), (b, _)| v_ord(a, b));
|
||||
assert_eq!(result, coords);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,268 +1,268 @@
|
||||
// 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.
|
||||
|
||||
//! Rendering a tile map is often not as simple as rendering each tile in its
|
||||
//! proper place. Sometimes tiles need to be made invisible or replaced by other
|
||||
//! tiles. Sometimes additional rendering needs to happen along with some tile, such
|
||||
//! as for creating highlights or other special effects.
|
||||
//!
|
||||
//! For this purpose, a tile map may optionally have a list of [`TileMapEffect`]
|
||||
//! references which can intervene in the rendering process of the tile map.
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
algebra::{Matrix4, Vector2, Vector3, Vector4},
|
||||
parking_lot::Mutex,
|
||||
},
|
||||
scene::mesh::vertex::StaticVertex,
|
||||
};
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A reference to [`TileMapEffect`]. A TileMap keeps some of these if it needs to be
|
||||
/// rendered specially.
|
||||
pub type TileMapEffectRef = Arc<Mutex<dyn TileMapEffect>>;
|
||||
|
||||
/// A trait for objects that can perform specialized rendering for a tile map by
|
||||
/// adding them to [`TileMap::before_effects`] or [`TileMap::after_effects`],
|
||||
/// depending on whether the effect should render before the tile map renders
|
||||
/// or after the tile map renders.
|
||||
pub trait TileMapEffect: Send + Debug {
|
||||
/// Use the given context to render the special effect for the [`TileMap`].
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext);
|
||||
}
|
||||
|
||||
/// Renders a rectangle of the given material at the given position in the tile map.
|
||||
#[derive(Debug)]
|
||||
pub struct TileCursorEffect {
|
||||
/// The position of the cursor in the tile map.
|
||||
pub position: Option<Vector2<i32>>,
|
||||
/// The material to use to render the cursor.
|
||||
pub material: Option<MaterialResource>,
|
||||
}
|
||||
|
||||
impl TileMapEffect for TileCursorEffect {
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext) {
|
||||
if let Some(material) = self.material.as_ref() {
|
||||
if let Some(position) = self.position {
|
||||
push_cursor(position, material, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders borders of the given material around the given positions in the tile map.
|
||||
#[derive(Debug)]
|
||||
pub struct TileSelectionEffect {
|
||||
/// This vector is added to the positions before rendering.
|
||||
pub offset: Option<Vector2<i32>>,
|
||||
/// The positions at which to draw the borders
|
||||
pub positions: FxHashSet<Vector2<i32>>,
|
||||
/// The size of the border
|
||||
pub thickness: f32,
|
||||
/// The material to use to render the border
|
||||
pub material: Option<MaterialResource>,
|
||||
}
|
||||
|
||||
impl TileMapEffect for TileSelectionEffect {
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext) {
|
||||
if let (Some(material), Some(offset)) = (self.material.as_ref(), self.offset) {
|
||||
for &position in self.positions.iter() {
|
||||
let position = position + offset;
|
||||
push_highlight(position, material, self.thickness, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the tiles at the given positions to invisible.
|
||||
#[derive(Debug)]
|
||||
pub struct TileEraseEffect {
|
||||
/// The positions of the tiles to hide
|
||||
pub positions: FxHashSet<Vector2<i32>>,
|
||||
}
|
||||
|
||||
impl TileMapEffect for TileEraseEffect {
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext) {
|
||||
for &position in self.positions.iter() {
|
||||
context.set_tile_visible(position, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the given tiles with the given offset, and sets the drawn tile positions
|
||||
/// to invisible so that no other tiles will be drawn there.
|
||||
#[derive(Debug)]
|
||||
pub struct TileOverlayEffect {
|
||||
/// True if the tiles are to be drawn. If false, then this effect does nothing.
|
||||
pub active: bool,
|
||||
/// Vector to be added to the positions of the tiles before rendering
|
||||
pub offset: Vector2<i32>,
|
||||
/// The tiles to render
|
||||
pub tiles: FxHashMap<Vector2<i32>, TileDefinitionHandle>,
|
||||
}
|
||||
|
||||
impl TileMapEffect for TileOverlayEffect {
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext) {
|
||||
if !self.active {
|
||||
return;
|
||||
}
|
||||
for (&position, &handle) in self.tiles.iter() {
|
||||
let position = position + self.offset;
|
||||
if context.is_tile_visible(position) {
|
||||
context.draw_tile(position, handle);
|
||||
context.set_tile_visible(position, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses the given tile update to render the replacement tiles and make
|
||||
/// the erased tiles invisible.
|
||||
#[derive(Debug)]
|
||||
pub struct TileUpdateEffect {
|
||||
/// True if the tiles are to be drawn. If false, then this effect does nothing.
|
||||
pub active: bool,
|
||||
/// The update data to render in the tile map
|
||||
pub update: TransTilesUpdate,
|
||||
}
|
||||
|
||||
impl TileMapEffect for TileUpdateEffect {
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext) {
|
||||
if !self.active {
|
||||
return;
|
||||
}
|
||||
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.as_ref().map(|v| v.pair()) {
|
||||
let handle = context
|
||||
.tile_set
|
||||
.get_transformed_version(transform, handle)
|
||||
.unwrap_or(handle);
|
||||
context.draw_tile(position, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_highlight_vertex(transform: &Matrix4<f32>, position: Vector2<f32>) -> StaticVertex {
|
||||
StaticVertex {
|
||||
position: transform
|
||||
.transform_point(&position.to_homogeneous().into())
|
||||
.coords,
|
||||
tex_coord: Vector2::default(),
|
||||
normal: Vector3::new(0.0, 0.0, 1.0),
|
||||
tangent: Vector4::new(0.0, 1.0, 0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_highlight(
|
||||
position: Vector2<i32>,
|
||||
material: &MaterialResource,
|
||||
thickness: f32,
|
||||
ctx: &mut TileMapRenderContext,
|
||||
) {
|
||||
let transform = ctx.transform();
|
||||
let position = position.cast::<f32>();
|
||||
let t = thickness;
|
||||
let vertices = [
|
||||
(0.0, 1.0),
|
||||
(1.0, 1.0),
|
||||
(1.0, 0.0),
|
||||
(0.0, 0.0),
|
||||
(t, 1.0 - t),
|
||||
(1.0 - t, 1.0 - t),
|
||||
(1.0 - t, t),
|
||||
(t, t),
|
||||
]
|
||||
.map(|(x, y)| Vector2::new(x, y))
|
||||
.map(|p| make_highlight_vertex(transform, position + p));
|
||||
|
||||
let triangles = [
|
||||
[0, 4, 5],
|
||||
[0, 1, 5],
|
||||
[1, 5, 6],
|
||||
[1, 2, 6],
|
||||
[2, 6, 7],
|
||||
[2, 3, 7],
|
||||
[3, 7, 4],
|
||||
[3, 0, 4],
|
||||
]
|
||||
.map(TriangleDefinition);
|
||||
|
||||
let sort_index = ctx
|
||||
.context
|
||||
.calculate_sorting_index(ctx.position())
|
||||
.saturating_sub(1);
|
||||
|
||||
ctx.context.storage.push_triangles(
|
||||
ctx.context.dynamic_surface_cache,
|
||||
StaticVertex::layout(),
|
||||
material,
|
||||
RenderPath::Forward,
|
||||
sort_index,
|
||||
ctx.tile_map_handle(),
|
||||
&mut move |mut vertex_buffer, mut triangle_buffer| {
|
||||
let start_vertex_index = vertex_buffer.vertex_count();
|
||||
|
||||
vertex_buffer.push_vertices(&vertices).unwrap();
|
||||
|
||||
triangle_buffer
|
||||
.push_triangles_iter_with_offset(start_vertex_index, triangles.into_iter());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn push_cursor(
|
||||
position: Vector2<i32>,
|
||||
material: &MaterialResource,
|
||||
ctx: &mut TileMapRenderContext,
|
||||
) {
|
||||
let transform = ctx.transform();
|
||||
let position = position.cast::<f32>();
|
||||
let vertices = [(0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]
|
||||
.map(|(x, y)| Vector2::new(x, y))
|
||||
.map(|p| make_highlight_vertex(transform, position + p));
|
||||
|
||||
let triangles = [[0, 1, 2], [0, 2, 3]].map(TriangleDefinition);
|
||||
|
||||
let sort_index = ctx.context.calculate_sorting_index(ctx.position());
|
||||
|
||||
ctx.context.storage.push_triangles(
|
||||
ctx.context.dynamic_surface_cache,
|
||||
StaticVertex::layout(),
|
||||
material,
|
||||
RenderPath::Forward,
|
||||
sort_index,
|
||||
ctx.tile_map_handle(),
|
||||
&mut move |mut vertex_buffer, mut triangle_buffer| {
|
||||
let start_vertex_index = vertex_buffer.vertex_count();
|
||||
|
||||
vertex_buffer.push_vertices(&vertices).unwrap();
|
||||
|
||||
triangle_buffer
|
||||
.push_triangles_iter_with_offset(start_vertex_index, triangles.into_iter());
|
||||
},
|
||||
);
|
||||
}
|
||||
// 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.
|
||||
|
||||
//! Rendering a tile map is often not as simple as rendering each tile in its
|
||||
//! proper place. Sometimes tiles need to be made invisible or replaced by other
|
||||
//! tiles. Sometimes additional rendering needs to happen along with some tile, such
|
||||
//! as for creating highlights or other special effects.
|
||||
//!
|
||||
//! For this purpose, a tile map may optionally have a list of [`TileMapEffect`]
|
||||
//! references which can intervene in the rendering process of the tile map.
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
algebra::{Matrix4, Vector2, Vector3, Vector4},
|
||||
parking_lot::Mutex,
|
||||
},
|
||||
scene::mesh::vertex::StaticVertex,
|
||||
};
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A reference to [`TileMapEffect`]. A TileMap keeps some of these if it needs to be
|
||||
/// rendered specially.
|
||||
pub type TileMapEffectRef = Arc<Mutex<dyn TileMapEffect>>;
|
||||
|
||||
/// A trait for objects that can perform specialized rendering for a tile map by
|
||||
/// adding them to [`TileMap::before_effects`] or [`TileMap::after_effects`],
|
||||
/// depending on whether the effect should render before the tile map renders
|
||||
/// or after the tile map renders.
|
||||
pub trait TileMapEffect: Send + Debug {
|
||||
/// Use the given context to render the special effect for the [`TileMap`].
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext);
|
||||
}
|
||||
|
||||
/// Renders a rectangle of the given material at the given position in the tile map.
|
||||
#[derive(Debug)]
|
||||
pub struct TileCursorEffect {
|
||||
/// The position of the cursor in the tile map.
|
||||
pub position: Option<Vector2<i32>>,
|
||||
/// The material to use to render the cursor.
|
||||
pub material: Option<MaterialResource>,
|
||||
}
|
||||
|
||||
impl TileMapEffect for TileCursorEffect {
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext) {
|
||||
if let Some(material) = self.material.as_ref() {
|
||||
if let Some(position) = self.position {
|
||||
push_cursor(position, material, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders borders of the given material around the given positions in the tile map.
|
||||
#[derive(Debug)]
|
||||
pub struct TileSelectionEffect {
|
||||
/// This vector is added to the positions before rendering.
|
||||
pub offset: Option<Vector2<i32>>,
|
||||
/// The positions at which to draw the borders
|
||||
pub positions: FxHashSet<Vector2<i32>>,
|
||||
/// The size of the border
|
||||
pub thickness: f32,
|
||||
/// The material to use to render the border
|
||||
pub material: Option<MaterialResource>,
|
||||
}
|
||||
|
||||
impl TileMapEffect for TileSelectionEffect {
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext) {
|
||||
if let (Some(material), Some(offset)) = (self.material.as_ref(), self.offset) {
|
||||
for &position in self.positions.iter() {
|
||||
let position = position + offset;
|
||||
push_highlight(position, material, self.thickness, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the tiles at the given positions to invisible.
|
||||
#[derive(Debug)]
|
||||
pub struct TileEraseEffect {
|
||||
/// The positions of the tiles to hide
|
||||
pub positions: FxHashSet<Vector2<i32>>,
|
||||
}
|
||||
|
||||
impl TileMapEffect for TileEraseEffect {
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext) {
|
||||
for &position in self.positions.iter() {
|
||||
context.set_tile_visible(position, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the given tiles with the given offset, and sets the drawn tile positions
|
||||
/// to invisible so that no other tiles will be drawn there.
|
||||
#[derive(Debug)]
|
||||
pub struct TileOverlayEffect {
|
||||
/// True if the tiles are to be drawn. If false, then this effect does nothing.
|
||||
pub active: bool,
|
||||
/// Vector to be added to the positions of the tiles before rendering
|
||||
pub offset: Vector2<i32>,
|
||||
/// The tiles to render
|
||||
pub tiles: FxHashMap<Vector2<i32>, TileDefinitionHandle>,
|
||||
}
|
||||
|
||||
impl TileMapEffect for TileOverlayEffect {
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext) {
|
||||
if !self.active {
|
||||
return;
|
||||
}
|
||||
for (&position, &handle) in self.tiles.iter() {
|
||||
let position = position + self.offset;
|
||||
if context.is_tile_visible(position) {
|
||||
context.draw_tile(position, handle);
|
||||
context.set_tile_visible(position, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses the given tile update to render the replacement tiles and make
|
||||
/// the erased tiles invisible.
|
||||
#[derive(Debug)]
|
||||
pub struct TileUpdateEffect {
|
||||
/// True if the tiles are to be drawn. If false, then this effect does nothing.
|
||||
pub active: bool,
|
||||
/// The update data to render in the tile map
|
||||
pub update: TransTilesUpdate,
|
||||
}
|
||||
|
||||
impl TileMapEffect for TileUpdateEffect {
|
||||
fn render_special_tiles(&self, context: &mut TileMapRenderContext) {
|
||||
if !self.active {
|
||||
return;
|
||||
}
|
||||
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.as_ref().map(|v| v.pair()) {
|
||||
let handle = context
|
||||
.tile_set
|
||||
.get_transformed_version(transform, handle)
|
||||
.unwrap_or(handle);
|
||||
context.draw_tile(position, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_highlight_vertex(transform: &Matrix4<f32>, position: Vector2<f32>) -> StaticVertex {
|
||||
StaticVertex {
|
||||
position: transform
|
||||
.transform_point(&position.to_homogeneous().into())
|
||||
.coords,
|
||||
tex_coord: Vector2::default(),
|
||||
normal: Vector3::new(0.0, 0.0, 1.0),
|
||||
tangent: Vector4::new(0.0, 1.0, 0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_highlight(
|
||||
position: Vector2<i32>,
|
||||
material: &MaterialResource,
|
||||
thickness: f32,
|
||||
ctx: &mut TileMapRenderContext,
|
||||
) {
|
||||
let transform = ctx.transform();
|
||||
let position = position.cast::<f32>();
|
||||
let t = thickness;
|
||||
let vertices = [
|
||||
(0.0, 1.0),
|
||||
(1.0, 1.0),
|
||||
(1.0, 0.0),
|
||||
(0.0, 0.0),
|
||||
(t, 1.0 - t),
|
||||
(1.0 - t, 1.0 - t),
|
||||
(1.0 - t, t),
|
||||
(t, t),
|
||||
]
|
||||
.map(|(x, y)| Vector2::new(x, y))
|
||||
.map(|p| make_highlight_vertex(transform, position + p));
|
||||
|
||||
let triangles = [
|
||||
[0, 4, 5],
|
||||
[0, 1, 5],
|
||||
[1, 5, 6],
|
||||
[1, 2, 6],
|
||||
[2, 6, 7],
|
||||
[2, 3, 7],
|
||||
[3, 7, 4],
|
||||
[3, 0, 4],
|
||||
]
|
||||
.map(TriangleDefinition);
|
||||
|
||||
let sort_index = ctx
|
||||
.context
|
||||
.calculate_sorting_index(ctx.position())
|
||||
.saturating_sub(1);
|
||||
|
||||
ctx.context.storage.push_triangles(
|
||||
ctx.context.dynamic_surface_cache,
|
||||
StaticVertex::layout(),
|
||||
material,
|
||||
RenderPath::Forward,
|
||||
sort_index,
|
||||
ctx.tile_map_handle(),
|
||||
&mut move |mut vertex_buffer, mut triangle_buffer| {
|
||||
let start_vertex_index = vertex_buffer.vertex_count();
|
||||
|
||||
vertex_buffer.push_vertices(&vertices).unwrap();
|
||||
|
||||
triangle_buffer
|
||||
.push_triangles_iter_with_offset(start_vertex_index, triangles.into_iter());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn push_cursor(
|
||||
position: Vector2<i32>,
|
||||
material: &MaterialResource,
|
||||
ctx: &mut TileMapRenderContext,
|
||||
) {
|
||||
let transform = ctx.transform();
|
||||
let position = position.cast::<f32>();
|
||||
let vertices = [(0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]
|
||||
.map(|(x, y)| Vector2::new(x, y))
|
||||
.map(|p| make_highlight_vertex(transform, position + p));
|
||||
|
||||
let triangles = [[0, 1, 2], [0, 2, 3]].map(TriangleDefinition);
|
||||
|
||||
let sort_index = ctx.context.calculate_sorting_index(ctx.position());
|
||||
|
||||
ctx.context.storage.push_triangles(
|
||||
ctx.context.dynamic_surface_cache,
|
||||
StaticVertex::layout(),
|
||||
material,
|
||||
RenderPath::Forward,
|
||||
sort_index,
|
||||
ctx.tile_map_handle(),
|
||||
&mut move |mut vertex_buffer, mut triangle_buffer| {
|
||||
let start_vertex_index = vertex_buffer.vertex_count();
|
||||
|
||||
vertex_buffer.push_vertices(&vertices).unwrap();
|
||||
|
||||
triangle_buffer
|
||||
.push_triangles_iter_with_offset(start_vertex_index, triangles.into_iter());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+263
-263
@@ -1,263 +1,263 @@
|
||||
// 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 nalgebra::{RealField, SVector, Scalar, Vector2};
|
||||
use num_traits::{One, Signed, Zero};
|
||||
use rectutils::{Number, Rect};
|
||||
|
||||
/// Line segment in two dimensions
|
||||
pub type LineSegment2<T> = LineSegment<T, 2>;
|
||||
/// Line segment in three dimensions
|
||||
pub type LineSegment3<T> = LineSegment<T, 3>;
|
||||
|
||||
/// Line segment in any number of dimensions
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LineSegment<T, const D: usize> {
|
||||
/// One end of the line segment, the point returned when interpolating at t = 0.0
|
||||
pub start: SVector<T, D>,
|
||||
/// One end of the line segment, the point returned when interpolating at t = 1.0
|
||||
pub end: SVector<T, D>,
|
||||
}
|
||||
|
||||
impl<T, const D: usize> LineSegment<T, D>
|
||||
where
|
||||
T: Zero + One + Scalar + RealField,
|
||||
{
|
||||
/// Create a new line segment with the given points.
|
||||
pub fn new(start: &SVector<T, D>, end: &SVector<T, D>) -> Self {
|
||||
Self {
|
||||
start: start.clone_owned(),
|
||||
end: end.clone_owned(),
|
||||
}
|
||||
}
|
||||
/// Creates a reversed line segment by swapping `start` and `end`.
|
||||
pub fn swapped(&self) -> Self {
|
||||
Self::new(&self.end, &self.start)
|
||||
}
|
||||
/// The two end-points of the line segment are equal.
|
||||
pub fn is_degenerate(&self) -> bool {
|
||||
self.start == self.end
|
||||
}
|
||||
/// Create a point somewhere between `start` and `end`.
|
||||
/// When t = 0.0, `start` is returned.
|
||||
/// When t = 1.0, `end` is returned.
|
||||
/// The result is `(1.0 - t) * start + t * end`, which may produce points off the line segment,
|
||||
/// if t < 0.0 or t > 1.0.
|
||||
pub fn interpolate(&self, t: T) -> SVector<T, D> {
|
||||
self.start.lerp(&self.end, t)
|
||||
}
|
||||
/// Create a point somewhere between `start` and `end`.
|
||||
/// This is just like [LineSegment::interpolate] except that t is clamped to between 0.0 and 1.0,
|
||||
/// so points off the line segment can never be returned.
|
||||
pub fn interpolate_clamped(&self, t: T) -> SVector<T, D> {
|
||||
self.interpolate(t.clamp(<T as Zero>::zero(), <T as One>::one()))
|
||||
}
|
||||
/// The vector from `start` to `end`
|
||||
pub fn vector(&self) -> SVector<T, D> {
|
||||
self.end.clone() - self.start.clone()
|
||||
}
|
||||
/// The distance between `start` and `end`
|
||||
pub fn length(&self) -> T {
|
||||
self.vector().norm()
|
||||
}
|
||||
/// The square of the distance between `start` and `end`
|
||||
pub fn length_squared(&self) -> T {
|
||||
self.vector().norm_squared()
|
||||
}
|
||||
/// The interpolation parameter of the point on this segment that is closest to the given point.
|
||||
///
|
||||
/// [Stack Exchange question: Find a point on a line segment which is the closest to other point not on the line segment](https://math.stackexchange.com/questions/2193720/find-a-point-on-a-line-segment-which-is-the-closest-to-other-point-not-on-the-li)
|
||||
pub fn nearest_t(&self, point: &SVector<T, D>) -> T {
|
||||
let v = self.vector();
|
||||
let u = self.start.clone() - point;
|
||||
let n2 = v.norm_squared();
|
||||
if n2.is_zero() {
|
||||
return T::zero();
|
||||
}
|
||||
-v.dot(&u) / n2
|
||||
}
|
||||
/// The point on this segment that is closest to the given point.
|
||||
pub fn nearest_point(&self, point: &SVector<T, D>) -> SVector<T, D> {
|
||||
self.interpolate_clamped(self.nearest_t(point))
|
||||
}
|
||||
/// The squared distance between the given point and the nearest point on this line segment.
|
||||
pub fn distance_squared(&self, point: &SVector<T, D>) -> T {
|
||||
(point - self.nearest_point(point)).norm_squared()
|
||||
}
|
||||
/// The distance between the given point and the nearest point on this line segment.
|
||||
pub fn distance(&self, point: &SVector<T, D>) -> T {
|
||||
(point - self.nearest_point(point)).norm()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> LineSegment2<T>
|
||||
where
|
||||
T: Zero + One + Scalar + RealField,
|
||||
{
|
||||
/// AABB for a 2D line segment
|
||||
pub fn bounds(&self) -> Rect<T>
|
||||
where
|
||||
T: Number,
|
||||
{
|
||||
Rect::from_points(self.start, self.end)
|
||||
}
|
||||
/// Test whether a point is collinear with this segment.
|
||||
/// * 0.0 means collinear. Near to 0.0 means near to collinear.
|
||||
/// * Negative means that the point is to the counter-clockwise of `end` as viewed from `start`.
|
||||
/// * Positive means that the point is to the clockwise of `end` as viewed from `start`.
|
||||
pub fn collinearity(&self, point: &Vector2<T>) -> T {
|
||||
let v = self.vector();
|
||||
let u = self.start.clone() - point;
|
||||
v.x.clone() * u.y.clone() - u.x.clone() * v.y.clone()
|
||||
}
|
||||
/// True if this segment intersects the given segment based on collinearity.
|
||||
pub fn intersects(&self, other: &LineSegment2<T>) -> bool {
|
||||
fn pos<T>(t: &T) -> bool
|
||||
where
|
||||
T: Zero + Signed,
|
||||
{
|
||||
t.is_positive() && !t.is_zero()
|
||||
}
|
||||
fn neg<T>(t: &T) -> bool
|
||||
where
|
||||
T: Zero + Signed,
|
||||
{
|
||||
t.is_negative() && !t.is_zero()
|
||||
}
|
||||
let o1 = self.collinearity(&other.start);
|
||||
let o2 = self.collinearity(&other.end);
|
||||
let s1 = other.collinearity(&self.start);
|
||||
let s2 = other.collinearity(&self.end);
|
||||
// If both points of self are left of `other` or both points are right of `other`...
|
||||
if neg(&s1) && neg(&s2) || pos(&s1) && pos(&s2) {
|
||||
return false;
|
||||
}
|
||||
// If both points of `other` are left of self or both points are right of self...
|
||||
if neg(&o1) && neg(&o2) || pos(&o1) && pos(&o2) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use nalgebra::Vector2;
|
||||
#[test]
|
||||
fn nearest_at_start() {
|
||||
let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0));
|
||||
assert_eq!(segment.nearest_t(&Vector2::new(-1.0, -1.0)).max(0.0), 0.0);
|
||||
assert_eq!(
|
||||
segment.nearest_point(&Vector2::new(-1.0, -1.0)),
|
||||
Vector2::new(0.0, 0.0)
|
||||
);
|
||||
assert_eq!(segment.distance_squared(&Vector2::new(-1.0, -1.0)), 2.0);
|
||||
assert_eq!(segment.distance(&Vector2::new(-1.0, 0.0)), 1.0);
|
||||
}
|
||||
#[test]
|
||||
fn nearest_at_end() {
|
||||
let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0));
|
||||
assert_eq!(segment.nearest_t(&Vector2::new(2.0, 2.0)).min(1.0), 1.0);
|
||||
assert_eq!(
|
||||
segment.nearest_point(&Vector2::new(2.0, 2.0)),
|
||||
Vector2::new(1.0, 2.0)
|
||||
);
|
||||
assert_eq!(segment.distance_squared(&Vector2::new(3.0, 2.0)), 4.0);
|
||||
assert_eq!(segment.distance(&Vector2::new(3.0, 2.0)), 2.0);
|
||||
}
|
||||
#[test]
|
||||
fn nearest_in_middle() {
|
||||
let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0));
|
||||
assert_eq!(segment.nearest_t(&Vector2::new(2.5, 0.0)), 0.5);
|
||||
assert_eq!(
|
||||
segment.nearest_point(&Vector2::new(2.5, 0.0)),
|
||||
Vector2::new(0.5, 1.0)
|
||||
);
|
||||
assert_eq!(segment.distance_squared(&Vector2::new(2.5, 0.0)), 5.0);
|
||||
}
|
||||
#[test]
|
||||
fn length() {
|
||||
let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(4.0, 3.0));
|
||||
assert_eq!(segment.length_squared(), 25.0);
|
||||
assert_eq!(segment.length(), 5.0);
|
||||
}
|
||||
#[test]
|
||||
fn degenerate() {
|
||||
let segment = LineSegment2::new(&Vector2::new(1.0, 2.0), &Vector2::new(1.0, 2.0));
|
||||
assert!(segment.is_degenerate());
|
||||
assert_eq!(segment.length_squared(), 0.0);
|
||||
assert_eq!(segment.length(), 0.0);
|
||||
}
|
||||
#[test]
|
||||
fn collinear() {
|
||||
let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0));
|
||||
assert_eq!(segment.collinearity(&Vector2::new(2.0, 4.0)), 0.0);
|
||||
assert_eq!(segment.collinearity(&Vector2::new(0.0, 0.0)), 0.0);
|
||||
assert_eq!(segment.collinearity(&Vector2::new(1.0, 2.0)), 0.0);
|
||||
assert!(
|
||||
segment.collinearity(&Vector2::new(1.0, 5.0)) < 0.0,
|
||||
"{} >= 0.0",
|
||||
segment.collinearity(&Vector2::new(1.0, 5.0))
|
||||
);
|
||||
assert!(
|
||||
segment.collinearity(&Vector2::new(1.0, 3.0)) < 0.0,
|
||||
"{} >= 0.0",
|
||||
segment.collinearity(&Vector2::new(1.0, 3.0))
|
||||
);
|
||||
assert!(
|
||||
segment.collinearity(&Vector2::new(1.0, 1.0)) > 0.0,
|
||||
"{} <= 0.0",
|
||||
segment.collinearity(&Vector2::new(1.0, 1.0))
|
||||
);
|
||||
assert!(
|
||||
segment.collinearity(&Vector2::new(-1.0, -5.0)) > 0.0,
|
||||
"{} <= 0.0",
|
||||
segment.collinearity(&Vector2::new(-1.0, -5.0))
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn intersects() {
|
||||
let a = LineSegment::new(&Vector2::new(1.0, 2.0), &Vector2::new(3.0, 1.0));
|
||||
let b = LineSegment::new(&Vector2::new(2.0, 0.0), &Vector2::new(2.5, 3.0));
|
||||
let c = LineSegment::new(&Vector2::new(1.0, 2.0), &Vector2::new(-3.0, 1.0));
|
||||
assert!(a.intersects(&b));
|
||||
assert!(a.intersects(&c));
|
||||
assert!(b.intersects(&a));
|
||||
assert!(c.intersects(&a));
|
||||
assert!(a.swapped().intersects(&b));
|
||||
assert!(a.swapped().intersects(&c));
|
||||
}
|
||||
#[test]
|
||||
fn not_intersects() {
|
||||
let a = LineSegment::new(&Vector2::new(1.0, 2.0), &Vector2::new(3.0, 1.0));
|
||||
let b = LineSegment::new(&Vector2::new(0.0, 0.0), &Vector2::new(-1.0, 6.0));
|
||||
let c = LineSegment::new(&Vector2::new(2.0, 0.0), &Vector2::new(2.0, -1.0));
|
||||
assert!(!a.intersects(&b));
|
||||
assert!(!b.intersects(&c));
|
||||
assert!(!c.intersects(&a));
|
||||
assert!(!b.intersects(&a));
|
||||
assert!(!c.intersects(&b));
|
||||
assert!(!a.intersects(&c));
|
||||
assert!(!a.swapped().intersects(&b));
|
||||
assert!(!b.swapped().intersects(&c));
|
||||
assert!(!c.swapped().intersects(&a));
|
||||
}
|
||||
}
|
||||
// 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 nalgebra::{RealField, SVector, Scalar, Vector2};
|
||||
use num_traits::{One, Signed, Zero};
|
||||
use rectutils::{Number, Rect};
|
||||
|
||||
/// Line segment in two dimensions
|
||||
pub type LineSegment2<T> = LineSegment<T, 2>;
|
||||
/// Line segment in three dimensions
|
||||
pub type LineSegment3<T> = LineSegment<T, 3>;
|
||||
|
||||
/// Line segment in any number of dimensions
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LineSegment<T, const D: usize> {
|
||||
/// One end of the line segment, the point returned when interpolating at t = 0.0
|
||||
pub start: SVector<T, D>,
|
||||
/// One end of the line segment, the point returned when interpolating at t = 1.0
|
||||
pub end: SVector<T, D>,
|
||||
}
|
||||
|
||||
impl<T, const D: usize> LineSegment<T, D>
|
||||
where
|
||||
T: Zero + One + Scalar + RealField,
|
||||
{
|
||||
/// Create a new line segment with the given points.
|
||||
pub fn new(start: &SVector<T, D>, end: &SVector<T, D>) -> Self {
|
||||
Self {
|
||||
start: start.clone_owned(),
|
||||
end: end.clone_owned(),
|
||||
}
|
||||
}
|
||||
/// Creates a reversed line segment by swapping `start` and `end`.
|
||||
pub fn swapped(&self) -> Self {
|
||||
Self::new(&self.end, &self.start)
|
||||
}
|
||||
/// The two end-points of the line segment are equal.
|
||||
pub fn is_degenerate(&self) -> bool {
|
||||
self.start == self.end
|
||||
}
|
||||
/// Create a point somewhere between `start` and `end`.
|
||||
/// When t = 0.0, `start` is returned.
|
||||
/// When t = 1.0, `end` is returned.
|
||||
/// The result is `(1.0 - t) * start + t * end`, which may produce points off the line segment,
|
||||
/// if t < 0.0 or t > 1.0.
|
||||
pub fn interpolate(&self, t: T) -> SVector<T, D> {
|
||||
self.start.lerp(&self.end, t)
|
||||
}
|
||||
/// Create a point somewhere between `start` and `end`.
|
||||
/// This is just like [LineSegment::interpolate] except that t is clamped to between 0.0 and 1.0,
|
||||
/// so points off the line segment can never be returned.
|
||||
pub fn interpolate_clamped(&self, t: T) -> SVector<T, D> {
|
||||
self.interpolate(t.clamp(<T as Zero>::zero(), <T as One>::one()))
|
||||
}
|
||||
/// The vector from `start` to `end`
|
||||
pub fn vector(&self) -> SVector<T, D> {
|
||||
self.end.clone() - self.start.clone()
|
||||
}
|
||||
/// The distance between `start` and `end`
|
||||
pub fn length(&self) -> T {
|
||||
self.vector().norm()
|
||||
}
|
||||
/// The square of the distance between `start` and `end`
|
||||
pub fn length_squared(&self) -> T {
|
||||
self.vector().norm_squared()
|
||||
}
|
||||
/// The interpolation parameter of the point on this segment that is closest to the given point.
|
||||
///
|
||||
/// [Stack Exchange question: Find a point on a line segment which is the closest to other point not on the line segment](https://math.stackexchange.com/questions/2193720/find-a-point-on-a-line-segment-which-is-the-closest-to-other-point-not-on-the-li)
|
||||
pub fn nearest_t(&self, point: &SVector<T, D>) -> T {
|
||||
let v = self.vector();
|
||||
let u = self.start.clone() - point;
|
||||
let n2 = v.norm_squared();
|
||||
if n2.is_zero() {
|
||||
return T::zero();
|
||||
}
|
||||
-v.dot(&u) / n2
|
||||
}
|
||||
/// The point on this segment that is closest to the given point.
|
||||
pub fn nearest_point(&self, point: &SVector<T, D>) -> SVector<T, D> {
|
||||
self.interpolate_clamped(self.nearest_t(point))
|
||||
}
|
||||
/// The squared distance between the given point and the nearest point on this line segment.
|
||||
pub fn distance_squared(&self, point: &SVector<T, D>) -> T {
|
||||
(point - self.nearest_point(point)).norm_squared()
|
||||
}
|
||||
/// The distance between the given point and the nearest point on this line segment.
|
||||
pub fn distance(&self, point: &SVector<T, D>) -> T {
|
||||
(point - self.nearest_point(point)).norm()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> LineSegment2<T>
|
||||
where
|
||||
T: Zero + One + Scalar + RealField,
|
||||
{
|
||||
/// AABB for a 2D line segment
|
||||
pub fn bounds(&self) -> Rect<T>
|
||||
where
|
||||
T: Number,
|
||||
{
|
||||
Rect::from_points(self.start, self.end)
|
||||
}
|
||||
/// Test whether a point is collinear with this segment.
|
||||
/// * 0.0 means collinear. Near to 0.0 means near to collinear.
|
||||
/// * Negative means that the point is to the counter-clockwise of `end` as viewed from `start`.
|
||||
/// * Positive means that the point is to the clockwise of `end` as viewed from `start`.
|
||||
pub fn collinearity(&self, point: &Vector2<T>) -> T {
|
||||
let v = self.vector();
|
||||
let u = self.start.clone() - point;
|
||||
v.x.clone() * u.y.clone() - u.x.clone() * v.y.clone()
|
||||
}
|
||||
/// True if this segment intersects the given segment based on collinearity.
|
||||
pub fn intersects(&self, other: &LineSegment2<T>) -> bool {
|
||||
fn pos<T>(t: &T) -> bool
|
||||
where
|
||||
T: Zero + Signed,
|
||||
{
|
||||
t.is_positive() && !t.is_zero()
|
||||
}
|
||||
fn neg<T>(t: &T) -> bool
|
||||
where
|
||||
T: Zero + Signed,
|
||||
{
|
||||
t.is_negative() && !t.is_zero()
|
||||
}
|
||||
let o1 = self.collinearity(&other.start);
|
||||
let o2 = self.collinearity(&other.end);
|
||||
let s1 = other.collinearity(&self.start);
|
||||
let s2 = other.collinearity(&self.end);
|
||||
// If both points of self are left of `other` or both points are right of `other`...
|
||||
if neg(&s1) && neg(&s2) || pos(&s1) && pos(&s2) {
|
||||
return false;
|
||||
}
|
||||
// If both points of `other` are left of self or both points are right of self...
|
||||
if neg(&o1) && neg(&o2) || pos(&o1) && pos(&o2) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use nalgebra::Vector2;
|
||||
#[test]
|
||||
fn nearest_at_start() {
|
||||
let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0));
|
||||
assert_eq!(segment.nearest_t(&Vector2::new(-1.0, -1.0)).max(0.0), 0.0);
|
||||
assert_eq!(
|
||||
segment.nearest_point(&Vector2::new(-1.0, -1.0)),
|
||||
Vector2::new(0.0, 0.0)
|
||||
);
|
||||
assert_eq!(segment.distance_squared(&Vector2::new(-1.0, -1.0)), 2.0);
|
||||
assert_eq!(segment.distance(&Vector2::new(-1.0, 0.0)), 1.0);
|
||||
}
|
||||
#[test]
|
||||
fn nearest_at_end() {
|
||||
let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0));
|
||||
assert_eq!(segment.nearest_t(&Vector2::new(2.0, 2.0)).min(1.0), 1.0);
|
||||
assert_eq!(
|
||||
segment.nearest_point(&Vector2::new(2.0, 2.0)),
|
||||
Vector2::new(1.0, 2.0)
|
||||
);
|
||||
assert_eq!(segment.distance_squared(&Vector2::new(3.0, 2.0)), 4.0);
|
||||
assert_eq!(segment.distance(&Vector2::new(3.0, 2.0)), 2.0);
|
||||
}
|
||||
#[test]
|
||||
fn nearest_in_middle() {
|
||||
let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0));
|
||||
assert_eq!(segment.nearest_t(&Vector2::new(2.5, 0.0)), 0.5);
|
||||
assert_eq!(
|
||||
segment.nearest_point(&Vector2::new(2.5, 0.0)),
|
||||
Vector2::new(0.5, 1.0)
|
||||
);
|
||||
assert_eq!(segment.distance_squared(&Vector2::new(2.5, 0.0)), 5.0);
|
||||
}
|
||||
#[test]
|
||||
fn length() {
|
||||
let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(4.0, 3.0));
|
||||
assert_eq!(segment.length_squared(), 25.0);
|
||||
assert_eq!(segment.length(), 5.0);
|
||||
}
|
||||
#[test]
|
||||
fn degenerate() {
|
||||
let segment = LineSegment2::new(&Vector2::new(1.0, 2.0), &Vector2::new(1.0, 2.0));
|
||||
assert!(segment.is_degenerate());
|
||||
assert_eq!(segment.length_squared(), 0.0);
|
||||
assert_eq!(segment.length(), 0.0);
|
||||
}
|
||||
#[test]
|
||||
fn collinear() {
|
||||
let segment = LineSegment2::new(&Vector2::new(0.0, 0.0), &Vector2::new(1.0, 2.0));
|
||||
assert_eq!(segment.collinearity(&Vector2::new(2.0, 4.0)), 0.0);
|
||||
assert_eq!(segment.collinearity(&Vector2::new(0.0, 0.0)), 0.0);
|
||||
assert_eq!(segment.collinearity(&Vector2::new(1.0, 2.0)), 0.0);
|
||||
assert!(
|
||||
segment.collinearity(&Vector2::new(1.0, 5.0)) < 0.0,
|
||||
"{} >= 0.0",
|
||||
segment.collinearity(&Vector2::new(1.0, 5.0))
|
||||
);
|
||||
assert!(
|
||||
segment.collinearity(&Vector2::new(1.0, 3.0)) < 0.0,
|
||||
"{} >= 0.0",
|
||||
segment.collinearity(&Vector2::new(1.0, 3.0))
|
||||
);
|
||||
assert!(
|
||||
segment.collinearity(&Vector2::new(1.0, 1.0)) > 0.0,
|
||||
"{} <= 0.0",
|
||||
segment.collinearity(&Vector2::new(1.0, 1.0))
|
||||
);
|
||||
assert!(
|
||||
segment.collinearity(&Vector2::new(-1.0, -5.0)) > 0.0,
|
||||
"{} <= 0.0",
|
||||
segment.collinearity(&Vector2::new(-1.0, -5.0))
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn intersects() {
|
||||
let a = LineSegment::new(&Vector2::new(1.0, 2.0), &Vector2::new(3.0, 1.0));
|
||||
let b = LineSegment::new(&Vector2::new(2.0, 0.0), &Vector2::new(2.5, 3.0));
|
||||
let c = LineSegment::new(&Vector2::new(1.0, 2.0), &Vector2::new(-3.0, 1.0));
|
||||
assert!(a.intersects(&b));
|
||||
assert!(a.intersects(&c));
|
||||
assert!(b.intersects(&a));
|
||||
assert!(c.intersects(&a));
|
||||
assert!(a.swapped().intersects(&b));
|
||||
assert!(a.swapped().intersects(&c));
|
||||
}
|
||||
#[test]
|
||||
fn not_intersects() {
|
||||
let a = LineSegment::new(&Vector2::new(1.0, 2.0), &Vector2::new(3.0, 1.0));
|
||||
let b = LineSegment::new(&Vector2::new(0.0, 0.0), &Vector2::new(-1.0, 6.0));
|
||||
let c = LineSegment::new(&Vector2::new(2.0, 0.0), &Vector2::new(2.0, -1.0));
|
||||
assert!(!a.intersects(&b));
|
||||
assert!(!b.intersects(&c));
|
||||
assert!(!c.intersects(&a));
|
||||
assert!(!b.intersects(&a));
|
||||
assert!(!c.intersects(&b));
|
||||
assert!(!a.intersects(&c));
|
||||
assert!(!a.swapped().intersects(&b));
|
||||
assert!(!b.swapped().intersects(&c));
|
||||
assert!(!c.swapped().intersects(&a));
|
||||
}
|
||||
}
|
||||
|
||||
+235
-235
@@ -1,235 +1,235 @@
|
||||
// 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 crate::core::type_traits::prelude::*;
|
||||
use crate::font::FontHeight;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[deprecated]
|
||||
pub type RunBuilder = Run;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Default, Reflect)]
|
||||
pub struct RunSet(Vec<Run>);
|
||||
|
||||
impl From<&[Run]> for RunSet {
|
||||
fn from(value: &[Run]) -> Self {
|
||||
Self(value.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Run>> for RunSet {
|
||||
fn from(value: Vec<Run>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for RunSet {
|
||||
fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
|
||||
self.0.visit(name, visitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RunSet {
|
||||
type Target = Vec<Run>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for RunSet {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl RunSet {
|
||||
pub fn font_at(&self, index: usize) -> Option<FontResource> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.font().is_some() {
|
||||
return Some(run.font().unwrap().clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn font_size_at(&self, index: usize) -> Option<f32> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.font_size().is_some() {
|
||||
return Some(run.font_size().unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn brush_at(&self, index: usize) -> Option<Brush> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.brush().is_some() {
|
||||
return Some(run.brush().unwrap().clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn shadow_at(&self, index: usize) -> Option<bool> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.shadow().is_some() {
|
||||
return Some(run.shadow().unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn shadow_brush_at(&self, index: usize) -> Option<Brush> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.shadow_brush().is_some() {
|
||||
return Some(run.shadow_brush().unwrap().clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn shadow_dilation_at(&self, index: usize) -> Option<f32> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.shadow_dilation().is_some() {
|
||||
return Some(run.shadow_dilation().unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn shadow_offset_at(&self, index: usize) -> Option<Vector2<f32>> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.shadow_offset().is_some() {
|
||||
return Some(run.shadow_offset().unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The style of a partion of text within a range.
|
||||
#[derive(Clone, PartialEq, Debug, Default, Visit, Reflect, TypeUuidProvider)]
|
||||
#[type_uuid(id = "f0e5cc5d-0b82-4d6f-a505-12f890ffe7ea")]
|
||||
pub struct Run {
|
||||
/// The range of characters that this run applies to within the text.
|
||||
pub range: Range<u32>,
|
||||
font: Option<FontResource>,
|
||||
brush: Option<Brush>,
|
||||
font_size: Option<f32>,
|
||||
shadow: Option<bool>,
|
||||
shadow_brush: Option<Brush>,
|
||||
shadow_dilation: Option<f32>,
|
||||
shadow_offset: Option<Vector2<f32>>,
|
||||
}
|
||||
|
||||
impl Run {
|
||||
pub fn new(range: Range<u32>) -> Self {
|
||||
Self {
|
||||
range,
|
||||
font: None,
|
||||
brush: None,
|
||||
font_size: None,
|
||||
shadow: None,
|
||||
shadow_brush: None,
|
||||
shadow_dilation: None,
|
||||
shadow_offset: None,
|
||||
}
|
||||
}
|
||||
/// The font of the characters in this run, or None if the font is unmodified.
|
||||
pub fn font(&self) -> Option<&FontResource> {
|
||||
self.font.as_ref()
|
||||
}
|
||||
/// The brush of the characters in this run, or None if the brush is unmodified.
|
||||
pub fn brush(&self) -> Option<&Brush> {
|
||||
self.brush.as_ref()
|
||||
}
|
||||
/// The size of the characters in this run, or None if the size is unmodified.
|
||||
pub fn font_size(&self) -> Option<f32> {
|
||||
self.font_size
|
||||
}
|
||||
/// True if the characters in this run should have a shadow. None if this run does not change
|
||||
/// whether the characters are shadowed.
|
||||
pub fn shadow(&self) -> Option<bool> {
|
||||
self.shadow
|
||||
}
|
||||
/// The brush for the shadow of the characters in this run, or None if the brush is unmodified.
|
||||
pub fn shadow_brush(&self) -> Option<&Brush> {
|
||||
self.shadow_brush.as_ref()
|
||||
}
|
||||
/// The dilation for the shadow in this run, or None if the dilation is unmodified.
|
||||
pub fn shadow_dilation(&self) -> Option<f32> {
|
||||
self.shadow_dilation
|
||||
}
|
||||
/// The offset for the shadow in this run, or None if the offset is unmodified.
|
||||
pub fn shadow_offset(&self) -> Option<Vector2<f32>> {
|
||||
self.shadow_offset
|
||||
}
|
||||
#[deprecated]
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the font of the text within the range.
|
||||
pub fn with_font(mut self, font: FontResource) -> Self {
|
||||
self.font = Some(font);
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the brush of the text within the range.
|
||||
pub fn with_brush(mut self, brush: Brush) -> Self {
|
||||
self.brush = Some(brush);
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the size of the text within the range.
|
||||
pub fn with_size(mut self, size: f32) -> Self {
|
||||
self.font_size = Some(size);
|
||||
self
|
||||
}
|
||||
/// Set this run to enable or disable the shadow of the text within the range.
|
||||
pub fn with_shadow(mut self, shadow: bool) -> Self {
|
||||
self.shadow = Some(shadow);
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the brush of the shadow within the range.
|
||||
pub fn with_shadow_brush(mut self, brush: Brush) -> Self {
|
||||
self.shadow_brush = Some(brush);
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the dilation of the shadow within the range.
|
||||
pub fn with_shadow_dilation(mut self, size: f32) -> Self {
|
||||
self.shadow_dilation = Some(size);
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the offset of the shadow within the range.
|
||||
pub fn with_shadow_offset(mut self, offset: Vector2<f32>) -> Self {
|
||||
self.shadow_offset = Some(offset);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub enum DrawValueLayer {
|
||||
Main,
|
||||
Shadow,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct GlyphDrawValues {
|
||||
pub atlas_page_index: usize,
|
||||
pub font: FontResource,
|
||||
pub brush: Brush,
|
||||
/// Font size scaled by super sampling scaling to pick correct atlas page.
|
||||
pub height: FontHeight,
|
||||
}
|
||||
// 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 crate::core::type_traits::prelude::*;
|
||||
use crate::font::FontHeight;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[deprecated]
|
||||
pub type RunBuilder = Run;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Default, Reflect)]
|
||||
pub struct RunSet(Vec<Run>);
|
||||
|
||||
impl From<&[Run]> for RunSet {
|
||||
fn from(value: &[Run]) -> Self {
|
||||
Self(value.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Run>> for RunSet {
|
||||
fn from(value: Vec<Run>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for RunSet {
|
||||
fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
|
||||
self.0.visit(name, visitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RunSet {
|
||||
type Target = Vec<Run>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for RunSet {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl RunSet {
|
||||
pub fn font_at(&self, index: usize) -> Option<FontResource> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.font().is_some() {
|
||||
return Some(run.font().unwrap().clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn font_size_at(&self, index: usize) -> Option<f32> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.font_size().is_some() {
|
||||
return Some(run.font_size().unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn brush_at(&self, index: usize) -> Option<Brush> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.brush().is_some() {
|
||||
return Some(run.brush().unwrap().clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn shadow_at(&self, index: usize) -> Option<bool> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.shadow().is_some() {
|
||||
return Some(run.shadow().unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn shadow_brush_at(&self, index: usize) -> Option<Brush> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.shadow_brush().is_some() {
|
||||
return Some(run.shadow_brush().unwrap().clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn shadow_dilation_at(&self, index: usize) -> Option<f32> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.shadow_dilation().is_some() {
|
||||
return Some(run.shadow_dilation().unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn shadow_offset_at(&self, index: usize) -> Option<Vector2<f32>> {
|
||||
for run in self.0.iter().rev() {
|
||||
if run.range.contains(&(index as u32)) && run.shadow_offset().is_some() {
|
||||
return Some(run.shadow_offset().unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The style of a partion of text within a range.
|
||||
#[derive(Clone, PartialEq, Debug, Default, Visit, Reflect, TypeUuidProvider)]
|
||||
#[type_uuid(id = "f0e5cc5d-0b82-4d6f-a505-12f890ffe7ea")]
|
||||
pub struct Run {
|
||||
/// The range of characters that this run applies to within the text.
|
||||
pub range: Range<u32>,
|
||||
font: Option<FontResource>,
|
||||
brush: Option<Brush>,
|
||||
font_size: Option<f32>,
|
||||
shadow: Option<bool>,
|
||||
shadow_brush: Option<Brush>,
|
||||
shadow_dilation: Option<f32>,
|
||||
shadow_offset: Option<Vector2<f32>>,
|
||||
}
|
||||
|
||||
impl Run {
|
||||
pub fn new(range: Range<u32>) -> Self {
|
||||
Self {
|
||||
range,
|
||||
font: None,
|
||||
brush: None,
|
||||
font_size: None,
|
||||
shadow: None,
|
||||
shadow_brush: None,
|
||||
shadow_dilation: None,
|
||||
shadow_offset: None,
|
||||
}
|
||||
}
|
||||
/// The font of the characters in this run, or None if the font is unmodified.
|
||||
pub fn font(&self) -> Option<&FontResource> {
|
||||
self.font.as_ref()
|
||||
}
|
||||
/// The brush of the characters in this run, or None if the brush is unmodified.
|
||||
pub fn brush(&self) -> Option<&Brush> {
|
||||
self.brush.as_ref()
|
||||
}
|
||||
/// The size of the characters in this run, or None if the size is unmodified.
|
||||
pub fn font_size(&self) -> Option<f32> {
|
||||
self.font_size
|
||||
}
|
||||
/// True if the characters in this run should have a shadow. None if this run does not change
|
||||
/// whether the characters are shadowed.
|
||||
pub fn shadow(&self) -> Option<bool> {
|
||||
self.shadow
|
||||
}
|
||||
/// The brush for the shadow of the characters in this run, or None if the brush is unmodified.
|
||||
pub fn shadow_brush(&self) -> Option<&Brush> {
|
||||
self.shadow_brush.as_ref()
|
||||
}
|
||||
/// The dilation for the shadow in this run, or None if the dilation is unmodified.
|
||||
pub fn shadow_dilation(&self) -> Option<f32> {
|
||||
self.shadow_dilation
|
||||
}
|
||||
/// The offset for the shadow in this run, or None if the offset is unmodified.
|
||||
pub fn shadow_offset(&self) -> Option<Vector2<f32>> {
|
||||
self.shadow_offset
|
||||
}
|
||||
#[deprecated]
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the font of the text within the range.
|
||||
pub fn with_font(mut self, font: FontResource) -> Self {
|
||||
self.font = Some(font);
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the brush of the text within the range.
|
||||
pub fn with_brush(mut self, brush: Brush) -> Self {
|
||||
self.brush = Some(brush);
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the size of the text within the range.
|
||||
pub fn with_size(mut self, size: f32) -> Self {
|
||||
self.font_size = Some(size);
|
||||
self
|
||||
}
|
||||
/// Set this run to enable or disable the shadow of the text within the range.
|
||||
pub fn with_shadow(mut self, shadow: bool) -> Self {
|
||||
self.shadow = Some(shadow);
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the brush of the shadow within the range.
|
||||
pub fn with_shadow_brush(mut self, brush: Brush) -> Self {
|
||||
self.shadow_brush = Some(brush);
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the dilation of the shadow within the range.
|
||||
pub fn with_shadow_dilation(mut self, size: f32) -> Self {
|
||||
self.shadow_dilation = Some(size);
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the offset of the shadow within the range.
|
||||
pub fn with_shadow_offset(mut self, offset: Vector2<f32>) -> Self {
|
||||
self.shadow_offset = Some(offset);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub enum DrawValueLayer {
|
||||
Main,
|
||||
Shadow,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct GlyphDrawValues {
|
||||
pub atlas_page_index: usize,
|
||||
pub font: FontResource,
|
||||
pub brush: Brush,
|
||||
/// Font size scaled by super sampling scaling to pick correct atlas page.
|
||||
pub height: FontHeight,
|
||||
}
|
||||
|
||||
@@ -1,261 +1,261 @@
|
||||
// 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::ops::Range;
|
||||
|
||||
pub trait TextWrapper {
|
||||
fn push(&mut self, c: char, advance: f32);
|
||||
fn finish(&mut self);
|
||||
}
|
||||
|
||||
pub trait LineSink {
|
||||
fn push_line(&mut self, range: Range<usize>, width: f32);
|
||||
fn max_width(&self) -> f32;
|
||||
}
|
||||
|
||||
fn is_newline(c: char) -> bool {
|
||||
c == '\n' || c == '\r'
|
||||
}
|
||||
|
||||
pub struct NoWrap<S> {
|
||||
sink: S,
|
||||
start: usize,
|
||||
position: usize,
|
||||
width: f32,
|
||||
}
|
||||
|
||||
impl<S> NoWrap<S> {
|
||||
pub fn new(sink: S) -> Self {
|
||||
NoWrap {
|
||||
sink,
|
||||
start: 0,
|
||||
position: 0,
|
||||
width: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: LineSink> TextWrapper for NoWrap<S> {
|
||||
fn push(&mut self, c: char, advance: f32) {
|
||||
if is_newline(c) {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
self.start = self.position + 1; // Next like starts after the newline, so skip ahead by one.
|
||||
self.position += 1;
|
||||
self.width = 0.0;
|
||||
} else {
|
||||
self.position += 1;
|
||||
self.width += advance;
|
||||
}
|
||||
}
|
||||
fn finish(&mut self) {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LetterWrap<S> {
|
||||
sink: S,
|
||||
start: usize,
|
||||
position: usize,
|
||||
width: f32,
|
||||
}
|
||||
|
||||
impl<S> LetterWrap<S> {
|
||||
pub fn new(sink: S) -> Self {
|
||||
LetterWrap {
|
||||
sink,
|
||||
start: 0,
|
||||
position: 0,
|
||||
width: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: LineSink> TextWrapper for LetterWrap<S> {
|
||||
fn push(&mut self, c: char, advance: f32) {
|
||||
if self.position != self.start && self.width + advance > self.sink.max_width() {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
self.start = self.position;
|
||||
self.width = 0.0;
|
||||
}
|
||||
self.position += 1;
|
||||
self.width += advance;
|
||||
if is_newline(c) {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
self.start = self.position; // Next line starts after the newline
|
||||
self.width = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&mut self) {
|
||||
if self.start != self.position {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WordWrap<S> {
|
||||
sink: S,
|
||||
// Start of the current line
|
||||
start: usize,
|
||||
// Position of the next character
|
||||
position: usize,
|
||||
// Width of the current line
|
||||
width: f32,
|
||||
// Start of the current word
|
||||
word_start: usize,
|
||||
// Width of the current word
|
||||
word_width: f32,
|
||||
}
|
||||
|
||||
impl<S> WordWrap<S> {
|
||||
pub fn new(sink: S) -> Self {
|
||||
WordWrap {
|
||||
sink,
|
||||
start: 0,
|
||||
position: 0,
|
||||
width: 0.0,
|
||||
word_start: 0,
|
||||
word_width: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: LineSink> TextWrapper for WordWrap<S> {
|
||||
fn push(&mut self, c: char, advance: f32) {
|
||||
if self.position != self.start && self.width + advance > self.sink.max_width() {
|
||||
if self.start < self.word_start {
|
||||
self.sink
|
||||
.push_line(self.start..self.word_start, self.width - self.word_width);
|
||||
// The current word becomes the current line.
|
||||
self.start = self.word_start;
|
||||
self.width = self.word_width;
|
||||
} else {
|
||||
// The current word started at or before the start of the current line,
|
||||
// so ignore the word and just wrap at the current position.
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
self.start = self.position;
|
||||
self.width = 0.0;
|
||||
}
|
||||
}
|
||||
self.position += 1;
|
||||
self.width += advance;
|
||||
if is_newline(c) {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
self.start = self.position;
|
||||
self.width = 0.0;
|
||||
// newline is not part of a word, so move word_start ahead.
|
||||
self.word_start = self.position;
|
||||
self.word_width = 0.0;
|
||||
} else if c.is_whitespace() {
|
||||
// We are not in a word, so move word_start ahead.
|
||||
self.word_start = self.position;
|
||||
self.word_width = 0.0;
|
||||
} else {
|
||||
// We are in a word, so leave word_start alone and increase word_width.
|
||||
self.word_width += advance;
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&mut self) {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct Sink(f32, Vec<(Range<usize>, f32)>);
|
||||
|
||||
impl Sink {
|
||||
fn new(width: f32) -> Self {
|
||||
Self(width, Vec::default())
|
||||
}
|
||||
fn result(&self) -> &[(Range<usize>, f32)] {
|
||||
self.1.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl LineSink for Sink {
|
||||
fn push_line(&mut self, range: Range<usize>, width: f32) {
|
||||
self.1.push((range, width));
|
||||
}
|
||||
|
||||
fn max_width(&self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap<W: TextWrapper>(wrap: &mut W, text: &str) {
|
||||
for c in text.chars() {
|
||||
wrap.push(c, 1.0);
|
||||
}
|
||||
wrap.finish();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_line_no_wrap() {
|
||||
let mut w = NoWrap::new(Sink::new(10.0));
|
||||
wrap(&mut w, "One line");
|
||||
assert_eq!(w.sink.result(), &[(0..8, 8.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn two_line_no_wrap() {
|
||||
let mut w = NoWrap::new(Sink::new(10.0));
|
||||
wrap(&mut w, "Two\nline");
|
||||
assert_eq!(w.sink.result(), &[(0..3, 3.0), (4..8, 4.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn one_line_letter() {
|
||||
let mut w = LetterWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "One line");
|
||||
assert_eq!(w.sink.result(), &[(0..8, 8.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn two_line_letter() {
|
||||
let mut w = LetterWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "Two line!");
|
||||
assert_eq!(w.sink.result(), &[(0..8, 8.0), (8..9, 1.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn new_line_letter() {
|
||||
let mut w = LetterWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "Two\nline");
|
||||
assert_eq!(w.sink.result(), &[(0..4, 4.0), (4..8, 4.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn one_line_word() {
|
||||
let mut w = WordWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "One line");
|
||||
assert_eq!(w.sink.result(), &[(0..8, 8.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn two_line_word() {
|
||||
let mut w = WordWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "Two line!");
|
||||
assert_eq!(w.sink.result(), &[(0..4, 4.0), (4..9, 5.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn new_line_word() {
|
||||
let mut w = WordWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "Two\nline");
|
||||
assert_eq!(w.sink.result(), &[(0..4, 4.0), (4..8, 4.0)]);
|
||||
}
|
||||
}
|
||||
// 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::ops::Range;
|
||||
|
||||
pub trait TextWrapper {
|
||||
fn push(&mut self, c: char, advance: f32);
|
||||
fn finish(&mut self);
|
||||
}
|
||||
|
||||
pub trait LineSink {
|
||||
fn push_line(&mut self, range: Range<usize>, width: f32);
|
||||
fn max_width(&self) -> f32;
|
||||
}
|
||||
|
||||
fn is_newline(c: char) -> bool {
|
||||
c == '\n' || c == '\r'
|
||||
}
|
||||
|
||||
pub struct NoWrap<S> {
|
||||
sink: S,
|
||||
start: usize,
|
||||
position: usize,
|
||||
width: f32,
|
||||
}
|
||||
|
||||
impl<S> NoWrap<S> {
|
||||
pub fn new(sink: S) -> Self {
|
||||
NoWrap {
|
||||
sink,
|
||||
start: 0,
|
||||
position: 0,
|
||||
width: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: LineSink> TextWrapper for NoWrap<S> {
|
||||
fn push(&mut self, c: char, advance: f32) {
|
||||
if is_newline(c) {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
self.start = self.position + 1; // Next like starts after the newline, so skip ahead by one.
|
||||
self.position += 1;
|
||||
self.width = 0.0;
|
||||
} else {
|
||||
self.position += 1;
|
||||
self.width += advance;
|
||||
}
|
||||
}
|
||||
fn finish(&mut self) {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LetterWrap<S> {
|
||||
sink: S,
|
||||
start: usize,
|
||||
position: usize,
|
||||
width: f32,
|
||||
}
|
||||
|
||||
impl<S> LetterWrap<S> {
|
||||
pub fn new(sink: S) -> Self {
|
||||
LetterWrap {
|
||||
sink,
|
||||
start: 0,
|
||||
position: 0,
|
||||
width: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: LineSink> TextWrapper for LetterWrap<S> {
|
||||
fn push(&mut self, c: char, advance: f32) {
|
||||
if self.position != self.start && self.width + advance > self.sink.max_width() {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
self.start = self.position;
|
||||
self.width = 0.0;
|
||||
}
|
||||
self.position += 1;
|
||||
self.width += advance;
|
||||
if is_newline(c) {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
self.start = self.position; // Next line starts after the newline
|
||||
self.width = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&mut self) {
|
||||
if self.start != self.position {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WordWrap<S> {
|
||||
sink: S,
|
||||
// Start of the current line
|
||||
start: usize,
|
||||
// Position of the next character
|
||||
position: usize,
|
||||
// Width of the current line
|
||||
width: f32,
|
||||
// Start of the current word
|
||||
word_start: usize,
|
||||
// Width of the current word
|
||||
word_width: f32,
|
||||
}
|
||||
|
||||
impl<S> WordWrap<S> {
|
||||
pub fn new(sink: S) -> Self {
|
||||
WordWrap {
|
||||
sink,
|
||||
start: 0,
|
||||
position: 0,
|
||||
width: 0.0,
|
||||
word_start: 0,
|
||||
word_width: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: LineSink> TextWrapper for WordWrap<S> {
|
||||
fn push(&mut self, c: char, advance: f32) {
|
||||
if self.position != self.start && self.width + advance > self.sink.max_width() {
|
||||
if self.start < self.word_start {
|
||||
self.sink
|
||||
.push_line(self.start..self.word_start, self.width - self.word_width);
|
||||
// The current word becomes the current line.
|
||||
self.start = self.word_start;
|
||||
self.width = self.word_width;
|
||||
} else {
|
||||
// The current word started at or before the start of the current line,
|
||||
// so ignore the word and just wrap at the current position.
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
self.start = self.position;
|
||||
self.width = 0.0;
|
||||
}
|
||||
}
|
||||
self.position += 1;
|
||||
self.width += advance;
|
||||
if is_newline(c) {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
self.start = self.position;
|
||||
self.width = 0.0;
|
||||
// newline is not part of a word, so move word_start ahead.
|
||||
self.word_start = self.position;
|
||||
self.word_width = 0.0;
|
||||
} else if c.is_whitespace() {
|
||||
// We are not in a word, so move word_start ahead.
|
||||
self.word_start = self.position;
|
||||
self.word_width = 0.0;
|
||||
} else {
|
||||
// We are in a word, so leave word_start alone and increase word_width.
|
||||
self.word_width += advance;
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&mut self) {
|
||||
self.sink.push_line(self.start..self.position, self.width);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct Sink(f32, Vec<(Range<usize>, f32)>);
|
||||
|
||||
impl Sink {
|
||||
fn new(width: f32) -> Self {
|
||||
Self(width, Vec::default())
|
||||
}
|
||||
fn result(&self) -> &[(Range<usize>, f32)] {
|
||||
self.1.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl LineSink for Sink {
|
||||
fn push_line(&mut self, range: Range<usize>, width: f32) {
|
||||
self.1.push((range, width));
|
||||
}
|
||||
|
||||
fn max_width(&self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap<W: TextWrapper>(wrap: &mut W, text: &str) {
|
||||
for c in text.chars() {
|
||||
wrap.push(c, 1.0);
|
||||
}
|
||||
wrap.finish();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_line_no_wrap() {
|
||||
let mut w = NoWrap::new(Sink::new(10.0));
|
||||
wrap(&mut w, "One line");
|
||||
assert_eq!(w.sink.result(), &[(0..8, 8.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn two_line_no_wrap() {
|
||||
let mut w = NoWrap::new(Sink::new(10.0));
|
||||
wrap(&mut w, "Two\nline");
|
||||
assert_eq!(w.sink.result(), &[(0..3, 3.0), (4..8, 4.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn one_line_letter() {
|
||||
let mut w = LetterWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "One line");
|
||||
assert_eq!(w.sink.result(), &[(0..8, 8.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn two_line_letter() {
|
||||
let mut w = LetterWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "Two line!");
|
||||
assert_eq!(w.sink.result(), &[(0..8, 8.0), (8..9, 1.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn new_line_letter() {
|
||||
let mut w = LetterWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "Two\nline");
|
||||
assert_eq!(w.sink.result(), &[(0..4, 4.0), (4..8, 4.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn one_line_word() {
|
||||
let mut w = WordWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "One line");
|
||||
assert_eq!(w.sink.result(), &[(0..8, 8.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn two_line_word() {
|
||||
let mut w = WordWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "Two line!");
|
||||
assert_eq!(w.sink.result(), &[(0..4, 4.0), (4..9, 5.0)]);
|
||||
}
|
||||
#[test]
|
||||
fn new_line_word() {
|
||||
let mut w = WordWrap::new(Sink::new(8.0));
|
||||
wrap(&mut w, "Two\nline");
|
||||
assert_eq!(w.sink.result(), &[(0..4, 4.0), (4..8, 4.0)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +1,93 @@
|
||||
// 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.
|
||||
|
||||
//! Property editor for [ImmutableString] properties based upon a [TextBox](crate::text_box::TextBox) widget.
|
||||
use fyrox_core::sstorage::ImmutableString;
|
||||
|
||||
use crate::{
|
||||
core::algebra::Vector2,
|
||||
formatted_text::WrapMode,
|
||||
inspector::{
|
||||
editors::{
|
||||
PropertyEditorBuildContext, PropertyEditorDefinition, PropertyEditorInstance,
|
||||
PropertyEditorMessageContext, PropertyEditorTranslationContext,
|
||||
},
|
||||
FieldKind, InspectorError, PropertyChanged,
|
||||
},
|
||||
message::{MessageDirection, UiMessage},
|
||||
text::TextMessage,
|
||||
text_box::{TextBoxBuilder, TextCommitMode},
|
||||
widget::WidgetBuilder,
|
||||
Thickness, VerticalAlignment,
|
||||
};
|
||||
use std::any::TypeId;
|
||||
|
||||
/// Property editor for [ImmutableString] properties based upon a [TextBox](crate::text_box::TextBox) widget.
|
||||
#[derive(Debug)]
|
||||
pub struct ImmutableStringPropertyEditorDefinition;
|
||||
|
||||
impl PropertyEditorDefinition for ImmutableStringPropertyEditorDefinition {
|
||||
fn value_type_id(&self) -> TypeId {
|
||||
TypeId::of::<ImmutableString>()
|
||||
}
|
||||
|
||||
fn create_instance(
|
||||
&self,
|
||||
ctx: PropertyEditorBuildContext,
|
||||
) -> Result<PropertyEditorInstance, InspectorError> {
|
||||
let value = ctx.property_info.cast_value::<ImmutableString>()?;
|
||||
Ok(PropertyEditorInstance::Simple {
|
||||
editor: TextBoxBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_min_size(Vector2::new(0.0, 17.0))
|
||||
.with_margin(Thickness::uniform(1.0)),
|
||||
)
|
||||
.with_wrap(WrapMode::Word)
|
||||
.with_text_commit_mode(TextCommitMode::Changed)
|
||||
.with_text(value)
|
||||
.with_vertical_text_alignment(VerticalAlignment::Center)
|
||||
.build(ctx.build_context),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_message(
|
||||
&self,
|
||||
ctx: PropertyEditorMessageContext,
|
||||
) -> Result<Option<UiMessage>, InspectorError> {
|
||||
let value = ctx.property_info.cast_value::<ImmutableString>()?;
|
||||
Ok(Some(TextMessage::text(
|
||||
ctx.instance,
|
||||
MessageDirection::ToWidget,
|
||||
value.to_mutable(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option<PropertyChanged> {
|
||||
if ctx.message.direction() == MessageDirection::FromWidget {
|
||||
if let Some(TextMessage::Text(value)) = ctx.message.data::<TextMessage>() {
|
||||
return Some(PropertyChanged {
|
||||
name: ctx.name.to_string(),
|
||||
value: FieldKind::object(ImmutableString::new(value)),
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
|
||||
//! Property editor for [ImmutableString] properties based upon a [TextBox](crate::text_box::TextBox) widget.
|
||||
use fyrox_core::sstorage::ImmutableString;
|
||||
|
||||
use crate::{
|
||||
core::algebra::Vector2,
|
||||
formatted_text::WrapMode,
|
||||
inspector::{
|
||||
editors::{
|
||||
PropertyEditorBuildContext, PropertyEditorDefinition, PropertyEditorInstance,
|
||||
PropertyEditorMessageContext, PropertyEditorTranslationContext,
|
||||
},
|
||||
FieldKind, InspectorError, PropertyChanged,
|
||||
},
|
||||
message::{MessageDirection, UiMessage},
|
||||
text::TextMessage,
|
||||
text_box::{TextBoxBuilder, TextCommitMode},
|
||||
widget::WidgetBuilder,
|
||||
Thickness, VerticalAlignment,
|
||||
};
|
||||
use std::any::TypeId;
|
||||
|
||||
/// Property editor for [ImmutableString] properties based upon a [TextBox](crate::text_box::TextBox) widget.
|
||||
#[derive(Debug)]
|
||||
pub struct ImmutableStringPropertyEditorDefinition;
|
||||
|
||||
impl PropertyEditorDefinition for ImmutableStringPropertyEditorDefinition {
|
||||
fn value_type_id(&self) -> TypeId {
|
||||
TypeId::of::<ImmutableString>()
|
||||
}
|
||||
|
||||
fn create_instance(
|
||||
&self,
|
||||
ctx: PropertyEditorBuildContext,
|
||||
) -> Result<PropertyEditorInstance, InspectorError> {
|
||||
let value = ctx.property_info.cast_value::<ImmutableString>()?;
|
||||
Ok(PropertyEditorInstance::Simple {
|
||||
editor: TextBoxBuilder::new(
|
||||
WidgetBuilder::new()
|
||||
.with_min_size(Vector2::new(0.0, 17.0))
|
||||
.with_margin(Thickness::uniform(1.0)),
|
||||
)
|
||||
.with_wrap(WrapMode::Word)
|
||||
.with_text_commit_mode(TextCommitMode::Changed)
|
||||
.with_text(value)
|
||||
.with_vertical_text_alignment(VerticalAlignment::Center)
|
||||
.build(ctx.build_context),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_message(
|
||||
&self,
|
||||
ctx: PropertyEditorMessageContext,
|
||||
) -> Result<Option<UiMessage>, InspectorError> {
|
||||
let value = ctx.property_info.cast_value::<ImmutableString>()?;
|
||||
Ok(Some(TextMessage::text(
|
||||
ctx.instance,
|
||||
MessageDirection::ToWidget,
|
||||
value.to_mutable(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option<PropertyChanged> {
|
||||
if ctx.message.direction() == MessageDirection::FromWidget {
|
||||
if let Some(TextMessage::Text(value)) = ctx.message.data::<TextMessage>() {
|
||||
return Some(PropertyChanged {
|
||||
name: ctx.name.to_string(),
|
||||
value: FieldKind::object(ImmutableString::new(value)),
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user