diff --git a/editor/src/consts.rs b/editor/src/consts.rs
index b4b3d23feb..240e42bc8e 100644
--- a/editor/src/consts.rs
+++ b/editor/src/consts.rs
@@ -122,6 +122,7 @@ pub const ASYMPTOTIC_EFFECT: f64 = 0.5;
 pub const SCALE_EFFECT: f64 = 0.5;
 
 // COLORS
+pub const COLOR_OVERLAY_TRANSPARENT: &str = "transparent";
 pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
 pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848";
 pub const COLOR_OVERLAY_GREEN: &str = "#63ce63";
diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs
index 76d2fc7bc6..38ab57a846 100644
--- a/editor/src/messages/input_mapper/input_mappings.rs
+++ b/editor/src/messages/input_mapper/input_mappings.rs
@@ -251,6 +251,8 @@ pub fn input_mappings() -> Mapping {
 		entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowLeft], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
 		entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowRight], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
 		entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=ToolMessage::Path(PathToolMessage::ClosePath)),
+		entry!(KeyDown(KeyP); modifiers=[Alt], action_dispatch=PathToolMessage::ToggleProportionalEditing),
+		entry!(WheelScroll; action_dispatch=PathToolMessage::AdjustProportionalRadius),
 		//
 		// PenToolMessage
 		entry!(PointerMove; refresh_keys=[Control, Alt, Shift, KeyC], action_dispatch=PenToolMessage::PointerMove { snap_angle: Shift, break_handle: Alt, lock_angle: Control, colinear: KeyC, move_anchor_with_handles: Space }),
@@ -376,9 +378,9 @@ pub fn input_mappings() -> Mapping {
 		entry!(KeyDown(ArrowRight); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }),
 		//
 		// TransformLayerMessage
-		entry!(KeyDown(KeyG); action_dispatch=TransformLayerMessage::BeginGRS { transform_type: TransformType::Grab }),
-		entry!(KeyDown(KeyR); action_dispatch=TransformLayerMessage::BeginGRS { transform_type: TransformType::Rotate }),
-		entry!(KeyDown(KeyS); action_dispatch=TransformLayerMessage::BeginGRS { transform_type: TransformType::Scale }),
+		entry!(KeyDown(KeyG); action_dispatch=TransformLayerMessage::BeginGRS { transform_type: TransformType::Grab, proportional_editing_data: None }),
+		entry!(KeyDown(KeyR); action_dispatch=TransformLayerMessage::BeginGRS { transform_type: TransformType::Rotate, proportional_editing_data: None }),
+		entry!(KeyDown(KeyS); action_dispatch=TransformLayerMessage::BeginGRS { transform_type: TransformType::Scale, proportional_editing_data: None }),
 		entry!(KeyDown(Digit0); action_dispatch=TransformLayerMessage::TypeDigit { digit: 0 }),
 		entry!(KeyDown(Digit1); action_dispatch=TransformLayerMessage::TypeDigit { digit: 1 }),
 		entry!(KeyDown(Digit2); action_dispatch=TransformLayerMessage::TypeDigit { digit: 2 }),
diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs
index 33353dbfb0..8b2ba344a1 100644
--- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs
+++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs
@@ -67,6 +67,10 @@ pub struct DropdownInput {
 
 	#[serde(skip)]
 	pub tooltip_shortcut: Option<ActionKeys>,
+
+	// Styling
+	#[serde(rename = "minWidth")]
+	pub min_width: u32,
 	//
 	// Callbacks
 	// `on_update` exists on the `MenuListEntry`, not this parent `DropdownInput`
diff --git a/editor/src/messages/portfolio/document/utility_types/mod.rs b/editor/src/messages/portfolio/document/utility_types/mod.rs
index e9ad9ae117..34fed91e42 100644
--- a/editor/src/messages/portfolio/document/utility_types/mod.rs
+++ b/editor/src/messages/portfolio/document/utility_types/mod.rs
@@ -4,4 +4,5 @@ pub mod error;
 pub mod misc;
 pub mod network_interface;
 pub mod nodes;
+pub mod proportional_editing;
 pub mod transformation;
diff --git a/editor/src/messages/portfolio/document/utility_types/proportional_editing.rs b/editor/src/messages/portfolio/document/utility_types/proportional_editing.rs
new file mode 100644
index 0000000000..470d892d7f
--- /dev/null
+++ b/editor/src/messages/portfolio/document/utility_types/proportional_editing.rs
@@ -0,0 +1,273 @@
+use glam::DVec2;
+use graphene_std::vector::PointId;
+use std::collections::{HashMap, HashSet};
+
+use super::document_metadata::LayerNodeIdentifier;
+use crate::messages::prelude::*;
+use crate::messages::tool::{
+	common_functionality::shape_editor::ShapeState,
+	tool_messages::{
+		path_tool::{PathOptionsUpdate, PathToolData, PathToolOptions},
+		tool_prelude::{DropdownInput, LayoutGroup, MenuListEntry, NumberInput, Separator, SeparatorType, TextLabel},
+	},
+};
+
+#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)]
+pub enum ProportionalFalloffType {
+	#[default]
+	Smooth = 0,
+	Sphere = 1,
+	Root = 2,
+	InverseSquare = 3,
+	Sharp = 4,
+	Linear = 5,
+	Constant = 6,
+	Random = 7,
+}
+
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
+pub struct ProportionalEditingData {
+	pub center: DVec2,
+	pub affected_points: HashMap<LayerNodeIdentifier, Vec<(PointId, f64)>>,
+	pub falloff_type: ProportionalFalloffType,
+	pub radius: u32,
+}
+
+pub fn proportional_editing_options(options: &PathToolOptions) -> Vec<LayoutGroup> {
+	let mut widgets = Vec::new();
+
+	// Header row with title
+	widgets.push(LayoutGroup::Row {
+		widgets: vec![TextLabel::new("Proportional Editing").bold(true).widget_holder()],
+	});
+
+	let callback = |message| Message::Batched(Box::new([PathToolMessage::UpdateOptions(PathOptionsUpdate::ProportionalEditingEnabled(true)).into(), message]));
+
+	// Falloff type row
+	widgets.push(LayoutGroup::Row {
+		widgets: vec![
+			TextLabel::new("Falloff").table_align(true).min_width(80).widget_holder(),
+			Separator::new(SeparatorType::Unrelated).widget_holder(),
+			DropdownInput::new(vec![vec![
+				MenuListEntry::new("Smooth")
+					.label("Smooth")
+					.on_commit(move |_| callback(PathToolMessage::UpdateOptions(PathOptionsUpdate::ProportionalFalloffType(ProportionalFalloffType::Smooth)).into())),
+				MenuListEntry::new("Sphere")
+					.label("Sphere")
+					.on_commit(move |_| callback(PathToolMessage::UpdateOptions(PathOptionsUpdate::ProportionalFalloffType(ProportionalFalloffType::Sphere)).into())),
+				MenuListEntry::new("Root")
+					.label("Root")
+					.on_commit(move |_| callback(PathToolMessage::UpdateOptions(PathOptionsUpdate::ProportionalFalloffType(ProportionalFalloffType::Root)).into())),
+				MenuListEntry::new("Inverse Square")
+					.label("Inverse Square")
+					.on_commit(move |_| callback(PathToolMessage::UpdateOptions(PathOptionsUpdate::ProportionalFalloffType(ProportionalFalloffType::InverseSquare)).into())),
+				MenuListEntry::new("Sharp")
+					.label("Sharp")
+					.on_commit(move |_| callback(PathToolMessage::UpdateOptions(PathOptionsUpdate::ProportionalFalloffType(ProportionalFalloffType::Sharp)).into())),
+				MenuListEntry::new("Linear")
+					.label("Linear")
+					.on_commit(move |_| callback(PathToolMessage::UpdateOptions(PathOptionsUpdate::ProportionalFalloffType(ProportionalFalloffType::Linear)).into())),
+				MenuListEntry::new("Constant")
+					.label("Constant")
+					.on_commit(move |_| callback(PathToolMessage::UpdateOptions(PathOptionsUpdate::ProportionalFalloffType(ProportionalFalloffType::Constant)).into())),
+				MenuListEntry::new("Random")
+					.label("Random")
+					.on_commit(move |_| callback(PathToolMessage::UpdateOptions(PathOptionsUpdate::ProportionalFalloffType(ProportionalFalloffType::Random)).into())),
+			]])
+			.min_width(120)
+			.selected_index(Some(options.proportional_falloff_type as u32))
+			.widget_holder(),
+		],
+	});
+
+	// Radius row
+	widgets.push(LayoutGroup::Row {
+		widgets: vec![
+			TextLabel::new("Radius").table_align(true).min_width(80).widget_holder(),
+			Separator::new(SeparatorType::Unrelated).widget_holder(),
+			NumberInput::new(Some(options.proportional_radius as f64))
+				.unit(" px")
+				.min(1.)
+				.int()
+				.min_width(120)
+				.on_update(move |number_input| callback(PathToolMessage::UpdateOptions(PathOptionsUpdate::ProportionalRadius(number_input.value.unwrap_or(1.) as u32)).into()))
+				.widget_holder(),
+		],
+	});
+
+	widgets
+}
+
+pub fn calculate_proportional_affected_points(
+	path_tool_data: &mut PathToolData,
+	document: &DocumentMessageHandler,
+	shape_editor: &ShapeState,
+	radius: u32,
+	proportional_falloff_type: ProportionalFalloffType,
+) {
+	path_tool_data.proportional_affected_points.clear();
+
+	let radius = radius as f64;
+
+	// If initial positions haven't been stored yet, do it now
+	if path_tool_data.initial_point_positions.is_empty() {
+		store_initial_point_positions(path_tool_data, document);
+	}
+
+	// Collect all selected points with their initial world positions
+	let mut selected_points_world_pos = Vec::new();
+	let selected_point_ids: HashSet<_> = shape_editor.selected_points().filter_map(|point| point.as_anchor()).collect();
+
+	// Extract initial positions of selected points
+	for (_layer, points_map) in &path_tool_data.initial_point_positions {
+		for &point_id in &selected_point_ids {
+			if let Some(&world_pos) = points_map.get(&point_id) {
+				selected_points_world_pos.push(world_pos);
+			}
+		}
+	}
+
+	// Find all affected points using initial positions
+	for (layer, points_map) in &path_tool_data.initial_point_positions {
+		let selected_points: HashSet<_> = shape_editor.selected_points().filter_map(|point| point.as_anchor()).collect();
+
+		let mut layer_affected_points = Vec::new();
+
+		// Check each point in the layer
+		for (&point_id, &initial_position) in points_map {
+			if !selected_points.contains(&point_id) {
+				// Find the smallest distance to any selected point using initial positions
+				let min_distance = selected_points_world_pos
+					.iter()
+					.map(|&selected_pos| initial_position.distance(selected_pos))
+					.filter(|&distance| distance <= radius)
+					.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
+
+				if let Some(distance) = min_distance {
+					let factor = path_tool_data.calculate_falloff_factor(distance, radius, proportional_falloff_type);
+					layer_affected_points.push((point_id, factor));
+				}
+			}
+		}
+
+		if !layer_affected_points.is_empty() {
+			path_tool_data.proportional_affected_points.insert(*layer, layer_affected_points);
+		}
+	}
+
+	// Find all affected points using initial positions
+	// NOTE: This works based on initial affected point location -> original selected point location for falloff calculation
+	for (layer, points_map) in &path_tool_data.initial_point_positions {
+		let selected_points: HashSet<_> = shape_editor.selected_points().filter_map(|point| point.as_anchor()).collect();
+
+		let mut layer_affected_points = Vec::new();
+
+		// Check each point in the layer
+		for (&point_id, &initial_position) in points_map {
+			if !selected_points.contains(&point_id) {
+				// Find the smallest distance to any selected point using initial positions
+				let min_distance = selected_points_world_pos
+					.iter()
+					.map(|&selected_pos| initial_position.distance(selected_pos))
+					.filter(|&distance| distance <= radius)
+					.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
+
+				if let Some(distance) = min_distance {
+					let factor = path_tool_data.calculate_falloff_factor(distance, radius, proportional_falloff_type);
+					layer_affected_points.push((point_id, factor));
+				}
+			}
+		}
+
+		if !layer_affected_points.is_empty() {
+			path_tool_data.proportional_affected_points.insert(*layer, layer_affected_points);
+		}
+	}
+}
+
+pub fn store_initial_point_positions(path_tool_data: &mut PathToolData, document: &DocumentMessageHandler) {
+	path_tool_data.initial_point_positions.clear();
+
+	// Store positions of all points in selected layers
+	for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
+		if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) {
+			let transform = document.metadata().transform_to_document(layer);
+			let mut layer_points = HashMap::new();
+
+			// Store all point positions in document space
+			for (i, &point_id) in vector_data.point_domain.ids().iter().enumerate() {
+				let position = vector_data.point_domain.positions()[i];
+				let world_pos = transform.transform_point2(position);
+				layer_points.insert(point_id, world_pos);
+			}
+
+			if !layer_points.is_empty() {
+				path_tool_data.initial_point_positions.insert(layer, layer_points);
+			}
+		}
+	}
+}
+
+pub fn update_proportional_positions(path_tool_data: &mut PathToolData, document: &DocumentMessageHandler, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>) {
+	// Get a set of all selected point IDs across all layers
+	let selected_points: HashSet<PointId> = shape_editor.selected_points().filter_map(|point| point.as_anchor()).collect();
+
+	for (layer, affected_points) in &path_tool_data.proportional_affected_points {
+		if let Some(vector_data) = document.network_interface.compute_modified_vector(*layer) {
+			let transform = document.metadata().transform_to_document(*layer);
+			let inverse_transform = transform.inverse();
+
+			for (point_id, factor) in affected_points {
+				// Skip this point if it's in the selected_points set
+				if selected_points.contains(point_id) {
+					continue;
+				}
+
+				if let Some(initial_doc_pos) = path_tool_data.initial_point_positions.get(layer).and_then(|pts| pts.get(point_id)) {
+					// Calculate displacement from initial position to target position
+					let displacement_document_space = path_tool_data.total_delta * (*factor);
+					let target_document_space_position = *initial_doc_pos + displacement_document_space;
+					let target_layer_space_position = inverse_transform.transform_point2(target_document_space_position);
+
+					// Get current position and calculate delta
+					if let Some(current_layer_space_position) = vector_data.point_domain.position_from_id(*point_id) {
+						let delta = target_layer_space_position - current_layer_space_position;
+						shape_editor.move_anchor(*point_id, &vector_data, delta, *layer, None, responses);
+					}
+				}
+			}
+		}
+	}
+}
+
+pub fn reset_removed_points(
+	path_tool_data: &mut PathToolData,
+	previous: &HashMap<LayerNodeIdentifier, Vec<(PointId, f64)>>,
+	document: &DocumentMessageHandler,
+	shape_editor: &mut ShapeState,
+	responses: &mut VecDeque<Message>,
+) {
+	for (layer, prev_points) in previous {
+		let current_points = path_tool_data
+			.proportional_affected_points
+			.get(layer)
+			.map(|v| v.iter().map(|(id, _)| *id).collect::<HashSet<_>>())
+			.unwrap_or_default();
+
+		for (point_id, _) in prev_points {
+			if !current_points.contains(point_id) {
+				if let Some(initial_doc_pos) = path_tool_data.initial_point_positions.get(layer).and_then(|pts| pts.get(point_id)) {
+					let inverse_transform = document.metadata().transform_to_document(*layer).inverse();
+					let target_layer_pos = inverse_transform.transform_point2(*initial_doc_pos);
+
+					if let Some(vector_data) = document.network_interface.compute_modified_vector(*layer) {
+						if let Some(current_layer_pos) = vector_data.point_domain.position_from_id(*point_id) {
+							let delta = target_layer_pos - current_layer_pos;
+							shape_editor.move_anchor(*point_id, &vector_data, delta, *layer, None, responses);
+						}
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs
index 7fa761acdc..6b30cfa530 100644
--- a/editor/src/messages/tool/common_functionality/shape_editor.rs
+++ b/editor/src/messages/tool/common_functionality/shape_editor.rs
@@ -253,6 +253,34 @@ impl ClosestSegment {
 
 // TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
 impl ShapeState {
+	/// Calculates the center point of all selected manipulator points (anchors and handles)
+	pub fn selection_center(&self, document: &DocumentMessageHandler) -> Option<DVec2> {
+		let mut sum = DVec2::ZERO;
+		let mut count = 0;
+
+		// Iterate through all selected layers and their selection states
+		for (&layer, state) in &self.selected_shape_state {
+			// Get the transform from layer space to document space
+			let transform = document.metadata().transform_to_document(layer);
+
+			// Get the vector data for this layer
+			if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) {
+				// Process each selected point in this layer
+				for point in state.selected() {
+					// Get the position in layer space coordinates
+					if let Some(position) = point.get_position(&vector_data) {
+						// Convert to document space and accumulate
+						sum += transform.transform_point2(position);
+						count += 1;
+					}
+				}
+			}
+		}
+
+		// Return average position if we have any points
+		if count > 0 { Some(sum / count as f64) } else { None }
+	}
+
 	pub fn close_selected_path(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
 		// First collect all selected anchor points across all layers
 		let all_selected_points: Vec<(LayerNodeIdentifier, PointId)> = self
diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs
index 41e5f1f1c7..7a432d593c 100644
--- a/editor/src/messages/tool/tool_messages/path_tool.rs
+++ b/editor/src/messages/tool/tool_messages/path_tool.rs
@@ -1,14 +1,16 @@
 use super::select_tool::extend_lasso;
 use super::tool_prelude::*;
 use crate::consts::{
-	COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE,
-	SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE,
+	COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_TRANSPARENT, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE,
+	SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE,
 };
+use crate::messages::input_mapper::utility_types::macros::action_keys;
 use crate::messages::portfolio::document::overlays::utility_functions::{path_overlays, selected_segments};
 use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext};
 use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
 use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
-use crate::messages::portfolio::document::utility_types::transformation::Axis;
+use crate::messages::portfolio::document::utility_types::proportional_editing::*;
+use crate::messages::portfolio::document::utility_types::transformation::{Axis, TransformType};
 use crate::messages::preferences::SelectionMode;
 use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
 use crate::messages::tool::common_functionality::shape_editor::{
@@ -18,6 +20,7 @@ use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandi
 use crate::messages::tool::common_functionality::utility_functions::calculate_segment_angle;
 use graphene_core::renderer::Quad;
 use graphene_core::vector::{ManipulatorPointId, PointId, VectorModificationType};
+use graphene_core::{ChaCha20Rng, Rng, SeedableRng};
 use graphene_std::vector::{HandleId, NoHashBuilder, SegmentId, VectorData};
 use std::vec;
 
@@ -28,9 +31,21 @@ pub struct PathTool {
 	options: PathToolOptions,
 }
 
-#[derive(Default)]
 pub struct PathToolOptions {
 	path_overlay_mode: PathOverlayMode,
+	pub proportional_editing_enabled: bool,
+	pub proportional_falloff_type: ProportionalFalloffType,
+	pub proportional_radius: u32,
+}
+impl Default for PathToolOptions {
+	fn default() -> Self {
+		Self {
+			path_overlay_mode: PathOverlayMode::default(),
+			proportional_editing_enabled: false,
+			proportional_falloff_type: ProportionalFalloffType::default(),
+			proportional_radius: 100,
+		}
+	}
 }
 
 #[impl_message(Message, ToolMessage, Path)]
@@ -99,6 +114,8 @@ pub enum PathToolMessage {
 	},
 	SwapSelectedHandles,
 	UpdateOptions(PathOptionsUpdate),
+	ToggleProportionalEditing,
+	AdjustProportionalRadius,
 }
 
 #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -112,6 +129,9 @@ pub enum PathOverlayMode {
 #[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
 pub enum PathOptionsUpdate {
 	OverlayModeType(PathOverlayMode),
+	ProportionalEditingEnabled(bool),
+	ProportionalFalloffType(ProportionalFalloffType),
+	ProportionalRadius(u32),
 }
 
 impl ToolMetadata for PathTool {
@@ -210,6 +230,15 @@ impl LayoutHolder for PathTool {
 		.selected_index(Some(self.options.path_overlay_mode as u32))
 		.widget_holder();
 
+		let proportional_editing_trigger = CheckboxInput::new(self.options.proportional_editing_enabled)
+			// TODO(Keavon): Replace placeholder icon with a proper one
+			.icon("Empty12px")
+			.tooltip("Proportional Editing")
+			.tooltip_shortcut(action_keys!(PathToolMessageDiscriminant::ToggleProportionalEditing))
+			.on_update(|checkbox| PathToolMessage::UpdateOptions(PathOptionsUpdate::ProportionalEditingEnabled(checkbox.checked)).into())
+			.widget_holder();
+		let proportional_editing_dropdown = PopoverButton::new().popover_layout(proportional_editing_options(&self.options)).widget_holder();
+
 		Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
 			widgets: vec![
 				x_location,
@@ -219,8 +248,11 @@ impl LayoutHolder for PathTool {
 				colinear_handle_checkbox,
 				related_seperator,
 				colinear_handles_label,
-				unrelated_seperator,
+				unrelated_seperator.clone(),
 				path_overlay_mode_widget,
+				unrelated_seperator,
+				proportional_editing_trigger,
+				proportional_editing_dropdown,
 			],
 		}]))
 	}
@@ -236,7 +268,86 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
 					self.options.path_overlay_mode = overlay_mode_type;
 					responses.add(OverlaysMessage::Draw);
 				}
+				PathOptionsUpdate::ProportionalEditingEnabled(enabled) => {
+					self.options.proportional_editing_enabled = enabled;
+
+					responses.add(OverlaysMessage::Draw);
+				}
+				PathOptionsUpdate::ProportionalFalloffType(falloff_type) => {
+					self.options.proportional_falloff_type = falloff_type.clone();
+					self.tool_data
+						.calculate_proportional_affected_points(&tool_data.document, &tool_data.shape_editor, self.options.proportional_radius, self.options.proportional_falloff_type);
+
+					if self.options.proportional_editing_enabled {
+						let proportional_data = ProportionalEditingData {
+							center: self.tool_data.proportional_editing_center.unwrap_or_default(),
+							affected_points: self.tool_data.proportional_affected_points.clone(),
+							falloff_type: self.options.proportional_falloff_type,
+							radius: self.options.proportional_radius,
+						};
+						responses.add(TransformLayerMessage::UpdateProportionalEditingData { data: proportional_data });
+					}
+					responses.add(OverlaysMessage::Draw);
+				}
+				PathOptionsUpdate::ProportionalRadius(radius) => {
+					self.options.proportional_radius = radius.clamp(1, 1000);
+					self.tool_data
+						.calculate_proportional_affected_points(&tool_data.document, &tool_data.shape_editor, self.options.proportional_radius, self.options.proportional_falloff_type);
+					responses.add(OverlaysMessage::Draw);
+				}
 			},
+			ToolMessage::Path(PathToolMessage::ToggleProportionalEditing) => {
+				self.options.proportional_editing_enabled ^= true;
+
+				responses.add(OverlaysMessage::Draw);
+			}
+			ToolMessage::Path(PathToolMessage::AdjustProportionalRadius) => {
+				if self.options.proportional_editing_enabled {
+					// Get the current radius and scroll delta
+					let current_radius = self.options.proportional_radius as f64;
+					let scroll_delta = (tool_data.input.mouse.scroll_delta.y as f64).min(1.).max(-1.);
+
+					// Base factor that determines how aggressive the scaling is
+					let base_factor = 0.15; // Sensitivity
+
+					// Calculate new radius using logarithmic scaling
+					let scale_factor = if scroll_delta > 0. {
+						1. + (base_factor * scroll_delta)
+					} else {
+						1. / (1. + (base_factor * -scroll_delta))
+					};
+
+					let new_radius = (current_radius * scale_factor).round() as u32;
+
+					// Ensure the radius stays within reasonable bounds
+					self.options.proportional_radius = new_radius.max(1);
+
+					// Store previous affected points
+					let previous_affected = self.tool_data.proportional_affected_points.clone();
+
+					self.tool_data
+						.calculate_proportional_affected_points(&tool_data.document, &tool_data.shape_editor, self.options.proportional_radius, self.options.proportional_falloff_type);
+					if self.tool_data.is_dragging {
+						// Reset points no longer affected
+						reset_removed_points(&mut self.tool_data, &previous_affected, tool_data.document, tool_data.shape_editor, responses);
+
+						// Update current points
+						update_proportional_positions(&mut self.tool_data, tool_data.document, tool_data.shape_editor, responses);
+					}
+					// Create updated proportional editing data
+					let proportional_data = ProportionalEditingData {
+						center: self.tool_data.proportional_editing_center.unwrap_or_default(),
+						affected_points: self.tool_data.proportional_affected_points.clone(),
+						falloff_type: self.options.proportional_falloff_type,
+						radius: self.options.proportional_radius,
+					};
+
+					// Send the updated data to any active GRS operation
+					responses.add(TransformLayerMessage::UpdateProportionalEditingData { data: proportional_data });
+
+					responses.add(OverlaysMessage::Draw);
+				}
+			}
 			ToolMessage::Path(PathToolMessage::ClosePath) => {
 				responses.add(DocumentMessage::AddTransaction);
 				tool_data.shape_editor.close_selected_path(tool_data.document, responses);
@@ -275,7 +386,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
 				BreakPath,
 				DeleteAndBreakPath,
 				ClosePath,
-				PointerMove,
+				ToggleProportionalEditing,
+				AdjustProportionalRadius,
+				GRS,
+				PointerMove
 			),
 			PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant;
 				Escape,
@@ -287,6 +401,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
 				BreakPath,
 				DeleteAndBreakPath,
 				SwapSelectedHandles,
+				AdjustProportionalRadius,
+				GRS
 			),
 			PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant;
 				FlipSmoothSharp,
@@ -298,6 +414,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
 				DeleteAndBreakPath,
 				Escape,
 				RightClick,
+				AdjustProportionalRadius,
 			),
 		}
 	}
@@ -313,6 +430,7 @@ impl ToolTransition for PathTool {
 		}
 	}
 }
+
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
 pub struct DraggingState {
 	point_select_state: PointSelectState,
@@ -338,7 +456,7 @@ enum PathToolFsmState {
 }
 
 #[derive(Default)]
-struct PathToolData {
+pub struct PathToolData {
 	snap_manager: SnapManager,
 	lasso_polygon: Vec<DVec2>,
 	selection_mode: Option<SelectionMode>,
@@ -367,6 +485,13 @@ struct PathToolData {
 	opposite_handle_position: Option<DVec2>,
 	last_clicked_point_was_selected: bool,
 	snapping_axis: Option<Axis>,
+
+	pub proportional_editing_center: Option<DVec2>,
+	pub proportional_affected_points: HashMap<LayerNodeIdentifier, Vec<(PointId, f64)>>,
+	pub initial_point_positions: HashMap<LayerNodeIdentifier, HashMap<PointId, DVec2>>,
+	pub total_delta: DVec2,
+	is_dragging: bool,
+
 	alt_clicked_on_anchor: bool,
 	alt_dragging_from_anchor: bool,
 	angle_locked: bool,
@@ -438,6 +563,7 @@ impl PathToolData {
 		extend_selection: bool,
 		lasso_select: bool,
 		handle_drag_from_anchor: bool,
+		tool_options: &PathToolOptions,
 	) -> PathToolFsmState {
 		self.double_click_handled = false;
 		self.opposing_handle_lengths = None;
@@ -500,7 +626,8 @@ impl PathToolData {
 					}
 				}
 
-				self.start_dragging_point(selected_points, input, document, shape_editor);
+				self.start_dragging_point(selected_points, input, document, shape_editor, tool_options);
+
 				responses.add(OverlaysMessage::Draw);
 			}
 			PathToolFsmState::Dragging(self.dragging_state)
@@ -548,9 +675,24 @@ impl PathToolData {
 		}
 	}
 
-	fn start_dragging_point(&mut self, selected_points: SelectedPointsInfo, input: &InputPreprocessorMessageHandler, document: &DocumentMessageHandler, shape_editor: &mut ShapeState) {
+	fn calculate_proportional_affected_points(&mut self, document: &DocumentMessageHandler, shape_editor: &ShapeState, radius: u32, proportional_falloff_type: ProportionalFalloffType) {
+		calculate_proportional_affected_points(self, document, shape_editor, radius, proportional_falloff_type);
+	}
+
+	fn start_dragging_point(
+		&mut self,
+		selected_points: SelectedPointsInfo,
+		input: &InputPreprocessorMessageHandler,
+		document: &DocumentMessageHandler,
+		shape_editor: &mut ShapeState,
+		tool_options: &PathToolOptions,
+	) {
 		let mut manipulators = HashMap::with_hasher(NoHashBuilder);
 		let mut unselected = Vec::new();
+		self.initial_point_positions.clear();
+		self.proportional_editing_center = shape_editor.selection_center(document);
+		self.calculate_proportional_affected_points(document, shape_editor, tool_options.proportional_radius, tool_options.proportional_falloff_type);
+		self.total_delta = DVec2::default();
 		for (&layer, state) in &shape_editor.selected_shape_state {
 			let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
 				continue;
@@ -646,8 +788,10 @@ impl PathToolData {
 					}
 				}
 			}
+
 			self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document));
 		}
+
 		false
 	}
 
@@ -828,7 +972,10 @@ impl PathToolData {
 		document: &DocumentMessageHandler,
 		input: &InputPreprocessorMessageHandler,
 		responses: &mut VecDeque<Message>,
+		tool_options: &PathToolOptions,
 	) {
+		self.is_dragging = true;
+
 		// First check if selection is not just a single handle point
 		let selected_points = shape_editor.selected_points();
 		let single_handle_selected = selected_points.count() == 1
@@ -868,6 +1015,8 @@ impl PathToolData {
 		let mut was_alt_dragging = false;
 
 		if self.snapping_axis.is_none() {
+			self.total_delta += snapped_delta;
+
 			if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
 				// Checking which direction the dragging begins
 				self.alt_dragging_from_anchor = true;
@@ -917,6 +1066,7 @@ impl PathToolData {
 				skip_opposite = true;
 			}
 			shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses);
+
 			self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
 		} else {
 			let Some(axis) = self.snapping_axis else { return };
@@ -925,10 +1075,18 @@ impl PathToolData {
 				Axis::Y => DVec2::new(0., unsnapped_delta.y),
 				_ => DVec2::new(unsnapped_delta.x, 0.),
 			};
+
+			self.total_delta += unsnapped_delta;
+
 			shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses);
+
 			self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
 		}
 
+		if tool_options.proportional_editing_enabled && !self.proportional_affected_points.is_empty() {
+			update_proportional_positions(self, document, shape_editor, responses);
+		}
+
 		if snap_angle && self.snapping_axis.is_some() {
 			let Some(current_axis) = self.snapping_axis else { return };
 			let total_delta = self.drag_start_pos - input.mouse.position;
@@ -939,6 +1097,34 @@ impl PathToolData {
 			}
 		}
 	}
+
+	pub fn calculate_falloff_factor(&self, distance: f64, radius: f64, falloff_type: ProportionalFalloffType) -> f64 {
+		// Handle edge cases
+		if distance >= radius {
+			return 0.;
+		}
+		if distance <= 0.001 {
+			return 1.;
+		}
+
+		let normalized_distance = distance / radius;
+
+		match falloff_type {
+			ProportionalFalloffType::Constant => 1.,
+			ProportionalFalloffType::Linear => 1. - normalized_distance,
+			ProportionalFalloffType::Sharp => (1. - normalized_distance).powi(2),
+			ProportionalFalloffType::Root => (1. - normalized_distance).sqrt(),
+			ProportionalFalloffType::Sphere => (1. - normalized_distance.powi(2)).sqrt(),
+			ProportionalFalloffType::Smooth => 1. - (normalized_distance.powi(2) * (3. - 2. * normalized_distance)),
+			ProportionalFalloffType::Random => {
+				// Seed RNG with position-based value for consistency
+				let seed = (distance * 1000.) as u64;
+				let mut rng = ChaCha20Rng::seed_from_u64(seed);
+				rng.random_range(0. ..1.) * (1. - normalized_distance)
+			}
+			ProportionalFalloffType::InverseSquare => 1. / (normalized_distance.powi(2) * 2. + 1.),
+		}
+	}
 }
 
 impl Fsm for PathToolFsmState {
@@ -1072,7 +1258,14 @@ impl Fsm for PathToolFsmState {
 					}
 					Self::Dragging(_) => {
 						tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
+						if tool_options.proportional_editing_enabled && tool_data.is_dragging {
+							if let Some(center) = tool_data.proportional_editing_center {
+								let viewport_center = document.metadata().document_to_viewport.transform_point2(center);
+								let radius_viewport = document.metadata().document_to_viewport.transform_vector2(DVec2::X * tool_options.proportional_radius as f64).x;
 
+								overlay_context.circle(viewport_center, radius_viewport, Some(COLOR_OVERLAY_TRANSPARENT), Some(COLOR_OVERLAY_BLUE));
+							}
+						}
 						// Draw the snapping axis lines
 						if tool_data.snapping_axis.is_some() {
 							let Some(axis) = tool_data.snapping_axis else { return self };
@@ -1099,6 +1292,38 @@ impl Fsm for PathToolFsmState {
 				}
 
 				responses.add(PathToolMessage::SelectedPointUpdated);
+
+				self
+			}
+			(_, PathToolMessage::GRS { key }) => {
+				// Calculate proportional editing center and affected points
+				tool_data.initial_point_positions.clear();
+				tool_data.proportional_editing_center = shape_editor.selection_center(document);
+				tool_data.calculate_proportional_affected_points(document, shape_editor, tool_options.proportional_radius, tool_options.proportional_falloff_type);
+
+				// Create proportional data to pass to transform layer
+				let mut proportional_data = Some(ProportionalEditingData {
+					center: tool_data.proportional_editing_center.unwrap_or_default(),
+					affected_points: tool_data.proportional_affected_points.clone(),
+					falloff_type: tool_options.proportional_falloff_type,
+					radius: tool_options.proportional_radius,
+				});
+
+				if !tool_options.proportional_editing_enabled {
+					proportional_data = None;
+				}
+
+				// Dispatch transform operation with proportional data
+				responses.add(TransformLayerMessage::BeginGRS {
+					transform_type: match key {
+						Key::KeyG => TransformType::Grab,
+						Key::KeyR => TransformType::Rotate,
+						Key::KeyS => TransformType::Scale,
+						_ => TransformType::Grab,
+					},
+					proportional_editing_data: proportional_data,
+				});
+
 				self
 			}
 
@@ -1119,7 +1344,7 @@ impl Fsm for PathToolFsmState {
 				tool_data.selection_mode = None;
 				tool_data.lasso_polygon.clear();
 
-				tool_data.mouse_down(shape_editor, document, input, responses, extend_selection, lasso_select, handle_drag_from_anchor)
+				tool_data.mouse_down(shape_editor, document, input, responses, extend_selection, lasso_select, handle_drag_from_anchor, tool_options)
 			}
 			(
 				PathToolFsmState::Drawing { selection_shape },
@@ -1232,6 +1457,7 @@ impl Fsm for PathToolFsmState {
 						tool_action_data.document,
 						input,
 						responses,
+						tool_options,
 					);
 				}
 
@@ -1368,6 +1594,8 @@ impl Fsm for PathToolFsmState {
 				PathToolFsmState::Ready
 			}
 			(PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => {
+				tool_data.is_dragging = false;
+				tool_data.initial_point_positions.clear();
 				if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
 					shape_editor.deselect_all_points();
 					shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag);
@@ -1380,6 +1608,8 @@ impl Fsm for PathToolFsmState {
 				PathToolFsmState::Ready
 			}
 			(PathToolFsmState::Drawing { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => {
+				tool_data.is_dragging = false;
+				tool_data.initial_point_positions.clear();
 				tool_data.snap_manager.cleanup(responses);
 				PathToolFsmState::Ready
 			}
@@ -1407,6 +1637,8 @@ impl Fsm for PathToolFsmState {
 						SelectionShapeType::Lasso => shape_editor.select_all_in_shape(&document.network_interface, SelectionShape::Lasso(&tool_data.lasso_polygon), select_kind),
 					}
 				}
+				tool_data.is_dragging = false;
+				tool_data.initial_point_positions.clear();
 				responses.add(OverlaysMessage::Draw);
 				responses.add(PathToolMessage::SelectedPointUpdated);
 
@@ -1470,6 +1702,8 @@ impl Fsm for PathToolFsmState {
 					tool_data.snapping_axis = None;
 				}
 
+				tool_data.is_dragging = false;
+
 				responses.add(DocumentMessage::EndTransaction);
 				responses.add(PathToolMessage::SelectedPointUpdated);
 				tool_data.snap_manager.cleanup(responses);
@@ -1504,6 +1738,7 @@ impl Fsm for PathToolFsmState {
 						responses.add(DocumentMessage::StartTransaction);
 						shape_editor.flip_smooth_sharp(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE, responses);
 						responses.add(DocumentMessage::EndTransaction);
+
 						responses.add(PathToolMessage::SelectedPointUpdated);
 					}
 
@@ -1572,13 +1807,16 @@ impl Fsm for PathToolFsmState {
 				responses.add(DocumentMessage::StartTransaction);
 				shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document);
 				responses.add(DocumentMessage::EndTransaction);
+
 				responses.add(PathToolMessage::SelectionChanged);
+
 				PathToolFsmState::Ready
 			}
 			(_, PathToolMessage::ManipulatorMakeHandlesFree) => {
 				responses.add(DocumentMessage::StartTransaction);
 				shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses);
 				responses.add(DocumentMessage::EndTransaction);
+
 				PathToolFsmState::Ready
 			}
 			(_, _) => PathToolFsmState::Ready,
@@ -1808,7 +2046,7 @@ fn calculate_lock_angle(
 			let angle_2 = calculate_segment_angle(anchor, segment, vector_data, false);
 
 			match (angle_1, angle_2) {
-				(Some(angle_1), Some(angle_2)) => Some((angle_1 + angle_2) / 2.0),
+				(Some(angle_1), Some(angle_2)) => Some((angle_1 + angle_2) / 2.),
 				(Some(angle_1), None) => Some(angle_1),
 				(None, Some(angle_2)) => Some(angle_2),
 				(None, None) => None,
diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message.rs b/editor/src/messages/tool/transform_layer/transform_layer_message.rs
index dfc45c1e05..f5bc6a9a27 100644
--- a/editor/src/messages/tool/transform_layer/transform_layer_message.rs
+++ b/editor/src/messages/tool/transform_layer/transform_layer_message.rs
@@ -1,7 +1,9 @@
 use crate::messages::input_mapper::utility_types::input_keyboard::Key;
 use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
+use crate::messages::portfolio::document::utility_types::proportional_editing::ProportionalEditingData;
 use crate::messages::portfolio::document::utility_types::transformation::TransformType;
 use crate::messages::prelude::*;
+
 use glam::DVec2;
 
 #[impl_message(Message, ToolMessage, TransformLayer)]
@@ -11,21 +13,43 @@ pub enum TransformLayerMessage {
 	Overlays(OverlayContext),
 
 	// Messages
-	ApplyTransformOperation { final_transform: bool },
+	ApplyTransformOperation {
+		final_transform: bool,
+	},
 	BeginGrab,
 	BeginRotate,
 	BeginScale,
-	BeginGRS { transform_type: TransformType },
-	BeginGrabPen { last_point: DVec2, handle: DVec2 },
-	BeginRotatePen { last_point: DVec2, handle: DVec2 },
-	BeginScalePen { last_point: DVec2, handle: DVec2 },
+	BeginGRS {
+		transform_type: TransformType,
+		proportional_editing_data: Option<ProportionalEditingData>,
+	},
+	BeginGrabPen {
+		last_point: DVec2,
+		handle: DVec2,
+	},
+	BeginRotatePen {
+		last_point: DVec2,
+		handle: DVec2,
+	},
+	BeginScalePen {
+		last_point: DVec2,
+		handle: DVec2,
+	},
 	CancelTransformOperation,
 	ConstrainX,
 	ConstrainY,
-	PointerMove { slow_key: Key, increments_key: Key },
+	PointerMove {
+		slow_key: Key,
+		increments_key: Key,
+	},
 	SelectionChanged,
 	TypeBackspace,
 	TypeDecimalPoint,
-	TypeDigit { digit: u8 },
+	TypeDigit {
+		digit: u8,
+	},
 	TypeNegate,
+	UpdateProportionalEditingData {
+		data: ProportionalEditingData,
+	},
 }
diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs
index 0e44efabca..ddf2bc7164 100644
--- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs
+++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs
@@ -1,8 +1,10 @@
-use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, SLOWING_DIVISOR};
+use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_TRANSPARENT, SLOWING_DIVISOR};
 use crate::messages::input_mapper::utility_types::input_mouse::{DocumentPosition, ViewportPosition};
 use crate::messages::portfolio::document::overlays::utility_types::{OverlayProvider, Pivot};
-use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
+use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
 use crate::messages::portfolio::document::utility_types::misc::PTZ;
+use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
+use crate::messages::portfolio::document::utility_types::proportional_editing::*;
 use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, TransformType, Typing};
 use crate::messages::prelude::*;
 use crate::messages::tool::common_functionality::shape_editor::ShapeState;
@@ -11,7 +13,7 @@ use crate::messages::tool::utility_types::{ToolData, ToolType};
 use glam::{DAffine2, DVec2};
 use graphene_core::renderer::Quad;
 use graphene_core::vector::ManipulatorPointId;
-use graphene_std::vector::{VectorData, VectorModificationType};
+use graphene_std::vector::{PointId, VectorData, VectorModificationType};
 use std::f64::consts::{PI, TAU};
 
 const TRANSFORM_GRS_OVERLAY_PROVIDER: OverlayProvider = |context| TransformLayerMessage::Overlays(context).into();
@@ -49,6 +51,10 @@ pub struct TransformLayerMessageHandler {
 	handle: DVec2,
 	last_point: DVec2,
 	grs_pen_handle: bool,
+
+	// Path tool (proportional editing)
+	initial_positions: HashMap<LayerNodeIdentifier, HashMap<PointId, DVec2>>,
+	proportional_editing_data: Option<ProportionalEditingData>,
 }
 
 impl TransformLayerMessageHandler {
@@ -59,6 +65,54 @@ impl TransformLayerMessageHandler {
 	pub fn hints(&self, responses: &mut VecDeque<Message>) {
 		self.transform_operation.hints(responses, self.local);
 	}
+
+	pub fn calculate_total_transformation_vp(&self, document_to_viewport: DAffine2) -> DAffine2 {
+		let pivot_vp = document_to_viewport.transform_point2(self.local_pivot);
+		let local_axis_transform_angle = (self.layer_bounding_box.0[1] - self.layer_bounding_box.0[0]).to_angle();
+
+		match self.transform_operation {
+			TransformOperation::Grabbing(translation) => {
+				let total_delta_doc = translation.to_dvec(self.initial_transform, self.increments);
+				let translate = DAffine2::from_translation(document_to_viewport.transform_vector2(total_delta_doc));
+				if self.local {
+					let resolved_angle = if local_axis_transform_angle > 0. {
+						local_axis_transform_angle
+					} else {
+						local_axis_transform_angle - std::f64::consts::PI
+					};
+					DAffine2::from_angle(resolved_angle) * translate * DAffine2::from_angle(-resolved_angle)
+				} else {
+					translate
+				}
+			}
+			TransformOperation::Rotating(rotation) => {
+				let total_angle = rotation.to_f64(self.increments);
+				let pivot_transform = DAffine2::from_translation(pivot_vp);
+				pivot_transform * DAffine2::from_angle(total_angle) * pivot_transform.inverse()
+			}
+			TransformOperation::Scaling(scale) => {
+				let total_scale_vec = scale.to_dvec(self.increments);
+				let pivot_transform = DAffine2::from_translation(pivot_vp);
+				if self.local {
+					pivot_transform
+						* DAffine2::from_angle(local_axis_transform_angle)
+						* DAffine2::from_scale(total_scale_vec)
+						* DAffine2::from_angle(-local_axis_transform_angle)
+						* pivot_transform.inverse()
+				} else {
+					pivot_transform * DAffine2::from_scale(total_scale_vec) * pivot_transform.inverse()
+				}
+			}
+			TransformOperation::None => DAffine2::IDENTITY,
+		}
+	}
+
+	// Apply proportional editing with the given transformation
+	fn apply_proportional_editing(&mut self, total_transformation_vp: DAffine2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
+		if let Some(prop_data) = &self.proportional_editing_data {
+			apply_proportionaling_edit(&self.initial_positions, prop_data, total_transformation_vp, &document.network_interface, document.metadata(), responses);
+		}
+	}
 }
 
 fn calculate_pivot(selected_points: &Vec<&ManipulatorPointId>, vector_data: &VectorData, viewspace: DAffine2, get_location: impl Fn(&ManipulatorPointId) -> Option<DVec2>) -> Option<(DVec2, DVec2)> {
@@ -105,7 +159,73 @@ fn project_edge_to_quad(edge: DVec2, quad: &Quad, local: bool, axis_constraint:
 		_ => edge,
 	}
 }
+fn apply_proportionaling_edit(
+	initial_positions: &HashMap<LayerNodeIdentifier, HashMap<PointId, DVec2>>,
+	proportional_data: &ProportionalEditingData,
+	total_transformation_vp: DAffine2,
+	network_interface: &NodeNetworkInterface,
+	document_metadata: &DocumentMetadata,
+	responses: &mut VecDeque<Message>,
+) {
+	// Iterate through layers that have initial positions
+	for (layer, layer_initial_positions) in initial_positions {
+		// Get current vector data for position comparison
+		let Some(current_vector_data) = network_interface.compute_modified_vector(*layer) else {
+			continue;
+		};
+
+		let viewspace = document_metadata.transform_to_viewport(*layer);
+
+		// Create a lookup map for affected points
+		let affected_points_map: HashMap<PointId, f64> = proportional_data
+			.affected_points
+			.get(layer)
+			.map(|points| points.iter().map(|(id, factor)| (*id, *factor)).collect())
+			.unwrap_or_default();
+
+		// Process all points that were stored in initial positions
+		for (point_id, initial_pos_local) in layer_initial_positions {
+			let Some(current_pos_local) = current_vector_data.point_domain.position_from_id(*point_id) else {
+				continue;
+			};
+
+			// Transform initial position to viewport space
+			let initial_pos_vp = viewspace.transform_point2(*initial_pos_local);
+
+			// Affected point:
+			// Apply proportional transformation
+			if let Some(factor) = affected_points_map.get(point_id) {
+				let target_pos_fully_transformed_vp = total_transformation_vp.transform_point2(initial_pos_vp);
+				let full_intended_delta_vp = target_pos_fully_transformed_vp - initial_pos_vp;
+
+				let scaled_intended_delta_vp = full_intended_delta_vp * (*factor);
+
+				let target_pos_proportional_vp = initial_pos_vp + scaled_intended_delta_vp;
+				let target_pos_proportional_local = viewspace.inverse().transform_point2(target_pos_proportional_vp);
 
+				let final_delta_local = target_pos_proportional_local - current_pos_local;
+
+				if final_delta_local.length_squared() > 1e-10 {
+					let modification_type = VectorModificationType::ApplyPointDelta {
+						point: *point_id,
+						delta: final_delta_local,
+					};
+					responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type });
+				}
+			}
+			// Non-affected point:
+			// Reset to original position
+			else {
+				let reset_delta = *initial_pos_local - current_pos_local;
+
+				if reset_delta.length_squared() > 1e-10 {
+					let modification_type = VectorModificationType::ApplyPointDelta { point: *point_id, delta: reset_delta };
+					responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type });
+				}
+			}
+		}
+	}
+}
 fn update_colinear_handles(selected_layers: &[LayerNodeIdentifier], document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
 	for &layer in selected_layers {
 		let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
@@ -212,6 +332,12 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				if !overlay_context.visibility_settings.transform_measurement() {
 					return;
 				}
+				if let Some(proportional_data) = &self.proportional_editing_data {
+					let viewport_center = document.metadata().document_to_viewport.transform_point2(proportional_data.center);
+					let radius_viewport = document.metadata().document_to_viewport.transform_vector2(DVec2::X * proportional_data.radius as f64).x;
+
+					overlay_context.circle(viewport_center, radius_viewport, Some(COLOR_OVERLAY_TRANSPARENT), Some(COLOR_OVERLAY_BLUE));
+				}
 
 				for layer in document.metadata().all_layers() {
 					if !document.network_interface.is_artboard(&layer.to_node(), &[]) {
@@ -350,6 +476,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				}
 
 				if final_transform {
+					self.proportional_editing_data = None;
+					self.initial_positions.clear();
 					responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
 				}
 			}
@@ -387,7 +515,10 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 					increments_key: INCREMENTS_KEY,
 				});
 			}
-			TransformLayerMessage::BeginGRS { transform_type } => {
+			TransformLayerMessage::BeginGRS {
+				transform_type,
+				proportional_editing_data,
+			} => {
 				let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
 				if (using_path_tool && selected_points.is_empty())
 					|| (!using_path_tool && !using_select_tool && !using_pen_tool)
@@ -396,11 +527,36 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				{
 					return;
 				}
-
 				let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) else {
 					selected.original_transforms.clear();
 					return;
 				};
+				self.proportional_editing_data = proportional_editing_data;
+				self.initial_positions.clear();
+
+				if let Some(_prop_data) = &self.proportional_editing_data {
+					// Store positions of all points in selected layers, not just affected points
+					for &layer in &selected_layers {
+						if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) {
+							let layer_initial_positions = self.initial_positions.entry(layer).or_default();
+
+							// Get all selected points in this layer to exclude them
+							let selected_points: HashSet<PointId> = shape_editor
+								.selected_points_in_layer(layer)
+								.map(|points| points.iter().filter_map(|p| p.as_anchor()).collect())
+								.unwrap_or_default();
+
+							// Store point positions only for unselected points
+							for (i, &point_id) in vector_data.point_domain.ids().iter().enumerate() {
+								// Skip points that are selected by the user
+								if !selected_points.contains(&point_id) {
+									let pos_local = vector_data.point_domain.positions()[i];
+									layer_initial_positions.insert(point_id, pos_local);
+								}
+							}
+						}
+					}
+				}
 
 				if let [point] = selected_points.as_slice() {
 					if matches!(point, ManipulatorPointId::Anchor(_)) {
@@ -408,7 +564,13 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 							let handle1_length = handle1.length(&vector_data);
 							let handle2_length = handle2.length(&vector_data);
 
-							if (handle1_length == 0. && handle2_length == 0. && !using_select_tool) || (handle1_length == f64::MAX && handle2_length == f64::MAX && !using_select_tool) {
+							// Check if proportional editing is enabled
+							let proportional_editing_enabled = self.proportional_editing_data.is_some();
+
+							// Only restrict R and S operations if proportional editing is not enabled
+							if !proportional_editing_enabled
+								&& ((handle1_length == 0. && handle2_length == 0. && !using_select_tool) || (handle1_length == f64::MAX && handle2_length == f64::MAX && !using_select_tool))
+							{
 								// G should work for this point but not R and S
 								if matches!(transform_type, TransformType::Rotate | TransformType::Scale) {
 									selected.original_transforms.clear();
@@ -481,7 +643,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 					self.operation_count = 0;
 					responses.add(ToolMessage::UpdateHints);
 				}
-
+				self.proportional_editing_data = None;
+				self.initial_positions.clear();
 				responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
 			}
 			TransformLayerMessage::ConstrainX => {
@@ -618,12 +781,76 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 					};
 				}
 
+				let pivot_vp = document_to_viewport.transform_point2(self.local_pivot);
+				let local_axis_transform_angle = (self.layer_bounding_box.0[1] - self.layer_bounding_box.0[0]).to_angle();
+
+				let total_transformation_vp = match self.transform_operation {
+					TransformOperation::Grabbing(translation) => {
+						let total_delta_doc = translation.to_dvec(self.initial_transform, self.increments);
+						let translate = DAffine2::from_translation(document_to_viewport.transform_vector2(total_delta_doc));
+						if self.local {
+							let resolved_angle = if local_axis_transform_angle > 0. {
+								local_axis_transform_angle
+							} else {
+								local_axis_transform_angle - std::f64::consts::PI
+							};
+							DAffine2::from_angle(resolved_angle) * translate * DAffine2::from_angle(-resolved_angle)
+						} else {
+							translate
+						}
+					}
+					TransformOperation::Rotating(rotation) => {
+						let total_angle = rotation.to_f64(self.increments);
+						let pivot_transform = DAffine2::from_translation(pivot_vp);
+						pivot_transform * DAffine2::from_angle(total_angle) * pivot_transform.inverse()
+					}
+					TransformOperation::Scaling(scale) => {
+						let total_scale_vec = scale.to_dvec(self.increments);
+						let pivot_transform = DAffine2::from_translation(pivot_vp);
+
+						if self.local {
+							pivot_transform
+								* DAffine2::from_angle(local_axis_transform_angle)
+								* DAffine2::from_scale(total_scale_vec)
+								* DAffine2::from_angle(-local_axis_transform_angle)
+								* pivot_transform.inverse()
+						} else {
+							pivot_transform * DAffine2::from_scale(total_scale_vec) * pivot_transform.inverse()
+						}
+					}
+					TransformOperation::None => DAffine2::IDENTITY,
+				};
+
+				if let Some(prop_data) = &self.proportional_editing_data {
+					apply_proportionaling_edit(&self.initial_positions, prop_data, total_transformation_vp, &document.network_interface, document.metadata(), responses);
+				}
+
 				self.mouse_position = input.mouse.position;
 			}
 			TransformLayerMessage::SelectionChanged => {
 				let target_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect();
 				shape_editor.set_selected_layers(target_layers);
 			}
+			TransformLayerMessage::TypeDecimalPoint => {
+				let pivot = document_to_viewport.transform_point2(self.local_pivot);
+				if self.transform_operation.can_begin_typing() {
+					self.transform_operation.grs_typed(
+						self.typing.type_decimal_point(),
+						&mut selected,
+						self.increments,
+						self.local,
+						self.layer_bounding_box,
+						document_to_viewport,
+						pivot,
+						self.initial_transform,
+					);
+
+					// Apply proportional editing
+					let total_transformation_vp = self.calculate_total_transformation_vp(document_to_viewport);
+					self.apply_proportional_editing(total_transformation_vp, document, responses);
+				}
+			}
+
 			TransformLayerMessage::TypeBackspace => {
 				let pivot = document_to_viewport.transform_point2(self.local_pivot);
 				if self.typing.digits.is_empty() && self.typing.negative {
@@ -641,22 +868,12 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 					pivot,
 					self.initial_transform,
 				);
+
+				// Apply proportional editing
+				let total_transformation_vp = self.calculate_total_transformation_vp(document_to_viewport);
+				self.apply_proportional_editing(total_transformation_vp, document, responses);
 			}
-			TransformLayerMessage::TypeDecimalPoint => {
-				let pivot = document_to_viewport.transform_point2(self.local_pivot);
-				if self.transform_operation.can_begin_typing() {
-					self.transform_operation.grs_typed(
-						self.typing.type_decimal_point(),
-						&mut selected,
-						self.increments,
-						self.local,
-						self.layer_bounding_box,
-						document_to_viewport,
-						pivot,
-						self.initial_transform,
-					)
-				}
-			}
+
 			TransformLayerMessage::TypeDigit { digit } => {
 				if self.transform_operation.can_begin_typing() {
 					let pivot = document_to_viewport.transform_point2(self.local_pivot);
@@ -669,7 +886,11 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 						document_to_viewport,
 						pivot,
 						self.initial_transform,
-					)
+					);
+
+					// Calculate total transformation and apply proportional editing
+					let total_transformation_vp = self.calculate_total_transformation_vp(document_to_viewport);
+					self.apply_proportional_editing(total_transformation_vp, document, responses);
 				}
 			}
 			TransformLayerMessage::TypeNegate => {
@@ -687,7 +908,28 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 					document_to_viewport,
 					pivot,
 					self.initial_transform,
-				)
+				);
+
+				// Apply proportional editing
+				let total_transformation_vp = self.calculate_total_transformation_vp(document_to_viewport);
+				self.apply_proportional_editing(total_transformation_vp, document, responses);
+			}
+			TransformLayerMessage::UpdateProportionalEditingData { data } => {
+				// Only update if we're in a transform operation with proportional editing
+				if let Some(current_proportional_data) = &mut self.proportional_editing_data {
+					// Update all fields from the new data
+					current_proportional_data.center = data.center;
+					current_proportional_data.affected_points = data.affected_points;
+					current_proportional_data.falloff_type = data.falloff_type;
+					current_proportional_data.radius = data.radius;
+
+					// TODO: Essentialy a hack to trigger redraw for updated values
+					responses.add(TransformLayerMessage::PointerMove {
+						slow_key: SLOW_KEY,
+						increments_key: INCREMENTS_KEY,
+					});
+					responses.add(OverlaysMessage::Draw);
+				}
 			}
 		}
 	}
@@ -708,6 +950,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				TypeNegate,
 				ConstrainX,
 				ConstrainY,
+				UpdateProportionalEditingData
 			);
 			common.extend(active);
 		}
@@ -797,7 +1040,7 @@ mod test_transform_layer {
 		let final_translation = final_transform.translation;
 		let original_translation = original_transform.translation;
 
-		// Verify transform is either restored to original OR reset to identity
+		// Verify transform is either restored to original or reset to identity
 		assert!(
 			(final_translation - original_translation).length() < 5. || final_translation.length() < 0.001,
 			"Transform neither restored to original nor reset to identity. Original: {:?}, Final: {:?}",
diff --git a/frontend/src/components/widgets/inputs/DropdownInput.svelte b/frontend/src/components/widgets/inputs/DropdownInput.svelte
index c976bf9c11..f6a6d1f2d5 100644
--- a/frontend/src/components/widgets/inputs/DropdownInput.svelte
+++ b/frontend/src/components/widgets/inputs/DropdownInput.svelte
@@ -21,16 +21,18 @@
 	export let interactive = true;
 	export let disabled = false;
 	export let tooltip: string | undefined = undefined;
+	export let minWidth = 0;
 
 	let activeEntry = makeActiveEntry();
 	let activeEntrySkipWatcher = false;
 	let initialSelectedIndex: number | undefined = undefined;
 	let open = false;
-	let minWidth = 0;
+	let measuredMinWidth = 0;
 
 	$: watchSelectedIndex(selectedIndex);
 	$: watchActiveEntry(activeEntry);
 	$: watchOpen(open);
+	$: minWidth = Math.max(minWidth, measuredMinWidth);
 
 	function watchOpen(open: boolean) {
 		initialSelectedIndex = open ? selectedIndex : undefined;
@@ -94,7 +96,7 @@
 		<IconLabel class="dropdown-arrow" icon="DropdownArrow" />
 	</LayoutRow>
 	<MenuList
-		on:naturalWidth={({ detail }) => (minWidth = detail)}
+		on:naturalWidth={({ detail }) => (measuredMinWidth = detail)}
 		{activeEntry}
 		on:activeEntry={({ detail }) => (activeEntry = detail)}
 		on:hoverInEntry={({ detail }) => dispatchHoverInEntry(detail)}
diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts
index 2272069cc9..445401c777 100644
--- a/frontend/src/messages.ts
+++ b/frontend/src/messages.ts
@@ -1085,6 +1085,10 @@ export class DropdownInput extends WidgetProps {
 
 	@Transform(({ value }: { value: string }) => value || undefined)
 	tooltip!: string | undefined;
+
+	// Styling
+
+	minWidth!: number;
 }
 
 export class FontInput extends WidgetProps {
diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs
index 1aa71eed73..13d4dacd07 100644
--- a/node-graph/gcore/src/lib.rs
+++ b/node-graph/gcore/src/lib.rs
@@ -13,7 +13,6 @@ pub use num_traits;
 
 #[cfg(feature = "reflections")]
 pub use ctor;
-
 pub mod animation;
 pub mod consts;
 pub mod context;
@@ -57,6 +56,9 @@ use core::any::TypeId;
 use core::pin::Pin;
 pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync};
 pub use memo::MemoHash;
+// TODO: Perhaps build a wrapper util for Rng
+pub use rand::{Rng, SeedableRng};
+pub use rand_chacha::ChaCha20Rng;
 pub use raster::Color;
 pub use types::Cow;