mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
Merge fix/617
This commit is contained in:
@@ -468,9 +468,9 @@ impl VectorNetwork {
|
||||
start_dir(&self.outward(seg, at_a)?)
|
||||
}
|
||||
|
||||
/// Whether any point has a stroke cap or join of its own.
|
||||
pub fn has_point_styles(&self) -> bool {
|
||||
self.vertices.iter().any(|v| v.cap.is_some() || v.join.is_some())
|
||||
/// Whether any point has a stroke cap of its own or a join other than the layer's `layer_join`.
|
||||
pub fn has_point_styles(&self, layer_join: StrokeJoin) -> bool {
|
||||
self.vertices.iter().any(|v| v.cap.is_some() || v.join.is_some_and(|j| j != layer_join))
|
||||
}
|
||||
|
||||
/// The fill path: every region's loops.
|
||||
|
||||
@@ -903,12 +903,20 @@ impl<'a> Mapper<'a> {
|
||||
size.zip(norm).filter(|(s, n)| s.is_finite() && *n > 1e-9).map_or(1.0, |(s, n)| s / n)
|
||||
};
|
||||
let (sx, sy) = (scale("x"), scale("y"));
|
||||
// An absent layer join is the source default.
|
||||
let layer_join = v.str("strokeJoin").unwrap_or("MITER");
|
||||
let mut unsupported_join = false;
|
||||
let mut unresolved_style = false;
|
||||
let mut unknown_join = false;
|
||||
let vertices: Vec<Value> = net
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let mut out = json!({"x": p.x * sx, "y": p.y * sy});
|
||||
let st = style(p.style);
|
||||
if p.style != 0 && st.is_none() {
|
||||
unresolved_style = true;
|
||||
}
|
||||
if let Some(r) = st.and_then(|s| num(s, "cornerRadius")).filter(|r| *r > 0.0) {
|
||||
out["radius"] = json!(r);
|
||||
}
|
||||
@@ -918,6 +926,15 @@ impl<'a> Mapper<'a> {
|
||||
Some("NONE") => out["mirror"] = json!("none"),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(raw) = st.and_then(|s| s.get("strokeJoin")) {
|
||||
match raw.str() {
|
||||
Some(join @ ("MITER" | "BEVEL" | "ROUND")) => {
|
||||
out["join"] = json!(join.to_ascii_lowercase());
|
||||
unsupported_join |= join != layer_join;
|
||||
}
|
||||
_ => unknown_join = true,
|
||||
}
|
||||
}
|
||||
out
|
||||
})
|
||||
.collect();
|
||||
@@ -928,9 +945,14 @@ impl<'a> Mapper<'a> {
|
||||
if net.regions.iter().any(|r| style(r.style).is_some_and(|s| s.get("fillPaints").is_some())) {
|
||||
self.warn("filled areas of a vector with their own fill use the layer's fill");
|
||||
}
|
||||
let join = v.str("strokeJoin");
|
||||
if net.vertices.iter().any(|p| style(p.style).and_then(|s| s.get("strokeJoin")).and_then(Kv::str).is_some_and(|j| Some(j) != join)) {
|
||||
self.warn("points with their own stroke join use the layer's");
|
||||
if unresolved_style {
|
||||
self.warn("some vector points refer to point settings missing from the file; they use the layer's joins and plain corners");
|
||||
}
|
||||
if unknown_join {
|
||||
self.warn("vector points with unknown stroke joins use the layer join");
|
||||
}
|
||||
if unsupported_join && (o.contains_key("strokeDashes") || o.contains_key("strokeWidthProfile")) {
|
||||
self.warn("point stroke joins were preserved; dashed or variable-width strokes render with the layer join");
|
||||
}
|
||||
// The stroke starts at the first end point and stops at the last (as the editor draws it).
|
||||
let mut degree = vec![0usize; net.vertices.len()];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! fixtures built in code with the parent module's helpers.
|
||||
|
||||
use figcraft_doc::proto::Action;
|
||||
use figcraft_doc::{HandleMirroring, StrokeCap, WindingRule};
|
||||
use figcraft_doc::{HandleMirroring, StrokeCap, StrokeJoin, WindingRule};
|
||||
|
||||
use super::super::network::{self, Network, Region, Segment, Vertex};
|
||||
use super::*;
|
||||
@@ -19,7 +19,7 @@ fn vtx(style: u32, x: f64, y: f64) -> Vertex {
|
||||
/// A square with one curved side and a rounded, mirrored corner, filled even-odd.
|
||||
fn square_blob() -> Vec<u8> {
|
||||
network::encode(&Network {
|
||||
vertices: vec![vtx(0, 0.0, 0.0), vtx(1, 50.0, 0.0), vtx(0, 50.0, 50.0), vtx(0, 0.0, 50.0)],
|
||||
vertices: vec![vtx(0, 0.0, 0.0), vtx(1, 50.0, 0.0), vtx(2, 50.0, 50.0), vtx(3, 0.0, 50.0)],
|
||||
segments: vec![
|
||||
seg(0, 1, (10.0, 0.0), (-10.0, 0.0)),
|
||||
seg(1, 2, (0.0, 0.0), (0.0, 0.0)),
|
||||
@@ -39,6 +39,15 @@ fn arrow_blob() -> Vec<u8> {
|
||||
})
|
||||
}
|
||||
|
||||
/// An open path with one missing style ID and one unsupported point join.
|
||||
fn join_fallback_blob() -> Vec<u8> {
|
||||
network::encode(&Network {
|
||||
vertices: vec![vtx(4, 0.0, 0.0), vtx(5, 50.0, 0.0), vtx(0, 100.0, 0.0)],
|
||||
segments: vec![seg(0, 1, (0.0, 0.0), (0.0, 0.0)), seg(1, 2, (0.0, 0.0), (0.0, 0.0))],
|
||||
regions: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn vector_data(blob: u64, nx: f64, ny: f64, styles: Vec<Kv<'static>>) -> Kv<'static> {
|
||||
o(vec![("vectorNetworkBlob", u(blob)), ("normalizedSize", xy(nx, ny)), ("styleOverrideTable", arr(styles))])
|
||||
}
|
||||
@@ -121,13 +130,24 @@ fn tail_message() -> Kv<'static> {
|
||||
("transform", at(700.0, 0.0)),
|
||||
("fillPaints", arr(vec![solid(1.0, 0.0, 0.0)])),
|
||||
("fillGeometry", outline(0)),
|
||||
("strokeJoin", e("MITER")),
|
||||
("dashPattern", arr(vec![f(4.0), f(2.0)])),
|
||||
(
|
||||
"vectorData",
|
||||
vector_data(
|
||||
1,
|
||||
100.0,
|
||||
50.0,
|
||||
vec![o(vec![("styleID", u(1)), ("cornerRadius", f(4.0)), ("handleMirroring", e("ANGLE_AND_LENGTH"))])],
|
||||
vec![
|
||||
o(vec![
|
||||
("styleID", u(1)),
|
||||
("cornerRadius", f(4.0)),
|
||||
("handleMirroring", e("ANGLE_AND_LENGTH")),
|
||||
("strokeJoin", e("BEVEL")),
|
||||
]),
|
||||
o(vec![("styleID", u(2)), ("strokeJoin", e("MITER"))]),
|
||||
o(vec![("styleID", u(3)), ("strokeJoin", e("ROUND"))]),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -146,6 +166,19 @@ fn tail_message() -> Kv<'static> {
|
||||
("vectorData", vector_data(2, 100.0, 0.0, vec![o(vec![("styleID", u(2)), ("strokeCap", e("ARROW_LINES"))])])),
|
||||
],
|
||||
),
|
||||
node(
|
||||
42,
|
||||
Some((1, "#")),
|
||||
"VECTOR",
|
||||
"Join Fallbacks",
|
||||
vec![
|
||||
("size", xy(100.0, 10.0)),
|
||||
("transform", at(0.0, 330.0)),
|
||||
stroke(),
|
||||
("strokeJoin", e("MITER")),
|
||||
("vectorData", vector_data(4, 100.0, 10.0, vec![o(vec![("styleID", u(5)), ("strokeJoin", u(999))])])),
|
||||
],
|
||||
),
|
||||
node(
|
||||
32,
|
||||
Some((1, "#")),
|
||||
@@ -223,7 +256,7 @@ fn tail_message() -> Kv<'static> {
|
||||
];
|
||||
let mut broken = square_blob();
|
||||
broken.truncate(30);
|
||||
let blobs = [triangle_blob(), square_blob(), arrow_blob(), broken];
|
||||
let blobs = [triangle_blob(), square_blob(), arrow_blob(), broken, join_fallback_blob()];
|
||||
o(vec![("nodeChanges", arr(nodes)), ("blobs", arr(blobs.into_iter().map(|b| o(vec![("bytes", Kv::Bytes(b))])).collect()))])
|
||||
}
|
||||
|
||||
@@ -247,6 +280,11 @@ fn vectors_import_as_editable_networks() {
|
||||
assert_eq!(pts, [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]);
|
||||
assert_eq!((wave.vertices[1].radius, wave.vertices[1].mirror), (4.0, Some(HandleMirroring::AngleAndLength)));
|
||||
assert_eq!(wave.vertices[0].mirror, None);
|
||||
assert_eq!(
|
||||
wave.vertices.iter().map(|v| v.join).collect::<Vec<_>>(),
|
||||
[None, Some(StrokeJoin::Bevel), Some(StrokeJoin::Miter), Some(StrokeJoin::Round)]
|
||||
);
|
||||
assert_eq!(named(&doc, "Wave").stroke.join, StrokeJoin::Miter);
|
||||
assert_eq!((wave.segments[0].ta, wave.segments[0].tb), ((20.0, 0.0), (-20.0, 0.0)));
|
||||
assert_eq!(wave.regions.len(), 1);
|
||||
assert_eq!((wave.regions[0].loops.clone(), wave.regions[0].winding), (vec![vec![0, 1, 2, 3]], WindingRule::EvenOdd));
|
||||
@@ -260,6 +298,17 @@ fn vectors_import_as_editable_networks() {
|
||||
// A damaged network falls back to the outline.
|
||||
assert_eq!(network_of(&doc, "Broken").regions.len(), 1);
|
||||
assert!(warnings.iter().any(|w| w.contains("damaged vector networks import as outlines")), "{warnings:?}");
|
||||
let fallback = network_of(&doc, "Join Fallbacks");
|
||||
assert!(fallback.vertices.iter().all(|v| v.join.is_none()));
|
||||
assert_eq!(named(&doc, "Join Fallbacks").stroke.join, StrokeJoin::Miter);
|
||||
assert!(
|
||||
warnings
|
||||
.iter()
|
||||
.any(|w| w.contains("vector points refer to point settings missing from the file; they use the layer's joins and plain corners")),
|
||||
"{warnings:?}"
|
||||
);
|
||||
assert!(warnings.iter().any(|w| w.contains("unknown stroke joins use the layer join")), "{warnings:?}");
|
||||
assert!(warnings.iter().any(|w| w.contains("dashed or variable-width strokes render with the layer join")), "{warnings:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -73,7 +73,7 @@ message Event InteractionType interactionType
|
||||
message Action GUID transitionNodeID, TransitionType transitionType, float transitionDuration, EasingType easingType, ConnectionType connectionType, NavigationType navigationType, VariableTarget targetVariable, VariableData targetVariableData, VariableSetID targetVariableSetID, GUID targetVariableModeID, ConditionalActions[] conditionalActions
|
||||
message Interaction Event event, Action[] actions
|
||||
message StartingPoint string name, string position
|
||||
message NodeChange GUID guid, ParentIndex parentIndex, NodeType type, string name, bool visible, float opacity, Vector size, Matrix transform, Paint[] fillPaints, Paint[] strokePaints, float strokeWeight, float cornerRadius, bool resizeToFit, bool internalOnly, float fontSize, FontName fontName, TextData textData, uint styleID, Path[] fillGeometry, Path[] strokeGeometry, StyleType styleType, StyleId styleIdForFill, SymbolData symbolData, PropDef[] componentPropDefs, PropAssignment[] componentPropAssignments, VariableDataMap parameterConsumptionMap, SetMode[] variableSetModes, VariableID variableSetID, ResolvedType variableResolvedType, Values variableDataValues, ModeMap variableModeBySetMap, Interaction[] prototypeInteractions, StartingPoint prototypeStartingPoint, GUIDPath guidPath, StackMode stackMode, float stackSpacing, float stackHorizontalPadding, float stackVerticalPadding, uint count, float starInnerScale, VectorData vectorData, StrokeCap strokeCap, StrokeJoin strokeJoin, VectorMirror handleMirroring, ShapeWithTextType shapeWithTextType, ConnectorEndpoint connectorStart, ConnectorEndpoint connectorEnd, StrokeCap connectorStartCap, StrokeCap connectorEndCap, ConnectorLineStyle connectorLineStyle, ConstraintType horizontalConstraint, ConstraintType verticalConstraint, NodeChange[] derivedSymbolData, string key, string version, LeadingTrim leadingTrim
|
||||
message NodeChange GUID guid, ParentIndex parentIndex, NodeType type, string name, bool visible, float opacity, Vector size, Matrix transform, Paint[] fillPaints, Paint[] strokePaints, float strokeWeight, float cornerRadius, bool resizeToFit, bool internalOnly, float fontSize, FontName fontName, TextData textData, uint styleID, Path[] fillGeometry, Path[] strokeGeometry, StyleType styleType, StyleId styleIdForFill, SymbolData symbolData, PropDef[] componentPropDefs, PropAssignment[] componentPropAssignments, VariableDataMap parameterConsumptionMap, SetMode[] variableSetModes, VariableID variableSetID, ResolvedType variableResolvedType, Values variableDataValues, ModeMap variableModeBySetMap, Interaction[] prototypeInteractions, StartingPoint prototypeStartingPoint, GUIDPath guidPath, StackMode stackMode, float stackSpacing, float stackHorizontalPadding, float stackVerticalPadding, uint count, float starInnerScale, VectorData vectorData, StrokeCap strokeCap, StrokeJoin strokeJoin, VectorMirror handleMirroring, ShapeWithTextType shapeWithTextType, ConnectorEndpoint connectorStart, ConnectorEndpoint connectorEnd, StrokeCap connectorStartCap, StrokeCap connectorEndCap, ConnectorLineStyle connectorLineStyle, ConstraintType horizontalConstraint, ConstraintType verticalConstraint, NodeChange[] derivedSymbolData, string key, string version, LeadingTrim leadingTrim, float[] dashPattern
|
||||
message Blob byte[] bytes
|
||||
message Message NodeChange[] nodeChanges, Blob[] blobs
|
||||
";
|
||||
|
||||
@@ -537,6 +537,50 @@ mod tests {
|
||||
assert!(arrowhead(&BezPath::new(), StrokeCap::LineArrow, 1.0, true).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_point_joins_keep_the_layer_stroke_and_differing_ones_apply() {
|
||||
let mut path = BezPath::new();
|
||||
path.move_to((0.0, 0.0));
|
||||
path.line_to((10.0, 10.0));
|
||||
path.line_to((20.0, 0.0));
|
||||
let net = VectorNetwork {
|
||||
vertices: vec![
|
||||
figcraft_doc::Vertex { cap: Some(StrokeCap::Square), ..figcraft_doc::Vertex::new(0.0, 0.0) },
|
||||
figcraft_doc::Vertex { x: 10.0, y: 10.0, radius: 2.0, join: Some(StrokeJoin::Miter), ..figcraft_doc::Vertex::new(10.0, 10.0) },
|
||||
figcraft_doc::Vertex { cap: Some(StrokeCap::Square), ..figcraft_doc::Vertex::new(20.0, 0.0) },
|
||||
],
|
||||
segments: vec![figcraft_doc::Segment::line(0, 1), figcraft_doc::Segment::line(1, 2)],
|
||||
..Default::default()
|
||||
};
|
||||
let props = StrokeProps { weight: 4.0, ..Default::default() };
|
||||
let inherited = stroke_outline_of(&path, &props, false, Some(&net));
|
||||
let mut without_join = net.clone();
|
||||
without_join.vertices[1].join = None;
|
||||
let baseline = stroke_outline_of(&path, &props, false, Some(&without_join));
|
||||
assert_eq!(inherited, baseline);
|
||||
|
||||
let different_layer = StrokeProps { join: StrokeJoin::Bevel, ..props };
|
||||
let explicit = stroke_outline_of(&path, &different_layer, false, Some(&net));
|
||||
let layer_only = stroke_outline_of(&path, &different_layer, false, Some(&without_join));
|
||||
assert_ne!(explicit, layer_only);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_joins_do_not_break_closed_stroke_runs() {
|
||||
let path = sq(0.0, 0.0, 20.0);
|
||||
let mut net = VectorNetwork::from_points(&[Point::new(0.0, 0.0), Point::new(20.0, 0.0), Point::new(20.0, 20.0), Point::new(0.0, 20.0)], true);
|
||||
for vertex in &mut net.vertices {
|
||||
vertex.join = Some(StrokeJoin::Round);
|
||||
}
|
||||
let props = StrokeProps { join: StrokeJoin::Round, weight: 4.0, ..Default::default() };
|
||||
let styled = stroke_outline_of(&path, &props, true, Some(&net));
|
||||
for vertex in &mut net.vertices {
|
||||
vertex.join = None;
|
||||
}
|
||||
let inherited = stroke_outline_of(&path, &props, true, Some(&net));
|
||||
assert_eq!(styled, inherited);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashes() {
|
||||
let mut line = BezPath::new();
|
||||
|
||||
@@ -18,11 +18,17 @@ pub(crate) struct Styled {
|
||||
impl Styled {
|
||||
/// `None` when no point has a style of its own, or the stroke is dashed, variable or a brush.
|
||||
/// shortcut: dashed, variable-width and brush strokes keep the layer's caps and joins at every point; stroke them run by run too if asked for.
|
||||
/// Joins equal to the layer do not require styled runs; stored overrides remain unchanged.
|
||||
pub(crate) fn new(net: &VectorNetwork, props: &StrokeProps) -> Option<Styled> {
|
||||
if !net.has_point_styles() || !props.dashes.is_empty() || props.profile().is_some() || props.brush.is_some() {
|
||||
if !net.has_point_styles(props.join) || !props.dashes.is_empty() || props.profile().is_some() || props.brush.is_some() {
|
||||
return None;
|
||||
}
|
||||
let net = net.rounded().unwrap_or_else(|| net.clone());
|
||||
let mut net = net.rounded().unwrap_or_else(|| net.clone());
|
||||
for vertex in &mut net.vertices {
|
||||
if vertex.join == Some(props.join) {
|
||||
vertex.join = None;
|
||||
}
|
||||
}
|
||||
let runs = net.stroke_runs();
|
||||
let mut degrees = vec![0usize; net.vertices.len()];
|
||||
for s in &net.segments {
|
||||
|
||||
@@ -155,7 +155,7 @@ pub fn stroke_area(n: &Node, path: &BezPath, closed: bool) -> Arc<BezPath> {
|
||||
props.dashes.len().hash(&mut h);
|
||||
// Points' own caps and joins (vector networks).
|
||||
let net = match &n.kind {
|
||||
NodeKind::Vector { network } if network.has_point_styles() => Some(network),
|
||||
NodeKind::Vector { network } if network.has_point_styles(props.join) => Some(network),
|
||||
_ => None,
|
||||
};
|
||||
for v in net.map(|n| n.vertices.as_slice()).unwrap_or_default() {
|
||||
|
||||
@@ -638,7 +638,7 @@ impl W<'_> {
|
||||
|| k.brush.is_some()
|
||||
|| k.cap_start.is_arrow()
|
||||
|| k.cap_end.is_arrow()
|
||||
|| matches!(&n.kind, NodeKind::Vector { network } if network.has_point_styles())
|
||||
|| matches!(&n.kind, NodeKind::Vector { network } if network.has_point_styles(k.join))
|
||||
|| !(w.is_finite() && w > 0.0)
|
||||
{
|
||||
return None;
|
||||
|
||||
@@ -115,3 +115,28 @@ fn text_run_fill_survives_live_text_export() {
|
||||
assert!(svg.contains("#000000"), "{svg}");
|
||||
assert!(svg.contains("fill=\"#00FF00\"") && svg.contains(">b</tspan>") && svg.contains(">a</tspan>"), "{svg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_joins_equal_to_the_layer_join_keep_a_plain_stroke() {
|
||||
use figcraft_doc::{StrokeJoin, VectorNetwork};
|
||||
use figcraft_geom::kurbo::Point;
|
||||
let svg_with = |join: StrokeJoin| {
|
||||
let mut network = VectorNetwork::from_points(&[Point::new(0.0, 0.0), Point::new(50.0, 50.0), Point::new(100.0, 0.0)], false);
|
||||
for v in &mut network.vertices {
|
||||
v.join = Some(StrokeJoin::Miter);
|
||||
}
|
||||
if let Some(v) = network.vertices.get_mut(1) {
|
||||
v.join = Some(join);
|
||||
}
|
||||
let mut d = Document::new("t");
|
||||
let p = d.first_page().unwrap();
|
||||
let v = d.create(p, NodeKind::Vector { network }, "V", 0.0, 0.0, 100.0, 50.0).unwrap();
|
||||
let n = d.get_mut(v).unwrap();
|
||||
n.strokes = vec![Paint::solid(Color::from_hex("000000").unwrap())];
|
||||
n.stroke.weight = 2.0;
|
||||
n.stroke.join = StrokeJoin::Miter;
|
||||
export_svg(&d, &[v], &SvgOptions::default()).unwrap()
|
||||
};
|
||||
assert!(svg_with(StrokeJoin::Miter).contains("stroke-width=\"2\""), "equal joins stay a stroke");
|
||||
assert!(!svg_with(StrokeJoin::Round).contains("stroke-width="), "a differing join is drawn as an outline");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user