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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-17 11:13:12 +03:00
1 parent a3b8acb59e
commit 96ec672d5b
14 files changed
+456 -45

No files matched your search

+1
View File
@@ -19,6 +19,7 @@ pub mod overlay;
pub mod page_setup;
pub mod plotstyle;
pub mod plugin_manager;
pub mod point_style;
pub mod properties;
pub mod ribbon;
pub mod scale_popup;
+211
View File
@@ -0,0 +1,211 @@
//! 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()
}