cargo fmt

This commit is contained in:
Hakan 2026-05-12 10:36:15 +03:00
commit ca07268c7c
204 changed files with 13988 additions and 5516 deletions

View file

@ -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,

View file

@ -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;

View file

@ -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);

View file

@ -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,
})
}

View file

@ -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.

View file

@ -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}")
}

View file

@ -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],
});
}
}

View file

@ -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;
}