{"record":{"id":"df9709159e62d2c7","repo":"GraphiteEditor/Graphite","slug":"no-handle-position","errorCode":null,"errorMessage":"No handle position","messagePattern":"No handle position","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"editor/src/messages/tool/common_functionality/shape_editor.rs","lineNumber":1162,"sourceCode":"\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 {\n\t\t\t\t\t\t\tManipulatorPointId::EndHandle(segment) => HandleId::end(segment),\n\t\t\t\t\t\t\tManipulatorPointId::PrimaryHandle(segment) => HandleId::primary(segment),\n\t\t\t\t\t\t\tManipulatorPointId::Anchor(_) => unreachable!(),","sourceCodeStart":1144,"sourceCodeEnd":1180,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/editor/src/messages/tool/common_functionality/shape_editor.rs#L1144-L1180","documentation":"The same comparator as error 122 also unwraps the current handle's own position with point.get_position(&vector).expect(\"No handle position\") (shape_editor.rs:1162). get_position (vector-types/src/vector/misc.rs:449) resolves PrimaryHandle/EndHandle via vector.segment_from_id(id).and_then(|bezier| bezier.handle_start()/handle_end()) — it is None when the SegmentId is gone from segment_domain or the bezier reports no distinct handle for that end. Because the comparator runs for every pair of candidates, one stale or handle-less segment among the selection is enough to panic.","triggerScenarios":"Sorting non-colinear handles for the opposite-handle behavior when the dragged handle's own segment no longer exists in the recomputed vector, or when its bezier has no stored start/end handle (e.g. segment reduced to a line after colinear-handle operations).","commonSituations":"Dragging a handle right after an undo that removed its segment; converting segments between curve types while selected; fuzz/automation sequences issuing drags between graph mutations.","solutions":["Pre-filter non_colinear_handles (and bail out early if the current point's own position is None) before entering sort_by","Use a let-else returning Ordering::Equal for missing positions so stale ids are skipped, not fatal","Refresh or prune selection state from compute_modified_vector before the interaction that triggers the sort"],"exampleFix":"// before\nlet orig_handle_pos = point.get_position(&vector).expect(\"No handle position\");\n// after\nlet Some(orig_handle_pos) = point.get_position(&vector) else {\n\treturn std::cmp::Ordering::Equal; // no distinct handle position; keep current order\n};","handlingStrategy":"validation","validationCode":"// Early-exit when the dragged handle itself cannot be positioned:\nlet (Some(_anchor), Some(orig_handle_pos)) = (point.get_anchor_position(&vector), point.get_position(&vector)) else {\n\treturn None; // stale or handle-less segment; skip opposite-handle logic\n};","typeGuard":"fn is_positionable(point: &ManipulatorPointId, vector: &Vector) -> bool {\n\tpoint.get_position(vector).is_some()\n}","tryCatchPattern":null,"preventionTips":["Filter handles by get_position().is_some() before entering the comparator","Treat segment/handle ids as ephemeral across graph recomputations; re-derive selection after any vector mutation","Write comparator fallbacks as Ordering::Equal, not unwrap/expect, so partial data degrades silently instead of crashing"],"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"}