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:
Adam Obuchowicz 2023-02-23 12:18:48 +01:00 committed by GitHub
parent 3a09ee88f6
commit 625172a6d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1147 additions and 399 deletions

1
Cargo.lock generated
View File

@ -2622,6 +2622,7 @@ dependencies = [
"nalgebra",
"num-traits",
"num_enum",
"ordered-float",
"rustc-hash",
"semver 1.0.16",
"serde",

View File

@ -71,7 +71,8 @@ enum RelativePosition {
// === Background ===
// ==================
mod background {
/// A background shape.
pub mod background {
use super::*;
ensogl::shape! {

View File

@ -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;
}

View File

@ -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,

View File

@ -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" }

View File

@ -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,

View File

@ -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)]),
// );
}
}

View File

@ -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,

View File

@ -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();

View File

@ -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);
}
// =================

View File

@ -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());
[

View File

@ -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));")
})
}

View File

@ -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 });
});
}
}
};
}

View File

@ -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))
)
);
}
}

View File

@ -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)]),
);
}
}

View File

@ -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,

View File

@ -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)
}

View File

@ -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>();

View File

@ -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()
}
}

View File

@ -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());

View File

@ -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);
}