forked from 1b-agent/opencadstudio-upstream
The codec and kernel repositories and crates are renamed opencadcodec and opencadkernel. Inside OCS they are depended on under short keys, so code reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node graph engine). File-format and protocol spellings stay as they were: the ACIS product id `acadrust`, the ACADRUST_* switches and the plugin version keys `acadrust_version` / `acadrust_source`. A button left of MCP on the command line opens a translucent node graph over the viewport, with a palette of object, input, math, logic, list, text and geometry nodes added by click or drag. An object node owns drawing entities and its rows are the Properties panel's own rows for them: the single-object page is extracted from refresh_properties as entity_property_sections, values use the control protocol's property_json, and writes go through set_property_value with a property target override, so the panel, MCP and the graph share one read/write path. A list reaching an object row makes one entity per element. Each graph action is one undo step; the canvas pans and zooms. Co-Authored-By: Claude Opus 5.5 <[email protected]>
198 lines
6.5 KiB
Rust
198 lines
6.5 KiB
Rust
use codec::entities::{Insert, MText};
|
|
use codec::tables::{BlockRecord, TextStyle};
|
|
use codec::types::Vector3;
|
|
use codec::{CadDocument, EntityType, Handle};
|
|
use OpenCADStudio::scene::cache::block_cache::{expand_insert, BlockCache};
|
|
use OpenCADStudio::scene::view::render::InheritStyle;
|
|
use OpenCADStudio::scene::WireModel;
|
|
|
|
fn drawable_point_count(wires: &[WireModel]) -> usize {
|
|
// SDF text carries glyph quads on `text_verts` (no stroke points/fills), so
|
|
// count those too — otherwise "did the text render?" reads as zero.
|
|
wires
|
|
.iter()
|
|
.map(|w| {
|
|
w.points.iter().filter(|p| p[0].is_finite()).count()
|
|
+ w.fill_tris.len()
|
|
+ w.text_verts.len()
|
|
})
|
|
.sum()
|
|
}
|
|
|
|
/// Build a one-block document holding a single MTEXT and expand the INSERT
|
|
/// through the block cache, returning the finalized wires. `mtext_at` is the
|
|
/// MTEXT's position inside the block; `insert_at` is where the block is placed.
|
|
fn expand_block_mtext(
|
|
value: &str,
|
|
font_file: &str,
|
|
mtext_at: Vector3,
|
|
insert_at: Vector3,
|
|
) -> Vec<WireModel> {
|
|
let mut doc = CadDocument::new();
|
|
|
|
let mut style = TextStyle::new("SHOP");
|
|
style.font_file = font_file.to_string();
|
|
doc.text_styles.add(style).unwrap();
|
|
|
|
let br_h = Handle::new(doc.next_handle());
|
|
let mut br = BlockRecord::new("LABEL_BLOCK");
|
|
br.handle = br_h;
|
|
doc.block_records.add(br).unwrap();
|
|
|
|
let mut mtext = MText::with_value(value, mtext_at);
|
|
mtext.style = "SHOP".to_string();
|
|
mtext.height = 20.0;
|
|
mtext.rectangle_width = 0.0;
|
|
let mut sub = EntityType::MText(mtext);
|
|
sub.common_mut().owner_handle = br_h;
|
|
doc.add_entity(sub).unwrap();
|
|
|
|
let ins = Insert::new("LABEL_BLOCK", insert_at);
|
|
doc.add_entity(EntityType::Insert(ins.clone())).unwrap();
|
|
let cache = BlockCache::build(
|
|
&doc,
|
|
1.0,
|
|
None,
|
|
true,
|
|
[0.0, 0.0, 0.0, 1.0],
|
|
None,
|
|
&Default::default(),
|
|
);
|
|
expand_insert(
|
|
&doc,
|
|
&cache,
|
|
&ins,
|
|
Handle::new(999),
|
|
[1.0, 1.0, 1.0, 1.0],
|
|
0,
|
|
0.0,
|
|
[0.0; 8],
|
|
1.0,
|
|
// The INSERT sits on layer "0" which resolves to the white/Continuous
|
|
// fallback — matching the resolved style passed above.
|
|
InheritStyle {
|
|
color: [1.0, 1.0, 1.0, 1.0],
|
|
pat_len: 0.0,
|
|
pat: [0.0; 8],
|
|
lw_px: 1.0,
|
|
},
|
|
0,
|
|
false,
|
|
false,
|
|
1.0,
|
|
None,
|
|
None,
|
|
false,
|
|
[0.0, 0.0, 0.0, 1.0],
|
|
// Annotation scale: 1.0 = no annotative scaling, matching the
|
|
// `BlockCache::build(&doc, 1.0, ...)` call above.
|
|
1.0,
|
|
OpenCADStudio::scene::BlockScalePolicy::FromInsert,
|
|
false,
|
|
)
|
|
.expect("block defn is cached")
|
|
}
|
|
|
|
#[test]
|
|
fn block_nested_mtext_uses_its_style_font() {
|
|
// NB: on a host without Arial, `arial.ttf` resolves to the LFF stroke
|
|
// fallback, so this exercises the "something renders" path rather than the
|
|
// TTF canonicalisation specifically. The font-canonicalisation logic itself
|
|
// is covered by the unit tests in `text_support`; the colour-split and
|
|
// fill-pairing tests below cover the block-cache rendering changes.
|
|
let wires = expand_block_mtext(
|
|
"FERRAGAMO",
|
|
"arial.ttf",
|
|
Vector3::new(0.0, 0.0, 0.0),
|
|
Vector3::new(100.0, 50.0, 0.0),
|
|
);
|
|
|
|
assert!(
|
|
drawable_point_count(&wires) > 0,
|
|
"block-nested MTEXT should render through its text style font"
|
|
);
|
|
|
|
assert!(
|
|
wires
|
|
.iter()
|
|
.all(|w| w.points.is_empty() || w.fill_tris.is_empty()),
|
|
"outline and fill wires should be separate for correct GPU classification"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn block_nested_colour_split_mtext_keeps_per_wire_colour() {
|
|
// `\C1;` = ACI red, `\C2;` = ACI yellow → two colour bins. The block cache
|
|
// must keep them as separate per-glyph colours; the fold-to-one-colour bug
|
|
// (PR #301, Kevin Griffin) collapsed every segment to the inherited colour.
|
|
// SDF text carries per-glyph colour on `text_verts`, so the split is checked
|
|
// there (not per-wire). Uses the builtin stroke font so it works without TTF.
|
|
let wires = expand_block_mtext(
|
|
"\\C1;AAA\\C2;BBB",
|
|
"txt",
|
|
Vector3::new(0.0, 0.0, 0.0),
|
|
Vector3::new(100.0, 50.0, 0.0),
|
|
);
|
|
|
|
let mut colours: Vec<[u8; 3]> = wires
|
|
.iter()
|
|
.flat_map(|w| w.text_verts.iter())
|
|
.map(|v| {
|
|
[
|
|
(v.color[0] * 255.0).round() as u8,
|
|
(v.color[1] * 255.0).round() as u8,
|
|
(v.color[2] * 255.0).round() as u8,
|
|
]
|
|
})
|
|
.collect();
|
|
colours.sort();
|
|
colours.dedup();
|
|
|
|
assert!(
|
|
colours.len() >= 2,
|
|
"colour-split MTEXT in a block must keep ≥2 distinct glyph colours, got {colours:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn block_nested_mtext_fill_tris_keep_paired_low_half_at_utm() {
|
|
// TTF glyph fills are host-dependent (builtin LFF stroke fonts produce
|
|
// none). When the host resolves a TTF and fills ARE produced, every fill
|
|
// wire must carry an index-paired `fill_tris_low`: emit_wire reconstructs
|
|
// `fill_tris[i] + fill_tris_low[i]`, so an unpaired wire panics (release
|
|
// too) and a dropped low half quantizes fills to ~0.5 m at UTM scale.
|
|
// The MTEXT sits at a UTM-scale coordinate so the low half is significant.
|
|
// "DejaVu Sans" is present on most Linux hosts; where no TTF resolves the
|
|
// fill set is empty and the assertions are skipped (gate below).
|
|
let wires = expand_block_mtext(
|
|
"FERRAGAMO",
|
|
"DejaVu Sans",
|
|
Vector3::new(500_000.0, 4_000_000.0, 0.0),
|
|
Vector3::new(0.0, 0.0, 0.0),
|
|
);
|
|
|
|
let fill_wires: Vec<&WireModel> = wires.iter().filter(|w| !w.fill_tris.is_empty()).collect();
|
|
if fill_wires.is_empty() {
|
|
eprintln!("no TTF fills resolvable on this host; skipping fill-pairing assertions");
|
|
return;
|
|
}
|
|
|
|
for w in &fill_wires {
|
|
assert_eq!(
|
|
w.fill_tris.len(),
|
|
w.fill_tris_low.len(),
|
|
"fill_tris and fill_tris_low must be index-paired for emit_wire"
|
|
);
|
|
}
|
|
|
|
let any_nonzero_low = fill_wires
|
|
.iter()
|
|
.flat_map(|w| w.fill_tris_low.iter())
|
|
.any(|p| p.iter().any(|&c| c != 0.0));
|
|
assert!(
|
|
any_nonzero_low,
|
|
"UTM-scale fills must keep a non-zero low half — a dropped fill_tris_low \
|
|
silently quantizes fill triangles to ~0.5 m"
|
|
);
|
|
}
|