feat: annotation scale system (CANNOSCALE, PSLTSCALE, MultiLeader, Leader)
- Scale popup dropdown replaces cycle behavior; paper-space viewport scale editable when a viewport is active/selected - CANNOSCALE persisted via DXF $USERR1 (read on open, write on save) - PSLTSCALE: linetype dash lengths scaled by viewport anno_scale so dashes appear uniform physical size in paper space - MultiLeader: arrow size, dogleg length, and text height scale with anno_scale; leader line paths (world coords) stay fixed - Leader: arrow size and hookline length scale with anno_scale; also corrects world_offset subtraction that was previously deferred to the TruckObject::Lines path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
543c626feb
commit
0e5dec0948
9 changed files with 471 additions and 162 deletions
|
|
@ -39,6 +39,7 @@ pub(super) struct H7CAD {
|
||||||
vp_size: (f32, f32),
|
vp_size: (f32, f32),
|
||||||
snapper: Snapper,
|
snapper: Snapper,
|
||||||
snap_popup_open: bool,
|
snap_popup_open: bool,
|
||||||
|
scale_popup_open: bool,
|
||||||
/// Whether Tangent snap was enabled before a tangent-pick command started.
|
/// Whether Tangent snap was enabled before a tangent-pick command started.
|
||||||
pre_cmd_tangent: Option<bool>,
|
pre_cmd_tangent: Option<bool>,
|
||||||
/// Orthogonal drawing constraint (F8): constrains picks to 0°/90°/180°/270°.
|
/// Orthogonal drawing constraint (F8): constrains picks to 0°/90°/180°/270°.
|
||||||
|
|
@ -322,6 +323,14 @@ pub enum Message {
|
||||||
TogglePolar,
|
TogglePolar,
|
||||||
/// Set polar tracking angle increment (right-click POLAR button).
|
/// Set polar tracking angle increment (right-click POLAR button).
|
||||||
SetPolarAngle(f32),
|
SetPolarAngle(f32),
|
||||||
|
/// Set the model-space annotation scale (CANNOSCALE equivalent).
|
||||||
|
SetAnnotationScale(f32),
|
||||||
|
/// Set the active viewport's custom_scale (paper space).
|
||||||
|
SetViewportScale(f64),
|
||||||
|
/// Toggle the scale picker popup open/closed.
|
||||||
|
ToggleScalePopup,
|
||||||
|
/// Close the scale picker popup.
|
||||||
|
CloseScalePopup,
|
||||||
/// Toggle dynamic input overlay (F12).
|
/// Toggle dynamic input overlay (F12).
|
||||||
ToggleDynInput,
|
ToggleDynInput,
|
||||||
/// Toggle object snap tracking (F11).
|
/// Toggle object snap tracking (F11).
|
||||||
|
|
@ -606,6 +615,7 @@ impl H7CAD {
|
||||||
vp_size: (1280.0, 720.0),
|
vp_size: (1280.0, 720.0),
|
||||||
snapper: Snapper::default(),
|
snapper: Snapper::default(),
|
||||||
snap_popup_open: false,
|
snap_popup_open: false,
|
||||||
|
scale_popup_open: false,
|
||||||
pre_cmd_tangent: None,
|
pre_cmd_tangent: None,
|
||||||
ortho_mode: false,
|
ortho_mode: false,
|
||||||
polar_mode: false,
|
polar_mode: false,
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ impl H7CAD {
|
||||||
|
|
||||||
self.tabs[i].current_path = Some(path.clone());
|
self.tabs[i].current_path = Some(path.clone());
|
||||||
self.tabs[i].scene.document = doc;
|
self.tabs[i].scene.document = doc;
|
||||||
|
let stored = self.tabs[i].scene.document.header.user_real1;
|
||||||
|
self.tabs[i].scene.annotation_scale = if stored > 1e-9 { stored as f32 } else { 1.0 };
|
||||||
|
|
||||||
// Auto-resolve XREFs relative to the opened file's directory.
|
// Auto-resolve XREFs relative to the opened file's directory.
|
||||||
if let Some(base_dir) = path.parent() {
|
if let Some(base_dir) = path.parent() {
|
||||||
|
|
@ -407,6 +409,7 @@ impl H7CAD {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if let Some(path) = &self.tabs[i].current_path {
|
if let Some(path) = &self.tabs[i].current_path {
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
|
self.tabs[i].scene.document.header.user_real1 = self.tabs[i].scene.annotation_scale as f64;
|
||||||
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.command_line
|
self.command_line
|
||||||
|
|
@ -468,6 +471,7 @@ impl H7CAD {
|
||||||
let (_, version) = crate::io::parse_save_format(&self.save_dialog_format);
|
let (_, version) = crate::io::parse_save_format(&self.save_dialog_format);
|
||||||
let close = self.close_save_dialog_window();
|
let close = self.close_save_dialog_window();
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
self.tabs[i].scene.document.header.user_real1 = self.tabs[i].scene.annotation_scale as f64;
|
||||||
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
|
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.command_line.push_output(&format!("Saved: {}", path.display()));
|
self.command_line.push_output(&format!("Saved: {}", path.display()));
|
||||||
|
|
@ -2019,6 +2023,23 @@ impl H7CAD {
|
||||||
self.ortho_mode = false;
|
self.ortho_mode = false;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::SetAnnotationScale(scale) => {
|
||||||
|
self.scale_popup_open = false;
|
||||||
|
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
||||||
|
tab.scene.annotation_scale = scale;
|
||||||
|
tab.scene.bump_geometry();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::SetViewportScale(scale) => {
|
||||||
|
self.scale_popup_open = false;
|
||||||
|
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
||||||
|
tab.scene.set_viewport_scale(scale);
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ToggleScalePopup => { self.scale_popup_open ^= true; Task::none() }
|
||||||
|
Message::CloseScalePopup => { self.scale_popup_open = false; Task::none() }
|
||||||
Message::ToggleSnap(t) => { self.snapper.toggle(t); Task::none() }
|
Message::ToggleSnap(t) => { self.snapper.toggle(t); Task::none() }
|
||||||
Message::ToggleSnapPopup => { self.snap_popup_open ^= true; Task::none() }
|
Message::ToggleSnapPopup => { self.snap_popup_open ^= true; Task::none() }
|
||||||
Message::CloseSnapPopup => { self.snap_popup_open = false; Task::none() }
|
Message::CloseSnapPopup => { self.snap_popup_open = false; Task::none() }
|
||||||
|
|
|
||||||
|
|
@ -344,7 +344,12 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
col.push(center_stack)
|
col.push(center_stack)
|
||||||
.push(self.command_line.view())
|
.push(self.command_line.view())
|
||||||
.push(self.status_bar.view(
|
.push({
|
||||||
|
let is_model = tab.scene.current_layout == "Model";
|
||||||
|
let scale_pill_enabled = is_model
|
||||||
|
|| tab.scene.active_viewport.is_some()
|
||||||
|
|| tab.scene.has_selected_viewport();
|
||||||
|
self.status_bar.view(
|
||||||
&self.snapper,
|
&self.snapper,
|
||||||
self.snap_popup_open,
|
self.snap_popup_open,
|
||||||
self.ortho_mode,
|
self.ortho_mode,
|
||||||
|
|
@ -360,7 +365,11 @@ impl H7CAD {
|
||||||
tab.scene.viewport_count(),
|
tab.scene.viewport_count(),
|
||||||
tab.scene.active_viewport.is_some(),
|
tab.scene.active_viewport.is_some(),
|
||||||
self.show_layout_tabs,
|
self.show_layout_tabs,
|
||||||
))
|
tab.scene.annotation_scale,
|
||||||
|
self.scale_popup_open,
|
||||||
|
scale_pill_enabled,
|
||||||
|
)
|
||||||
|
})
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
})
|
})
|
||||||
|
|
@ -377,6 +386,17 @@ impl H7CAD {
|
||||||
iced::widget::Space::new().width(0).height(0).into()
|
iced::widget::Space::new().width(0).height(0).into()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let scale_layer: Element<'_, Message> = if self.scale_popup_open {
|
||||||
|
let is_model = tab.scene.current_layout == "Model";
|
||||||
|
crate::ui::scale_popup::scale_popup_overlay(
|
||||||
|
is_model,
|
||||||
|
tab.scene.annotation_scale,
|
||||||
|
tab.scene.first_viewport_scale(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
iced::widget::Space::new().width(0).height(0).into()
|
||||||
|
};
|
||||||
|
|
||||||
let dropdown_layer: Element<'_, Message> = self
|
let dropdown_layer: Element<'_, Message> = self
|
||||||
.ribbon
|
.ribbon
|
||||||
.dropdown_overlay(
|
.dropdown_overlay(
|
||||||
|
|
@ -406,7 +426,7 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, viewport_ctx_layer].into()
|
stack![main_ui, self.app_menu.view(), snap_layer, scale_layer, dropdown_layer, layout_ctx_layer, viewport_ctx_layer].into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscription(&self) -> Subscription<Message> {
|
pub fn subscription(&self) -> Subscription<Message> {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use crate::scene::wire_model::{SnapHint, TangentGeom};
|
||||||
// ── TruckConvertible ────────────────────────────────────────────────────────
|
// ── TruckConvertible ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Catmull-Rom spline tessellation through `ctrl` points, `segs_per_span` segments each.
|
/// Catmull-Rom spline tessellation through `ctrl` points, `segs_per_span` segments each.
|
||||||
fn catmull_rom_pts(ctrl: &[[f32; 3]], segs_per_span: u32) -> Vec<[f32; 3]> {
|
pub(crate) fn catmull_rom_pts(ctrl: &[[f32; 3]], segs_per_span: u32) -> Vec<[f32; 3]> {
|
||||||
let n = ctrl.len();
|
let n = ctrl.len();
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for i in 0..n.saturating_sub(1) {
|
for i in 0..n.saturating_sub(1) {
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,10 @@ pub struct Scene {
|
||||||
/// EXTMIN/EXTMAX (10× safety margin). Used by fit_all() to ignore garbage
|
/// EXTMIN/EXTMAX (10× safety margin). Used by fit_all() to ignore garbage
|
||||||
/// entity coordinates (origin-stuck entities, bad Ray/XLine direction vectors).
|
/// entity coordinates (origin-stuck entities, bad Ray/XLine direction vectors).
|
||||||
pub local_extent_max: f32,
|
pub local_extent_max: f32,
|
||||||
|
/// Current annotation scale (CANNOSCALE equivalent).
|
||||||
|
/// Multiplier applied to Text/MText/Dimension sizes during tessellation.
|
||||||
|
/// 1.0 = no scaling. 50.0 = "1:50" drawing scale.
|
||||||
|
pub annotation_scale: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
|
|
@ -162,6 +166,7 @@ impl Scene {
|
||||||
paper_bg_color: [1.0, 1.0, 1.0, 1.0],
|
paper_bg_color: [1.0, 1.0, 1.0, 1.0],
|
||||||
world_offset: [0.0; 3],
|
world_offset: [0.0; 3],
|
||||||
local_extent_max: 1e9,
|
local_extent_max: 1e9,
|
||||||
|
annotation_scale: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,6 +444,57 @@ impl Scene {
|
||||||
}).count()
|
}).count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if any currently selected entity is a Viewport.
|
||||||
|
/// Used to enable the scale picker when a viewport is selected in paper space.
|
||||||
|
pub fn has_selected_viewport(&self) -> bool {
|
||||||
|
self.selected.iter().any(|&h| {
|
||||||
|
matches!(self.document.get_entity(h), Some(EntityType::Viewport(_)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// First content viewport handle in the current layout, used as fallback target
|
||||||
|
/// when no viewport is active or explicitly selected.
|
||||||
|
fn first_viewport_handle(&self) -> Option<Handle> {
|
||||||
|
if self.current_layout == "Model" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let layout_block = self.current_layout_block_handle();
|
||||||
|
if layout_block.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.document.entities().find_map(|e| {
|
||||||
|
if let EntityType::Viewport(vp) = e {
|
||||||
|
if Self::is_content_viewport(vp) && vp.common.owner_handle == layout_block {
|
||||||
|
return Some(vp.common.handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the scale of the active/selected viewport.
|
||||||
|
/// Priority: active_viewport → first selected viewport → first viewport in layout.
|
||||||
|
pub fn set_viewport_scale(&mut self, scale: f64) {
|
||||||
|
let target = self.active_viewport
|
||||||
|
.or_else(|| {
|
||||||
|
self.selected.iter().copied().find(|&h| {
|
||||||
|
matches!(self.document.get_entity(h), Some(EntityType::Viewport(_)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| self.first_viewport_handle());
|
||||||
|
|
||||||
|
if let Some(handle) = target {
|
||||||
|
if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(handle) {
|
||||||
|
if !vp.status.locked && scale > 1e-9 {
|
||||||
|
vp.custom_scale = scale;
|
||||||
|
vp.view_height = vp.height / scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.viewport_wire_cache.borrow_mut().remove(&handle);
|
||||||
|
self.bump_geometry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sorted list of layout names: "Model" first, then paper layouts by tab order.
|
/// Sorted list of layout names: "Model" first, then paper layouts by tab order.
|
||||||
pub fn layout_names(&self) -> Vec<String> {
|
pub fn layout_names(&self) -> Vec<String> {
|
||||||
let mut names = vec!["Model".to_string()];
|
let mut names = vec!["Model".to_string()];
|
||||||
|
|
@ -672,9 +728,10 @@ impl Scene {
|
||||||
// coordinates, so world_offset must not be subtracted from them.
|
// coordinates, so world_offset must not be subtracted from them.
|
||||||
let woff = if self.current_layout == "Model" { self.world_offset } else { [0.0; 3] };
|
let woff = if self.current_layout == "Model" { self.world_offset } else { [0.0; 3] };
|
||||||
let bg = if self.current_layout == "Model" { self.bg_color } else { self.paper_bg_color };
|
let bg = if self.current_layout == "Model" { self.bg_color } else { self.paper_bg_color };
|
||||||
|
let anno = if self.current_layout == "Model" { self.annotation_scale } else { 1.0 };
|
||||||
let mut wires: Vec<WireModel> = visible
|
let mut wires: Vec<WireModel> = visible
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.flat_map(|e| tessellate_entity(doc, sel, avp, woff, bg, e))
|
.flat_map(|e| tessellate_entity(doc, sel, avp, woff, bg, anno, e))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Apply draw order via the cached index (O(1) block lookup).
|
// Apply draw order via the cached index (O(1) block lookup).
|
||||||
|
|
@ -733,7 +790,8 @@ impl Scene {
|
||||||
/// Full tessellation pipeline for one entity.
|
/// Full tessellation pipeline for one entity.
|
||||||
fn tessellate_one(&self, e: &EntityType) -> Vec<WireModel> {
|
fn tessellate_one(&self, e: &EntityType) -> Vec<WireModel> {
|
||||||
let bg = if self.current_layout == "Model" { self.bg_color } else { self.paper_bg_color };
|
let bg = if self.current_layout == "Model" { self.bg_color } else { self.paper_bg_color };
|
||||||
tessellate_entity(&self.document, &self.selected, self.active_viewport, self.world_offset, bg, e)
|
let anno = if self.current_layout == "Model" { self.annotation_scale } else { 1.0 };
|
||||||
|
tessellate_entity(&self.document, &self.selected, self.active_viewport, self.world_offset, bg, anno, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_space_block_handle(&self) -> Handle {
|
fn model_space_block_handle(&self) -> Handle {
|
||||||
|
|
@ -2941,9 +2999,20 @@ impl Scene {
|
||||||
fn model_wires_for_viewport(&self, vp_handle: Handle) -> Vec<WireModel> {
|
fn model_wires_for_viewport(&self, vp_handle: Handle) -> Vec<WireModel> {
|
||||||
use std::collections::HashSet as HSet;
|
use std::collections::HashSet as HSet;
|
||||||
|
|
||||||
let frozen: HSet<Handle> = match self.document.get_entity(vp_handle) {
|
let (frozen, vp_anno_scale) = match self.document.get_entity(vp_handle) {
|
||||||
Some(EntityType::Viewport(vp)) => vp.frozen_layers.iter().cloned().collect(),
|
Some(EntityType::Viewport(vp)) => {
|
||||||
_ => HSet::new(),
|
let f: HSet<Handle> = vp.frozen_layers.iter().cloned().collect();
|
||||||
|
let vp_scale = if vp.custom_scale.abs() > 1e-9 {
|
||||||
|
vp.custom_scale
|
||||||
|
} else if vp.view_height.abs() > 1e-9 {
|
||||||
|
vp.height / vp.view_height
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let anno = if vp_scale > 1e-9 { (1.0 / vp_scale) as f32 } else { 1.0_f32 };
|
||||||
|
(f, anno)
|
||||||
|
}
|
||||||
|
_ => (HSet::new(), 1.0_f32),
|
||||||
};
|
};
|
||||||
|
|
||||||
let model_block = self.model_space_block_handle();
|
let model_block = self.model_space_block_handle();
|
||||||
|
|
@ -2982,6 +3051,7 @@ impl Scene {
|
||||||
self.active_viewport,
|
self.active_viewport,
|
||||||
self.world_offset,
|
self.world_offset,
|
||||||
self.bg_color,
|
self.bg_color,
|
||||||
|
vp_anno_scale,
|
||||||
e,
|
e,
|
||||||
))
|
))
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -3136,6 +3206,7 @@ fn tessellate_entity(
|
||||||
active_viewport: Option<Handle>,
|
active_viewport: Option<Handle>,
|
||||||
world_offset: [f64; 3],
|
world_offset: [f64; 3],
|
||||||
bg_color: [f32; 4],
|
bg_color: [f32; 4],
|
||||||
|
anno_scale: f32,
|
||||||
e: &EntityType,
|
e: &EntityType,
|
||||||
) -> Vec<WireModel> {
|
) -> Vec<WireModel> {
|
||||||
let h = e.common().handle;
|
let h = e.common().handle;
|
||||||
|
|
@ -3164,7 +3235,7 @@ fn tessellate_entity(
|
||||||
(0.0_f32, [0.0f32; 8])
|
(0.0_f32, [0.0f32; 8])
|
||||||
};
|
};
|
||||||
let mut wire = tessellate::tessellate(
|
let mut wire = tessellate::tessellate(
|
||||||
document, h, e, sel, color, pattern_length, pattern, 1.5, world_offset,
|
document, h, e, sel, color, pattern_length, pattern, 1.5, world_offset, 1.0,
|
||||||
);
|
);
|
||||||
wire.aabb = entity_aabb(e, world_offset);
|
wire.aabb = entity_aabb(e, world_offset);
|
||||||
return vec![wire];
|
return vec![wire];
|
||||||
|
|
@ -3175,11 +3246,15 @@ fn tessellate_entity(
|
||||||
let entity_color = render::adapt_to_bg(entity_color, bg_color);
|
let entity_color = render::adapt_to_bg(entity_color, bg_color);
|
||||||
let lt_scale = document.header.linetype_scale as f32 * e.common().linetype_scale as f32;
|
let lt_scale = document.header.linetype_scale as f32 * e.common().linetype_scale as f32;
|
||||||
let lt_name = render::linetype_name_for(document, e);
|
let lt_name = render::linetype_name_for(document, e);
|
||||||
|
// PSLTSCALE: scale linetype dashes by viewport anno_scale so they appear uniform in paper space.
|
||||||
|
let pslt_factor = if document.header.paper_space_linetype_scaling { anno_scale } else { 1.0 };
|
||||||
|
let pattern_length = pattern_length * pslt_factor;
|
||||||
|
let pattern = pattern.map(|v| v * pslt_factor);
|
||||||
|
|
||||||
if let EntityType::Dimension(dim) = e {
|
if let EntityType::Dimension(dim) = e {
|
||||||
let aabb = entity_aabb(e, world_offset);
|
let aabb = entity_aabb(e, world_offset);
|
||||||
let mut wires = tessellate::tessellate_dimension(
|
let mut wires = tessellate::tessellate_dimension(
|
||||||
document, h, dim, sel, entity_color, line_weight_px, world_offset,
|
document, h, dim, sel, entity_color, line_weight_px, world_offset, anno_scale,
|
||||||
);
|
);
|
||||||
for w in &mut wires {
|
for w in &mut wires {
|
||||||
w.aci = aci;
|
w.aci = aci;
|
||||||
|
|
@ -3208,6 +3283,8 @@ fn tessellate_entity(
|
||||||
);
|
);
|
||||||
let sub_color = render::adapt_to_bg(sub_color, bg_color);
|
let sub_color = render::adapt_to_bg(sub_color, bg_color);
|
||||||
let sub_aabb = entity_aabb(&sub, world_offset);
|
let sub_aabb = entity_aabb(&sub, world_offset);
|
||||||
|
let sub_pattern_length = sub_pattern_length * pslt_factor;
|
||||||
|
let sub_pattern = sub_pattern.map(|v| v * pslt_factor);
|
||||||
let mut wire = tessellate::tessellate(
|
let mut wire = tessellate::tessellate(
|
||||||
document,
|
document,
|
||||||
h,
|
h,
|
||||||
|
|
@ -3218,6 +3295,7 @@ fn tessellate_entity(
|
||||||
sub_pattern,
|
sub_pattern,
|
||||||
sub_line_weight_px,
|
sub_line_weight_px,
|
||||||
world_offset,
|
world_offset,
|
||||||
|
anno_scale,
|
||||||
);
|
);
|
||||||
wire.name = h.value().to_string();
|
wire.name = h.value().to_string();
|
||||||
wire.aci = sub_aci;
|
wire.aci = sub_aci;
|
||||||
|
|
@ -3253,7 +3331,7 @@ fn tessellate_entity(
|
||||||
|
|
||||||
let aabb = entity_aabb(e, world_offset);
|
let aabb = entity_aabb(e, world_offset);
|
||||||
let mut base = tessellate::tessellate(
|
let mut base = tessellate::tessellate(
|
||||||
document, h, e, sel, entity_color, pattern_length, pattern, line_weight_px, world_offset,
|
document, h, e, sel, entity_color, pattern_length, pattern, line_weight_px, world_offset, anno_scale,
|
||||||
);
|
);
|
||||||
base.aci = aci;
|
base.aci = aci;
|
||||||
base.aabb = aabb;
|
base.aabb = aabb;
|
||||||
|
|
@ -3263,7 +3341,7 @@ fn tessellate_entity(
|
||||||
&base.name,
|
&base.name,
|
||||||
&base.points,
|
&base.points,
|
||||||
clt,
|
clt,
|
||||||
lt_scale.max(1e-4),
|
(lt_scale * pslt_factor).max(1e-4),
|
||||||
entity_color,
|
entity_color,
|
||||||
sel,
|
sel,
|
||||||
base.line_weight_px,
|
base.line_weight_px,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
// Entities not handled by acad_to_truck (Viewport, Hatch, …) are tessellated
|
// Entities not handled by acad_to_truck (Viewport, Hatch, …) are tessellated
|
||||||
// by the legacy geometry() path so nothing regresses.
|
// by the legacy geometry() path so nothing regresses.
|
||||||
|
|
||||||
use acadrust::entities::{Dimension, Leader, MultiLeader, MultiLeaderPathType, Text};
|
use acadrust::entities::{Dimension, Leader, LeaderContentType, MultiLeader, MultiLeaderPathType, Text};
|
||||||
|
use crate::entities::multileader::catmull_rom_pts;
|
||||||
use acadrust::types::{Color as AcadColor, Vector3};
|
use acadrust::types::{Color as AcadColor, Vector3};
|
||||||
use acadrust::{CadDocument, EntityType, Handle};
|
use acadrust::{CadDocument, EntityType, Handle};
|
||||||
use glam::Vec3;
|
use glam::Vec3;
|
||||||
|
|
@ -52,6 +53,7 @@ pub fn tessellate(
|
||||||
pattern: [f32; 8],
|
pattern: [f32; 8],
|
||||||
line_weight_px: f32,
|
line_weight_px: f32,
|
||||||
world_offset: [f64; 3],
|
world_offset: [f64; 3],
|
||||||
|
anno_scale: f32,
|
||||||
) -> WireModel {
|
) -> WireModel {
|
||||||
let color = if selected {
|
let color = if selected {
|
||||||
WireModel::SELECTED
|
WireModel::SELECTED
|
||||||
|
|
@ -60,6 +62,18 @@ pub fn tessellate(
|
||||||
};
|
};
|
||||||
let name = handle.value().to_string();
|
let name = handle.value().to_string();
|
||||||
|
|
||||||
|
// MultiLeader/Leader need anno_scale for arrow/dogleg/text — handle before generic path.
|
||||||
|
if let EntityType::MultiLeader(ml) = entity {
|
||||||
|
return tessellate_multileader_single(
|
||||||
|
document, handle, ml, selected, entity_color, line_weight_px, world_offset, anno_scale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let EntityType::Leader(leader) = entity {
|
||||||
|
return tessellate_leader_single(
|
||||||
|
handle, leader, selected, entity_color, line_weight_px, world_offset, anno_scale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Try the truck path first ───────────────────────────────────────────
|
// ── Try the truck path first ───────────────────────────────────────────
|
||||||
if let Some(te) = convert(entity, document) {
|
if let Some(te) = convert(entity, document) {
|
||||||
match te.object {
|
match te.object {
|
||||||
|
|
@ -73,9 +87,17 @@ pub fn tessellate(
|
||||||
|
|
||||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
|
// Annotation scale: scale glyph strokes relative to the text
|
||||||
|
// insertion point (first group's origin) so multi-line MText
|
||||||
|
// lines spread apart correctly as well as growing in size.
|
||||||
|
let ref_origin = stroke_groups.first().map(|g| g.origin).unwrap_or([0.0, 0.0]);
|
||||||
|
let ref_lx = (ref_origin[0] - ox) as f32;
|
||||||
|
let ref_ly = (ref_origin[1] - oy) as f32;
|
||||||
for group in &stroke_groups {
|
for group in &stroke_groups {
|
||||||
let lx = (group.origin[0] - ox) as f32;
|
let lx = (group.origin[0] - ox) as f32;
|
||||||
let ly = (group.origin[1] - oy) as f32;
|
let ly = (group.origin[1] - oy) as f32;
|
||||||
|
let slx = (lx - ref_lx) * anno_scale + ref_lx;
|
||||||
|
let sly = (ly - ref_ly) * anno_scale + ref_ly;
|
||||||
for stroke in &group.strokes {
|
for stroke in &group.strokes {
|
||||||
if stroke.len() < 2 {
|
if stroke.len() < 2 {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -85,7 +107,7 @@ pub fn tessellate(
|
||||||
}
|
}
|
||||||
first = false;
|
first = false;
|
||||||
for &[x, y] in stroke {
|
for &[x, y] in stroke {
|
||||||
points.push([x + lx, y + ly, elev]);
|
points.push([x * anno_scale + slx, y * anno_scale + sly, elev]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -309,6 +331,7 @@ pub fn tessellate_dimension(
|
||||||
entity_color: [f32; 4],
|
entity_color: [f32; 4],
|
||||||
line_weight_px: f32,
|
line_weight_px: f32,
|
||||||
world_offset: [f64; 3],
|
world_offset: [f64; 3],
|
||||||
|
anno_scale: f32,
|
||||||
) -> Vec<WireModel> {
|
) -> Vec<WireModel> {
|
||||||
let color = if selected {
|
let color = if selected {
|
||||||
WireModel::SELECTED
|
WireModel::SELECTED
|
||||||
|
|
@ -323,7 +346,7 @@ pub fn tessellate_dimension(
|
||||||
.find(|s| s.name.eq_ignore_ascii_case(style_name)
|
.find(|s| s.name.eq_ignore_ascii_case(style_name)
|
||||||
|| (style_name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard")))
|
|| (style_name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard")))
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
let scale = if s.dimscale > 1e-6 { s.dimscale } else { 1.0 };
|
let scale = (if s.dimscale > 1e-6 { s.dimscale } else { 1.0 }) * anno_scale as f64;
|
||||||
(
|
(
|
||||||
((s.dimasz * scale) as f32).max(0.001),
|
((s.dimasz * scale) as f32).max(0.001),
|
||||||
(s.dimexo * scale) as f32,
|
(s.dimexo * scale) as f32,
|
||||||
|
|
@ -368,6 +391,7 @@ pub fn tessellate_dimension(
|
||||||
[0.0; 8],
|
[0.0; 8],
|
||||||
line_weight_px,
|
line_weight_px,
|
||||||
world_offset,
|
world_offset,
|
||||||
|
anno_scale,
|
||||||
);
|
);
|
||||||
wire.name = name;
|
wire.name = name;
|
||||||
wires.push(wire);
|
wires.push(wire);
|
||||||
|
|
@ -376,91 +400,81 @@ pub fn tessellate_dimension(
|
||||||
wires
|
wires
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kept for backwards compatibility — geometry now lives in entities/leader.rs.
|
fn tessellate_leader_single(
|
||||||
#[allow(dead_code)]
|
|
||||||
fn tessellate_leader(
|
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
leader: &Leader,
|
leader: &Leader,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
entity_color: [f32; 4],
|
entity_color: [f32; 4],
|
||||||
line_weight_px: f32,
|
line_weight_px: f32,
|
||||||
) -> Vec<WireModel> {
|
world_offset: [f64; 3],
|
||||||
|
anno_scale: f32,
|
||||||
|
) -> WireModel {
|
||||||
let color = if selected { WireModel::SELECTED } else { entity_color };
|
let color = if selected { WireModel::SELECTED } else { entity_color };
|
||||||
let name = handle.value().to_string();
|
let name = handle.value().to_string();
|
||||||
|
let [ox, oy, oz] = world_offset;
|
||||||
|
let p3 = |v: &Vector3| -> [f32; 3] {
|
||||||
|
[(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]
|
||||||
|
};
|
||||||
|
let nan = [f32::NAN; 3];
|
||||||
|
|
||||||
let verts = &leader.vertices;
|
let verts = &leader.vertices;
|
||||||
|
|
||||||
if verts.len() < 2 {
|
if verts.len() < 2 {
|
||||||
return vec![WireModel {
|
return WireModel {
|
||||||
name,
|
name,
|
||||||
points: vec![],
|
points: vec![],
|
||||||
color,
|
color,
|
||||||
selected,
|
selected,
|
||||||
|
aci: 0,
|
||||||
pattern_length: 0.0,
|
pattern_length: 0.0,
|
||||||
pattern: [0.0; 8],
|
pattern: [0.0; 8],
|
||||||
line_weight_px,
|
line_weight_px,
|
||||||
snap_pts: vec![],
|
snap_pts: vec![],
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: vec![],
|
||||||
aci: 0,
|
|
||||||
key_vertices: vec![],
|
key_vertices: vec![],
|
||||||
aabb: WireModel::UNBOUNDED_AABB,
|
aabb: WireModel::UNBOUNDED_AABB,
|
||||||
plinegen: true,
|
plinegen: true,
|
||||||
vp_scissor: None,
|
vp_scissor: None,
|
||||||
}];
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let to_f32 = |v: &Vector3| -> [f32; 3] { [v.x as f32, v.y as f32, v.z as f32] };
|
let mut points: Vec<[f32; 3]> = verts.iter().map(|v| p3(v)).collect();
|
||||||
let nan = [f32::NAN; 3];
|
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||||||
|
let key_vertices: Vec<[f32; 3]> = verts.iter().map(|v| p3(v)).collect();
|
||||||
|
|
||||||
// Main path
|
for i in 0..verts.len().saturating_sub(1) {
|
||||||
let mut points: Vec<[f32; 3]> = verts.iter().map(to_f32).collect();
|
tangents.push(TangentGeom::Line { p1: p3(&verts[i]), p2: p3(&verts[i + 1]) });
|
||||||
|
}
|
||||||
|
|
||||||
// Arrowhead at vertex[0] — only when arrow_enabled
|
|
||||||
if leader.arrow_enabled {
|
if leader.arrow_enabled {
|
||||||
let tip = verts[0];
|
let tip = &verts[0];
|
||||||
let next = verts[1];
|
let next = &verts[1];
|
||||||
let dx = (next.x - tip.x) as f32;
|
let dx = (next.x - tip.x) as f32;
|
||||||
let dy = (next.y - tip.y) as f32;
|
let dy = (next.y - tip.y) as f32;
|
||||||
let len = (dx * dx + dy * dy).sqrt().max(1e-9);
|
let len = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||||
let (dx, dy) = (dx / len, dy / len);
|
let (dx, dy) = (dx / len, dy / len);
|
||||||
let arrow_size = (leader.text_height as f32).max(1.0) * 0.8;
|
let sz = (leader.text_height as f32).max(1.0) * 0.8 * anno_scale;
|
||||||
let angle = std::f32::consts::PI / 6.0;
|
let a = std::f32::consts::PI / 6.0;
|
||||||
let (s, c) = angle.sin_cos();
|
let (s, c) = a.sin_cos();
|
||||||
let wing1 = [
|
let tip_f = p3(tip);
|
||||||
tip.x as f32 + (dx * c - dy * s) * arrow_size,
|
|
||||||
tip.y as f32 + (dx * s + dy * c) * arrow_size,
|
|
||||||
tip.z as f32,
|
|
||||||
];
|
|
||||||
let wing2 = [
|
|
||||||
tip.x as f32 + (dx * c + dy * s) * arrow_size,
|
|
||||||
tip.y as f32 + (-dx * s + dy * c) * arrow_size,
|
|
||||||
tip.z as f32,
|
|
||||||
];
|
|
||||||
points.push(nan);
|
points.push(nan);
|
||||||
points.push(wing1);
|
points.push([tip_f[0] + (dx*c - dy*s)*sz, tip_f[1] + (dx*s + dy*c)*sz, tip_f[2]]);
|
||||||
points.push(to_f32(&tip));
|
points.push(tip_f);
|
||||||
points.push(wing2);
|
points.push([tip_f[0] + (dx*c + dy*s)*sz, tip_f[1] + (-dx*s + dy*c)*sz, tip_f[2]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Landing line at last vertex
|
|
||||||
if leader.hookline_enabled {
|
if leader.hookline_enabled {
|
||||||
let last = *verts.last().unwrap();
|
let last = verts.last().unwrap();
|
||||||
let prev = verts[verts.len() - 2];
|
let prev = &verts[verts.len() - 2];
|
||||||
let last_dir_x = (last.x - prev.x) as f32;
|
let sign = if (last.x - prev.x) >= 0.0 { 1.0_f32 } else { -1.0_f32 };
|
||||||
let sign = if last_dir_x >= 0.0 { 1.0_f32 } else { -1.0_f32 };
|
let land_len = leader.text_height as f32 * 1.5 * anno_scale;
|
||||||
let landing_len = leader.text_height as f32 * 1.5;
|
let last_f = p3(last);
|
||||||
let landing_pt = [
|
|
||||||
last.x as f32 + sign * landing_len,
|
|
||||||
last.y as f32,
|
|
||||||
last.z as f32,
|
|
||||||
];
|
|
||||||
points.push(nan);
|
points.push(nan);
|
||||||
points.push(to_f32(&last));
|
points.push(last_f);
|
||||||
points.push(landing_pt);
|
points.push([last_f[0] + sign * land_len, last_f[1], last_f[2]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let key_vertices: Vec<[f32; 3]> = verts.iter().map(to_f32).collect();
|
WireModel {
|
||||||
|
|
||||||
vec![WireModel {
|
|
||||||
name,
|
name,
|
||||||
points,
|
points,
|
||||||
color,
|
color,
|
||||||
|
|
@ -470,17 +484,15 @@ fn tessellate_leader(
|
||||||
pattern: [0.0; 8],
|
pattern: [0.0; 8],
|
||||||
line_weight_px,
|
line_weight_px,
|
||||||
snap_pts: vec![],
|
snap_pts: vec![],
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: tangents,
|
||||||
key_vertices,
|
key_vertices,
|
||||||
aabb: WireModel::UNBOUNDED_AABB,
|
aabb: WireModel::UNBOUNDED_AABB,
|
||||||
plinegen: true,
|
plinegen: true,
|
||||||
vp_scissor: None,
|
vp_scissor: None,
|
||||||
}]
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kept for backwards compatibility — geometry now lives in entities/multileader.rs.
|
fn tessellate_multileader_single(
|
||||||
#[allow(dead_code)]
|
|
||||||
fn tessellate_multileader(
|
|
||||||
document: &CadDocument,
|
document: &CadDocument,
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
ml: &MultiLeader,
|
ml: &MultiLeader,
|
||||||
|
|
@ -488,128 +500,125 @@ fn tessellate_multileader(
|
||||||
entity_color: [f32; 4],
|
entity_color: [f32; 4],
|
||||||
line_weight_px: f32,
|
line_weight_px: f32,
|
||||||
world_offset: [f64; 3],
|
world_offset: [f64; 3],
|
||||||
) -> Vec<WireModel> {
|
anno_scale: f32,
|
||||||
|
) -> WireModel {
|
||||||
let color = if selected { WireModel::SELECTED } else { entity_color };
|
let color = if selected { WireModel::SELECTED } else { entity_color };
|
||||||
let name = handle.value().to_string();
|
let name = handle.value().to_string();
|
||||||
let nan = [f32::NAN; 3];
|
let nan = [f32::NAN; 3];
|
||||||
|
|
||||||
let [ox, oy, oz] = world_offset;
|
let [ox, oy, oz] = world_offset;
|
||||||
let to_f32 = |v: &acadrust::types::Vector3| -> [f32; 3] {
|
let p3 = |v: &acadrust::types::Vector3| -> [f32; 3] {
|
||||||
[(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]
|
[(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]
|
||||||
};
|
};
|
||||||
|
|
||||||
let arrow_size = ml.arrowhead_size as f32;
|
let arrow_size = ml.arrowhead_size as f32 * anno_scale;
|
||||||
let draw_arrow = arrow_size > 0.0;
|
let draw_arrow = arrow_size > 0.0;
|
||||||
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
|
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
|
||||||
|
|
||||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||||
let mut key_verts: Vec<[f32; 3]> = Vec::new();
|
let mut key_verts: Vec<[f32; 3]> = Vec::new();
|
||||||
let mut first_segment = true;
|
let mut snap_pts: Vec<(Vec3, SnapHint)> = Vec::new();
|
||||||
|
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||||||
|
let mut first = true;
|
||||||
|
|
||||||
for root in &ml.context.leader_roots {
|
for root in &ml.context.leader_roots {
|
||||||
let cp = &root.connection_point;
|
let cp = &root.connection_point;
|
||||||
let cp_f = to_f32(cp);
|
let cp_f = p3(cp);
|
||||||
|
snap_pts.push((Vec3::from(cp_f), SnapHint::Node));
|
||||||
|
|
||||||
for line in &root.lines {
|
for line in &root.lines {
|
||||||
if line.points.is_empty() { continue; }
|
if line.points.is_empty() { continue; }
|
||||||
|
|
||||||
// Leader line segments (hidden when path_type = Invisible)
|
|
||||||
if !invisible {
|
if !invisible {
|
||||||
if !first_segment { points.push(nan); }
|
if !first { points.push(nan); }
|
||||||
first_segment = false;
|
first = false;
|
||||||
|
|
||||||
for p in &line.points {
|
let mut ctrl: Vec<[f32; 3]> = line.points.iter().map(|p| p3(p)).collect();
|
||||||
points.push(to_f32(p));
|
let last_f = *ctrl.last().unwrap_or(&cp_f);
|
||||||
key_verts.push(to_f32(p));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closing segment: last bend point → connection_point
|
|
||||||
let last = line.points.last().unwrap();
|
|
||||||
let last_f = to_f32(last);
|
|
||||||
let dist = ((last_f[0]-cp_f[0]).powi(2) + (last_f[1]-cp_f[1]).powi(2)).sqrt();
|
let dist = ((last_f[0]-cp_f[0]).powi(2) + (last_f[1]-cp_f[1]).powi(2)).sqrt();
|
||||||
if dist > 1e-9 {
|
if dist > 1e-9 { ctrl.push(cp_f); }
|
||||||
points.push(cp_f);
|
for &c in &ctrl {
|
||||||
key_verts.push(cp_f);
|
key_verts.push(c);
|
||||||
|
snap_pts.push((Vec3::from(c), SnapHint::Node));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ml.path_type == MultiLeaderPathType::Spline && ctrl.len() >= 2 {
|
||||||
|
for pt in catmull_rom_pts(&ctrl, 8) { points.push(pt); }
|
||||||
|
} else {
|
||||||
|
for &c in &ctrl { points.push(c); }
|
||||||
|
}
|
||||||
|
for i in 0..ctrl.len().saturating_sub(1) {
|
||||||
|
tangents.push(TangentGeom::Line { p1: ctrl[i], p2: ctrl[i + 1] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arrowhead — only when arrowhead_size > 0
|
|
||||||
if draw_arrow {
|
if draw_arrow {
|
||||||
let tip = line.points[0];
|
let tip = &line.points[0];
|
||||||
let tip_f = to_f32(&tip);
|
let tip_f = p3(tip);
|
||||||
let next_dir = if line.points.len() >= 2 { line.points[1] } else { *cp };
|
let next = if line.points.len() >= 2 { line.points[1] } else { *cp };
|
||||||
let dx = (next_dir.x - tip.x) as f32;
|
let dx = (next.x - tip.x) as f32;
|
||||||
let dy = (next_dir.y - tip.y) as f32;
|
let dy = (next.y - tip.y) as f32;
|
||||||
let dlen = (dx * dx + dy * dy).sqrt().max(1e-9);
|
let dl = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||||
let (dx, dy) = (dx / dlen, dy / dlen);
|
let (dx, dy) = (dx / dl, dy / dl);
|
||||||
let angle = std::f32::consts::PI / 6.0;
|
let a = std::f32::consts::PI / 6.0;
|
||||||
let (s, c) = angle.sin_cos();
|
let (s, c) = a.sin_cos();
|
||||||
let w1 = [tip_f[0] + (dx*c - dy*s)*arrow_size,
|
|
||||||
tip_f[1] + (dx*s + dy*c)*arrow_size, tip_f[2]];
|
|
||||||
let w2 = [tip_f[0] + (dx*c + dy*s)*arrow_size,
|
|
||||||
tip_f[1] + (-dx*s + dy*c)*arrow_size, tip_f[2]];
|
|
||||||
points.push(nan);
|
points.push(nan);
|
||||||
points.push(w1);
|
points.push([tip_f[0] + (dx*c - dy*s)*arrow_size, tip_f[1] + (dx*s + dy*c)*arrow_size, tip_f[2]]);
|
||||||
points.push(tip_f);
|
points.push(tip_f);
|
||||||
points.push(w2);
|
points.push([tip_f[0] + (dx*c + dy*s)*arrow_size, tip_f[1] + (-dx*s + dy*c)*arrow_size, tip_f[2]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short landing shelf at connection_point — respects enable_landing and enable_dogleg
|
|
||||||
if ml.enable_landing && ml.enable_dogleg && ml.dogleg_length > 0.0 {
|
if ml.enable_landing && ml.enable_dogleg && ml.dogleg_length > 0.0 {
|
||||||
let dir = &root.direction;
|
let dir = &root.direction;
|
||||||
let dlen = (dir.x * dir.x + dir.y * dir.y).sqrt().max(1e-9);
|
let dl = (dir.x * dir.x + dir.y * dir.y).sqrt().max(1e-9);
|
||||||
let dl = ml.dogleg_length;
|
let d = ml.dogleg_length * anno_scale as f64;
|
||||||
let end = [
|
|
||||||
(cp.x + dir.x / dlen * dl - ox) as f32,
|
|
||||||
(cp.y + dir.y / dlen * dl - oy) as f32,
|
|
||||||
(cp.z - oz) as f32,
|
|
||||||
];
|
|
||||||
points.push(nan);
|
points.push(nan);
|
||||||
points.push(cp_f);
|
points.push(cp_f);
|
||||||
points.push(end);
|
points.push([(cp.x + dir.x/dl*d - ox) as f32, (cp.y + dir.y/dl*d - oy) as f32, cp_f[2]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut wires = vec![WireModel {
|
// Text strokes — scale height, keep insertion point fixed.
|
||||||
name: name.clone(),
|
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
||||||
|
let height = if ml.context.text_height > 0.0 { ml.context.text_height } else { ml.text_height };
|
||||||
|
let ins = &ml.context.text_location;
|
||||||
|
let z = (ins.z - oz) as f32;
|
||||||
|
snap_pts.push((Vec3::new((ins.x - ox) as f32, (ins.y - oy) as f32, z), SnapHint::Node));
|
||||||
|
let style = crate::entities::text_support::resolve_text_style("STANDARD", document);
|
||||||
|
let strokes = crate::scene::cxf::tessellate_text_ex(
|
||||||
|
[ins.x as f32, ins.y as f32],
|
||||||
|
(height as f32) * anno_scale,
|
||||||
|
0.0,
|
||||||
|
style.width_factor.max(0.01),
|
||||||
|
style.oblique_angle,
|
||||||
|
&style.font_name,
|
||||||
|
&ml.context.text_string,
|
||||||
|
);
|
||||||
|
for stroke in &strokes {
|
||||||
|
if stroke.len() < 2 { continue; }
|
||||||
|
points.push(nan);
|
||||||
|
for &[x, y] in stroke {
|
||||||
|
points.push([x - ox as f32, y - oy as f32, z]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WireModel {
|
||||||
|
name,
|
||||||
points,
|
points,
|
||||||
color,
|
color,
|
||||||
selected,
|
selected,
|
||||||
|
aci: 0,
|
||||||
pattern_length: 0.0,
|
pattern_length: 0.0,
|
||||||
pattern: [0.0; 8],
|
pattern: [0.0; 8],
|
||||||
line_weight_px,
|
line_weight_px,
|
||||||
snap_pts: vec![],
|
snap_pts,
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: tangents,
|
||||||
aci: 0,
|
|
||||||
key_vertices: key_verts,
|
key_vertices: key_verts,
|
||||||
aabb: WireModel::UNBOUNDED_AABB,
|
aabb: WireModel::UNBOUNDED_AABB,
|
||||||
plinegen: true,
|
plinegen: true,
|
||||||
vp_scissor: None,
|
vp_scissor: None,
|
||||||
}];
|
|
||||||
|
|
||||||
// Render text content as MText wire
|
|
||||||
if ml.content_type == acadrust::entities::LeaderContentType::MText
|
|
||||||
&& !ml.context.text_string.is_empty()
|
|
||||||
{
|
|
||||||
let mut mtext = acadrust::entities::MText::new();
|
|
||||||
mtext.value = ml.context.text_string.clone();
|
|
||||||
mtext.insertion_point = ml.context.text_location;
|
|
||||||
mtext.height = if ml.context.text_height > 0.0 {
|
|
||||||
ml.context.text_height
|
|
||||||
} else {
|
|
||||||
ml.text_height
|
|
||||||
};
|
|
||||||
mtext.common.layer = ml.common.layer.clone();
|
|
||||||
let mut w = tessellate(
|
|
||||||
document, handle, &EntityType::MText(mtext),
|
|
||||||
selected, entity_color, 0.0, [0.0; 8], line_weight_px, world_offset,
|
|
||||||
);
|
|
||||||
w.name = name;
|
|
||||||
wires.push(w);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wires
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tessellate a Solid3D entity into a MeshModel (truck Shell/Solid path).
|
/// Tessellate a Solid3D entity into a MeshModel (truck Shell/Solid path).
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ pub mod plotstyle;
|
||||||
pub mod properties;
|
pub mod properties;
|
||||||
pub mod ribbon;
|
pub mod ribbon;
|
||||||
pub mod shortcuts;
|
pub mod shortcuts;
|
||||||
|
pub mod scale_popup;
|
||||||
pub mod snap_popup;
|
pub mod snap_popup;
|
||||||
pub mod statusbar;
|
pub mod statusbar;
|
||||||
pub mod tablestyle;
|
pub mod tablestyle;
|
||||||
|
|
|
||||||
126
src/ui/scale_popup.rs
Normal file
126
src/ui/scale_popup.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
//! Scale picker dropdown — annotation scale (model space) or viewport scale (paper space).
|
||||||
|
//!
|
||||||
|
//! Rendered as a floating overlay above the status bar, same pattern as snap_popup.
|
||||||
|
|
||||||
|
use iced::widget::{button, column, container, mouse_area, row, text};
|
||||||
|
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||||
|
|
||||||
|
use crate::app::Message;
|
||||||
|
|
||||||
|
/// (label, annotation_scale_multiplier, viewport_scale_factor)
|
||||||
|
/// annotation_scale = multiplier for text/dim sizes (50.0 for "1:50")
|
||||||
|
/// vp_scale = custom_scale value on the Viewport entity (0.02 for "1:50")
|
||||||
|
const COMMON_SCALES: &[(&str, f32, f64)] = &[
|
||||||
|
("1:100", 100.0, 0.01),
|
||||||
|
("1:50", 50.0, 0.02),
|
||||||
|
("1:20", 20.0, 0.05),
|
||||||
|
("1:10", 10.0, 0.10),
|
||||||
|
("1:5", 5.0, 0.20),
|
||||||
|
("1:2", 2.0, 0.50),
|
||||||
|
("1:1", 1.0, 1.00),
|
||||||
|
("2:1", 0.5, 2.00),
|
||||||
|
("5:1", 0.2, 5.00),
|
||||||
|
("10:1", 0.1, 10.00),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn anno_scale_for_label(label: &str) -> f32 {
|
||||||
|
COMMON_SCALES
|
||||||
|
.iter()
|
||||||
|
.find(|&&(l, _, _)| l == label)
|
||||||
|
.map(|&(_, a, _)| a)
|
||||||
|
.unwrap_or(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full-screen overlay: transparent click-catcher + scale list panel pinned bottom-right.
|
||||||
|
///
|
||||||
|
/// - `is_model`: true = model space (dispatches SetAnnotationScale), false = paper space (SetViewportScale).
|
||||||
|
/// - `current_anno_scale`: current annotation_scale from Scene (used to highlight active row in model space).
|
||||||
|
/// - `viewport_scale`: current vp.custom_scale (used to highlight in paper space).
|
||||||
|
pub fn scale_popup_overlay(
|
||||||
|
is_model: bool,
|
||||||
|
current_anno_scale: f32,
|
||||||
|
viewport_scale: Option<f64>,
|
||||||
|
) -> Element<'static, Message> {
|
||||||
|
let rows: Vec<Element<'static, Message>> = COMMON_SCALES
|
||||||
|
.iter()
|
||||||
|
.map(|&(label, anno_scale, vp_scale)| {
|
||||||
|
let active = if is_model {
|
||||||
|
(current_anno_scale - anno_scale).abs() < 0.001 * current_anno_scale.max(0.001)
|
||||||
|
} else {
|
||||||
|
viewport_scale
|
||||||
|
.map(|vs| (vs - vp_scale).abs() < 0.001 * vp_scale.max(0.001))
|
||||||
|
.unwrap_or(false)
|
||||||
|
};
|
||||||
|
let msg = if is_model {
|
||||||
|
Message::SetAnnotationScale(anno_scale)
|
||||||
|
} else {
|
||||||
|
Message::SetViewportScale(vp_scale)
|
||||||
|
};
|
||||||
|
scale_row(label, active, msg)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let panel = container(column(rows))
|
||||||
|
.style(|_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(PANEL_BG)),
|
||||||
|
border: Border {
|
||||||
|
color: PANEL_BORDER,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 3.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.width(Length::Fixed(120.0));
|
||||||
|
|
||||||
|
let positioned = container(panel)
|
||||||
|
.align_right(Fill)
|
||||||
|
.align_bottom(Fill)
|
||||||
|
.padding(Padding {
|
||||||
|
bottom: 27.0,
|
||||||
|
right: 4.0,
|
||||||
|
top: 0.0,
|
||||||
|
left: 0.0,
|
||||||
|
})
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill);
|
||||||
|
|
||||||
|
mouse_area(positioned)
|
||||||
|
.on_press(Message::CloseScalePopup)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scale_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> {
|
||||||
|
let check = text(if active { "✓" } else { " " })
|
||||||
|
.size(11)
|
||||||
|
.color(if active { CHECK_COLOR } else { Color::TRANSPARENT })
|
||||||
|
.width(Length::Fixed(14.0));
|
||||||
|
|
||||||
|
let lbl = text(label)
|
||||||
|
.size(11)
|
||||||
|
.color(if active { LABEL_ON } else { LABEL_OFF });
|
||||||
|
|
||||||
|
let content = row![check, lbl].spacing(6).align_y(iced::Center);
|
||||||
|
|
||||||
|
button(content)
|
||||||
|
.on_press(msg)
|
||||||
|
.style(|_: &Theme, status| button::Style {
|
||||||
|
background: Some(Background::Color(match status {
|
||||||
|
button::Status::Hovered => ROW_HOVER,
|
||||||
|
_ => Color::TRANSPARENT,
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.width(Fill)
|
||||||
|
.padding([4, 10])
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Colours ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PANEL_BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
|
||||||
|
const PANEL_BORDER: Color = Color { r: 0.32, g: 0.32, b: 0.32, a: 1.0 };
|
||||||
|
const ROW_HOVER: Color = Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 };
|
||||||
|
const CHECK_COLOR: Color = Color { r: 0.35, g: 0.75, b: 1.00, a: 1.0 };
|
||||||
|
const LABEL_ON: Color = Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 };
|
||||||
|
const LABEL_OFF: Color = Color { r: 0.65, g: 0.65, b: 0.65, a: 1.0 };
|
||||||
|
|
@ -42,6 +42,12 @@ impl StatusBar {
|
||||||
in_mspace: bool,
|
in_mspace: bool,
|
||||||
// Whether the layout tabs (Model/Paper) are visible (LAYOUTTAB).
|
// Whether the layout tabs (Model/Paper) are visible (LAYOUTTAB).
|
||||||
show_layout_tabs: bool,
|
show_layout_tabs: bool,
|
||||||
|
// Current annotation scale for model space (1.0 = 1:1, 50.0 = 1:50, etc.).
|
||||||
|
annotation_scale: f32,
|
||||||
|
// True when the scale picker popup is open.
|
||||||
|
scale_popup_open: bool,
|
||||||
|
// True when the scale pill is interactive (always model space; paper space only when a viewport is active/selected).
|
||||||
|
scale_pill_enabled: bool,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let menu_btn = button(text("≡").size(14).color(ICON_COLOR))
|
let menu_btn = button(text("≡").size(14).color(ICON_COLOR))
|
||||||
.on_press(Message::Command("MENU".into()))
|
.on_press(Message::Command("MENU".into()))
|
||||||
|
|
@ -63,12 +69,24 @@ impl StatusBar {
|
||||||
let osnap_active = snapper.is_active();
|
let osnap_active = snapper.is_active();
|
||||||
let snap_grid_on = snapper.is_on(crate::snap::SnapType::Grid) && snapper.snap_enabled;
|
let snap_grid_on = snapper.is_on(crate::snap::SnapType::Grid) && snapper.snap_enabled;
|
||||||
|
|
||||||
let scale_label = format_scale(viewport_scale);
|
|
||||||
let vp_label = if viewport_count > 0 {
|
let vp_label = if viewport_count > 0 {
|
||||||
format!("{} VP", viewport_count)
|
format!("{} VP", viewport_count)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
// Scale pill: opens the scale picker popup.
|
||||||
|
// Model space: always interactive, shows annotation scale.
|
||||||
|
// Paper space: interactive only when a viewport is active/selected.
|
||||||
|
let scale_label = if current_layout == "Model" {
|
||||||
|
format_scale(Some(1.0 / annotation_scale as f64))
|
||||||
|
} else {
|
||||||
|
format_scale(viewport_scale)
|
||||||
|
};
|
||||||
|
let scale_element: Element<'_, Message> = if scale_pill_enabled {
|
||||||
|
tip(scale_popup_btn(&scale_label, scale_popup_open), "Annotation / Viewport Scale\nClick to change")
|
||||||
|
} else {
|
||||||
|
status_pill(scale_label).into()
|
||||||
|
};
|
||||||
let mut right_status = row![
|
let mut right_status = row![
|
||||||
tip(
|
tip(
|
||||||
toggle_pill("SNAP", snap_grid_on, Message::ToggleGridSnap),
|
toggle_pill("SNAP", snap_grid_on, Message::ToggleGridSnap),
|
||||||
|
|
@ -96,7 +114,7 @@ impl StatusBar {
|
||||||
space_mode_btn(¤t_layout, in_mspace),
|
space_mode_btn(¤t_layout, in_mspace),
|
||||||
"PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space",
|
"PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space",
|
||||||
),
|
),
|
||||||
status_pill(scale_label),
|
scale_element,
|
||||||
]
|
]
|
||||||
.spacing(2);
|
.spacing(2);
|
||||||
if !vp_label.is_empty() {
|
if !vp_label.is_empty() {
|
||||||
|
|
@ -609,6 +627,32 @@ const SNAP_OFF_HOVER: Color = Color {
|
||||||
a: 1.0,
|
a: 1.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Scale popup button ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn scale_popup_btn(label: &str, open: bool) -> Element<'static, Message> {
|
||||||
|
let label = label.to_string();
|
||||||
|
button(text(label).size(10).color(if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT }))
|
||||||
|
.on_press(Message::ToggleScalePopup)
|
||||||
|
.style(move |_: &Theme, status| button::Style {
|
||||||
|
background: Some(Background::Color(match (open, status) {
|
||||||
|
(true, button::Status::Hovered) => SNAP_ON_HOVER,
|
||||||
|
(true, _) => SNAP_ON_BG,
|
||||||
|
(false, button::Status::Hovered) => SNAP_OFF_HOVER,
|
||||||
|
(false, _) => SNAP_OFF_BG,
|
||||||
|
})),
|
||||||
|
border: Border {
|
||||||
|
color: if open { SNAP_BORDER_ON } else { BORDER_COLOR },
|
||||||
|
width: 1.0,
|
||||||
|
radius: 2.0.into(),
|
||||||
|
},
|
||||||
|
text_color: if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT },
|
||||||
|
shadow: iced::Shadow::default(),
|
||||||
|
snap: false,
|
||||||
|
})
|
||||||
|
.padding([2, 6])
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Scale display ─────────────────────────────────────────────────────────
|
// ── Scale display ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Formats a viewport scale factor as a human-readable ratio string.
|
/// Formats a viewport scale factor as a human-readable ratio string.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue