mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Add a Connect Cells option to the 'Grid' node (#4374)
Add a Connect Cells option to the Grid node
This commit is contained in:
committed by
Dennis Kobert
parent
804831a1ca
commit
c4da22f4a4
@@ -306,84 +306,76 @@ fn grid<T: GridSpacing>(
|
||||
#[default(10)] columns: u32,
|
||||
#[default(10)] rows: u32,
|
||||
#[default(30., 30.)] angles: DVec2,
|
||||
#[default(true)] connect_cells: bool,
|
||||
) -> Vector {
|
||||
let (x_spacing, y_spacing) = spacing.as_dvec2().into();
|
||||
let (angle_a, angle_b) = angles.into();
|
||||
|
||||
// Isometric grid spacing based on the two skew angles. Unused for rectangular grids.
|
||||
let tan_a = angle_a.to_radians().tan();
|
||||
let tan_b = angle_b.to_radians().tan();
|
||||
let isometric_spacing = DVec2::new(y_spacing / (tan_a + tan_b), y_spacing);
|
||||
|
||||
// The position of the grid point at column `x`, row `y`.
|
||||
let position = |x: u32, y: u32| -> DVec2 {
|
||||
match grid_type {
|
||||
GridType::Rectangular => DVec2::new(x_spacing * x as f64, y_spacing * y as f64),
|
||||
GridType::Isometric => {
|
||||
// Odd columns are offset vertically so the cells skew into the isometric shape.
|
||||
let a_angles_eaten = x.div_ceil(2) as f64;
|
||||
let b_angles_eaten = (x / 2) as f64;
|
||||
let offset_y_fraction = b_angles_eaten * tan_b - a_angles_eaten * tan_a;
|
||||
DVec2::new(isometric_spacing.x * x as f64, isometric_spacing.y * y as f64 + offset_y_fraction * isometric_spacing.x)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// When the cells aren't connected, each one is its own closed quadrilateral subpath.
|
||||
// The vertices are ordered counter-clockwise to match the framework's fill winding.
|
||||
if !connect_cells {
|
||||
let mut cells = Vec::new();
|
||||
for y in 0..rows.saturating_sub(1) {
|
||||
for x in 0..columns.saturating_sub(1) {
|
||||
cells.push(vec![position(x, y), position(x + 1, y), position(x + 1, y + 1), position(x, y + 1)]);
|
||||
}
|
||||
}
|
||||
let mut vector = Vector::default();
|
||||
crate::vector_nodes::replace_with_polygons(&mut vector, cells, connect_cells);
|
||||
return vector;
|
||||
}
|
||||
|
||||
let mut vector = Vector::default();
|
||||
let mut segment_id = SegmentId::ZERO;
|
||||
let mut point_id = PointId::ZERO;
|
||||
|
||||
match grid_type {
|
||||
GridType::Rectangular => {
|
||||
// Create rectangular grid points and connect them with line segments
|
||||
for y in 0..rows {
|
||||
for x in 0..columns {
|
||||
// Add current point to the grid
|
||||
let current_index = vector.point_domain.ids().len();
|
||||
vector.point_domain.push(point_id.next_id(), DVec2::new(x_spacing * x as f64, y_spacing * y as f64));
|
||||
for y in 0..rows {
|
||||
for x in 0..columns {
|
||||
// Add the current point to the grid.
|
||||
let current_index = vector.point_domain.ids().len();
|
||||
vector.point_domain.push(point_id.next_id(), position(x, y));
|
||||
|
||||
// Helper function to connect points with line segments
|
||||
let mut push_segment = |to_index: Option<usize>| {
|
||||
if let Some(other_index) = to_index {
|
||||
vector
|
||||
.segment_domain
|
||||
.push(segment_id.next_id(), other_index, current_index, subpath::BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the point to the left (horizontal connection)
|
||||
push_segment((x > 0).then(|| current_index - 1));
|
||||
|
||||
// Connect to the point above (vertical connection)
|
||||
push_segment(current_index.checked_sub(columns as usize));
|
||||
// Helper function to connect points with line segments.
|
||||
let mut push_segment = |to_index: Option<usize>| {
|
||||
if let Some(other_index) = to_index {
|
||||
vector
|
||||
.segment_domain
|
||||
.push(segment_id.next_id(), other_index, current_index, subpath::BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
}
|
||||
}
|
||||
GridType::Isometric => {
|
||||
// Calculate isometric grid spacing based on angles
|
||||
let tan_a = angle_a.to_radians().tan();
|
||||
let tan_b = angle_b.to_radians().tan();
|
||||
let spacing = DVec2::new(y_spacing / (tan_a + tan_b), y_spacing);
|
||||
};
|
||||
|
||||
// Create isometric grid points and connect them with line segments
|
||||
for y in 0..rows {
|
||||
for x in 0..columns {
|
||||
// Add current point to the grid with offset for odd columns
|
||||
let current_index = vector.point_domain.ids().len();
|
||||
// Connect to the point to the left (horizontal connection).
|
||||
push_segment((x > 0).then(|| current_index - 1));
|
||||
|
||||
let a_angles_eaten = x.div_ceil(2) as f64;
|
||||
let b_angles_eaten = (x / 2) as f64;
|
||||
// Connect to the point directly above (vertical connection).
|
||||
push_segment(current_index.checked_sub(columns as usize));
|
||||
|
||||
let offset_y_fraction = b_angles_eaten * tan_b - a_angles_eaten * tan_a;
|
||||
// Isometric grids additionally connect odd columns diagonally, splitting each cell into triangles.
|
||||
if grid_type == GridType::Isometric && x % 2 == 1 {
|
||||
// Connect to the point diagonally up-right (if not at the right edge).
|
||||
push_segment(current_index.checked_sub(columns as usize - 1).filter(|_| x + 1 < columns));
|
||||
|
||||
let position = DVec2::new(spacing.x * x as f64, spacing.y * y as f64 + offset_y_fraction * spacing.x);
|
||||
vector.point_domain.push(point_id.next_id(), position);
|
||||
|
||||
// Helper function to connect points with line segments
|
||||
let mut push_segment = |to_index: Option<usize>| {
|
||||
if let Some(other_index) = to_index {
|
||||
vector
|
||||
.segment_domain
|
||||
.push(segment_id.next_id(), other_index, current_index, subpath::BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the point to the left
|
||||
push_segment((x > 0).then(|| current_index - 1));
|
||||
|
||||
// Connect to the point directly above
|
||||
push_segment(current_index.checked_sub(columns as usize));
|
||||
|
||||
// Additional diagonal connections for odd columns (creates hexagonal pattern)
|
||||
if x % 2 == 1 {
|
||||
// Connect to the point diagonally up-right (if not at right edge)
|
||||
push_segment(current_index.checked_sub(columns as usize - 1).filter(|_| x + 1 < columns));
|
||||
|
||||
// Connect to the point diagonally up-left
|
||||
push_segment(current_index.checked_sub(columns as usize + 1));
|
||||
}
|
||||
}
|
||||
// Connect to the point diagonally up-left.
|
||||
push_segment(current_index.checked_sub(columns as usize + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,11 +389,11 @@ mod tests {
|
||||
#[test]
|
||||
fn isometric_grid_test() {
|
||||
// Doesn't crash with weird angles
|
||||
grid(&(), (), GridType::Isometric, 0., 5, 5, (0., 0.).into());
|
||||
grid(&(), (), GridType::Isometric, 90., 5, 5, (90., 90.).into());
|
||||
grid(&(), (), GridType::Isometric, 0., 5, 5, (0., 0.).into(), true);
|
||||
grid(&(), (), GridType::Isometric, 90., 5, 5, (90., 90.).into(), true);
|
||||
|
||||
// Works properly
|
||||
let grid = grid(&(), (), GridType::Isometric, 10., 5, 5, (30., 30.).into());
|
||||
let grid = grid(&(), (), GridType::Isometric, 10., 5, 5, (30., 30.).into(), true);
|
||||
assert_eq!(grid.point_domain.ids().len(), 5 * 5);
|
||||
assert_eq!(grid.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
|
||||
for (_, bezier, _, _) in grid.segment_bezier_iter() {
|
||||
@@ -416,7 +408,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn skew_isometric_grid_test() {
|
||||
let grid = grid(&(), (), GridType::Isometric, 10., 5, 5, (40., 30.).into());
|
||||
let grid = grid(&(), (), GridType::Isometric, 10., 5, 5, (40., 30.).into(), true);
|
||||
assert_eq!(grid.point_domain.ids().len(), 5 * 5);
|
||||
assert_eq!(grid.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
|
||||
for (_, bezier, _, _) in grid.segment_bezier_iter() {
|
||||
@@ -427,6 +419,24 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grid_disconnected_cells_test() {
|
||||
// A 3x3 rectangular grid has a 2x2 arrangement of cells, each its own closed quad subpath with a fillable region.
|
||||
let grid = grid(&(), (), GridType::Rectangular, 10., 3_u32, 3_u32, (30., 30.).into(), false);
|
||||
let vector = grid;
|
||||
assert_eq!(vector.region_domain.ids().len(), 4);
|
||||
assert_eq!(vector.point_domain.ids().len(), 4 * 4);
|
||||
assert_eq!(vector.segment_domain.ids().len(), 4 * 4);
|
||||
|
||||
// Each cell winds counter-clockwise (positive signed area), matching the shape generators.
|
||||
for (group, closed) in vector.stroke_manipulator_groups() {
|
||||
assert!(closed);
|
||||
let anchors: Vec<DVec2> = group.iter().map(|g| g.anchor).collect();
|
||||
let signed_area: f64 = (0..anchors.len()).map(|i| anchors[i].perp_dot(anchors[(i + 1) % anchors.len()])).sum::<f64>() / 2.;
|
||||
assert!(signed_area > 0., "grid cell should wind counter-clockwise");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qr_code_test() {
|
||||
let qr = qr_code(&(), (), "https://graphite.art".to_string(), false, 1., QRCodeErrorCorrectionLevel::Low, true);
|
||||
|
||||
Reference in New Issue
Block a user