{"record":{"id":"487551eb03a341d4","repo":"GraphiteEditor/Graphite","slug":"no-anchor-position-for-handle","errorCode":null,"errorMessage":"No anchor position for handle","messagePattern":"No anchor position for handle","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"editor/src/messages/tool/common_functionality/shape_editor.rs","lineNumber":1159,"sourceCode":"\t\t\t\tif let ManipulatorPointId::Anchor(anchor) = point\n\t\t\t\t\t&& vector.all_connected(anchor).count() > 2\n\t\t\t\t{\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Here we take handles as the current handle and the most opposite non-colinear-handle\n\n\t\t\t\tlet is_handle_colinear = |handle: HandleId| -> bool { vector.colinear_manipulators.iter().any(|&handles| handles[0] == handle || handles[1] == handle) };\n\n\t\t\t\tlet other_handles = if matches!(point, ManipulatorPointId::Anchor(_)) {\n\t\t\t\t\tpoint.get_handle_pair(&vector)\n\t\t\t\t} else {\n\t\t\t\t\tpoint.get_all_connected_handles(&vector).and_then(|handles| {\n\t\t\t\t\t\tlet mut non_colinear_handles = handles.iter().filter(|&handle| !is_handle_colinear(*handle)).clone().collect::<Vec<_>>();\n\n\t\t\t\t\t\t// Sort these by angle from the current handle\n\t\t\t\t\t\tnon_colinear_handles.sort_by(|&handle_a, &handle_b| {\n\t\t\t\t\t\t\tlet anchor = point.get_anchor_position(&vector).expect(\"No anchor position for handle\");\n\t\t\t\t\t\t\tlet orig_handle_pos = point.get_position(&vector).expect(\"No handle position\");\n\n\t\t\t\t\t\t\tlet a_pos = handle_a.to_manipulator_point().get_position(&vector).expect(\"No handle position\");\n\t\t\t\t\t\t\tlet b_pos = handle_b.to_manipulator_point().get_position(&vector).expect(\"No handle position\");\n\n\t\t\t\t\t\t\tlet v_orig = (orig_handle_pos - anchor).normalize_or_zero();\n\n\t\t\t\t\t\t\tlet v_a = (a_pos - anchor).normalize_or_zero();\n\t\t\t\t\t\t\tlet v_b = (b_pos - anchor).normalize_or_zero();\n\n\t\t\t\t\t\t\tlet angle_a = v_orig.angle_to(v_a).abs();\n\t\t\t\t\t\t\tlet angle_b = v_orig.angle_to(v_b).abs();\n\n\t\t\t\t\t\t\t// Sort by descending angle (180° is furthest)\n\t\t\t\t\t\t\tangle_b.partial_cmp(&angle_a).unwrap_or(std::cmp::Ordering::Equal)\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tlet current = match point {","sourceCodeStart":1141,"sourceCodeEnd":1177,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/editor/src/messages/tool/common_functionality/shape_editor.rs#L1141-L1177","documentation":"In the shape editor's opposite-handle pick logic, candidate handles are sorted by angle around the current handle's anchor, and the sort comparator calls point.get_anchor_position(&vector).expect(\"No anchor position for handle\") (shape_editor.rs:1159). get_anchor_position (vector-types/src/vector/misc.rs:457) chains segment-domain lookups with point_domain.position_from_id, all Options — it returns None when the selected handle's SegmentId, or the anchor PointId it maps to, no longer resolves in the freshly computed VectorData. The expect panics whenever selection state references manipulators the current vector does not contain.","triggerScenarios":"Drag-selecting or dragging handles in the path editor on a layer whose vector changed after selected_shape_state was populated (undo, upstream node edit, layer restructure), or a selected handle whose segment start/end point is missing from point_domain, so get_all_connected_handles returns candidates the comparator cannot position.","commonSituations":"Undo or rapid tool switching while handles are selected; scripted test sequences that mutate the graph between selection and drag; degenerate geometry where points were deleted but the selection retained their ids; upstream generator nodes changing segment topology between frames.","solutions":["Filter candidates before sorting: retain only handles h where h.to_manipulator_point().get_position(&vector).is_some() and the current point's anchor resolves","Replace the expects inside the comparator with a let-else returning std::cmp::Ordering::Equal so the comparator stays total and non-panicking on stale ids","Revalidate selected_shape_state against compute_modified_vector output at interaction start and drop manipulator ids that no longer resolve","Add a regression test: select a handle, delete its anchor via the graph, then drag — it must not panic"],"exampleFix":"// before (inside sort_by comparator)\nlet anchor = point.get_anchor_position(&vector).expect(\"No anchor position for handle\");\nlet orig_handle_pos = point.get_position(&vector).expect(\"No handle position\");\n// after\nlet (Some(anchor), Some(orig_handle_pos)) = (point.get_anchor_position(&vector), point.get_position(&vector)) else {\n\treturn std::cmp::Ordering::Equal; // stale manipulator id: skip instead of panicking\n};","handlingStrategy":"validation","validationCode":"// Before sorting candidates, keep only resolvable ones:\nnon_colinear_handles.retain(|&h| h.to_manipulator_point().get_position(&vector).is_some());\nif point.get_anchor_position(&vector).is_none() { return None; } // bail: current handle is stale","typeGuard":"fn has_resolvable_geometry(point: &ManipulatorPointId, vector: &Vector) -> bool {\n\tpoint.get_position(vector).is_some() && point.get_anchor_position(vector).is_some()\n}","tryCatchPattern":null,"preventionTips":["Never call expect inside a sort_by comparator — comparators must be total and panic-free on any input","Reconcile selected_shape_state against compute_modified_vector output at interaction start and drop ids that no longer resolve","Cover undo-during-selection and mutate-then-drag sequences in tests for the path editor"],"tags":["rust","graphite","panic","expect","shape-editor","bezier-handles","sort-comparator","stale-state"],"backgroundTag":"manipulator-position-lookup-failed","analyzedSha":"c507b356453361e31638b8bff8f6d46b6da2961e","analyzedAt":"2026-08-16T21:57:18.596Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}