Render stencils to atlas
This commit is contained in:
parent
b538f5cfb7
commit
98845663a9
6 changed files with 140 additions and 121 deletions
|
@ -13,17 +13,18 @@ pub struct AtlasAllocator {
|
||||||
|
|
||||||
impl AtlasAllocator {
|
impl AtlasAllocator {
|
||||||
pub fn new(device: Device, texture_descriptor: TextureDescriptor) -> Self {
|
pub fn new(device: Device, texture_descriptor: TextureDescriptor) -> Self {
|
||||||
let me = Self {
|
let mut me = Self {
|
||||||
device,
|
device,
|
||||||
texture_descriptor,
|
texture_descriptor,
|
||||||
atlasses: Vec::new(),
|
atlasses: Vec::new(),
|
||||||
free_atlasses: Vec::new(),
|
free_atlasses: Vec::new(),
|
||||||
};
|
};
|
||||||
me.atlasses.push(me.new_atlas());
|
let atlas = me.new_atlas();
|
||||||
|
me.atlasses.push(atlas);
|
||||||
me
|
me
|
||||||
}
|
}
|
||||||
|
|
||||||
fn atlas_size(&self) -> Vector2I {
|
pub fn atlas_size(&self) -> Vector2I {
|
||||||
vec2i(
|
vec2i(
|
||||||
self.texture_descriptor.width() as i32,
|
self.texture_descriptor.width() as i32,
|
||||||
self.texture_descriptor.height() as i32,
|
self.texture_descriptor.height() as i32,
|
||||||
|
@ -62,6 +63,10 @@ impl AtlasAllocator {
|
||||||
self.free_atlasses.extend(self.atlasses.drain(1..));
|
self.free_atlasses.extend(self.atlasses.drain(1..));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
|
||||||
|
self.atlasses.get(atlas_id).map(|a| a.texture.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
fn new_atlas(&mut self) -> Atlas {
|
fn new_atlas(&mut self) -> Atlas {
|
||||||
self.free_atlasses.pop().unwrap_or_else(|| {
|
self.free_atlasses.pop().unwrap_or_else(|| {
|
||||||
Atlas::new(
|
Atlas::new(
|
||||||
|
|
|
@ -21,7 +21,6 @@ const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decisio
|
||||||
|
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
device: metal::Device,
|
device: metal::Device,
|
||||||
command_buffer: metal::CommandBuffer,
|
|
||||||
sprite_cache: SpriteCache,
|
sprite_cache: SpriteCache,
|
||||||
path_stencils: AtlasAllocator,
|
path_stencils: AtlasAllocator,
|
||||||
quad_pipeline_state: metal::RenderPipelineState,
|
quad_pipeline_state: metal::RenderPipelineState,
|
||||||
|
@ -41,7 +40,6 @@ struct PathSprite {
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
device: metal::Device,
|
device: metal::Device,
|
||||||
command_buffer: metal::CommandBuffer,
|
|
||||||
pixel_format: metal::MTLPixelFormat,
|
pixel_format: metal::MTLPixelFormat,
|
||||||
fonts: Arc<dyn platform::FontSystem>,
|
fonts: Arc<dyn platform::FontSystem>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
@ -75,52 +73,63 @@ impl Renderer {
|
||||||
path_stencil_descriptor.set_usage(metal::MTLTextureUsage::RenderTarget);
|
path_stencil_descriptor.set_usage(metal::MTLTextureUsage::RenderTarget);
|
||||||
path_stencil_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
|
path_stencil_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
|
||||||
|
|
||||||
|
let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts);
|
||||||
|
let path_stencils = AtlasAllocator::new(device.clone(), path_stencil_descriptor);
|
||||||
|
let quad_pipeline_state = build_pipeline_state(
|
||||||
|
&device,
|
||||||
|
&library,
|
||||||
|
"quad",
|
||||||
|
"quad_vertex",
|
||||||
|
"quad_fragment",
|
||||||
|
pixel_format,
|
||||||
|
)?;
|
||||||
|
let shadow_pipeline_state = build_pipeline_state(
|
||||||
|
&device,
|
||||||
|
&library,
|
||||||
|
"shadow",
|
||||||
|
"shadow_vertex",
|
||||||
|
"shadow_fragment",
|
||||||
|
pixel_format,
|
||||||
|
)?;
|
||||||
|
let sprite_pipeline_state = build_pipeline_state(
|
||||||
|
&device,
|
||||||
|
&library,
|
||||||
|
"sprite",
|
||||||
|
"sprite_vertex",
|
||||||
|
"sprite_fragment",
|
||||||
|
pixel_format,
|
||||||
|
)?;
|
||||||
|
let path_stencil_pipeline_state = build_stencil_pipeline_state(
|
||||||
|
&device,
|
||||||
|
&library,
|
||||||
|
"path_winding",
|
||||||
|
"path_winding_vertex",
|
||||||
|
"path_winding_fragment",
|
||||||
|
path_stencil_pixel_format,
|
||||||
|
)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
device,
|
device,
|
||||||
command_buffer,
|
sprite_cache,
|
||||||
sprite_cache: SpriteCache::new(device.clone(), vec2i(1024, 768), fonts),
|
path_stencils,
|
||||||
path_stencils: AtlasAllocator::new(device.clone(), path_stencil_descriptor),
|
quad_pipeline_state,
|
||||||
quad_pipeline_state: build_pipeline_state(
|
shadow_pipeline_state,
|
||||||
&device,
|
sprite_pipeline_state,
|
||||||
&library,
|
path_stencil_pipeline_state,
|
||||||
"quad",
|
|
||||||
"quad_vertex",
|
|
||||||
"quad_fragment",
|
|
||||||
pixel_format,
|
|
||||||
)?,
|
|
||||||
shadow_pipeline_state: build_pipeline_state(
|
|
||||||
&device,
|
|
||||||
&library,
|
|
||||||
"shadow",
|
|
||||||
"shadow_vertex",
|
|
||||||
"shadow_fragment",
|
|
||||||
pixel_format,
|
|
||||||
)?,
|
|
||||||
sprite_pipeline_state: build_pipeline_state(
|
|
||||||
&device,
|
|
||||||
&library,
|
|
||||||
"sprite",
|
|
||||||
"sprite_vertex",
|
|
||||||
"sprite_fragment",
|
|
||||||
pixel_format,
|
|
||||||
)?,
|
|
||||||
path_stencil_pipeline_state: build_stencil_pipeline_state(
|
|
||||||
&device,
|
|
||||||
&library,
|
|
||||||
"path_winding",
|
|
||||||
"path_winding_vertex",
|
|
||||||
"path_winding_fragment",
|
|
||||||
path_stencil_pixel_format,
|
|
||||||
)?,
|
|
||||||
unit_vertices,
|
unit_vertices,
|
||||||
instances,
|
instances,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self, scene: &Scene, drawable_size: Vector2F, output: &metal::TextureRef) {
|
pub fn render(
|
||||||
|
&mut self,
|
||||||
|
scene: &Scene,
|
||||||
|
drawable_size: Vector2F,
|
||||||
|
command_buffer: &metal::CommandBufferRef,
|
||||||
|
output: &metal::TextureRef,
|
||||||
|
) {
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
self.render_path_stencils(scene, &mut offset, drawable_size);
|
self.render_path_stencils(scene, &mut offset, drawable_size, command_buffer);
|
||||||
self.render_layers(scene, &mut offset, drawable_size, output);
|
self.render_layers(scene, &mut offset, drawable_size, command_buffer, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_path_stencils(
|
fn render_path_stencils(
|
||||||
|
@ -128,6 +137,7 @@ impl Renderer {
|
||||||
scene: &Scene,
|
scene: &Scene,
|
||||||
offset: &mut usize,
|
offset: &mut usize,
|
||||||
drawable_size: Vector2F,
|
drawable_size: Vector2F,
|
||||||
|
command_buffer: &metal::CommandBufferRef,
|
||||||
) -> Vec<PathSprite> {
|
) -> Vec<PathSprite> {
|
||||||
let mut stencils = Vec::new();
|
let mut stencils = Vec::new();
|
||||||
let mut vertices = Vec::<shaders::GPUIPathVertex>::new();
|
let mut vertices = Vec::<shaders::GPUIPathVertex>::new();
|
||||||
|
@ -150,11 +160,10 @@ impl Renderer {
|
||||||
|
|
||||||
if current_atlas_id.map_or(false, |current_atlas_id| atlas_id != current_atlas_id) {
|
if current_atlas_id.map_or(false, |current_atlas_id| atlas_id != current_atlas_id) {
|
||||||
self.render_path_stencils_for_atlas(
|
self.render_path_stencils_for_atlas(
|
||||||
scene,
|
|
||||||
offset,
|
offset,
|
||||||
drawable_size,
|
&vertices,
|
||||||
vertices.as_slice(),
|
atlas_id,
|
||||||
self.path_stencils.texture(atlas_id).unwrap(),
|
command_buffer,
|
||||||
);
|
);
|
||||||
vertices.clear();
|
vertices.clear();
|
||||||
}
|
}
|
||||||
|
@ -163,19 +172,19 @@ impl Renderer {
|
||||||
|
|
||||||
// Populate the vertices by translating them to their appropriate location in the atlas.
|
// Populate the vertices by translating them to their appropriate location in the atlas.
|
||||||
for vertex in &path.vertices {
|
for vertex in &path.vertices {
|
||||||
vertices.push(todo!());
|
let xy_position = (vertex.xy_position - path.bounds.origin())
|
||||||
|
* scene.scale_factor()
|
||||||
|
+ atlas_origin.to_f32();
|
||||||
|
vertices.push(shaders::GPUIPathVertex {
|
||||||
|
xy_position: xy_position.to_float2(),
|
||||||
|
st_position: vertex.st_position.to_float2(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(atlas_id) = current_atlas_id {
|
if let Some(atlas_id) = current_atlas_id {
|
||||||
self.render_path_stencils_for_atlas(
|
self.render_path_stencils_for_atlas(offset, &vertices, atlas_id, command_buffer);
|
||||||
scene,
|
|
||||||
offset,
|
|
||||||
drawable_size,
|
|
||||||
vertices.as_slice(),
|
|
||||||
self.path_stencils.texture(atlas_id).unwrap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stencils
|
stencils
|
||||||
|
@ -183,66 +192,73 @@ impl Renderer {
|
||||||
|
|
||||||
fn render_path_stencils_for_atlas(
|
fn render_path_stencils_for_atlas(
|
||||||
&mut self,
|
&mut self,
|
||||||
scene: &Scene,
|
|
||||||
offset: &mut usize,
|
offset: &mut usize,
|
||||||
drawable_size: Vector2F,
|
|
||||||
vertices: &[shaders::GPUIPathVertex],
|
vertices: &[shaders::GPUIPathVertex],
|
||||||
texture: &metal::TextureRef,
|
atlas_id: usize,
|
||||||
|
command_buffer: &metal::CommandBufferRef,
|
||||||
) {
|
) {
|
||||||
// let render_pass_descriptor = metal::RenderPassDescriptor::new();
|
align_offset(offset);
|
||||||
// let stencil_attachment = render_pass_descriptor.stencil_attachment().unwrap();
|
let next_offset = *offset + vertices.len() * mem::size_of::<shaders::GPUIPathVertex>();
|
||||||
// stencil_attachment.set_texture(Some(&self.path_winding_texture));
|
assert!(
|
||||||
// stencil_attachment.set_load_action(metal::MTLLoadAction::Clear);
|
next_offset <= INSTANCE_BUFFER_SIZE,
|
||||||
// stencil_attachment.set_store_action(metal::MTLStoreAction::Store);
|
"instance buffer exhausted"
|
||||||
// let winding_command_encoder = self
|
);
|
||||||
// .command_buffer
|
|
||||||
// .new_render_command_encoder(render_pass_descriptor);
|
|
||||||
|
|
||||||
// Dubious shit that may be valuable:
|
let render_pass_descriptor = metal::RenderPassDescriptor::new();
|
||||||
|
|
||||||
// for path in scene.paths() {
|
let stencil_attachment = render_pass_descriptor.stencil_attachment().unwrap();
|
||||||
// winding_command_encoder.set_render_pipeline_state(&self.path_stencil_pipeline_state);
|
let stencil_texture = self.path_stencils.texture(atlas_id).unwrap();
|
||||||
// winding_command_encoder.set_vertex_buffer(
|
stencil_attachment.set_texture(Some(stencil_texture));
|
||||||
// shaders::GPUIPathWindingVertexInputIndex_GPUIPathWindingVertexInputIndexVertices
|
stencil_attachment.set_load_action(metal::MTLLoadAction::Clear);
|
||||||
// as u64,
|
stencil_attachment.set_store_action(metal::MTLStoreAction::Store);
|
||||||
// Some(&self.instances),
|
|
||||||
// *offset as u64,
|
|
||||||
// );
|
|
||||||
// winding_command_encoder.set_vertex_bytes(
|
|
||||||
// shaders::GPUIPathWindingVertexInputIndex_GPUIPathWindingVertexInputIndexViewportSize
|
|
||||||
// as u64,
|
|
||||||
// mem::size_of::<shaders::vector_float2>() as u64,
|
|
||||||
// [drawable_size.to_float2()].as_ptr() as *const c_void,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// let buffer_contents = unsafe {
|
let stencil_descriptor = metal::DepthStencilDescriptor::new();
|
||||||
// (self.instances.contents() as *mut u8).offset(*offset as isize)
|
let front_face_stencil = stencil_descriptor.front_face_stencil().unwrap();
|
||||||
// as *mut shaders::GPUIPathVertex
|
front_face_stencil.set_depth_stencil_pass_operation(metal::MTLStencilOperation::Invert);
|
||||||
// };
|
front_face_stencil.set_depth_failure_operation(metal::MTLStencilOperation::Keep);
|
||||||
|
front_face_stencil.set_stencil_compare_function(metal::MTLCompareFunction::Always);
|
||||||
|
front_face_stencil.set_read_mask(0x1);
|
||||||
|
front_face_stencil.set_write_mask(0x1);
|
||||||
|
let depth_stencil_state = self.device.new_depth_stencil_state(&stencil_descriptor);
|
||||||
|
|
||||||
// for (ix, vertex) in paths.iter().flat_map(|p| &p.vertices).enumerate() {
|
let winding_command_encoder =
|
||||||
// let shader_vertex = shaders::GPUIPathVertex {
|
command_buffer.new_render_command_encoder(render_pass_descriptor);
|
||||||
// xy_position: vertex.xy_position.to_float2(),
|
winding_command_encoder.set_depth_stencil_state(&depth_stencil_state);
|
||||||
// st_position: vertex.st_position.to_float2(),
|
winding_command_encoder.set_render_pipeline_state(&self.path_stencil_pipeline_state);
|
||||||
// };
|
winding_command_encoder.set_vertex_buffer(
|
||||||
// unsafe {
|
shaders::GPUIPathWindingVertexInputIndex_GPUIPathWindingVertexInputIndexVertices as u64,
|
||||||
// *(buffer_contents.offset(ix as isize)) = shader_vertex;
|
Some(&self.instances),
|
||||||
// }
|
*offset as u64,
|
||||||
// }
|
);
|
||||||
|
winding_command_encoder.set_vertex_bytes(
|
||||||
|
shaders::GPUIPathWindingVertexInputIndex_GPUIPathWindingVertexInputIndexAtlasSize
|
||||||
|
as u64,
|
||||||
|
mem::size_of::<shaders::vector_float2>() as u64,
|
||||||
|
[self.path_stencils.atlas_size().to_float2()].as_ptr() as *const c_void,
|
||||||
|
);
|
||||||
|
|
||||||
// self.instances.did_modify_range(NSRange {
|
let buffer_contents = unsafe {
|
||||||
// location: *offset as u64,
|
(self.instances.contents() as *mut u8).add(*offset) as *mut shaders::GPUIPathVertex
|
||||||
// length: (next_offset - *offset) as u64,
|
};
|
||||||
// });
|
|
||||||
// *offset = next_offset;
|
|
||||||
|
|
||||||
// winding_command_encoder.draw_primitives(
|
for (ix, vertex) in vertices.iter().enumerate() {
|
||||||
// metal::MTLPrimitiveType::Triangle,
|
unsafe {
|
||||||
// 0,
|
*buffer_contents.add(ix) = *vertex;
|
||||||
// vertex_count as u64,
|
}
|
||||||
// );
|
}
|
||||||
// winding_command_encoder.end_encoding();
|
|
||||||
// }
|
self.instances.did_modify_range(NSRange {
|
||||||
|
location: *offset as u64,
|
||||||
|
length: (next_offset - *offset) as u64,
|
||||||
|
});
|
||||||
|
*offset = next_offset;
|
||||||
|
|
||||||
|
winding_command_encoder.draw_primitives(
|
||||||
|
metal::MTLPrimitiveType::Triangle,
|
||||||
|
0,
|
||||||
|
vertices.len() as u64,
|
||||||
|
);
|
||||||
|
winding_command_encoder.end_encoding();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_layers(
|
fn render_layers(
|
||||||
|
@ -250,6 +266,7 @@ impl Renderer {
|
||||||
scene: &Scene,
|
scene: &Scene,
|
||||||
offset: &mut usize,
|
offset: &mut usize,
|
||||||
drawable_size: Vector2F,
|
drawable_size: Vector2F,
|
||||||
|
command_buffer: &metal::CommandBufferRef,
|
||||||
output: &metal::TextureRef,
|
output: &metal::TextureRef,
|
||||||
) {
|
) {
|
||||||
let render_pass_descriptor = metal::RenderPassDescriptor::new();
|
let render_pass_descriptor = metal::RenderPassDescriptor::new();
|
||||||
|
@ -261,9 +278,7 @@ impl Renderer {
|
||||||
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
|
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
|
||||||
color_attachment.set_store_action(metal::MTLStoreAction::Store);
|
color_attachment.set_store_action(metal::MTLStoreAction::Store);
|
||||||
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
|
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
|
||||||
let command_encoder = self
|
let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
|
||||||
.command_buffer
|
|
||||||
.new_render_command_encoder(render_pass_descriptor);
|
|
||||||
|
|
||||||
command_encoder.set_viewport(metal::MTLViewport {
|
command_encoder.set_viewport(metal::MTLViewport {
|
||||||
originX: 0.0,
|
originX: 0.0,
|
||||||
|
@ -276,9 +291,9 @@ impl Renderer {
|
||||||
|
|
||||||
for layer in scene.layers() {
|
for layer in scene.layers() {
|
||||||
self.clip(scene, layer, drawable_size, command_encoder);
|
self.clip(scene, layer, drawable_size, command_encoder);
|
||||||
self.render_shadows(scene, layer, &mut offset, drawable_size, command_encoder);
|
self.render_shadows(scene, layer, offset, drawable_size, command_encoder);
|
||||||
self.render_quads(scene, layer, &mut offset, drawable_size, command_encoder);
|
self.render_quads(scene, layer, offset, drawable_size, command_encoder);
|
||||||
self.render_sprites(scene, layer, &mut offset, drawable_size, command_encoder);
|
self.render_sprites(scene, layer, offset, drawable_size, command_encoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
command_encoder.end_encoding();
|
command_encoder.end_encoding();
|
||||||
|
|
|
@ -56,7 +56,7 @@ typedef struct {
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GPUIPathWindingVertexInputIndexVertices = 0,
|
GPUIPathWindingVertexInputIndexVertices = 0,
|
||||||
GPUIPathWindingVertexInputIndexViewportSize = 1,
|
GPUIPathWindingVertexInputIndexAtlasSize = 1,
|
||||||
} GPUIPathWindingVertexInputIndex;
|
} GPUIPathWindingVertexInputIndex;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
|
@ -210,10 +210,10 @@ struct PathWindingFragmentInput {
|
||||||
vertex PathWindingFragmentInput path_winding_vertex(
|
vertex PathWindingFragmentInput path_winding_vertex(
|
||||||
uint vertex_id [[vertex_id]],
|
uint vertex_id [[vertex_id]],
|
||||||
constant GPUIPathVertex *vertices [[buffer(GPUIPathWindingVertexInputIndexVertices)]],
|
constant GPUIPathVertex *vertices [[buffer(GPUIPathWindingVertexInputIndexVertices)]],
|
||||||
constant float2 *viewport_size [[buffer(GPUIPathWindingVertexInputIndexViewportSize)]]
|
constant float2 *atlas_size [[buffer(GPUIPathWindingVertexInputIndexAtlasSize)]]
|
||||||
) {
|
) {
|
||||||
GPUIPathVertex v = vertices[vertex_id];
|
GPUIPathVertex v = vertices[vertex_id];
|
||||||
float4 device_position = to_device_position(v.xy_position, *viewport_size);
|
float4 device_position = to_device_position(v.xy_position, *atlas_size);
|
||||||
return PathWindingFragmentInput {
|
return PathWindingFragmentInput {
|
||||||
device_position,
|
device_position,
|
||||||
v.st_position,
|
v.st_position,
|
||||||
|
@ -223,9 +223,9 @@ vertex PathWindingFragmentInput path_winding_vertex(
|
||||||
fragment float4 path_winding_fragment(
|
fragment float4 path_winding_fragment(
|
||||||
PathWindingFragmentInput input [[stage_in]]
|
PathWindingFragmentInput input [[stage_in]]
|
||||||
) {
|
) {
|
||||||
if (input.st_position.x * input.st_position.x - input.st_position.y > 0.0) {
|
if (input.st_position.x * input.st_position.x - input.st_position.y > 0.) {
|
||||||
return float4(0.0);
|
return float4(0.);
|
||||||
} else {
|
} else {
|
||||||
return float4(float3(0.0), 1.0 / 255.0);
|
return float4(1.);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -431,7 +431,6 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
|
||||||
window_state.renderer.render(
|
window_state.renderer.render(
|
||||||
&scene,
|
&scene,
|
||||||
size * scale_factor,
|
size * scale_factor,
|
||||||
&device,
|
|
||||||
command_buffer,
|
command_buffer,
|
||||||
drawable.texture(),
|
drawable.texture(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -637,7 +637,7 @@ impl Selection {
|
||||||
);
|
);
|
||||||
path.line_to(vec2f(first_line.end_x - corner_radius, start_y));
|
path.line_to(vec2f(first_line.end_x - corner_radius, start_y));
|
||||||
|
|
||||||
scene.push_path(ColorU::from_u32(0xff0000ff), path.build());
|
scene.push_path(path.build(ColorU::from_u32(0xff0000ff)));
|
||||||
|
|
||||||
// rounded_corner(&mut path, corner, corner_radius, Right, Down);
|
// rounded_corner(&mut path, corner, corner_radius, Right, Down);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue