forked from 1b-agent/opencadstudio-upstream
feat(gradient): align editing and rendering
This commit is contained in:
commit
2a852f4479
37 files changed
+539
-371
No files matched your search
@@ -545,8 +545,8 @@ impl Scene {
|
||||
|
||||
/// Build closed planar regions from the visible wire geometry.
|
||||
///
|
||||
/// Unlike `closed_outlines()`, the source entities do not need to be closed
|
||||
/// individually. Intersections are inserted as temporary graph vertices and
|
||||
/// Source entities do not need to be closed individually. Intersections are
|
||||
/// inserted as temporary graph vertices and
|
||||
/// the bounded faces of that planar graph are returned as hatch candidates.
|
||||
///
|
||||
/// Curved entities participate through their already-tessellated WireModel
|
||||
|
||||
+98
-52
@@ -90,6 +90,36 @@ fn family_from_stored_line(
|
||||
}
|
||||
}
|
||||
|
||||
/// Preserve the selected hue while moving its HSL lightness towards the
|
||||
/// persisted one-colour tint/shade target (0 = black, 1 = white).
|
||||
fn gradient_tint_color(base: [f32; 4], target: f32) -> [f32; 4] {
|
||||
let max = base[0].max(base[1]).max(base[2]);
|
||||
let min = base[0].min(base[1]).min(base[2]);
|
||||
let lightness = (max + min) * 0.5;
|
||||
let target = target.clamp(0.0, 1.0);
|
||||
let mut result = base;
|
||||
if target <= lightness {
|
||||
let factor = if lightness > 1.0e-6 {
|
||||
target / lightness
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
for channel in &mut result[..3] {
|
||||
*channel *= factor;
|
||||
}
|
||||
} else {
|
||||
let factor = if lightness < 1.0 - 1.0e-6 {
|
||||
(target - lightness) / (1.0 - lightness)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
for channel in &mut result[..3] {
|
||||
*channel += (1.0 - *channel) * factor;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
// ── Entity management ─────────────────────────────────────────────────
|
||||
|
||||
@@ -1041,8 +1071,13 @@ impl Scene {
|
||||
m.angle_offset = dxf.pattern_angle as f32;
|
||||
m.scale = dxf.pattern_scale as f32;
|
||||
}
|
||||
model::hatch_model::HatchPattern::Gradient { angle_deg, .. } => {
|
||||
*angle_deg = dxf.pattern_angle.to_degrees() as f32;
|
||||
model::hatch_model::HatchPattern::Gradient {
|
||||
angle_deg,
|
||||
shift,
|
||||
..
|
||||
} => {
|
||||
*angle_deg = dxf.gradient_color.angle.to_degrees() as f32;
|
||||
*shift = dxf.gradient_color.shift as f32;
|
||||
}
|
||||
model::hatch_model::HatchPattern::Pattern(_)
|
||||
| model::hatch_model::HatchPattern::Solid => {}
|
||||
@@ -1520,8 +1555,8 @@ impl Scene {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut boundary: Vec<[f64; 2]> = Vec::new();
|
||||
let mut boundary_exterior = Vec::new();
|
||||
let mut rings = Vec::new();
|
||||
let mut ring_sources = Vec::new();
|
||||
|
||||
for path in &dxf.paths {
|
||||
// Skip TEXTBOX boundary paths (flag bit 3). These are text
|
||||
@@ -1531,12 +1566,6 @@ impl Scene {
|
||||
if path.flags.bits() & 8 != 0 {
|
||||
continue;
|
||||
}
|
||||
let before_path = boundary.len();
|
||||
if !boundary.is_empty() {
|
||||
boundary.push([f64::NAN, f64::NAN]);
|
||||
}
|
||||
let path_start = boundary.len();
|
||||
|
||||
let mut edge_polys: Vec<Vec<[f64; 2]>> = Vec::new();
|
||||
for edge in &path.edges {
|
||||
if let Some(curve) = crate::entities::hatch::edge_curve(edge) {
|
||||
@@ -1549,29 +1578,44 @@ impl Scene {
|
||||
);
|
||||
}
|
||||
}
|
||||
boundary.extend(chain_path_edges(edge_polys));
|
||||
|
||||
if boundary.len() == path_start {
|
||||
boundary.truncate(before_path);
|
||||
let mut ring = chain_path_edges(edge_polys);
|
||||
if ring.is_empty() {
|
||||
continue;
|
||||
}
|
||||
boundary_exterior.push(path.flags.is_external() || path.flags.is_outermost());
|
||||
if boundary.len() >= path_start + 3 {
|
||||
let first = boundary[path_start];
|
||||
let last = *boundary.last().unwrap();
|
||||
if ring.len() >= 3 {
|
||||
let first = ring[0];
|
||||
let last = *ring.last().unwrap();
|
||||
if (first[0] - last[0]).abs() > 1e-5 || (first[1] - last[1]).abs() > 1e-5 {
|
||||
boundary.push(first);
|
||||
ring.push(first);
|
||||
}
|
||||
}
|
||||
rings.push(ring);
|
||||
ring_sources.push(path.boundary_handles.clone());
|
||||
}
|
||||
|
||||
if boundary.is_empty() {
|
||||
if rings.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if !boundary_exterior.iter().any(|role| *role) {
|
||||
if let Some(first) = boundary_exterior.first_mut() {
|
||||
*first = true;
|
||||
|
||||
let depths = cadkernel::geom2d::ring_nesting_depths(&rings);
|
||||
let mut boundary = Vec::new();
|
||||
let mut boundary_exterior = Vec::new();
|
||||
let mut boundary_sources = Vec::new();
|
||||
for ((ring, sources), depth) in rings.into_iter().zip(ring_sources).zip(depths) {
|
||||
let keep = match dxf.style {
|
||||
acadrust::entities::HatchStyleType::Normal => true,
|
||||
acadrust::entities::HatchStyleType::Outer => depth <= 1,
|
||||
acadrust::entities::HatchStyleType::Ignore => depth == 0,
|
||||
};
|
||||
if !keep {
|
||||
continue;
|
||||
}
|
||||
if !boundary.is_empty() {
|
||||
boundary.push([f64::NAN, f64::NAN]);
|
||||
}
|
||||
boundary.extend(ring);
|
||||
boundary_exterior.push(depth == 0);
|
||||
boundary_sources.push(sources);
|
||||
}
|
||||
// The batched hatch renderer keeps boundaries in a GPU storage
|
||||
// buffer (no fixed length), so a hatch with many island loops must
|
||||
@@ -1618,8 +1662,13 @@ impl Scene {
|
||||
)
|
||||
};
|
||||
gradient_color1 = stop(0);
|
||||
let color2 = stop(1).unwrap_or(color);
|
||||
let angle_deg = dxf.pattern_angle.to_degrees() as f32;
|
||||
let color1 = gradient_color1.unwrap_or(color);
|
||||
let color2 = if dxf.gradient_color.is_single_color {
|
||||
gradient_tint_color(color1, dxf.gradient_color.color_tint as f32)
|
||||
} else {
|
||||
stop(1).unwrap_or(color)
|
||||
};
|
||||
let angle_deg = dxf.gradient_color.angle.to_degrees() as f32;
|
||||
let (kind, invert) =
|
||||
model::hatch_model::GradientKind::from_name(&dxf.gradient_color.name);
|
||||
model::hatch_model::HatchPattern::Gradient {
|
||||
@@ -1627,6 +1676,7 @@ impl Scene {
|
||||
color2,
|
||||
kind,
|
||||
invert,
|
||||
shift: dxf.gradient_color.shift as f32,
|
||||
}
|
||||
} else if dxf.is_solid {
|
||||
model::hatch_model::HatchPattern::Solid
|
||||
@@ -1768,13 +1818,7 @@ impl Scene {
|
||||
boundary: std::sync::Arc::new(boundary_f32),
|
||||
boundary_wcs: None,
|
||||
boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)),
|
||||
boundary_sources: Some(std::sync::Arc::new(
|
||||
dxf.paths
|
||||
.iter()
|
||||
.filter(|path| path.flags.bits() & 8 == 0)
|
||||
.map(|path| path.boundary_handles.clone())
|
||||
.collect(),
|
||||
)),
|
||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||
pattern,
|
||||
name,
|
||||
// A gradient starts from its first stop; other fills use the
|
||||
@@ -2144,27 +2188,28 @@ impl Scene {
|
||||
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
|
||||
if !r.is_empty() {
|
||||
let edge = PolylineEdge::new(std::mem::take(r), true);
|
||||
let mut path = if is_outer {
|
||||
let mut p = BoundaryPath::external();
|
||||
p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits(
|
||||
p.flags.bits()
|
||||
| acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(),
|
||||
);
|
||||
p
|
||||
} else {
|
||||
BoundaryPath::new()
|
||||
};
|
||||
path.add_edge(BoundaryEdge::Polyline(edge));
|
||||
if let Some(handles) = model
|
||||
let handles: Vec<_> = model
|
||||
.boundary_sources
|
||||
.as_deref()
|
||||
.and_then(|sources| sources.get(index))
|
||||
{
|
||||
for &handle in handles {
|
||||
if handle.is_valid() {
|
||||
path.add_boundary_handle(handle);
|
||||
}
|
||||
}
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.copied()
|
||||
.filter(|handle| handle.is_valid())
|
||||
.collect();
|
||||
let mut bits = 0;
|
||||
if is_outer {
|
||||
bits |= acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits();
|
||||
}
|
||||
if !handles.is_empty() {
|
||||
bits |= acadrust::entities::hatch::BoundaryPathFlags::EXTERNAL.bits();
|
||||
}
|
||||
let mut path = BoundaryPath::with_flags(
|
||||
acadrust::entities::hatch::BoundaryPathFlags::from_bits(bits),
|
||||
);
|
||||
path.add_edge(BoundaryEdge::Polyline(edge));
|
||||
for handle in handles {
|
||||
path.add_boundary_handle(handle);
|
||||
}
|
||||
dxf.paths.push(path);
|
||||
}
|
||||
@@ -2254,6 +2299,7 @@ impl Scene {
|
||||
color2,
|
||||
kind,
|
||||
invert,
|
||||
shift,
|
||||
} = &model.pattern
|
||||
{
|
||||
let to_color = |c: [f32; 4]| acadrust::types::Color::Rgb {
|
||||
@@ -2264,10 +2310,10 @@ impl Scene {
|
||||
dxf.is_solid = true;
|
||||
dxf.gradient_color.enabled = true;
|
||||
dxf.gradient_color.name = kind.dxf_name(*invert).to_string();
|
||||
// The render model reads the gradient angle from pattern_angle
|
||||
// (radians); the gradient record keeps its own copy for the file.
|
||||
// Keep both persisted angle fields aligned.
|
||||
dxf.pattern_angle = (*angle_deg as f64).to_radians();
|
||||
dxf.gradient_color.angle = (*angle_deg as f64).to_radians();
|
||||
dxf.gradient_color.shift = *shift as f64;
|
||||
dxf.gradient_color.is_single_color = false;
|
||||
// Linear has no INV name in the standard set: persist an inverted
|
||||
// linear by swapping the colour stops instead.
|
||||
|
||||
@@ -4652,71 +4652,6 @@ impl Scene {
|
||||
names
|
||||
}
|
||||
|
||||
/// Collect closed polygon outlines (world XY) from the current layout.
|
||||
pub fn closed_outlines(&self) -> Vec<Vec<[f64; 2]>> {
|
||||
self.entity_wires()
|
||||
.iter()
|
||||
.filter_map(|wire| {
|
||||
let pts: Vec<[f64; 2]> = wire
|
||||
.points
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.filter_map(|(index, high)| {
|
||||
if !high[0].is_finite() || !high[1].is_finite() {
|
||||
return None;
|
||||
}
|
||||
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
let point = [
|
||||
high[0] as f64 + low[0] as f64,
|
||||
high[1] as f64 + low[1] as f64,
|
||||
];
|
||||
point.iter().all(|value| value.is_finite()).then_some(point)
|
||||
})
|
||||
.collect();
|
||||
if pts.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
let f = pts.first()?;
|
||||
let l = pts.last()?;
|
||||
let dx = f[0] - l[0];
|
||||
let dy = f[1] - l[1];
|
||||
if (dx * dx + dy * dy).sqrt() > 1e-2 {
|
||||
return None;
|
||||
}
|
||||
// Segment-list wires (e.g. LwPolyline) store each segment as an
|
||||
// independent NaN-separated pair, so every shared corner repeats
|
||||
// (`A B | B C | C D | D A`). Collapse that back into a clean ring:
|
||||
// skip the NaN separators and any vertex coincident with the
|
||||
// previous one, so consumers (point-in-polygon, the hatch /
|
||||
// boundary commands) see one vertex per corner — not the doubled
|
||||
// ring that otherwise shows two grips at every corner.
|
||||
let mut ring: Vec<[f64; 2]> = Vec::with_capacity(pts.len());
|
||||
for q in pts {
|
||||
if let Some(&last) = ring.last() {
|
||||
if (last[0] - q[0]).abs() < 1e-4 && (last[1] - q[1]).abs() < 1e-4 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ring.push(q);
|
||||
}
|
||||
// Drop a trailing vertex equal to the first — the ring is closed
|
||||
// implicitly, so keeping it would be a duplicate corner.
|
||||
if ring.len() > 1 {
|
||||
let first = ring[0];
|
||||
let last = *ring.last().unwrap();
|
||||
if (first[0] - last[0]).abs() < 1e-4 && (first[1] - last[1]).abs() < 1e-4 {
|
||||
ring.pop();
|
||||
}
|
||||
}
|
||||
if ring.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
Some(ring)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Wire set for the Model layout, shared by every tile.
|
||||
///
|
||||
/// The model wire geometry is **camera-independent**, so it is tessellated
|
||||
|
||||
@@ -50,12 +50,19 @@ pub enum GradientKind {
|
||||
}
|
||||
|
||||
impl GradientKind {
|
||||
pub const ALL: [GradientKind; 5] = [
|
||||
GradientKind::Linear,
|
||||
GradientKind::Cylinder,
|
||||
GradientKind::Spherical,
|
||||
GradientKind::Hemispherical,
|
||||
GradientKind::Curved,
|
||||
/// User-facing gradient definitions in their standard persisted order.
|
||||
/// Inverted definitions are real named patterns rather than a separate
|
||||
/// property, so Properties and the draw command share this single list.
|
||||
pub const CHOICES: [(GradientKind, bool); 9] = [
|
||||
(GradientKind::Linear, false),
|
||||
(GradientKind::Cylinder, false),
|
||||
(GradientKind::Cylinder, true),
|
||||
(GradientKind::Spherical, false),
|
||||
(GradientKind::Hemispherical, false),
|
||||
(GradientKind::Curved, false),
|
||||
(GradientKind::Spherical, true),
|
||||
(GradientKind::Hemispherical, true),
|
||||
(GradientKind::Curved, true),
|
||||
];
|
||||
|
||||
/// Radial fills shade from the boundary centre outward.
|
||||
@@ -63,18 +70,25 @@ impl GradientKind {
|
||||
matches!(self, GradientKind::Spherical | GradientKind::Hemispherical)
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
GradientKind::Linear => "Linear",
|
||||
GradientKind::Cylinder => "Cylinder",
|
||||
GradientKind::Spherical => "Spherical",
|
||||
GradientKind::Hemispherical => "Hemispherical",
|
||||
GradientKind::Curved => "Curved",
|
||||
pub fn choice_label(self, inverted: bool) -> &'static str {
|
||||
match (self, inverted) {
|
||||
(GradientKind::Linear, _) => "Linear",
|
||||
(GradientKind::Cylinder, false) => "Cylindrical",
|
||||
(GradientKind::Cylinder, true) => "Inverted cylindrical",
|
||||
(GradientKind::Spherical, false) => "Spherical",
|
||||
(GradientKind::Spherical, true) => "Inverted spherical",
|
||||
(GradientKind::Hemispherical, false) => "Hemispherical",
|
||||
(GradientKind::Hemispherical, true) => "Inverted hemispherical",
|
||||
(GradientKind::Curved, false) => "Curved",
|
||||
(GradientKind::Curved, true) => "Inverted curved",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_label(label: &str) -> Option<Self> {
|
||||
Self::ALL.iter().copied().find(|k| k.label() == label)
|
||||
pub fn from_choice_label(label: &str) -> Option<(Self, bool)> {
|
||||
Self::CHOICES
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|(kind, inverted)| kind.choice_label(*inverted).eq_ignore_ascii_case(label))
|
||||
}
|
||||
|
||||
/// Parse the DXF gradient name (`LINEAR`, `INVCYLINDER`, …) into the kind
|
||||
@@ -139,6 +153,8 @@ pub enum HatchPattern {
|
||||
color2: [f32; 4],
|
||||
kind: GradientKind,
|
||||
invert: bool,
|
||||
/// 0 = centred, 1 = shifted towards the upper-left light source.
|
||||
shift: f32,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -208,6 +224,25 @@ pub struct HatchModel {
|
||||
}
|
||||
|
||||
impl HatchModel {
|
||||
pub(crate) fn gradient_frame(
|
||||
&self,
|
||||
angle_deg: f32,
|
||||
shift: f32,
|
||||
) -> Option<cadkernel::geom2d::GradientFrame> {
|
||||
let boundary: Vec<[f64; 2]> = self
|
||||
.boundary
|
||||
.iter()
|
||||
.filter(|point| point[0].is_finite() && point[1].is_finite())
|
||||
.map(|point| [point[0] as f64, point[1] as f64])
|
||||
.collect();
|
||||
cadkernel::geom2d::gradient_frame(
|
||||
&boundary,
|
||||
(angle_deg as f64).to_radians(),
|
||||
shift as f64,
|
||||
Tolerance::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Indexed local-space fill mesh with even-odd loop containment.
|
||||
pub(crate) fn fill_mesh(&self) -> (Vec<[f32; 2]>, Vec<u32>) {
|
||||
let mut rings = Vec::new();
|
||||
|
||||
@@ -262,21 +262,26 @@ impl StorageHatchBatch {
|
||||
let mut grad_kind = 0u32;
|
||||
let (mode, color2, grad_cos, grad_sin, grad_min, grad_range) = match &h.pattern {
|
||||
HatchPattern::Solid => (1u32, [0.0; 4], 0.0, 0.0, 0.0, 1.0),
|
||||
HatchPattern::Gradient { angle_deg, color2, kind, invert } => {
|
||||
HatchPattern::Gradient {
|
||||
angle_deg,
|
||||
color2,
|
||||
kind,
|
||||
invert,
|
||||
shift,
|
||||
} => {
|
||||
grad_kind = kind.shader_kind() | if *invert { 16 } else { 0 };
|
||||
let frame = h.gradient_frame(*angle_deg, *shift);
|
||||
if kind.radial() {
|
||||
// Radial fill: the boundary is stored relative to its
|
||||
// centre (`world_origin`), so the centre is the local
|
||||
// origin; radius = the farthest boundary vertex. mode 3.
|
||||
let radius = radial_radius(&h.boundary);
|
||||
(3u32, *color2, 0.0, 0.0, 0.0, radius)
|
||||
let center = frame.map_or([0.0, 0.0], |frame| {
|
||||
[frame.center[0] as f32, frame.center[1] as f32]
|
||||
});
|
||||
let radius = frame.map_or(1.0, |frame| frame.radius as f32);
|
||||
(3u32, *color2, center[0], center[1], 0.0, radius)
|
||||
} else {
|
||||
let r = angle_deg.to_radians();
|
||||
// Gradient projection range (proj_min / proj_range) —
|
||||
// computed at upload time, identical to per-hatch path.
|
||||
let (gmin, gmax) = boundary_projection_range(&h.boundary, r);
|
||||
let grange = (gmax - gmin).max(1.0);
|
||||
(2u32, *color2, r.cos(), r.sin(), gmin, grange)
|
||||
let minimum = frame.map_or(0.0, |frame| frame.projection_min as f32);
|
||||
let span = frame.map_or(1.0, |frame| frame.projection_span as f32);
|
||||
(2u32, *color2, r.cos(), r.sin(), minimum, span)
|
||||
}
|
||||
}
|
||||
HatchPattern::Pattern(fams) => {
|
||||
@@ -635,42 +640,6 @@ impl LineFamilyGpu {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Project every boundary vertex onto the gradient direction
|
||||
/// `(cos θ, sin θ)` and return the (min, max) projection. Used to set
|
||||
/// up the gradient's normalized parameter range. Same math as the
|
||||
/// per-hatch path; duplicated here to keep this module self-contained.
|
||||
fn boundary_projection_range(boundary: &[[f32; 2]], theta: f32) -> (f32, f32) {
|
||||
let (cs, sn) = (theta.cos(), theta.sin());
|
||||
let mut lo = f32::INFINITY;
|
||||
let mut hi = f32::NEG_INFINITY;
|
||||
for &[x, y] in boundary {
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
continue;
|
||||
}
|
||||
let p = x * cs + y * sn;
|
||||
if p < lo { lo = p; }
|
||||
if p > hi { hi = p; }
|
||||
}
|
||||
if !lo.is_finite() {
|
||||
return (0.0, 1.0);
|
||||
}
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
/// Radius of a radial gradient — the distance from the boundary centre (the
|
||||
/// local origin, since vertices are stored relative to it) to the farthest
|
||||
/// vertex, so `t = 1` (the end colour) reaches the corners.
|
||||
fn radial_radius(boundary: &[[f32; 2]]) -> f32 {
|
||||
boundary
|
||||
.iter()
|
||||
.filter(|p| p[0].is_finite() && p[1].is_finite())
|
||||
.map(|p| (p[0] * p[0] + p[1] * p[1]).sqrt())
|
||||
.fold(0.0_f32, f32::max)
|
||||
.max(1.0)
|
||||
}
|
||||
|
||||
// PatFamily is re-exported by hatch_model so we don't need to import
|
||||
// it explicitly anywhere else — but rust needs the type referenced to
|
||||
// confirm the layout assumption above.
|
||||
|
||||
@@ -165,7 +165,7 @@ impl TextureHatch {
|
||||
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
|
||||
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
||||
HatchPattern::Pattern(_) => (0u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
||||
HatchPattern::Gradient { angle_deg, color2, kind, invert } => {
|
||||
HatchPattern::Gradient { angle_deg, color2, kind, invert, .. } => {
|
||||
let gk = (kind.shader_kind() | if *invert { 16 } else { 0 }) << 8;
|
||||
if kind.radial() {
|
||||
// Radial: centre is the local origin; grad_cos/sin unused.
|
||||
@@ -191,7 +191,6 @@ impl TextureHatch {
|
||||
}
|
||||
|
||||
let origin = model.world_origin;
|
||||
let drift = [0.0f32, 0.0f32];
|
||||
let (mesh_points, indices) = model.fill_mesh();
|
||||
if mesh_points.is_empty() || indices.is_empty() {
|
||||
return None;
|
||||
@@ -239,33 +238,31 @@ impl TextureHatch {
|
||||
});
|
||||
|
||||
// ── Gradient projection range (snapped-local space) ───────────────
|
||||
let (grad_min, grad_range) = if mode == 2 {
|
||||
let projs: Vec<f32> = model
|
||||
.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)
|
||||
.collect();
|
||||
if projs.is_empty() {
|
||||
(0.0, 1.0)
|
||||
} else {
|
||||
let proj_min = projs.iter().cloned().fold(f32::INFINITY, f32::min);
|
||||
let proj_max = projs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
|
||||
// Floor matches the storage backend.
|
||||
(proj_min, (proj_max - proj_min).max(1.0))
|
||||
}
|
||||
} else if mode == 3 {
|
||||
// Radial: range = the farthest boundary vertex from the centre.
|
||||
let radius = model
|
||||
.boundary
|
||||
.iter()
|
||||
.filter(|v| v[0].is_finite() && v[1].is_finite())
|
||||
.map(|&[x, y]| (x * x + y * y).sqrt())
|
||||
.fold(0.0_f32, f32::max)
|
||||
.max(1.0);
|
||||
(0.0, radius)
|
||||
let base_mode = mode & 0xFF;
|
||||
let frame = match &model.pattern {
|
||||
HatchPattern::Gradient {
|
||||
angle_deg, shift, ..
|
||||
} => model.gradient_frame(*angle_deg, *shift),
|
||||
_ => None,
|
||||
};
|
||||
let (grad_min, grad_range, radial_center) = if base_mode == 2 {
|
||||
frame.map_or((0.0, 1.0, [0.0, 0.0]), |frame| {
|
||||
(
|
||||
frame.projection_min as f32,
|
||||
frame.projection_span as f32,
|
||||
[0.0, 0.0],
|
||||
)
|
||||
})
|
||||
} else if base_mode == 3 {
|
||||
frame.map_or((0.0, 1.0, [0.0, 0.0]), |frame| {
|
||||
(
|
||||
0.0,
|
||||
frame.radius as f32,
|
||||
[frame.center[0] as f32, frame.center[1] as f32],
|
||||
)
|
||||
})
|
||||
} else {
|
||||
(0.0, 1.0)
|
||||
(0.0, 1.0, [0.0, 0.0])
|
||||
};
|
||||
|
||||
// ── Pack the data texture: families | dashes ─────────────────────
|
||||
@@ -355,8 +352,8 @@ impl TextureHatch {
|
||||
// Clamp like the desktop renderer so scale==0 can't make perp_step 0
|
||||
// → round(perp/0)=NaN → an invisible hatch.
|
||||
scale: model.scale.max(1e-6),
|
||||
grad_cos,
|
||||
grad_sin,
|
||||
grad_cos: if base_mode == 3 { radial_center[0] } else { grad_cos },
|
||||
grad_sin: if base_mode == 3 { radial_center[1] } else { grad_sin },
|
||||
grad_min,
|
||||
grad_range,
|
||||
origin: [origin[0] as f32, origin[1] as f32],
|
||||
|
||||
@@ -1345,6 +1345,7 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
|
||||
color2,
|
||||
kind,
|
||||
invert,
|
||||
shift,
|
||||
} => {
|
||||
2_u8.hash(&mut h);
|
||||
angle_deg.to_bits().hash(&mut h);
|
||||
@@ -1353,6 +1354,7 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
|
||||
}
|
||||
kind.shader_kind().hash(&mut h);
|
||||
invert.hash(&mut h);
|
||||
shift.to_bits().hash(&mut h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user