forked from 1b-agent/opencadstudio-upstream
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:
1 parent
a3b8acb59e
commit
96ec672d5b
14 files changed
+456
-45
No files matched your search
@@ -4801,6 +4801,50 @@ impl OpenCADStudio {
|
||||
self.command_line.push_error("Usage: LTSCALE [value]");
|
||||
}
|
||||
}
|
||||
cmd if cmd == "PDMODE" || cmd.starts_with("PDMODE ") => {
|
||||
let val_str = cmd.trim_start_matches("PDMODE").trim();
|
||||
if val_str.is_empty() {
|
||||
let v = self.tabs[i].scene.document.header.point_display_mode;
|
||||
self.command_line.push_output(&format!("PDMODE = {v}"));
|
||||
} else if let Ok(v) = val_str.parse::<i16>() {
|
||||
self.push_undo_snapshot(i, "PDMODE");
|
||||
self.tabs[i].scene.document.header.point_display_mode = v;
|
||||
// Point glyphs are built at tessellation time — rebuild them.
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!("PDMODE set to {v}"));
|
||||
} else {
|
||||
self.command_line.push_error(
|
||||
"Usage: PDMODE [value] (0=dot 1=none 2=+ 3=x 4=tick; +32 circle, +64 square)",
|
||||
);
|
||||
}
|
||||
}
|
||||
cmd if cmd == "PDSIZE" || cmd.starts_with("PDSIZE ") => {
|
||||
let val_str = cmd.trim_start_matches("PDSIZE").trim();
|
||||
if val_str.is_empty() {
|
||||
let v = self.tabs[i].scene.document.header.point_display_size;
|
||||
self.command_line.push_output(&format!("PDSIZE = {v:.4}"));
|
||||
} else if let Ok(v) = val_str.parse::<f64>() {
|
||||
self.push_undo_snapshot(i, "PDSIZE");
|
||||
self.tabs[i].scene.document.header.point_display_size = v;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!("PDSIZE set to {v:.4}"));
|
||||
} else {
|
||||
self.command_line.push_error(
|
||||
"Usage: PDSIZE [value] (>0 absolute size, <0 percent of viewport, 0 default)",
|
||||
);
|
||||
}
|
||||
}
|
||||
cmd if cmd == "DDPTYPE" => {
|
||||
// The dialog shows the magnitude; the sign (relative/absolute)
|
||||
// is driven by the radio buttons. A positive PDSIZE is absolute;
|
||||
// zero or negative is relative.
|
||||
let pdsize = self.tabs[i].scene.document.header.point_display_size;
|
||||
self.point_size_relative = pdsize <= 0.0;
|
||||
self.point_size_buf = format!("{}", pdsize.abs());
|
||||
self.active_modal = Some(super::ModalKind::PointStyle);
|
||||
}
|
||||
cmd if cmd == "LWDISPLAY" || cmd.starts_with("LWDISPLAY ") => {
|
||||
let val_str = cmd.trim_start_matches("LWDISPLAY").trim();
|
||||
let parsed: Result<Option<bool>, ()> =
|
||||
|
||||
@@ -297,6 +297,12 @@ pub(super) struct OpenCADStudio {
|
||||
/// keep their manifest listed but drop their ribbon tab and command
|
||||
/// dispatch. Persisted via [`settings::UserSettings::disabled_plugins`].
|
||||
disabled_plugins: rustc_hash::FxHashSet<String>,
|
||||
/// PDSIZE text buffer for the Point Style (DDPTYPE) dialog.
|
||||
point_size_buf: String,
|
||||
/// Point Style size mode: `true` = relative to screen, `false` = absolute.
|
||||
/// Tracked separately from the PDSIZE sign so a size of 0 (sign-less) still
|
||||
/// remembers which radio is active.
|
||||
point_size_relative: bool,
|
||||
/// New-release notification window — opened on startup when the
|
||||
/// GitHub releases API reports a newer version than this build.
|
||||
/// First-launch "make Open CAD Studio the default for .dwg/.dxf?" prompt
|
||||
@@ -603,6 +609,7 @@ pub enum ModalKind {
|
||||
Unsaved,
|
||||
SaveDialog,
|
||||
AssocPrompt,
|
||||
PointStyle,
|
||||
}
|
||||
|
||||
/// Identifies a DimStyle field that can be edited in the dialog.
|
||||
@@ -1031,6 +1038,18 @@ pub enum Message {
|
||||
PluginManagerClose,
|
||||
/// Enable (`true`) or disable (`false`) the plugin with this id.
|
||||
SetPluginEnabled(String, bool),
|
||||
// ── Point Style (DDPTYPE) dialog ──────────────────────────────────────
|
||||
/// Set the full PDMODE value from a glyph-grid cell.
|
||||
PointStyleSetMode(i16),
|
||||
/// Choose size mode: `true` = relative to screen (PDSIZE < 0), `false` =
|
||||
/// absolute units (PDSIZE > 0).
|
||||
PointStyleSizeRelative(bool),
|
||||
/// Edit the point-size text field (magnitude only).
|
||||
PointStyleSizeInput(String),
|
||||
/// Commit the point-size text field to the header with the current sign.
|
||||
PointStyleApplySize,
|
||||
/// Apply the point-size field and close the dialog (OK button).
|
||||
PointStyleOk,
|
||||
// ── Quick Select / Select Similar ───────────────────────────────────
|
||||
/// Extend the current selection with every entity in the active
|
||||
/// layout that matches a selected entity by (type, layer).
|
||||
@@ -1442,6 +1461,8 @@ impl OpenCADStudio {
|
||||
modal_drag_last: None,
|
||||
modal_dragging: false,
|
||||
disabled_plugins: rustc_hash::FxHashSet::default(),
|
||||
point_size_buf: String::new(),
|
||||
point_size_relative: true,
|
||||
default_assoc_prompted: false,
|
||||
update_notice_version: None,
|
||||
update_notice_body: None,
|
||||
|
||||
@@ -408,6 +408,7 @@ impl super::OpenCADStudio {
|
||||
1.0,
|
||||
woff,
|
||||
anno,
|
||||
None,
|
||||
);
|
||||
// Per-character boxes in the same offset-subtracted frame as the
|
||||
// preview wires (tessellate removes world_offset; mirror that).
|
||||
|
||||
@@ -178,6 +178,50 @@ impl OpenCADStudio {
|
||||
self.disabled_plugins.clone()
|
||||
}
|
||||
|
||||
/// Write PDSIZE from the dialog buffer with the current relative/absolute
|
||||
/// sign. A relative size is stored negative; absolute positive. Switching to
|
||||
/// absolute with an empty/zero size seeds a positive value from the current
|
||||
/// on-screen size so the point stays representable (PDSIZE 0 always reads as
|
||||
/// relative, so absolute needs a non-zero magnitude).
|
||||
fn apply_point_size(&mut self) {
|
||||
let i = self.active_tab;
|
||||
let mut mag = self
|
||||
.point_size_buf
|
||||
.trim()
|
||||
.parse::<f64>()
|
||||
.unwrap_or(0.0)
|
||||
.abs();
|
||||
if !self.point_size_relative && mag == 0.0 {
|
||||
let wpp = self.tabs[i].scene.world_per_pixel().unwrap_or(0.0);
|
||||
mag = if wpp > 0.0 {
|
||||
crate::entities::point::relative_world_size(0.0, wpp)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
self.point_size_buf = format!("{mag:.4}");
|
||||
}
|
||||
let next = if self.point_size_relative { -mag } else { mag };
|
||||
self.push_undo_snapshot(i, "PDSIZE");
|
||||
self.tabs[i].scene.document.header.point_display_size = next;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
|
||||
/// Replace the `mask` bits of PDMODE with `value`, rebuild the point glyphs
|
||||
/// and mark the document dirty. Used by the Point Style (DDPTYPE) dialog.
|
||||
fn set_point_mode_bits(&mut self, mask: i16, value: i16) {
|
||||
let i = self.active_tab;
|
||||
let cur = self.tabs[i].scene.document.header.point_display_mode;
|
||||
let next = (cur & !mask) | (value & mask);
|
||||
if next == cur {
|
||||
return;
|
||||
}
|
||||
self.push_undo_snapshot(i, "PDMODE");
|
||||
self.tabs[i].scene.document.header.point_display_mode = next;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
|
||||
/// Write preferences to disk only when they differ from the last write,
|
||||
/// so a toggle persists immediately without thrashing the file.
|
||||
fn persist_settings_if_changed(&mut self) {
|
||||
@@ -5499,6 +5543,28 @@ impl OpenCADStudio {
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
Message::PointStyleSetMode(mode) => {
|
||||
self.set_point_mode_bits(!0, mode);
|
||||
Task::none()
|
||||
}
|
||||
Message::PointStyleSizeRelative(relative) => {
|
||||
self.point_size_relative = relative;
|
||||
self.apply_point_size();
|
||||
Task::none()
|
||||
}
|
||||
Message::PointStyleSizeInput(s) => {
|
||||
self.point_size_buf = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PointStyleApplySize => {
|
||||
self.apply_point_size();
|
||||
Task::none()
|
||||
}
|
||||
Message::PointStyleOk => {
|
||||
self.apply_point_size();
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::EnterViewport(handle) => {
|
||||
let i = self.active_tab;
|
||||
|
||||
@@ -1100,6 +1100,7 @@ impl OpenCADStudio {
|
||||
AssocPrompt => (440, 210),
|
||||
Unsaved => (420, 160),
|
||||
SaveDialog => (560, 480),
|
||||
PointStyle => (360, 470),
|
||||
};
|
||||
Some((w as f32 + EXTRA_W, h as f32 + EXTRA_H))
|
||||
}
|
||||
@@ -1572,6 +1573,15 @@ impl OpenCADStudio {
|
||||
};
|
||||
sized(unsaved_changes_dialog_window(&tab_name), 420, 160)
|
||||
}
|
||||
super::ModalKind::PointStyle => sized(
|
||||
crate::ui::point_style::view_window(
|
||||
self.tabs[self.active_tab].scene.document.header.point_display_mode,
|
||||
self.point_size_relative,
|
||||
&self.point_size_buf,
|
||||
),
|
||||
360,
|
||||
470,
|
||||
),
|
||||
super::ModalKind::SaveDialog => sized(
|
||||
save_as_dialog_window(
|
||||
&self.save_dialog_filename,
|
||||
|
||||
@@ -181,6 +181,7 @@ pub(crate) fn append_insert_attribute_wires(
|
||||
sub_lw_px,
|
||||
world_offset,
|
||||
anno_scale,
|
||||
None,
|
||||
);
|
||||
// Use the INSERT's handle so selection / picking groups attribute
|
||||
// text with the parent insert instead of treating it as a stray text.
|
||||
|
||||
@@ -1340,6 +1340,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
leader_lw_px,
|
||||
world_offset,
|
||||
1.0,
|
||||
None,
|
||||
);
|
||||
for w in &mut sub_wires {
|
||||
w.name = name.clone();
|
||||
|
||||
+86
-43
@@ -1,4 +1,5 @@
|
||||
use acadrust::entities::Point;
|
||||
use acadrust::EntityType;
|
||||
use glam::Vec3;
|
||||
use truck_modeling::{builder, Point3};
|
||||
|
||||
@@ -9,20 +10,96 @@ use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::model::wire_model::SnapHint;
|
||||
|
||||
/// Resolve PDSIZE (negative = % of viewport, 0 = 5% default, positive = world).
|
||||
/// We don't know the viewport height at tessellation time, so percentages and
|
||||
/// the 0 default expand to a small fixed world size as a best-effort fallback.
|
||||
/// Nominal viewport height (px) used to turn a relative PDSIZE percentage into
|
||||
/// an on-screen pixel size. The exact viewport height isn't threaded into
|
||||
/// tessellation; this reference keeps relative points a roughly constant
|
||||
/// fraction of the screen across zoom levels.
|
||||
const REL_REF_PX: f64 = 600.0;
|
||||
|
||||
/// Resolve a positive (absolute) PDSIZE to a world size. Relative/zero PDSIZE
|
||||
/// is handled by [`relative_truck`] when a world-per-pixel factor is available;
|
||||
/// without one it falls back to a small fixed world size.
|
||||
fn pdsize_world(pdsize: f64) -> f64 {
|
||||
if pdsize > 0.0 {
|
||||
pdsize
|
||||
} else {
|
||||
// Both PDSIZE = 0 and negative (relative) fall back to a sensible
|
||||
// visible default until viewport-aware sizing is wired up.
|
||||
2.0
|
||||
}
|
||||
}
|
||||
|
||||
fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, pdsize: f64) -> Vec<[f64; 3]> {
|
||||
/// Build the truck entity for a point given the glyph half-size `s` in world
|
||||
/// units. Shared by the header-driven path ([`to_truck`]) and the
|
||||
/// viewport-aware relative path ([`relative_truck`]).
|
||||
fn point_truck(pt: &Point, pdmode: i16, s: f64) -> TruckEntity {
|
||||
let normal = (pt.normal.x, pt.normal.y, pt.normal.z);
|
||||
let (wx, wy, wz) = crate::scene::view::transform::ocs_point_to_wcs(
|
||||
(pt.location.x, pt.location.y, pt.location.z),
|
||||
normal,
|
||||
);
|
||||
let snap = Vec3::new(wx as f32, wy as f32, wz as f32);
|
||||
if pdmode == 0 {
|
||||
// Default: a single vertex (driver handles the dot pixel).
|
||||
let p = Point3::new(wx, wy, wz);
|
||||
return TruckEntity {
|
||||
object: TruckObject::Point(builder::vertex(p)),
|
||||
snap_pts: vec![(snap, SnapHint::Node)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
fill_tris: vec![],
|
||||
};
|
||||
}
|
||||
let pts = point_glyph(wx, wy, wz, pdmode, s);
|
||||
if pts.is_empty() {
|
||||
// PDMODE 1 = nothing — emit an empty Lines wire so picking still works.
|
||||
return TruckEntity {
|
||||
object: TruckObject::Lines(vec![]),
|
||||
snap_pts: vec![(snap, SnapHint::Node)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![[wx, wy, wz]],
|
||||
fill_tris: vec![],
|
||||
};
|
||||
}
|
||||
TruckEntity {
|
||||
object: TruckObject::Lines(pts),
|
||||
snap_pts: vec![(snap, SnapHint::Node)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![[wx, wy, wz]],
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Viewport-aware override for a relative (≤ 0) PDSIZE: size the glyph from the
|
||||
/// world-per-pixel factor so the point stays a roughly constant on-screen size
|
||||
/// across zoom. Returns `None` for an absolute PDSIZE, the size-independent
|
||||
/// default dot (PDMODE 0), or when no `wpp` is available — the caller then uses
|
||||
/// the normal header-driven path.
|
||||
pub fn relative_truck(
|
||||
entity: &EntityType,
|
||||
document: &acadrust::CadDocument,
|
||||
wpp: Option<f32>,
|
||||
) -> Option<TruckEntity> {
|
||||
let EntityType::Point(pt) = entity else {
|
||||
return None;
|
||||
};
|
||||
let pdsize = document.header.point_display_size;
|
||||
let pdmode = document.header.point_display_mode;
|
||||
if pdsize > 0.0 || pdmode == 0 {
|
||||
return None;
|
||||
}
|
||||
let wpp = wpp.filter(|w| *w > 0.0)?;
|
||||
Some(point_truck(pt, pdmode, relative_world_size(pdsize, wpp) * 0.5))
|
||||
}
|
||||
|
||||
/// Full on-screen glyph size (world units) for a relative (≤ 0) PDSIZE at the
|
||||
/// given world-per-pixel factor. PDSIZE 0 is the 5% default; negative is the
|
||||
/// percentage. Used both for rendering and to seed an absolute size when the
|
||||
/// user switches the Point Style dialog to absolute units.
|
||||
pub fn relative_world_size(pdsize: f64, wpp: f32) -> f64 {
|
||||
let pct = if pdsize == 0.0 { 5.0 } else { -pdsize };
|
||||
(pct / 100.0) * REL_REF_PX * wpp as f64
|
||||
}
|
||||
|
||||
fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, s_half: f64) -> Vec<[f64; 3]> {
|
||||
// PDMODE bits:
|
||||
// shape: 0=dot, 1=nothing, 2='+', 3='×', 4='|'
|
||||
// +32 = enclose in a circle
|
||||
@@ -31,7 +108,7 @@ fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, pdsize: f64) -> Vec<[f64;
|
||||
let shape = (pdmode & 0x0F) as i32;
|
||||
let circle = (pdmode & 32) != 0;
|
||||
let square = (pdmode & 64) != 0;
|
||||
let s = pdsize_world(pdsize) * 0.5;
|
||||
let s = s_half;
|
||||
let nan = [f64::NAN, f64::NAN, f64::NAN];
|
||||
let mut pts: Vec<[f64; 3]> = Vec::new();
|
||||
let mut push_seg = |a: [f64; 3], b: [f64; 3]| {
|
||||
@@ -92,43 +169,9 @@ fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, pdsize: f64) -> Vec<[f64;
|
||||
}
|
||||
|
||||
fn to_truck(pt: &Point, document: &acadrust::CadDocument) -> TruckEntity {
|
||||
let normal = (pt.normal.x, pt.normal.y, pt.normal.z);
|
||||
let (wx, wy, wz) = crate::scene::view::transform::ocs_point_to_wcs(
|
||||
(pt.location.x, pt.location.y, pt.location.z),
|
||||
normal,
|
||||
);
|
||||
let snap = Vec3::new(wx as f32, wy as f32, wz as f32);
|
||||
let pdmode = document.header.point_display_mode;
|
||||
let pdsize = document.header.point_display_size;
|
||||
if pdmode == 0 {
|
||||
// Default: a single vertex (driver handles the dot pixel).
|
||||
let p = Point3::new(wx, wy, wz);
|
||||
return TruckEntity {
|
||||
object: TruckObject::Point(builder::vertex(p)),
|
||||
snap_pts: vec![(snap, SnapHint::Node)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
fill_tris: vec![],
|
||||
};
|
||||
}
|
||||
let pts = point_glyph(wx, wy, wz, pdmode, pdsize);
|
||||
if pts.is_empty() {
|
||||
// PDMODE 1 = nothing — emit an empty Lines wire so picking still works.
|
||||
return TruckEntity {
|
||||
object: TruckObject::Lines(vec![]),
|
||||
snap_pts: vec![(snap, SnapHint::Node)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![[wx, wy, wz]],
|
||||
fill_tris: vec![],
|
||||
};
|
||||
}
|
||||
TruckEntity {
|
||||
object: TruckObject::Lines(pts),
|
||||
snap_pts: vec![(snap, SnapHint::Node)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![[wx, wy, wz]],
|
||||
fill_tris: vec![],
|
||||
}
|
||||
let s = pdsize_world(document.header.point_display_size) * 0.5;
|
||||
point_truck(pt, pdmode, s)
|
||||
}
|
||||
|
||||
fn grips(pt: &Point) -> Vec<GripDef> {
|
||||
|
||||
@@ -59,3 +59,7 @@ impl CadCommand for PointCommand {
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["PO", "POINT"] }); // PointCommand
|
||||
// Point display style system variables + dialog.
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["PDMODE", "PDSIZE", "DDPTYPE"]
|
||||
});
|
||||
Vendored
+1
-1
@@ -450,7 +450,7 @@ fn tessellate_sub_local(
|
||||
// top-level MTEXT outside blocks the colour split is preserved via the
|
||||
// hot path in `tessellate_entity`.
|
||||
let mut wires_out = tessellate::tessellate(
|
||||
doc, h, sub, false, sub_color, pat_len, pat, lw_px, local_offset, anno_scale,
|
||||
doc, h, sub, false, sub_color, pat_len, pat, lw_px, local_offset, anno_scale, None,
|
||||
);
|
||||
if wires_out.is_empty() {
|
||||
return None;
|
||||
|
||||
@@ -42,6 +42,7 @@ pub fn tessellate(
|
||||
line_weight_px: f32,
|
||||
world_offset: [f64; 3],
|
||||
anno_scale: f32,
|
||||
world_per_pixel: Option<f32>,
|
||||
) -> Vec<WireModel> {
|
||||
let color = if selected {
|
||||
WireModel::SELECTED
|
||||
@@ -87,7 +88,11 @@ pub fn tessellate(
|
||||
}
|
||||
|
||||
// ── Try the truck path first ───────────────────────────────────────────
|
||||
if let Some(te) = convert(entity, document) {
|
||||
// Relative-PDSIZE points size their glyph from the current zoom so they
|
||||
// stay a roughly constant on-screen size; otherwise the header-driven path.
|
||||
let te = crate::entities::point::relative_truck(entity, document, world_per_pixel)
|
||||
.or_else(|| convert(entity, document));
|
||||
if let Some(te) = te {
|
||||
match te.object {
|
||||
// ── Text / MText: pre-tessellated glyph strokes ───────────────
|
||||
//
|
||||
|
||||
@@ -7109,6 +7109,7 @@ fn tessellate_entity(
|
||||
1.5,
|
||||
world_offset,
|
||||
1.0,
|
||||
world_per_pixel,
|
||||
);
|
||||
let ab = entity_aabb(e, world_offset);
|
||||
for w in &mut wires {
|
||||
@@ -7423,6 +7424,7 @@ fn tessellate_entity(
|
||||
sub_line_weight_px,
|
||||
world_offset,
|
||||
anno_scale,
|
||||
world_per_pixel,
|
||||
);
|
||||
for w in &mut wires {
|
||||
w.name = h.value().to_string();
|
||||
@@ -7584,6 +7586,7 @@ fn tessellate_entity(
|
||||
line_weight_px,
|
||||
world_offset,
|
||||
anno_scale,
|
||||
world_per_pixel,
|
||||
);
|
||||
for b in &mut bases {
|
||||
b.aci = aci;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user