Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
39 changes: 21 additions & 18 deletions editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

if let Some(NodeInput::Value { tagged_value, exposed: _ }) = index_3_value
&& matches!(*tagged_value, TaggedValue::DVec2(_))
{
// Move index 3 to the end
// 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 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);
}
}
}

Expand Down
158 changes: 84 additions & 74 deletions node-graph/nodes/vector/src/generator_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

// 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));
}
}
}
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();

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;

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));
}
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 directly above (vertical connection).
push_segment(current_index.checked_sub(columns as usize));

// 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));

// Connect to the point diagonally up-left.
push_segment(current_index.checked_sub(columns as usize + 1));
}
}
}
Expand All @@ -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() {
Expand All @@ -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() {
Expand All @@ -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(
Expand Down
Loading