ronantakizawa/codereview-bench huggingface dataset:
Repository: GraphiteEditor/Graphite
File: editor/src/messages/defer/defer_message_handler.rs (Rust)

Code before:
use crate::messages::prelude::*;

#[derive(Debug, Default, ExtractField)]
pub struct DeferMessageHandler {
	after_graph_run: Vec<(u64, Message)>,
	after_viewport_resize: Vec<Message>,
	current_graph_submission_id: u64,
}

#[message_handler_data]
impl MessageHandler<DeferMessage, ()> for DeferMessageHandler {
	fn process_message(&mut self, message: DeferMessage, responses: &mut VecDeque<Message>, _: ()) {
		match message {
			DeferMessage::AfterGraphRun { mut messages } => {
				self.after_graph_run.extend(messages.drain(..).map(|m| (self.current_graph_submission_id, m)));
			}
			DeferMessage::AfterViewportReady { messages } => {
				self.after_viewport_resize.extend_from_slice(&messages);
			}
			DeferMessage::TriggerGraphRun(execution_id) => {
				self.current_graph_submission_id = execution_id;
				for message in self.after_graph_run.extract_if(.., |x| x.0 < self.current_graph_submission_id) {
					responses.push_front(message.1);
				}
			}
			DeferMessage::TriggerViewportReady => {
				for message in self.after_viewport_resize.drain(..) {
					responses.push_front(message);
				}
			}
		}
	}

Review comment (suggestion):
~~I don't see why we have to associate queued messages with their execution id, since they should always be run after the next execution.~~ (answer: If multiple graph evaluations are queued, we do not want messages which require data from a later evaluation to be run on the first evaluation )

Code after:
use crate::messages::prelude::*;

#[derive(Debug, Default, ExtractField)]
pub struct DeferMessageHandler {
	after_graph_run: Vec<(u64, Message)>,
	after_viewport_resize: Vec<Message>,
	current_graph_submission_id: u64,
}

#[message_handler_data]
impl MessageHandler<DeferMessage, ()> for DeferMessageHandler {
	fn process_message(&mut self, message: DeferMessage, responses: &mut VecDeque<Message>, _: ()) {
		match message {
			DeferMessage::AfterGraphRun { mut messages } => {
				self.after_graph_run.extend(messages.drain(..).map(|m| (self.current_graph_submission_id, m)));
			}
			DeferMessage::AfterNavigationReady { messages } => {
				self.after_viewport_resize.extend_from_slice(&messages);
			}
			DeferMessage::TriggerGraphRun(execution_id) => {
				self.current_graph_submission_id = execution_id;
				for message in self.after_graph_run.extract_if(.., |x| x.0 < self.current_graph_submission_id) {
					responses.push_front(message.1);
				}
			}
			DeferMessage::TriggerNavigationReady => {
				for message in self.after_viewport_resize.drain(..) {
					responses.push_front(message);
				}
			}
		}
	}
Repository: GraphiteEditor/Graphite
File: desktop/src/cli.rs (Rust)

Code before:
use std::path::PathBuf;

use clap::{Parser, ValueEnum};

#[derive(Debug, Clone, ValueEnum)]
pub enum UIAceleratedPainting {
	Auto,
	Yes,
	No,
}

#[derive(Debug, Parser)]
#[clap(name = "graphite-cli", version)]
pub struct Cli {
	pub files: Option<Vec<PathBuf>>,

	#[arg(long, value_enum, default_value_t = UIAceleratedPainting::Auto,  help = "Enable UI accelerated painting using GPU")]
	pub ui_accelerated_painting: UIAceleratedPainting,
}


Review comment (suggestion):
I would prefer only a `--disable-ui-accaleration` flag. Users should never run into a situation were acceleration is possible but we disable it.

Code after:
#[derive(clap::Parser)]
#[clap(name = "graphite-editor", version)]
pub struct Cli {
	#[arg(help = "Files to open on startup")]
	pub files: Vec<std::path::PathBuf>,

	#[arg(long, action = clap::ArgAction::SetTrue, help = "Disable hardware accelerated UI rendering")]
	pub disable_ui_acceleration: bool,
}
Repository: GraphiteEditor/Graphite
File: node-graph/graster-nodes/src/adjustments.rs (Rust)

Code before:
		// Midtones (Range: 0-1)
		let color = color.gamma(gamma);

		// Output levels (Range: 0-1)
		let color = color.map_rgb(|c| c * (output_maximums - output_minimums) + output_minimums);

		color.to_linear_srgb()
	});
	image
}

// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27blwh%27%20%3D%20Black%20and%20White
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Black%20White%20(Photoshop%20CS3)
//
// Algorithm from:
// https://stackoverflow.com/a/55233732/775283
// Works the same for gamma and linear color
#[node_macro::node(name("Black & White"), category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
fn black_and_white<T: Adjust<Color>>(
	_: impl Ctx,
	#[implementations(
		Table<Raster<CPU>>,
		Table<Color>,
		Table<GradientStops>,
		GradientStops,
	)]
	mut image: T,
	#[default(Color::BLACK)] tint: Color,
	#[default(40.)]
	#[range((-200., 300.))]
	reds: Percentage,
	#[default(60.)]
	#[range((-200., 300.))]
	yellows: Percentage,
	#[default(40.)]
	#[range((-200., 300.))]
	greens: Percentage,
	#[default(60.)]
	#[range((-200., 300.))]
	cyans: Percentage,
	#[default(20.)]
	#[range((-200., 300.))]
	blues: Percentage,
	#[default(80.)]
	#[range((-200., 300.))]
	magentas: Percentage,
) -> T {
	image.adjust(|color| {
		let color = color.to_gamma_srgb();


Review comment (suggestion):
Please verify that changing `Table<Color>` -> `Color` indeed changes absolutely nothing, since with the table you're just using the first entry of the table anyway. Affects both this `black_and_white` node and the `color_overlay` node.

Code after:
		// Midtones (Range: 0-1)
		let color = color.gamma(gamma);

		// Output levels (Range: 0-1)
		let color = color.map_rgb(|c| c * (output_maximums - output_minimums) + output_minimums);

		color.to_linear_srgb()
	});
	image
}

// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27blwh%27%20%3D%20Black%20and%20White
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Black%20White%20(Photoshop%20CS3)
//
// Algorithm from:
// https://stackoverflow.com/a/55233732/775283
// Works the same for gamma and linear color
#[node_macro::node(name("Black & White"), category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
fn black_and_white<T: Adjust<Color>>(
	_: impl Ctx,
	#[implementations(
		Table<Raster<CPU>>,
		Table<Color>,
		Table<GradientStops>,
		GradientStops,
	)]
	mut image: T,
	#[default(Color::BLACK)] tint: Table<Color>,
	#[default(40.)]
	#[range((-200., 300.))]
	reds: Percentage,
	#[default(60.)]
	#[range((-200., 300.))]
	yellows: Percentage,
	#[default(40.)]
	#[range((-200., 300.))]
	greens: Percentage,
	#[default(60.)]
	#[range((-200., 300.))]
	cyans: Percentage,
	#[default(20.)]
	#[range((-200., 300.))]
	blues: Percentage,
	#[default(80.)]
	#[range((-200., 300.))]
	magentas: Percentage,
) -> T {
	let tint: Option<Color> = tint.into();
	let tint = tint.unwrap_or(Color::BLACK);
Repository: GraphiteEditor/Graphite
File: desktop/wrapper/src/handle_desktop_wrapper_message.rs (Rust)

Code before:
				image,
				mouse: None,
				parent_and_insert_index: None,
			};
			dispatcher.queue_editor_message(message.into());
		}
		DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
		DesktopWrapperMessage::UpdatePlatform(platform) => {
			let platform = match platform {
				Platform::Windows => AppWindowPlatform::Windows,
				Platform::Mac => AppWindowPlatform::Mac,
				Platform::Linux => AppWindowPlatform::Linux,
			};
			let message = AppWindowMessage::AppWindowUpdatePlatform { platform };
			dispatcher.queue_editor_message(message.into());
		}
		DesktopWrapperMessage::LoadDocument { id, document, to_front } => {
			let message = PortfolioMessage::OpenDocumentFileWithId {
				document_id: id,
				document_name: Some(document.name),
				document_path: document.path,
				document_serialized_content: document.content,
				document_is_auto_saved: true,
				document_is_saved: document.is_saved,
				to_front,
				select_after_open: false,
			};
			dispatcher.queue_editor_message(message.into());
		}
		DesktopWrapperMessage::SelectDocument { id } => {
			let message = PortfolioMessage::SelectDocument { document_id: id };
			dispatcher.queue_editor_message(message.into());
		}
	}
}


Review comment (suggestion):
This is the rebase conflict I fixed, since I introduced this `select_after_open` field in a PR I added yesterday. You may want to check if `false` is appropriate here, or if you want to feed this in as a variable (like `to_front` on the line above gets fed in as a variable) so callers of this message can decide on a case-by-case basis.

Code after:
				image,
				mouse: None,
				parent_and_insert_index: None,
			};
			dispatcher.queue_editor_message(message.into());
		}
		DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
		DesktopWrapperMessage::UpdatePlatform(platform) => {
			let platform = match platform {
				Platform::Windows => AppWindowPlatform::Windows,
				Platform::Mac => AppWindowPlatform::Mac,
				Platform::Linux => AppWindowPlatform::Linux,
			};
			let message = AppWindowMessage::AppWindowUpdatePlatform { platform };
			dispatcher.queue_editor_message(message.into());
		}
		DesktopWrapperMessage::LoadDocument {
			id,
			document,
			to_front,
			select_after_open,
		} => {
			let message = PortfolioMessage::OpenDocumentFileWithId {
				document_id: id,
				document_name: Some(document.name),
				document_path: document.path,
				document_serialized_content: document.content,
				document_is_auto_saved: true,
				document_is_saved: document.is_saved,
				to_front,
				select_after_open,
			};
			dispatcher.queue_editor_message(message.into());
		}
		DesktopWrapperMessage::SelectDocument { id } => {
			let message = PortfolioMessage::SelectDocument { document_id: id };
			dispatcher.queue_editor_message(message.into());
		}
	}
}
Repository: GraphiteEditor/Graphite
File: editor/src/messages/frontend/frontend_message.rs (Rust)

Code before:
	TriggerSaveDocument {
		document_id: DocumentId,
		name: String,
		path: Option<PathBuf>,
		content: Vec<u8>,
	},
	TriggerSaveFile {
		name: String,
		content: Vec<u8>,
	},
	TriggerExportImage {
		svg: String,
		name: String,
		mime: String,
		size: (f64, f64),
	},
	#[cfg(feature = "gpu")]
	#[serde(skip)]
	TriggerExportImageBuffer {
		data: Vec<u8>,
		width: u32,
		height: u32,
		transparent: bool,
		file_type: super::utility_types::FileType,
		name: String,
	},
	TriggerFetchAndOpenDocument {
		name: String,
		filename: String,
	},
	TriggerFontLoad {
		font: Font,
	},
	TriggerImport,
	TriggerPersistenceRemoveDocument {
		#[serde(rename = "documentId")]
		document_id: DocumentId,
	},
	TriggerPersistenceWriteDocument {
		#[serde(rename = "documentId")]
		document_id: DocumentId,
		document: String,
		details: DocumentDetails,
	},
	TriggerLoadFirstAutoSaveDocument,
	TriggerLoadRestAutoSaveDocuments,
	TriggerLoadPreferences,
	TriggerOpenDocument,
	TriggerPaste,
	TriggerSavePreferences {
		preferences: PreferencesMessageHandler,

Review comment (refactor):
Hmm, I'm wondering if this is the correct abstraction level. I'd prefer if the encoding was handled in the editor crate (e.g. when we add quality options etc. it would make it easier to keep them in sync between frontend and desktop)

Code after:
	TriggerSaveDocument {
		document_id: DocumentId,
		name: String,
		path: Option<PathBuf>,
		content: Vec<u8>,
	},
	TriggerSaveFile {
		name: String,
		content: Vec<u8>,
	},
	TriggerExportImage {
		svg: String,
		name: String,
		mime: String,
		size: (f64, f64),
	},
	TriggerFetchAndOpenDocument {
		name: String,
		filename: String,
	},
	TriggerFontLoad {
		font: Font,
	},
	TriggerImport,
	TriggerPersistenceRemoveDocument {
		#[serde(rename = "documentId")]
		document_id: DocumentId,
	},
	TriggerPersistenceWriteDocument {
		#[serde(rename = "documentId")]
		document_id: DocumentId,
		document: String,
		details: DocumentDetails,
	},
	TriggerLoadFirstAutoSaveDocument,
	TriggerLoadRestAutoSaveDocuments,
	TriggerLoadPreferences,
	TriggerOpenDocument,
	TriggerPaste,
	TriggerSavePreferences {
		preferences: PreferencesMessageHandler,
	},
	TriggerSaveActiveDocument {
		#[serde(rename = "documentId")]
		document_id: DocumentId,
	},
	TriggerTextCommit,
	TriggerTextCopy {
		#[serde(rename = "copyText")]
		copy_text: String,
	},
Repository: GraphiteEditor/Graphite
File: .github/workflows/scripts/artifact-upload.bash (Shell)

Code before:
#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<EOF
Usage: $0 <owner> <repo> <branch> <target-path> <artifact-file> <commit-message> <github-token>

Arguments:
  owner           : GitHub user or organization of the target repo
  repo            : Target repo name
  branch          : Branch name (e.g. main)
  target-path     : Full path (including folders + filename) in the target repo where to upload
  artifact-file   : Local file path to upload
  commit-message  : Commit message for creating/updating the file
  github-token    : GitHub token (PAT or equivalent) with write access to the target repo

This will perform a GitHub API PUT to /repos/{owner}/{repo}/contents/{target-path}.
If a file already exists at that path, it will auto-detect the SHA and update; otherwise it will create.
EOF
  exit 1
}

if [ $# -ne 7 ]; then
  usage
fi

OWNER="$1"
REPO="$2"
BRANCH="$3"
TARGET_PATH="$4"
ARTIFACT_PATH="$5"
COMMIT_MSG="$6"
TOKEN="$7"

if [ ! -f "$ARTIFACT_PATH" ]; then
  echo "Error: artifact file not found: $ARTIFACT_PATH" >&2
  exit 1
fi

LOCAL_SHA=$(git hash-object "$ARTIFACT_PATH")
echo "Local blob SHA: $LOCAL_SHA"

GET_URL="https://api.github.com/repos/${OWNER}/${REPO}/contents/${TARGET_PATH}?ref=${BRANCH}"

Review comment (suggestion):
```suggestion
If a file already exists at that path, it will auto-detect the SHA and update; otherwise it will create a new one.
```

Code after:
#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<EOF
Usage: $0 <owner> <repo> <branch> <target-path> <artifact-file> <commit-message> <github-token>

Arguments:
  owner           : GitHub user or organization of the target repo
  repo            : Target repo name
  branch          : Branch name (e.g. main)
  target-path     : Full path (including folders + filename) in the target repo where to upload
  artifact-file   : Local file path to upload
  commit-message  : Commit message for creating/updating the file
  github-token    : GitHub token (PAT or equivalent) with write access to the target repo

This will perform a GitHub API PUT to /repos/{owner}/{repo}/contents/{target-path}.
If a file already exists at that path, it will auto-detect the SHA and update; otherwise it will create a new one.
EOF
  exit 1
}

if [ $# -ne 7 ]; then
  usage
fi

OWNER="$1"
REPO="$2"
BRANCH="$3"
TARGET_PATH="$4"
ARTIFACT_PATH="$5"
COMMIT_MSG="$6"
TOKEN="$7"

if [ ! -f "$ARTIFACT_PATH" ]; then
  echo "Error: artifact file not found: $ARTIFACT_PATH" >&2
  exit 1
fi

LOCAL_SHA=$(git hash-object "$ARTIFACT_PATH")
echo "Local blob SHA: $LOCAL_SHA"

GET_URL="https://api.github.com/repos/${OWNER}/${REPO}/contents/${TARGET_PATH}?ref=${BRANCH}"
Repository: GraphiteEditor/Graphite
File: node-graph/libraries/vector-types/src/vector/misc.rs (Rust)

Code before:

	fn tangent_at_end(&self) -> DVec2 {
		self.tangent_at(1.0)
	}
}

impl Tangent for kurbo::Line {
	fn tangent_at(&self, _t: f64) -> DVec2 {
		self.tangent_at_start()
	}

	fn tangent_at_start(&self) -> DVec2 {
		let delta = self.p1 - self.p0;
		DVec2 { x: delta.x, y: delta.y }
	}

	fn tangent_at_end(&self) -> DVec2 {
		self.tangent_at_start()
	}
}

impl Tangent for kurbo::QuadBez {
	fn tangent_at(&self, t: f64) -> DVec2 {
		let deriv = self.deriv();
		let tangent = deriv.eval(t);
		DVec2 { x: tangent.x, y: tangent.y }
	}

	fn tangent_at_start(&self) -> DVec2 {
		if self.p0 == self.p1 { self.deriv().tangent_at_start() } else { self.tangent_at(0.0) }
	}

	fn tangent_at_end(&self) -> DVec2 {
		if self.p2 == self.p1 { self.deriv().tangent_at_end() } else { self.tangent_at(1.0) }
	}
}

impl Tangent for kurbo::CubicBez {
	fn tangent_at(&self, t: f64) -> DVec2 {
		let deriv = self.deriv();
		let tangent = deriv.eval(t);
		DVec2 { x: tangent.x, y: tangent.y }
	}

	fn tangent_at_start(&self) -> DVec2 {
		if self.p0 == self.p1 { self.deriv().tangent_at_start() } else { self.tangent_at(0.0) }
	}

	fn tangent_at_end(&self) -> DVec2 {
		if self.p3 == self.p2 { self.deriv().tangent_at_end() } else { self.tangent_at(1.0) }
	}

Review comment (suggestion):
If the handle is placed on top of the anchor then this will be zero.

For reference, here is my impl of the tangent at start which attempts to avoid reporting zero length tangents:
https://github.com/GraphiteEditor/Graphite/blob/06749ecea52f3085903bca5a0b242f3a83f9538c/node-graph/libraries/vector-types/src/vector/algorithms/bezpath_algorithms.rs#L704-L724

Code after:

	fn tangent_at_end(&self) -> DVec2 {
		self.tangent_at(1.0)
	}
}

trait ControlPoints {
	type Points: AsRef<[Point]>;
	fn control_points(&self) -> Self::Points;
}

impl ControlPoints for kurbo::Line {
	type Points = [Point; 2];
	fn control_points(&self) -> Self::Points {
		[self.p0, self.p1]
	}
}

impl ControlPoints for kurbo::QuadBez {
	type Points = [Point; 3];
	fn control_points(&self) -> Self::Points {
		[self.p0, self.p1, self.p2]
	}
}

impl ControlPoints for kurbo::CubicBez {
	type Points = [Point; 4];
	fn control_points(&self) -> Self::Points {
		[self.p0, self.p1, self.p2, self.p3]
	}
}

impl<T: ControlPoints + ParamCurveDeriv> Tangent for T {
	fn tangent_at(&self, t: f64) -> DVec2 {
		point_to_dvec2(self.deriv().eval(t))
	}

	fn tangent_at_start(&self) -> DVec2 {
		let pts = self.control_points();
		let pts = pts.as_ref();
		let mut iter = pts.iter();
		iter.next()
			.and_then(|&start| iter.find(|&&p| p != start).map(|&p| DVec2 { x: p.x - start.x, y: p.y - start.y }))
			.unwrap_or_default()
	}

	fn tangent_at_end(&self) -> DVec2 {
		let pts = self.control_points();
		let pts = pts.as_ref();
		let mut iter = pts.iter().rev();
		iter.next()
Repository: GraphiteEditor/Graphite
File: node-graph/nodes/vector/src/generator_nodes.rs (Rust)

Code before:

/// Generates an n-pointed star shape with inner and outer points at chosen radii from the center.
#[node_macro::node(category("Vector: Shape"))]
fn star<T: AsU64>(
	_: impl Ctx,
	_primary: (),
	#[default(5)]
	#[hard_min(2.)]
	#[implementations(u32, u64, f64)]
	sides: T,
	#[unit(" px")]
	#[default(50)]
	radius_1: f64,
	#[unit(" px")]
	#[default(25)]
	radius_2: f64,
) -> Table<Vector> {
	let points = sides.as_u64();
	let diameter: f64 = radius_1 * 2.;
	let inner_diameter = radius_2 * 2.;

	Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter)))
}

/// Generates a QR code from the input text.
#[node_macro::node(category("Vector: Shape"))]
fn qr_code(
	_: impl Ctx,
	_primary: (),
	#[default("https://graphite.art")] text: String,
	#[default(true)]
	#[name("Merge Adjacent Tiles")]
	merge: bool,
) -> Table<Vector> {
	let ecc = qrcodegen::QrCodeEcc::Medium;

	let Ok(qr_code) = qrcodegen::QrCode::encode_text(&text, ecc) else {
		return Table::default();
	};

	let size = qr_code.size();
	let mut vector = Vector::default();
	let offset = DVec2::splat(0.0);

	if merge {
		use std::collections::{HashMap, HashSet, VecDeque};

		let mut remaining: HashSet<(i32, i32)> = HashSet::new();
		for y in 0..size {
			for x in 0..size {
				if qr_code.get_module(x, y) {

Review comment (suggestion):
```suggestion
#[node_macro::node(category("Vector: Shape"), name("QR Code"))]
```

Code after:

/// Generates an n-pointed star shape with inner and outer points at chosen radii from the center.
#[node_macro::node(category("Vector: Shape"))]
fn star<T: AsU64>(
	_: impl Ctx,
	_primary: (),
	#[default(5)]
	#[hard_min(2.)]
	#[implementations(u32, u64, f64)]
	sides: T,
	#[unit(" px")]
	#[default(50)]
	radius_1: f64,
	#[unit(" px")]
	#[default(25)]
	radius_2: f64,
) -> Table<Vector> {
	let points = sides.as_u64();
	let diameter: f64 = radius_1 * 2.;
	let inner_diameter = radius_2 * 2.;

	Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter)))
}

/// Generates a QR code from the input text.
#[node_macro::node(category("Vector: Shape"), name("QR Code"))]
fn qr_code(
	_: impl Ctx,
	_primary: (),
	#[default("https://graphite.art")] text: String,
	/// Error correction level, from low (0) to high (3).
	#[default(1)]
	error_correction: u32,
	#[default(false)] individual_squares: bool,
) -> Table<Vector> {
	let ecc = match error_correction.min(3) {
		0 => qrcodegen::QrCodeEcc::Low,
		1 => qrcodegen::QrCodeEcc::Medium,
		2 => qrcodegen::QrCodeEcc::Quartile,
		3 => qrcodegen::QrCodeEcc::High,
		_ => unreachable!(),
	};

	let Ok(qr_code) = qrcodegen::QrCode::encode_text(&text, ecc) else {
		return Table::default();
	};

	let size = qr_code.size() as usize;
	let mut vector = Vector::default();

	if individual_squares {
Repository: GraphiteEditor/Graphite
File: editor/src/messages/tool/tool_messages/shape_tool.rs (Rust)

Code before:
	vertices: u32,
	shape_type: ShapeType,
	arc_type: ArcType,
	grid_type: GridType,
	spiral_type: SpiralType,
	turns: f64,
	arrow_shaft_width: f64,
	arrow_head_width: f64,
	arrow_head_length: f64,
}

impl Default for ShapeToolOptions {
	fn default() -> Self {
		Self {
			line_weight: DEFAULT_STROKE_WIDTH,
			fill: ToolColorOptions::new_secondary(),
			stroke: ToolColorOptions::new_primary(),
			vertices: 5,
			shape_type: ShapeType::Polygon,
			arc_type: ArcType::Open,
			spiral_type: SpiralType::Archimedean,
			turns: 5.,
			grid_type: GridType::Rectangular,
			arrow_shaft_width: 14.0,
			arrow_head_width: 32.0,
			arrow_head_length: 28.0,
		}
	}
}

#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ShapeOptionsUpdate {
	FillColor(Option<Color>),
	FillColorType(ToolColorType),
	LineWeight(f64),
	StrokeColor(Option<Color>),
	StrokeColorType(ToolColorType),
	WorkingColors(Option<Color>, Option<Color>),
	Vertices(u32),
	ShapeType(ShapeType),
	ArcType(ArcType),
	SpiralType(SpiralType),
	Turns(f64),
	GridType(GridType),
	ArrowShaftWidth(f64),
	ArrowHeadWidth(f64),
	ArrowHeadLength(f64),
}

#[impl_message(Message, ToolMessage, Shape)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]

Review comment (suggestion):
```suggestion
			arrow_shaft_width: 14.,
			arrow_head_width: 32.,
			arrow_head_length: 28.,
```

Code after:
	vertices: u32,
	shape_type: ShapeType,
	arc_type: ArcType,
	grid_type: GridType,
	spiral_type: SpiralType,
	turns: f64,
	arrow_shaft_width: f64,
	arrow_head_width: f64,
	arrow_head_length: f64,
}

impl Default for ShapeToolOptions {
	fn default() -> Self {
		Self {
			line_weight: DEFAULT_STROKE_WIDTH,
			fill: ToolColorOptions::new_secondary(),
			stroke: ToolColorOptions::new_primary(),
			vertices: 5,
			shape_type: ShapeType::Polygon,
			arc_type: ArcType::Open,
			spiral_type: SpiralType::Archimedean,
			turns: 5.,
			grid_type: GridType::Rectangular,
			arrow_shaft_width: 14.,
			arrow_head_width: 32.,
			arrow_head_length: 28.,
		}
	}
}

#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ShapeOptionsUpdate {
	FillColor(Option<Color>),
	FillColorType(ToolColorType),
	LineWeight(f64),
	StrokeColor(Option<Color>),
	StrokeColorType(ToolColorType),
	WorkingColors(Option<Color>, Option<Color>),
	Vertices(u32),
	ShapeType(ShapeType),
	ArcType(ArcType),
	SpiralType(SpiralType),
	Turns(f64),
	GridType(GridType),
	ArrowShaftWidth(f64),
	ArrowHeadWidth(f64),
	ArrowHeadLength(f64),
}

#[impl_message(Message, ToolMessage, Shape)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]