Add rotation to Repeat node

This commit is contained in:
Keavon Chambers 2024-04-20 01:35:40 -07:00
parent 5f4960db9b
commit 72ba4ddfe4
7 changed files with 506 additions and 539 deletions

843
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2628,11 +2628,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNodeDefinition {
name: "Repeat",
category: "Vector",
implementation: DocumentNodeImplementation::proto("graphene_core::vector::RepeatNode<_, _>"),
implementation: DocumentNodeImplementation::proto("graphene_core::vector::RepeatNode<_, _, _>"),
inputs: vec![
DocumentInputType::value("Instance", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
DocumentInputType::value("Direction", TaggedValue::DVec2((100., 0.).into()), false),
DocumentInputType::value("Count", TaggedValue::U32(10), false),
DocumentInputType::value("Direction", TaggedValue::DVec2((100., 100.).into()), false),
DocumentInputType::value("Angle", TaggedValue::F64(0.), false),
DocumentInputType::value("Instances", TaggedValue::U32(5), false),
],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
properties: node_properties::repeat_properties,
@ -2646,7 +2647,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentInputType::value("Instance", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
DocumentInputType::value("Angle Offset", TaggedValue::F64(0.), false),
DocumentInputType::value("Radius", TaggedValue::F64(5.), false),
DocumentInputType::value("Count", TaggedValue::U32(10), false),
DocumentInputType::value("Instances", TaggedValue::U32(5), false),
],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
properties: node_properties::circular_repeat_properties,

View file

@ -2289,17 +2289,22 @@ pub fn stroke_properties(document_node: &DocumentNode, node_id: NodeId, _context
pub fn repeat_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let direction = vec2_widget(document_node, node_id, 1, "Direction", "X", "Y", " px", None, add_blank_assist);
let count = number_widget(document_node, node_id, 2, "Count", NumberInput::default().min(1.), true);
let angle = number_widget(document_node, node_id, 2, "Angle", NumberInput::default().unit("°"), true);
let instances = number_widget(document_node, node_id, 3, "Instances", NumberInput::default().min(1.).is_integer(true), true);
vec![direction, LayoutGroup::Row { widgets: count }]
vec![direction, LayoutGroup::Row { widgets: angle }, LayoutGroup::Row { widgets: instances }]
}
pub fn circular_repeat_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let angle_offset = number_widget(document_node, node_id, 1, "Angle Offset", NumberInput::default().unit("°"), true);
let radius = number_widget(document_node, node_id, 2, "Radius", NumberInput::default(), true); // TODO: What units?
let count = number_widget(document_node, node_id, 3, "Count", NumberInput::default().min(1.), true);
let instances = number_widget(document_node, node_id, 3, "Instances", NumberInput::default().min(1.).is_integer(true), true);
vec![LayoutGroup::Row { widgets: angle_offset }, LayoutGroup::Row { widgets: radius }, LayoutGroup::Row { widgets: count }]
vec![
LayoutGroup::Row { widgets: angle_offset },
LayoutGroup::Row { widgets: radius },
LayoutGroup::Row { widgets: instances },
]
}
pub fn copy_to_points_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {

View file

@ -116,7 +116,7 @@ impl Fsm for FillToolFsmState {
let hint_data = match self {
FillToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::Lmb, "Fill with Primary"),
HintInfo::keys_and_mouse([Key::Shift], MouseMotion::Lmb, "Fill with Secondary"),
HintInfo::keys([Key::Shift], "Fill with Secondary").prepend_plus(),
])]),
FillToolFsmState::Filling => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
};

View file

@ -58,8 +58,8 @@
$: gridSpacing = calculateGridSpacing(transform.scale);
$: dotRadius = 1 + Math.floor(transform.scale - 0.5 + 0.001) / 2;
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
$: nodeListX = ((nodeListLocation?.x || 0) * GRID_SIZE + transform.x) * transform.scale;
$: nodeListY = ((nodeListLocation?.y || 0) * GRID_SIZE + transform.y) * transform.scale;
$: nodeListX = ((nodeListLocation?.x || 0) + transform.x) * transform.scale;
$: nodeListY = ((nodeListLocation?.y || 0) + transform.y) * transform.scale;
let appearAboveMouse = false;
let appearRightOfMouse = false;
@ -69,8 +69,8 @@
if (!bounds) return;
const { width, height } = bounds;
appearRightOfMouse = nodeListX > width - ADD_NODE_MENU_WIDTH / 2;
appearAboveMouse = nodeListY > height - ADD_NODE_MENU_HEIGHT / 2;
appearRightOfMouse = nodeListX > width - ADD_NODE_MENU_WIDTH;
appearAboveMouse = nodeListY > height - ADD_NODE_MENU_HEIGHT;
})();
$: linkPathInProgress = createLinkPathInProgress(linkInProgressFromConnector, linkInProgressToConnector);
@ -320,8 +320,8 @@
function loadNodeList(e: PointerEvent, graphBounds: DOMRect) {
nodeListLocation = {
x: Math.round(((e.clientX - graphBounds.x) / transform.scale - transform.x) / GRID_SIZE),
y: Math.round(((e.clientY - graphBounds.y) / transform.scale - transform.y) / GRID_SIZE),
x: (e.clientX - graphBounds.x) / transform.scale - transform.x,
y: (e.clientY - graphBounds.y) / transform.scale - transform.y,
};
// Find actual relevant child and focus it (setTimeout is required to actually focus the input element)
@ -639,10 +639,7 @@
if (!nodeListLocation) return;
let nodeListLocation2: { x: number; y: number } = nodeListLocation;
linkInProgressToConnector = new DOMRect(
(nodeListLocation2.x * GRID_SIZE + transform.x) * transform.scale + graphBounds.x,
(nodeListLocation2.y * GRID_SIZE + transform.y) * transform.scale + graphBounds.y,
);
linkInProgressToConnector = new DOMRect((nodeListLocation2.x + transform.x) * transform.scale + graphBounds.x, (nodeListLocation2.y + transform.y) * transform.scale + graphBounds.y);
return;
} else if (draggingNodes) {
@ -671,7 +668,9 @@
if (!nodeListLocation) return;
const inputNodeConnectionIndex = 0;
const inputConnectedNodeID = editor.instance.createNode(nodeType, nodeListLocation.x, nodeListLocation.y - 1);
const x = Math.round(nodeListLocation.x / GRID_SIZE);
const y = Math.round(nodeListLocation.y / GRID_SIZE) - 1;
const inputConnectedNodeID = editor.instance.createNode(nodeType, x, y);
nodeListLocation = undefined;
if (!linkInProgressFromConnector) return;
@ -751,18 +750,18 @@
class="node-list"
data-node-list
styles={{
transform: `translate(${appearRightOfMouse ? -100 : 0}%, ${appearAboveMouse ? -100 : 0}%)`,
left: `${nodeListX}px`,
top: `${nodeListY}px`,
transform: `translate(${appearRightOfMouse ? -100 : 0}%, ${appearAboveMouse ? -100 : 0}%)`,
width: `${ADD_NODE_MENU_WIDTH}px`,
height: `${ADD_NODE_MENU_HEIGHT}px`,
}}
>
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
<div class="list-nodes" style={`height: ${ADD_NODE_MENU_HEIGHT}px;`} on:wheel|passive|stopPropagation>
<div class="list-results" on:wheel|passive|stopPropagation>
{#each nodeCategories as nodeCategory}
<details style="display: flex; flex-direction: column;" open={nodeCategory[1].open}>
<details open={nodeCategory[1].open}>
<summary>
<IconLabel icon="DropdownArrow" />
<TextLabel>{nodeCategory[0]}</TextLabel>
</summary>
{#each nodeCategory[1].nodes as nodeType}
@ -770,7 +769,7 @@
{/each}
</details>
{:else}
<div style="margin-right: 4px;"><TextLabel>No search results</TextLabel></div>
<TextLabel>No search results</TextLabel>
{/each}
</div>
</LayoutCol>
@ -1081,47 +1080,59 @@
.node-list {
width: max-content;
position: absolute;
box-sizing: border-box;
padding: 5px;
z-index: 3;
background-color: var(--color-3-darkgray);
.text-button {
width: 100%;
.text-input {
flex: 0 0 auto;
margin-bottom: 4px;
}
.list-nodes {
overflow-y: scroll;
}
.list-results {
overflow-y: auto;
flex: 1 1 auto;
details {
margin-right: 4px;
cursor: pointer;
}
details {
cursor: pointer;
display: flex;
flex-direction: column;
summary {
list-style-type: none;
display: flex;
align-items: center;
gap: 2px;
&[open] summary .text-label::before {
transform: rotate(90deg);
}
span {
white-space: break-spaces;
summary {
display: flex;
align-items: center;
gap: 2px;
.text-label {
padding-left: 16px;
position: relative;
width: 100%;
&::before {
content: "";
position: absolute;
margin: auto;
top: 0;
bottom: 0;
left: 0;
width: 8px;
height: 8px;
background: var(--icon-expand-collapse-arrow);
}
}
}
.text-button {
width: 100%;
margin: 4px 0;
}
}
}
details summary svg {
transform: rotate(-90deg);
}
details[open] summary svg {
transform: rotate(0deg);
}
.text-button + .text-button {
display: block;
margin-left: 0;
margin-top: 4px;
}
}
.wires {

View file

@ -79,17 +79,34 @@ fn set_vector_data_stroke(
}
#[derive(Debug, Clone, Copy)]
pub struct RepeatNode<Direction, Count> {
pub struct RepeatNode<Direction, Angle, Instances> {
direction: Direction,
count: Count,
angle: Angle,
instances: Instances,
}
#[node_macro::node_fn(RepeatNode)]
fn repeat_vector_data(vector_data: VectorData, direction: DVec2, count: u32) -> VectorData {
fn repeat_vector_data(vector_data: VectorData, direction: DVec2, angle: f64, instances: u32) -> VectorData {
let angle = angle.to_radians();
let instances = instances.max(1);
let total = (instances - 1) as f64;
if instances == 1 {
return vector_data;
}
// Repeat the vector data
let mut result = VectorData::empty();
for i in 0..count {
let transform = DAffine2::from_translation(direction * i as f64);
let Some(bounding_box) = vector_data.bounding_box() else { return vector_data };
let center = (bounding_box[0] + bounding_box[1]) / 2.;
for i in 0..instances {
let translation = i as f64 * direction / total;
let angle = i as f64 * angle / total;
let transform = DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(translation) * DAffine2::from_translation(-center);
result.concat(&vector_data, transform);
}
@ -97,14 +114,20 @@ fn repeat_vector_data(vector_data: VectorData, direction: DVec2, count: u32) ->
}
#[derive(Debug, Clone, Copy)]
pub struct CircularRepeatNode<AngleOffset, Radius, Count> {
pub struct CircularRepeatNode<AngleOffset, Radius, Instances> {
angle_offset: AngleOffset,
radius: Radius,
count: Count,
instances: Instances,
}
#[node_macro::node_fn(CircularRepeatNode)]
fn circular_repeat_vector_data(vector_data: VectorData, angle_offset: f64, radius: f64, count: u32) -> VectorData {
fn circular_repeat_vector_data(vector_data: VectorData, angle_offset: f64, radius: f64, instances: u32) -> VectorData {
let instances = instances.max(1);
if instances == 1 {
return vector_data;
}
let mut result = VectorData::empty();
let Some(bounding_box) = vector_data.bounding_box() else { return vector_data };
@ -112,8 +135,8 @@ fn circular_repeat_vector_data(vector_data: VectorData, angle_offset: f64, radiu
let base_transform = DVec2::new(0., radius) - center;
for i in 0..count {
let angle = (2. * std::f64::consts::PI / count as f64) * i as f64 + angle_offset.to_radians();
for i in 0..instances {
let angle = (std::f64::consts::TAU / instances as f64) * i as f64 + angle_offset.to_radians();
let rotation = DAffine2::from_angle(angle);
let transform = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform);
result.concat(&vector_data, transform);
@ -533,27 +556,31 @@ mod test {
#[test]
fn repeat() {
let direction = DVec2::X * 1.5;
let instances = 3;
let repeated = RepeatNode {
direction: ClonedNode::new(direction),
count: ClonedNode::new(3),
angle: ClonedNode::new(0.),
instances: ClonedNode::new(instances),
}
.eval(VectorData::from_subpath(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)));
assert_eq!(repeated.region_bezier_paths().count(), 3);
for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() {
assert_eq!(subpath.manipulator_groups()[0].anchor, direction * index as f64);
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
}
}
#[test]
fn repeat_transform_position() {
let direction = DVec2::new(12., 10.);
let instances = 8;
let repeated = RepeatNode {
direction: ClonedNode::new(direction),
count: ClonedNode::new(8),
angle: ClonedNode::new(0.),
instances: ClonedNode::new(instances),
}
.eval(VectorData::from_subpath(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)));
assert_eq!(repeated.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() {
assert_eq!(subpath.manipulator_groups()[0].anchor, direction * index as f64);
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
}
}
#[test]
@ -561,29 +588,29 @@ mod test {
let repeated = CircularRepeatNode {
angle_offset: ClonedNode::new(45.),
radius: ClonedNode::new(4.),
count: ClonedNode::new(8),
instances: ClonedNode::new(8),
}
.eval(VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)));
assert_eq!(repeated.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() {
let expected_angle = (index as f64 + 1.) * 45.;
let centre = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.;
let actual_angle = DVec2::Y.angle_between(centre).to_degrees();
let center = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.;
let actual_angle = DVec2::Y.angle_between(center).to_degrees();
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5);
}
}
#[test]
fn bounding_box() {
let bouding_box = BoundingBoxNode.eval(VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)));
assert_eq!(bouding_box.region_bezier_paths().count(), 1);
let subpath = bouding_box.region_bezier_paths().next().unwrap().1;
let bounding_box = BoundingBoxNode.eval(VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)));
assert_eq!(bounding_box.region_bezier_paths().count(), 1);
let subpath = bounding_box.region_bezier_paths().next().unwrap().1;
assert_eq!(&subpath.anchors()[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]);
}
#[tokio::test]
async fn copy_to_points() {
let points = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.));
let expected_points = points.point_domain.positions().to_vec();
let bouding_box = CopyToPoints {
let bounding_box = CopyToPoints {
points: CullNode::new(FutureWrapperNode(ClonedNode(points))),
instance: CullNode::new(FutureWrapperNode(ClonedNode(VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))))),
random_scale_min: FutureWrapperNode(ClonedNode(1.)),
@ -593,8 +620,8 @@ mod test {
}
.eval(Footprint::default())
.await;
assert_eq!(bouding_box.region_bezier_paths().count(), expected_points.len());
for (index, (_, subpath)) in bouding_box.region_bezier_paths().enumerate() {
assert_eq!(bounding_box.region_bezier_paths().count(), expected_points.len());
for (index, (_, subpath)) in bounding_box.region_bezier_paths().enumerate() {
let offset = expected_points[index];
assert_eq!(
&subpath.anchors()[..4],

View file

@ -701,7 +701,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
register_node!(graphene_core::transform::SetTransformNode<_>, input: ImageFrame<Color>, params: [DAffine2]),
register_node!(graphene_core::vector::SetFillNode<_, _, _, _, _, _, _>, input: VectorData, params: [graphene_core::vector::style::FillType, Option<graphene_core::Color>, graphene_core::vector::style::GradientType, DVec2, DVec2, DAffine2, Vec<(f64, graphene_core::Color)>]),
register_node!(graphene_core::vector::SetStrokeNode<_, _, _, _, _, _, _>, input: VectorData, params: [Option<graphene_core::Color>, f64, Vec<f64>, f64, graphene_core::vector::style::LineCap, graphene_core::vector::style::LineJoin, f64]),
register_node!(graphene_core::vector::RepeatNode<_, _>, input: VectorData, params: [DVec2, u32]),
register_node!(graphene_core::vector::RepeatNode<_, _, _>, input: VectorData, params: [DVec2, f64, u32]),
register_node!(graphene_core::vector::BoundingBoxNode, input: VectorData, params: []),
register_node!(graphene_core::vector::SolidifyStrokeNode, input: VectorData, params: []),
register_node!(graphene_core::vector::CircularRepeatNode<_, _, _>, input: VectorData, params: [f64, f64, u32]),