{"record":{"id":"001a209142853ddb","repo":"GraphiteEditor/Graphite","slug":"in-check-layer-there-should-be-a-target","errorCode":null,"errorMessage":"In `check_layer()`: there should be a `target`","messagePattern":"In `check_layer\\(\\)`: there should be a `target`","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"editor/src/messages/portfolio/document/document_message_handler.rs","lineNumber":3879,"sourceCode":"\t\t\t} else {\n\t\t\t\t// All child layers will use the new clipped target area\n\t\t\t\tself.parent_targets.push((layer, XRayTarget::Path(subtracted)));\n\t\t\t}\n\t\t}\n\t\tXRayResult { clicked, use_children }\n\t}\n\n\t/// Handles the checking of the layer to find if it has been clicked\n\tfn check_layer(&mut self, layer: LayerNodeIdentifier) -> XRayResult {\n\t\tlet selected_layers = self.network_interface.selected_nodes();\n\t\t// Discard invisible and locked layers\n\t\tif !selected_layers.layer_visible(layer, self.network_interface) || selected_layers.layer_locked(layer, self.network_interface) {\n\t\t\treturn XRayResult { clicked: false, use_children: false };\n\t\t}\n\n\t\tlet click_targets = self.network_interface.document_metadata().click_targets(layer);\n\t\tlet transform = self.network_interface.document_metadata().transform_to_document(layer);\n\t\tlet target = &self.parent_targets.last().expect(\"In `check_layer()`: there should be a `target`\").1;\n\t\tlet clip = self.network_interface.document_metadata().is_clip(layer.to_node());\n\n\t\tmatch target {\n\t\t\t// Single points are much cheaper than paths so have their own special case\n\t\t\tXRayTarget::Point(point) => {\n\t\t\t\tlet intersects = click_targets.is_some_and(|targets| targets.iter().any(|target| target.intersect_point(*point, transform)));\n\t\t\t\tXRayResult {\n\t\t\t\t\tclicked: intersects,\n\t\t\t\t\tuse_children: !clip || intersects,\n\t\t\t\t}\n\t\t\t}\n\t\t\tXRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_kurbo(*quad), transform),\n\t\t\tXRayTarget::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform),\n\t\t\tXRayTarget::Polygon(polygon) => {\n\t\t\t\tlet polygon = BezPath::from_path_segments(polygon.iter_closed());\n\t\t\t\tself.check_layer_area_target(click_targets, clip, layer, polygon, transform)\n\t\t\t}\n\t\t}","sourceCodeStart":3861,"sourceCodeEnd":3897,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/editor/src/messages/portfolio/document/document_message_handler.rs#L3861-L3897","documentation":"Panics in `check_layer` when `self.parent_targets` is empty at `.last().expect(...)`. `parent_targets` is a traversal stack seeded by the click-xray entry point (`calculate_click` initializes it with `(ROOT_PARENT, target)` at document_message_handler.rs:3834) and pushed/popped while walking child layers (3863, 3968-3969). The expect encodes the invariant that `check_layer` is only ever called inside an active xray traversal where at least the root target entry exists.","triggerScenarios":"`check_layer` runs during click hit-testing (`XRayMessage`/`calculate_click` handling). The panic occurs when the stack is empty: a code path calls `check_layer` directly without starting a traversal, or push/pop bookkeeping over-pops (an ancestor-pop check at line 3968 mismatches the layer actually being exited, draining the seeded ROOT_PARENT entry).","commonSituations":"Adding a new message handler that invokes `check_layer` outside the standard traversal; refactoring the traversal so the initial `(ROOT_PARENT, target)` seed is skipped; a mismatch between the layer passed to `calculate_click`'s recursive walk and the ancestor comparison used for popping (e.g. after layer reparenting mid-traversal).","solutions":["Trace the call path: ensure `check_layer` is only reached via the traversal that seeds `parent_targets` with `(LayerNodeIdentifier::ROOT_PARENT, target)` at line 3834.","Audit the pop logic at lines 3968-3969 — the ancestor comparison must pop exactly the entries the walk pushed; fix the comparison if traversal order or layer identity changed.","If you added a new entry point that hit-tests layers, route it through `calculate_click`/`calculate_click_x_y_layer` instead of calling `check_layer` directly.","Replace the `expect` with a `let-else` that logs and returns `XRayResult { clicked: false, use_children: false }` so a bookkeeping bug cannot hard-crash the editor."],"exampleFix":"// before\nlet target = &self.parent_targets.last().expect(\"In `check_layer()`: there should be a `target`\").1;\n\n// after\nlet Some((_, target)) = self.parent_targets.last() else {\n    log::error!(\"check_layer called with empty parent_targets; no active xray traversal\");\n    return XRayResult { clicked: false, use_children: false };\n};","handlingStrategy":"validation","validationCode":"// Guard before hit-testing a layer:\nif self.parent_targets.is_empty() {\n    log::error!(\"check_layer invoked outside an xray traversal\");\n    return XRayResult { clicked: false, use_children: false };\n}","typeGuard":"fn has_xray_target(parent_targets: &[(LayerNodeIdentifier, XRayTarget)]) -> bool {\n    !parent_targets.is_empty()\n}","tryCatchPattern":"// Rust has no try/catch; use catch_unwind only to isolate editor crashes:\nlet result = std::panic::catch_unwind(|| self.check_layer(layer))\n);\nlet xray = result.unwrap_or(XRayResult { clicked: false, use_children: false });","preventionTips":["Only enter check_layer through the traversal that seeds parent_targets with (ROOT_PARENT, target)","Keep the debug_assert that parent_targets is empty after traversal completes (line 3978) enabled in CI","When modifying the pop logic, assert stack depth matches traversal depth each frame","Never call check_layer from new message handlers; route through calculate_click instead"],"tags":["rust","panic","expect","invariant","hit-testing","xray","stack-underflow","graphite"],"backgroundTag":"invariant-stack-empty","analyzedSha":"c507b356453361e31638b8bff8f6d46b6da2961e","analyzedAt":"2026-08-16T21:57:18.596Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}