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
+22
-2
@@ -480,7 +480,15 @@ pub fn annotative_offscale_for(
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn annotation_scale_handles_for_entity(
|
||||
doc: &CadDocument,
|
||||
entity_handle: Handle,
|
||||
) -> Vec<Handle> {
|
||||
object_scale_memberships(doc, entity_handle)
|
||||
.into_iter()
|
||||
.map(|(_, scale_handle)| scale_handle)
|
||||
.collect()
|
||||
}
|
||||
pub fn scale_handle_by_name(doc: &CadDocument, name: &str) -> Option<Handle> {
|
||||
doc.objects.iter().find_map(|(handle, object)| match object {
|
||||
ObjectType::Scale(scale)
|
||||
@@ -662,7 +670,19 @@ pub fn effective_annotation_scale_for(
|
||||
if !context_annotative && (text_like || !style_annotative) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Annotative TEXT/MTEXT store their paper text height as the base value.
|
||||
// `fallback` is the absolute paper-to-model multiplier for the active
|
||||
// annotation scale and already includes the drawing's INSUNITS conversion.
|
||||
//
|
||||
// Example for a metre drawing with 2 mm paper text:
|
||||
// 1:1 -> 2 * 0.001 = 0.002 m
|
||||
// 1:100 -> 2 * 0.100 = 0.200 m
|
||||
//
|
||||
// Using active/native here would only produce 1 / 100 and would lose the
|
||||
// millimetre-to-metre conversion entirely.
|
||||
if matches!(entity, EntityType::Text(_) | EntityType::MText(_)) {
|
||||
return fallback;
|
||||
}
|
||||
if matches!(
|
||||
entity,
|
||||
EntityType::Dimension(_)
|
||||
|
||||
+307
-34
@@ -1,6 +1,9 @@
|
||||
use super::*;
|
||||
|
||||
use cadkernel::geom2d::{bounded_faces, Line, Tolerance};
|
||||
use cadkernel::geom2d::{
|
||||
bounded_faces, contains, distance_to, segment_crossing, triangulate, Curve, Line,
|
||||
SegmentCrossing, Tolerance,
|
||||
};
|
||||
|
||||
/// How far apart two points may be and still be taken for the same one.
|
||||
///
|
||||
@@ -10,7 +13,292 @@ use cadkernel::geom2d::{bounded_faces, Line, Tolerance};
|
||||
/// enough not to weld genuinely separate corners together.
|
||||
const WELD_TOLERANCE: f64 = 1.0e-6;
|
||||
|
||||
fn wire_segments(wire: &WireModel) -> Vec<Line> {
|
||||
let mut segments = Vec::new();
|
||||
let mut previous: Option<[f64; 2]> = None;
|
||||
|
||||
for (index, high) in wire.points.iter().copied().enumerate() {
|
||||
if !high[0].is_finite() || !high[1].is_finite() {
|
||||
previous = None;
|
||||
continue;
|
||||
}
|
||||
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
let current = [
|
||||
high[0] as f64 + low[0] as f64,
|
||||
high[1] as f64 + low[1] as f64,
|
||||
];
|
||||
if let Some(start) = previous {
|
||||
let (dx, dy) = (current[0] - start[0], current[1] - start[1]);
|
||||
if dx.hypot(dy) > WELD_TOLERANCE {
|
||||
segments.push(Line { start, end: current });
|
||||
}
|
||||
}
|
||||
previous = Some(current);
|
||||
}
|
||||
segments
|
||||
}
|
||||
|
||||
fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> {
|
||||
let mut ring = Vec::new();
|
||||
let mut index = 0usize;
|
||||
for &[x, y] in model.boundary.iter() {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
if index == wanted {
|
||||
ring.push([
|
||||
model.world_origin[0] + x as f64,
|
||||
model.world_origin[1] + y as f64,
|
||||
]);
|
||||
}
|
||||
} else if index == wanted {
|
||||
break;
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
if ring.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let (points, triangles) = triangulate(&ring, &[]);
|
||||
if let Some(triangle) = triangles.first() {
|
||||
let [a, b, c] = triangle.map(|vertex| points[vertex]);
|
||||
return Some([
|
||||
(a[0] + b[0] + c[0]) / 3.0,
|
||||
(a[1] + b[1] + c[1]) / 3.0,
|
||||
]);
|
||||
}
|
||||
Some([
|
||||
ring.iter().map(|point| point[0]).sum::<f64>() / ring.len() as f64,
|
||||
ring.iter().map(|point| point[1]).sum::<f64>() / ring.len() as f64,
|
||||
])
|
||||
}
|
||||
|
||||
fn face_curves(face: &[[f64; 2]]) -> Vec<Curve> {
|
||||
face.iter()
|
||||
.copied()
|
||||
.zip(face.iter().copied().cycle().skip(1))
|
||||
.take(face.len())
|
||||
.map(|(start, end)| Curve::Line(Line { start, end }))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn matching_face(faces: &[Vec<[f64; 2]>], seed: Option<[f64; 2]>) -> Option<&Vec<[f64; 2]>> {
|
||||
if faces.len() == 1 {
|
||||
return faces.first();
|
||||
}
|
||||
let seed = seed?;
|
||||
let tolerance = Tolerance::new(WELD_TOLERANCE);
|
||||
if let Some(face) = faces.iter().find(|face| {
|
||||
let curves = face_curves(face);
|
||||
contains(&curves, seed, tolerance)
|
||||
}) {
|
||||
return Some(face);
|
||||
}
|
||||
faces.iter().min_by(|a, b| {
|
||||
let nearest = |face: &Vec<[f64; 2]>| {
|
||||
face_curves(face)
|
||||
.iter()
|
||||
.map(|curve| distance_to(curve, seed))
|
||||
.fold(f64::INFINITY, f64::min)
|
||||
};
|
||||
nearest(a).total_cmp(&nearest(b))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ring_source_handles(
|
||||
ring: &[[f64; 2]],
|
||||
sources: &rustc_hash::FxHashMap<acadrust::Handle, Vec<Line>>,
|
||||
) -> Vec<acadrust::Handle> {
|
||||
let mut handles = rustc_hash::FxHashSet::default();
|
||||
for (&start, &end) in ring
|
||||
.iter()
|
||||
.zip(ring.iter().cycle().skip(1))
|
||||
.take(ring.len())
|
||||
{
|
||||
let edge = Line { start, end };
|
||||
let edge_length = (end[0] - start[0]).hypot(end[1] - start[1]);
|
||||
for (&handle, lines) in sources {
|
||||
if lines.iter().any(|line| {
|
||||
matches!(
|
||||
segment_crossing(edge, *line, Tolerance::new(WELD_TOLERANCE)),
|
||||
SegmentCrossing::Overlap { a, .. }
|
||||
if (a[1] - a[0]).abs() * edge_length > WELD_TOLERANCE
|
||||
)
|
||||
}) {
|
||||
handles.insert(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut handles: Vec<_> = handles.into_iter().collect();
|
||||
handles.sort_by_key(|handle| handle.value());
|
||||
handles
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec<acadrust::EntityType> {
|
||||
rings
|
||||
.iter()
|
||||
.filter_map(|ring| {
|
||||
let mut points = Vec::with_capacity(ring.len());
|
||||
for &point in ring {
|
||||
if point.iter().all(|value| value.is_finite())
|
||||
&& points.last() != Some(&point)
|
||||
{
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
if points.len() > 1 && points.first() == points.last() {
|
||||
points.pop();
|
||||
}
|
||||
if points.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let mut polyline = acadrust::entities::LwPolyline::new();
|
||||
polyline.is_closed = true;
|
||||
polyline.vertices = points
|
||||
.into_iter()
|
||||
.map(|[x, y]| {
|
||||
acadrust::entities::LwVertex::new(acadrust::types::Vector2::new(x, y))
|
||||
})
|
||||
.collect();
|
||||
Some(acadrust::EntityType::LwPolyline(polyline))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
fn associative_boundary_segments(&self, handles: &[Handle]) -> Vec<Line> {
|
||||
self.wire_models_for(handles)
|
||||
.iter()
|
||||
.flat_map(wire_segments)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn associative_hatch_dependents(
|
||||
&self,
|
||||
changed: &rustc_hash::FxHashSet<Handle>,
|
||||
) -> Vec<Handle> {
|
||||
if self.associative_hatch_source_cache.borrow().is_none() {
|
||||
let mut index: HashMap<Handle, Vec<Handle>> = HashMap::default();
|
||||
for entity in self.document.entities() {
|
||||
let EntityType::Hatch(hatch) = entity else {
|
||||
continue;
|
||||
};
|
||||
if !hatch.is_associative {
|
||||
continue;
|
||||
}
|
||||
for source in hatch
|
||||
.paths
|
||||
.iter()
|
||||
.flat_map(|path| path.boundary_handles.iter().copied())
|
||||
{
|
||||
let dependents = index.entry(source).or_default();
|
||||
if !dependents.contains(&hatch.common.handle) {
|
||||
dependents.push(hatch.common.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
*self.associative_hatch_source_cache.borrow_mut() = Some(index);
|
||||
}
|
||||
let cache = self.associative_hatch_source_cache.borrow();
|
||||
let index = cache.as_ref().expect("associative hatch index");
|
||||
let mut handles = rustc_hash::FxHashSet::default();
|
||||
for source in changed {
|
||||
if let Some(dependents) = index.get(source) {
|
||||
handles.extend(dependents.iter().copied());
|
||||
}
|
||||
}
|
||||
handles.into_iter().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_associative_hatches(
|
||||
&mut self,
|
||||
changes: &[(Handle, ChangeKind)],
|
||||
) -> Vec<(Handle, ChangeKind)> {
|
||||
if changes.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let changed: rustc_hash::FxHashSet<_> =
|
||||
changes.iter().map(|(handle, _)| *handle).collect();
|
||||
let candidates: Vec<_> = self
|
||||
.associative_hatch_dependents(&changed)
|
||||
.into_iter()
|
||||
.filter_map(|handle| {
|
||||
let EntityType::Hatch(hatch) = self.document.get_entity(handle)? else {
|
||||
return None;
|
||||
};
|
||||
Some({
|
||||
let seeds = self
|
||||
.hatches
|
||||
.get(&hatch.common.handle)
|
||||
.map(|model| {
|
||||
(0..hatch.paths.len())
|
||||
.map(|index| ring_seed(model, index))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
(handle, hatch.clone(), seeds)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut refreshed = Vec::new();
|
||||
for (handle, mut hatch, seeds) in candidates {
|
||||
let normal = hatch.normal;
|
||||
if normal.x.abs() > 1.0e-8 || normal.y.abs() > 1.0e-8 {
|
||||
continue;
|
||||
}
|
||||
let mut modified = false;
|
||||
let mut association_changed = false;
|
||||
for (index, path) in hatch.paths.iter_mut().enumerate() {
|
||||
if !path
|
||||
.boundary_handles
|
||||
.iter()
|
||||
.any(|source| changed.contains(source))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let old_count = path.boundary_handles.len();
|
||||
path.boundary_handles
|
||||
.retain(|source| self.document.get_entity(*source).is_some());
|
||||
association_changed |= path.boundary_handles.len() != old_count;
|
||||
modified |= association_changed;
|
||||
let segments = self.associative_boundary_segments(&path.boundary_handles);
|
||||
let faces = bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE));
|
||||
let Some(face) = matching_face(&faces, seeds.get(index).copied().flatten()) else {
|
||||
continue;
|
||||
};
|
||||
path.edges = vec![acadrust::entities::hatch::BoundaryEdge::Polyline(
|
||||
acadrust::entities::hatch::PolylineEdge::new(
|
||||
face.iter()
|
||||
.map(|point| acadrust::types::Vector2::new(point[0], point[1]))
|
||||
.collect(),
|
||||
true,
|
||||
),
|
||||
)];
|
||||
modified = true;
|
||||
}
|
||||
hatch.is_associative = hatch
|
||||
.paths
|
||||
.iter()
|
||||
.any(|path| !path.boundary_handles.is_empty());
|
||||
if !modified {
|
||||
continue;
|
||||
}
|
||||
if association_changed {
|
||||
self.associative_hatch_source_cache.borrow_mut().take();
|
||||
}
|
||||
if self.is_recording_undo() {
|
||||
let before = self.document.get_entity_arc(handle);
|
||||
self.record_undo_before(handle, before);
|
||||
}
|
||||
if let Some(slot) = self.document.get_entity_mut(handle) {
|
||||
*slot = EntityType::Hatch(hatch);
|
||||
self.refresh_fill_model(handle);
|
||||
refreshed.push((handle, ChangeKind::Modified));
|
||||
}
|
||||
}
|
||||
refreshed
|
||||
}
|
||||
|
||||
/// Build closed planar regions from the visible wire geometry.
|
||||
///
|
||||
/// Unlike `closed_outlines()`, the source entities do not need to be closed
|
||||
@@ -30,41 +318,26 @@ impl Scene {
|
||||
let mut segments = Vec::<Line>::new();
|
||||
|
||||
for wire in self.entity_wires().iter() {
|
||||
let mut previous: Option<[f64; 2]> = None;
|
||||
|
||||
for (index, high) in wire.points.iter().copied().enumerate() {
|
||||
// NaNs delimit independent segments inside some WireModels,
|
||||
// notably polylines stored as A-B | B-C | C-D.
|
||||
if !high[0].is_finite() || !high[1].is_finite() {
|
||||
previous = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The wire carries its coordinates as a double-single pair, so
|
||||
// both halves are needed to recover the f64 the tessellation
|
||||
// produced. Reading only the high half would put every vertex
|
||||
// of a survey-coordinate drawing on a grid coarser than the
|
||||
// weld tolerance.
|
||||
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
let current = [
|
||||
high[0] as f64 + low[0] as f64,
|
||||
high[1] as f64 + low[1] as f64,
|
||||
];
|
||||
|
||||
if let Some(start) = previous {
|
||||
let (dx, dy) = (current[0] - start[0], current[1] - start[1]);
|
||||
if dx.hypot(dy) > WELD_TOLERANCE {
|
||||
segments.push(Line {
|
||||
start,
|
||||
end: current,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
previous = Some(current);
|
||||
}
|
||||
segments.extend(wire_segments(wire));
|
||||
}
|
||||
|
||||
bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE))
|
||||
}
|
||||
|
||||
/// Tessellated boundary segments grouped by their selectable entity.
|
||||
pub fn hatch_boundary_sources(
|
||||
&self,
|
||||
) -> rustc_hash::FxHashMap<acadrust::Handle, Vec<Line>> {
|
||||
let mut sources = rustc_hash::FxHashMap::default();
|
||||
for wire in self.entity_wires().iter() {
|
||||
let Some(handle) = Self::handle_from_wire_name(&wire.name) else {
|
||||
continue;
|
||||
};
|
||||
sources
|
||||
.entry(handle)
|
||||
.or_insert_with(Vec::new)
|
||||
.extend(wire_segments(wire));
|
||||
}
|
||||
sources
|
||||
}
|
||||
}
|
||||
+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();
|
||||
|
||||
+54
-17
@@ -33,6 +33,8 @@ mod project;
|
||||
mod scene_markers;
|
||||
mod selection;
|
||||
|
||||
pub(crate) use boundary::{boundary_entities, ring_source_handles};
|
||||
|
||||
// Parallel tessellation free functions live in `convert::tess` (alongside the
|
||||
// other tessellation code); re-exported here so this root and sibling topic
|
||||
// modules (each does `use super::*`) keep referencing them unqualified.
|
||||
@@ -147,6 +149,7 @@ use acadrust::entities::{
|
||||
BoundaryEdge, BoundaryPath, Hatch as DxfHatch, PolylineEdge, Solid as DxfSolid,
|
||||
};
|
||||
use acadrust::objects::ObjectType;
|
||||
use acadrust::tables::normalize_name;
|
||||
use acadrust::types::Vector2;
|
||||
use acadrust::{CadDocument, EntityType, Handle, TableEntry};
|
||||
use glam;
|
||||
@@ -872,10 +875,12 @@ pub fn prepare_open_geometry(
|
||||
scene.local_center = caches.local_center;
|
||||
scene.bg_color = model_bg;
|
||||
let cannoscale_value = scene.document.header.annotation_scale_value;
|
||||
let unit_factor = scene.annotation_scale_unit_factor();
|
||||
|
||||
scene.annotation_scale = if cannoscale_value > 1e-9 {
|
||||
(1.0 / cannoscale_value) as f32
|
||||
((1.0 / cannoscale_value) / unit_factor) as f32
|
||||
} else {
|
||||
1.0
|
||||
(1.0 / unit_factor) as f32
|
||||
};
|
||||
scene.current_layout = "Model".to_string();
|
||||
let camera = scene.camera.borrow().clone();
|
||||
@@ -1675,6 +1680,9 @@ pub struct Scene {
|
||||
/// `geometry_epoch`: a layer colour toggle can reuse the index, invalidate
|
||||
/// only its dependants, and avoid a whole-document scan on every toggle.
|
||||
dependency_index_cache: RefCell<Option<SceneDependencyIndex>>,
|
||||
/// Boundary source → associative hatch handles. Source edits reuse this
|
||||
/// index instead of scanning the document during every drag step.
|
||||
associative_hatch_source_cache: RefCell<Option<HashMap<Handle, Vec<Handle>>>>,
|
||||
/// Tessellated block definitions in block-local coords, keyed by render
|
||||
/// background and block epoch. Model and Paper adapt black/white colours
|
||||
/// differently; retaining both variants prevents a full block rebuild on
|
||||
@@ -1891,6 +1899,7 @@ impl Scene {
|
||||
model_extents_cache: RefCell::new(None),
|
||||
entity_block_map_cache: RefCell::new(None),
|
||||
dependency_index_cache: RefCell::new(None),
|
||||
associative_hatch_source_cache: RefCell::new(None),
|
||||
block_defn_cache: RefCell::new(HashMap::default()),
|
||||
entity_index_cache: RefCell::new(None),
|
||||
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
||||
@@ -2366,6 +2375,21 @@ impl Scene {
|
||||
}
|
||||
|
||||
pub fn bump_entities(&mut self, changes: &[(Handle, ChangeKind)]) {
|
||||
if changes.iter().any(|(handle, kind)| {
|
||||
matches!(kind, ChangeKind::Removed)
|
||||
|| self
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Hatch(_)))
|
||||
}) {
|
||||
self.associative_hatch_source_cache.borrow_mut().take();
|
||||
}
|
||||
let mut changes = changes.to_vec();
|
||||
for change in self.refresh_associative_hatches(&changes) {
|
||||
if !changes.iter().any(|(handle, _)| *handle == change.0) {
|
||||
changes.push(change);
|
||||
}
|
||||
}
|
||||
if !changes.is_empty() {
|
||||
// A Modified delta can change layer/style/block references as well
|
||||
// as coordinates. Rebuild the reverse dependency map lazily on its
|
||||
@@ -2384,14 +2408,14 @@ impl Scene {
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Light(_)))
|
||||
});
|
||||
if cached_light_changed || live_light_changed {
|
||||
for (handle, _) in changes {
|
||||
for &(handle, _) in &changes {
|
||||
let exists = self
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.get_entity(handle)
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Light(_)));
|
||||
crate::entities::object_data::update_light_entity(
|
||||
&mut self.object_data_cache,
|
||||
*handle,
|
||||
handle,
|
||||
exists,
|
||||
);
|
||||
}
|
||||
@@ -2402,7 +2426,7 @@ impl Scene {
|
||||
{
|
||||
let mut tm = self.tess_memo.borrow_mut();
|
||||
let mut rm = self.resident_tess_memo.borrow_mut();
|
||||
for &(h, k) in changes {
|
||||
for &(h, k) in &changes {
|
||||
// A changed or removed entity must re-tessellate; a fresh Add has
|
||||
// no memo entry yet, so there is nothing to drop.
|
||||
if matches!(k, ChangeKind::Modified | ChangeKind::Removed) {
|
||||
@@ -2411,7 +2435,7 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.push_geometry_delta(epoch, changes.to_vec(), false);
|
||||
self.push_geometry_delta(epoch, changes, false);
|
||||
}
|
||||
|
||||
/// True when any changed handle belongs to an ordinary block definition
|
||||
@@ -3179,6 +3203,8 @@ impl Scene {
|
||||
world_origin: [0.0, 0.0],
|
||||
boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
pattern: crate::scene::model::hatch_model::HatchPattern::Solid,
|
||||
name: "SOLID".to_string(),
|
||||
color: self.paper_bg_color,
|
||||
@@ -3553,7 +3579,7 @@ impl Scene {
|
||||
/// The built-in scale factors are defined assuming model and paper use
|
||||
/// the same base unit. A drawing in metres therefore needs an extra
|
||||
/// factor of 1000 when its paper side is measured in millimetres.
|
||||
fn annotation_scale_unit_factor(&self) -> f64 {
|
||||
pub(crate) fn annotation_scale_unit_factor(&self) -> f64 {
|
||||
let paper_unit = if self.prefers_imperial_scales() == Some(true) {
|
||||
1 // Inches
|
||||
} else {
|
||||
@@ -3836,14 +3862,25 @@ impl Scene {
|
||||
|
||||
pub fn set_annotation_scale_named(&mut self, name: &str) -> Option<Handle> {
|
||||
let handle = self.scale_handle_ensuring(name)?;
|
||||
let ObjectType::Scale(scale) = self.document.objects.get(&handle)? else {
|
||||
return None;
|
||||
let unit_factor = self.annotation_scale_unit_factor();
|
||||
|
||||
let (scale_name, scale_factor, multiplier) = {
|
||||
let ObjectType::Scale(scale) = self.document.objects.get(&handle)? else {
|
||||
return None;
|
||||
};
|
||||
|
||||
(
|
||||
scale.name.clone(),
|
||||
scale.factor(),
|
||||
scale.inverse_factor() / unit_factor,
|
||||
)
|
||||
};
|
||||
let multiplier = scale.inverse_factor();
|
||||
|
||||
self.annotation_scale = multiplier as f32;
|
||||
self.document.header.current_annotation_scale = scale.name.clone();
|
||||
self.document.header.annotation_scale_value = scale.factor();
|
||||
self.document.header.current_annotation_scale = scale_name;
|
||||
self.document.header.annotation_scale_value = scale_factor;
|
||||
self.invalidate_annotation_dependencies();
|
||||
|
||||
Some(handle)
|
||||
}
|
||||
|
||||
@@ -8555,7 +8592,7 @@ impl Scene {
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.map(|record| (record.handle, record.name.to_ascii_uppercase()))
|
||||
.map(|record| (record.handle, normalize_name(&record.name)))
|
||||
.collect();
|
||||
let membership: HashMap<Handle, Handle> = self
|
||||
.document
|
||||
@@ -8582,7 +8619,7 @@ impl Scene {
|
||||
let EntityType::Insert(insert) = entity else {
|
||||
continue;
|
||||
};
|
||||
let target = insert.block_name.to_ascii_uppercase();
|
||||
let target = normalize_name(&insert.block_name);
|
||||
let common = &insert.common;
|
||||
let owner = if common.owner_handle.is_null() {
|
||||
membership
|
||||
@@ -8666,7 +8703,7 @@ impl Scene {
|
||||
extend_category(&mut index.annotation_geometry);
|
||||
}
|
||||
let add = |map: &mut HashMap<String, DependencyTargets>, name: &str| {
|
||||
let target = map.entry(name.to_ascii_uppercase()).or_default();
|
||||
let target = map.entry(normalize_name(name)).or_default();
|
||||
target.render_handles.extend(render_handles.iter().copied());
|
||||
target.source_handles.insert(common.handle);
|
||||
target.touches_block_definition |= inside_block;
|
||||
@@ -8805,7 +8842,7 @@ impl Scene {
|
||||
};
|
||||
let mut combined = DependencyTargets::default();
|
||||
for name in names {
|
||||
let Some(target) = map.get(&name.to_ascii_uppercase()) else {
|
||||
let Some(target) = map.get(&normalize_name(name)) else {
|
||||
continue;
|
||||
};
|
||||
combined
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// HatchModel — CPU-side hatch fill data; rendered entirely on the GPU.
|
||||
//
|
||||
// The boundary is a closed polygon in world XY coordinates.
|
||||
// The GPU fragment shader performs point-in-polygon and hatch-line tests so
|
||||
// no line geometry is ever tessellated on the CPU.
|
||||
// CPU-side hatch fill data. The kernel triangulates the boundary and the GPU
|
||||
// evaluates the pattern inside that mesh.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -183,6 +180,10 @@ pub struct HatchModel {
|
||||
/// rebuilt from a DXF entity — `add_hatch` then reconstructs the persisted
|
||||
/// vertices from `boundary` + `world_origin` instead.
|
||||
pub boundary_wcs: Option<Arc<Vec<[f64; 2]>>>,
|
||||
/// Per-ring DXF role, aligned with the NaN-separated boundary paths.
|
||||
pub boundary_exterior: Option<Arc<Vec<bool>>>,
|
||||
/// Source entity handles for each boundary ring.
|
||||
pub boundary_sources: Option<Arc<Vec<Vec<acadrust::Handle>>>>,
|
||||
/// Fill pattern.
|
||||
pub pattern: HatchPattern,
|
||||
/// Catalog name for this pattern (e.g. "ANSI31", "SOLID", "LINEAR").
|
||||
@@ -207,6 +208,40 @@ pub struct HatchModel {
|
||||
}
|
||||
|
||||
impl HatchModel {
|
||||
/// Indexed local-space fill mesh with even-odd loop containment.
|
||||
pub(crate) fn fill_mesh(&self) -> (Vec<[f32; 2]>, Vec<u32>) {
|
||||
let mut rings = Vec::new();
|
||||
let mut ring = Vec::new();
|
||||
for &[x, y] in self.boundary.iter() {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
ring.push([x as f64, y as f64]);
|
||||
} else if ring.len() >= 3 {
|
||||
rings.push(std::mem::take(&mut ring));
|
||||
} else {
|
||||
ring.clear();
|
||||
}
|
||||
}
|
||||
if ring.len() >= 3 {
|
||||
rings.push(ring);
|
||||
}
|
||||
|
||||
let (points, triangles) = cadkernel::geom2d::triangulate_rings(&rings);
|
||||
let vertices = points
|
||||
.into_iter()
|
||||
.map(|[x, y]| [x as f32, y as f32])
|
||||
.collect();
|
||||
let mut indices = Vec::with_capacity(triangles.len() * 3);
|
||||
for triangle in triangles {
|
||||
for index in triangle {
|
||||
let Ok(index) = u32::try_from(index) else {
|
||||
return (Vec::new(), Vec::new());
|
||||
};
|
||||
indices.push(index);
|
||||
}
|
||||
}
|
||||
(vertices, indices)
|
||||
}
|
||||
|
||||
/// CPU-side rasteriser for `HatchPattern::Pattern` — produces the line
|
||||
/// segments inside the boundary so non-GPU consumers (PDF export,
|
||||
/// `paper_canvas`, print preview) can draw the actual pattern instead
|
||||
|
||||
+141
-3
@@ -551,11 +551,43 @@ impl Scene {
|
||||
}
|
||||
|
||||
pub fn copy_entities(&mut self, handles: &[Handle], t: &EntityTransform) -> Vec<Handle> {
|
||||
let copy_handles = self.handles_expanded_for_leader_annotations(handles);
|
||||
|
||||
// LEADER + attached MTEXT are a logical pair. Their entity clones must not
|
||||
// retain the source extension dictionary, otherwise both copies share the
|
||||
// same annotation-context objects.
|
||||
let leader_pair_handles: Vec<Handle> = copy_handles
|
||||
.iter()
|
||||
.flat_map(|&handle| {
|
||||
let annotation = match self.document.get_entity(handle) {
|
||||
Some(EntityType::Leader(leader)) if !leader.annotation_handle.is_null() => {
|
||||
Some(leader.annotation_handle)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
std::iter::once(handle).chain(annotation)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Objects on a locked layer can be selected but not copied.
|
||||
let clones: Vec<(Handle, EntityType)> = handles
|
||||
let clones: Vec<(Handle, EntityType, Vec<Handle>)> = copy_handles
|
||||
.iter()
|
||||
.filter(|&&h| !self.is_layer_locked(h))
|
||||
.filter_map(|&h| self.document.get_entity(h).cloned().map(|e| (h, e)))
|
||||
.filter_map(|&h| {
|
||||
let entity = self.document.get_entity(h)?.clone();
|
||||
|
||||
let annotation_scales = if leader_pair_handles.contains(&h) {
|
||||
crate::scene::annotative::annotation_scale_handles_for_entity(
|
||||
&self.document,
|
||||
h,
|
||||
)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Some((h, entity, annotation_scales))
|
||||
})
|
||||
.collect();
|
||||
// MIRRTEXT also governs the copy path (default MIRROR keeps the source
|
||||
// and adds a mirrored copy): keep the copied text right-reading when the
|
||||
@@ -567,7 +599,7 @@ impl Scene {
|
||||
let mut new_handles = Vec::with_capacity(clones.len());
|
||||
let mut handle_map = rustc_hash::FxHashMap::default();
|
||||
let mut refresh_solid_handles = Vec::new();
|
||||
for (src_handle, mut entity) in clones {
|
||||
for (src_handle, mut entity, annotation_scales) in clones {
|
||||
let text_orient = if preserve_text_orientation {
|
||||
capture_text_orient(&entity)
|
||||
} else {
|
||||
@@ -598,9 +630,31 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
Self::reset_clone_subhandles(&mut self.document, &mut entity);
|
||||
|
||||
// An annotative LEADER/MTEXT pair must receive a fresh extension dictionary.
|
||||
// Keeping this handle would make the copy share the source's context tree.
|
||||
if !annotation_scales.is_empty() {
|
||||
entity.common_mut().xdictionary_handle = None;
|
||||
}
|
||||
|
||||
entity.common_mut().handle = Handle::NULL;
|
||||
let h = self.document.add_entity(entity).unwrap_or(Handle::NULL);
|
||||
if !h.is_null() {
|
||||
if !annotation_scales.is_empty() {
|
||||
for scale_handle in annotation_scales {
|
||||
crate::scene::annotative::create_annotation_context(
|
||||
&mut self.document,
|
||||
h,
|
||||
scale_handle,
|
||||
);
|
||||
}
|
||||
|
||||
// Annotation contexts add dictionary/object records outside the entity
|
||||
// delta itself, so keep undo on the safe full-snapshot path.
|
||||
if self.is_recording_undo() {
|
||||
self.poison_undo_recording();
|
||||
}
|
||||
}
|
||||
// Delta-undo: a copy's before-image is "nothing" (undo erases it).
|
||||
if self.is_recording_undo() {
|
||||
self.record_undo_before(h, None);
|
||||
@@ -640,7 +694,35 @@ impl Scene {
|
||||
handle_map.insert(src_handle, h);
|
||||
}
|
||||
}
|
||||
// A copied LEADER must reference the copied annotation, never the
|
||||
// source annotation. Both entities now exist, so remap the stored handle.
|
||||
let leader_links: Vec<(Handle, Handle)> = handle_map
|
||||
.iter()
|
||||
.filter_map(|(&source_handle, &copied_handle)| {
|
||||
let EntityType::Leader(source_leader) =
|
||||
self.document.get_entity(source_handle)?
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let copied_annotation = handle_map
|
||||
.get(&source_leader.annotation_handle)
|
||||
.copied()
|
||||
.unwrap_or(Handle::NULL);
|
||||
|
||||
Some((copied_handle, copied_annotation))
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (leader_handle, annotation_handle) in leader_links {
|
||||
if let Some(EntityType::Leader(leader)) =
|
||||
self.document.get_entity_mut(leader_handle)
|
||||
{
|
||||
leader.annotation_handle = annotation_handle;
|
||||
}
|
||||
|
||||
let _ = self.sync_displayed_annotation_context(leader_handle);
|
||||
}
|
||||
// Complete group copies record their new Group objects and dictionary
|
||||
// entry as targeted object deltas inside copy_complete_groups.
|
||||
self.copy_complete_groups(&handle_map);
|
||||
@@ -956,10 +1038,66 @@ impl Scene {
|
||||
.get_entity(handle)
|
||||
.and_then(crate::entities::solid3d::point_of_reference)
|
||||
.map(|p| [p.x, p.y, p.z]);
|
||||
// A LEADER's final vertex is the end of its horizontal landing.
|
||||
// Remember its old position and linked MTEXT so the annotation can follow
|
||||
// when that grip stretches the landing.
|
||||
let leader_landing_before = self.document.get_entity(handle).and_then(|entity| {
|
||||
let EntityType::Leader(leader) = entity else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let n = leader.vertices.len();
|
||||
if n < 3 || (grip_id != n - 1 && grip_id != n - 2) || leader.annotation_handle.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let point = leader.vertices.last()?;
|
||||
|
||||
Some((
|
||||
leader.annotation_handle,
|
||||
glam::DVec3::new(point.x, point.y, point.z),
|
||||
))
|
||||
});
|
||||
if let Some(entity) = self.document.get_entity_mut(handle) {
|
||||
view::dispatch::apply_grip(entity, grip_id, apply);
|
||||
}
|
||||
if let Some((annotation_handle, old_landing)) = leader_landing_before {
|
||||
let new_landing = self.document.get_entity(handle).and_then(|entity| {
|
||||
let EntityType::Leader(leader) = entity else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let point = leader.vertices.last()?;
|
||||
Some(glam::DVec3::new(point.x, point.y, point.z))
|
||||
});
|
||||
|
||||
if let Some(new_landing) = new_landing {
|
||||
let delta = new_landing - old_landing;
|
||||
|
||||
if delta.length_squared() > 1.0e-20 {
|
||||
if self.is_recording_undo() {
|
||||
if let Some(before) = self.document.get_entity_arc(annotation_handle) {
|
||||
self.record_undo_before(annotation_handle, Some(before));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(annotation) = self.document.get_entity_mut(annotation_handle) {
|
||||
view::dispatch::apply_transform(
|
||||
annotation,
|
||||
&crate::command::EntityTransform::Translate(delta),
|
||||
);
|
||||
}
|
||||
|
||||
if self.sync_displayed_annotation_context(annotation_handle) {
|
||||
self.poison_undo_recording();
|
||||
}
|
||||
self.bump_entities(&[(
|
||||
annotation_handle,
|
||||
crate::scene::ChangeKind::Modified,
|
||||
)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.sync_displayed_annotation_context(handle) {
|
||||
self.poison_undo_recording();
|
||||
}
|
||||
|
||||
@@ -692,7 +692,7 @@ fn mesh_click_result<'a>(
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Option<(f64, Handle, glam::DVec3)> {
|
||||
let profile = crate::perf::enabled().then(std::time::Instant::now);
|
||||
let profile = crate::perf::enabled().then(iced::time::Instant::now);
|
||||
let mut set_count = 0usize;
|
||||
let mut bound_hits = 0usize;
|
||||
let mut exact_triangles = 0usize;
|
||||
|
||||
@@ -16,9 +16,8 @@ impl DeviceCapabilities {
|
||||
/// Indexed wires read one per-wire constants storage buffer.
|
||||
const WIRE_STORAGE_BINDINGS: u32 = 1;
|
||||
|
||||
/// Batched hatch uses five storage bindings in one shader stage:
|
||||
/// instances, boundaries, families, dashes, and visibility.
|
||||
const HATCH_STORAGE_BINDINGS: u32 = 5;
|
||||
/// Batched hatch stores instances, families, dashes, and visibility.
|
||||
const HATCH_STORAGE_BINDINGS: u32 = 4;
|
||||
|
||||
pub fn detect(device: &wgpu::Device) -> Self {
|
||||
Self::from_limits(&device.limits())
|
||||
|
||||
@@ -34,8 +34,8 @@ impl HatchBackendKind {
|
||||
|
||||
enum HatchBackend {
|
||||
Storage {
|
||||
resident: Option<StorageHatchBatch>,
|
||||
preview: Option<StorageHatchBatch>,
|
||||
resident: Vec<StorageHatchBatch>,
|
||||
preview: Vec<StorageHatchBatch>,
|
||||
},
|
||||
Texture {
|
||||
resident: Vec<TextureHatch>,
|
||||
@@ -137,8 +137,8 @@ impl HatchGpu {
|
||||
});
|
||||
let backend = match backend_kind {
|
||||
HatchBackendKind::Storage => HatchBackend::Storage {
|
||||
resident: None,
|
||||
preview: None,
|
||||
resident: Vec::new(),
|
||||
preview: Vec::new(),
|
||||
},
|
||||
HatchBackendKind::Texture => HatchBackend::Texture {
|
||||
resident: Vec::new(),
|
||||
@@ -223,28 +223,29 @@ impl HatchGpu {
|
||||
queue: &wgpu::Queue,
|
||||
mut is_visible: impl FnMut([f32; 4]) -> bool,
|
||||
) -> usize {
|
||||
let HatchBackend::Storage {
|
||||
resident: Some(batch),
|
||||
..
|
||||
} = &mut self.backend
|
||||
let HatchBackend::Storage { resident, .. } = &mut self.backend
|
||||
else {
|
||||
return 0;
|
||||
};
|
||||
for index in 0..batch.unique_source_count {
|
||||
batch.source_visibility[index] = u32::from(is_visible(batch.source_aabbs[index]));
|
||||
let mut updated = 0;
|
||||
for batch in resident {
|
||||
for index in 0..batch.unique_source_count {
|
||||
batch.source_visibility[index] = u32::from(is_visible(batch.source_aabbs[index]));
|
||||
}
|
||||
for index in batch.unique_source_count..batch.source_visibility.len() {
|
||||
batch.source_visibility[index] = 1;
|
||||
}
|
||||
for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() {
|
||||
batch.placements[index].visible = if index == 0 && batch.unique_source_count > 0 {
|
||||
1
|
||||
} else {
|
||||
u32::from(is_visible(aabb))
|
||||
};
|
||||
}
|
||||
batch.upload_visibility(queue);
|
||||
updated += batch.instance_aabbs.len() + batch.unique_source_count;
|
||||
}
|
||||
for index in batch.unique_source_count..batch.source_visibility.len() {
|
||||
batch.source_visibility[index] = 1;
|
||||
}
|
||||
for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() {
|
||||
batch.placements[index].visible = if index == 0 && batch.unique_source_count > 0 {
|
||||
1
|
||||
} else {
|
||||
u32::from(is_visible(aabb))
|
||||
};
|
||||
}
|
||||
batch.upload_visibility(queue);
|
||||
batch.instance_aabbs.len() + batch.unique_source_count
|
||||
updated
|
||||
}
|
||||
|
||||
pub fn draw<'pass>(
|
||||
@@ -258,12 +259,13 @@ impl HatchGpu {
|
||||
pass.set_stencil_reference(stencil_reference);
|
||||
match &self.backend {
|
||||
HatchBackend::Storage { resident, preview } => {
|
||||
for batch in [resident.as_ref(), preview.as_ref()].into_iter().flatten() {
|
||||
for batch in resident.iter().chain(preview) {
|
||||
pass.set_bind_group(1, &batch.bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
|
||||
pass.set_vertex_buffer(1, batch.placement_buffer.slice(..));
|
||||
for (vertices, instances) in &batch.draws {
|
||||
pass.draw(vertices.clone(), instances.clone());
|
||||
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||
for (indices, instances) in &batch.draws {
|
||||
pass.draw_indexed(indices.clone(), 0, instances.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,7 +274,11 @@ impl HatchGpu {
|
||||
pass.set_bind_group(1, &hatch.bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
|
||||
pass.set_vertex_buffer(1, hatch.placement_buffer.slice(..));
|
||||
pass.draw(0..6, 0..hatch.instance_count);
|
||||
pass.set_index_buffer(
|
||||
hatch.index_buffer.slice(..),
|
||||
wgpu::IndexFormat::Uint32,
|
||||
);
|
||||
pass.draw_indexed(0..hatch.index_count, 0, 0..hatch.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,11 +300,11 @@ mod tests {
|
||||
#[test]
|
||||
fn selects_backend_from_device_limits() {
|
||||
assert_eq!(
|
||||
HatchBackendKind::select(capabilities(5), false),
|
||||
HatchBackendKind::select(capabilities(4), false),
|
||||
HatchBackendKind::Storage
|
||||
);
|
||||
assert_eq!(
|
||||
HatchBackendKind::select(capabilities(4), false),
|
||||
HatchBackendKind::select(capabilities(3), false),
|
||||
HatchBackendKind::Texture
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,48 +1,5 @@
|
||||
// Hatch rendering — the single, canonical GPU hatch renderer. One draw
|
||||
// call for all hatches, per-instance data fetched from storage buffers in
|
||||
// the vertex shader. There is NO per-family cap here: every pattern line
|
||||
// family is uploaded (`family_count` per instance) and the fragment shader
|
||||
// loops over all of them, so complex baked patterns (ROOFING, GRAVEL,
|
||||
// CAROLINA-LEDGESTONE with dozens–hundreds of families) draw in full,
|
||||
// matching the PDF export. Unavailable on devices that lack vertex-stage
|
||||
// storage buffers. The old per-hatch renderer with the
|
||||
// 16-family cap now lives in `wipeout_gpu.rs` and serves only wipeouts.
|
||||
//
|
||||
// Data layout — three storage buffers fed from `HatchModel`s:
|
||||
//
|
||||
// InstanceBuffer (binding 0) : HatchInstance[] (128 B each)
|
||||
// color, color2, mode, gradient,
|
||||
// pattern angle/scale, world_origin,
|
||||
// boundary_offset, boundary_count,
|
||||
// family_offset, family_count,
|
||||
// dash_offset, dash_count, aabb,
|
||||
// visibility flag (CPU writes; GPU
|
||||
// skip)
|
||||
// BoundaryBuffer (binding 1) : vec4<f32>[] (all boundary
|
||||
// verts concatenated; NaN markers
|
||||
// preserved as separators just like
|
||||
// the per-hatch path)
|
||||
// FamilyBuffer (binding 2) : LineFamilyGpu[] (all line
|
||||
// families concatenated)
|
||||
// DashBuffer (binding 3) : f32[] (all dash
|
||||
// lengths concatenated)
|
||||
//
|
||||
// The vertex buffer holds each hatch's tessellated boundary triangles as
|
||||
// per-vertex local-space positions (`local_xy`) plus a per-vertex
|
||||
// `instance_index` (u32 attribute) — instance_index lets us avoid relying
|
||||
// on `@builtin(instance_index)` for portability. An instance whose
|
||||
// boundary failed to tessellate instead gets an AABB quad here, with
|
||||
// `poly_test == 1` on its `HatchInstance` so the fragment shader still
|
||||
// runs the `in_polygon` test to clip the quad to the real shape; on the
|
||||
// tessellated fast path (`poly_test == 0`) the triangles already bound
|
||||
// the fill and the fragment shader skips that test. When the visibility
|
||||
// flag is 0, the vertex shader returns a degenerate position and the
|
||||
// fragment shader runs zero times for that instance.
|
||||
//
|
||||
// Two storage usages — vertex shader reads InstanceBuffer + Boundary
|
||||
// for the AABB / boundary range; fragment shader reads
|
||||
// InstanceBuffer + Boundary + Family + Dash. Both stages share group
|
||||
// 1 with `read_only` access.
|
||||
// Batched hatch rendering. The kernel triangulates each boundary once; the
|
||||
// fragment shader only evaluates the fill pattern inside that mesh.
|
||||
|
||||
use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily};
|
||||
use iced::wgpu;
|
||||
@@ -70,23 +27,16 @@ pub struct HatchInstance {
|
||||
pub grad_range: f32, // 84
|
||||
pub mode: u32, // 88 (0=pattern, 1=solid, 2=gradient)
|
||||
pub visible: u32, // 92 (CPU sets to 0 to skip)
|
||||
pub boundary_offset: u32, // 96 (first boundary vert index)
|
||||
pub boundary_count: u32, // 100
|
||||
pub family_offset: u32, // 104
|
||||
pub family_count: u32, // 108
|
||||
pub family_offset: u32, // 96
|
||||
pub family_count: u32, // 100
|
||||
/// Signed draw-order depth (-1,1); 0.0 = neutral. Applied as a clip-z
|
||||
/// bias in the vertex shader so this fill orders against other types.
|
||||
pub draw_depth: f32, // 112
|
||||
/// 1 = run the per-fragment `in_polygon` boundary test (fallback path for
|
||||
/// an instance whose boundary failed to tessellate); 0 = the rasterized
|
||||
/// triangles already bound the fill, skip the test.
|
||||
pub poly_test: u32, // 116
|
||||
pub draw_depth: f32, // 104
|
||||
/// Gradient shape (`GradientKind::shader_kind`), bit 4 = inverted stops.
|
||||
pub grad_kind: u32, // 120
|
||||
pub _pad2: u32, // 124
|
||||
pub grad_kind: u32, // 108
|
||||
}
|
||||
|
||||
const _: () = assert!(std::mem::size_of::<HatchInstance>() == 128);
|
||||
const _: () = assert!(std::mem::size_of::<HatchInstance>() == 112);
|
||||
|
||||
/// Split a hatch's f64 world-origin anchor into double-single (high, low) f32
|
||||
/// pairs so the GPU keeps sub-unit precision at UTM-scale coordinates.
|
||||
@@ -120,10 +70,7 @@ pub struct LineFamilyGpu {
|
||||
|
||||
const _: () = assert!(std::mem::size_of::<LineFamilyGpu>() == 48);
|
||||
|
||||
/// Per-vertex data — tessellated boundary triangles in local (world_origin-
|
||||
/// relative) space, each carrying its instance index (avoids relying on
|
||||
/// `@builtin(instance_index)` across backends). Instances whose boundary
|
||||
/// failed to tessellate emit an AABB quad here instead (see `build`).
|
||||
/// Kernel mesh vertex in local space with its source instance.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub(super) struct HatchVertex {
|
||||
@@ -184,6 +131,7 @@ impl HatchPlacement {
|
||||
/// hatch render pass entirely).
|
||||
pub(super) struct StorageHatchBatch {
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
pub index_buffer: wgpu::Buffer,
|
||||
pub placement_buffer: wgpu::Buffer,
|
||||
pub draws: Vec<(std::ops::Range<u32>, std::ops::Range<u32>)>,
|
||||
// The four storage buffers below are referenced via `bind_group` —
|
||||
@@ -191,7 +139,6 @@ pub(super) struct StorageHatchBatch {
|
||||
// only direct consumer. Keep them as fields to keep ownership in
|
||||
// one place; `#[allow(dead_code)]` silences the read-never warning.
|
||||
#[allow(dead_code)] pub instance_buffer: wgpu::Buffer,
|
||||
#[allow(dead_code)] pub boundary_buffer: wgpu::Buffer,
|
||||
#[allow(dead_code)] pub family_buffer: wgpu::Buffer,
|
||||
#[allow(dead_code)] pub dash_buffer: wgpu::Buffer,
|
||||
/// Per-instance visibility flag (1=draw, 0=skip). Stored in its
|
||||
@@ -205,8 +152,7 @@ pub(super) struct StorageHatchBatch {
|
||||
#[allow(dead_code)] pub instance_count: u32,
|
||||
/// CPU mirror — `update_visibility` re-uploads this whole slice
|
||||
/// when any flag changes. ~4 B per hatch, so 40 KB / 10 k hatches
|
||||
/// per pan tick. Far cheaper than touching the 128 B-per-instance
|
||||
/// data.
|
||||
/// per pan tick. Far cheaper than touching the instance data.
|
||||
pub placements: Vec<HatchPlacement>,
|
||||
pub source_visibility: Vec<u32>,
|
||||
pub source_aabbs: Vec<[f32; 4]>,
|
||||
@@ -218,24 +164,76 @@ pub(super) struct StorageHatchBatch {
|
||||
pub instance_aabbs: Vec<[f32; 4]>,
|
||||
}
|
||||
|
||||
fn hatch_buffer_cost(hatch: &HatchModel) -> [u64; 7] {
|
||||
let boundary = hatch.boundary.len() as u64;
|
||||
let (families, dashes) = match &hatch.pattern {
|
||||
HatchPattern::Pattern(families) => (
|
||||
families.len() as u64,
|
||||
families.iter().map(|family| family.dashes.len() as u64).sum(),
|
||||
),
|
||||
_ => (0, 0),
|
||||
};
|
||||
[
|
||||
boundary.saturating_mul(std::mem::size_of::<HatchVertex>() as u64),
|
||||
boundary.saturating_mul(24),
|
||||
std::mem::size_of::<HatchInstance>() as u64,
|
||||
families.saturating_mul(std::mem::size_of::<LineFamilyGpu>() as u64),
|
||||
dashes.saturating_mul(std::mem::size_of::<f32>() as u64),
|
||||
std::mem::size_of::<u32>() as u64,
|
||||
std::mem::size_of::<HatchPlacement>() as u64,
|
||||
]
|
||||
}
|
||||
|
||||
impl StorageHatchBatch {
|
||||
/// One-time build from the full hatch list. Re-uploaded only when
|
||||
/// `geometry_epoch` advances (mirrors the existing per-hatch
|
||||
/// upload trigger). Per-frame visibility flips go through
|
||||
/// [`upload_visibility`].
|
||||
/// Builds whole-hatch chunks within the device's buffer limits.
|
||||
pub(super) fn build(
|
||||
device: &wgpu::Device,
|
||||
bgl: &wgpu::BindGroupLayout,
|
||||
hatches: &[HatchModel],
|
||||
) -> Option<Self> {
|
||||
) -> Vec<Self> {
|
||||
if hatches.is_empty() {
|
||||
return None;
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let limits = device.limits();
|
||||
let chunk_limit = limits
|
||||
.max_buffer_size
|
||||
.min(limits.max_storage_buffer_binding_size as u64);
|
||||
let mut ranges = Vec::new();
|
||||
let mut start = 0;
|
||||
let mut used = [0u64; 7];
|
||||
for (index, hatch) in hatches.iter().enumerate() {
|
||||
let cost = hatch_buffer_cost(hatch);
|
||||
let overflow = used
|
||||
.iter()
|
||||
.zip(cost)
|
||||
.any(|(used, cost)| used.saturating_add(cost) > chunk_limit);
|
||||
if overflow && index > start {
|
||||
ranges.push(start..index);
|
||||
start = index;
|
||||
used = [0; 7];
|
||||
}
|
||||
for (used, cost) in used.iter_mut().zip(cost) {
|
||||
*used = used.saturating_add(cost);
|
||||
}
|
||||
}
|
||||
ranges.push(start..hatches.len());
|
||||
ranges
|
||||
.into_iter()
|
||||
.filter_map(|range| Self::build_chunk(device, bgl, &hatches[range]))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_chunk(
|
||||
device: &wgpu::Device,
|
||||
bgl: &wgpu::BindGroupLayout,
|
||||
hatches: &[HatchModel],
|
||||
) -> Option<Self> {
|
||||
|
||||
let mut instances: Vec<HatchInstance> = Vec::with_capacity(hatches.len());
|
||||
let mut boundary: Vec<[f32; 4]> = Vec::new();
|
||||
let mut families: Vec<LineFamilyGpu> = Vec::new();
|
||||
let mut dashes: Vec<f32> = Vec::new();
|
||||
let mut meshes = Vec::with_capacity(hatches.len());
|
||||
let mut slots = rustc_hash::FxHashMap::default();
|
||||
let mut groups: Vec<Vec<&HatchModel>> = Vec::new();
|
||||
for (index, hatch) in hatches.iter().enumerate() {
|
||||
@@ -255,22 +253,9 @@ impl StorageHatchBatch {
|
||||
|
||||
for group in &groups {
|
||||
let h = group[0];
|
||||
let boundary_offset = boundary.len() as u32;
|
||||
for &[x, y] in h.boundary.iter() {
|
||||
// NaN sub-loop separators become the finite sentinel on the
|
||||
// GPU: Intel drivers fold the shader NaN self-compare to
|
||||
// `true`, turning separators into vertices and bleeding the
|
||||
// fill outside its boundary (#386, #416).
|
||||
if x.is_finite() && y.is_finite() {
|
||||
boundary.push([x, y, 0.0, 0.0]);
|
||||
} else {
|
||||
let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP;
|
||||
boundary.push([s, s, 0.0, 0.0]);
|
||||
}
|
||||
}
|
||||
let boundary_count = boundary.len() as u32 - boundary_offset;
|
||||
|
||||
|
||||
let mesh = h.fill_mesh();
|
||||
let has_mesh = !mesh.0.is_empty() && !mesh.1.is_empty();
|
||||
meshes.push(mesh);
|
||||
let family_offset = families.len() as u32;
|
||||
let mut family_count = 0u32;
|
||||
|
||||
@@ -301,11 +286,8 @@ impl StorageHatchBatch {
|
||||
dashes.push(d);
|
||||
}
|
||||
let n_dashes = (dashes.len() as u32 - dash_offset).min(u32::MAX);
|
||||
// QCAD PAT local-frame convention (mirrors
|
||||
// the storage batch convention: `dy` is
|
||||
// the perpendicular spacing, `dx` is the along-line
|
||||
// phase shift — both in family-local coords. The
|
||||
// shader applies cos_off/sin_off to rotate them.
|
||||
// PAT local frame: perpendicular spacing and
|
||||
// along-line phase.
|
||||
let perp_step = fam.dy;
|
||||
let along_step = fam.dx;
|
||||
// Screen-space derivative drives 1-px line width
|
||||
@@ -366,39 +348,19 @@ impl StorageHatchBatch {
|
||||
grad_range,
|
||||
mode,
|
||||
visible: 0,
|
||||
boundary_offset,
|
||||
boundary_count,
|
||||
family_offset,
|
||||
family_count,
|
||||
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
|
||||
poly_test: 1,
|
||||
grad_kind,
|
||||
_pad2: 0,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pad the AABB so the quad covers any pattern halo + the
|
||||
// family origin. Mirrors the per-hatch shader's quad sizing
|
||||
// logic — `diag * 0.8 + max_spacing * 2 * scale`.
|
||||
let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt();
|
||||
// `perp_step.abs()` per family — uses the same QCAD local-
|
||||
// frame convention as `LineFamilyGpu.perp_step` above so
|
||||
// the quad padding matches what the shader will sample.
|
||||
let max_spacing = match &h.pattern {
|
||||
HatchPattern::Pattern(fs) => fs
|
||||
.iter()
|
||||
.map(|f| f.dy.abs())
|
||||
.fold(0.0f32, f32::max),
|
||||
_ => 5.0,
|
||||
};
|
||||
let pad = (diag * 0.8 + max_spacing * 2.0 * h.scale).max(1.0);
|
||||
|
||||
let (wo_hi, wo_lo) = split_origin_ds(h.world_origin);
|
||||
instances.push(HatchInstance {
|
||||
color: h.color,
|
||||
color2,
|
||||
aabb: [min_x - pad, min_y - pad, max_x + pad, max_y + pad],
|
||||
aabb: [min_x, min_y, max_x, max_y],
|
||||
world_origin: wo_hi,
|
||||
world_origin_low: wo_lo,
|
||||
angle_offset: h.angle_offset,
|
||||
@@ -408,26 +370,14 @@ impl StorageHatchBatch {
|
||||
grad_min,
|
||||
grad_range,
|
||||
mode,
|
||||
visible: 1,
|
||||
boundary_offset,
|
||||
boundary_count,
|
||||
visible: u32::from(has_mesh),
|
||||
family_offset,
|
||||
family_count,
|
||||
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
|
||||
// Always resolve inside/outside per fragment on the GPU (the
|
||||
// boundary is uploaded below). Dropping CPU pre-triangulation
|
||||
// keeps the vertex buffer at 6 verts/hatch regardless of
|
||||
// boundary complexity, so a hatch-dense drawing can't overflow
|
||||
// the device buffer limit. Each instance draws its AABB quad.
|
||||
poly_test: 1,
|
||||
grad_kind,
|
||||
_pad2: 0,
|
||||
});
|
||||
}
|
||||
// Empty fallbacks — storage buffers can't be zero-sized.
|
||||
if boundary.is_empty() {
|
||||
boundary.push([0.0; 4]);
|
||||
}
|
||||
if families.is_empty() {
|
||||
families.push(LineFamilyGpu::default_filler());
|
||||
}
|
||||
@@ -435,21 +385,33 @@ impl StorageHatchBatch {
|
||||
dashes.push(0.0);
|
||||
}
|
||||
|
||||
// Vertex buffer — one AABB quad (two triangles, BL,BR,TL, BR,TR,TL) per
|
||||
// instance; the GPU shader clips each fragment to the boundary via the
|
||||
// in_polygon test (poly_test == 1). 6 verts/hatch, independent of
|
||||
// boundary complexity, so the buffer can never overflow the device
|
||||
// limit no matter how dense the drawing.
|
||||
let mut verts: Vec<HatchVertex> = Vec::with_capacity(instances.len() * 6);
|
||||
for (i, inst) in instances.iter().enumerate() {
|
||||
let [xmin, ymin, xmax, ymax] = inst.aabb;
|
||||
let quad = [
|
||||
[xmin, ymin], [xmax, ymin], [xmin, ymax],
|
||||
[xmax, ymin], [xmax, ymax], [xmin, ymax],
|
||||
];
|
||||
for c in quad {
|
||||
verts.push(HatchVertex { local_xy: c, instance_index: i as u32 });
|
||||
let mut verts = Vec::new();
|
||||
let mut indices = Vec::new();
|
||||
let mut mesh_ranges = Vec::with_capacity(meshes.len());
|
||||
for (instance_index, (points, mesh_indices)) in meshes.into_iter().enumerate() {
|
||||
let Ok(base) = u32::try_from(verts.len()) else {
|
||||
return None;
|
||||
};
|
||||
let Ok(start) = u32::try_from(indices.len()) else {
|
||||
return None;
|
||||
};
|
||||
verts.extend(points.into_iter().map(|local_xy| HatchVertex {
|
||||
local_xy,
|
||||
instance_index: instance_index as u32,
|
||||
}));
|
||||
for index in mesh_indices {
|
||||
let Some(index) = base.checked_add(index) else {
|
||||
return None;
|
||||
};
|
||||
indices.push(index);
|
||||
}
|
||||
let Ok(end) = u32::try_from(indices.len()) else {
|
||||
return None;
|
||||
};
|
||||
mesh_ranges.push(start..end);
|
||||
}
|
||||
if indices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut placements = Vec::new();
|
||||
let mut draws = Vec::with_capacity(groups.len());
|
||||
@@ -461,7 +423,10 @@ impl StorageHatchBatch {
|
||||
draw_depth: 0.0,
|
||||
visible: 1,
|
||||
});
|
||||
draws.push((0..unique_source_count as u32 * 6, 0..1));
|
||||
let end = mesh_ranges[unique_source_count - 1].end;
|
||||
if end > 0 {
|
||||
draws.push((0..end, 0..1));
|
||||
}
|
||||
let mut union = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
|
||||
for inst in instances.iter().take(unique_source_count) {
|
||||
union[0] = union[0].min(inst.aabb[0] + inst.world_origin[0]);
|
||||
@@ -500,10 +465,32 @@ impl StorageHatchBatch {
|
||||
inst.aabb[3] + inst.world_origin[1] + high[1],
|
||||
]);
|
||||
}
|
||||
draws.push((
|
||||
source_index as u32 * 6..source_index as u32 * 6 + 6,
|
||||
placement_start..placements.len() as u32,
|
||||
));
|
||||
if !mesh_ranges[source_index].is_empty() {
|
||||
draws.push((
|
||||
mesh_ranges[source_index].clone(),
|
||||
placement_start..placements.len() as u32,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let visibility: Vec<u32> = instances.iter().map(|instance| instance.visible).collect();
|
||||
let limits = device.limits();
|
||||
let buffer_fits = |count: usize, stride: usize| {
|
||||
(count as u64).saturating_mul(stride as u64) <= limits.max_buffer_size
|
||||
};
|
||||
let storage_fits = |count: usize, stride: usize| {
|
||||
(count as u64).saturating_mul(stride as u64)
|
||||
<= limits.max_storage_buffer_binding_size as u64
|
||||
};
|
||||
if !buffer_fits(verts.len(), std::mem::size_of::<HatchVertex>())
|
||||
|| !buffer_fits(indices.len(), std::mem::size_of::<u32>())
|
||||
|| !buffer_fits(placements.len(), std::mem::size_of::<HatchPlacement>())
|
||||
|| !storage_fits(instances.len(), std::mem::size_of::<HatchInstance>())
|
||||
|| !storage_fits(families.len(), std::mem::size_of::<LineFamilyGpu>())
|
||||
|| !storage_fits(dashes.len(), std::mem::size_of::<f32>())
|
||||
|| !storage_fits(visibility.len(), std::mem::size_of::<u32>())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
@@ -511,6 +498,11 @@ impl StorageHatchBatch {
|
||||
contents: bytemuck::cast_slice(&verts),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("hatch.index"),
|
||||
contents: bytemuck::cast_slice(&indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("hatch.placements"),
|
||||
contents: bytemuck::cast_slice(&placements),
|
||||
@@ -521,11 +513,6 @@ impl StorageHatchBatch {
|
||||
contents: bytemuck::cast_slice(&instances),
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
let boundary_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("hatch.boundary"),
|
||||
contents: bytemuck::cast_slice(&boundary),
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
});
|
||||
let family_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("hatch.families"),
|
||||
contents: bytemuck::cast_slice(&families),
|
||||
@@ -537,7 +524,6 @@ impl StorageHatchBatch {
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
});
|
||||
|
||||
let visibility: Vec<u32> = instances.iter().map(|i| i.visible).collect();
|
||||
let visibility_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("hatch.visibility"),
|
||||
contents: bytemuck::cast_slice(&visibility),
|
||||
@@ -554,18 +540,14 @@ impl StorageHatchBatch {
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: boundary_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: family_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
binding: 2,
|
||||
resource: dash_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
binding: 3,
|
||||
resource: visibility_buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
@@ -582,10 +564,10 @@ impl StorageHatchBatch {
|
||||
|
||||
Some(Self {
|
||||
vertex_buffer,
|
||||
index_buffer,
|
||||
placement_buffer,
|
||||
draws,
|
||||
instance_buffer,
|
||||
boundary_buffer,
|
||||
family_buffer,
|
||||
dash_buffer,
|
||||
visibility_buffer,
|
||||
@@ -614,10 +596,7 @@ impl StorageHatchBatch {
|
||||
);
|
||||
}
|
||||
|
||||
/// Group-1 bind group layout — shared by the pipeline so it can be
|
||||
/// constructed once at startup. All four bindings are read-only
|
||||
/// storage and visible to both VS (AABB+visibility lookup) and FS
|
||||
/// (boundary / family / dash sampling).
|
||||
/// Shared layout for instance, family, dash, and visibility storage.
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
||||
let entry = |binding: u32| wgpu::BindGroupLayoutEntry {
|
||||
@@ -632,7 +611,7 @@ impl StorageHatchBatch {
|
||||
};
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("hatch.bgl"),
|
||||
entries: &[entry(0), entry(1), entry(2), entry(3), entry(4)],
|
||||
entries: &[entry(0), entry(1), entry(2), entry(3)],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
// Storage-free hatch renderer — texture-backed, UNCAPPED.
|
||||
//
|
||||
// Devices without storage buffers use this per-hatch renderer. It reuses the
|
||||
// storage-free hatch algorithm (see wipeout.wgsl / hatch_texture.wgsl) but packs the
|
||||
// variable-length boundary / family / dash arrays into ONE RGBA32F data texture
|
||||
// read via textureLoad — removing the MAX_FAMILIES / MAX_HATCH_BOUNDARY_VERTS /
|
||||
// MAX_DASHES caps of the uniform (WipeoutGpu) path. Every hatch type — solid,
|
||||
// gradient, and arbitrarily complex line patterns — renders in compat mode.
|
||||
//
|
||||
// Storage-capable devices use the sibling storage backend; wipeout masks use
|
||||
// wipeout_gpu.rs.
|
||||
// Texture-backed hatch renderer for devices without storage buffers. Boundary
|
||||
// geometry comes from the same kernel mesh as the storage backend.
|
||||
|
||||
use crate::scene::model::hatch_model::{HatchModel, HatchPattern};
|
||||
use iced::wgpu;
|
||||
@@ -71,7 +62,7 @@ struct TextureHatchUniform {
|
||||
color: [f32; 4], // 0
|
||||
color2: [f32; 4], // 16
|
||||
mode: u32, // 32
|
||||
vcount: u32, // 36
|
||||
_reserved: u32, // 36
|
||||
angle_offset: f32, // 40
|
||||
scale: f32, // 44
|
||||
grad_cos: f32, // 48
|
||||
@@ -90,7 +81,9 @@ struct TextureHatchUniform {
|
||||
|
||||
pub(super) struct TextureHatch {
|
||||
pub(super) vertex_buffer: wgpu::Buffer,
|
||||
pub(super) index_buffer: wgpu::Buffer,
|
||||
pub(super) placement_buffer: wgpu::Buffer,
|
||||
pub(super) index_count: u32,
|
||||
pub(super) instance_count: u32,
|
||||
pub(super) bind_group: wgpu::BindGroup,
|
||||
/// Reserved for per-frame AABB LOD (mirrors `WipeoutGpu`); not yet wired
|
||||
@@ -125,7 +118,7 @@ impl TextureHatch {
|
||||
}
|
||||
groups
|
||||
.into_iter()
|
||||
.map(|group| Self::new(device, queue, &group, bgl1))
|
||||
.filter_map(|group| Self::new(device, queue, &group, bgl1))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -164,7 +157,7 @@ impl TextureHatch {
|
||||
queue: &wgpu::Queue,
|
||||
models: &[&HatchModel],
|
||||
bgl1: &wgpu::BindGroupLayout,
|
||||
) -> Self {
|
||||
) -> Option<Self> {
|
||||
let model = models[0];
|
||||
// ── Decode pattern mode (mirrors WipeoutGpu::new) ─────────────────
|
||||
// The gradient shape (kind + invert bit) rides in the mode's high
|
||||
@@ -197,41 +190,26 @@ impl TextureHatch {
|
||||
max_y = max_y.max(y);
|
||||
}
|
||||
|
||||
let max_spacing = match &model.pattern {
|
||||
HatchPattern::Pattern(families) => {
|
||||
families.iter().map(|f| f.dy.abs()).fold(0.0f32, f32::max)
|
||||
}
|
||||
_ => 5.0,
|
||||
};
|
||||
let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt();
|
||||
let pad = (diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0);
|
||||
|
||||
// Anchor pattern phase at `world_origin` with the boundary stored raw,
|
||||
// matching the storage backend — NOT WipeoutGpu,
|
||||
// whose f64 origin grid-snap is dead code (wipeouts are always solid)
|
||||
// and would phase-shift every line pattern relative to desktop. No drift.
|
||||
let origin = model.world_origin;
|
||||
let drift = [0.0f32, 0.0f32];
|
||||
let (x0, x1, y0, y1) = (
|
||||
min_x + drift[0] - pad,
|
||||
max_x + drift[0] + pad,
|
||||
min_y + drift[1] - pad,
|
||||
max_y + drift[1] + pad,
|
||||
);
|
||||
|
||||
let quad = [
|
||||
HatchVertex { pos: [x0, y0, 0.0], _pad: 0.0 },
|
||||
HatchVertex { pos: [x1, y0, 0.0], _pad: 0.0 },
|
||||
HatchVertex { pos: [x1, y1, 0.0], _pad: 0.0 },
|
||||
HatchVertex { pos: [x0, y0, 0.0], _pad: 0.0 },
|
||||
HatchVertex { pos: [x1, y1, 0.0], _pad: 0.0 },
|
||||
HatchVertex { pos: [x0, y1, 0.0], _pad: 0.0 },
|
||||
];
|
||||
let (mesh_points, indices) = model.fill_mesh();
|
||||
if mesh_points.is_empty() || indices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let vertices: Vec<HatchVertex> = mesh_points
|
||||
.into_iter()
|
||||
.map(|[x, y]| HatchVertex { pos: [x, y, 0.0], _pad: 0.0 })
|
||||
.collect();
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("hatch.texture.vbuf"),
|
||||
contents: bytemuck::cast_slice(&quad),
|
||||
contents: bytemuck::cast_slice(&vertices),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("hatch.texture.ibuf"),
|
||||
contents: bytemuck::cast_slice(&indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
let base = model
|
||||
.render_instance
|
||||
.map_or([0.0; 3], |instance| instance.translation);
|
||||
@@ -290,21 +268,8 @@ impl TextureHatch {
|
||||
(0.0, 1.0)
|
||||
};
|
||||
|
||||
// ── Pack the data texture: boundary | families | dashes ───────────
|
||||
// ── Pack the data texture: families | dashes ─────────────────────
|
||||
let mut texels: Vec<[f32; 4]> = Vec::new();
|
||||
// Boundary section (texels 0..vcount). NaN separators become the
|
||||
// finite GPU sentinel — the shader NaN self-compare is folded to
|
||||
// `true` on some drivers (#386, #416).
|
||||
for &[x, y] in model.boundary.iter() {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
texels.push([x + drift[0], y + drift[1], 0.0, 0.0]);
|
||||
} else {
|
||||
let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP;
|
||||
texels.push([s, s, 0.0, 0.0]);
|
||||
}
|
||||
}
|
||||
let vcount = texels.len() as u32;
|
||||
|
||||
// Family section (3 texels each) + a flat dash pool.
|
||||
let fam_off = texels.len() as u32;
|
||||
let mut n_families = 0u32;
|
||||
@@ -320,7 +285,7 @@ impl TextureHatch {
|
||||
0.0
|
||||
};
|
||||
let angle_r = fam.angle_deg.to_radians();
|
||||
// QCAD PAT convention: perp_step = dy, along_step = dx.
|
||||
// PAT local frame: perpendicular spacing and along-line phase.
|
||||
texels.push([angle_r.cos(), angle_r.sin(), fam.x0, fam.y0]);
|
||||
texels.push([fam.dx, fam.dy, fam.dy, fam.dx]);
|
||||
// Counts as exact f32 (small integers → no denormal/bitcast risk).
|
||||
@@ -385,7 +350,7 @@ impl TextureHatch {
|
||||
color: model.color,
|
||||
color2,
|
||||
mode,
|
||||
vcount,
|
||||
_reserved: 0,
|
||||
angle_offset: model.angle_offset,
|
||||
// Clamp like the desktop renderer so scale==0 can't make perp_step 0
|
||||
// → round(perp/0)=NaN → an invisible hatch.
|
||||
@@ -433,14 +398,16 @@ impl TextureHatch {
|
||||
[min_x, min_y, max_x, max_y]
|
||||
};
|
||||
|
||||
Self {
|
||||
Some(Self {
|
||||
vertex_buffer,
|
||||
index_buffer,
|
||||
placement_buffer,
|
||||
index_count: indices.len() as u32,
|
||||
instance_count: placements.len() as u32,
|
||||
bind_group,
|
||||
world_aabb,
|
||||
_uniform_buf,
|
||||
_data_tex: data_tex,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1148,7 +1148,7 @@ fn build_instanced_chunk(
|
||||
parts: &[MeshBatchPart<'_>],
|
||||
profile_enabled: bool,
|
||||
) -> Option<(MeshBatchChunk, u64, InstancedBuildProfile)> {
|
||||
let started = profile_enabled.then(std::time::Instant::now);
|
||||
let started = profile_enabled.then(iced::time::Instant::now);
|
||||
let first = parts.first()?;
|
||||
let source = first.set.instance_source.as_ref()?;
|
||||
let mesh = first.mesh;
|
||||
@@ -1209,7 +1209,7 @@ fn build_instanced_chunk(
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let vertices_at = profile_enabled.then(std::time::Instant::now);
|
||||
let vertices_at = profile_enabled.then(iced::time::Instant::now);
|
||||
let edge_color = first
|
||||
.set
|
||||
.visual_style
|
||||
@@ -1250,7 +1250,7 @@ fn build_instanced_chunk(
|
||||
]);
|
||||
}
|
||||
}
|
||||
let edges_at = profile_enabled.then(std::time::Instant::now);
|
||||
let edges_at = profile_enabled.then(iced::time::Instant::now);
|
||||
let transparent = material_is_transparent(material, color);
|
||||
let face_indices = first.include_faces.then_some(first.indices.as_ref());
|
||||
let opaque_indices = if transparent {
|
||||
@@ -1298,7 +1298,7 @@ fn build_instanced_chunk(
|
||||
}
|
||||
}
|
||||
}
|
||||
let instances_at = profile_enabled.then(std::time::Instant::now);
|
||||
let instances_at = profile_enabled.then(iced::time::Instant::now);
|
||||
let triangles = if first.include_faces {
|
||||
(first.indices.len() / 3) as u64 * instances.len() as u64
|
||||
} else {
|
||||
@@ -1332,7 +1332,7 @@ fn build_instanced_chunk(
|
||||
.entry(edge_key)
|
||||
.or_insert_with(|| chunk.edge_vertex_buffer.clone());
|
||||
}
|
||||
let uploaded_at = profile_enabled.then(std::time::Instant::now);
|
||||
let uploaded_at = profile_enabled.then(iced::time::Instant::now);
|
||||
Some((
|
||||
chunk,
|
||||
triangles,
|
||||
@@ -1559,7 +1559,7 @@ pub fn build_mesh_batch_filtered(
|
||||
sets: &[MeshLodSet],
|
||||
handles: Option<&rustc_hash::FxHashSet<acadrust::Handle>>,
|
||||
) -> (Vec<MeshBatchChunk>, u64) {
|
||||
let perf_started = crate::perf::enabled().then(std::time::Instant::now);
|
||||
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
||||
// Derive the caps from the real device limit and vertex size. The previous
|
||||
// fixed 6 M-vertex cap assumed 40 B/vertex, but `position_low` (RTE) grew
|
||||
// MeshVertex to 52 B, so 6 M × 52 B = 312 MB blew past the 256 MB cap.
|
||||
@@ -1741,7 +1741,7 @@ pub fn build_mesh_batch_filtered(
|
||||
});
|
||||
}
|
||||
}
|
||||
let prepared_at = std::time::Instant::now();
|
||||
let prepared_at = iced::time::Instant::now();
|
||||
let spatial_bounds = sets.iter().fold(
|
||||
[
|
||||
f32::INFINITY,
|
||||
@@ -1828,7 +1828,7 @@ pub fn build_mesh_batch_filtered(
|
||||
mesh_spatial_key(part.set, spatial_bounds),
|
||||
)
|
||||
});
|
||||
let grouped_at = std::time::Instant::now();
|
||||
let grouped_at = iced::time::Instant::now();
|
||||
let ordered = direct_parts;
|
||||
let mut active_key: Option<MaterialBatchKey> = None;
|
||||
let mut active_material: Option<&crate::scene::model::material_model::MeshMaterial> = None;
|
||||
@@ -2224,7 +2224,7 @@ pub fn build_mesh_batch_filtered(
|
||||
None,
|
||||
));
|
||||
}
|
||||
let direct_at = std::time::Instant::now();
|
||||
let direct_at = iced::time::Instant::now();
|
||||
let mut instanced_profile = InstancedBuildProfile::default();
|
||||
let mut instanced_buffers = InstancedBufferCache::default();
|
||||
for parts in instance_groups.values() {
|
||||
@@ -2249,7 +2249,7 @@ pub fn build_mesh_batch_filtered(
|
||||
}
|
||||
}
|
||||
if let Some(started) = perf_started {
|
||||
let done = std::time::Instant::now();
|
||||
let done = iced::time::Instant::now();
|
||||
crate::perf_record!(
|
||||
"[perf] mesh-build-detail prepare={:.1} group={:.1} direct={:.1} instanced={:.1}",
|
||||
prepared_at.duration_since(started).as_secs_f64() * 1000.0,
|
||||
|
||||
@@ -29,6 +29,10 @@ impl Scene {
|
||||
self.preview_hatches = std::sync::Arc::new(models);
|
||||
}
|
||||
|
||||
pub fn set_command_preview_hatches(&mut self, models: Vec<HatchModel>) {
|
||||
self.preview_hatches = std::sync::Arc::new(models);
|
||||
}
|
||||
|
||||
pub fn set_area_preview_regions(&mut self, regions: &[AreaPreviewRegion]) {
|
||||
let mut models = Vec::new();
|
||||
for region in regions {
|
||||
@@ -154,6 +158,8 @@ impl Scene {
|
||||
world_origin: origin,
|
||||
boundary: std::sync::Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
pattern: HatchPattern::Solid,
|
||||
name: "AREA_PREVIEW".into(),
|
||||
color: [0.0; 4],
|
||||
|
||||
+54
-3
@@ -3,12 +3,46 @@ use super::*;
|
||||
|
||||
impl Scene {
|
||||
// ── Selection ─────────────────────────────────────────────────────────
|
||||
/// Treat a classic LEADER and its attached annotation as one logical object.
|
||||
/// Clicking/copying/deleting either side expands to the complete pair.
|
||||
pub(crate) fn handles_expanded_for_leader_annotations(
|
||||
&self,
|
||||
handles: &[Handle],
|
||||
) -> Vec<Handle> {
|
||||
let mut expanded = handles.to_vec();
|
||||
|
||||
for &handle in handles {
|
||||
// LEADER -> annotation.
|
||||
if let Some(EntityType::Leader(leader)) = self.document.get_entity(handle) {
|
||||
if !leader.annotation_handle.is_null() {
|
||||
expanded.push(leader.annotation_handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Annotation -> LEADER.
|
||||
expanded.extend(self.document.entities().filter_map(|entity| match entity {
|
||||
EntityType::Leader(leader)
|
||||
if !leader.annotation_handle.is_null()
|
||||
&& leader.annotation_handle == handle =>
|
||||
{
|
||||
Some(entity.common().handle)
|
||||
}
|
||||
_ => None,
|
||||
}));
|
||||
}
|
||||
|
||||
expanded.sort_unstable_by_key(|handle| handle.value());
|
||||
expanded.dedup();
|
||||
expanded
|
||||
}
|
||||
pub fn select_entity(&mut self, handle: Handle, exclusive: bool) {
|
||||
let handles = self.handles_expanded_for_leader_annotations(&[handle]);
|
||||
|
||||
if exclusive {
|
||||
self.selected.clear();
|
||||
}
|
||||
self.selected.insert(handle);
|
||||
|
||||
self.selected.extend(handles);
|
||||
self.bump_selection();
|
||||
}
|
||||
|
||||
@@ -21,6 +55,12 @@ impl Scene {
|
||||
/// only when its contents actually changed. History/file/command paths must
|
||||
/// use this instead of assigning `selected` directly.
|
||||
pub(crate) fn replace_selection(&mut self, selected: HashSet<Handle>) {
|
||||
let handles: Vec<Handle> = selected.iter().copied().collect();
|
||||
let selected: HashSet<Handle> = self
|
||||
.handles_expanded_for_leader_annotations(&handles)
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
if self.selected != selected {
|
||||
self.selected = selected;
|
||||
self.bump_selection();
|
||||
@@ -29,7 +69,14 @@ impl Scene {
|
||||
|
||||
/// Remove a single entity from the selection (Shift+click subtractive pick).
|
||||
pub fn deselect_entity(&mut self, handle: Handle) {
|
||||
if self.selected.remove(&handle) {
|
||||
let handles = self.handles_expanded_for_leader_annotations(&[handle]);
|
||||
let mut changed = false;
|
||||
|
||||
for handle in handles {
|
||||
changed |= self.selected.remove(&handle);
|
||||
}
|
||||
|
||||
if changed {
|
||||
self.bump_selection();
|
||||
}
|
||||
}
|
||||
@@ -608,10 +655,14 @@ impl Scene {
|
||||
// ── Erase ─────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn erase_entities(&mut self, handles: &[Handle]) {
|
||||
|
||||
let erase_handles = self.handles_expanded_for_leader_annotations(handles);
|
||||
|
||||
let mut handle_set: HashSet<Handle> = HashSet::default();
|
||||
let mut erased: Vec<(Handle, ChangeKind)> = Vec::new();
|
||||
let mut highlight_changed = false;
|
||||
for &h in handles {
|
||||
|
||||
for &h in &erase_handles {
|
||||
// Objects on a locked layer can't be erased.
|
||||
if self.is_layer_locked(h) {
|
||||
continue;
|
||||
|
||||
+39
-24
@@ -17,11 +17,12 @@ use std::sync::Arc;
|
||||
|
||||
/// A script we ship a Noto subset for. [`script_of`] maps a char to one.
|
||||
///
|
||||
/// CJK is split by language — Chinese, Japanese and Korean each get their own
|
||||
/// file. Their ideographs (Han, U+4E00–9FFF) share the same code points but
|
||||
/// differ in glyph shape, so the shared block is routed by the document's
|
||||
/// language (see [`set_cjk_lang_from_codepage`]); kana is always Japanese and
|
||||
/// Hangul always Korean.
|
||||
/// CJK is split by language — Simplified Chinese, Traditional Chinese,
|
||||
/// Japanese and Korean each get their own file. Their ideographs (Han,
|
||||
/// U+4E00–9FFF) share the same code points but differ in glyph shape, so the
|
||||
/// shared block is routed by the document's language (see
|
||||
/// [`set_cjk_lang_from_codepage`]); kana is always Japanese and Hangul always
|
||||
/// Korean.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Script {
|
||||
Latin,
|
||||
@@ -32,6 +33,7 @@ pub enum Script {
|
||||
Thai,
|
||||
Devanagari,
|
||||
Chinese,
|
||||
TraditionalChinese,
|
||||
Japanese,
|
||||
Korean,
|
||||
}
|
||||
@@ -49,6 +51,7 @@ impl Script {
|
||||
Script::Thai => "fonts/thai.ttf",
|
||||
Script::Devanagari => "fonts/devanagari.ttf",
|
||||
Script::Chinese => "fonts/chinese.ttf",
|
||||
Script::TraditionalChinese => "fonts/traditional_chinese.ttf",
|
||||
Script::Japanese => "fonts/japanese.ttf",
|
||||
Script::Korean => "fonts/korean.ttf",
|
||||
}
|
||||
@@ -57,6 +60,7 @@ impl Script {
|
||||
pub fn family(self) -> &'static str {
|
||||
match self {
|
||||
Script::Chinese => "Noto Sans CJK SC",
|
||||
Script::TraditionalChinese => "Noto Sans CJK TC",
|
||||
Script::Japanese => "Noto Sans CJK JP",
|
||||
Script::Korean => "Noto Sans CJK KR",
|
||||
Script::Latin
|
||||
@@ -80,6 +84,7 @@ impl Script {
|
||||
7 => Script::Chinese,
|
||||
8 => Script::Japanese,
|
||||
9 => Script::Korean,
|
||||
10 => Script::TraditionalChinese,
|
||||
_ => Script::Latin,
|
||||
}
|
||||
}
|
||||
@@ -97,12 +102,13 @@ impl Script {
|
||||
Script::Chinese => 7,
|
||||
Script::Japanese => 8,
|
||||
Script::Korean => 9,
|
||||
Script::TraditionalChinese => 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Language used to render shared Han ideographs: 0 = Chinese, 1 = Japanese,
|
||||
/// 2 = Korean. Set from the active document's code page.
|
||||
/// Language used to render shared Han ideographs: 0 = Simplified Chinese,
|
||||
/// 1 = Japanese, 2 = Korean, 3 = Traditional Chinese.
|
||||
static CJK_LANG: AtomicU8 = AtomicU8::new(0);
|
||||
static PRIMARY_SCRIPT: AtomicU8 = AtomicU8::new(0);
|
||||
static FONT_GENERATION: AtomicU64 = AtomicU64::new(0);
|
||||
@@ -117,22 +123,29 @@ fn bump_generation() {
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn scripts_for_language_tag(language: &str) -> Vec<Script> {
|
||||
let language = language
|
||||
let full_language = language.to_ascii_lowercase().replace('_', "-");
|
||||
let language = full_language
|
||||
.split(['-', '_'])
|
||||
.next()
|
||||
.unwrap_or(language)
|
||||
.unwrap_or(&full_language)
|
||||
.to_ascii_lowercase();
|
||||
let script = match language.as_str() {
|
||||
"ar" | "fa" | "ur" => Some(Script::Arabic),
|
||||
"el" => Some(Script::Greek),
|
||||
"he" | "yi" => Some(Script::Hebrew),
|
||||
"hi" | "mr" | "ne" => Some(Script::Devanagari),
|
||||
"ja" => Some(Script::Japanese),
|
||||
"ko" => Some(Script::Korean),
|
||||
"ru" | "uk" | "bg" | "sr" => Some(Script::Cyrillic),
|
||||
"th" => Some(Script::Thai),
|
||||
"zh" => Some(Script::Chinese),
|
||||
_ => None,
|
||||
let script = if matches!(full_language.as_str(), "zh-tw" | "zh-hk")
|
||||
|| full_language.starts_with("zh-hant")
|
||||
{
|
||||
Some(Script::TraditionalChinese)
|
||||
} else {
|
||||
match language.as_str() {
|
||||
"ar" | "fa" | "ur" => Some(Script::Arabic),
|
||||
"el" => Some(Script::Greek),
|
||||
"he" | "yi" => Some(Script::Hebrew),
|
||||
"hi" | "mr" | "ne" => Some(Script::Devanagari),
|
||||
"ja" => Some(Script::Japanese),
|
||||
"ko" => Some(Script::Korean),
|
||||
"ru" | "uk" | "bg" | "sr" => Some(Script::Cyrillic),
|
||||
"th" => Some(Script::Thai),
|
||||
"zh" => Some(Script::Chinese),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
vec![script.unwrap_or(Script::Latin)]
|
||||
}
|
||||
@@ -184,22 +197,24 @@ fn cjk_lang() -> Script {
|
||||
match CJK_LANG.load(Ordering::Relaxed) {
|
||||
1 => Script::Japanese,
|
||||
2 => Script::Korean,
|
||||
3 => Script::TraditionalChinese,
|
||||
_ => Script::Chinese,
|
||||
}
|
||||
}
|
||||
|
||||
/// Point the shared-Han routing at a language based on a DWG/DXF code page
|
||||
/// (`$DWGCODEPAGE`), e.g. `ANSI_932` → Japanese, `ANSI_949` → Korean, GB/936 or
|
||||
/// anything else → Chinese. Returns `true` if the language changed (the caller
|
||||
/// then clears the glyph cache and re-tessellates).
|
||||
/// (`$DWGCODEPAGE`), e.g. `ANSI_932` → Japanese, `ANSI_949` → Korean and
|
||||
/// `ANSI_950` → Traditional Chinese. Returns `true` if the language changed.
|
||||
pub fn set_cjk_lang_from_codepage(code_page: &str) -> bool {
|
||||
let c = code_page.to_ascii_uppercase();
|
||||
let lang = if c.contains("932") || c.contains("SJIS") || c.contains("SHIFT") {
|
||||
1 // Japanese (Shift-JIS)
|
||||
} else if c.contains("949") || c.contains("KOR") || c.contains("UHC") {
|
||||
2 // Korean
|
||||
} else if c.contains("950") || c.contains("BIG5") {
|
||||
3 // Traditional Chinese
|
||||
} else {
|
||||
0 // Chinese (936 / GB / 950 / Big5) or non-CJK default
|
||||
0 // Simplified Chinese (936 / GB) or non-CJK default
|
||||
};
|
||||
CJK_LANG.swap(lang, Ordering::Relaxed) != lang
|
||||
}
|
||||
|
||||
@@ -3112,7 +3112,7 @@ impl Scene {
|
||||
}
|
||||
let context_scale = match self.document.objects.get(&scale) {
|
||||
Some(acadrust::objects::ObjectType::Scale(value)) => {
|
||||
value.inverse_factor() as f32
|
||||
(value.inverse_factor() / self.annotation_scale_unit_factor()) as f32
|
||||
}
|
||||
_ => annotation_scale,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user