feat(frame): complete frame controls

Preserve frame geometry for selection while separating viewport and plot visibility across supported reference entities.

Refs #642
This commit is contained in:
Hakan Seven committed 2026-08-20 23:37:02 +03:00
commit 44efb8fd53
88 files changed
+1538 -702

No files matched your search

+28
View File
@@ -73,6 +73,8 @@ pub struct LocalWire {
/// shader band grows with a scaled insert. `0.0` = a normal wire.
pub world_width: f32,
pub plinegen: bool,
pub plot_visible: bool,
pub hide_unselected: bool,
/// Set at construction; used to discriminate fill-only GPU batches from
/// stroke batches in [`StyleKey`]. Derived from
/// `points.is_empty() && !fill_tris.is_empty()`.
@@ -729,6 +731,8 @@ fn tessellate_sub_local(
return vec![];
}
let frame_mode = crate::scene::frame::entity_kind(sub)
.map(|kind| crate::scene::frame::mode(doc, kind));
let mut result = Vec::with_capacity(wires_out.len());
for wire in wires_out {
// Per-wire point-count cap: a single wire that exceeds this is skipped
@@ -804,6 +808,8 @@ fn tessellate_sub_local(
line_weight_px: lw_px,
world_width: wire.world_width,
plinegen: wire.plinegen,
plot_visible: frame_mode.is_none_or(|mode| mode == 1),
hide_unselected: frame_mode == Some(0),
is_fill_only,
color_is_byblock: color_is_byblock && wire_on_base_color,
lt_is_byblock,
@@ -1362,6 +1368,8 @@ struct StyleKey {
/// switchable after block geometry is merged by style.
fill_is_2d_solid: bool,
fill_is_3d: bool,
plot_visible: bool,
hide_unselected: bool,
/// Bit-cast composed block-local depth for band wires (`0` = no override).
/// Keeps bands of different in-block draw ranks in separate batches so
/// each finalized WireModel carries one correct `depth_override`.
@@ -1396,6 +1404,8 @@ struct BatchEntry {
fill_tris_low: Vec<[f32; 3]>,
fill_is_3d: bool,
fill_is_2d_solid: bool,
plot_visible: bool,
hide_unselected: bool,
/// Accumulated thickness-wall pick geometry, paired high/low like
/// `fill_tris`. Pick-only — no GPU batch reads this.
pick_tris: Vec<[f32; 3]>,
@@ -1460,6 +1470,8 @@ impl BatchEntry {
_is_fill_only: bool,
fill_is_2d_solid: bool,
fill_is_3d: bool,
plot_visible: bool,
hide_unselected: bool,
) -> Self {
// `is_fill_only` is part of the StyleKey hash so greek fills never
// share a batch with regular wires (otherwise the finalized
@@ -1479,6 +1491,8 @@ impl BatchEntry {
plinegen,
fill_is_2d_solid,
fill_is_3d,
plot_visible,
hide_unselected,
min_x: f32::INFINITY,
min_y: f32::INFINITY,
max_x: f32::NEG_INFINITY,
@@ -1496,6 +1510,10 @@ impl Batches {
.into_iter()
.chain(self.by_style.into_values())
.map(|mut b| {
if b.hide_unselected && !selected {
b.points.clear();
b.points_low.clear();
}
let aabb = if b.min_x.is_infinite() {
WireModel::UNBOUNDED_AABB
} else {
@@ -1521,6 +1539,8 @@ impl Batches {
taper_widths: Vec::new(),
world_width: b.world_width,
depth_override: b.local_depth,
display_visible: !b.hide_unselected || selected,
plot_visible: b.plot_visible,
fill_is_3d: b.fill_is_3d,
fill_is_2d_solid: b.fill_is_2d_solid,
render_instance: None,
@@ -1573,6 +1593,8 @@ fn style_key(
is_fill_only: bool,
fill_is_2d_solid: bool,
fill_is_3d: bool,
plot_visible: bool,
hide_unselected: bool,
local_depth: Option<f32>,
) -> StyleKey {
StyleKey {
@@ -1603,6 +1625,8 @@ fn style_key(
is_fill_only,
fill_is_2d_solid,
fill_is_3d,
plot_visible,
hide_unselected,
depth_bits: local_depth.map_or(0, f32::to_bits),
}
}
@@ -2014,6 +2038,8 @@ fn emit_wire(
lw.is_fill_only,
lw.fill_is_2d_solid,
lw.fill_is_3d,
lw.plot_visible,
lw.hide_unselected,
local_depth,
);
@@ -2041,6 +2067,8 @@ fn emit_wire(
lw.is_fill_only,
lw.fill_is_2d_solid,
lw.fill_is_3d,
lw.plot_visible,
lw.hide_unselected,
)
});
entry.local_depth = local_depth;
+31 -4
View File
@@ -711,6 +711,8 @@ pub(crate) fn tessellate_entity(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -1063,6 +1065,8 @@ pub(crate) fn tessellate_entity(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -1148,14 +1152,21 @@ pub(crate) fn tessellate_entity(
wire.render_instance = Some(instance);
}
}
if document.header.xclip_frame != 0 && polygon.len() >= 3 {
wires.push(pick::xclip::frame_wire(
let frame_mode = crate::scene::frame::mode(
document,
crate::scene::frame::FrameKind::Xclip,
);
if polygon.len() >= 3 {
let mut frame = pick::xclip::frame_wire(
&polygon,
format!("{}_xclipframe", h.value()),
h.value().to_string(),
ins_color,
sel,
ins_lw_px,
));
);
frame.display_visible = frame_mode != 0;
frame.plot_visible = frame_mode == 1;
wires.push(frame);
}
}
@@ -1200,6 +1211,8 @@ pub(crate) fn tessellate_entity(
bg_color,
false,
);
let frame_mode = crate::scene::frame::entity_kind(e)
.map(|kind| crate::scene::frame::mode(document, kind));
for b in &mut bases {
b.aci = aci;
// SDF text wires carry a glyph-bounds AABB (the true text extent) set
@@ -1208,8 +1221,18 @@ pub(crate) fn tessellate_entity(
if b.text_verts.is_empty() {
set_wire_aabb(b, aabb);
}
if let Some(mode) = frame_mode {
b.display_visible = mode != 0;
b.plot_visible = mode == 1;
}
if matches!(e, EntityType::Wipeout(_)) {
b.depth_override = Some(0.5);
}
}
// A hidden mask frame remains selectable and appears while selected, but
// contributes no visible line work during normal display. The interior
// pick triangles remain intact.
// Complex linetypes (with embedded shapes / text) expand the *base*
// polyline along its tangent. Text-type entities never have a complex
// linetype assigned, so we only consult the first wire here — multi-wire
@@ -1437,6 +1460,8 @@ fn lod_stub_wire(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -1528,6 +1553,8 @@ fn lod_stub_wire_3d(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
+112
View File
@@ -127,6 +127,75 @@ pub(crate) fn points_to_ds(
(high, low)
}
fn point_cloud_wires(
document: &CadDocument,
handle: Handle,
entity: &EntityType,
selected: bool,
color: [f32; 4],
line_weight_px: f32,
) -> Option<Vec<WireModel>> {
let EntityType::Extended(extended) = entity else {
return None;
};
let frame_points = crate::entities::extended::point_cloud_frame_lines(extended)?;
let rendered = convert(entity, document)?;
let RenderObject::Lines(body_points) = rendered.object else {
return None;
};
let (points, points_low) = points_to_ds(body_points);
let mut wires = vec![WireModel {
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: handle.value().to_string(),
points,
points_low,
color,
selected,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px,
snap_pts: rendered.snap_pts,
tangent_geoms: rendered.tangent_geoms,
aci: 0,
key_vertices: rendered.key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
fill_tris: Vec::new(),
fill_tris_low: Vec::new(),
}];
let mode = crate::scene::frame::mode(
document,
crate::scene::frame::FrameKind::PointCloudClip,
);
if !frame_points.is_empty() {
let (points, points_low) = points_to_ds(frame_points);
let mut frame = WireModel::solid(
handle.value().to_string(),
points,
color,
selected,
);
frame.points_low = points_low;
frame.line_weight_px = line_weight_px;
frame.display_visible = mode != 0;
frame.plot_visible = mode == 1;
wires.push(frame);
}
Some(wires)
}
/// Lift a WireModel built in a local frame (a fixed f64 origin subtracted) back
/// to absolute world coordinates, re-splitting every position — polyline points
/// and SDF glyph vertices — into double-single so it stays precise at UTM scale.
@@ -434,6 +503,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -482,6 +553,17 @@ pub fn tessellate(
return out;
}
if let Some(wires) = point_cloud_wires(
document,
handle,
entity,
selected,
color,
line_weight_px,
) {
return wires;
}
// ── Try the kernel path first ───────────────────────────────────────────
// Relative-PDSIZE points size their glyph from the current zoom so they
// stay a roughly constant on-screen size; otherwise the header-driven path.
@@ -728,6 +810,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -775,6 +859,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -824,6 +910,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -854,6 +942,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -914,6 +1004,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -956,6 +1048,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -995,6 +1089,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -1027,6 +1123,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -1093,6 +1191,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -1214,6 +1314,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: polyline_band_width(entity),
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d,
fill_is_2d_solid: false,
render_instance: None,
@@ -1279,6 +1381,8 @@ pub fn tessellate(
fill_is_2d_solid: matches!(entity, EntityType::Solid(_)),
render_instance: None,
depth_override: None,
display_visible: true,
plot_visible: true,
});
}
@@ -1287,6 +1391,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -1332,6 +1438,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: polyline_band_width(entity),
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -1378,6 +1486,8 @@ pub fn tessellate(
taper_widths: widths,
world_width,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -1497,6 +1607,8 @@ pub fn tessellate(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
+125 -57
View File
@@ -1393,6 +1393,9 @@ impl Scene {
wipeout.v_vector =
context.transform.apply_rotation(source.v_vector);
}
let Some(mut fill_plane) = Self::wipeout_fill_plane(&wipeout) else {
return;
};
let (world_origin, mut boundary) =
Self::wipeout_boundary_2d(&wipeout);
for clip in &context.clips {
@@ -1409,6 +1412,16 @@ impl Scene {
if boundary.len() < 3 {
return;
}
if !context.clips.is_empty() {
let Some(local) = Self::wipeout_boundary_at_xy(
fill_plane.0,
world_origin,
&boundary,
) else {
return;
};
fill_plane.1 = local;
}
let selection_handle =
if tint_insert_selection && context.is_instanced() {
context.root_handle
@@ -1446,10 +1459,14 @@ impl Scene {
} else {
None
};
let fill_plane_boundary = Some(Arc::new(fill_plane.1));
let fill_plane = Some(fill_plane.0);
models.push(HatchModel {
render_instance,
boundary: Arc::new(boundary),
boundary_wcs: None,
fill_plane,
fill_plane_boundary,
boundary_exterior: None,
boundary_sources: None,
pattern: model::hatch_model::HatchPattern::Solid,
@@ -1474,66 +1491,113 @@ impl Scene {
pub(super) fn wipeout_boundary_2d(
wo: &acadrust::entities::Wipeout,
) -> ([f64; 2], Vec<[f32; 2]>) {
use acadrust::entities::WipeoutClipType;
let origin = [wo.insertion_point.x, wo.insertion_point.y];
let is_polygon = wo.clipping_enabled
&& wo.clip_boundary_vertices.len() >= 3
&& matches!(wo.clip_type, WipeoutClipType::Polygonal);
if is_polygon {
// DXF clip vertices live in image-pixel space, centred on the
// image (range −size/2 … +size/2). Image-bottom-left → insertion,
// image-y-axis points DOWN (per the DXF "v_vector points down the
// image" convention), so map:
// x_off = (clip.x + size.x/2) × u_vec
// y_off = (size.y/2 − clip.y) × v_vec ← y flipped
// Offsets are relative to `origin` (the insertion point).
let cx_of = |v: &acadrust::types::Vector2| v.x + wo.size.x * 0.5;
let cy_of = |v: &acadrust::types::Vector2| wo.size.y * 0.5 - v.y;
let mut poly: Vec<[f32; 2]> = wo
.clip_boundary_vertices
.iter()
.map(|v| {
let cx = cx_of(v);
let cy = cy_of(v);
let wx = (wo.u_vector.x * cx + wo.v_vector.x * cy) as f32;
let wy = (wo.u_vector.y * cx + wo.v_vector.y * cy) as f32;
[wx, wy]
})
.collect();
// Close the loop: the GPU `in_polygon` ray-cast walks
// sequential pairs and doesn't wrap, so without an explicit
// closing vertex the last edge (vN-1 → v0) is never tested and
// the fill bleeds far past the boundary.
if let Some(&first) = poly.first() {
if poly.last() != Some(&first) {
poly.push(first);
let plane = cadkernel::space::Plane::from_axes(
[wo.insertion_point.x, wo.insertion_point.y, wo.insertion_point.z],
[wo.u_vector.x, wo.u_vector.y, wo.u_vector.z],
[wo.v_vector.x, wo.v_vector.y, wo.v_vector.z],
);
let boundary = Self::wipeout_local_boundary(wo)
.into_iter()
.map(|point| {
if point[0].is_finite() && point[1].is_finite() {
let offset = plane.vector_at(point);
[offset[0] as f32, offset[1] as f32]
} else {
[f32::NAN; 2]
}
}
(origin, poly)
} else {
// Rectangular boundary from 4 corners, as offsets from `origin`.
let ux = (wo.u_vector.x * wo.size.x) as f32;
let uy = (wo.u_vector.y * wo.size.x) as f32;
let vx = (wo.v_vector.x * wo.size.y) as f32;
let vy = (wo.v_vector.y * wo.size.y) as f32;
// Close the loop (repeat corner 0): the GPU `in_polygon` ray-cast
// walks sequential vertex pairs and never wraps last→first, so an
// unclosed quad leaves the v3→v0 edge untested and the solid mask
// bleeds past the boundary — same reason the polygon branch closes.
(
origin,
vec![
[0.0, 0.0],
[ux, uy],
[ux + vx, uy + vy],
[vx, vy],
[0.0, 0.0],
],
)
})
.collect();
(origin, boundary)
}
fn wipeout_local_boundary(wo: &acadrust::entities::Wipeout) -> Vec<[f64; 2]> {
use acadrust::entities::{WipeoutClipMode, WipeoutClipType};
let mut outer = vec![
[0.0, 0.0],
[wo.size.x, 0.0],
[wo.size.x, wo.size.y],
[0.0, wo.size.y],
[0.0, 0.0],
];
if !wo.clipping_enabled
|| wo.clip_boundary_vertices.len() < 3
|| !matches!(wo.clip_type, WipeoutClipType::Polygonal)
{
return outer;
}
let mut clip: Vec<[f64; 2]> = wo
.clip_boundary_vertices
.iter()
.map(|point| {
[
point.x + wo.size.x * 0.5,
wo.size.y * 0.5 - point.y,
]
})
.collect();
if clip.last() != clip.first() {
clip.push(clip[0]);
}
if matches!(wo.clip_mode, WipeoutClipMode::Inside) {
outer.push([f64::NAN; 2]);
outer.extend(clip);
outer
} else {
clip
}
}
fn wipeout_fill_plane(
wipeout: &acadrust::entities::Wipeout,
) -> Option<(model::hatch_model::FillPlane, Vec<[f32; 2]>)> {
let origin = [
wipeout.insertion_point.x,
wipeout.insertion_point.y,
wipeout.insertion_point.z,
];
let x_axis = [wipeout.u_vector.x, wipeout.u_vector.y, wipeout.u_vector.z];
let y_axis = [wipeout.v_vector.x, wipeout.v_vector.y, wipeout.v_vector.z];
cadkernel::space::Plane::from_axes(origin, x_axis, y_axis).normal()?;
let boundary = Self::wipeout_local_boundary(wipeout)
.into_iter()
.map(|point| [point[0] as f32, point[1] as f32])
.collect();
Some((
model::hatch_model::FillPlane {
origin,
x_axis,
y_axis,
},
boundary,
))
}
fn wipeout_boundary_at_xy(
plane: model::hatch_model::FillPlane,
world_origin: [f64; 2],
boundary: &[[f32; 2]],
) -> Option<Vec<[f32; 2]>> {
let plane = cadkernel::space::Plane::from_axes(
plane.origin,
plane.x_axis,
plane.y_axis,
);
boundary
.iter()
.map(|point| {
if point[0].is_finite() && point[1].is_finite() {
plane
.coordinates_at_xy([
world_origin[0] + point[0] as f64,
world_origin[1] + point[1] as f64,
])
.map(|point| [point[0] as f32, point[1] as f32])
} else {
Some([f32::NAN; 2])
}
})
.collect()
}
pub(crate) fn hatch_model_from_dxf(
@@ -1817,6 +1881,8 @@ impl Scene {
render_instance: None,
boundary: std::sync::Arc::new(boundary_f32),
boundary_wcs: None,
fill_plane: None,
fill_plane_boundary: None,
boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)),
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
pattern,
@@ -2133,6 +2199,8 @@ impl Scene {
render_instance: None,
boundary: std::sync::Arc::new(boundary),
boundary_wcs: None,
fill_plane: None,
fill_plane_boundary: None,
boundary_exterior: None,
boundary_sources: None,
pattern: model::hatch_model::HatchPattern::Solid,
+211
View File
@@ -0,0 +1,211 @@
use acadrust::objects::{ObjectType, RasterVariables, WipeoutVariables};
use acadrust::{CadDocument, EntityType, Handle};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FrameKind {
Image,
Pdf,
Wipeout,
Xclip,
PointCloudClip,
}
pub(crate) const ALL_KINDS: [FrameKind; 5] = [
FrameKind::Image,
FrameKind::Pdf,
FrameKind::Wipeout,
FrameKind::Xclip,
FrameKind::PointCloudClip,
];
fn root_entry(document: &CadDocument, name: &str) -> Option<Handle> {
let from_dictionary = |handle| match document.objects.get(&handle) {
Some(ObjectType::Dictionary(dictionary)) => dictionary
.entries
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, handle)| *handle),
_ => None,
};
from_dictionary(document.header.named_objects_dict_handle).or_else(|| {
document.objects.values().find_map(|object| match object {
ObjectType::Dictionary(dictionary) if dictionary.owner.is_null() => {
from_dictionary(dictionary.handle)
}
_ => None,
})
})
}
fn raster_variables_handle(document: &CadDocument) -> Option<Handle> {
root_entry(document, "ACAD_IMAGE_VARS")
.filter(|handle| matches!(document.objects.get(handle), Some(ObjectType::RasterVariables(_))))
}
fn wipeout_variables_handle(document: &CadDocument) -> Option<Handle> {
root_entry(document, "ACAD_WIPEOUT_VARS")
.filter(|handle| matches!(document.objects.get(handle), Some(ObjectType::WipeoutVariables(_))))
}
fn drawing_mode(document: &CadDocument, name: &str, default: i16) -> i16 {
crate::io::drawing_variable(document, name)
.and_then(|value| value.parse::<i16>().ok())
.unwrap_or(default)
.clamp(0, 2)
}
pub(crate) fn mode(document: &CadDocument, kind: FrameKind) -> i16 {
match kind {
FrameKind::Image => raster_variables_handle(document)
.and_then(|handle| match document.objects.get(&handle) {
Some(ObjectType::RasterVariables(value)) => Some(value.display_image_frame),
_ => None,
})
.unwrap_or(1)
.clamp(0, 2),
FrameKind::Pdf => drawing_mode(document, "PDFFRAME", 1),
FrameKind::Wipeout => crate::io::drawing_variable(document, "WIPEOUTFRAME")
.and_then(|value| value.parse::<i16>().ok())
.or_else(|| {
wipeout_variables_handle(document).and_then(|handle| {
match document.objects.get(&handle) {
Some(ObjectType::WipeoutVariables(value)) => Some(value.display_frame),
_ => None,
}
})
})
.unwrap_or(1)
.clamp(0, 2),
FrameKind::Xclip => document.header.xclip_frame.clamp(0, 2),
FrameKind::PointCloudClip => drawing_mode(document, "POINTCLOUDCLIPFRAME", 2),
}
}
pub(crate) fn master_mode(document: &CadDocument) -> i16 {
let first = mode(document, ALL_KINDS[0]);
ALL_KINDS
.iter()
.skip(1)
.all(|kind| mode(document, *kind) == first)
.then_some(first)
.unwrap_or(3)
}
fn attach_root_entry(document: &mut CadDocument, name: &str, handle: Handle) {
let root = crate::scene::annotative::root_named_dict_handle(document);
if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&root) {
dictionary
.entries
.retain(|(key, _)| !key.eq_ignore_ascii_case(name));
dictionary.add_entry(name, handle);
}
}
fn set_image_mode(document: &mut CadDocument, value: i16) {
let owner = crate::scene::annotative::root_named_dict_handle(document);
let handle = raster_variables_handle(document).unwrap_or_else(|| {
let handle = document.allocate_handle();
let mut variables = RasterVariables::new();
variables.handle = handle;
variables.owner = owner;
document
.objects
.insert(handle, ObjectType::RasterVariables(variables));
handle
});
if let Some(ObjectType::RasterVariables(variables)) = document.objects.get_mut(&handle) {
variables.owner = owner;
variables.display_image_frame = value;
}
attach_root_entry(document, "ACAD_IMAGE_VARS", handle);
}
fn set_wipeout_mode(document: &mut CadDocument, value: i16) {
crate::io::set_drawing_variable(document, "WIPEOUTFRAME", &value.to_string());
let owner = crate::scene::annotative::root_named_dict_handle(document);
let handle = wipeout_variables_handle(document).unwrap_or_else(|| {
let handle = document.allocate_handle();
let mut variables = WipeoutVariables::new();
variables.handle = handle;
variables.owner = owner;
document
.objects
.insert(handle, ObjectType::WipeoutVariables(variables));
handle
});
if let Some(ObjectType::WipeoutVariables(variables)) = document.objects.get_mut(&handle) {
variables.owner = owner;
variables.display_frame = value;
}
attach_root_entry(document, "ACAD_WIPEOUT_VARS", handle);
}
pub(crate) fn set_mode(document: &mut CadDocument, kind: FrameKind, value: i16) {
let value = value.clamp(0, 2);
match kind {
FrameKind::Image => set_image_mode(document, value),
FrameKind::Pdf => crate::io::set_drawing_variable(document, "PDFFRAME", &value.to_string()),
FrameKind::Wipeout => set_wipeout_mode(document, value),
FrameKind::Xclip => document.header.xclip_frame = value,
FrameKind::PointCloudClip => crate::io::set_drawing_variable(
document,
"POINTCLOUDCLIPFRAME",
&value.to_string(),
),
}
}
pub(crate) fn set_master_mode(document: &mut CadDocument, value: i16) {
let value = value.clamp(0, 2);
for kind in ALL_KINDS {
if mode(document, kind) != value {
set_mode(document, kind, value);
}
}
}
pub(crate) fn kind_for_name(name: &str) -> Option<FrameKind> {
match name {
"IMAGEFRAME" => Some(FrameKind::Image),
"PDFFRAME" => Some(FrameKind::Pdf),
"WIPEOUTFRAME" => Some(FrameKind::Wipeout),
"XCLIPFRAME" => Some(FrameKind::Xclip),
"POINTCLOUDCLIPFRAME" => Some(FrameKind::PointCloudClip),
_ => None,
}
}
pub(crate) fn entity_kind(entity: &EntityType) -> Option<FrameKind> {
match entity {
EntityType::RasterImage(_) => Some(FrameKind::Image),
EntityType::Underlay(underlay)
if matches!(underlay.underlay_type, acadrust::entities::UnderlayType::Pdf) =>
{
Some(FrameKind::Pdf)
}
EntityType::Wipeout(_) => Some(FrameKind::Wipeout),
_ => None,
}
}
pub(crate) fn affected(entity: &EntityType, kind: FrameKind) -> bool {
match kind {
FrameKind::Image => matches!(entity, EntityType::RasterImage(_)),
FrameKind::Pdf => matches!(
entity,
EntityType::Underlay(underlay)
if matches!(underlay.underlay_type, acadrust::entities::UnderlayType::Pdf)
),
FrameKind::Wipeout => matches!(entity, EntityType::Wipeout(_)),
FrameKind::Xclip => matches!(entity, EntityType::Insert(_)),
FrameKind::PointCloudClip => matches!(
entity,
EntityType::Extended(extended)
if matches!(
&extended.data,
acadrust::entities::ExtendedEntityData::PointCloud(_)
| acadrust::entities::ExtendedEntityData::PointCloudEx(_)
)
),
}
}
+3
View File
@@ -9,6 +9,7 @@ pub mod annotative;
pub mod cache;
pub mod convert;
pub mod creation_style;
pub(crate) mod frame;
pub mod model;
pub mod pick;
pub mod pipeline;
@@ -3210,6 +3211,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,
fill_plane: None,
fill_plane_boundary: None,
boundary_exterior: None,
boundary_sources: None,
pattern: crate::scene::model::hatch_model::HatchPattern::Solid,
+10
View File
@@ -165,6 +165,13 @@ pub enum HatchPattern {
/// (anchor-relative), so real coordinates never approach it.
pub const GPU_BOUNDARY_SEP: f32 = 1.0e30;
#[derive(Clone, Copy, Debug)]
pub struct FillPlane {
pub origin: [f64; 3],
pub x_axis: [f64; 3],
pub y_axis: [f64; 3],
}
/// A hatched region defined by a closed polygon boundary.
#[derive(Clone, Debug)]
pub struct HatchModel {
@@ -196,6 +203,9 @@ 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]>>>,
/// Optional 3-D placement used by wipeout fills.
pub fill_plane: Option<FillPlane>,
pub fill_plane_boundary: Option<Arc<Vec<[f32; 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.
+8
View File
@@ -146,6 +146,10 @@ pub struct WireModel {
/// mirroring how exploded block fills seed their depth, so bands and fills
/// from the same block interleave by the block's internal draw order.
pub depth_override: Option<f32>,
/// Whether this wire is drawn in the normal viewport pass.
pub display_visible: bool,
/// Whether this wire is included in plotted output.
pub plot_visible: bool,
/// `true` when [`fill_tris`] is a real 3-D surface (PolyfaceMesh /
/// PolygonMesh face) that must render with hidden-surface depth and only in
/// shaded modes. `false` for a flat 2-D overlay fill (SOLID arrowhead,
@@ -208,6 +212,8 @@ impl WireModel {
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -502,6 +508,8 @@ impl Default for WireModel {
fill_tris: Vec::new(),
fill_tris_low: Vec::new(),
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
+2
View File
@@ -260,6 +260,8 @@ pub fn frame_wire(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
+7 -4
View File
@@ -948,10 +948,8 @@ impl Pipeline {
);
// ── Hatch pipeline ─────────────────────────────────────────────────
// binding 0 (HatchUniforms) is read by the vertex shader too — it
// pulls `origin` to undo the CPU-side hatch-local pre-shift when
// computing clip position. bindings 1 (Boundary) and 2
// (FamilyBatch) stay fragment-only.
// Binding 0 carries the fill plane; boundary and family data are
// fragment-only.
let hatch_entry = |binding: u32, vis: wgpu::ShaderStages| wgpu::BindGroupLayoutEntry {
binding,
visibility: vis,
@@ -2364,6 +2362,10 @@ impl Pipeline {
let mut block_wires: Vec<&WireModel> = Vec::new();
let mut i = 0;
while i < wires.len() {
if !wires[i].display_visible {
i += 1;
continue;
}
if wires[i].render_instance.is_some() {
block_wires.push(&wires[i]);
i += 1;
@@ -2372,6 +2374,7 @@ impl Pipeline {
let mesh_edge = is_mesh_edge(&wires[i]);
let mut j = i + 1;
while j < wires.len()
&& wires[j].display_visible
&& wires[j].render_instance.is_none()
&& is_mesh_edge(&wires[j]) == mesh_edge
{
+3
View File
@@ -433,6 +433,9 @@ pub fn upload_block_vertex_refs(
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&crate::scene::model::wire_model::WireModel>> = Vec::new();
for &wire in wires {
if tint.is_none() && !wire.display_visible {
continue;
}
let Some(instance) = wire.render_instance else {
continue;
};
+107 -115
View File
@@ -9,11 +9,13 @@
// a limit on visible hatch fills.
//
// Group 1 bindings per fill:
// binding 0 — HatchUniformData (64 bytes) mode, color, angle_offset, scale, gradient params
// binding 0 — HatchUniformData fill and plane parameters
// binding 1 — BoundaryData (16384 bytes) boundary polygon vertices
// binding 2 — FamilyBatchData (1296 bytes) up to 16 line families + 128 dash values (unused for wipeouts)
use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily, MAX_HATCH_BOUNDARY_VERTS};
use crate::scene::model::hatch_model::{
FillPlane, HatchModel, HatchPattern, PatFamily, MAX_HATCH_BOUNDARY_VERTS,
};
use iced::wgpu;
use iced::wgpu::util::DeviceExt;
@@ -48,10 +50,10 @@ impl HatchVertex {
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct WipeoutPlacement {
pub translation: [f32; 2],
pub translation_low: [f32; 2],
pub translation: [f32; 3],
pub translation_low: [f32; 3],
pub draw_depth: f32,
pub _pad: [f32; 3],
pub _pad: f32,
}
impl WipeoutPlacement {
@@ -60,9 +62,9 @@ impl WipeoutPlacement {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 8, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 16, shader_location: 3, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: 12, shader_location: 2, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: 24, shader_location: 3, format: wgpu::VertexFormat::Float32 },
],
}
}
@@ -70,7 +72,7 @@ impl WipeoutPlacement {
// ── Uniform structs ───────────────────────────────────────────────────────
/// Per-hatch parameters (binding 0). Must be 96 bytes (16-byte aligned).
/// Per-fill parameters (binding 0).
///
/// `mode` encoding:
/// 0 → Pattern (families in FamilyBatchData)
@@ -89,18 +91,15 @@ pub struct HatchUniformData {
pub grad_sin: f32, // 52: gradient direction sin
pub grad_min: f32, // 56: gradient proj_min
pub grad_range: f32, // 60: gradient proj_range
pub origin: [f32; 2], // 64: hatch-local origin (boundary AABB centre),
// // high half of the double-single anchor.
// // The pattern fragment shader subtracts this
// // from xz before the perp/perp_step quotient
// // so f32 doesn't catastrophically cancel when
// // world coords are large and pattern spacing
// // is small (e.g. UTM drawing + sub-mm hatch).
pub origin_low: [f32; 2], // 72: anchor low residual (double-single) so the
// // vertex clip position stays precise at UTM scale.
pub origin: [f32; 2], // 64: reserved pattern origin
pub origin_low: [f32; 2], // 72: reserved low residual
pub draw_depth: f32, // 80: signed (-1,1) draw-order bias
pub _pad: [f32; 3], // 84..96
} // total 96 bytes
pub plane_origin_high: [f32; 4],
pub plane_origin_low: [f32; 4],
pub plane_x: [f32; 4],
pub plane_y: [f32; 4],
}
/// Boundary polygon (binding 1). Matches WGSL `Boundary`.
#[repr(C)]
@@ -194,13 +193,22 @@ impl WipeoutGpu {
};
// ── Bounding box ─────────────────────────────────────────────────
let plane = model.fill_plane.unwrap_or(FillPlane {
origin: [model.world_origin[0], model.world_origin[1], 0.0],
x_axis: [1.0, 0.0, 0.0],
y_axis: [0.0, 1.0, 0.0],
});
let gpu_boundary = model
.fill_plane_boundary
.as_deref()
.unwrap_or(model.boundary.as_ref());
let (mut min_x, mut max_x, mut min_y, mut max_y) = (
f32::INFINITY,
f32::NEG_INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
);
for &[x, y] in model.boundary.iter() {
for &[x, y] in gpu_boundary {
if !x.is_finite() || !y.is_finite() {
continue;
}
@@ -218,68 +226,16 @@ impl WipeoutGpu {
_ => 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);
// ── Pattern phase anchor.
//
// Boundary verts are already stored as small f32 offsets from
// `model.world_origin` (an f64 anchor near the AABB centre), so
// the in_polygon ray-cast and the pattern math both run at full
// f32 precision in the fragment shader. The remaining question
// is *which* offset-rel point we hand to the vertex shader as
// `h.origin` (so the quad lands at the correct WCS position
// after `view_proj * (v.pos + h.origin)`).
//
// For pattern alignment across adjacent hatches with the same
// family, snap `h.origin` to the family's perp/along grid in
// f64. Without the snap, each hatch's pattern phase is per-hatch
// and stripes don't continue across hatch boundaries (you see
// pattern misalignment along a road etc).
let mut origin = model.world_origin;
if let HatchPattern::Pattern(families) = &model.pattern {
if let Some(fam) = families.first() {
// Must mirror the shader's pattern math exactly. QCAD PAT
// convention (matching build_family_batch): `dy` is the
// perpendicular spacing and `dx` is the along-line phase
// shift, both in family-local space. Multiplied by
// `model.scale` in the fragment shader. Combined angle
// (family + pattern_angle) rotates the perp/along axes.
let angle = (fam.angle_deg.to_radians() + model.angle_offset) as f64;
let cos_a = angle.cos();
let sin_a = angle.sin();
let scale = model.scale as f64;
let perp_step = fam.dy as f64 * scale;
let along_step = fam.dx as f64 * scale;
let o_perp = -origin[0] * sin_a + origin[1] * cos_a;
let o_along = origin[0] * cos_a + origin[1] * sin_a;
let snapped_perp = if perp_step.abs() > 1e-12 {
(o_perp / perp_step).round() * perp_step
} else {
o_perp
};
let snapped_along = if along_step.abs() > 1e-12 {
(o_along / along_step).round() * along_step
} else {
o_along
};
origin = [
-snapped_perp * sin_a + snapped_along * cos_a,
snapped_perp * cos_a + snapped_along * sin_a,
];
}
}
// Boundary buffer + quad now live in "snap-shifted" hatch-local
// coords: subtract any drift between model.world_origin and the
// snapped origin so the in_polygon ray-cast in fragment shader
// (which reads the boundary verts straight) lines up with v.xz.
let drift = [
(model.world_origin[0] - origin[0]) as f32,
(model.world_origin[1] - origin[1]) as f32,
];
let pad = if mode == 1 {
0.0
} else {
(diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0)
};
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,
min_x - pad,
max_x + pad,
min_y - pad,
max_y + pad,
);
let quad = [
@@ -316,11 +272,9 @@ impl WipeoutGpu {
// ── Gradient: projection range (in snapped-local space) ──────────
let (grad_min, grad_range) = if mode == 2 {
let projs: Vec<f32> = model
.boundary
.iter()
let projs: Vec<f32> = gpu_boundary.iter()
.filter(|v| v[0].is_finite() && v[1].is_finite())
.map(|&[x, y]| (x + drift[0]) * grad_cos + (y + drift[1]) * grad_sin)
.map(|&[x, y]| x * grad_cos + y * grad_sin)
.collect();
if projs.is_empty() {
(0.0, 1.0)
@@ -334,7 +288,11 @@ impl WipeoutGpu {
};
// ── HatchUniformData ─────────────────────────────────────────────
let n = model.boundary.len().min(MAX_HATCH_BOUNDARY_VERTS);
let n = gpu_boundary.len().min(MAX_HATCH_BOUNDARY_VERTS);
let plane_origin_high = plane.origin.map(|value| value as f32);
let plane_origin_low: [f32; 3] = std::array::from_fn(|index| {
(plane.origin[index] - plane_origin_high[index] as f64) as f32
});
let uniform_data = HatchUniformData {
color: model.color,
color2,
@@ -346,30 +304,46 @@ impl WipeoutGpu {
grad_sin,
grad_min,
grad_range,
origin: [origin[0] as f32, origin[1] as f32],
origin_low: [
(origin[0] - origin[0] as f32 as f64) as f32,
(origin[1] - origin[1] as f32 as f64) as f32,
],
origin: [0.0; 2],
origin_low: [0.0; 2],
draw_depth: 0.0,
_pad: [0.0; 3],
plane_origin_high: [
plane_origin_high[0],
plane_origin_high[1],
plane_origin_high[2],
0.0,
],
plane_origin_low: [
plane_origin_low[0],
plane_origin_low[1],
plane_origin_low[2],
0.0,
],
plane_x: [
plane.x_axis[0] as f32,
plane.x_axis[1] as f32,
plane.x_axis[2] as f32,
0.0,
],
plane_y: [
plane.y_axis[0] as f32,
plane.y_axis[1] as f32,
plane.y_axis[2] as f32,
0.0,
],
};
// ── BoundaryData (in snapped-local space) ────────────────────────
let mut boundary_data = BoundaryData {
verts: [[0.0; 4]; MAX_HATCH_BOUNDARY_VERTS],
};
for (i, &[x, y]) in model
.boundary
.iter()
for (i, &[x, y]) in gpu_boundary.iter()
.take(MAX_HATCH_BOUNDARY_VERTS)
.enumerate()
{
// Path-break separators go up as the finite GPU sentinel, not
// NaN — the shader NaN self-compare is folded to `true` on some
// drivers (#386, #416) — and must not be shifted by `drift`.
if x.is_finite() && y.is_finite() {
boundary_data.verts[i] = [x + drift[0], y + drift[1], 0.0, 0.0];
boundary_data.verts[i] = [x, y, 0.0, 0.0];
} else {
let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP;
boundary_data.verts[i] = [s, s, 0.0, 0.0];
@@ -433,16 +407,15 @@ impl WipeoutGpu {
let translation = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let delta = [translation[0] - base[0], translation[1] - base[1]];
let high = [delta[0] as f32, delta[1] as f32];
let delta = std::array::from_fn(|index| translation[index] - base[index]);
let high = delta.map(|value| value as f32);
WipeoutPlacement {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
],
translation_low: std::array::from_fn(|index| {
(delta[index] - high[index] as f64) as f32
}),
draw_depth: model.draw_depth,
_pad: [0.0; 3],
_pad: 0.0,
}
})
.collect();
@@ -451,19 +424,38 @@ impl WipeoutGpu {
contents: bytemuck::cast_slice(&placements),
usage: wgpu::BufferUsages::VERTEX,
});
let ox = model.world_origin[0] as f32;
let oy = model.world_origin[1] as f32;
let world_aabb = if min_x.is_finite() && min_y.is_finite() {
let mut world_bounds = [
f32::INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
];
for &[x, y] in model.boundary.iter() {
if x.is_finite() && y.is_finite() {
world_bounds[0] = world_bounds[0].min(x);
world_bounds[1] = world_bounds[1].min(y);
world_bounds[2] = world_bounds[2].max(x);
world_bounds[3] = world_bounds[3].max(y);
}
}
let tilted = plane.origin[2].abs() > f64::EPSILON
|| plane.x_axis[2].abs() > f64::EPSILON
|| plane.y_axis[2].abs() > f64::EPSILON;
let world_aabb = if tilted {
[f32::NAN; 4]
} else if world_bounds[0].is_finite() && world_bounds[1].is_finite() {
let ox = model.world_origin[0] as f32;
let oy = model.world_origin[1] as f32;
let mut aabb = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
for placement in &placements {
aabb[0] = aabb[0].min(min_x + ox + placement.translation[0]);
aabb[1] = aabb[1].min(min_y + oy + placement.translation[1]);
aabb[2] = aabb[2].max(max_x + ox + placement.translation[0]);
aabb[3] = aabb[3].max(max_y + oy + placement.translation[1]);
aabb[0] = aabb[0].min(world_bounds[0] + ox + placement.translation[0]);
aabb[1] = aabb[1].min(world_bounds[1] + oy + placement.translation[1]);
aabb[2] = aabb[2].max(world_bounds[2] + ox + placement.translation[0]);
aabb[3] = aabb[3].max(world_bounds[3] + oy + placement.translation[1]);
}
aabb
} else {
[min_x, min_y, max_x, max_y]
world_bounds
};
Self {
@@ -482,7 +474,7 @@ impl WipeoutGpu {
// ── Helpers ───────────────────────────────────────────────────────────────
/// Perpendicular spacing between adjacent parallel lines.
/// QCAD PAT format stores (dx, dy) in line-local coordinates: dy = perpendicular, dx = along-line.
/// Pattern spacing perpendicular to adjacent parallel lines.
fn perp_spacing(f: &PatFamily) -> f32 {
f.dy
}
@@ -519,7 +511,7 @@ fn build_family_batch(pattern: &HatchPattern) -> FamilyBatchData {
let angle_r = family.angle_deg.to_radians();
let cos_a = angle_r.cos();
let sin_a = angle_r.sin();
// QCAD PAT local-frame convention: dy = perpendicular spacing, dx = along-line phase shift.
// Pattern offsets are stored in the line-local frame.
let perp_step = family.dy;
let along_step = family.dx;
let line_width = 0.0_f32; // unused: shader uses screen-space derivative for 1px lines
+3
View File
@@ -655,6 +655,9 @@ impl BlockWireGpu {
rustc_hash::FxHashMap::default();
let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new();
for &wire in wires {
if color_override.is_none() && !wire.display_visible {
continue;
}
let Some(instance) = wire.render_instance else {
continue;
};
+2
View File
@@ -158,6 +158,8 @@ impl Scene {
world_origin: origin,
boundary: std::sync::Arc::new(boundary),
boundary_wcs: None,
fill_plane: None,
fill_plane_boundary: None,
boundary_exterior: None,
boundary_sources: None,
pattern: HatchPattern::Solid,
+36 -13
View File
@@ -409,8 +409,8 @@ impl Scene {
} else {
0.0
};
let project = |x: f64, y: f64| -> Option<[f32; 2]> {
let delta = glam::DVec3::new(x, y, 0.0) - camera.target;
let project_3d = |point: [f64; 3]| -> Option<[f32; 2]> {
let delta = glam::DVec3::from(point) - camera.target;
let u = delta.dot(view_right.as_dvec3());
let v = delta.dot(view_up.as_dvec3());
let factor = if perspective {
@@ -427,6 +427,7 @@ impl Scene {
(center_y + v * factor * viewport_scale) as f32,
])
};
let project = |x: f64, y: f64| project_3d([x, y, 0.0]);
let frozen: rustc_hash::FxHashSet<Handle> =
viewport.frozen_layers.iter().copied().collect();
@@ -481,7 +482,7 @@ impl Scene {
continue;
}
if let Some(hatch) =
project_plot_fill(hatch, &project, xmin, ymin, xmax, ymax)
project_plot_fill(hatch, &project_3d, xmin, ymin, xmax, ymax)
{
projected_hatches.push(hatch);
}
@@ -495,7 +496,7 @@ impl Scene {
false,
) {
if let Some(wipeout) =
project_plot_fill(wipeout, &project, xmin, ymin, xmax, ymax)
project_plot_fill(wipeout, &project_3d, xmin, ymin, xmax, ymax)
{
projected_wipeouts.push(wipeout);
}
@@ -753,7 +754,7 @@ fn project_plot_fill<F>(
ymax: f32,
) -> Option<HatchModel>
where
F: Fn(f64, f64) -> Option<[f32; 2]>,
F: Fn([f64; 3]) -> Option<[f32; 2]>,
{
let mut output = Vec::new();
let mut ring = Vec::new();
@@ -778,15 +779,35 @@ where
}
output.extend(clipped);
};
for &[x, y] in fill.boundary.iter() {
if x.is_nan() || y.is_nan() {
flush_ring(&mut ring, &mut output);
continue;
if let (Some(plane), Some(boundary)) = (fill.fill_plane, fill.fill_plane_boundary.as_deref()) {
let plane = cadkernel::space::Plane::from_axes(
plane.origin,
plane.x_axis,
plane.y_axis,
);
for &[x, y] in boundary {
if x.is_nan() || y.is_nan() {
flush_ring(&mut ring, &mut output);
continue;
}
let point = plane.point_at([x as f64, y as f64]);
if let Some(point) = project(point) {
ring.push(point);
}
}
let absolute_x = fill.world_origin[0] + x as f64;
let absolute_y = fill.world_origin[1] + y as f64;
if let Some(point) = project(absolute_x, absolute_y) {
ring.push(point);
} else {
for &[x, y] in fill.boundary.iter() {
if x.is_nan() || y.is_nan() {
flush_ring(&mut ring, &mut output);
continue;
}
if let Some(point) = project([
fill.world_origin[0] + x as f64,
fill.world_origin[1] + y as f64,
0.0,
]) {
ring.push(point);
}
}
}
flush_ring(&mut ring, &mut output);
@@ -796,6 +817,8 @@ where
fill.world_origin = [0.0, 0.0];
fill.boundary = std::sync::Arc::new(output);
fill.boundary_wcs = None;
fill.fill_plane = None;
fill.fill_plane_boundary = None;
Some(fill)
}
+4
View File
@@ -312,6 +312,8 @@ pub fn apply_along(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
@@ -356,6 +358,8 @@ pub fn apply_along(
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
+3
View File
@@ -2925,6 +2925,9 @@ impl Scene {
}
let mut out: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
for w in wires {
if !w.display_visible {
continue;
}
if w.render_instance.is_some() {
continue;
}