init
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "hcie-vector"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
hcie-protocol = { path = "../hcie-protocol" }
|
||||
hcie-blend = { path = "../hcie-blend" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
bincode = "1.3"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.23"
|
||||
proptest = "1.5"
|
||||
approx = "0.5"
|
||||
egui = "0.34"
|
||||
eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow"] }
|
||||
@@ -0,0 +1,207 @@
|
||||
# hcie-vector
|
||||
|
||||
HCIE projelerinde kullanılan vektör şekil rasterizasyon motoru. 16 farklı şekil türünü piksel düzeyinde render eder, polyline dolgusu, kenar yumuşatma (hardness), döndürme ve boolean birleşim işlemlerini destekler. Ayrıca C-ABI/FFI üzerinden dinamik kütüphane olarak harici projelerden kullanılabilir.
|
||||
|
||||
## Kütüphane İçeriği
|
||||
|
||||
### Şekil Türleri (`VectorShape`)
|
||||
|
||||
| Şekil | Alanları |
|
||||
|---|---|
|
||||
| `Line` | x1, y1, x2, y2, stroke, color, angle, cap_start, cap_end, opacity, hardness |
|
||||
| `Rect` | x1, y1, x2, y2, stroke, color, fill_color, fill, radius, angle, opacity, hardness |
|
||||
| `Circle` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness |
|
||||
| `Arrow` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness, thick |
|
||||
| `Star` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness, points, inner_radius |
|
||||
| `Polygon` | x1, y1, x2, y2, stroke, color, fill_color, fill, sides, angle, opacity, hardness |
|
||||
| `Rhombus` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness |
|
||||
| `Cylinder` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness |
|
||||
| `Heart` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness |
|
||||
| `Bubble` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness |
|
||||
| `Gear` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness |
|
||||
| `Cross` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness |
|
||||
| `Crescent` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness |
|
||||
| `Bolt` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness |
|
||||
| `Arrow4` | x1, y1, x2, y2, stroke, color, fill_color, fill, angle, opacity, hardness |
|
||||
| `FreePath` | pts, closed, stroke, color, fill_color, fill, opacity, hardness |
|
||||
|
||||
### Public API
|
||||
|
||||
```rust
|
||||
// Şekilleri bir Layer'ın piksel tamponuna render eder
|
||||
pub fn render_vector_shapes(layer: &mut Layer);
|
||||
|
||||
// Vektör-only katmanlar için piksel tamponunu temizler (0x00 ile doldurur)
|
||||
pub fn render_vector_clear_bg(layer: &mut Layer);
|
||||
|
||||
// Şekli delta derece kadar döndürür
|
||||
pub fn rotate_shape(shape: &mut VectorShape, delta_degrees: f32);
|
||||
|
||||
// Şeklin açısını derece cinsinden belirler
|
||||
pub fn set_shape_angle(shape: &mut VectorShape, new_angle_degrees: f32);
|
||||
```
|
||||
|
||||
### Path Boolean Modülü (`path_boolean`)
|
||||
|
||||
```rust
|
||||
pub enum BooleanOp { Union, Intersect, Subtract, Xor }
|
||||
|
||||
// İki FreePath nokta dizisi üzerinde boolean işlemi
|
||||
pub fn boolean_path(op: BooleanOp, path_a: &[[f32; 2]], path_b: &[[f32; 2]]) -> Vec<[f32; 2]>;
|
||||
|
||||
// İki FreePath VectorShape üzerinde boolean işlemi
|
||||
pub fn boolean_shapes(op: BooleanOp, a: &VectorShape, b: &VectorShape) -> Option<VectorShape>;
|
||||
```
|
||||
|
||||
### FFI/C-ABI Arayüzü
|
||||
|
||||
Kütüphane `cdylib` olarak derlendiğinde şu fonksiyonlar dışa aktarılır:
|
||||
|
||||
```c
|
||||
// Şekil verilerini deserialize edip piksel tamponuna render eder
|
||||
bool vector_render_shapes(
|
||||
uint8_t* pixels_ptr, size_t pixels_len,
|
||||
uint32_t width, uint32_t height,
|
||||
const uint8_t* shapes_data_ptr, size_t shapes_data_len
|
||||
);
|
||||
|
||||
// İki FreePath şekli üzerinde boolean işlemi
|
||||
bool vector_boolean_shapes(
|
||||
int32_t op_val, // 0=Union, 1=Intersect, 2=Subtract, 3=Xor
|
||||
const uint8_t* shape_a_ptr, size_t shape_a_len,
|
||||
const uint8_t* shape_b_ptr, size_t shape_b_len,
|
||||
uint8_t** out_buffer_ptr, size_t* out_buffer_len
|
||||
);
|
||||
|
||||
// Boolean işleminden dönen tamponu serbest bırakır
|
||||
void free_buffer_c(uint8_t* ptr, size_t len);
|
||||
```
|
||||
|
||||
## Derleme
|
||||
|
||||
### Geliştirme (debug)
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
```
|
||||
|
||||
### Release
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
### Dinamik kütüphane (cdylib)
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
# Çıktı: target/release/libhcie_vector.so (Linux)
|
||||
# Çıktı: target/release/libhcie_vector.dylib (macOS)
|
||||
# Çıktı: target/release/hcie_vector.dll (Windows)
|
||||
```
|
||||
|
||||
## Test Çalıştırma
|
||||
|
||||
### Birim testler
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
### Etkileşimli GUI testi
|
||||
|
||||
```bash
|
||||
cargo run --example vector_test
|
||||
```
|
||||
|
||||
GUI özellikleri:
|
||||
- **Shape seçimi**: 16 şekil türü (Line, Rect, Circle, Arrow, Star, Polygon, Rhombus, Cylinder, Heart, Bubble, Gear, Cross, Crescent, Bolt, Arrow4, FreePath)
|
||||
- **Parametre slider'ları**: Stroke, Angle, Opacity, Hardness, Corner Radius, Star Points, Inner Radius, Polygon Sides, Line Cap, Thick Arrow — şekle göre aktif/pasif
|
||||
- **Stroke ve Fill renkleri**: RGBA slider'lar
|
||||
- **Canvas boyutu**: 512×512, 1024×1024, 2048×2048, 4096×4096
|
||||
- **FPS limitleyici**: 30, 60, 165, ∞
|
||||
- **FPS sayacı ve tile/dirty durumu**
|
||||
- **Clear Canvas** butonu
|
||||
|
||||
## Kaynak Kodu Olmadan Kullanım (FFI)
|
||||
|
||||
Kaynak koda erişimi olmayan projelerde `cdylib` olarak derlenmiş `.so`/`.dylib`/`.dll` dosyası ile kullanılabilir.
|
||||
|
||||
### Örnek: C üzerinden şekil render
|
||||
|
||||
```c
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// .so dosyasını linkleyin: -lhcie_vector
|
||||
extern bool vector_render_shapes(
|
||||
uint8_t* pixels_ptr, size_t pixels_len,
|
||||
uint32_t width, uint32_t height,
|
||||
const uint8_t* shapes_data_ptr, size_t shapes_data_len
|
||||
);
|
||||
|
||||
extern void free_buffer_c(uint8_t* ptr, size_t len);
|
||||
|
||||
int main() {
|
||||
uint32_t w = 800, h = 600;
|
||||
size_t px_len = (size_t)w * h * 4;
|
||||
uint8_t* pixels = malloc(px_len);
|
||||
memset(pixels, 255, px_len); // beyaz arkaplan
|
||||
|
||||
// bincode ile serialize edilmiş Vec<VectorShape> verisi
|
||||
// (örneğin Rust tarafında bincode::serialize(&shapes) ile üretilir)
|
||||
uint8_t* shapes_data = ...;
|
||||
size_t shapes_data_len = ...;
|
||||
|
||||
bool ok = vector_render_shapes(pixels, px_len, w, h, shapes_data, shapes_data_len);
|
||||
if (ok) {
|
||||
// pixels artık render edilmiş şekilleri içerir
|
||||
printf("Render başarılı\n");
|
||||
}
|
||||
|
||||
free(pixels);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Örnek: C üzerinden boolean işlemi
|
||||
|
||||
```c
|
||||
extern bool vector_boolean_shapes(
|
||||
int32_t op_val,
|
||||
const uint8_t* shape_a_ptr, size_t shape_a_len,
|
||||
const uint8_t* shape_b_ptr, size_t shape_b_len,
|
||||
uint8_t** out_buffer_ptr, size_t* out_buffer_len
|
||||
);
|
||||
|
||||
// Union (0), Intersect (1), Subtract (2), Xor (3)
|
||||
uint8_t* result_buf = NULL;
|
||||
size_t result_len = 0;
|
||||
bool ok = vector_boolean_shapes(0, shape_a, shape_a_len, shape_b, shape_b_len, &result_buf, &result_len);
|
||||
if (ok) {
|
||||
// result_buf -> bincode deserialize -> VectorShape
|
||||
free_buffer_c(result_buf, result_len);
|
||||
}
|
||||
```
|
||||
|
||||
## Bağımlılıklar
|
||||
|
||||
| Kütüphane | Açıklama |
|
||||
|---|---|
|
||||
| `hcie-protocol` | `Layer`, `VectorShape`, `LineCap` tipleri |
|
||||
| `hcie-blend` | `blend_pixels`, `BlendMode` — piksel karıştırma motoru |
|
||||
| `serde` | Serileştirme (Serialize, Deserialize) |
|
||||
| `bincode` | FFI için binary serileştirme |
|
||||
|
||||
## Proje Yapısı
|
||||
|
||||
```
|
||||
hcie-vector/
|
||||
├── Cargo.toml
|
||||
├── src/
|
||||
│ ├── lib.rs # Ana render mantığı, FFI giriş noktaları
|
||||
│ └── path_boolean.rs # Boolean birleşim işlemleri (Union, Intersect, Subtract, Xor)
|
||||
└── examples/
|
||||
└── vector_test.rs # Etkileşimli GUI test uygulaması
|
||||
```
|
||||
@@ -0,0 +1,846 @@
|
||||
#![allow(unreachable_patterns, unused_assignments, unused_must_use)]
|
||||
#![allow(deprecated)]
|
||||
use eframe::egui;
|
||||
use eframe::egui::Color32;
|
||||
use egui::ColorImage;
|
||||
use hcie_protocol::{Layer, LayerData, VectorShape, LineCap};
|
||||
use hcie_vector::render_vector_shapes;
|
||||
|
||||
const TILE_SIZE: u32 = 512;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum CanvasPreset { S512, S1024, S2048, S4096 }
|
||||
|
||||
impl CanvasPreset {
|
||||
fn size(self) -> (u32, u32) {
|
||||
match self {
|
||||
Self::S512 => (512, 512),
|
||||
Self::S1024 => (1024, 1024),
|
||||
Self::S2048 => (2048, 2048),
|
||||
Self::S4096 => (4096, 4096),
|
||||
}
|
||||
}
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::S512 => "512×512",
|
||||
Self::S1024 => "1024×1024",
|
||||
Self::S2048 => "2048×2048",
|
||||
Self::S4096 => "4096×4096",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum FpsCap { Unlimited, Fps30, Fps60, Fps165 }
|
||||
|
||||
impl FpsCap {
|
||||
fn value(self) -> Option<f32> {
|
||||
match self {
|
||||
Self::Fps30 => Some(30.0),
|
||||
Self::Fps60 => Some(60.0),
|
||||
Self::Fps165 => Some(165.0),
|
||||
Self::Unlimited => None,
|
||||
}
|
||||
}
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Fps30 => "30",
|
||||
Self::Fps60 => "60",
|
||||
Self::Fps165 => "165",
|
||||
Self::Unlimited => "∞",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Tile {
|
||||
texture: Option<egui::TextureHandle>,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
struct TiledCanvas {
|
||||
pixels: Vec<u8>,
|
||||
tiles: Vec<Tile>,
|
||||
w: u32,
|
||||
h: u32,
|
||||
cols: u32,
|
||||
dirty_count: usize,
|
||||
}
|
||||
|
||||
impl TiledCanvas {
|
||||
fn new(w: u32, h: u32) -> Self {
|
||||
let cols = w.div_ceil(TILE_SIZE);
|
||||
let rows = h.div_ceil(TILE_SIZE);
|
||||
let total = (cols * rows) as usize;
|
||||
Self {
|
||||
pixels: vec![255; (w * h * 4) as usize],
|
||||
tiles: (0..total).map(|_| Tile { texture: None, dirty: true }).collect(),
|
||||
w,
|
||||
h,
|
||||
cols,
|
||||
dirty_count: total,
|
||||
}
|
||||
}
|
||||
|
||||
fn resize(&mut self, w: u32, h: u32) {
|
||||
*self = Self::new(w, h);
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.pixels.fill(255);
|
||||
self.mark_all_dirty();
|
||||
}
|
||||
|
||||
fn mark_dirty_rect(&mut self, cx: f32, cy: f32, radius: f32) {
|
||||
let tx0 = ((cx - radius).max(0.0) / TILE_SIZE as f32) as u32;
|
||||
let tx1 = (((cx + radius).min(self.w as f32 - 1.0) / TILE_SIZE as f32) as u32)
|
||||
.min(self.cols.saturating_sub(1));
|
||||
let ty0 = ((cy - radius).max(0.0) / TILE_SIZE as f32) as u32;
|
||||
let ty1 = (((cy + radius).min(self.h as f32 - 1.0) / TILE_SIZE as f32) as u32)
|
||||
.min(self.h.div_ceil(TILE_SIZE).saturating_sub(1));
|
||||
for ty in ty0..=ty1 {
|
||||
for tx in tx0..=tx1 {
|
||||
let idx = (ty * self.cols + tx) as usize;
|
||||
if !self.tiles[idx].dirty {
|
||||
self.tiles[idx].dirty = true;
|
||||
self.dirty_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_all_dirty(&mut self) {
|
||||
for t in &mut self.tiles { t.dirty = true; }
|
||||
self.dirty_count = self.tiles.len();
|
||||
}
|
||||
|
||||
fn upload_dirty(&mut self, ctx: &egui::Context) {
|
||||
if self.dirty_count == 0 { return; }
|
||||
let rows = self.h.div_ceil(TILE_SIZE);
|
||||
for ty in 0..rows {
|
||||
let tile_h = if ty == rows - 1 {
|
||||
self.h - ty * TILE_SIZE
|
||||
} else {
|
||||
TILE_SIZE
|
||||
} as usize;
|
||||
for tx in 0..self.cols {
|
||||
let idx = (ty * self.cols + tx) as usize;
|
||||
if !self.tiles[idx].dirty { continue; }
|
||||
self.tiles[idx].dirty = false;
|
||||
self.dirty_count = self.dirty_count.saturating_sub(1);
|
||||
|
||||
let tile_w = if tx == self.cols - 1 {
|
||||
self.w - tx * TILE_SIZE
|
||||
} else {
|
||||
TILE_SIZE
|
||||
} as usize;
|
||||
|
||||
let px = (tx * TILE_SIZE) as usize;
|
||||
let py = (ty * TILE_SIZE) as usize;
|
||||
let cap = tile_w * tile_h * 4;
|
||||
let mut data = vec![0u8; cap];
|
||||
for row in 0..tile_h {
|
||||
let src = ((py + row) * self.w as usize + px) * 4;
|
||||
let dst = row * tile_w * 4;
|
||||
data[dst..dst + tile_w * 4]
|
||||
.copy_from_slice(&self.pixels[src..src + tile_w * 4]);
|
||||
}
|
||||
|
||||
let img = ColorImage::from_rgba_unmultiplied([tile_w, tile_h], &data);
|
||||
let key = format!("t{}_{}", tx, ty);
|
||||
if let Some(tex) = &mut self.tiles[idx].texture {
|
||||
tex.set(img, egui::TextureOptions::NEAREST);
|
||||
} else {
|
||||
self.tiles[idx].texture =
|
||||
Some(ctx.load_texture(key, img, egui::TextureOptions::NEAREST));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ShapeKind {
|
||||
Line,
|
||||
Rect,
|
||||
Circle,
|
||||
Arrow,
|
||||
Star,
|
||||
Polygon,
|
||||
Rhombus,
|
||||
Cylinder,
|
||||
Heart,
|
||||
Bubble,
|
||||
Gear,
|
||||
Cross,
|
||||
Crescent,
|
||||
Bolt,
|
||||
Arrow4,
|
||||
FreePath,
|
||||
}
|
||||
|
||||
impl ShapeKind {
|
||||
fn all() -> &'static [ShapeKind] {
|
||||
&[
|
||||
ShapeKind::Line,
|
||||
ShapeKind::Rect,
|
||||
ShapeKind::Circle,
|
||||
ShapeKind::Arrow,
|
||||
ShapeKind::Star,
|
||||
ShapeKind::Polygon,
|
||||
ShapeKind::Rhombus,
|
||||
ShapeKind::Cylinder,
|
||||
ShapeKind::Heart,
|
||||
ShapeKind::Bubble,
|
||||
ShapeKind::Gear,
|
||||
ShapeKind::Cross,
|
||||
ShapeKind::Crescent,
|
||||
ShapeKind::Bolt,
|
||||
ShapeKind::Arrow4,
|
||||
ShapeKind::FreePath,
|
||||
]
|
||||
}
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
ShapeKind::Line => "Line",
|
||||
ShapeKind::Rect => "Rect",
|
||||
ShapeKind::Circle => "Circle",
|
||||
ShapeKind::Arrow => "Arrow",
|
||||
ShapeKind::Star => "Star",
|
||||
ShapeKind::Polygon => "Polygon",
|
||||
ShapeKind::Rhombus => "Rhombus",
|
||||
ShapeKind::Cylinder => "Cylinder",
|
||||
ShapeKind::Heart => "Heart",
|
||||
ShapeKind::Bubble => "Bubble",
|
||||
ShapeKind::Gear => "Gear",
|
||||
ShapeKind::Cross => "Cross",
|
||||
ShapeKind::Crescent => "Crescent",
|
||||
ShapeKind::Bolt => "Bolt",
|
||||
ShapeKind::Arrow4 => "Arrow4",
|
||||
ShapeKind::FreePath => "FreePath",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct SliderCaps {
|
||||
stroke: bool,
|
||||
angle: bool,
|
||||
opacity: bool,
|
||||
hardness: bool,
|
||||
fill: bool,
|
||||
fill_color: bool,
|
||||
rect_radius: bool,
|
||||
star_points: bool,
|
||||
star_inner_radius: bool,
|
||||
polygon_sides: bool,
|
||||
arrow_thick: bool,
|
||||
line_cap: bool,
|
||||
}
|
||||
|
||||
impl SliderCaps {
|
||||
fn for_shape(kind: ShapeKind) -> Self {
|
||||
match kind {
|
||||
ShapeKind::Line => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: false, fill_color: false, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: true,
|
||||
},
|
||||
ShapeKind::Rect => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: true,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Circle => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Arrow => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: true, line_cap: false,
|
||||
},
|
||||
ShapeKind::Star => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: true, star_inner_radius: true, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Polygon => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: true,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Rhombus => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Cylinder => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Heart => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Bubble => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Gear => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Cross => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Crescent => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Bolt => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::Arrow4 => SliderCaps {
|
||||
stroke: true, angle: true, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
ShapeKind::FreePath => SliderCaps {
|
||||
stroke: true, angle: false, opacity: true, hardness: true,
|
||||
fill: true, fill_color: true, rect_radius: false,
|
||||
star_points: false, star_inner_radius: false, polygon_sides: false,
|
||||
arrow_thick: false, line_cap: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
canvas: TiledCanvas,
|
||||
canvas_preset: CanvasPreset,
|
||||
base_pixels: Vec<u8>,
|
||||
shape_kind: ShapeKind,
|
||||
stroke: f32,
|
||||
color: [u8; 4],
|
||||
fill_color: [u8; 4],
|
||||
fill: bool,
|
||||
angle: f32,
|
||||
opacity: f32,
|
||||
hardness: f32,
|
||||
star_points: u32,
|
||||
star_inner_radius: f32,
|
||||
polygon_sides: u32,
|
||||
rect_radius: f32,
|
||||
arrow_thick: bool,
|
||||
line_cap: LineCap,
|
||||
drag_start: Option<egui::Pos2>,
|
||||
shape_count: u64,
|
||||
fps_cap: FpsCap,
|
||||
last_frame: std::time::Instant,
|
||||
fps: f32,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
let preset = CanvasPreset::S1024;
|
||||
let (w, h) = preset.size();
|
||||
let canvas = TiledCanvas::new(w, h);
|
||||
let base_pixels = canvas.pixels.clone();
|
||||
let now = std::time::Instant::now();
|
||||
Self {
|
||||
canvas,
|
||||
canvas_preset: preset,
|
||||
base_pixels,
|
||||
shape_kind: ShapeKind::Rect,
|
||||
stroke: 3.0,
|
||||
color: [0, 0, 0, 255],
|
||||
fill_color: [255, 128, 0, 200],
|
||||
fill: true,
|
||||
angle: 0.0,
|
||||
opacity: 1.0,
|
||||
hardness: 1.0,
|
||||
star_points: 5,
|
||||
star_inner_radius: 0.4,
|
||||
polygon_sides: 6,
|
||||
rect_radius: 0.0,
|
||||
arrow_thick: false,
|
||||
line_cap: LineCap::Round,
|
||||
drag_start: None,
|
||||
shape_count: 0,
|
||||
fps_cap: FpsCap::Fps60,
|
||||
last_frame: now,
|
||||
fps: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_canvas_size(&mut self, p: CanvasPreset) {
|
||||
if self.canvas_preset == p { return; }
|
||||
self.canvas_preset = p;
|
||||
let (w, h) = p.size();
|
||||
self.canvas.resize(w, h);
|
||||
self.base_pixels = self.canvas.pixels.clone();
|
||||
self.shape_count = 0;
|
||||
}
|
||||
|
||||
fn make_shape(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> VectorShape {
|
||||
match self.shape_kind {
|
||||
ShapeKind::Line => VectorShape::Line {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
angle: self.angle,
|
||||
cap_start: self.line_cap,
|
||||
cap_end: self.line_cap,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Rect => VectorShape::Rect {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
radius: self.rect_radius,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Circle => VectorShape::Circle {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Arrow => VectorShape::Arrow {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
thick: self.arrow_thick,
|
||||
},
|
||||
ShapeKind::Star => VectorShape::Star {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
points: self.star_points,
|
||||
inner_radius: self.star_inner_radius,
|
||||
},
|
||||
ShapeKind::Polygon => VectorShape::Polygon {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
sides: self.polygon_sides,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Rhombus => VectorShape::Rhombus {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Cylinder => VectorShape::Cylinder {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Heart => VectorShape::Heart {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Bubble => VectorShape::Bubble {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Gear => VectorShape::Gear {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Cross => VectorShape::Cross {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Crescent => VectorShape::Crescent {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Bolt => VectorShape::Bolt {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::Arrow4 => VectorShape::Arrow4 {
|
||||
x1, y1, x2, y2,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
angle: self.angle,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
},
|
||||
ShapeKind::FreePath => {
|
||||
let pts = vec![
|
||||
[x1, y1],
|
||||
[(x1 + x2) / 2.0, y1 - 50.0],
|
||||
[x2, y2],
|
||||
[(x1 + x2) / 2.0, y2 + 50.0],
|
||||
];
|
||||
VectorShape::FreePath {
|
||||
pts,
|
||||
closed: true,
|
||||
stroke: self.stroke,
|
||||
color: self.color,
|
||||
fill_color: self.fill_color,
|
||||
fill: self.fill,
|
||||
opacity: self.opacity,
|
||||
hardness: self.hardness,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
let ctx = ui.ctx().clone();
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
if let Some(limit) = self.fps_cap.value() {
|
||||
let interval = std::time::Duration::from_secs_f32(1.0 / limit);
|
||||
let elapsed = now - self.last_frame;
|
||||
if elapsed < interval {
|
||||
std::thread::sleep(interval - elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
let after_sleep = std::time::Instant::now();
|
||||
let dt = (after_sleep - self.last_frame).as_secs_f32();
|
||||
self.last_frame = after_sleep;
|
||||
if dt > 0.0 {
|
||||
self.fps = self.fps * 0.9 + (1.0 / dt) * 0.1;
|
||||
}
|
||||
|
||||
self.canvas.upload_dirty(&ctx);
|
||||
|
||||
let caps = SliderCaps::for_shape(self.shape_kind);
|
||||
|
||||
egui::Panel::left("panel").show(&ctx, |ui| {
|
||||
ui.heading("Vector Shape Test");
|
||||
let limit_label = match self.fps_cap {
|
||||
FpsCap::Fps30 => "30",
|
||||
FpsCap::Fps60 => "60",
|
||||
FpsCap::Fps165 => "165",
|
||||
FpsCap::Unlimited => "∞",
|
||||
};
|
||||
ui.label(format!(
|
||||
"FPS: {:.0}/{} │ dirty: {} │ shapes: {}",
|
||||
self.fps, limit_label, self.canvas.dirty_count, self.shape_count
|
||||
));
|
||||
ui.label(format!(
|
||||
"Canvas: {}×{} │ Tiles: {}×{}",
|
||||
self.canvas.w, self.canvas.h,
|
||||
self.canvas.cols,
|
||||
self.canvas.h.div_ceil(TILE_SIZE),
|
||||
));
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Canvas Size");
|
||||
egui::ComboBox::from_id_salt("canvas_preset")
|
||||
.selected_text(self.canvas_preset.name())
|
||||
.show_ui(ui, |ui| {
|
||||
for p in &[CanvasPreset::S512, CanvasPreset::S1024, CanvasPreset::S2048, CanvasPreset::S4096] {
|
||||
if ui.selectable_label(self.canvas_preset == *p, p.name()).clicked() {
|
||||
self.set_canvas_size(*p);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.label("FPS Cap");
|
||||
egui::ComboBox::from_id_salt("fps_cap")
|
||||
.selected_text(self.fps_cap.name())
|
||||
.show_ui(ui, |ui| {
|
||||
for c in &[FpsCap::Fps30, FpsCap::Fps60, FpsCap::Fps165, FpsCap::Unlimited] {
|
||||
if ui.selectable_label(self.fps_cap == *c, c.name()).clicked() {
|
||||
self.fps_cap = *c;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Shape");
|
||||
egui::ComboBox::from_id_salt("shape_kind")
|
||||
.selected_text(self.shape_kind.name())
|
||||
.show_ui(ui, |ui| {
|
||||
for &s in ShapeKind::all() {
|
||||
ui.selectable_value(&mut self.shape_kind, s, s.name());
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_enabled(caps.stroke, egui::Slider::new(&mut self.stroke, 1.0..=50.0).text("Stroke"));
|
||||
ui.add_enabled(caps.angle, egui::Slider::new(&mut self.angle, 0.0..=360.0).text("Angle (°)"));
|
||||
ui.add_enabled(caps.opacity, egui::Slider::new(&mut self.opacity, 0.0..=1.0).text("Opacity"));
|
||||
ui.add_enabled(caps.hardness, egui::Slider::new(&mut self.hardness, 0.0..=1.0).text("Hardness"));
|
||||
ui.add_enabled(caps.rect_radius, egui::Slider::new(&mut self.rect_radius, 0.0..=200.0).text("Corner Radius"));
|
||||
ui.add_enabled(caps.star_points, egui::Slider::new(&mut self.star_points, 3..=20).text("Star Points"));
|
||||
ui.add_enabled(caps.star_inner_radius, egui::Slider::new(&mut self.star_inner_radius, 0.1..=0.9).text("Inner Radius"));
|
||||
ui.add_enabled(caps.polygon_sides, egui::Slider::new(&mut self.polygon_sides, 3..=12).text("Polygon Sides"));
|
||||
ui.add_enabled(caps.arrow_thick, egui::Checkbox::new(&mut self.arrow_thick, "Thick Arrow"));
|
||||
|
||||
if caps.line_cap {
|
||||
ui.label("Line Cap");
|
||||
egui::ComboBox::from_id_salt("line_cap")
|
||||
.selected_text(self.line_cap.label())
|
||||
.show_ui(ui, |ui| {
|
||||
for &cap in LineCap::ALL {
|
||||
ui.selectable_value(&mut self.line_cap, cap, cap.label());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut rgba = [
|
||||
self.color[0] as f32 / 255.,
|
||||
self.color[1] as f32 / 255.,
|
||||
self.color[2] as f32 / 255.,
|
||||
self.color[3] as f32 / 255.,
|
||||
];
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Stroke Color");
|
||||
if ui.color_edit_button_rgba_unmultiplied(&mut rgba).changed() {
|
||||
self.color = [
|
||||
(rgba[0] * 255.) as u8, (rgba[1] * 255.) as u8,
|
||||
(rgba[2] * 255.) as u8, (rgba[3] * 255.) as u8,
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
let mut fill_rgba = [
|
||||
self.fill_color[0] as f32 / 255.,
|
||||
self.fill_color[1] as f32 / 255.,
|
||||
self.fill_color[2] as f32 / 255.,
|
||||
self.fill_color[3] as f32 / 255.,
|
||||
];
|
||||
ui.add_enabled(caps.fill, egui::Slider::new(&mut fill_rgba[0], 0.0..=1.0).text("Fill R"));
|
||||
ui.add_enabled(caps.fill, egui::Slider::new(&mut fill_rgba[1], 0.0..=1.0).text("Fill G"));
|
||||
ui.add_enabled(caps.fill, egui::Slider::new(&mut fill_rgba[2], 0.0..=1.0).text("Fill B"));
|
||||
ui.add_enabled(caps.fill, egui::Slider::new(&mut fill_rgba[3], 0.0..=1.0).text("Fill A"));
|
||||
self.fill_color = [
|
||||
(fill_rgba[0] * 255.) as u8, (fill_rgba[1] * 255.) as u8,
|
||||
(fill_rgba[2] * 255.) as u8, (fill_rgba[3] * 255.) as u8,
|
||||
];
|
||||
ui.add_enabled(caps.fill, egui::Checkbox::new(&mut self.fill, "Fill"));
|
||||
|
||||
ui.separator();
|
||||
if ui.button("Clear Canvas").clicked() {
|
||||
self.canvas.clear();
|
||||
self.base_pixels = self.canvas.pixels.clone();
|
||||
self.shape_count = 0;
|
||||
}
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(&ctx, |ui| {
|
||||
let available = ui.available_size_before_wrap();
|
||||
let view_w = available.x.max(64.0);
|
||||
let view_h = available.y.max(64.0);
|
||||
let scale = (view_w / self.canvas.w as f32).min(view_h / self.canvas.h as f32);
|
||||
let disp_w = self.canvas.w as f32 * scale;
|
||||
let disp_h = self.canvas.h as f32 * scale;
|
||||
|
||||
let (rect, resp) =
|
||||
ui.allocate_exact_size(egui::vec2(disp_w, disp_h), egui::Sense::click_and_drag());
|
||||
|
||||
if self.canvas.tiles.first().and_then(|t| t.texture.as_ref()).is_some() {
|
||||
let rows = self.canvas.h.div_ceil(TILE_SIZE);
|
||||
for ty in 0..rows {
|
||||
for tx in 0..self.canvas.cols {
|
||||
let idx = (ty * self.canvas.cols + tx) as usize;
|
||||
if let Some(tex) = &self.canvas.tiles[idx].texture {
|
||||
let tile_w = if tx == self.canvas.cols - 1 {
|
||||
self.canvas.w - tx * TILE_SIZE
|
||||
} else {
|
||||
TILE_SIZE
|
||||
} as f32;
|
||||
let tile_h = if ty == rows - 1 {
|
||||
self.canvas.h - ty * TILE_SIZE
|
||||
} else {
|
||||
TILE_SIZE
|
||||
} as f32;
|
||||
let src_x = (tx * TILE_SIZE) as f32 * scale;
|
||||
let src_y = (ty * TILE_SIZE) as f32 * scale;
|
||||
let r = egui::Rect::from_min_size(
|
||||
rect.min + egui::vec2(src_x, src_y),
|
||||
egui::vec2(tile_w * scale, tile_h * scale),
|
||||
);
|
||||
ui.painter().image(
|
||||
tex.id(),
|
||||
r,
|
||||
egui::Rect::from_min_max(egui::pos2(0., 0.), egui::pos2(1., 1.)),
|
||||
Color32::WHITE,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resp.drag_started() {
|
||||
self.base_pixels = self.canvas.pixels.clone();
|
||||
self.drag_start = resp.interact_pointer_pos().or(resp.hover_pos()).or(Some(rect.min));
|
||||
}
|
||||
|
||||
if let Some(start) = self.drag_start {
|
||||
if resp.drag_stopped() {
|
||||
if let Some(end_pos) = resp.hover_pos() {
|
||||
let sx = (start.x - rect.min.x) / scale;
|
||||
let sy = (start.y - rect.min.y) / scale;
|
||||
let ex = (end_pos.x - rect.min.x) / scale;
|
||||
let ey = (end_pos.y - rect.min.y) / scale;
|
||||
|
||||
if sx >= 0.0 && sy >= 0.0 && sx < self.canvas.w as f32
|
||||
&& sy >= 0.0 && sy < self.canvas.h as f32
|
||||
&& ex >= 0.0 && ey >= 0.0 && ex < self.canvas.w as f32
|
||||
&& ey >= 0.0 && ey < self.canvas.h as f32
|
||||
{
|
||||
self.canvas.pixels.copy_from_slice(&self.base_pixels);
|
||||
let shape = self.make_shape(sx, sy, ex, ey);
|
||||
let mut layer = Layer::from_rgba("test", self.canvas.w, self.canvas.h, self.canvas.pixels.clone());
|
||||
layer.data = LayerData::Vector { shapes: vec![shape] };
|
||||
render_vector_shapes(&mut layer);
|
||||
self.canvas.pixels.copy_from_slice(&layer.pixels);
|
||||
self.base_pixels = self.canvas.pixels.clone();
|
||||
|
||||
let max_x = sx.max(ex);
|
||||
let min_x = sx.min(ex);
|
||||
let max_y = sy.max(ey);
|
||||
let min_y = sy.min(ey);
|
||||
let radius = self.stroke + 20.0;
|
||||
self.canvas.mark_dirty_rect(min_x, min_y, radius);
|
||||
self.canvas.mark_dirty_rect(max_x, max_y, radius);
|
||||
self.canvas.mark_dirty_rect((min_x + max_x) / 2.0, (min_y + max_y) / 2.0, (max_x - min_x).max(max_y - min_y) / 2.0 + radius);
|
||||
self.canvas.upload_dirty(&ctx);
|
||||
self.shape_count += 1;
|
||||
}
|
||||
}
|
||||
self.drag_start = None;
|
||||
} else {
|
||||
let start = start;
|
||||
if let Some(cur) = resp.hover_pos() {
|
||||
let sx = (start.x - rect.min.x) / scale;
|
||||
let sy = (start.y - rect.min.y) / scale;
|
||||
let ex = (cur.x - rect.min.x) / scale;
|
||||
let ey = (cur.y - rect.min.y) / scale;
|
||||
|
||||
self.canvas.pixels.copy_from_slice(&self.base_pixels);
|
||||
let shape = self.make_shape(sx, sy, ex, ey);
|
||||
let mut layer = Layer::from_rgba("preview", self.canvas.w, self.canvas.h, self.canvas.pixels.clone());
|
||||
layer.data = LayerData::Vector { shapes: vec![shape] };
|
||||
render_vector_shapes(&mut layer);
|
||||
self.canvas.pixels.copy_from_slice(&layer.pixels);
|
||||
self.canvas.mark_all_dirty();
|
||||
self.canvas.upload_dirty(&ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result<()> {
|
||||
eframe::run_native(
|
||||
"HCIE Vector Engine — Shape Test",
|
||||
eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size([1400., 900.]),
|
||||
vsync: true,
|
||||
..Default::default()
|
||||
},
|
||||
Box::new(|cc| Ok(Box::new(App::new(cc)))),
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,409 @@
|
||||
use hcie_protocol::VectorShape;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum BooleanOp {
|
||||
Union,
|
||||
Intersect,
|
||||
Subtract,
|
||||
Xor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
struct Vec2 {
|
||||
x: f64,
|
||||
y: f64,
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
fn new(x: f64, y: f64) -> Self { Self { x, y } }
|
||||
fn from_pt(p: &[f32; 2]) -> Self { Self { x: p[0] as f64, y: p[1] as f64 } }
|
||||
}
|
||||
|
||||
fn cross(a: Vec2, b: Vec2, c: Vec2) -> f64 {
|
||||
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
|
||||
}
|
||||
|
||||
fn is_convex(poly: &[Vec2]) -> bool {
|
||||
if poly.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
let n = poly.len();
|
||||
let mut sign: Option<bool> = None;
|
||||
for i in 0..n {
|
||||
let a = poly[i];
|
||||
let b = poly[(i + 1) % n];
|
||||
let c = poly[(i + 2) % n];
|
||||
let v = cross(a, b, c);
|
||||
if v.abs() > 1e-12 {
|
||||
let pos = v > 0.0;
|
||||
match sign {
|
||||
Some(s) if s != pos => return false,
|
||||
_ => sign = Some(pos),
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn is_clockwise(poly: &[Vec2]) -> bool {
|
||||
if poly.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
let mut sum = 0.0;
|
||||
for i in 0..poly.len() {
|
||||
let a = poly[i];
|
||||
let b = poly[(i + 1) % poly.len()];
|
||||
sum += (b.x - a.x) * (b.y + a.y);
|
||||
}
|
||||
sum > 0.0
|
||||
}
|
||||
|
||||
fn ensure_ccw(poly: &[Vec2]) -> Vec<Vec2> {
|
||||
if is_clockwise(poly) {
|
||||
let mut rev = poly.to_vec();
|
||||
rev.reverse();
|
||||
rev
|
||||
} else {
|
||||
poly.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
fn intersect_lines(a1: Vec2, a2: Vec2, b1: Vec2, b2: Vec2) -> Option<Vec2> {
|
||||
let d = (a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x);
|
||||
if d.abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let t = ((a1.x - b1.x) * (b1.y - b2.y) - (a1.y - b1.y) * (b1.x - b2.x)) / d;
|
||||
let px = a1.x + t * (a2.x - a1.x);
|
||||
let py = a1.y + t * (a2.y - a1.y);
|
||||
let t2 = ((b1.x - a1.x) * (a1.y - a2.y) + (b1.y - a1.y) * (a2.x - a1.x)) / (-d);
|
||||
if t >= 0.0 && t <= 1.0 && t2 >= 0.0 && t2 <= 1.0 {
|
||||
Some(Vec2::new(px, py))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn point_inside_convex(p: Vec2, poly: &[Vec2]) -> bool {
|
||||
for i in 0..poly.len() {
|
||||
let a = poly[i];
|
||||
let b = poly[(i + 1) % poly.len()];
|
||||
if cross(a, b, p) < -1e-9 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn clip_convex(subject: &[Vec2], clip: &[Vec2]) -> Vec<Vec2> {
|
||||
let mut output = subject.to_vec();
|
||||
for i in 0..clip.len() {
|
||||
if output.is_empty() {
|
||||
return output;
|
||||
}
|
||||
let clip_a = clip[i];
|
||||
let clip_b = clip[(i + 1) % clip.len()];
|
||||
let input = output;
|
||||
output = Vec::new();
|
||||
if input.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut s = *input.last().unwrap();
|
||||
for &e in &input {
|
||||
let s_inside = cross(clip_a, clip_b, s) >= -1e-9;
|
||||
let e_inside = cross(clip_a, clip_b, e) >= -1e-9;
|
||||
if e_inside {
|
||||
if !s_inside {
|
||||
if let Some(pt) = intersect_lines(clip_a, clip_b, s, e) {
|
||||
output.push(pt);
|
||||
}
|
||||
}
|
||||
output.push(e);
|
||||
} else if s_inside {
|
||||
if let Some(pt) = intersect_lines(clip_a, clip_b, s, e) {
|
||||
output.push(pt);
|
||||
}
|
||||
}
|
||||
s = e;
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn polygon_union_convex(a: &[Vec2], b: &[Vec2]) -> Vec<Vec2> {
|
||||
let a = ensure_ccw(a);
|
||||
let b = ensure_ccw(b);
|
||||
|
||||
let mut all_pts: Vec<Vec2> = Vec::new();
|
||||
|
||||
for &p in &a {
|
||||
if !point_inside_convex(p, &b) {
|
||||
all_pts.push(p);
|
||||
}
|
||||
}
|
||||
for &p in &b {
|
||||
if !point_inside_convex(p, &a) {
|
||||
all_pts.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..a.len() {
|
||||
let a1 = a[i];
|
||||
let a2 = a[(i + 1) % a.len()];
|
||||
for j in 0..b.len() {
|
||||
let b1 = b[j];
|
||||
let b2 = b[(j + 1) % b.len()];
|
||||
if let Some(pt) = intersect_lines(a1, a2, b1, b2) {
|
||||
all_pts.push(pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if all_pts.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let cx: f64 = all_pts.iter().map(|p| p.x).sum::<f64>() / all_pts.len() as f64;
|
||||
let cy: f64 = all_pts.iter().map(|p| p.y).sum::<f64>() / all_pts.len() as f64;
|
||||
|
||||
let mut angle_pts: Vec<(usize, f64)> = all_pts.iter().enumerate().map(|(i, p)| {
|
||||
(i, (p.y - cy).atan2(p.x - cx))
|
||||
}).collect();
|
||||
angle_pts.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
|
||||
angle_pts.iter().map(|&(i, _)| all_pts[i]).collect()
|
||||
}
|
||||
|
||||
fn polygon_intersect_convex(a: &[Vec2], b: &[Vec2]) -> Vec<Vec2> {
|
||||
let a = ensure_ccw(a);
|
||||
let b = ensure_ccw(b);
|
||||
let result = clip_convex(&a, &b);
|
||||
if result.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn polygon_subtract_convex(a: &[Vec2], b: &[Vec2]) -> Vec<Vec2> {
|
||||
let a = ensure_ccw(a);
|
||||
let mut b_rev = b.to_vec();
|
||||
b_rev.reverse();
|
||||
let result = clip_convex(&a, &b_rev);
|
||||
if result.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn point_in_polygon(p: Vec2, poly: &[Vec2]) -> bool {
|
||||
let n = poly.len();
|
||||
let mut inside = false;
|
||||
for i in 0..n {
|
||||
let a = poly[i];
|
||||
let b = poly[(i + 1) % n];
|
||||
if ((a.y > p.y) != (b.y > p.y))
|
||||
&& (p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x)
|
||||
{
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
inside
|
||||
}
|
||||
|
||||
fn polygon_union_general(a: &[Vec2], b: &[Vec2]) -> Vec<Vec2> {
|
||||
let a = ensure_ccw(a);
|
||||
let b = ensure_ccw(b);
|
||||
|
||||
let mut points: Vec<Vec2> = Vec::new();
|
||||
|
||||
for &p in &a {
|
||||
if !point_in_polygon(p, &b) {
|
||||
points.push(p);
|
||||
}
|
||||
}
|
||||
for &p in &b {
|
||||
if !point_in_polygon(p, &a) {
|
||||
points.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..a.len() {
|
||||
let a1 = a[i];
|
||||
let a2 = a[(i + 1) % a.len()];
|
||||
for j in 0..b.len() {
|
||||
let b1 = b[j];
|
||||
let b2 = b[(j + 1) % b.len()];
|
||||
if let Some(pt) = intersect_lines(a1, a2, b1, b2) {
|
||||
points.push(pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if points.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let cx: f64 = points.iter().map(|p| p.x).sum::<f64>() / points.len() as f64;
|
||||
let cy: f64 = points.iter().map(|p| p.y).sum::<f64>() / points.len() as f64;
|
||||
let mut angle_pts: Vec<(usize, f64)> = points.iter().enumerate()
|
||||
.map(|(i, p)| (i, (p.y - cy).atan2(p.x - cx)))
|
||||
.collect();
|
||||
angle_pts.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
angle_pts.iter().map(|&(i, _)| points[i]).collect()
|
||||
}
|
||||
|
||||
fn polygon_intersect_concave(a: &[Vec2], b: &[Vec2]) -> Vec<Vec2> {
|
||||
let a_ccw = ensure_ccw(a);
|
||||
let b_ccw = ensure_ccw(b);
|
||||
let mut result = Vec::new();
|
||||
|
||||
for i in 0..a_ccw.len() {
|
||||
let p = a_ccw[i];
|
||||
if point_in_polygon(p, &b_ccw) {
|
||||
result.push(p);
|
||||
}
|
||||
}
|
||||
for i in 0..b_ccw.len() {
|
||||
let p = b_ccw[i];
|
||||
if point_in_polygon(p, &a_ccw) {
|
||||
result.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..a_ccw.len() {
|
||||
let a1 = a_ccw[i];
|
||||
let a2 = a_ccw[(i + 1) % a_ccw.len()];
|
||||
for j in 0..b_ccw.len() {
|
||||
let b1 = b_ccw[j];
|
||||
let b2 = b_ccw[(j + 1) % b_ccw.len()];
|
||||
if let Some(pt) = intersect_lines(a1, a2, b1, b2) {
|
||||
result.push(pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let cx: f64 = result.iter().map(|p| p.x).sum::<f64>() / result.len() as f64;
|
||||
let cy: f64 = result.iter().map(|p| p.y).sum::<f64>() / result.len() as f64;
|
||||
let mut angle_pts: Vec<(usize, f64)> = result.iter().enumerate()
|
||||
.map(|(i, p)| (i, (p.y - cy).atan2(p.x - cx)))
|
||||
.collect();
|
||||
angle_pts.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
angle_pts.iter().map(|&(i, _)| result[i]).collect()
|
||||
}
|
||||
|
||||
fn polygon_subtract_concave(a: &[Vec2], b: &[Vec2]) -> Vec<Vec2> {
|
||||
let a_ccw = ensure_ccw(a);
|
||||
let b_cw: Vec<Vec2> = {
|
||||
let mut rev = b.to_vec();
|
||||
rev.reverse();
|
||||
rev
|
||||
};
|
||||
let mut result = Vec::new();
|
||||
|
||||
for i in 0..a_ccw.len() {
|
||||
let p = a_ccw[i];
|
||||
if !point_in_polygon(p, &b_cw) {
|
||||
result.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..a_ccw.len() {
|
||||
let a1 = a_ccw[i];
|
||||
let a2 = a_ccw[(i + 1) % a_ccw.len()];
|
||||
for j in 0..b_cw.len() {
|
||||
let b1 = b_cw[j];
|
||||
let b2 = b_cw[(j + 1) % b_cw.len()];
|
||||
if let Some(pt) = intersect_lines(a1, a2, b1, b2) {
|
||||
result.push(pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let cx: f64 = result.iter().map(|p| p.x).sum::<f64>() / result.len() as f64;
|
||||
let cy: f64 = result.iter().map(|p| p.y).sum::<f64>() / result.len() as f64;
|
||||
let mut angle_pts: Vec<(usize, f64)> = result.iter().enumerate()
|
||||
.map(|(i, p)| (i, (p.y - cy).atan2(p.x - cx)))
|
||||
.collect();
|
||||
angle_pts.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
angle_pts.iter().map(|&(i, _)| result[i]).collect()
|
||||
}
|
||||
|
||||
/// Perform boolean operation on two FreePath point arrays.
|
||||
/// Returns the resulting point array as `Vec<[f32; 2]>`, or empty on failure.
|
||||
pub fn boolean_path(op: BooleanOp, path_a: &[[f32; 2]], path_b: &[[f32; 2]]) -> Vec<[f32; 2]> {
|
||||
if path_a.len() < 3 || path_b.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let a: Vec<Vec2> = path_a.iter().map(Vec2::from_pt).collect();
|
||||
let b: Vec<Vec2> = path_b.iter().map(Vec2::from_pt).collect();
|
||||
|
||||
let result = if is_convex(&a) && is_convex(&b) {
|
||||
match op {
|
||||
BooleanOp::Union => polygon_union_convex(&a, &b),
|
||||
BooleanOp::Intersect => polygon_intersect_convex(&a, &b),
|
||||
BooleanOp::Subtract => polygon_subtract_convex(&a, &b),
|
||||
BooleanOp::Xor => {
|
||||
let u = polygon_union_convex(&a, &b);
|
||||
let i = polygon_intersect_convex(&a, &b);
|
||||
polygon_subtract_convex(&u, &i)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match op {
|
||||
BooleanOp::Union => polygon_union_general(&a, &b),
|
||||
BooleanOp::Intersect => polygon_intersect_concave(&a, &b),
|
||||
BooleanOp::Subtract => polygon_subtract_concave(&a, &b),
|
||||
BooleanOp::Xor => {
|
||||
let u = polygon_union_general(&a, &b);
|
||||
let i = polygon_intersect_concave(&a, &b);
|
||||
polygon_subtract_concave(&u, &i)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result.iter().map(|p| [p.x as f32, p.y as f32]).collect()
|
||||
}
|
||||
|
||||
/// Perform boolean on two FreePath VectorShapes, return new FreePath.
|
||||
pub fn boolean_shapes(op: BooleanOp, a: &VectorShape, b: &VectorShape) -> Option<VectorShape> {
|
||||
let pts_a = match a {
|
||||
VectorShape::FreePath { pts, .. } => pts.as_slice(),
|
||||
_ => return None,
|
||||
};
|
||||
let pts_b = match b {
|
||||
VectorShape::FreePath { pts, .. } => pts.as_slice(),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let result_pts = boolean_path(op, pts_a, pts_b);
|
||||
if result_pts.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (stroke, color, fill, fill_color, opacity, hardness) = match a {
|
||||
VectorShape::FreePath { stroke, color, fill, fill_color, opacity, hardness, .. } => {
|
||||
(*stroke, *color, *fill, *fill_color, *opacity, *hardness)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(VectorShape::FreePath {
|
||||
pts: result_pts,
|
||||
closed: true,
|
||||
stroke,
|
||||
color,
|
||||
fill,
|
||||
fill_color,
|
||||
opacity,
|
||||
hardness,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user