Feat: implement CTB/STB plot style table with PDF export integration

- src/io/plot_style.rs: PlotStyleTable and PlotStyleEntry data structures;
  CTB reader (deflate/zlib decompressed text format) with ACI→RGB and
  ACI→lineweight lookup; CTB writer (zlib compressed); LW_TABLE constant
- src/io/pdf_export.rs: accept optional PlotStyleTable; apply ACI color
  and lineweight overrides during PDF rendering
- src/scene/wire_model.rs: add `aci: u8` field to carry ACI index
- src/scene/render.rs: render_style() now returns ACI alongside RGBA
- src/scene/mod.rs + tessellate.rs: propagate aci through wire building
- app: PlotStyleLoad/Loaded/Clear messages and handlers;
  PLOTSTYLE LOAD | CLEAR | STATUS command; active_plot_style state on H7CAD
- flate2 added as dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-04-06 14:37:23 +03:00
commit 1db3ad6a33
11 changed files with 429 additions and 28 deletions

View file

@ -15,3 +15,4 @@ rfd = "0.15"
acadrust = "0.3.3" acadrust = "0.3.3"
open = "5" open = "5"
printpdf = "0.9.1" printpdf = "0.9.1"
flate2 = "1"

View file

@ -2788,6 +2788,39 @@ impl H7CAD {
"PRINT"|"PLOT"|"EXPORT" => { "PRINT"|"PLOT"|"EXPORT" => {
return Task::done(Message::PlotExport); 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" => { "PAGESETUP" => {
if self.tabs[i].scene.current_layout == "Model" { if self.tabs[i].scene.current_layout == "Model" {
self.command_line.push_error("PAGESETUP: switch to a paper space layout first."); self.command_line.push_error("PAGESETUP: switch to a paper space layout first.");

View file

@ -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". /// 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, page_setup_scale: String,
// ── Plot Style Table ──────────────────────────────────────────────────
/// Currently loaded CTB/STB table (None = no override).
active_plot_style: Option<crate::io::plot_style::PlotStyleTable>,
// ── DimStyle Dialog ─────────────────────────────────────────────────── // ── DimStyle Dialog ───────────────────────────────────────────────────
dimstyle_open: bool, dimstyle_open: bool,
/// Name of the style currently shown in the dialog. /// Name of the style currently shown in the dialog.
@ -324,6 +328,13 @@ pub enum Message {
PlotExport, PlotExport,
/// Callback after the user picks (or cancels) the export path. /// Callback after the user picks (or cancels) the export path.
PlotExportPath(Option<std::path::PathBuf>), PlotExportPath(Option<std::path::PathBuf>),
// ── 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<crate::io::plot_style::PlotStyleTable>),
/// Clear the active plot style table.
PlotStyleClear,
// ── DimStyle Dialog ─────────────────────────────────────────────────── // ── DimStyle Dialog ───────────────────────────────────────────────────
DimStyleDialogOpen, DimStyleDialogOpen,
DimStyleDialogClose, DimStyleDialogClose,
@ -383,6 +394,8 @@ impl H7CAD {
page_setup_offset_y: "0.0".to_string(), page_setup_offset_y: "0.0".to_string(),
page_setup_rotation: "0".to_string(), page_setup_rotation: "0".to_string(),
page_setup_scale: "Fit".to_string(), page_setup_scale: "Fit".to_string(),
// Plot style
active_plot_style: None,
// DimStyle dialog // DimStyle dialog
dimstyle_open: false, dimstyle_open: false,
dimstyle_selected: "Standard".to_string(), dimstyle_selected: "Standard".to_string(),

View file

@ -2092,6 +2092,7 @@ impl H7CAD {
&wires, eff_w, eff_h, &wires, eff_w, eff_h,
draw_ox as f32, draw_oy as f32, draw_ox as f32, draw_oy as f32,
rotation_deg, &path, rotation_deg, &path,
self.active_plot_style.as_ref(),
) { ) {
Ok(()) => self.command_line.push_info(&format!( Ok(()) => self.command_line.push_info(&format!(
"Exported: {}", "Exported: {}",
@ -2102,6 +2103,29 @@ impl H7CAD {
Task::none() 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 ─────────────────────────────────────────────── // ── DimStyle Dialog ───────────────────────────────────────────────
Message::DimStyleDialogOpen => { Message::DimStyleDialogOpen => {
let i = self.active_tab; let i = self.active_tab;

View file

@ -4,6 +4,7 @@
// Default save format: DWG (AC1032 / R2018+). // Default save format: DWG (AC1032 / R2018+).
pub mod pdf_export; pub mod pdf_export;
pub mod plot_style;
use acadrust::io::dwg::DwgReader; use acadrust::io::dwg::DwgReader;
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter}; use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
@ -97,6 +98,21 @@ pub async fn pick_save_path() -> Option<PathBuf> {
dlg.save_file().await.map(|h| h.path().to_path_buf()) 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<plot_style::PlotStyleTable> {
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 ──────────────────────────────────────────────────────────────────
/// Save the document to the given path. /// Save the document to the given path.

View file

@ -9,6 +9,7 @@
// drawing origin at the paper origin. // drawing origin at the paper origin.
use crate::scene::WireModel; use crate::scene::WireModel;
use crate::io::plot_style::PlotStyleTable;
use printpdf::{Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PdfDocument, use printpdf::{Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PdfDocument,
PdfPage, PdfSaveOptions, Point, Pt, Rgb}; PdfPage, PdfSaveOptions, Point, Pt, Rgb};
use std::io::Write; use std::io::Write;
@ -30,8 +31,9 @@ pub fn export_pdf(
offset_y: f32, offset_y: f32,
rotation_deg: i32, rotation_deg: i32,
path: &Path, path: &Path,
plot_style: Option<&PlotStyleTable>,
) -> Result<(), String> { ) -> 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())?; let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?;
file.write_all(&bytes).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<std::path::PathBuf> {
// ── PDF builder ─────────────────────────────────────────────────────────── // ── PDF builder ───────────────────────────────────────────────────────────
fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32, rotation_deg: i32) -> Vec<u8> { fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32, rotation_deg: i32, plot_style: Option<&PlotStyleTable>) -> Vec<u8> {
let mut doc = PdfDocument::new("H7CAD Export"); let mut doc = PdfDocument::new("H7CAD Export");
let mut ops: Vec<Op> = Vec::new(); let mut ops: Vec<Op> = 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_color: Option<[f32; 3]> = None;
let mut last_lw: Option<f32> = None; let mut last_lw: Option<f32> = 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 { for wire in wires {
let [mut r, mut g, mut b, a] = wire.color; let [mut r, mut g, mut b, a] = wire.color;
@ -103,7 +109,20 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32,
if wire.name == "__paper_boundary__" { if wire.name == "__paper_boundary__" {
continue; continue;
} }
// Near-white and near-yellow (viewport active border) → dark grey for print. // Apply CTB plot style table overrides (color + lineweight).
let mut lw_override: Option<f32> = 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_light = r > 0.80 && g > 0.80 && b > 0.80;
let is_yellow = r > 0.80 && g > 0.70 && b < 0.30; let is_yellow = r > 0.80 && g > 0.70 && b < 0.30;
let is_cyan = r < 0.30 && g > 0.70 && b > 0.70; let is_cyan = r < 0.30 && g > 0.70 && b > 0.70;
@ -113,6 +132,7 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32,
// Viewport border: print as dark blue. // Viewport border: print as dark blue.
r = 0.0; g = 0.15; b = 0.50; r = 0.0; g = 0.15; b = 0.50;
} }
}
if last_color.map(|c| { if last_color.map(|c| {
(c[0]-r).abs() > 0.01 || (c[1]-g).abs() > 0.01 || (c[2]-b).abs() > 0.01 (c[0]-r).abs() > 0.01 || (c[1]-g).abs() > 0.01 || (c[2]-b).abs() > 0.01
@ -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]); last_color = Some([r, g, b]);
} }
// Line weight: screen px → points (approximate 1 px ≈ 0.35 pt). // Line weight: CTB override (in pt) or screen px → points.
let lw_pt = (wire.line_weight_px * 0.35278_f32).max(0.1); 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) { if last_lw.map(|l| (l - lw_pt).abs() > 0.01).unwrap_or(true) {
ops.push(Op::SetOutlineThickness { pt: Pt(lw_pt) }); ops.push(Op::SetOutlineThickness { pt: Pt(lw_pt) });
last_lw = Some(lw_pt); last_lw = Some(lw_pt);

270
src/io/plot_style.rs Normal file
View file

@ -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<NamedPlotStyle>`.
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<PlotStyleEntry>, // 256 entries, index = ACI
/// For STB: named style entries.
pub named_entries: HashMap<String, PlotStyleEntry>,
}
impl PlotStyleTable {
/// Create an identity CTB table (no overrides for any color).
#[allow(dead_code)]
pub fn identity(name: impl Into<String>) -> 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<Self, String> {
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<f32> {
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<String, String> {
// 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<Vec<u8>, String> {
use flate2::{write::ZlibEncoder, Compression};
use std::io::Write;
let header = b"PIAFILEVERSION_2.0\r\n";
let mut compressed: Vec<u8> = 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<PlotStyleTable, String> {
let mut aci_entries: Vec<PlotStyleEntry> = (0..=255).map(|_| PlotStyleEntry::default()).collect();
let mut named_entries: HashMap<String, PlotStyleEntry> = HashMap::new();
let mut style_index: usize = 1; // CTB: 1-based ACI index
let mut current: Option<PlotStyleEntry> = 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::<u8>() { entry.screening = v; }
}
"lineweight" => {
if let Ok(v) = val.parse::<u8>() { 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::<i32>() {
// 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 })
}

View file

@ -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_scale = e.common().linetype_scale as f32;
let lt_name = self.resolved_linetype_name(e); let lt_name = self.resolved_linetype_name(e);
if let EntityType::Dimension(dim) = e { if let EntityType::Dimension(dim) = e {
return tessellate::tessellate_dimension( let mut wires = tessellate::tessellate_dimension(
&self.document, &self.document,
h, h,
dim, dim,
@ -469,6 +469,8 @@ impl Scene {
entity_color, entity_color,
line_weight_px, line_weight_px,
); );
for w in &mut wires { w.aci = aci; }
return wires;
} }
if let EntityType::Insert(ins) = e { if let EntityType::Insert(ins) = e {
@ -478,7 +480,7 @@ impl Scene {
.cloned() .cloned()
.map(crate::modules::home::modify::explode::normalize_insert_entity) .map(crate::modules::home::modify::explode::normalize_insert_entity)
.flat_map(|sub| { .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); self.render_style(&sub);
let mut wire = tessellate::tessellate( let mut wire = tessellate::tessellate(
&self.document, &self.document,
@ -491,12 +493,13 @@ impl Scene {
sub_line_weight_px, sub_line_weight_px,
); );
wire.name = h.value().to_string(); wire.name = h.value().to_string();
wire.aci = sub_aci;
vec![wire] vec![wire]
}) })
.collect(); .collect();
} }
let base = tessellate::tessellate( let mut base = tessellate::tessellate(
&self.document, &self.document,
h, h,
e, e,
@ -506,6 +509,7 @@ impl Scene {
pattern, pattern,
line_weight_px, line_weight_px,
); );
base.aci = aci;
if let Some(clt) = crate::linetypes::complex_lt(lt_name) { if let Some(clt) = crate::linetypes::complex_lt(lt_name) {
let wires = complex_lt::apply_along( 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, line_weight_px: 1.5,
snap_pts: vec![], snap_pts: vec![],
tangent_geoms: vec![], tangent_geoms: vec![],
aci: 0,
key_vertices: vec![], key_vertices: vec![],
} }
} }

View file

@ -150,9 +150,10 @@ impl shader::Primitive for Primitive {
// ── Render-style helpers (impl Scene) ──────────────────────────────────── // ── Render-style helpers (impl Scene) ────────────────────────────────────
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 layer_name = &e.common().layer;
let entity_color = { let (entity_color, aci) = {
let ec = &e.common().color; let ec = &e.common().color;
let resolved = if *ec == AcadColor::ByLayer { let resolved = if *ec == AcadColor::ByLayer {
self.document self.document
@ -163,9 +164,13 @@ impl Scene {
} else { } else {
ec ec
}; };
let aci = match resolved {
AcadColor::Index(i) => *i,
_ => 0,
};
let [r, g, b, _] = tessellate::aci_to_rgba(resolved); let [r, g, b, _] = tessellate::aci_to_rgba(resolved);
let alpha = 1.0 - e.common().transparency.as_percent() as f32; 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); let lt_name = self.resolved_linetype_name(e);
@ -191,7 +196,7 @@ impl Scene {
.unwrap_or(1.0) .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 { pub(super) fn resolved_linetype_name<'a>(&'a self, e: &'a EntityType) -> &'a str {

View file

@ -94,6 +94,7 @@ pub fn tessellate(
line_weight_px, line_weight_px,
snap_pts: te.snap_pts, snap_pts: te.snap_pts,
tangent_geoms: te.tangent_geoms, tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices: te.key_vertices, key_vertices: te.key_vertices,
}; };
} }
@ -119,6 +120,7 @@ pub fn tessellate(
line_weight_px: 1.0, line_weight_px: 1.0,
snap_pts: te.snap_pts, snap_pts: te.snap_pts,
tangent_geoms: te.tangent_geoms, tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices: te.key_vertices, key_vertices: te.key_vertices,
}; };
} }
@ -138,6 +140,7 @@ pub fn tessellate(
line_weight_px, line_weight_px,
snap_pts: te.snap_pts, snap_pts: te.snap_pts,
tangent_geoms: te.tangent_geoms, tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices: te.key_vertices, key_vertices: te.key_vertices,
}; };
} }
@ -155,6 +158,7 @@ pub fn tessellate(
line_weight_px, line_weight_px,
snap_pts: te.snap_pts, snap_pts: te.snap_pts,
tangent_geoms: te.tangent_geoms, tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices: te.key_vertices, key_vertices: te.key_vertices,
}; };
} }
@ -171,6 +175,7 @@ pub fn tessellate(
line_weight_px, line_weight_px,
snap_pts: te.snap_pts, snap_pts: te.snap_pts,
tangent_geoms: te.tangent_geoms, tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices: te.key_vertices, key_vertices: te.key_vertices,
}; };
} }
@ -189,6 +194,7 @@ pub fn tessellate(
points, points,
color, color,
selected, selected,
aci: 0,
pattern_length, pattern_length,
pattern, pattern,
line_weight_px, line_weight_px,
@ -224,6 +230,7 @@ pub fn tessellate_dimension(
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,
@ -274,6 +281,7 @@ fn tessellate_leader(
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![],
}]; }];
} }
@ -335,6 +343,7 @@ fn tessellate_leader(
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,
@ -445,6 +454,7 @@ fn tessellate_multileader(
line_weight_px, line_weight_px,
snap_pts: vec![], snap_pts: vec![],
tangent_geoms: vec![], tangent_geoms: vec![],
aci: 0,
key_vertices: key_verts, key_vertices: key_verts,
}]; }];

View file

@ -44,6 +44,8 @@ pub struct WireModel {
pub pattern: [f32; 8], pub pattern: [f32; 8],
/// Rendered line width in screen pixels (half-width = line_weight_px / 2). /// Rendered line width in screen pixels (half-width = line_weight_px / 2).
pub line_weight_px: f32, 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). /// Pre-baked snap candidates (Center, Node, Quadrant, Insertion).
pub snap_pts: Vec<(glam::Vec3, SnapHint)>, pub snap_pts: Vec<(glam::Vec3, SnapHint)>,
/// Per-segment tangent geometry for Tangent snap. /// Per-segment tangent geometry for Tangent snap.
@ -67,6 +69,7 @@ impl WireModel {
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: 1.0, line_weight_px: 1.0,