diff --git a/Cargo.toml b/Cargo.toml index b3efeb619..83f85dea7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ rfd = "0.15" acadrust = "0.3.3" open = "5" printpdf = "0.9.1" +flate2 = "1" diff --git a/src/app/commands.rs b/src/app/commands.rs index 686e0fc15..69edee6e6 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -2788,6 +2788,39 @@ impl H7CAD { "PRINT"|"PLOT"|"EXPORT" => { return Task::done(Message::PlotExport); } + // PLOTSTYLE — load or clear CTB/STB plot style table + cmd if cmd == "PLOTSTYLE" || cmd.starts_with("PLOTSTYLE ") => { + let sub = cmd.split_once(' ') + .map(|(_, r)| r.trim().to_uppercase()) + .unwrap_or_default(); + match sub.as_str() { + "CLEAR" | "NONE" => { + return Task::done(Message::PlotStyleClear); + } + "" | "LOAD" => { + let active = self.active_plot_style.as_ref() + .map(|t| format!("Active: {}", t.name)) + .unwrap_or_else(|| "No plot style loaded.".into()); + self.command_line.push_info(&active); + return Task::done(Message::PlotStyleLoad); + } + "?" | "STATUS" => { + let msg = self.active_plot_style.as_ref() + .map(|t| format!( + "Plot style: {} ({} color overrides)", + t.name, + t.aci_entries.iter().filter(|e| e.color.is_some()).count() + )) + .unwrap_or_else(|| "No plot style table loaded.".into()); + self.command_line.push_output(&msg); + } + _ => { + self.command_line.push_error( + "Usage: PLOTSTYLE [LOAD | CLEAR | STATUS]" + ); + } + } + } "PAGESETUP" => { if self.tabs[i].scene.current_layout == "Model" { self.command_line.push_error("PAGESETUP: switch to a paper space layout first."); diff --git a/src/app/mod.rs b/src/app/mod.rs index 46411238c..5b083462b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -86,6 +86,10 @@ pub(super) struct H7CAD { /// Plot scale: "Fit" | "1:1" | "1:2" | "1:4" | "1:5" | "1:10" | "1:20" | "1:50" | "1:100" | "2:1". page_setup_scale: String, + // ── Plot Style Table ────────────────────────────────────────────────── + /// Currently loaded CTB/STB table (None = no override). + active_plot_style: Option, + // ── DimStyle Dialog ─────────────────────────────────────────────────── dimstyle_open: bool, /// Name of the style currently shown in the dialog. @@ -324,6 +328,13 @@ pub enum Message { PlotExport, /// Callback after the user picks (or cancels) the export path. PlotExportPath(Option), + // ── Plot Style Table ───────────────────────────────────────────────── + /// Open file dialog to load a CTB/STB plot style table. + PlotStyleLoad, + /// Callback when the user picks (or cancels) a CTB/STB file. + PlotStyleLoaded(Option), + /// Clear the active plot style table. + PlotStyleClear, // ── DimStyle Dialog ─────────────────────────────────────────────────── DimStyleDialogOpen, DimStyleDialogClose, @@ -383,6 +394,8 @@ impl H7CAD { page_setup_offset_y: "0.0".to_string(), page_setup_rotation: "0".to_string(), page_setup_scale: "Fit".to_string(), + // Plot style + active_plot_style: None, // DimStyle dialog dimstyle_open: false, dimstyle_selected: "Standard".to_string(), diff --git a/src/app/update.rs b/src/app/update.rs index c9384e436..647af0668 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -2092,6 +2092,7 @@ impl H7CAD { &wires, eff_w, eff_h, draw_ox as f32, draw_oy as f32, rotation_deg, &path, + self.active_plot_style.as_ref(), ) { Ok(()) => self.command_line.push_info(&format!( "Exported: {}", @@ -2102,6 +2103,29 @@ impl H7CAD { Task::none() } + // ── Plot Style Table ────────────────────────────────────────────── + Message::PlotStyleLoad => { + Task::perform( + crate::io::pick_plot_style(), + Message::PlotStyleLoaded, + ) + } + Message::PlotStyleLoaded(Some(table)) => { + self.command_line.push_output(&format!( + "Plot style '{}' loaded ({} color entries).", + table.name, + table.aci_entries.iter().filter(|e| e.color.is_some()).count() + )); + self.active_plot_style = Some(table); + Task::none() + } + Message::PlotStyleLoaded(None) => Task::none(), + Message::PlotStyleClear => { + self.active_plot_style = None; + self.command_line.push_output("Plot style table cleared."); + Task::none() + } + // ── DimStyle Dialog ─────────────────────────────────────────────── Message::DimStyleDialogOpen => { let i = self.active_tab; diff --git a/src/io/mod.rs b/src/io/mod.rs index 2a114483a..a5e6d817b 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -4,6 +4,7 @@ // Default save format: DWG (AC1032 / R2018+). pub mod pdf_export; +pub mod plot_style; use acadrust::io::dwg::DwgReader; use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter}; @@ -97,6 +98,21 @@ pub async fn pick_save_path() -> Option { dlg.save_file().await.map(|h| h.path().to_path_buf()) } +// ── Plot Style Table ────────────────────────────────────────────────────── + +/// Show a file-open dialog and load the selected CTB or STB file. +pub async fn pick_plot_style() -> Option { + let handle = rfd::AsyncFileDialog::new() + .set_title("Load Plot Style Table") + .add_filter("Plot Style Tables", &["ctb", "stb", "CTB", "STB"]) + .add_filter("CTB Files", &["ctb", "CTB"]) + .add_filter("STB Files", &["stb", "STB"]) + .add_filter("All Files", &["*"]) + .pick_file() + .await?; + plot_style::PlotStyleTable::load(handle.path()).ok() +} + // ── Save ────────────────────────────────────────────────────────────────── /// Save the document to the given path. diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index b9413cc77..70ac50f6b 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -9,6 +9,7 @@ // drawing origin at the paper origin. use crate::scene::WireModel; +use crate::io::plot_style::PlotStyleTable; use printpdf::{Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PdfDocument, PdfPage, PdfSaveOptions, Point, Pt, Rgb}; use std::io::Write; @@ -30,8 +31,9 @@ pub fn export_pdf( offset_y: f32, rotation_deg: i32, path: &Path, + plot_style: Option<&PlotStyleTable>, ) -> Result<(), String> { - let bytes = build_pdf(wires, paper_w as f32, paper_h as f32, offset_x, offset_y, rotation_deg); + let bytes = build_pdf(wires, paper_w as f32, paper_h as f32, offset_x, offset_y, rotation_deg, plot_style); let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?; file.write_all(&bytes).map_err(|e| e.to_string()) } @@ -50,7 +52,7 @@ pub async fn pick_pdf_path_owned(stem: String) -> Option { // ── PDF builder ─────────────────────────────────────────────────────────── -fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32, rotation_deg: i32) -> Vec { +fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32, rotation_deg: i32, plot_style: Option<&PlotStyleTable>) -> Vec { let mut doc = PdfDocument::new("H7CAD Export"); let mut ops: Vec = Vec::new(); @@ -93,6 +95,10 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32, let mut last_color: Option<[f32; 3]> = None; let mut last_lw: Option = None; + // mm to PDF points (1 mm = 2.834645 pt). + const MM_TO_PT: f32 = 2.834645; + // Screen px to PDF points (approximate at 96 dpi). + const PX_TO_PT: f32 = 0.35278; for wire in wires { let [mut r, mut g, mut b, a] = wire.color; @@ -103,15 +109,29 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32, if wire.name == "__paper_boundary__" { continue; } - // Near-white and near-yellow (viewport active border) → dark grey for print. - let is_light = r > 0.80 && g > 0.80 && b > 0.80; - let is_yellow = r > 0.80 && g > 0.70 && b < 0.30; - let is_cyan = r < 0.30 && g > 0.70 && b > 0.70; - if is_light || is_yellow { - r = 0.0; g = 0.0; b = 0.0; - } else if is_cyan { - // Viewport border: print as dark blue. - r = 0.0; g = 0.15; b = 0.50; + // Apply CTB plot style table overrides (color + lineweight). + let mut lw_override: Option = None; + if let Some(ctb) = plot_style { + if wire.aci > 0 { + if let Some([cr, cg, cb]) = ctb.resolve_color(wire.aci) { + r = cr; g = cg; b = cb; + } + lw_override = ctb.resolve_lineweight(wire.aci) + .map(|mm| (mm * MM_TO_PT).max(0.1)); + } + } + // Near-white and near-yellow (viewport active border) → dark grey for print + // (only when no CTB override was applied). + if lw_override.is_none() { + let is_light = r > 0.80 && g > 0.80 && b > 0.80; + let is_yellow = r > 0.80 && g > 0.70 && b < 0.30; + let is_cyan = r < 0.30 && g > 0.70 && b > 0.70; + if is_light || is_yellow { + r = 0.0; g = 0.0; b = 0.0; + } else if is_cyan { + // Viewport border: print as dark blue. + r = 0.0; g = 0.15; b = 0.50; + } } if last_color.map(|c| { @@ -123,8 +143,9 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32, last_color = Some([r, g, b]); } - // Line weight: screen px → points (approximate 1 px ≈ 0.35 pt). - let lw_pt = (wire.line_weight_px * 0.35278_f32).max(0.1); + // Line weight: CTB override (in pt) or screen px → points. + let lw_pt = lw_override + .unwrap_or_else(|| (wire.line_weight_px * PX_TO_PT).max(0.1)); if last_lw.map(|l| (l - lw_pt).abs() > 0.01).unwrap_or(true) { ops.push(Op::SetOutlineThickness { pt: Pt(lw_pt) }); last_lw = Some(lw_pt); diff --git a/src/io/plot_style.rs b/src/io/plot_style.rs new file mode 100644 index 000000000..14d36756e --- /dev/null +++ b/src/io/plot_style.rs @@ -0,0 +1,270 @@ +//! Plot Style Table — CTB (color-based) and STB (named) file support. +//! +//! CTB files map AutoCAD Color Index (ACI, 1-255) to pen properties: +//! RGB color override, lineweight, and screeing percentage. +//! +//! File format: deflate-compressed text (key = value pairs) with +//! 255 `begin_plot_style … end_plot_style` blocks. +//! +//! STB files follow the same format but use named styles instead of +//! ACI indices; they are read into a `Vec`. + +use std::collections::HashMap; +use std::io::Read; +use std::path::Path; + +// ── AutoCAD standard lineweight table (index → mm) ─────────────────────────── + +/// Lineweight table: index value → mm, matching AutoCAD's LWEIGHT codes. +/// Index 0 = 0.00 mm (hairline), others follow the DXF lineweight enum. +pub const LW_TABLE: &[f32] = &[ + 0.00, 0.05, 0.09, 0.10, 0.13, 0.15, 0.18, 0.20, + 0.25, 0.30, 0.35, 0.40, 0.50, 0.53, 0.60, 0.70, + 0.80, 0.90, 1.00, 1.06, 1.20, 1.40, 1.58, 2.00, 2.11, +]; + +// ── Per-color entry ─────────────────────────────────────────────────────────── + +/// A single entry in a CTB or STB plot style table. +#[derive(Debug, Clone)] +pub struct PlotStyleEntry { + /// Optional display name (empty for CTB; the style name for STB). + pub description: String, + /// If `Some([r,g,b])`, override the entity color with this RGB value (0..255). + /// If `None`, use the object color. + pub color: Option<[u8; 3]>, + /// Lineweight index into `LW_TABLE`. 255 = use object lineweight. + pub lineweight: u8, + /// Screen percentage 0–100 (100 = opaque). + pub screening: u8, +} + +impl Default for PlotStyleEntry { + fn default() -> Self { + PlotStyleEntry { + description: String::new(), + color: None, + lineweight: 255, // use object lineweight + screening: 100, + } + } +} + +// ── Plot Style Table ────────────────────────────────────────────────────────── + +/// A loaded CTB or STB plot style table. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct PlotStyleTable { + /// File name (without path), e.g. "monochrome.ctb". + pub name: String, + /// Whether this is a named-style (STB) table rather than color-based (CTB). + pub is_stb: bool, + /// For CTB: entries indexed by ACI (index 0 unused; 1..=255 are valid). + pub aci_entries: Vec, // 256 entries, index = ACI + /// For STB: named style entries. + pub named_entries: HashMap, +} + +impl PlotStyleTable { + /// Create an identity CTB table (no overrides for any color). + #[allow(dead_code)] + pub fn identity(name: impl Into) -> Self { + PlotStyleTable { + name: name.into(), + is_stb: false, + aci_entries: (0..=255).map(|_| PlotStyleEntry::default()).collect(), + named_entries: HashMap::new(), + } + } + + /// Load a CTB or STB file from disk. + pub fn load(path: &Path) -> Result { + let raw = std::fs::read(path).map_err(|e| e.to_string())?; + let name = path.file_name() + .unwrap_or_default().to_string_lossy().into_owned(); + let is_stb = name.to_lowercase().ends_with(".stb"); + let text = decompress_ctb(&raw)?; + parse_plot_style_text(&text, name, is_stb) + } + + /// Write this table to disk as a CTB/STB file. + #[allow(dead_code)] + pub fn save(&self, path: &Path) -> Result<(), String> { + let text = self.to_text(); + let compressed = compress_ctb(text.as_bytes())?; + std::fs::write(path, compressed).map_err(|e| e.to_string()) + } + + /// Resolve the effective print RGB color for the given ACI index. + /// Returns None if no override (use object color). + pub fn resolve_color(&self, aci: u8) -> Option<[f32; 3]> { + let entry = self.aci_entries.get(aci as usize)?; + entry.color.map(|[r, g, b]| { + [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0] + }) + } + + /// Resolve the effective lineweight in mm for the given ACI index. + /// Returns None if no override (use object lineweight). + pub fn resolve_lineweight(&self, aci: u8) -> Option { + let entry = self.aci_entries.get(aci as usize)?; + if entry.lineweight == 255 { + None + } else { + LW_TABLE.get(entry.lineweight as usize).copied() + } + } + + // ── Internal serialisation ──────────────────────────────────────────── + + fn to_text(&self) -> String { + let mut s = String::new(); + s.push_str("description=\n"); + s.push_str("apply_factor=0\n"); + s.push_str("unit_type=1\n"); + s.push_str("custom_lineweight_display_units=0\n"); + for (_idx, entry) in self.aci_entries.iter().enumerate().skip(1) { + s.push_str("begin_plot_style\n"); + s.push_str(&format!(" description={}\n", entry.description)); + s.push_str(" physical_pen_number=0\n"); + s.push_str(" virtual_pen_number=0\n"); + s.push_str(&format!(" screen={}\n", entry.screening)); + s.push_str(" linepattern_size=0.5\n"); + s.push_str(" linetype=31\n"); + s.push_str(" adaptive_linetype=TRUE\n"); + s.push_str(&format!(" lineweight={}\n", entry.lineweight)); + s.push_str(" fill_style=64\n"); + s.push_str(" end_style=0\n"); + s.push_str(" join_style=0\n"); + if let Some([r, g, b]) = entry.color { + s.push_str(&format!(" color1=#{:02X}{:02X}{:02X}\n", r, g, b)); + } else { + // 0xC2000000 = "use object color" + s.push_str(" color1=-1056964608\n"); + } + s.push_str("end_plot_style\n"); + } + s + } +} + +// ── Deflate helpers ─────────────────────────────────────────────────────────── + +/// Decompress a CTB/STB file's raw bytes into the text content. +/// +/// CTB files start with a plain-text header (first line: "PIAFILEVERSION_2.0") +/// followed by raw-deflate compressed content. Some tools write pure zlib +/// (with the two-byte zlib header 0x78 0x9C) instead — we handle both. +fn decompress_ctb(data: &[u8]) -> Result { + // Find the first newline — everything after it is the compressed payload. + let split_at = data.iter().position(|&b| b == b'\n') + .map(|p| p + 1) + .unwrap_or(0); + let payload = &data[split_at..]; + + // Try zlib (0x78 prefix) first, then raw deflate. + let mut text = String::new(); + if payload.starts_with(&[0x78]) { + use flate2::read::ZlibDecoder; + ZlibDecoder::new(payload) + .read_to_string(&mut text) + .map_err(|e| format!("zlib decompress: {e}"))?; + } else { + use flate2::read::DeflateDecoder; + DeflateDecoder::new(payload) + .read_to_string(&mut text) + .map_err(|e| format!("deflate decompress: {e}"))?; + } + Ok(text) +} + +/// Compress plot-style text content as a CTB/STB file. +fn compress_ctb(text: &[u8]) -> Result, String> { + use flate2::{write::ZlibEncoder, Compression}; + use std::io::Write; + let header = b"PIAFILEVERSION_2.0\r\n"; + let mut compressed: Vec = Vec::new(); + { + let mut enc = ZlibEncoder::new(&mut compressed, Compression::default()); + enc.write_all(text).map_err(|e| e.to_string())?; + } + let mut out = header.to_vec(); + out.extend_from_slice(&compressed); + Ok(out) +} + +// ── Text parser ─────────────────────────────────────────────────────────────── + +fn parse_plot_style_text( + text: &str, + name: String, + is_stb: bool, +) -> Result { + let mut aci_entries: Vec = (0..=255).map(|_| PlotStyleEntry::default()).collect(); + let mut named_entries: HashMap = HashMap::new(); + let mut style_index: usize = 1; // CTB: 1-based ACI index + let mut current: Option = None; + let mut current_name: String = String::new(); + + for line in text.lines() { + let line = line.trim(); + if line == "begin_plot_style" { + current = Some(PlotStyleEntry::default()); + current_name = format!("Color_{}", style_index); + continue; + } + if line == "end_plot_style" { + if let Some(entry) = current.take() { + if is_stb { + named_entries.insert(current_name.clone(), entry); + } else if style_index <= 255 { + aci_entries[style_index] = entry; + style_index += 1; + } + } + continue; + } + let Some(entry) = current.as_mut() else { continue }; + if let Some((key, val)) = line.split_once('=') { + let key = key.trim(); + let val = val.trim(); + match key { + "description" => { + if !val.is_empty() { + entry.description = val.to_string(); + current_name = val.to_string(); + } + } + "screen" => { + if let Ok(v) = val.parse::() { entry.screening = v; } + } + "lineweight" => { + if let Ok(v) = val.parse::() { entry.lineweight = v; } + } + "color1" => { + if val.starts_with('#') && val.len() == 7 { + // #RRGGBB + let r = u8::from_str_radix(&val[1..3], 16).unwrap_or(0); + let g = u8::from_str_radix(&val[3..5], 16).unwrap_or(0); + let b = u8::from_str_radix(&val[5..7], 16).unwrap_or(0); + entry.color = Some([r, g, b]); + } else if let Ok(packed) = val.parse::() { + // AutoCAD packs RGB as 0xC0RRGGBB negative int. + // Value 0xC2000000 (-1056964608) = use object color. + if packed != -1056964608i32 { + let u = packed as u32; + let r = ((u >> 16) & 0xFF) as u8; + let g = ((u >> 8) & 0xFF) as u8; + let b = ( u & 0xFF) as u8; + entry.color = Some([r, g, b]); + } + } + } + _ => {} + } + } + } + + Ok(PlotStyleTable { name, is_stb, aci_entries, named_entries }) +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 2f7656cdc..a4ce82b3b 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -456,12 +456,12 @@ impl Scene { )]; } - let (entity_color, pattern_length, pattern, line_weight_px) = self.render_style(e); + let (entity_color, pattern_length, pattern, line_weight_px, aci) = self.render_style(e); let lt_scale = e.common().linetype_scale as f32; let lt_name = self.resolved_linetype_name(e); if let EntityType::Dimension(dim) = e { - return tessellate::tessellate_dimension( + let mut wires = tessellate::tessellate_dimension( &self.document, h, dim, @@ -469,6 +469,8 @@ impl Scene { entity_color, line_weight_px, ); + for w in &mut wires { w.aci = aci; } + return wires; } if let EntityType::Insert(ins) = e { @@ -478,7 +480,7 @@ impl Scene { .cloned() .map(crate::modules::home::modify::explode::normalize_insert_entity) .flat_map(|sub| { - let (sub_color, sub_pattern_length, sub_pattern, sub_line_weight_px) = + let (sub_color, sub_pattern_length, sub_pattern, sub_line_weight_px, sub_aci) = self.render_style(&sub); let mut wire = tessellate::tessellate( &self.document, @@ -491,12 +493,13 @@ impl Scene { sub_line_weight_px, ); wire.name = h.value().to_string(); + wire.aci = sub_aci; vec![wire] }) .collect(); } - let base = tessellate::tessellate( + let mut base = tessellate::tessellate( &self.document, h, e, @@ -506,6 +509,7 @@ impl Scene { pattern, line_weight_px, ); + base.aci = aci; if let Some(clt) = crate::linetypes::complex_lt(lt_name) { let wires = complex_lt::apply_along( @@ -1966,6 +1970,7 @@ fn paper_boundary_wire(x0: f32, y0: f32, x1: f32, y1: f32) -> WireModel { line_weight_px: 1.5, snap_pts: vec![], tangent_geoms: vec![], - key_vertices: vec![], + aci: 0, + key_vertices: vec![], } } diff --git a/src/scene/render.rs b/src/scene/render.rs index 8e87203a1..f55fdb924 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -150,9 +150,10 @@ impl shader::Primitive for Primitive { // ── Render-style helpers (impl Scene) ──────────────────────────────────── impl Scene { - pub(super) fn render_style(&self, e: &EntityType) -> ([f32; 4], f32, [f32; 8], f32) { + /// Returns (entity_color, pattern_length, pattern, line_weight_px, aci). + pub(super) fn render_style(&self, e: &EntityType) -> ([f32; 4], f32, [f32; 8], f32, u8) { let layer_name = &e.common().layer; - let entity_color = { + let (entity_color, aci) = { let ec = &e.common().color; let resolved = if *ec == AcadColor::ByLayer { self.document @@ -163,9 +164,13 @@ impl Scene { } else { ec }; + let aci = match resolved { + AcadColor::Index(i) => *i, + _ => 0, + }; let [r, g, b, _] = tessellate::aci_to_rgba(resolved); let alpha = 1.0 - e.common().transparency.as_percent() as f32; - [r, g, b, alpha] + ([r, g, b, alpha], aci) }; let lt_name = self.resolved_linetype_name(e); @@ -191,7 +196,7 @@ impl Scene { .unwrap_or(1.0) }; - (entity_color, pattern_length, pattern, line_weight_px) + (entity_color, pattern_length, pattern, line_weight_px, aci) } pub(super) fn resolved_linetype_name<'a>(&'a self, e: &'a EntityType) -> &'a str { diff --git a/src/scene/tessellate.rs b/src/scene/tessellate.rs index 8806f5055..f12cacb4d 100644 --- a/src/scene/tessellate.rs +++ b/src/scene/tessellate.rs @@ -94,7 +94,8 @@ pub fn tessellate( line_weight_px, snap_pts: te.snap_pts, tangent_geoms: te.tangent_geoms, - key_vertices: te.key_vertices, + aci: 0, + key_vertices: te.key_vertices, }; } @@ -119,7 +120,8 @@ pub fn tessellate( line_weight_px: 1.0, snap_pts: te.snap_pts, tangent_geoms: te.tangent_geoms, - key_vertices: te.key_vertices, + aci: 0, + key_vertices: te.key_vertices, }; } _ => {} @@ -138,7 +140,8 @@ pub fn tessellate( line_weight_px, snap_pts: te.snap_pts, tangent_geoms: te.tangent_geoms, - key_vertices: te.key_vertices, + aci: 0, + key_vertices: te.key_vertices, }; } } @@ -155,7 +158,8 @@ pub fn tessellate( line_weight_px, snap_pts: te.snap_pts, tangent_geoms: te.tangent_geoms, - key_vertices: te.key_vertices, + aci: 0, + key_vertices: te.key_vertices, }; } } @@ -171,7 +175,8 @@ pub fn tessellate( line_weight_px, snap_pts: te.snap_pts, tangent_geoms: te.tangent_geoms, - key_vertices: te.key_vertices, + aci: 0, + key_vertices: te.key_vertices, }; } @@ -189,6 +194,7 @@ pub fn tessellate( points, color, selected, + aci: 0, pattern_length, pattern, line_weight_px, @@ -224,6 +230,7 @@ pub fn tessellate_dimension( points, color, selected, + aci: 0, pattern_length: 0.0, pattern: [0.0; 8], line_weight_px, @@ -274,6 +281,7 @@ fn tessellate_leader( line_weight_px, snap_pts: vec![], tangent_geoms: vec![], + aci: 0, key_vertices: vec![], }]; } @@ -335,6 +343,7 @@ fn tessellate_leader( points, color, selected, + aci: 0, pattern_length: 0.0, pattern: [0.0; 8], line_weight_px, @@ -445,7 +454,8 @@ fn tessellate_multileader( line_weight_px, snap_pts: vec![], tangent_geoms: vec![], - key_vertices: key_verts, + aci: 0, + key_vertices: key_verts, }]; // Render text content as MText wire diff --git a/src/scene/wire_model.rs b/src/scene/wire_model.rs index a4d461a1b..c36cc4b95 100644 --- a/src/scene/wire_model.rs +++ b/src/scene/wire_model.rs @@ -44,6 +44,8 @@ pub struct WireModel { pub pattern: [f32; 8], /// Rendered line width in screen pixels (half-width = line_weight_px / 2). pub line_weight_px: f32, + /// ACI color index (1-255). 0 means true-color or unknown (no CTB lookup). + pub aci: u8, /// Pre-baked snap candidates (Center, Node, Quadrant, Insertion). pub snap_pts: Vec<(glam::Vec3, SnapHint)>, /// Per-segment tangent geometry for Tangent snap. @@ -67,6 +69,7 @@ impl WireModel { points, color, selected, + aci: 0, pattern_length: 0.0, pattern: [0.0; 8], line_weight_px: 1.0,