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:
Hakan Seven committed 2026-08-15 20:17:43 +03:00
commit f247d15f6e
110 files changed
+32159 -1254

No files matched your search

+168 -62
View File
@@ -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();