app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
Drew Owens
0.3.3 source + crate build: BOM exported from the SOLIDWORKS assembly, build log §1–11, release script
477e64f
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
//! BOM assembly: the com host returns raw component rows; the SERVER turns them
//! into the deliverable.
//!
//! That split is deliberate. The host process is doing single-threaded COM work
//! against a live application — the less time it spends sorting strings and
//! building CSV, the shorter the window in which SOLIDWORKS is blocked. So the
//! host walks the tree and the server does the paperwork: sort by name, work out
//! the union of custom-property names (they differ per component — that union is
//! the column set), and write RFC 4180 CSV.
use serde_json::Value;
/// Rows sorted by name, case-insensitively — the order a human reads a BOM in.
pub fn sort_rows(rows: &mut [Value]) {
rows.sort_by_key(|r| r.get("name").and_then(|v| v.as_str()).unwrap_or("").to_lowercase());
}
/// The union of every row's custom-property names, sorted: the CSV columns.
pub fn prop_columns(rows: &[Value]) -> Vec<String> {
let mut names: Vec<String> = rows
.iter()
.filter_map(|r| r.get("properties").and_then(|p| p.as_object()))
.flat_map(|p| p.keys().cloned())
.collect();
names.sort();
names.dedup();
names
}
/// A property's display value: `resolved` (what SOLIDWORKS evaluates the
/// expression to) beats the raw `value`; missing is empty.
fn prop_cell(row: &Value, name: &str) -> String {
let p = row.get("properties").and_then(|p| p.get(name));
for key in ["resolved", "value"] {
if let Some(v) = p.and_then(|p| p.get(key)) {
let s = cell(v);
if !s.is_empty() {
return s;
}
}
}
String::new()
}
fn cell(v: &Value) -> String {
match v {
Value::Null => String::new(),
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
fn escape(field: &str) -> String {
if field.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", field.replace('"', "\"\""))
} else {
field.to_string()
}
}
fn write_row(out: &mut String, fields: &[String]) {
out.push_str(&fields.iter().map(|f| escape(f)).collect::<Vec<_>>().join(","));
out.push_str("\r\n");
}
/// The whole CSV: fixed columns first, then one column per custom property.
pub fn to_csv(rows: &[Value], prop_names: &[String]) -> String {
let mut out = String::new();
let mut header: Vec<String> =
["item", "name", "configuration", "quantity", "file"].iter().map(|s| s.to_string()).collect();
header.extend(prop_names.iter().cloned());
write_row(&mut out, &header);
for (i, row) in rows.iter().enumerate() {
let mut fields = vec![
(i + 1).to_string(),
cell(row.get("name").unwrap_or(&Value::Null)),
cell(row.get("configuration").unwrap_or(&Value::Null)),
cell(row.get("quantity").unwrap_or(&Value::Null)),
cell(row.get("file").unwrap_or(&Value::Null)),
];
fields.extend(prop_names.iter().map(|p| prop_cell(row, p)));
write_row(&mut out, &fields);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn rows() -> Vec<Value> {
vec![
json!({"name": "shaft", "configuration": "Long", "quantity": 1,
"file": "C:\\m\\shaft.sldprt",
"properties": {"PartNo": {"value": "SHF-220", "resolved": "SHF-220"}}}),
json!({"name": "Bracket", "configuration": "Default", "quantity": 2,
"file": "C:\\m\\bracket.sldprt",
"properties": {"PartNo": {"value": "BRK-100", "resolved": "BRK-100"},
"Material": {"value": "6061-T6", "resolved": "6061-T6"}}}),
]
}
#[test]
fn sorts_case_insensitively_by_name() {
let mut r = rows();
sort_rows(&mut r);
assert_eq!(r[0]["name"], "Bracket");
}
#[test]
fn columns_are_the_sorted_union_of_property_names() {
assert_eq!(prop_columns(&rows()), vec!["Material".to_string(), "PartNo".to_string()]);
}
#[test]
fn csv_has_a_header_item_numbers_and_blank_cells_for_absent_props() {
let mut r = rows();
sort_rows(&mut r);
let cols = prop_columns(&r);
let csv = to_csv(&r, &cols);
let lines: Vec<&str> = csv.lines().collect();
assert_eq!(lines[0], "item,name,configuration,quantity,file,Material,PartNo");
assert_eq!(lines[1], "1,Bracket,Default,2,C:\\m\\bracket.sldprt,6061-T6,BRK-100");
// shaft has no Material -> empty cell, and keeps its item number
assert_eq!(lines[2], "2,shaft,Long,1,C:\\m\\shaft.sldprt,,SHF-220");
assert!(csv.ends_with("\r\n"), "RFC 4180 line endings");
}
#[test]
fn fields_with_commas_and_quotes_are_escaped() {
let r = vec![json!({
"name": "odd, part", "configuration": "\"quoted\"", "quantity": 1, "file": "x",
"properties": {"Note": {"resolved": "a,b"}}
})];
let csv = to_csv(&r, &["Note".to_string()]);
assert!(csv.contains("\"odd, part\""), "{csv}");
assert!(csv.contains("\"\"\"quoted\"\"\""), "{csv}");
assert!(csv.contains("\"a,b\""), "{csv}");
}
#[test]
fn resolved_wins_over_value_and_missing_is_blank() {
let r = vec![json!({
"name": "n", "quantity": 1, "file": "f", "configuration": "",
"properties": {"A": {"value": "raw", "resolved": "evaluated"}, "B": {}}
})];
let csv = to_csv(&r, &["A".to_string(), "B".to_string()]);
assert!(csv.contains("evaluated,"), "{csv}");
assert!(csv.trim_end().ends_with("evaluated,"), "{csv}");
}
}