Files
OneCAD-Draft/src/io/plot_style.rs
T
Hakan SevenandClaude Opus 4.8 70702d063f perf(hash): swap std HashMap/HashSet for rustc-hash FxHash
Phase 4.1. Handle / u64 keys dominate the hot maps (block_defn,
hatch / image / mesh caches, draw-depth map, viewport wire caches);
the default SipHash is wasted work on integer keys. Alias the std
types to rustc_hash::FxHashMap / FxHashSet crate-wide so every map and
set — including the cross-module draw-depth map shared between scene
and the GPU pipelines — uses the faster hasher consistently.

Mechanical: no behavioural change. Constructors moved from ::new() to
::default() (Fx variants carry no RandomState). Build + 13 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 00:35:53 +03:00

282 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 rustc_hash::FxHashMap as 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).
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::default(),
}
}
/// 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::default();
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,
})
}