Files
OneCAD-Draft/src/ui/point_style.rs
T
Hakan SevenandClaude Opus 4.8 96ec672d5b feat(points): point style commands + DDPTYPE dialog, viewport-aware size
Points could only render as a 1px dot — no way to change PDMODE/PDSIZE.

- PDMODE / PDSIZE commands set the header value and rebuild the glyphs.
- DDPTYPE opens a Point Style dialog: a 5x4 grid of rendered glyphs
  (shape x enclosure), a size field, and relative/absolute radios.
- Relative (<=0) PDSIZE now sizes from the current world-per-pixel so the
  marker stays a roughly constant on-screen size across zoom, rebuilding
  on zoom via the existing camera-generation cache key. Absolute (>0)
  scales with the world as before. Switching to absolute with a zero size
  seeds a positive value from the on-screen size so it stays representable.

Part of the #100 extensibility epic (point style surfaced in #106).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:13:12 +03:00

212 lines
7.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Point Style (DDPTYPE) dialog — pick a PDMODE glyph from a grid and set the
//! point size (PDSIZE), relative to the screen or in absolute units.
//!
//! Changes apply live to the active document header; the renderer rebuilds the
//! point glyphs (see `entities::point`). Command entry (`PDMODE` / `PDSIZE`)
//! does the same without the dialog.
use crate::app::Message;
use iced::widget::{button, canvas, column, container, radio, row, text, text_input, Space};
use iced::{mouse, Background, Border, Color, Element, Length, Point, Rectangle, Size, Theme};
const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
const WHITE: Color = Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 };
const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
const ACCENT: Color = Color { r: 0.30, g: 0.62, b: 0.95, a: 1.0 };
const CELL: Color = Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 };
const CELL_SEL: Color = Color { r: 0.30, g: 0.62, b: 0.95, a: 1.0 };
const GLYPH: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
const CELL_PX: f32 = 44.0;
/// Glyph-grid columns (low-nibble shape) × rows (enclosure bits):
/// shapes: 0=dot, 1=none, 2='+', 3='×', 4='|'
/// enclosures: 0=none, 32=circle, 64=square, 96=both
const ENCLOSURES: [i16; 4] = [0, 32, 64, 96];
const SHAPES: [i16; 5] = [0, 1, 2, 3, 4];
/// Canvas that renders a single PDMODE glyph inside a grid cell.
struct GlyphCanvas {
mode: i16,
}
impl canvas::Program<Message> for GlyphCanvas {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced::Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
let (cx, cy) = (bounds.width * 0.5, bounds.height * 0.5);
let r = bounds.width.min(bounds.height) * 0.30;
let stroke = canvas::Stroke {
width: 1.4,
style: canvas::Style::Solid(GLYPH),
..Default::default()
};
let line = |a: Point, b: Point| canvas::Path::line(a, b);
match self.mode & 0x0F {
0 => frame.fill(&canvas::Path::circle(Point::new(cx, cy), 2.4), GLYPH),
1 => {}
2 => {
frame.stroke(&line(Point::new(cx - r, cy), Point::new(cx + r, cy)), stroke.clone());
frame.stroke(&line(Point::new(cx, cy - r), Point::new(cx, cy + r)), stroke.clone());
}
3 => {
frame.stroke(
&line(Point::new(cx - r, cy - r), Point::new(cx + r, cy + r)),
stroke.clone(),
);
frame.stroke(
&line(Point::new(cx - r, cy + r), Point::new(cx + r, cy - r)),
stroke.clone(),
);
}
4 => frame.stroke(&line(Point::new(cx, cy - r), Point::new(cx, cy + r)), stroke.clone()),
_ => {}
}
if self.mode & 32 != 0 {
frame.stroke(&canvas::Path::circle(Point::new(cx, cy), r), stroke.clone());
}
if self.mode & 64 != 0 {
let sq = canvas::Path::rectangle(Point::new(cx - r, cy - r), Size::new(2.0 * r, 2.0 * r));
frame.stroke(&sq, stroke.clone());
}
vec![frame.into_geometry()]
}
}
fn cell<'a>(value: i16, selected: bool) -> Element<'a, Message> {
let glyph = canvas(GlyphCanvas { mode: value })
.width(Length::Fixed(CELL_PX))
.height(Length::Fixed(CELL_PX));
button(glyph)
.padding(0)
.on_press(Message::PointStyleSetMode(value))
.style(move |_: &Theme, status| {
let bg = if selected {
CELL_SEL
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }
} else {
CELL
};
button::Style {
background: Some(Background::Color(bg)),
border: Border {
color: Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 },
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
}
})
.into()
}
fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
text_input::Style {
background: Background::Color(Color { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }),
border: Border {
color: Color { r: 0.3, g: 0.3, b: 0.3, a: 1.0 },
width: 1.0,
radius: 4.0.into(),
},
icon: DIM,
placeholder: DIM,
value: WHITE,
selection: ACCENT,
}
}
pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<'a, Message> {
// Glyph grid: a row per enclosure, a cell per shape.
let mut grid = column![].spacing(6);
for enc in ENCLOSURES {
let mut r = row![].spacing(6);
for sh in SHAPES {
let value = enc + sh;
r = r.push(cell(value, value == pdmode));
}
grid = grid.push(r);
}
let size_row = row![
text("Point Size:").size(13).color(WHITE),
Space::new().width(10),
text_input("0", size_buf)
.on_input(Message::PointStyleSizeInput)
.on_submit(Message::PointStyleApplySize)
.style(field_style)
.size(13)
.width(110),
Space::new().width(6),
text(if relative { "%" } else { "units" }).size(12).color(DIM),
]
.align_y(iced::Center);
let radios = column![
radio(
"Set Size Relative to Screen",
true,
Some(relative),
Message::PointStyleSizeRelative,
)
.size(15)
.text_size(13),
radio(
"Set Size in Absolute Units",
false,
Some(relative),
Message::PointStyleSizeRelative,
)
.size(15)
.text_size(13),
]
.spacing(6);
let ok = button(text("OK").size(13).color(WHITE))
.padding([5, 22])
.on_press(Message::PointStyleOk)
.style(|_: &Theme, status| {
let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
Color { r: 0.32, g: 0.55, b: 0.85, a: 1.0 }
} else {
ACCENT
};
button::Style {
background: Some(Background::Color(bg)),
text_color: WHITE,
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
}
});
container(
column![
text("Point Style").size(18).color(WHITE),
Space::new().height(6),
grid,
Space::new().height(12),
size_row,
Space::new().height(8),
radios,
Space::new().height(12),
row![Space::new().width(Length::Fill), ok],
]
.spacing(4)
.padding(20),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.into()
}