//! Viewport overlay widgets:
//!   • info_bar()        — top-left bracket buttons showing view name / style
//!   • nav_toolbar()     — vertical orbit/pan/zoom buttons on the right

use glam::{Mat4, Vec3};
use iced::mouse;
use iced::widget::{button, canvas, column, container, row, text};
use iced::{Background, Border, Color, Element, Length, Point, Size, Theme};

use crate::scene::object::GripShape;
use crate::scene::SelectionState;
use crate::app::Message;

/// Half-size of the crosshair center square in screen pixels (square = SQ*2 × SQ*2).
pub const CROSSHAIR_SQ: f32 = 7.5;
/// Arm length of the crosshair from center — used as the snap aperture radius.
pub const CROSSHAIR_ARM: f32 = 60.0;
use crate::snap::SnapType;

// ── Grip marker data ──────────────────────────────────────────────────────

/// Describes one grip to be drawn in the viewport overlay.
#[derive(Clone, Debug)]
pub struct GripMarker {
    /// Screen-space position (viewport-relative pixels).
    pub pos: Point,
    /// Explicit marker shape.
    pub shape: GripShape,
    /// True → grip is currently being dragged (drawn filled red).
    pub is_hot: bool,
}

// ── Info bar ─────────────────────────────────────────────────────────────

pub fn info_bar<'a>(view_name: &'a str, visual_style: &'a str) -> Element<'a, Message> {
    let bracket = |s: &'a str| -> Element<'a, Message> {
        container(text(s).size(10).color(Color {
            r: 0.78,
            g: 0.78,
            b: 0.78,
            a: 1.0,
        }))
        .style(|_: &Theme| container::Style {
            background: Some(Background::Color(Color {
                r: 0.10,
                g: 0.10,
                b: 0.10,
                a: 0.75,
            })),
            border: Border {
                color: Color {
                    r: 0.35,
                    g: 0.35,
                    b: 0.35,
                    a: 1.0,
                },
                width: 1.0,
                radius: 0.0.into(),
            },
            ..Default::default()
        })
        .padding([2, 6])
        .into()
    };
    row![bracket("[-]"), bracket(view_name), bracket(visual_style)]
        .spacing(0)
        .into()
}

// ── Nav toolbar ───────────────────────────────────────────────────────────

pub fn nav_toolbar<'a>() -> Element<'a, Message> {
    let b = |icon: &'a str, cmd: &'a str| -> Element<'a, Message> {
        button(text(icon).size(14).color(Color::WHITE))
            .on_press(Message::Command(cmd.into()))
            .style(|_: &Theme, status| button::Style {
                background: Some(Background::Color(match status {
                    button::Status::Hovered => Color {
                        r: 0.32,
                        g: 0.32,
                        b: 0.32,
                        a: 0.95,
                    },
                    button::Status::Pressed => Color {
                        r: 0.18,
                        g: 0.42,
                        b: 0.70,
                        a: 1.00,
                    },
                    _ => Color {
                        r: 0.20,
                        g: 0.20,
                        b: 0.20,
                        a: 0.85,
                    },
                })),
                border: Border {
                    color: Color {
                        r: 0.30,
                        g: 0.30,
                        b: 0.30,
                        a: 1.0,
                    },
                    width: 1.0,
                    radius: 2.0.into(),
                },
                text_color: Color::WHITE,
                shadow: iced::Shadow::default(),
                snap: false,
            })
            .padding([6, 8])
            .into()
    };
    container(
        column![
            b("⟳", "3DORBIT"),
            b("✥", "PAN"),
            b("⊕", "ZOOMIN"),
            b("⊖", "ZOOMOUT"),
            b("⊡", "ZOOMEXTENTS")
        ]
        .spacing(2),
    )
    .padding(4)
    .into()
}

// ── Grid display params ───────────────────────────────────────────────────

/// Which world-space plane the grid is drawn on — switches with camera angle.
#[derive(Clone, Copy, PartialEq)]
pub enum GridPlane {
    /// Horizontal XY plane (Z = 0).  Default top-down view (Z-up).
    Xy,
    /// Vertical XZ plane (Y = 0).  Front/back view.
    Xz,
    /// Vertical YZ plane (X = 0).  Side view.
    Yz,
}

/// Passed to the canvas when the GRID display is active.
#[derive(Clone)]
pub struct GridParams {
    pub view_proj: Mat4,
    pub bounds: iced::Rectangle,
    pub plane: GridPlane,
}

/// Parameters for the screen-space UCS icon drawn in the viewport corner.
pub struct UcsIconParams {
    /// View-projection matrix used to project world axis directions to screen.
    pub view_proj: Mat4,
    /// Viewport bounds (used for NDC → pixel conversion).
    pub bounds: iced::Rectangle,
}

// ── Selection overlay ───────────────────────────────────────────────────

pub fn selection_overlay<'a>(
    selection: SelectionState,
    snap: Option<(Point, SnapType)>,
    grips: Vec<GripMarker>,
    grid: Option<GridParams>,
    ucs_icon: Option<UcsIconParams>,
) -> Element<'a, Message> {
    canvas(SelectionCanvas {
        selection,
        snap,
        grips,
        grid,
        ucs_icon,
    })
    .width(Length::Fill)
    .height(Length::Fill)
    .into()
}

struct SelectionCanvas {
    selection: SelectionState,
    snap: Option<(Point, SnapType)>,
    grips: Vec<GripMarker>,
    grid: Option<GridParams>,
    ucs_icon: Option<UcsIconParams>,
}

impl canvas::Program<Message> for SelectionCanvas {
    type State = ();

    fn mouse_interaction(
        &self,
        _state: &(),
        bounds: iced::Rectangle,
        cursor: mouse::Cursor,
    ) -> mouse::Interaction {
        if cursor.is_over(bounds) {
            mouse::Interaction::Crosshair
        } else {
            mouse::Interaction::default()
        }
    }

    fn draw(
        &self,
        _state: &(),
        renderer: &iced::Renderer,
        _theme: &Theme,
        bounds: iced::Rectangle,
        _cursor: mouse::Cursor,
    ) -> Vec<canvas::Geometry> {
        let mut frame = canvas::Frame::new(renderer, bounds.size());

        // ── Grid display ──────────────────────────────────────────────────
        if let Some(ref g) = self.grid {
            draw_grid(&mut frame, g.view_proj, g.plane, g.bounds);
        }

        if let (Some(a), Some(b)) = (self.selection.box_anchor, self.selection.box_current) {
            let (fill, stroke) = if self.selection.box_crossing {
                (
                    Color {
                        r: 0.20,
                        g: 0.72,
                        b: 0.44,
                        a: 0.12,
                    },
                    Color {
                        r: 0.20,
                        g: 0.72,
                        b: 0.44,
                        a: 0.9,
                    },
                )
            } else {
                (
                    Color {
                        r: 0.20,
                        g: 0.44,
                        b: 0.72,
                        a: 0.12,
                    },
                    Color {
                        r: 0.20,
                        g: 0.44,
                        b: 0.72,
                        a: 0.9,
                    },
                )
            };
            let x0 = a.x.min(b.x);
            let y0 = a.y.min(b.y);
            let w = (a.x - b.x).abs();
            let h = (a.y - b.y).abs();
            let rect = canvas::Path::rectangle(Point::new(x0, y0), Size::new(w, h));
            frame.fill(&rect, fill);
            frame.stroke(
                &rect,
                canvas::Stroke {
                    width: 1.0,
                    style: canvas::Style::Solid(stroke),
                    ..Default::default()
                },
            );
        }

        if self.selection.poly_active && self.selection.poly_points.len() > 1 {
            let (fill, stroke) = if self.selection.poly_crossing {
                (
                    Color {
                        r: 0.20,
                        g: 0.72,
                        b: 0.44,
                        a: 0.12,
                    },
                    Color {
                        r: 0.20,
                        g: 0.72,
                        b: 0.44,
                        a: 0.9,
                    },
                )
            } else {
                (
                    Color {
                        r: 0.20,
                        g: 0.44,
                        b: 0.72,
                        a: 0.12,
                    },
                    Color {
                        r: 0.20,
                        g: 0.44,
                        b: 0.72,
                        a: 0.9,
                    },
                )
            };
            if let Some(cur) = self.selection.last_move_pos {
                let start = self.selection.poly_points[0];
                let fill_path = canvas::Path::new(|p| {
                    p.move_to(start);
                    for pt in &self.selection.poly_points[1..] {
                        p.line_to(*pt);
                    }
                    p.line_to(cur);
                    p.line_to(start);
                });
                frame.fill(&fill_path, fill);
            }
            let path = canvas::Path::new(|p| {
                p.move_to(self.selection.poly_points[0]);
                for pt in &self.selection.poly_points[1..] {
                    p.line_to(*pt);
                }
            });
            frame.stroke(
                &path,
                canvas::Stroke {
                    width: 1.0,
                    style: canvas::Style::Solid(stroke),
                    ..Default::default()
                },
            );
            if let Some(cur) = self.selection.last_move_pos {
                let start = self.selection.poly_points[0];
                let last = *self.selection.poly_points.last().unwrap();
                let preview = canvas::Path::new(|p| {
                    p.move_to(last);
                    p.line_to(cur);
                    p.line_to(start);
                });
                frame.stroke(
                    &preview,
                    canvas::Stroke {
                        width: 1.0,
                        style: canvas::Style::Solid(stroke),
                        ..Default::default()
                    },
                );
            }
        }

        if let Some(p) = self.selection.context_menu {
            let w = 140.0;
            let h = 72.0;
            let rect = canvas::Path::rectangle(Point::new(p.x, p.y), Size::new(w, h));
            frame.fill(
                &rect,
                Color {
                    r: 0.12,
                    g: 0.12,
                    b: 0.12,
                    a: 0.95,
                },
            );
            frame.stroke(
                &rect,
                canvas::Stroke {
                    width: 1.0,
                    style: canvas::Style::Solid(Color {
                        r: 0.30,
                        g: 0.30,
                        b: 0.30,
                        a: 1.0,
                    }),
                    ..Default::default()
                },
            );
            let items = ["Open", "Properties", "Hide"];
            for (i, item) in items.iter().enumerate() {
                frame.fill_text(canvas::Text {
                    content: item.to_string(),
                    position: Point::new(p.x + 10.0, p.y + 10.0 + i as f32 * 20.0),
                    color: Color::WHITE,
                    size: iced::Pixels(11.0),
                    font: iced::Font::DEFAULT,
                    align_x: iced::alignment::Horizontal::Left.into(),
                    align_y: iced::alignment::Vertical::Top.into(),
                    max_width: f32::INFINITY,
                    line_height: iced::widget::text::LineHeight::default(),
                    shaping: iced::widget::text::Shaping::default(),
                });
            }
        }

        // ── Grip markers ──────────────────────────────────────────────────
        for grip in &self.grips {
            let sp = grip.pos;
            let h = crate::scene::grip::GRIP_HALF_PX;
            let path = match grip.shape {
                GripShape::Square => canvas::Path::rectangle(
                    Point::new(sp.x - h, sp.y - h),
                    Size::new(h * 2.0, h * 2.0),
                ),
                GripShape::Diamond => canvas::Path::new(|b| {
                    b.move_to(Point::new(sp.x, sp.y - h));
                    b.line_to(Point::new(sp.x + h, sp.y));
                    b.line_to(Point::new(sp.x, sp.y + h));
                    b.line_to(Point::new(sp.x - h, sp.y));
                    b.close();
                }),
                GripShape::Triangle => canvas::Path::new(|b| {
                    b.move_to(Point::new(sp.x, sp.y - h));
                    b.line_to(Point::new(sp.x + h, sp.y + h));
                    b.line_to(Point::new(sp.x - h, sp.y + h));
                    b.close();
                }),
            };

            if grip.is_hot {
                // Hot grip: filled red marker
                let color = Color {
                    r: 1.0,
                    g: 0.15,
                    b: 0.10,
                    a: 1.0,
                };
                frame.fill(&path, color);
            } else {
                // Normal grip: hollow blue marker
                let color = Color {
                    r: 0.10,
                    g: 0.45,
                    b: 0.90,
                    a: 1.0,
                };
                let stroke = canvas::Stroke {
                    width: 1.5,
                    style: canvas::Style::Solid(color),
                    ..Default::default()
                };
                // Fill with semi-transparent background then stroke
                frame.fill(
                    &path,
                    Color {
                        r: 0.10,
                        g: 0.10,
                        b: 0.20,
                        a: 0.7,
                    },
                );
                frame.stroke(&path, stroke);
            }
        }

        // ── Snap marker ───────────────────────────────────────────────────
        if let Some((sp, snap_type)) = self.snap {
            let yellow = Color {
                r: 1.0,
                g: 0.9,
                b: 0.1,
                a: 1.0,
            };
            let stroke = canvas::Stroke {
                width: 1.5,
                style: canvas::Style::Solid(yellow),
                ..Default::default()
            };
            match snap_type {
                SnapType::Endpoint => {
                    let half = 5.0_f32;
                    let rect = canvas::Path::rectangle(
                        Point::new(sp.x - half, sp.y - half),
                        Size::new(half * 2.0, half * 2.0),
                    );
                    frame.stroke(&rect, stroke);
                }
                SnapType::Midpoint => {
                    let r = 6.0_f32;
                    let path = canvas::Path::new(|b| {
                        b.move_to(Point::new(sp.x, sp.y - r));
                        b.line_to(Point::new(sp.x + r * 0.866, sp.y + r * 0.5));
                        b.line_to(Point::new(sp.x - r * 0.866, sp.y + r * 0.5));
                        b.close();
                    });
                    frame.stroke(&path, stroke);
                }
                SnapType::Grid => {
                    let arm = 4.0_f32;
                    let h = canvas::Path::new(|b| {
                        b.move_to(Point::new(sp.x - arm, sp.y));
                        b.line_to(Point::new(sp.x + arm, sp.y));
                    });
                    let v = canvas::Path::new(|b| {
                        b.move_to(Point::new(sp.x, sp.y - arm));
                        b.line_to(Point::new(sp.x, sp.y + arm));
                    });
                    frame.stroke(&h, stroke.clone());
                    frame.stroke(&v, stroke);
                }
                // Other snap types use the same diamond marker.
                _ => {
                    let r = 5.0_f32;
                    let path = canvas::Path::new(|b| {
                        b.move_to(Point::new(sp.x, sp.y - r));
                        b.line_to(Point::new(sp.x + r, sp.y));
                        b.line_to(Point::new(sp.x, sp.y + r));
                        b.line_to(Point::new(sp.x - r, sp.y));
                        b.close();
                    });
                    frame.stroke(&path, stroke);
                }
            }
        }

        // ── CAD crosshair cursor ──────────────────────────────────────────────
        if let Some(cp) = self.selection.last_move_pos {
            let color = Color {
                r: 0.85,
                g: 0.85,
                b: 0.85,
                a: 0.90,
            };
            let stroke = canvas::Stroke {
                width: 1.0,
                style: canvas::Style::Solid(color),
                ..Default::default()
            };
            let sq = CROSSHAIR_SQ; // square half-size → 15×15
            let arm = CROSSHAIR_ARM; // crosshair arm length from center

            // Horizontal arms (start at square edge, end at arm length)
            let h_left = canvas::Path::new(|b| {
                b.move_to(Point::new(cp.x - sq, cp.y));
                b.line_to(Point::new(cp.x - arm, cp.y));
            });
            let h_right = canvas::Path::new(|b| {
                b.move_to(Point::new(cp.x + sq, cp.y));
                b.line_to(Point::new(cp.x + arm, cp.y));
            });
            // Vertical arms
            let v_top = canvas::Path::new(|b| {
                b.move_to(Point::new(cp.x, cp.y - sq));
                b.line_to(Point::new(cp.x, cp.y - arm));
            });
            let v_bot = canvas::Path::new(|b| {
                b.move_to(Point::new(cp.x, cp.y + sq));
                b.line_to(Point::new(cp.x, cp.y + arm));
            });
            // Center square
            let square = canvas::Path::rectangle(
                Point::new(cp.x - sq, cp.y - sq),
                Size::new(sq * 2.0, sq * 2.0),
            );

            frame.stroke(&h_left, stroke.clone());
            frame.stroke(&h_right, stroke.clone());
            frame.stroke(&v_top, stroke.clone());
            frame.stroke(&v_bot, stroke.clone());
            frame.stroke(&square, stroke);
        }

        // ── UCS icon ──────────────────────────────────────────────────────
        if let Some(ref ucs) = self.ucs_icon {
            draw_ucs_icon(&mut frame, ucs.view_proj, ucs.bounds);
        }

        vec![frame.into_geometry()]
    }
}

// ── Grid line drawing ─────────────────────────────────────────────────────

/// Minimum pixel gap between adjacent grid lines before stepping up to next spacing.
const MIN_GRID_PX: f32 = 20.0;

fn draw_grid(frame: &mut canvas::Frame, vp: Mat4, plane: GridPlane, bounds: iced::Rectangle) {
    let w2s = |world: Vec3| -> Point {
        let ndc = vp.project_point3(world);
        Point::new(
            (ndc.x + 1.0) * 0.5 * bounds.width,
            (1.0 - ndc.y) * 0.5 * bounds.height,
        )
    };

    // Plane-tangent axes: axis1 and axis2 span the grid plane.
    let (axis1, axis2) = match plane {
        GridPlane::Xz => (Vec3::X, Vec3::Z),
        GridPlane::Xy => (Vec3::X, Vec3::Y),
        GridPlane::Yz => (Vec3::Y, Vec3::Z),
    };

    // Adaptive spacing: measure pixels per 1-unit step along each axis,
    // then find the smallest power-of-5 multiple that gives ≥ MIN_GRID_PX.
    let o = w2s(Vec3::ZERO);
    let a1s = w2s(axis1);
    let a2s = w2s(axis2);
    let px1 = ((a1s.x - o.x).powi(2) + (a1s.y - o.y).powi(2)).sqrt();
    let px2 = ((a2s.x - o.x).powi(2) + (a2s.y - o.y).powi(2)).sqrt();
    let px_per_unit = px1.max(px2);
    if px_per_unit < 1e-6 {
        return;
    }

    let mut s = 1.0_f32;
    while s * px_per_unit < MIN_GRID_PX {
        s *= 5.0;
        if s > 1e9 {
            return;
        }
    }

    // Visible world extent: unproject screen corners (mid-depth approximation)
    // and project them onto the grid axes.
    let inv = vp.inverse();
    let unproject = |sx: f32, sy: f32| -> Vec3 {
        let ndc_x = (sx / bounds.width) * 2.0 - 1.0;
        let ndc_y = 1.0 - (sy / bounds.height) * 2.0;
        inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.5))
    };
    let corners = [
        unproject(0.0, 0.0),
        unproject(bounds.width, 0.0),
        unproject(0.0, bounds.height),
        unproject(bounds.width, bounds.height),
    ];
    let range = |ax: Vec3| -> (f32, f32) {
        let vals: Vec<f32> = corners.iter().map(|p| p.dot(ax)).collect();
        (
            vals.iter().cloned().fold(f32::INFINITY, f32::min),
            vals.iter().cloned().fold(f32::NEG_INFINITY, f32::max),
        )
    };
    let (min1, max1) = range(axis1);
    let (min2, max2) = range(axis2);

    let n1_s = (min1 / s).floor() as i32 - 1;
    let n1_e = (max1 / s).ceil() as i32 + 1;
    let n2_s = (min2 / s).floor() as i32 - 1;
    let n2_e = (max2 / s).ceil() as i32 + 1;
    if (n1_e - n1_s) > 500 || (n2_e - n2_s) > 500 {
        return;
    }

    let gc = Color {
        r: 0.28,
        g: 0.28,
        b: 0.28,
        a: 0.7,
    };
    let st = canvas::Stroke {
        width: 0.5,
        style: canvas::Style::Solid(gc),
        ..Default::default()
    };

    // Lines parallel to axis2 (varying axis1 position)
    for i in n1_s..=n1_e {
        let v = i as f32 * s;
        let p0 = w2s(axis1 * v + axis2 * (min2 - s));
        let p1 = w2s(axis1 * v + axis2 * (max2 + s));
        frame.stroke(
            &canvas::Path::new(|b| {
                b.move_to(p0);
                b.line_to(p1);
            }),
            st.clone(),
        );
    }
    // Lines parallel to axis1 (varying axis2 position)
    for i in n2_s..=n2_e {
        let v = i as f32 * s;
        let p0 = w2s(axis2 * v + axis1 * (min1 - s));
        let p1 = w2s(axis2 * v + axis1 * (max1 + s));
        frame.stroke(
            &canvas::Path::new(|b| {
                b.move_to(p0);
                b.line_to(p1);
            }),
            st.clone(),
        );
    }

    // World-space axes always drawn on top of the grid lines.
    let extent = (min1.abs().max(max1.abs()).max(min2.abs()).max(max2.abs()) + s) * 1.5;
    draw_axes(frame, vp, bounds, extent.max(10.0));
}

// ── Coloured world-space axes ─────────────────────────────────────────────

fn draw_axes(frame: &mut canvas::Frame, vp: Mat4, bounds: iced::Rectangle, extent: f32) {
    let w2s = |world: Vec3| -> Point {
        let ndc = vp.project_point3(world);
        Point::new(
            (ndc.x + 1.0) * 0.5 * bounds.width,
            (1.0 - ndc.y) * 0.5 * bounds.height,
        )
    };
    let e = extent;
    let axis_stroke = |r: f32, g: f32, b: f32| canvas::Stroke {
        width: 1.5,
        style: canvas::Style::Solid(Color { r, g, b, a: 0.85 }),
        ..Default::default()
    };
    // X — red
    frame.stroke(
        &canvas::Path::new(|p| {
            p.move_to(w2s(Vec3::new(-e, 0.0, 0.0)));
            p.line_to(w2s(Vec3::new(e, 0.0, 0.0)));
        }),
        axis_stroke(0.90, 0.20, 0.20),
    );
    // Y — green
    frame.stroke(
        &canvas::Path::new(|p| {
            p.move_to(w2s(Vec3::new(0.0, -e, 0.0)));
            p.line_to(w2s(Vec3::new(0.0, e, 0.0)));
        }),
        axis_stroke(0.20, 0.85, 0.20),
    );
    // Z — blue
    frame.stroke(
        &canvas::Path::new(|p| {
            p.move_to(w2s(Vec3::new(0.0, 0.0, -e)));
            p.line_to(w2s(Vec3::new(0.0, 0.0, e)));
        }),
        axis_stroke(0.20, 0.40, 0.90),
    );
}

// ── UCS icon ──────────────────────────────────────────────────────────────
//
// Draws a small X/Y/Z axis tripod in the bottom-left corner of the viewport.
// The axis directions are projected from world space so the icon rotates with
// the camera, matching the orientation of the drawing plane.

const UCS_ICON_MARGIN: f32 = 50.0; // px from bottom-left corner to icon origin
const UCS_ICON_LEN: f32 = 35.0;    // axis arm length in screen pixels
const UCS_ICON_TIP: f32 = 6.0;     // arrowhead size in pixels

fn draw_ucs_icon(frame: &mut canvas::Frame, vp: Mat4, bounds: iced::Rectangle) {
    // Guard against zero-size viewport (before layout pass) or degenerate matrix.
    if bounds.width < 10.0 || bounds.height < 10.0 {
        return;
    }

    // Project world origin and axis unit vectors to NDC, then to screen px.
    let w2s = |world: Vec3| -> Option<Point> {
        let ndc = vp.project_point3(world);
        if !ndc.x.is_finite() || !ndc.y.is_finite() {
            return None;
        }
        Some(Point::new(
            (ndc.x + 1.0) * 0.5 * bounds.width,
            (1.0 - ndc.y) * 0.5 * bounds.height,
        ))
    };

    // Origin in screen space — used to compute axis directions.
    let Some(origin_s) = w2s(Vec3::ZERO) else { return };
    let Some(x_tip_s) = w2s(Vec3::X) else { return };
    let Some(y_tip_s) = w2s(Vec3::Y) else { return };
    let Some(z_tip_s) = w2s(Vec3::Z) else { return };

    // Icon origin: fixed bottom-left corner.
    let ox = UCS_ICON_MARGIN;
    let oy = (bounds.height - UCS_ICON_MARGIN).max(UCS_ICON_MARGIN);
    let icon_origin = Point::new(ox, oy);

    // Compute normalized screen-space axis directions, then scale to UCS_ICON_LEN.
    let axis_dir = |tip: Point| -> Option<Point> {
        let dx = tip.x - origin_s.x;
        let dy = tip.y - origin_s.y;
        let len = (dx * dx + dy * dy).sqrt();
        if len < 1e-4 {
            return None;
        }
        Some(Point::new(dx / len * UCS_ICON_LEN, dy / len * UCS_ICON_LEN))
    };

    let draw_axis = |frame: &mut canvas::Frame, dir: Point, r: f32, g: f32, b: f32| {
        let tip = Point::new(icon_origin.x + dir.x, icon_origin.y + dir.y);
        if !tip.x.is_finite() || !tip.y.is_finite() { return; }
        let path = canvas::Path::new(|p| {
            p.move_to(icon_origin);
            p.line_to(tip);
        });
        frame.stroke(
            &path,
            canvas::Stroke {
                width: 2.0,
                style: canvas::Style::Solid(Color { r, g, b, a: 1.0 }),
                line_cap: canvas::LineCap::Round,
                ..Default::default()
            },
        );
        // Arrowhead: small filled triangle at tip.
        let len = (dir.x * dir.x + dir.y * dir.y).sqrt().max(1e-4);
        let nx = dir.x / len;
        let ny = dir.y / len;
        let px = -ny;
        let py = nx;
        let tip_l = Point::new(tip.x - nx * UCS_ICON_TIP + px * (UCS_ICON_TIP * 0.4), tip.y - ny * UCS_ICON_TIP + py * (UCS_ICON_TIP * 0.4));
        let tip_r = Point::new(tip.x - nx * UCS_ICON_TIP - px * (UCS_ICON_TIP * 0.4), tip.y - ny * UCS_ICON_TIP - py * (UCS_ICON_TIP * 0.4));
        let arrow = canvas::Path::new(|p| {
            p.move_to(tip);
            p.line_to(tip_l);
            p.line_to(tip_r);
            p.close();
        });
        frame.fill(&arrow, Color { r, g, b, a: 1.0 });
    };

    // Draw Z first (behind), then Y, then X (in front).
    if let Some(dir) = axis_dir(z_tip_s) {
        draw_axis(frame, dir, 0.20, 0.40, 0.90); // Z blue
    }
    if let Some(dir) = axis_dir(y_tip_s) {
        draw_axis(frame, dir, 0.20, 0.85, 0.20); // Y green
    }
    if let Some(dir) = axis_dir(x_tip_s) {
        draw_axis(frame, dir, 0.90, 0.20, 0.20); // X red
    }

    // Small circle at origin.
    let circle = canvas::Path::circle(icon_origin, 3.0);
    frame.fill(&circle, Color { r: 0.9, g: 0.9, b: 0.9, a: 0.9 });
}
