cargo fmt
This commit is contained in:
parent
468cbde5e1
commit
ca07268c7c
204 changed files with 13988 additions and 5516 deletions
|
|
@ -11,10 +11,10 @@ pub mod step;
|
|||
pub mod stl;
|
||||
pub mod xref;
|
||||
|
||||
use crate::scene::DerivedCaches;
|
||||
use acadrust::entities::{Dimension, EntityType};
|
||||
use acadrust::io::dwg::DwgReader;
|
||||
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
|
||||
use crate::scene::DerivedCaches;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// ── Open ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -43,7 +43,9 @@ pub async fn pick_and_open() -> Result<(String, PathBuf, CadDocument, DerivedCac
|
|||
/// Load a CAD file from a known path (used by recent files).
|
||||
/// Parsing and cache building run on a dedicated OS thread so the async
|
||||
/// executor stays free for rendering during the load.
|
||||
pub async fn open_path(path: PathBuf) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||
pub async fn open_path(
|
||||
path: PathBuf,
|
||||
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||
let name = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
|
|
@ -102,7 +104,9 @@ pub fn read_dir_entries(dir: &std::path::Path) -> Vec<(String, bool, PathBuf)> {
|
|||
if let Ok(rd) = std::fs::read_dir(dir) {
|
||||
for entry in rd.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if name.starts_with('.') { continue; }
|
||||
if name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
|
||||
if is_dir {
|
||||
dirs.push((name, true, entry.path()));
|
||||
|
|
@ -126,13 +130,21 @@ pub fn parse_save_format(format: &str) -> (&'static str, acadrust::DxfVersion) {
|
|||
let f = format.to_ascii_uppercase();
|
||||
let is_dxf = f.starts_with("DXF");
|
||||
let ext = if is_dxf { "dxf" } else { "dwg" };
|
||||
let version = if f.contains("2013") { DxfVersion::AC1027 }
|
||||
else if f.contains("2010") { DxfVersion::AC1024 }
|
||||
else if f.contains("2007") { DxfVersion::AC1021 }
|
||||
else if f.contains("2004") { DxfVersion::AC1018 }
|
||||
else if f.contains("2000") { DxfVersion::AC1015 }
|
||||
else if f.contains("R14") { DxfVersion::AC1014 }
|
||||
else { DxfVersion::AC1032 }; // 2018
|
||||
let version = if f.contains("2013") {
|
||||
DxfVersion::AC1027
|
||||
} else if f.contains("2010") {
|
||||
DxfVersion::AC1024
|
||||
} else if f.contains("2007") {
|
||||
DxfVersion::AC1021
|
||||
} else if f.contains("2004") {
|
||||
DxfVersion::AC1018
|
||||
} else if f.contains("2000") {
|
||||
DxfVersion::AC1015
|
||||
} else if f.contains("R14") {
|
||||
DxfVersion::AC1014
|
||||
} else {
|
||||
DxfVersion::AC1032
|
||||
}; // 2018
|
||||
(ext, version)
|
||||
}
|
||||
|
||||
|
|
@ -185,8 +197,10 @@ pub fn save_as_version(
|
|||
.map(|e| e.to_string_lossy().to_lowercase())
|
||||
.unwrap_or_default();
|
||||
match ext.as_str() {
|
||||
"dxf" => DxfWriter::new(&doc).write_to_file(path).map_err(|e| e.to_string()),
|
||||
_ => DwgWriter::write_to_file(path, &doc).map_err(|e| e.to_string()),
|
||||
"dxf" => DxfWriter::new(&doc)
|
||||
.write_to_file(path)
|
||||
.map_err(|e| e.to_string()),
|
||||
_ => DwgWriter::write_to_file(path, &doc).map_err(|e| e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -195,7 +209,6 @@ pub fn save(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
|||
save_as_version(doc, path, doc.version)
|
||||
}
|
||||
|
||||
|
||||
// ── Post-load fixups ──────────────────────────────────────────────────────
|
||||
|
||||
/// acadrust's ViewportStatusFlags::from_bits() maps bit 0 → is_on and bit 15 → locked,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ pub fn parse_obj(src: &str, color: [f32; 4]) -> Option<MeshModel> {
|
|||
|
||||
for line in src.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with('#') || line.is_empty() { continue; }
|
||||
if line.starts_with('#') || line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut parts = line.split_whitespace();
|
||||
let keyword = parts.next().unwrap_or("");
|
||||
|
|
@ -64,9 +66,9 @@ pub fn parse_obj(src: &str, color: [f32; 4]) -> Option<MeshModel> {
|
|||
}
|
||||
|
||||
// Build flat (un-indexed) vertex + normal arrays.
|
||||
let mut verts: Vec<[f32; 3]> = Vec::with_capacity(face_verts.len());
|
||||
let mut norms: Vec<[f32; 3]> = Vec::with_capacity(face_verts.len());
|
||||
let mut indices: Vec<u32> = Vec::with_capacity(face_verts.len());
|
||||
let mut verts: Vec<[f32; 3]> = Vec::with_capacity(face_verts.len());
|
||||
let mut norms: Vec<[f32; 3]> = Vec::with_capacity(face_verts.len());
|
||||
let mut indices: Vec<u32> = Vec::with_capacity(face_verts.len());
|
||||
|
||||
for (vi, (pos_i, norm_i)) in face_verts.iter().enumerate() {
|
||||
let pos = *positions.get(*pos_i).unwrap_or(&[0.0; 3]);
|
||||
|
|
@ -84,13 +86,13 @@ pub fn parse_obj(src: &str, color: [f32; 4]) -> Option<MeshModel> {
|
|||
let a = verts[tri[0] as usize];
|
||||
let b = verts[tri[1] as usize];
|
||||
let c = verts[tri[2] as usize];
|
||||
let ab = [b[0]-a[0], b[1]-a[1], b[2]-a[2]];
|
||||
let ac = [c[0]-a[0], c[1]-a[1], c[2]-a[2]];
|
||||
let nx = ab[1]*ac[2] - ab[2]*ac[1];
|
||||
let ny = ab[2]*ac[0] - ab[0]*ac[2];
|
||||
let nz = ab[0]*ac[1] - ab[1]*ac[0];
|
||||
let len = (nx*nx + ny*ny + nz*nz).sqrt().max(1e-12);
|
||||
let n = [nx/len, ny/len, nz/len];
|
||||
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
||||
let nx = ab[1] * ac[2] - ab[2] * ac[1];
|
||||
let ny = ab[2] * ac[0] - ab[0] * ac[2];
|
||||
let nz = ab[0] * ac[1] - ab[1] * ac[0];
|
||||
let len = (nx * nx + ny * ny + nz * nz).sqrt().max(1e-12);
|
||||
let n = [nx / len, ny / len, nz / len];
|
||||
norms[tri[0] as usize] = n;
|
||||
norms[tri[1] as usize] = n;
|
||||
norms[tri[2] as usize] = n;
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@
|
|||
// is needed — we shift the coordinates by (offset_x, offset_y) to place the
|
||||
// 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 crate::scene::WireModel;
|
||||
use printpdf::{
|
||||
Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PdfDocument, PdfPage,
|
||||
PdfSaveOptions, Point, Pt, Rgb,
|
||||
};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -33,7 +35,15 @@ pub fn export_pdf(
|
|||
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, plot_style);
|
||||
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())
|
||||
}
|
||||
|
|
@ -52,21 +62,38 @@ pub async fn pick_pdf_path_owned(stem: String) -> Option<std::path::PathBuf> {
|
|||
|
||||
// ── PDF builder ───────────────────────────────────────────────────────────
|
||||
|
||||
fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32, rotation_deg: i32, plot_style: Option<&PlotStyleTable>) -> 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 ops: Vec<Op> = Vec::new();
|
||||
|
||||
// White page background.
|
||||
ops.push(Op::SetFillColor {
|
||||
col: Color::Rgb(Rgb { r: 1.0, g: 1.0, b: 1.0, icc_profile: None }),
|
||||
col: Color::Rgb(Rgb {
|
||||
r: 1.0,
|
||||
g: 1.0,
|
||||
b: 1.0,
|
||||
icc_profile: None,
|
||||
}),
|
||||
});
|
||||
ops.push(Op::DrawRectangle {
|
||||
rectangle: printpdf::Rect::from_wh(Mm(paper_w).into(), Mm(paper_h).into()),
|
||||
});
|
||||
|
||||
// Round line caps/joins for CAD aesthetics.
|
||||
ops.push(Op::SetLineCapStyle { cap: LineCapStyle::Round });
|
||||
ops.push(Op::SetLineJoinStyle { join: LineJoinStyle::Round });
|
||||
ops.push(Op::SetLineCapStyle {
|
||||
cap: LineCapStyle::Round,
|
||||
});
|
||||
ops.push(Op::SetLineJoinStyle {
|
||||
join: LineJoinStyle::Round,
|
||||
});
|
||||
|
||||
// Apply rotation transform if needed.
|
||||
// PDF uses mm-based coordinate system with origin at bottom-left.
|
||||
|
|
@ -74,10 +101,10 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32,
|
|||
let needs_rotation = rotation_deg != 0;
|
||||
if needs_rotation {
|
||||
let (cos_a, sin_a, tx, ty) = match rotation_deg {
|
||||
90 => ( 0.0_f64, 1.0_f64, 0.0, paper_h as f64),
|
||||
180 => (-1.0_f64, 0.0_f64, paper_w as f64, paper_h as f64),
|
||||
270 => ( 0.0_f64, -1.0_f64, paper_w as f64, 0.0),
|
||||
_ => ( 1.0_f64, 0.0_f64, 0.0, 0.0),
|
||||
90 => (0.0_f64, 1.0_f64, 0.0, paper_h as f64),
|
||||
180 => (-1.0_f64, 0.0_f64, paper_w as f64, paper_h as f64),
|
||||
270 => (0.0_f64, -1.0_f64, paper_w as f64, 0.0),
|
||||
_ => (1.0_f64, 0.0_f64, 0.0, 0.0),
|
||||
};
|
||||
// PDF CTM: [a b c d e f] = [cos sin -sin cos tx ty]
|
||||
ops.push(Op::SaveGraphicsState);
|
||||
|
|
@ -86,9 +113,12 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32,
|
|||
let ty_pt = (ty * 2.834645) as f32;
|
||||
ops.push(Op::SetTransformationMatrix {
|
||||
matrix: printpdf::CurTransMat::Raw([
|
||||
cos_a as f32, sin_a as f32,
|
||||
-(sin_a as f32), cos_a as f32,
|
||||
tx_pt, ty_pt,
|
||||
cos_a as f32,
|
||||
sin_a as f32,
|
||||
-(sin_a as f32),
|
||||
cos_a as f32,
|
||||
tx_pt,
|
||||
ty_pt,
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
|
@ -114,9 +144,12 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32,
|
|||
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;
|
||||
r = cr;
|
||||
g = cg;
|
||||
b = cb;
|
||||
}
|
||||
lw_override = ctb.resolve_lineweight(wire.aci)
|
||||
lw_override = ctb
|
||||
.resolve_lineweight(wire.aci)
|
||||
.map(|mm| (mm * MM_TO_PT).max(0.1));
|
||||
}
|
||||
}
|
||||
|
|
@ -125,27 +158,36 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32,
|
|||
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;
|
||||
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;
|
||||
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;
|
||||
r = 0.0;
|
||||
g = 0.15;
|
||||
b = 0.50;
|
||||
}
|
||||
}
|
||||
|
||||
if last_color.map(|c| {
|
||||
(c[0]-r).abs() > 0.01 || (c[1]-g).abs() > 0.01 || (c[2]-b).abs() > 0.01
|
||||
}).unwrap_or(true) {
|
||||
if last_color
|
||||
.map(|c| (c[0] - r).abs() > 0.01 || (c[1] - g).abs() > 0.01 || (c[2] - b).abs() > 0.01)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
ops.push(Op::SetOutlineColor {
|
||||
col: Color::Rgb(Rgb { r, g, b, icc_profile: None }),
|
||||
col: Color::Rgb(Rgb {
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
icc_profile: None,
|
||||
}),
|
||||
});
|
||||
last_color = Some([r, g, b]);
|
||||
}
|
||||
|
||||
// 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));
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ use std::path::Path;
|
|||
/// 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.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,
|
||||
];
|
||||
|
||||
|
|
@ -81,8 +80,11 @@ impl PlotStyleTable {
|
|||
/// 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 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)
|
||||
|
|
@ -100,9 +102,9 @@ impl PlotStyleTable {
|
|||
/// 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]
|
||||
})
|
||||
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.
|
||||
|
|
@ -158,7 +160,9 @@ impl PlotStyleTable {
|
|||
/// (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')
|
||||
let split_at = data
|
||||
.iter()
|
||||
.position(|&b| b == b'\n')
|
||||
.map(|p| p + 1)
|
||||
.unwrap_or(0);
|
||||
let payload = &data[split_at..];
|
||||
|
|
@ -196,12 +200,9 @@ fn compress_ctb(text: &[u8]) -> Result<Vec<u8>, String> {
|
|||
|
||||
// ── 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();
|
||||
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;
|
||||
|
|
@ -225,7 +226,9 @@ fn parse_plot_style_text(
|
|||
}
|
||||
continue;
|
||||
}
|
||||
let Some(entry) = current.as_mut() else { 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();
|
||||
|
|
@ -237,10 +240,14 @@ fn parse_plot_style_text(
|
|||
}
|
||||
}
|
||||
"screen" => {
|
||||
if let Ok(v) = val.parse::<u8>() { entry.screening = v; }
|
||||
if let Ok(v) = val.parse::<u8>() {
|
||||
entry.screening = v;
|
||||
}
|
||||
}
|
||||
"lineweight" => {
|
||||
if let Ok(v) = val.parse::<u8>() { entry.lineweight = v; }
|
||||
if let Ok(v) = val.parse::<u8>() {
|
||||
entry.lineweight = v;
|
||||
}
|
||||
}
|
||||
"color1" => {
|
||||
if val.starts_with('#') && val.len() == 7 {
|
||||
|
|
@ -255,8 +262,8 @@ fn parse_plot_style_text(
|
|||
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;
|
||||
let g = ((u >> 8) & 0xFF) as u8;
|
||||
let b = (u & 0xFF) as u8;
|
||||
entry.color = Some([r, g, b]);
|
||||
}
|
||||
}
|
||||
|
|
@ -266,5 +273,10 @@ fn parse_plot_style_text(
|
|||
}
|
||||
}
|
||||
|
||||
Ok(PlotStyleTable { name, is_stb, aci_entries, named_entries })
|
||||
Ok(PlotStyleTable {
|
||||
name,
|
||||
is_stb,
|
||||
aci_entries,
|
||||
named_entries,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
//
|
||||
// The function is async so the UI remains responsive while the job is queued.
|
||||
|
||||
use crate::io::plot_style::PlotStyleTable;
|
||||
use crate::io::pdf_export;
|
||||
use crate::io::plot_style::PlotStyleTable;
|
||||
use crate::scene::WireModel;
|
||||
|
||||
/// Render `wires` to a temp PDF and dispatch it to the default system printer.
|
||||
|
|
|
|||
137
src/io/step.rs
137
src/io/step.rs
|
|
@ -27,10 +27,10 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option<String> {
|
|||
|
||||
let mut tris: Vec<Tri> = Vec::new();
|
||||
for mesh in meshes {
|
||||
let verts = &mesh.verts;
|
||||
let verts = &mesh.verts;
|
||||
let normals = &mesh.normals;
|
||||
let idx = &mesh.indices;
|
||||
let n_tri = idx.len() / 3;
|
||||
let idx = &mesh.indices;
|
||||
let n_tri = idx.len() / 3;
|
||||
for t in 0..n_tri {
|
||||
let i0 = idx[t * 3] as usize;
|
||||
let i1 = idx[t * 3 + 1] as usize;
|
||||
|
|
@ -44,13 +44,13 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option<String> {
|
|||
let n = if !normals.is_empty() && i0 < normals.len() {
|
||||
normals[i0]
|
||||
} else {
|
||||
let ab = [b[0]-a[0], b[1]-a[1], b[2]-a[2]];
|
||||
let ac = [c[0]-a[0], c[1]-a[1], c[2]-a[2]];
|
||||
let nx = ab[1]*ac[2] - ab[2]*ac[1];
|
||||
let ny = ab[2]*ac[0] - ab[0]*ac[2];
|
||||
let nz = ab[0]*ac[1] - ab[1]*ac[0];
|
||||
let len = (nx*nx + ny*ny + nz*nz).sqrt().max(f32::EPSILON);
|
||||
[nx/len, ny/len, nz/len]
|
||||
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
||||
let nx = ab[1] * ac[2] - ab[2] * ac[1];
|
||||
let ny = ab[2] * ac[0] - ab[0] * ac[2];
|
||||
let nz = ab[0] * ac[1] - ab[1] * ac[0];
|
||||
let len = (nx * nx + ny * ny + nz * nz).sqrt().max(f32::EPSILON);
|
||||
[nx / len, ny / len, nz / len]
|
||||
};
|
||||
tris.push(Tri { v: [a, b, c], n });
|
||||
}
|
||||
|
|
@ -95,18 +95,22 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option<String> {
|
|||
// Edge loop.
|
||||
let el = alloc();
|
||||
// Plane normal direction and axis placement.
|
||||
let norm_dir = alloc();
|
||||
let plane_ax = alloc();
|
||||
let plane = alloc();
|
||||
let norm_dir = alloc();
|
||||
let plane_ax = alloc();
|
||||
let plane = alloc();
|
||||
// Advanced face.
|
||||
let face_id = alloc();
|
||||
let face_id = alloc();
|
||||
face_ids.push(face_id);
|
||||
|
||||
// Emit cartesian points.
|
||||
for k in 0..3 {
|
||||
let [x, y, z] = tri.v[k];
|
||||
writeln!(data, "#{} = CARTESIAN_POINT('',({:.6},{:.6},{:.6}));",
|
||||
cp[k], x, y, z).ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = CARTESIAN_POINT('',({:.6},{:.6},{:.6}));",
|
||||
cp[k], x, y, z
|
||||
)
|
||||
.ok();
|
||||
writeln!(data, "#{} = VERTEX_POINT('',#{});", vpref[k], cp[k]).ok();
|
||||
}
|
||||
// Emit vertex points (binding).
|
||||
|
|
@ -124,52 +128,99 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option<String> {
|
|||
tri.v[k1][1] - tri.v[k][1],
|
||||
tri.v[k1][2] - tri.v[k][2],
|
||||
];
|
||||
let len = (dx*dx + dy*dy + dz*dz).sqrt().max(f32::EPSILON);
|
||||
writeln!(data, "#{} = DIRECTION('',({:.6},{:.6},{:.6}));",
|
||||
dir[k], dx/len, dy/len, dz/len).ok();
|
||||
writeln!(data, "#{} = LINE('',#{},VECTOR('',#{},1.));",
|
||||
lc[k], cp[k], dir[k]).ok();
|
||||
writeln!(data, "#{} = EDGE_CURVE('',#{},#{},#{},.T.);",
|
||||
ec[k], vpref[k], vpref[k1], lc[k]).ok();
|
||||
writeln!(data, "#{} = ORIENTED_EDGE('',*,*,#{},.T.);",
|
||||
oe[k], ec[k]).ok();
|
||||
let len = (dx * dx + dy * dy + dz * dz).sqrt().max(f32::EPSILON);
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = DIRECTION('',({:.6},{:.6},{:.6}));",
|
||||
dir[k],
|
||||
dx / len,
|
||||
dy / len,
|
||||
dz / len
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = LINE('',#{},VECTOR('',#{},1.));",
|
||||
lc[k], cp[k], dir[k]
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = EDGE_CURVE('',#{},#{},#{},.T.);",
|
||||
ec[k], vpref[k], vpref[k1], lc[k]
|
||||
)
|
||||
.ok();
|
||||
writeln!(data, "#{} = ORIENTED_EDGE('',*,*,#{},.T.);", oe[k], ec[k]).ok();
|
||||
}
|
||||
|
||||
// Edge loop and face normal.
|
||||
writeln!(data, "#{} = EDGE_LOOP('',({},{},{}));",
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = EDGE_LOOP('',({},{},{}));",
|
||||
el,
|
||||
format!("#{}", oe[0]),
|
||||
format!("#{}", oe[1]),
|
||||
format!("#{}", oe[2])).ok();
|
||||
format!("#{}", oe[2])
|
||||
)
|
||||
.ok();
|
||||
|
||||
let [nx, ny, nz] = tri.n;
|
||||
writeln!(data, "#{} = DIRECTION('',({:.6},{:.6},{:.6}));",
|
||||
norm_dir, nx, ny, nz).ok();
|
||||
writeln!(data, "#{} = AXIS2_PLACEMENT_3D('',#{},#{},#{});",
|
||||
plane_ax, cp[0], norm_dir, dir[0]).ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = DIRECTION('',({:.6},{:.6},{:.6}));",
|
||||
norm_dir, nx, ny, nz
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = AXIS2_PLACEMENT_3D('',#{},#{},#{});",
|
||||
plane_ax, cp[0], norm_dir, dir[0]
|
||||
)
|
||||
.ok();
|
||||
writeln!(data, "#{} = PLANE('',#{});", plane, plane_ax).ok();
|
||||
writeln!(data, "#{} = ADVANCED_FACE('',(FACE_BOUND('',#{},.T.)),#{},.T.);",
|
||||
face_id, el, plane).ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = ADVANCED_FACE('',(FACE_BOUND('',#{},.T.)),#{},.T.);",
|
||||
face_id, el, plane
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Closed shell wrapping all faces.
|
||||
let shell_id = alloc();
|
||||
let face_list: String = face_ids.iter().map(|id| format!("#{id}")).collect::<Vec<_>>().join(",");
|
||||
let face_list: String = face_ids
|
||||
.iter()
|
||||
.map(|id| format!("#{id}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
writeln!(data, "#{} = CLOSED_SHELL('',({face_list}));", shell_id).ok();
|
||||
|
||||
// Manifold solid B-rep.
|
||||
let msb_id = alloc();
|
||||
writeln!(data, "#{} = MANIFOLD_SOLID_BREP('H7CAD_Solid',#{});", msb_id, shell_id).ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = MANIFOLD_SOLID_BREP('H7CAD_Solid',#{});",
|
||||
msb_id, shell_id
|
||||
)
|
||||
.ok();
|
||||
|
||||
// Shape representation.
|
||||
let sr_id = alloc();
|
||||
let pu_id = alloc();
|
||||
let gc_id = alloc();
|
||||
writeln!(data, "#{} = (LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));", pu_id).ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = (LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));",
|
||||
pu_id
|
||||
)
|
||||
.ok();
|
||||
writeln!(data, "#{} = GEOMETRIC_REPRESENTATION_CONTEXT(3);", gc_id).ok();
|
||||
writeln!(data,
|
||||
writeln!(
|
||||
data,
|
||||
"#{sr_id} = SHAPE_REPRESENTATION('H7CAD_Shape',(#{}),#{gc_id});",
|
||||
msb_id).ok();
|
||||
msb_id
|
||||
)
|
||||
.ok();
|
||||
|
||||
// ── Assemble file ─────────────────────────────────────────────────────
|
||||
let ts = chrono_timestamp();
|
||||
|
|
@ -199,16 +250,16 @@ fn chrono_timestamp() -> String {
|
|||
.unwrap_or(0);
|
||||
// Format: YYYY-MM-DDTHH:MM:SS (approximate UTC from epoch seconds).
|
||||
let s = secs;
|
||||
let mins = s / 60;
|
||||
let mins = s / 60;
|
||||
let hours = mins / 60;
|
||||
let days = hours / 24;
|
||||
let days = hours / 24;
|
||||
let hh = hours % 24;
|
||||
let mm = mins % 60;
|
||||
let ss = s % 60;
|
||||
// Days since epoch → year/month/day (approximate, ignoring leap years).
|
||||
let year = 1970 + days / 365;
|
||||
let doy = days % 365;
|
||||
let year = 1970 + days / 365;
|
||||
let doy = days % 365;
|
||||
let month = doy / 30 + 1;
|
||||
let day = doy % 30 + 1;
|
||||
let day = doy % 30 + 1;
|
||||
format!("{year:04}-{month:02}-{day:02}T{hh:02}:{mm:02}:{ss:02}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ pub fn build_stl(meshes: &[&MeshModel]) -> Option<Vec<u8>> {
|
|||
[nx / len, ny / len, nz / len]
|
||||
};
|
||||
|
||||
tris.push(Tri { normal, v: [a, b, c] });
|
||||
tris.push(Tri {
|
||||
normal,
|
||||
v: [a, b, c],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,9 +108,9 @@ fn resolve_path(raw: &str, base_dir: &Path) -> Option<PathBuf> {
|
|||
/// Make sure `doc` has BLOCK + ENDBLK entities for `block_name`.
|
||||
/// These are required so renderers can find the block content.
|
||||
fn ensure_block_entities(doc: &mut CadDocument, block_name: &str) {
|
||||
let has_block = doc.entities().any(|e| {
|
||||
matches!(e, EntityType::Block(b) if b.name == block_name)
|
||||
});
|
||||
let has_block = doc
|
||||
.entities()
|
||||
.any(|e| matches!(e, EntityType::Block(b) if b.name == block_name));
|
||||
if has_block {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue