feat: introduce SVG-based complex vector shapes
- Added `SvgShape` variant to `VectorShape` in `hcie-protocol`, allowing for SVG rendering of complex shapes. - Updated serialization to skip old shape variants for backward compatibility. - Implemented SVG generation functions in `svg_templates.rs` for various shapes (arrow, star, rhombus, etc.). - Created `svg_render.rs` to parse SVG strings and tessellate paths using `usvg`. - Modified rendering logic in `hcie-vector` to handle `SvgShape` and integrate with existing shape rendering. - Updated shape creation API in `hcie-engine-api` to support SVG shapes. - Enhanced property editing and shape synchronization in both `hcie-egui-app` and `hcie-iced-app` to accommodate new SVG shapes. - Added comprehensive documentation for new features and changes.
This commit is contained in:
@@ -0,0 +1,260 @@
|
|||||||
|
# SVG-based Complex Vector Shapes
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Replace hard-coded vertex generation for complex `VectorShape` variants with SVG strings rendered at runtime via `usvg`. Simple primitives (Line, Rect, Circle, Polygon, FreePath) stay as-is.
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
| Decision | Choice |
|
||||||
|
|----------|--------|
|
||||||
|
| SVG coordinate system | `viewBox="0 0 1 1"` (normalized). Resize only updates x1,y1,x2,y2 — SVG never regenerated. |
|
||||||
|
| Shape kind field | `kind: String` in SvgShape ("arrow", "star", etc.) for property panel controls. |
|
||||||
|
| Backward compat | Keep old variants with `#[serde(skip_serializing)]`. Deserialized old variants stay as-is in memory but convert to SvgShape on first user edit. On serialize, only SvgShape writes out. |
|
||||||
|
| SVG path parser | `usvg` 0.43 (already workspace dep) — parse SVG, extract path segments, tessellate curves, scale to bounding box. |
|
||||||
|
| Both GUIs | egui + iced both updated. |
|
||||||
|
|
||||||
|
## Changes (17 files, 5 crates)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. `hcie-protocol/src/lib.rs` — Add `SvgShape` variant
|
||||||
|
|
||||||
|
```rust
|
||||||
|
SvgShape {
|
||||||
|
x1: f32, y1: f32, x2: f32, y2: f32,
|
||||||
|
kind: String, // "arrow", "star", "rhombus", "cylinder", "heart", "bubble", "gear", "cross", "crescent", "bolt", "arrow4"
|
||||||
|
svg: String, // SVG with viewBox="0 0 1 1", path data in normalized coords
|
||||||
|
stroke: f32,
|
||||||
|
color: [u8; 4],
|
||||||
|
fill_color: [u8; 4],
|
||||||
|
fill: bool,
|
||||||
|
angle: f32,
|
||||||
|
opacity: f32,
|
||||||
|
hardness: f32,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Old variants (Arrow, Star, Rhombus, Cylinder, Heart, Bubble, Gear, Cross, Crescent, Bolt, Arrow4) keep `#[serde(skip_serializing)]` — they deserialize fine for backward compat but never serialize.
|
||||||
|
|
||||||
|
Update every exhaustive match arm in `impl VectorShape`:
|
||||||
|
- `bounds()` — add `SvgShape { x1,y1,x2,y2,.. }`
|
||||||
|
- `normalized_bounds()` — no change, delegates to bounds
|
||||||
|
- `color()` / `set_color()`
|
||||||
|
- `shape_opacity()` / `set_shape_opacity()`
|
||||||
|
- `hardness()` / `set_hardness()`
|
||||||
|
- `stroke()` / `set_stroke()` / `get_stroke_mut()`
|
||||||
|
- `angle()` / `set_angle()` / `get_angle_mut()` / `rotate()`
|
||||||
|
- `is_filled()` / `fill_color()` / `fill()` / `get_fill_mut()` / `get_fill_color_mut()`
|
||||||
|
- `coords_mut()`
|
||||||
|
- `set_bounds()` — SvgShape updates x1,y1,x2,y2 like other non-FreePath variants (no SVG regeneration needed since viewBox is normalized)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `hcie-vector/Cargo.toml` — Add dependency
|
||||||
|
|
||||||
|
```toml
|
||||||
|
usvg = { workspace = true }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. `hcie-vector/src/svg_templates.rs` — NEW: SVG template functions
|
||||||
|
|
||||||
|
11 functions, each returns an SVG string with `viewBox="0 0 1 1"`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn arrow_svg(kind: &str, thick: bool) -> String;
|
||||||
|
pub fn star_svg(kind: &str, points: u32, inner_radius: f32) -> String;
|
||||||
|
pub fn rhombus_svg(kind: &str) -> String;
|
||||||
|
pub fn cylinder_svg(kind: &str) -> String;
|
||||||
|
pub fn heart_svg(kind: &str) -> String;
|
||||||
|
pub fn bubble_svg(kind: &str) -> String;
|
||||||
|
pub fn gear_svg(kind: &str) -> String;
|
||||||
|
pub fn cross_svg(kind: &str) -> String;
|
||||||
|
pub fn crescent_svg(kind: &str) -> String;
|
||||||
|
pub fn bolt_svg(kind: &str) -> String;
|
||||||
|
pub fn arrow4_svg(kind: &str) -> String;
|
||||||
|
```
|
||||||
|
|
||||||
|
Each generates SVG path data mathematically equivalent to the current Rust vertex code. For example, `arrow_svg` (non-thick):
|
||||||
|
```svg
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1">
|
||||||
|
<path d="M 0,1 L 1,0.5 L 0,0 L 0.2,0.5 Z"/>
|
||||||
|
</svg>
|
||||||
|
```
|
||||||
|
(`arrow4_svg` has 4 arrowheads → returns `<path d="..."/>` with the full 12-point cross+arrowhead polygon.)
|
||||||
|
|
||||||
|
Also add a registry function:
|
||||||
|
```rust
|
||||||
|
pub fn create_svg(kind: &str, params: &HashMap<String, f32>) -> String {
|
||||||
|
match kind {
|
||||||
|
"arrow" => arrow_svg(kind, params.get("thick").copied().unwrap_or(0.0) > 0.5),
|
||||||
|
"star" => star_svg(kind, params.get("points").copied().unwrap_or(5.0) as u32, params.get("inner_radius").copied().unwrap_or(0.5)),
|
||||||
|
// ... etc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. `hcie-vector/src/svg_render.rs` — NEW: SVG path tessellator
|
||||||
|
|
||||||
|
Uses `usvg` to parse SVG string, extract path segments, tessellate curves, scale to bounding box:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Parse SVG string, extract all path polygons, scale to fit (x1,y1)-(x2,y2).
|
||||||
|
/// Returns one polygon per subpath.
|
||||||
|
pub fn svg_to_points(svg: &str, x1: f32, y1: f32, x2: f32, y2: f32) -> Result<Vec<Vec<(f32, f32)>>, String>;
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
1. `usvg::Tree::from_data(svg.as_bytes(), &usvg::Options::default())`
|
||||||
|
2. Traverse `tree.root()` children, collect `usvg::Node::Path` nodes
|
||||||
|
3. For each path:
|
||||||
|
- Get `path.data().segments()`
|
||||||
|
- Accumulate points per subpath (separate Vec per `MoveTo` after `Close`)
|
||||||
|
- Tessellate `CubicTo` and `QuadTo` via cubic/quad bezier splitting (use `hcie-io/src/svg_import.rs` pattern)
|
||||||
|
- Apply viewBox → bounding-box scaling: since viewBox is `0 0 1 1`, map point (px,py) → (x1 + px*(x2-x1), y1 + py*(y2-y1))
|
||||||
|
4. Return `Vec<Vec<(f32,f32)>>`
|
||||||
|
|
||||||
|
Edge cases:
|
||||||
|
- Empty SVG → return empty vec
|
||||||
|
- No `<path>` elements → return empty vec
|
||||||
|
- Invalid SVG → return `Err` with usvg error message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. `hcie-vector/src/lib.rs` — Update render entry points
|
||||||
|
|
||||||
|
Add modules:
|
||||||
|
```rust
|
||||||
|
pub mod svg_render;
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `render_shape` match:
|
||||||
|
```rust
|
||||||
|
SvgShape { svg, x1, y1, x2, y2, fill, color, fill_color, stroke, opacity, hardness, .. } => {
|
||||||
|
let points_list = svg_render::svg_to_points(svg, *x1, *y1, *x2, *y2).unwrap_or_default();
|
||||||
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
||||||
|
let px_color = [color[0], color[1], color[2], alpha];
|
||||||
|
if *fill {
|
||||||
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
||||||
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
||||||
|
for poly in &points_list { fill_polygon(layer, poly, px_fill); }
|
||||||
|
}
|
||||||
|
for poly in &points_list {
|
||||||
|
for i in 0..poly.len() {
|
||||||
|
let j = (i + 1) % poly.len();
|
||||||
|
draw_line(layer, poly[i].0, poly[i].1, poly[j].0, poly[j].1, px_color, *stroke, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `get_shape_hardness` — add `SvgShape { hardness, .. }` arm.
|
||||||
|
|
||||||
|
Update `rotate_shape` / `set_shape_angle` — add `SvgShape { angle, .. }` arm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. `hcie-vector/examples/vector_test.rs` — Update creation
|
||||||
|
|
||||||
|
Replace old variant construction with SvgShape, calling `svg_templates::create_svg()` and wrapping in `VectorShape::SvgShape { ... }`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. `hcie-engine-api/src/lib.rs` — Add shape creation API
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn create_vector_shape(kind: &str, x1: f32, y1: f32, x2: f32, y2: f32, params: &HashMap<String, f32>) -> VectorShape;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `hcie-vector = { path = "../hcie-vector" }` dependency.
|
||||||
|
|
||||||
|
Implementation: call `hcie_vector::svg_templates::create_svg(kind, params)`, return `VectorShape::SvgShape { svg, kind, x1, y1, x2, y2, stroke: 2.0, color: [0,0,0,255], fill_color: [255,255,255,255], fill: true, angle: 0.0, opacity: 1.0, hardness: 1.0 }`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. `hcie-egui-app/…/canvas/mod.rs` — Update shape creation (~lines 2179–2379)
|
||||||
|
|
||||||
|
Each `Tool::VectorXxx` arm:
|
||||||
|
```
|
||||||
|
Tool::VectorArrow => {
|
||||||
|
let params = HashMap::from([("thick".into(), if thick { 1.0 } else { 0.0 })]);
|
||||||
|
Some(engine.create_vector_shape("arrow", x1, y1, x2, y2, ¶ms))
|
||||||
|
}
|
||||||
|
Tool::VectorStar => { /* params: points, inner_radius */ }
|
||||||
|
Tool::VectorRhombus => { /* no params */ }
|
||||||
|
// ... etc for all 11 complex shapes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. `hcie-egui-app/…/canvas/render.rs` — `get_shape_key_points` SvgShape arm
|
||||||
|
|
||||||
|
Add:
|
||||||
|
```rust
|
||||||
|
VectorShape::SvgShape { x1, y1, x2, y2, .. } => {
|
||||||
|
let r = normalized_rect(*x1, *y1, *x2, *y2);
|
||||||
|
vec![(r.0, r.1), (r.2, r.1), (r.2, r.3), (r.0, r.3)]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. `hcie-egui-app/…/app/mod.rs` — Property editing
|
||||||
|
|
||||||
|
In the property editing match (~line 4151 area), add `SvgShape { kind, svg, fill, fill_color, .. }` arm. When user changes star points/inner_radius (via kind-specific UI), regenerate SVG via `create_vector_shape` and update the `svg` field.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. `hcie-egui-app/…/tools/geometry_panel.rs` — Labels + kind UI
|
||||||
|
|
||||||
|
Add `SvgShape { kind, .. }` to the label matching and shape-type dropdown. Show kind-specific controls (e.g., points slider for "star", thick toggle for "arrow").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. `hcie-egui-app/…/tools/shape_sync.rs` — Param sync
|
||||||
|
|
||||||
|
Handle `SvgShape` in the sync match arms: read/write `kind` field, regenerate SVG on param changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13-17. `hcie-iced-app/…` — Mirror of steps 8-12
|
||||||
|
|
||||||
|
Same pattern in the iced GUI crate:
|
||||||
|
- `app.rs` — shape creation
|
||||||
|
- `panels/properties.rs` — labels
|
||||||
|
- `panels/geometry.rs` — labels + editing
|
||||||
|
- `shape_sync.rs` — param sync
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 18. `hcie-io/src/svg_import.rs` — No change needed
|
||||||
|
|
||||||
|
Already uses `usvg` correctly. SVG files imported via `import_svg()` already convert to `FreePath` — fine as-is.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compile all crates
|
||||||
|
cargo check -p hcie-protocol -p hcie-vector -p hcie-engine-api -p hcie-gui-egui -p hcie-iced-gui
|
||||||
|
|
||||||
|
# Run vector engine tests
|
||||||
|
cargo test -p hcie-vector --lib
|
||||||
|
cargo test -p hcie-vector --examples
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
cargo test -p hcie-vector -p hcie-protocol -p hcie-engine-api
|
||||||
|
```
|
||||||
|
|
||||||
|
## Risk Mitigation
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
|------|------------|
|
||||||
|
| SVG path differs from old vertex code visually | Compare screenshots before/after for each shape kind |
|
||||||
|
| Old documents fail to load | Old variants still deserialize (kept with `#[serde(skip_serializing)]`), stay as old enum variants in memory |
|
||||||
|
| Performance regression from usvg parsing at render time | usvg parse is fast (<0.1ms for our tiny SVGs). Cache can be added later if needed |
|
||||||
|
| Bezier tessellation quality differs | Use same tessellation density as current code (8 steps per curve quad, matching existing) |
|
||||||
Generated
+1
@@ -2867,6 +2867,7 @@ dependencies = [
|
|||||||
"proptest",
|
"proptest",
|
||||||
"rstest",
|
"rstest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"usvg 0.43.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -4586,6 +4586,24 @@ impl HcieApp {
|
|||||||
d, stroke_col, stroke, if *fill { fill_col } else { "none".to_string() }, if *fill { opacity } else { &0.0 }, opacity
|
d, stroke_col, stroke, if *fill { fill_col } else { "none".to_string() }, if *fill { opacity } else { &0.0 }, opacity
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
VectorShape::SvgShape { svg: inner_svg, x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, .. } => {
|
||||||
|
let stroke_col = format_color(color);
|
||||||
|
let fill_col = format_color(fill_color);
|
||||||
|
let x = x1.min(*x2);
|
||||||
|
let y = y1.min(*y2);
|
||||||
|
let w = (x2 - x1).abs();
|
||||||
|
let h = (y2 - y1).abs();
|
||||||
|
if let Some(di) = inner_svg.find(" d=\"") {
|
||||||
|
let start = di + 4;
|
||||||
|
if let Some(end) = inner_svg[start..].find('"') {
|
||||||
|
let d = &inner_svg[start..start+end];
|
||||||
|
svg.push_str(&format!(
|
||||||
|
"<path d=\"{}\" transform=\"translate({},{}) scale({},{})\" stroke=\"{}\" stroke-width=\"{}\" fill=\"{}\" fill-opacity=\"{}\" stroke-opacity=\"{}\"/>\n",
|
||||||
|
d, x, y, w, h, stroke_col, stroke, if *fill { fill_col } else { "none".to_string() }, if *fill { opacity } else { &0.0 }, opacity
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
svg.push_str("</svg>");
|
svg.push_str("</svg>");
|
||||||
|
|||||||
@@ -26,6 +26,23 @@ fn shape_label(shapes: &[VectorShape], i: usize) -> String {
|
|||||||
VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1),
|
VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1),
|
||||||
VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1),
|
VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1),
|
||||||
VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1),
|
VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1),
|
||||||
|
VectorShape::SvgShape { kind, .. } => {
|
||||||
|
let label = match kind.as_str() {
|
||||||
|
"arrow" => "Arrow",
|
||||||
|
"star" => "Star",
|
||||||
|
"rhombus" => "Rhombus",
|
||||||
|
"cylinder" => "Cylinder",
|
||||||
|
"heart" => "Heart",
|
||||||
|
"bubble" => "Bubble",
|
||||||
|
"gear" => "Gear",
|
||||||
|
"cross" => "Cross",
|
||||||
|
"crescent" => "Crescent",
|
||||||
|
"bolt" => "Bolt",
|
||||||
|
"arrow4" => "4-way Arrow",
|
||||||
|
_ => kind,
|
||||||
|
};
|
||||||
|
format!("{} #{}", label, i + 1)
|
||||||
|
}
|
||||||
VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1),
|
VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,6 +116,23 @@ pub fn show_geometry(
|
|||||||
VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1),
|
VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1),
|
||||||
VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1),
|
VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1),
|
||||||
VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1),
|
VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1),
|
||||||
|
VectorShape::SvgShape { kind, .. } => {
|
||||||
|
let label = match kind.as_str() {
|
||||||
|
"arrow" => "Arrow",
|
||||||
|
"star" => "Star",
|
||||||
|
"rhombus" => "Rhombus",
|
||||||
|
"cylinder" => "Cylinder",
|
||||||
|
"heart" => "Heart",
|
||||||
|
"bubble" => "Bubble",
|
||||||
|
"gear" => "Gear",
|
||||||
|
"cross" => "Cross",
|
||||||
|
"crescent" => "Crescent",
|
||||||
|
"bolt" => "Bolt",
|
||||||
|
"arrow4" => "4-way Arrow",
|
||||||
|
_ => kind,
|
||||||
|
};
|
||||||
|
format!("{} #{}", label, i + 1)
|
||||||
|
}
|
||||||
VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1),
|
VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1),
|
||||||
};
|
};
|
||||||
let is_sel = state.selected_vector_shape == Some(i);
|
let is_sel = state.selected_vector_shape == Some(i);
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ pub fn sync_shape_from_tool(doc: &mut AppDocument, state: &mut ToolState) {
|
|||||||
VectorShape::Polygon { .. } => {
|
VectorShape::Polygon { .. } => {
|
||||||
// polygon_sides via engine when available
|
// polygon_sides via engine when available
|
||||||
}
|
}
|
||||||
|
VectorShape::SvgShape { .. } => {
|
||||||
|
// shape-specific params are baked into the SVG
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,6 +139,9 @@ pub fn sync_tool_from_shape(doc: &AppDocument, state: &mut ToolState) {
|
|||||||
} => {
|
} => {
|
||||||
v.line_cap = *cap_start;
|
v.line_cap = *cap_start;
|
||||||
}
|
}
|
||||||
|
VectorShape::SvgShape { .. } => {
|
||||||
|
// params baked into SVG, nothing to sync
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2216,34 +2216,28 @@ fn create_shape_from_drag(
|
|||||||
opacity,
|
opacity,
|
||||||
hardness,
|
hardness,
|
||||||
},
|
},
|
||||||
Tool::VectorArrow => VectorShape::Arrow {
|
Tool::VectorArrow => {
|
||||||
x1,
|
let mut p = std::collections::HashMap::new();
|
||||||
y1,
|
p.insert("thick".to_string(), 0.0);
|
||||||
x2,
|
match hcie_engine_api::create_vector_shape("arrow", x1, y1, x2, y2, &p) {
|
||||||
y2,
|
VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape {
|
||||||
stroke,
|
kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill,
|
||||||
color,
|
angle: 0.0, opacity, hardness,
|
||||||
fill_color,
|
},
|
||||||
fill,
|
_ => unreachable!(),
|
||||||
angle: 0.0,
|
}
|
||||||
opacity,
|
|
||||||
hardness,
|
|
||||||
thick: false,
|
|
||||||
},
|
},
|
||||||
Tool::VectorStar => VectorShape::Star {
|
Tool::VectorStar => {
|
||||||
x1,
|
let mut p = std::collections::HashMap::new();
|
||||||
y1,
|
p.insert("points".to_string(), state.tool_configs.vector.star_points as f32);
|
||||||
x2,
|
p.insert("inner_radius".to_string(), state.tool_configs.vector.star_inner_radius);
|
||||||
y2,
|
match hcie_engine_api::create_vector_shape("star", x1, y1, x2, y2, &p) {
|
||||||
stroke,
|
VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape {
|
||||||
color,
|
kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill,
|
||||||
fill_color,
|
angle: 0.0, opacity, hardness,
|
||||||
fill,
|
},
|
||||||
angle: 0.0,
|
_ => unreachable!(),
|
||||||
opacity,
|
}
|
||||||
hardness,
|
|
||||||
points: state.tool_configs.vector.star_points,
|
|
||||||
inner_radius: state.tool_configs.vector.star_inner_radius,
|
|
||||||
},
|
},
|
||||||
Tool::VectorPolygon => VectorShape::Polygon {
|
Tool::VectorPolygon => VectorShape::Polygon {
|
||||||
x1,
|
x1,
|
||||||
@@ -2259,122 +2253,86 @@ fn create_shape_from_drag(
|
|||||||
hardness,
|
hardness,
|
||||||
sides: state.tool_configs.vector.polygon_sides,
|
sides: state.tool_configs.vector.polygon_sides,
|
||||||
},
|
},
|
||||||
Tool::VectorRhombus => VectorShape::Rhombus {
|
Tool::VectorRhombus => {
|
||||||
x1,
|
match hcie_engine_api::create_vector_shape("rhombus", x1, y1, x2, y2, &std::collections::HashMap::new()) {
|
||||||
y1,
|
VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape {
|
||||||
x2,
|
kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill,
|
||||||
y2,
|
angle: 0.0, opacity, hardness,
|
||||||
stroke,
|
},
|
||||||
color,
|
_ => unreachable!(),
|
||||||
fill_color,
|
}
|
||||||
fill,
|
|
||||||
angle: 0.0,
|
|
||||||
opacity,
|
|
||||||
hardness,
|
|
||||||
},
|
},
|
||||||
Tool::VectorCylinder => VectorShape::Cylinder {
|
Tool::VectorCylinder => {
|
||||||
x1,
|
match hcie_engine_api::create_vector_shape("cylinder", x1, y1, x2, y2, &std::collections::HashMap::new()) {
|
||||||
y1,
|
VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape {
|
||||||
x2,
|
kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill,
|
||||||
y2,
|
angle: 0.0, opacity, hardness,
|
||||||
stroke,
|
},
|
||||||
color,
|
_ => unreachable!(),
|
||||||
fill_color,
|
}
|
||||||
fill,
|
|
||||||
angle: 0.0,
|
|
||||||
opacity,
|
|
||||||
hardness,
|
|
||||||
},
|
},
|
||||||
Tool::VectorHeart => VectorShape::Heart {
|
Tool::VectorHeart => {
|
||||||
x1,
|
match hcie_engine_api::create_vector_shape("heart", x1, y1, x2, y2, &std::collections::HashMap::new()) {
|
||||||
y1,
|
VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape {
|
||||||
x2,
|
kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill,
|
||||||
y2,
|
angle: 0.0, opacity, hardness,
|
||||||
stroke,
|
},
|
||||||
color,
|
_ => unreachable!(),
|
||||||
fill_color,
|
}
|
||||||
fill,
|
|
||||||
angle: 0.0,
|
|
||||||
opacity,
|
|
||||||
hardness,
|
|
||||||
},
|
},
|
||||||
Tool::VectorBubble => VectorShape::Bubble {
|
Tool::VectorBubble => {
|
||||||
x1,
|
match hcie_engine_api::create_vector_shape("bubble", x1, y1, x2, y2, &std::collections::HashMap::new()) {
|
||||||
y1,
|
VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape {
|
||||||
x2,
|
kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill,
|
||||||
y2,
|
angle: 0.0, opacity, hardness,
|
||||||
stroke,
|
},
|
||||||
color,
|
_ => unreachable!(),
|
||||||
fill_color,
|
}
|
||||||
fill,
|
|
||||||
angle: 0.0,
|
|
||||||
opacity,
|
|
||||||
hardness,
|
|
||||||
},
|
},
|
||||||
Tool::VectorGear => VectorShape::Gear {
|
Tool::VectorGear => {
|
||||||
x1,
|
match hcie_engine_api::create_vector_shape("gear", x1, y1, x2, y2, &std::collections::HashMap::new()) {
|
||||||
y1,
|
VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape {
|
||||||
x2,
|
kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill,
|
||||||
y2,
|
angle: 0.0, opacity, hardness,
|
||||||
stroke,
|
},
|
||||||
color,
|
_ => unreachable!(),
|
||||||
fill_color,
|
}
|
||||||
fill,
|
|
||||||
angle: 0.0,
|
|
||||||
opacity,
|
|
||||||
hardness,
|
|
||||||
},
|
},
|
||||||
Tool::VectorCross => VectorShape::Cross {
|
Tool::VectorCross => {
|
||||||
x1,
|
match hcie_engine_api::create_vector_shape("cross", x1, y1, x2, y2, &std::collections::HashMap::new()) {
|
||||||
y1,
|
VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape {
|
||||||
x2,
|
kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill,
|
||||||
y2,
|
angle: 0.0, opacity, hardness,
|
||||||
stroke,
|
},
|
||||||
color,
|
_ => unreachable!(),
|
||||||
fill_color,
|
}
|
||||||
fill,
|
|
||||||
angle: 0.0,
|
|
||||||
opacity,
|
|
||||||
hardness,
|
|
||||||
},
|
},
|
||||||
Tool::VectorCrescent => VectorShape::Crescent {
|
Tool::VectorCrescent => {
|
||||||
x1,
|
match hcie_engine_api::create_vector_shape("crescent", x1, y1, x2, y2, &std::collections::HashMap::new()) {
|
||||||
y1,
|
VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape {
|
||||||
x2,
|
kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill,
|
||||||
y2,
|
angle: 0.0, opacity, hardness,
|
||||||
stroke,
|
},
|
||||||
color,
|
_ => unreachable!(),
|
||||||
fill_color,
|
}
|
||||||
fill,
|
|
||||||
angle: 0.0,
|
|
||||||
opacity,
|
|
||||||
hardness,
|
|
||||||
},
|
},
|
||||||
Tool::VectorBolt => VectorShape::Bolt {
|
Tool::VectorBolt => {
|
||||||
x1,
|
match hcie_engine_api::create_vector_shape("bolt", x1, y1, x2, y2, &std::collections::HashMap::new()) {
|
||||||
y1,
|
VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape {
|
||||||
x2,
|
kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill,
|
||||||
y2,
|
angle: 0.0, opacity, hardness,
|
||||||
stroke,
|
},
|
||||||
color,
|
_ => unreachable!(),
|
||||||
fill_color,
|
}
|
||||||
fill,
|
|
||||||
angle: 0.0,
|
|
||||||
opacity,
|
|
||||||
hardness,
|
|
||||||
},
|
},
|
||||||
Tool::VectorArrow4 => VectorShape::Arrow4 {
|
Tool::VectorArrow4 => {
|
||||||
x1,
|
match hcie_engine_api::create_vector_shape("arrow4", x1, y1, x2, y2, &std::collections::HashMap::new()) {
|
||||||
y1,
|
VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape {
|
||||||
x2,
|
kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill,
|
||||||
y2,
|
angle: 0.0, opacity, hardness,
|
||||||
stroke,
|
},
|
||||||
color,
|
_ => unreachable!(),
|
||||||
fill_color,
|
}
|
||||||
fill,
|
|
||||||
angle: 0.0,
|
|
||||||
opacity,
|
|
||||||
hardness,
|
|
||||||
},
|
},
|
||||||
_ => VectorShape::Line {
|
_ => VectorShape::Line {
|
||||||
x1,
|
x1,
|
||||||
|
|||||||
@@ -386,6 +386,14 @@ fn get_shape_key_points(shape: &VectorShape) -> (f32, f32, f32, Vec<(f32, f32)>)
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
VectorShape::SvgShape { .. } => {
|
||||||
|
vec![
|
||||||
|
rotate(left, top),
|
||||||
|
rotate(right, top),
|
||||||
|
rotate(right, bottom),
|
||||||
|
rotate(left, bottom),
|
||||||
|
]
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
vec![
|
vec![
|
||||||
rotate(left, top),
|
rotate(left, top),
|
||||||
|
|||||||
@@ -415,6 +415,39 @@ pub struct Engine {
|
|||||||
std::sync::Arc<std::sync::Mutex<Vec<crate::stroke_cache::PendingHistoryItem>>>,
|
std::sync::Arc<std::sync::Mutex<Vec<crate::stroke_cache::PendingHistoryItem>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a `VectorShape::SvgShape` from a shape kind name and parameter map.
|
||||||
|
///
|
||||||
|
/// The `kind` string must be one of: "arrow", "star", "rhombus", "cylinder",
|
||||||
|
/// "heart", "bubble", "gear", "cross", "crescent", "bolt", "arrow4".
|
||||||
|
///
|
||||||
|
/// Shape-specific parameters (e.g. `points`, `inner_radius`, `thick`) are passed
|
||||||
|
/// via `params`.
|
||||||
|
pub fn create_vector_shape(
|
||||||
|
kind: &str,
|
||||||
|
x1: f32,
|
||||||
|
y1: f32,
|
||||||
|
x2: f32,
|
||||||
|
y2: f32,
|
||||||
|
params: &std::collections::HashMap<String, f32>,
|
||||||
|
) -> VectorShape {
|
||||||
|
let svg = hcie_vector::svg_templates::create_svg(kind, params);
|
||||||
|
VectorShape::SvgShape {
|
||||||
|
x1,
|
||||||
|
y1,
|
||||||
|
x2,
|
||||||
|
y2,
|
||||||
|
kind: kind.to_string(),
|
||||||
|
svg,
|
||||||
|
stroke: 2.0,
|
||||||
|
color: [0, 0, 0, 255],
|
||||||
|
fill_color: [255, 255, 255, 255],
|
||||||
|
fill: true,
|
||||||
|
angle: 0.0,
|
||||||
|
opacity: 1.0,
|
||||||
|
hardness: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
/// Create a new engine with a default transparent document.
|
/// Create a new engine with a default transparent document.
|
||||||
///
|
///
|
||||||
@@ -4038,6 +4071,14 @@ fn set_shape_color(
|
|||||||
*color = stroke_color;
|
*color = stroke_color;
|
||||||
*fc = fill_color;
|
*fc = fill_color;
|
||||||
}
|
}
|
||||||
|
SvgShape {
|
||||||
|
color,
|
||||||
|
fill_color: fc,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
*color = stroke_color;
|
||||||
|
*fc = fill_color;
|
||||||
|
}
|
||||||
FreePath {
|
FreePath {
|
||||||
color,
|
color,
|
||||||
fill_color: fc,
|
fill_color: fc,
|
||||||
@@ -4147,6 +4188,12 @@ fn move_shape(
|
|||||||
*x2 += dx;
|
*x2 += dx;
|
||||||
*y2 += dy;
|
*y2 += dy;
|
||||||
}
|
}
|
||||||
|
SvgShape { x1, y1, x2, y2, .. } => {
|
||||||
|
*x1 += dx;
|
||||||
|
*y1 += dy;
|
||||||
|
*x2 += dx;
|
||||||
|
*y2 += dy;
|
||||||
|
}
|
||||||
FreePath { pts, .. } => {
|
FreePath { pts, .. } => {
|
||||||
for p in pts.iter_mut() {
|
for p in pts.iter_mut() {
|
||||||
p[0] += dx;
|
p[0] += dx;
|
||||||
@@ -4279,6 +4326,14 @@ fn scale_shape(shape: &mut hcie_protocol::VectorShape, scale_x: f32, scale_y: f3
|
|||||||
*x2 = cx + (*x2 - cx) * scale_x;
|
*x2 = cx + (*x2 - cx) * scale_x;
|
||||||
*y2 = cy + (*y2 - cy) * scale_y;
|
*y2 = cy + (*y2 - cy) * scale_y;
|
||||||
}
|
}
|
||||||
|
SvgShape { x1, y1, x2, y2, .. } => {
|
||||||
|
let cx = (*x1 + *x2) / 2.0;
|
||||||
|
let cy = (*y1 + *y2) / 2.0;
|
||||||
|
*x1 = cx + (*x1 - cx) * scale_x;
|
||||||
|
*y1 = cy + (*y1 - cy) * scale_y;
|
||||||
|
*x2 = cx + (*x2 - cx) * scale_x;
|
||||||
|
*y2 = cy + (*y2 - cy) * scale_y;
|
||||||
|
}
|
||||||
FreePath { pts, .. } => {
|
FreePath { pts, .. } => {
|
||||||
if pts.is_empty() {
|
if pts.is_empty() {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -5061,35 +5061,36 @@ impl HcieIcedApp {
|
|||||||
opacity,
|
opacity,
|
||||||
hardness: 0.5,
|
hardness: 0.5,
|
||||||
}),
|
}),
|
||||||
Tool::VectorArrow => Some(hcie_engine_api::VectorShape::Arrow {
|
Tool::VectorArrow => {
|
||||||
x1: x0,
|
let mut p = std::collections::HashMap::new();
|
||||||
y1: y0,
|
p.insert("thick".to_string(), 0.0);
|
||||||
x2: x1,
|
match hcie_engine_api::create_vector_shape("arrow", x0, y0, x1, y1, &p) {
|
||||||
y2: y1,
|
hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } =>
|
||||||
stroke,
|
Some(hcie_engine_api::VectorShape::SvgShape {
|
||||||
color,
|
kind, svg,
|
||||||
fill,
|
x1: x0, y1: y0, x2: x1, y2: y1,
|
||||||
fill_color,
|
stroke, color, fill_color, fill,
|
||||||
angle: 0.0,
|
angle: 0.0, opacity, hardness: 0.5,
|
||||||
opacity,
|
}),
|
||||||
hardness: 0.5,
|
_ => unreachable!(),
|
||||||
thick: false,
|
}
|
||||||
}),
|
}
|
||||||
Tool::VectorStar => Some(hcie_engine_api::VectorShape::Star {
|
Tool::VectorStar => {
|
||||||
x1: x0.min(x1),
|
let mut p = std::collections::HashMap::new();
|
||||||
y1: y0.min(y1),
|
p.insert("points".to_string(), points as f32);
|
||||||
x2: x0.max(x1),
|
p.insert("inner_radius".to_string(), 0.5);
|
||||||
y2: y0.max(y1),
|
let (sx1, sy1, sx2, sy2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1));
|
||||||
stroke,
|
match hcie_engine_api::create_vector_shape("star", sx1, sy1, sx2, sy2, &p) {
|
||||||
color,
|
hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } =>
|
||||||
fill,
|
Some(hcie_engine_api::VectorShape::SvgShape {
|
||||||
fill_color,
|
kind, svg,
|
||||||
angle: 0.0,
|
x1: sx1, y1: sy1, x2: sx2, y2: sy2,
|
||||||
opacity,
|
stroke, color, fill_color, fill,
|
||||||
hardness: 0.5,
|
angle: 0.0, opacity, hardness: 0.5,
|
||||||
points,
|
}),
|
||||||
inner_radius: 0.5,
|
_ => unreachable!(),
|
||||||
}),
|
}
|
||||||
|
}
|
||||||
Tool::VectorPolygon => Some(hcie_engine_api::VectorShape::Polygon {
|
Tool::VectorPolygon => Some(hcie_engine_api::VectorShape::Polygon {
|
||||||
x1: x0.min(x1),
|
x1: x0.min(x1),
|
||||||
y1: y0.min(y1),
|
y1: y0.min(y1),
|
||||||
@@ -5104,123 +5105,123 @@ impl HcieIcedApp {
|
|||||||
hardness: 0.5,
|
hardness: 0.5,
|
||||||
sides,
|
sides,
|
||||||
}),
|
}),
|
||||||
Tool::VectorRhombus => Some(hcie_engine_api::VectorShape::Rhombus {
|
Tool::VectorRhombus => {
|
||||||
x1: x0.min(x1),
|
let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1));
|
||||||
y1: y0.min(y1),
|
match hcie_engine_api::create_vector_shape("rhombus", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) {
|
||||||
x2: x0.max(x1),
|
hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } =>
|
||||||
y2: y0.max(y1),
|
Some(hcie_engine_api::VectorShape::SvgShape {
|
||||||
stroke,
|
kind, svg,
|
||||||
color,
|
x1: nx1, y1: ny1, x2: nx2, y2: ny2,
|
||||||
fill,
|
stroke, color, fill_color, fill,
|
||||||
fill_color,
|
angle: 0.0, opacity, hardness: 0.5,
|
||||||
angle: 0.0,
|
}),
|
||||||
opacity,
|
_ => unreachable!(),
|
||||||
hardness: 0.5,
|
}
|
||||||
}),
|
}
|
||||||
Tool::VectorCylinder => Some(hcie_engine_api::VectorShape::Cylinder {
|
Tool::VectorCylinder => {
|
||||||
x1: x0.min(x1),
|
let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1));
|
||||||
y1: y0.min(y1),
|
match hcie_engine_api::create_vector_shape("cylinder", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) {
|
||||||
x2: x0.max(x1),
|
hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } =>
|
||||||
y2: y0.max(y1),
|
Some(hcie_engine_api::VectorShape::SvgShape {
|
||||||
stroke,
|
kind, svg,
|
||||||
color,
|
x1: nx1, y1: ny1, x2: nx2, y2: ny2,
|
||||||
fill,
|
stroke, color, fill_color, fill,
|
||||||
fill_color,
|
angle: 0.0, opacity, hardness: 0.5,
|
||||||
angle: 0.0,
|
}),
|
||||||
opacity,
|
_ => unreachable!(),
|
||||||
hardness: 0.5,
|
}
|
||||||
}),
|
}
|
||||||
Tool::VectorHeart => Some(hcie_engine_api::VectorShape::Heart {
|
Tool::VectorHeart => {
|
||||||
x1: x0.min(x1),
|
let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1));
|
||||||
y1: y0.min(y1),
|
match hcie_engine_api::create_vector_shape("heart", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) {
|
||||||
x2: x0.max(x1),
|
hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } =>
|
||||||
y2: y0.max(y1),
|
Some(hcie_engine_api::VectorShape::SvgShape {
|
||||||
stroke,
|
kind, svg,
|
||||||
color,
|
x1: nx1, y1: ny1, x2: nx2, y2: ny2,
|
||||||
fill,
|
stroke, color, fill_color, fill,
|
||||||
fill_color,
|
angle: 0.0, opacity, hardness: 0.5,
|
||||||
angle: 0.0,
|
}),
|
||||||
opacity,
|
_ => unreachable!(),
|
||||||
hardness: 0.5,
|
}
|
||||||
}),
|
}
|
||||||
Tool::VectorBubble => Some(hcie_engine_api::VectorShape::Bubble {
|
Tool::VectorBubble => {
|
||||||
x1: x0.min(x1),
|
let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1));
|
||||||
y1: y0.min(y1),
|
match hcie_engine_api::create_vector_shape("bubble", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) {
|
||||||
x2: x0.max(x1),
|
hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } =>
|
||||||
y2: y0.max(y1),
|
Some(hcie_engine_api::VectorShape::SvgShape {
|
||||||
stroke,
|
kind, svg,
|
||||||
color,
|
x1: nx1, y1: ny1, x2: nx2, y2: ny2,
|
||||||
fill,
|
stroke, color, fill_color, fill,
|
||||||
fill_color,
|
angle: 0.0, opacity, hardness: 0.5,
|
||||||
angle: 0.0,
|
}),
|
||||||
opacity,
|
_ => unreachable!(),
|
||||||
hardness: 0.5,
|
}
|
||||||
}),
|
}
|
||||||
Tool::VectorGear => Some(hcie_engine_api::VectorShape::Gear {
|
Tool::VectorGear => {
|
||||||
x1: x0.min(x1),
|
let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1));
|
||||||
y1: y0.min(y1),
|
match hcie_engine_api::create_vector_shape("gear", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) {
|
||||||
x2: x0.max(x1),
|
hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } =>
|
||||||
y2: y0.max(y1),
|
Some(hcie_engine_api::VectorShape::SvgShape {
|
||||||
stroke,
|
kind, svg,
|
||||||
color,
|
x1: nx1, y1: ny1, x2: nx2, y2: ny2,
|
||||||
fill,
|
stroke, color, fill_color, fill,
|
||||||
fill_color,
|
angle: 0.0, opacity, hardness: 0.5,
|
||||||
angle: 0.0,
|
}),
|
||||||
opacity,
|
_ => unreachable!(),
|
||||||
hardness: 0.5,
|
}
|
||||||
}),
|
}
|
||||||
Tool::VectorCross => Some(hcie_engine_api::VectorShape::Cross {
|
Tool::VectorCross => {
|
||||||
x1: x0.min(x1),
|
let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1));
|
||||||
y1: y0.min(y1),
|
match hcie_engine_api::create_vector_shape("cross", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) {
|
||||||
x2: x0.max(x1),
|
hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } =>
|
||||||
y2: y0.max(y1),
|
Some(hcie_engine_api::VectorShape::SvgShape {
|
||||||
stroke,
|
kind, svg,
|
||||||
color,
|
x1: nx1, y1: ny1, x2: nx2, y2: ny2,
|
||||||
fill,
|
stroke, color, fill_color, fill,
|
||||||
fill_color,
|
angle: 0.0, opacity, hardness: 0.5,
|
||||||
angle: 0.0,
|
}),
|
||||||
opacity,
|
_ => unreachable!(),
|
||||||
hardness: 0.5,
|
}
|
||||||
}),
|
}
|
||||||
Tool::VectorCrescent => Some(hcie_engine_api::VectorShape::Crescent {
|
Tool::VectorCrescent => {
|
||||||
x1: x0.min(x1),
|
let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1));
|
||||||
y1: y0.min(y1),
|
match hcie_engine_api::create_vector_shape("crescent", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) {
|
||||||
x2: x0.max(x1),
|
hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } =>
|
||||||
y2: y0.max(y1),
|
Some(hcie_engine_api::VectorShape::SvgShape {
|
||||||
stroke,
|
kind, svg,
|
||||||
color,
|
x1: nx1, y1: ny1, x2: nx2, y2: ny2,
|
||||||
fill,
|
stroke, color, fill_color, fill,
|
||||||
fill_color,
|
angle: 0.0, opacity, hardness: 0.5,
|
||||||
angle: 0.0,
|
}),
|
||||||
opacity,
|
_ => unreachable!(),
|
||||||
hardness: 0.5,
|
}
|
||||||
}),
|
}
|
||||||
Tool::VectorBolt => Some(hcie_engine_api::VectorShape::Bolt {
|
Tool::VectorBolt => {
|
||||||
x1: x0.min(x1),
|
let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1));
|
||||||
y1: y0.min(y1),
|
match hcie_engine_api::create_vector_shape("bolt", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) {
|
||||||
x2: x0.max(x1),
|
hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } =>
|
||||||
y2: y0.max(y1),
|
Some(hcie_engine_api::VectorShape::SvgShape {
|
||||||
stroke,
|
kind, svg,
|
||||||
color,
|
x1: nx1, y1: ny1, x2: nx2, y2: ny2,
|
||||||
fill,
|
stroke, color, fill_color, fill,
|
||||||
fill_color,
|
angle: 0.0, opacity, hardness: 0.5,
|
||||||
angle: 0.0,
|
}),
|
||||||
opacity,
|
_ => unreachable!(),
|
||||||
hardness: 0.5,
|
}
|
||||||
}),
|
}
|
||||||
Tool::VectorArrow4 => Some(hcie_engine_api::VectorShape::Arrow4 {
|
Tool::VectorArrow4 => {
|
||||||
x1: x0.min(x1),
|
let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1));
|
||||||
y1: y0.min(y1),
|
match hcie_engine_api::create_vector_shape("arrow4", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) {
|
||||||
x2: x0.max(x1),
|
hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } =>
|
||||||
y2: y0.max(y1),
|
Some(hcie_engine_api::VectorShape::SvgShape {
|
||||||
stroke,
|
kind, svg,
|
||||||
color,
|
x1: nx1, y1: ny1, x2: nx2, y2: ny2,
|
||||||
fill,
|
stroke, color, fill_color, fill,
|
||||||
fill_color,
|
angle: 0.0, opacity, hardness: 0.5,
|
||||||
angle: 0.0,
|
}),
|
||||||
opacity,
|
_ => unreachable!(),
|
||||||
hardness: 0.5,
|
}
|
||||||
}),
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ fn shape_label(shapes: &[VectorShape], i: usize) -> String {
|
|||||||
VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1),
|
VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1),
|
||||||
VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1),
|
VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1),
|
||||||
VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1),
|
VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1),
|
||||||
|
VectorShape::SvgShape { kind, .. } => format!("{} #{}", kind, i + 1),
|
||||||
VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1),
|
VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ fn shape_label(shapes: &[VectorShape], i: usize) -> String {
|
|||||||
VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1),
|
VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1),
|
||||||
VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1),
|
VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1),
|
||||||
VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1),
|
VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1),
|
||||||
|
VectorShape::SvgShape { kind, .. } => format!("{} #{}", kind, i + 1),
|
||||||
VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1),
|
VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,23 +263,24 @@ fn vector_select_properties<'a>(
|
|||||||
let stroke_color = shape.color();
|
let stroke_color = shape.color();
|
||||||
let fill_color = shape.fill_color().unwrap_or([0, 0, 0, 255]);
|
let fill_color = shape.fill_color().unwrap_or([0, 0, 0, 255]);
|
||||||
|
|
||||||
let shape_type = match shape {
|
let shape_type: String = match shape {
|
||||||
VectorShape::Line { .. } => "Line",
|
VectorShape::Line { .. } => "Line".into(),
|
||||||
VectorShape::Rect { .. } => "Rect",
|
VectorShape::Rect { .. } => "Rect".into(),
|
||||||
VectorShape::Circle { .. } => "Circle",
|
VectorShape::Circle { .. } => "Circle".into(),
|
||||||
VectorShape::Arrow { .. } => "Arrow",
|
VectorShape::Arrow { .. } => "Arrow".into(),
|
||||||
VectorShape::Star { .. } => "Star",
|
VectorShape::Star { .. } => "Star".into(),
|
||||||
VectorShape::Polygon { .. } => "Polygon",
|
VectorShape::Polygon { .. } => "Polygon".into(),
|
||||||
VectorShape::Rhombus { .. } => "Rhombus",
|
VectorShape::Rhombus { .. } => "Rhombus".into(),
|
||||||
VectorShape::Cylinder { .. } => "Cylinder",
|
VectorShape::Cylinder { .. } => "Cylinder".into(),
|
||||||
VectorShape::Heart { .. } => "Heart",
|
VectorShape::Heart { .. } => "Heart".into(),
|
||||||
VectorShape::Bubble { .. } => "Bubble",
|
VectorShape::Bubble { .. } => "Bubble".into(),
|
||||||
VectorShape::Gear { .. } => "Gear",
|
VectorShape::Gear { .. } => "Gear".into(),
|
||||||
VectorShape::Cross { .. } => "Cross",
|
VectorShape::Cross { .. } => "Cross".into(),
|
||||||
VectorShape::Crescent { .. } => "Crescent",
|
VectorShape::Crescent { .. } => "Crescent".into(),
|
||||||
VectorShape::Bolt { .. } => "Bolt",
|
VectorShape::Bolt { .. } => "Bolt".into(),
|
||||||
VectorShape::Arrow4 { .. } => "4-way Arrow",
|
VectorShape::Arrow4 { .. } => "4-way Arrow".into(),
|
||||||
VectorShape::FreePath { .. } => "Free Path",
|
VectorShape::SvgShape { kind, .. } => kind.clone(),
|
||||||
|
VectorShape::FreePath { .. } => "Free Path".into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
column![
|
column![
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ pub fn sync_shape_from_tool(app: &mut HcieIcedApp) {
|
|||||||
VectorShape::Polygon { .. } => {
|
VectorShape::Polygon { .. } => {
|
||||||
// polygon_sides via engine when available
|
// polygon_sides via engine when available
|
||||||
}
|
}
|
||||||
|
VectorShape::SvgShape { .. } => {
|
||||||
|
// SVG shapes have no extra per-shape properties
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,6 +164,9 @@ pub fn sync_tool_from_shape(app: &mut HcieIcedApp) {
|
|||||||
VectorShape::Polygon { sides, .. } => {
|
VectorShape::Polygon { sides, .. } => {
|
||||||
ts.vector_sides = *sides;
|
ts.vector_sides = *sides;
|
||||||
}
|
}
|
||||||
|
VectorShape::SvgShape { .. } => {
|
||||||
|
// No shape-specific properties for SVG shapes
|
||||||
|
}
|
||||||
VectorShape::Line {
|
VectorShape::Line {
|
||||||
cap_start,
|
cap_start,
|
||||||
cap_end: _,
|
cap_end: _,
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ pub enum VectorShape {
|
|||||||
#[serde(default = "default_hardness")]
|
#[serde(default = "default_hardness")]
|
||||||
hardness: f32,
|
hardness: f32,
|
||||||
},
|
},
|
||||||
|
#[serde(skip_serializing)]
|
||||||
Arrow {
|
Arrow {
|
||||||
x1: f32,
|
x1: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
@@ -371,6 +372,7 @@ pub enum VectorShape {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
thick: bool,
|
thick: bool,
|
||||||
},
|
},
|
||||||
|
#[serde(skip_serializing)]
|
||||||
Star {
|
Star {
|
||||||
x1: f32,
|
x1: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
@@ -411,6 +413,7 @@ pub enum VectorShape {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
sides: u32,
|
sides: u32,
|
||||||
},
|
},
|
||||||
|
#[serde(skip_serializing)]
|
||||||
Rhombus {
|
Rhombus {
|
||||||
x1: f32,
|
x1: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
@@ -428,6 +431,7 @@ pub enum VectorShape {
|
|||||||
#[serde(default = "default_hardness")]
|
#[serde(default = "default_hardness")]
|
||||||
hardness: f32,
|
hardness: f32,
|
||||||
},
|
},
|
||||||
|
#[serde(skip_serializing)]
|
||||||
Cylinder {
|
Cylinder {
|
||||||
x1: f32,
|
x1: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
@@ -445,6 +449,7 @@ pub enum VectorShape {
|
|||||||
#[serde(default = "default_hardness")]
|
#[serde(default = "default_hardness")]
|
||||||
hardness: f32,
|
hardness: f32,
|
||||||
},
|
},
|
||||||
|
#[serde(skip_serializing)]
|
||||||
Heart {
|
Heart {
|
||||||
x1: f32,
|
x1: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
@@ -462,6 +467,7 @@ pub enum VectorShape {
|
|||||||
#[serde(default = "default_hardness")]
|
#[serde(default = "default_hardness")]
|
||||||
hardness: f32,
|
hardness: f32,
|
||||||
},
|
},
|
||||||
|
#[serde(skip_serializing)]
|
||||||
Bubble {
|
Bubble {
|
||||||
x1: f32,
|
x1: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
@@ -479,6 +485,7 @@ pub enum VectorShape {
|
|||||||
#[serde(default = "default_hardness")]
|
#[serde(default = "default_hardness")]
|
||||||
hardness: f32,
|
hardness: f32,
|
||||||
},
|
},
|
||||||
|
#[serde(skip_serializing)]
|
||||||
Gear {
|
Gear {
|
||||||
x1: f32,
|
x1: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
@@ -496,6 +503,7 @@ pub enum VectorShape {
|
|||||||
#[serde(default = "default_hardness")]
|
#[serde(default = "default_hardness")]
|
||||||
hardness: f32,
|
hardness: f32,
|
||||||
},
|
},
|
||||||
|
#[serde(skip_serializing)]
|
||||||
Cross {
|
Cross {
|
||||||
x1: f32,
|
x1: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
@@ -513,6 +521,7 @@ pub enum VectorShape {
|
|||||||
#[serde(default = "default_hardness")]
|
#[serde(default = "default_hardness")]
|
||||||
hardness: f32,
|
hardness: f32,
|
||||||
},
|
},
|
||||||
|
#[serde(skip_serializing)]
|
||||||
Crescent {
|
Crescent {
|
||||||
x1: f32,
|
x1: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
@@ -530,6 +539,7 @@ pub enum VectorShape {
|
|||||||
#[serde(default = "default_hardness")]
|
#[serde(default = "default_hardness")]
|
||||||
hardness: f32,
|
hardness: f32,
|
||||||
},
|
},
|
||||||
|
#[serde(skip_serializing)]
|
||||||
Bolt {
|
Bolt {
|
||||||
x1: f32,
|
x1: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
@@ -547,6 +557,7 @@ pub enum VectorShape {
|
|||||||
#[serde(default = "default_hardness")]
|
#[serde(default = "default_hardness")]
|
||||||
hardness: f32,
|
hardness: f32,
|
||||||
},
|
},
|
||||||
|
#[serde(skip_serializing)]
|
||||||
Arrow4 {
|
Arrow4 {
|
||||||
x1: f32,
|
x1: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
@@ -564,6 +575,25 @@ pub enum VectorShape {
|
|||||||
#[serde(default = "default_hardness")]
|
#[serde(default = "default_hardness")]
|
||||||
hardness: f32,
|
hardness: f32,
|
||||||
},
|
},
|
||||||
|
SvgShape {
|
||||||
|
x1: f32,
|
||||||
|
y1: f32,
|
||||||
|
x2: f32,
|
||||||
|
y2: f32,
|
||||||
|
kind: String,
|
||||||
|
svg: String,
|
||||||
|
stroke: f32,
|
||||||
|
color: [u8; 4],
|
||||||
|
#[serde(default = "default_fill_color")]
|
||||||
|
fill_color: [u8; 4],
|
||||||
|
fill: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
angle: f32,
|
||||||
|
#[serde(default = "default_opacity")]
|
||||||
|
opacity: f32,
|
||||||
|
#[serde(default = "default_hardness")]
|
||||||
|
hardness: f32,
|
||||||
|
},
|
||||||
FreePath {
|
FreePath {
|
||||||
pts: Vec<[f32; 2]>,
|
pts: Vec<[f32; 2]>,
|
||||||
closed: bool,
|
closed: bool,
|
||||||
@@ -1986,6 +2016,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Cross { x1, y1, x2, y2, .. }
|
| VectorShape::Cross { x1, y1, x2, y2, .. }
|
||||||
| VectorShape::Crescent { x1, y1, x2, y2, .. }
|
| VectorShape::Crescent { x1, y1, x2, y2, .. }
|
||||||
| VectorShape::Arrow4 { x1, y1, x2, y2, .. }
|
| VectorShape::Arrow4 { x1, y1, x2, y2, .. }
|
||||||
|
| VectorShape::SvgShape { x1, y1, x2, y2, .. }
|
||||||
| VectorShape::Bolt { x1, y1, x2, y2, .. } => (*x1, *y1, *x2, *y2),
|
| VectorShape::Bolt { x1, y1, x2, y2, .. } => (*x1, *y1, *x2, *y2),
|
||||||
VectorShape::FreePath { pts, .. } => {
|
VectorShape::FreePath { pts, .. } => {
|
||||||
let x1 = pts.iter().map(|p| p[0]).fold(f32::INFINITY, f32::min);
|
let x1 = pts.iter().map(|p| p[0]).fold(f32::INFINITY, f32::min);
|
||||||
@@ -2024,6 +2055,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Crescent { color, .. }
|
| VectorShape::Crescent { color, .. }
|
||||||
| VectorShape::Arrow4 { color, .. }
|
| VectorShape::Arrow4 { color, .. }
|
||||||
| VectorShape::Bolt { color, .. }
|
| VectorShape::Bolt { color, .. }
|
||||||
|
| VectorShape::SvgShape { color, .. }
|
||||||
| VectorShape::FreePath { color, .. } => *color,
|
| VectorShape::FreePath { color, .. } => *color,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2045,6 +2077,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Crescent { color, .. }
|
| VectorShape::Crescent { color, .. }
|
||||||
| VectorShape::Arrow4 { color, .. }
|
| VectorShape::Arrow4 { color, .. }
|
||||||
| VectorShape::Bolt { color, .. }
|
| VectorShape::Bolt { color, .. }
|
||||||
|
| VectorShape::SvgShape { color, .. }
|
||||||
| VectorShape::FreePath { color, .. } => *color = new_color,
|
| VectorShape::FreePath { color, .. } => *color = new_color,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2066,6 +2099,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Crescent { opacity, .. }
|
| VectorShape::Crescent { opacity, .. }
|
||||||
| VectorShape::Arrow4 { opacity, .. }
|
| VectorShape::Arrow4 { opacity, .. }
|
||||||
| VectorShape::Bolt { opacity, .. }
|
| VectorShape::Bolt { opacity, .. }
|
||||||
|
| VectorShape::SvgShape { opacity, .. }
|
||||||
| VectorShape::FreePath { opacity, .. } => *opacity,
|
| VectorShape::FreePath { opacity, .. } => *opacity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2088,6 +2122,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Crescent { opacity, .. }
|
| VectorShape::Crescent { opacity, .. }
|
||||||
| VectorShape::Arrow4 { opacity, .. }
|
| VectorShape::Arrow4 { opacity, .. }
|
||||||
| VectorShape::Bolt { opacity, .. }
|
| VectorShape::Bolt { opacity, .. }
|
||||||
|
| VectorShape::SvgShape { opacity, .. }
|
||||||
| VectorShape::FreePath { opacity, .. } => *opacity = clamped,
|
| VectorShape::FreePath { opacity, .. } => *opacity = clamped,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2109,6 +2144,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Crescent { hardness, .. }
|
| VectorShape::Crescent { hardness, .. }
|
||||||
| VectorShape::Arrow4 { hardness, .. }
|
| VectorShape::Arrow4 { hardness, .. }
|
||||||
| VectorShape::Bolt { hardness, .. }
|
| VectorShape::Bolt { hardness, .. }
|
||||||
|
| VectorShape::SvgShape { hardness, .. }
|
||||||
| VectorShape::FreePath { hardness, .. } => *hardness,
|
| VectorShape::FreePath { hardness, .. } => *hardness,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2131,6 +2167,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Crescent { hardness, .. }
|
| VectorShape::Crescent { hardness, .. }
|
||||||
| VectorShape::Arrow4 { hardness, .. }
|
| VectorShape::Arrow4 { hardness, .. }
|
||||||
| VectorShape::Bolt { hardness, .. }
|
| VectorShape::Bolt { hardness, .. }
|
||||||
|
| VectorShape::SvgShape { hardness, .. }
|
||||||
| VectorShape::FreePath { hardness, .. } => *hardness = clamped,
|
| VectorShape::FreePath { hardness, .. } => *hardness = clamped,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2152,6 +2189,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Crescent { stroke, .. }
|
| VectorShape::Crescent { stroke, .. }
|
||||||
| VectorShape::Arrow4 { stroke, .. }
|
| VectorShape::Arrow4 { stroke, .. }
|
||||||
| VectorShape::Bolt { stroke, .. }
|
| VectorShape::Bolt { stroke, .. }
|
||||||
|
| VectorShape::SvgShape { stroke, .. }
|
||||||
| VectorShape::FreePath { stroke, .. } => *stroke,
|
| VectorShape::FreePath { stroke, .. } => *stroke,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2174,6 +2212,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Crescent { stroke, .. }
|
| VectorShape::Crescent { stroke, .. }
|
||||||
| VectorShape::Arrow4 { stroke, .. }
|
| VectorShape::Arrow4 { stroke, .. }
|
||||||
| VectorShape::Bolt { stroke, .. }
|
| VectorShape::Bolt { stroke, .. }
|
||||||
|
| VectorShape::SvgShape { stroke, .. }
|
||||||
| VectorShape::FreePath { stroke, .. } => *stroke = clamped,
|
| VectorShape::FreePath { stroke, .. } => *stroke = clamped,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2387,6 +2426,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Cross { angle, .. }
|
| VectorShape::Cross { angle, .. }
|
||||||
| VectorShape::Crescent { angle, .. }
|
| VectorShape::Crescent { angle, .. }
|
||||||
| VectorShape::Arrow4 { angle, .. }
|
| VectorShape::Arrow4 { angle, .. }
|
||||||
|
| VectorShape::SvgShape { angle, .. }
|
||||||
| VectorShape::Bolt { angle, .. } => *angle,
|
| VectorShape::Bolt { angle, .. } => *angle,
|
||||||
VectorShape::FreePath { .. } => 0.0,
|
VectorShape::FreePath { .. } => 0.0,
|
||||||
}
|
}
|
||||||
@@ -2408,6 +2448,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Cross { angle, .. }
|
| VectorShape::Cross { angle, .. }
|
||||||
| VectorShape::Crescent { angle, .. }
|
| VectorShape::Crescent { angle, .. }
|
||||||
| VectorShape::Arrow4 { angle, .. }
|
| VectorShape::Arrow4 { angle, .. }
|
||||||
|
| VectorShape::SvgShape { angle, .. }
|
||||||
| VectorShape::Bolt { angle, .. } => {
|
| VectorShape::Bolt { angle, .. } => {
|
||||||
*angle += delta;
|
*angle += delta;
|
||||||
}
|
}
|
||||||
@@ -2431,6 +2472,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Cross { angle, .. }
|
| VectorShape::Cross { angle, .. }
|
||||||
| VectorShape::Crescent { angle, .. }
|
| VectorShape::Crescent { angle, .. }
|
||||||
| VectorShape::Arrow4 { angle, .. }
|
| VectorShape::Arrow4 { angle, .. }
|
||||||
|
| VectorShape::SvgShape { angle, .. }
|
||||||
| VectorShape::Bolt { angle, .. } => {
|
| VectorShape::Bolt { angle, .. } => {
|
||||||
*angle = new_angle;
|
*angle = new_angle;
|
||||||
}
|
}
|
||||||
@@ -2454,6 +2496,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Cross { angle, .. }
|
| VectorShape::Cross { angle, .. }
|
||||||
| VectorShape::Crescent { angle, .. }
|
| VectorShape::Crescent { angle, .. }
|
||||||
| VectorShape::Arrow4 { angle, .. }
|
| VectorShape::Arrow4 { angle, .. }
|
||||||
|
| VectorShape::SvgShape { angle, .. }
|
||||||
| VectorShape::Bolt { angle, .. } => Some(angle),
|
| VectorShape::Bolt { angle, .. } => Some(angle),
|
||||||
VectorShape::FreePath { .. } => None,
|
VectorShape::FreePath { .. } => None,
|
||||||
}
|
}
|
||||||
@@ -2474,6 +2517,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Cross { fill, .. }
|
| VectorShape::Cross { fill, .. }
|
||||||
| VectorShape::Crescent { fill, .. }
|
| VectorShape::Crescent { fill, .. }
|
||||||
| VectorShape::Arrow4 { fill, .. }
|
| VectorShape::Arrow4 { fill, .. }
|
||||||
|
| VectorShape::SvgShape { fill, .. }
|
||||||
| VectorShape::Bolt { fill, .. } => *fill,
|
| VectorShape::Bolt { fill, .. } => *fill,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
@@ -2494,6 +2538,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Cross { fill_color, .. }
|
| VectorShape::Cross { fill_color, .. }
|
||||||
| VectorShape::Crescent { fill_color, .. }
|
| VectorShape::Crescent { fill_color, .. }
|
||||||
| VectorShape::Arrow4 { fill_color, .. }
|
| VectorShape::Arrow4 { fill_color, .. }
|
||||||
|
| VectorShape::SvgShape { fill_color, .. }
|
||||||
| VectorShape::Bolt { fill_color, .. } => Some(*fill_color),
|
| VectorShape::Bolt { fill_color, .. } => Some(*fill_color),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -2514,6 +2559,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Cross { fill, .. }
|
| VectorShape::Cross { fill, .. }
|
||||||
| VectorShape::Crescent { fill, .. }
|
| VectorShape::Crescent { fill, .. }
|
||||||
| VectorShape::Arrow4 { fill, .. }
|
| VectorShape::Arrow4 { fill, .. }
|
||||||
|
| VectorShape::SvgShape { fill, .. }
|
||||||
| VectorShape::Bolt { fill, .. } => Some(*fill),
|
| VectorShape::Bolt { fill, .. } => Some(*fill),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -2534,6 +2580,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Cross { fill, .. }
|
| VectorShape::Cross { fill, .. }
|
||||||
| VectorShape::Crescent { fill, .. }
|
| VectorShape::Crescent { fill, .. }
|
||||||
| VectorShape::Arrow4 { fill, .. }
|
| VectorShape::Arrow4 { fill, .. }
|
||||||
|
| VectorShape::SvgShape { fill, .. }
|
||||||
| VectorShape::Bolt { fill, .. } => Some(fill),
|
| VectorShape::Bolt { fill, .. } => Some(fill),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -2554,6 +2601,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Cross { fill_color, .. }
|
| VectorShape::Cross { fill_color, .. }
|
||||||
| VectorShape::Crescent { fill_color, .. }
|
| VectorShape::Crescent { fill_color, .. }
|
||||||
| VectorShape::Arrow4 { fill_color, .. }
|
| VectorShape::Arrow4 { fill_color, .. }
|
||||||
|
| VectorShape::SvgShape { fill_color, .. }
|
||||||
| VectorShape::Bolt { fill_color, .. } => Some(fill_color),
|
| VectorShape::Bolt { fill_color, .. } => Some(fill_color),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -2576,6 +2624,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Crescent { stroke, .. }
|
| VectorShape::Crescent { stroke, .. }
|
||||||
| VectorShape::Arrow4 { stroke, .. }
|
| VectorShape::Arrow4 { stroke, .. }
|
||||||
| VectorShape::Bolt { stroke, .. }
|
| VectorShape::Bolt { stroke, .. }
|
||||||
|
| VectorShape::SvgShape { stroke, .. }
|
||||||
| VectorShape::FreePath { stroke, .. } => Some(stroke),
|
| VectorShape::FreePath { stroke, .. } => Some(stroke),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2659,6 +2708,7 @@ impl VectorShape {
|
|||||||
| VectorShape::Cross { x1, y1, x2, y2, .. }
|
| VectorShape::Cross { x1, y1, x2, y2, .. }
|
||||||
| VectorShape::Crescent { x1, y1, x2, y2, .. }
|
| VectorShape::Crescent { x1, y1, x2, y2, .. }
|
||||||
| VectorShape::Arrow4 { x1, y1, x2, y2, .. }
|
| VectorShape::Arrow4 { x1, y1, x2, y2, .. }
|
||||||
|
| VectorShape::SvgShape { x1, y1, x2, y2, .. }
|
||||||
| VectorShape::Bolt { x1, y1, x2, y2, .. } => (x1, y1, x2, y2),
|
| VectorShape::Bolt { x1, y1, x2, y2, .. } => (x1, y1, x2, y2),
|
||||||
VectorShape::FreePath { .. } => unreachable!("coords_mut not valid for FreePath"),
|
VectorShape::FreePath { .. } => unreachable!("coords_mut not valid for FreePath"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ hcie-protocol = { path = "../hcie-protocol" }
|
|||||||
hcie-blend = { path = "../hcie-blend" }
|
hcie-blend = { path = "../hcie-blend" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
|
usvg = { workspace = true }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["staticlib", "rlib"]
|
crate-type = ["staticlib", "rlib"]
|
||||||
|
|||||||
+28
-2
@@ -1,6 +1,8 @@
|
|||||||
//! Vector shape rasterization.
|
//! Vector shape rasterization.
|
||||||
|
|
||||||
pub mod path_boolean;
|
pub mod path_boolean;
|
||||||
|
pub mod svg_templates;
|
||||||
|
pub mod svg_render;
|
||||||
|
|
||||||
use hcie_protocol::{Layer, LayerData, VectorShape};
|
use hcie_protocol::{Layer, LayerData, VectorShape};
|
||||||
use hcie_blend::blend_pixels;
|
use hcie_blend::blend_pixels;
|
||||||
@@ -28,6 +30,7 @@ fn get_shape_hardness(shape: &VectorShape) -> f32 {
|
|||||||
Crescent { hardness, .. } => *hardness,
|
Crescent { hardness, .. } => *hardness,
|
||||||
Bolt { hardness, .. } => *hardness,
|
Bolt { hardness, .. } => *hardness,
|
||||||
Arrow4 { hardness, .. } => *hardness,
|
Arrow4 { hardness, .. } => *hardness,
|
||||||
|
SvgShape { hardness, .. } => *hardness,
|
||||||
FreePath { hardness, .. } => *hardness,
|
FreePath { hardness, .. } => *hardness,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -748,6 +751,27 @@ fn render_shape(layer: &mut Layer, shape: &VectorShape) {
|
|||||||
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SvgShape { svg, x1, y1, x2, y2, fill, color, fill_color, stroke, opacity, hardness, .. } => {
|
||||||
|
let points_list = svg_render::svg_to_points(svg, *x1, *y1, *x2, *y2).unwrap_or_default();
|
||||||
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
||||||
|
let px_color = [color[0], color[1], color[2], alpha];
|
||||||
|
let prev = ACTIVE_HARDNESS.with(|h| h.get());
|
||||||
|
ACTIVE_HARDNESS.with(|h| h.set(*hardness));
|
||||||
|
if *fill {
|
||||||
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
||||||
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
||||||
|
for poly in &points_list {
|
||||||
|
fill_polygon(layer, poly, px_fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for poly in &points_list {
|
||||||
|
for i in 0..poly.len() {
|
||||||
|
let j = (i + 1) % poly.len();
|
||||||
|
draw_line(layer, poly[i].0, poly[i].1, poly[j].0, poly[j].1, px_color, *stroke, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ACTIVE_HARDNESS.with(|h| h.set(prev));
|
||||||
|
}
|
||||||
FreePath { pts, stroke, color, fill, fill_color, opacity, .. } => {
|
FreePath { pts, stroke, color, fill, fill_color, opacity, .. } => {
|
||||||
let alpha = (color[3] as f32 * opacity).round() as u8;
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
||||||
let px_color = [color[0], color[1], color[2], alpha];
|
let px_color = [color[0], color[1], color[2], alpha];
|
||||||
@@ -848,7 +872,8 @@ pub fn rotate_shape(shape: &mut VectorShape, delta_degrees: f32) {
|
|||||||
| Arrow { angle, .. } | Star { angle, .. } | Polygon { angle, .. }
|
| Arrow { angle, .. } | Star { angle, .. } | Polygon { angle, .. }
|
||||||
| Rhombus { angle, .. } | Cylinder { angle, .. } | Heart { angle, .. }
|
| Rhombus { angle, .. } | Cylinder { angle, .. } | Heart { angle, .. }
|
||||||
| Bubble { angle, .. } | Gear { angle, .. } | Cross { angle, .. }
|
| Bubble { angle, .. } | Gear { angle, .. } | Cross { angle, .. }
|
||||||
| Crescent { angle, .. } | Bolt { angle, .. } | Arrow4 { angle, .. } => {
|
| Crescent { angle, .. } | Bolt { angle, .. } | Arrow4 { angle, .. }
|
||||||
|
| SvgShape { angle, .. } => {
|
||||||
*angle += delta_degrees * std::f32::consts::PI / 180.0;
|
*angle += delta_degrees * std::f32::consts::PI / 180.0;
|
||||||
}
|
}
|
||||||
FreePath { .. } => {}
|
FreePath { .. } => {}
|
||||||
@@ -863,7 +888,8 @@ pub fn set_shape_angle(shape: &mut VectorShape, new_angle_degrees: f32) {
|
|||||||
| Arrow { angle, .. } | Star { angle, .. } | Polygon { angle, .. }
|
| Arrow { angle, .. } | Star { angle, .. } | Polygon { angle, .. }
|
||||||
| Rhombus { angle, .. } | Cylinder { angle, .. } | Heart { angle, .. }
|
| Rhombus { angle, .. } | Cylinder { angle, .. } | Heart { angle, .. }
|
||||||
| Bubble { angle, .. } | Gear { angle, .. } | Cross { angle, .. }
|
| Bubble { angle, .. } | Gear { angle, .. } | Cross { angle, .. }
|
||||||
| Crescent { angle, .. } | Bolt { angle, .. } | Arrow4 { angle, .. } => {
|
| Crescent { angle, .. } | Bolt { angle, .. } | Arrow4 { angle, .. }
|
||||||
|
| SvgShape { angle, .. } => {
|
||||||
*angle = rad;
|
*angle = rad;
|
||||||
}
|
}
|
||||||
FreePath { .. } => {}
|
FreePath { .. } => {}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/// SVG path tessellator using `usvg`.
|
||||||
|
///
|
||||||
|
/// Parses an SVG string, extracts `<path>` elements, tessellates bezier curves
|
||||||
|
/// to line segments, and scales the result from normalized `viewBox="0 0 1 1"`
|
||||||
|
/// coordinates to the target bounding box `(x1,y1)-(x2,y2)`.
|
||||||
|
|
||||||
|
use usvg::tiny_skia_path::PathSegment;
|
||||||
|
|
||||||
|
/// Parse an SVG string and return tessellated polygons scaled to the target bounds.
|
||||||
|
///
|
||||||
|
/// Returns one polygon per subpath (separated by MoveTo commands).
|
||||||
|
/// Polygons are in document pixel coordinates ready for `fill_polygon` / `draw_line`.
|
||||||
|
pub fn svg_to_points(
|
||||||
|
svg: &str,
|
||||||
|
x1: f32,
|
||||||
|
y1: f32,
|
||||||
|
x2: f32,
|
||||||
|
y2: f32,
|
||||||
|
) -> Result<Vec<Vec<(f32, f32)>>, String> {
|
||||||
|
let opt = usvg::Options::default();
|
||||||
|
let tree = usvg::Tree::from_data(svg.as_bytes(), &opt).map_err(|e| e.to_string())?;
|
||||||
|
let w = (x2 - x1).abs().max(1.0);
|
||||||
|
let h = (y2 - y1).abs().max(1.0);
|
||||||
|
let ox = x1.min(x2);
|
||||||
|
let oy = y1.min(y2);
|
||||||
|
|
||||||
|
let mut result: Vec<Vec<(f32, f32)>> = Vec::new();
|
||||||
|
collect_paths(tree.root(), &mut result, ox, oy, w, h);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_paths(
|
||||||
|
node: &usvg::Group,
|
||||||
|
out: &mut Vec<Vec<(f32, f32)>>,
|
||||||
|
ox: f32,
|
||||||
|
oy: f32,
|
||||||
|
w: f32,
|
||||||
|
h: f32,
|
||||||
|
) {
|
||||||
|
for child in node.children() {
|
||||||
|
match child {
|
||||||
|
usvg::Node::Path(path) => {
|
||||||
|
let data = path.data();
|
||||||
|
let mut current_poly: Vec<(f32, f32)> = Vec::new();
|
||||||
|
|
||||||
|
for seg in data.segments() {
|
||||||
|
match seg {
|
||||||
|
PathSegment::MoveTo(p) => {
|
||||||
|
if !current_poly.is_empty() && current_poly.len() > 2 {
|
||||||
|
out.push(std::mem::take(&mut current_poly));
|
||||||
|
}
|
||||||
|
current_poly.push((
|
||||||
|
ox + p.x * w,
|
||||||
|
oy + p.y * h,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
PathSegment::LineTo(p) => {
|
||||||
|
current_poly.push((
|
||||||
|
ox + p.x * w,
|
||||||
|
oy + p.y * h,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
PathSegment::QuadTo(p1, p) => {
|
||||||
|
if let Some(&(sx, sy)) = current_poly.last() {
|
||||||
|
let steps = 8;
|
||||||
|
for i in 1..=steps {
|
||||||
|
let t = i as f32 / steps as f32;
|
||||||
|
let bx = quad_bezier(sx, ox + p1.x * w, ox + p.x * w, t);
|
||||||
|
let by = quad_bezier(sy, oy + p1.y * w, oy + p.y * h, t);
|
||||||
|
current_poly.push((bx, by));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathSegment::CubicTo(p1, p2, p) => {
|
||||||
|
if let Some(&(sx, sy)) = current_poly.last() {
|
||||||
|
let steps = 8;
|
||||||
|
for i in 1..=steps {
|
||||||
|
let t = i as f32 / steps as f32;
|
||||||
|
let bx = cubic_bezier(sx, ox + p1.x * w, ox + p2.x * w, ox + p.x * w, t);
|
||||||
|
let by = cubic_bezier(sy, oy + p1.y * h, oy + p2.y * h, oy + p.y * h, t);
|
||||||
|
current_poly.push((bx, by));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathSegment::Close => {
|
||||||
|
if let Some(&first) = current_poly.first() {
|
||||||
|
current_poly.push(first);
|
||||||
|
}
|
||||||
|
if current_poly.len() > 2 {
|
||||||
|
out.push(std::mem::take(&mut current_poly));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush remaining open poly
|
||||||
|
if !current_poly.is_empty() && current_poly.len() > 2 {
|
||||||
|
// Close it if it looks like a closed shape
|
||||||
|
if let Some(&first) = current_poly.first() {
|
||||||
|
current_poly.push(first);
|
||||||
|
}
|
||||||
|
out.push(current_poly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usvg::Node::Group(g) => {
|
||||||
|
collect_paths(g, out, ox, oy, w, h);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cubic_bezier(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
|
||||||
|
let u = 1.0 - t;
|
||||||
|
u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quad_bezier(p0: f32, p1: f32, p2: f32, t: f32) -> f32 {
|
||||||
|
let u = 1.0 - t;
|
||||||
|
u * u * p0 + 2.0 * u * t * p1 + t * t * p2
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Generate an SVG string for a vector shape.
|
||||||
|
///
|
||||||
|
/// Each shape is defined in a normalized `viewBox="0 0 1 1"` coordinate system.
|
||||||
|
/// At render time the tessellator scales points to the shape's bounding box.
|
||||||
|
pub fn create_svg(kind: &str, params: &HashMap<String, f32>) -> String {
|
||||||
|
match kind {
|
||||||
|
"arrow" => arrow_svg(params.get("thick").copied().unwrap_or(0.0) > 0.5),
|
||||||
|
"star" => star_svg(
|
||||||
|
params.get("points").copied().unwrap_or(5.0) as u32,
|
||||||
|
params.get("inner_radius").copied().unwrap_or(0.5),
|
||||||
|
),
|
||||||
|
"rhombus" => rhombus_svg(),
|
||||||
|
"cylinder" => cylinder_svg(),
|
||||||
|
"heart" => heart_svg(),
|
||||||
|
"bubble" => bubble_svg(),
|
||||||
|
"gear" => gear_svg(),
|
||||||
|
"cross" => cross_svg(),
|
||||||
|
"crescent" => crescent_svg(),
|
||||||
|
"bolt" => bolt_svg(),
|
||||||
|
"arrow4" => arrow4_svg(),
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap(d: &str) -> String {
|
||||||
|
format!(
|
||||||
|
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><path d="{}"/></svg>"#,
|
||||||
|
d
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arrow: tail → shaft → arrowhead triangle.
|
||||||
|
fn arrow_svg(thick: bool) -> String {
|
||||||
|
let shaft_w = if thick { 0.12 } else { 0.06 };
|
||||||
|
let base_x = 0.7;
|
||||||
|
wrap(&format!(
|
||||||
|
"M 0,{sw} L {bx},{sw} L {bx},{lw} L 1,0.5 L {bx},{rw} L {bx},{sw2} L 0,{sw2} Z",
|
||||||
|
sw = 0.5 - shaft_w,
|
||||||
|
sw2 = 0.5 + shaft_w,
|
||||||
|
bx = base_x,
|
||||||
|
lw = 0.5 - 0.4,
|
||||||
|
rw = 0.5 + 0.4,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Star: parametric points/inner_radius.
|
||||||
|
fn star_svg(points: u32, inner_radius: f32) -> String {
|
||||||
|
let n = points.max(3) as i32;
|
||||||
|
let inner_r = inner_radius.max(0.05) * 0.5;
|
||||||
|
let outer_r = 0.5;
|
||||||
|
let cx = 0.5f32;
|
||||||
|
let cy = 0.5f32;
|
||||||
|
let mut d = String::from("M");
|
||||||
|
for i in 0..(n * 2) {
|
||||||
|
let a = std::f32::consts::PI * 2.0 * i as f32 / (n * 2) as f32
|
||||||
|
- std::f32::consts::PI / 2.0;
|
||||||
|
let r = if i % 2 == 0 { outer_r } else { inner_r };
|
||||||
|
let px = cx + r * a.cos();
|
||||||
|
let py = cy + r * a.sin();
|
||||||
|
if i == 0 {
|
||||||
|
d = format!("M {},{}", px, py);
|
||||||
|
} else {
|
||||||
|
d = format!("{} L {},{}", d, px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.push_str(" Z");
|
||||||
|
wrap(&d)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rhombus (diamond): 4 points.
|
||||||
|
fn rhombus_svg() -> String {
|
||||||
|
wrap("M 0.5,0 L 1,0.5 L 0.5,1 L 0,0.5 Z")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cylinder: top ellipse, bottom ellipse, two side lines.
|
||||||
|
fn cylinder_svg() -> String {
|
||||||
|
wrap("M 0,0.1 A 0.5,0.08 0 0,1 1,0.1 L 1,0.9 A 0.5,0.08 0 0,1 0,0.9 Z")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Heart: parametric heart curve.
|
||||||
|
fn heart_svg() -> String {
|
||||||
|
let steps = 32;
|
||||||
|
let mut d = String::new();
|
||||||
|
for i in 0..steps {
|
||||||
|
let t = std::f32::consts::PI * 2.0 * i as f32 / steps as f32;
|
||||||
|
let x = 0.5 + 0.5 * (16.0 * t.sin().powi(3)) / 16.0;
|
||||||
|
let y = 0.5 - 0.5 * (13.0 * t.cos() - 5.0 * (2.0 * t).cos() - 2.0 * (3.0 * t).cos() - (4.0 * t).cos()) / 16.0;
|
||||||
|
if i == 0 {
|
||||||
|
d = format!("M {},{}", x, y);
|
||||||
|
} else {
|
||||||
|
d = format!("{} L {},{}", d, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.push_str(" Z");
|
||||||
|
wrap(&d)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bubble: elliptical body + triangular tail at bottom-left.
|
||||||
|
fn bubble_svg() -> String {
|
||||||
|
wrap("M 0.86,0.5 A 0.36,0.4 0 1,0 0.14,0.5 A 0.36,0.4 0 0,0 0.5,0.86 L 0.3,1.0 L 0.5,0.82 A 0.36,0.4 0 0,0 0.86,0.5 Z")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gear: 8-tooth gear with inner hole.
|
||||||
|
fn gear_svg() -> String {
|
||||||
|
let teeth = 8;
|
||||||
|
let outer_r = 0.45;
|
||||||
|
let inner_r = 0.3;
|
||||||
|
let cx = 0.5f32;
|
||||||
|
let cy = 0.5f32;
|
||||||
|
let mut d = String::new();
|
||||||
|
for i in 0..(teeth * 2) {
|
||||||
|
let a = std::f32::consts::PI * 2.0 * i as f32 / (teeth * 2) as f32
|
||||||
|
- std::f32::consts::PI / 2.0;
|
||||||
|
let r = if i % 2 == 0 { outer_r } else { inner_r };
|
||||||
|
let px = cx + r * a.cos();
|
||||||
|
let py = cy + r * a.sin();
|
||||||
|
if i == 0 {
|
||||||
|
d = format!("M {},{}", px, py);
|
||||||
|
} else {
|
||||||
|
d = format!("{} L {},{}", d, px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.push_str(" Z");
|
||||||
|
// Add inner hole as a separate subpath in reverse direction
|
||||||
|
let hole_r = 0.12;
|
||||||
|
for i in (0..=16).rev() {
|
||||||
|
let a = std::f32::consts::PI * 2.0 * i as f32 / 16.0;
|
||||||
|
let px = cx + hole_r * a.cos();
|
||||||
|
let py = cy + hole_r * a.sin();
|
||||||
|
if i == 16 {
|
||||||
|
d = format!("{} M {},{}", d, px, py);
|
||||||
|
} else {
|
||||||
|
d = format!("{} L {},{}", d, px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wrap(&d)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cross: four arms extending from center.
|
||||||
|
fn cross_svg() -> String {
|
||||||
|
let arm = 0.25;
|
||||||
|
let cx = 0.5f32;
|
||||||
|
let cy = 0.5f32;
|
||||||
|
let pts = vec![
|
||||||
|
(cx - arm, 0.0),
|
||||||
|
(cx + arm, 0.0),
|
||||||
|
(cx + arm, cy - arm),
|
||||||
|
(1.0, cy - arm),
|
||||||
|
(1.0, cy + arm),
|
||||||
|
(cx + arm, cy + arm),
|
||||||
|
(cx + arm, 1.0),
|
||||||
|
(cx - arm, 1.0),
|
||||||
|
(cx - arm, cy + arm),
|
||||||
|
(0.0, cy + arm),
|
||||||
|
(0.0, cy - arm),
|
||||||
|
(cx - arm, cy - arm),
|
||||||
|
];
|
||||||
|
let mut d = String::new();
|
||||||
|
for (i, &(px, py)) in pts.iter().enumerate() {
|
||||||
|
if i == 0 {
|
||||||
|
d = format!("M {},{}", px, py);
|
||||||
|
} else {
|
||||||
|
d = format!("{} L {},{}", d, px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.push_str(" Z");
|
||||||
|
wrap(&d)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crescent moon: two elliptical arcs.
|
||||||
|
fn crescent_svg() -> String {
|
||||||
|
let steps = 32;
|
||||||
|
let cx = 0.5f32;
|
||||||
|
let cy = 0.5f32;
|
||||||
|
let rx = 0.45;
|
||||||
|
let ry = 0.45;
|
||||||
|
let start_angle = -std::f32::consts::PI * 0.2;
|
||||||
|
let end_angle = std::f32::consts::PI * 1.2;
|
||||||
|
let mut d = String::new();
|
||||||
|
|
||||||
|
// Outer arc
|
||||||
|
for i in 0..=steps {
|
||||||
|
let a = start_angle + (end_angle - start_angle) * i as f32 / steps as f32;
|
||||||
|
let px = cx + rx * a.cos();
|
||||||
|
let py = cy + ry * a.sin();
|
||||||
|
if i == 0 {
|
||||||
|
d = format!("M {},{}", px, py);
|
||||||
|
} else {
|
||||||
|
d = format!("{} L {},{}", d, px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner arc (reversed to create the crescent shape)
|
||||||
|
let inner_cx = cx + rx * 0.4;
|
||||||
|
let inner_rx = rx * 0.7;
|
||||||
|
let inner_ry = ry * 0.7;
|
||||||
|
// Get correction at tips
|
||||||
|
let p_start_outer = (cx + rx * start_angle.cos(), cy + ry * start_angle.sin());
|
||||||
|
let inner_start = (
|
||||||
|
inner_cx + inner_rx * start_angle.cos(),
|
||||||
|
cy + inner_ry * start_angle.sin(),
|
||||||
|
);
|
||||||
|
let p_end_outer = (cx + rx * end_angle.cos(), cy + ry * end_angle.sin());
|
||||||
|
let inner_end = (
|
||||||
|
inner_cx + inner_rx * end_angle.cos(),
|
||||||
|
cy + inner_ry * end_angle.sin(),
|
||||||
|
);
|
||||||
|
let diff_start = (p_start_outer.0 - inner_start.0, p_start_outer.1 - inner_start.1);
|
||||||
|
let diff_end = (p_end_outer.0 - inner_end.0, p_end_outer.1 - inner_end.1);
|
||||||
|
|
||||||
|
for i in (0..=steps).rev() {
|
||||||
|
let t = i as f32 / steps as f32;
|
||||||
|
let a = start_angle + (end_angle - start_angle) * t;
|
||||||
|
let raw_x = inner_cx + inner_rx * a.cos();
|
||||||
|
let raw_y = cy + inner_ry * a.sin();
|
||||||
|
let ox = (1.0 - t) * diff_start.0 + t * diff_end.0;
|
||||||
|
let oy = (1.0 - t) * diff_start.1 + t * diff_end.1;
|
||||||
|
d = format!("{} L {},{}", d, raw_x + ox, raw_y + oy);
|
||||||
|
}
|
||||||
|
d.push_str(" Z");
|
||||||
|
wrap(&d)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lightning bolt: zigzag polygon.
|
||||||
|
fn bolt_svg() -> String {
|
||||||
|
wrap("M 0.68,0 L 1,0.28 L 0.55,0.47 L 0.68,0.55 L 0,0.72 L 0.45,0.53 L 0.32,0.45 L 0.68,0 Z")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 4-way arrow: cross with triangular arrowheads at all four ends.
|
||||||
|
fn arrow4_svg() -> String {
|
||||||
|
let cx = 0.5f32;
|
||||||
|
let cy = 0.5f32;
|
||||||
|
let arm_w = 0.125;
|
||||||
|
let arm_h = 0.125;
|
||||||
|
let head_ext = 0.15;
|
||||||
|
|
||||||
|
let pts = vec![
|
||||||
|
// Up arm
|
||||||
|
(cx - arm_w, cy - arm_h),
|
||||||
|
(cx - arm_w, 0.0 + cy - 0.5 + head_ext),
|
||||||
|
(cx, 0.0),
|
||||||
|
(cx + arm_w, 0.0 + cy - 0.5 + head_ext),
|
||||||
|
(cx + arm_w, cy - arm_h),
|
||||||
|
// Right arm
|
||||||
|
(cx + arm_w, cy - arm_h),
|
||||||
|
(1.0 - head_ext, cy - arm_h),
|
||||||
|
(1.0, cy),
|
||||||
|
(1.0 - head_ext, cy + arm_h),
|
||||||
|
(cx + arm_w, cy + arm_h),
|
||||||
|
// Down arm
|
||||||
|
(cx + arm_w, cy + arm_h),
|
||||||
|
(cx + arm_w, 1.0 - cy + 0.5 - head_ext),
|
||||||
|
(cx, 1.0),
|
||||||
|
(cx - arm_w, 1.0 - cy + 0.5 - head_ext),
|
||||||
|
(cx - arm_w, cy + arm_h),
|
||||||
|
// Left arm
|
||||||
|
(cx - arm_w, cy + arm_h),
|
||||||
|
(0.0 + head_ext, cy + arm_h),
|
||||||
|
(0.0, cy),
|
||||||
|
(0.0 + head_ext, cy - arm_h),
|
||||||
|
(cx - arm_w, cy - arm_h),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut d = String::new();
|
||||||
|
for (i, &(px, py)) in pts.iter().enumerate() {
|
||||||
|
if i == 0 {
|
||||||
|
d = format!("M {},{}", px, py);
|
||||||
|
} else {
|
||||||
|
d = format!("{} L {},{}", d, px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.push_str(" Z");
|
||||||
|
wrap(&d)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user