forked from 1b-agent/opencadstudio-upstream
feat(plugin): harden V4 host integration
Preserve legacy wire indices and route concurrent V4 responses and background requests by correlation and stable tab ID.
This commit is contained in:
commit
f247d15f6e
110 files changed
+32159
-1254
No files matched your search
+168
-62
@@ -306,6 +306,14 @@ impl Scene {
|
||||
handle
|
||||
}
|
||||
|
||||
pub fn rename_layer(&mut self, old: &str, new: &str) -> bool {
|
||||
if self.document.rename_layer(old, new).is_err() {
|
||||
return false;
|
||||
}
|
||||
self.invalidate_dependency_index();
|
||||
true
|
||||
}
|
||||
|
||||
/// Rename a block definition: re-key its record, update the Block marker's
|
||||
/// name, and repoint every INSERT that referenced the old name so all
|
||||
/// instances keep resolving. Returns false if `old` is missing or
|
||||
@@ -737,7 +745,12 @@ impl Scene {
|
||||
|
||||
let mut insert = DxfInsert::new(name, acadrust::types::Vector3::ZERO);
|
||||
acadrust::Entity::apply_transform(&mut insert, block_to_world);
|
||||
Ok(self.add_entity(EntityType::Insert(insert)))
|
||||
let insert_handle = self.add_entity(EntityType::Insert(insert));
|
||||
// A new block definition landed in the document; advance the block
|
||||
// epoch so consumers (the block palette stale check) notice it even
|
||||
// when the panel stays open. Mirrors `define_block_from_owned_entities`.
|
||||
self.bump_geometry();
|
||||
Ok(insert_handle)
|
||||
}
|
||||
|
||||
/// Define a new block named `name` from `entities` (owned, not yet in the
|
||||
@@ -1401,6 +1414,8 @@ impl Scene {
|
||||
render_instance,
|
||||
boundary: Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
pattern: model::hatch_model::HatchPattern::Solid,
|
||||
name: "WIPEOUT_FILL".into(),
|
||||
color,
|
||||
@@ -1505,6 +1520,7 @@ impl Scene {
|
||||
}
|
||||
|
||||
let mut boundary: Vec<[f64; 2]> = Vec::new();
|
||||
let mut boundary_exterior = Vec::new();
|
||||
|
||||
for path in &dxf.paths {
|
||||
// Skip TEXTBOX boundary paths (flag bit 3). These are text
|
||||
@@ -1538,6 +1554,7 @@ impl Scene {
|
||||
boundary.truncate(before_path);
|
||||
continue;
|
||||
}
|
||||
boundary_exterior.push(path.flags.is_external() || path.flags.is_outermost());
|
||||
if boundary.len() >= path_start + 3 {
|
||||
let first = boundary[path_start];
|
||||
let last = *boundary.last().unwrap();
|
||||
@@ -1550,6 +1567,11 @@ impl Scene {
|
||||
if boundary.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if !boundary_exterior.iter().any(|role| *role) {
|
||||
if let Some(first) = boundary_exterior.first_mut() {
|
||||
*first = true;
|
||||
}
|
||||
}
|
||||
// The batched hatch renderer keeps boundaries in a GPU storage
|
||||
// buffer (no fixed length), so a hatch with many island loops must
|
||||
// retain *every* loop or even-odd island detection breaks. The old
|
||||
@@ -1744,6 +1766,14 @@ impl Scene {
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(boundary_f32),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)),
|
||||
boundary_sources: Some(std::sync::Arc::new(
|
||||
dxf.paths
|
||||
.iter()
|
||||
.filter(|path| path.flags.bits() & 8 == 0)
|
||||
.map(|path| path.boundary_handles.clone())
|
||||
.collect(),
|
||||
)),
|
||||
pattern,
|
||||
name,
|
||||
// A gradient starts from its first stop; other fills use the
|
||||
@@ -2058,6 +2088,8 @@ impl Scene {
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
pattern: model::hatch_model::HatchPattern::Solid,
|
||||
name: "SOLID".into(),
|
||||
color,
|
||||
@@ -2070,79 +2102,148 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_hatch(&mut self, model: HatchModel, layer: Option<&str>) -> Handle {
|
||||
pub fn add_hatch(
|
||||
&mut self,
|
||||
model: HatchModel,
|
||||
layer: Option<&str>,
|
||||
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
|
||||
) -> Handle {
|
||||
let mut dxf = DxfHatch::new();
|
||||
dxf.is_solid = matches!(
|
||||
model.pattern,
|
||||
crate::scene::model::hatch_model::HatchPattern::Solid
|
||||
);
|
||||
// Prefer the command-supplied exact f64 boundary so a typed vertex is
|
||||
// persisted without f32 quantization (issue #311). Falling back to the
|
||||
// render-side `boundary`, the points arrive in local render space
|
||||
// (world_offset already subtracted), so add `world_origin` back —
|
||||
// otherwise the boundary wire lands `world_offset` away from the fill.
|
||||
// Build one DXF boundary path per ring. The command layer separates
|
||||
// the outer boundary from its holes with NaN sentinels in
|
||||
// `boundary_wcs`; split on those so nested hatches (e.g. a small
|
||||
// rectangle inside a big one) persist with real holes instead of a
|
||||
// single self-intersecting polyline. Only the FIRST ring carries the
|
||||
// external / outermost flags — every later ring is a hole and must not
|
||||
// be flagged external, or DXF/DWG consumers treat the inner loop as
|
||||
// another outer island rather than a hole.
|
||||
if let Some(wcs) = &model.boundary_wcs {
|
||||
let mut ring: Vec<Vector2> = Vec::new();
|
||||
let mut first = true;
|
||||
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool| {
|
||||
if !r.is_empty() {
|
||||
let edge = PolylineEdge::new(std::mem::take(r), true);
|
||||
let mut path = if is_outer {
|
||||
let mut p = BoundaryPath::external();
|
||||
p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits(
|
||||
p.flags.bits()
|
||||
| acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(),
|
||||
);
|
||||
p
|
||||
} else {
|
||||
BoundaryPath::new()
|
||||
};
|
||||
path.add_edge(BoundaryEdge::Polyline(edge));
|
||||
dxf.paths.push(path);
|
||||
}
|
||||
};
|
||||
for &[x, y] in wcs.iter() {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
ring.push(Vector2::new(x, y));
|
||||
} else {
|
||||
let is_outer = first;
|
||||
first = false;
|
||||
push_ring(&mut ring, is_outer);
|
||||
}
|
||||
}
|
||||
push_ring(&mut ring, first);
|
||||
}
|
||||
if dxf.paths.is_empty() {
|
||||
let wx = model.world_origin[0];
|
||||
let wy = model.world_origin[1];
|
||||
let verts: Vec<Vector2> = model
|
||||
// Prefer exact command geometry; otherwise reconstruct every ring from
|
||||
// the render offsets without dropping its separators.
|
||||
// Build one DXF path per NaN-separated ring and retain each outer/hole role.
|
||||
let reconstructed_wcs: Vec<[f64; 2]> = if model.boundary_wcs.is_none() {
|
||||
let [wx, wy] = model.world_origin;
|
||||
model
|
||||
.boundary
|
||||
.iter()
|
||||
.filter(|v| v[0].is_finite() && v[1].is_finite())
|
||||
.map(|&[x, y]| Vector2::new(x as f64 + wx, y as f64 + wy))
|
||||
.collect();
|
||||
let edge = PolylineEdge::new(verts, true);
|
||||
let mut path = BoundaryPath::external();
|
||||
path.add_edge(BoundaryEdge::Polyline(edge));
|
||||
dxf.paths.push(path);
|
||||
.map(|&[x, y]| {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
[x as f64 + wx, y as f64 + wy]
|
||||
} else {
|
||||
[f64::NAN, f64::NAN]
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let wcs = model
|
||||
.boundary_wcs
|
||||
.as_deref()
|
||||
.map(|points| points.as_slice())
|
||||
.unwrap_or(reconstructed_wcs.as_slice());
|
||||
let mut ring: Vec<Vector2> = Vec::new();
|
||||
let mut first = true;
|
||||
let mut ring_index = 0usize;
|
||||
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
|
||||
if !r.is_empty() {
|
||||
let edge = PolylineEdge::new(std::mem::take(r), true);
|
||||
let mut path = if is_outer {
|
||||
let mut p = BoundaryPath::external();
|
||||
p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits(
|
||||
p.flags.bits()
|
||||
| acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(),
|
||||
);
|
||||
p
|
||||
} else {
|
||||
BoundaryPath::new()
|
||||
};
|
||||
path.add_edge(BoundaryEdge::Polyline(edge));
|
||||
if let Some(handles) = model
|
||||
.boundary_sources
|
||||
.as_deref()
|
||||
.and_then(|sources| sources.get(index))
|
||||
{
|
||||
for &handle in handles {
|
||||
if handle.is_valid() {
|
||||
path.add_boundary_handle(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
dxf.paths.push(path);
|
||||
}
|
||||
};
|
||||
for &[x, y] in wcs {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
ring.push(Vector2::new(x, y));
|
||||
} else {
|
||||
let is_outer = model
|
||||
.boundary_exterior
|
||||
.as_deref()
|
||||
.and_then(|roles| roles.get(ring_index))
|
||||
.copied()
|
||||
.unwrap_or(first);
|
||||
first = false;
|
||||
if !ring.is_empty() {
|
||||
push_ring(&mut ring, is_outer, ring_index);
|
||||
ring_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(entry) = crate::scene::model::hatch_patterns::find(&model.name) {
|
||||
dxf.pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry);
|
||||
}
|
||||
dxf.pattern_angle = model.angle_offset as f64;
|
||||
dxf.pattern_scale = if model.scale.abs() > 1e-6 {
|
||||
let is_outer = model
|
||||
.boundary_exterior
|
||||
.as_deref()
|
||||
.and_then(|roles| roles.get(ring_index))
|
||||
.copied()
|
||||
.unwrap_or(first);
|
||||
push_ring(&mut ring, is_outer, ring_index);
|
||||
dxf.is_associative = dxf
|
||||
.paths
|
||||
.iter()
|
||||
.any(|path| !path.boundary_handles.is_empty());
|
||||
let pattern_scale = if model.scale.abs() > 1e-6 {
|
||||
model.scale as f64
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
if let crate::scene::model::hatch_model::HatchPattern::Pattern(families) = &model.pattern {
|
||||
let mut pattern = acadrust::entities::HatchPattern::new(&model.name);
|
||||
let rotation = model.angle_offset as f64;
|
||||
let (rotation_sin, rotation_cos) = rotation.sin_cos();
|
||||
for family in families {
|
||||
let family_angle = (family.angle_deg as f64).to_radians();
|
||||
let angle = family_angle + rotation;
|
||||
let (family_sin, family_cos) = family_angle.sin_cos();
|
||||
let local_offset_x = family.dx as f64 * family_cos
|
||||
- family.dy as f64 * family_sin;
|
||||
let local_offset_y = family.dx as f64 * family_sin
|
||||
+ family.dy as f64 * family_cos;
|
||||
let base_x = family.x0 as f64 * pattern_scale;
|
||||
let base_y = family.y0 as f64 * pattern_scale;
|
||||
pattern.lines.push(acadrust::entities::HatchPatternLine {
|
||||
angle,
|
||||
base_point: Vector2::new(
|
||||
model.world_origin[0]
|
||||
+ base_x * rotation_cos
|
||||
- base_y * rotation_sin,
|
||||
model.world_origin[1]
|
||||
+ base_x * rotation_sin
|
||||
+ base_y * rotation_cos,
|
||||
),
|
||||
offset: Vector2::new(
|
||||
(local_offset_x * rotation_cos
|
||||
- local_offset_y * rotation_sin)
|
||||
* pattern_scale,
|
||||
(local_offset_x * rotation_sin
|
||||
+ local_offset_y * rotation_cos)
|
||||
* pattern_scale,
|
||||
),
|
||||
dash_lengths: family
|
||||
.dashes
|
||||
.iter()
|
||||
.map(|dash| *dash as f64 * pattern_scale)
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
dxf.pattern = pattern;
|
||||
}
|
||||
dxf.pattern_angle = model.angle_offset as f64;
|
||||
dxf.pattern_scale = pattern_scale;
|
||||
// A gradient fill must be encoded on the DXF entity itself: the render
|
||||
// model is rebuilt from the entity below (`add_entity` →
|
||||
// `hatch_model_from_dxf`), so a gradient kept only on the command's
|
||||
@@ -2198,6 +2299,10 @@ impl Scene {
|
||||
if let Some(layer) = layer {
|
||||
entity.as_entity_mut().set_layer(layer.to_string());
|
||||
}
|
||||
if let Some((color, transparency)) = entity_style {
|
||||
entity.common_mut().color = color;
|
||||
entity.common_mut().transparency = transparency;
|
||||
}
|
||||
|
||||
self.add_entity(entity)
|
||||
}
|
||||
@@ -2210,6 +2315,7 @@ impl Scene {
|
||||
self.preview_text = vec![];
|
||||
self.current_layout = "Model".to_string();
|
||||
self.hatches = HashMap::default();
|
||||
self.associative_hatch_source_cache.borrow_mut().take();
|
||||
self.images = HashMap::default();
|
||||
self.meshes = HashMap::default();
|
||||
self.block_meshes = HashMap::default();
|
||||
|
||||
Reference in new issue
Block a user