mirror of
https://github.com/maplibre/maplibre-rs.git
synced 2025-12-08 19:05:57 +00:00
Add a stencil pattern which prepares the stencil buffer accordingly
This commit is contained in:
parent
c54f3ee84b
commit
e9ed013489
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 275 KiB |
@ -1,6 +1,7 @@
|
|||||||
use crate::render::shader_ffi::Vec3f32;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::render::shader_ffi::Vec3f32;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct TileCoords {
|
pub struct TileCoords {
|
||||||
pub x: u32,
|
pub x: u32,
|
||||||
@ -42,21 +43,29 @@ pub struct WorldTileCoords {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WorldTileCoords {
|
impl WorldTileCoords {
|
||||||
pub fn into_world(self, extent: u16) -> WorldCoords {
|
pub fn into_world(self, extent: f32) -> WorldCoords {
|
||||||
WorldCoords {
|
WorldCoords {
|
||||||
x: self.x as f32 * extent as f32,
|
x: self.x as f32 * extent,
|
||||||
y: self.y as f32 * extent as f32 + extent as f32, // We add extent here as we want the upper left corner
|
y: self.y as f32 * extent + extent, // We add extent here as we want the upper left corner
|
||||||
z: self.z as f32,
|
z: self.z as f32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_aligned(self) -> AlignedWorldTileCoords {
|
||||||
|
return AlignedWorldTileCoords(WorldTileCoords {
|
||||||
|
x: self.x / 2 * 2,
|
||||||
|
y: self.y / 2 * 2 - 1,
|
||||||
|
z: self.z,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn stencil_reference_value(&self) -> u8 {
|
pub fn stencil_reference_value(&self) -> u8 {
|
||||||
match (self.x, self.y) {
|
match (self.x, self.y) {
|
||||||
(x, y) if x % 2 == 0 && y % 2 == 0 => 1,
|
(x, y) if x % 2 == 0 && y % 2 == 0 => 1,
|
||||||
(x, y) if x % 2 == 0 && y % 2 != 0 => 2,
|
(x, y) if x % 2 == 0 && y % 2 != 0 => 2,
|
||||||
(x, y) if x % 2 != 0 && y % 2 == 0 => 3,
|
(x, y) if x % 2 != 0 && y % 2 == 0 => 3,
|
||||||
(x, y) if x % 2 != 0 && y % 2 != 0 => 4,
|
(x, y) if x % 2 != 0 && y % 2 != 0 => 4,
|
||||||
_ => 0,
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,6 +86,38 @@ impl From<(i32, i32, u8)> for WorldTileCoords {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AlignedWorldTileCoords(pub WorldTileCoords);
|
||||||
|
|
||||||
|
impl AlignedWorldTileCoords {
|
||||||
|
pub fn into_upper_left(self) -> WorldTileCoords {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_upper_right(&self) -> WorldTileCoords {
|
||||||
|
WorldTileCoords {
|
||||||
|
x: self.0.x + 1,
|
||||||
|
y: self.0.y + 1,
|
||||||
|
z: self.0.z,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_lower_left(&self) -> WorldTileCoords {
|
||||||
|
WorldTileCoords {
|
||||||
|
x: self.0.x,
|
||||||
|
y: self.0.y - 1,
|
||||||
|
z: self.0.z,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_lower_right(&self) -> WorldTileCoords {
|
||||||
|
WorldTileCoords {
|
||||||
|
x: self.0.x - 1,
|
||||||
|
y: self.0.y - 1,
|
||||||
|
z: self.0.z,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct WorldCoords {
|
pub struct WorldCoords {
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
|
|||||||
@ -5,8 +5,8 @@ pub const MUNICH_Y: u32 = 11360;
|
|||||||
pub const MUNICH_Z: u8 = 15;
|
pub const MUNICH_Z: u8 = 15;
|
||||||
|
|
||||||
pub fn fetch_munich_tiles(cache: &Cache) {
|
pub fn fetch_munich_tiles(cache: &Cache) {
|
||||||
for x in 0..10 {
|
for x in 0..15 {
|
||||||
for y in 0..10 {
|
for y in 0..15 {
|
||||||
cache.fetch((MUNICH_X + x, MUNICH_Y + y, MUNICH_Z).into())
|
cache.fetch((MUNICH_X + x, MUNICH_Y + y, MUNICH_Z).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
|
mod buffer_pool;
|
||||||
mod piplines;
|
mod piplines;
|
||||||
|
mod shaders;
|
||||||
|
mod stencil_pattern;
|
||||||
mod texture;
|
mod texture;
|
||||||
|
|
||||||
mod shaders;
|
|
||||||
|
|
||||||
mod buffer_pool;
|
|
||||||
pub mod camera;
|
pub mod camera;
|
||||||
pub mod shader_ffi;
|
pub mod shader_ffi;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|||||||
@ -26,13 +26,15 @@ fn main(
|
|||||||
[[builtin(vertex_index)]] vertex_idx: u32,
|
[[builtin(vertex_index)]] vertex_idx: u32,
|
||||||
[[builtin(instance_index)]] instance_idx: u32 // instance_index is used when we have multiple instances of the same "object"
|
[[builtin(instance_index)]] instance_idx: u32 // instance_index is used when we have multiple instances of the same "object"
|
||||||
) -> VertexOutput {
|
) -> VertexOutput {
|
||||||
var VERTICES: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
|
let z = 0.0;
|
||||||
vec2<f32>(0.0, 0.0),
|
|
||||||
vec2<f32>(0.0, EXTENT),
|
var VERTICES: array<vec3<f32>, 6> = array<vec3<f32>, 6>(
|
||||||
vec2<f32>(EXTENT, 0.0),
|
vec3<f32>(0.0, 0.0, z),
|
||||||
vec2<f32>(EXTENT, 0.0),
|
vec3<f32>(0.0, EXTENT, z),
|
||||||
vec2<f32>(0.0, EXTENT),
|
vec3<f32>(EXTENT, 0.0, z),
|
||||||
vec2<f32>(EXTENT, EXTENT)
|
vec3<f32>(EXTENT, 0.0, z),
|
||||||
|
vec3<f32>(0.0, EXTENT, z),
|
||||||
|
vec3<f32>(EXTENT, EXTENT, z)
|
||||||
);
|
);
|
||||||
let a_position = VERTICES[vertex_idx];
|
let a_position = VERTICES[vertex_idx];
|
||||||
|
|
||||||
@ -42,10 +44,7 @@ fn main(
|
|||||||
vec3<f32>(0.0, 0.0, 1.0)
|
vec3<f32>(0.0, 0.0, 1.0)
|
||||||
);
|
);
|
||||||
|
|
||||||
let z = 0.0;
|
let world_pos = scaling * a_position + vec3<f32>(mask_offset, z);
|
||||||
|
|
||||||
let world_pos_3d = vec3<f32>(a_position + mask_offset, z);
|
|
||||||
let world_pos = scaling * world_pos_3d;
|
|
||||||
|
|
||||||
let position = globals.camera.view_proj * vec4<f32>(world_pos, 1.0);
|
let position = globals.camera.view_proj * vec4<f32>(world_pos, 1.0);
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,6 @@ use std::default::Default;
|
|||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use crate::coords::{TileCoords, WorldTileCoords};
|
|
||||||
use crate::example::{MUNICH_X, MUNICH_Y};
|
|
||||||
use log::{trace, warn};
|
use log::{trace, warn};
|
||||||
use lyon::tessellation::VertexBuffers;
|
use lyon::tessellation::VertexBuffers;
|
||||||
use wgpu::util::DeviceExt;
|
use wgpu::util::DeviceExt;
|
||||||
@ -15,11 +13,14 @@ use winit::event::{
|
|||||||
};
|
};
|
||||||
use winit::window::Window;
|
use winit::window::Window;
|
||||||
|
|
||||||
|
use crate::coords::{TileCoords, WorldTileCoords};
|
||||||
|
use crate::example::{MUNICH_X, MUNICH_Y};
|
||||||
use crate::fps_meter::FPSMeter;
|
use crate::fps_meter::FPSMeter;
|
||||||
use crate::io::cache::Cache;
|
use crate::io::cache::Cache;
|
||||||
use crate::io::static_database;
|
use crate::io::static_database;
|
||||||
use crate::platform::{COLOR_TEXTURE_FORMAT, MIN_BUFFER_SIZE};
|
use crate::platform::{COLOR_TEXTURE_FORMAT, MIN_BUFFER_SIZE};
|
||||||
use crate::render::buffer_pool::{BackingBufferDescriptor, BufferPool};
|
use crate::render::buffer_pool::{BackingBufferDescriptor, BufferPool};
|
||||||
|
use crate::render::stencil_pattern::TileMaskPattern;
|
||||||
use crate::render::{camera, shaders};
|
use crate::render::{camera, shaders};
|
||||||
use crate::tesselation::{IndexDataType, Tesselated};
|
use crate::tesselation::{IndexDataType, Tesselated};
|
||||||
use crate::util::measure::Measure;
|
use crate::util::measure::Measure;
|
||||||
@ -68,7 +69,8 @@ pub struct State {
|
|||||||
|
|
||||||
buffer_pool: BufferPool<Queue, Buffer, GpuVertexUniform, IndexDataType>,
|
buffer_pool: BufferPool<Queue, Buffer, GpuVertexUniform, IndexDataType>,
|
||||||
|
|
||||||
tile_mask_instances: wgpu::Buffer,
|
tile_mask_pattern: TileMaskPattern,
|
||||||
|
tile_mask_instances_buffer: wgpu::Buffer,
|
||||||
|
|
||||||
pub camera: camera::Camera,
|
pub camera: camera::Camera,
|
||||||
projection: camera::Projection,
|
projection: camera::Projection,
|
||||||
@ -161,24 +163,14 @@ impl State {
|
|||||||
mapped_at_creation: false,
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let instances = [
|
let tile_masks_uniform_buffer_size =
|
||||||
// Step 1
|
std::mem::size_of::<MaskInstanceUniform>() as u64 * 128; // FIXME: Tile count?
|
||||||
MaskInstanceUniform::new([0.0, 0.0], 4.0, -4.0, [1.0, 0.0, 0.0, 1.0]), // horizontal
|
|
||||||
// Step 2
|
|
||||||
MaskInstanceUniform::new([1.0 * 4096.0, 0.0], 1.0, -4.0, [0.0, 1.0, 0.0, 1.0]), // vertical
|
|
||||||
MaskInstanceUniform::new([3.0 * 4096.0, 0.0], 1.0, -4.0, [0.0, 1.0, 0.0, 1.0]), // vertical
|
|
||||||
// Step 3
|
|
||||||
MaskInstanceUniform::new([0.0, -1.0 * 4096.0], 4.0, 1.0, [0.0, 0.0, 1.0, 1.0]), // horizontal
|
|
||||||
MaskInstanceUniform::new([0.0, -3.0 * 4096.0], 4.0, 1.0, [0.0, 0.0, 1.0, 1.0]), // horizontal
|
|
||||||
// Step 4
|
|
||||||
MaskInstanceUniform::new([1.0 * 4096.0, 0.0], 1.0, -4.0, [0.5, 0.25, 0.5, 1.0]), // vertical
|
|
||||||
MaskInstanceUniform::new([3.0 * 4096.0, 0.0], 1.0, -4.0, [0.5, 0.25, 0.5, 1.0]), // vertical
|
|
||||||
];
|
|
||||||
|
|
||||||
let tile_mask_instances = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
let tile_mask_instances = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: None,
|
label: None,
|
||||||
contents: bytemuck::cast_slice(&instances),
|
size: tile_masks_uniform_buffer_size,
|
||||||
usage: wgpu::BufferUsages::VERTEX,
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let globals_buffer_byte_size = cmp::max(
|
let globals_buffer_byte_size = cmp::max(
|
||||||
@ -309,7 +301,7 @@ impl State {
|
|||||||
globals_uniform_buffer,
|
globals_uniform_buffer,
|
||||||
tiles_uniform_buffer,
|
tiles_uniform_buffer,
|
||||||
fps_meter: FPSMeter::new(),
|
fps_meter: FPSMeter::new(),
|
||||||
tile_mask_instances,
|
tile_mask_instances_buffer: tile_mask_instances,
|
||||||
camera,
|
camera,
|
||||||
projection,
|
projection,
|
||||||
suspended: false, // Initially the app is not suspended
|
suspended: false, // Initially the app is not suspended
|
||||||
@ -317,6 +309,7 @@ impl State {
|
|||||||
BackingBufferDescriptor(vertex_uniform_buffer, 1024 * 1024 * 16),
|
BackingBufferDescriptor(vertex_uniform_buffer, 1024 * 1024 * 16),
|
||||||
BackingBufferDescriptor(indices_uniform_buffer, 1024 * 1024 * 16),
|
BackingBufferDescriptor(indices_uniform_buffer, 1024 * 1024 * 16),
|
||||||
),
|
),
|
||||||
|
tile_mask_pattern: TileMaskPattern::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,28 +364,29 @@ impl State {
|
|||||||
let upload = cache.pop_all();
|
let upload = cache.pop_all();
|
||||||
|
|
||||||
for tile in upload.iter() {
|
for tile in upload.iter() {
|
||||||
let new_coords = TileCoords {
|
let world_coords = tile.coords.into_world_tile();
|
||||||
x: tile.coords.x,
|
self.tile_mask_pattern.update_bounds(&world_coords);
|
||||||
y: tile.coords.y,
|
|
||||||
z: tile.coords.z,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.buffer_pool
|
self.buffer_pool
|
||||||
.allocate_geometry(&self.queue, tile.id, new_coords, &tile.geometry);
|
.allocate_geometry(&self.queue, tile.id, tile.coords, &tile.geometry);
|
||||||
|
|
||||||
let uniform = TileUniform::new(
|
|
||||||
[0.0, 0.0, 0.0, 1.0],
|
|
||||||
new_coords
|
|
||||||
.into_world_tile()
|
|
||||||
.into_world(4096)
|
|
||||||
.into_shader_coords(),
|
|
||||||
);
|
|
||||||
self.queue.write_buffer(
|
self.queue.write_buffer(
|
||||||
&self.tiles_uniform_buffer,
|
&self.tiles_uniform_buffer,
|
||||||
std::mem::size_of::<TileUniform>() as u64 * tile.id as u64,
|
std::mem::size_of::<TileUniform>() as u64 * tile.id as u64,
|
||||||
bytemuck::cast_slice(&[uniform]),
|
bytemuck::cast_slice(&[TileUniform::new(
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
world_coords.into_world(4096.0).into_shader_coords(),
|
||||||
|
)]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.tile_mask_pattern.update_pattern(15u8, 4096.0);
|
||||||
|
|
||||||
|
self.queue.write_buffer(
|
||||||
|
&self.tile_mask_instances_buffer,
|
||||||
|
0,
|
||||||
|
bytemuck::cast_slice(self.tile_mask_pattern.as_slice()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
|
pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
|
||||||
@ -460,9 +454,9 @@ impl State {
|
|||||||
{
|
{
|
||||||
// Draw masks
|
// Draw masks
|
||||||
pass.set_pipeline(&self.mask_pipeline);
|
pass.set_pipeline(&self.mask_pipeline);
|
||||||
pass.set_vertex_buffer(0, self.tile_mask_instances.slice(..));
|
pass.set_vertex_buffer(0, self.tile_mask_instances_buffer.slice(..));
|
||||||
// Draw 7 squares each out of 6 vertices
|
// Draw 7 squares each out of 6 vertices
|
||||||
pass.draw(0..6, 0..7);
|
pass.draw(0..6, 0..self.tile_mask_pattern.instances());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
for entry in self.buffer_pool.available_vertices() {
|
for entry in self.buffer_pool.available_vertices() {
|
||||||
|
|||||||
134
src/render/stencil_pattern.rs
Normal file
134
src/render/stencil_pattern.rs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
use std::num;
|
||||||
|
|
||||||
|
use crate::coords::{AlignedWorldTileCoords, WorldTileCoords};
|
||||||
|
use crate::render::shader_ffi::MaskInstanceUniform;
|
||||||
|
|
||||||
|
struct MinMaxBoundingBox {
|
||||||
|
min_x: i32,
|
||||||
|
min_y: i32,
|
||||||
|
max_x: i32,
|
||||||
|
max_y: i32,
|
||||||
|
initialized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinMaxBoundingBox {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
min_x: i32::MAX,
|
||||||
|
min_y: i32::MAX,
|
||||||
|
max_x: i32::MIN,
|
||||||
|
max_y: i32::MIN,
|
||||||
|
initialized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_initialized(&self) -> bool {
|
||||||
|
self.initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, world_coords: &WorldTileCoords) {
|
||||||
|
self.initialized = true;
|
||||||
|
|
||||||
|
if world_coords.x < self.min_x {
|
||||||
|
self.min_x = world_coords.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if world_coords.y < self.min_y {
|
||||||
|
self.min_y = world_coords.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if world_coords.x > self.max_x {
|
||||||
|
self.max_x = world_coords.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if world_coords.y > self.max_y {
|
||||||
|
self.max_y = world_coords.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TileMaskPattern {
|
||||||
|
bounding_box: MinMaxBoundingBox,
|
||||||
|
pattern: Vec<MaskInstanceUniform>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TileMaskPattern {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
bounding_box: MinMaxBoundingBox::new(),
|
||||||
|
pattern: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_bounds(&mut self, world_coords: &WorldTileCoords) {
|
||||||
|
self.bounding_box.update(world_coords)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_slice(&self) -> &[MaskInstanceUniform] {
|
||||||
|
self.pattern.as_slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn instances(&self) -> u32 {
|
||||||
|
self.pattern.len() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vertical(&mut self, dx: i32, dy: i32, anchor_x: f32, anchor_y: f32, extent: f32) {
|
||||||
|
for i in 0..(dx.abs() / 2) {
|
||||||
|
self.pattern.push(MaskInstanceUniform::new(
|
||||||
|
[anchor_x + ((i * 2) + 1) as f32 * extent, anchor_y],
|
||||||
|
1.0,
|
||||||
|
dy as f32,
|
||||||
|
[0.0, 1.0, 0.0, 1.0],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn horizontal(&mut self, dx: i32, dy: i32, anchor_x: f32, anchor_y: f32, extent: f32) {
|
||||||
|
for i in 0..(dy.abs() / 2) {
|
||||||
|
self.pattern.push(MaskInstanceUniform::new(
|
||||||
|
[anchor_x, anchor_y - extent - (i * 2) as f32 * extent],
|
||||||
|
dx as f32,
|
||||||
|
1.0,
|
||||||
|
[0.0, 0.0, 1.0, 1.0],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_pattern(&mut self, z: u8, extent: f32) {
|
||||||
|
if !self.bounding_box.is_initialized() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pattern.clear();
|
||||||
|
|
||||||
|
let start: WorldTileCoords = (self.bounding_box.min_x, self.bounding_box.max_y, z).into(); // upper left corner
|
||||||
|
let end: WorldTileCoords = (self.bounding_box.max_x, self.bounding_box.min_y, z).into(); // lower right corner
|
||||||
|
|
||||||
|
let aligned_start = start.into_aligned();
|
||||||
|
let aligned_end = end.into_aligned().to_lower_right();
|
||||||
|
|
||||||
|
let start_world = start.into_world(extent);
|
||||||
|
|
||||||
|
let dy = aligned_end.y - aligned_start.0.y;
|
||||||
|
let dx = aligned_end.x - aligned_start.0.x;
|
||||||
|
|
||||||
|
let anchor_x = start_world.x;
|
||||||
|
let anchor_y = start_world.y;
|
||||||
|
// red step
|
||||||
|
self.pattern.push(MaskInstanceUniform::new(
|
||||||
|
[anchor_x, anchor_y],
|
||||||
|
dx as f32,
|
||||||
|
dy as f32,
|
||||||
|
[1.0, 0.0, 0.0, 1.0],
|
||||||
|
));
|
||||||
|
|
||||||
|
// green step
|
||||||
|
self.vertical(dx, dy, anchor_x, anchor_y, extent);
|
||||||
|
|
||||||
|
// blue step
|
||||||
|
self.horizontal(dx, dy, anchor_x, anchor_y, extent);
|
||||||
|
|
||||||
|
// violet step
|
||||||
|
self.vertical(dx, dy, anchor_x, anchor_y, extent);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user