forked from 1b-agent/opencadstudio-upstream
feat(automation): entity, block, publishing and REST operations from the fork
The operations docs/automation/API-SPEC.md (PR #1383) describes, brought over from huaninstratech/OpenCADStudio so the specification no longer runs ahead of the code: - entity CRUD: entities_create, entities_delete, entities_transform, entities_copy_to, and `where` filters on query - blocks and export: block_define, block_delete, a normalised wblock - publishing: layout_create, page_setup_set, plot (per_page included) - metadata: sysvar, xdata_set / xdata_get, file_identity - collections: group_create, selection_set_save / load - control plane: batch, events, view_focus, close - the REST transport: `--http <port>`, the /api/v1 routes and their OpenAPI 3 description - person-in-the-loop picks: user_select and getpoint, hosted by the GUI over the same REST routes, with on-screen pending signals Taken from the fork's commits b7a7ed80, 69396b42, 64f1cf6a, c3aafc8e, bb5a7bfa, 52317a5c, 50d293ba, b90e194f, 517c6397, 53a5c4c9, a49ac045, 37de5a46, a78b1822, 6d9aed2a, 33f522da, 5b5cafe2, 02a7e78d, 94df40dc, fd5bc14d, 485fac61 and 2c5fc994. Left out on purpose: 37de5a46 also removed the donation surface (the Patreon fetch, the donation prompt, the Start-page supporters rail, the DONATE command and its locale strings) and replaced the About dialog's call to action; that is the fork's own product decision and stays out. The fork's version bump and its unrelated IFC/BIM work are not part of this. Adapted while porting: the REST server and the GUI bridge are desktop-only and a template start is refused in the web build, so the wasm target still compiles; the two plot tests read the PDF through the shared stream helper, because exported streams are Flate-compressed; the benches set the new crosshair field; comments and docs describe each operation in its own terms rather than by another product's API names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
531ba44faa
commit
279a87418e
37 files changed
+8795
-82
No files matched your search
@@ -1673,6 +1673,37 @@ fn emit_text(
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything searchable in an exported file: the raw bytes (printpdf
|
||||
/// leaves small content streams uncompressed) plus any zlib streams that
|
||||
/// decode to mostly-printable text, so the assertions hold either way.
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
pub(crate) fn pdf_stream_text(bytes: &[u8]) -> String {
|
||||
use std::io::Read as _;
|
||||
let mut text = String::from_utf8_lossy(bytes).into_owned();
|
||||
for start in 0..bytes.len().saturating_sub(2) {
|
||||
// zlib streams start with a 2-byte header: deflate method, valid check.
|
||||
let (cmf, flg) = (bytes[start], bytes[start + 1]);
|
||||
if cmf & 0x0f != 8 || ((cmf as u16) << 8 | flg as u16) % 31 != 0 {
|
||||
continue;
|
||||
}
|
||||
let mut decoded = Vec::new();
|
||||
if flate2::read::ZlibDecoder::new(&bytes[start..])
|
||||
.read_to_end(&mut decoded)
|
||||
.is_ok()
|
||||
&& decoded.len() > 32
|
||||
{
|
||||
let printable = decoded
|
||||
.iter()
|
||||
.filter(|&&b| matches!(b, b'\n' | b'\r' | b'\t' | 32..=126))
|
||||
.count();
|
||||
if printable * 10 >= decoded.len() * 9 {
|
||||
text.push_str(&String::from_utf8_lossy(&decoded));
|
||||
}
|
||||
}
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1730,6 +1761,116 @@ mod tests {
|
||||
assert!(bytes.len() > 200, "suspiciously small: {}", bytes.len());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn export_fallback_plot_style_recolors_wires() {
|
||||
// ACI 1 carrying pure device red: without a style it must export as an
|
||||
// RGB operator; the monochrome fallback must recolor it. Guards the
|
||||
// automation plot path, where the per-page style can be dropped.
|
||||
let make_page = || {
|
||||
let mut wire = PlotWire {
|
||||
wire: WireModel::solid(
|
||||
"test".into(),
|
||||
vec![[0.0, 0.0, 0.0], [50.0, 50.0, 0.0]],
|
||||
[1.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
),
|
||||
draw_depth: 0.0,
|
||||
};
|
||||
wire.wire.aci = 1;
|
||||
test_page(vec![wire])
|
||||
};
|
||||
|
||||
let unstyled = pdf_stream_text(&build_pdf_pages(&[make_page()], None).unwrap());
|
||||
let monochrome =
|
||||
PlotStyleTable::builtin("monochrome.ctb").expect("shipped monochrome.ctb parses");
|
||||
let styled =
|
||||
pdf_stream_text(&build_pdf_pages(&[make_page()], Some(&monochrome)).unwrap());
|
||||
|
||||
assert!(
|
||||
unstyled.contains("1 0 0 rg") || unstyled.contains("1 0 0 RG"),
|
||||
"unstyled export should keep the wire's RGB color"
|
||||
);
|
||||
assert!(
|
||||
!styled.contains("1 0 0 rg") && !styled.contains("1 0 0 RG"),
|
||||
"monochrome fallback must recolor the wire"
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse every stroke-width operator (`<pt> w`) out of the content text.
|
||||
fn stroke_widths(text: &str) -> Vec<f32> {
|
||||
let tokens: Vec<&str> = text.split_whitespace().collect();
|
||||
tokens
|
||||
.windows(2)
|
||||
.filter_map(|pair| {
|
||||
if pair[1] == "w" {
|
||||
pair[0].parse::<f32>().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn distinct_widths(mut widths: Vec<f32>) -> Vec<f32> {
|
||||
widths.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
widths.dedup_by(|a, b| (*a - *b).abs() < 0.01);
|
||||
widths
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plot_honors_per_object_lineweight_with_and_without_style() {
|
||||
// Two objects whose resolved lineweights differ (as two layers set to
|
||||
// 0.13 mm and 0.50 mm produce): the PDF must carry two distinct pen
|
||||
// widths, and a plot style that leaves weights at "use object" must
|
||||
// keep that hierarchy.
|
||||
let make_page = |px: f32| {
|
||||
let mut wire = PlotWire {
|
||||
wire: WireModel::solid(
|
||||
"test".into(),
|
||||
vec![[0.0, 0.0, 0.0], [50.0, 50.0, 0.0]],
|
||||
[1.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
),
|
||||
draw_depth: 0.0,
|
||||
};
|
||||
wire.wire.aci = 7;
|
||||
wire.wire.line_weight_px = px;
|
||||
test_page(vec![wire])
|
||||
};
|
||||
|
||||
let monochrome =
|
||||
PlotStyleTable::builtin("monochrome.ctb").expect("shipped monochrome.ctb parses");
|
||||
eprintln!(
|
||||
"monochrome resolve_lineweight(7) = {:?}",
|
||||
monochrome.resolve_lineweight(7)
|
||||
);
|
||||
|
||||
let unstyled = stroke_widths(&pdf_stream_text(
|
||||
&build_pdf_pages(&[make_page(1.0), make_page(3.0)], None).unwrap(),
|
||||
));
|
||||
let unstyled = distinct_widths(unstyled);
|
||||
assert_eq!(unstyled.len(), 2, "two object weights, two pens: {unstyled:?}");
|
||||
assert!(
|
||||
(unstyled[1] / unstyled[0] - 3.0).abs() < 0.1,
|
||||
"pen widths follow the object weights 1px:3px: {unstyled:?}"
|
||||
);
|
||||
|
||||
let styled = stroke_widths(&pdf_stream_text(
|
||||
&build_pdf_pages(&[make_page(1.0), make_page(3.0)], Some(&monochrome)).unwrap(),
|
||||
));
|
||||
let styled = distinct_widths(styled);
|
||||
assert_eq!(
|
||||
styled.len(),
|
||||
2,
|
||||
"monochrome.ctb leaves weights at 'use object', so the two weights survive: {styled:?}"
|
||||
);
|
||||
assert!(
|
||||
(styled[1] / styled[0] - 3.0).abs() < 0.1,
|
||||
"weight hierarchy matches the unstyled plot: {styled:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Build a WireModel carrying the SDF glyph quads for `text` in the embedded
|
||||
// "txt" stroke font, laid out into the process-wide atlas emit_text reads.
|
||||
fn text_wire(text: &str, origin: [f64; 3]) -> PlotWire {
|
||||
|
||||
Reference in new issue
Block a user