{"record":{"id":"37385f0498d7d230","repo":"GraphiteEditor/Graphite","slug":"cannot-find-connected-anchor","errorCode":null,"errorMessage":"Cannot find connected anchor","messagePattern":"Cannot find connected anchor","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"editor/src/messages/tool/tool_messages/path_tool.rs","lineNumber":663,"sourceCode":"\t\tlet document_to_viewport = metadata.document_to_viewport;\n\t\tlet previous_mouse = document_to_viewport.transform_point2(self.previous_mouse_position);\n\t\tif previous_mouse == self.drag_start_pos {\n\t\t\tlet tolerance = DVec2::splat(SELECTION_TOLERANCE);\n\t\t\t[self.drag_start_pos - tolerance, self.drag_start_pos + tolerance]\n\t\t} else {\n\t\t\t[self.drag_start_pos, previous_mouse]\n\t\t}\n\t}\n\n\tfn update_selection_status(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler) {\n\t\tlet selection_status = get_selection_status(&document.network_interface, shape_editor);\n\n\t\tself.can_toggle_colinearity = match &selection_status {\n\t\t\tSelectionStatus::None => false,\n\t\t\tSelectionStatus::One(single_selected_point) => {\n\t\t\t\tlet vector = document.network_interface.compute_modified_vector(single_selected_point.layer).unwrap();\n\t\t\t\tif single_selected_point.id.get_handle_pair(&vector).is_some() {\n\t\t\t\t\tlet anchor = single_selected_point.id.get_anchor(&vector).expect(\"Cannot find connected anchor\");\n\t\t\t\t\tvector.all_connected(anchor).count() <= 2\n\t\t\t\t} else {\n\t\t\t\t\tfalse\n\t\t\t\t}\n\t\t\t}\n\t\t\tSelectionStatus::Multiple(_) => true,\n\t\t};\n\t\tself.selection_status = selection_status;\n\t}\n\n\tfn remove_saved_points(&mut self) {\n\t\tself.saved_points_before_anchor_select_toggle.clear();\n\t}\n\n\tfn reset_drill_through_cycle(&mut self) {\n\t\tself.last_drill_through_click_position = None;\n\t\tself.drill_through_cycle_index = 0;\n\t}","sourceCodeStart":645,"sourceCodeEnd":681,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/editor/src/messages/tool/tool_messages/path_tool.rs#L645-L681","documentation":"update_selection_status decides whether the colinearity toggle applies to the single selected point: it first checks that the point has a handle pair (get_handle_pair is Some) and then expects get_anchor to return the connected anchor. The expect encodes the invariant that a point which owns handles in the computed vector always has a resolvable anchor. It fails when the PointId refers to handles whose anchor is no longer in the freshly computed (modified) vector — typically because the vector changed between the selection being built and this lookup, or the selection holds stale ids after edits/undo removed the anchor.","triggerScenarios":"In the path tool, selecting exactly one handle/point of a bezier path (SelectionStatus::One) so that compute_modified_vector runs and get_handle_pair(&vector).is_some() passes, while single_selected_point.id.get_anchor(&vector) returns None — e.g., after the anchor was deleted in the same transaction, or the shape state was carried over from a previous vector version.","commonSituations":"Undo/redo sequences that leave ShapeState with ids from an older vector; plugins or scripted edits that mutate vector points without notifying the shape editor; racing edits where the document graph re-executes between selection and status update; corrupted path documents where handles exist without anchors.","solutions":["Replace the expect with if let Some(anchor) = ... and treat None as 'colinearity toggle unavailable' (set can_toggle_colinearity = false).","Before acting on the selection, validate stored point ids against the current vector (e.g., filter selected ids through the computed vector's points) and drop stale ones.","Recompute the vector immediately before both lookups so get_handle_pair and get_anchor observe the same data.","If the panic reproduces from a saved document, minimize the file and check whether the shape's handles reference an anchor index past the anchor count."],"exampleFix":"// before\nif single_selected_point.id.get_handle_pair(&vector).is_some() {\n\tlet anchor = single_selected_point.id.get_anchor(&vector).expect(\"Cannot find connected anchor\");\n\tvector.all_connected(anchor).count() <= 2\n}\n\n// after\nsingle_selected_point\n\t.id\n\t.get_handle_pair(&vector)\n\t.and_then(|_| single_selected_point.id.get_anchor(&vector))\n\t.map(|anchor| vector.all_connected(anchor).count() <= 2)\n\t.unwrap_or(false)","handlingStrategy":"validation","validationCode":"// Validate the selected point against the freshly computed vector before use\nlet vector = document.network_interface.compute_modified_vector(single_selected_point.layer).unwrap();\nlet can_toggle = single_selected_point\n\t.id\n\t.get_handle_pair(&vector)\n\t.and_then(|_| single_selected_point.id.get_anchor(&vector))\n\t.map(|anchor| vector.all_connected(anchor).count() <= 2)\n\t.unwrap_or(false);","typeGuard":"fn point_has_valid_anchor(vector: &VectorData, point: &PointId) -> bool {\n\tpoint.get_handle_pair(vector).is_some() && point.get_anchor(vector).is_some()\n}","tryCatchPattern":null,"preventionTips":["Treat any 'lookup succeeded then second lookup expected' pattern as a bug risk: chain both lookups and default on None.","Recompute the modified vector immediately before reading handles/anchors so both checks see the same data.","Filter shape-editor selection ids through the current vector after undo/redo or structural edits."],"tags":["rust","graphite-editor","path-tool","stale-id","invariant-violation","expect-panic","bezier"],"backgroundTag":"stale-id-lookup","analyzedSha":"c507b356453361e31638b8bff8f6d46b6da2961e","analyzedAt":"2026-08-16T21:57:18.596Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}