refactor(viewport): unify content-viewport clipping on the stencil path

Rectangular paper viewports now go through the same GPU stencil clip as
non-rectangular ones: viewport_clip_boundary_ndc returns the four rect
corners (a full-target mask) instead of nothing, so the render path has
no rect/non-rect special case — every content viewport carries a clip
boundary and draws with stencil reference 0xFF.

With a single clip path, the old per-wire / -image / -wipeout scissor
mechanism is fully dead and removed: compute_*_scissors, the
*_pixel_scissors buffers, the project_scissor helper, the per-pass
set_scissor_rect blocks, the vp_scissor field threaded through
WireModel / ImageModel / HatchModel and their GPU mirrors (WireGpu /
ImageGpu / WipeoutGpu / HatchWebGpu), from_run's scissor parameter, the
scissor grouping in build_wire_buffers, and the is_arena_eligible gate.
Net -245 lines across 54 files.

Rectangular viewports still clip exactly via their own render rectangle;
the stencil mask is a redundant full-target no-op there, kept only so
the clip code path stays uniform. XCLIP keeps its CPU wire clip.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-20 11:06:28 +03:00
1 parent c97c2a8250
commit 626e8091f7
54 files changed
+32 -277

No files matched your search

-2
View File
@@ -1258,7 +1258,6 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
key_vertices: vec![],
aabb: crate::scene::WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
};
@@ -1297,7 +1296,6 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
key_vertices: vec![],
aabb: crate::scene::WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
-5
View File
@@ -1372,7 +1372,6 @@ fn tessellate_dimension_inner(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -1399,7 +1398,6 @@ fn tessellate_dimension_inner(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -1426,7 +1424,6 @@ fn tessellate_dimension_inner(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -1454,7 +1451,6 @@ fn tessellate_dimension_inner(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: geom.arrow_fill,
// fill_tris_low intentionally empty: this fill renders on the top-level
// path, where consumers (face3d_gpu, xclip) treat a short low half as
@@ -1498,7 +1494,6 @@ fn tessellate_dimension_inner(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: rect,
// fill_tris_low intentionally empty: this fill renders on the
// top-level path, where consumers (face3d_gpu, xclip) treat a
-2
View File
@@ -610,7 +610,6 @@ impl LeaderTess for Leader {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
};
@@ -717,7 +716,6 @@ impl LeaderTess for Leader {
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris,
fill_tris_low: Vec::new(),
}
-4
View File
@@ -1424,7 +1424,6 @@ impl MultiLeaderTess for MultiLeader {
key_vertices: key_verts,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: arrow_fill,
// fill_tris_low intentionally empty: this fill renders on the
// top-level path, where consumers (face3d_gpu, xclip) treat a short
@@ -1717,7 +1716,6 @@ impl MultiLeaderTess for MultiLeader {
key_vertices: vec![],
aabb: [nx as f32, ny as f32, xx as f32, xy as f32],
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -1782,7 +1780,6 @@ impl MultiLeaderTess for MultiLeader {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris,
fill_tris_low: Vec::new(),
});
@@ -1818,7 +1815,6 @@ impl MultiLeaderTess for MultiLeader {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
-1
View File
@@ -773,7 +773,6 @@ pub fn tessellate_table(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris,
// fill_tris_low intentionally empty: this fill renders on the
// top-level path, where consumers (face3d_gpu, xclip) treat a
-2
View File
@@ -206,7 +206,6 @@ impl CadCommand for AlignedDimensionCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
@@ -267,7 +266,6 @@ fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -150,7 +150,6 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -200,7 +200,6 @@ fn preview_line(a: Vec3, b: Vec3) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -208,7 +208,6 @@ impl CadCommand for DimBaselineCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -188,7 +188,6 @@ impl CadCommand for DimContinueCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -113,7 +113,6 @@ impl CadCommand for DimJogLineCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -129,7 +129,6 @@ impl CadCommand for DimTeditCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -183,7 +183,6 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -241,7 +241,6 @@ fn preview_wire(points: Vec<DVec3>) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -170,7 +170,6 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -478,7 +478,6 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -141,7 +141,6 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -110,7 +110,6 @@ impl CadCommand for QdimCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -205,7 +205,6 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -168,7 +168,6 @@ impl CadCommand for TableCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -110,7 +110,6 @@ impl CadCommand for ToleranceCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -186,7 +186,6 @@ impl CadCommand for AttdefCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-3
View File
@@ -242,7 +242,6 @@ impl HatchCommand {
scale: 1.0,
world_origin: origin,
boundary_wcs: Some(std::sync::Arc::new(wcs)),
vp_scissor: None,
draw_depth: 0.0,
}
}
@@ -403,7 +402,6 @@ impl GradientCommand {
scale: 1.0,
world_origin: origin,
boundary_wcs: Some(std::sync::Arc::new(wcs)),
vp_scissor: None,
draw_depth: 0.0,
}
}
@@ -577,7 +575,6 @@ impl CadCommand for BoundaryCommand {
scale: 1.0,
world_origin: origin,
boundary_wcs: Some(std::sync::Arc::new(wcs)),
vp_scissor: None,
draw_depth: 0.0,
};
CmdResult::CommitHatch(model)
-1
View File
@@ -156,7 +156,6 @@ impl CadCommand for MlineCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -128,7 +128,6 @@ impl CadCommand for ImageCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-2
View File
@@ -104,7 +104,6 @@ impl CadCommand for RayCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
@@ -203,7 +202,6 @@ impl CadCommand for XLineCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -109,7 +109,6 @@ impl CadCommand for RevCloudCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-2
View File
@@ -140,7 +140,6 @@ impl CadCommand for WipeoutCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
@@ -181,7 +180,6 @@ impl CadCommand for WipeoutCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -89,7 +89,6 @@ impl CadCommand for AreaCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -86,7 +86,6 @@ impl CadCommand for DistCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -87,7 +87,6 @@ impl MeasureGeomCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -3079,7 +3079,6 @@ fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -107,7 +107,6 @@ impl CadCommand for MviewCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -325,7 +325,6 @@ fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-1
View File
@@ -77,7 +77,6 @@ impl CadCommand for PlotWindowCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -91,7 +91,6 @@ impl CadCommand for ZoomWindowCommand {
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
-1
View File
@@ -868,7 +868,6 @@ impl Batches {
key_vertices: b.key_vertices,
aabb,
plinegen: b.plinegen,
vp_scissor: None,
fill_tris: b.fill_tris,
fill_tris_low: b.fill_tris_low,
}
-3
View File
@@ -670,7 +670,6 @@ pub(crate) fn tessellate_entity(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
};
@@ -1055,7 +1054,6 @@ fn lod_stub_wire(
key_vertices: vec![[cx as f64, cy as f64, cz as f64]],
aabb,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
@@ -1140,7 +1138,6 @@ fn lod_stub_wire_3d(
key_vertices: vec![],
aabb,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
-17
View File
@@ -394,7 +394,6 @@ pub fn tessellate(
key_vertices: Vec::new(),
aabb: WireModel::UNBOUNDED_AABB,
plinegen: false,
vp_scissor: None,
fill_tris: Vec::new(),
fill_tris_low: Vec::new(),
});
@@ -681,7 +680,6 @@ pub fn tessellate(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: ft,
fill_tris_low: ftl,
});
@@ -719,7 +717,6 @@ pub fn tessellate(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -764,7 +761,6 @@ pub fn tessellate(
key_vertices: Vec::new(),
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -790,7 +786,6 @@ pub fn tessellate(
key_vertices,
aabb: text_aabb,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -846,7 +841,6 @@ pub fn tessellate(
key_vertices: keys,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -884,7 +878,6 @@ pub fn tessellate(
key_vertices: keys,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: bin.fill_tris,
fill_tris_low: bin.fill_tris_low,
});
@@ -919,7 +912,6 @@ pub fn tessellate(
key_vertices: Vec::new(),
aabb: text_aabb,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -947,7 +939,6 @@ pub fn tessellate(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -1006,7 +997,6 @@ pub fn tessellate(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}];
@@ -1046,7 +1036,6 @@ pub fn tessellate(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}];
@@ -1086,7 +1075,6 @@ pub fn tessellate(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}];
@@ -1160,7 +1148,6 @@ pub fn tessellate(
key_vertices: keys,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -1196,7 +1183,6 @@ pub fn tessellate(
key_vertices: keys,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris,
fill_tris_low,
fill_is_3d,
@@ -1225,7 +1211,6 @@ pub fn tessellate(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
@@ -1264,7 +1249,6 @@ pub fn tessellate(
aci: 0,
key_vertices,
plinegen: false,
vp_scissor: None,
aabb: WireModel::UNBOUNDED_AABB,
fill_tris: vec![],
fill_tris_low: Vec::new(),
@@ -1334,7 +1318,6 @@ pub fn tessellate(
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}]
-5
View File
@@ -727,7 +727,6 @@ impl Scene {
angle_offset: 0.0,
scale: 1.0,
world_origin: fill_origin,
vp_scissor: None,
draw_depth: depth_map.get(&common.handle.value()).copied().unwrap_or(0.0),
});
}
@@ -1002,7 +1001,6 @@ impl Scene {
angle_offset: 0.0,
scale: 1.0,
world_origin: origin,
vp_scissor: None,
draw_depth: 0.0,
});
}
@@ -1058,7 +1056,6 @@ impl Scene {
angle_offset: 0.0,
scale: 1.0,
world_origin: fill_origin,
vp_scissor: None,
draw_depth: 0.0,
});
}
@@ -1593,7 +1590,6 @@ impl Scene {
angle_offset: if prebaked { 0.0 } else { dxf.pattern_angle as f32 },
scale: if prebaked { 1.0 } else { dxf.pattern_scale as f32 },
world_origin,
vp_scissor: None,
draw_depth: 0.0,
})
}
@@ -1808,7 +1804,6 @@ impl Scene {
angle_offset: 0.0,
scale: 1.0,
world_origin,
vp_scissor: None,
draw_depth: 0.0,
}
}
+1 -4
View File
@@ -886,9 +886,7 @@ pub struct Scene {
/// Cached wipeout fill models, keyed by geometry_epoch. Same
/// reasoning as `hatch_cache`.
wipeout_cache: RefCell<Option<(u64, Arc<Vec<HatchModel>>)>>,
/// Cached image models, keyed by geometry_epoch. Images do their own
/// per-frame culling in the GPU pipeline (vp_scissor); no camera key
/// needed here.
/// Cached image models, keyed by geometry_epoch. No camera key needed here.
image_cache: RefCell<Option<(u64, Arc<Vec<ImageModel>>)>>,
/// Cached mesh models, keyed by geometry_epoch.
mesh_cache: RefCell<Option<(u64, Arc<Vec<MeshLodSet>>)>>,
@@ -2025,7 +2023,6 @@ impl Scene {
color: self.paper_bg_color,
angle_offset: 0.0,
scale: 1.0,
vp_scissor: None,
// Draw-order bias is signed: entity fills/wires land in (-1, 1)
// (0 = neutral). A value below -1 forces the sheet strictly behind
// EVERY object, in every case, with a tiny z offset (BIAS = 0.001,
-6
View File
@@ -84,12 +84,6 @@ pub struct HatchModel {
pub angle_offset: f32,
/// Pattern scale multiplier (from DXF `pattern_scale`).
pub scale: f32,
/// Optional world-space XY rect [x0, y0, x1, y1] for paper-space
/// viewport clipping. When `Some`, the pipeline translates it into
/// a per-frame pixel scissor and clips this hatch's draw call to it,
/// preventing viewport-projected content from bleeding past the
/// viewport frame. Mirrors `WireModel.vp_scissor`.
pub vp_scissor: Option<[f32; 4]>,
/// Normalized draw-order depth in (0,1); higher draws on top. Fed to the
/// hatch pipeline as a small clip-z bias so this fill orders correctly
/// against other entity types. 0.0 for transient/preview hatches.
-6
View File
@@ -100,10 +100,6 @@ pub struct ImageModel {
/// Low residual paired with `corners` (double-single) so the GPU keeps
/// sub-unit precision at UTM-scale insertion points.
pub corners_low: [[f32; 3]; 4],
/// Optional world-space XY rect [x0, y0, x1, y1] for paper-space
/// viewport clipping. Mirrors `WireModel.vp_scissor` /
/// `HatchModel.vp_scissor`.
pub vp_scissor: Option<[f32; 4]>,
/// Normalized draw-order depth in (0,1); higher draws on top. Fed to the
/// image pipeline as a small clip-z bias so the raster orders correctly
/// against other entity types.
@@ -179,7 +175,6 @@ impl ImageModel {
opacity,
corners,
corners_low,
vp_scissor: None,
draw_depth: 0.0,
verts,
})
@@ -234,7 +229,6 @@ impl ImageModel {
opacity: 1.0,
corners,
corners_low,
vp_scissor: None,
draw_depth: 0.0,
verts,
})
-5
View File
@@ -86,9 +86,6 @@ pub struct WireModel {
/// share one interior phase (perpendicular dashes line up) yet each still
/// ends on a dash. `None` (the default) = per-wire A-type / from-start.
pub dash_align_end: Option<f32>,
/// Paper-space bounding box [x0, y0, x1, y1] for GPU scissor clipping.
/// Set only for viewport-projected wires in paper-space layouts.
pub vp_scissor: Option<[f32; 4]>,
/// Pre-triangulated solid fill: flat vertex list, 3 per triangle (world-offset applied).
/// Non-empty only for PolyfaceMesh / PolygonMesh entities.
pub fill_tris: Vec<[f32; 3]>,
@@ -196,7 +193,6 @@ impl WireModel {
plinegen: true,
dash_from_start: false,
dash_align_end: None,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
@@ -406,7 +402,6 @@ impl Default for WireModel {
plinegen: true,
dash_from_start: false,
dash_align_end: None,
vp_scissor: None,
fill_tris: Vec::new(),
fill_tris_low: Vec::new(),
fill_is_3d: false,
-1
View File
@@ -290,7 +290,6 @@ impl Scene {
angle_offset: 0.0,
scale: 1.0,
world_origin: fill_origin,
vp_scissor: None,
draw_depth: 0.0,
});
}
+3 -6
View File
@@ -58,11 +58,9 @@ struct HatchWebUniform {
pub struct HatchWebGpu {
pub vertex_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
/// Reserved for viewport-clip scissor / per-frame AABB LOD (mirrors
/// `WipeoutGpu`); not yet wired into the web hatch draw loop — the native
/// batched path doesn't do per-hatch scissor/LOD either.
#[allow(dead_code)]
pub vp_scissor: Option<[f32; 4]>,
/// Reserved for per-frame AABB LOD (mirrors `WipeoutGpu`); not yet wired
/// into the web hatch draw loop — the native batched path doesn't do
/// per-hatch LOD either.
#[allow(dead_code)]
pub world_aabb: [f32; 4],
_uniform_buf: wgpu::Buffer,
@@ -344,7 +342,6 @@ impl HatchWebGpu {
Self {
vertex_buffer,
bind_group,
vp_scissor: model.vp_scissor,
world_aabb,
_uniform_buf,
_data_tex: data_tex,
-3
View File
@@ -66,8 +66,6 @@ pub struct ImageGpu {
/// more when the raster is clipped to a triangulated polygon.
pub vertex_count: u32,
pub bind_group: wgpu::BindGroup,
/// Mirrors `ImageModel.vp_scissor` — see `HatchGpu.vp_scissor`.
pub vp_scissor: Option<[f32; 4]>,
_texture: wgpu::Texture,
_sampler: wgpu::Sampler,
_params_buf: wgpu::Buffer,
@@ -186,7 +184,6 @@ impl ImageGpu {
vertex_buffer,
vertex_count,
bind_group,
vp_scissor: model.vp_scissor,
_texture: texture,
_sampler,
_params_buf,
+6 -124
View File
@@ -169,8 +169,6 @@ pub struct Pipeline {
/// The Model content id both arenas currently mirror (`u64::MAX` = none).
#[cfg(not(target_arch = "wasm32"))]
pub(crate) wire_arena_id: u64,
/// Pixel scissor rects [x, y, w, h] for viewport-clipped wires. Recomputed each frame.
wire_pixel_scissors: Vec<Option<[u32; 4]>>,
/// This content viewport's non-rectangular clip boundary as a triangle-fan
/// vertex buffer in the render target's normalized device coords (`None` =
/// rectangular / unclipped, where the viewport's own render rectangle does
@@ -198,11 +196,7 @@ pub struct Pipeline {
/// when the wipeout's projected AABB sits entirely outside the
/// viewport rect. Recomputed by `compute_wipeout_lod`.
wipeout_skip_flags: Vec<bool>,
/// Pixel scissor rects [x, y, w, h] for viewport-clipped wipeouts. Recomputed each frame.
wipeout_pixel_scissors: Vec<Option<[u32; 4]>>,
gpu_images: Vec<ImageGpu>,
/// Pixel scissor rects [x, y, w, h] for viewport-clipped images. Recomputed each frame.
image_pixel_scissors: Vec<Option<[u32; 4]>>,
gpu_meshes: Vec<MeshLodGpu>,
/// Batched mesh geometry — every solid's LOD0 concatenated into a few large
/// buffers so the whole set draws in a handful of calls instead of one per
@@ -1442,7 +1436,6 @@ impl Pipeline {
wire_arena_mesh: None,
#[cfg(not(target_arch = "wasm32"))]
wire_arena_id: u64::MAX,
wire_pixel_scissors: vec![],
clip_boundary: None,
gpu_selected_wires: vec![],
gpu_preview_wires: vec![],
@@ -1451,9 +1444,7 @@ impl Pipeline {
gpu_hatches_web: vec![],
gpu_wipeouts: vec![],
wipeout_skip_flags: vec![],
wipeout_pixel_scissors: vec![],
gpu_images: vec![],
image_pixel_scissors: vec![],
gpu_meshes: vec![],
gpu_mesh_batch: vec![],
cached_mesh_batch_epoch: u64::MAX,
@@ -1516,16 +1507,12 @@ impl Pipeline {
let mut batches: Vec<WireGpu> = Vec::new();
let mut i = 0;
while i < wires.len() {
let scissor = wires[i].vp_scissor;
let mesh_edge = is_mesh_edge(&wires[i]);
let mut j = i + 1;
while j < wires.len()
&& wires[j].vp_scissor == scissor
&& is_mesh_edge(&wires[j]) == mesh_edge
{
while j < wires.len() && is_mesh_edge(&wires[j]) == mesh_edge {
j += 1;
}
batches.extend(WireGpu::from_run(device, &wires[i..j], depth_map, scissor, mesh_edge, self.wire_const_bgl.as_ref()));
batches.extend(WireGpu::from_run(device, &wires[i..j], depth_map, mesh_edge, self.wire_const_bgl.as_ref()));
i = j;
}
@@ -1587,7 +1574,7 @@ impl Pipeline {
push(h.value(), WireModel::HOVER, wires, &mut out);
}
self.gpu_selected_wires =
WireGpu::from_run(device, &out, depth_map, None, false, self.wire_const_bgl.as_ref());
WireGpu::from_run(device, &out, depth_map, false, self.wire_const_bgl.as_ref());
}
/// Build the text-highlight overlay: the glyph quads of just the selected /
@@ -1645,7 +1632,7 @@ impl Pipeline {
self.gpu_preview_wires = if wires.is_empty() {
vec![]
} else {
WireGpu::from_run(device, wires, depth_map, None, false, self.wire_const_bgl.as_ref())
WireGpu::from_run(device, wires, depth_map, false, self.wire_const_bgl.as_ref())
};
}
@@ -1848,16 +1835,6 @@ impl Pipeline {
self.silhouette_vcount = verts.len() as u32;
}
/// Recompute pixel scissor rects for viewport-clipped wires from the current view_proj.
/// Called every frame from prepare() because scissor pixels shift with pan/zoom.
pub fn compute_wire_scissors(&mut self, view_rot: glam::Mat4, eye: glam::DVec3, clip_w: u32, clip_h: u32) {
self.wire_pixel_scissors = self
.gpu_wires
.iter()
.map(|w| project_scissor(w.vp_scissor, view_rot, eye, clip_w, clip_h))
.collect();
}
/// Upload this content viewport's non-rectangular clip boundary as a
/// triangle-fan vertex buffer in render-target NDC. The boundary is already
/// projected (paper shape → the same visible-sub-rect crop as the content),
@@ -1914,15 +1891,6 @@ impl Pipeline {
batch.upload_visibility(queue);
}
/// Recompute pixel scissor rects for viewport-clipped wipeouts.
pub fn compute_wipeout_scissors(&mut self, view_rot: glam::Mat4, eye: glam::DVec3, clip_w: u32, clip_h: u32) {
self.wipeout_pixel_scissors = self
.gpu_wipeouts
.iter()
.map(|h| project_scissor(h.vp_scissor, view_rot, eye, clip_w, clip_h))
.collect();
}
/// Per-frame wipeout frustum-skip flag (Phase 2.3). Mirrors
/// `compute_hatch_lod`'s frustum branch. No sub-pixel skip:
/// wipeouts mask, so dropping a sub-pixel one wouldn't be wrong
@@ -1935,15 +1903,6 @@ impl Pipeline {
.collect();
}
/// Recompute pixel scissor rects for viewport-clipped raster images.
pub fn compute_image_scissors(&mut self, view_rot: glam::Mat4, eye: glam::DVec3, clip_w: u32, clip_h: u32) {
self.image_pixel_scissors = self
.gpu_images
.iter()
.map(|i| project_scissor(i.vp_scissor, view_rot, eye, clip_w, clip_h))
.collect();
}
/// Upload all 3DFACE entities as two batched GPU objects:
/// - `gpu_face3d_fill`: filled triangles (1 buffer, 1 draw call)
/// - `gpu_face3d_edges`: merged edge wires (1 buffer, 1 draw call)
@@ -2324,26 +2283,11 @@ impl Pipeline {
pass.set_pipeline(&self.image_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
let mut scissor_active = false;
for (i, img) in self.gpu_images.iter().enumerate() {
match self.image_pixel_scissors.get(i) {
Some(Some([x, y, w, h])) => {
pass.set_scissor_rect(*x, *y, *w, *h);
scissor_active = true;
}
_ if scissor_active => {
pass.set_scissor_rect(0, 0, vp.width, vp.height);
scissor_active = false;
}
_ => {}
}
for img in self.gpu_images.iter() {
pass.set_bind_group(1, &img.bind_group, &[]);
pass.set_vertex_buffer(0, img.vertex_buffer.slice(..));
pass.draw(0..img.vertex_count, 0..1);
}
if scissor_active {
pass.set_scissor_rect(0, 0, vp.width, vp.height);
}
}
// ── Pass 4: solid meshes (batched) ────────────────────────────────
@@ -2647,10 +2591,9 @@ impl Pipeline {
// colour. `wire_black_pipeline` shares the wire layout, so the switch
// needs no bind-group rebind.
let want_solid_with_edges = !hidden_line && !mesh_wireframe && show_3d_edges;
let mut scissor_active = false;
let mut black_active = false;
pass.set_stencil_reference(stencil_ref);
for (i, wire) in self.gpu_wires.iter().enumerate() {
for wire in self.gpu_wires.iter() {
if wire.instance_count == 0 {
continue;
}
@@ -2673,26 +2616,12 @@ impl Pipeline {
});
black_active = use_black;
}
match self.wire_pixel_scissors.get(i) {
Some(Some([x, y, w, h])) => {
pass.set_scissor_rect(*x, *y, *w, *h);
scissor_active = true;
}
_ if scissor_active => {
pass.set_scissor_rect(0, 0, vp.width, vp.height);
scissor_active = false;
}
_ => {}
}
if let Some(bg) = &wire.const_bind_group {
pass.set_bind_group(1, bg.as_ref(), &[]);
}
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
pass.draw(0..6, 0..wire.instance_count);
}
if scissor_active {
pass.set_scissor_rect(0, 0, vp.width, vp.height);
}
// Live overlay wires (command preview / interim / grip drag) always
// on top: the xray pipeline (depth_compare=Always, no depth write)
// keeps them visible through any occluding geometry — a 3D solid, or
@@ -2798,29 +2727,14 @@ impl Pipeline {
pass.set_pipeline(&self.wipeout_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
let mut scissor_active = false;
for (i, wipeout) in self.gpu_wipeouts.iter().enumerate() {
if self.wipeout_skip_flags.get(i).copied().unwrap_or(false) {
continue;
}
match self.wipeout_pixel_scissors.get(i) {
Some(Some([x, y, w, h])) => {
pass.set_scissor_rect(*x, *y, *w, *h);
scissor_active = true;
}
_ if scissor_active => {
pass.set_scissor_rect(0, 0, vp.width, vp.height);
scissor_active = false;
}
_ => {}
}
pass.set_bind_group(1, &wipeout.bind_group, &[]);
pass.set_vertex_buffer(0, wipeout.vertex_buffer.slice(..));
pass.draw(0..6, 0..1);
}
if scissor_active {
pass.set_scissor_rect(0, 0, vp.width, vp.height);
}
}
// ── Pass 7: selected wire overlay pass ───────────────────────────
@@ -3124,38 +3038,6 @@ fn aabb_below_pixel(
(max_px - min_px).max(max_py - min_py) < threshold_px
}
/// Project a world-space XY scissor rect through `view_proj` into the four
/// pixel-space corners and return the smallest aligned-rect that bounds them,
/// clamped to the clip viewport. Returns `None` when the rect is missing or
/// the projection collapses (off-screen, behind camera).
fn project_scissor(
rect: Option<[f32; 4]>,
view_rot: glam::Mat4,
eye: glam::DVec3,
clip_w: u32,
clip_h: u32,
) -> Option<[u32; 4]> {
let [x0, y0, x1, y1] = rect?;
let w = clip_w as f32;
let h = clip_h as f32;
let corners = [
view_rot.project_point3((glam::DVec3::new(x0 as f64, y0 as f64, 0.0) - eye).as_vec3()),
view_rot.project_point3((glam::DVec3::new(x1 as f64, y0 as f64, 0.0) - eye).as_vec3()),
view_rot.project_point3((glam::DVec3::new(x0 as f64, y1 as f64, 0.0) - eye).as_vec3()),
view_rot.project_point3((glam::DVec3::new(x1 as f64, y1 as f64, 0.0) - eye).as_vec3()),
];
let px: Vec<f32> = corners.iter().map(|c| (c.x + 1.0) * 0.5 * w).collect();
let py: Vec<f32> = corners.iter().map(|c| (1.0 - c.y) * 0.5 * h).collect();
let sx0 = px.iter().cloned().fold(f32::INFINITY, f32::min).max(0.0) as u32;
let sy0 = py.iter().cloned().fold(f32::INFINITY, f32::min).max(0.0) as u32;
let sx1 = (px.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as u32).min(clip_w);
let sy1 = (py.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as u32).min(clip_h);
if sx1 <= sx0 || sy1 <= sy0 {
return None;
}
Some([sx0, sy0, sx1 - sx0, sy1 - sy0])
}
fn create_depth_texture(device: &wgpu::Device, size: Size<u32>) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("viewer.depth_texture"),
-5
View File
@@ -119,10 +119,6 @@ pub struct FamilyBatchData {
pub struct WipeoutGpu {
pub vertex_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
/// Mirrors `HatchModel.vp_scissor`. Forwarded to the pipeline so each
/// frame's `compute_hatch_scissors` can translate the world rect into
/// a pixel scissor.
pub vp_scissor: Option<[f32; 4]>,
/// World-space XY bounding rect [min_x, min_y, max_x, max_y] of the
/// boundary polygon. Used by the per-frame LOD pass to skip hatches
/// whose entire footprint projects to less than ~2 px (Phase 3.3).
@@ -382,7 +378,6 @@ impl WipeoutGpu {
Self {
vertex_buffer,
bind_group,
vp_scissor: model.vp_scissor,
world_aabb,
_uniform_buf,
_boundary_buf,
-8
View File
@@ -90,13 +90,6 @@ fn handle_of(w: &WireModel) -> Option<Handle> {
crate::scene::Scene::handle_from_wire_name(&w.name)
}
/// True if the set can use the arena: no per-wire scissor (paper viewports). Mesh
/// fills no longer disqualify it — the render layer splits the set into a regular
/// and a mesh-edge subset, one arena each.
pub fn is_arena_eligible(wires: &[WireModel]) -> bool {
wires.iter().all(|w| w.vp_scissor.is_none())
}
/// True when `w`'s edge segments belong to a mesh/solid whose fill is drawn in a
/// separate pass — such wires need the draw loop's mesh-edge treatment, so they
/// go in their own arena. `mesh_names` are the entities that emit a fill.
@@ -439,7 +432,6 @@ impl WireArena {
vec![WireGpu {
instance_buffer: self.inst_buf.clone(),
instance_count: self.inst_tail,
vp_scissor: None,
is_3d_mesh_edge: self.mesh_edge,
const_bind_group: Some(self.const_bind_group.clone()),
}]
-8
View File
@@ -208,9 +208,6 @@ impl WireInstance {
pub struct WireGpu {
pub instance_buffer: wgpu::Buffer,
pub instance_count: u32,
/// Paper-space bbox [x0, y0, x1, y1] for GPU scissor clipping.
/// Set only for viewport-projected wires; None for regular wires.
pub vp_scissor: Option<[f32; 4]>,
/// `true` when the source `WireModel` also carries `fill_tris`
/// (i.e. it is a 3D mesh face — PolyfaceMesh / PolygonMesh — whose
/// outline lives in `points`). The wire pass skips these instances
@@ -521,7 +518,6 @@ impl WireGpu {
device: &wgpu::Device,
wires: &[WireModel],
depth_map: &rustc_hash::FxHashMap<u64, f32>,
scissor: Option<[f32; 4]>,
mesh_edge: bool,
const_bgl: Option<&wgpu::BindGroupLayout>,
) -> Vec<Self> {
@@ -562,7 +558,6 @@ impl WireGpu {
Self {
instance_buffer: buf,
instance_count: chunk.len() as u32,
vp_scissor: scissor,
is_3d_mesh_edge: mesh_edge,
const_bind_group: bg.clone(),
}
@@ -594,7 +589,6 @@ impl WireGpu {
Self {
instance_buffer: buf,
instance_count: chunk.len() as u32,
vp_scissor: scissor,
is_3d_mesh_edge: mesh_edge,
const_bind_group: None,
}
@@ -652,7 +646,6 @@ impl WireGpu {
Self {
instance_buffer,
instance_count: chunk.len() as u32,
vp_scissor: None,
is_3d_mesh_edge: false,
const_bind_group: bg.clone(),
}
@@ -678,7 +671,6 @@ impl WireGpu {
Self {
instance_buffer,
instance_count: chunk.len() as u32,
vp_scissor: None,
is_3d_mesh_edge: false,
const_bind_group: None,
}
+21 -16
View File
@@ -334,7 +334,6 @@ impl Scene {
// to keep dimensional consistency in the GPU shader.
out.pattern_length = wire.pattern_length * scale;
out.pattern = wire.pattern.map(|v| v * scale);
out.vp_scissor = Some([vp_x0, vp_y0, vp_x1, vp_y1]);
projected.push(out);
}
@@ -348,17 +347,13 @@ impl Scene {
result
}
/// Clip regions for every on content viewport in `paper_block`: the paper
/// bounding rect (matching the wires' `vp_scissor`) plus the boundary
/// polygon whose interior the GPU keeps via the stencil buffer. A plain
/// rectangular viewport contributes its four corners; a viewport with a
/// non-rectangular clip (`clip_boundary_handle`) contributes that entity's
/// tessellated outline (circle / ellipse / spline / polyline), reusing the
/// entity's own `to_truck` tessellation.
/// A content viewport's non-rectangular clip boundary, projected into that
/// viewport's render-target normalized device coords. Returns an empty vec
/// for a rectangular viewport (no `clip_boundary_handle`) — those clip via
/// their own render rectangle and need no stencil mask.
/// A content viewport's clip boundary, projected into that viewport's
/// render-target normalized device coords. Every content viewport goes
/// through the same stencil path: a rectangular viewport contributes its
/// own four corners (a full-target mask, since the render rectangle already
/// clips the edges), a viewport with a `clip_boundary_handle` contributes
/// that entity's tessellated outline. Returns empty only when the viewport
/// is degenerate.
///
/// `uo/vo/us/vs` are the visible-sub-rect crop the content camera applies
/// (see `crop_view_proj`); the same crop is applied here so the boundary
@@ -376,9 +371,6 @@ impl Scene {
return vec![];
};
let vp: &Viewport = vp;
if vp.clip_boundary_handle.is_null() {
return vec![];
}
let pcx = vp.center.x as f32;
let pcy = vp.center.y as f32;
let z = vp.center.z as f32;
@@ -387,7 +379,20 @@ impl Scene {
if hw.abs() < 1e-6 || hh.abs() < 1e-6 {
return vec![];
}
let poly = self.clip_boundary_polygon(vp.clip_boundary_handle, z);
let poly = if vp.clip_boundary_handle.is_null() {
// Rectangular viewport → its own four corners (paper coords). These
// map to full-rect NDC [-1, 1], a no-op mask over a render target
// that already clips to the rectangle, but it keeps rect and
// non-rect viewports on one uniform stencil path.
vec![
[pcx - hw, pcy - hh, z],
[pcx + hw, pcy - hh, z],
[pcx + hw, pcy + hh, z],
[pcx - hw, pcy + hh, z],
]
} else {
self.clip_boundary_polygon(vp.clip_boundary_handle, z)
};
if poly.len() < 3 {
return vec![];
}
-2
View File
@@ -325,7 +325,6 @@ pub fn apply_along(
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
@@ -365,7 +364,6 @@ pub fn apply_along(
key_vertices: vec![],
aabb: [nx as f32, ny as f32, xx as f32, xy as f32],
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
+1 -7
View File
@@ -387,10 +387,7 @@ impl shader::Primitive for Primitive {
#[cfg(not(target_arch = "wasm32"))]
let mut _patched = false;
#[cfg(not(target_arch = "wasm32"))]
if crate::scene::wire_gpu_patch_enabled()
&& inner.wire_const_bgl.is_some()
&& crate::scene::pipeline::wire_arena::is_arena_eligible(&vp.wires)
{
if crate::scene::wire_gpu_patch_enabled() && inner.wire_const_bgl.is_some() {
use crate::scene::pipeline::wire_arena::{self, WireArena};
// Split the resident set into the regular 2D wires and the
// mesh/solid EDGE wires (which need the mesh-edge draw
@@ -576,9 +573,6 @@ impl shader::Primitive for Primitive {
inner.upload_silhouettes(device, &[], vp.view_dir);
}
inner.upload_clip_boundary(device, &vp.clip_boundary_ndc);
inner.compute_wire_scissors(view_rot, eye, clip_size.width, clip_size.height);
inner.compute_wipeout_scissors(view_rot, eye, clip_size.width, clip_size.height);
inner.compute_image_scissors(view_rot, eye, clip_size.width, clip_size.height);
inner.compute_hatch_lod(queue, view_rot, eye, clip_size.width, clip_size.height);
inner.compute_wipeout_lod(view_rot, eye, clip_size.width, clip_size.height);
inner.compute_mesh_lod(view_rot, eye, clip_size.width, clip_size.height);