mirror of
https://github.com/penpot/penpot.git
synced 2026-07-21 05:27:50 +00:00
🐛 Fix stroke to path extra points (#10190)
* 🐛 Fix stroke to path extra points * 🐛 Set evenodd when needed on stroke to path (#10446) --------- Co-authored-by: Elena Torró <elenatorro@gmail.com>
This commit is contained in:
parent
a9f3949abc
commit
2d4a24bf97
@ -104,17 +104,19 @@
|
||||
(into []
|
||||
(keep-indexed
|
||||
(fn [idx stroke]
|
||||
(let [content (wasm.api/stroke-to-path (:id shape) idx)]
|
||||
(when (some? content)
|
||||
(let [result (wasm.api/stroke-to-path (:id shape) idx)]
|
||||
(when (some? result)
|
||||
(cts/setup-shape
|
||||
{:type :path
|
||||
:id (uuid/next)
|
||||
:name (str (:name shape) " (stroke)")
|
||||
:parent-id parent-id
|
||||
:frame-id frame-id
|
||||
:content content
|
||||
:fills [(stroke->fill stroke)]
|
||||
:strokes []})))))
|
||||
(cond-> {:type :path
|
||||
:id (uuid/next)
|
||||
:name (str (:name shape) " (stroke)")
|
||||
:parent-id parent-id
|
||||
:frame-id frame-id
|
||||
:content (:content result)
|
||||
:fills [(stroke->fill stroke)]
|
||||
:strokes []}
|
||||
(:even-odd? result)
|
||||
(assoc :svg-attrs {:fillRule "evenodd"})))))))
|
||||
(:strokes shape)))
|
||||
|
||||
(defn convert-selected-strokes-to-path
|
||||
|
||||
@ -2360,21 +2360,25 @@
|
||||
|
||||
(defn stroke-to-path
|
||||
"Converts a shape's stroke at the given index into a filled path.
|
||||
Returns the stroke outline as PathData content."
|
||||
Returns a map {:content <PathData> :even-odd? <boolean>}, or nil when the
|
||||
stroke produces no geometry. The buffer carries two header words ahead of
|
||||
the segments: [even-odd flag][length] (the flat segment list can't encode
|
||||
the fill rule itself)."
|
||||
[id stroke-index]
|
||||
(use-shape id)
|
||||
(try
|
||||
(let [offset (-> (h/call wasm/internal-module "_convert_stroke_to_path" stroke-index)
|
||||
(mem/->offset-32))
|
||||
heap (mem/get-heap-u32)
|
||||
length (aget heap offset)]
|
||||
(let [offset (-> (h/call wasm/internal-module "_convert_stroke_to_path" stroke-index)
|
||||
(mem/->offset-32))
|
||||
heap (mem/get-heap-u32)
|
||||
even-odd? (not (zero? (aget heap offset)))
|
||||
length (aget heap (inc offset))]
|
||||
(if (pos? length)
|
||||
(let [data (mem/slice heap
|
||||
(+ offset 1)
|
||||
(+ offset 2)
|
||||
(* length path.impl/SEGMENT-U32-SIZE))
|
||||
content (path/from-bytes data)]
|
||||
(mem/free)
|
||||
content)
|
||||
{:content content :even-odd? even-odd?})
|
||||
(do (mem/free)
|
||||
nil)))
|
||||
(catch :default cause
|
||||
|
||||
@ -95,3 +95,23 @@ pub fn write_vec<T: SerializableResult>(result: Vec<T>) -> *mut u8 {
|
||||
|
||||
write_bytes(result_bytes)
|
||||
}
|
||||
|
||||
/*
|
||||
Like `write_vec`, but prepends an extra u32 header word before the
|
||||
length. Layout: [header u32][length u32][items...]
|
||||
*/
|
||||
pub fn write_vec_with_header<T: SerializableResult>(header: u32, result: Vec<T>) -> *mut u8 {
|
||||
let elem_size = size_of::<T::BytesType>();
|
||||
let bytes_len = 8 + result.len() * elem_size;
|
||||
let mut result_bytes = vec![0; bytes_len];
|
||||
|
||||
result_bytes[0..4].clone_from_slice(&header.to_le_bytes());
|
||||
result_bytes[4..8].clone_from_slice(&result.len().to_le_bytes());
|
||||
|
||||
for (i, item) in result.iter().enumerate() {
|
||||
let base = 8 + i * elem_size;
|
||||
item.clone_to_slice(&mut result_bytes[base..base + elem_size]);
|
||||
}
|
||||
|
||||
write_bytes(result_bytes)
|
||||
}
|
||||
|
||||
@ -156,6 +156,7 @@ impl Path {
|
||||
let mut current_point = 0;
|
||||
let mut current_conic = 0;
|
||||
let mut last_point = skia::Point::new(0.0, 0.0);
|
||||
let mut subpath_start = skia::Point::new(0.0, 0.0);
|
||||
|
||||
for verb in verbs {
|
||||
match verb {
|
||||
@ -163,12 +164,15 @@ impl Path {
|
||||
let p = points[current_point];
|
||||
segments.push(Segment::MoveTo((p.x, p.y)));
|
||||
last_point = p;
|
||||
subpath_start = p;
|
||||
current_point += 1;
|
||||
}
|
||||
skia::PathVerb::Line => {
|
||||
let p = points[current_point];
|
||||
segments.push(Segment::LineTo((p.x, p.y)));
|
||||
last_point = p;
|
||||
if p != last_point {
|
||||
segments.push(Segment::LineTo((p.x, p.y)));
|
||||
last_point = p;
|
||||
}
|
||||
current_point += 1;
|
||||
}
|
||||
skia::PathVerb::Quad => {
|
||||
@ -239,6 +243,13 @@ impl Path {
|
||||
current_point += 3;
|
||||
}
|
||||
skia::PathVerb::Close => {
|
||||
if let Some(Segment::LineTo(p)) = segments.last() {
|
||||
if (p.0 - subpath_start.x).abs() < 1e-5
|
||||
&& (p.1 - subpath_start.y).abs() < 1e-5
|
||||
{
|
||||
segments.pop();
|
||||
}
|
||||
}
|
||||
segments.push(Segment::Close);
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,27 +58,24 @@ pub fn stroke_to_path(
|
||||
// For inner/outer strokes, use boolean ops to clip
|
||||
// the 2×-width stroke outline to the correct region.
|
||||
// Set EvenOdd to preserve the annular ring's inner hole,
|
||||
// then as_winding() on the result fixes contour winding
|
||||
// for Penpot's NonZero fill rule.
|
||||
// then switch to Winding for Penpot's NonZero fill rule.
|
||||
// Use set_fill_type instead of as_winding() because as_winding()
|
||||
// decomposes self-intersecting geometry, which removes points
|
||||
// at intersections of straight lines in closed paths.
|
||||
// Center strokes skip the conversion: fill_path_with_paint
|
||||
// already produces correctly-wound contours.
|
||||
let final_path = match render_kind {
|
||||
StrokeKind::Inner => {
|
||||
stroke_outline.set_fill_type(skia::PathFillType::EvenOdd);
|
||||
let inner = stroke_outline
|
||||
.op(&transformed_shape_path, skia::PathOp::Intersect)
|
||||
.unwrap_or(stroke_outline);
|
||||
inner.as_winding().unwrap_or(inner)
|
||||
}
|
||||
StrokeKind::Outer => {
|
||||
stroke_outline.set_fill_type(skia::PathFillType::EvenOdd);
|
||||
let outer = stroke_outline
|
||||
.op(&transformed_shape_path, skia::PathOp::Difference)
|
||||
.unwrap_or(stroke_outline);
|
||||
outer.as_winding().unwrap_or(outer)
|
||||
}
|
||||
StrokeKind::Center => {
|
||||
stroke_outline.set_fill_type(skia::PathFillType::EvenOdd);
|
||||
stroke_outline.as_winding().unwrap_or(stroke_outline)
|
||||
}
|
||||
StrokeKind::Inner => stroke_outline
|
||||
.simplify()
|
||||
.unwrap()
|
||||
.op(&transformed_shape_path, skia::PathOp::Intersect)
|
||||
.unwrap_or(stroke_outline),
|
||||
StrokeKind::Outer => stroke_outline
|
||||
.simplify()
|
||||
.unwrap()
|
||||
.op(&transformed_shape_path, skia::PathOp::Difference)
|
||||
.unwrap_or(stroke_outline),
|
||||
StrokeKind::Center => stroke_outline.simplify().unwrap_or(stroke_outline),
|
||||
};
|
||||
|
||||
// If there was a path_transform, invert it back to local coords
|
||||
|
||||
@ -243,6 +243,7 @@ pub extern "C" fn current_to_path() -> *mut u8 {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn convert_stroke_to_path(stroke_index: i32) -> *mut u8 {
|
||||
let mut result = Vec::<RawSegmentData>::default();
|
||||
let mut even_odd = false;
|
||||
with_current_shape!(state, |shape: &Shape| {
|
||||
let idx = stroke_index as usize;
|
||||
if let Some(stroke) = shape.strokes.get(idx) {
|
||||
@ -257,6 +258,7 @@ pub extern "C" fn convert_stroke_to_path(stroke_index: i32) -> *mut u8 {
|
||||
shape.svg_attrs.as_ref(),
|
||||
false,
|
||||
) {
|
||||
even_odd = path.is_even_odd();
|
||||
result = path
|
||||
.segments()
|
||||
.iter()
|
||||
@ -267,7 +269,7 @@ pub extern "C" fn convert_stroke_to_path(stroke_index: i32) -> *mut u8 {
|
||||
}
|
||||
});
|
||||
|
||||
mem::write_vec(result)
|
||||
mem::write_vec_with_header(even_odd as u32, result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user