0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-28 09:46:51 -05:00

Merge pull request #5930 from penpot/ladybenko-10321-split-fill-strokes

 Split rendering of fills and strokes in separate surfaces
This commit is contained in:
Alejandro 2025-02-26 11:57:37 +01:00 committed by GitHub
commit 0eaa43f36b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 241 additions and 181 deletions

View file

@ -19,7 +19,7 @@ use crate::shapes::{Corners, Shape, Type};
use cache::CachedSurfaceImage;
use gpu_state::GpuState;
use options::RenderOptions;
use surfaces::Surfaces;
use surfaces::{SurfaceId, Surfaces};
pub use blend::BlendMode;
pub use images::*;
@ -56,7 +56,6 @@ pub(crate) struct RenderState {
gpu_state: GpuState,
pub options: RenderOptions,
pub surfaces: Surfaces,
pub sampling_options: skia::SamplingOptions,
pub font_provider: skia::textlayout::TypefaceFontProvider,
pub cached_surface_image: Option<CachedSurfaceImage>,
pub viewbox: Viewbox,
@ -75,7 +74,10 @@ impl RenderState {
pub fn new(width: i32, height: i32) -> RenderState {
// This needs to be done once per WebGL context.
let mut gpu_state = GpuState::new();
let surfaces = Surfaces::new(&mut gpu_state, (width, height));
let sampling_options =
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest);
let surfaces = Surfaces::new(&mut gpu_state, (width, height), sampling_options);
let mut font_provider = skia::textlayout::TypefaceFontProvider::new();
let default_font = skia::FontMgr::default()
.new_from_data(DEFAULT_FONT_BYTES, None)
@ -84,15 +86,12 @@ impl RenderState {
// This is used multiple times everywhere so instead of creating new instances every
// time we reuse this one.
let sampling_options =
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest);
RenderState {
gpu_state,
surfaces,
cached_surface_image: None,
font_provider,
sampling_options,
options: RenderOptions::default(),
viewbox: Viewbox::new(width as f32, height as f32),
images: ImageStore::new(),
@ -150,88 +149,76 @@ impl RenderState {
}
pub fn flush(&mut self) {
self.gpu_state
.context
.flush_and_submit_surface(&mut self.surfaces.target, None);
self.surfaces
.flush_and_submit(&mut self.gpu_state, SurfaceId::Target);
}
pub fn reset_canvas(&mut self) {
self.surfaces.shape.canvas().restore_to_count(1);
self.surfaces.current.canvas().restore_to_count(1);
self.surfaces.canvas(SurfaceId::Fills).restore_to_count(1);
self.surfaces.canvas(SurfaceId::Strokes).restore_to_count(1);
self.surfaces.canvas(SurfaceId::Current).restore_to_count(1);
self.surfaces.apply_mut(
&[
SurfaceId::Fills,
SurfaceId::Strokes,
SurfaceId::Current,
SurfaceId::Shadow,
SurfaceId::Overlay,
],
|s| {
s.canvas().clear(self.background_color).reset_matrix();
},
);
self.surfaces
.shape
.canvas()
.clear(self.background_color)
.reset_matrix();
self.surfaces
.current
.canvas()
.clear(self.background_color)
.reset_matrix();
self.surfaces
.shadow
.canvas()
.clear(self.background_color)
.reset_matrix();
self.surfaces
.overlay
.canvas()
.clear(self.background_color)
.reset_matrix();
self.surfaces
.debug
.canvas()
.canvas(SurfaceId::Debug)
.clear(skia::Color::TRANSPARENT)
.reset_matrix();
}
pub fn apply_render_to_final_canvas(&mut self) {
self.surfaces.current.draw(
&mut self.surfaces.target.canvas(),
(0.0, 0.0),
self.sampling_options,
self.surfaces.draw_into(
SurfaceId::Current,
SurfaceId::Target,
Some(&skia::Paint::default()),
);
}
pub fn apply_drawing_to_render_canvas(&mut self) {
self.gpu_state
.context
.flush_and_submit_surface(&mut self.surfaces.shape, None);
self.surfaces.shape.draw(
&mut self.surfaces.current.canvas(),
(0.0, 0.0),
self.sampling_options,
self.surfaces
.flush_and_submit(&mut self.gpu_state, SurfaceId::Fills);
self.surfaces.draw_into(
SurfaceId::Fills,
SurfaceId::Current,
Some(&skia::Paint::default()),
);
self.surfaces
.flush_and_submit(&mut self.gpu_state, SurfaceId::Strokes);
self.surfaces.draw_into(
SurfaceId::Strokes,
SurfaceId::Current,
Some(&skia::Paint::default()),
);
self.gpu_state
.context
.flush_and_submit_surface(&mut self.surfaces.current, None);
self.surfaces
.flush_and_submit(&mut self.gpu_state, SurfaceId::Current);
self.surfaces
.flush_and_submit(&mut self.gpu_state, SurfaceId::Overlay);
self.surfaces
.draw_into(SurfaceId::Overlay, SurfaceId::Current, None);
self.gpu_state
.context
.flush_and_submit_surface(&mut self.surfaces.overlay, None);
self.surfaces.overlay.draw(
&mut self.surfaces.current.canvas(),
(0.0, 0.0),
self.sampling_options,
None,
self.surfaces.apply_mut(
&[
SurfaceId::Shadow,
SurfaceId::Overlay,
SurfaceId::Fills,
SurfaceId::Strokes,
],
|s| {
s.canvas().clear(skia::Color::TRANSPARENT);
},
);
self.surfaces
.shadow
.canvas()
.clear(skia::Color::TRANSPARENT);
self.surfaces
.overlay
.canvas()
.clear(skia::Color::TRANSPARENT);
self.surfaces.shape.canvas().clear(skia::Color::TRANSPARENT);
}
pub fn invalidate_cache_if_needed(&mut self) {
@ -246,20 +233,27 @@ impl RenderState {
modifiers: Option<&Matrix>,
clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
) {
self.surfaces.shape.canvas().save();
let surface_ids = &[SurfaceId::Fills, SurfaceId::Strokes];
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().save();
});
// set clipping
if let Some((bounds, corners, transform)) = clip_bounds {
self.surfaces.shape.canvas().concat(&transform);
self.surfaces
.apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| {
s.canvas().concat(&transform);
});
if let Some(corners) = corners {
let rrect = RRect::new_rect_radii(bounds, &corners);
self.surfaces
.shape
.canvas()
.clip_rrect(rrect, skia::ClipOp::Intersect, true);
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().clip_rrect(rrect, skia::ClipOp::Intersect, true);
});
} else {
self.surfaces
.shape
.canvas()
.clip_rect(bounds, skia::ClipOp::Intersect, true);
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().clip_rect(bounds, skia::ClipOp::Intersect, true);
});
}
if self.options.is_debug_visible() {
@ -267,13 +261,15 @@ impl RenderState {
paint.set_style(skia::PaintStyle::Stroke);
paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
paint.set_stroke_width(4.);
self.surfaces.shape.canvas().draw_rect(bounds, &paint);
self.surfaces
.canvas(SurfaceId::Fills)
.draw_rect(bounds, &paint);
}
self.surfaces
.shape
.canvas()
.concat(&transform.invert().unwrap_or(Matrix::default()));
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas()
.concat(&transform.invert().unwrap_or(Matrix::default()));
});
}
// Clone so we don't change the value in the global state
@ -284,7 +280,6 @@ impl RenderState {
}
let center = shape.center();
let mut matrix = shape.transform;
matrix.post_translate(center);
matrix.pre_translate(-center);
@ -292,17 +287,17 @@ impl RenderState {
match &shape.shape_type {
Type::SVGRaw(sr) => {
if let Some(modifiers) = modifiers {
self.surfaces.shape.canvas().concat(&modifiers);
self.surfaces.canvas(SurfaceId::Fills).concat(&modifiers);
}
self.surfaces.shape.canvas().concat(&matrix);
self.surfaces.canvas(SurfaceId::Fills).concat(&matrix);
if let Some(svg) = shape.svg.as_ref() {
svg.render(self.surfaces.shape.canvas())
svg.render(self.surfaces.canvas(SurfaceId::Fills))
} else {
let font_manager = skia::FontMgr::from(self.font_provider.clone());
let dom_result = skia::svg::Dom::from_str(sr.content.to_string(), font_manager);
match dom_result {
Ok(dom) => {
dom.render(self.surfaces.shape.canvas());
dom.render(self.surfaces.canvas(SurfaceId::Fills));
shape.set_svg(dom);
}
Err(e) => {
@ -312,7 +307,10 @@ impl RenderState {
}
}
_ => {
self.surfaces.shape.canvas().concat(&matrix);
self.surfaces
.apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| {
s.canvas().concat(&matrix);
});
for fill in shape.fills().rev() {
fills::render(self, &shape, fill);
@ -341,7 +339,10 @@ impl RenderState {
};
self.apply_drawing_to_render_canvas();
self.surfaces.shape.canvas().restore();
self.surfaces
.apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| {
s.canvas().restore();
});
}
pub fn start_render_loop(
@ -350,21 +351,23 @@ impl RenderState {
modifiers: &HashMap<Uuid, Matrix>,
timestamp: i32,
) -> Result<(), String> {
let surface_ids = &[SurfaceId::Fills, SurfaceId::Strokes];
if self.render_in_progress {
if let Some(frame_id) = self.render_request_id {
self.cancel_animation_frame(frame_id);
}
}
self.reset_canvas();
self.surfaces.shape.canvas().scale((
self.viewbox.zoom * self.options.dpr(),
self.viewbox.zoom * self.options.dpr(),
));
self.surfaces
.shape
.canvas()
.translate((self.viewbox.pan_x, self.viewbox.pan_y));
//
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().scale((
self.viewbox.zoom * self.options.dpr(),
self.viewbox.zoom * self.options.dpr(),
));
s.canvas()
.translate((self.viewbox.pan_x, self.viewbox.pan_y));
});
self.pending_nodes = vec![NodeRenderState {
id: Uuid::nil(),
visited_children: false,
@ -423,7 +426,7 @@ impl RenderState {
.map_or(true, |img| img.invalid)
{
self.cached_surface_image = Some(CachedSurfaceImage {
image: self.surfaces.current.image_snapshot(),
image: self.surfaces.snapshot(SurfaceId::Current),
viewbox: self.viewbox,
invalid: false,
has_all_shapes: self.render_complete,
@ -452,29 +455,31 @@ impl RenderState {
let image = &cached.image;
let paint = skia::Paint::default();
self.surfaces.target.canvas().save();
self.surfaces.shape.canvas().save();
self.surfaces.canvas(SurfaceId::Target).save();
self.surfaces.canvas(SurfaceId::Fills).save();
self.surfaces.canvas(SurfaceId::Strokes).save();
let navigate_zoom = self.viewbox.zoom / cached.viewbox.zoom;
let navigate_x = cached.viewbox.zoom * (self.viewbox.pan_x - cached.viewbox.pan_x);
let navigate_y = cached.viewbox.zoom * (self.viewbox.pan_y - cached.viewbox.pan_y);
self.surfaces
.target
.canvas()
.canvas(SurfaceId::Target)
.scale((navigate_zoom, navigate_zoom));
self.surfaces.target.canvas().translate((
self.surfaces.canvas(SurfaceId::Target).translate((
navigate_x * self.options.dpr(),
navigate_y * self.options.dpr(),
));
self.surfaces.target.canvas().clear(self.background_color);
self.surfaces
.target
.canvas()
.canvas(SurfaceId::Target)
.clear(self.background_color);
self.surfaces
.canvas(SurfaceId::Target)
.draw_image(image, (0, 0), Some(&paint));
self.surfaces.target.canvas().restore();
self.surfaces.shape.canvas().restore();
self.surfaces.canvas(SurfaceId::Target).restore();
self.surfaces.canvas(SurfaceId::Fills).restore();
self.surfaces.canvas(SurfaceId::Strokes).restore();
self.flush();
@ -495,7 +500,9 @@ impl RenderState {
if group.masked {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces.current.canvas().save_layer(&layer_rec);
self.surfaces
.canvas(SurfaceId::Current)
.save_layer(&layer_rec);
}
}
_ => {}
@ -512,7 +519,9 @@ impl RenderState {
let mut mask_paint = skia::Paint::default();
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
self.surfaces.current.canvas().save_layer(&mask_rec);
self.surfaces
.canvas(SurfaceId::Current)
.save_layer(&mask_rec);
}
if let Some(image_filter) = element.image_filter(self.viewbox.zoom * self.options.dpr()) {
@ -520,7 +529,9 @@ impl RenderState {
}
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces.current.canvas().save_layer(&layer_rec);
self.surfaces
.canvas(SurfaceId::Current)
.save_layer(&layer_rec);
}
pub fn render_shape_exit(&mut self, element: &mut Shape, visited_mask: bool) {
@ -531,13 +542,13 @@ impl RenderState {
match element.shape_type {
Type::Group(group) => {
if group.masked {
self.surfaces.current.canvas().restore();
self.surfaces.canvas(SurfaceId::Current).restore();
}
}
_ => {}
}
}
self.surfaces.current.canvas().restore();
self.surfaces.canvas(SurfaceId::Current).restore();
}
pub fn render_shape_tree(

View file

@ -1,7 +1,7 @@
use crate::shapes::Shape;
use skia_safe as skia;
use super::RenderState;
use super::{RenderState, SurfaceId};
fn render_debug_view(render_state: &mut RenderState) {
let mut paint = skia::Paint::default();
@ -18,13 +18,12 @@ fn render_debug_view(render_state: &mut RenderState) {
render_state
.surfaces
.debug
.canvas()
.canvas(SurfaceId::Debug)
.draw_rect(scaled_rect, &paint);
}
pub fn render_wasm_label(render_state: &mut RenderState) {
let canvas = render_state.surfaces.current.canvas();
let canvas = render_state.surfaces.canvas(SurfaceId::Current);
let skia::ISize { width, height } = canvas.base_layer_size();
let p = skia::Point::new(width as f32 - 100.0, height as f32 - 25.0);
@ -59,18 +58,15 @@ pub fn render_debug_shape(render_state: &mut RenderState, element: &Shape, inter
render_state
.surfaces
.debug
.canvas()
.canvas(SurfaceId::Debug)
.draw_rect(scaled_rect, &paint);
}
pub fn render(render_state: &mut RenderState) {
let paint = skia::Paint::default();
render_debug_view(render_state);
render_state.surfaces.debug.draw(
&mut render_state.surfaces.current.canvas(),
(0.0, 0.0),
render_state.sampling_options,
Some(&paint),
render_state.surfaces.draw_into(
SurfaceId::Debug,
SurfaceId::Current,
Some(&skia::Paint::default()),
);
}

View file

@ -1,8 +1,8 @@
use crate::shapes::{Fill, ImageFill, Shape, Type};
use skia_safe::{self as skia, RRect};
use super::RenderState;
use super::{RenderState, SurfaceId};
use crate::math::Rect;
use crate::shapes::{Fill, ImageFill, Shape, Type};
fn draw_image_fill_in_container(
render_state: &mut RenderState,
@ -16,7 +16,7 @@ fn draw_image_fill_in_container(
}
let size = image_fill.size();
let canvas = render_state.surfaces.shape.canvas();
let canvas = render_state.surfaces.canvas(SurfaceId::Fills);
let container = &shape.selrect;
let path_transform = shape.to_path_transform();
let paint = fill.to_paint(container);
@ -96,7 +96,7 @@ fn draw_image_fill_in_container(
* This SHOULD be the only public function in this module.
*/
pub fn render(render_state: &mut RenderState, shape: &Shape, fill: &Fill) {
let canvas = render_state.surfaces.shape.canvas();
let canvas = render_state.surfaces.canvas(SurfaceId::Fills);
let selrect = shape.selrect;
let path_transform = shape.to_path_transform();

View file

@ -1,51 +1,42 @@
use skia_safe::{self as skia};
use super::RenderState;
use super::{RenderState, SurfaceId};
use crate::shapes::Shadow;
pub fn render_drop_shadow(render_state: &mut RenderState, shadow: &Shadow, scale: f32) {
let shadow_paint = shadow.to_paint(scale);
render_state.surfaces.shape.draw(
&mut render_state.surfaces.shadow.canvas(),
(0.0, 0.0),
render_state.sampling_options,
Some(&shadow_paint),
);
render_state
.surfaces
.draw_into(SurfaceId::Fills, SurfaceId::Shadow, Some(&shadow_paint));
render_state
.surfaces
.draw_into(SurfaceId::Strokes, SurfaceId::Shadow, Some(&shadow_paint));
render_state.surfaces.shadow.draw(
&mut render_state.surfaces.current.canvas(),
(0.0, 0.0),
render_state.sampling_options,
render_state.surfaces.draw_into(
SurfaceId::Shadow,
SurfaceId::Current,
Some(&skia::Paint::default()),
);
render_state
.surfaces
.shadow
.canvas()
.canvas(SurfaceId::Shadow)
.clear(skia::Color::TRANSPARENT);
}
pub fn render_inner_shadow(render_state: &mut RenderState, shadow: &Shadow, scale: f32) {
let shadow_paint = shadow.to_paint(scale);
render_state.surfaces.shape.draw(
render_state.surfaces.shadow.canvas(),
(0.0, 0.0),
render_state.sampling_options,
Some(&shadow_paint),
);
render_state.surfaces.shadow.draw(
&mut render_state.surfaces.overlay.canvas(),
(0.0, 0.0),
render_state.sampling_options,
None,
);
render_state
.surfaces
.draw_into(SurfaceId::Fills, SurfaceId::Shadow, Some(&shadow_paint));
render_state
.surfaces
.shadow
.canvas()
.draw_into(SurfaceId::Shadow, SurfaceId::Overlay, None); // , None
render_state
.surfaces
.canvas(SurfaceId::Shadow)
.clear(skia::Color::TRANSPARENT);
}

View file

@ -5,7 +5,7 @@ use crate::math::{Matrix, Point, Rect};
use crate::shapes::{Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type};
use skia_safe::{self as skia, RRect};
use super::RenderState;
use super::{RenderState, SurfaceId};
fn draw_stroke_on_rect(
canvas: &skia::Canvas,
@ -330,7 +330,7 @@ fn draw_image_stroke_in_container(
}
let size = image_fill.size();
let canvas = render_state.surfaces.shape.canvas();
let canvas = render_state.surfaces.canvas(SurfaceId::Fills);
let container = &shape.selrect;
let path_transform = shape.to_path_transform();
let svg_attrs = &shape.svg_attrs;
@ -432,7 +432,7 @@ fn draw_image_stroke_in_container(
* This SHOULD be the only public function in this module.
*/
pub fn render(render_state: &mut RenderState, shape: &Shape, stroke: &Stroke) {
let canvas = render_state.surfaces.shape.canvas();
let canvas = render_state.surfaces.canvas(SurfaceId::Strokes);
let dpr_scale = render_state.viewbox.zoom * render_state.options.dpr();
let selrect = shape.selrect;
let path_transform = shape.to_path_transform();

View file

@ -1,28 +1,48 @@
use super::gpu_state::GpuState;
use skia_safe as skia;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum SurfaceId {
Target,
Current,
Fills,
Strokes,
Shadow,
Overlay,
Debug,
}
pub struct Surfaces {
// is the final destination surface, the one that it is represented in the canvas element.
pub target: skia::Surface,
target: skia::Surface,
// keeps the current render
pub current: skia::Surface,
// keeps the current shape
pub shape: skia::Surface,
current: skia::Surface,
// keeps the current shape's fills
shape_fills: skia::Surface,
// keeps the current shape's strokes
shape_strokes: skia::Surface,
// used for rendering shadows
pub shadow: skia::Surface,
shadow: skia::Surface,
// for drawing the things that are over shadows.
pub overlay: skia::Surface,
overlay: skia::Surface,
// for drawing debug info.
pub debug: skia::Surface,
debug: skia::Surface,
sampling_options: skia::SamplingOptions,
}
impl Surfaces {
pub fn new(gpu_state: &mut GpuState, (width, height): (i32, i32)) -> Self {
pub fn new(
gpu_state: &mut GpuState,
(width, height): (i32, i32),
sampling_options: skia::SamplingOptions,
) -> Self {
let mut target = gpu_state.create_target_surface(width, height);
let current = target.new_surface_with_dimensions((width, height)).unwrap();
let shadow = target.new_surface_with_dimensions((width, height)).unwrap();
let overlay = target.new_surface_with_dimensions((width, height)).unwrap();
let shape = target.new_surface_with_dimensions((width, height)).unwrap();
let shape_fills = target.new_surface_with_dimensions((width, height)).unwrap();
let shape_strokes = target.new_surface_with_dimensions((width, height)).unwrap();
let debug = target.new_surface_with_dimensions((width, height)).unwrap();
Surfaces {
@ -30,22 +50,64 @@ impl Surfaces {
current,
shadow,
overlay,
shape,
shape_fills,
shape_strokes,
debug,
sampling_options,
}
}
pub fn set(&mut self, new_surface: skia::Surface) {
let dim = (new_surface.width(), new_surface.height());
self.target = new_surface;
pub fn resize(&mut self, gpu_state: &mut GpuState, new_width: i32, new_height: i32) {
self.reset_from_target(gpu_state.create_target_surface(new_width, new_height));
}
pub fn snapshot(&mut self, id: SurfaceId) -> skia::Image {
self.get_mut(id).image_snapshot()
}
pub fn canvas(&mut self, id: SurfaceId) -> &skia::Canvas {
self.get_mut(id).canvas()
}
pub fn flush_and_submit(&mut self, gpu_state: &mut GpuState, id: SurfaceId) {
let surface = self.get_mut(id);
gpu_state.context.flush_and_submit_surface(surface, None);
}
pub fn draw_into(&mut self, from: SurfaceId, to: SurfaceId, paint: Option<&skia::Paint>) {
let sampling_options = self.sampling_options;
self.get_mut(from)
.clone()
.draw(self.canvas(to), (0.0, 0.0), sampling_options, paint);
}
pub fn apply_mut(&mut self, ids: &[SurfaceId], mut f: impl FnMut(&mut skia::Surface) -> ()) {
for id in ids {
let surface = self.get_mut(*id);
f(surface);
}
}
fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
match id {
SurfaceId::Target => &mut self.target,
SurfaceId::Current => &mut self.current,
SurfaceId::Shadow => &mut self.shadow,
SurfaceId::Overlay => &mut self.overlay,
SurfaceId::Fills => &mut self.shape_fills,
SurfaceId::Strokes => &mut self.shape_strokes,
SurfaceId::Debug => &mut self.debug,
}
}
fn reset_from_target(&mut self, target: skia::Surface) {
let dim = (target.width(), target.height());
self.target = target;
self.current = self.target.new_surface_with_dimensions(dim).unwrap();
self.overlay = self.target.new_surface_with_dimensions(dim).unwrap();
self.shadow = self.target.new_surface_with_dimensions(dim).unwrap();
self.shape = self.target.new_surface_with_dimensions(dim).unwrap();
self.shape_fills = self.target.new_surface_with_dimensions(dim).unwrap();
self.debug = self.target.new_surface_with_dimensions(dim).unwrap();
}
pub fn resize(&mut self, gpu_state: &mut GpuState, new_width: i32, new_height: i32) {
self.set(gpu_state.create_target_surface(new_width, new_height));
}
}