mirror of
https://github.com/enso-org/enso.git
synced 2024-11-26 17:06:48 +03:00
Cached Shape Parameter (#5685)
Fixes #5023
This PR adds the ability to add a parameter to shapes defined, with `shape!` macro being a reference to a cached shape.
The API and results may be read [in the example scene](33b6f5937e/lib/rust/ensogl/example/cached-shape/src/lib.rs
)
It also contains many other changes, required to have it working:
* We render cached shapes to texture in a different mode than normal shapes: the alpha channel is replaced with information about signed distance. That allows us using cached shapes as normal shapes, i.e. translate them, add to other shapes etc.
* We initialize and arrange shapes as a part of Word initialization, not in pass.
* We keep and blend colors in RGBA instead of LCHA - this is preparation for replacing colors in the next task, and also speeds up our shaders a bit.
The code was refactored in the process: the cached-shape related things were moved to a single module.
This commit is contained in:
parent
3a09ee88f6
commit
625172a6d2
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2622,6 +2622,7 @@ dependencies = [
|
||||
"nalgebra",
|
||||
"num-traits",
|
||||
"num_enum",
|
||||
"ordered-float",
|
||||
"rustc-hash",
|
||||
"semver 1.0.16",
|
||||
"serde",
|
||||
|
@ -71,7 +71,8 @@ enum RelativePosition {
|
||||
// === Background ===
|
||||
// ==================
|
||||
|
||||
mod background {
|
||||
/// A background shape.
|
||||
pub mod background {
|
||||
use super::*;
|
||||
|
||||
ensogl::shape! {
|
||||
|
@ -148,6 +148,8 @@ impl Model {
|
||||
|
||||
ensogl::shapes_order_dependencies! {
|
||||
app.display.default_scene => {
|
||||
ide_view_graph_editor::component::breadcrumbs::background -> close::shape;
|
||||
ide_view_graph_editor::component::breadcrumbs::background -> fullscreen::shape;
|
||||
shape -> close::shape;
|
||||
shape -> fullscreen::shape;
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ impl crate::Entry for Entry {
|
||||
});
|
||||
layout <- all(contour, text_size, text_offset);
|
||||
eval layout ((&(c, ts, to)) data.update_layout(c, ts, to));
|
||||
eval bg_color ((color) data.background.color.set(color.into()));
|
||||
eval bg_color ((color) data.background.color.set(color::Rgba::from(color).into()));
|
||||
disabled <- input.set_model.map(|m| *m.disabled);
|
||||
data.label.set_property_default <+ all_with3(
|
||||
&text_color,
|
||||
|
@ -38,6 +38,7 @@ js-sys = { workspace = true }
|
||||
nalgebra = { workspace = true }
|
||||
num_enum = { version = "0.5.1" }
|
||||
num-traits = { version = "0.2" }
|
||||
ordered-float = { workspace = true }
|
||||
rustc-hash = { version = "1.0.1" }
|
||||
semver = { version = "1.0.9" }
|
||||
serde = { version = "1" }
|
||||
|
@ -27,7 +27,7 @@ use nalgebra::clamp;
|
||||
/// ┄+┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄▶
|
||||
/// ┆
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq)]
|
||||
pub struct BoundingBox {
|
||||
top: f32,
|
||||
bottom: f32,
|
||||
|
@ -3,33 +3,16 @@
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::data::bounding_box::BoundingBox;
|
||||
use crate::display;
|
||||
use crate::display::render::pass;
|
||||
use crate::display::render::pass::Instance;
|
||||
use crate::display::scene::Layer;
|
||||
use crate::display::scene::UpdateStatus;
|
||||
use crate::display::shape::glsl::codes::DisplayModes;
|
||||
use crate::display::world::with_context;
|
||||
use crate::display::world::CachedShapeDefinition;
|
||||
use crate::display::Scene;
|
||||
use crate::gui::component::AnyShapeView;
|
||||
|
||||
use itertools::iproduct;
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/// A parameter of [`arrange_shapes_on_texture`] algorithm: the initial assumed size for the texture
|
||||
/// with cached shapes. If it turn out to be too small, we extend this size and try again.
|
||||
const INITIAL_TEXTURE_SIZE: i32 = 512;
|
||||
|
||||
/// A parameter of [`arrange_shapes_on_texture`] algorithm: the factor how much the texture size is
|
||||
/// increased when the current size is insufficient.
|
||||
const TEXTURE_SIZE_MULTIPLIER: i32 = 2;
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
@ -38,8 +21,9 @@ const TEXTURE_SIZE_MULTIPLIER: i32 = 2;
|
||||
|
||||
/// Definition of pass rendering cached shapes to texture.
|
||||
///
|
||||
/// On each run it checks what not-yet-rendered shapes has compiled shaders and render them to
|
||||
/// the texture, which is stored in `pass_cached_shapes` uniform.
|
||||
/// On each run it checks what not-yet-rendered shapes has compiled shaders and render their color
|
||||
/// and SDF information to the texture, which is stored in `pass_cached_shapes` uniform. See also
|
||||
/// the [full documentation of cached shapes](display::shape::primitive::system::cached).
|
||||
///
|
||||
/// # Implementation
|
||||
///
|
||||
@ -55,8 +39,7 @@ pub struct CacheShapesPass {
|
||||
framebuffer: Option<pass::Framebuffer>,
|
||||
#[derivative(Debug = "ignore")]
|
||||
shapes_to_render: Vec<Rc<dyn AnyShapeView>>,
|
||||
texture_width: i32,
|
||||
texture_height: i32,
|
||||
texture_size: Vector2<i32>,
|
||||
layer: Layer,
|
||||
}
|
||||
|
||||
@ -68,8 +51,7 @@ impl CacheShapesPass {
|
||||
shapes_to_render: default(),
|
||||
layer: Layer::new("Cached Shapes"),
|
||||
scene: scene.clone_ref(),
|
||||
texture_width: default(),
|
||||
texture_height: default(),
|
||||
texture_size: default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -79,18 +61,18 @@ impl CacheShapesPass {
|
||||
|
||||
impl pass::Definition for CacheShapesPass {
|
||||
fn initialize(&mut self, instance: &Instance) {
|
||||
let ArrangedShapes { texture_width, texture_height, shapes } =
|
||||
display::world::CACHED_SHAPES_DEFINITIONS
|
||||
.with_borrow(|shapes| arrange_shapes_on_texture(shapes));
|
||||
self.shapes_to_render = shapes.into_iter().map(Box::into).collect();
|
||||
self.texture_width = texture_width;
|
||||
self.texture_height = texture_height;
|
||||
display::world::CACHED_SHAPES_DEFINITIONS.with_borrow(|shapes| {
|
||||
self.shapes_to_render =
|
||||
shapes.iter().map(|def| (def.for_texture_constructor)().into()).collect()
|
||||
});
|
||||
let texture_size = display::shape::primitive::system::cached::texture_size();
|
||||
self.texture_size = texture_size;
|
||||
|
||||
for shape in &self.shapes_to_render {
|
||||
self.scene.add_child(&**shape);
|
||||
self.layer.add(&**shape);
|
||||
}
|
||||
self.layer.camera().set_screen(texture_width as f32, texture_height as f32);
|
||||
self.layer.camera().set_screen(texture_size.x as f32, texture_size.y as f32);
|
||||
// We must call update of layer and display object hierarchy at this point, because:
|
||||
// 1. the [`self.layer`] is not in the Layer hierarchy, so it's not updated during routine
|
||||
// layers update.
|
||||
@ -101,7 +83,7 @@ impl pass::Definition for CacheShapesPass {
|
||||
self.layer.update();
|
||||
|
||||
let output = pass::OutputDefinition::new_rgba("cached_shapes");
|
||||
let texture = instance.new_texture(&output, texture_width, texture_height);
|
||||
let texture = instance.new_texture(&output, self.texture_size.x, self.texture_size.y);
|
||||
self.framebuffer = Some(instance.new_framebuffer(&[&texture]));
|
||||
}
|
||||
|
||||
@ -112,11 +94,13 @@ impl pass::Definition for CacheShapesPass {
|
||||
if ready_to_render.peek().is_some() {
|
||||
if let Some(framebuffer) = self.framebuffer.as_ref() {
|
||||
framebuffer.with_bound(|| {
|
||||
instance.with_viewport(self.texture_width, self.texture_height, || {
|
||||
with_context(|ctx| ctx.set_camera(&self.layer.camera()));
|
||||
for shape in ready_to_render {
|
||||
shape.sprite().symbol.render();
|
||||
}
|
||||
instance.with_viewport(self.texture_size.x, self.texture_size.y, || {
|
||||
with_display_mode(DisplayModes::CachedShapesTexture, || {
|
||||
with_context(|ctx| ctx.set_camera(&self.layer.camera()));
|
||||
for shape in ready_to_render {
|
||||
shape.sprite().symbol.render();
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@ -126,251 +110,13 @@ impl pass::Definition for CacheShapesPass {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===================================
|
||||
// === Arranging Shapes on Texture ===
|
||||
// ===================================
|
||||
|
||||
/// For shape of given size, find the unoccupied space in the square texture.
|
||||
///
|
||||
/// A step of the [`arrange_shapes_on_texture`] algorithm. See its docs for details.
|
||||
fn find_free_place(
|
||||
shape_size: Vector2<i32>,
|
||||
tex_size: i32,
|
||||
placed_so_far: &[BoundingBox],
|
||||
) -> Option<BoundingBox> {
|
||||
// There is no need of iterating over all columns and rows, as the new shape is always "glued"
|
||||
// to right/top boundary of another shape, or left/bottom boundary of the texture.
|
||||
let right_bounds = placed_so_far.iter().map(|bbox| bbox.right().ceil() as i32);
|
||||
let allowed_right_bounds = right_bounds.filter(|col| col + shape_size.x <= tex_size);
|
||||
let top_bounds = placed_so_far.iter().map(|bbox| bbox.top().ceil() as i32);
|
||||
let allowed_top_bounds = top_bounds.filter(|row| row + shape_size.y <= tex_size);
|
||||
let candidate_rows = iter::once(0).chain(allowed_top_bounds).sorted();
|
||||
let candidate_cols = iter::once(0).chain(allowed_right_bounds).sorted();
|
||||
let candidate_positions = iproduct!(candidate_rows, candidate_cols);
|
||||
let mut candidate_bboxes = candidate_positions.map(|(y, x)| {
|
||||
BoundingBox::from_position_and_size(
|
||||
Vector2(x as f32, y as f32),
|
||||
shape_size.map(|i| i as f32),
|
||||
)
|
||||
});
|
||||
candidate_bboxes.find(|bbox| {
|
||||
let is_collision = placed_so_far.iter().any(|placed| placed.interior_intersects(bbox));
|
||||
!is_collision
|
||||
fn with_display_mode<R>(mode: DisplayModes, f: impl FnOnce() -> R) -> R {
|
||||
with_context(move |ctx| {
|
||||
let mode_before = ctx.display_mode.get();
|
||||
let code: u32 = mode.into();
|
||||
ctx.display_mode.set(code as i32);
|
||||
let result = f();
|
||||
ctx.display_mode.set(mode_before);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
/// The results of [`arrange_shapes_on_texture`] function.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug)]
|
||||
struct ArrangedShapes {
|
||||
texture_width: i32,
|
||||
texture_height: i32,
|
||||
#[derivative(Debug = "ignore")]
|
||||
shapes: Vec<Box<dyn AnyShapeView>>,
|
||||
}
|
||||
|
||||
/// Arrange Cached Shapes on texture.
|
||||
///
|
||||
/// The function takes the definition of cached shapes and returns the created
|
||||
/// [shape views](AnyShapeView) with positions and sizes already set to proper place in the texture.
|
||||
///
|
||||
/// # The Texture Pack Algorithm
|
||||
///
|
||||
/// The algorithm is a simple heuristic: we sort shapes from the biggest to the smallest, and take
|
||||
/// the shapes one by one by that order and scans possible left-bottom corner positions, starting
|
||||
/// from left-bottom corner of the texture and going row by row, from left to right and from bottom
|
||||
/// to top, picking the first position where the current shape does not collide with any already
|
||||
/// placed one. According to
|
||||
/// https://gamedev.stackexchange.com/questions/2829/texture-packing-algorithm this is actually one
|
||||
/// of the best packing algorithm in terms of texture space.
|
||||
///
|
||||
/// As the above algorithm assumes some texture size, we starts with predefined square texture with
|
||||
/// [`INITIAL_TEXTURE_SIZE`] and - if the shapes does not fit it - we repeatedly increase the size
|
||||
/// and try again.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// Assuming initial texture size 5x5 and having the following shapes:
|
||||
///
|
||||
/// ```text
|
||||
/// ┌───┐ ┌───┐ ┌───────┐ ┌───┐ ┌───────────┐
|
||||
/// │1x1│ │1x1│ │2x2 │ │1x2│ │3x3 │
|
||||
/// └───┘ └───┘ │ │ │ │ │ │
|
||||
/// │ │ │ │ │ │
|
||||
/// └───────┘ └───┘ │ │
|
||||
/// │ │
|
||||
/// └───────────┘
|
||||
/// ```
|
||||
///
|
||||
/// this function will arrange it this way:
|
||||
/// ```text
|
||||
/// ┌───────────────────┐
|
||||
/// │ │
|
||||
/// ├───┐ ┌───┐ │
|
||||
/// │1x1│ │1x2│ │
|
||||
/// ├───┴───────┤ ├───┤
|
||||
/// │3x3 │ │1x1│
|
||||
/// │ ├───┴───┤
|
||||
/// │ │2x2 │
|
||||
/// │ │ │
|
||||
/// └───────────┴───────┘
|
||||
/// ```
|
||||
/// And return the texture size 5x4.
|
||||
///
|
||||
/// # Implementation
|
||||
///
|
||||
/// As the new shape is always "glued" to
|
||||
/// - right boundary of another shape or left boundary of the texture
|
||||
/// - top boundary of another shape or bottom boundary of the texture
|
||||
/// We do not need to iterate over every row and column, only over those containing the
|
||||
/// boundaries.
|
||||
///
|
||||
/// Therefore, this simple implementation has the time complexity of O(n^3) where n is number of
|
||||
/// placed shapes.
|
||||
fn arrange_shapes_on_texture<'a>(
|
||||
shapes: impl IntoIterator<Item = &'a CachedShapeDefinition>,
|
||||
) -> ArrangedShapes {
|
||||
let sorted_shapes =
|
||||
shapes.into_iter().sorted_by_key(|shape| shape.size.x * shape.size.y).rev().collect_vec();
|
||||
let mut tex_size = INITIAL_TEXTURE_SIZE;
|
||||
loop {
|
||||
if let Some(arranged) = try_to_fit_shapes_on_texture(&sorted_shapes, tex_size) {
|
||||
break arranged;
|
||||
}
|
||||
tex_size *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to fit shapes on texture assuming maximum texture size. Returns [`None`] if was unable to
|
||||
/// do it.
|
||||
fn try_to_fit_shapes_on_texture(
|
||||
sorted_shapes: &[impl std::borrow::Borrow<CachedShapeDefinition>],
|
||||
max_texture_size: i32,
|
||||
) -> Option<ArrangedShapes> {
|
||||
let mut placed_so_far: Vec<BoundingBox> = vec![];
|
||||
let sorted_shapes_ref = sorted_shapes.iter().map(|shape| shape.borrow());
|
||||
let shapes_with_positions_iter = sorted_shapes_ref.map(|shape_def| {
|
||||
find_free_place(shape_def.size, max_texture_size, &placed_so_far).map(|bbox| {
|
||||
placed_so_far.push(bbox);
|
||||
(shape_def, bbox.center())
|
||||
})
|
||||
});
|
||||
// We collect all positions, so `placed_so_far` will contain all shapes and we can compute
|
||||
// texture width.
|
||||
let shapes_with_positions: Option<Vec<_>> = shapes_with_positions_iter.collect();
|
||||
let texture_width =
|
||||
placed_so_far.iter().map(BoundingBox::right).reduce(f32::max).unwrap_or_default();
|
||||
let texture_height =
|
||||
placed_so_far.iter().map(BoundingBox::top).reduce(f32::max).unwrap_or_default();
|
||||
let texture_origin = Vector2(texture_width, texture_height) / 2.0;
|
||||
shapes_with_positions.map(|shapes_with_positions| {
|
||||
let shapes = shapes_with_positions.into_iter().map(|(shape_def, position)| {
|
||||
let shape = (shape_def.cons)();
|
||||
shape.set_size(shape_def.size);
|
||||
shape.set_xy(position - texture_origin);
|
||||
shape
|
||||
});
|
||||
ArrangedShapes {
|
||||
shapes: shapes.collect(),
|
||||
texture_width: texture_width as i32,
|
||||
texture_height: texture_height as i32,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Tests ===
|
||||
// =============
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::display::shape::*;
|
||||
use crate::display::world::World;
|
||||
|
||||
mod mock_shape {
|
||||
use super::*;
|
||||
|
||||
crate::shape! {
|
||||
(style: Style) {
|
||||
Plane().into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shape_entries_from_sizes(
|
||||
sizes: impl IntoIterator<Item = (i32, i32)>,
|
||||
) -> Vec<CachedShapeDefinition> {
|
||||
sizes
|
||||
.into_iter()
|
||||
.map(|(width, height)| CachedShapeDefinition {
|
||||
size: Vector2(width, height),
|
||||
cons: Box::new(|| Box::new(mock_shape::View::new())),
|
||||
})
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn texture_size() {
|
||||
fn run_case(shape_sizes: impl IntoIterator<Item = (i32, i32)>, expected_size: (i32, i32)) {
|
||||
let shape_entries = shape_entries_from_sizes(shape_sizes);
|
||||
let result = arrange_shapes_on_texture(shape_entries.iter());
|
||||
assert_eq!((result.texture_width, result.texture_height), expected_size);
|
||||
}
|
||||
|
||||
let _world = World::new();
|
||||
|
||||
run_case([(32, 32), (16, 16)], (48, 32));
|
||||
run_case([(16, 2), (2, 20)], (18, 20));
|
||||
run_case([(256, 2), (257, 2)], (257, 4));
|
||||
run_case(iter::repeat((32, 32)).take(2), (64, 32));
|
||||
run_case(iter::repeat((32, 32)).take(16), (512, 32));
|
||||
run_case(iter::repeat((32, 32)).take(17), (512, 64));
|
||||
run_case(iter::repeat((64, 64)).take(64), (512, 512));
|
||||
// Shapes does not fit initial texture size: the texture is extended.
|
||||
run_case(iter::repeat((64, 64)).take(65), (1024, 320));
|
||||
// This will extend the texture several times.
|
||||
run_case(iter::repeat((512, 512)).take(17), (4096, 1536));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fitting_shapes_on_texture() {
|
||||
fn run_case<const N: usize>(
|
||||
tex_size: i32,
|
||||
sizes: [(i32, i32); N],
|
||||
expected_position: Option<[(f32, f32); N]>,
|
||||
) {
|
||||
let shape_entries = shape_entries_from_sizes(sizes);
|
||||
let result = try_to_fit_shapes_on_texture(&shape_entries, tex_size);
|
||||
let positions = result.map(|shapes| {
|
||||
shapes.shapes.iter().map(|shape| (shape.x(), shape.y())).collect_vec()
|
||||
});
|
||||
assert_eq!(positions.as_deref(), expected_position.as_ref().map(|arr| arr.as_slice()));
|
||||
}
|
||||
|
||||
let _world = World::new();
|
||||
// run_case(64, [(32, 32), (32, 32)], Some([(-16.0, 0.0), (16.0, 0.0)]));
|
||||
// run_case(63, [(32, 32), (32, 32)], None);
|
||||
// run_case(
|
||||
// 64,
|
||||
// [(32, 32), (32, 32), (32, 32), (32, 32)],
|
||||
// Some([(-16.0, -16.0), (16.0, -16.0), (-16.0, 16.0), (16.0, 16.0)]),
|
||||
// );
|
||||
// run_case(64, [(32, 32), (32, 32), (32, 32), (2, 33)], None);
|
||||
run_case(
|
||||
64,
|
||||
[(32, 32), (16, 16), (16, 16), (16, 16)],
|
||||
Some([(-16.0, 0.0), (8.0, -8.0), (24.0, -8.0), (8.0, 8.0)]),
|
||||
);
|
||||
// run_case(
|
||||
// 32,
|
||||
// [(16, 8), (2, 32), (16, 2), (12, 2)],
|
||||
// Some([(-8.0, -12.0), (1.0, 0.0), (-8.0, -7.0), (8.0, -15.0)]),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ pub enum DisplayModes {
|
||||
DebugSdf,
|
||||
DebugShapeAaSpan,
|
||||
DebugInstanceId,
|
||||
CachedShapesTexture,
|
||||
}
|
||||
|
||||
// This is not derived, as then [`num_enum::TryFromPrimitive`] will return the default value for
|
||||
@ -57,6 +58,7 @@ impl DisplayModes {
|
||||
pub fn allow_mouse_events(self) -> bool {
|
||||
match self {
|
||||
DisplayModes::Normal => true,
|
||||
DisplayModes::CachedShapesTexture => false,
|
||||
DisplayModes::DebugSpriteUv => true,
|
||||
DisplayModes::DebugSpriteOverview => false,
|
||||
DisplayModes::DebugSpriteGrid => false,
|
||||
|
@ -43,10 +43,16 @@ if (input_display_mode == DISPLAY_MODE_NORMAL) {
|
||||
output_color = srgba(shape.color).raw;
|
||||
output_color.rgb *= alpha;
|
||||
|
||||
} else if (input_display_mode == DISPLAY_MODE_CACHED_SHAPES_TEXTURE) {
|
||||
output_color = rgba(shape.color).raw;
|
||||
// The signed distance is stored in the texture's alpha channel in a special way. See
|
||||
// [`crate::display::shape::primitive::system::cached`] documentation for details.
|
||||
output_color.a = -shape.sdf.distance / CACHED_SHAPE_MAX_DISTANCE / 2.0 + 0.5;
|
||||
|
||||
} else if (input_display_mode == DISPLAY_MODE_DEBUG_SHAPE_AA_SPAN) {
|
||||
output_color = srgba(shape.color).raw;
|
||||
output_color.rgb *= alpha;
|
||||
output_color = outside_of_uv() ? vec4(1.0,0.0,0.0,1.0) : output_color;
|
||||
output_color = outside_of_uv() ? vec4(1.0, 0.0, 0.0, 1.0) : output_color;
|
||||
|
||||
} else if (input_display_mode == DISPLAY_MODE_DEBUG_SDF) {
|
||||
float zoom = zoom();
|
||||
|
@ -5,18 +5,21 @@
|
||||
// === Color ===
|
||||
// =============
|
||||
|
||||
/// The default color used for [`Shape`]s. The LCHA representation was chosen because it gives good
|
||||
/// results for color blending (better than RGB and way better than sRGB).
|
||||
/// The default color used for [`Shape`]s.
|
||||
struct Color {
|
||||
Lcha repr;
|
||||
Rgba repr;
|
||||
};
|
||||
|
||||
Color color (Lcha color) {
|
||||
Color color (Rgba color) {
|
||||
return Color(color);
|
||||
}
|
||||
|
||||
Color color(vec3 lch, float a) {
|
||||
return Color(lcha(lch, a));
|
||||
Color color(vec3 rgb, float a) {
|
||||
return Color(rgba(rgb, a));
|
||||
}
|
||||
|
||||
Color color(Rgb rgb, float a) {
|
||||
return Color(rgba(rgb, a));
|
||||
}
|
||||
|
||||
Srgba srgba(Color color) {
|
||||
@ -31,13 +34,13 @@ Srgba srgba(Color color) {
|
||||
|
||||
/// The premultiplied version of [`Color`] (the `xyz` components are multiplied by its alpha).
|
||||
struct PremultipliedColor {
|
||||
Lcha repr;
|
||||
Rgba repr;
|
||||
};
|
||||
|
||||
PremultipliedColor premultiply(Color t) {
|
||||
float alpha = a(t.repr);
|
||||
vec3 rgb = t.repr.raw.rgb * alpha;
|
||||
return PremultipliedColor(lcha(rgb, alpha));
|
||||
return PremultipliedColor(rgba(rgb, alpha));
|
||||
}
|
||||
|
||||
Color unpremultiply(PremultipliedColor c) {
|
||||
@ -50,13 +53,17 @@ Color unpremultiply(PremultipliedColor c) {
|
||||
/// in the [`Color`]'s color space. See docs of [`Color`] to learn more.
|
||||
PremultipliedColor blend(PremultipliedColor bg, PremultipliedColor fg) {
|
||||
vec4 raw = fg.repr.raw + (1.0 - fg.repr.raw.a) * bg.repr.raw;
|
||||
return PremultipliedColor(lcha(raw));
|
||||
return PremultipliedColor(rgba(raw));
|
||||
}
|
||||
|
||||
Srgba srgba(PremultipliedColor color) {
|
||||
return srgba(unpremultiply(color));
|
||||
}
|
||||
|
||||
Rgba rgba(PremultipliedColor color) {
|
||||
return unpremultiply(color).repr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
@ -79,6 +86,10 @@ BoundingBox bounding_box (float w, float h) {
|
||||
return BoundingBox(-w, w, -h, h);
|
||||
}
|
||||
|
||||
BoundingBox bounding_box (vec2 center_point, float w, float h) {
|
||||
return BoundingBox(center_point.x - w, center_point.x + w, center_point.y - h, center_point.y + h);
|
||||
}
|
||||
|
||||
BoundingBox bounding_box (vec2 size) {
|
||||
float w2 = size.x / 2.0;
|
||||
float h2 = size.y / 2.0;
|
||||
@ -125,6 +136,10 @@ BoundingBox grow (BoundingBox a, float value) {
|
||||
return BoundingBox(min_x, max_x, min_y, max_y);
|
||||
}
|
||||
|
||||
bool contains(BoundingBox box, vec2 point) {
|
||||
return box.min_x <= point.x && box.max_x >= point.x && box.min_y <= point.y && box.max_y >= point.y;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===========
|
||||
@ -270,13 +285,19 @@ struct Shape {
|
||||
Id id;
|
||||
/// The Signed Distance Field, describing the shape boundaries.
|
||||
BoundSdf sdf;
|
||||
/// The color of the shape. Please note that we are storing the premultiplied version of the
|
||||
/// color, as blending requires premultiplied values. We could store the non-premultiplied,
|
||||
/// however, then we would need to unpremultiply the color after blending, which leads to
|
||||
/// a serious issue. If the alpha is 0, then either unpremultiplication needs to be more costly
|
||||
/// and check for this condition, or it will produce infinite/invalid values for the `xyz`
|
||||
/// components. If we blend such a color with another one, then we will get artifacts, as
|
||||
/// multiplying an infinite/invalid value by 0 is an undefined behavior.
|
||||
/// The color of the shape.
|
||||
///
|
||||
/// We are storing the premultiplied version of the color, as blending requires premultiplied
|
||||
/// values. We could store the non-premultiplied, however, then we would need to unpremultiply
|
||||
/// the color after blending, which leads to a serious issue. If the alpha is 0, then either
|
||||
/// unpremultiplication needs to be more costly and check for this condition, or it will produce
|
||||
/// infinite/invalid values for the `xyz` components. If we blend such a color with another one,
|
||||
/// then we will get artifacts, as multiplying an infinite/invalid value by 0 is an undefined
|
||||
/// behavior.
|
||||
///
|
||||
/// The [`alpha`] field is applied on thin field, except if we are in
|
||||
/// `DISPLAY_MODE_CACHED_SHAPES_TEXTURE`. This display mode is an exception, because otherwise
|
||||
/// cached shapes would have darker borders if not aligned to the texture pixels.
|
||||
PremultipliedColor color;
|
||||
/// The opacity of the shape. It is the result of rendering of the [`sdf`].
|
||||
float alpha;
|
||||
@ -284,7 +305,11 @@ struct Shape {
|
||||
|
||||
Shape shape (Id id, BoundSdf bound_sdf, PremultipliedColor color) {
|
||||
float alpha = render(bound_sdf);
|
||||
color.repr.raw *= alpha;
|
||||
// See [`color`] field documentation for explanation why DISPLAY_MODE_CACHED_SHAPES_TEXTURE is
|
||||
// an exception.
|
||||
if (input_display_mode != DISPLAY_MODE_CACHED_SHAPES_TEXTURE) {
|
||||
color.repr.raw *= alpha;
|
||||
}
|
||||
return Shape(id, bound_sdf, color, alpha);
|
||||
}
|
||||
|
||||
@ -292,16 +317,12 @@ Shape shape (Id id, BoundSdf bound_sdf, Color color) {
|
||||
return shape(id, bound_sdf, premultiply(color));
|
||||
}
|
||||
|
||||
Shape shape (Id id, BoundSdf bound_sdf, Srgba rgba) {
|
||||
return shape(id, bound_sdf, Color(lcha(rgba)));
|
||||
Shape shape (Id id, BoundSdf bound_sdf, Rgba rgba) {
|
||||
return shape(id, bound_sdf, Color(rgba));
|
||||
}
|
||||
|
||||
Shape shape (Id id, BoundSdf bound_sdf) {
|
||||
return shape(id, bound_sdf, srgba(1.0, 0.0, 0.0, 1.0));
|
||||
}
|
||||
|
||||
Shape shape (Id id, BoundSdf bound_sdf, Lcha lcha) {
|
||||
return shape(id, bound_sdf, Color(lcha));
|
||||
return shape(id, bound_sdf, rgba(1.0, 0.0, 0.0, 1.0));
|
||||
}
|
||||
|
||||
/// A debug [`Shape`] constructor. Should not be used to create shapes that are rendered to the
|
||||
@ -315,21 +336,33 @@ Shape debug_shape (BoundSdf bound_sdf) {
|
||||
Shape resample (Shape s, float multiplier) {
|
||||
Id id = s.id;
|
||||
BoundSdf sdf = resample(s.sdf, multiplier);
|
||||
s.color.repr.raw.a /= s.alpha;
|
||||
// The [`alpha`] field is not applied on [`color`] if we are in
|
||||
// `DISPLAY_MODE_CACHED_SHAPES_TEXTURE`. See [`color`] docs for explanation.
|
||||
if (input_display_mode != DISPLAY_MODE_CACHED_SHAPES_TEXTURE) {
|
||||
s.color.repr.raw.a /= s.alpha;
|
||||
}
|
||||
return shape(id, sdf, s.color);
|
||||
}
|
||||
|
||||
Shape pixel_snap (Shape s) {
|
||||
Id id = s.id;
|
||||
BoundSdf sdf = pixel_snap(s.sdf);
|
||||
s.color.repr.raw.a /= s.alpha;
|
||||
// The [`alpha`] field is not applied on [`color`] if we are in
|
||||
// `DISPLAY_MODE_CACHED_SHAPES_TEXTURE`. See [`color`] docs for explanation.
|
||||
if (input_display_mode != DISPLAY_MODE_CACHED_SHAPES_TEXTURE) {
|
||||
s.color.repr.raw.a /= s.alpha;
|
||||
}
|
||||
return shape(id, sdf, s.color);
|
||||
}
|
||||
|
||||
Shape grow (Shape s, float value) {
|
||||
Id id = s.id;
|
||||
BoundSdf sdf = grow(s.sdf,value);
|
||||
s.color.repr.raw.a /= s.alpha;
|
||||
// The [`alpha`] field is not applied on [`color`] if we are in
|
||||
// `DISPLAY_MODE_CACHED_SHAPES_TEXTURE`. See [`color`] docs for explanation.
|
||||
if (input_display_mode != DISPLAY_MODE_CACHED_SHAPES_TEXTURE) {
|
||||
s.color.repr.raw.a /= s.alpha;
|
||||
}
|
||||
return shape(id, sdf, s.color);
|
||||
}
|
||||
|
||||
@ -338,6 +371,22 @@ Shape inverse (Shape s1) {
|
||||
}
|
||||
|
||||
Shape unify (Shape s1, Shape s2) {
|
||||
if (input_display_mode == DISPLAY_MODE_CACHED_SHAPES_TEXTURE) {
|
||||
// In DISPLAY_MODE_CACHED_SHAPES_TEXTURE the color has not [`alpha`] field applied (See
|
||||
// [`color`] documentation for explanation). That means, that even outside the
|
||||
// foreground shape the [`color`] field will be in the full shape's intensity. However we
|
||||
// need to make the foregroud color to give way for background colors, so it won't "cover"
|
||||
// the background shape.
|
||||
// There are two conditions we want to met:
|
||||
// * If we are outside the foreground shape, but inside the background shape, the foreground
|
||||
// color should blend properly with background color, so it alpha must apply the sdf render
|
||||
// results ([`alpha`] field).
|
||||
// * We want to keep the color consistent near border of the both shapes.
|
||||
// The code below meets the both conditions.
|
||||
if (s2.sdf.distance > s1.sdf.distance) {
|
||||
s2.color.repr.raw *= s2.alpha;
|
||||
}
|
||||
}
|
||||
return shape(s1.id, unify(s1.sdf, s2.sdf), blend(s1.color, s2.color));
|
||||
}
|
||||
|
||||
@ -353,9 +402,11 @@ Shape intersection_no_blend (Shape s1, Shape s2) {
|
||||
return shape(s1.id, intersection(s1.sdf, s2.sdf), s1.color);
|
||||
}
|
||||
|
||||
Shape set_color(Shape shape, Srgba t) {
|
||||
t.raw.a *= shape.alpha;
|
||||
shape.color = premultiply(Color(lcha(t)));
|
||||
Shape set_color(Shape shape, Rgba t) {
|
||||
if (input_display_mode != DISPLAY_MODE_CACHED_SHAPES_TEXTURE) {
|
||||
t.raw.a *= shape.alpha;
|
||||
}
|
||||
shape.color = premultiply(Color(t));
|
||||
return shape;
|
||||
}
|
||||
|
||||
@ -365,6 +416,32 @@ Shape with_infinite_bounds (Shape s) {
|
||||
return shape(s.id, sdf, s.color);
|
||||
}
|
||||
|
||||
/// Read the shape from the Cached Shapes Texture.
|
||||
///
|
||||
/// The `tex_bbox` is expressed in texture pixels, where origin is at the texture center.
|
||||
Shape cached_shape(Id id, vec2 position, vec4 tex_bbox) {
|
||||
BoundingBox texture_bbox = bounding_box(tex_bbox.x, tex_bbox.z, tex_bbox.y, tex_bbox.w);
|
||||
BoundingBox shape_bbox = bounding_box(position, (tex_bbox.z - tex_bbox.x) / 2.0, (tex_bbox.w - tex_bbox.y) / 2.0);
|
||||
|
||||
vec2 texture_bbox_center = (tex_bbox.xy + tex_bbox.zw) / 2.0;
|
||||
vec2 texture_position = texture_bbox_center + position;
|
||||
vec2 texture_uv_origin = vec2(0.5, 0.5);
|
||||
vec2 texture_uv = (texture_position / vec2(textureSize(input_pass_cached_shapes, 0))) + texture_uv_origin;
|
||||
Rgba color_and_distance;
|
||||
if (contains(texture_bbox, texture_position)) {
|
||||
color_and_distance = rgba(texture(input_pass_cached_shapes, texture_uv));
|
||||
} else {
|
||||
color_and_distance = rgba(0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
// The signed distance is stored in the texture's alpha channel in a special way. See
|
||||
// [`crate::display::shape::primitive::system::cached`] documentation for details.
|
||||
float alpha_representing_0_sdf = 0.5;
|
||||
float distance = -(color_and_distance.raw.a - alpha_representing_0_sdf) * 2.0 * CACHED_SHAPE_MAX_DISTANCE;
|
||||
BoundSdf sdf = bound_sdf(distance, shape_bbox);
|
||||
Color shape_color = color(color_and_distance.raw.rgb, 1.0);
|
||||
return shape(id, sdf, shape_color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::display::shape::cached::CACHED_TEXTURE_MAX_DISTANCE;
|
||||
use crate::display::shape::primitive::def::primitive;
|
||||
use crate::display::shape::primitive::glsl::codes;
|
||||
use crate::display::shape::primitive::shader::overload;
|
||||
@ -84,10 +85,15 @@ fn glsl_codes() -> String {
|
||||
format!("{header}\n\n{display_modes}\n{error_codes}")
|
||||
}
|
||||
|
||||
/// The GLSL common code and debug codes.
|
||||
pub fn glsl_prelude_and_codes() -> String {
|
||||
fn glsl_constants() -> String {
|
||||
let codes = glsl_codes();
|
||||
format!("{GLSL_PRELUDE}\n\n{codes}")
|
||||
format!("{codes}\n\nconst float CACHED_SHAPE_MAX_DISTANCE = {CACHED_TEXTURE_MAX_DISTANCE:?};")
|
||||
}
|
||||
|
||||
/// The GLSL common code and shared constants (including debug codes).
|
||||
pub fn glsl_prelude_and_constants() -> String {
|
||||
let constants = glsl_constants();
|
||||
format!("{GLSL_PRELUDE}\n\n{constants}")
|
||||
}
|
||||
|
||||
fn gen_glsl_boilerplate() -> String {
|
||||
@ -96,7 +102,7 @@ fn gen_glsl_boilerplate() -> String {
|
||||
let color = overload::allow_overloading(COLOR);
|
||||
let debug = overload::allow_overloading(DEBUG);
|
||||
let shape = overload::allow_overloading(SHAPE);
|
||||
let codes_and_prelude = glsl_prelude_and_codes();
|
||||
let codes_and_prelude = glsl_prelude_and_constants();
|
||||
let defs_header = header("SDF Primitives");
|
||||
let sdf_defs = overload::allow_overloading(&primitive::all_shapes_glsl_definitions());
|
||||
[
|
||||
|
@ -241,7 +241,7 @@ impl Canvas {
|
||||
let color: Glsl = color.into().glsl();
|
||||
this.add_current_function_code_line(format!("Shape shape = {};", s.getter()));
|
||||
this.add_current_function_code_line(format!("Srgba color = srgba({color});"));
|
||||
this.new_shape_from_expr("return set_color(shape,color);")
|
||||
this.new_shape_from_expr("return set_color(shape,rgba(color));")
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,7 @@ use crate::system::gpu::types::*;
|
||||
use crate::display;
|
||||
use crate::display::object::instance::GenericLayoutApi;
|
||||
use crate::display::shape::primitive::shader;
|
||||
use crate::display::shape::Var;
|
||||
use crate::display::symbol;
|
||||
use crate::display::symbol::geometry::Sprite;
|
||||
use crate::display::symbol::geometry::SpriteSystem;
|
||||
@ -77,6 +78,16 @@ use crate::system::gpu::data::InstanceIndex;
|
||||
use super::def;
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub mod cached;
|
||||
|
||||
pub use cached::AnyCachedShape;
|
||||
pub use cached::CachedShape;
|
||||
|
||||
|
||||
|
||||
// =====================
|
||||
// === ShapeSystemId ===
|
||||
@ -389,13 +400,14 @@ impl ShapeSystemModel {
|
||||
material.add_input_def::<Vector2<i32>>("mouse_position");
|
||||
material.add_input_def::<i32>("mouse_click_count");
|
||||
material.add_input("display_mode", 0);
|
||||
material.add_input_def::<texture::FloatSampler>("pass_cached_shapes");
|
||||
material.add_output("id", Vector4::<f32>::zero());
|
||||
material
|
||||
}
|
||||
|
||||
fn default_geometry_material() -> Material {
|
||||
let mut material = SpriteSystem::default_geometry_material();
|
||||
material.set_before_main(shader::builder::glsl_prelude_and_codes());
|
||||
material.set_before_main(shader::builder::glsl_prelude_and_constants());
|
||||
// The GLSL vertex shader implementing automatic shape padding for anti-aliasing. See the
|
||||
// docs of [`aa_side_padding`] to learn more about the concept of shape padding.
|
||||
//
|
||||
@ -546,6 +558,35 @@ where
|
||||
}
|
||||
|
||||
|
||||
// =================
|
||||
// === Parameter ===
|
||||
// =================
|
||||
|
||||
/// A type which can be a shape parameter.
|
||||
///
|
||||
/// All types representable in Glsl (primitives, Vectors etc.) implements this by default.
|
||||
pub trait Parameter {
|
||||
/// The type representation in GLSL. To be usable, it should implement [`Storable`] trait.
|
||||
type GpuType;
|
||||
/// The type representation in shader definition code.
|
||||
type Variable;
|
||||
|
||||
/// A constructor of [`Self::Variable`] representing parameter with given name.
|
||||
///
|
||||
/// The `name` should contain the obligatory `input_` prefix.
|
||||
fn create_var(name: &str) -> Self::Variable;
|
||||
}
|
||||
|
||||
impl<T: Storable> Parameter for T {
|
||||
type GpuType = T;
|
||||
type Variable = Var<T>;
|
||||
|
||||
default fn create_var(name: &str) -> Self::Variable {
|
||||
name.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Macros ===
|
||||
@ -850,7 +891,7 @@ macro_rules! _shape {
|
||||
) -> Self::GpuParams {
|
||||
$(
|
||||
let name = stringify!($gpu_param);
|
||||
let val = gpu::data::default::gpu_default::<$gpu_param_type>();
|
||||
let val = gpu::data::default::gpu_default::<<$gpu_param_type as Parameter>::GpuType>();
|
||||
let $gpu_param = shape_system.add_input(name,val);
|
||||
)*
|
||||
Self::GpuParams {$($gpu_param),*}
|
||||
@ -868,8 +909,7 @@ macro_rules! _shape {
|
||||
// Silencing warnings about not used style.
|
||||
let _unused = &$style;
|
||||
$(
|
||||
let $gpu_param : $crate::display::shape::primitive::def::Var<$gpu_param_type> =
|
||||
concat!("input_",stringify!($gpu_param)).into();
|
||||
let $gpu_param = <$gpu_param_type as Parameter>::create_var(concat!("input_",stringify!($gpu_param)));
|
||||
// Silencing warnings about not used shader input variables.
|
||||
let _unused = &$gpu_param;
|
||||
)*
|
||||
@ -903,7 +943,7 @@ macro_rules! _shape {
|
||||
#[derive(Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct InstanceParams {
|
||||
$(pub $gpu_param : ProxyParam<Attribute<$gpu_param_type>>),*
|
||||
$(pub $gpu_param : ProxyParam<Attribute<<$gpu_param_type as Parameter>::GpuType>>),*
|
||||
}
|
||||
|
||||
impl InstanceParamsTrait for InstanceParams {
|
||||
@ -915,7 +955,7 @@ macro_rules! _shape {
|
||||
#[derive(Clone, CloneRef, Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct GpuParams {
|
||||
$(pub $gpu_param: gpu::data::Buffer<$gpu_param_type>),*
|
||||
$(pub $gpu_param: gpu::data::Buffer<<$gpu_param_type as Parameter>::GpuType>),*
|
||||
}
|
||||
|
||||
|
||||
@ -929,52 +969,3 @@ macro_rules! _shape {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Defines a new cached shape system.
|
||||
///
|
||||
/// The outcome is the same as for [`shape!`] macro, but the shape will be near application start
|
||||
/// to the special "cached shapes" texture. The texture is available in GLSL as "pass_cached_shapes"
|
||||
/// uniform. In the future there will be also a possibility of parametrization of normal shapes by
|
||||
/// cached shapes (see [#184212663](https://www.pivotaltracker.com/story/show/184212663)).
|
||||
///
|
||||
/// Because shape, once cached, is not redrawn, we don't allow for any parameterization except
|
||||
/// styles.
|
||||
#[macro_export]
|
||||
macro_rules! cached_shape {
|
||||
(
|
||||
$width:literal x $height:literal;
|
||||
$(type SystemData = $system_data:ident;)?
|
||||
$(type ShapeData = $shape_data:ident;)?
|
||||
$(flavor = $flavor:path;)?
|
||||
$(above = [$($always_above_1:tt $(::$always_above_2:tt)*),*];)?
|
||||
$(below = [$($always_below_1:tt $(::$always_below_2:tt)*),*];)?
|
||||
$(pointer_events = $pointer_events:tt;)?
|
||||
($style:ident : Style) {$($body:tt)*}
|
||||
) => {
|
||||
$crate::_shape! {
|
||||
$(SystemData($system_data))?
|
||||
$(ShapeData($shape_data))?
|
||||
$(flavor = [$flavor];)?
|
||||
$(above = [$($always_above_1 $(::$always_above_2)*),*];)?
|
||||
$(below = [$($always_below_1 $(::$always_below_2)*),*];)?
|
||||
$(pointer_events = $pointer_events;)?
|
||||
[$style] (){$($body)*}
|
||||
}
|
||||
|
||||
mod cached_shape_system_definition {
|
||||
use $crate::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use super::shape_system_definition::*;
|
||||
|
||||
#[before_main]
|
||||
pub fn register_cached_shape() {
|
||||
$crate::display::world::CACHED_SHAPES_DEFINITIONS.with(|shapes| {
|
||||
let cons: $crate::display::world::ShapeCons = Box::new(|| Box::new(View::new()));
|
||||
let size = Vector2($width, $height);
|
||||
let mut shapes = shapes.borrow_mut();
|
||||
shapes.push($crate::display::world::CachedShapeDefinition { cons, size });
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,482 @@
|
||||
//! Shape Systems with instance cached on global texture
|
||||
//!
|
||||
//! These systems are a valid [shape systems](super) which cannot be parameterized, but have
|
||||
//! a single instance cached on a special texture (available in GLSL as `pass_cached_shapes`). The
|
||||
//! texture keeps the color (except alpha) and the signed distance. The cached instance may be later
|
||||
//! used inside other shape systems by using [`AnyCachedShape`].
|
||||
//!
|
||||
//! # Limitations
|
||||
//!
|
||||
//! * The cached shapes do not support alpha channels fully: it will be taken into consideration
|
||||
//! when adding shapes in their definition, but the alpha value will be dropped during rendering
|
||||
//! to the texture. See _Cached Shapes Texture_ section for details.
|
||||
//! * The signed distance kept in the texture is capped at [`CACHED_TEXTURE_MAX_DISTANCE`]. This may
|
||||
//! give aliasing effects when zoomed out more than this distance.
|
||||
//! * The shapes rendered from the texture may not always give the best quality results, see
|
||||
//! _Limitations_ section of [`AnyCachedShape`] docs.
|
||||
//!
|
||||
//! # Usages
|
||||
//!
|
||||
//! The main idea behind cached shapes is speeding up rendering applications with many shape
|
||||
//! systems. Because each shape system is drawn with a different draw call, having e.g. a grid of
|
||||
//! icons can increase the number of those, greatly worsening the application performance. But
|
||||
//! instead all the icons can be rendered to texture, and the icon grid can use only one shape
|
||||
//! system parameterized by the exact icon ID.
|
||||
//!
|
||||
//! # Cached Shapes Texture
|
||||
//!
|
||||
//! As a part of the [`World`](crate::display::world::World) initialization, the exact size of the
|
||||
//! texture and shape positions is computed. The packing algorithm is defined in
|
||||
//! [`arrange_on_texture`] module. Then, the shapes are rendered to texture in
|
||||
//! [`CacheShapesPass`](crate::display::render::passes::CacheShapesPass) as soon as they are ready
|
||||
//! (their shader is compiled).
|
||||
//!
|
||||
//! The texture is rendered in
|
||||
//! [`CachdShapesTexture` display
|
||||
//! mode](crate::display::shape::primitive::glsl::codes::DisplayModes), where the RGB channels keeps
|
||||
//! the color of a given pixel (as one would expect), and the alpha channel contains the information
|
||||
//! about signed distance from the shape boundary, linearly transformed so:
|
||||
//! * 0.5 value means distance 0.0 distance,
|
||||
//! * 0.0 is [`CACHED_TEXTURE_MAX_DISTANCE`],
|
||||
//! * 1.0 is `-CACHED_TEXTURE_MAX_DISTANCE`.
|
||||
//! As the alpha channel is capped at `0.0..=1.0` range, this will limit the stored signed distance
|
||||
//! to `-CACHED_TEXTURE_MAX_DISTANCE..=CACHED_TEXTURE_MAX_DISTANCE`, which is one of the limitations
|
||||
//! of cached shapes quality (see _Limitations_ section).
|
||||
//!
|
||||
//! # Using Cached Shapes
|
||||
//!
|
||||
//! The shapes may be read from the texture by creating [`AnyCachedShape`]. This structure can also
|
||||
//! be a parameter for shape systems defined with [`shape!`] macro. See [the docs](AnyCachedShape)
|
||||
//! for details.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ensogl_core::display::shape::*;
|
||||
//!
|
||||
//!
|
||||
//! // === Defining Cached Shapes ===
|
||||
//!
|
||||
//! mod cached {
|
||||
//! use super::*;
|
||||
//! ensogl_core::cached_shape! { 32 x 32;
|
||||
//! (_style: Style) { Circle(16.px()).into() }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! mod another_cached {
|
||||
//! use super::*;
|
||||
//! ensogl_core::cached_shape! { 32 x 32;
|
||||
//! (_style: Style) { Circle(16.px()).into() }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//!
|
||||
//! // === Using Cached Shapes ===
|
||||
//!
|
||||
//! mod shape {
|
||||
//! use super::*;
|
||||
//!
|
||||
//! ensogl_core::shape! {
|
||||
//! (_style: Style) {
|
||||
//! let bg = Rect((100.px(), 100.px())).fill(color::Rgba::white());
|
||||
//! // Our shape may be very complex, lets read it from the texture.
|
||||
//! let with_bg = &bg + &AnyCachedShape::<crate::cached::Shape>();
|
||||
//! with_bg.into()
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! /// A parametrized shape, allowing us to draw many cached shapes in a single draw call.
|
||||
//! mod parameterized_shape {
|
||||
//! use super::*;
|
||||
//! ensogl_core::shape! {
|
||||
//! (_style: Style, icon: AnyCachedShape) {
|
||||
//! let bg = Rect((100.px(), 100.px())).fill(color::Rgba::white());
|
||||
//! let with_bg = &bg + &icon;
|
||||
//! with_bg.into()
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! # fn main() {
|
||||
//! # let _world = ensogl_core::display::world::World::new();
|
||||
//! let shape = parameterized_shape::View::new();
|
||||
//! shape.icon.set(cached::Shape::any_cached_shape_parameter());
|
||||
//! // We can change the icon if we want:
|
||||
//! shape.icon.set(another_cached::Shape::any_cached_shape_parameter());
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::data::bounding_box::BoundingBox;
|
||||
use crate::display::shape;
|
||||
use crate::display::shape::canvas;
|
||||
use crate::display::shape::canvas::Canvas;
|
||||
use crate::display::shape::class::ShapeRef;
|
||||
use crate::display::shape::system::cached::arrange_on_texture::arrange_shapes_on_texture;
|
||||
use crate::display::shape::system::cached::arrange_on_texture::ShapeWithPosition;
|
||||
use crate::display::shape::system::cached::arrange_on_texture::ShapeWithSize;
|
||||
use crate::display::shape::AnyShape;
|
||||
use crate::display::shape::Parameter;
|
||||
use crate::display::shape::Var;
|
||||
use crate::display::world::CACHED_SHAPES_DEFINITIONS;
|
||||
use crate::display::IntoGlsl;
|
||||
use crate::gui::component::ShapeView;
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub mod arrange_on_texture;
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/// The maximum absolute value of signed distance stored in the Texture. See
|
||||
/// [full cached shapes documentation](self) for information why we need it.
|
||||
pub const CACHED_TEXTURE_MAX_DISTANCE: f32 = 8.0;
|
||||
|
||||
/// A parameter of [`arrange_shapes_on_texture`] algorithm: the initial assumed size for the texture
|
||||
/// with cached shapes. If it turn out to be too small, we extend its size and try again.
|
||||
const INITIAL_TEXTURE_SIZE: i32 = 512;
|
||||
|
||||
|
||||
|
||||
// ======================
|
||||
// === Texture Layout ===
|
||||
// ======================
|
||||
|
||||
// === Texture Size ===
|
||||
|
||||
thread_local! {
|
||||
static TEXTURE_SIZE: Cell<Vector2<i32>> = default();
|
||||
}
|
||||
|
||||
/// Read the texture size.
|
||||
///
|
||||
/// The texture size is initialized as a part of the [`World`](crate::display::world::World)
|
||||
/// initialization. See [`arrange_shapes_on_texture`] for texture initialization details.
|
||||
pub fn texture_size() -> Vector2<i32> {
|
||||
TEXTURE_SIZE.get()
|
||||
}
|
||||
|
||||
|
||||
// === initialize_cached_shape_positions_in_texture ===
|
||||
|
||||
/// Initialize texture size and cached shape positions.
|
||||
///
|
||||
/// Is done as part of the [`World`](crate::display::world::World) initialization. As a result,
|
||||
/// every shape's [`get_position_in_texture`](CachedShape::get_position_in_texture) and
|
||||
/// [`location_in_texture`](CachedShape::location_in_texture), as well as [`texture_size`] will
|
||||
/// return a proper result.
|
||||
///
|
||||
/// See [`arrange_shapes_on_texture`] for packing algorithm details.
|
||||
pub fn initialize_cached_shape_positions_in_texture() {
|
||||
CACHED_SHAPES_DEFINITIONS.with_borrow(|shape_defs| {
|
||||
let with_sizes = shape_defs.iter().map(|shape| {
|
||||
let size = shape.size;
|
||||
ShapeWithSize { shape, size }
|
||||
});
|
||||
let arranged = arrange_shapes_on_texture(with_sizes, INITIAL_TEXTURE_SIZE);
|
||||
TEXTURE_SIZE.set(arranged.texture_size);
|
||||
for ShapeWithPosition { shape, position } in arranged.shape_positions {
|
||||
(shape.position_on_texture_setter)(position)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
// === CachedShape ===
|
||||
// ===================
|
||||
|
||||
/// A cached shape system definition. You do not need to implement it manually, use the
|
||||
/// [`cached_shape!`] macro instead.
|
||||
pub trait CachedShape: shape::system::Shape {
|
||||
/// The declared width of the cached shape.
|
||||
const WIDTH: f32;
|
||||
/// The height of the cached shape in texture.
|
||||
const HEIGHT: f32;
|
||||
/// The width of the space taken by the cached shape on the texture.
|
||||
const TEX_WIDTH: f32 = Self::WIDTH + CACHED_TEXTURE_MAX_DISTANCE * 2.0;
|
||||
/// The height of the space taken by the cached shape on the texture.
|
||||
const TEX_HEIGHT: f32 = Self::HEIGHT + CACHED_TEXTURE_MAX_DISTANCE * 2.0;
|
||||
|
||||
/// Set the position of the shape in the texture.
|
||||
///
|
||||
/// Setting the proper position is done as a part of
|
||||
/// [texture initialization](initialize_cached_shape_positions_in_texture).
|
||||
fn set_position_in_texture(position: Vector2);
|
||||
|
||||
/// Read the position of the shape in the texture.
|
||||
fn get_position_in_texture() -> Vector2;
|
||||
|
||||
/// Create view which will have position and all other properties set, so it will be ready to
|
||||
/// being render to cached shapes texture.
|
||||
fn create_view_for_texture() -> ShapeView<Self>
|
||||
where Self::ShapeData: Default {
|
||||
let shape = ShapeView::<Self>::new();
|
||||
shape.set_size(Vector2(Self::TEX_WIDTH, Self::TEX_HEIGHT));
|
||||
shape.set_xy(Self::get_position_in_texture());
|
||||
shape
|
||||
}
|
||||
|
||||
/// Return the bounding box of the shape in the texture.
|
||||
fn location_in_texture() -> BoundingBox {
|
||||
let position = Self::get_position_in_texture();
|
||||
let left = position.x - Self::TEX_WIDTH / 2.0;
|
||||
let right = position.x + Self::TEX_WIDTH / 2.0;
|
||||
let bottom = position.y - Self::TEX_HEIGHT / 2.0;
|
||||
let top = position.y + Self::TEX_HEIGHT / 2.0;
|
||||
BoundingBox::from_corners(Vector2(left, bottom), Vector2(right, top))
|
||||
}
|
||||
|
||||
/// Return the parameter for [`AnyCachedShape`] pointing to this specific shape.
|
||||
fn any_cached_shape_parameter() -> Vector4 {
|
||||
let bbox = Self::location_in_texture();
|
||||
Vector4(bbox.left(), bbox.bottom(), bbox.right(), bbox.top())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ======================
|
||||
// === AnyCachedShape ===
|
||||
// ======================
|
||||
|
||||
/// The mutable part of [`AnyCachedShape`].
|
||||
pub mod mutable {
|
||||
use super::*;
|
||||
|
||||
/// The [`AnyCachedShape`] definition.
|
||||
#[derive(Debug)]
|
||||
pub struct AnyCachedShape {
|
||||
/// A bounding box locating the cached shape on the texture. `xy` is a left-bottom corner,
|
||||
/// and `zw` is a top-right corner.
|
||||
pub tex_location: Var<Vector4>,
|
||||
}
|
||||
}
|
||||
|
||||
/// One of the cached shapes represented as SDF shape.
|
||||
///
|
||||
/// This structure has the same API as other SDF shapes like [`Rect`](shape::primitive::def::Rect)
|
||||
/// or [`Circle`](shape::primitive::def::Circle). Internally it reads from `pass_cached_shapes`
|
||||
/// texture the SDF and proper color.
|
||||
///
|
||||
/// # Limitations
|
||||
///
|
||||
/// As the SDF and colors are aliased in the texture, the displayed AnyShape may be not as good
|
||||
/// as rendering the shape directly. To have the ideal results, the shape cannot be scaled, zoomed
|
||||
/// in or out, and it should be aligned with pixels. Otherwise the edges of the shape and different
|
||||
/// color joints will be blurred.
|
||||
///
|
||||
/// # As Shape Parameter
|
||||
///
|
||||
/// [`AnyCachedShape`] may be a valid parameter for a different shape defined with [`shape!`] macro.
|
||||
/// You can then set the concrete shape using [`CachedShape::any_cached_shape_parameter`] as in the
|
||||
/// following example:
|
||||
///
|
||||
///
|
||||
/// The parameter is passed to glsl as a single `vec4` describing the location of the cached shape
|
||||
/// in the texture.
|
||||
pub type AnyCachedShape = ShapeRef<mutable::AnyCachedShape>;
|
||||
|
||||
/// Create [`AnyCachedShape`] instance displaying given shape
|
||||
#[allow(non_snake_case)]
|
||||
pub fn AnyCachedShape<T: CachedShape>() -> AnyCachedShape {
|
||||
ShapeRef::new(mutable::AnyCachedShape { tex_location: T::any_cached_shape_parameter().into() })
|
||||
}
|
||||
|
||||
impl AsOwned for AnyCachedShape {
|
||||
type Owned = AnyCachedShape;
|
||||
}
|
||||
|
||||
impl canvas::Draw for AnyCachedShape {
|
||||
fn draw(&self, canvas: &mut Canvas) -> canvas::Shape {
|
||||
canvas.if_not_defined(self.id(), |canvas| {
|
||||
canvas.new_shape_from_expr(&format!(
|
||||
"return cached_shape(Id({}), position, {});",
|
||||
self.id(),
|
||||
self.tex_location.glsl()
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyCachedShape> for AnyShape {
|
||||
fn from(value: AnyCachedShape) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AnyCachedShape> for AnyShape {
|
||||
fn from(value: &AnyCachedShape) -> Self {
|
||||
Self::new(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Parameter for AnyCachedShape {
|
||||
type GpuType = Vector4;
|
||||
type Variable = AnyCachedShape;
|
||||
|
||||
fn create_var(name: &str) -> Self::Variable {
|
||||
ShapeRef::new(mutable::AnyCachedShape { tex_location: name.into() })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============================
|
||||
// === `cached_shape!` Macro ===
|
||||
// =============================
|
||||
|
||||
|
||||
/// Defines a new cached shape system.
|
||||
///
|
||||
/// The outcome is the same as for [`shape!`] macro, but quickly after application start the shape
|
||||
/// will be rendered to the special "cached shapes" texture. The cached shapes may be then used
|
||||
/// efficiently with [`AnyCachedShape`] structure. See [the module documentation](self) for the
|
||||
/// usage overview and examples.
|
||||
///
|
||||
/// Because shape, once cached, is not redrawn, we don't allow for any parameterization except
|
||||
/// styles.
|
||||
#[macro_export]
|
||||
macro_rules! cached_shape {
|
||||
(
|
||||
$width:literal x $height:literal;
|
||||
$(type SystemData = $system_data:ident;)?
|
||||
$(type ShapeData = $shape_data:ident;)?
|
||||
$(flavor = $flavor:path;)?
|
||||
$(above = [$($always_above_1:tt $(::$always_above_2:tt)*),*];)?
|
||||
$(below = [$($always_below_1:tt $(::$always_below_2:tt)*),*];)?
|
||||
$(pointer_events = $pointer_events:tt;)?
|
||||
($style:ident : Style) {$($body:tt)*}
|
||||
) => {
|
||||
$crate::_shape! {
|
||||
$(SystemData($system_data))?
|
||||
$(ShapeData($shape_data))?
|
||||
$(flavor = [$flavor];)?
|
||||
$(above = [$($always_above_1 $(::$always_above_2)*),*];)?
|
||||
$(below = [$($always_below_1 $(::$always_below_2)*),*];)?
|
||||
$(pointer_events = $pointer_events;)?
|
||||
[$style] (){$($body)*}
|
||||
}
|
||||
|
||||
pub mod cached_shape_system_definition {
|
||||
use $crate::prelude::*;
|
||||
use super::shape_system_definition::Shape;
|
||||
use $crate::display::shape::primitive::system::cached::CachedShape;
|
||||
|
||||
thread_local! {
|
||||
static POSITION_IN_TEXTURE: Cell<Vector2> = default();
|
||||
}
|
||||
|
||||
impl CachedShape for Shape {
|
||||
const WIDTH: f32 = $width as f32;
|
||||
const HEIGHT: f32 = $height as f32;
|
||||
|
||||
fn set_position_in_texture(position: Vector2) {
|
||||
POSITION_IN_TEXTURE.with(|pos| pos.set(position))
|
||||
}
|
||||
fn get_position_in_texture() -> Vector2 {
|
||||
POSITION_IN_TEXTURE.with(|pos| pos.get())
|
||||
}
|
||||
}
|
||||
|
||||
#[before_main]
|
||||
pub fn register_cached_shape_entry_point() {
|
||||
register_cached_shape()
|
||||
}
|
||||
|
||||
pub fn register_cached_shape() {
|
||||
$crate::display::world::CACHED_SHAPES_DEFINITIONS.with(|shapes| {
|
||||
let mut shapes = shapes.borrow_mut();
|
||||
shapes.push($crate::display::world::CachedShapeDefinition {
|
||||
for_texture_constructor: Box::new(|| Box::new(Shape::create_view_for_texture())),
|
||||
position_on_texture_setter: Box::new(Shape::set_position_in_texture),
|
||||
size: Vector2(Shape::TEX_WIDTH, Shape::TEX_HEIGHT),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============
|
||||
// === Test ===
|
||||
// ============
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::display::object::layout;
|
||||
use crate::display::world::World;
|
||||
|
||||
mod shape1 {
|
||||
use super::*;
|
||||
cached_shape! { 240 x 240;
|
||||
(_style: Style) {
|
||||
Plane().into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod shape2 {
|
||||
use super::*;
|
||||
cached_shape! { 240 x 112;
|
||||
(_style: Style) {
|
||||
Plane().into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod shape3 {
|
||||
use super::*;
|
||||
cached_shape! { 112 x 114;
|
||||
(_style: Style) {
|
||||
Plane().into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_shapes_initialization_and_texture_location() {
|
||||
shape1::cached_shape_system_definition::register_cached_shape();
|
||||
shape2::cached_shape_system_definition::register_cached_shape();
|
||||
shape3::cached_shape_system_definition::register_cached_shape();
|
||||
let _world = World::new();
|
||||
|
||||
// Those sizes includes margins for sdf. The margins are of [`CACHED_TEXTURE_MAX_DISTANCE`]
|
||||
// size.
|
||||
let tex_width = 512.0;
|
||||
let tex_height = 258.0;
|
||||
assert_eq!(texture_size(), Vector2(tex_width as i32, tex_height as i32));
|
||||
|
||||
assert_eq!(shape1::Shape::get_position_in_texture(), Vector2(-128.0, -1.0));
|
||||
assert_eq!(shape2::Shape::get_position_in_texture(), Vector2(128.0, -65.0));
|
||||
assert_eq!(shape3::Shape::get_position_in_texture(), Vector2(64.0, 64.0));
|
||||
|
||||
assert_eq!(shape1::Shape::location_in_texture(), ((-256.0, -129.0), (0.0, 127.0)).into());
|
||||
assert_eq!(shape2::Shape::location_in_texture(), ((0.0, -129.0), (256.0, -1.0)).into());
|
||||
assert_eq!(shape3::Shape::location_in_texture(), ((0.0, -1.0), (128.0, 129.0)).into());
|
||||
|
||||
let view = shape1::Shape::create_view_for_texture();
|
||||
assert_eq!(view.xy(), Vector2(-128.0, -1.0));
|
||||
assert_eq!(
|
||||
view.size(),
|
||||
Vector2(
|
||||
layout::Size::Fixed(layout::Unit::Pixels(256.0)),
|
||||
layout::Size::Fixed(layout::Unit::Pixels(256.0))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,309 @@
|
||||
//! A module containing the cached shapes pack algorithm.
|
||||
//!
|
||||
//! The main entry point of the algorithm is [`arrange_shapes_on_texture`] function.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::data::bounding_box::BoundingBox;
|
||||
|
||||
use itertools::iproduct;
|
||||
use ordered_float::OrderedFloat;
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/// A parameter of [`arrange_shapes_on_texture`] algorithm: the factor how much the texture size is
|
||||
/// increased when the current size is insufficient.
|
||||
const TEXTURE_SIZE_MULTIPLIER: i32 = 2;
|
||||
|
||||
|
||||
|
||||
// =========================
|
||||
// === Helper Structures ===
|
||||
// =========================
|
||||
|
||||
// === ShapeWithSize ===
|
||||
|
||||
/// A shape paired with information about its size in the texture.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct ShapeWithSize<Shape> {
|
||||
pub shape: Shape,
|
||||
pub size: Vector2,
|
||||
}
|
||||
|
||||
|
||||
// === ShapeWithPosition ===
|
||||
|
||||
/// A shape paired with information about its position in the texture.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct ShapeWithPosition<Shape> {
|
||||
pub shape: Shape,
|
||||
pub position: Vector2,
|
||||
}
|
||||
|
||||
impl<Shape> ShapeWithPosition<Shape> {
|
||||
/// Change the shapes' position
|
||||
pub fn map_position(self, f: impl FnOnce(Vector2) -> Vector2) -> Self {
|
||||
Self { shape: self.shape, position: f(self.position) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === ArrangedShapes ===
|
||||
|
||||
/// The results of [`arrange_shapes_on_texture`] function.
|
||||
#[derive(Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct ArrangedShapes<Shape> {
|
||||
pub texture_size: Vector2<i32>,
|
||||
pub shape_positions: Vec<ShapeWithPosition<Shape>>,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === The Logic ===
|
||||
// =================
|
||||
|
||||
/// Arrange Cached Shapes on texture.
|
||||
///
|
||||
/// The function takes the definition of cached shapes and returns the created
|
||||
/// [shape views](AnyShapeView) with positions and sizes already set to proper place in the texture.
|
||||
///
|
||||
/// # The Texture Pack Algorithm
|
||||
///
|
||||
/// The algorithm is a simple heuristic: we sort shapes from the biggest to the smallest, and take
|
||||
/// the shapes one by one by that order and scans possible left-bottom corner positions, starting
|
||||
/// from left-bottom corner of the texture and going row by row, from left to right and from bottom
|
||||
/// to top, picking the first position where the current shape does not collide with any already
|
||||
/// placed one. According to
|
||||
/// https://gamedev.stackexchange.com/questions/2829/texture-packing-algorithm this is actually one
|
||||
/// of the best packing algorithm in terms of texture space.
|
||||
///
|
||||
/// As the above algorithm assumes some texture size, we starts with predefined square texture with
|
||||
/// [`INITIAL_TEXTURE_SIZE`] and - if the shapes does not fit it - we repeatedly increase the size
|
||||
/// and try again.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// Assuming initial texture size 5x5 and having the following shapes:
|
||||
///
|
||||
/// ```text
|
||||
/// ┌───┐ ┌───┐ ┌───────┐ ┌───┐ ┌───────────┐
|
||||
/// │1x1│ │1x1│ │2x2 │ │1x2│ │3x3 │
|
||||
/// └───┘ └───┘ │ │ │ │ │ │
|
||||
/// │ │ │ │ │ │
|
||||
/// └───────┘ └───┘ │ │
|
||||
/// │ │
|
||||
/// └───────────┘
|
||||
/// ```
|
||||
///
|
||||
/// this function will arrange it this way:
|
||||
/// ```text
|
||||
/// ┌───────────────────┐
|
||||
/// │ │
|
||||
/// ├───┐ ┌───┐ │
|
||||
/// │1x1│ │1x2│ │
|
||||
/// ├───┴───────┤ ├───┤
|
||||
/// │3x3 │ │1x1│
|
||||
/// │ ├───┴───┤
|
||||
/// │ │2x2 │
|
||||
/// │ │ │
|
||||
/// └───────────┴───────┘
|
||||
/// ```
|
||||
/// And return the texture size 5x4.
|
||||
///
|
||||
/// # Implementation
|
||||
///
|
||||
/// As the new shape is always "glued" to
|
||||
/// - right boundary of another shape or left boundary of the texture
|
||||
/// - top boundary of another shape or bottom boundary of the texture
|
||||
/// We do not need to iterate over every row and column, only over those containing the
|
||||
/// boundaries.
|
||||
///
|
||||
/// Therefore, this simple implementation has the time complexity of O(n^3) where n is number of
|
||||
/// placed shapes.
|
||||
pub fn arrange_shapes_on_texture<Shape>(
|
||||
shapes: impl IntoIterator<Item = ShapeWithSize<Shape>>,
|
||||
initial_texture_size: i32,
|
||||
) -> ArrangedShapes<Shape>
|
||||
where
|
||||
Shape: Clone,
|
||||
{
|
||||
let sorted_shapes = shapes
|
||||
.into_iter()
|
||||
.sorted_by_key(|shape| OrderedFloat(shape.size.x * shape.size.y))
|
||||
.rev()
|
||||
.collect_vec();
|
||||
let mut tex_size = initial_texture_size;
|
||||
loop {
|
||||
if let Some(arranged) = try_to_fit_shapes_on_texture(&sorted_shapes, tex_size) {
|
||||
break arranged;
|
||||
}
|
||||
tex_size *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === try_to_fit_shapes_on_texture ===
|
||||
|
||||
/// Try to fit shapes on texture assuming maximum texture size. Returns [`None`] if was unable to
|
||||
/// do it.
|
||||
fn try_to_fit_shapes_on_texture<Shape>(
|
||||
sorted_shapes: &[ShapeWithSize<Shape>],
|
||||
max_texture_size: i32,
|
||||
) -> Option<ArrangedShapes<Shape>>
|
||||
where
|
||||
Shape: Clone,
|
||||
{
|
||||
let mut placed_so_far: Vec<BoundingBox> = vec![];
|
||||
// First, we compute the shapes position with origin placed in left-bottom corner. This way
|
||||
// we don't need to know the eventual texture size upfront.
|
||||
let shapes_with_pos_left_bottom_origin_iter =
|
||||
sorted_shapes.iter().map(|ShapeWithSize { shape, size }| {
|
||||
find_free_place(*size, max_texture_size, &placed_so_far).map(|bbox| {
|
||||
placed_so_far.push(bbox);
|
||||
ShapeWithPosition { shape: shape.clone(), position: bbox.center() }
|
||||
})
|
||||
});
|
||||
// We collect all positions, so `placed_so_far` will contain all shapes and we can compute
|
||||
// texture size.
|
||||
let shapes_with_pos_left_bottom_origin: Option<Vec<_>> =
|
||||
shapes_with_pos_left_bottom_origin_iter.collect();
|
||||
let texture_width =
|
||||
placed_so_far.iter().map(BoundingBox::right).reduce(f32::max).unwrap_or_default();
|
||||
let texture_height =
|
||||
placed_so_far.iter().map(BoundingBox::top).reduce(f32::max).unwrap_or_default();
|
||||
let texture_origin = Vector2(texture_width, texture_height) / 2.0;
|
||||
shapes_with_pos_left_bottom_origin.map(|shapes_with_pos| ArrangedShapes {
|
||||
shape_positions: shapes_with_pos
|
||||
.into_iter()
|
||||
.map(|shape| shape.map_position(|pos| pos - texture_origin))
|
||||
.collect(),
|
||||
texture_size: Vector2(texture_width as i32, texture_height as i32),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// === find_free_place ===
|
||||
|
||||
/// For shape of given size, find the unoccupied space in the square texture.
|
||||
///
|
||||
/// A step of the [`arrange_shapes_on_texture`] algorithm. See its docs for details.
|
||||
fn find_free_place(
|
||||
shape_size: Vector2,
|
||||
tex_size: i32,
|
||||
placed_so_far: &[BoundingBox],
|
||||
) -> Option<BoundingBox> {
|
||||
let tex_size = tex_size as f32;
|
||||
// There is no need of iterating over all columns and rows, as the new shape is always "glued"
|
||||
// to right/top boundary of another shape, or left/bottom boundary of the texture.
|
||||
let right_bounds = placed_so_far.iter().map(|bbox| bbox.right().ceil());
|
||||
let allowed_right_bounds = right_bounds.filter(|col| col + shape_size.x <= tex_size);
|
||||
let top_bounds = placed_so_far.iter().map(|bbox| bbox.top().ceil());
|
||||
let allowed_top_bounds = top_bounds.filter(|row| row + shape_size.y <= tex_size);
|
||||
let candidate_rows =
|
||||
iter::once(0.0).chain(allowed_top_bounds).sorted_by_key(|&x| OrderedFloat(x));
|
||||
let candidate_cols =
|
||||
iter::once(0.0).chain(allowed_right_bounds).sorted_by_key(|&x| OrderedFloat(x));
|
||||
let candidate_positions = iproduct!(candidate_rows, candidate_cols);
|
||||
let mut candidate_bboxes = candidate_positions
|
||||
.map(|(y, x)| BoundingBox::from_position_and_size(Vector2(x, y), shape_size));
|
||||
candidate_bboxes.find(|bbox| {
|
||||
let is_collision = placed_so_far.iter().any(|placed| placed.interior_intersects(bbox));
|
||||
!is_collision
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Tests ===
|
||||
// =============
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::display::shape::system::cached::INITIAL_TEXTURE_SIZE;
|
||||
|
||||
type MockShape = usize;
|
||||
|
||||
fn shape_entries_from_sizes(
|
||||
sizes: impl IntoIterator<Item = (f32, f32)>,
|
||||
) -> Vec<ShapeWithSize<MockShape>> {
|
||||
sizes
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (width, height))| ShapeWithSize {
|
||||
size: Vector2(width, height),
|
||||
shape: index,
|
||||
})
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn texture_size() {
|
||||
fn run_case(shape_sizes: impl IntoIterator<Item = (f32, f32)>, expected_size: (i32, i32)) {
|
||||
let shape_entries = shape_entries_from_sizes(shape_sizes);
|
||||
let result = arrange_shapes_on_texture(shape_entries.into_iter(), INITIAL_TEXTURE_SIZE);
|
||||
assert_eq!(result.texture_size, IntoVector2::into_vector(expected_size));
|
||||
}
|
||||
|
||||
run_case([(32.0, 32.0), (16.0, 16.0)], (48, 32));
|
||||
run_case([(16.0, 2.0), (2.0, 20.0)], (18, 20));
|
||||
run_case([(256.0, 2.0), (257.0, 2.0)], (257, 4));
|
||||
run_case(iter::repeat((32.0, 32.0)).take(2), (64, 32));
|
||||
run_case(iter::repeat((32.0, 32.0)).take(16), (512, 32));
|
||||
run_case(iter::repeat((32.0, 32.0)).take(17), (512, 64));
|
||||
run_case(iter::repeat((64.0, 64.0)).take(64), (512, 512));
|
||||
// Shapes does not fit initial texture size: the texture is extended.
|
||||
run_case(iter::repeat((64.0, 64.0)).take(65), (1024, 320));
|
||||
// This will extend the texture several times.
|
||||
run_case(iter::repeat((512.0, 512.0)).take(17), (4096, 1536));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fitting_shapes_on_texture() {
|
||||
fn run_case<const N: usize>(
|
||||
tex_size: i32,
|
||||
sizes: [(f32, f32); N],
|
||||
expected_position: Option<[(f32, f32); N]>,
|
||||
) {
|
||||
let shape_entries = shape_entries_from_sizes(sizes);
|
||||
let result = try_to_fit_shapes_on_texture(&shape_entries, tex_size);
|
||||
let positions = result.map(|shapes| {
|
||||
shapes
|
||||
.shape_positions
|
||||
.iter()
|
||||
.map(|shape| (shape.position.x, shape.position.y))
|
||||
.collect_vec()
|
||||
});
|
||||
assert_eq!(positions.as_deref(), expected_position.as_ref().map(|arr| arr.as_slice()));
|
||||
}
|
||||
|
||||
run_case(64, [(32.0, 32.0), (32.0, 32.0)], Some([(-16.0, 0.0), (16.0, 0.0)]));
|
||||
run_case(63, [(32.0, 32.0), (32.0, 32.0)], None);
|
||||
run_case(
|
||||
64,
|
||||
[(32.0, 32.0), (32.0, 32.0), (32.0, 32.0), (32.0, 32.0)],
|
||||
Some([(-16.0, -16.0), (16.0, -16.0), (-16.0, 16.0), (16.0, 16.0)]),
|
||||
);
|
||||
run_case(64, [(32.0, 32.0), (32.0, 32.0), (32.0, 32.0), (2.0, 33.0)], None);
|
||||
run_case(
|
||||
64,
|
||||
[(32.0, 32.0), (16.0, 16.0), (16.0, 16.0), (16.0, 16.0)],
|
||||
Some([(-16.0, 0.0), (8.0, -8.0), (24.0, -8.0), (8.0, 8.0)]),
|
||||
);
|
||||
run_case(
|
||||
32,
|
||||
[(16.0, 8.0), (2.0, 32.0), (16.0, 2.0), (12.0, 2.0)],
|
||||
Some([(-7.0, -12.0), (2.0, 0.0), (-7.0, -7.0), (9.0, -15.0)]),
|
||||
);
|
||||
}
|
||||
}
|
@ -89,6 +89,7 @@ pub struct SymbolRegistry {
|
||||
pub dirty: Dirty,
|
||||
view_projection: Uniform<Matrix4<f32>>,
|
||||
z_zoom_1: Uniform<f32>,
|
||||
pub display_mode: Uniform<i32>,
|
||||
pub variables: UniformScope,
|
||||
context: Rc<RefCell<Option<Context>>>,
|
||||
pub stats: Stats,
|
||||
@ -108,6 +109,7 @@ impl SymbolRegistry {
|
||||
let variables = UniformScope::new();
|
||||
let view_projection = variables.add_or_panic("view_projection", Matrix4::<f32>::identity());
|
||||
let z_zoom_1 = variables.add_or_panic("z_zoom_1", 1.0);
|
||||
let display_mode = variables.add_or_panic("display_mode", 0);
|
||||
let context = default();
|
||||
let stats = debug::stats::Stats::new(web::window.performance_or_panic());
|
||||
let global_id_provider = default();
|
||||
@ -122,6 +124,7 @@ impl SymbolRegistry {
|
||||
dirty,
|
||||
view_projection,
|
||||
z_zoom_1,
|
||||
display_mode,
|
||||
variables,
|
||||
context,
|
||||
stats,
|
||||
|
@ -123,9 +123,12 @@ impl {
|
||||
/// particular mesh and thus this code can be used for optimization purposes only.
|
||||
pub fn abstract_shader_code_in_glsl_310(&self) -> crate::system::gpu::shader::Code {
|
||||
let bindings = self.collect_variables().map(|(name, decl)| {
|
||||
let instance = crate::display::symbol::geometry::primitive::mesh::ScopeType::Instance;
|
||||
let scope = Some(ScopeType::Mesh(instance));
|
||||
VarBinding::new(name, decl, scope)
|
||||
let scope = if decl.tp.uniform_or_function_parameter_only() {
|
||||
ScopeType::Global
|
||||
} else {
|
||||
ScopeType::Mesh(crate::display::symbol::geometry::primitive::mesh::ScopeType::Instance)
|
||||
};
|
||||
VarBinding::new(name, decl, Some(scope))
|
||||
}).collect_vec();
|
||||
self.gen_gpu_code(glsl::Version::V310, &bindings)
|
||||
}
|
||||
|
@ -55,8 +55,8 @@ thread_local! {
|
||||
}
|
||||
|
||||
/// Perform an action with a reference to the global context.
|
||||
pub fn with_context<T>(f: impl Fn(&SymbolRegistry) -> T) -> T {
|
||||
CONTEXT.with_borrow(|t| f(t.as_ref().unwrap()))
|
||||
pub fn with_context<T>(f: impl FnOnce(&SymbolRegistry) -> T) -> T {
|
||||
CONTEXT.with_borrow(move |t| f(t.as_ref().unwrap()))
|
||||
}
|
||||
|
||||
/// Initialize global state (set stack trace size, logger output, etc).
|
||||
@ -110,11 +110,18 @@ impl ShapeDefinition {
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug)]
|
||||
pub struct CachedShapeDefinition {
|
||||
/// The size of the shape in the texture.
|
||||
pub size: Vector2<i32>,
|
||||
/// A constructor of single shape view.
|
||||
/// The size of the texture's space occupied by the shape.
|
||||
pub size: Vector2,
|
||||
/// A pointer to function setting the global information about position in the cached shapes
|
||||
/// texture, usually a concrete implementation of
|
||||
/// [`set_position_in_texture`](crate::display::shape::system::cached::CachedShape::set_position_in_texture)
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub cons: ShapeCons,
|
||||
pub position_on_texture_setter: Box<dyn Fn(Vector2)>,
|
||||
/// A pointer to function creating a shape instance properly placed for cached texture
|
||||
/// rendering, usually a concrete implementation of
|
||||
/// [`create_view_for_texture`](crate::display::shape::system::cached::CachedShape::create_view_for_texture)
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub for_texture_constructor: ShapeCons,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@ -251,16 +258,14 @@ fn gather_shaders() -> HashMap<&'static str, shader::Code> {
|
||||
/// Uniforms managed by world.
|
||||
#[derive(Clone, CloneRef, Debug)]
|
||||
pub struct Uniforms {
|
||||
time: Uniform<f32>,
|
||||
display_mode: Uniform<i32>,
|
||||
time: Uniform<f32>,
|
||||
}
|
||||
|
||||
impl Uniforms {
|
||||
/// Constructor.
|
||||
pub fn new(scope: &UniformScope) -> Self {
|
||||
let time = scope.add_or_panic("time", 0.0);
|
||||
let display_mode = scope.add_or_panic("display_mode", 0);
|
||||
Self { time, display_mode }
|
||||
Self { time }
|
||||
}
|
||||
}
|
||||
|
||||
@ -367,6 +372,7 @@ impl WorldDataWithLoop {
|
||||
// Context is initialized automatically before main entry point starts in WASM. We are
|
||||
// performing manual initialization for native tests to work correctly.
|
||||
init_context();
|
||||
display::shape::primitive::system::cached::initialize_cached_shape_positions_in_texture();
|
||||
let frp = Frp::new();
|
||||
let data = WorldData::new(&frp.private.output);
|
||||
let on_frame_start = animation::on_frame_start();
|
||||
@ -508,7 +514,7 @@ impl WorldData {
|
||||
fn init_debug_hotkeys(&self) {
|
||||
let stats_monitor = self.stats_monitor.clone_ref();
|
||||
let display_mode = self.display_mode.clone_ref();
|
||||
let display_mode_uniform = self.uniforms.display_mode.clone_ref();
|
||||
let display_mode_uniform = with_context(|ctx| ctx.display_mode.clone_ref());
|
||||
let emit_measurements_handle = self.emit_measurements_handle.clone_ref();
|
||||
let closure: Closure<dyn Fn(JsValue)> = Closure::new(move |val: JsValue| {
|
||||
let event = val.unchecked_into::<web::KeyboardEvent>();
|
||||
|
@ -152,7 +152,7 @@ pub mod shape {
|
||||
let width = &width - &press_diff * 2.0 - &sides_padding;
|
||||
let height = &height - &press_diff * 2.0 - &sides_padding;
|
||||
let cursor = Rect((width,height)).corners_radius(radius);
|
||||
let cursor = cursor.fill("srgba(input_color)");
|
||||
let cursor = cursor.fill(color);
|
||||
cursor.into()
|
||||
}
|
||||
}
|
||||
|
@ -644,6 +644,29 @@ define_prim_type! {
|
||||
USampler2dArray { name = "usampler2DArray", layout_size = 0 },
|
||||
}
|
||||
|
||||
impl PrimType {
|
||||
pub fn uniform_or_function_parameter_only(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
PrimType::Sampler2d
|
||||
| PrimType::Sampler3d
|
||||
| PrimType::SamplerCube
|
||||
| PrimType::Sampler2dShadow
|
||||
| PrimType::SamplerCubeShadow
|
||||
| PrimType::Sampler2dArray
|
||||
| PrimType::Sampler2dArrayShadow
|
||||
| PrimType::ISampler2d
|
||||
| PrimType::ISampler3d
|
||||
| PrimType::ISamplerCube
|
||||
| PrimType::ISampler2dArray
|
||||
| PrimType::USampler2d
|
||||
| PrimType::USampler3d
|
||||
| PrimType::USamplerCube
|
||||
| PrimType::USampler2dArray
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasCodeRepr for PrimType {
|
||||
fn build(&self, builder: &mut CodeBuilder) {
|
||||
builder.add(self.glsl_name());
|
||||
|
@ -32,7 +32,7 @@ mod icon1 {
|
||||
|
||||
mod icon2 {
|
||||
use super::*;
|
||||
ensogl_core::cached_shape! { 202 x 312;
|
||||
ensogl_core::cached_shape! { 200 x 310;
|
||||
(_style: Style) {
|
||||
let shape = Rect((200.px(), 310.px())).fill(color::Rgba::red());
|
||||
shape.into()
|
||||
@ -40,6 +40,60 @@ mod icon2 {
|
||||
}
|
||||
}
|
||||
|
||||
/// A rounded rectangle with an arrow pointing in from the left.
|
||||
pub mod data_input {
|
||||
use super::*;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
/// An arrow shape consisting of a straight line and a triangular head. The arrow points upwards
|
||||
/// and the tip is positioned at the origin.
|
||||
pub fn arrow(length: f32, width: f32, head_length: f32, head_width: f32) -> AnyShape {
|
||||
// We overlap the line with the head by this amount to make sure that the renderer does not
|
||||
// display a gap between them.
|
||||
const OVERLAP: f32 = 1.0;
|
||||
let line_length = length - head_length + OVERLAP;
|
||||
let line = Rect((width.px(), line_length.px()));
|
||||
let line = line.translate_y((-line_length / 2.0 - head_length + OVERLAP).px());
|
||||
let head = Triangle(head_width, head_length).translate_y((-head_length / 2.0).px());
|
||||
(line + head).into()
|
||||
}
|
||||
|
||||
ensogl_core::cached_shape! { 16 x 16;
|
||||
(style: Style) {
|
||||
let vivid_color: Var<color::Rgba> = "srgba(1.0, 0.0, 0.0, 1.0)".into();
|
||||
let dull_color: Var<color::Rgba> = "srgba(0.0, 1.0, 0.0, 1.0)".into();
|
||||
|
||||
// === Rectangle ===
|
||||
|
||||
let rect = Rect((11.0.px(),12.0.px())).corners_radius(2.0.px());
|
||||
let rect = rect.translate_x(2.5.px());
|
||||
let rect = rect.fill(dull_color);
|
||||
|
||||
|
||||
// === Arrow ===
|
||||
|
||||
let arrow = arrow(11.0,2.0,4.0,6.0).rotate((PI/2.0).radians());
|
||||
let arrow = arrow.translate_x(4.0.px());
|
||||
let arrow = arrow.fill(vivid_color);
|
||||
|
||||
|
||||
// === Shape ===
|
||||
|
||||
(rect + arrow).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod shape {
|
||||
use super::*;
|
||||
ensogl_core::shape! {
|
||||
(_style: Style, shape: cached::AnyCachedShape) {
|
||||
let bg = Rect((100.px(), 100.px())).fill(color::Rgba::white());
|
||||
let with_bg = &bg + &shape;
|
||||
with_bg.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ======================
|
||||
@ -55,7 +109,7 @@ pub struct TextureSystemData {}
|
||||
impl TextureSystemData {
|
||||
fn material() -> Material {
|
||||
let mut material = Material::new();
|
||||
let shader = "output_color = texture(input_pass_cached_shapes,input_uv); output_id=vec4(0.0,0.0,0.0,0.0);";
|
||||
let shader = "output_color = texture(input_pass_cached_shapes,input_uv); output_color.rgb *= output_color.a; output_id=vec4(0.0,0.0,0.0,0.0);";
|
||||
material.add_input_def::<FloatSampler>("pass_cached_shapes");
|
||||
material.add_output("id", Vector4::<f32>::new(0.0, 0.0, 0.0, 0.0));
|
||||
material.set_main(shader);
|
||||
@ -79,7 +133,7 @@ mod background {
|
||||
ensogl_core::shape! {
|
||||
below = [texture, icon1, icon2];
|
||||
(style: Style,) {
|
||||
Rect((1024.0.px(), 1024.0.px())).fill(color::Rgba::white()).into()
|
||||
Rect((296.0.px(), 326.0.px())).fill(color::Rgba::black()).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -109,19 +163,54 @@ pub fn main() {
|
||||
let navigator = Navigator::new(scene, &camera);
|
||||
|
||||
let background = background::View::new();
|
||||
background.set_size(Vector2(1024.0, 1024.0));
|
||||
background.set_xy(Vector2(100.0, 0.0));
|
||||
background.set_size(Vector2(296.0, 326.0));
|
||||
background.set_xy(Vector2(0.0, -1000.0));
|
||||
world.default_scene.add_child(&background);
|
||||
world.default_scene.layers.main.add(&background);
|
||||
|
||||
let texture_preview = texture::View::new();
|
||||
texture_preview.set_size(Vector2(1024.0, 1024.0));
|
||||
texture_preview.set_xy(Vector2(100.0, 0.0));
|
||||
texture_preview.set_size(Vector2(296.0, 326.0));
|
||||
texture_preview.set_xy(Vector2(0.0, -1000.0));
|
||||
world.default_scene.add_child(&texture_preview);
|
||||
world.default_scene.layers.main.add(&texture_preview);
|
||||
|
||||
let shapes = [shape::View::new(), shape::View::new(), shape::View::new()];
|
||||
for shape in &shapes {
|
||||
shape.set_size(Vector2(100.0, 100.0));
|
||||
world.default_scene.add_child(shape);
|
||||
world.default_scene.layers.main.add(shape);
|
||||
}
|
||||
shapes[0].set_xy((-60.0, 0.0));
|
||||
shapes[0].shape.set(icon1::Shape::any_cached_shape_parameter());
|
||||
shapes[1].set_xy((60.0, 0.0));
|
||||
shapes[1].shape.set(icon2::Shape::any_cached_shape_parameter());
|
||||
shapes[2].set_xy((180.0, 0.0));
|
||||
shapes[2].shape.set(data_input::Shape::any_cached_shape_parameter());
|
||||
|
||||
let icon1 = icon1::View::new();
|
||||
icon1.set_size(Vector2(100.0, 100.0));
|
||||
icon1.set_xy((-60.0, -120.0));
|
||||
world.default_scene.add_child(&icon1);
|
||||
world.default_scene.layers.main.add(&icon1);
|
||||
|
||||
let icon2 = icon2::View::new();
|
||||
icon2.set_size(Vector2(100.0, 100.0));
|
||||
icon2.set_xy((60.0, -120.0));
|
||||
world.default_scene.add_child(&icon2);
|
||||
world.default_scene.layers.main.add(&icon2);
|
||||
|
||||
let icon3 = data_input::View::new();
|
||||
icon3.set_size(Vector2(100.0, 100.0));
|
||||
icon3.set_xy((180.0, -120.0));
|
||||
world.default_scene.add_child(&icon3);
|
||||
world.default_scene.layers.main.add(&icon3);
|
||||
|
||||
world.keep_alive_forever();
|
||||
mem::forget(navigator);
|
||||
mem::forget(background);
|
||||
mem::forget(texture_preview);
|
||||
mem::forget(shapes);
|
||||
mem::forget(icon1);
|
||||
mem::forget(icon2);
|
||||
mem::forget(icon3);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user