forked from 1b-agent/opencadstudio-upstream
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:
commit
44efb8fd53
88 files changed
+1538
-702
No files matched your search
Vendored
+28
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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(_)
|
||||
)
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user