use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use bone_document::{
    BrepSolid, ContourSelection, DatumFeature, DatumGeometry, DimensionValue, Document,
    DocumentFolder, EditOutcome, EvaluatedExtrude, EvaluatedModel, ExtrudeError, ExtrudeFeature,
    ExtrudeOperation, FeatureNode, LineData, PlaneRecipe, PrincipalPlane, ProjectedCurve,
    RebuildBudget, RebuildCost, RebuildPass, RecomputeScope, RevolveAxis, RevolveFeature, Sketch,
    Sketch3, Sketch3Entity, Sketch3Relation, SketchDimension, SketchEdit, SketchEditError,
    SketchEntity, SketchPlaneRef, SketchRelation, SketchVersion, SolverError, SweepAlignment,
    SweepFeature, SweepPath, SweepProfileKind, UndoStack, evaluate_extrude_reaching,
    evaluate_revolve_reaching, evaluate_sketch,
};
use bone_render::{
    BodyKey, Camera2, CameraTween, ChromeInstance, ChromePipeline, ChromeTextPipeline,
    ConvexInstance, ConvexPolyPipeline, DragModifiers, EdgeScene, EdgeStroke, EntityKindTag,
    GenuineEdge, Gpu, IconInstance, IconPipeline, NavGesture, PickId, PickIdError, PickIndex,
    PickQuery, PickedItem, Picker, PixelsPerMm, PreviewArc, PreviewCircle, RenderTargets,
    SdfGlyphInstance, SketchPreview, SketchRenderer, SketchScene, SolidEdgeRef, SolidFaceRef,
    SolidFrameView, SolidPickTable, SolidRenderer, SolidScene, SolidVertexRef, StrokeInstance,
    StrokePipeline, Style, ViewportExtent, ViewportNavigator, ViewportPoint, ViewportPx,
    ViewportRegion, frame_current, frame_standard_view, frame_view_direction, orbit_pitch,
    orbit_yaw, pan_pixels, roll_by, single_body_pick, zoom_about_pixel,
};
use bone_types::{
    Aabb3, Angle, AngleTolerance, BodyOrdinal, BodyRef, BrepFaceId, BudgetCeiling, Camera3,
    ChordHeightTolerance, CreaseAngle, CubicEasing, DatumId, DatumTarget, DisplayMode, DocumentId,
    EdgeLabel, ExtrudeId, FaceLabel, FeatureId, GeometryGeneration, Length, LinearRgba, Plane3,
    Point2, Point3, Projection, ProjectionKind, RebuildError, RebuildStatus, Sketch3EntityRef,
    SketchEntityId, SketchId, SketchItemId, SketchPlaneBasis, StandardView, UnitVec3, Vec2,
    VertexLabel, ZoomFactor,
};
use bone_ui::a11y::AccessTreeBuilder;
use bone_ui::focus::FocusManager;
use bone_ui::frame::FrameCtx;
use bone_ui::hit_test::{HitFrame, HitState, resolve};
use bone_ui::hotkey::{ActionId, HotkeyScope, HotkeyScopes, HotkeyTable};
use bone_ui::input::{
    FrameInstant, InputSnapshot, KeyCode as UiKeyCode, KeyEvent as UiKeyEvent, ModifierMask,
    NamedKey, PointerButton, PointerButtonMask, PointerSample,
};
use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use bone_ui::strings::StringTable;
use bone_ui::theme::Theme;
use bone_ui::widgets::{LabelText, TreeBadge};
use bone_ui::{MaskAtlas, MaskAtlasParams, Shaper};
use swash::FontRef;
use uom::si::angle::{degree, radian};
use uom::si::length::millimeter;

use crate::clock::FrameClock;
use crate::dimension_editor::{
    DimensionEditorAction, DimensionEditorOutcome, DimensionEditorState,
};
use crate::input::{InputDispatched, InputEvent, KeyDown, NavKey, ScrollDelta, WindowPoint};
use crate::reference_planes::{self, DatumPlaneInput, PlaneLabel, ReferencePlaneScene};
use crate::selection::Selection;
use crate::sketch_mode::{
    ClickAnchor, DatumArming, DimensionFlow, DragPins, DragSession, EndConditionKind,
    ExtrudeArming, FeatureTool, Mode, ModeExit, Pending, PendingDimension, Plane, ReferenceKind,
    RevolveArming, SketchPlaneSource, SketchTool, Spatial, SweepArming,
};
use crate::snap::{Anchor, SnapHit};
use crate::status_badge::ExtrudeStatus;
use crate::{
    chrome, dimension_editor, file_menu, hotkeys, native_picker, selection, settings, shell,
    shortcut_bar, sketch_mode, sketch3_mode, sketch3_project, smart_dimension, snap, step_jobs,
    strings, tools, view_cube,
};
const ZOOM_STEP_PER_LINE: f64 = 1.1;
const ZOOM_STEP_PER_PIXEL: f64 = 1.0025;
const ZOOM_KEY_STEP: f64 = 1.25;
const ORBIT_KEY_STEP_DEG: f64 = 15.0;
const ORBIT_KEY_SNAP_DEG: f64 = 90.0;
const ZOOM_MIN: f64 = 0.01;
const ZOOM_MAX: f64 = 1.0e5;
const INITIAL_ZOOM_PX_PER_MM: f64 = 12.0;
const PAN_STEP_PX: f64 = 40.0;
const PAN_FAST_MULTIPLIER: f64 = 5.0;
const ZOOM_FIT_MARGIN: f64 = 0.9;
const UNDO_CAPACITY: usize = 256;
const SNAP_TOLERANCE_PX: f64 = 8.0;
const SNAP_TOLERANCE_MAX_MM: f64 = 5.0;

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum RevolveArm {
    Idle,
    Axis,
    Contour,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum SweepArm {
    Idle,
    Path,
    Guide(shell::SweepGuideSlot),
    AlignmentAxis,
    ToolBody,
}

struct SketchDraft {
    sketch: SketchId,
    before: Document,
}

struct AppState {
    extent: ViewportExtent,
    renderer: SketchRenderer,
    chrome_pipeline: ChromePipeline,
    convex_pipeline: ConvexPolyPipeline,
    stroke_pipeline: StrokePipeline,
    icon_pipeline: IconPipeline,
    text_pipeline: ChromeTextPipeline,
    sdf_atlas: MaskAtlas,
    chrome_shaper: Shaper,
    sans_font: FontRef<'static>,
    mono_font: FontRef<'static>,
    scene: SketchScene,
    camera: Camera2,
    style: Style,
    theme: Arc<Theme>,
    shell: shell::Shell,
    document: Document,
    mode: Mode,
    extrude_preview: Option<ExtrudePreview>,
    model: EvaluatedModel,
    model_passes: Vec<(FeatureId, RebuildPass)>,
    mesh_quality: MeshQuality,
    changed_features: BTreeSet<FeatureId>,
    needs_rebuild: bool,
    pending_reattach: Option<SketchId>,
    pending_sketch_draft: Option<SketchDraft>,
    extrude_reference_arm: Option<shell::ReferenceSlot>,
    revolve_arm: RevolveArm,
    sweep_arm: SweepArm,
    brep_selection: Vec<PickedItem>,
    hidden_planes: BTreeSet<FeatureId>,
    planes_visible: bool,
    solid_renderer: SolidRenderer,
    solid_view: Option<SolidViewData>,
    camera3: Option<Camera3>,
    framed_extrude: Option<ExtrudeId>,
    navigator: ViewportNavigator,
    view: view_cube::ViewUi,
    focus: FocusManager,
    hit_state: HitState,
    hotkeys: HotkeyTable,
    strings: StringTable,
    viewport_rect: LayoutRect,
    undo: UndoStack,
    selection: Selection,
    settings: settings::Settings,
    dim_editor: DimensionEditorState,
    dim_editor_bounds: Option<LayoutRect>,
    pending_exit: bool,
    current_folder: Option<DocumentFolder>,
    documents_root: PathBuf,
    file_picker: Option<file_menu::FilePickerSession>,
    native_picker: Option<native_picker::PendingHandle>,
    step_job: Option<step_jobs::StepJob>,
    pending_overwrite: Option<PendingOverwrite>,
    last_saved: Option<Document>,
    pending_discard: Option<PendingDiscard>,
    notification: Option<Notification>,
    shortcut_bar: Option<shortcut_bar::ShortcutBarState>,
}

#[derive(Clone, Debug, PartialEq)]
enum PendingDiscard {
    New,
    Open(PathBuf),
    ImportStep(PathBuf),
    InstallImported {
        document: Box<Document>,
        file_name: String,
    },
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum PickedVia {
    NativePortal,
    CustomPicker,
}

#[derive(Clone, Debug, PartialEq)]
enum PendingOverwrite {
    Document(DocumentFolder),
    StepExport(PathBuf),
}

fn modal_active(state: &AppState) -> bool {
    state.file_picker.is_some()
        || state.native_picker.is_some()
        || state
            .step_job
            .as_ref()
            .is_some_and(|job| job.meta().show_progress)
        || state.pending_overwrite.is_some()
        || state.pending_discard.is_some()
        || state.shortcut_bar.is_some()
        || state.shell.state.ribbon_overflow_open.values().any(|v| *v)
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum NotificationKind {
    Info,
    Error,
}

#[derive(Clone, Debug, PartialEq)]
struct Notification {
    kind: NotificationKind,
    headline: bone_ui::strings::StringKey,
    detail: Option<String>,
}

struct InputState {
    cursor_px: Option<WindowPoint>,
    left_pan: bool,
    middle_pan: bool,
    modifiers: ModifierMask,
    pending_pressed: PointerButtonMask,
    pending_released: PointerButtonMask,
    pending_keys: Vec<UiKeyEvent>,
    pending_text: String,
    pending_scroll_y: f32,
}

impl Default for InputState {
    fn default() -> Self {
        Self {
            cursor_px: None,
            left_pan: false,
            middle_pan: false,
            modifiers: ModifierMask::NONE,
            pending_pressed: PointerButtonMask::EMPTY,
            pending_released: PointerButtonMask::EMPTY,
            pending_keys: Vec::new(),
            pending_text: String::new(),
            pending_scroll_y: 0.0,
        }
    }
}

impl InputState {
    fn shift(&self) -> bool {
        self.modifiers.contains(ModifierMask::SHIFT)
    }

    fn ctrl_or_meta(&self) -> bool {
        self.modifiers.contains(ModifierMask::CTRL) || self.modifiers.contains(ModifierMask::META)
    }

    fn panning(&self) -> bool {
        self.middle_pan || (self.left_pan && self.shift())
    }

    fn pan_step_px(&self) -> f64 {
        if self.shift() {
            PAN_STEP_PX * PAN_FAST_MULTIPLIER
        } else {
            PAN_STEP_PX
        }
    }

    fn pointer_sample(&self) -> Option<PointerSample> {
        self.cursor_px
            .map(window_to_layout_pos)
            .map(PointerSample::new)
    }

    fn cursor_in(&self, rect: LayoutRect) -> bool {
        self.cursor_px
            .map(window_to_layout_pos)
            .is_some_and(|p| rect.contains(p))
    }

    fn drain_snapshot(&mut self, now: FrameInstant) -> InputSnapshot {
        let mut snap = InputSnapshot::idle(now);
        snap.pointer = self.pointer_sample();
        snap.buttons_pressed =
            core::mem::replace(&mut self.pending_pressed, PointerButtonMask::EMPTY);
        snap.buttons_released =
            core::mem::replace(&mut self.pending_released, PointerButtonMask::EMPTY);
        snap.keys_pressed = core::mem::take(&mut self.pending_keys);
        snap.text_committed = core::mem::take(&mut self.pending_text);
        snap.modifiers = self.modifiers;
        snap.scroll_y = core::mem::replace(&mut self.pending_scroll_y, 0.0);
        snap
    }

    fn forget_pan_state(&mut self) {
        self.cursor_px = None;
        self.left_pan = false;
        self.middle_pan = false;
        self.modifiers = ModifierMask::NONE;
    }
}

#[allow(
    clippy::cast_possible_truncation,
    reason = "window cursor px (f64) collapses to LayoutPx (f32) at the sub-pixel limit"
)]
fn window_to_layout_pos(p: WindowPoint) -> LayoutPos {
    LayoutPos::new(
        LayoutPx::saturating(p.x as f32),
        LayoutPx::saturating(p.y as f32),
    )
}

pub trait FrameTarget {
    fn picker(&self, index: PickIndex) -> Picker<'_>;
    fn render(
        &mut self,
        build_passes: impl FnOnce(
            &mut wgpu::CommandEncoder,
            &wgpu::TextureView,
            &wgpu::TextureView,
            &wgpu::TextureView,
        ),
    );
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[must_use]
pub struct FrameReport {
    pub kick: bool,
    pub exit: bool,
}

#[derive(Debug, thiserror::Error)]
pub enum OpenError {
    #[error("import step {path}: {source}")]
    ImportStep {
        path: PathBuf,
        #[source]
        source: bone_interop::StepError,
    },
    #[error("load document folder {path}: {source}")]
    LoadFolder {
        path: PathBuf,
        #[source]
        source: bone_document::FolderError,
    },
}

pub struct AppCore {
    state: AppState,
    input: InputState,
    clock: FrameClock,
    a11y: AccessTreeBuilder,
}

fn default_sketch() -> Sketch {
    Sketch::new(Plane::Xy.basis())
}

#[cfg(test)]
fn initial_document(sketch: &Sketch) -> (Document, SketchId) {
    let mut document = Document::new(DocumentId::default(), "Untitled".to_owned());
    let sketch_id = SketchId::default();
    document.insert_sketch(
        sketch_id,
        "Sketch1".to_owned(),
        sketch3_project::embed_sketch(sketch),
    );
    (document, sketch_id)
}

fn empty_document() -> Document {
    Document::new(DocumentId::default(), "Untitled".to_owned())
}

#[allow(
    clippy::cast_precision_loss,
    reason = "viewport pixel counts at any realistic display size fit f32 mantissa"
)]
fn layout_size_from_extent(extent: ViewportExtent) -> LayoutSize {
    LayoutSize::new(
        LayoutPx::new(extent.width().value() as f32),
        LayoutPx::new(extent.height().value() as f32),
    )
}

fn empty_rect() -> LayoutRect {
    LayoutRect::new(LayoutPos::ORIGIN, LayoutSize::ZERO)
}

fn zoom_factor(delta: ScrollDelta) -> f64 {
    match delta {
        ScrollDelta::Lines { y, .. } => ZOOM_STEP_PER_LINE.powf(f64::from(y)),
        ScrollDelta::Pixels { y, .. } => ZOOM_STEP_PER_PIXEL.powf(y),
    }
}

const WHEEL_LINE_PX: f32 = 48.0;

fn wheel_offset_px(delta: ScrollDelta) -> f32 {
    match delta {
        ScrollDelta::Lines { y, .. } => -y * WHEEL_LINE_PX,
        #[allow(
            clippy::cast_possible_truncation,
            reason = "trackpad pixel delta collapses to f32 at the sub-pixel limit"
        )]
        ScrollDelta::Pixels { y, .. } => -(y as f32),
    }
}

fn zoom_about(camera: Camera2, cursor: Option<WindowPoint>, factor: f64) -> Camera2 {
    if !factor.is_finite() || factor <= 0.0 {
        return camera;
    }
    let zoom_before = camera.zoom().value();
    let zoom_after = (zoom_before * factor).clamp(ZOOM_MIN, ZOOM_MAX);
    if !zoom_after.is_finite() {
        return camera;
    }
    let extent = camera.extent();
    let w = f64::from(extent.width().value());
    let h = f64::from(extent.height().value());
    let (pan_x, pan_y) = camera.pan_mm().coords_mm();
    let (cursor_x, cursor_y) = cursor.map_or((w * 0.5, h * 0.5), |c| (c.x, c.y));
    let horizontal_px = cursor_x - w * 0.5;
    let vertical_px = h * 0.5 - cursor_y;
    let world_x = pan_x + horizontal_px / zoom_before;
    let world_y = pan_y + vertical_px / zoom_before;
    let new_pan_x = world_x - horizontal_px / zoom_after;
    let new_pan_y = world_y - vertical_px / zoom_after;
    camera
        .with_zoom(PixelsPerMm::new(zoom_after))
        .with_pan(Vec2::from_mm(new_pan_x, new_pan_y))
}

fn dragging_in_sketch(mode: &Mode) -> bool {
    matches!(
        mode,
        Mode::Sketch { session, .. } if session.drag.is_some()
    )
}

fn dim_flow_active(mode: &Mode) -> bool {
    matches!(
        mode,
        Mode::Sketch { session, .. } if session.dim_flow.is_some()
    )
}

fn cursor_to_world(camera: Camera2, cursor: WindowPoint) -> Option<Point2> {
    let extent = camera.extent();
    let w = f64::from(extent.width().value());
    let h = f64::from(extent.height().value());
    let zoom = camera.zoom().value();
    if w <= 0.0 || h <= 0.0 || !zoom.is_finite() || zoom <= 0.0 {
        return None;
    }
    let (pan_x, pan_y) = camera.pan_mm().coords_mm();
    Some(Point2::from_mm(
        pan_x + (cursor.x - w * 0.5) / zoom,
        pan_y + (h * 0.5 - cursor.y) / zoom,
    ))
}

fn try_place(state: &mut AppState, world: Point2) {
    let Mode::Sketch { sketch_id, session } = &state.mode else {
        return;
    };
    let Some(tool) = session.tool else {
        return;
    };
    let prev_pending = session.pending;
    let sketch_id = *sketch_id;
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let basis = base.plane();
    let view = sketch3_project::project(base, basis);
    let snap = snap_tolerance(&state.camera)
        .and_then(|tol| tool_snap(view.sketch(), tool, prev_pending, world, tol));
    if tool == SketchTool::Spline {
        let existing = match snap {
            Some(SnapHit {
                kind: snap::SnapKind::Endpoint(id2),
                ..
            }) => view.to_3d(id2),
            _ => None,
        };
        let (next, draft_point) = if let Some(id) = existing {
            (base.clone(), sketch_mode::SplineDraftPoint::reused(id))
        } else {
            let (next, id) = sketch3_project::embed_point(base, basis, world);
            (next, sketch_mode::SplineDraftPoint::fresh(id))
        };
        push_spline_draft_point(state, sketch_id, next, draft_point);
        return;
    }
    let placed = sketch3_project::embed_place(
        base,
        &view,
        tool,
        world,
        prev_pending,
        snap,
        sketch3_project::PlaneLock::Locked,
    );
    let changed = placed.sketch != *base;
    commit_placement(state, sketch_id, placed, changed);
}

fn commit_placement(
    state: &mut AppState,
    sketch_id: SketchId,
    placed: sketch3_project::Placed3,
    changed: bool,
) {
    if changed {
        record_sketch_edit(state, sketch_id);
        state
            .document
            .replace_sketch(sketch_id, sketch3_mode::solved(placed.sketch));
        refresh_active_scene(state);
    }
    if let Mode::Sketch {
        ref mut session, ..
    } = state.mode
    {
        session.pending = placed.pending;
    }
}

const fn latest_anchor(pending: Option<Pending>) -> Option<ClickAnchor> {
    match pending {
        None => None,
        Some(Pending::First(a) | Pending::Second(_, a)) => Some(a),
    }
}

fn pending_anchor_world(base: &bone_document::Sketch3, pending: Option<Pending>) -> Option<Point3> {
    match latest_anchor(pending)? {
        ClickAnchor::Endpoint(id) => base.point_position(id),
        ClickAnchor::Position(_) | ClickAnchor::MidpointOf { .. } | ClickAnchor::OnEdge { .. } => {
            None
        }
    }
}

fn reorigin_through(basis: SketchPlaneBasis, anchor: Point3) -> SketchPlaneBasis {
    let (nx, ny, nz) = basis.normal().components();
    let (ox, oy, oz) = basis.origin().coords_mm();
    let (ax, ay, az) = anchor.coords_mm();
    let depth = (ax - ox).mul_add(nx, (ay - oy).mul_add(ny, (az - oz) * nz));
    let origin = Point3::from_mm(
        nx.mul_add(depth, ox),
        ny.mul_add(depth, oy),
        nz.mul_add(depth, oz),
    );
    SketchPlaneBasis::new(
        origin,
        basis.x_axis(),
        basis.y_axis(),
        bone_types::Tolerance::new(1.0e-9),
    )
    .unwrap_or(basis)
}

fn sketch3_aid_basis(
    base: &bone_document::Sketch3,
    basis: SketchPlaneBasis,
    pending: Option<Pending>,
    session_anchor: Option<Point3>,
) -> SketchPlaneBasis {
    if pending.is_none() {
        return basis;
    }
    pending_anchor_world(base, pending)
        .or(session_anchor)
        .map_or(basis, |anchor| reorigin_through(basis, anchor))
}

fn body_key(feature: FeatureId, ordinal: BodyOrdinal) -> BodyKey {
    BodyKey::new(feature, ordinal)
}

fn solid_pick_table(model: &EvaluatedModel) -> Option<SolidPickTable> {
    let table = model
        .bodies()
        .try_fold(
            SolidPickTable::new(),
            |mut table, (feature, ordinal, solid)| {
                table
                    .add_body(
                        body_key(feature, ordinal),
                        solid.iter_faces().map(bone_document::BrepFace::id),
                        solid.iter_edges().map(bone_document::BrepEdge::id),
                        solid.iter_vertices().map(bone_document::BrepVertex::id),
                    )
                    .map(|()| table)
            },
        )
        .ok()?;
    (!table.is_empty()).then_some(table)
}

fn solid_pick_index(model: &EvaluatedModel) -> Option<PickIndex> {
    solid_pick_table(model).map(|table| table.pick_index())
}

fn sketch3_pick_index(state: &AppState) -> Option<PickIndex> {
    let sketch3_id = state.mode.sketch3_id()?;
    let sketch = state.document.sketch3(sketch3_id)?;
    let entities = sketch.entity_order().iter().filter_map(|id| {
        let tag = match sketch.entities().get(*id)? {
            bone_document::Sketch3Entity::Point(_) => EntityKindTag::Point,
            bone_document::Sketch3Entity::Line(_) => EntityKindTag::Line,
            bone_document::Sketch3Entity::Circle(_) => EntityKindTag::Circle,
            bone_document::Sketch3Entity::Arc(_) => EntityKindTag::Arc,
            bone_document::Sketch3Entity::Ellipse(_) => EntityKindTag::Ellipse,
            bone_document::Sketch3Entity::Spline(_) => EntityKindTag::Spline,
        };
        Some((*id, tag))
    });
    let relations = sketch.relation_order().iter().copied();
    PickIndex::build(entities, relations, core::iter::empty()).ok()
}

fn active_pick_index(state: &AppState) -> Option<PickIndex> {
    if state.mode.is_sketch3() {
        return sketch3_pick_index(state);
    }
    let solid_mode = state.extrude_reference_arm.is_some()
        || state.revolve_arm == RevolveArm::Axis
        || matches!(
            state.sweep_arm,
            SweepArm::Path | SweepArm::Guide(_) | SweepArm::AlignmentAxis | SweepArm::ToolBody
        )
        || matches!(state.mode, Mode::Idle);
    if solid_mode && state.solid_view.is_some() {
        return solid_pick_index(&state.model);
    }
    let sketch_index = match state.scene.pick_index() {
        Ok(index) => index,
        Err(error) => {
            tracing::warn!(error = %error, "build sketch pick index");
            return None;
        }
    };
    if !(state.mode.is_sketch() && state.solid_view.is_some()) {
        return Some(sketch_index);
    }
    match solid_pick_table(&state.model) {
        Some(table) => Some(sketch_index.with_solid(&table)),
        None => Some(sketch_index),
    }
}

fn pick_at(state: &AppState, target: &impl FrameTarget, cursor: WindowPoint) -> Option<PickedItem> {
    if !cursor.x.is_finite() || !cursor.y.is_finite() || cursor.x < 0.0 || cursor.y < 0.0 {
        return None;
    }
    let extent = state.extent;
    #[allow(
        clippy::cast_possible_truncation,
        clippy::cast_sign_loss,
        reason = "cursor px is bounds-checked against surface extent before the cast"
    )]
    let qx = cursor.x.round() as u32;
    #[allow(
        clippy::cast_possible_truncation,
        clippy::cast_sign_loss,
        reason = "cursor px is bounds-checked against surface extent before the cast"
    )]
    let qy = cursor.y.round() as u32;
    if qx >= extent.width().value() || qy >= extent.height().value() {
        return None;
    }
    let index = active_pick_index(state)?;
    let query = PickQuery::new(ViewportPx::new(qx), ViewportPx::new(qy))
        .with_aperture(state.settings.pick_aperture);
    match target.picker(index).at(query) {
        Ok(item) => item,
        Err(e) => {
            tracing::warn!(error = %e, "pick failed");
            None
        }
    }
}

fn handle_viewport_click(
    state: &mut AppState,
    target: &impl FrameTarget,
    cursor: WindowPoint,
    additive: bool,
) {
    if state.mode.is_sketch3() && handle_sketch3_click(state, cursor) {
        return;
    }
    let picked = pick_at(state, target, cursor);
    if let Some(sketch) = state.pending_reattach {
        if let Some(PickedItem::BrepFace(face)) = picked {
            apply_reattach(state, sketch, face.face());
        }
        return;
    }
    if state.extrude_reference_arm.is_some() && capture_extrude_reference(state, picked) {
        return;
    }
    if state.revolve_arm == RevolveArm::Axis && capture_revolve_axis(state, picked) {
        return;
    }
    if state.revolve_arm == RevolveArm::Contour && capture_revolve_contour(state, picked) {
        return;
    }
    if state.sweep_arm != SweepArm::Idle && capture_sweep_rail(state, picked) {
        return;
    }
    if !additive
        && matches!(state.mode, Mode::Idle)
        && let Some(PickedItem::BrepFace(face)) = picked
        && begin_face_sketch(state, face.face())
    {
        return;
    }
    if state.mode.is_sketch()
        && let Some(brep @ (PickedItem::BrepEdge(_) | PickedItem::BrepFace(_))) = picked
    {
        toggle_brep_selection(state, brep, additive);
        return;
    }
    if !additive {
        state.brep_selection.clear();
    }
    let item = picked.and_then(selection::picked_to_item);
    state.selection = std::mem::take(&mut state.selection).picked(item, additive);
    if additive || !state.mode.is_sketch() {
        return;
    }
    let Some(SketchItemId::Entity(entity_id)) = item else {
        return;
    };
    let Mode::Sketch { sketch_id, session } = &state.mode else {
        return;
    };
    let sketch_id = *sketch_id;
    let spatial = session.spatial;
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let basis = spatial.map_or_else(|| base.plane(), |sp| sp.basis);
    let Some(entity3) = resolve_entity_3d(base, spatial.is_some(), entity_id) else {
        return;
    };
    let Some(pins) = DragPins::from_sketch3_entity(base, entity3) else {
        return;
    };
    let Some(world) = active_plane_cursor(state, spatial.is_some(), basis, cursor) else {
        return;
    };
    let drag = DragSession {
        entity: entity3,
        press: world,
        pins,
    };
    state.undo.record(state.document.clone());
    state.mode = core::mem::take(&mut state.mode).start_drag(drag);
}

fn resolve_entity_3d(
    base: &bone_document::Sketch3,
    in_3d: bool,
    entity: SketchEntityId,
) -> Option<SketchEntityId> {
    if in_3d {
        base.entities().contains_key(entity).then_some(entity)
    } else {
        base.project().to_3d(entity)
    }
}

fn toggle_brep_selection(state: &mut AppState, item: PickedItem, additive: bool) {
    if additive {
        match state.brep_selection.iter().position(|held| *held == item) {
            Some(index) => {
                state.brep_selection.remove(index);
            }
            None => state.brep_selection.push(item),
        }
    } else {
        state.brep_selection = vec![item];
        state.selection = Selection::default();
    }
}

fn active_plane_cursor(
    state: &AppState,
    in_3d: bool,
    basis: SketchPlaneBasis,
    cursor: WindowPoint,
) -> Option<Point2> {
    if in_3d {
        let camera = state.camera3?;
        let region = solid_viewport_region(state.viewport_rect, state.extent)?;
        let pixel = viewport_local_point(cursor, region)?;
        let world = sketch3_mode::ray_plane_point(camera, region.extent(), pixel, basis)?;
        Some(sketch3_project::flatten(basis, world))
    } else {
        cursor_to_world(state.camera, cursor)
    }
}

fn try_drag_to(state: &mut AppState, cursor: WindowPoint) {
    let Mode::Sketch { sketch_id, session } = &state.mode else {
        return;
    };
    let Some(drag) = &session.drag else {
        return;
    };
    let sketch_id = *sketch_id;
    let spatial = session.spatial;
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let basis = spatial.map_or_else(|| base.plane(), |sp| sp.basis);
    let Some(world) = active_plane_cursor(state, spatial.is_some(), basis, cursor) else {
        return;
    };
    let targets = drag.pins.to_targets(basis, drag.press, world);
    let Ok(next) = base
        .solve_with_drag_pins(&targets, BudgetCeiling::FRAME_16MS)
        .map_err(|e| tracing::trace!(error = %e, "drag solve failed, keeping last-good sketch"))
    else {
        return;
    };
    state.document.replace_sketch(sketch_id, next);
    refresh_active_scene(state);
}

fn refresh_active_scene(state: &mut AppState) {
    let Some(active_id) = active_sketch_id(&state.mode) else {
        return;
    };
    let Some(base) = state.document.sketch3(active_id) else {
        return;
    };
    let sketch = sketch3_project::project_sketch(base, base.plane());
    match SketchScene::extract(&sketch) {
        Ok(scene) => state.scene = scene,
        Err(e) => tracing::warn!(error = %e, "refresh active scene failed"),
    }
}

fn projected_id_map(
    base: &bone_document::Sketch3,
    view: &bone_document::ProjectedSketch,
) -> HashMap<bone_types::SketchEntityId, bone_types::SketchEntityId> {
    base.entity_order()
        .iter()
        .filter_map(|id3| view.to_2d(*id3).map(|id2| (id2, *id3)))
        .collect()
}

fn active_sketch_id(mode: &Mode) -> Option<SketchId> {
    match mode {
        Mode::Sketch { sketch_id, .. } => Some(*sketch_id),
        Mode::Extrude(ExtrudeArming::Profile { feature, .. }) => Some(feature.sketch),
        Mode::Revolve(RevolveArming::Profile { feature, .. }) => Some(feature.sketch),
        Mode::Sweep(SweepArming::Profile { feature, .. }) => Some(feature.profile),
        Mode::Extrude(ExtrudeArming::AwaitingSketch(_))
        | Mode::Revolve(RevolveArming::AwaitingSketch(_))
        | Mode::Sweep(SweepArming::AwaitingSketch(_))
        | Mode::Idle
        | Mode::Datum(_) => None,
    }
}

enum ProfileChoice {
    NoSketch,
    Unique(SketchId),
    Ambiguous,
}

fn unconsumed_profile_sketches(document: &Document) -> Vec<SketchId> {
    let tree = document.feature_tree();
    let consumed = |sketch: FeatureId| {
        tree.children(sketch)
            .into_iter()
            .filter_map(|child| tree.node(child))
            .any(|node| matches!(node, FeatureNode::Extrude(_)))
    };
    let has_geometry = |id: SketchId| {
        document
            .sketch3(id)
            .is_some_and(|sketch| !sketch.entity_order().is_empty())
    };
    tree.iter()
        .filter_map(|(feature, node)| match node {
            FeatureNode::Sketch(id) if !consumed(feature) && has_geometry(id) => Some(id),
            _ => None,
        })
        .collect()
}

fn classify_extrude_profile(document: &Document) -> ProfileChoice {
    let mut sketches = unconsumed_profile_sketches(document).into_iter();
    match (sketches.next(), sketches.next()) {
        (None, _) => ProfileChoice::NoSketch,
        (Some(id), None) => ProfileChoice::Unique(id),
        (Some(_), Some(_)) => ProfileChoice::Ambiguous,
    }
}

fn sole_other_profile_sketch(document: &Document, exclude: SketchId) -> Option<SketchId> {
    let mut others = unconsumed_profile_sketches(document)
        .into_iter()
        .filter(|&id| id != exclude);
    match (others.next(), others.next()) {
        (Some(only), None) => Some(only),
        _ => None,
    }
}

fn apply_feature_tool(state: &mut AppState, tool: Option<FeatureTool>) {
    let Some(tool) = tool else {
        return;
    };
    let lifecycle = state.mode.lifecycle();
    if lifecycle.exit == ModeExit::Toggle && lifecycle.button == Some(tool) {
        state.mode = Mode::Idle;
        return;
    }
    match tool {
        FeatureTool::ExtrudedBossBase => arm_extruded_boss_base(state),
        FeatureTool::ExtrudedCut => arm_extruded_cut(state),
        FeatureTool::RevolvedBossBase => arm_revolve(state, ExtrudeOperation::default()),
        FeatureTool::RevolvedCut => arm_revolve(state, ExtrudeOperation::Cut),
        FeatureTool::SweptBossBase => arm_sweep(state, ExtrudeOperation::default()),
        FeatureTool::SweptCut => arm_sweep(state, ExtrudeOperation::Cut),
        FeatureTool::Sketch3D => arm_sketch3(state),
        datum if datum.is_datum() => arm_datum(state, datum),
        _ => {}
    }
}

fn sketch3_tool_for_key(text: &str) -> Option<SketchTool> {
    SketchTool::ENTITIES
        .iter()
        .copied()
        .find(|tool| sketch3_tool_hotkey(*tool).is_some_and(|key| key.eq_ignore_ascii_case(text)))
}

fn sketch3_tool_hotkey(tool: SketchTool) -> Option<&'static str> {
    match tool {
        SketchTool::Point => Some("p"),
        SketchTool::Line => Some("l"),
        SketchTool::Centerline => Some("c"),
        SketchTool::Circle => Some("o"),
        SketchTool::CenterpointArc => Some("a"),
        SketchTool::Spline => Some("s"),
        SketchTool::TangentArc
        | SketchTool::ThreePointArc
        | SketchTool::PerimeterCircle
        | SketchTool::Ellipse
        | SketchTool::CornerRectangle
        | SketchTool::CenterRectangle
        | SketchTool::ThreePointCornerRectangle
        | SketchTool::ThreePointCenterRectangle
        | SketchTool::Parallelogram => None,
    }
}

const SKETCH3_SNAP_MM: f64 = 1.0;

fn handle_sketch3_click(state: &mut AppState, cursor: WindowPoint) -> bool {
    let Mode::Sketch { sketch_id, session } = &state.mode else {
        return false;
    };
    let Some(spatial) = session.spatial else {
        return false;
    };
    let Some(tool) = session.tool else {
        return false;
    };
    let sketch3_id = *sketch_id;
    let spatial_basis = spatial.basis;
    let pending = session.pending;
    let aid_anchor = session.aid_anchor;
    let Some(camera) = state.camera3 else {
        return false;
    };
    let Some(region) = solid_viewport_region(state.viewport_rect, state.extent) else {
        return false;
    };
    let Some(pixel) = viewport_local_point(cursor, region) else {
        return false;
    };
    let Some(base) = state.document.sketch3(sketch3_id) else {
        return false;
    };
    let basis = sketch3_aid_basis(base, spatial_basis, pending, aid_anchor);
    let Some(probe) = sketch3_probe(base, basis, camera, region, pixel, tool, pending) else {
        return false;
    };
    let placed_world = sketch3_project::lift(probe.basis, probe.effective);
    if tool == SketchTool::Spline {
        let existing = match probe.hit {
            Some(SnapHit {
                kind: snap::SnapKind::Endpoint(id2),
                ..
            }) => probe.view.to_3d(id2),
            _ => None,
        };
        commit_sketch3_spline_point(state, sketch3_id, placed_world, existing);
        state.mode = core::mem::take(&mut state.mode).with_aid_anchor(placed_world);
        return true;
    }
    let placed = sketch3_project::embed_place(
        base,
        &probe.view,
        tool,
        probe.effective,
        pending,
        probe.hit,
        sketch3_project::PlaneLock::Free,
    );
    let changed = placed.sketch != *base;
    commit_placement(state, sketch3_id, placed, changed);
    state.mode = core::mem::take(&mut state.mode).with_aid_anchor(placed_world);
    true
}

fn commit_sketch3_spline_point(
    state: &mut AppState,
    sketch3_id: SketchId,
    world: Point3,
    existing: Option<SketchEntityId>,
) {
    let Some(base) = state.document.sketch3(sketch3_id) else {
        return;
    };
    let (next, draft_point) = if let Some(id) = existing {
        (base.clone(), sketch_mode::SplineDraftPoint::reused(id))
    } else {
        let (next, id) = base.with_entity(bone_document::Sketch3Entity::point(world));
        (next, sketch_mode::SplineDraftPoint::fresh(id))
    };
    push_spline_draft_point(state, sketch3_id, next, draft_point);
}

fn push_spline_draft_point(
    state: &mut AppState,
    sketch3_id: SketchId,
    next: bone_document::Sketch3,
    draft_point: sketch_mode::SplineDraftPoint,
) {
    record_sketch_edit(state, sketch3_id);
    state
        .document
        .replace_sketch(sketch3_id, sketch3_mode::solved(next));
    state.mode = core::mem::take(&mut state.mode).with_spline_point(draft_point);
    refresh_active_scene(state);
}

fn dedup_spline_control(
    base: &bone_document::Sketch3,
    draft: &[sketch_mode::SplineDraftPoint],
) -> Vec<SketchEntityId> {
    draft.iter().fold(Vec::new(), |mut acc, point| {
        let coincident = acc.last().copied().is_some_and(|prev| {
            prev == point.id
                || match (base.point_position(prev), base.point_position(point.id)) {
                    (Some(a), Some(b)) => a == b,
                    _ => false,
                }
        });
        if !coincident {
            acc.push(point.id);
        }
        acc
    })
}

fn is_loose_point(sketch: &bone_document::Sketch3, id: SketchEntityId) -> bool {
    let is_point = matches!(
        sketch.entities().get(id),
        Some(bone_document::Sketch3Entity::Point(_))
    );
    is_point
        && sketch.entity_order().iter().all(|eid| {
            *eid == id
                || match sketch.entities().get(*eid) {
                    Some(entity) => !entity.references().into_iter().any(|r| r == id),
                    None => true,
                }
        })
}

fn delete_loose_points(state: &mut AppState, sketch_id: SketchId, ids: &[SketchEntityId]) {
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let next = ids
        .iter()
        .filter(|id| is_loose_point(base, **id))
        .fold(base.clone(), |acc, id| acc.without_entity(*id));
    let changed = next != *base;
    if !changed {
        return;
    }
    state.undo.record(state.document.clone());
    state
        .document
        .replace_sketch(sketch_id, sketch3_mode::solved(next));
    refresh_active_scene(state);
}

fn abandon_spline_draft(state: &mut AppState) {
    let Some(sketch_id) = state.mode.sketch_id() else {
        return;
    };
    let fresh = state.mode.spline_draft_fresh();
    delete_loose_points(state, sketch_id, &fresh);
}

fn reconcile_spline_draft(
    state: &mut AppState,
    sketch_id: Option<SketchId>,
    prev_fresh: &[SketchEntityId],
) {
    if prev_fresh.is_empty() {
        return;
    }
    let Some(sketch_id) = sketch_id else {
        return;
    };
    let still: HashSet<SketchEntityId> = state.mode.spline_draft().iter().map(|p| p.id).collect();
    let orphans: Vec<SketchEntityId> = prev_fresh
        .iter()
        .copied()
        .filter(|id| !still.contains(id))
        .collect();
    delete_loose_points(state, sketch_id, &orphans);
}

fn finalize_sketch3_spline(state: &mut AppState) {
    let Some(sketch_id) = state.mode.sketch_id() else {
        return;
    };
    let committed = state.document.sketch3(sketch_id).and_then(|base| {
        let control = dedup_spline_control(base, state.mode.spline_draft());
        if control.len() < 2 {
            return None;
        }
        let (next, _) =
            base.with_entity(bone_document::Sketch3Entity::spline(control, false, false));
        next.validate().is_ok().then_some(next)
    });
    match committed {
        Some(next) => {
            state.undo.record(state.document.clone());
            state
                .document
                .replace_sketch(sketch_id, sketch3_mode::solved(next));
        }
        None => abandon_spline_draft(state),
    }
    state.mode = core::mem::take(&mut state.mode).clear_spline_draft();
    refresh_active_scene(state);
}

struct Sketch3Probe {
    basis: SketchPlaneBasis,
    view: sketch3_project::PlaneView,
    hit: Option<SnapHit>,
    effective: Point2,
}

fn sketch3_snap_hit(
    snap: sketch3_mode::Snap3,
    view: &sketch3_project::PlaneView,
    world: Point2,
) -> Option<SnapHit> {
    let kind = match snap {
        sketch3_mode::Snap3::Origin { .. } => return None,
        sketch3_mode::Snap3::Endpoint { point, .. } => snap::SnapKind::Endpoint(view.to_2d(point)?),
        sketch3_mode::Snap3::Midpoint { line, .. } => snap::SnapKind::Midpoint(view.to_2d(line)?),
        sketch3_mode::Snap3::OnEdge { entity, .. } => snap::SnapKind::OnEdge(view.to_2d(entity)?),
    };
    Some(SnapHit { kind, world })
}

fn sketch3_probe(
    base: &bone_document::Sketch3,
    basis: SketchPlaneBasis,
    camera: Camera3,
    region: ViewportRegion,
    pixel: ViewportPoint,
    tool: SketchTool,
    pending: Option<sketch_mode::Pending>,
) -> Option<Sketch3Probe> {
    let tolerance = sketch3_snap_tolerance(camera, region)
        .unwrap_or_else(|| Length::new::<millimeter>(SKETCH3_SNAP_MM));
    let exclude = match latest_anchor(pending) {
        Some(ClickAnchor::Endpoint(id)) => Some(id),
        _ => None,
    };
    let reference = Some(base.plane().origin());
    if let Some(snap3) = sketch3_mode::nearest_snap3(
        base,
        camera,
        region.extent(),
        pixel,
        tolerance,
        exclude,
        reference,
    ) {
        let basis = reorigin_through(basis, snap3.at());
        let view = sketch3_project::project(base, basis);
        let effective = sketch3_project::flatten(basis, snap3.at());
        let hit = sketch3_snap_hit(snap3, &view, effective);
        return Some(Sketch3Probe {
            basis,
            view,
            hit,
            effective,
        });
    }
    let world = sketch3_mode::ray_plane_point(camera, region.extent(), pixel, basis)?;
    let cursor2d = sketch3_project::flatten(basis, world);
    let view = sketch3_project::project(base, basis);
    let pending2 = sketch3_project::to_plane_pending(&view, pending);
    let hit = tool_snap(view.sketch(), tool, pending2, cursor2d, tolerance);
    let effective = hit.map_or(cursor2d, |h| h.world);
    Some(Sketch3Probe {
        basis,
        view,
        hit,
        effective,
    })
}

const SKETCH3_CURSOR_MARKER_MM: f64 = 1.5;

fn preview_cursor_edges(at: Point3) -> [GenuineEdge; 3] {
    let (x, y, z) = at.coords_mm();
    let h = SKETCH3_CURSOR_MARKER_MM;
    let crease = CreaseAngle::from_dot(-1.0);
    let seg = |from: Point3, to: Point3| GenuineEdge::new(from, to, PickId::NONE, crease);
    [
        seg(Point3::from_mm(x - h, y, z), Point3::from_mm(x + h, y, z)),
        seg(Point3::from_mm(x, y - h, z), Point3::from_mm(x, y + h, z)),
        seg(Point3::from_mm(x, y, z - h), Point3::from_mm(x, y, z + h)),
    ]
}

fn sketch3_preview_edges(
    state: &AppState,
    region: ViewportRegion,
    cursor: Option<WindowPoint>,
) -> Option<EdgeScene> {
    let Mode::Sketch { sketch_id, session } = &state.mode else {
        return None;
    };
    let spatial = session.spatial?;
    let tool = session.tool?;
    let camera = state.camera3?;
    let pixel = viewport_local_point(cursor?, region)?;
    let base = state.document.sketch3(*sketch_id)?;
    let basis = sketch3_aid_basis(base, spatial.basis, session.pending, session.aid_anchor);
    let probe = sketch3_probe(base, basis, camera, region, pixel, tool, session.pending)?;
    if tool == SketchTool::Spline {
        let cursor3 = sketch3_project::lift(probe.basis, probe.effective);
        let mut positions: Vec<Point3> = session
            .spline_draft
            .iter()
            .filter_map(|point| base.point_position(point.id))
            .collect();
        positions.push(cursor3);
        let crease = CreaseAngle::from_dot(-1.0);
        let mut edges = if positions.len() >= 3 {
            polyline_edges(
                &sketch3_mode::spline3_polyline(&positions, false),
                PickId::NONE,
            )
        } else {
            positions
                .windows(2)
                .map(|w| GenuineEdge::new(w[0], w[1], PickId::NONE, crease))
                .collect()
        };
        edges.extend(preview_cursor_edges(cursor3));
        return (!edges.is_empty()).then(|| EdgeScene::from_genuine(edges));
    }
    let pending2 = sketch3_project::to_plane_pending(&probe.view, session.pending);
    let preview = tools::preview(
        probe.view.sketch(),
        tool,
        probe.effective,
        pending2,
        probe.hit,
    );
    let mut edges = preview_edges_3d(&preview, probe.basis);
    edges.extend(preview_cursor_edges(sketch3_project::lift(
        probe.basis,
        probe.effective,
    )));
    (!edges.is_empty()).then(|| EdgeScene::from_genuine(edges))
}

fn preview_edges_3d(preview: &SketchPreview, basis: SketchPlaneBasis) -> Vec<GenuineEdge> {
    let crease = CreaseAngle::from_dot(-1.0);
    let lift = |p: Point2| sketch3_project::lift(basis, p);
    let (u, v) = (basis.x_axis(), basis.y_axis());
    let circle_points = |c: &PreviewCircle| -> Vec<Point3> {
        let center = lift(c.center);
        let radius = c.radius.get::<millimeter>();
        (0..=sketch3_mode::SKETCH3_CURVE_SEGMENTS)
            .map(|i| {
                let angle = core::f64::consts::TAU * f64::from(i)
                    / f64::from(sketch3_mode::SKETCH3_CURVE_SEGMENTS);
                sketch3_mode::around(center, u, v, radius, radius, angle)
            })
            .collect()
    };
    let arc_points = |arc: &PreviewArc| -> Vec<Point3> {
        let center = lift(arc.center);
        let radius = arc.radius.get::<millimeter>();
        let start = arc.start_angle.get::<radian>();
        let sweep = arc.sweep_angle.get::<radian>();
        (0..=sketch3_mode::SKETCH3_CURVE_SEGMENTS)
            .map(|i| {
                let t = f64::from(i) / f64::from(sketch3_mode::SKETCH3_CURVE_SEGMENTS);
                sketch3_mode::around(center, u, v, radius, radius, sweep.mul_add(t, start))
            })
            .collect()
    };
    let segments = preview
        .segments
        .iter()
        .map(|(a, b)| GenuineEdge::new(lift(*a), lift(*b), PickId::NONE, crease));
    let circles = preview
        .circles
        .iter()
        .flat_map(|c| polyline_edges(&circle_points(c), PickId::NONE));
    let arcs = preview
        .arcs
        .iter()
        .flat_map(|a| polyline_edges(&arc_points(a), PickId::NONE));
    let anchors = preview
        .anchors
        .iter()
        .flat_map(|p| preview_cursor_edges(lift(*p)));
    segments.chain(circles).chain(arcs).chain(anchors).collect()
}

fn arm_sketch3(state: &mut AppState) {
    let before = state.document.clone();
    let id = state.document.allocate_sketch();
    state
        .document
        .insert_sketch(id, "3D Sketch".to_owned(), bone_document::Sketch3::new());
    state.mode = Mode::enter_sketch3(id, Spatial::principal(Plane::Xy));
    state.pending_sketch_draft = Some(SketchDraft { sketch: id, before });
    recompute_model(state, RecomputeScope::Full);
    rebuild_model_view(state);
}

fn cycle_sketch3_plane(state: &mut AppState) {
    state.mode = core::mem::take(&mut state.mode).cycle_sketch3_plane();
    let Some(plane) = state.mode.sketch3_plane() else {
        return;
    };
    notify_info(
        state,
        strings::NOTIFY_SKETCH3_PLANE,
        Some(plane.label().to_owned()),
    );
}

fn begin_plane_sketch(state: &mut AppState, plane: Plane, now: FrameInstant) {
    let principal = PrincipalPlane::from(plane);
    let Some(datum) = state
        .document
        .feature_tree()
        .principal_plane_feature(principal)
    else {
        return;
    };
    let before = state.document.clone();
    let sketch_id = state.document.allocate_sketch();
    state.document.insert_sketch(
        sketch_id,
        next_sketch_name(&state.document),
        Sketch3::on_plane(plane.basis()),
    );
    if let Err(error) = state.document.bind_sketch_to_datum_plane(sketch_id, datum) {
        tracing::warn!(?error, "failed to bind plane sketch to its datum");
    }
    state.pending_sketch_draft = Some(SketchDraft {
        sketch: sketch_id,
        before,
    });
    enter_plane_sketch_on(state, sketch_id, Spatial::principal(plane), now);
}

fn begin_datum_sketch(state: &mut AppState, feature: FeatureId, now: FrameInstant) -> bool {
    let Some(DatumGeometry::Plane(plane)) = state.model.datum(feature) else {
        return false;
    };
    let basis = SketchPlaneBasis::from(plane);
    let before = state.document.clone();
    let sketch_id = state.document.allocate_sketch();
    state.document.insert_sketch(
        sketch_id,
        next_sketch_name(&state.document),
        Sketch3::on_plane(basis),
    );
    if let Err(error) = state
        .document
        .bind_sketch_to_datum_plane(sketch_id, feature)
    {
        tracing::warn!(?error, "failed to bind sketch to its datum plane");
    }
    state.pending_sketch_draft = Some(SketchDraft {
        sketch: sketch_id,
        before,
    });
    enter_plane_sketch_on(state, sketch_id, Spatial::datum(feature, basis), now);
    true
}

fn edit_existing_sketch(state: &mut AppState, sketch_id: SketchId, now: FrameInstant) -> bool {
    let Some(base) = state.document.sketch3(sketch_id) else {
        return false;
    };
    let spatial = match state.document.sketch_plane_binding(sketch_id) {
        Some(SketchPlaneRef::Datum(datum)) => {
            let source = principal_plane_of_datum(&state.document, datum)
                .map_or(SketchPlaneSource::Datum(datum), |principal| {
                    SketchPlaneSource::Principal(Plane::from(principal))
                });
            Spatial {
                basis: base.plane(),
                source,
            }
        }
        None => Spatial {
            basis: base.plane(),
            source: SketchPlaneSource::Principal(Plane::Xy),
        },
        Some(SketchPlaneRef::Face(_)) => return false,
    };
    enter_plane_sketch_on(state, sketch_id, spatial, now);
    true
}

fn enter_plane_sketch_on(
    state: &mut AppState,
    sketch_id: SketchId,
    spatial: Spatial,
    now: FrameInstant,
) {
    state.mode = Mode::enter_sketch3(sketch_id, spatial);
    aim_camera_normal_to(state, Plane3::from(spatial.basis), now);
}

fn principal_plane_of_datum(document: &Document, datum: FeatureId) -> Option<PrincipalPlane> {
    let tree = document.feature_tree();
    [PrincipalPlane::Xy, PrincipalPlane::Yz, PrincipalPlane::Zx]
        .into_iter()
        .find(|kind| tree.principal_plane_feature(*kind) == Some(datum))
}

fn draft_sketch(state: &AppState) -> Option<SketchId> {
    state
        .pending_sketch_draft
        .as_ref()
        .map(|draft| draft.sketch)
}

fn record_sketch_edit(state: &mut AppState, sketch_id: SketchId) {
    match state.pending_sketch_draft.take() {
        Some(draft) if draft.sketch == sketch_id => state.undo.record(draft.before),
        restored => {
            state.pending_sketch_draft = restored;
            state.undo.record(state.document.clone());
        }
    }
}

fn commit_pending_sketch_draft(state: &mut AppState) {
    let Some(sketch) = draft_sketch(state) else {
        return;
    };
    let has_geometry = state
        .document
        .sketch3(sketch)
        .is_some_and(|sketch| !sketch.entity_order().is_empty());
    if has_geometry && let Some(draft) = state.pending_sketch_draft.take() {
        state.undo.record(draft.before);
    }
}

fn gc_abandoned_sketch(state: &mut AppState, entered_from: Option<SketchId>) {
    let Some(id) = entered_from else {
        return;
    };
    if state.mode.sketch3_id() == Some(id) {
        return;
    }
    let empty = state
        .document
        .sketch3(id)
        .is_some_and(|sketch| sketch.entity_order().is_empty());
    if empty {
        state.document.remove_sketch(id);
    }
    if draft_sketch(state) == Some(id) {
        state.pending_sketch_draft = None;
    }
}

fn next_sketch_name(document: &Document) -> String {
    let next = document
        .sketches()
        .filter_map(|(id, _)| document.sketch_label(id))
        .filter_map(|label| label.strip_prefix("Sketch"))
        .filter_map(|suffix| suffix.parse::<usize>().ok())
        .max()
        .map_or(1, |highest| highest + 1);
    format!("Sketch{next}")
}

fn aim_camera_normal_to(state: &mut AppState, plane3: Plane3, now: FrameInstant) {
    let Some(region) = solid_viewport_region(state.viewport_rect, state.extent) else {
        return;
    };
    let half = reference_planes::plane_half_extent(state.solid_view.as_ref().map(|view| view.aabb));
    let Some(scene) =
        reference_planes::build(&state.document, &[], Some(plane3), None, half, |_| true)
    else {
        return;
    };
    if let Ok(camera) = frame_standard_view(
        scene.aabb,
        region.extent(),
        StandardView::NormalTo,
        Some(plane3),
    ) {
        start_view_tween(state, camera, now);
    }
}

fn line_endpoint_pair(
    sketch: &Sketch3,
    selected: &[SketchEntityId],
) -> Option<(SketchEntityId, SketchEntityId)> {
    let line_ends = |id: SketchEntityId| match sketch.entities().get(id) {
        Some(Sketch3Entity::Line(line)) => Some((line.a(), line.b())),
        _ => None,
    };
    let explicit = selected.iter().copied().find_map(|line| {
        let (a, b) = line_ends(line)?;
        selected
            .iter()
            .copied()
            .find(|&other| other == a || other == b)
            .map(|point| (line, point))
    });
    if let Some(pair) = explicit {
        return Some(pair);
    }
    let point = selected
        .iter()
        .copied()
        .find(|&id| matches!(sketch.entities().get(id), Some(Sketch3Entity::Point(_))))?;
    let mut incident = sketch
        .entity_order()
        .iter()
        .copied()
        .filter(|&id| line_ends(id).is_some_and(|(a, b)| a == point || b == point));
    let line = incident.next()?;
    incident.next().is_none().then_some((line, point))
}

pub(crate) fn normal_to_curve_datum(
    document: &Document,
    mode: &Mode,
    selection: &[SketchEntityId],
) -> Option<DatumFeature> {
    let sketch_id = mode.sketch3_id()?;
    let sketch_feature = document.feature_tree().feature_of_sketch(sketch_id)?;
    let sketch = document.sketch3(sketch_id)?;
    let (line, point) = line_endpoint_pair(sketch, selection)?;
    Some(DatumFeature::plane(PlaneRecipe::NormalToCurve {
        curve: DatumTarget::Sketch3Entity(Sketch3EntityRef::new(sketch_feature, line)),
        point: DatumTarget::Sketch3Entity(Sketch3EntityRef::new(sketch_feature, point)),
        set_origin_on_curve: true,
    }))
}

fn normal_to_curve_from_selection(state: &AppState) -> Option<DatumFeature> {
    normal_to_curve_datum(&state.document, &state.mode, state.selection.entity_ids())
}

fn arm_datum(state: &mut AppState, tool: FeatureTool) {
    let normal_to_curve = normal_to_curve_from_selection(state);
    let tree = state.document.feature_tree();
    let origin = tree.origin_feature();
    let xy = tree.principal_plane_feature(PrincipalPlane::Xy);
    let yz = tree.principal_plane_feature(PrincipalPlane::Yz);
    let zx = tree.principal_plane_feature(PrincipalPlane::Zx);
    let feature = match tool {
        FeatureTool::DatumPlane => {
            normal_to_curve.or_else(|| xy.map(sketch_mode::default_datum_plane))
        }
        FeatureTool::DatumAxis => xy
            .zip(yz)
            .map(|(a, b)| sketch_mode::default_datum_axis(a, b)),
        FeatureTool::DatumPoint => origin.map(sketch_mode::default_datum_point),
        FeatureTool::DatumCoordinateSystem => origin
            .zip(yz.zip(zx))
            .map(|(o, (x, y))| sketch_mode::default_datum_coordinate_system(o, x, y)),
        FeatureTool::ExtrudedBossBase
        | FeatureTool::ExtrudedCut
        | FeatureTool::RevolvedBossBase
        | FeatureTool::RevolvedCut
        | FeatureTool::SweptBossBase
        | FeatureTool::SweptCut
        | FeatureTool::Sketch3D => None,
    };
    if let Some(feature) = feature {
        state.mode = Mode::Datum(DatumArming::new(feature));
    }
}

fn arm_extruded_boss_base(state: &mut AppState) {
    arm_extrude(state, ExtrudeOperation::default());
}

fn arm_extruded_cut(state: &mut AppState) {
    arm_extrude(state, ExtrudeOperation::Cut);
}

fn arm_extrude(state: &mut AppState, operation: ExtrudeOperation) {
    match classify_extrude_profile(&state.document) {
        ProfileChoice::NoSketch => notify_info(state, strings::NOTIFY_EXTRUDE_NO_SKETCH, None),
        ProfileChoice::Unique(id) => {
            state.mode = Mode::Extrude(ExtrudeArming::armed(id, operation));
        }
        ProfileChoice::Ambiguous => {
            state.mode = Mode::Extrude(ExtrudeArming::AwaitingSketch(operation));
        }
    }
}

fn arm_revolve(state: &mut AppState, operation: ExtrudeOperation) {
    match classify_extrude_profile(&state.document) {
        ProfileChoice::NoSketch => notify_info(state, strings::NOTIFY_EXTRUDE_NO_SKETCH, None),
        ProfileChoice::Unique(id) => {
            let axis = RevolveAxis::SketchLine(
                state
                    .document
                    .sketch3(id)
                    .and_then(bone_document::Sketch3::axis_candidate)
                    .unwrap_or_default(),
            );
            state.mode = Mode::Revolve(RevolveArming::armed(id, axis, operation));
        }
        ProfileChoice::Ambiguous => {
            state.mode = Mode::Revolve(RevolveArming::AwaitingSketch(operation));
        }
    }
}

fn apply_revolve_edit(state: &mut AppState, edit: Option<shell::RevolveEdit>) {
    let Some(edit) = edit else { return };
    let Mode::Revolve(RevolveArming::Profile { feature, target }) = &state.mode else {
        return;
    };
    let next = edit.apply(feature);
    let target = *target;
    state.mode = Mode::Revolve(RevolveArming::Profile {
        feature: Box::new(next),
        target,
    });
}

fn apply_feature_arm(state: &mut AppState, arm: Option<shell::FeatureArm>) {
    match arm {
        Some(shell::FeatureArm::RevolveAxis)
            if matches!(state.mode, Mode::Revolve(RevolveArming::Profile { .. })) =>
        {
            state.revolve_arm = RevolveArm::Axis;
        }
        Some(shell::FeatureArm::RevolveContour)
            if matches!(state.mode, Mode::Revolve(RevolveArming::Profile { .. })) =>
        {
            state.revolve_arm = RevolveArm::Contour;
        }
        Some(shell::FeatureArm::SweepPath)
            if matches!(state.mode, Mode::Sweep(SweepArming::Profile { .. })) =>
        {
            state.sweep_arm = SweepArm::Path;
        }
        Some(shell::FeatureArm::SweepGuide(slot))
            if matches!(state.mode, Mode::Sweep(SweepArming::Profile { .. })) =>
        {
            state.sweep_arm = SweepArm::Guide(slot);
        }
        Some(shell::FeatureArm::SweepAlignmentAxis)
            if matches!(state.mode, Mode::Sweep(SweepArming::Profile { .. })) =>
        {
            state.sweep_arm = SweepArm::AlignmentAxis;
        }
        Some(shell::FeatureArm::SweepToolBody)
            if matches!(state.mode, Mode::Sweep(SweepArming::Profile { .. })) =>
        {
            state.sweep_arm = SweepArm::ToolBody;
        }
        _ => {}
    }
}

fn apply_revolve_confirm(state: &mut AppState, confirm: Option<shell::ConfirmAction>) {
    commit_armed_revolve(&mut state.document, &mut state.undo, &state.mode, confirm);
}

fn commit_armed_revolve(
    document: &mut Document,
    undo: &mut UndoStack,
    mode: &Mode,
    confirm: Option<shell::ConfirmAction>,
) -> Option<bone_types::RevolveId> {
    let Some(shell::ConfirmAction::Accept) = confirm else {
        return None;
    };
    let Mode::Revolve(RevolveArming::Profile { feature, target }) = mode else {
        return None;
    };
    let snapshot = document.clone();
    let committed = match target {
        Some(id) => {
            document.insert_revolve(*id, **feature);
            *id
        }
        None => document.commit_revolve(**feature),
    };
    undo.record(snapshot);
    Some(committed)
}

fn arm_sweep(state: &mut AppState, operation: ExtrudeOperation) {
    match classify_extrude_profile(&state.document) {
        ProfileChoice::NoSketch => notify_info(state, strings::NOTIFY_EXTRUDE_NO_SKETCH, None),
        ProfileChoice::Unique(id) => {
            state.mode = Mode::Sweep(SweepArming::armed(id, operation));
            state.sweep_arm = SweepArm::Path;
        }
        ProfileChoice::Ambiguous => {
            state.mode = Mode::Sweep(SweepArming::AwaitingSketch(operation));
        }
    }
}

fn apply_sweep_edit(state: &mut AppState, edit: Option<shell::SweepEdit>) {
    let Some(edit) = edit else { return };
    let Mode::Sweep(SweepArming::Profile { feature, target }) = &state.mode else {
        return;
    };
    let next = edit.apply(feature);
    let target = *target;
    state.mode = Mode::Sweep(SweepArming::Profile {
        feature: Box::new(next),
        target,
    });
    state.sweep_arm = SweepArm::Idle;
}

fn autoselect_sweep_path(state: &mut AppState) {
    let Mode::Sweep(SweepArming::Profile { feature, .. }) = &state.mode else {
        return;
    };
    let path_unset = matches!(feature.path, SweepPath::Sketch(id) if id == SketchId::default());
    if !path_unset {
        return;
    }
    let profile = feature.profile;
    let Some(path) = sole_other_profile_sketch(&state.document, profile) else {
        return;
    };
    apply_sweep_edit(state, Some(shell::SweepEdit::Path(SweepPath::Sketch(path))));
    if state.sweep_arm == SweepArm::Path {
        state.sweep_arm = SweepArm::Idle;
    }
}

fn apply_sweep_sketch_pick(state: &mut AppState, sketch: SketchId) -> bool {
    match &state.mode {
        Mode::Sweep(SweepArming::AwaitingSketch(operation)) => {
            let operation = *operation;
            state.mode = Mode::Sweep(SweepArming::armed(sketch, operation));
            state.sweep_arm = SweepArm::Path;
            autoselect_sweep_path(state);
            true
        }
        Mode::Sweep(SweepArming::Profile { .. }) => {
            match state.sweep_arm {
                SweepArm::Path => {
                    apply_sweep_edit(
                        state,
                        Some(shell::SweepEdit::Path(SweepPath::Sketch(sketch))),
                    );
                    state.sweep_arm = SweepArm::Idle;
                }
                SweepArm::Guide(slot) => {
                    apply_sweep_edit(
                        state,
                        Some(shell::SweepEdit::Guide {
                            slot,
                            guide: Some(SweepPath::Sketch(sketch)),
                        }),
                    );
                    state.sweep_arm = SweepArm::Idle;
                }
                SweepArm::Idle | SweepArm::AlignmentAxis | SweepArm::ToolBody => {
                    let operation = match &state.mode {
                        Mode::Sweep(SweepArming::Profile { feature, .. }) => feature.operation,
                        _ => return true,
                    };
                    state.mode = Mode::Sweep(SweepArming::armed(sketch, operation));
                    state.sweep_arm = SweepArm::Path;
                    autoselect_sweep_path(state);
                }
            }
            true
        }
        _ => false,
    }
}

fn apply_sweep_confirm(state: &mut AppState, confirm: Option<shell::ConfirmAction>) {
    commit_armed_sweep(&mut state.document, &mut state.undo, &state.mode, confirm);
}

fn commit_armed_sweep(
    document: &mut Document,
    undo: &mut UndoStack,
    mode: &Mode,
    confirm: Option<shell::ConfirmAction>,
) -> Option<bone_types::SweepId> {
    let Some(shell::ConfirmAction::Accept) = confirm else {
        return None;
    };
    let Mode::Sweep(SweepArming::Profile { feature, target }) = mode else {
        return None;
    };
    let snapshot = document.clone();
    let committed = match target {
        Some(id) => {
            document.insert_sweep(*id, feature.as_ref().clone());
            *id
        }
        None => document.commit_sweep(feature.as_ref().clone()),
    };
    undo.record(snapshot);
    Some(committed)
}

fn capture_sweep_rail(state: &mut AppState, picked: Option<PickedItem>) -> bool {
    let Mode::Sweep(SweepArming::Profile { feature, target }) = &state.mode else {
        return false;
    };
    let next = match state.sweep_arm {
        SweepArm::Path => {
            let Some(rail) = picked_sweep_path(&state.model, picked) else {
                return false;
            };
            shell::SweepEdit::Path(rail).apply(feature)
        }
        SweepArm::Guide(slot) => {
            let Some(rail) = picked_sweep_path(&state.model, picked) else {
                return false;
            };
            shell::SweepEdit::Guide {
                slot,
                guide: Some(rail),
            }
            .apply(feature)
        }
        SweepArm::AlignmentAxis => {
            let Some(axis) = picked_revolve_axis(&state.model, picked) else {
                return false;
            };
            shell::SweepEdit::Alignment(SweepAlignment::DirectionVector { axis }).apply(feature)
        }
        SweepArm::ToolBody => {
            let Some(kind) = picked_sweep_tool(picked) else {
                return false;
            };
            shell::SweepEdit::ProfileKind(kind).apply(feature)
        }
        SweepArm::Idle => return false,
    };
    let target = *target;
    state.mode = Mode::Sweep(SweepArming::Profile {
        feature: Box::new(next),
        target,
    });
    state.sweep_arm = SweepArm::Idle;
    true
}

fn picked_sweep_path(model: &EvaluatedModel, picked: Option<PickedItem>) -> Option<SweepPath> {
    match picked? {
        PickedItem::BrepEdge(id) => model_edge_label(model, id).map(SweepPath::Edge),
        _ => None,
    }
}

fn apply_extrude_edit(state: &mut AppState, edit: Option<shell::ExtrudeEdit>) {
    let Some(edit) = edit else { return };
    let Mode::Extrude(ExtrudeArming::Profile { feature, target }) = &state.mode else {
        return;
    };
    let next = edit.apply(**feature);
    let target = *target;
    let arm = unresolved_reference_arm(edit, &next);
    state.mode = Mode::Extrude(ExtrudeArming::Profile {
        feature: Box::new(next),
        target,
    });
    if matches!(edit, shell::ExtrudeEdit::EndCondition(_)) {
        state.extrude_reference_arm = arm;
    }
}

fn unresolved_reference_arm(
    edit: shell::ExtrudeEdit,
    feature: &ExtrudeFeature,
) -> Option<shell::ReferenceSlot> {
    match edit {
        shell::ExtrudeEdit::EndCondition(kind)
            if kind.needs_reference()
                && EndConditionKind::of(&feature.end_condition) != Some(kind) =>
        {
            Some(shell::ReferenceSlot::Direction1(kind))
        }
        _ => None,
    }
}

fn apply_extrude_arm(state: &mut AppState, arm: Option<shell::ReferenceSlot>) {
    let Some(slot) = arm else { return };
    if matches!(state.mode, Mode::Extrude(ExtrudeArming::Profile { .. })) {
        state.extrude_reference_arm = Some(slot);
    }
}

fn capture_extrude_reference(state: &mut AppState, picked: Option<PickedItem>) -> bool {
    let Some(slot) = state.extrude_reference_arm else {
        return false;
    };
    let Mode::Extrude(ExtrudeArming::Profile { feature, target }) = &state.mode else {
        return false;
    };
    let Some(kind) = slot.reference_kind() else {
        return false;
    };
    let Some(captured) = captured_reference(&state.model, kind, picked) else {
        return false;
    };
    let next = shell::apply_extrude_reference(**feature, slot, captured);
    let target = *target;
    state.mode = Mode::Extrude(ExtrudeArming::Profile {
        feature: Box::new(next),
        target,
    });
    state.extrude_reference_arm = None;
    true
}

fn captured_reference(
    model: &EvaluatedModel,
    kind: ReferenceKind,
    picked: Option<PickedItem>,
) -> Option<shell::CapturedReference> {
    match (kind, picked?) {
        (ReferenceKind::Face, PickedItem::BrepFace(id)) => {
            model_face_label(model, id).map(shell::CapturedReference::Face)
        }
        (ReferenceKind::Body, PickedItem::BrepFace(id)) => {
            model_face_label(model, id).map(|label| shell::CapturedReference::Body(label.feature))
        }
        (ReferenceKind::Vertex, PickedItem::BrepVertex(id)) => {
            model_vertex_label(model, id).map(shell::CapturedReference::Vertex)
        }
        _ => None,
    }
}

fn capture_revolve_axis(state: &mut AppState, picked: Option<PickedItem>) -> bool {
    let Mode::Revolve(RevolveArming::Profile { feature, target }) = &state.mode else {
        return false;
    };
    let Some(axis) = picked_revolve_axis(&state.model, picked) else {
        return false;
    };
    let next = shell::RevolveEdit::Axis(axis).apply(feature);
    let target = *target;
    state.mode = Mode::Revolve(RevolveArming::Profile {
        feature: Box::new(next),
        target,
    });
    state.revolve_arm = RevolveArm::Idle;
    true
}

fn picked_revolve_axis(model: &EvaluatedModel, picked: Option<PickedItem>) -> Option<RevolveAxis> {
    match picked? {
        PickedItem::BrepEdge(id) => model_edge_label(model, id).map(RevolveAxis::Edge),
        _ => None,
    }
}

fn picked_sweep_tool(picked: Option<PickedItem>) -> Option<SweepProfileKind> {
    let PickedItem::BrepFace(face) = picked? else {
        return None;
    };
    let tool = BodyRef::new(face.feature(), face.ordinal());
    Some(SweepProfileKind::Solid { tool })
}

fn capture_revolve_contour(state: &mut AppState, picked: Option<PickedItem>) -> bool {
    let Mode::Revolve(RevolveArming::Profile { feature, target }) = &state.mode else {
        return false;
    };
    let Some(SketchItemId::Entity(entity)) = picked.and_then(selection::picked_to_item) else {
        return false;
    };
    let next = shell::RevolveEdit::Contour(ContourSelection::Contour(entity)).apply(feature);
    let target = *target;
    state.mode = Mode::Revolve(RevolveArming::Profile {
        feature: Box::new(next),
        target,
    });
    state.revolve_arm = RevolveArm::Idle;
    true
}

fn model_edge_label(model: &EvaluatedModel, reference: SolidEdgeRef) -> Option<EdgeLabel> {
    model
        .body_at(reference.feature(), reference.ordinal())?
        .iter_edges()
        .find(|edge| edge.id() == reference.edge())
        .map(bone_document::BrepEdge::label)
}

fn model_face_label(model: &EvaluatedModel, reference: SolidFaceRef) -> Option<FaceLabel> {
    model
        .body_at(reference.feature(), reference.ordinal())?
        .iter_faces()
        .find(|face| face.id() == reference.face())
        .map(bone_document::BrepFace::label)
}

fn model_vertex_label(model: &EvaluatedModel, reference: SolidVertexRef) -> Option<VertexLabel> {
    model
        .body_at(reference.feature(), reference.ordinal())?
        .iter_vertices()
        .find(|vertex| vertex.id() == reference.vertex())
        .map(bone_document::BrepVertex::label)
}

fn apply_extrude_activation(state: &mut AppState, activated: Option<ExtrudeId>) {
    if let Some(mode) = extrude_edit_mode(&state.document, &state.mode, activated) {
        if let Mode::Extrude(ExtrudeArming::Profile {
            target: Some(id), ..
        }) = mode
        {
            state.framed_extrude = Some(id);
        }
        state.mode = mode;
    }
}

fn extrude_edit_mode(
    document: &Document,
    current: &Mode,
    activated: Option<ExtrudeId>,
) -> Option<Mode> {
    let id = activated?;
    if current.is_sketch() {
        return None;
    }
    let feature = document.extrude(id).copied()?;
    Some(Mode::Extrude(ExtrudeArming::edit(id, &feature)))
}

fn apply_extrude_confirm(state: &mut AppState, confirm: Option<shell::ConfirmAction>) {
    if let Some(id) =
        commit_armed_extrude(&mut state.document, &mut state.undo, &state.mode, confirm)
    {
        state.framed_extrude = Some(id);
    }
}

fn apply_datum_confirm(state: &mut AppState, confirm: Option<shell::ConfirmAction>) {
    if !state.mode.is_datum() {
        return;
    }
    match confirm {
        Some(shell::ConfirmAction::Accept) => {
            commit_armed_datum(&mut state.document, &mut state.undo, &state.mode);
            state.mode = Mode::Idle;
        }
        Some(shell::ConfirmAction::Cancel) => state.mode = Mode::Idle,
        None => {}
    }
}

fn commit_armed_datum(
    document: &mut Document,
    undo: &mut UndoStack,
    mode: &Mode,
) -> Option<DatumId> {
    let Mode::Datum(DatumArming { feature, target }) = mode else {
        return None;
    };
    let snapshot = document.clone();
    let committed = match target {
        Some(id) => {
            document.insert_datum(*id, feature.clone()).ok()?;
            *id
        }
        None => document.commit_datum(feature.clone()),
    };
    undo.record(snapshot);
    Some(committed)
}

fn commit_armed_extrude(
    document: &mut Document,
    undo: &mut UndoStack,
    mode: &Mode,
    confirm: Option<shell::ConfirmAction>,
) -> Option<ExtrudeId> {
    let Some(shell::ConfirmAction::Accept) = confirm else {
        return None;
    };
    let Mode::Extrude(ExtrudeArming::Profile { feature, target }) = mode else {
        return None;
    };
    let snapshot = document.clone();
    let committed = match target {
        Some(id) => {
            document.insert_extrude(*id, **feature);
            *id
        }
        None => document.commit_extrude(**feature),
    };
    undo.record(snapshot);
    Some(committed)
}

struct SolidViewData {
    faces: SolidScene,
    edges: EdgeScene,
    aabb: Aabb3,
}

#[derive(Clone, Debug, PartialEq)]
enum PreviewFeature {
    Extrude(ExtrudeFeature),
    Revolve(RevolveFeature),
    Sweep(SweepFeature),
}

impl PreviewFeature {
    fn sketch(&self) -> bone_types::SketchId {
        match self {
            Self::Extrude(feature) => feature.sketch,
            Self::Revolve(feature) => feature.sketch,
            Self::Sweep(feature) => feature.profile,
        }
    }
}

struct ExtrudePreview {
    feature: PreviewFeature,
    inputs: Vec<(SketchId, SketchVersion)>,
    generation: Option<GeometryGeneration>,
    failed: bool,
    error: Option<ExtrudeError>,
}

impl ExtrudePreview {
    fn status(&self) -> ExtrudeStatus<'_> {
        match &self.error {
            Some(error) => ExtrudeStatus::Failed(error),
            None => ExtrudeStatus::Valid,
        }
    }
}

const PREVIEW_CHORD_MM: f64 = 0.05;
const PREVIEW_ANGLE: AngleTolerance = AngleTolerance::from_radians(0.2);
const DRAG_CHORD_FRACTION: f64 = 0.006;
const DRAG_ANGLE: AngleTolerance = AngleTolerance::from_radians(0.5);

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum MeshQuality {
    Fine,
    Dragging,
}

fn desired_mesh_quality(mode: &Mode) -> MeshQuality {
    if dragging_in_sketch(mode) {
        MeshQuality::Dragging
    } else {
        MeshQuality::Fine
    }
}

fn mesh_tolerances(quality: MeshQuality, aabb: &Aabb3) -> (ChordHeightTolerance, AngleTolerance) {
    match quality {
        MeshQuality::Fine => (
            ChordHeightTolerance::from_mm(PREVIEW_CHORD_MM),
            PREVIEW_ANGLE,
        ),
        MeshQuality::Dragging => {
            let coarse_mm = (aabb.extent().norm_mm() * DRAG_CHORD_FRACTION).max(PREVIEW_CHORD_MM);
            (ChordHeightTolerance::from_mm(coarse_mm), DRAG_ANGLE)
        }
    }
}

fn active_solid_feature(
    mode: &Mode,
    document: &Document,
    framed: Option<ExtrudeId>,
) -> Option<PreviewFeature> {
    match mode {
        Mode::Extrude(ExtrudeArming::Profile { feature, .. }) => {
            Some(PreviewFeature::Extrude(**feature))
        }
        Mode::Revolve(RevolveArming::Profile { feature, .. }) => {
            Some(PreviewFeature::Revolve(**feature))
        }
        Mode::Sweep(SweepArming::Profile { feature, .. }) => {
            Some(PreviewFeature::Sweep((**feature).clone()))
        }
        Mode::Sketch { .. } | Mode::Datum(_) => None,
        Mode::Idle
        | Mode::Extrude(ExtrudeArming::AwaitingSketch(_))
        | Mode::Revolve(RevolveArming::AwaitingSketch(_))
        | Mode::Sweep(SweepArming::AwaitingSketch(_)) => framed
            .and_then(|id| document.extrude(id).copied())
            .or_else(|| {
                document
                    .feature_tree()
                    .iter()
                    .filter_map(|(_, node)| match node {
                        FeatureNode::Extrude(id) => Some(id),
                        _ => None,
                    })
                    .last()
                    .and_then(|id| document.extrude(id).copied())
            })
            .map(PreviewFeature::Extrude),
    }
}

fn clear_solid(state: &mut AppState) {
    state.solid_view = None;
    state.camera3 = None;
    state.view.home = None;
    state.view.tween = None;
}

fn body_passes(model: &EvaluatedModel) -> Vec<(FeatureId, RebuildPass)> {
    model
        .bodies()
        .filter_map(|(feature, _, _)| model.built_at(feature).map(|pass| (feature, pass)))
        .collect()
}

fn build_combined_view(model: &EvaluatedModel, quality: MeshQuality) -> Option<SolidViewData> {
    let table = solid_pick_table(model)?;
    let mut parts = model.bodies().filter_map(|(feature, ordinal, solid)| {
        build_solid_view(solid, body_key(feature, ordinal), &table, quality).ok()
    });
    let first = parts.next()?;
    Some(parts.fold(first, |acc, part| SolidViewData {
        faces: acc.faces.merge(part.faces),
        edges: acc.edges.merge(part.edges),
        aabb: acc.aabb.union(part.aabb),
    }))
}

fn recompute_model(state: &mut AppState, scope: RecomputeScope) {
    state.model.recompute(
        &state.document,
        state.document.suppressed(),
        state.document.rollback(),
        scope,
    );
}

fn rebuild_model_view(state: &mut AppState) {
    state.model_passes = body_passes(&state.model);
    state.mesh_quality = desired_mesh_quality(&state.mode);
    let model_view = build_combined_view(&state.model, state.mesh_quality);
    state.solid_view = match (model_view, sketch3_overlay_view(state)) {
        (Some(model), Some(overlay)) => Some(SolidViewData {
            faces: model.faces,
            edges: model.edges.merge(overlay.edges),
            aabb: model.aabb.union(overlay.aabb),
        }),
        (Some(view), None) | (None, Some(view)) => Some(view),
        (None, None) => None,
    };
    if state.solid_view.is_none() {
        clear_solid(state);
    }
}

fn sketch3_overlay_view(state: &AppState) -> Option<SolidViewData> {
    match &state.mode {
        Mode::Sketch { sketch_id, session } => {
            let spatial = session.spatial?;
            let sketch = state.document.sketch3(*sketch_id)?;
            Some(sketch3_aabb_view(sketch, spatial.basis))
        }
        Mode::Idle | Mode::Sweep(_) | Mode::Datum(_) => unconsumed_sketches(&state.document)
            .into_iter()
            .map(|(_, sketch)| sketch3_aabb_view(sketch, sketch.plane()))
            .reduce(|a, b| SolidViewData {
                faces: a.faces,
                edges: a.edges,
                aabb: a.aabb.union(b.aabb),
            }),
        Mode::Extrude(_) | Mode::Revolve(_) => None,
    }
}

fn unconsumed_sketches(document: &Document) -> Vec<(SketchId, &bone_document::Sketch3)> {
    let tree = document.feature_tree();
    let consumed = |feature: FeatureId| {
        tree.children(feature)
            .into_iter()
            .filter_map(|child| tree.node(child))
            .any(|node| matches!(node, FeatureNode::Extrude(_) | FeatureNode::Revolve(_)))
    };
    tree.iter()
        .filter_map(|(feature, node)| match node {
            FeatureNode::Sketch(id) if !consumed(feature) => {
                document.sketch3(id).map(|sketch| (id, sketch))
            }
            _ => None,
        })
        .filter(|(_, sketch)| !sketch.entity_order().is_empty())
        .collect()
}

const SKETCH3_DEFAULT_HALF_MM: f64 = 25.0;
const SKETCH3_POINT_RADIUS_PX: f64 = 4.0;
const SKETCH3_POINT_MIN_MM: f64 = 0.02;
const SKETCH3_BALL_RINGS: u32 = 6;
const SKETCH3_BALL_SEGMENTS: u32 = 10;
const SKETCH3_MIN_DEPTH_MM: f64 = 1.0;
const SKETCH3_SELECTED: LinearRgba = LinearRgba::new(0.10, 0.62, 0.30, 1.0);
const SKETCH3_ORIGIN: LinearRgba = LinearRgba::new(0.95, 0.55, 0.16, 1.0);
const SKETCH3_ORIGIN_SCALE: f64 = 1.7;
const PLANE_BORDER: LinearRgba = LinearRgba::new(0.34, 0.42, 0.60, 1.0);
const PLANE_BORDER_HIGHLIGHT: LinearRgba = LinearRgba::new(0.95, 0.55, 0.16, 1.0);
const PLANE_FILL: LinearRgba = LinearRgba::new(0.34, 0.42, 0.60, 0.07);
const PLANE_FILL_HIGHLIGHT: LinearRgba = LinearRgba::new(0.95, 0.55, 0.16, 0.20);

struct Sketch3Overlay {
    points: SolidScene,
    normal_lines: EdgeScene,
    construction_lines: EdgeScene,
    selected_lines: EdgeScene,
}

impl Sketch3Overlay {
    fn merge(self, other: Self) -> Self {
        Self {
            points: self.points.merge(other.points),
            normal_lines: self.normal_lines.merge(other.normal_lines),
            construction_lines: self.construction_lines.merge(other.construction_lines),
            selected_lines: self.selected_lines.merge(other.selected_lines),
        }
    }
}

fn sketch3_aabb_view(
    sketch: &bone_document::Sketch3,
    plane: bone_types::SketchPlaneBasis,
) -> SolidViewData {
    let curve_extents = sketch.entity_order().iter().flat_map(|id| {
        sketch
            .entities()
            .get(*id)
            .and_then(|entity| sketch3_entity_edges(sketch, *id, entity))
            .into_iter()
            .flat_map(|(_, edges)| edges)
            .flat_map(|edge| [edge.a(), edge.b()])
    });
    let points = sketch
        .entity_order()
        .iter()
        .filter_map(|id| sketch.point_position(*id))
        .chain(curve_extents);
    let aabb = Aabb3::from_points(points).unwrap_or_else(|| {
        let (ox, oy, oz) = plane.origin().coords_mm();
        Aabb3::from_corners(
            Point3::from_mm(
                ox - SKETCH3_DEFAULT_HALF_MM,
                oy - SKETCH3_DEFAULT_HALF_MM,
                oz - SKETCH3_DEFAULT_HALF_MM,
            ),
            Point3::from_mm(
                ox + SKETCH3_DEFAULT_HALF_MM,
                oy + SKETCH3_DEFAULT_HALF_MM,
                oz + SKETCH3_DEFAULT_HALF_MM,
            ),
        )
    });
    SolidViewData {
        faces: SolidScene::empty(),
        edges: EdgeScene::empty(),
        aabb,
    }
}

fn sketch3_frame_layers(
    state: &AppState,
    region: Option<ViewportRegion>,
    cursor_px: Option<WindowPoint>,
) -> (Option<EdgeScene>, Option<Sketch3Overlay>) {
    let preview = region.and_then(|region| {
        let cursor = cursor_px.filter(|c| state.viewport_rect.contains(window_to_layout_pos(*c)));
        sketch3_preview_edges(state, region, cursor)
    });
    let overlay = region.and_then(|region| {
        sketch3_overlay(state, region).or_else(|| workspace_sketch3_overlay(state, region))
    });
    (preview, overlay)
}

fn sketch3_overlay(state: &AppState, region: ViewportRegion) -> Option<Sketch3Overlay> {
    let sketch3_id = state.mode.sketch3_id()?;
    let sketch = state.document.sketch3(sketch3_id)?;
    let camera = state.camera3?;
    let normal_color = clear_to_linear(state.style.strokes().stroke());
    let mut overlay = build_sketch3_overlay(
        sketch,
        state.selection.entity_ids(),
        camera,
        region,
        normal_color,
    );
    let origin = sketch.plane().origin();
    let radius = sketch3_point_radius(camera, region, origin) * SKETCH3_ORIGIN_SCALE;
    overlay.points = overlay
        .points
        .merge(point_ball(origin, radius, SKETCH3_ORIGIN, PickId::NONE));
    Some(overlay)
}

fn workspace_sketch3_overlay(state: &AppState, region: ViewportRegion) -> Option<Sketch3Overlay> {
    if !shows_workspace(&state.mode) {
        return None;
    }
    let camera = state.camera3?;
    let normal_color = clear_to_linear(state.style.strokes().stroke());
    unconsumed_sketches(&state.document)
        .into_iter()
        .map(|(_, sketch)| build_sketch3_overlay(sketch, &[], camera, region, normal_color))
        .reduce(Sketch3Overlay::merge)
}

fn build_sketch3_overlay(
    sketch: &bone_document::Sketch3,
    selected: &[SketchEntityId],
    camera: Camera3,
    region: ViewportRegion,
    normal_color: LinearRgba,
) -> Sketch3Overlay {
    let (normal, construction, selected_lines) = sketch
        .entity_order()
        .iter()
        .filter_map(|id| {
            let entity = sketch.entities().get(*id)?;
            let (for_construction, edges) = sketch3_entity_edges(sketch, *id, entity)?;
            Some((*id, for_construction, edges))
        })
        .fold(
            (Vec::new(), Vec::new(), Vec::new()),
            |(mut normal, mut construction, mut selected_lines), (id, for_construction, edges)| {
                if selected.contains(&id) {
                    selected_lines.extend(edges);
                } else if for_construction {
                    construction.extend(edges);
                } else {
                    normal.extend(edges);
                }
                (normal, construction, selected_lines)
            },
        );

    let points = sketch
        .entity_order()
        .iter()
        .filter_map(|id| {
            let at = sketch.point_position(*id)?;
            let pick = PickId::point(*id).ok()?;
            let color = if selected.contains(id) {
                SKETCH3_SELECTED
            } else {
                normal_color
            };
            Some(point_ball(
                at,
                sketch3_point_radius(camera, region, at),
                color,
                pick,
            ))
        })
        .fold(SolidScene::empty(), SolidScene::merge);

    Sketch3Overlay {
        points,
        normal_lines: EdgeScene::from_genuine(normal),
        construction_lines: EdgeScene::from_genuine(construction),
        selected_lines: EdgeScene::from_genuine(selected_lines),
    }
}

fn polyline_edges(points: &[Point3], pick: PickId) -> Vec<GenuineEdge> {
    let crease = CreaseAngle::from_dot(-1.0);
    points
        .windows(2)
        .map(|w| GenuineEdge::new(w[0], w[1], pick, crease))
        .collect()
}

fn sketch3_entity_edges(
    sketch: &bone_document::Sketch3,
    id: SketchEntityId,
    entity: &bone_document::Sketch3Entity,
) -> Option<(bool, Vec<GenuineEdge>)> {
    use bone_document::Sketch3Entity as E;
    let pos = |eid| sketch.point_position(eid);
    match entity {
        E::Point(_) => None,
        E::Line(line) => {
            let edge = GenuineEdge::new(
                pos(line.a())?,
                pos(line.b())?,
                PickId::line(id).ok()?,
                CreaseAngle::from_dot(-1.0),
            );
            Some((line.for_construction(), vec![edge]))
        }
        E::Circle(circle) => {
            let points = sketch3_mode::circle3_polyline(
                pos(circle.center())?,
                circle.radius().get::<millimeter>(),
                circle.normal(),
            );
            Some((
                circle.for_construction(),
                polyline_edges(&points, PickId::circle(id).ok()?),
            ))
        }
        E::Arc(arc) => {
            let points = sketch3_mode::arc3_polyline(
                pos(arc.center())?,
                pos(arc.start())?,
                pos(arc.end())?,
                arc.normal(),
            );
            Some((
                arc.for_construction(),
                polyline_edges(&points, PickId::arc(id).ok()?),
            ))
        }
        E::Ellipse(ellipse) => {
            let points = sketch3_mode::ellipse3_polyline(
                pos(ellipse.center())?,
                pos(ellipse.major())?,
                pos(ellipse.minor())?,
                ellipse.normal(),
            );
            Some((
                ellipse.for_construction(),
                polyline_edges(&points, PickId::ellipse(id).ok()?),
            ))
        }
        E::Spline(spline) => {
            let control = spline
                .control()
                .iter()
                .map(|id| pos(*id))
                .collect::<Option<Vec<_>>>()?;
            let points = sketch3_mode::spline3_polyline(&control, spline.closed());
            Some((
                spline.for_construction(),
                polyline_edges(&points, PickId::spline(id).ok()?),
            ))
        }
    }
}

fn clear_to_linear(color: bone_render::ClearColor) -> LinearRgba {
    let [r, g, b, a] = color.to_rgba_array();
    LinearRgba::new(r, g, b, a)
}

fn with_theme_tokens(style: &Style, theme: &Theme) -> Style {
    let [r, g, b] = theme.cad.sketch_driven.srgb_f32();
    let driven = bone_render::ClearColor::new(f64::from(r), f64::from(g), f64::from(b), 1.0);
    style.with_text(style.text().with_driven_color(driven))
}

fn sketch3_point_radius(camera: Camera3, region: ViewportRegion, at: Point3) -> f64 {
    let height_px = f64::from(region.extent().height().value());
    let (ex, ey, ez) = camera.eye().coords_mm();
    let (tx, ty, tz) = camera.target().coords_mm();
    let (vx, vy, vz) = (tx - ex, ty - ey, tz - ez);
    let view_len = vx.mul_add(vx, vy.mul_add(vy, vz * vz)).sqrt();
    let (ax, ay, az) = at.coords_mm();
    let (rx, ry, rz) = (ax - ex, ay - ey, az - ez);
    let depth = (rx.mul_add(vx, ry.mul_add(vy, rz * vz)) / view_len).max(SKETCH3_MIN_DEPTH_MM);
    let world_per_px = match camera.projection().kind() {
        ProjectionKind::Perspective { fov, .. } => {
            2.0 * depth * (0.5 * fov.get::<radian>()).tan() / height_px
        }
        ProjectionKind::Orthographic { half_height } => {
            2.0 * half_height.get::<millimeter>() / height_px
        }
    };
    (SKETCH3_POINT_RADIUS_PX * world_per_px).max(SKETCH3_POINT_MIN_MM)
}

fn point_ball(center: Point3, radius: f64, color: LinearRgba, pick: PickId) -> SolidScene {
    let (cx, cy, cz) = center.coords_mm();
    let direction = |i: u32, j: u32| -> (f64, f64, f64) {
        let theta = core::f64::consts::PI * f64::from(i) / f64::from(SKETCH3_BALL_RINGS);
        let phi = core::f64::consts::TAU * f64::from(j) / f64::from(SKETCH3_BALL_SEGMENTS);
        (
            theta.sin() * phi.cos(),
            theta.cos(),
            theta.sin() * phi.sin(),
        )
    };
    let grid: Vec<(u32, u32)> = (0..=SKETCH3_BALL_RINGS)
        .flat_map(|i| (0..=SKETCH3_BALL_SEGMENTS).map(move |j| (i, j)))
        .collect();
    let positions: Vec<Point3> = grid
        .iter()
        .map(|&(i, j)| {
            let (dx, dy, dz) = direction(i, j);
            Point3::from_mm(
                radius.mul_add(dx, cx),
                radius.mul_add(dy, cy),
                radius.mul_add(dz, cz),
            )
        })
        .collect();
    let normals: Vec<UnitVec3> = grid
        .iter()
        .map(|&(i, j)| {
            let (dx, dy, dz) = direction(i, j);
            UnitVec3::new_unchecked(dx, dy, dz)
        })
        .collect();
    let colors = vec![color; positions.len()];
    let picks = vec![pick; positions.len()];
    let stride = SKETCH3_BALL_SEGMENTS + 1;
    let index = |i: u32, j: u32| -> u32 { i * stride + j };
    let triangles: Vec<[u32; 3]> = (0..SKETCH3_BALL_RINGS)
        .flat_map(|i| (0..SKETCH3_BALL_SEGMENTS).map(move |j| (i, j)))
        .flat_map(|(i, j)| {
            [
                [index(i, j), index(i + 1, j), index(i + 1, j + 1)],
                [index(i, j), index(i + 1, j + 1), index(i, j + 1)],
            ]
        })
        .collect();
    SolidScene::from_parts_picked(positions, normals, colors, picks, triangles)
}

fn defer_rebuild(first: bool, cost: RebuildCost, budget: RebuildBudget) -> bool {
    !first && !cost.within(budget)
}

fn sync_model_view(state: &mut AppState) {
    state.extrude_preview = None;
    recompute_model(state, RecomputeScope::Full);
    let passes = body_passes(&state.model);
    if passes.is_empty() {
        if state.solid_view.is_some() || !state.model_passes.is_empty() {
            state.model_passes.clear();
            clear_solid(state);
        }
        state.changed_features.clear();
        state.needs_rebuild = false;
        return;
    }
    let refine_only = passes == state.model_passes && state.solid_view.is_some();
    if refine_only && state.mesh_quality == desired_mesh_quality(&state.mode) {
        return;
    }
    let first = state.model_passes.is_empty() || state.solid_view.is_none();
    if !refine_only
        && defer_rebuild(
            first,
            state.model.rebuild_cost(),
            RebuildBudget::INTERACTIVE,
        )
    {
        let changed = passes
            .iter()
            .filter(|entry| !state.model_passes.contains(entry))
            .map(|(feature, _)| *feature);
        state.changed_features.extend(changed);
        state.needs_rebuild = true;
        return;
    }
    rebuild_model_view(state);
    state.changed_features.clear();
    state.needs_rebuild = false;
}

fn status_to_badge(status: RebuildStatus) -> Option<TreeBadge> {
    match status {
        RebuildStatus::Error(_) => Some(TreeBadge::Error),
        RebuildStatus::Warning(_) => Some(TreeBadge::Warning),
        RebuildStatus::NeedsRebuild => Some(TreeBadge::RebuildNeeded),
        RebuildStatus::UpToDate => None,
    }
}

fn feature_badge(state: &AppState, feature: FeatureId) -> Option<TreeBadge> {
    let status = state.model.status(feature).and_then(status_to_badge);
    let rebuild = state
        .changed_features
        .contains(&feature)
        .then_some(TreeBadge::RebuildNeeded);
    [status, rebuild].into_iter().flatten().max()
}

fn compute_feature_badges(state: &AppState) -> BTreeMap<FeatureId, TreeBadge> {
    state
        .document
        .feature_tree()
        .iter()
        .filter_map(|(feature, _)| feature_badge(state, feature).map(|badge| (feature, badge)))
        .collect()
}

fn frame_badges(
    state: &mut AppState,
) -> (BTreeMap<FeatureId, TreeBadge>, Vec<shell::BodyRow>, bool) {
    recompute_model(state, RecomputeScope::Full);
    (
        compute_feature_badges(state),
        compute_body_rows(state),
        state.needs_rebuild,
    )
}

fn feature_node_label(document: &Document, node: FeatureNode) -> Option<String> {
    match node {
        FeatureNode::Extrude(id) => Some(document.extrude_label(id).unwrap_or("").to_owned()),
        FeatureNode::Revolve(id) => Some(document.revolve_label(id).unwrap_or("").to_owned()),
        FeatureNode::Sweep(id) => Some(document.sweep_label(id).unwrap_or("").to_owned()),
        FeatureNode::Datum(_)
        | FeatureNode::Sketch(_)
        | FeatureNode::Origin
        | FeatureNode::PrincipalPlane(_)
        | FeatureNode::ImportedBody(_) => None,
    }
}

fn compute_body_rows(state: &AppState) -> Vec<shell::BodyRow> {
    let labels: BTreeMap<FeatureId, String> = state
        .document
        .feature_tree()
        .iter()
        .filter_map(|(feature, node)| {
            feature_node_label(&state.document, node).map(|label| (feature, label))
        })
        .collect();
    let counts: BTreeMap<FeatureId, usize> =
        state
            .model
            .bodies()
            .fold(BTreeMap::new(), |mut acc, (feature, _, _)| {
                *acc.entry(feature).or_insert(0) += 1;
                acc
            });
    state
        .model
        .bodies()
        .map(|(feature, ordinal, _)| {
            let base = labels.get(&feature).cloned().unwrap_or_default();
            let label = if counts.get(&feature).copied().unwrap_or(0) > 1 {
                format!("{base}[{}]", ordinal.value() + 1)
            } else {
                base
            };
            shell::BodyRow { label }
        })
        .collect()
}

fn feature_label_text(document: &Document, feature: FeatureId) -> String {
    match document.feature_tree().node(feature) {
        Some(FeatureNode::Sketch(sketch_id)) => {
            document.sketch_label(sketch_id).unwrap_or("").to_owned()
        }
        Some(FeatureNode::Extrude(extrude_id)) => {
            document.extrude_label(extrude_id).unwrap_or("").to_owned()
        }
        _ => String::new(),
    }
}

fn sketch_of_feature(document: &Document, feature: FeatureId) -> Option<SketchId> {
    match document.feature_tree().node(feature)? {
        FeatureNode::Sketch(sketch_id) => Some(sketch_id),
        _ => None,
    }
}

struct WhatsWrongKind {
    message: bone_ui::strings::StringKey,
    is_error: bool,
    offers_reattach: bool,
}

fn whats_wrong_kind(status: RebuildStatus) -> Option<WhatsWrongKind> {
    let kind = match status {
        RebuildStatus::Error(RebuildError::DanglingReference(_)) => WhatsWrongKind {
            message: strings::WHATS_WRONG_DANGLING,
            is_error: true,
            offers_reattach: true,
        },
        RebuildStatus::Error(RebuildError::NonPlanarSketchTarget) => WhatsWrongKind {
            message: strings::WHATS_WRONG_NON_PLANAR,
            is_error: true,
            offers_reattach: false,
        },
        RebuildStatus::Error(RebuildError::UpstreamUnresolved) => WhatsWrongKind {
            message: strings::WHATS_WRONG_UPSTREAM,
            is_error: true,
            offers_reattach: false,
        },
        RebuildStatus::Error(RebuildError::Build(_)) => WhatsWrongKind {
            message: strings::WHATS_WRONG_BUILD_FAILED,
            is_error: true,
            offers_reattach: false,
        },
        RebuildStatus::Warning(_) => WhatsWrongKind {
            message: strings::WHATS_WRONG_REPAIRED,
            is_error: false,
            offers_reattach: false,
        },
        RebuildStatus::UpToDate | RebuildStatus::NeedsRebuild => return None,
    };
    Some(kind)
}

fn whats_wrong_entry(
    model: &EvaluatedModel,
    document: &Document,
    feature: FeatureId,
    strings_table: &StringTable,
) -> Option<shell::WhatsWrong> {
    let status = model.status(feature)?;
    let kind = whats_wrong_kind(status)?;
    let reattach = kind
        .offers_reattach
        .then(|| sketch_of_feature(document, feature))
        .flatten();
    let message = match status {
        RebuildStatus::Error(RebuildError::Build(_)) => model
            .extrude_error(feature)
            .map_or(LabelText::Key(kind.message), |error| {
                crate::status_badge::extrude_panel_line(&error, strings_table)
            }),
        _ => LabelText::Key(kind.message),
    };
    Some(shell::WhatsWrong {
        label: feature_label_text(document, feature),
        message,
        is_error: kind.is_error,
        reattach,
    })
}

fn compute_whats_wrong(
    model: &EvaluatedModel,
    document: &Document,
    strings_table: &StringTable,
) -> Vec<shell::WhatsWrong> {
    document
        .feature_tree()
        .iter()
        .filter_map(|(feature, _)| whats_wrong_entry(model, document, feature, strings_table))
        .collect()
}

fn live_dim_anchor(mode: &Mode, document: &Document, pending: &PendingDimension) -> Point2 {
    mode.sketch_id()
        .and_then(|id| document.sketch(id))
        .and_then(|sketch| smart_dimension::live_anchor(&sketch, pending.proto))
        .unwrap_or(pending.anchor)
}

fn rebuild_changed(state: &mut AppState) {
    recompute_model(state, RecomputeScope::Full);
    if matches!(state.mode, Mode::Idle) {
        rebuild_model_view(state);
    }
    state.needs_rebuild = false;
    state.changed_features.clear();
}

fn force_rebuild_all(state: &mut AppState) {
    state.model = EvaluatedModel::new();
    recompute_model(state, RecomputeScope::Full);
    if matches!(state.mode, Mode::Idle) {
        rebuild_model_view(state);
    }
    state.needs_rebuild = false;
    state.changed_features.clear();
}

fn sync_solid_view(state: &mut AppState) {
    if shows_workspace(&state.mode) {
        sync_model_view(state);
        return;
    }
    if matches!(state.mode, Mode::Sketch { .. }) {
        sync_sketch_overlay_view(state);
        return;
    }
    state.model_passes.clear();
    let Some(feature) = active_solid_feature(&state.mode, &state.document, state.framed_extrude)
    else {
        state.extrude_preview = None;
        state.solid_view = None;
        state.camera3 = None;
        state.view.home = None;
        state.view.tween = None;
        return;
    };
    if state.document.sketch(feature.sketch()).is_none() {
        state.extrude_preview = None;
        state.solid_view = None;
        state.camera3 = None;
        state.view.home = None;
        state.view.tween = None;
        return;
    }
    let inputs = preview_inputs(&state.document, &feature);
    if extrude_preview_is_current(state.extrude_preview.as_ref(), &feature, &inputs) {
        return;
    }
    let previous_generation = state
        .extrude_preview
        .as_ref()
        .and_then(|cached| cached.generation);
    let previously_failed = state
        .extrude_preview
        .as_ref()
        .is_some_and(|cached| cached.failed);
    let first_preview = state.extrude_preview.is_none();
    let preview = match &feature {
        PreviewFeature::Sweep(sweep) => state.model.sweep_preview(&state.document, sweep),
        PreviewFeature::Extrude(_) | PreviewFeature::Revolve(_) => {
            let obstacles: Vec<&BrepSolid> = match state.mode {
                Mode::Extrude(ExtrudeArming::Profile { .. })
                | Mode::Revolve(RevolveArming::Profile { .. }) => {
                    state.model.bodies().map(|(_, _, solid)| solid).collect()
                }
                _ => Vec::new(),
            };
            compute_extrude_preview(&state.document, &obstacles, &feature)
        }
    };
    let generation = preview.as_ref().and_then(EvaluatedExtrude::generation);
    let error = preview
        .as_ref()
        .and_then(|evaluated| evaluated.result().as_ref().err().cloned());
    let mut failure = error.as_ref().map(ToString::to_string);
    if generation != previous_generation {
        state.solid_view = match preview.as_ref().and_then(EvaluatedExtrude::solid) {
            Some(solid) => match preview_solid_view(solid) {
                Ok(view) => Some(view),
                Err(error) => {
                    failure = Some(error);
                    None
                }
            },
            None => None,
        };
    }
    let now_failed = failure.is_some();
    let is_sweep = matches!(feature, PreviewFeature::Sweep(_));
    state.extrude_preview = Some(ExtrudePreview {
        feature,
        inputs,
        generation,
        failed: now_failed,
        error,
    });
    let newly_failed = first_preview || !previously_failed;
    if let Some(detail) = failure.filter(|_| newly_failed) {
        tracing::warn!(error = %detail, "solid preview evaluation failed");
        if !is_sweep {
            notify_error(state, strings::NOTIFY_EXTRUDE_FAILED, detail);
        }
    }
    if is_sweep && state.solid_view.is_none() {
        rebuild_model_view(state);
    }
}

fn preview_inputs(document: &Document, feature: &PreviewFeature) -> Vec<(SketchId, SketchVersion)> {
    let ids: Vec<SketchId> = match feature {
        PreviewFeature::Extrude(f) => vec![f.sketch],
        PreviewFeature::Revolve(f) => vec![f.sketch],
        PreviewFeature::Sweep(f) => core::iter::once(f.profile)
            .chain(sweep_path_sketch(f.path))
            .chain(f.guides.iter().copied().filter_map(sweep_path_sketch))
            .collect(),
    };
    ids.into_iter()
        .filter_map(|id| document.sketch(id).map(|sketch| (id, sketch.version())))
        .collect()
}

fn sweep_path_sketch(path: SweepPath) -> Option<SketchId> {
    match path {
        SweepPath::Sketch(id) => Some(id),
        SweepPath::Edge(_) => None,
    }
}

fn sync_sketch_overlay_view(state: &mut AppState) {
    state.extrude_preview = None;
    let passes = body_passes(&state.model);
    if passes.is_empty() {
        if state.solid_view.is_some() || !state.model_passes.is_empty() {
            state.model_passes.clear();
            clear_solid(state);
        }
        return;
    }
    if passes == state.model_passes
        && state.solid_view.is_some()
        && state.mesh_quality == desired_mesh_quality(&state.mode)
    {
        return;
    }
    rebuild_model_view(state);
}

fn sketch_overlay_camera(
    basis: SketchPlaneBasis,
    camera: Camera2,
    aabb: Aabb3,
    region: ViewportRegion,
) -> Option<Camera3> {
    let zoom = camera.zoom().value();
    let (region_x, region_y, region_w, region_h) = region.scissor();
    let half_height = f64::from(region_h) / (2.0 * zoom);
    if !(half_height.is_finite() && half_height > 0.0) {
        return None;
    }
    let surface = camera.extent();
    let region_center_x = f64::from(region_x) + f64::from(region_w) / 2.0;
    let region_center_y = f64::from(region_y) + f64::from(region_h) / 2.0;
    let surface_center_x = f64::from(surface.width().value()) / 2.0;
    let surface_center_y = f64::from(surface.height().value()) / 2.0;
    let (pan_x, pan_y) = camera.pan_mm().coords_mm();
    let center_u = pan_x + (region_center_x - surface_center_x) / zoom;
    let center_v = pan_y + (surface_center_y - region_center_y) / zoom;
    let (x_axis, y_axis, normal) = (basis.x_axis(), basis.y_axis(), basis.normal());
    let target = basis.origin()
        + x_axis.into_vec(Length::new::<millimeter>(center_u))
        + y_axis.into_vec(Length::new::<millimeter>(center_v));
    let standoff = (1.5 * aabb.extent().norm_mm()).max(half_height);
    let eye = target + normal.into_vec(Length::new::<millimeter>(standoff));
    let projection = Projection::orthographic(Length::new::<millimeter>(half_height)).ok()?;
    Camera3::new(eye, target, y_axis, projection).ok()
}

fn face_plane_bounds(basis: SketchPlaneBasis, aabb: Aabb3) -> Option<(f64, f64, f64, f64)> {
    const CORNERS: [(f64, f64, f64); 8] = [
        (-1.0, -1.0, -1.0),
        (-1.0, -1.0, 1.0),
        (-1.0, 1.0, -1.0),
        (-1.0, 1.0, 1.0),
        (1.0, -1.0, -1.0),
        (1.0, -1.0, 1.0),
        (1.0, 1.0, -1.0),
        (1.0, 1.0, 1.0),
    ];
    let (ox, oy, oz) = basis.origin().coords_mm();
    let (ux, uy, uz) = basis.x_axis().components();
    let (vx, vy, vz) = basis.y_axis().components();
    let (cx, cy, cz) = aabb.center().coords_mm();
    let (sx, sy, sz) = aabb.extent().coords_mm();
    let (hx, hy, hz) = (0.5 * sx, 0.5 * sy, 0.5 * sz);
    CORNERS
        .iter()
        .map(|&(dx, dy, dz)| {
            let (px, py, pz) = (cx + dx * hx - ox, cy + dy * hy - oy, cz + dz * hz - oz);
            (px * ux + py * uy + pz * uz, px * vx + py * vy + pz * vz)
        })
        .fold(None, |acc, (su, sv)| {
            Some(acc.map_or((su, sv, su, sv), |(mnu, mnv, mxu, mxv)| {
                (mnu.min(su), mnv.min(sv), mxu.max(su), mxv.max(sv))
            }))
        })
}

fn frame_face_sketch_camera(state: &AppState, basis: SketchPlaneBasis) -> Camera2 {
    let base =
        Camera2::new(state.camera.extent()).with_zoom(PixelsPerMm::new(INITIAL_ZOOM_PX_PER_MM));
    match state
        .solid_view
        .as_ref()
        .and_then(|view| face_plane_bounds(basis, view.aabb))
    {
        Some(bounds) => zoom_fit_to_bounds(base, bounds, state.viewport_rect),
        None => zoom_fit(base, &state.scene, state.viewport_rect),
    }
}

fn sketch_overlay_frame(
    state: &AppState,
    region: Option<ViewportRegion>,
) -> Option<SolidFrameView> {
    if state.mode.is_sketch3() {
        return None;
    }
    let region = region?;
    let view = state.solid_view.as_ref()?;
    let sketch = state.document.sketch(state.mode.sketch_id()?)?;
    let camera = sketch_overlay_camera(sketch.plane(), state.camera, view.aabb, region)?;
    SolidFrameView::new(camera, region).ok()
}

fn extrude_preview_is_current(
    cached: Option<&ExtrudePreview>,
    feature: &PreviewFeature,
    inputs: &[(SketchId, SketchVersion)],
) -> bool {
    cached.is_some_and(|cached| cached.feature == *feature && cached.inputs == inputs)
}

fn compute_extrude_preview(
    document: &Document,
    obstacles: &[&BrepSolid],
    feature: &PreviewFeature,
) -> Option<EvaluatedExtrude> {
    let sketch = document.sketch3(feature.sketch())?;
    let evaluated_sketch = evaluate_sketch(sketch);
    match feature {
        PreviewFeature::Extrude(extrude) => Some(evaluate_extrude_reaching(
            FeatureId::default(),
            &evaluated_sketch,
            extrude,
            obstacles,
        )),
        PreviewFeature::Revolve(revolve) => Some(evaluate_revolve_reaching(
            FeatureId::default(),
            &evaluated_sketch,
            revolve,
        )),
        PreviewFeature::Sweep(_) => None,
    }
}

fn preview_solid_view(solid: &bone_document::BrepSolid) -> Result<SolidViewData, String> {
    let (body, table) = single_body_pick(solid).map_err(|error| error.to_string())?;
    build_solid_view(solid, body, &table, MeshQuality::Fine)
}

fn build_solid_view(
    solid: &bone_document::BrepSolid,
    body: BodyKey,
    table: &SolidPickTable,
    quality: MeshQuality,
) -> Result<SolidViewData, String> {
    let aabb = solid
        .bounding_box()
        .ok_or_else(|| "degenerate solid has no bounding box".to_owned())?;
    let (chord, angle) = mesh_tolerances(quality, &aabb);
    let mesh = solid
        .tessellate(chord, angle)
        .map_err(|error| error.to_string())?;
    let faces = SolidScene::from_mesh(&mesh, body, table).map_err(|error| error.to_string())?;
    let edges = EdgeScene::from_solid(solid, &mesh, chord, body, table)
        .map_err(|error| error.to_string())?;
    Ok(SolidViewData { faces, edges, aabb })
}

fn sync_solid_camera(
    state: &mut AppState,
    region: Option<ViewportRegion>,
    planes: Option<&ReferencePlaneScene>,
) {
    if state.camera3.is_some() {
        return;
    }
    let Some(region) = region else {
        return;
    };
    let Some(aabb) = state
        .solid_view
        .as_ref()
        .map(|view| view.aabb)
        .or_else(|| planes.map(|planes| planes.aabb))
    else {
        return;
    };
    let framed = frame_standard_view(aabb, region.extent(), StandardView::Isometric, None).ok();
    state.camera3 = framed;
    if state.view.home.is_none() {
        state.view.home = framed;
    }
}

fn shows_workspace(mode: &Mode) -> bool {
    matches!(
        mode,
        Mode::Idle | Mode::Datum(_) | Mode::Sweep(SweepArming::AwaitingSketch(_))
    )
}

fn show_reference_planes(mode: &Mode) -> bool {
    shows_workspace(mode) || mode.is_sketch3()
}

fn viewport_is_spatial(state: &AppState) -> bool {
    state.solid_view.is_some() || show_reference_planes(&state.mode)
}

fn reference_plane_scene(state: &AppState) -> Option<ReferencePlaneScene> {
    if !show_reference_planes(&state.mode) {
        return None;
    }
    let model_aabb = state.solid_view.as_ref().map(|view| view.aabb);
    let half = reference_planes::plane_half_extent(model_aabb);
    let datums = datum_plane_inputs(state);
    let preview = armed_datum_plane(state);
    let active = active_plane_feature(state);
    let sketching = state.mode.is_sketch3();
    let visible = |feature: FeatureId| {
        if sketching {
            Some(feature) == active
        } else {
            state.planes_visible && !state.hidden_planes.contains(&feature)
        }
    };
    reference_planes::build(&state.document, &datums, preview, active, half, visible)
}

fn datum_plane_inputs(state: &AppState) -> Vec<DatumPlaneInput<'_>> {
    state
        .document
        .feature_tree()
        .iter()
        .filter_map(|(feature, node)| {
            let FeatureNode::Datum(datum) = node else {
                return None;
            };
            let DatumGeometry::Plane(plane) = state.model.datum(feature)? else {
                return None;
            };
            let name = state.document.datum_label(datum)?;
            Some(DatumPlaneInput {
                feature,
                plane,
                name,
            })
        })
        .collect()
}

fn armed_datum_plane(state: &AppState) -> Option<Plane3> {
    let Mode::Datum(arming) = &state.mode else {
        return None;
    };
    match state.model.datum_preview(&arming.feature)? {
        DatumGeometry::Plane(plane) => Some(plane),
        DatumGeometry::Point(_) | DatumGeometry::Axis(_) | DatumGeometry::CoordinateSystem(_) => {
            None
        }
    }
}

fn active_plane_feature(state: &AppState) -> Option<FeatureId> {
    match state.mode.sketch3_source()? {
        SketchPlaneSource::Principal(plane) => state
            .document
            .feature_tree()
            .principal_plane_feature(PrincipalPlane::from(plane)),
        SketchPlaneSource::Datum(feature) => Some(feature),
    }
}

fn plane_label_key(kind: PrincipalPlane) -> bone_ui::strings::StringKey {
    match kind {
        PrincipalPlane::Xy => strings::FEATURE_PLANE_XY,
        PrincipalPlane::Yz => strings::FEATURE_PLANE_YZ,
        PrincipalPlane::Zx => strings::FEATURE_PLANE_ZX,
    }
}

fn project_to_viewport(
    clip_from_world: [f32; 16],
    point: Point3,
    region: ViewportRegion,
) -> Option<(f64, f64)> {
    let (x, y, z) = point.coords_mm();
    let m = clip_from_world.map(f64::from);
    let cx = m[0] * x + m[4] * y + m[8] * z + m[12];
    let cy = m[1] * x + m[5] * y + m[9] * z + m[13];
    let cw = m[3] * x + m[7] * y + m[11] * z + m[15];
    if cw <= f64::EPSILON {
        return None;
    }
    let ndc_x = cx / cw;
    let ndc_y = cy / cw;
    let (rx, ry, rw, rh) = region.scissor();
    let px = f64::from(rx) + (ndc_x * 0.5 + 0.5) * f64::from(rw);
    let py = f64::from(ry) + (0.5 - ndc_y * 0.5) * f64::from(rh);
    Some((px, py))
}

fn plane_label_paints(
    state: &AppState,
    scene: &ReferencePlaneScene,
    region: Option<ViewportRegion>,
) -> Vec<bone_ui::widgets::WidgetPaint> {
    let (Some(region), Some(camera)) = (region, state.camera3) else {
        return Vec::new();
    };
    let Ok(clip) = bone_render::clip_from_world(camera, region.extent()) else {
        return Vec::new();
    };
    let color = state.theme.colors.text_secondary();
    let role = state.theme.typography.caption;
    scene
        .planes
        .iter()
        .filter_map(|plane| {
            let text = plane_label_text(&plane.label)?;
            let (px, py) = project_to_viewport(clip, plane.label_anchor(), region)?;
            let rect = LayoutRect::new(
                LayoutPos::new(
                    LayoutPx::new(bone_render::lower_f32(px)),
                    LayoutPx::new(bone_render::lower_f32(py)),
                ),
                LayoutSize::new(LayoutPx::new(96.0), LayoutPx::new(16.0)),
            );
            Some(bone_ui::widgets::WidgetPaint::Label {
                rect,
                text,
                color,
                role,
            })
        })
        .collect()
}

fn plane_label_text(label: &PlaneLabel) -> Option<bone_ui::widgets::LabelText> {
    match label {
        PlaneLabel::Principal(kind) => Some(plane_label_key(*kind).into()),
        PlaneLabel::Named(name) => Some(bone_ui::widgets::LabelText::Owned(name.clone())),
        PlaneLabel::Preview => None,
    }
}

const VIEW_TWEEN_MS: u64 = 220;

fn step_view_tween(state: &mut AppState, now: FrameInstant) {
    let Some(active) = state.view.tween else {
        return;
    };
    let elapsed = now.since(active.started);
    if let Ok(camera) = active.tween.sample(elapsed) {
        state.camera3 = Some(camera);
    }
    if active.tween.is_done(elapsed) {
        state.camera3 = Some(active.tween.to());
        state.view.tween = None;
    }
}

fn start_view_tween(state: &mut AppState, target: Camera3, now: FrameInstant) {
    let tween = if state.settings.reduce_motion {
        CameraTween::immediate(target)
    } else {
        CameraTween::eased(
            state.camera3.unwrap_or(target),
            target,
            std::time::Duration::from_millis(VIEW_TWEEN_MS),
            CubicEasing::STANDARD,
        )
    };
    state.view.tween = Some(view_cube::ActiveTween {
        tween,
        started: now,
    });
}

fn apply_nav_camera(state: &mut AppState, camera: Camera3) {
    state.camera3 = Some(camera);
    state.view.tween = None;
}

fn solid_aabb_and_extent(state: &AppState) -> Option<(Aabb3, ViewportExtent)> {
    let region = solid_viewport_region(state.viewport_rect, state.extent)?;
    let view = state.solid_view.as_ref()?;
    Some((view.aabb, region.extent()))
}

fn normal_to_plane(state: &AppState) -> Option<Plane3> {
    Some(Plane3::from(state.mode.sketch3_basis()?))
}

fn frame_target_camera(state: &AppState, pick: view_cube::ViewPick) -> Option<Camera3> {
    match pick {
        view_cube::ViewPick::Home => state.view.home,
        view_cube::ViewPick::Standard(view) => {
            let (aabb, extent) = solid_aabb_and_extent(state)?;
            frame_standard_view(aabb, extent, view, normal_to_plane(state)).ok()
        }
        view_cube::ViewPick::Direction(direction) => {
            let (aabb, extent) = solid_aabb_and_extent(state)?;
            frame_view_direction(aabb, extent, direction).ok()
        }
    }
}

fn apply_view_pick(state: &mut AppState, pick: Option<view_cube::ViewPick>, now: FrameInstant) {
    let Some(pick) = pick else {
        return;
    };
    if let Some(target) = frame_target_camera(state, pick) {
        start_view_tween(state, target, now);
    }
}

fn view_nav_enabled(state: &AppState) -> bool {
    state.solid_view.is_some()
        && !state.mode.is_sketch()
        && !modal_active(state)
        && state.focus.focused().is_none()
        && !dim_flow_active(&state.mode)
}

fn apply_view_menu(
    state: &mut AppState,
    action: Option<view_cube::ViewCubeMenuAction>,
    now: FrameInstant,
) {
    match action {
        Some(view_cube::ViewCubeMenuAction::SetAsHome) => {
            state.view.home = state.camera3;
        }
        Some(view_cube::ViewCubeMenuAction::FitToWindow) => {
            if let (Some(camera), Some((aabb, extent))) =
                (state.camera3, solid_aabb_and_extent(state))
                && let Ok(target) = frame_current(camera, aabb, extent)
            {
                start_view_tween(state, target, now);
            }
        }
        Some(view_cube::ViewCubeMenuAction::ViewNormalTo) => {
            apply_view_pick(
                state,
                Some(view_cube::ViewPick::Standard(StandardView::NormalTo)),
                now,
            );
        }
        None => {}
    }
}

fn preview_solid_frame(
    solid_view: Option<&SolidViewData>,
    camera: Option<Camera3>,
    region: ViewportRegion,
) -> Option<(&SolidViewData, SolidFrameView)> {
    let view = solid_view?;
    Some((view, SolidFrameView::new(camera?, region).ok()?))
}

fn solid_viewport_region(viewport: LayoutRect, surface: ViewportExtent) -> Option<ViewportRegion> {
    let (surface_w, surface_h) = (surface.width().value(), surface.height().value());
    let min_x = round_layout_px(viewport.min_x().value()).min(surface_w);
    let min_y = round_layout_px(viewport.min_y().value()).min(surface_h);
    let width = round_layout_px(viewport.size.width.value()).min(surface_w - min_x);
    let height = round_layout_px(viewport.size.height.value()).min(surface_h - min_y);
    (width > 0 && height > 0).then(|| {
        ViewportRegion::new(
            ViewportPx::new(min_x),
            ViewportPx::new(min_y),
            ViewportExtent::new(ViewportPx::new(width), ViewportPx::new(height)),
        )
    })
}

#[allow(
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "the saturating cast of a non-negative rounded px is clamped to the surface extent by the caller"
)]
fn round_layout_px(value: f32) -> u32 {
    value.round().max(0.0) as u32
}

fn viewport_local_point(cursor: WindowPoint, region: ViewportRegion) -> Option<ViewportPoint> {
    let (min_x, min_y, _, _) = region.scissor();
    ViewportPoint::new(cursor.x - f64::from(min_x), cursor.y - f64::from(min_y)).ok()
}

fn drag_gesture(modifiers: ModifierMask) -> NavGesture {
    let with_ctrl =
        if modifiers.contains(ModifierMask::CTRL) || modifiers.contains(ModifierMask::META) {
            DragModifiers::NONE.with_ctrl()
        } else {
            DragModifiers::NONE
        };
    let with_shift = if modifiers.contains(ModifierMask::SHIFT) {
        with_ctrl.with_shift()
    } else {
        with_ctrl
    };
    let resolved = if modifiers.contains(ModifierMask::ALT) {
        with_shift.with_alt()
    } else {
        with_shift
    };
    resolved.gesture()
}

fn build_preview(
    mode: &Mode,
    document: &Document,
    cursor_world: Option<Point2>,
    camera: &Camera2,
) -> SketchPreview {
    let Mode::Sketch { sketch_id, session } = mode else {
        return SketchPreview::empty();
    };
    if session.spatial.is_some() || session.drag.is_some() || session.dim_flow.is_some() {
        return SketchPreview::empty();
    }
    let Some(tool) = session.tool else {
        return SketchPreview::empty();
    };
    let Some(base) = document.sketch3(*sketch_id) else {
        return SketchPreview::empty();
    };
    let view = sketch3_project::project(base, base.plane());
    let sketch = view.sketch();
    let pending = sketch3_project::to_plane_pending(&view, session.pending);
    let Some(cursor) = cursor_world else {
        return tools::preview_anchors_only(sketch, pending);
    };
    let snap = snap_tolerance(camera).and_then(|tol| tool_snap(sketch, tool, pending, cursor, tol));
    tools::preview(sketch, tool, cursor, pending, snap)
}

fn snap_tolerance_for_mm_per_px(mm_per_px: f64) -> Option<Length> {
    (mm_per_px.is_finite() && mm_per_px > 0.0).then(|| {
        Length::new::<millimeter>((SNAP_TOLERANCE_PX * mm_per_px).min(SNAP_TOLERANCE_MAX_MM))
    })
}

fn snap_tolerance(camera: &Camera2) -> Option<Length> {
    snap_tolerance_for_mm_per_px(camera.world_mm_per_pixel())
}

fn sketch3_snap_tolerance(camera: Camera3, region: ViewportRegion) -> Option<Length> {
    let bone_types::ProjectionKind::Orthographic { half_height } = camera.projection().kind()
    else {
        return None;
    };
    let height_px = f64::from(region.extent().height().value());
    snap_tolerance_for_mm_per_px(2.0 * half_height.get::<millimeter>() / height_px)
}

fn snap_for_tool(
    sketch: &Sketch,
    tool: SketchTool,
    anchor: Option<Anchor>,
    cursor: Point2,
    tolerance: Length,
) -> Option<SnapHit> {
    match tool {
        SketchTool::Line | SketchTool::Centerline => {
            snap::detect(cursor, anchor, sketch, tolerance)
        }
        _ => snap::detect_endpoint_only(cursor, sketch, tolerance),
    }
}

fn tool_snap(
    sketch: &Sketch,
    tool: SketchTool,
    pending: Option<Pending>,
    cursor: Point2,
    tolerance: Length,
) -> Option<SnapHit> {
    let anchor = latest_anchor(pending).and_then(|c| resolve_anchor(Some(sketch), c));
    snap_for_tool(sketch, tool, anchor, cursor, tolerance)
}

fn resolve_anchor(sketch: Option<&Sketch>, click: ClickAnchor) -> Option<Anchor> {
    match click {
        ClickAnchor::Position(pos) | ClickAnchor::MidpointOf { position: pos, .. } => {
            Some(Anchor { pos, id: None })
        }
        ClickAnchor::OnEdge { entity, position } => Some(Anchor {
            pos: position,
            id: Some(entity),
        }),
        ClickAnchor::Endpoint(id) => match sketch?.entities().get(id)? {
            SketchEntity::Point(p) => Some(Anchor {
                pos: p.at(),
                id: Some(id),
            }),
            _ => None,
        },
    }
}

fn pan_by_px(camera: Camera2, horizontal_px: f64, vertical_px: f64) -> Camera2 {
    let mm_per_px = camera.world_mm_per_pixel();
    let (pan_x, pan_y) = camera.pan_mm().coords_mm();
    camera.with_pan(Vec2::from_mm(
        pan_x - horizontal_px * mm_per_px,
        pan_y + vertical_px * mm_per_px,
    ))
}

fn zoom_fit(camera: Camera2, scene: &SketchScene, viewport: LayoutRect) -> Camera2 {
    let Some(aabb) = scene.aabb() else {
        return camera;
    };
    let (mnx, mny) = aabb.min().coords_mm();
    let (mxx, mxy) = aabb.max().coords_mm();
    zoom_fit_to_bounds(camera, (mnx, mny, mxx, mxy), viewport)
}

fn zoom_fit_to_bounds(
    camera: Camera2,
    (mnx, mny, mxx, mxy): (f64, f64, f64, f64),
    viewport: LayoutRect,
) -> Camera2 {
    let center_x = (mnx + mxx) * 0.5;
    let center_y = (mny + mxy) * 0.5;
    let world_w = mxx - mnx;
    let world_h = mxy - mny;
    let v_w = f64::from(viewport.size.width.value());
    let v_h = f64::from(viewport.size.height.value());
    if v_w <= 0.0 || v_h <= 0.0 {
        return camera;
    }
    let axis_zoom = |pixels: f64, world: f64| {
        if world > 0.0 {
            pixels / world
        } else {
            f64::INFINITY
        }
    };
    let raw_zoom = axis_zoom(v_w, world_w).min(axis_zoom(v_h, world_h)) * ZOOM_FIT_MARGIN;
    let zoom = raw_zoom.clamp(ZOOM_MIN, ZOOM_MAX);
    let pan = pan_centering((center_x, center_y), camera.extent(), viewport, zoom);
    camera.with_zoom(PixelsPerMm::new(zoom)).with_pan(pan)
}

fn pan_centering(
    target_world: (f64, f64),
    window: ViewportExtent,
    viewport: LayoutRect,
    zoom: f64,
) -> Vec2 {
    let v_w = f64::from(viewport.size.width.value());
    let v_h = f64::from(viewport.size.height.value());
    let viewport_center_x = f64::from(viewport.origin.x.value()) + v_w * 0.5;
    let viewport_center_y = f64::from(viewport.origin.y.value()) + v_h * 0.5;
    let window_center_x = f64::from(window.width().value()) * 0.5;
    let window_center_y = f64::from(window.height().value()) * 0.5;
    let pan_x = target_world.0 - (viewport_center_x - window_center_x) / zoom;
    let pan_y = target_world.1 + (viewport_center_y - window_center_y) / zoom;
    Vec2::from_mm(pan_x, pan_y)
}

fn keyboard_camera(nav: NavKey, input: &InputState, state: &AppState) -> Option<Camera2> {
    if input.ctrl_or_meta() {
        return None;
    }
    let camera = state.camera;
    let step = input.pan_step_px();
    let shift = input.shift();
    match nav {
        NavKey::Left => Some(pan_by_px(camera, step, 0.0)),
        NavKey::Right => Some(pan_by_px(camera, -step, 0.0)),
        NavKey::Up => Some(pan_by_px(camera, 0.0, step)),
        NavKey::Down => Some(pan_by_px(camera, 0.0, -step)),
        NavKey::Zoom => Some(zoom_about(
            camera,
            input.cursor_px,
            if shift {
                1.0 / ZOOM_KEY_STEP
            } else {
                ZOOM_KEY_STEP
            },
        )),
        NavKey::ZoomIn => Some(zoom_about(camera, input.cursor_px, ZOOM_KEY_STEP)),
        NavKey::ZoomOut => Some(zoom_about(camera, input.cursor_px, 1.0 / ZOOM_KEY_STEP)),
    }
}

fn zoom_key3(
    camera: Camera3,
    extent: ViewportExtent,
    pixel: ViewportPoint,
    factor: f64,
) -> Option<Camera3> {
    ZoomFactor::new(factor)
        .ok()
        .and_then(|f| zoom_about_pixel(camera, extent, pixel, f).ok())
}

fn keyboard_camera3(nav: NavKey, input: &InputState, state: &AppState) -> Option<Camera3> {
    let camera = state.camera3?;
    let region = solid_viewport_region(state.viewport_rect, state.extent)?;
    let extent = region.extent();
    let ctrl = input.ctrl_or_meta();
    let shift = input.shift();
    let alt = input.modifiers.contains(ModifierMask::ALT);
    let cx = f64::from(extent.width().value()) * 0.5;
    let cy = f64::from(extent.height().value()) * 0.5;
    let center = ViewportPoint::new(cx, cy).ok()?;
    let pan_to = |dx: f64, dy: f64| {
        ViewportPoint::new(cx + dx, cy + dy)
            .ok()
            .and_then(|to| pan_pixels(camera, extent, center, to).ok())
    };
    let step = Angle::new::<degree>(if shift {
        ORBIT_KEY_SNAP_DEG
    } else {
        ORBIT_KEY_STEP_DEG
    });
    match nav {
        NavKey::Left if ctrl => pan_to(-PAN_STEP_PX, 0.0),
        NavKey::Right if ctrl => pan_to(PAN_STEP_PX, 0.0),
        NavKey::Up if ctrl => pan_to(0.0, -PAN_STEP_PX),
        NavKey::Down if ctrl => pan_to(0.0, PAN_STEP_PX),
        NavKey::Left if alt => roll_by(camera, step).ok(),
        NavKey::Right if alt => roll_by(camera, -step).ok(),
        NavKey::Left => orbit_yaw(camera, step).ok(),
        NavKey::Right => orbit_yaw(camera, -step).ok(),
        NavKey::Up => orbit_pitch(camera, step).ok(),
        NavKey::Down => orbit_pitch(camera, -step).ok(),
        NavKey::Zoom if shift => zoom_key3(camera, extent, center, 1.0 / ZOOM_KEY_STEP),
        NavKey::Zoom | NavKey::ZoomIn => zoom_key3(camera, extent, center, ZOOM_KEY_STEP),
        NavKey::ZoomOut => zoom_key3(camera, extent, center, 1.0 / ZOOM_KEY_STEP),
    }
}

fn build_hotkey_table() -> HotkeyTable {
    let Ok(table) = hotkeys::compose_table(&hotkeys::HotkeyOverrides::default()) else {
        unreachable!("default hotkey bindings are conflict-free");
    };
    table
}

fn scopes_for_mode(mode: &Mode) -> HotkeyScopes {
    let mut scopes = HotkeyScopes::from_outer_to_inner([HotkeyScope::Global]);
    if mode.is_sketch() {
        scopes.push(HotkeyScope::Sketch);
    }
    if mode.is_extrude() {
        scopes.push(HotkeyScope::Extrude);
    }
    if mode.is_revolve() {
        scopes.push(HotkeyScope::Revolve);
    }
    if mode.is_sweep() {
        scopes.push(HotkeyScope::Sweep);
    }
    if mode.is_datum() {
        scopes.push(HotkeyScope::Datum);
    }
    scopes
}

fn next_mode(mode: Mode, frame: &shell::ShellFrame, escape_requested: bool) -> Mode {
    let after_pick = resolve_pick(mode, frame);
    let after_escape = if escape_requested {
        cancel_pending_or_exit(after_pick)
    } else {
        after_pick
    };
    let after_exit = if frame.exit_sketch {
        Mode::Idle
    } else {
        after_escape
    };
    match frame.activated_tool {
        Some(t) => toggle_or_arm(after_exit, t),
        None => after_exit,
    }
}

fn resolve_pick(mode: Mode, frame: &shell::ShellFrame) -> Mode {
    if mode.is_extrude() {
        return match frame.sketch_activated {
            Some(id) => match &mode {
                Mode::Extrude(ExtrudeArming::Profile { feature, .. }) if feature.sketch == id => {
                    mode
                }
                Mode::Extrude(ExtrudeArming::Profile { feature, .. }) => {
                    Mode::Extrude(ExtrudeArming::armed(id, feature.operation))
                }
                Mode::Extrude(ExtrudeArming::AwaitingSketch(operation)) => {
                    Mode::Extrude(ExtrudeArming::armed(id, *operation))
                }
                _ => Mode::Extrude(ExtrudeArming::profile(id)),
            },
            None => mode,
        };
    }
    let sketch_pick = frame.sketch_activated.filter(|_| !mode.is_sketch());
    sketch_pick.map_or(mode, Mode::enter_sketch)
}

fn toggle_or_arm(mode: Mode, tool: SketchTool) -> Mode {
    let already_active = matches!(
        &mode,
        Mode::Sketch { session, .. } if session.tool == Some(tool)
    );
    if already_active {
        mode.disarm_tool()
    } else {
        mode.arm_tool(tool)
    }
}

fn cancel_pending_or_exit(mode: Mode) -> Mode {
    match &mode {
        Mode::Sketch { session, .. } if !session.spline_draft.is_empty() => {
            mode.clear_spline_draft()
        }
        Mode::Sketch { session, .. } if session.pending.is_some() => mode.clear_pending(),
        Mode::Sketch { session, .. } if session.tool.is_some() => mode.disarm_tool(),
        Mode::Sketch { .. }
        | Mode::Idle
        | Mode::Extrude(_)
        | Mode::Revolve(_)
        | Mode::Sweep(_)
        | Mode::Datum(_) => Mode::Idle,
    }
}

impl AppCore {
    pub fn new(
        gpu: &Gpu,
        color_format: wgpu::TextureFormat,
        extent: ViewportExtent,
    ) -> Result<Self, PickIdError> {
        let renderer = SketchRenderer::new(gpu, color_format);
        let solid_renderer = SolidRenderer::new(gpu, color_format);
        let chrome_pipeline = ChromePipeline::new(gpu, color_format);
        let convex_pipeline = ConvexPolyPipeline::new(gpu, color_format);
        let stroke_pipeline = StrokePipeline::new(gpu, color_format);
        let icon_pipeline = IconPipeline::new(gpu, color_format);
        let sdf_atlas = MaskAtlas::new(MaskAtlasParams::STANDARD);
        let text_pipeline = ChromeTextPipeline::new(gpu, color_format, sdf_atlas.extent());
        let chrome_shaper = Shaper::new();
        let sans_font = bone_text::load_font(bone_text::FontFace::Sans);
        let mono_font = bone_text::load_font(bone_text::FontFace::Mono);
        let scene = SketchScene::extract(&default_sketch())?;
        let camera = Camera2::new(extent).with_zoom(PixelsPerMm::new(INITIAL_ZOOM_PX_PER_MM));
        let theme = Arc::new(Theme::light());
        let style = with_theme_tokens(&Style::light(), &theme);
        let shell = shell::Shell::new();
        let document = empty_document();
        let last_saved_baseline = document.clone();
        let strings = strings::make_strings(bone_ui::strings::Locale::EnUs);
        let viewport_rect = empty_rect();
        let Some(undo_capacity) = NonZeroUsize::new(UNDO_CAPACITY) else {
            unreachable!("UNDO_CAPACITY constant is non-zero");
        };
        let loaded_settings = settings::load();
        let initial_hotkeys = match hotkeys::compose_table(&loaded_settings.hotkey_overrides) {
            Ok(table) => table,
            Err(e) => {
                tracing::warn!(error = %e, "stored hotkey overrides conflict, using defaults");
                build_hotkey_table()
            }
        };
        let state = AppState {
            extent,
            renderer,
            chrome_pipeline,
            convex_pipeline,
            stroke_pipeline,
            icon_pipeline,
            text_pipeline,
            sdf_atlas,
            chrome_shaper,
            sans_font,
            mono_font,
            scene,
            camera,
            style,
            theme,
            shell,
            document,
            mode: Mode::Idle,
            extrude_preview: None,
            model: EvaluatedModel::new(),
            model_passes: Vec::new(),
            mesh_quality: MeshQuality::Fine,
            changed_features: BTreeSet::new(),
            needs_rebuild: false,
            pending_reattach: None,
            pending_sketch_draft: None,
            extrude_reference_arm: None,
            revolve_arm: RevolveArm::Idle,
            sweep_arm: SweepArm::Idle,
            brep_selection: Vec::new(),
            hidden_planes: BTreeSet::new(),
            planes_visible: true,
            solid_renderer,
            solid_view: None,
            camera3: None,
            framed_extrude: None,
            navigator: ViewportNavigator::new(),
            view: view_cube::ViewUi::default(),
            focus: FocusManager::new(),
            hit_state: HitState::new(),
            hotkeys: initial_hotkeys,
            strings,
            viewport_rect,
            undo: UndoStack::with_capacity(undo_capacity),
            selection: Selection::default(),
            settings: loaded_settings,
            dim_editor: DimensionEditorState::default(),
            dim_editor_bounds: None,
            pending_exit: false,
            current_folder: None,
            documents_root: file_menu::documents_root(),
            file_picker: None,
            native_picker: None,
            step_job: None,
            pending_overwrite: None,
            last_saved: Some(last_saved_baseline),
            pending_discard: None,
            notification: None,
            shortcut_bar: None,
        };
        Ok(Self {
            state,
            input: InputState::default(),
            clock: FrameClock::start(),
            a11y: AccessTreeBuilder::new(),
        })
    }

    pub fn handle_input(
        &mut self,
        target: &impl FrameTarget,
        event: InputEvent,
    ) -> InputDispatched {
        match event {
            InputEvent::Resize(extent) => {
                self.state.extent = extent;
                self.state.camera = self.state.camera.with_extent(extent);
                self.state.viewport_rect = empty_rect();
            }
            InputEvent::Focus(focused) => {
                if !focused {
                    self.input.forget_pan_state();
                    self.state.navigator.end_drag();
                }
            }
            InputEvent::Modifiers(mods) => {
                self.input.modifiers = mods;
            }
            InputEvent::CursorMove(position) => {
                let state = &mut self.state;
                let prev = self.input.cursor_px;
                self.input.cursor_px = Some(position);
                let modal = modal_active(state);
                if !modal
                    && state.navigator.is_dragging()
                    && let Some(camera) = state.camera3
                    && let Some(region) = solid_viewport_region(state.viewport_rect, state.extent)
                    && let Some(cursor) = viewport_local_point(position, region)
                    && let Ok(next) = state.navigator.drag_to(cursor, camera, region.extent())
                {
                    apply_nav_camera(state, next);
                } else if !modal
                    && self.input.panning()
                    && let Some(p) = prev
                {
                    state.camera = pan_by_px(state.camera, position.x - p.x, position.y - p.y);
                } else if !modal && dragging_in_sketch(&state.mode) {
                    try_drag_to(state, position);
                }
            }
            InputEvent::CursorLeft => {
                self.input.cursor_px = None;
            }
            InputEvent::CursorEntered => {}
            InputEvent::Pointer { button, pressed } => {
                self.dispatch_pointer_button(target, button, pressed);
            }
            InputEvent::Wheel(delta) => {
                self.dispatch_wheel(delta);
            }
            InputEvent::KeyDown(key) => {
                self.dispatch_keydown(key);
            }
        }
        InputDispatched::after_input()
    }

    fn dispatch_wheel(&mut self, delta: ScrollDelta) {
        let over_viewport = self.input.cursor_in(self.state.viewport_rect);
        if !over_viewport || modal_active(&self.state) {
            self.input.pending_scroll_y += wheel_offset_px(delta);
            return;
        }
        self.dispatch_viewport_wheel(delta);
    }

    fn dispatch_viewport_wheel(&mut self, delta: ScrollDelta) {
        let state = &mut self.state;
        if !viewport_is_spatial(state) {
            state.camera = zoom_about(state.camera, self.input.cursor_px, zoom_factor(delta));
            return;
        }
        let (Some(camera), Some(region)) = (
            state.camera3,
            solid_viewport_region(state.viewport_rect, state.extent),
        ) else {
            return;
        };
        match delta {
            ScrollDelta::Pixels { x, y } => {
                if let Ok(next) = state.navigator.orbit_pixels(camera, region.extent(), x, y) {
                    apply_nav_camera(state, next);
                }
            }
            ScrollDelta::Lines { .. } => {
                if let Some(cursor) = self
                    .input
                    .cursor_px
                    .and_then(|p| viewport_local_point(p, region))
                    && let Ok(factor) = ZoomFactor::new(zoom_factor(delta))
                    && let Ok(next) = zoom_about_pixel(camera, region.extent(), cursor, factor)
                {
                    apply_nav_camera(state, next);
                }
            }
        }
    }

    fn dispatch_pointer_button(
        &mut self,
        target: &impl FrameTarget,
        button: PointerButton,
        pressed: bool,
    ) {
        let state = &mut self.state;
        let modal = modal_active(state);
        match button {
            PointerButton::Primary => {
                if pressed {
                    let in_viewport = self.input.cursor_in(state.viewport_rect);
                    let over_dim_editor = state
                        .dim_editor_bounds
                        .is_some_and(|r| self.input.cursor_in(r));
                    let dim_active = dim_flow_active(&state.mode);
                    self.input.left_pan = !modal && in_viewport && !over_dim_editor && !dim_active;
                    self.input.pending_pressed =
                        self.input.pending_pressed.with(PointerButton::Primary);
                    if !modal
                        && in_viewport
                        && !over_dim_editor
                        && !dim_active
                        && !self.input.shift()
                        && let Some(cursor) = self.input.cursor_px
                    {
                        if state.mode.active_tool().is_some() {
                            if let Some(world) = cursor_to_world(state.camera, cursor) {
                                try_place(state, world);
                            }
                        } else {
                            let additive = self.input.ctrl_or_meta();
                            handle_viewport_click(state, target, cursor, additive);
                        }
                    }
                } else {
                    self.input.left_pan = false;
                    state.mode = core::mem::take(&mut state.mode).end_drag();
                    self.input.pending_released =
                        self.input.pending_released.with(PointerButton::Primary);
                }
            }
            PointerButton::Secondary => {
                if pressed {
                    self.input.pending_pressed =
                        self.input.pending_pressed.with(PointerButton::Secondary);
                    if !modal && self.input.cursor_in(state.viewport_rect) {
                        abandon_spline_draft(state);
                        state.mode = core::mem::take(&mut state.mode).clear_pending();
                    }
                } else {
                    self.input.pending_released =
                        self.input.pending_released.with(PointerButton::Secondary);
                }
            }
            PointerButton::Middle => {
                if pressed {
                    let in_viewport = !modal && self.input.cursor_in(state.viewport_rect);
                    if in_viewport
                        && viewport_is_spatial(state)
                        && let Some(region) =
                            solid_viewport_region(state.viewport_rect, state.extent)
                        && let Some(cursor) = self
                            .input
                            .cursor_px
                            .and_then(|p| viewport_local_point(p, region))
                    {
                        state
                            .navigator
                            .begin_drag(drag_gesture(self.input.modifiers), cursor);
                    } else {
                        self.input.middle_pan = in_viewport;
                    }
                    self.input.pending_pressed =
                        self.input.pending_pressed.with(PointerButton::Middle);
                } else {
                    self.input.middle_pan = false;
                    state.navigator.end_drag();
                    self.input.pending_released =
                        self.input.pending_released.with(PointerButton::Middle);
                }
            }
        }
    }

    fn dispatch_keydown(&mut self, key: KeyDown) {
        let state = &mut self.state;
        let mods = self.input.modifiers;
        let named = match key.code {
            Some(UiKeyCode::Named(named)) => Some(named),
            Some(UiKeyCode::Char(_)) | None => None,
        };
        let typing_active = dim_flow_active(&state.mode) || state.focus.focused().is_some();
        if state.mode.is_sketch()
            && !typing_active
            && let Some(tool) = key.text.as_deref().and_then(sketch3_tool_for_key)
        {
            abandon_spline_draft(state);
            state.mode = core::mem::take(&mut state.mode).arm_tool(tool);
        }
        if state.mode.is_sketch3() && named == Some(NamedKey::Tab) && !key.repeat {
            cycle_sketch3_plane(state);
        }
        if state.mode.is_sketch()
            && named == Some(NamedKey::Enter)
            && !key.repeat
            && !state.mode.spline_draft().is_empty()
        {
            finalize_sketch3_spline(state);
        }
        let repeat_space = key.repeat && named == Some(NamedKey::Space);
        if let Some(code) = key.code
            && !repeat_space
        {
            self.input.pending_keys.push(UiKeyEvent::new(code, mods));
        }
        if let Some(typed) = key.text {
            let filtered: String = typed.chars().filter(|c| !c.is_control()).collect();
            if !filtered.is_empty() {
                self.input.pending_text.push_str(&filtered);
            }
        }
        let suppress_camera = typing_active;
        if matches!(named, Some(NamedKey::Escape)) && state.notification.is_some() {
            state.notification = None;
        }
        if let Some(nav) = key.nav
            && !suppress_camera
        {
            if viewport_is_spatial(state) {
                if let Some(next) = keyboard_camera3(nav, &self.input, state) {
                    apply_nav_camera(state, next);
                }
            } else if let Some(next) = keyboard_camera(nav, &self.input, state) {
                state.camera = next;
            }
        }
    }

    pub fn render_frame(&mut self, target: &mut impl FrameTarget) -> FrameReport {
        render_frame(
            &mut self.state,
            target,
            &mut self.input,
            &mut self.a11y,
            self.clock.now(),
        )
    }

    #[must_use]
    pub fn access_tree(&self) -> accesskit::TreeUpdate {
        self.a11y
            .build(&self.state.strings, self.state.focus.focused())
    }

    pub fn open_document(&mut self, path: PathBuf) -> Result<(), OpenError> {
        if file_menu::is_step_file(&path) {
            let document =
                bone_interop::read(&path, bone_interop::CancelFlag::never()).map_err(|source| {
                    OpenError::ImportStep {
                        path: path.clone(),
                        source,
                    }
                })?;
            install_imported_document(&mut self.state, document);
        } else {
            let folder = DocumentFolder::new(path.clone());
            let document = bone_document::load(&folder)
                .map_err(|source| OpenError::LoadFolder { path, source })?;
            install_loaded_document(&mut self.state, document, Some(folder));
        }
        Ok(())
    }

    pub fn set_theme(&mut self, mode: bone_ui::theme::ThemeMode) {
        self.state.theme = Arc::new(match mode {
            bone_ui::theme::ThemeMode::Light => Theme::light(),
            bone_ui::theme::ThemeMode::Dark => Theme::dark(),
        });
        let base = match mode {
            bone_ui::theme::ThemeMode::Light => Style::light(),
            bone_ui::theme::ThemeMode::Dark => Style::default(),
        };
        self.state.style = with_theme_tokens(&base, &self.state.theme);
    }

    pub fn clock_mut(&mut self) -> &mut FrameClock {
        &mut self.clock
    }

    #[must_use]
    pub fn next_wake_deadline(&self) -> Option<FrameInstant> {
        next_wake_deadline(&self.state, self.clock.now())
    }
}

fn next_wake_deadline(state: &AppState, now: FrameInstant) -> Option<FrameInstant> {
    let native_poll = state
        .native_picker
        .is_some()
        .then(|| now.after(std::time::Duration::from_millis(40)));
    let step_poll = state
        .step_job
        .is_some()
        .then(|| now.after(std::time::Duration::from_millis(40)));
    let rename_deadline = state
        .shell
        .state
        .feature_tree
        .pending_rename
        .map(|pending| {
            let window = bone_ui::input::DoubleClickWindow::DEFAULT.duration();
            let slack = std::time::Duration::from_millis(8);
            pending.at.after(window + slack)
        });
    let tween_tick = state
        .view
        .tween
        .is_some()
        .then(|| now.after(std::time::Duration::from_millis(8)));
    [native_poll, step_poll, rename_deadline, tween_tick]
        .into_iter()
        .flatten()
        .min()
}

#[allow(
    clippy::cast_precision_loss,
    reason = "viewport pixel counts at any realistic display size fit f32 mantissa"
)]
#[allow(
    clippy::too_many_lines,
    reason = "splitting hides the per-outcome dispatch table"
)]
fn render_frame(
    state: &mut AppState,
    target: &mut impl FrameTarget,
    input_state: &mut InputState,
    a11y: &mut AccessTreeBuilder,
    now: FrameInstant,
) -> FrameReport {
    poll_native_picker(state);
    poll_step_job(state);
    let extent = state.extent;
    let layout_size = layout_size_from_extent(extent);
    let theme = Arc::clone(&state.theme);
    let mut input = input_state.drain_snapshot(now);
    step_view_tween(state, input.frame);
    let mut hits = HitFrame::new();
    a11y.begin_frame();
    let scopes = scopes_for_mode(&state.mode);
    let chrome_cursor_world = input_state
        .cursor_px
        .filter(|c| state.viewport_rect.contains(window_to_layout_pos(*c)))
        .and_then(|c| cursor_to_world(state.camera, c));
    let FrameOutcomes {
        mut frame,
        hotkey_actions,
        dim: dim_outcome,
        dim_conflict: conflict_outcome,
        picker: picker_outcome,
        overwrite: overwrite_outcome,
        discard: discard_outcome,
        step_progress: step_progress_outcome,
        notification: notification_outcome,
        shortcut_bar: shortcut_bar_outcome,
    } = run_frame_ui(
        state,
        theme,
        &mut input,
        &mut hits,
        a11y,
        &scopes,
        layout_size,
        chrome_cursor_world,
    );
    state.dim_editor_bounds = apply_popup_overlays(
        &mut frame.overlay_paints,
        dim_outcome.as_ref(),
        conflict_outcome.as_ref(),
        picker_outcome.as_ref(),
        overwrite_outcome.as_ref(),
        discard_outcome.as_ref(),
        step_progress_outcome.as_ref(),
        notification_outcome.as_ref(),
        shortcut_bar_outcome.as_ref(),
    );
    apply_shortcut_bar_outcome(state, shortcut_bar_outcome.as_ref());
    let picker_kind = state.file_picker.as_ref().map(|s| s.kind);
    if let (Some(cmd), Some(kind)) = (picker_outcome.and_then(|o| o.command), picker_kind) {
        apply_picker_command(state, kind, cmd);
    }
    apply_overwrite_outcome(state, overwrite_outcome);
    apply_discard_outcome(state, discard_outcome);
    apply_step_progress_outcome(state, step_progress_outcome);
    apply_notification_outcome(state, notification_outcome);
    let claimed_pointer = dim_outcome.as_ref().is_some_and(|o| o.claimed_pointer);
    let mut frame = if claimed_pointer {
        suppress_pointer_activations(frame)
    } else {
        frame
    };
    state.viewport_rect = frame.viewport_rect;
    state.hit_state = resolve(&state.hit_state, &hits, &input, state.focus.focused());
    let kick = any_actionable_interaction(&state.hit_state);
    let entry_sketch3 = state.mode.sketch3_id();
    if let Some(plane) = frame.plane_picked
        && !state.mode.is_sketch()
    {
        begin_plane_sketch(state, plane, input.frame);
    }
    if let Some(feature) = frame.datum_sketch_target
        && !state.mode.is_sketch()
    {
        begin_datum_sketch(state, feature, input.frame);
    }
    if let Some(sketch_id) = frame.sketch_activated
        && apply_sweep_sketch_pick(state, sketch_id)
    {
        frame.sketch_activated = None;
    }
    if let Some(sketch_id) = frame.sketch_activated
        && matches!(state.mode, Mode::Idle)
    {
        edit_existing_sketch(state, sketch_id, input.frame);
    }
    let escape_requested = hotkey_actions.contains(&sketch_mode::ESCAPE_ACTION);
    if escape_requested {
        state.pending_reattach = None;
        state.extrude_reference_arm = None;
        state.revolve_arm = RevolveArm::Idle;
        state.sweep_arm = SweepArm::Idle;
    }
    apply_extrude_edit(state, frame.extrude_edit);
    apply_extrude_arm(state, frame.extrude_arm_reference);
    apply_extrude_confirm(state, frame.confirm_action);
    apply_revolve_edit(state, frame.revolve_edit);
    apply_revolve_confirm(state, frame.confirm_action);
    apply_sweep_edit(state, frame.sweep_edit);
    apply_feature_arm(state, frame.feature_arm);
    autoselect_sweep_path(state);
    apply_sweep_confirm(state, frame.confirm_action);
    apply_datum_confirm(state, frame.confirm_action);
    let prev_spline_sketch = state.mode.sketch_id();
    let prev_spline_fresh = state.mode.spline_draft_fresh();
    let prev_active_sketch = active_sketch_id(&state.mode);
    state.mode = next_mode(core::mem::take(&mut state.mode), &frame, escape_requested);
    if !matches!(state.mode, Mode::Sweep(_)) {
        state.sweep_arm = SweepArm::Idle;
    }
    reconcile_spline_draft(state, prev_spline_sketch, &prev_spline_fresh);
    apply_feature_tool(state, frame.activated_feature_tool);
    apply_extrude_activation(state, frame.extrude_activated);
    commit_pending_sketch_draft(state);
    gc_abandoned_sketch(state, entry_sketch3);
    if active_sketch_id(&state.mode) != prev_active_sketch {
        refresh_active_scene(state);
    }
    apply_dimension_outcome(state, dim_outcome);
    apply_dim_conflict_outcome(state, conflict_outcome);
    apply_dimension_request(state, frame.activated_dimension);
    let dimension_edit = match frame.confirm_action {
        Some(shell::ConfirmAction::Cancel) => None,
        _ => frame.dimension_edit,
    };
    apply_dimension_edit(state, dimension_edit);
    sync_solid_view(state);
    let solid_region = solid_viewport_region(state.viewport_rect, extent);
    let reference_scene = reference_plane_scene(state);
    sync_solid_camera(state, solid_region, reference_scene.as_ref());
    let cursor_layout = input_state.cursor_px.map(window_to_layout_pos);
    apply_hotkey_actions(state, &hotkey_actions, cursor_layout, input.frame);
    apply_view_pick(state, frame.view_pick, input.frame);
    apply_view_menu(state, frame.view_menu, input.frame);
    apply_menu_action(state, frame.menu_action);
    apply_settings_change(state, frame.settings_change);
    apply_relation_action(state, frame.activated_relation);
    apply_sketch_rename(state, frame.sketch_rename.clone());
    apply_extrude_rename(state, frame.extrude_rename.clone());
    apply_feature_command(state, frame.feature_command);
    apply_feature_reorder(state, frame.feature_reorder);
    apply_rollback_change(state, frame.rollback_change);
    begin_reattach(state, frame.reattach_request);
    let cursor_world = input_state
        .cursor_px
        .filter(|c| state.viewport_rect.contains(window_to_layout_pos(*c)))
        .and_then(|c| cursor_to_world(state.camera, c));
    let mut preview = build_preview(&state.mode, &state.document, cursor_world, &state.camera);
    extend_preview_with_brep_selection(&mut preview, state);
    let main_layer = build_chrome_layer(state, &frame.paints);
    let label_paints = reference_scene
        .as_ref()
        .map(|scene| plane_label_paints(state, scene, solid_region))
        .unwrap_or_default();
    let mut overlay_layer = build_chrome_layer(state, &frame.overlay_paints);
    let plane_labels = build_chrome_layer(state, &label_paints);
    overlay_layer.chrome.extend(plane_labels.chrome);
    overlay_layer.convex.extend(plane_labels.convex);
    overlay_layer.stroke.extend(plane_labels.stroke);
    overlay_layer.icons.extend(plane_labels.icons);
    overlay_layer.glyphs.extend(plane_labels.glyphs);
    let atlas_pixels = state.sdf_atlas.pixels();
    let atlas_version = state.sdf_atlas.version();
    let viewport_px = [
        extent.width().value() as f32,
        extent.height().value() as f32,
    ];
    let overlay_frame = sketch_overlay_frame(state, solid_region);
    let (sketch3_preview, sketch3_overlay) =
        sketch3_frame_layers(state, solid_region, input_state.cursor_px);
    let renderer = &mut state.renderer;
    let mut chrome_stage = ChromeStage {
        chrome: &mut state.chrome_pipeline,
        convex: &mut state.convex_pipeline,
        stroke: &mut state.stroke_pipeline,
        icon: &mut state.icon_pipeline,
        text: &mut state.text_pipeline,
        atlas_pixels,
        atlas_version,
        viewport_px,
    };
    let scene = &state.scene;
    let style = &state.style;
    renderer.prepare(scene, style);
    let solid_frame = match overlay_frame {
        Some(frame) => Some(frame),
        None => solid_region.and_then(|region| {
            preview_solid_frame(state.solid_view.as_ref(), state.camera3, region)
                .map(|(_, frame)| frame)
                .or_else(|| SolidFrameView::new(state.camera3?, region).ok())
        }),
    };
    let overlay = overlay_frame.is_some() && state.solid_view.is_some();
    let viewport = ViewportEncode {
        frame: solid_frame,
        solid: state.solid_view.as_ref(),
        planes: reference_scene.as_ref(),
        overlay,
        solid_renderer: &state.solid_renderer,
        sketch_renderer: renderer,
        scene,
        preview: &preview,
        sketch3_preview,
        sketch3_overlay,
        camera: state.camera,
        style,
    };
    target.render(|encoder, color, pick, depth| {
        viewport.encode(encoder, color, pick, depth);
        chrome_stage.encode_layered(encoder, color, &main_layer, &overlay_layer);
    });
    FrameReport {
        kick,
        exit: state.pending_exit,
    }
}

struct ViewportEncode<'a> {
    frame: Option<SolidFrameView>,
    solid: Option<&'a SolidViewData>,
    planes: Option<&'a ReferencePlaneScene>,
    overlay: bool,
    solid_renderer: &'a SolidRenderer,
    sketch_renderer: &'a SketchRenderer,
    scene: &'a SketchScene,
    preview: &'a SketchPreview,
    sketch3_preview: Option<EdgeScene>,
    sketch3_overlay: Option<Sketch3Overlay>,
    camera: Camera2,
    style: &'a Style,
}

impl ViewportEncode<'_> {
    fn draw_reference_planes(
        &self,
        encoder: &mut wgpu::CommandEncoder,
        color: &wgpu::TextureView,
        pick: &wgpu::TextureView,
        depth: &wgpu::TextureView,
        frame: &SolidFrameView,
        planes: &ReferencePlaneScene,
    ) {
        let fill = reference_planes::fill_scene(planes, |plane| {
            if plane.highlighted {
                PLANE_FILL_HIGHLIGHT
            } else {
                PLANE_FILL
            }
        });
        self.solid_renderer.encode_planes(
            encoder,
            RenderTargets::new(color, pick),
            depth,
            &fill,
            frame,
        );
        planes.planes.iter().for_each(|plane| {
            let color_rgba = if plane.highlighted {
                PLANE_BORDER_HIGHLIGHT
            } else {
                PLANE_BORDER
            };
            self.solid_renderer.encode_styled_edges(
                encoder,
                RenderTargets::new(color, pick),
                depth,
                &plane.border(),
                frame,
                EdgeStroke::solid(color_rgba.to_array()),
            );
        });
    }

    fn encode(
        &self,
        encoder: &mut wgpu::CommandEncoder,
        color: &wgpu::TextureView,
        pick: &wgpu::TextureView,
        depth: &wgpu::TextureView,
    ) {
        let Some(frame) = self.frame else {
            self.sketch_renderer.encode_passes(
                encoder,
                RenderTargets::new(color, pick),
                self.scene,
                self.preview,
                self.camera,
                self.style,
            );
            return;
        };
        let empty_faces = SolidScene::empty();
        let empty_edges = EdgeScene::empty();
        let (faces, edges) = match self.solid {
            Some(view) => (&view.faces, &view.edges),
            None => (&empty_faces, &empty_edges),
        };
        self.solid_renderer.encode_passes(
            encoder,
            RenderTargets::new(color, pick),
            depth,
            faces,
            edges,
            &bone_render::SolidDisplay {
                view: &frame,
                style: self.style,
                mode: DisplayMode::ShadedWithEdges,
            },
        );
        if let Some(planes) = self.planes {
            self.draw_reference_planes(encoder, color, pick, depth, &frame, planes);
        }
        if let Some(preview) = &self.sketch3_preview {
            self.solid_renderer.encode_preview_edges(
                encoder,
                RenderTargets::new(color, pick),
                depth,
                preview,
                &frame,
                self.style,
            );
        }
        if let Some(overlay) = &self.sketch3_overlay {
            let strokes = self.style.strokes();
            self.solid_renderer.encode_overlay_solid(
                encoder,
                RenderTargets::new(color, pick),
                depth,
                &overlay.points,
                &frame,
                self.style,
            );
            self.solid_renderer.encode_styled_edges(
                encoder,
                RenderTargets::new(color, pick),
                depth,
                &overlay.normal_lines,
                &frame,
                EdgeStroke::solid(strokes.stroke().to_rgba_array()),
            );
            self.solid_renderer.encode_styled_edges(
                encoder,
                RenderTargets::new(color, pick),
                depth,
                &overlay.construction_lines,
                &frame,
                EdgeStroke {
                    color: strokes.construction().to_rgba_array(),
                    dash_period_px: strokes.construction_dash_period_px(),
                    dash_on_ratio: strokes.construction_dash_on_ratio(),
                },
            );
            self.solid_renderer.encode_styled_edges(
                encoder,
                RenderTargets::new(color, pick),
                depth,
                &overlay.selected_lines,
                &frame,
                EdgeStroke::solid(SKETCH3_SELECTED.to_array()),
            );
        }
        if self.overlay {
            self.sketch_renderer.encode_overlay_passes(
                encoder,
                RenderTargets::new(color, pick),
                self.scene,
                self.preview,
                self.camera,
                self.style,
            );
        }
    }
}

struct ChromeStage<'a> {
    chrome: &'a mut ChromePipeline,
    convex: &'a mut ConvexPolyPipeline,
    stroke: &'a mut StrokePipeline,
    icon: &'a mut IconPipeline,
    text: &'a mut ChromeTextPipeline,
    atlas_pixels: &'a [u8],
    atlas_version: u64,
    viewport_px: [f32; 2],
}

fn merge_layers<T: Copy>(main: &[T], overlay: &[T]) -> Vec<T> {
    main.iter().chain(overlay.iter()).copied().collect()
}

fn count_u32(len: usize) -> u32 {
    let Ok(count) = u32::try_from(len) else {
        unreachable!("instance counts fit in u32");
    };
    count
}

impl ChromeStage<'_> {
    fn encode_layered(
        &mut self,
        encoder: &mut wgpu::CommandEncoder,
        color: &wgpu::TextureView,
        main: &ChromeLayer,
        overlay: &ChromeLayer,
    ) {
        let chrome = merge_layers(&main.chrome, &overlay.chrome);
        let convex = merge_layers(&main.convex, &overlay.convex);
        let stroke = merge_layers(&main.stroke, &overlay.stroke);
        let icons = merge_layers(&main.icons, &overlay.icons);
        let glyphs = merge_layers(&main.glyphs, &overlay.glyphs);
        self.chrome.upload(self.viewport_px, &chrome);
        self.convex.upload(self.viewport_px, &convex);
        self.stroke.upload(self.viewport_px, &stroke);
        self.icon.upload(self.viewport_px, &icons);
        self.text.upload(
            self.viewport_px,
            self.atlas_pixels,
            self.atlas_version,
            &glyphs,
        );
        let (mc, tc) = (count_u32(main.chrome.len()), count_u32(chrome.len()));
        let (mv, tv) = (count_u32(main.convex.len()), count_u32(convex.len()));
        let (ms, ts) = (count_u32(main.stroke.len()), count_u32(stroke.len()));
        let (mi, ti) = (count_u32(main.icons.len()), count_u32(icons.len()));
        let (mg, tg) = (count_u32(main.glyphs.len()), count_u32(glyphs.len()));
        self.chrome.draw_range(encoder, color, 0..mc);
        self.convex.draw_range(encoder, color, 0..mv);
        self.stroke.draw_range(encoder, color, 0..ms);
        self.icon.draw_range(encoder, color, 0..mi);
        self.text.draw_range(encoder, color, 0..mg);
        self.chrome.draw_range(encoder, color, mc..tc);
        self.convex.draw_range(encoder, color, mv..tv);
        self.stroke.draw_range(encoder, color, ms..ts);
        self.icon.draw_range(encoder, color, mi..ti);
        self.text.draw_range(encoder, color, mg..tg);
    }
}

fn any_actionable_interaction(hit_state: &HitState) -> bool {
    use bone_ui::hit_test::InteractionState;
    hit_state.interactions.values().any(|i| {
        i.state.contains(InteractionState::CLICK)
            || i.state.contains(InteractionState::DOUBLE_CLICK)
            || i.state.contains(InteractionState::DRAG_START)
            || i.state.contains(InteractionState::DRAG_RELEASE)
    })
}

struct ChromeLayer {
    chrome: Vec<ChromeInstance>,
    convex: Vec<ConvexInstance>,
    stroke: Vec<StrokeInstance>,
    icons: Vec<IconInstance>,
    glyphs: Vec<SdfGlyphInstance>,
}

fn build_chrome_layer(
    state: &mut AppState,
    paints: &[bone_ui::widgets::WidgetPaint],
) -> ChromeLayer {
    let chrome = chrome::paint_to_instances(&state.theme, paints);
    let convex = chrome::paint_to_convex_instances(paints);
    let stroke = chrome::paint_to_stroke_instances(paints);
    let spans = chrome::paint_to_text_spans(paints, &state.strings);
    let glyphs = chrome::build_glyph_instances(
        &spans,
        &mut state.sdf_atlas,
        &mut state.chrome_shaper,
        &state.sans_font,
        &state.mono_font,
    );
    ChromeLayer {
        chrome,
        convex,
        stroke,
        icons: chrome::paint_to_icon_instances(paints),
        glyphs,
    }
}

fn apply_hotkey_actions(
    state: &mut AppState,
    actions: &[ActionId],
    cursor_layout: Option<LayoutPos>,
    now: FrameInstant,
) {
    actions
        .iter()
        .filter_map(|a| hotkeys::command_for_action(*a))
        .for_each(|cmd| dispatch_hotkey_command(state, cmd, cursor_layout, now));
}

fn dispatch_hotkey_command(
    state: &mut AppState,
    cmd: hotkeys::HotkeyCommand,
    cursor_layout: Option<LayoutPos>,
    now: FrameInstant,
) {
    use hotkeys::HotkeyCommand as C;
    match cmd {
        C::Undo => {
            if state.undo.undo(&mut state.document) {
                refresh_active_scene(state);
            }
        }
        C::Redo => {
            if state.undo.redo(&mut state.document) {
                refresh_active_scene(state);
            }
        }
        C::NewDocument => apply_menu_action(state, Some(shell::MenuAction::NewDocument)),
        C::OpenDocument => apply_menu_action(state, Some(shell::MenuAction::OpenDocument)),
        C::SaveDocument => apply_menu_action(state, Some(shell::MenuAction::SaveDocument)),
        C::ImportStep => apply_menu_action(state, Some(shell::MenuAction::ImportStep)),
        C::ExportStep => apply_menu_action(state, Some(shell::MenuAction::ExportStep)),
        C::ZoomFit => apply_menu_action(state, Some(shell::MenuAction::ZoomFit)),
        C::Quit => {
            state.pending_exit = true;
        }
        C::RebuildChanged => rebuild_changed(state),
        C::ForceRebuild => force_rebuild_all(state),
        C::OpenShortcutBar => {
            if state.shortcut_bar.is_none() {
                let anchor = cursor_layout.unwrap_or(LayoutPos::ORIGIN);
                state.shortcut_bar = Some(shortcut_bar::ShortcutBarState { anchor });
            }
        }
        C::ToggleConstruction => apply_construction_toggle(state),
        C::Mirror => apply_mirror(state),
        C::ConvertEntities => apply_convert_entities(state),
        C::StandardView(view) => {
            if view_nav_enabled(state) {
                apply_view_pick(state, Some(view_cube::ViewPick::Standard(view)), now);
            }
        }
        C::ToggleViewSelector => {
            if view_nav_enabled(state) {
                state.view.toggle_selector();
            }
        }
        C::ToggleViewCube => {
            if view_nav_enabled(state) {
                state.view.toggle_cube();
            }
        }
        C::DeleteSelection => apply_delete_selection(state),
        C::SmartDimension => apply_smart_dimension(state),
        C::SelectAll | C::EnterSketch | C::Trim | C::Extend => {
            notify_stub(state, hotkeys::label_for_command(cmd));
        }
    }
}

fn selected_entities3(state: &AppState, base: &bone_document::Sketch3) -> Vec<SketchEntityId> {
    let ids = state.selection.entity_ids();
    if state.mode.is_sketch3() {
        ids.to_vec()
    } else {
        let view = base.project();
        ids.iter().filter_map(|id2| view.to_3d(*id2)).collect()
    }
}

fn apply_delete_selection(state: &mut AppState) {
    let Some(sketch_id) = state.mode.sketch_id() else {
        return;
    };
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let pruned = match &state.selection {
        Selection::Entities(_) => {
            let ids3 = selected_entities3(state, base);
            if ids3.is_empty() {
                return;
            }
            ids3.iter()
                .fold(base.clone(), |acc, id| acc.without_entity(*id))
        }
        selection => match relation_to_delete(base, selection) {
            Some(rid) => base.without_relation(rid),
            None => return,
        },
    };
    state.undo.record(state.document.clone());
    state
        .document
        .replace_sketch(sketch_id, sketch3_mode::solved(pruned));
    state.selection = Selection::default();
    refresh_active_scene(state);
}

fn relation_to_delete(
    base: &bone_document::Sketch3,
    selection: &Selection,
) -> Option<bone_types::SketchRelationId> {
    let item = match selection {
        Selection::Relation(id) => {
            if base.relations().contains_key(*id) {
                return Some(*id);
            }
            SketchItemId::Relation(*id)
        }
        Selection::Dimension(id) => SketchItemId::Dimension(*id),
        Selection::Entities(_) => return None,
    };
    sketch3_project::relation_for_item(base, base.plane(), item)
}

fn apply_construction_toggle(state: &mut AppState) {
    let Some(sketch_id) = state.mode.sketch_id() else {
        return;
    };
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let ids3 = selected_entities3(state, base);
    let pivot = ids3.iter().find_map(|id| {
        let entity = base.entities().get(*id)?;
        (!entity.is_point()).then(|| entity.for_construction())
    });
    let Some(current) = pivot else {
        return;
    };
    let target = !current;
    let next = ids3
        .iter()
        .fold(base.clone(), |acc, id| acc.with_construction(*id, target));
    state.undo.record(state.document.clone());
    state.document.replace_sketch(sketch_id, next);
    refresh_active_scene(state);
}

fn apply_driven_toggle(state: &mut AppState) {
    let Some(sketch_id) = state.mode.sketch_id() else {
        return;
    };
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let Some(rid) = relation_to_delete(base, &state.selection) else {
        return;
    };
    let Some(relation) = base.relations().get(rid).copied() else {
        return;
    };
    if relation.as_reference() == relation.as_driving() {
        return;
    }
    let next = base.with_reference(rid, !relation.is_reference());
    state.undo.record(state.document.clone());
    state
        .document
        .replace_sketch(sketch_id, sketch3_mode::solved(next));
    refresh_active_scene(state);
}

fn apply_mirror(state: &mut AppState) {
    let Mode::Sketch { sketch_id, .. } = state.mode else {
        return;
    };
    let entity_ids: Vec<bone_types::SketchEntityId> = state.selection.entity_ids().to_vec();
    if entity_ids.is_empty() {
        return;
    }
    let Some(sketch) = state.document.sketch(sketch_id) else {
        return;
    };
    let axis_lines: Vec<(bone_types::SketchEntityId, LineData)> = entity_ids
        .iter()
        .filter_map(|id| match sketch.entities().get(*id)? {
            SketchEntity::Line(l) => Some((*id, *l)),
            _ => None,
        })
        .collect();
    let [(axis_id, axis_line)] = axis_lines.as_slice() else {
        notify_mirror_hint(state);
        return;
    };
    let axis_id = *axis_id;
    let Some(pa) = lookup_point(&sketch, axis_line.a()) else {
        return;
    };
    let Some(pb) = lookup_point(&sketch, axis_line.b()) else {
        return;
    };
    let axis = MirrorAxis::from_points(pa, pb);
    if axis.is_degenerate() {
        notify_mirror_hint(state);
        return;
    }
    let source_ids: std::collections::BTreeSet<bone_types::SketchEntityId> = entity_ids
        .iter()
        .copied()
        .filter(|id| *id != axis_id)
        .collect();
    if source_ids.is_empty() {
        notify_mirror_hint(state);
        return;
    }
    let mirrored = match mirror_targets(sketch.clone(), &source_ids, axis_id, &axis) {
        Ok(next) => next,
        Err(e) => {
            tracing::warn!(error = %e, "mirror failed");
            return;
        }
    };
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let basis = base.plane();
    let view = sketch3_project::project(base, basis);
    let (next3, _) =
        sketch3_project::embed_delta(base, &view, &mirrored, sketch3_project::PlaneLock::Locked);
    state.undo.record(state.document.clone());
    state
        .document
        .replace_sketch(sketch_id, sketch3_mode::solved(next3));
    state.selection = Selection::default();
    refresh_active_scene(state);
}

fn apply_convert_entities(state: &mut AppState) {
    let Mode::Sketch { sketch_id, .. } = state.mode else {
        return;
    };
    let Some(sketch) = state.document.sketch(sketch_id) else {
        return;
    };
    let basis = sketch.plane();
    let curves: Vec<ProjectedCurve> = state
        .brep_selection
        .iter()
        .flat_map(|item| convert_targets(&state.model, *item, basis))
        .collect();
    if curves.is_empty() {
        return;
    }
    let converted = match curves
        .into_iter()
        .try_fold(sketch.clone(), convert_one_curve)
    {
        Ok(next) => next,
        Err(e) => {
            tracing::warn!(error = %e, "convert entities failed");
            return;
        }
    };
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let view = sketch3_project::project(base, basis);
    let (next3, _) =
        sketch3_project::embed_delta(base, &view, &converted, sketch3_project::PlaneLock::Locked);
    record_sketch_edit(state, sketch_id);
    state
        .document
        .replace_sketch(sketch_id, sketch3_mode::solved(next3));
    state.brep_selection.clear();
    state.selection = Selection::default();
    refresh_active_scene(state);
}

fn convert_targets(
    model: &EvaluatedModel,
    item: PickedItem,
    basis: SketchPlaneBasis,
) -> Vec<ProjectedCurve> {
    match item {
        PickedItem::BrepEdge(edge) => model
            .body_at(edge.feature(), edge.ordinal())
            .and_then(|solid| solid.project_edge(edge.edge(), basis))
            .into_iter()
            .collect(),
        PickedItem::BrepFace(face) => model
            .body_at(face.feature(), face.ordinal())
            .map(|solid| {
                solid
                    .face_boundary_edges(face.face())
                    .iter()
                    .filter_map(|edge| solid.project_edge(*edge, basis))
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default(),
        _ => Vec::new(),
    }
}

fn extend_preview_with_brep_selection(preview: &mut SketchPreview, state: &AppState) {
    if state.brep_selection.is_empty() {
        return;
    }
    let Some(sketch) = state
        .mode
        .sketch_id()
        .and_then(|id| state.document.sketch(id))
    else {
        return;
    };
    let basis = sketch.plane();
    state
        .brep_selection
        .iter()
        .flat_map(|item| convert_targets(&state.model, *item, basis))
        .for_each(|curve| match curve {
            ProjectedCurve::Line { a, b } => preview.segments.push((a, b)),
            ProjectedCurve::Circle { center, radius } => {
                preview.circles.push(PreviewCircle { center, radius });
            }
            ProjectedCurve::Arc { center, start, end } => {
                if let Some(arc) = highlight_arc(center, start, end) {
                    preview.arcs.push(arc);
                }
            }
        });
}

fn highlight_arc(center: Point2, start: Point2, end: Point2) -> Option<PreviewArc> {
    use core::f64::consts::{PI, TAU};
    let (cx, cy) = center.coords_mm();
    let (sx, sy) = start.coords_mm();
    let (ex, ey) = end.coords_mm();
    let radius = (sx - cx).hypot(sy - cy);
    if radius <= 0.0 {
        return None;
    }
    let start_angle = (sy - cy).atan2(sx - cx);
    let mut sweep = (ey - cy).atan2(ex - cx) - start_angle;
    while sweep <= -PI {
        sweep += TAU;
    }
    while sweep > PI {
        sweep -= TAU;
    }
    Some(PreviewArc {
        center,
        radius: Length::new::<millimeter>(radius),
        start_angle: Angle::new::<radian>(start_angle),
        sweep_angle: Angle::new::<radian>(sweep),
    })
}

fn convert_one_curve(sketch: Sketch, curve: ProjectedCurve) -> Result<Sketch, SketchEditError> {
    match curve {
        ProjectedCurve::Line { a, b } => {
            let (sketch, start) = add_sketch_point(sketch, a)?;
            let (sketch, end) = add_sketch_point(sketch, b)?;
            add_sketch_entity(sketch, SketchEntity::line(start, end, false))
        }
        ProjectedCurve::Arc { center, start, end } => {
            let (sketch, center) = add_sketch_point(sketch, center)?;
            let (sketch, start) = add_sketch_point(sketch, start)?;
            let (sketch, end) = add_sketch_point(sketch, end)?;
            add_sketch_entity(sketch, SketchEntity::arc(center, start, end, false))
        }
        ProjectedCurve::Circle { center, radius } => {
            let (sketch, center) = add_sketch_point(sketch, center)?;
            add_sketch_entity(sketch, SketchEntity::circle(center, radius, false))
        }
    }
}

fn add_sketch_point(
    sketch: Sketch,
    at: Point2,
) -> Result<(Sketch, bone_types::SketchEntityId), SketchEditError> {
    let (next, outcome) = sketch.apply(SketchEdit::AddEntity(SketchEntity::point(at)))?;
    let EditOutcome::Entity(id) = outcome else {
        unreachable!("adding a point yields an entity outcome");
    };
    Ok((next, id))
}

fn add_sketch_entity(sketch: Sketch, entity: SketchEntity) -> Result<Sketch, SketchEditError> {
    sketch
        .apply(SketchEdit::AddEntity(entity))
        .map(|(next, _)| next)
}

fn notify_mirror_hint(state: &mut AppState) {
    state.notification = Some(Notification {
        kind: NotificationKind::Info,
        headline: strings::HOTKEY_LABEL_MIRROR,
        detail: Some(
            state
                .strings
                .resolve(strings::NOTIFY_MIRROR_SELECTION_HINT)
                .to_owned(),
        ),
    });
}

#[derive(Copy, Clone, Debug)]
struct MirrorAxis {
    anchor_x: f64,
    anchor_y: f64,
    direction_x: f64,
    direction_y: f64,
    length_sq: f64,
}

impl MirrorAxis {
    fn from_points(a: Point2, b: Point2) -> Self {
        let (ax, ay) = a.coords_mm();
        let (bx, by) = b.coords_mm();
        let dx = bx - ax;
        let dy = by - ay;
        Self {
            anchor_x: ax,
            anchor_y: ay,
            direction_x: dx,
            direction_y: dy,
            length_sq: dx * dx + dy * dy,
        }
    }

    fn is_degenerate(self) -> bool {
        !self.length_sq.is_finite() || self.length_sq <= f64::EPSILON
    }

    fn reflect(self, p: Point2) -> Point2 {
        let (px, py) = p.coords_mm();
        let vx = px - self.anchor_x;
        let vy = py - self.anchor_y;
        let t = (vx * self.direction_x + vy * self.direction_y) / self.length_sq;
        let foot_x = self.anchor_x + t * self.direction_x;
        let foot_y = self.anchor_y + t * self.direction_y;
        Point2::from_mm(2.0 * foot_x - px, 2.0 * foot_y - py)
    }

    fn is_on_axis(self, p: Point2) -> bool {
        let (px, py) = p.coords_mm();
        let vx = px - self.anchor_x;
        let vy = py - self.anchor_y;
        let cross = vx * self.direction_y - vy * self.direction_x;
        let perp_dist_sq = cross * cross / self.length_sq;
        perp_dist_sq < ON_AXIS_TOLERANCE_MM_SQ
    }
}

const ON_AXIS_TOLERANCE_MM_SQ: f64 = 1e-12;

struct MirrorBuilder {
    sketch: Sketch,
    point_map: std::collections::BTreeMap<bone_types::SketchEntityId, bone_types::SketchEntityId>,
    entity_map: std::collections::BTreeMap<bone_types::SketchEntityId, bone_types::SketchEntityId>,
}

impl MirrorBuilder {
    fn new(sketch: Sketch) -> Self {
        Self {
            sketch,
            point_map: std::collections::BTreeMap::new(),
            entity_map: std::collections::BTreeMap::new(),
        }
    }

    fn mirror_point(
        &mut self,
        id: bone_types::SketchEntityId,
        axis: &MirrorAxis,
    ) -> Result<bone_types::SketchEntityId, bone_document::SketchEditError> {
        if let Some(&existing) = self.point_map.get(&id) {
            return Ok(existing);
        }
        let pos = require_point(&self.sketch, id)?;
        if axis.is_on_axis(pos) {
            self.point_map.insert(id, id);
            self.entity_map.insert(id, id);
            return Ok(id);
        }
        let (next, new_id) = add_point(self.sketch.clone(), axis.reflect(pos))?;
        self.sketch = next;
        self.point_map.insert(id, new_id);
        self.entity_map.insert(id, new_id);
        Ok(new_id)
    }

    fn mirror_entity(
        &mut self,
        id: bone_types::SketchEntityId,
        axis: &MirrorAxis,
    ) -> Result<(), bone_document::SketchEditError> {
        if self.entity_map.contains_key(&id) {
            return Ok(());
        }
        let entity = self
            .sketch
            .entities()
            .get(id)
            .cloned()
            .ok_or(bone_document::SketchEditError::EntityNotFound(id))?;
        match entity {
            SketchEntity::Point(_) => {
                self.mirror_point(id, axis)?;
            }
            SketchEntity::Line(l) => {
                let new_a = self.mirror_point(l.a(), axis)?;
                let new_b = self.mirror_point(l.b(), axis)?;
                self.add_mirrored(id, SketchEntity::line(new_a, new_b, l.for_construction()))?;
            }
            SketchEntity::Arc(a) => {
                let new_center = self.mirror_point(a.center(), axis)?;
                let new_start = self.mirror_point(a.start(), axis)?;
                let new_end = self.mirror_point(a.end(), axis)?;
                self.add_mirrored(
                    id,
                    SketchEntity::arc(new_center, new_end, new_start, a.for_construction()),
                )?;
            }
            SketchEntity::Circle(c) => {
                let new_center = self.mirror_point(c.center(), axis)?;
                self.add_mirrored(
                    id,
                    SketchEntity::circle(new_center, c.radius(), c.for_construction()),
                )?;
            }
            SketchEntity::Ellipse(e) => {
                let new_center = self.mirror_point(e.center(), axis)?;
                let new_major = self.mirror_point(e.major(), axis)?;
                let new_minor = self.mirror_point(e.minor(), axis)?;
                self.add_mirrored(
                    id,
                    SketchEntity::ellipse(new_center, new_major, new_minor, e.for_construction()),
                )?;
            }
            SketchEntity::Spline(s) => {
                let new_control = s
                    .control()
                    .iter()
                    .map(|point| self.mirror_point(*point, axis))
                    .collect::<Result<Vec<_>, _>>()?;
                self.add_mirrored(
                    id,
                    SketchEntity::spline(new_control, s.closed(), s.for_construction()),
                )?;
            }
        }
        Ok(())
    }

    fn add_mirrored(
        &mut self,
        original: bone_types::SketchEntityId,
        entity: SketchEntity,
    ) -> Result<(), bone_document::SketchEditError> {
        let (next, outcome) = self
            .sketch
            .clone()
            .apply(bone_document::SketchEdit::AddEntity(entity))?;
        let EditOutcome::Entity(new_id) = outcome else {
            unreachable!("AddEntity yields Entity outcome")
        };
        self.sketch = next;
        self.entity_map.insert(original, new_id);
        Ok(())
    }
}

fn mirror_targets(
    sketch: Sketch,
    source_ids: &std::collections::BTreeSet<bone_types::SketchEntityId>,
    axis_id: bone_types::SketchEntityId,
    axis: &MirrorAxis,
) -> Result<Sketch, bone_document::SketchEditError> {
    let mut builder = MirrorBuilder::new(sketch);
    source_ids
        .iter()
        .try_for_each(|id| builder.mirror_entity(*id, axis))?;
    builder.entity_map.insert(axis_id, axis_id);
    builder.sketch = symmetric_relations_for_pairs(builder.sketch, &builder.point_map, axis_id)?;
    builder.sketch = copy_relations(builder.sketch, source_ids, axis_id, &builder.entity_map)?;
    builder.sketch = copy_dimensions(builder.sketch, source_ids, axis_id, &builder.entity_map)?;
    Ok(builder.sketch)
}

fn symmetric_relations_for_pairs(
    sketch: Sketch,
    point_map: &std::collections::BTreeMap<bone_types::SketchEntityId, bone_types::SketchEntityId>,
    axis_id: bone_types::SketchEntityId,
) -> Result<Sketch, bone_document::SketchEditError> {
    point_map
        .iter()
        .filter(|(source, mirrored)| source != mirrored)
        .try_fold(sketch, |acc, (&source, &mirrored)| {
            let (next, _) = acc.apply(bone_document::SketchEdit::AddRelation(
                SketchRelation::Symmetric {
                    a: source,
                    b: mirrored,
                    axis: axis_id,
                },
            ))?;
            Ok(next)
        })
}

fn copy_relations(
    sketch: Sketch,
    source_ids: &std::collections::BTreeSet<bone_types::SketchEntityId>,
    axis_id: bone_types::SketchEntityId,
    entity_map: &std::collections::BTreeMap<bone_types::SketchEntityId, bone_types::SketchEntityId>,
) -> Result<Sketch, bone_document::SketchEditError> {
    let relations: Vec<SketchRelation> = sketch
        .relations()
        .iter()
        .map(|(_, r)| *r)
        .filter(|r| relation_is_mirrorable(r, source_ids, axis_id))
        .filter_map(|r| remap_relation(r, entity_map))
        .collect();
    relations.into_iter().try_fold(sketch, |acc, rel| {
        match acc
            .clone()
            .apply(bone_document::SketchEdit::AddRelation(rel))
        {
            Ok((next, _)) => Ok(next),
            Err(e) => {
                tracing::warn!(error = %e, relation = ?rel, "mirror: skipped relation");
                Ok(acc)
            }
        }
    })
}

fn relation_is_mirrorable(
    rel: &SketchRelation,
    source_ids: &std::collections::BTreeSet<bone_types::SketchEntityId>,
    axis_id: bone_types::SketchEntityId,
) -> bool {
    let refs: Vec<_> = rel.references().into_iter().collect();
    let touches_source = refs.iter().any(|id| source_ids.contains(id));
    let all_known = refs
        .iter()
        .all(|id| source_ids.contains(id) || *id == axis_id);
    touches_source && all_known
}

fn remap_relation(
    rel: SketchRelation,
    entity_map: &std::collections::BTreeMap<bone_types::SketchEntityId, bone_types::SketchEntityId>,
) -> Option<SketchRelation> {
    let get = |id| entity_map.get(&id).copied();
    match rel {
        SketchRelation::Coincident(a, b) => Some(SketchRelation::Coincident(get(a)?, get(b)?)),
        SketchRelation::Horizontal(a) => Some(SketchRelation::Horizontal(get(a)?)),
        SketchRelation::Vertical(a) => Some(SketchRelation::Vertical(get(a)?)),
        SketchRelation::Parallel(a, b) => Some(SketchRelation::Parallel(get(a)?, get(b)?)),
        SketchRelation::Perpendicular(a, b) => {
            Some(SketchRelation::Perpendicular(get(a)?, get(b)?))
        }
        SketchRelation::Tangent(a, b) => Some(SketchRelation::Tangent(get(a)?, get(b)?)),
        SketchRelation::Equal(a, b) => Some(SketchRelation::Equal(get(a)?, get(b)?)),
        SketchRelation::Concentric(a, b) => Some(SketchRelation::Concentric(get(a)?, get(b)?)),
        SketchRelation::Midpoint { point, line } => Some(SketchRelation::Midpoint {
            point: get(point)?,
            line: get(line)?,
        }),
        SketchRelation::Symmetric { a, b, axis } => Some(SketchRelation::Symmetric {
            a: get(a)?,
            b: get(b)?,
            axis: get(axis)?,
        }),
        SketchRelation::Fix(a) => Some(SketchRelation::Fix(get(a)?)),
    }
}

fn copy_dimensions(
    sketch: Sketch,
    source_ids: &std::collections::BTreeSet<bone_types::SketchEntityId>,
    axis_id: bone_types::SketchEntityId,
    entity_map: &std::collections::BTreeMap<bone_types::SketchEntityId, bone_types::SketchEntityId>,
) -> Result<Sketch, bone_document::SketchEditError> {
    let dims: Vec<SketchDimension> = sketch
        .dimensions()
        .iter()
        .map(|(_, d)| *d)
        .filter(|d| dimension_is_mirrorable(d, source_ids, axis_id))
        .filter_map(|d| remap_dimension(d, entity_map))
        .collect();
    dims.into_iter().try_fold(sketch, |acc, dim| {
        match acc
            .clone()
            .apply(bone_document::SketchEdit::AddDimension(dim))
        {
            Ok((next, _)) => Ok(next),
            Err(e) => {
                tracing::warn!(error = %e, dimension = ?dim, "mirror: skipped dimension");
                Ok(acc)
            }
        }
    })
}

fn dimension_is_mirrorable(
    dim: &SketchDimension,
    source_ids: &std::collections::BTreeSet<bone_types::SketchEntityId>,
    axis_id: bone_types::SketchEntityId,
) -> bool {
    let refs: Vec<_> = dim.references().into_iter().collect();
    let touches_source = refs.iter().any(|id| source_ids.contains(id));
    let all_known = refs
        .iter()
        .all(|id| source_ids.contains(id) || *id == axis_id);
    touches_source && all_known
}

fn remap_dimension(
    dim: SketchDimension,
    entity_map: &std::collections::BTreeMap<bone_types::SketchEntityId, bone_types::SketchEntityId>,
) -> Option<SketchDimension> {
    let get = |id| entity_map.get(&id).copied();
    match dim {
        SketchDimension::Linear { a, b, value, kind } => Some(SketchDimension::Linear {
            a: get(a)?,
            b: get(b)?,
            value,
            kind,
        }),
        SketchDimension::Radius {
            target,
            value,
            kind,
        } => Some(SketchDimension::Radius {
            target: get(target)?,
            value,
            kind,
        }),
        SketchDimension::Diameter {
            target,
            value,
            kind,
        } => Some(SketchDimension::Diameter {
            target: get(target)?,
            value,
            kind,
        }),
        SketchDimension::Angular { a, b, value, kind } => Some(SketchDimension::Angular {
            a: get(a)?,
            b: get(b)?,
            value,
            kind,
        }),
    }
}

fn add_point(
    sketch: Sketch,
    at: Point2,
) -> Result<(Sketch, bone_types::SketchEntityId), bone_document::SketchEditError> {
    let (next, outcome) = sketch.apply(bone_document::SketchEdit::AddEntity(
        SketchEntity::point(at),
    ))?;
    let EditOutcome::Entity(id) = outcome else {
        unreachable!("AddEntity must yield Entity outcome")
    };
    Ok((next, id))
}

fn require_point(
    sketch: &Sketch,
    id: bone_types::SketchEntityId,
) -> Result<Point2, bone_document::SketchEditError> {
    match sketch
        .entities()
        .get(id)
        .ok_or(bone_document::SketchEditError::EntityNotFound(id))?
    {
        SketchEntity::Point(p) => Ok(p.at()),
        _ => Err(bone_document::SketchEditError::ExpectedPoint(id)),
    }
}

fn lookup_point(sketch: &Sketch, id: bone_types::SketchEntityId) -> Option<Point2> {
    match sketch.entities().get(id)? {
        SketchEntity::Point(p) => Some(p.at()),
        _ => None,
    }
}

fn suppress_pointer_activations(frame: shell::ShellFrame) -> shell::ShellFrame {
    shell::ShellFrame {
        paints: frame.paints,
        overlay_paints: frame.overlay_paints,
        viewport_rect: frame.viewport_rect,
        activated_tool: None,
        activated_feature_tool: None,
        activated_relation: None,
        activated_dimension: None,
        dimension_edit: None,
        extrude_edit: frame.extrude_edit,
        extrude_arm_reference: frame.extrude_arm_reference,
        revolve_edit: frame.revolve_edit,
        sweep_edit: frame.sweep_edit,
        feature_arm: frame.feature_arm,
        plane_picked: None,
        datum_sketch_target: None,
        sketch_activated: None,
        sketch_rename: None,
        extrude_activated: None,
        extrude_rename: None,
        feature_command: None,
        feature_reorder: None,
        rollback_change: None,
        reattach_request: None,
        exit_sketch: false,
        confirm_action: None,
        menu_action: None,
        settings_change: None,
        view_pick: None,
        view_menu: None,
    }
}

#[allow(
    clippy::too_many_arguments,
    reason = "popup overlay dispatch threads every transient surface"
)]
fn apply_popup_overlays(
    overlay: &mut Vec<bone_ui::widgets::WidgetPaint>,
    dim_outcome: Option<&DimensionEditorOutcome>,
    conflict_outcome: Option<&DimConflictOutcome>,
    picker_outcome: Option<&file_menu::PickerModalOutcome>,
    overwrite_outcome: Option<&OverwriteOutcome>,
    discard_outcome: Option<&DiscardOutcome>,
    step_progress_outcome: Option<&StepProgressOutcome>,
    notification_outcome: Option<&NotificationOutcome>,
    shortcut_bar_outcome: Option<&shortcut_bar::ShortcutBarOutcome>,
) -> Option<LayoutRect> {
    let dim_closing = matches!(
        dim_outcome.map(|o| &o.action),
        Some(DimensionEditorAction::Commit(_) | DimensionEditorAction::Cancel),
    );
    extend_when_open(
        overlay,
        dim_outcome.map(|o| o.paints.as_slice()),
        dim_closing,
    );
    let conflict_closing = matches!(
        conflict_outcome.map(|o| o.action),
        Some(DimConflictAction::MakeDriven | DimConflictAction::Cancel),
    );
    extend_when_open(
        overlay,
        conflict_outcome.map(|o| o.paints.as_slice()),
        conflict_closing,
    );
    let picker_closing = picker_outcome.and_then(|o| o.command.as_ref()).is_some();
    extend_when_open(
        overlay,
        picker_outcome.map(|o| o.paints.as_slice()),
        picker_closing,
    );
    let overwrite_closing = matches!(
        overwrite_outcome.map(|o| o.action),
        Some(OverwriteAction::Replace | OverwriteAction::Cancel),
    );
    extend_when_open(
        overlay,
        overwrite_outcome.map(|o| o.paints.as_slice()),
        overwrite_closing,
    );
    let discard_closing = matches!(
        discard_outcome.map(|o| o.action),
        Some(DiscardAction::Confirm | DiscardAction::Cancel),
    );
    extend_when_open(
        overlay,
        discard_outcome.map(|o| o.paints.as_slice()),
        discard_closing,
    );
    if let Some(progress) = step_progress_outcome {
        overlay.extend(progress.paints.iter().cloned());
    }
    if let Some(notification) = notification_outcome {
        overlay.extend(notification.paints.iter().cloned());
    }
    let bar_closing = shortcut_bar_outcome.is_some_and(|o| o.dismissed || o.activated.is_some());
    extend_when_open(
        overlay,
        shortcut_bar_outcome.map(|o| o.paints.as_slice()),
        bar_closing,
    );
    if dim_closing {
        None
    } else {
        dim_outcome.map(|o| o.bounds)
    }
}

fn extend_when_open(
    overlay: &mut Vec<bone_ui::widgets::WidgetPaint>,
    paints: Option<&[bone_ui::widgets::WidgetPaint]>,
    closing: bool,
) {
    if let Some(p) = paints
        && !closing
    {
        overlay.extend(p.iter().cloned());
    }
}

struct FrameOutcomes {
    frame: shell::ShellFrame,
    hotkey_actions: Vec<ActionId>,
    dim: Option<DimensionEditorOutcome>,
    dim_conflict: Option<DimConflictOutcome>,
    picker: Option<file_menu::PickerModalOutcome>,
    overwrite: Option<OverwriteOutcome>,
    discard: Option<DiscardOutcome>,
    step_progress: Option<StepProgressOutcome>,
    notification: Option<NotificationOutcome>,
    shortcut_bar: Option<shortcut_bar::ShortcutBarOutcome>,
}

fn strip_plain_letter_chords(input: &mut InputSnapshot) {
    input.keys_pressed.retain(|event| {
        !matches!(event.code, bone_ui::input::KeyCode::Char(_))
            || event.modifiers != ModifierMask::NONE
    });
}

fn dim_render_view(
    camera2: Camera2,
    camera3: Option<Camera3>,
    extent: ViewportExtent,
    viewport_rect: LayoutRect,
) -> DimRenderView {
    DimRenderView {
        camera2,
        camera3,
        region: solid_viewport_region(viewport_rect, extent),
        viewport_rect,
    }
}

#[allow(
    clippy::too_many_arguments,
    reason = "run_frame_ui threads every per-frame UI subsystem"
)]
fn run_frame_ui(
    state: &mut AppState,
    theme: Arc<Theme>,
    input: &mut InputSnapshot,
    hits: &mut HitFrame,
    a11y: &mut AccessTreeBuilder,
    scopes: &HotkeyScopes,
    layout_size: LayoutSize,
    cursor_world: Option<Point2>,
) -> FrameOutcomes {
    let (feature_badges, body_rows, needs_rebuild) = frame_badges(state);
    let whats_wrong = compute_whats_wrong(&state.model, &state.document, &state.strings);
    let mut ctx = FrameCtx::new(
        theme,
        input,
        &mut state.focus,
        &state.hotkeys,
        &state.strings,
        hits,
        &state.hit_state,
        a11y,
        &mut state.chrome_shaper,
    );
    let extrude_status = state.extrude_preview.as_ref().map(ExtrudePreview::status);
    let frame = state.shell.render(
        &mut ctx,
        &state.document,
        &state.mode,
        &state.selection,
        &state.settings,
        layout_size,
        cursor_world,
        state.camera3.filter(|_| state.solid_view.is_some()),
        extrude_status,
        &mut state.view,
        &feature_badges,
        &body_rows,
        needs_rebuild,
        &whats_wrong,
        &state.hidden_planes,
    );
    let dim_view = dim_render_view(
        state.camera,
        state.camera3,
        state.extent,
        frame.viewport_rect,
    );
    let dim_outcome = render_active_dimension(
        &mut ctx,
        &state.mode,
        &state.document,
        &dim_view,
        &mut state.dim_editor,
    );
    let conflict_outcome =
        dim_conflict_pending(&state.mode).map(|_| render_dim_conflict_modal(&mut ctx, layout_size));
    let picker_outcome = state
        .file_picker
        .as_mut()
        .map(|session| file_menu::render(&mut ctx, session, layout_size));
    let overwrite_outcome = state
        .pending_overwrite
        .as_ref()
        .map(|pending| render_overwrite_modal(&mut ctx, layout_size, pending));
    let discard_outcome = state
        .pending_discard
        .as_ref()
        .map(|pending| render_discard_modal(&mut ctx, layout_size, pending));
    let reduce_motion = state.settings.reduce_motion;
    let step_progress_outcome = state
        .step_job
        .as_ref()
        .filter(|job| job.meta().show_progress)
        .map(|job| render_step_progress_dialog(&mut ctx, layout_size, job, reduce_motion));
    let notification_outcome = state
        .notification
        .as_ref()
        .map(|notification| render_notification_toast(&mut ctx, layout_size, notification));
    let is_sketch = state.mode.is_sketch();
    let shortcut_bar_outcome = state
        .shortcut_bar
        .map(|bar_state| shortcut_bar::render(&mut ctx, bar_state, layout_size, is_sketch));
    let any_modal_open = state.shell.state.keyboard_dialog_open
        || conflict_outcome.is_some()
        || dim_outcome.is_some()
        || picker_outcome.is_some()
        || overwrite_outcome.is_some()
        || discard_outcome.is_some()
        || step_progress_outcome.is_some()
        || shortcut_bar_outcome.is_some();
    let activated = shortcut_bar_outcome.as_ref().and_then(|o| o.activated);
    let actions = frame_actions(&mut ctx, scopes, any_modal_open, activated);
    FrameOutcomes {
        frame,
        hotkey_actions: actions,
        dim: dim_outcome,
        dim_conflict: conflict_outcome,
        picker: picker_outcome,
        overwrite: overwrite_outcome,
        discard: discard_outcome,
        step_progress: step_progress_outcome,
        notification: notification_outcome,
        shortcut_bar: shortcut_bar_outcome,
    }
}

fn frame_actions(
    ctx: &mut FrameCtx<'_>,
    scopes: &HotkeyScopes,
    any_modal_open: bool,
    activated: Option<ActionId>,
) -> Vec<ActionId> {
    if !any_modal_open && ctx.focus.is_text_input_focused() {
        strip_plain_letter_chords(ctx.input);
    }
    let mut actions = if any_modal_open {
        Vec::new()
    } else {
        ctx.dispatch_hotkeys(scopes)
    };
    if let Some(activated) = activated {
        actions.push(activated);
    }
    actions
}

fn dim_conflict_pending(mode: &Mode) -> Option<Sketch3Relation> {
    match mode.dim_flow() {
        Some(DimensionFlow::Conflict(relation)) => Some(relation),
        Some(DimensionFlow::Editing(_)) | None => None,
    }
}

#[derive(Clone, Debug, PartialEq)]
struct DimConflictOutcome {
    paints: Vec<bone_ui::widgets::WidgetPaint>,
    action: DimConflictAction,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum DimConflictAction {
    Idle,
    MakeDriven,
    Cancel,
}

fn render_dim_conflict_modal(
    ctx: &mut FrameCtx<'_>,
    layout_size: LayoutSize,
) -> DimConflictOutcome {
    use bone_ui::widgets::{ConfirmationDialog, ConfirmationOutcome, show_confirmation};
    use bone_ui::{WidgetId, WidgetKey};
    let viewport = LayoutRect::new(LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), layout_size);
    let dialog_size = LayoutSize::new(LayoutPx::new(440.0), LayoutPx::new(180.0));
    let id = WidgetId::ROOT.child(WidgetKey::new("dim.conflict"));
    let response = show_confirmation(
        ctx,
        ConfirmationDialog {
            id,
            viewport,
            size: dialog_size,
            title: strings::DIM_CONFLICT_TITLE,
            message: strings::DIM_CONFLICT_MESSAGE,
            confirm_label: strings::DIM_CONFLICT_MAKE_DRIVEN,
            cancel_label: strings::DIM_CONFLICT_CANCEL,
            destructive: false,
        },
    );
    let action = match response.outcome {
        Some(ConfirmationOutcome::Confirm) => DimConflictAction::MakeDriven,
        Some(ConfirmationOutcome::Cancel) => DimConflictAction::Cancel,
        None => DimConflictAction::Idle,
    };
    DimConflictOutcome {
        paints: response.paint,
        action,
    }
}

#[derive(Clone, Debug, PartialEq)]
struct OverwriteOutcome {
    paints: Vec<bone_ui::widgets::WidgetPaint>,
    action: OverwriteAction,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum OverwriteAction {
    Idle,
    Replace,
    Cancel,
}

fn render_overwrite_modal(
    ctx: &mut FrameCtx<'_>,
    layout_size: LayoutSize,
    pending: &PendingOverwrite,
) -> OverwriteOutcome {
    use bone_ui::widgets::{ConfirmationDialog, ConfirmationOutcome, show_confirmation};
    use bone_ui::{WidgetId, WidgetKey};
    let viewport = LayoutRect::new(LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), layout_size);
    let dialog_size = LayoutSize::new(LayoutPx::new(440.0), LayoutPx::new(180.0));
    let id = WidgetId::ROOT.child(WidgetKey::new("file.overwrite"));
    let (title, message) = match pending {
        PendingOverwrite::Document(_) => (
            strings::FILE_OVERWRITE_TITLE,
            strings::FILE_OVERWRITE_MESSAGE,
        ),
        PendingOverwrite::StepExport(_) => (
            strings::FILE_OVERWRITE_TITLE_STEP,
            strings::FILE_OVERWRITE_MESSAGE_STEP,
        ),
    };
    let response = show_confirmation(
        ctx,
        ConfirmationDialog {
            id,
            viewport,
            size: dialog_size,
            title,
            message,
            confirm_label: strings::FILE_OVERWRITE_REPLACE,
            cancel_label: strings::FILE_OVERWRITE_CANCEL,
            destructive: true,
        },
    );
    let action = match response.outcome {
        Some(ConfirmationOutcome::Confirm) => OverwriteAction::Replace,
        Some(ConfirmationOutcome::Cancel) => OverwriteAction::Cancel,
        None => OverwriteAction::Idle,
    };
    OverwriteOutcome {
        paints: response.paint,
        action,
    }
}

#[derive(Clone, Debug, PartialEq)]
struct DiscardOutcome {
    paints: Vec<bone_ui::widgets::WidgetPaint>,
    action: DiscardAction,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum DiscardAction {
    Idle,
    Confirm,
    Cancel,
}

fn render_discard_modal(
    ctx: &mut FrameCtx<'_>,
    layout_size: LayoutSize,
    pending: &PendingDiscard,
) -> DiscardOutcome {
    use bone_ui::widgets::{ConfirmationDialog, ConfirmationOutcome, show_confirmation};
    use bone_ui::{WidgetId, WidgetKey};
    let viewport = LayoutRect::new(LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), layout_size);
    let dialog_size = LayoutSize::new(LayoutPx::new(460.0), LayoutPx::new(190.0));
    let id = WidgetId::ROOT.child(WidgetKey::new("file.discard"));
    let (title, message, confirm_label, cancel_label) = match pending {
        PendingDiscard::New | PendingDiscard::Open(_) | PendingDiscard::ImportStep(_) => (
            strings::FILE_DISCARD_TITLE,
            strings::FILE_DISCARD_MESSAGE,
            strings::FILE_DISCARD_CONFIRM,
            strings::FILE_DISCARD_CANCEL,
        ),
        PendingDiscard::InstallImported { .. } => (
            strings::FILE_IMPORT_REPLACE_TITLE,
            strings::FILE_IMPORT_REPLACE_MESSAGE,
            strings::FILE_IMPORT_REPLACE_CONFIRM,
            strings::FILE_IMPORT_REPLACE_CANCEL,
        ),
    };
    let response = show_confirmation(
        ctx,
        ConfirmationDialog {
            id,
            viewport,
            size: dialog_size,
            title,
            message,
            confirm_label,
            cancel_label,
            destructive: true,
        },
    );
    let action = match response.outcome {
        Some(ConfirmationOutcome::Confirm) => DiscardAction::Confirm,
        Some(ConfirmationOutcome::Cancel) => DiscardAction::Cancel,
        None => DiscardAction::Idle,
    };
    DiscardOutcome {
        paints: response.paint,
        action,
    }
}

fn apply_discard_outcome(state: &mut AppState, outcome: Option<DiscardOutcome>) {
    let Some(outcome) = outcome else { return };
    match outcome.action {
        DiscardAction::Idle => {}
        DiscardAction::Cancel => {
            state.pending_discard = None;
        }
        DiscardAction::Confirm => {
            let Some(pending) = state.pending_discard.take() else {
                return;
            };
            match pending {
                PendingDiscard::New => apply_new_document(state),
                PendingDiscard::Open(path) => apply_open_folder(state, path),
                PendingDiscard::ImportStep(path) => start_step_import(state, path),
                PendingDiscard::InstallImported {
                    document,
                    file_name,
                } => {
                    install_imported_document(state, *document);
                    notify_info(state, strings::NOTIFY_IMPORTED, Some(file_name));
                }
            }
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
struct StepProgressOutcome {
    paints: Vec<bone_ui::widgets::WidgetPaint>,
    cancel_requested: bool,
}

fn render_step_progress_dialog(
    ctx: &mut FrameCtx<'_>,
    layout_size: LayoutSize,
    job: &step_jobs::StepJob,
    reduce_motion: bool,
) -> StepProgressOutcome {
    use bone_ui::widgets::{Dialog, DialogButton, LabelText, WidgetPaint, show_dialog};
    use bone_ui::{WidgetId, WidgetKey};
    let viewport = LayoutRect::new(LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), layout_size);
    let dialog_size = LayoutSize::new(LayoutPx::new(440.0), LayoutPx::new(170.0));
    let id = WidgetId::ROOT.child(WidgetKey::new("step.progress"));
    let cancel_id = id.child(WidgetKey::new("cancel"));
    let title = match job {
        step_jobs::StepJob::Import { .. } => strings::STEP_PROGRESS_TITLE_IMPORT,
        step_jobs::StepJob::Export { .. } => strings::STEP_PROGRESS_TITLE_EXPORT,
    };
    let buttons = [DialogButton::secondary(
        cancel_id,
        strings::STEP_PROGRESS_CANCEL,
    )];
    let sweep = sweep_phase(ctx.input.frame, reduce_motion);
    let file_name = job.meta().file_name.clone();
    let (response, ()) = show_dialog(
        ctx,
        Dialog::new(id, viewport, dialog_size, title, &buttons),
        |ctx, body_rect, paint| {
            let label_rect = LayoutRect::new(
                LayoutPos::new(
                    LayoutPx::new(body_rect.origin.x.value() + 16.0),
                    LayoutPx::new(body_rect.origin.y.value() + 12.0),
                ),
                LayoutSize::new(
                    LayoutPx::saturating_nonneg(body_rect.size.width.value() - 32.0),
                    LayoutPx::new(20.0),
                ),
            );
            paint.push(WidgetPaint::Label {
                rect: label_rect,
                text: LabelText::Owned(file_name),
                color: ctx.theme().colors.text_secondary(),
                role: ctx.theme().typography.body,
            });
            push_progress_bar(ctx, body_rect, sweep, paint);
        },
    );
    StepProgressOutcome {
        paints: response.paint,
        cancel_requested: response.dismissed || response.activated == Some(cancel_id),
    }
}

const SWEEP_PERIOD_SECS: f32 = 1.2;
const SWEEP_SPAN: f32 = 0.3;

fn sweep_phase(now: bone_ui::input::FrameInstant, reduce_motion: bool) -> f32 {
    if reduce_motion {
        return 0.5;
    }
    (now.duration().as_secs_f32() / SWEEP_PERIOD_SECS).fract()
}

fn push_progress_bar(
    ctx: &FrameCtx<'_>,
    body: LayoutRect,
    sweep: f32,
    paint: &mut Vec<bone_ui::widgets::WidgetPaint>,
) {
    use bone_ui::widgets::WidgetPaint;
    let track = LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(body.origin.x.value() + 16.0),
            LayoutPx::new(body.origin.y.value() + 48.0),
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(body.size.width.value() - 32.0),
            LayoutPx::new(8.0),
        ),
    );
    paint.push(WidgetPaint::Surface {
        rect: track,
        fill: ctx.theme().colors.surface(bone_ui::theme::SurfaceLevel::L0),
        border: Some(bone_ui::theme::Border {
            width: bone_ui::theme::StrokeWidth::HAIRLINE,
            color: ctx
                .theme()
                .colors
                .neutral
                .step(bone_ui::theme::Step12::SUBTLE_BORDER),
        }),
        radius: ctx.theme().radius.sm,
        elevation: None,
    });
    let start = sweep * (1.0 + SWEEP_SPAN) - SWEEP_SPAN;
    let left = start.max(0.0);
    let right = (start + SWEEP_SPAN).min(1.0);
    if right <= left {
        return;
    }
    let width = track.size.width.value();
    let fill_rect = LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(track.origin.x.value() + width * left),
            track.origin.y,
        ),
        LayoutSize::new(LayoutPx::new(width * (right - left)), track.size.height),
    );
    paint.push(WidgetPaint::Surface {
        rect: fill_rect,
        fill: ctx.theme().colors.accent_solid(),
        border: None,
        radius: ctx.theme().radius.sm,
        elevation: None,
    });
}

fn apply_step_progress_outcome(state: &AppState, outcome: Option<StepProgressOutcome>) {
    let cancel = outcome.is_some_and(|o| o.cancel_requested);
    if !cancel {
        return;
    }
    if let Some(job) = state.step_job.as_ref() {
        job.meta().request_cancel();
    }
}

#[derive(Clone, Debug, PartialEq)]
struct NotificationOutcome {
    paints: Vec<bone_ui::widgets::WidgetPaint>,
    dismissed: bool,
}

fn render_notification_toast(
    ctx: &mut FrameCtx<'_>,
    layout_size: LayoutSize,
    notification: &Notification,
) -> NotificationOutcome {
    use bone_ui::widgets::{Button, ButtonVariant, WidgetPaint, show_button};
    use bone_ui::{WidgetId, WidgetKey};
    let theme = ctx.theme();
    let bg = match notification.kind {
        NotificationKind::Info => theme.colors.surface(bone_ui::theme::SurfaceLevel::L2),
        NotificationKind::Error => theme.colors.danger.step(bone_ui::theme::Step12::SUBTLE_BG),
    };
    let fg = match notification.kind {
        NotificationKind::Info => theme.colors.text_primary(),
        NotificationKind::Error => theme.colors.danger.step(bone_ui::theme::Step12::TEXT_HIGH),
    };
    let toast_width = layout_size.width.value().clamp(280.0, 420.0);
    let toast_height = if notification.detail.is_some() {
        72.0
    } else {
        44.0
    };
    let margin = 24.0;
    let toast_rect = LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(margin),
            LayoutPx::new(layout_size.height.value() - toast_height - margin),
        ),
        LayoutSize::new(LayoutPx::new(toast_width), LayoutPx::new(toast_height)),
    );
    let id = WidgetId::ROOT.child(WidgetKey::new("notification.toast"));
    let mut paints = vec![WidgetPaint::Surface {
        rect: toast_rect,
        fill: bg,
        border: Some(bone_ui::theme::Border {
            width: bone_ui::theme::StrokeWidth::HAIRLINE,
            color: theme.colors.neutral.step(bone_ui::theme::Step12::BORDER),
        }),
        radius: theme.radius.sm,
        elevation: Some(theme.elevation.level1),
    }];
    let headline_rect = LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(toast_rect.origin.x.value() + 16.0),
            LayoutPx::new(toast_rect.origin.y.value() + 12.0),
        ),
        LayoutSize::new(LayoutPx::new(toast_width - 120.0), LayoutPx::new(20.0)),
    );
    paints.push(WidgetPaint::Label {
        rect: headline_rect,
        text: bone_ui::widgets::LabelText::Key(notification.headline),
        color: fg,
        role: theme.typography.label,
    });
    if let Some(detail) = &notification.detail {
        let detail_rect = LayoutRect::new(
            LayoutPos::new(
                LayoutPx::new(toast_rect.origin.x.value() + 16.0),
                LayoutPx::new(toast_rect.origin.y.value() + 36.0),
            ),
            LayoutSize::new(LayoutPx::new(toast_width - 32.0), LayoutPx::new(24.0)),
        );
        paints.push(WidgetPaint::Label {
            rect: detail_rect,
            text: bone_ui::widgets::LabelText::Owned(detail.clone()),
            color: theme.colors.text_secondary(),
            role: theme.typography.caption,
        });
    }
    let dismiss_rect = LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(toast_rect.origin.x.value() + toast_width - 96.0),
            LayoutPx::new(toast_rect.origin.y.value() + 8.0),
        ),
        LayoutSize::new(LayoutPx::new(84.0), LayoutPx::new(28.0)),
    );
    let dismiss_id = id.child(WidgetKey::new("dismiss"));
    let response = show_button(
        ctx,
        Button::new(
            dismiss_id,
            dismiss_rect,
            strings::NOTIFY_DISMISS,
            ButtonVariant::Secondary,
        ),
    );
    paints.extend(response.paint);
    NotificationOutcome {
        paints,
        dismissed: response.activated,
    }
}

fn apply_notification_outcome(state: &mut AppState, outcome: Option<NotificationOutcome>) {
    let Some(outcome) = outcome else { return };
    if outcome.dismissed {
        state.notification = None;
    }
}

fn apply_overwrite_outcome(state: &mut AppState, outcome: Option<OverwriteOutcome>) {
    let Some(outcome) = outcome else { return };
    match outcome.action {
        OverwriteAction::Idle => {}
        OverwriteAction::Cancel => {
            state.pending_overwrite = None;
        }
        OverwriteAction::Replace => match state.pending_overwrite.take() {
            Some(PendingOverwrite::Document(folder)) => perform_save_to(state, folder),
            Some(PendingOverwrite::StepExport(path)) => start_step_export(state, path),
            None => {}
        },
    }
}

fn pending_dim(mode: &Mode) -> Option<PendingDimension> {
    match mode.dim_flow() {
        Some(DimensionFlow::Editing(p)) => Some(p),
        Some(DimensionFlow::Conflict(_)) | None => None,
    }
}

fn apply_smart_dimension(state: &mut AppState) {
    let Some(sketch_id) = state.mode.sketch_id() else {
        return;
    };
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let view = sketch3_project::project(base, base.plane());
    let ids2: Vec<SketchEntityId> = if state.mode.is_sketch3() {
        state
            .selection
            .entity_ids()
            .iter()
            .filter_map(|id3| view.to_2d(*id3))
            .collect()
    } else {
        state.selection.entity_ids().to_vec()
    };
    let request = match smart_dimension::eligibility(view.sketch(), &ids2) {
        smart_dimension::Eligibility::Eligible(req) => Some(req),
        smart_dimension::Eligibility::Disabled(_) => None,
    };
    apply_dimension_request(state, request);
}

fn apply_dimension_request(state: &mut AppState, request: Option<PendingDimension>) {
    let Some(request) = request else { return };
    let Mode::Sketch { .. } = state.mode else {
        return;
    };
    state.mode = core::mem::take(&mut state.mode).start_dimension(request);
}

fn apply_dimension_outcome(state: &mut AppState, outcome: Option<DimensionEditorOutcome>) {
    let Some(outcome) = outcome else { return };
    let Some(pending) = pending_dim(&state.mode) else {
        return;
    };
    match outcome.action {
        DimensionEditorAction::Idle => {}
        DimensionEditorAction::Cancel => {
            state.mode = core::mem::take(&mut state.mode).cancel_dimension();
            state.dim_editor.close();
        }
        DimensionEditorAction::Swap(next_proto) => {
            state.mode = core::mem::take(&mut state.mode).start_dimension(PendingDimension {
                proto: next_proto,
                anchor: pending.anchor,
            });
        }
        DimensionEditorAction::Commit(value) => {
            commit_pending_dimension(state, pending, value);
        }
    }
}

fn apply_dim_conflict_outcome(state: &mut AppState, outcome: Option<DimConflictOutcome>) {
    let Some(outcome) = outcome else { return };
    let Some(relation) = dim_conflict_pending(&state.mode) else {
        return;
    };
    match outcome.action {
        DimConflictAction::Idle => {}
        DimConflictAction::Cancel => {
            state.mode = core::mem::take(&mut state.mode).cancel_dim_conflict();
        }
        DimConflictAction::MakeDriven => {
            confirm_dim_conflict_make_driven(state, relation);
        }
    }
}

fn commit_pending_dimension(
    state: &mut AppState,
    pending: PendingDimension,
    value: DimensionValue,
) {
    let Mode::Sketch { sketch_id, .. } = state.mode else {
        return;
    };
    let proto = match pending.proto.with_value(value) {
        Ok(p) => p,
        Err(e) => {
            tracing::warn!(error = %e, ?pending.proto, ?value, "dimension value type mismatch");
            return;
        }
    };
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let view = base.project();
    let map = projected_id_map(base, &view);
    let Some(relation) = sketch3_project::map_dimension(proto, &map) else {
        tracing::warn!(?proto, "dimension references do not map into the sketch");
        return;
    };
    let (next, relation_id) = base.with_relation(relation);
    let solved = match next.solve() {
        Ok(s) => s,
        Err(SolverError::OverDefined { .. }) => {
            state.mode = core::mem::take(&mut state.mode).start_dim_conflict(relation);
            state.dim_editor.close();
            return;
        }
        Err(e) => {
            tracing::warn!(error = %e, "solve after add dim did not converge; rejecting");
            return;
        }
    };
    state.undo.record(state.document.clone());
    state.document.replace_sketch(sketch_id, solved);
    state.selection = Selection::Relation(relation_id);
    state.mode = core::mem::take(&mut state.mode).cancel_dimension();
    state.dim_editor.close();
    refresh_active_scene(state);
}

fn confirm_dim_conflict_make_driven(state: &mut AppState, relation: Sketch3Relation) {
    let Mode::Sketch { sketch_id, .. } = state.mode else {
        return;
    };
    let Some(base) = state.document.sketch3(sketch_id).cloned() else {
        return;
    };
    let (next, relation_id) = base.with_relation(relation.as_reference());
    let solved = match next.solve() {
        Ok(solved) => solved,
        Err(error) => {
            tracing::warn!(error = %error, "reference dimension fails to converge; rejecting");
            state.mode = core::mem::take(&mut state.mode).cancel_dim_conflict();
            state.dim_editor.close();
            return;
        }
    };
    state.undo.record(state.document.clone());
    state.document.replace_sketch(sketch_id, solved);
    state.selection = Selection::Relation(relation_id);
    state.mode = core::mem::take(&mut state.mode).cancel_dim_conflict();
    state.dim_editor.close();
    refresh_active_scene(state);
    if state.mode.is_sketch3() {
        rebuild_model_view(state);
    }
}

fn project_point3_to_screen(
    camera: Camera3,
    region: ViewportRegion,
    point: Point3,
) -> Option<LayoutPos> {
    let clip = bone_render::clip_from_world(camera, region.extent()).ok()?;
    let (wx, wy, wz) = point.coords_mm();
    let homogeneous = [wx, wy, wz, 1.0];
    let row = |r: usize| {
        (0..4)
            .map(|c| f64::from(clip[c * 4 + r]) * homogeneous[c])
            .sum::<f64>()
    };
    let clip_w = row(3);
    if clip_w.abs() < 1.0e-9 {
        return None;
    }
    let ndc_x = row(0) / clip_w;
    let ndc_y = row(1) / clip_w;
    let (origin_x, origin_y, width, height) = region.scissor();
    let px = f64::from(origin_x) + ndc_x.mul_add(0.5, 0.5) * f64::from(width);
    let py = f64::from(origin_y) + (0.5 - ndc_y * 0.5) * f64::from(height);
    Some(LayoutPos::new(
        LayoutPx::saturating(bone_render::lower_f32(px)),
        LayoutPx::saturating(bone_render::lower_f32(py)),
    ))
}

struct DimRenderView {
    camera2: Camera2,
    camera3: Option<Camera3>,
    region: Option<ViewportRegion>,
    viewport_rect: LayoutRect,
}

fn render_active_dimension(
    ctx: &mut FrameCtx<'_>,
    mode: &Mode,
    document: &Document,
    view: &DimRenderView,
    dim_editor: &mut DimensionEditorState,
) -> Option<DimensionEditorOutcome> {
    let pending = pending_dim(mode)?;
    let anchor = live_dim_anchor(mode, document, &pending);
    if mode.is_sketch3() {
        let basis = document.sketch3(mode.sketch3_id()?)?.plane();
        let at = sketch3_project::lift(basis, anchor);
        let anchor_screen = view
            .camera3
            .zip(view.region)
            .and_then(|(camera, region)| project_point3_to_screen(camera, region, at))?;
        return Some(dimension_editor::render_at(
            ctx,
            pending,
            anchor_screen,
            view.viewport_rect,
            dim_editor,
        ));
    }
    Some(dimension_editor::render(
        ctx,
        pending,
        anchor,
        &view.camera2,
        view.viewport_rect,
        dim_editor,
    ))
}

fn apply_dimension_edit(state: &mut AppState, edit: Option<shell::DimensionEdit>) {
    let Some(edit) = edit else { return };
    let Mode::Sketch { sketch_id, .. } = state.mode else {
        return;
    };
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let basis = base.plane();
    let Some(relation_id) = sketch3_project::dimension_relation_map(base, basis)
        .get(&edit.id)
        .copied()
    else {
        tracing::warn!(?edit, "dimension edit does not map to a sketch relation");
        return;
    };
    let updated = base.with_relation_value(relation_id, edit.value);
    let solved = match updated.solve() {
        Ok(s) => s,
        Err(e) => {
            tracing::warn!(error = %e, ?edit, "solve after dimension edit failed");
            return;
        }
    };
    state.undo.record(state.document.clone());
    state.document.replace_sketch(sketch_id, solved);
    refresh_active_scene(state);
}

fn apply_relation_action(state: &mut AppState, relation: Option<SketchRelation>) {
    let Some(relation) = relation else { return };
    let Mode::Sketch { sketch_id, .. } = state.mode else {
        return;
    };
    let Some(base) = state.document.sketch3(sketch_id) else {
        return;
    };
    let view = base.project();
    let map = projected_id_map(base, &view);
    let Some(relation3) = sketch3_project::map_relation(relation, &map, base) else {
        tracing::warn!(?relation, "relation references do not map into the sketch");
        return;
    };
    if let Err(err) = base.validate_relation(relation3) {
        tracing::warn!(?relation3, %err, "relation rejected for incompatible operands");
        return;
    }
    let next = base.with_relation(relation3).0;
    state.undo.record(state.document.clone());
    state
        .document
        .replace_sketch(sketch_id, sketch3_mode::solved(next));
    state.selection = Selection::default();
    refresh_active_scene(state);
}

fn apply_menu_action(state: &mut AppState, action: Option<shell::MenuAction>) {
    match action {
        Some(shell::MenuAction::Quit) => {
            state.pending_exit = true;
        }
        Some(shell::MenuAction::Undo) if state.undo.undo(&mut state.document) => {
            refresh_active_scene(state);
        }
        Some(shell::MenuAction::Redo) if state.undo.redo(&mut state.document) => {
            refresh_active_scene(state);
        }
        Some(shell::MenuAction::ZoomFit) => {
            let solid_fit = state
                .solid_view
                .as_ref()
                .map(|view| view.aabb)
                .and_then(|aabb| {
                    let region = solid_viewport_region(state.viewport_rect, state.extent)?;
                    frame_current(state.camera3?, aabb, region.extent()).ok()
                });
            if let Some(next) = solid_fit {
                apply_nav_camera(state, next);
            } else {
                state.camera = zoom_fit(state.camera, &state.scene, state.viewport_rect);
            }
        }
        Some(shell::MenuAction::OpenSettings) => {
            state.shell.state.settings_dialog_open = true;
        }
        Some(shell::MenuAction::OpenKeyboardCustomize) => {
            state.shell.state.keyboard_dialog_open = true;
        }
        Some(shell::MenuAction::NewDocument) => {
            request_new_document(state);
        }
        Some(shell::MenuAction::OpenDocument) => {
            open_picker(
                state,
                bone_ui::widgets::FilePickerMode::Open,
                file_menu::FileKind::Document,
                None,
            );
        }
        Some(shell::MenuAction::SaveDocument) => {
            apply_save_in_place(state);
        }
        Some(shell::MenuAction::SaveDocumentAs) => {
            let seed = state.document.name().to_owned();
            open_picker(
                state,
                bone_ui::widgets::FilePickerMode::Save,
                file_menu::FileKind::Document,
                Some(seed),
            );
        }
        Some(shell::MenuAction::ImportStep) => {
            if state.step_job.is_none() {
                open_picker(
                    state,
                    bone_ui::widgets::FilePickerMode::Open,
                    file_menu::FileKind::Step,
                    None,
                );
            }
        }
        Some(shell::MenuAction::ExportStep) => {
            if state.step_job.is_none() {
                let seed = format!("{}.step", state.document.name());
                open_picker(
                    state,
                    bone_ui::widgets::FilePickerMode::Save,
                    file_menu::FileKind::Step,
                    Some(seed),
                );
            }
        }
        Some(shell::MenuAction::ConvertEntities) => apply_convert_entities(state),
        Some(shell::MenuAction::ToggleConstruction) => apply_construction_toggle(state),
        Some(shell::MenuAction::ToggleDriven) => apply_driven_toggle(state),
        Some(shell::MenuAction::TogglePlanes) => state.planes_visible = !state.planes_visible,
        Some(shell::MenuAction::Undo | shell::MenuAction::Redo | shell::MenuAction::ExitSketch)
        | None => {}
    }
}

fn request_new_document(state: &mut AppState) {
    if is_dirty(state) {
        state.pending_discard = Some(PendingDiscard::New);
    } else {
        apply_new_document(state);
    }
}

fn request_open_folder(state: &mut AppState, path: PathBuf) {
    if is_dirty(state) {
        state.pending_discard = Some(PendingDiscard::Open(path));
    } else {
        apply_open_folder(state, path);
    }
}

fn request_import_step(state: &mut AppState, path: PathBuf) {
    if is_dirty(state) {
        state.pending_discard = Some(PendingDiscard::ImportStep(path));
    } else {
        start_step_import(state, path);
    }
}

fn start_step_import(state: &mut AppState, path: PathBuf) {
    if state.step_job.is_some() {
        return;
    }
    match step_jobs::spawn_import(path, state.document.clone()) {
        Ok(job) => state.step_job = Some(job),
        Err(e) => notify_error(state, strings::NOTIFY_IMPORT_FAILED, e.to_string()),
    }
}

fn apply_export_step_as(state: &mut AppState, path: PathBuf, via: PickedVia) {
    let extension_appended = !file_menu::is_step_file(&path);
    let path = file_menu::with_step_extension(path);
    let unconfirmed = matches!(via, PickedVia::CustomPicker) || extension_appended;
    if unconfirmed && path.is_file() {
        state.pending_overwrite = Some(PendingOverwrite::StepExport(path));
        return;
    }
    start_step_export(state, path);
}

fn start_step_export(state: &mut AppState, path: PathBuf) {
    if state.step_job.is_some() {
        return;
    }
    match step_jobs::spawn_export(state.document.clone(), path) {
        Ok(job) => state.step_job = Some(job),
        Err(e) => notify_error(state, strings::NOTIFY_EXPORT_FAILED, e.to_string()),
    }
}

fn poll_step_job(state: &mut AppState) {
    let Some(job) = state.step_job.take() else {
        return;
    };
    match job {
        step_jobs::StepJob::Import { rx, baseline, meta } => match step_jobs::poll(&rx) {
            std::task::Poll::Pending => {
                state.step_job = Some(step_jobs::StepJob::Import { rx, baseline, meta });
            }
            std::task::Poll::Ready(result) => finish_import(state, result, &baseline, &meta),
        },
        step_jobs::StepJob::Export { rx, meta } => match step_jobs::poll(&rx) {
            std::task::Poll::Pending => {
                state.step_job = Some(step_jobs::StepJob::Export { rx, meta });
            }
            std::task::Poll::Ready(result) => finish_export(state, result, &meta),
        },
    }
}

fn finish_import(
    state: &mut AppState,
    result: step_jobs::JobResult<Box<Document>>,
    baseline: &Document,
    meta: &step_jobs::JobMeta,
) {
    match result {
        step_jobs::JobResult::Finished(_) if meta.cancel_requested() => {
            tracing::info!(file = %meta.file_name, "discarding import that finished after cancel");
        }
        step_jobs::JobResult::Finished(document) if *baseline != state.document => {
            state.pending_discard = Some(PendingDiscard::InstallImported {
                document,
                file_name: meta.file_name.clone(),
            });
        }
        step_jobs::JobResult::Finished(document) => {
            install_imported_document(state, *document);
            notify_info(
                state,
                strings::NOTIFY_IMPORTED,
                Some(meta.file_name.clone()),
            );
        }
        step_jobs::JobResult::Failed(bone_interop::StepError::Canceled) => {
            tracing::info!(file = %meta.file_name, "step import canceled");
        }
        step_jobs::JobResult::Failed(e) => {
            tracing::warn!(error = %e, file = %meta.file_name, "step import failed");
            notify_error(state, strings::NOTIFY_IMPORT_FAILED, e.to_string());
        }
        step_jobs::JobResult::WorkerLost => {
            tracing::error!(file = %meta.file_name, "step import worker stopped before reporting a result");
            notify_error(
                state,
                strings::NOTIFY_IMPORT_FAILED,
                "worker stopped before reporting a result".to_owned(),
            );
        }
    }
}

fn finish_export(
    state: &mut AppState,
    result: step_jobs::JobResult<()>,
    meta: &step_jobs::JobMeta,
) {
    match result {
        step_jobs::JobResult::Finished(()) => {
            notify_info(
                state,
                strings::NOTIFY_EXPORTED,
                Some(meta.file_name.clone()),
            );
        }
        step_jobs::JobResult::Failed(bone_interop::StepError::Canceled) => {
            tracing::info!(file = %meta.file_name, "step export canceled");
        }
        step_jobs::JobResult::Failed(e) => {
            tracing::warn!(error = %e, file = %meta.file_name, "step export failed");
            notify_error(state, strings::NOTIFY_EXPORT_FAILED, e.to_string());
        }
        step_jobs::JobResult::WorkerLost => {
            tracing::error!(file = %meta.file_name, "step export worker stopped before reporting a result");
            notify_error(
                state,
                strings::NOTIFY_EXPORT_FAILED,
                "worker stopped before reporting a result".to_owned(),
            );
        }
    }
}

fn is_dirty(state: &AppState) -> bool {
    state.last_saved.as_ref() != Some(&state.document)
}

fn apply_new_document(state: &mut AppState) {
    let sketch = default_sketch();
    let scene = match SketchScene::extract(&sketch) {
        Ok(s) => s,
        Err(e) => {
            tracing::warn!(error = %e, "scene extract failed on new document");
            notify_error(state, strings::NOTIFY_LOAD_FAILED, e.to_string());
            return;
        }
    };
    let document = empty_document();
    state.last_saved = Some(document.clone());
    state.document = document;
    state.scene = scene;
    state.mode = Mode::Idle;
    state.selection = Selection::default();
    state.framed_extrude = None;
    state.pending_sketch_draft = None;
    state.current_folder = None;
    state.pending_overwrite = None;
    let Some(undo_capacity) = NonZeroUsize::new(UNDO_CAPACITY) else {
        unreachable!("UNDO_CAPACITY constant is non-zero");
    };
    state.undo = UndoStack::with_capacity(undo_capacity);
    state.camera = zoom_fit(state.camera, &state.scene, state.viewport_rect);
}

fn open_picker(
    state: &mut AppState,
    mode: bone_ui::widgets::FilePickerMode,
    kind: file_menu::FileKind,
    seed_filename: Option<String>,
) {
    if state.file_picker.is_some() || state.native_picker.is_some() {
        return;
    }
    let starting_folder = state
        .current_folder
        .as_ref()
        .map(|f| f.path().to_owned())
        .or_else(|| {
            state
                .documents_root
                .is_dir()
                .then(|| state.documents_root.clone())
        });
    let title_key = file_menu::title_key(kind, mode);
    let accept_key = file_menu::accept_key(kind, mode);
    let title = state.strings.resolve(title_key).to_owned();
    let accept_label = state.strings.resolve(accept_key).to_owned();
    let native_req = native_picker::Request {
        mode,
        kind,
        title: title.as_str(),
        accept_label: accept_label.as_str(),
        seed_filename: seed_filename.as_deref(),
        current_folder: starting_folder.as_deref(),
    };
    match native_picker::spawn(native_req) {
        Ok(handle) => {
            state.native_picker = Some(handle);
            return;
        }
        Err(native_picker::SpawnError::Unsupported) => {
            tracing::debug!("native picker unavailable, falling back to custom picker");
        }
    }
    open_custom_picker(state, mode, kind, seed_filename);
}

fn open_custom_picker(
    state: &mut AppState,
    mode: bone_ui::widgets::FilePickerMode,
    kind: file_menu::FileKind,
    seed_filename: Option<String>,
) {
    let scan = match kind {
        file_menu::FileKind::Document => file_menu::scan_document_folders(&state.documents_root),
        file_menu::FileKind::Step => file_menu::scan_step_files(&state.documents_root),
    };
    let entries = match scan {
        Ok(v) => v,
        Err(e) => {
            tracing::warn!(error = %e, path = %state.documents_root.display(), "scan documents root failed");
            notify_error(state, strings::NOTIFY_SCAN_FAILED, e.to_string());
            Vec::new()
        }
    };
    state.file_picker = Some(file_menu::FilePickerSession::open(
        state.documents_root.clone(),
        mode,
        kind,
        seed_filename,
        entries,
    ));
}

fn poll_native_picker(state: &mut AppState) {
    let Some(handle) = state.native_picker.as_ref() else {
        return;
    };
    let outcome = match handle.poll() {
        std::task::Poll::Pending => return,
        std::task::Poll::Ready(o) => o,
    };
    let mode = handle.mode;
    let kind = handle.kind;
    state.native_picker = None;
    match outcome {
        native_picker::NativeOutcome::Path(path) => {
            route_picked_path(state, kind, mode, path, PickedVia::NativePortal);
        }
        native_picker::NativeOutcome::Cancelled => {}
        native_picker::NativeOutcome::Error(message) => {
            tracing::warn!(error = %message, "native picker errored, falling back to custom picker");
            let seed = matches!(mode, bone_ui::widgets::FilePickerMode::Save).then(|| match kind {
                file_menu::FileKind::Document => state.document.name().to_owned(),
                file_menu::FileKind::Step => format!("{}.step", state.document.name()),
            });
            open_custom_picker(state, mode, kind, seed);
        }
    }
}

fn route_picked_path(
    state: &mut AppState,
    kind: file_menu::FileKind,
    mode: bone_ui::widgets::FilePickerMode,
    path: PathBuf,
    via: PickedVia,
) {
    match (kind, mode) {
        (file_menu::FileKind::Document, bone_ui::widgets::FilePickerMode::Open) => {
            request_open_folder(state, path);
        }
        (file_menu::FileKind::Document, bone_ui::widgets::FilePickerMode::Save) => {
            apply_save_as(state, path);
        }
        (file_menu::FileKind::Step, bone_ui::widgets::FilePickerMode::Open) => {
            request_import_step(state, path);
        }
        (file_menu::FileKind::Step, bone_ui::widgets::FilePickerMode::Save) => {
            apply_export_step_as(state, path, via);
        }
    }
}

fn apply_save_in_place(state: &mut AppState) {
    let Some(folder) = state.current_folder.clone() else {
        let seed = state.document.name().to_owned();
        open_picker(
            state,
            bone_ui::widgets::FilePickerMode::Save,
            file_menu::FileKind::Document,
            Some(seed),
        );
        return;
    };
    if let Err(e) = bone_document::save(&state.document, &folder) {
        tracing::warn!(error = %e, path = %folder.path().display(), "save failed");
        notify_error(state, strings::NOTIFY_SAVE_FAILED, e.to_string());
        return;
    }
    state.last_saved = Some(state.document.clone());
    notify_info(state, strings::NOTIFY_SAVED, None);
}

fn apply_picker_command(
    state: &mut AppState,
    kind: file_menu::FileKind,
    command: file_menu::PickerCommand,
) {
    state.file_picker = None;
    match command {
        file_menu::PickerCommand::Cancel => {}
        file_menu::PickerCommand::Open(path) => {
            route_picked_path(
                state,
                kind,
                bone_ui::widgets::FilePickerMode::Open,
                path,
                PickedVia::CustomPicker,
            );
        }
        file_menu::PickerCommand::SaveAs(path) => {
            route_picked_path(
                state,
                kind,
                bone_ui::widgets::FilePickerMode::Save,
                path,
                PickedVia::CustomPicker,
            );
        }
    }
}

fn apply_open_folder(state: &mut AppState, path: PathBuf) {
    let folder = DocumentFolder::new(path);
    let document = match bone_document::load(&folder) {
        Ok(d) => d,
        Err(e) => {
            tracing::warn!(error = %e, path = %folder.path().display(), "load failed");
            notify_error(state, strings::NOTIFY_LOAD_FAILED, e.to_string());
            return;
        }
    };
    install_loaded_document(state, document, Some(folder));
}

fn apply_save_as(state: &mut AppState, path: PathBuf) {
    let folder = DocumentFolder::new(path);
    let in_place = state
        .current_folder
        .as_ref()
        .is_some_and(|current| same_folder(current.path(), folder.path()));
    if folder.document_file().is_file() && !in_place {
        state.pending_overwrite = Some(PendingOverwrite::Document(folder));
        return;
    }
    perform_save_to(state, folder);
}

fn perform_save_to(state: &mut AppState, folder: DocumentFolder) {
    let prior_name = state.document.name().to_owned();
    state
        .document
        .set_name(folder_display_name(&folder, &state.strings));
    if let Err(e) = bone_document::save(&state.document, &folder) {
        tracing::warn!(error = %e, path = %folder.path().display(), "save as failed");
        state.document.set_name(prior_name);
        notify_error(state, strings::NOTIFY_SAVE_FAILED, e.to_string());
        return;
    }
    state.current_folder = Some(folder);
    state.last_saved = Some(state.document.clone());
    notify_info(state, strings::NOTIFY_SAVED, None);
}

fn folder_display_name(folder: &DocumentFolder, string_table: &StringTable) -> String {
    folder.path().file_name().map_or_else(
        || {
            string_table
                .resolve(strings::DEFAULT_DOCUMENT_NAME)
                .to_owned()
        },
        |s| s.to_string_lossy().into_owned(),
    )
}

fn same_folder(a: &Path, b: &Path) -> bool {
    match (resolve_path(a), resolve_path(b)) {
        (Some(x), Some(y)) => x == y,
        _ => false,
    }
}

fn resolve_path(path: &Path) -> Option<PathBuf> {
    if let Ok(canon) = std::fs::canonicalize(path) {
        return Some(canon);
    }
    let parent = path.parent()?;
    let file_name = path.file_name()?;
    let parent_canon = std::fs::canonicalize(parent).ok()?;
    Some(parent_canon.join(file_name))
}

fn notify_error(state: &mut AppState, headline: bone_ui::strings::StringKey, detail: String) {
    state.notification = Some(Notification {
        kind: NotificationKind::Error,
        headline,
        detail: Some(detail),
    });
}

fn notify_info(
    state: &mut AppState,
    headline: bone_ui::strings::StringKey,
    detail: Option<String>,
) {
    state.notification = Some(Notification {
        kind: NotificationKind::Info,
        headline,
        detail,
    });
}

fn notify_stub(state: &mut AppState, label: bone_ui::strings::StringKey) {
    let detail = state.strings.resolve(label).to_owned();
    tracing::info!(label = %detail, "stub action invoked");
    notify_info(state, strings::NOTIFY_COMING_SOON, Some(detail));
}

fn apply_shortcut_bar_outcome(
    state: &mut AppState,
    outcome: Option<&shortcut_bar::ShortcutBarOutcome>,
) {
    let Some(outcome) = outcome else { return };
    if outcome.dismissed || outcome.activated.is_some() {
        state.shortcut_bar = None;
    }
}

fn install_imported_document(state: &mut AppState, document: Document) {
    install_loaded_document(state, document, None);
    state.last_saved = None;
}

fn install_loaded_document(
    state: &mut AppState,
    document: Document,
    folder: Option<DocumentFolder>,
) {
    state.last_saved = Some(document.clone());
    state.document = document;
    state.mode = Mode::Idle;
    state.selection = Selection::default();
    state.framed_extrude = None;
    state.pending_sketch_draft = None;
    state.current_folder = folder;
    state.pending_overwrite = None;
    let Some(undo_capacity) = NonZeroUsize::new(UNDO_CAPACITY) else {
        unreachable!("UNDO_CAPACITY constant is non-zero");
    };
    state.undo = UndoStack::with_capacity(undo_capacity);
    state.scene = SketchScene::empty();
    state.camera = zoom_fit(state.camera, &state.scene, state.viewport_rect);
}

fn persist_settings(state: &AppState) {
    settings::save(&state.settings);
}

fn apply_settings_change(state: &mut AppState, change: Option<settings::Settings>) {
    let Some(next) = change else { return };
    let overrides_changed = next.hotkey_overrides != state.settings.hotkey_overrides;
    if !overrides_changed {
        state.settings = next;
        persist_settings(state);
        return;
    }
    match hotkeys::compose_table(&next.hotkey_overrides) {
        Ok(table) => {
            state.hotkeys = table;
            state.settings = next;
            persist_settings(state);
        }
        Err(error) => {
            tracing::warn!(?error, "hotkey override rejected, retaining prior settings");
            state.shell.state.hotkey_capture.clear();
            notify_error(state, strings::NOTIFY_HOTKEY_CONFLICT, format!("{error}"));
        }
    }
}

fn apply_sketch_rename(state: &mut AppState, request: Option<shell::SketchRenameRequest>) {
    let Some(req) = request else { return };
    apply_sketch_rename_into(&mut state.document, &mut state.undo, req);
}

fn apply_extrude_rename(state: &mut AppState, request: Option<shell::ExtrudeRenameRequest>) {
    let Some(req) = request else { return };
    apply_extrude_rename_into(&mut state.document, &mut state.undo, req);
}

fn apply_feature_command(state: &mut AppState, command: Option<shell::FeatureCommand>) {
    let Some(command) = command else { return };
    if let shell::FeatureCommand::TogglePlaneHidden(feature) = command {
        if !state.hidden_planes.remove(&feature) {
            state.hidden_planes.insert(feature);
        }
        return;
    }
    if apply_feature_command_into(&mut state.document, &mut state.undo, command) {
        let tree = state.document.feature_tree();
        state
            .hidden_planes
            .retain(|feature| tree.node(*feature).is_some());
        refresh_active_scene(state);
        sync_solid_view(state);
    }
}

fn apply_feature_command_into(
    document: &mut Document,
    undo: &mut UndoStack,
    command: shell::FeatureCommand,
) -> bool {
    let snapshot = document.clone();
    let changed = match command {
        shell::FeatureCommand::Suppress(feature) => {
            document.suppress(feature);
            true
        }
        shell::FeatureCommand::Unsuppress(feature) => {
            document.unsuppress(feature);
            true
        }
        shell::FeatureCommand::RollbackToHere(feature) => {
            document.roll_to_here(feature);
            true
        }
        shell::FeatureCommand::Delete(target) => apply_feature_delete(document, target),
        shell::FeatureCommand::TogglePlaneHidden(_) => false,
    };
    if changed {
        undo.record(snapshot);
    }
    changed
}

fn apply_feature_delete(document: &mut Document, target: shell::FeatureTarget) -> bool {
    match target {
        shell::FeatureTarget::Sketch(sketch_id) => document.remove_sketch(sketch_id).is_some(),
        shell::FeatureTarget::Extrude(extrude_id) => document.remove_extrude(extrude_id).is_some(),
        shell::FeatureTarget::Datum(datum_id) => document.remove_datum(datum_id).is_some(),
        shell::FeatureTarget::Plane(_) => false,
    }
}

fn apply_feature_reorder(state: &mut AppState, reorder: Option<shell::FeatureReorder>) {
    let Some(reorder) = reorder else { return };
    let snapshot = state.document.clone();
    if state
        .document
        .reorder_feature(reorder.moved, reorder.anchor, reorder.before)
    {
        state.undo.record(snapshot);
        refresh_active_scene(state);
        sync_solid_view(state);
    }
}

fn begin_reattach(state: &mut AppState, request: Option<SketchId>) {
    let Some(sketch) = request else { return };
    state.pending_reattach = Some(sketch);
    notify_info(state, strings::NOTIFY_REATTACH_PICK, None);
}

fn apply_reattach(state: &mut AppState, sketch: SketchId, face: BrepFaceId) {
    let Some((face_ref, _)) = state.model.face_for_sketch(face) else {
        notify_info(state, strings::NOTIFY_REATTACH_NON_PLANAR, None);
        return;
    };
    let snapshot = state.document.clone();
    if state.document.bind_sketch_to_face(sketch, face_ref).is_ok() {
        state.pending_reattach = None;
        state.undo.record(snapshot);
        refresh_active_scene(state);
        sync_solid_view(state);
    }
}

fn next_sketch_label(document: &Document) -> String {
    format!("Sketch{}", document.sketches().count() + 1)
}

fn begin_face_sketch(state: &mut AppState, face: BrepFaceId) -> bool {
    let Some((face_ref, basis)) = state.model.face_for_sketch(face) else {
        return false;
    };
    state.brep_selection.clear();
    let snapshot = state.document.clone();
    let sketch_id = state.document.allocate_sketch();
    let label = next_sketch_label(&state.document);
    state
        .document
        .insert_sketch(sketch_id, label, bone_document::Sketch3::on_plane(basis));
    if state
        .document
        .bind_sketch_to_face(sketch_id, face_ref)
        .is_err()
    {
        state.document = snapshot;
        return false;
    }
    state.undo.record(snapshot);
    state.mode = Mode::enter_sketch(sketch_id);
    refresh_active_scene(state);
    sync_solid_view(state);
    state.camera = frame_face_sketch_camera(state, basis);
    true
}

fn apply_rollback_change(state: &mut AppState, change: Option<shell::RollbackChange>) {
    let Some(change) = change else { return };
    let before = state.document.rollback();
    let snapshot = state.document.clone();
    match change {
        shell::RollbackChange::ToEnd => state.document.roll_to_end(),
        shell::RollbackChange::ToFeature(feature) => state.document.roll_to_here(feature),
    }
    if state.document.rollback() != before {
        state.undo.record(snapshot);
        refresh_active_scene(state);
        sync_solid_view(state);
    }
}

fn apply_extrude_rename_into(
    document: &mut Document,
    undo: &mut UndoStack,
    request: shell::ExtrudeRenameRequest,
) {
    let shell::ExtrudeRenameRequest { id, label } = request;
    let trimmed = label.trim();
    let Some(current) = document.extrude_label(id) else {
        return;
    };
    if trimmed.is_empty() || current == trimmed {
        return;
    }
    let snapshot = document.clone();
    match document.rename_extrude(id, &label) {
        Ok(()) => undo.record(snapshot),
        Err(e) => tracing::warn!(error = %e, ?id, "extrude rename rejected"),
    }
}

fn apply_sketch_rename_into(
    document: &mut Document,
    undo: &mut UndoStack,
    request: shell::SketchRenameRequest,
) {
    let shell::SketchRenameRequest { id, label } = request;
    let trimmed = label.trim();
    let Some(current) = document.sketch_label(id) else {
        return;
    };
    if trimmed.is_empty() || current == trimmed {
        return;
    }
    let snapshot = document.clone();
    match document.rename_sketch(id, &label) {
        Ok(()) => undo.record(snapshot),
        Err(e) => tracing::warn!(error = %e, ?id, "sketch rename rejected"),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sketch_mode::{SketchSession, SplineDraftPoint};
    use bone_document::BrepError;
    use bone_ui::hotkey::KeyChord;
    use bone_ui::input::{KeyChar, KeyCode, KeyEvent, NamedKey};

    fn now16() -> FrameInstant {
        FrameInstant::from_duration(std::time::Duration::from_millis(16))
    }

    #[test]
    fn sketch3_point_helpers_dedup_coincident_and_flag_loose() {
        use bone_document::{Sketch3, Sketch3Entity};
        let (s, a) =
            Sketch3::new().with_entity(Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 0.0)));
        let (s, b) = s.with_entity(Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 0.0)));
        let (s, c) = s.with_entity(Sketch3Entity::point(Point3::from_mm(5.0, 0.0, 0.0)));
        assert_eq!(
            dedup_spline_control(
                &s,
                &[
                    SplineDraftPoint::fresh(a),
                    SplineDraftPoint::fresh(b),
                    SplineDraftPoint::fresh(c),
                ],
            ),
            vec![a, c],
            "two control points at the same position collapse to one",
        );

        let (s, standalone) = s.with_entity(Sketch3Entity::point(Point3::from_mm(0.0, 5.0, 0.0)));
        let (s, _line) = s.with_entity(Sketch3Entity::line(a, c, false));
        assert!(
            is_loose_point(&s, standalone),
            "a point no entity references is loose and safe to drop on abandon",
        );
        assert!(
            !is_loose_point(&s, a),
            "a point owned by a line is not loose"
        );
    }

    #[test]
    fn theme_tokens_drive_the_driven_dimension_color() {
        let theme = Theme::light();
        let styled = with_theme_tokens(&Style::light(), &theme);
        let [r, g, b] = theme.cad.sketch_driven.srgb_f32();
        let driven = styled.text().driven_color().to_rgba_array();
        assert!(
            (driven[0] - r).abs() < 1.0e-6
                && (driven[1] - g).abs() < 1.0e-6
                && (driven[2] - b).abs() < 1.0e-6,
            "the driven dimension color is the sketch_driven design token, not a baked render constant",
        );
    }

    #[test]
    fn strip_plain_letter_chords_keeps_modified_and_named_keys() {
        let plain_s = KeyEvent::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::NONE);
        let ctrl_s = KeyEvent::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL);
        let esc = KeyEvent::new(KeyCode::Named(NamedKey::Escape), ModifierMask::NONE);
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        input.keys_pressed = vec![plain_s, ctrl_s, esc];
        strip_plain_letter_chords(&mut input);
        assert_eq!(
            input.keys_pressed,
            vec![ctrl_s, esc],
            "strip removes plain letters, keeps modified chords and named keys"
        );

        let enter = KeyEvent::new(KeyCode::Named(NamedKey::Enter), ModifierMask::NONE);
        input.keys_pressed = vec![enter];
        strip_plain_letter_chords(&mut input);
        assert_eq!(
            input.keys_pressed,
            vec![enter],
            "with no plain letters strip is a no-op"
        );
    }

    #[test]
    fn sketch_overlay_camera_matches_the_sketch_camera_mapping()
    -> Result<(), Box<dyn std::error::Error>> {
        use bone_render::clip_from_world;
        use bone_types::{Point3, Tolerance, UnitVec3};

        let extent = ViewportExtent::new(ViewportPx::new(800), ViewportPx::new(600));
        let tol = Tolerance::new(1.0e-9);
        let (Ok(x_axis), Ok(y_axis)) = (
            UnitVec3::try_from_components(1.0, 1.0, 0.0, tol),
            UnitVec3::try_from_components(0.0, 0.0, 1.0, tol),
        ) else {
            panic!("the axes normalize");
        };
        let basis = SketchPlaneBasis::new(Point3::from_mm(3.0, -2.0, 5.0), x_axis, y_axis, tol)?;
        let camera = Camera2::new(extent)
            .with_zoom(PixelsPerMm::new(7.5))
            .with_pan(Vec2::from_mm(1.5, -0.5));
        let aabb = Aabb3::from_corners(
            Point3::from_mm(0.0, 0.0, 0.0),
            Point3::from_mm(6.0, 4.0, 8.0),
        );
        let region = ViewportRegion::at_origin(extent);
        let camera3 = sketch_overlay_camera(basis, camera, aabb, region)
            .ok_or("a positive zoom yields an overlay camera")?;
        let clip3 = clip_from_world(camera3, extent)?;
        let clip2 = camera.clip_from_world_mm();
        let ndc = |m: &[f32; 16], x: f64, y: f64, z: f64| {
            let v = [x, y, z, 1.0];
            let row = |r: usize| (0..4).map(|c| f64::from(m[c * 4 + r]) * v[c]).sum::<f64>();
            let w = row(3);
            (row(0) / w, row(1) / w)
        };
        [(0.0, 0.0), (2.0, 1.0), (-1.5, 3.0)]
            .into_iter()
            .for_each(|(sx, sy)| {
                let world = basis.origin()
                    + x_axis.into_vec(Length::new::<millimeter>(sx))
                    + y_axis.into_vec(Length::new::<millimeter>(sy));
                let (wx, wy, wz) = world.coords_mm();
                let (nx3, ny3) = ndc(&clip3, wx, wy, wz);
                let (nx2, ny2) = ndc(&clip2, sx, sy, 0.0);
                assert!(
                    (nx3 - nx2).abs() < 1.0e-4 && (ny3 - ny2).abs() < 1.0e-4,
                    "sketch point ({sx}, {sy}) lands at the same pixel under both cameras: \
                     overlay ({nx3}, {ny3}) vs sketch ({nx2}, {ny2})",
                );
            });
        Ok(())
    }

    #[test]
    fn cursor_to_world_maps_center_inverts_y_and_rejects_zero_extent()
    -> Result<(), Box<dyn std::error::Error>> {
        let extent = ViewportExtent::new(ViewportPx::new(200), ViewportPx::new(100));
        let camera = Camera2::new(extent)
            .with_pan(Vec2::from_mm(7.0, -3.0))
            .with_zoom(PixelsPerMm::new(5.0));
        let world = cursor_to_world(camera, WindowPoint::new(100.0, 50.0)).ok_or("center maps")?;
        let (x, y) = world.coords_mm();
        assert!(
            (x - 7.0).abs() < 1e-9 && (y - -3.0).abs() < 1e-9,
            "center maps to the camera pan",
        );

        let unpanned = Camera2::new(extent).with_zoom(PixelsPerMm::new(10.0));
        let (Some(above), Some(below)) = (
            cursor_to_world(unpanned, WindowPoint::new(100.0, 0.0)),
            cursor_to_world(unpanned, WindowPoint::new(100.0, 100.0)),
        ) else {
            panic!("top and bottom map");
        };
        assert!(
            above.coords_mm().1 > below.coords_mm().1,
            "up in the window is up in the world",
        );

        let zero = ViewportExtent::new(ViewportPx::new(0), ViewportPx::new(100));
        assert!(
            cursor_to_world(Camera2::new(zero), WindowPoint::new(0.0, 0.0)).is_none(),
            "a zero-width viewport has no world mapping",
        );
        Ok(())
    }

    fn empty_frame() -> shell::ShellFrame {
        shell::ShellFrame {
            paints: Vec::new(),
            overlay_paints: Vec::new(),
            viewport_rect: empty_rect(),
            activated_tool: None,
            activated_feature_tool: None,
            activated_relation: None,
            activated_dimension: None,
            dimension_edit: None,
            extrude_edit: None,
            extrude_arm_reference: None,
            revolve_edit: None,
            sweep_edit: None,
            feature_arm: None,
            plane_picked: None,
            datum_sketch_target: None,
            sketch_activated: None,
            sketch_rename: None,
            extrude_activated: None,
            extrude_rename: None,
            feature_command: None,
            feature_reorder: None,
            rollback_change: None,
            reattach_request: None,
            exit_sketch: false,
            confirm_action: None,
            menu_action: None,
            settings_change: None,
            view_pick: None,
            view_menu: None,
        }
    }

    #[test]
    fn classify_extrude_profile_separates_no_sketch_from_unique() {
        let empty = Document::new(DocumentId::default(), "Untitled".to_owned());
        assert!(matches!(
            super::classify_extrude_profile(&empty),
            super::ProfileChoice::NoSketch
        ));

        let (blank, _) = super::initial_document(&Sketch::new(Plane::Xy.basis()));
        assert!(
            matches!(
                super::classify_extrude_profile(&blank),
                super::ProfileChoice::NoSketch
            ),
            "an entity-less sketch is not an extrude profile",
        );

        let (one, id) = super::initial_document(&rectangle_sketch());
        assert!(matches!(
            super::classify_extrude_profile(&one),
            super::ProfileChoice::Unique(found) if found == id
        ));
    }

    fn rectangle_sketch() -> Sketch {
        let sketch = Sketch::new(Plane::Xy.basis());
        let (sketch, p0) = tools::add_point(sketch, Point2::from_mm(0.0, 0.0));
        let (sketch, p1) = tools::add_point(sketch, Point2::from_mm(10.0, 0.0));
        let (sketch, p2) = tools::add_point(sketch, Point2::from_mm(10.0, 6.0));
        let (sketch, p3) = tools::add_point(sketch, Point2::from_mm(0.0, 6.0));
        let (sketch, _) = tools::add_line(sketch, p0, p1, false);
        let (sketch, _) = tools::add_line(sketch, p1, p2, false);
        let (sketch, _) = tools::add_line(sketch, p2, p3, false);
        let (sketch, _) = tools::add_line(sketch, p3, p0, false);
        sketch
    }

    fn tall_rectangle_sketch() -> Sketch {
        let sketch = Sketch::new(Plane::Xy.basis());
        let (sketch, p0) = tools::add_point(sketch, Point2::from_mm(0.0, 0.0));
        let (sketch, p1) = tools::add_point(sketch, Point2::from_mm(10.0, 0.0));
        let (sketch, p2) = tools::add_point(sketch, Point2::from_mm(10.0, 20.0));
        let (sketch, p3) = tools::add_point(sketch, Point2::from_mm(0.0, 20.0));
        let (sketch, _) = tools::add_line(sketch, p0, p1, false);
        let (sketch, _) = tools::add_line(sketch, p1, p2, false);
        let (sketch, _) = tools::add_line(sketch, p2, p3, false);
        let (sketch, _) = tools::add_line(sketch, p3, p0, false);
        sketch
    }

    fn circle_profile_zx() -> bone_document::Sketch3 {
        use bone_document::{Sketch3, Sketch3Entity};
        let sketch = Sketch3::on_plane(PrincipalPlane::Zx.basis());
        let (sketch, center) =
            sketch.with_entity(Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 0.0)));
        let (sketch, _) = sketch.with_entity(Sketch3Entity::circle(
            center,
            Length::new::<millimeter>(10.0),
            UnitVec3::y_axis(),
            false,
        ));
        sketch
    }

    fn line_path_xy(length_mm: f64) -> bone_document::Sketch3 {
        use bone_document::{Sketch3, Sketch3Entity};
        let sketch = Sketch3::on_plane(PrincipalPlane::Xy.basis());
        let (sketch, a) = sketch.with_entity(Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 0.0)));
        let (sketch, b) =
            sketch.with_entity(Sketch3Entity::point(Point3::from_mm(0.0, length_mm, 0.0)));
        let (sketch, _) = sketch.with_entity(Sketch3Entity::line(a, b, false));
        sketch
    }

    #[test]
    fn extrude_preview_evaluates_edits_reaches_and_frames() -> Result<(), Box<dyn std::error::Error>>
    {
        use bone_document::ExtrudeEndCondition;

        let id = SketchId::default();
        let cache_feature =
            super::PreviewFeature::Extrude(sketch_mode::default_extrude_feature(id));
        let base_version = Sketch::new(Plane::Xy.basis()).version();
        let edited_version = rectangle_sketch().version();
        assert_ne!(
            base_version, edited_version,
            "a sketch edit must bump the version this gate keys on"
        );
        let base_inputs = vec![(id, base_version)];
        let cached = super::ExtrudePreview {
            feature: cache_feature.clone(),
            inputs: base_inputs.clone(),
            generation: None,
            failed: false,
            error: None,
        };
        assert!(super::extrude_preview_is_current(
            Some(&cached),
            &cache_feature,
            &base_inputs
        ));
        assert!(
            !super::extrude_preview_is_current(
                Some(&cached),
                &cache_feature,
                &[(id, edited_version)],
            ),
            "editing the sketch under the same feature must invalidate the cached preview",
        );
        assert!(!super::extrude_preview_is_current(
            None,
            &cache_feature,
            &base_inputs
        ));

        let empty = Document::new(DocumentId::default(), "Empty".to_owned());
        assert!(
            super::compute_extrude_preview(
                &empty,
                &[],
                &super::PreviewFeature::Extrude(sketch_mode::default_extrude_feature(
                    SketchId::default(),
                )),
            )
            .is_none(),
            "a missing sketch previews nothing",
        );

        let (mut document, sketch_id) = super::initial_document(&rectangle_sketch());
        let feature = sketch_mode::default_extrude_feature(sketch_id);
        let preview_feature = super::PreviewFeature::Extrude(feature);
        let preview = super::compute_extrude_preview(&document, &[], &preview_feature)
            .ok_or("a registered profile sketch yields an evaluated preview")?;
        let solid = preview
            .solid()
            .ok_or("the closed rectangle extrudes to a solid")?;
        let view = super::preview_solid_view(solid)?;
        let extent = ViewportExtent::new(ViewportPx::new(256), ViewportPx::new(256));
        let region = ViewportRegion::at_origin(extent);
        let camera = frame_standard_view(view.aabb, extent, StandardView::Isometric, None).ok();
        assert!(
            camera.is_some(),
            "the solid aabb frames an isometric camera"
        );
        assert!(
            super::preview_solid_frame(Some(&view), camera, region).is_some(),
            "a framed preview lowers to a solid frame view",
        );
        assert!(
            super::preview_solid_frame(Some(&view), None, region).is_none(),
            "without a camera there is nothing to frame",
        );
        let first = preview.generation();

        let base_sketch = document.sketch3(sketch_id).ok_or("sketch is registered")?;
        let evaluated = evaluate_sketch(base_sketch);
        let base_body = evaluate_extrude_reaching(FeatureId::default(), &evaluated, &feature, &[])
            .solid()
            .cloned()
            .ok_or("base block builds")?;
        let up_to_next = super::PreviewFeature::Extrude(ExtrudeFeature {
            end_condition: ExtrudeEndCondition::UpToNext,
            ..feature
        });
        assert!(
            super::compute_extrude_preview(&document, &[&base_body], &up_to_next)
                .and_then(|preview| preview.solid().cloned())
                .is_some(),
            "up to next reaches the obstacle body and previews a solid",
        );
        assert!(
            super::compute_extrude_preview(&document, &[], &up_to_next)
                .is_some_and(|preview| preview.result().is_err()),
            "with no obstacle body there is no next face to stop at",
        );

        document.replace_sketch(
            sketch_id,
            super::sketch3_project::embed_sketch(&tall_rectangle_sketch()),
        );
        let second = super::compute_extrude_preview(&document, &[], &preview_feature)
            .and_then(|preview| preview.generation());
        let (Some(first), Some(second)) = (first, second) else {
            panic!("both rectangles extrude to a solid");
        };
        assert_ne!(
            first, second,
            "a sketch edit under the same feature must re-evaluate against the edited geometry",
        );
        Ok(())
    }

    #[test]
    #[cfg_attr(
        debug_assertions,
        ignore = "frame budget assertions are only meaningful in release builds"
    )]
    fn extrude_live_preview_under_frame_budget() -> Result<(), Box<dyn std::error::Error>> {
        use bone_document::ExtrudeEndCondition;
        use bone_types::PositiveLength;
        use std::time::{Duration, Instant};
        use uom::si::length::millimeter;

        const BASE_MM: f64 = 4.0;
        const STEP_MM: f64 = 0.5;
        const STEPS: u32 = 16;

        let (document, id) = super::initial_document(&rectangle_sketch());
        let blind = |depth_mm: f64| {
            let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(depth_mm)) else {
                panic!("{depth_mm} mm is a positive depth");
            };
            ExtrudeFeature {
                end_condition: ExtrudeEndCondition::Blind { depth },
                ..sketch_mode::default_extrude_feature(id)
            }
        };
        let edit = |depth_mm: f64| {
            let started = Instant::now();
            let Some(preview) = super::compute_extrude_preview(
                &document,
                &[],
                &super::PreviewFeature::Extrude(blind(depth_mm)),
            ) else {
                panic!("the rectangle extrudes at {depth_mm} mm");
            };
            let Some(solid) = preview.solid() else {
                panic!("the rectangle yields a solid at {depth_mm} mm");
            };
            let Ok(_view) = super::preview_solid_view(solid) else {
                panic!("the slab tessellates and packs scenes at {depth_mm} mm");
            };
            started.elapsed()
        };

        let _warmup = edit(BASE_MM);
        let durations: Vec<Duration> = (0..STEPS)
            .map(|i| edit(BASE_MM + STEP_MM * f64::from(i)))
            .collect();
        let sorted = {
            let mut v = durations.clone();
            v.sort();
            v
        };
        let median = sorted[sorted.len() / 2];
        let &worst = sorted.last().ok_or("preview loop produced zero samples")?;
        let budget = BudgetCeiling::FRAME_16MS.duration();
        assert!(
            median <= budget,
            "median evaluate+tessellate+scene step {median:?} exceeds {budget:?} frame budget; samples {durations:?}",
        );
        assert!(
            worst <= budget * 2,
            "worst evaluate+tessellate+scene step {worst:?} exceeds the relaxed ceiling; samples {durations:?}",
        );
        Ok(())
    }

    #[test]
    fn the_startup_sketch_is_empty() {
        let sketch = super::default_sketch();
        assert!(
            sketch.entities().is_empty(),
            "a fresh document opens with an empty sketch to draw into",
        );
    }

    #[test]
    fn unconsumed_sketches_lists_only_standalone_geometry() {
        let (mut document, consumed) = super::initial_document(&rectangle_sketch());
        let _extrude = document.commit_extrude(sketch_mode::default_extrude_feature(consumed));
        let standalone = document.allocate_sketch();
        document.insert_sketch(
            standalone,
            "Standalone".to_owned(),
            super::sketch3_project::embed_sketch(&rectangle_sketch()),
        );
        let empty = document.allocate_sketch();
        document.insert_sketch(empty, "Empty".to_owned(), bone_document::Sketch3::new());
        let listed: Vec<_> = super::unconsumed_sketches(&document)
            .into_iter()
            .map(|(id, _)| id)
            .collect();
        assert_eq!(
            listed,
            vec![standalone],
            "an extrude-consumed sketch and an empty sketch both stay hidden in Idle",
        );
    }

    #[test]
    fn build_combined_view_merges_every_evaluated_body() -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, s1) = super::initial_document(&rectangle_sketch());
        let e1 = document.commit_extrude(sketch_mode::default_extrude_feature(s1));
        let s2 = document.allocate_sketch();
        document.insert_sketch(
            s2,
            "Sketch2".to_owned(),
            super::sketch3_project::embed_sketch(&rectangle_sketch()),
        );
        let _e2 = document.commit_extrude(sketch_mode::default_extrude_feature(s2));
        let mut model = EvaluatedModel::new();
        model.recompute(
            &document,
            document.suppressed(),
            document.rollback(),
            RecomputeScope::Full,
        );
        assert_eq!(
            super::body_passes(&model).len(),
            2,
            "two separate bodies build"
        );
        let f1 = document
            .feature_tree()
            .feature_of_extrude(e1)
            .ok_or("the first extrude resolves to a feature id")?;
        let solid = model.body(f1).ok_or("the first body is evaluated")?;
        let single = super::preview_solid_view(solid)?;
        let combined = super::build_combined_view(&model, super::MeshQuality::Fine)
            .ok_or("the combined view is built from both bodies")?;
        assert_eq!(
            combined.faces.triangles().len(),
            2 * single.faces.triangles().len(),
            "the combined mesh holds both bodies",
        );
        Ok(())
    }

    #[test]
    fn mesh_quality_and_tolerances_track_the_drag() {
        assert_eq!(
            super::desired_mesh_quality(&Mode::Idle),
            super::MeshQuality::Fine
        );
        assert_eq!(
            super::desired_mesh_quality(&Mode::enter_sketch(SketchId::default())),
            super::MeshQuality::Fine,
            "a sketch with no active drag stays fine",
        );
        let dragging = Mode::Sketch {
            sketch_id: SketchId::default(),
            session: Box::new(SketchSession {
                drag: Some(DragSession {
                    entity: bone_types::SketchEntityId::default(),
                    press: Point2::origin(),
                    pins: DragPins::from_array([None, None, None]),
                }),
                ..SketchSession::default()
            }),
        };
        assert_eq!(
            super::desired_mesh_quality(&dragging),
            super::MeshQuality::Dragging,
            "dragging a sketch entity switches to the coarse mesh",
        );

        let close = |a: f64, b: f64| (a - b).abs() < 1e-9;
        let big = Aabb3::from_corners(
            Point3::from_mm(0.0, 0.0, 0.0),
            Point3::from_mm(100.0, 100.0, 100.0),
        );
        let (fine_chord, fine_angle) = super::mesh_tolerances(super::MeshQuality::Fine, &big);
        assert!(close(fine_chord.millimeters(), super::PREVIEW_CHORD_MM));
        assert!(close(fine_angle.radians(), super::PREVIEW_ANGLE.radians()));
        let (drag_chord, drag_angle) = super::mesh_tolerances(super::MeshQuality::Dragging, &big);
        assert!(
            drag_chord.millimeters() > fine_chord.millimeters(),
            "a large body tessellates coarser while dragging",
        );
        assert!(close(drag_angle.radians(), super::DRAG_ANGLE.radians()));
        let tiny = Aabb3::from_corners(
            Point3::from_mm(0.0, 0.0, 0.0),
            Point3::from_mm(1.0, 1.0, 1.0),
        );
        let (tiny_chord, _) = super::mesh_tolerances(super::MeshQuality::Dragging, &tiny);
        assert!(
            close(tiny_chord.millimeters(), super::PREVIEW_CHORD_MM),
            "a tiny body never drops below the fine chord",
        );
    }

    fn full_recompute(model: &mut EvaluatedModel, document: &Document) {
        model.recompute(
            document,
            document.suppressed(),
            document.rollback(),
            RecomputeScope::Full,
        );
    }

    #[test]
    fn face_bound_sketch_creation_then_break_offers_reattach()
    -> Result<(), Box<dyn std::error::Error>> {
        use bone_types::FaceRole;
        let (mut document, base_sketch) = super::initial_document(&rectangle_sketch());
        let base = document.commit_extrude(sketch_mode::default_extrude_feature(base_sketch));
        let mut model = EvaluatedModel::new();
        full_recompute(&mut model, &document);

        let body = document
            .feature_tree()
            .feature_of_extrude(base)
            .ok_or("the base extrude resolves to a feature id")?;
        let solid = model.body(body).ok_or("the base body is evaluated")?;
        let face = solid
            .iter_faces()
            .find_map(|candidate| {
                matches!(candidate.label().role, FaceRole::EndCap).then(|| candidate.id())
            })
            .ok_or("the box has a planar cap")?;
        let (face_ref, _basis) = model
            .face_for_sketch(face)
            .ok_or("a planar cap yields a face reference and basis")?;

        let face_sketch = document.allocate_sketch();
        document.insert_sketch(
            face_sketch,
            "Sketch2".to_owned(),
            super::sketch3_project::embed_sketch(&rectangle_sketch()),
        );
        document.bind_sketch_to_face(face_sketch, face_ref)?;
        let face_extrude =
            document.commit_extrude(sketch_mode::default_extrude_feature(face_sketch));
        full_recompute(&mut model, &document);
        assert_eq!(
            super::body_passes(&model).len(),
            1,
            "the merging face-bound boss unions into the base body",
        );

        document.remove_extrude(base);
        full_recompute(&mut model, &document);
        let face_feature = document
            .feature_tree()
            .feature_of_sketch(face_sketch)
            .ok_or("the face-bound sketch keeps its feature id")?;
        assert!(
            matches!(
                model.status(face_feature),
                Some(RebuildStatus::Error(RebuildError::DanglingReference(_)))
            ),
            "deleting the base dangles the face-bound sketch",
        );
        let face_extrude_feature = document
            .feature_tree()
            .feature_of_extrude(face_extrude)
            .ok_or("the face-bound extrude keeps its feature id")?;
        assert_eq!(
            model.status(face_extrude_feature),
            Some(RebuildStatus::Error(RebuildError::UpstreamUnresolved)),
            "the downstream extrude is blamed as upstream-unresolved, not a second dangling owner",
        );
        let strings = strings::make_strings(bone_ui::strings::Locale::EnUs);
        let wrong = super::compute_whats_wrong(&model, &document, &strings);
        assert_eq!(
            wrong
                .iter()
                .filter(|entry| entry.reattach.is_some())
                .count(),
            1,
            "only the sketch that owns the dangling reference offers reattach",
        );
        assert!(
            wrong
                .iter()
                .any(|entry| entry.reattach == Some(face_sketch)),
            "the dangling face-bound sketch offers reattach",
        );
        assert!(
            wrong.iter().any(|entry| entry.message
                == LabelText::Key(strings::WHATS_WRONG_UPSTREAM)
                && entry.reattach.is_none()),
            "the downstream body reports an upstream failure with no reattach of its own",
        );
        Ok(())
    }

    fn offscreen_context(extent: ViewportExtent, remaining: u32) -> bone_render::OffscreenContext {
        use bone_render::{AdapterPolicy, OffscreenContext, RenderError};
        match pollster::block_on(OffscreenContext::new(extent, AdapterPolicy::Platform)) {
            Ok(ctx) => ctx,
            Err(RenderError::NoAdapter(_) | RenderError::Device(_)) if remaining > 0 => {
                std::thread::sleep(std::time::Duration::from_millis(100));
                offscreen_context(extent, remaining - 1)
            }
            Err(e) => panic!("offscreen context init failed: {e}"),
        }
    }

    struct OffscreenFrameTarget {
        ctx: bone_render::OffscreenContext,
    }

    impl FrameTarget for OffscreenFrameTarget {
        fn picker(&self, index: PickIndex) -> Picker<'_> {
            self.ctx.picker(index)
        }

        fn render(
            &mut self,
            build_passes: impl FnOnce(
                &mut wgpu::CommandEncoder,
                &wgpu::TextureView,
                &wgpu::TextureView,
                &wgpu::TextureView,
            ),
        ) {
            self.ctx.render_passes(build_passes);
        }
    }

    fn fresh_core() -> (AppCore, OffscreenFrameTarget) {
        let extent = ViewportExtent::new(ViewportPx::new(960), ViewportPx::new(640));
        let ctx = offscreen_context(extent, 3);
        let Ok(mut core) = AppCore::new(ctx.gpu(), ctx.color_format(), extent) else {
            panic!("app core init");
        };
        let mut target = OffscreenFrameTarget { ctx };
        let _ = core.render_frame(&mut target);
        (core, target)
    }

    fn extruded_box_model() -> (Document, EvaluatedModel) {
        let (mut document, sketch) = super::initial_document(&rectangle_sketch());
        let _extrude = document.commit_extrude(sketch_mode::default_extrude_feature(sketch));
        let mut model = EvaluatedModel::new();
        full_recompute(&mut model, &document);
        (document, model)
    }

    #[test]
    fn plane_and_free_sketch_lifecycle_enter_edit_and_gc() -> Result<(), Box<dyn std::error::Error>>
    {
        use bone_document::{Sketch3, Sketch3Entity};

        let (mut core, mut target) = fresh_core();
        let now = now16();

        assert!(
            core.state.solid_view.is_none(),
            "a fresh part has no solid body"
        );
        assert!(
            core.state.camera3.is_some(),
            "the reference planes still seed a 3D camera to orbit",
        );
        assert!(
            super::viewport_is_spatial(&core.state),
            "an empty part middle-drag orbits the reference planes rather than panning a flat sketch",
        );

        super::begin_plane_sketch(&mut core.state, Plane::Zx, now);
        assert!(
            core.state.mode.is_sketch3(),
            "picking the top plane enters a native 3D sketch",
        );
        assert_eq!(core.state.mode.sketch3_plane(), Some(Plane::Zx));
        let plane_id = core
            .state
            .mode
            .sketch3_id()
            .ok_or("the new sketch has an id")?;
        let datum = core
            .state
            .document
            .feature_tree()
            .principal_plane_feature(PrincipalPlane::Zx);
        assert_eq!(
            core.state.document.sketch_plane_binding(plane_id),
            datum.map(SketchPlaneRef::Datum),
            "the sketch is bound to the top-plane datum",
        );

        core.state
            .document
            .replace_sketch(plane_id, sketch3_project::embed_sketch(&rectangle_sketch()));
        core.state.mode = Mode::Idle;
        assert!(
            super::edit_existing_sketch(&mut core.state, plane_id, now),
            "a datum-plane sketch re-opens as a native 3D sketch",
        );
        assert!(
            core.state.mode.is_sketch3(),
            "re-edit stays in 3D, not flat 2D"
        );
        assert_eq!(core.state.mode.sketch3_id(), Some(plane_id));
        assert_eq!(core.state.mode.sketch3_plane(), Some(Plane::Zx));
        core.state.mode = Mode::Idle;

        super::arm_sketch3(&mut core.state);
        let free_id = core
            .state
            .mode
            .sketch3_id()
            .ok_or("the 3D sketch tool enters a native 3D sketch")?;
        let line = {
            let sketch = Sketch3::new();
            let (sketch, a) =
                sketch.with_entity(Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 0.0)));
            let (sketch, b) =
                sketch.with_entity(Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 20.0)));
            let (sketch, _) = sketch.with_entity(Sketch3Entity::line(a, b, false));
            sketch
        };
        core.state.document.replace_sketch(free_id, line);
        core.state.mode = Mode::Idle;
        assert!(
            super::edit_existing_sketch(&mut core.state, free_id, now),
            "a 3D-sketch line re-opens for editing even though it is not bound to a plane",
        );
        assert!(core.state.mode.is_sketch3(), "it re-opens as a 3D sketch");
        assert_eq!(core.state.mode.sketch3_id(), Some(free_id));
        core.state.mode = Mode::Idle;

        super::begin_plane_sketch(&mut core.state, Plane::Xy, now);
        let empty_id = core.state.mode.sketch3_id().ok_or("empty sketch id")?;
        core.state.mode = Mode::Idle;
        super::gc_abandoned_sketch(&mut core.state, Some(empty_id));
        assert!(
            core.state.document.sketch3(empty_id).is_none(),
            "leaving a plane sketch you never drew on removes it, SolidWorks-style",
        );

        super::begin_plane_sketch(&mut core.state, Plane::Xy, now);
        let drawn_id = core.state.mode.sketch3_id().ok_or("drawn sketch id")?;
        core.state
            .document
            .replace_sketch(drawn_id, sketch3_project::embed_sketch(&rectangle_sketch()));
        core.state.mode = Mode::Idle;
        super::gc_abandoned_sketch(&mut core.state, Some(drawn_id));
        assert!(
            core.state.document.sketch3(drawn_id).is_some(),
            "a sketch with geometry is never garbage collected on exit",
        );

        super::begin_plane_sketch(&mut core.state, Plane::Yz, now);
        let active_id = core.state.mode.sketch3_id().ok_or("sketch id")?;
        super::gc_abandoned_sketch(&mut core.state, Some(active_id));
        assert!(
            core.state.document.sketch3(active_id).is_some(),
            "while still in the sketch, it is not collected even though it is empty",
        );

        let _ = core.render_frame(&mut target);
        Ok(())
    }

    #[test]
    fn extrude_arms_on_the_drawn_profile_ignoring_empty_plane_sketches()
    -> Result<(), Box<dyn std::error::Error>> {
        let now = now16();
        [false, true].into_iter().try_for_each(|empty_distractor| -> Result<(), Box<dyn std::error::Error>> {
            let (mut core, _target) = fresh_core();

            super::begin_plane_sketch(&mut core.state, Plane::Xy, now);
            let front_sketch = core.state.mode.sketch3_id().ok_or("front sketch id")?;
            let rect = sketch3_project::embed_sketch(&rectangle_sketch());
            core.state.document.replace_sketch(front_sketch, rect);
            core.state.mode = Mode::Idle;

            if empty_distractor {
                super::begin_plane_sketch(&mut core.state, Plane::Zx, now);
                core.state.mode = Mode::Idle;
            }

            super::arm_extrude(&mut core.state, ExtrudeOperation::default());
            assert!(
                matches!(
                    core.state.mode,
                    Mode::Extrude(super::ExtrudeArming::Profile { .. })
                ),
                "saving a plane profile arms extrude even past an empty second plane sketch; got {:?}",
                core.state.mode,
            );
        Ok(())
})?;
        Ok(())
    }

    #[test]
    fn re_editing_a_sweeps_path_line_on_a_plane_rebuilds_the_sweep()
    -> Result<(), Box<dyn std::error::Error>> {
        use bone_document::SweepPath;

        let (mut core, _target) = fresh_core();
        let now = now16();

        super::begin_plane_sketch(&mut core.state, Plane::Zx, now);
        let profile_id = core
            .state
            .mode
            .sketch3_id()
            .ok_or("profile plane sketch id")?;
        core.state
            .document
            .replace_sketch(profile_id, circle_profile_zx());
        core.state.mode = Mode::Idle;

        super::begin_plane_sketch(&mut core.state, Plane::Xy, now);
        let path_id = core.state.mode.sketch3_id().ok_or("path plane sketch id")?;
        core.state
            .document
            .replace_sketch(path_id, line_path_xy(40.0));
        core.state.mode = Mode::Idle;

        super::apply_feature_tool(&mut core.state, Some(FeatureTool::SweptBossBase));
        if matches!(core.state.mode, Mode::Sweep(SweepArming::AwaitingSketch(_))) {
            assert!(super::apply_sweep_sketch_pick(&mut core.state, profile_id));
        }
        let Mode::Sweep(SweepArming::Profile { feature, .. }) = &core.state.mode else {
            panic!("the sweep arms on its profile; got {:?}", core.state.mode);
        };
        assert_eq!(feature.path, SweepPath::Sketch(path_id));
        super::apply_sweep_confirm(&mut core.state, Some(shell::ConfirmAction::Accept));
        super::recompute_model(&mut core.state, RecomputeScope::Full);

        let sweep = core
            .state
            .document
            .feature_tree()
            .iter()
            .find_map(|(feature, node)| matches!(node, FeatureNode::Sweep(_)).then_some(feature))
            .ok_or("the sweep committed")?;
        let built_before = core.state.model.built_at(sweep);
        let span_before = core
            .state
            .model
            .body(sweep)
            .and_then(BrepSolid::bounding_box)
            .map(|aabb| aabb.extent().norm_mm())
            .ok_or("the swept body has a bounding box")?;

        assert!(
            super::edit_existing_sketch(&mut core.state, path_id, now),
            "the sweep's path line, drawn on a plane, re-opens for editing after the fact",
        );
        assert!(core.state.mode.is_sketch3(), "it re-opens as a 3D sketch");
        assert_eq!(core.state.mode.sketch3_id(), Some(path_id));

        core.state
            .document
            .replace_sketch(path_id, line_path_xy(70.0));
        core.state.mode = Mode::Idle;
        super::recompute_model(&mut core.state, RecomputeScope::Full);

        assert_ne!(
            core.state.model.built_at(sweep),
            built_before,
            "stretching the path line re-dirties and rebuilds the sweep",
        );
        let span_after = core
            .state
            .model
            .body(sweep)
            .and_then(BrepSolid::bounding_box)
            .map(|aabb| aabb.extent().norm_mm())
            .ok_or("the rebuilt swept body has a bounding box")?;
        assert!(
            span_after > span_before + 1.0,
            "the rebuilt sweep follows the longer path: {span_before} -> {span_after}",
        );
        Ok(())
    }

    #[test]
    fn document_level_sweep_builds_or_errors_by_path_resolution()
    -> Result<(), Box<dyn std::error::Error>> {
        use bone_document::{SweepFeature, SweepPath};

        let mut document = super::empty_document();
        let tree = document.feature_tree();
        let xy = tree
            .principal_plane_feature(PrincipalPlane::Xy)
            .ok_or("front plane seeded")?;
        let zx = tree
            .principal_plane_feature(PrincipalPlane::Zx)
            .ok_or("top plane seeded")?;
        let path_id = document.allocate_sketch();
        document.insert_sketch(path_id, "Path".to_owned(), line_path_xy(40.0));
        document.bind_sketch_to_datum_plane(path_id, xy)?;
        let profile_id = document.allocate_sketch();
        document.insert_sketch(profile_id, "Profile".to_owned(), circle_profile_zx());
        document.bind_sketch_to_datum_plane(profile_id, zx)?;
        let _sweep =
            document.commit_sweep(SweepFeature::boss(profile_id, SweepPath::Sketch(path_id)));
        let mut model = EvaluatedModel::new();
        full_recompute(&mut model, &document);
        assert!(
            model.bodies().next().is_some(),
            "a circle profile swept along a line on a perpendicular plane builds a solid",
        );

        let mut document = super::empty_document();
        let profile_id = document.allocate_sketch();
        document.insert_sketch(profile_id, "Profile".to_owned(), circle_profile_zx());
        let sweep = document.commit_sweep(SweepFeature::boss(
            profile_id,
            SweepPath::Sketch(SketchId::default()),
        ));
        let mut model = EvaluatedModel::new();
        full_recompute(&mut model, &document);
        let feature = document
            .feature_tree()
            .feature_of_sweep(sweep)
            .ok_or("sweep has a feature id")?;
        assert!(
            matches!(model.status(feature), Some(RebuildStatus::Error(_))),
            "an unresolved sweep path is a red error, not a silent rebuild-pending; got {:?}",
            model.status(feature),
        );
        assert!(
            model.bodies().next().is_none(),
            "the unresolved sweep produces no phantom body",
        );
        Ok(())
    }

    #[test]
    fn a_plane_sketch_records_an_undo_step_only_when_drawn()
    -> Result<(), Box<dyn std::error::Error>> {
        let now = now16();
        [false, true].into_iter().try_for_each(
            |drawn| -> Result<(), Box<dyn std::error::Error>> {
                let (mut core, _target) = fresh_core();
                assert!(
                    !core.state.undo.can_undo(),
                    "a fresh part has nothing to undo",
                );

                super::begin_plane_sketch(&mut core.state, Plane::Yz, now);
                let id = core.state.mode.sketch3_id().ok_or("plane sketch id")?;
                if drawn {
                    core.state
                        .document
                        .replace_sketch(id, sketch3_project::embed_sketch(&rectangle_sketch()));
                }
                core.state.mode = Mode::Idle;
                super::commit_pending_sketch_draft(&mut core.state);
                super::gc_abandoned_sketch(&mut core.state, Some(id));

                assert_eq!(
                    core.state.undo.can_undo(),
                    drawn,
                    "a plane sketch becomes a distinct undo step only once you draw in it",
                );
                assert_eq!(
                    core.state.document.sketch3(id).is_some(),
                    drawn,
                    "an untouched plane sketch is garbage collected while a drawn one survives",
                );
                Ok(())
            },
        )?;
        Ok(())
    }

    #[test]
    fn backing_out_of_an_untouched_plane_preserves_a_pending_redo()
    -> Result<(), Box<dyn std::error::Error>> {
        let (mut core, _target) = fresh_core();
        let now = now16();

        super::begin_plane_sketch(&mut core.state, Plane::Xy, now);
        let front = core
            .state
            .mode
            .sketch3_id()
            .ok_or("front plane sketch id")?;
        core.state
            .document
            .replace_sketch(front, sketch3_project::embed_sketch(&rectangle_sketch()));
        super::commit_pending_sketch_draft(&mut core.state);
        core.state.mode = Mode::Idle;
        super::gc_abandoned_sketch(&mut core.state, Some(front));

        assert!(core.state.undo.undo(&mut core.state.document));
        assert!(
            core.state.undo.can_redo(),
            "an undone edit is redoable before we go plane-hopping",
        );

        super::begin_plane_sketch(&mut core.state, Plane::Yz, now);
        let scratch = core
            .state
            .mode
            .sketch3_id()
            .ok_or("scratch plane sketch id")?;
        core.state.mode = Mode::Idle;
        super::commit_pending_sketch_draft(&mut core.state);
        super::gc_abandoned_sketch(&mut core.state, Some(scratch));

        assert!(
            core.state.undo.can_redo(),
            "double-clicking a plane and drawing nothing must not shred a pending redo",
        );
        Ok(())
    }

    #[test]
    fn two_candidate_sketches_arm_reassign_preview_and_commit_a_sweep()
    -> Result<(), Box<dyn std::error::Error>> {
        use bone_document::SweepPath;

        let (mut core, mut target) = fresh_core();

        let circle_id = core.state.document.allocate_sketch();
        core.state
            .document
            .insert_sketch(circle_id, "Circle".to_owned(), circle_profile_zx());
        let line_id = core.state.document.allocate_sketch();
        core.state
            .document
            .insert_sketch(line_id, "Line".to_owned(), line_path_xy(40.0));

        super::apply_feature_tool(&mut core.state, Some(FeatureTool::SweptBossBase));
        assert!(
            matches!(core.state.mode, Mode::Sweep(SweepArming::AwaitingSketch(_))),
            "two candidate sketches leave the profile ambiguous",
        );
        assert!(
            super::apply_sweep_sketch_pick(&mut core.state, circle_id),
            "clicking the profile sketch arms the sweep",
        );
        let Mode::Sweep(SweepArming::Profile { feature, .. }) = &core.state.mode else {
            panic!("the first pick arms the profile");
        };
        assert_eq!(feature.profile, circle_id);
        assert_eq!(
            feature.path,
            SweepPath::Sketch(line_id),
            "the lone other sketch auto-fills the path",
        );

        assert!(
            super::apply_sweep_sketch_pick(&mut core.state, line_id),
            "a second pick is not swallowed",
        );
        let Mode::Sweep(SweepArming::Profile { feature, .. }) = &core.state.mode else {
            panic!("the sweep is still armed after re-picking");
        };
        assert_eq!(
            feature.profile, line_id,
            "double-clicking a different sketch re-assigns the profile, no dead-end",
        );
        assert_eq!(
            feature.path,
            SweepPath::Sketch(circle_id),
            "the path re-auto-fills from the remaining sketch",
        );

        assert!(
            super::apply_sweep_sketch_pick(&mut core.state, circle_id),
            "re-picking the circle restores it as the profile",
        );
        let Mode::Sweep(SweepArming::Profile { feature, .. }) = &core.state.mode else {
            panic!("the sweep arms on its profile; got {:?}", core.state.mode);
        };
        assert_eq!(
            feature.path,
            SweepPath::Sketch(line_id),
            "the path auto-fills so no broken dropdown is needed",
        );

        super::recompute_model(&mut core.state, RecomputeScope::Full);
        super::sync_solid_view(&mut core.state);
        assert!(
            core.state.solid_view.is_some(),
            "the armed sweep renders a tentative preview before it is committed",
        );

        super::apply_sweep_confirm(&mut core.state, Some(shell::ConfirmAction::Accept));
        super::recompute_model(&mut core.state, RecomputeScope::Full);

        let sweep = core
            .state
            .document
            .feature_tree()
            .iter()
            .find_map(|(feature, node)| matches!(node, FeatureNode::Sweep(_)).then_some(feature))
            .ok_or("the sweep was committed to the document")?;
        assert_eq!(
            core.state.model.status(sweep),
            Some(RebuildStatus::UpToDate),
            "the committed sweep rebuilds clean, no red error",
        );
        assert!(
            core.state.model.bodies().next().is_some(),
            "drawing a profile + a path and hitting Sweep then confirm yields a real solid",
        );

        core.state.mode = Mode::Idle;
        let _ = core.render_frame(&mut target);
        assert!(
            core.state
                .solid_view
                .as_ref()
                .is_some_and(|view| !view.faces.triangles().is_empty()),
            "the swept solid actually reaches the viewport, not just the model",
        );
        Ok(())
    }

    #[test]
    fn a_normal_to_curve_datum_evaluates_its_plane_and_renders_it()
    -> Result<(), Box<dyn std::error::Error>> {
        use bone_document::{DatumGeometry, Sketch3, Sketch3Entity};

        let (mut core, _target) = fresh_core();

        let sketch = Sketch3::on_plane(PrincipalPlane::Xy.basis());
        let (sketch, a) = sketch.with_entity(Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 0.0)));
        let (sketch, end) =
            sketch.with_entity(Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 40.0)));
        let (sketch, _line) = sketch.with_entity(Sketch3Entity::line(a, end, false));
        let path_id = core.state.document.allocate_sketch();
        core.state
            .document
            .insert_sketch(path_id, "Path".to_owned(), sketch);
        super::recompute_model(&mut core.state, RecomputeScope::Full);

        core.state.mode = Mode::enter_sketch3(path_id, Spatial::principal(Plane::Xy));
        core.state.selection = crate::selection::Selection::Entities(vec![end]);
        super::apply_feature_tool(&mut core.state, Some(FeatureTool::DatumPlane));
        assert!(
            core.state.mode.is_datum(),
            "picking the plane tool with an endpoint selected arms a datum; got {:?}",
            core.state.mode,
        );

        let armed = super::reference_plane_scene(&core.state)
            .ok_or("arming a datum keeps the reference planes on screen")?;
        let previews: Vec<_> = armed
            .planes
            .iter()
            .filter(|plane| matches!(plane.label, PlaneLabel::Preview))
            .collect();
        assert_eq!(
            previews.len(),
            1,
            "the armed datum previews as a plane before the user accepts",
        );
        assert!(
            previews[0]
                .corners
                .iter()
                .all(|corner| (corner.coords_mm().2 - 40.0).abs() < 1.0e-6),
            "the preview plane sits at the endpoint depth",
        );

        super::apply_datum_confirm(&mut core.state, Some(shell::ConfirmAction::Accept));
        super::recompute_model(&mut core.state, RecomputeScope::Full);

        let datum = core
            .state
            .document
            .feature_tree()
            .iter()
            .find_map(|(feature, node)| matches!(node, FeatureNode::Datum(_)).then_some(feature))
            .ok_or("the datum was committed")?;
        assert_eq!(
            core.state.model.status(datum),
            Some(RebuildStatus::UpToDate),
            "the normal-to-curve datum rebuilds clean",
        );
        let Some(DatumGeometry::Plane(datum_plane)) = core.state.model.datum(datum) else {
            panic!("the datum evaluates to a plane");
        };
        let (x, y, z) = datum_plane.origin().coords_mm();
        assert!(
            x.abs() < 1.0e-9 && y.abs() < 1.0e-9 && (z - 40.0).abs() < 1.0e-9,
            "the plane sits on the picked path endpoint",
        );

        let scene = super::reference_plane_scene(&core.state)
            .ok_or("the committed part keeps its reference planes")?;
        let committed: Vec<_> = scene
            .planes
            .iter()
            .filter(|plane| matches!(plane.label, PlaneLabel::Named(_)))
            .collect();
        assert_eq!(
            committed.len(),
            1,
            "the accepted datum plane is handed to the renderer, not dropped",
        );
        let plane = committed[0];
        assert!(
            plane
                .corners
                .iter()
                .all(|corner| (corner.coords_mm().2 - 40.0).abs() < 1.0e-6),
            "the committed plane sits on the path endpoint",
        );
        let (nx, ny, nz) = plane.normal.components();
        assert!(
            nx.abs() < 1.0e-9 && ny.abs() < 1.0e-9 && (nz.abs() - 1.0).abs() < 1.0e-9,
            "the plane faces along the path line it was built normal to",
        );
        Ok(())
    }

    fn commit_normal_to_curve_datum(
        core: &mut AppCore,
        tip: Point3,
        select_tip: bool,
    ) -> (SketchId, FeatureId, bone_types::SketchEntityId) {
        use bone_document::{Sketch3, Sketch3Entity};
        let path = Sketch3::on_plane(PrincipalPlane::Xy.basis());
        let (path, start) = path.with_entity(Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 0.0)));
        let (path, tip_entity) = path.with_entity(Sketch3Entity::point(tip));
        let (path, line) = path.with_entity(Sketch3Entity::line(start, tip_entity, false));
        let path_id = core.state.document.allocate_sketch();
        core.state
            .document
            .insert_sketch(path_id, "Path".to_owned(), path);
        super::recompute_model(&mut core.state, RecomputeScope::Full);

        core.state.mode = Mode::enter_sketch3(path_id, Spatial::principal(Plane::Xy));
        let selected = if select_tip { tip_entity } else { start };
        core.state.selection = crate::selection::Selection::Entities(vec![selected]);
        super::apply_feature_tool(&mut core.state, Some(FeatureTool::DatumPlane));
        super::apply_datum_confirm(&mut core.state, Some(shell::ConfirmAction::Accept));
        super::recompute_model(&mut core.state, RecomputeScope::Full);

        let Some(datum) = core
            .state
            .document
            .feature_tree()
            .iter()
            .find_map(|(feature, node)| matches!(node, FeatureNode::Datum(_)).then_some(feature))
        else {
            panic!("the normal-to-curve datum was committed");
        };
        (path_id, datum, line)
    }

    #[test]
    fn sketching_on_a_tilted_datum_plane_draws_in_its_oblique_basis()
    -> Result<(), Box<dyn std::error::Error>> {
        use bone_document::DatumGeometry;

        let (mut core, mut target) = fresh_core();
        let (_path_id, datum, _path_line) =
            commit_normal_to_curve_datum(&mut core, Point3::from_mm(0.0, 40.0, 40.0), true);

        let Some(DatumGeometry::Plane(datum_plane)) = core.state.model.datum(datum) else {
            panic!("the datum evaluates to a plane");
        };
        let (nx, ny, nz) = datum_plane.normal().components();
        let root_half = core::f64::consts::FRAC_1_SQRT_2;
        assert!(
            nx.abs() < 1.0e-9
                && (ny.abs() - root_half).abs() < 1.0e-9
                && (nz.abs() - root_half).abs() < 1.0e-9,
            "the datum is genuinely oblique, not parallel to a principal plane",
        );

        let now = now16();
        core.state.mode = Mode::Idle;
        assert!(
            super::begin_datum_sketch(&mut core.state, datum, now),
            "double-activating the datum plane starts a sketch on it",
        );
        assert!(
            core.state.mode.is_sketch3(),
            "the datum host enters sketch3"
        );
        assert_eq!(
            core.state.mode.sketch3_plane(),
            None,
            "a datum host is not one of the three principal planes",
        );
        assert_eq!(
            core.state.mode.sketch3_source(),
            Some(SketchPlaneSource::Datum(datum)),
            "the session remembers it is sketching on the datum",
        );
        let sketch_basis = core
            .state
            .mode
            .sketch3_basis()
            .ok_or("the datum sketch carries a basis")?;
        assert_eq!(
            sketch_basis.normal().components(),
            datum_plane.normal().components(),
            "the sketch basis is the datum plane, not a principal fallback",
        );

        let sketch_id = core
            .state
            .mode
            .sketch3_id()
            .ok_or("the datum sketch has an id")?;
        assert_eq!(
            core.state.document.sketch_plane_binding(sketch_id),
            Some(SketchPlaneRef::Datum(datum)),
            "the new sketch is bound to the datum plane",
        );
        let base = core
            .state
            .document
            .sketch3(sketch_id)
            .ok_or("the bound sketch exists")?;
        let (drawn, dot) =
            sketch3_project::embed_point(base, sketch_basis, Point2::from_mm(12.0, 7.0));
        let world = drawn
            .point_position(dot)
            .ok_or("the placed point has a position")?;
        let (ox, oy, oz) = datum_plane.origin().coords_mm();
        let (px, py, pz) = world.coords_mm();
        let normal_offset = (px - ox) * nx + (py - oy) * ny + (pz - oz) * nz;
        assert!(
            normal_offset.abs() < 1.0e-6,
            "a point drawn on the datum sketch lies in the oblique plane, off every principal plane",
        );
        let _ = core.render_frame(&mut target);
        Ok(())
    }

    #[test]
    fn a_profile_pierced_to_a_normal_to_curve_datum_sweeps_along_the_path()
    -> Result<(), Box<dyn std::error::Error>> {
        use bone_document::{Sketch3, Sketch3Edit, Sketch3Entity, Sketch3Relation, SweepFeature};
        use bone_types::{Length, Sketch3EntityRef};
        use uom::si::length::millimeter;

        let (mut core, mut target) = fresh_core();
        let (path_id, datum, path_line) =
            commit_normal_to_curve_datum(&mut core, Point3::from_mm(0.0, 40.0, 0.0), false);
        let path_feature = core
            .state
            .document
            .feature_tree()
            .feature_of_sketch(path_id)
            .ok_or("path feature")?;

        let profile = Sketch3::new();
        let (profile, center) =
            profile.with_entity(Sketch3Entity::point(Point3::from_mm(12.0, 0.0, 0.0)));
        let (profile, _circle) = profile.with_entity(Sketch3Entity::circle(
            center,
            Length::new::<millimeter>(8.0),
            UnitVec3::z_axis(),
            false,
        ));
        let (profile, _pierce) = {
            let (next, _) = profile.apply(Sketch3Edit::AddRelation(Sketch3Relation::Pierce {
                point: center,
                curve: Sketch3EntityRef::new(path_feature, path_line),
            }));
            (next, ())
        };
        let profile_id = core.state.document.allocate_sketch();
        core.state
            .document
            .insert_sketch(profile_id, "Profile".to_owned(), profile);
        core.state
            .document
            .bind_sketch_to_datum_plane(profile_id, datum)?;

        let _sweep = core
            .state
            .document
            .commit_sweep(SweepFeature::boss(profile_id, SweepPath::Sketch(path_id)));
        core.state.mode = Mode::Idle;
        super::recompute_model(&mut core.state, RecomputeScope::Full);

        let sweep = core
            .state
            .document
            .feature_tree()
            .iter()
            .find_map(|(feature, node)| matches!(node, FeatureNode::Sweep(_)).then_some(feature))
            .ok_or("the sweep is committed")?;
        assert_eq!(
            core.state.model.status(sweep),
            Some(RebuildStatus::UpToDate),
            "profile on a normal-to-curve datum, pierced to the path, sweeps clean",
        );
        assert!(
            core.state.model.bodies().next().is_some(),
            "the arbitrary-path sweep yields a real solid",
        );

        let _ = core.render_frame(&mut target);
        assert!(
            core.state
                .solid_view
                .as_ref()
                .is_some_and(|view| !view.faces.triangles().is_empty()),
            "the swept solid reaches the viewport",
        );
        Ok(())
    }

    #[test]
    fn feature_arming_keeps_the_3d_workspace_not_the_flat_grid() {
        assert!(
            super::show_reference_planes(&Mode::Sweep(SweepArming::AwaitingSketch(
                ExtrudeOperation::default()
            ))),
            "a sweep shows planes + 3D sketches so the profile is not smooshed to 2D",
        );
        assert!(super::show_reference_planes(&Mode::Idle));
        assert!(super::show_reference_planes(&Mode::enter_sketch3(
            SketchId::default(),
            Spatial::principal(Plane::Xy)
        )));
        assert!(
            !super::show_reference_planes(&Mode::enter_sketch(SketchId::default())),
            "a flat 2D face sketch stays 2D, no reference planes",
        );
    }

    fn check_solid_golden(
        frame: &bone_render::SnapshotFrame,
        golden_rel: &str,
    ) -> Result<(), Box<dyn std::error::Error>> {
        use bone_render::{PixelDiff, PixelDiffThreshold, decode_png, encode_png};
        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(golden_rel);
        if std::env::var_os("BONE_UPDATE_REBUILD_GOLDENS").is_some() {
            let bytes = encode_png(frame)?;
            if let Some(parent) = path.parent() {
                std::fs::create_dir_all(parent)?;
            }
            std::fs::write(&path, &bytes)?;
            return Ok(());
        }
        let bytes = std::fs::read(&path)?;
        let (golden_extent, golden_rgba) = decode_png(&bytes)?;
        assert_eq!(golden_extent, frame.extent(), "golden extent drift");
        let threshold = PixelDiffThreshold::new(16.0 / 255.0);
        let report = PixelDiff::compare(frame, &golden_rgba, threshold)?;
        assert!(
            report.is_clean(),
            "rebuild-downstream golden drifted: {} mismatches, worst {:?}",
            report.over_threshold(),
            report.worst(),
        );
        Ok(())
    }

    #[test]
    fn rebuild_downstream_scene_matches_golden() -> Result<(), Box<dyn std::error::Error>> {
        use bone_types::FaceRole;
        let (mut document, base_sketch) = super::initial_document(&rectangle_sketch());
        let base = document.commit_extrude(sketch_mode::default_extrude_feature(base_sketch));
        let mut model = EvaluatedModel::new();
        full_recompute(&mut model, &document);

        let body = document
            .feature_tree()
            .feature_of_extrude(base)
            .ok_or("the base extrude resolves to a feature id")?;
        let solid = model.body(body).ok_or("the base body is evaluated")?;
        let cap = solid
            .iter_faces()
            .find_map(|candidate| {
                matches!(candidate.label().role, FaceRole::EndCap).then(|| candidate.id())
            })
            .ok_or("the box has a planar top cap")?;
        let (face_ref, _basis) = model
            .face_for_sketch(cap)
            .ok_or("a planar cap yields a face reference and basis")?;

        let face_sketch = document.allocate_sketch();
        document.insert_sketch(
            face_sketch,
            "Sketch2".to_owned(),
            super::sketch3_project::embed_sketch(&rectangle_sketch()),
        );
        document.bind_sketch_to_face(face_sketch, face_ref)?;
        let face_boss = bone_document::ExtrudeFeature {
            operation: bone_document::ExtrudeOperation::Boss(bone_document::MergeResult::Separate),
            ..sketch_mode::default_extrude_feature(face_sketch)
        };
        let _ = document.commit_extrude(face_boss);
        full_recompute(&mut model, &document);

        document.replace_sketch(
            base_sketch,
            super::sketch3_project::embed_sketch(&tall_rectangle_sketch()),
        );
        full_recompute(&mut model, &document);
        assert_eq!(
            super::body_passes(&model).len(),
            2,
            "editing the base sketch keeps both bodies",
        );

        let combined = super::build_combined_view(&model, super::MeshQuality::Fine)
            .ok_or("the rebuilt model lowers to a combined view")?;
        let extent = ViewportExtent::new(ViewportPx::new(256), ViewportPx::new(256));
        let camera = frame_standard_view(combined.aabb, extent, StandardView::Isometric, None)?;

        let ctx = offscreen_context(extent, 3);
        let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format());
        let frame = renderer.render_display(
            &ctx,
            &combined.faces,
            &combined.edges,
            camera,
            &Style::default(),
            DisplayMode::ShadedWithEdges,
        )?;
        check_solid_golden(&frame, "tests/goldens/rebuild_downstream_iso_256.png")?;
        Ok(())
    }

    #[test]
    fn unresolved_reference_arm_arms_reference_pickers_only() {
        let blind = sketch_mode::default_extrude_feature(SketchId::default());
        let check = |kind: EndConditionKind, expected: Option<shell::ReferenceSlot>| {
            let edit = shell::ExtrudeEdit::EndCondition(kind);
            assert_eq!(
                super::unresolved_reference_arm(edit, &edit.apply(blind)),
                expected,
                "{kind:?} arming",
            );
        };
        [
            EndConditionKind::UpToSurface,
            EndConditionKind::UpToVertex,
            EndConditionKind::UpToBody,
            EndConditionKind::OffsetFromSurface,
        ]
        .into_iter()
        .for_each(|kind| check(kind, Some(shell::ReferenceSlot::Direction1(kind))));
        [
            EndConditionKind::Blind,
            EndConditionKind::ThroughAll,
            EndConditionKind::MidPlane,
        ]
        .into_iter()
        .for_each(|kind| check(kind, None));
    }

    #[test]
    fn body_picks_map_to_index_labels_axes_and_tools() -> Result<(), Box<dyn std::error::Error>> {
        use super::PickedItem;
        use crate::sketch_mode::ReferenceKind;
        use bone_document::RevolveAxis;

        let (_document, model) = extruded_box_model();
        assert!(
            super::solid_pick_index(&model).is_some(),
            "an evaluated body yields a solid pick index",
        );
        assert!(
            super::solid_pick_index(&EvaluatedModel::new()).is_none(),
            "an empty model has nothing to pick",
        );

        let (body, ordinal, solid) = model.bodies().next().ok_or("the model owns a body")?;
        let key = super::BodyKey::new(body, ordinal);
        let face = solid.iter_faces().next().ok_or("the body owns a face")?;
        let label = face.label();
        let face_pick = Some(PickedItem::BrepFace(super::SolidFaceRef::new(
            key,
            face.id(),
        )));
        assert_eq!(
            super::captured_reference(&model, ReferenceKind::Face, face_pick),
            Some(shell::CapturedReference::Face(label)),
        );
        assert_eq!(
            super::captured_reference(&model, ReferenceKind::Body, face_pick),
            Some(shell::CapturedReference::Body(body)),
        );
        assert_eq!(
            super::captured_reference(&model, ReferenceKind::Vertex, face_pick),
            None,
            "a face pick cannot satisfy a vertex reference",
        );
        assert_eq!(
            super::picked_sweep_tool(face_pick),
            Some(SweepProfileKind::Solid {
                tool: BodyRef::new(body, ordinal),
            }),
            "picking a body's face designates that body as the tool",
        );
        assert_eq!(
            super::picked_revolve_axis(&model, face_pick),
            None,
            "a face pick cannot set the revolve axis",
        );

        let edge = solid.iter_edges().next().ok_or("the body owns an edge")?;
        let edge_label = edge.label();
        let edge_pick = Some(PickedItem::BrepEdge(super::SolidEdgeRef::new(
            key,
            edge.id(),
        )));
        assert_eq!(
            super::picked_revolve_axis(&model, edge_pick),
            Some(RevolveAxis::Edge(edge_label)),
        );
        assert_eq!(
            super::picked_sweep_tool(edge_pick),
            None,
            "an edge pick cannot designate a sweep tool body",
        );
        Ok(())
    }

    #[test]
    fn selecting_a_body_face_in_sketch_mode_highlights_its_boundary()
    -> Result<(), Box<dyn std::error::Error>> {
        use super::PickedItem;

        let (_document, model) = extruded_box_model();
        let basis = Plane::Xy.basis();

        let (body, ordinal, solid) = model.bodies().next().ok_or("the model owns a body")?;
        let key = super::BodyKey::new(body, ordinal);
        let face = solid.iter_faces().next().ok_or("the body owns a face")?;
        let pick = PickedItem::BrepFace(super::SolidFaceRef::new(key, face.id()));

        let boundary = super::convert_targets(&model, pick, basis);
        assert!(
            !boundary.is_empty(),
            "selecting a body face projects its boundary edges into the sketch highlight",
        );
        Ok(())
    }

    #[test]
    fn sketch3_tool_keys_select_point_and_line() {
        assert_eq!(super::sketch3_tool_for_key("l"), Some(SketchTool::Line));
        assert_eq!(super::sketch3_tool_for_key("P"), Some(SketchTool::Point));
        assert_eq!(super::sketch3_tool_for_key("x"), None);
    }

    fn sketch3_overlay_camera(aabb: bone_types::Aabb3) -> (Camera3, bone_render::ViewportRegion) {
        let extent = ViewportExtent::new(ViewportPx::new(256), ViewportPx::new(256));
        let Ok(camera) = frame_standard_view(aabb, extent, StandardView::Isometric, None) else {
            panic!("the overlay aabb frames a camera");
        };
        (camera, bone_render::ViewportRegion::at_origin(extent))
    }

    fn overlay_of(
        sketch: &bone_document::Sketch3,
        plane: SketchPlaneBasis,
        selection: &[bone_types::SketchEntityId],
    ) -> super::Sketch3Overlay {
        let (camera, region) = sketch3_overlay_camera(super::sketch3_aabb_view(sketch, plane).aabb);
        super::build_sketch3_overlay(
            sketch,
            selection,
            camera,
            region,
            LinearRgba::new(0.14, 0.24, 0.62, 1.0),
        )
    }

    #[test]
    fn sketch3_overlay_splits_lines_tessellates_circles_and_builds_ball_points() {
        let plane = sketch_mode::Plane::Xy.basis();
        let (sketch, start) = sketch3_project::embed_point(
            &bone_document::Sketch3::new(),
            plane,
            Point2::from_mm(0.0, 0.0),
        );
        let (sketch, end) = sketch3_project::embed_point(&sketch, plane, Point2::from_mm(5.0, 0.0));
        let (sketch, _solid_line) = sketch3_project::embed_line(&sketch, start, end, false);
        let (sketch, ctor_end) =
            sketch3_project::embed_point(&sketch, plane, Point2::from_mm(5.0, 5.0));
        let (sketch, _ctor_line) = sketch3_project::embed_line(&sketch, end, ctor_end, true);

        let overlay = overlay_of(&sketch, plane, &[start]);
        assert_eq!(
            overlay.normal_lines.genuine().len(),
            1,
            "the plain segment is a normal line",
        );
        assert_eq!(
            overlay.construction_lines.genuine().len(),
            1,
            "the construction segment routes to the dashed layer",
        );
        assert!(
            overlay.selected_lines.genuine().is_empty(),
            "no whole line is selected",
        );
        assert!(
            !overlay.points.triangles().is_empty(),
            "each endpoint renders a shaded ball, not a star",
        );
        assert!(
            overlay
                .normal_lines
                .genuine()
                .iter()
                .all(|edge| edge.pick() != PickId::NONE),
            "lines stay pickable",
        );

        let (circle_sketch, center) = sketch3_project::embed_point(
            &bone_document::Sketch3::new(),
            plane,
            Point2::from_mm(0.0, 0.0),
        );
        let (circle_sketch, _circle) =
            circle_sketch.with_entity(bone_document::Sketch3Entity::circle(
                center,
                Length::new::<millimeter>(5.0),
                plane.normal(),
                false,
            ));
        let circle_overlay = overlay_of(&circle_sketch, plane, &[]);
        assert!(
            circle_overlay.normal_lines.genuine().len() >= 8,
            "the circle renders as a tessellated polyline, got {}",
            circle_overlay.normal_lines.genuine().len(),
        );
    }

    #[test]
    fn embed_place_line_with_horizontal_snap_locks_to_plane_x() {
        let plane = sketch_mode::Plane::Xy.basis();
        let base = bone_document::Sketch3::new();
        let view0 = sketch3_project::project(&base, plane);
        let first = sketch3_project::embed_place(
            &base,
            &view0,
            SketchTool::Line,
            Point2::from_mm(0.0, 0.0),
            None,
            None,
            sketch3_project::PlaneLock::Locked,
        );
        let view1 = sketch3_project::project(&first.sketch, plane);
        let horizontal = SnapHit {
            kind: snap::SnapKind::Horizontal,
            world: Point2::from_mm(5.0, 0.0),
        };
        let second = sketch3_project::embed_place(
            &first.sketch,
            &view1,
            SketchTool::Line,
            Point2::from_mm(5.0, 0.0),
            first.pending,
            Some(horizontal),
            sketch3_project::PlaneLock::Locked,
        );
        let locked = second
            .sketch
            .relations()
            .values()
            .any(|relation| matches!(relation, Sketch3Relation::Horizontal(_)));
        assert!(
            locked,
            "a horizontal snap lands a first-class horizontal relation"
        );
        let lines = second
            .sketch
            .entities()
            .values()
            .filter(|e| matches!(e, bone_document::Sketch3Entity::Line(_)))
            .count();
        assert_eq!(lines, 1, "the second click embeds exactly one line");
    }

    fn place_two_point_line(lock: sketch3_project::PlaneLock) -> bone_document::Sketch3 {
        let plane = sketch_mode::Plane::Xy.basis();
        let base = bone_document::Sketch3::new();
        let view0 = sketch3_project::project(&base, plane);
        let first = sketch3_project::embed_place(
            &base,
            &view0,
            SketchTool::Line,
            Point2::from_mm(0.0, 0.0),
            None,
            None,
            lock,
        );
        let view1 = sketch3_project::project(&first.sketch, plane);
        let second = sketch3_project::embed_place(
            &first.sketch,
            &view1,
            SketchTool::Line,
            Point2::from_mm(5.0, 0.0),
            first.pending,
            None,
            lock,
        );
        second.sketch
    }

    fn on_plane_count(sketch: &bone_document::Sketch3) -> usize {
        sketch
            .relations()
            .values()
            .filter(|relation| matches!(relation, Sketch3Relation::OnPlane { .. }))
            .count()
    }

    #[test]
    fn plane_lock_controls_whether_clicks_pin_to_the_plane() {
        [
            (
                sketch3_project::PlaneLock::Free,
                0,
                "a native 3D click pins no point to the active plane",
            ),
            (
                sketch3_project::PlaneLock::Locked,
                2,
                "a planar click pins both endpoints onto the sketch plane",
            ),
        ]
        .into_iter()
        .for_each(|(lock, expected, why)| {
            assert_eq!(
                on_plane_count(&place_two_point_line(lock)),
                expected,
                "{why}"
            );
        });
    }

    type CenterRay = (
        Camera3,
        ViewportExtent,
        ViewportPoint,
        (f64, f64, f64),
        (f64, f64, f64),
    );

    fn center_ray(half_mm: f64) -> CenterRay {
        let extent = ViewportExtent::new(ViewportPx::new(256), ViewportPx::new(256));
        let aabb = Aabb3::from_corners(
            Point3::from_mm(-half_mm, -half_mm, -half_mm),
            Point3::from_mm(half_mm, half_mm, half_mm),
        );
        let Ok(camera) = frame_standard_view(aabb, extent, StandardView::Isometric, None) else {
            panic!("the box aabb frames a camera");
        };
        let Ok(pixel) = ViewportPoint::new(128.0, 128.0) else {
            panic!("the center pixel is valid");
        };
        let Ok((origin, direction)) = bone_render::world_ray(camera, extent, pixel) else {
            panic!("the center pixel casts a ray");
        };
        (
            camera,
            extent,
            pixel,
            origin.coords_mm(),
            direction.components(),
        )
    }

    #[test]
    fn nearest_snap3_grabs_ray_points_and_the_pierce_reference() {
        let (camera, extent, pixel, (ox, oy, oz), (dx, dy, dz)) = center_ray(20.0);
        let on_ray = Point3::from_mm(
            dx.mul_add(40.0, ox),
            dy.mul_add(40.0, oy),
            dz.mul_add(40.0, oz),
        );
        let off_ray = Point3::from_mm(
            dx.mul_add(40.0, ox) + 14.0,
            dy.mul_add(40.0, oy),
            dz.mul_add(40.0, oz) - 14.0,
        );
        let tol = Length::new::<millimeter>(5.0);

        let (sketch, _) =
            bone_document::Sketch3::new().with_entity(bone_document::Sketch3Entity::point(off_ray));
        let (sketch, on_id) = sketch.with_entity(bone_document::Sketch3Entity::point(on_ray));
        assert_eq!(
            sketch3_mode::nearest_snap3(&sketch, camera, extent, pixel, tol, None, None),
            Some(sketch3_mode::Snap3::Endpoint {
                point: on_id,
                at: on_ray,
            }),
            "the point under the cursor ray snaps, the offset point stays clear",
        );
        assert_eq!(
            sketch3_mode::nearest_snap3(&sketch, camera, extent, pixel, tol, Some(on_id), None),
            None,
            "excluding the on-ray point leaves nothing within tolerance",
        );

        let empty = bone_document::Sketch3::new();
        assert_eq!(
            sketch3_mode::nearest_snap3(&empty, camera, extent, pixel, tol, None, Some(on_ray)),
            Some(sketch3_mode::Snap3::Origin { at: on_ray }),
            "the plane's pierce point magnetizes even before any sketch geometry exists",
        );
        assert_eq!(
            sketch3_mode::nearest_snap3(&empty, camera, extent, pixel, tol, None, Some(off_ray)),
            None,
            "an off-ray pierce point does not snap",
        );
        let (with_point, _pt) = empty.with_entity(bone_document::Sketch3Entity::point(on_ray));
        assert!(
            matches!(
                sketch3_mode::nearest_snap3(
                    &with_point,
                    camera,
                    extent,
                    pixel,
                    tol,
                    None,
                    Some(on_ray),
                ),
                Some(sketch3_mode::Snap3::Origin { .. }),
            ),
            "the pierce reference takes priority over an entity point at the same spot",
        );
    }

    #[test]
    fn nearest_snap3_grabs_line_midpoint_and_edge() {
        let (camera, extent, pixel, (ox, oy, oz), (dx, dy, dz)) = center_ray(80.0);
        let m = (
            dx.mul_add(40.0, ox),
            dy.mul_add(40.0, oy),
            dz.mul_add(40.0, oz),
        );
        let plen = dx.hypot(dy);
        let (qx, qy, qz) = (dy / plen, -dx / plen, 0.0_f64);
        let along =
            |k: f64| Point3::from_mm(qx.mul_add(k, m.0), qy.mul_add(k, m.1), qz.mul_add(k, m.2));
        let tol = Length::new::<millimeter>(5.0);

        let (mid_sketch, ma) = bone_document::Sketch3::new()
            .with_entity(bone_document::Sketch3Entity::point(along(10.0)));
        let (mid_sketch, mb) =
            mid_sketch.with_entity(bone_document::Sketch3Entity::point(along(-10.0)));
        let (mid_sketch, mid_line) =
            mid_sketch.with_entity(bone_document::Sketch3Entity::line(ma, mb, false));
        match sketch3_mode::nearest_snap3(&mid_sketch, camera, extent, pixel, tol, None, None) {
            Some(sketch3_mode::Snap3::Midpoint { line, .. }) => assert_eq!(line, mid_line),
            other => panic!("expected a midpoint snap, got {other:?}"),
        }

        let (edge_sketch, ea) = bone_document::Sketch3::new()
            .with_entity(bone_document::Sketch3Entity::point(along(30.0)));
        let (edge_sketch, eb) =
            edge_sketch.with_entity(bone_document::Sketch3Entity::point(along(-70.0)));
        let (edge_sketch, edge_line) =
            edge_sketch.with_entity(bone_document::Sketch3Entity::line(ea, eb, false));
        match sketch3_mode::nearest_snap3(&edge_sketch, camera, extent, pixel, tol, None, None) {
            Some(sketch3_mode::Snap3::OnEdge { entity, .. }) => assert_eq!(entity, edge_line),
            other => panic!("expected an on-edge snap, got {other:?}"),
        }

        let (rim_sketch, center) = bone_document::Sketch3::new()
            .with_entity(bone_document::Sketch3Entity::point(along(-50.0)));
        let (rim_sketch, circle) = rim_sketch.with_entity(bone_document::Sketch3Entity::circle(
            center,
            Length::new::<millimeter>(50.0),
            bone_types::UnitVec3::z_axis(),
            false,
        ));
        match sketch3_mode::nearest_snap3(&rim_sketch, camera, extent, pixel, tol, None, None) {
            Some(sketch3_mode::Snap3::OnEdge { entity, .. }) => assert_eq!(entity, circle),
            other => panic!("expected a circle-rim on-edge snap, got {other:?}"),
        }
    }

    fn foreground_pixel_count(rgba: &[u8]) -> usize {
        let background = [rgba[0], rgba[1], rgba[2]];
        let channel_delta = |a: u8, b: u8| i16::from(a) - i16::from(b);
        rgba.chunks_exact(4)
            .filter(|pixel| {
                channel_delta(pixel[0], background[0]).abs()
                    + channel_delta(pixel[1], background[1]).abs()
                    + channel_delta(pixel[2], background[2]).abs()
                    > 24
            })
            .count()
    }

    #[test]
    fn sketch3_overlay_draws_visible_edge_pixels() -> Result<(), Box<dyn std::error::Error>> {
        let plane = sketch_mode::Plane::Xy.basis();
        let (sketch, a) = sketch3_project::embed_point(
            &bone_document::Sketch3::new(),
            plane,
            Point2::from_mm(-10.0, -10.0),
        );
        let (sketch, b) =
            sketch3_project::embed_point(&sketch, plane, Point2::from_mm(10.0, -10.0));
        let (sketch, _l1) = sketch3_project::embed_line(&sketch, a, b, false);
        let (sketch, c) = sketch3_project::embed_point(&sketch, plane, Point2::from_mm(10.0, 10.0));
        let (sketch, _l2) = sketch3_project::embed_line(&sketch, b, c, false);
        let aabb = super::sketch3_aabb_view(&sketch, plane).aabb;

        let extent = ViewportExtent::new(ViewportPx::new(256), ViewportPx::new(256));
        let camera = frame_standard_view(aabb, extent, StandardView::Isometric, None)?;
        let overlay = super::build_sketch3_overlay(
            &sketch,
            &[],
            camera,
            bone_render::ViewportRegion::at_origin(extent),
            LinearRgba::new(0.14, 0.24, 0.62, 1.0),
        );
        let ctx = offscreen_context(extent, 3);
        let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format());
        let frame = renderer.render_display(
            &ctx,
            &overlay.points,
            &overlay.normal_lines,
            camera,
            &Style::default(),
            DisplayMode::ShadedWithEdges,
        )?;
        let drawn = foreground_pixel_count(frame.rgba());
        assert!(
            drawn > 50,
            "the sketch3 overlay must draw visible edge pixels, drew {drawn}",
        );
        Ok(())
    }

    #[test]
    fn sketch3_overlay_renders_through_an_offset_region() -> Result<(), Box<dyn std::error::Error>>
    {
        let plane = sketch_mode::Plane::Xy.basis();
        let (sketch, a) = sketch3_project::embed_point(
            &bone_document::Sketch3::new(),
            plane,
            Point2::from_mm(-32.0, -32.0),
        );
        let (sketch, b) =
            sketch3_project::embed_point(&sketch, plane, Point2::from_mm(32.0, -32.0));
        let (sketch, _l1) = sketch3_project::embed_line(&sketch, a, b, false);
        let (sketch, c) = sketch3_project::embed_point(&sketch, plane, Point2::from_mm(32.0, 32.0));
        let (sketch, _l2) = sketch3_project::embed_line(&sketch, b, c, false);
        let (sketch, d) =
            sketch3_project::embed_point(&sketch, plane, Point2::from_mm(-32.0, 32.0));
        let (sketch, _l3) = sketch3_project::embed_line(&sketch, c, d, false);

        let surface = ViewportExtent::new(ViewportPx::new(1024), ViewportPx::new(640));
        let region = bone_render::ViewportRegion::new(
            ViewportPx::new(320),
            ViewportPx::new(96),
            ViewportExtent::new(ViewportPx::new(640), ViewportPx::new(480)),
        );
        let fallback = bone_types::Aabb3::from_corners(
            Point3::from_mm(-25.0, -25.0, -25.0),
            Point3::from_mm(25.0, 25.0, 25.0),
        );
        let camera = frame_standard_view(fallback, region.extent(), StandardView::Isometric, None)?;
        let overlay = super::build_sketch3_overlay(
            &sketch,
            &[],
            camera,
            region,
            LinearRgba::new(0.14, 0.24, 0.62, 1.0),
        );
        let ctx = offscreen_context(surface, 3);
        let renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format());
        let frame_view = bone_render::SolidFrameView::new(camera, region)?;
        ctx.render_passes(|encoder, color, pick, depth| {
            renderer.encode_passes(
                encoder,
                bone_render::RenderTargets::new(color, pick),
                depth,
                &overlay.points,
                &overlay.normal_lines,
                &bone_render::SolidDisplay {
                    view: &frame_view,
                    style: &Style::light(),
                    mode: DisplayMode::ShadedWithEdges,
                },
            );
        });
        let frame = ctx.capture()?;
        let drawn = foreground_pixel_count(frame.rgba());
        assert!(
            drawn > 50,
            "3d edges must survive an inset viewport offset, drew {drawn}",
        );
        Ok(())
    }

    #[test]
    fn a_center_click_in_sketch3_lands_on_the_plane() -> Result<(), Box<dyn std::error::Error>> {
        let aabb = bone_types::Aabb3::from_corners(
            Point3::from_mm(-25.0, -25.0, -25.0),
            Point3::from_mm(25.0, 25.0, 25.0),
        );
        let region = bone_render::ViewportRegion::new(
            bone_render::ViewportPx::new(320),
            bone_render::ViewportPx::new(96),
            bone_render::ViewportExtent::new(
                bone_render::ViewportPx::new(800),
                bone_render::ViewportPx::new(600),
            ),
        );
        let camera = bone_render::frame_standard_view(
            aabb,
            region.extent(),
            bone_types::StandardView::Isometric,
            None,
        )?;
        let cursor = super::WindowPoint::new(320.0 + 400.0, 96.0 + 300.0);
        let pixel = super::viewport_local_point(cursor, region)
            .ok_or("a center cursor sits in the region")?;
        let plane = sketch_mode::Plane::Xy.basis();
        let world = sketch3_mode::ray_plane_point(camera, region.extent(), pixel, plane)
            .ok_or("a center click must intersect the sketch plane")?;
        let (x, y, z) = world.coords_mm();
        assert!(z.abs() < 1e-6, "the hit lies on the XY plane, got z={z}");
        assert!(
            x.abs() < 5.0 && y.abs() < 5.0,
            "a center click lands near the plane origin, got ({x}, {y})",
        );
        Ok(())
    }

    #[test]
    fn reattach_binds_a_sketch_to_a_picked_cap_face() -> Result<(), Box<dyn std::error::Error>> {
        use bone_types::FaceRole;
        let (mut document, base_sketch) = super::initial_document(&rectangle_sketch());
        let base = document.commit_extrude(sketch_mode::default_extrude_feature(base_sketch));
        let mut model = EvaluatedModel::new();
        model.recompute(
            &document,
            document.suppressed(),
            document.rollback(),
            RecomputeScope::Full,
        );
        let body = document
            .feature_tree()
            .feature_of_extrude(base)
            .ok_or("the base extrude resolves to a feature id")?;
        let solid = model.body(body).ok_or("the base body is evaluated")?;
        let face = solid
            .iter_faces()
            .find_map(|candidate| {
                matches!(candidate.label().role, FaceRole::EndCap).then(|| candidate.id())
            })
            .ok_or("the box has a planar cap")?;
        let face_ref = model
            .face_ref_any(face)
            .ok_or("a picked cap yields a face reference")?;
        let new_sketch = document.allocate_sketch();
        document.insert_sketch(
            new_sketch,
            "Sketch2".to_owned(),
            super::sketch3_project::embed_sketch(&rectangle_sketch()),
        );
        assert!(
            document.bind_sketch_to_face(new_sketch, face_ref).is_ok(),
            "reattaching a sketch to a planar cap binds cleanly",
        );
        Ok(())
    }

    #[test]
    fn compute_whats_wrong_is_empty_for_a_healthy_document()
    -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, sketch) = super::initial_document(&rectangle_sketch());
        let extrude = document.commit_extrude(sketch_mode::default_extrude_feature(sketch));
        let mut model = EvaluatedModel::new();
        model.recompute(
            &document,
            document.suppressed(),
            document.rollback(),
            RecomputeScope::Full,
        );
        let strings = strings::make_strings(bone_ui::strings::Locale::EnUs);
        assert!(
            super::compute_whats_wrong(&model, &document, &strings).is_empty(),
            "a clean rebuild has nothing wrong",
        );
        let feature = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("the extrude resolves to a feature id")?;
        document.rename_extrude(extrude, "Boss")?;
        assert_eq!(super::feature_label_text(&document, feature), "Boss");
        Ok(())
    }

    fn layout_rect(x: f32, y: f32, w: f32, h: f32) -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y)),
            LayoutSize::new(LayoutPx::new(w), LayoutPx::new(h)),
        )
    }

    #[test]
    fn solid_viewport_region_offsets_clamps_and_rejects_degenerate()
    -> Result<(), Box<dyn std::error::Error>> {
        let surface = ViewportExtent::new(ViewportPx::new(1280), ViewportPx::new(800));
        let inset = super::solid_viewport_region(layout_rect(320.0, 96.0, 800.0, 600.0), surface)
            .ok_or("an inset viewport yields a region")?;
        assert_eq!(
            inset.scissor(),
            (320, 96, 800, 600),
            "the region carries the viewport offset and size, not the whole window",
        );

        let small = ViewportExtent::new(ViewportPx::new(640), ViewportPx::new(480));
        let clamped = super::solid_viewport_region(layout_rect(600.0, 400.0, 400.0, 400.0), small)
            .ok_or("a partly off-surface viewport still yields a clamped region")?;
        let (x, y, w, h) = clamped.scissor();
        assert!(
            x + w <= 640 && y + h <= 480,
            "the scissor never runs past the surface: {x}+{w}, {y}+{h}",
        );

        assert!(
            super::solid_viewport_region(layout_rect(0.0, 0.0, 0.0, 0.0), surface).is_none(),
            "a degenerate viewport yields no region",
        );
        Ok(())
    }

    #[test]
    fn defer_rebuild_holds_only_for_a_costly_later_build() {
        let cheap = RebuildCost::new(10);
        let costly = RebuildCost::new(10_000);
        let budget = RebuildBudget::new(100);
        assert!(
            !super::defer_rebuild(true, costly, budget),
            "the first build always renders, however heavy",
        );
        assert!(
            super::defer_rebuild(false, costly, budget),
            "a heavy later rebuild defers behind the rebuild-needed badge",
        );
        assert!(
            !super::defer_rebuild(false, cheap, budget),
            "a cheap later rebuild stays eager and never raises the badge",
        );
    }

    #[test]
    fn whats_wrong_classifies_status_and_names_extrude_errors()
    -> Result<(), Box<dyn std::error::Error>> {
        let upstream =
            super::whats_wrong_kind(RebuildStatus::Error(RebuildError::UpstreamUnresolved))
                .ok_or("an upstream failure is reported")?;
        assert_eq!(upstream.message, strings::WHATS_WRONG_UPSTREAM);
        assert!(upstream.is_error);
        assert!(
            !upstream.offers_reattach,
            "a downstream feature blames the upstream, it never offers its own reattach",
        );

        let non_planar =
            super::whats_wrong_kind(RebuildStatus::Error(RebuildError::NonPlanarSketchTarget))
                .ok_or("a non-planar target is reported")?;
        assert!(non_planar.is_error && !non_planar.offers_reattach);

        assert!(super::whats_wrong_kind(RebuildStatus::UpToDate).is_none());
        assert!(super::whats_wrong_kind(RebuildStatus::NeedsRebuild).is_none());

        let strings = strings::make_strings(bone_ui::strings::Locale::EnUs);
        let line = |error: ExtrudeError| crate::status_badge::extrude_panel_line(&error, &strings);
        assert_eq!(
            line(ExtrudeError::CutWithoutBody),
            LabelText::Key(strings::EXTRUDE_PANEL_CUT_WITHOUT_BODY),
            "a cut with no body to remove material from says so, not a generic build failure",
        );
        assert_eq!(
            line(ExtrudeError::Kernel(BrepError::BooleanUnsupported {
                detail: bone_document::BooleanGap::NonConvexFace,
            })),
            LabelText::Owned("Cut blocked by a non-convex face".to_owned()),
            "an unsupported boolean gate names the geometry it choked on",
        );
        assert_eq!(
            line(ExtrudeError::Kernel(BrepError::BooleanDisjoint)),
            LabelText::Key(strings::EXTRUDE_PANEL_CUT_NO_INTERSECT),
        );
        assert_eq!(
            line(ExtrudeError::Kernel(BrepError::RevolveAxisUnresolved)),
            LabelText::Key(strings::EXTRUDE_PANEL_REVOLVE_AXIS),
            "a revolve with no centerline names the missing axis, not a generic build failure",
        );
        assert_eq!(
            line(ExtrudeError::Kernel(BrepError::RevolveProfileCrossesAxis)),
            LabelText::Key(strings::EXTRUDE_PANEL_REVOLVE_CROSSES),
        );
        Ok(())
    }

    #[test]
    fn viewport_local_point_centers_the_inset_viewport_not_the_window()
    -> Result<(), Box<dyn std::error::Error>> {
        let region = ViewportRegion::new(
            ViewportPx::new(282),
            ViewportPx::new(120),
            ViewportExtent::new(ViewportPx::new(998), ViewportPx::new(636)),
        );
        let center = WindowPoint::new(282.0 + 499.0, 120.0 + 318.0);
        let local = super::viewport_local_point(center, region)
            .ok_or("a cursor inside the surface yields a viewport-local point")?;
        assert!(
            (local.x() - 499.0).abs() < 1e-9 && (local.y() - 318.0).abs() < 1e-9,
            "a cursor at the inset viewport center maps to the region-local center: ({}, {})",
            local.x(),
            local.y(),
        );
        Ok(())
    }

    #[test]
    fn drag_gesture_maps_modifiers_to_orbit_pan_zoom_roll() {
        assert_eq!(super::drag_gesture(ModifierMask::NONE), NavGesture::Orbit);
        assert_eq!(super::drag_gesture(ModifierMask::CTRL), NavGesture::Pan);
        assert_eq!(super::drag_gesture(ModifierMask::SHIFT), NavGesture::Zoom);
        assert_eq!(super::drag_gesture(ModifierMask::ALT), NavGesture::Roll);
        assert_eq!(
            super::drag_gesture(ModifierMask::CTRL | ModifierMask::SHIFT),
            NavGesture::Pan,
            "ctrl outranks shift so a held ctrl always pans",
        );
    }

    fn sketch_session_of(mode: Mode) -> SketchSession {
        match mode {
            Mode::Sketch { session, .. } => *session,
            other => panic!("expected sketch mode, got {other:?}"),
        }
    }

    fn tool_frame(tool: SketchTool) -> shell::ShellFrame {
        shell::ShellFrame {
            activated_tool: Some(tool),
            ..empty_frame()
        }
    }

    fn exit_frame() -> shell::ShellFrame {
        shell::ShellFrame {
            exit_sketch: true,
            ..empty_frame()
        }
    }

    #[test]
    fn next_mode_2d_transitions_cover_exit_escape_and_tool_arming() {
        let plane_frame = shell::ShellFrame {
            plane_picked: Some(Plane::Xy),
            ..empty_frame()
        };
        assert_eq!(
            next_mode(Mode::Idle, &plane_frame, false),
            Mode::Idle,
            "plane entry is owned by begin_plane_sketch at the app layer, not next_mode",
        );
        let sketching = Mode::enter_sketch(SketchId::default());
        assert_eq!(next_mode(sketching.clone(), &plane_frame, false), sketching);

        assert_eq!(
            next_mode(sketching.clone(), &exit_frame(), false),
            Mode::Idle,
            "ribbon exit returns idle",
        );
        assert_eq!(
            next_mode(sketching, &empty_frame(), true),
            Mode::Idle,
            "the exit action returns idle",
        );
        assert_eq!(
            next_mode(Mode::Idle, &tool_frame(SketchTool::Line), false),
            Mode::Idle,
            "a tool in idle does not promote to sketch",
        );

        let pending = Mode::Sketch {
            sketch_id: SketchId::default(),
            session: Box::new(SketchSession {
                tool: Some(SketchTool::Line),
                pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm(
                    1.0, 2.0,
                )))),
                ..SketchSession::default()
            }),
        };
        let after_escape = sketch_session_of(next_mode(pending.clone(), &empty_frame(), true));
        assert_eq!(
            after_escape.tool,
            Some(SketchTool::Line),
            "escape keeps the tool"
        );
        assert_eq!(
            after_escape.pending, None,
            "escape clears the pending chain"
        );
        assert_eq!(
            next_mode(pending, &exit_frame(), false),
            Mode::Idle,
            "ribbon exit overrides a pending chain",
        );

        let armed = Mode::enter_sketch(SketchId::default()).arm_tool(SketchTool::Line);
        let disarmed = sketch_session_of(next_mode(armed.clone(), &empty_frame(), true));
        assert_eq!(
            disarmed.tool, None,
            "escape with no pending disarms the armed tool"
        );
        assert_eq!(disarmed.pending, None);
        assert_eq!(
            next_mode(armed.clone(), &exit_frame(), false),
            Mode::Idle,
            "the exit action wins over an armed tool",
        );
        assert_eq!(
            sketch_session_of(next_mode(
                armed.clone(),
                &tool_frame(SketchTool::Line),
                false
            ))
            .tool,
            None,
            "clicking the active tool disarms it",
        );
        assert_eq!(
            sketch_session_of(next_mode(armed, &tool_frame(SketchTool::Point), false)).tool,
            Some(SketchTool::Point),
            "clicking a different tool swaps",
        );
        assert_eq!(
            sketch_session_of(next_mode(
                Mode::enter_sketch(SketchId::default()),
                &tool_frame(SketchTool::Line),
                false,
            ))
            .tool,
            Some(SketchTool::Line),
            "a tool click in a bare sketch arms the session",
        );
    }

    #[test]
    fn next_mode_3d_transitions_cover_tool_arming_and_escape() {
        let base = Mode::enter_sketch3(
            SketchId::default(),
            Spatial::principal(sketch_mode::Plane::Xy),
        );
        let armed = next_mode(base, &tool_frame(SketchTool::Line), false);
        assert_eq!(armed.sketch3_tool(), Some(SketchTool::Line));
        assert_eq!(
            next_mode(armed, &tool_frame(SketchTool::Line), false).sketch3_tool(),
            None,
            "clicking the active 3D tool disarms it",
        );

        let multi = Mode::enter_sketch3(
            SketchId::default(),
            Spatial::principal(sketch_mode::Plane::Xy),
        )
        .arm_tool(SketchTool::Line);
        assert_eq!(
            next_mode(multi, &tool_frame(SketchTool::Parallelogram), false).sketch3_tool(),
            Some(SketchTool::Parallelogram),
            "every 2D tool now arms in the 3D sketch",
        );

        let after_first_mode = next_mode(armed_3d_line_with_pending(), &empty_frame(), true);
        let after_first = sketch_session_of(after_first_mode.clone());
        assert!(after_first.spatial.is_some(), "escape keeps the 3D sketch");
        assert_eq!(after_first.tool, Some(SketchTool::Line));
        assert_eq!(
            after_first.pending, None,
            "escape ends the chain, keeps the tool"
        );
        let after_second = sketch_session_of(next_mode(after_first_mode, &empty_frame(), true));
        assert!(
            after_second.spatial.is_some(),
            "disarming keeps the 3D sketch"
        );
        assert_eq!(
            after_second.tool, None,
            "the second escape disarms the tool"
        );

        let cleared = sketch_session_of(armed_3d_line_with_pending().clear_pending());
        assert!(cleared.spatial.is_some(), "clearing keeps the 3D sketch");
        assert_eq!(cleared.tool, Some(SketchTool::Line));
        assert_eq!(
            cleared.pending, None,
            "a right-click clears the pending, keeps the tool"
        );
    }

    fn far_camera() -> Camera2 {
        Camera2::new(ViewportExtent::new(
            ViewportPx::new(800),
            ViewportPx::new(600),
        ))
        .with_zoom(PixelsPerMm::new(1_000_000.0))
    }

    #[test]
    fn build_preview_covers_idle_pending_ghosts_and_drag() {
        let camera = far_camera();
        let empty_doc = Document::new(DocumentId::default(), "doc".to_owned());
        let mk_mode = |sketch_id, tool, pending| Mode::Sketch {
            sketch_id,
            session: Box::new(SketchSession {
                tool: Some(tool),
                pending: Some(pending),
                ..SketchSession::default()
            }),
        };

        assert!(
            build_preview(
                &Mode::Idle,
                &empty_doc,
                Some(Point2::from_mm(1.0, 1.0)),
                &camera
            )
            .is_empty(),
            "idle previews nothing",
        );
        assert!(
            build_preview(
                &Mode::enter_sketch(SketchId::default()),
                &empty_doc,
                Some(Point2::from_mm(0.0, 0.0)),
                &camera,
            )
            .is_empty(),
            "a sketch with no armed tool previews nothing",
        );

        let (document, sketch_id) = initial_document(&Sketch::new(Plane::Xy.basis()));
        let anchor = Point2::from_mm(2.0, 3.0);
        let cursor = Point2::from_mm(5.0, 7.0);
        let preview = build_preview(
            &mk_mode(
                sketch_id,
                SketchTool::Line,
                Pending::First(ClickAnchor::Position(anchor)),
            ),
            &document,
            Some(cursor),
            &camera,
        );
        assert_eq!(preview.anchors, vec![anchor]);
        assert_eq!(preview.segments, vec![(anchor, cursor)]);

        let sketch = Sketch::new(Plane::Xy.basis());
        let target = Point2::from_mm(-4.0, 6.0);
        let (sketch, endpoint) = tools::add_point(sketch, target);
        let (document, sketch_id) = initial_document(&sketch);
        let cursor = Point2::from_mm(0.0, 0.0);
        let preview = build_preview(
            &mk_mode(
                sketch_id,
                SketchTool::Line,
                Pending::First(ClickAnchor::Endpoint(endpoint)),
            ),
            &document,
            Some(cursor),
            &camera,
        );
        assert_eq!(
            preview.anchors,
            vec![target],
            "endpoint pending resolves via document"
        );
        assert_eq!(preview.segments, vec![(target, cursor)]);

        let (document, sketch_id) = initial_document(&Sketch::new(Plane::Xy.basis()));
        let anchor = Point2::from_mm(1.0, 1.0);
        let preview = build_preview(
            &mk_mode(
                sketch_id,
                SketchTool::Line,
                Pending::First(ClickAnchor::Position(anchor)),
            ),
            &document,
            None,
            &camera,
        );
        assert_eq!(
            preview.anchors,
            vec![anchor],
            "anchor kept when cursor outside viewport"
        );
        assert!(preview.segments.is_empty());

        let drag_mode = Mode::Sketch {
            sketch_id: SketchId::default(),
            session: Box::new(SketchSession {
                tool: Some(SketchTool::Line),
                pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm(
                    0.0, 0.0,
                )))),
                drag: Some(DragSession {
                    entity: bone_types::SketchEntityId::default(),
                    press: Point2::origin(),
                    pins: DragPins::from_array([
                        Some((bone_types::SketchEntityId::default(), Point3::origin())),
                        None,
                        None,
                    ]),
                }),
                ..SketchSession::default()
            }),
        };
        assert!(
            build_preview(
                &drag_mode,
                &empty_doc,
                Some(Point2::from_mm(1.0, 1.0)),
                &camera
            )
            .is_empty(),
            "an in-flight drag previews nothing",
        );

        let (document, sketch_id) = initial_document(&Sketch::new(Plane::Xy.basis()));
        let center = Point2::from_mm(0.0, 0.0);
        let preview = build_preview(
            &mk_mode(
                sketch_id,
                SketchTool::Circle,
                Pending::First(ClickAnchor::Position(center)),
            ),
            &document,
            Some(Point2::from_mm(3.0, 4.0)),
            &camera,
        );
        assert_eq!(preview.anchors, vec![center]);
        assert_eq!(preview.circles.len(), 1, "circle tool emits a ghost circle");
        let r = preview.circles[0].radius.get::<millimeter>();
        assert!((r - 5.0).abs() < 1e-9, "r={r}");

        let (document, sketch_id) = initial_document(&Sketch::new(Plane::Xy.basis()));
        let corner = Point2::from_mm(0.0, 0.0);
        let preview = build_preview(
            &mk_mode(
                sketch_id,
                SketchTool::CornerRectangle,
                Pending::First(ClickAnchor::Position(corner)),
            ),
            &document,
            Some(Point2::from_mm(5.0, 3.0)),
            &camera,
        );
        assert_eq!(preview.anchors, vec![corner]);
        assert_eq!(
            preview.segments.len(),
            4,
            "corner rectangle emits four segments"
        );

        let sketch = Sketch::new(Plane::Xy.basis());
        let (sketch, a) = tools::add_point(sketch, Point2::from_mm(0.0, 0.0));
        let (sketch, b) = tools::add_point(sketch, Point2::from_mm(10.0, 0.0));
        let (sketch, _) = tools::add_line(sketch, a, b, false);
        let (document, sketch_id) = initial_document(&sketch);
        let preview = build_preview(
            &mk_mode(
                sketch_id,
                SketchTool::TangentArc,
                Pending::First(ClickAnchor::Endpoint(b)),
            ),
            &document,
            Some(Point2::from_mm(10.0, 6.0)),
            &camera,
        );
        assert_eq!(preview.anchors.len(), 1, "start anchor visible");
        assert_eq!(
            preview.arcs.len(),
            1,
            "tangent arc ghost emitted after endpoint click"
        );

        let (document, sketch_id) = initial_document(&Sketch::new(Plane::Xy.basis()));
        let center = Point2::from_mm(0.0, 0.0);
        let start = Point2::from_mm(5.0, 0.0);
        let preview = build_preview(
            &mk_mode(
                sketch_id,
                SketchTool::CenterpointArc,
                Pending::Second(ClickAnchor::Position(center), ClickAnchor::Position(start)),
            ),
            &document,
            Some(Point2::from_mm(0.0, 5.0)),
            &camera,
        );
        assert_eq!(
            preview.arcs.len(),
            1,
            "centerpoint arc ghost after two clicks"
        );
        assert_eq!(preview.anchors.len(), 2);
    }

    fn armed_3d_line_with_pending() -> Mode {
        Mode::Sketch {
            sketch_id: SketchId::default(),
            session: Box::new(sketch_mode::SketchSession {
                tool: Some(SketchTool::Line),
                pending: Some(sketch_mode::Pending::First(
                    sketch_mode::ClickAnchor::Position(Point2::origin()),
                )),
                spatial: Some(sketch_mode::Spatial::principal(sketch_mode::Plane::Xy)),
                ..sketch_mode::SketchSession::default()
            }),
        }
    }

    fn doc_with_default_sketch() -> (Document, SketchId) {
        let sketch = bone_document::Sketch::new(Plane::Xy.basis());
        let mut document = Document::new(DocumentId::default(), "Untitled".to_owned());
        let id = SketchId::default();
        document.insert_sketch(
            id,
            "Sketch1".to_owned(),
            super::sketch3_project::embed_sketch(&sketch),
        );
        (document, id)
    }

    #[test]
    fn apply_sketch_rename_into_writes_records_or_skips_by_label() {
        [
            (
                "Profile",
                "Profile",
                1usize,
                "successful rename records one undo snapshot",
            ),
            (
                "   ",
                "Sketch1",
                0,
                "an all-whitespace label is dropped without undo",
            ),
            (
                "  Sketch1 ",
                "Sketch1",
                0,
                "a trimmed-equal rename must not record undo",
            ),
        ]
        .into_iter()
        .for_each(|(label, expected, undo_len, why)| {
            let (mut document, id) = doc_with_default_sketch();
            let mut undo = UndoStack::with_capacity(NonZeroUsize::MIN);
            apply_sketch_rename_into(
                &mut document,
                &mut undo,
                shell::SketchRenameRequest {
                    id,
                    label: label.to_owned(),
                },
            );
            assert_eq!(document.sketch_label(id), Some(expected), "{why}");
            assert_eq!(undo.past_len(), undo_len, "{why}");
        });
    }

    fn extrude_node_count(document: &Document) -> usize {
        document
            .feature_tree()
            .iter()
            .filter(|(_, node)| matches!(node, FeatureNode::Extrude(_)))
            .count()
    }

    #[test]
    fn commit_armed_extrude_accepts_ignores_and_edits_in_place()
    -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, sketch) = doc_with_default_sketch();
        let mut undo = UndoStack::with_capacity(NonZeroUsize::MIN);
        commit_armed_extrude(
            &mut document,
            &mut undo,
            &Mode::Extrude(ExtrudeArming::profile(sketch)),
            Some(shell::ConfirmAction::Accept),
        );
        assert_eq!(extrude_node_count(&document), 1, "accept adds a node");
        assert_eq!(undo.past_len(), 1);

        let (mut document, sketch) = doc_with_default_sketch();
        let mut undo = UndoStack::with_capacity(NonZeroUsize::MIN);
        commit_armed_extrude(
            &mut document,
            &mut undo,
            &Mode::Extrude(ExtrudeArming::profile(sketch)),
            Some(shell::ConfirmAction::Cancel),
        );
        commit_armed_extrude(
            &mut document,
            &mut undo,
            &Mode::Idle,
            Some(shell::ConfirmAction::Accept),
        );
        assert_eq!(
            extrude_node_count(&document),
            0,
            "cancel and a non-extrude mode commit nothing"
        );
        assert_eq!(undo.past_len(), 0);

        let (mut document, sketch) = doc_with_default_sketch();
        let id = document.commit_extrude(sketch_mode::default_extrude_feature(sketch));
        document.rename_extrude(id, "Boss")?;
        let mut undo = UndoStack::with_capacity(NonZeroUsize::MIN);
        commit_armed_extrude(
            &mut document,
            &mut undo,
            &Mode::Extrude(ExtrudeArming::edit(
                id,
                &sketch_mode::default_extrude_feature(sketch),
            )),
            Some(shell::ConfirmAction::Accept),
        );
        assert_eq!(extrude_node_count(&document), 1, "editing reuses the node");
        assert_eq!(document.extrude_label(id), Some("Boss"));
        Ok(())
    }

    #[test]
    fn apply_feature_command_suppresses_deletes_and_rolls_back()
    -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, sketch) = doc_with_default_sketch();
        let extrude = document.commit_extrude(sketch_mode::default_extrude_feature(sketch));
        let feature = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("the committed extrude has a feature id")?;
        let mut undo = UndoStack::with_capacity(NonZeroUsize::MIN);
        assert!(apply_feature_command_into(
            &mut document,
            &mut undo,
            shell::FeatureCommand::Suppress(feature),
        ));
        assert!(document.suppression_state(feature).is_suppressed());
        assert_eq!(
            undo.past_len(),
            1,
            "a suppression records one undo snapshot"
        );

        let (mut document, sketch) = doc_with_default_sketch();
        let extrude = document.commit_extrude(sketch_mode::default_extrude_feature(sketch));
        let mut undo = UndoStack::with_capacity(NonZeroUsize::MIN);
        assert!(apply_feature_command_into(
            &mut document,
            &mut undo,
            shell::FeatureCommand::Delete(shell::FeatureTarget::Extrude(extrude)),
        ));
        assert_eq!(
            document.extrude(extrude),
            None,
            "delete removes the extrude"
        );
        assert_eq!(undo.past_len(), 1);

        let (mut document, sketch) = doc_with_default_sketch();
        let extrude = document.commit_extrude(sketch_mode::default_extrude_feature(sketch));
        let feature = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("the committed extrude has a feature id")?;
        let mut undo = UndoStack::with_capacity(NonZeroUsize::MIN);
        apply_feature_command_into(
            &mut document,
            &mut undo,
            shell::FeatureCommand::RollbackToHere(feature),
        );
        assert!(
            document.is_rolled_back(feature),
            "rollback marks the feature"
        );
        Ok(())
    }

    #[test]
    fn extrude_edit_mode_arms_from_idle_but_not_from_sketch() {
        let (mut document, sketch) = doc_with_default_sketch();
        let id = document.commit_extrude(sketch_mode::default_extrude_feature(sketch));
        let from_idle = extrude_edit_mode(&document, &Mode::Idle, Some(id));
        assert!(matches!(
            from_idle,
            Some(Mode::Extrude(ExtrudeArming::Profile { target: Some(t), .. })) if t == id
        ));
        assert_eq!(
            extrude_edit_mode(&document, &Mode::enter_sketch(sketch), Some(id)),
            None,
            "double-click is inert while sketching",
        );
        assert_eq!(
            extrude_edit_mode(&document, &Mode::Idle, Some(ExtrudeId::default())),
            None,
            "unknown extrude id arms nothing",
        );
    }

    #[test]
    fn active_solid_feature_tracks_arming_committed_and_framed() {
        let (mut document, sketch) = doc_with_default_sketch();
        let armed = sketch_mode::default_extrude_feature(sketch);
        assert_eq!(
            active_solid_feature(
                &Mode::Extrude(ExtrudeArming::profile(sketch)),
                &document,
                None
            ),
            Some(PreviewFeature::Extrude(armed)),
            "an armed profile previews its own feature",
        );
        assert_eq!(
            active_solid_feature(&Mode::enter_sketch(sketch), &document, None),
            None,
            "sketching shows the 2D scene, not a solid",
        );
        assert_eq!(
            active_solid_feature(&Mode::Idle, &document, None),
            None,
            "idle with no committed extrude shows no solid",
        );
        let revolve_mode = Mode::Revolve(sketch_mode::RevolveArming::armed(
            sketch,
            RevolveAxis::SketchLine(bone_types::SketchEntityId::default()),
            bone_document::ExtrudeOperation::default(),
        ));
        assert!(
            matches!(
                active_solid_feature(&revolve_mode, &document, None),
                Some(PreviewFeature::Revolve(_)),
            ),
            "an armed revolve drives a live solid preview, not a blank viewport",
        );
        let _ = document.commit_extrude(armed);
        assert_eq!(
            active_solid_feature(&Mode::Idle, &document, None),
            Some(PreviewFeature::Extrude(armed)),
            "idle falls back to the committed extrude",
        );

        let (mut document, sketch) = doc_with_default_sketch();
        let first_feature = sketch_mode::default_extrude_feature(sketch);
        let mut second_feature = first_feature;
        second_feature.operation =
            bone_document::ExtrudeOperation::Boss(bone_document::MergeResult::Separate);
        let first = document.commit_extrude(first_feature);
        let _second = document.commit_extrude(second_feature);
        assert_eq!(
            active_solid_feature(&Mode::Idle, &document, None),
            Some(PreviewFeature::Extrude(second_feature)),
            "with no framed id, idle frames the last-committed extrude",
        );
        assert_eq!(
            active_solid_feature(&Mode::Idle, &document, Some(first)),
            Some(PreviewFeature::Extrude(first_feature)),
            "a framed id wins over the tree tip, so editing a non-last extrude stays framed",
        );
        assert_eq!(
            active_solid_feature(&Mode::Idle, &document, Some(ExtrudeId::default())),
            Some(PreviewFeature::Extrude(second_feature)),
            "a stale framed id self-heals to the last-committed extrude",
        );
    }

    #[test]
    fn revolve_live_preview_evaluates_to_a_solid() -> Result<(), Box<dyn std::error::Error>> {
        let sketch = Sketch::new(Plane::Xy.basis());
        let (sketch, a0) = tools::add_point(sketch, Point2::from_mm(0.0, 0.0));
        let (sketch, a1) = tools::add_point(sketch, Point2::from_mm(0.0, 10.0));
        let (sketch, _axis) = tools::add_line(sketch, a0, a1, true);
        let (sketch, p0) = tools::add_point(sketch, Point2::from_mm(2.0, 0.0));
        let (sketch, p1) = tools::add_point(sketch, Point2::from_mm(6.0, 0.0));
        let (sketch, p2) = tools::add_point(sketch, Point2::from_mm(6.0, 8.0));
        let (sketch, p3) = tools::add_point(sketch, Point2::from_mm(2.0, 8.0));
        let (sketch, _) = tools::add_line(sketch, p0, p1, false);
        let (sketch, _) = tools::add_line(sketch, p1, p2, false);
        let (sketch, _) = tools::add_line(sketch, p2, p3, false);
        let (sketch, _) = tools::add_line(sketch, p3, p0, false);
        let (document, id) = super::initial_document(&sketch);
        let axis = document
            .sketch3(id)
            .and_then(bone_document::Sketch3::axis_candidate)
            .ok_or("the construction line is the revolve axis candidate")?;
        let feature = super::PreviewFeature::Revolve(sketch_mode::revolve_feature(
            id,
            RevolveAxis::SketchLine(axis),
            ExtrudeOperation::default(),
        ));
        let preview = super::compute_extrude_preview(&document, &[], &feature)
            .ok_or("an armed revolve previews an evaluated solid")?;
        assert!(
            preview.solid().is_some(),
            "an offset rectangle about a centerline previews a revolved tube live, before commit",
        );
        Ok(())
    }

    #[test]
    fn apply_extrude_rename_into_writes_label_and_records_undo() {
        let (mut document, sketch) = doc_with_default_sketch();
        let id = document.commit_extrude(sketch_mode::default_extrude_feature(sketch));
        let mut undo = UndoStack::with_capacity(NonZeroUsize::MIN);
        apply_extrude_rename_into(
            &mut document,
            &mut undo,
            shell::ExtrudeRenameRequest {
                id,
                label: "Boss".to_owned(),
            },
        );
        assert_eq!(document.extrude_label(id), Some("Boss"));
        assert_eq!(undo.past_len(), 1);
    }

    #[test]
    fn next_mode_sketch_activation_and_extrude_exit() {
        let sketch_id = SketchId::default();
        let activate = shell::ShellFrame {
            sketch_activated: Some(sketch_id),
            ..empty_frame()
        };
        assert_eq!(
            next_mode(Mode::Idle, &activate, false),
            Mode::enter_sketch(sketch_id),
            "activating a sketch from idle enters it",
        );
        assert_eq!(
            next_mode(
                Mode::Extrude(ExtrudeArming::AwaitingSketch(ExtrudeOperation::default())),
                &activate,
                false,
            ),
            Mode::Extrude(ExtrudeArming::profile(sketch_id)),
            "a sketch pick while arming sets the extrude profile",
        );
        assert_eq!(
            next_mode(
                Mode::Extrude(ExtrudeArming::profile(sketch_id)),
                &activate,
                false
            ),
            Mode::Extrude(ExtrudeArming::profile(sketch_id)),
            "a pick while armed re-targets the profile, never drops into sketch editing",
        );
        let in_sketch = Mode::enter_sketch(SketchId::default());
        assert_eq!(
            next_mode(in_sketch.clone(), &activate, false),
            in_sketch,
            "activating a sketch while already sketching is ignored",
        );

        let awaiting = Mode::Extrude(ExtrudeArming::AwaitingSketch(ExtrudeOperation::default()));
        assert_eq!(
            next_mode(awaiting.clone(), &empty_frame(), true),
            Mode::Idle,
            "escape exits an awaiting extrude",
        );
        assert_eq!(
            next_mode(
                Mode::Extrude(ExtrudeArming::profile(SketchId::default())),
                &empty_frame(),
                true,
            ),
            Mode::Idle,
            "escape exits a profiled extrude",
        );
        assert_eq!(
            cancel_pending_or_exit(awaiting),
            Mode::Idle,
            "cancel drops extrude arming",
        );
    }

    #[test]
    fn mode_scopes_and_escape_hotkey_dispatch() {
        let collect = |mode: &Mode| -> Vec<HotkeyScope> {
            scopes_for_mode(mode).innermost_first().copied().collect()
        };

        let idle = collect(&Mode::Idle);
        assert!(
            !idle.contains(&HotkeyScope::Sketch),
            "idle omits the sketch scope"
        );
        assert!(idle.contains(&HotkeyScope::Global));

        let in_sketch = collect(&Mode::enter_sketch(SketchId::default()));
        assert!(in_sketch.contains(&HotkeyScope::Sketch));
        assert!(in_sketch.contains(&HotkeyScope::Global));
        assert!(!in_sketch.contains(&HotkeyScope::Extrude));

        let awaiting = Mode::Extrude(ExtrudeArming::AwaitingSketch(ExtrudeOperation::default()));
        let in_extrude = collect(&awaiting);
        assert!(in_extrude.contains(&HotkeyScope::Extrude));
        assert!(
            !in_extrude.contains(&HotkeyScope::Sketch),
            "extrude must not activate the sketch-tool scope",
        );

        let table = build_hotkey_table();
        let chord = KeyChord::new(UiKeyCode::Named(NamedKey::Escape), ModifierMask::NONE);
        assert_eq!(
            table.dispatch(
                chord,
                &scopes_for_mode(&Mode::enter_sketch(SketchId::default()))
            ),
            Some(sketch_mode::ESCAPE_ACTION),
            "escape exits under the sketch scope",
        );
        assert_eq!(
            table.dispatch(chord, &scopes_for_mode(&Mode::Idle)),
            None,
            "escape is unbound in idle",
        );
        assert_eq!(
            table.dispatch(chord, &scopes_for_mode(&awaiting)),
            Some(sketch_mode::ESCAPE_ACTION),
            "escape exits under the extrude scope",
        );
    }

    #[test]
    fn dim_conflict_pending_tracks_the_conflicting_relation() {
        let relation = Sketch3Relation::Distance {
            a: bone_types::SketchEntityId::default(),
            b: bone_types::SketchEntityId::default(),
            value: Length::new::<millimeter>(2.0),
            reference: false,
        };
        let mode = Mode::enter_sketch(SketchId::default()).start_dim_conflict(relation);
        assert_eq!(dim_conflict_pending(&mode), Some(relation));
        assert_eq!(
            dim_conflict_pending(&Mode::Idle),
            None,
            "idle has no pending conflict"
        );
    }

    fn horizontal_line_fixture() -> (
        Sketch,
        bone_types::SketchEntityId,
        bone_types::SketchEntityId,
        bone_types::SketchEntityId,
    ) {
        let sketch = Sketch::new(Plane::Xy.basis());
        let (sketch, a) = tools::add_point(sketch, Point2::from_mm(0.0, 0.0));
        let (sketch, b) = tools::add_point(sketch, Point2::from_mm(10.0, 0.0));
        let (sketch, line) = tools::add_line(sketch, a, b, false);
        let sketch = tools::add_relation(sketch, SketchRelation::Horizontal(line));
        let sketch = tools::add_relation(sketch, SketchRelation::Fix(a));
        (sketch, a, b, line)
    }

    #[test]
    fn drag_translates_endpoint_and_preserves_horizontal() -> Result<(), Box<dyn std::error::Error>>
    {
        use bone_document::{Sketch3, Sketch3Relation};
        let plane = SketchPlaneBasis::xy();
        let (s, a) =
            sketch3_project::embed_point(&Sketch3::new(), plane, Point2::from_mm(0.0, 0.0));
        let (s, b) = sketch3_project::embed_point(&s, plane, Point2::from_mm(10.0, 0.0));
        let (s, line) = sketch3_project::embed_line(&s, a, b, false);
        let (s, _) = s.with_relation(Sketch3Relation::AlongAxis {
            line,
            axis: plane.x_axis(),
        });
        let (s, _) = s.with_relation(Sketch3Relation::Fix(a));
        let pins = DragPins::from_sketch3_entity(&s, b).ok_or("b is a point")?;
        let targets = pins.to_targets(
            plane,
            Point2::from_mm(10.0, 0.0),
            Point2::from_mm(13.0, 0.0),
        );
        let next = s.solve_with_drag_pins(&targets, BudgetCeiling::FRAME_16MS)?;
        let (Some(b_at), Some(a_at)) = (next.point_position(b), next.point_position(a)) else {
            panic!("a and b stay points");
        };
        let (bx, by, bz) = b_at.coords_mm();
        assert!((bx - 13.0).abs() < 1e-6, "b.x slides under cursor: {bx}");
        assert!(by.abs() < 1e-6 && bz.abs() < 1e-6, "horizontal preserved");
        let (ax, ay, az) = a_at.coords_mm();
        assert!(
            ax.abs() < 1e-9 && ay.abs() < 1e-9 && az.abs() < 1e-9,
            "fixed a stays put"
        );
        Ok(())
    }

    #[test]
    fn mirror_axis_reflects_and_classifies_points() {
        let x_axis = MirrorAxis::from_points(Point2::from_mm(-5.0, 0.0), Point2::from_mm(5.0, 0.0));
        let (rx, ry) = x_axis.reflect(Point2::from_mm(3.0, 4.0)).coords_mm();
        assert!((rx - 3.0).abs() < 1e-9, "x preserved across x-axis");
        assert!((ry - -4.0).abs() < 1e-9, "y inverted across x-axis");

        let diagonal =
            MirrorAxis::from_points(Point2::from_mm(0.0, 0.0), Point2::from_mm(1.0, 1.0));
        let (dx, dy) = diagonal.reflect(Point2::from_mm(2.0, 0.0)).coords_mm();
        assert!((dx - 0.0).abs() < 1e-9, "x reflects to y on y=x diagonal");
        assert!((dy - 2.0).abs() < 1e-9, "y reflects to x on y=x diagonal");

        let degenerate =
            MirrorAxis::from_points(Point2::from_mm(1.0, 1.0), Point2::from_mm(1.0, 1.0));
        assert!(
            degenerate.is_degenerate(),
            "coincident endpoints must be degenerate"
        );

        assert!(x_axis.is_on_axis(Point2::from_mm(2.0, 0.0)));
        assert!(x_axis.is_on_axis(Point2::from_mm(-5.0, 0.0)));
        assert!(
            !x_axis.is_on_axis(Point2::from_mm(2.0, 0.5)),
            "off-axis point is not on axis"
        );
    }

    #[test]
    fn mirror_targets_creates_reflected_circle_with_symmetric_relations()
    -> Result<(), Box<dyn std::error::Error>> {
        let (sketch, _, _, axis_line) = horizontal_line_fixture();
        let (sketch, center) = tools::add_point(sketch, Point2::from_mm(0.0, 3.0));
        let (sketch, circle_id) =
            tools::add_circle(sketch, center, Length::new::<millimeter>(1.0), false);
        let axis_geom =
            MirrorAxis::from_points(Point2::from_mm(0.0, 0.0), Point2::from_mm(1.0, 0.0));
        let source_ids: std::collections::BTreeSet<_> = [center, circle_id].into_iter().collect();
        let mirrored = mirror_targets(sketch.clone(), &source_ids, axis_line, &axis_geom)?;
        let new_circles: Vec<_> = mirrored
            .entities()
            .iter()
            .filter_map(|(_, e)| match *e {
                SketchEntity::Circle(c) => Some(c),
                _ => None,
            })
            .collect();
        assert_eq!(new_circles.len(), 2, "original + mirrored circle");
        let new_center_pos = new_circles
            .iter()
            .map(|c| {
                let SketchEntity::Point(p) = mirrored.entities()[c.center()] else {
                    panic!("circle center is a point");
                };
                p.at().coords_mm()
            })
            .find(|(_, y)| *y < 0.0);
        let (cx, cy) = new_center_pos.ok_or("mirrored circle must lie below x-axis")?;
        assert!(cx.abs() < 1e-9 && (cy + 3.0).abs() < 1e-9, "({cx}, {cy})");
        let symmetric_count = mirrored
            .relations()
            .iter()
            .filter(
                |(_, r)| matches!(r, SketchRelation::Symmetric { axis, .. } if *axis == axis_line),
            )
            .count();
        assert!(
            symmetric_count >= 1,
            "mirror must emit at least one Symmetric relation tied to the axis",
        );
        Ok(())
    }

    #[test]
    fn mirror_copies_relations_referencing_source_and_axis_lines()
    -> Result<(), Box<dyn std::error::Error>> {
        use bone_document::SketchEdit;
        let axis_and_line = || {
            let sketch = Sketch::new(Plane::Xy.basis());
            let (sketch, axis_a) = tools::add_point(sketch, Point2::from_mm(-5.0, 0.0));
            let (sketch, axis_b) = tools::add_point(sketch, Point2::from_mm(5.0, 0.0));
            let (sketch, axis_line) = tools::add_line(sketch, axis_a, axis_b, true);
            let (sketch, p0) = tools::add_point(sketch, Point2::from_mm(-2.0, 3.0));
            let (sketch, p1) = tools::add_point(sketch, Point2::from_mm(2.0, 3.0));
            let (sketch, line) = tools::add_line(sketch, p0, p1, false);
            (sketch, axis_line, line, p0, p1)
        };
        let axis_geom =
            MirrorAxis::from_points(Point2::from_mm(-5.0, 0.0), Point2::from_mm(5.0, 0.0));

        let (sketch, axis_line, line, p0, p1) = axis_and_line();
        let (sketch, _) =
            sketch.apply(SketchEdit::AddRelation(SketchRelation::Horizontal(line)))?;
        let source_ids: std::collections::BTreeSet<_> = [p0, p1, line].into_iter().collect();
        let mirrored = mirror_targets(sketch, &source_ids, axis_line, &axis_geom)?;
        let horizontal_count = mirrored
            .relations()
            .iter()
            .filter(|(_, r)| matches!(r, SketchRelation::Horizontal(_)))
            .count();
        assert_eq!(
            horizontal_count, 2,
            "original + mirrored horizontal must both exist"
        );

        let (sketch, axis_line, line, p0, p1) = axis_and_line();
        let (sketch, _) = sketch.apply(SketchEdit::AddRelation(SketchRelation::Parallel(
            line, axis_line,
        )))?;
        let source_ids: std::collections::BTreeSet<_> = [p0, p1, line].into_iter().collect();
        let mirrored = mirror_targets(sketch, &source_ids, axis_line, &axis_geom)?;
        let parallel_count = mirrored
            .relations()
            .iter()
            .filter(|(_, r)| matches!(r, SketchRelation::Parallel(_, b) if *b == axis_line))
            .count();
        assert_eq!(
            parallel_count, 2,
            "original + mirrored Parallel(line, axis_line) both expected: {parallel_count}",
        );
        Ok(())
    }

    #[test]
    fn construction_toggle_flips_line_flag() -> Result<(), Box<dyn std::error::Error>> {
        let (sketch, _, _, line) = horizontal_line_fixture();
        let before = match sketch.entities()[line] {
            SketchEntity::Line(l) => l.for_construction(),
            _ => panic!("line"),
        };
        let (next, _) = sketch.apply(SketchEdit::SetConstruction {
            id: line,
            for_construction: !before,
        })?;
        let after = match next.entities()[line] {
            SketchEntity::Line(l) => l.for_construction(),
            _ => panic!("line"),
        };
        assert_ne!(before, after);
        Ok(())
    }

    #[test]
    fn mirror_on_axis_source_point_is_identity_no_self_symmetric()
    -> Result<(), Box<dyn std::error::Error>> {
        let sketch = Sketch::new(Plane::Xy.basis());
        let (sketch, axis_a) = tools::add_point(sketch, Point2::from_mm(-5.0, 0.0));
        let (sketch, axis_b) = tools::add_point(sketch, Point2::from_mm(5.0, 0.0));
        let (sketch, axis_line) = tools::add_line(sketch, axis_a, axis_b, true);
        let (sketch, on_axis_point) = tools::add_point(sketch, Point2::from_mm(0.0, 0.0));
        let axis_geom =
            MirrorAxis::from_points(Point2::from_mm(-5.0, 0.0), Point2::from_mm(5.0, 0.0));
        let source_ids: std::collections::BTreeSet<_> = [on_axis_point].into_iter().collect();
        let mirrored = mirror_targets(sketch, &source_ids, axis_line, &axis_geom)?;
        let point_count = mirrored
            .entities()
            .iter()
            .filter(|(_, e)| matches!(e, SketchEntity::Point(_)))
            .count();
        assert_eq!(
            point_count, 3,
            "on-axis source must not produce a duplicate point: {point_count}"
        );
        let symmetric_count = mirrored
            .relations()
            .iter()
            .filter(|(_, r)| matches!(r, SketchRelation::Symmetric { .. }))
            .count();
        assert_eq!(
            symmetric_count, 0,
            "on-axis source must not emit a self-pair Symmetric relation"
        );
        Ok(())
    }
}
