Skip to content

Commit

Permalink
Support dashes in rule mark
Browse files Browse the repository at this point in the history
  • Loading branch information
jonmmease committed Jan 13, 2024
1 parent 8a287e8 commit 2d1f711
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"width": 410,
"height": 410,
"origin_x": 5,
"origin_y": 5
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions sg2d-vega-test-data/vega-scenegraphs/rule/dashed_rules.sg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"marktype": "group",
"name": "root",
"role": "frame",
"interactive": true,
"clip": false,
"items": [
{
"items": [
{
"marktype": "rule",
"name": "marks",
"role": "mark",
"interactive": true,
"clip": false,
"items": [
{
"x": 340,
"y": 15,
"opacity": 0.7,
"stroke": "orange",
"strokeWidth": 8,
"strokeCap": "butt",
"strokeDash": "12 8,4",
"x2": 60,
"y2": 380
},
{
"x": 20,
"y": 15,
"opacity": 0.7,
"stroke": "blue",
"strokeWidth": 4,
"strokeCap": "square",
"strokeDash": "8,16",
"x2": 320,
"y2": 340
},
{
"x": 80,
"y": 15,
"opacity": 0.7,
"stroke": "green",
"strokeWidth": 6,
"strokeCap": "round",
"strokeDash": "12",
"x2": 220,
"y2": 380
}
],
"zindex": 0
}
],
"x": 0,
"y": 0,
"width": 400,
"height": 400,
"fill": "transparent",
"stroke": "transparent"
}
],
"zindex": 0
}
39 changes: 39 additions & 0 deletions sg2d-vega-test-data/vega-specs/rule/dashed_rules.vg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"description": "A scatterplot showing horsepower and miles per gallons for various cars.",
"background": "white",
"padding": 5,
"width": 400,
"height": 400,
"style": "cell",
"config": {"style": {"cell": {"stroke": "transparent"}}},
"data": [{
"name": "source_0",
"values": [
{"x": 340, "x2": 60, "y": 15, "y2": 380, "fill": "orange", "cap": "butt", "width": 8, "dash": "12 8,4"},
{"x": 20, "x2": 320, "y": 15, "y2": 340, "fill": "blue", "cap": "square", "width": 4, "dash": "8,16"},
{"x": 80, "x2": 220, "y": 15, "y2": 380, "fill": "green", "cap": "round", "width": 6, "dash": "12"}
]
}],
"marks": [
{
"name": "marks",
"type": "rule",
"style": ["rule"],
"from": {"data": "source_0"},
"encode": {
"update": {
"strokeWidth": {"field": "width"},
"stroke": {"field": "fill"},
"x": {"field": "x" },
"x2": {"field": "x2"},
"y": {"field": "y"},
"y2": {"field": "y2"},
"opacity": {"value": 0.7},
"strokeCap": {"field": "cap"},
"strokeDash": {"field": "dash"}
}
}
}
]
}
16 changes: 0 additions & 16 deletions sg2d-vega-test-data/vega-specs/rule/wide_transparent_caps.vg.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,5 @@
}
}
}
],
"scales": [
{
"name": "x",
"type": "linear",
"domain": [0, 100],
"range": [0, {"signal": "width"}],
"zero": true
},
{
"name": "y",
"type": "linear",
"domain": [0, 100],
"range": [{"signal": "height"}, 0],
"zero": true
}
]
}
3 changes: 3 additions & 0 deletions sg2d-vega/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub enum VegaSceneGraphError {
// ParseError doesn't implement std::Error, so #[from] doesn't seem to work
#[error("Error parsing SVG path")]
InvalidSvgPath(lyon_extra::parser::ParseError),

#[error("Invalid dash string: {0}")]
InvalidDashString(String),
}

impl From<lyon_extra::parser::ParseError> for VegaSceneGraphError {
Expand Down
24 changes: 24 additions & 0 deletions sg2d-vega/src/marks/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub struct VegaRuleItem {
pub stroke_width: Option<f32>,
pub stroke_cap: Option<StrokeCap>,
pub stroke_opacity: Option<f32>,
pub stroke_dash: Option<String>,
pub opacity: Option<f32>,
pub zindex: Option<i32>,
}
Expand All @@ -41,6 +42,7 @@ impl VegaMarkContainer<VegaRuleItem> {
let mut stroke = Vec::<[f32; 4]>::new();
let mut stroke_width = Vec::<f32>::new();
let mut stroke_cap = Vec::<StrokeCap>::new();
let mut stroke_dash = Vec::<Vec<f32>>::new();
let mut zindex = Vec::<i32>::new();

// For each item, append explicit values to corresponding vector
Expand All @@ -64,6 +66,10 @@ impl VegaMarkContainer<VegaRuleItem> {
stroke_cap.push(s);
}

if let Some(dash) = &item.stroke_dash {
stroke_dash.push(parse_dash_str(dash)?);
}

if let Some(v) = item.zindex {
zindex.push(v);
}
Expand Down Expand Up @@ -96,6 +102,11 @@ impl VegaMarkContainer<VegaRuleItem> {
if stroke_cap.len() == len {
mark.stroke_cap = EncodingValue::Array { values: stroke_cap };
}
if stroke_dash.len() == len {
mark.stroke_dash = Some(EncodingValue::Array {
values: stroke_dash,
});
}
if zindex.len() == len {
let mut indices: Vec<usize> = (0..len).collect();
indices.sort_by_key(|i| zindex[*i]);
Expand All @@ -105,3 +116,16 @@ impl VegaMarkContainer<VegaRuleItem> {
Ok(SceneMark::Rule(mark))
}
}

fn parse_dash_str(dash_str: &str) -> Result<Vec<f32>, VegaSceneGraphError> {
let clean_dash_str = dash_str.replace(',', " ");
let mut dashes: Vec<f32> = Vec::new();
for s in clean_dash_str.split_whitespace() {
let d = s
.parse::<f32>()
.map_err(|_| VegaSceneGraphError::InvalidDashString(dash_str.to_string()))?
.abs();
dashes.push(d);
}
Ok(dashes)
}
132 changes: 109 additions & 23 deletions sg2d-wgpu/src/marks/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,115 @@ const INSTANCE_ATTRIBUTES: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array!
];

impl RuleInstance {
pub fn iter_from_spec(mark: &RuleMark) -> impl Iterator<Item = RuleInstance> + '_ {
izip!(
mark.x0_iter(),
mark.y0_iter(),
mark.x1_iter(),
mark.y1_iter(),
mark.stroke_iter(),
mark.stroke_width_iter(),
mark.stroke_cap_iter(),
)
.map(|(x0, y0, x1, y1, stroke, stroke_width, cap)| RuleInstance {
x0: *x0,
y0: *y0,
x1: *x1,
y1: *y1,
stroke: *stroke,
stroke_width: *stroke_width,
stroke_cap: match cap {
StrokeCap::Butt => STROKE_CAP_BUTT,
StrokeCap::Square => STROKE_CAP_SQUARE,
StrokeCap::Round => STROKE_CAP_ROUND,
},
})
pub fn iter_from_spec(mark: &RuleMark) -> Box<dyn Iterator<Item = RuleInstance> + '_> {
if let Some(stroke_dash_iter) = mark.stroke_dash_iter() {
// Rule has a dash specification, so we create an individual RuleInstance for each dash
// in each Rule mark item.
Box::new(
izip!(
stroke_dash_iter,
mark.x0_iter(),
mark.y0_iter(),
mark.x1_iter(),
mark.y1_iter(),
mark.stroke_iter(),
mark.stroke_width_iter(),
mark.stroke_cap_iter(),
)
.flat_map(
|(stroke_dash, x0, y0, x1, y1, stroke, stroke_width, cap)| {
// Next index into stroke_dash array
let mut dash_idx = 0;

// Distance along line from (x0,y0) to (x1,y1) where the next dash will start
let mut start_dash_dist: f32 = 0.0;

// Length of the line from (x0,y0) to (x1,y1)
let rule_len = ((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt();

// Coponents of unit vector along (x0,y0) to (x1,y1)
let xhat = (x1 - x0) / rule_len;
let yhat = (y1 - y0) / rule_len;

// Vector of rule instances, one for each dash segment
let mut dash_rules: Vec<RuleInstance> = Vec::new();

// Whether the next dash length represents a drawn dash (draw == true)
// or a gap (draw == false)
let mut draw = true;

while start_dash_dist < rule_len {
let end_dash_dist =
if start_dash_dist + stroke_dash[dash_idx] >= rule_len {
// The final dash/gap should be truncated to the end of the rule
rule_len
} else {
// The dash/gap fits entirely in the rule
start_dash_dist + stroke_dash[dash_idx]
};

if draw {
let dash_x0 = x0 + xhat * start_dash_dist;
let dash_y0 = y0 + yhat * start_dash_dist;
let dash_x1 = x0 + xhat * end_dash_dist;
let dash_y1 = y0 + yhat * end_dash_dist;

dash_rules.push(RuleInstance {
x0: dash_x0,
y0: dash_y0,
x1: dash_x1,
y1: dash_y1,
stroke: *stroke,
stroke_width: *stroke_width,
stroke_cap: match cap {
StrokeCap::Butt => STROKE_CAP_BUTT,
StrokeCap::Square => STROKE_CAP_SQUARE,
StrokeCap::Round => STROKE_CAP_ROUND,
},
})
}

// update start dist for next dash/gap
start_dash_dist = end_dash_dist;

// increment index and cycle back to start of start of dash array
dash_idx = (dash_idx + 1) % stroke_dash.len();

// Alternate between drawn dash and gap
draw = !draw;
}

dash_rules
},
),
)
} else {
// Rule has no dash specification, so we create one RuleInstance per Rule mark item
Box::new(
izip!(
mark.x0_iter(),
mark.y0_iter(),
mark.x1_iter(),
mark.y1_iter(),
mark.stroke_iter(),
mark.stroke_width_iter(),
mark.stroke_cap_iter(),
)
.map(|(x0, y0, x1, y1, stroke, stroke_width, cap)| RuleInstance {
x0: *x0,
y0: *y0,
x1: *x1,
y1: *y1,
stroke: *stroke,
stroke_width: *stroke_width,
stroke_cap: match cap {
StrokeCap::Butt => STROKE_CAP_BUTT,
StrokeCap::Square => STROKE_CAP_SQUARE,
StrokeCap::Round => STROKE_CAP_ROUND,
},
}),
)
}
}
}

Expand Down
1 change: 1 addition & 0 deletions sg2d-wgpu/tests/test_image_baselines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ mod test_image_baselines {
case("symbol", "mixed_symbols", 0.001),
case("rule", "wide_rule_axes", 0.0001),
case("rule", "wide_transparent_caps", 0.0001),
case("rule", "dashed_rules", 0.0001),
case("text", "bar_axis_labels", 0.025)
)]
fn test_image_baseline(category: &str, spec_name: &str, tolerance: f64) {
Expand Down
9 changes: 9 additions & 0 deletions sg2d/src/marks/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub struct RuleMark {
pub name: String,
pub clip: bool,
pub len: u32,
pub stroke_dash: Option<EncodingValue<Vec<f32>>>,
pub x0: EncodingValue<f32>,
pub y0: EncodingValue<f32>,
pub x1: EncodingValue<f32>,
Expand Down Expand Up @@ -42,6 +43,13 @@ impl RuleMark {
self.stroke_cap
.as_iter(self.len as usize, self.indices.as_ref())
}
pub fn stroke_dash_iter(&self) -> Option<Box<dyn Iterator<Item = &Vec<f32>> + '_>> {
if let Some(stroke_dash) = &self.stroke_dash {
Some(stroke_dash.as_iter(self.len as usize, self.indices.as_ref()))
} else {
None
}
}
}

impl Default for RuleMark {
Expand All @@ -50,6 +58,7 @@ impl Default for RuleMark {
name: "rule_mark".to_string(),
clip: true,
len: 1,
stroke_dash: None,
x0: EncodingValue::Scalar { value: 0.0 },
y0: EncodingValue::Scalar { value: 0.0 },
x1: EncodingValue::Scalar { value: 0.0 },
Expand Down

0 comments on commit 2d1f711

Please sign in to comment.