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:
@@ -1901,7 +1901,9 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
let columns = number_widget(ParameterWidgetsInfo::new(node_id, ColumnsInput::INDEX, true, context), NumberInput::default().min(1.));
|
||||
let rows = number_widget(ParameterWidgetsInfo::new(node_id, RowsInput::INDEX, true, context), NumberInput::default().min(1.));
|
||||
|
||||
widgets.extend([LayoutGroup::row(columns), LayoutGroup::row(rows)]);
|
||||
let connect_cells = bool_widget(ParameterWidgetsInfo::new(node_id, ConnectCellsInput::INDEX, true, context), CheckboxInput::default());
|
||||
|
||||
widgets.extend([LayoutGroup::row(columns), LayoutGroup::row(rows), LayoutGroup::row(connect_cells)]);
|
||||
|
||||
widgets
|
||||
}
|
||||
|
||||
@@ -2268,33 +2268,36 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
}
|
||||
}
|
||||
|
||||
// Make the "Grid" node, if its input of index 3 is a DVec2 for "angles" instead of a u32 for the "columns" input that now succeeds "angles", move the angle to index 5 (after "columns" and "rows")
|
||||
// Upgrade the "Grid" node from its six-input layout to the current seven-input layout (which adds a trailing
|
||||
// "connect_cells" toggle, defaulting to the connected mesh that older grids produced). Legacy documents also placed
|
||||
// "angles" at index 3 instead of index 5 (after "columns" and "rows"), so we reorder those. Either way, "connect_cells"
|
||||
// is left at its default from the new node template.
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::grid::IDENTIFIER) && inputs_count == 6 {
|
||||
let node_definition = resolve_document_node_type(&reference)?;
|
||||
let mut new_node_template = node_definition.default_node_template();
|
||||
|
||||
let mut current_node_template = document.network_interface.create_node_template(node_id, network_path)?;
|
||||
let mut new_node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut new_node_template)?;
|
||||
let index_3_value = old_inputs.get(3).cloned();
|
||||
|
||||
let mut upgraded = false;
|
||||
// The two six-input layouts differ only in where the DVec2 "angles" and the u32 "columns"/"rows" sit:
|
||||
// Legacy: [primary, grid_type, spacing, angles (DVec2), columns (u32), rows (u32)]
|
||||
// Modern: [primary, grid_type, spacing, columns (u32), rows (u32), angles (DVec2)]
|
||||
// So a DVec2 "angles" at index 3, or a u32 "rows" at index 5, marks the legacy order. Checking both slots classifies
|
||||
// correctly even when one of them is a wired or imported connection rather than a literal value.
|
||||
let index_3_is_angles = matches!(old_inputs.get(3), Some(NodeInput::Value { tagged_value, .. }) if matches!(**tagged_value, TaggedValue::DVec2(_)));
|
||||
let index_5_is_rows = matches!(old_inputs.get(5), Some(NodeInput::Value { tagged_value, .. }) if matches!(**tagged_value, TaggedValue::U32(_)));
|
||||
let legacy_angles_layout = index_3_is_angles || index_5_is_rows;
|
||||
|
||||
if let Some(NodeInput::Value { tagged_value, exposed: _ }) = index_3_value
|
||||
&& matches!(*tagged_value, TaggedValue::DVec2(_))
|
||||
{
|
||||
// Move index 3 to the end
|
||||
if legacy_angles_layout {
|
||||
// Old order: [primary, grid_type, spacing, angles, columns, rows]. Move "angles" from index 3 to index 5.
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[4].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path);
|
||||
|
||||
upgraded = true;
|
||||
}
|
||||
|
||||
if !upgraded {
|
||||
let _ = document.network_interface.replace_inputs(node_id, network_path, &mut current_node_template);
|
||||
} else {
|
||||
// Modern six-input order. Carry each input over to the same index.
|
||||
for (index, input) in old_inputs.iter().take(6).enumerate() {
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, index), input.clone(), network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -321,86 +321,78 @@ fn grid<T: GridSpacing>(
|
||||
#[default(10)] columns: Item<u32>,
|
||||
#[default(10)] rows: Item<u32>,
|
||||
#[default(30., 30.)] angles: Item<DVec2>,
|
||||
#[default(true)] connect_cells: Item<bool>,
|
||||
) -> Item<Vector> {
|
||||
let (grid_type, columns, rows, angles) = (grid_type.into_element(), *columns.element(), *rows.element(), *angles.element());
|
||||
let (grid_type, columns, rows, angles, connect_cells) = (grid_type.into_element(), *columns.element(), *rows.element(), *angles.element(), *connect_cells.element());
|
||||
|
||||
let (x_spacing, y_spacing) = spacing.element().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 Item::new_from_element(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,11 +411,11 @@ mod tests {
|
||||
#[test]
|
||||
fn isometric_grid_test() {
|
||||
// Doesn't crash with weird angles
|
||||
grid((), (), item(GridType::Isometric), item(0.), item(5_u32), item(5_u32), item((0., 0.).into()));
|
||||
grid((), (), item(GridType::Isometric), item(90.), item(5_u32), item(5_u32), item((90., 90.).into()));
|
||||
grid((), (), item(GridType::Isometric), item(0.), item(5_u32), item(5_u32), item((0., 0.).into()), item(true));
|
||||
grid((), (), item(GridType::Isometric), item(90.), item(5_u32), item(5_u32), item((90., 90.).into()), item(true));
|
||||
|
||||
// Works properly
|
||||
let grid = grid((), (), item(GridType::Isometric), item(10.), item(5_u32), item(5_u32), item((30., 30.).into()));
|
||||
let grid = grid((), (), item(GridType::Isometric), item(10.), item(5_u32), item(5_u32), item((30., 30.).into()), item(true));
|
||||
assert_eq!(grid.element().point_domain.ids().len(), 5 * 5);
|
||||
assert_eq!(grid.element().segment_bezier_iter().count(), 4 * 5 + 4 * 9);
|
||||
for (_, bezier, _, _) in grid.element().segment_bezier_iter() {
|
||||
@@ -438,7 +430,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn skew_isometric_grid_test() {
|
||||
let grid = grid((), (), item(GridType::Isometric), item(10.), item(5_u32), item(5_u32), item((40., 30.).into()));
|
||||
let grid = grid((), (), item(GridType::Isometric), item(10.), item(5_u32), item(5_u32), item((40., 30.).into()), item(true));
|
||||
assert_eq!(grid.element().point_domain.ids().len(), 5 * 5);
|
||||
assert_eq!(grid.element().segment_bezier_iter().count(), 4 * 5 + 4 * 9);
|
||||
for (_, bezier, _, _) in grid.element().segment_bezier_iter() {
|
||||
@@ -449,6 +441,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((), (), item(GridType::Rectangular), item(10.), item(3_u32), item(3_u32), item((30., 30.).into()), item(false));
|
||||
let vector = grid.element();
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user