refactor: split complex render, CSS and WGSL checker functions (S3776, S107)

Extract helpers from curves_graph, draw_group, parse_css and the WGSL
bitwise checker so each stays under the cognitive complexity limit.
curves_graph now takes its pencil and natural flags as one CurveMode
value, which brings it to seven parameters.
This commit is contained in:
Dylan M. Taylor committed 2026-10-26 08:31:58 -04:00
1 parent 2961258879
commit 0b9385874b
6 files changed
+327 -236

No files matched your search

+108 -72
View File
@@ -22,9 +22,22 @@ enum Tok<'a> {
const BITWISE: [&str; 3] = ["&", "|", "^"];
const OPS: [&str; 18] = ["&&", "||", "==", "!=", "<=", ">=", "<<", ">>", "<", ">", "+", "-", "*", "/", "%", "&", "|", "^"];
/// Drop comments and template argument lists (`vec3<f32>`, `array<vec4<u32>>`), whose
/// angle brackets are not relational operators.
fn strip(src: &str) -> String {
let mut out = drop_comments(src);
loop {
let (next, changed) = drop_template_lists_once(&out);
out = next;
if !changed {
return out;
}
}
}
fn drop_comments(src: &str) -> String {
let mut out = String::with_capacity(src.len());
let b = src.as_bytes();
let mut i = 0;
@@ -45,31 +58,98 @@ fn strip(src: &str) -> String {
i += 1;
}
}
loop {
let mut changed = false;
let mut next = String::with_capacity(out.len());
let c: Vec<char> = out.chars().collect();
let mut i = 0;
while i < c.len() {
if c[i] == '<' && i > 0 && (c[i - 1].is_alphanumeric() || c[i - 1] == '_') {
let mut j = i + 1;
while j < c.len() && (c[j].is_alphanumeric() || c[j] == '_' || c[j] == ',' || c[j] == ' ') {
j += 1;
}
if j < c.len() && c[j] == '>' {
i = j + 1;
changed = true;
continue;
}
out
}
/// One pass over `out` dropping every template argument list. Returns whether one was dropped.
fn drop_template_lists_once(out: &str) -> (String, bool) {
let mut changed = false;
let mut next = String::with_capacity(out.len());
let c: Vec<char> = out.chars().collect();
let mut i = 0;
while i < c.len() {
if let Some(end) = template_list_end(&c, i) {
i = end;
changed = true;
continue;
}
next.push(c[i]);
i += 1;
}
(next, changed)
}
/// If `c[i]` opens a template argument list after an identifier, the index just past its `>`.
fn template_list_end(c: &[char], i: usize) -> Option<usize> {
if c[i] != '<' || i == 0 || !(c[i - 1].is_alphanumeric() || c[i - 1] == '_') {
return None;
}
let mut j = i + 1;
while j < c.len() && (c[j].is_alphanumeric() || c[j] == '_' || c[j] == ',' || c[j] == ' ') {
j += 1;
}
(j < c.len() && c[j] == '>').then_some(j + 1)
}
/// Scans the token starting at `*at` (not whitespace) and advances `*at` past it.
fn next_token<'a>(s: &'a str, at: &mut usize) -> Tok<'a> {
let b = s.as_bytes();
let ch = b[*at];
let mut i = *at;
let tok = if ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'.' {
// Identifier, member access or number (`1e-6`, `0x9E3779B9u`).
let start = i;
while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_' || b[i] == b'.') {
if (b[i] == b'e' || b[i] == b'E')
&& b[start].is_ascii_digit()
&& !s[start..i].starts_with("0x")
&& i + 1 < b.len()
&& (b[i + 1] == b'-' || b[i + 1] == b'+')
{
i += 1;
}
next.push(c[i]);
i += 1;
}
out = next;
if !changed {
return out;
Tok::Operand
} else if ch == b'(' || ch == b'[' {
i += 1;
Tok::Open
} else if ch == b')' || ch == b']' {
i += 1;
Tok::Close
} else if b[i..].starts_with(b"->") || b[i..].starts_with(b"++") || b[i..].starts_with(b"--") {
i += 2;
Tok::Sep
} else if matches!(ch, b',' | b';' | b'{' | b'}' | b':' | b'@') {
i += 1;
Tok::Sep
} else if let Some(op) = OPS.iter().find(|op| b[i..].starts_with(op.as_bytes())) {
i += op.len();
if b.get(i) == Some(&b'=') && !matches!(*op, "==" | "!=" | "<=" | ">=") {
i += 1;
Tok::Sep // compound assignment
} else {
Tok::Op(op)
}
} else if ch == b'=' {
i += 1;
Tok::Sep
} else {
i += 1;
Tok::Prefix
};
*at = i;
tok
}
/// Records a binary operator at the current parenthesis level, reporting it if it mixes with
/// one already seen there.
fn note_operator<'a>(level: &mut Vec<&'a str>, op: &'a str, line: usize, out: &mut Vec<(usize, String)>) {
let mixes = |a: &str, b: &str| a != b && (BITWISE.contains(&a) || BITWISE.contains(&b));
if let Some(other) = level.iter().find(|o| mixes(o, op)) {
out.push((line, format!("mixing '{other}' and '{op}' requires parentheses")));
}
level.push(op);
}
/// `(line, message)` for every bitwise operator mixed with another binary operator at
@@ -82,7 +162,6 @@ fn violations(src: &str) -> Vec<(usize, String)> {
let mut levels: Vec<Vec<&str>> = vec![vec![]];
let mut prev = Tok::Sep;
let (mut i, mut line) = (0, 1);
const OPS: [&str; 18] = ["&&", "||", "==", "!=", "<=", ">=", "<<", ">>", "<", ">", "+", "-", "*", "/", "%", "&", "|", "^"];
while i < b.len() {
let ch = b[i];
if ch == b'\n' {
@@ -94,53 +173,14 @@ fn violations(src: &str) -> Vec<(usize, String)> {
i += 1;
continue;
}
let tok = if ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'.' {
// Identifier, member access or number (`1e-6`, `0x9E3779B9u`).
let start = i;
while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_' || b[i] == b'.') {
if (b[i] == b'e' || b[i] == b'E')
&& b[start].is_ascii_digit()
&& !s[start..i].starts_with("0x")
&& i + 1 < b.len()
&& (b[i + 1] == b'-' || b[i + 1] == b'+')
{
i += 1;
}
i += 1;
}
Tok::Operand
} else if ch == b'(' || ch == b'[' {
i += 1;
levels.push(vec![]);
Tok::Open
} else if ch == b')' || ch == b']' {
i += 1;
if levels.len() > 1 {
levels.pop();
}
Tok::Close
} else if b[i..].starts_with(b"->") || b[i..].starts_with(b"++") || b[i..].starts_with(b"--") {
i += 2;
Tok::Sep
} else if matches!(ch, b',' | b';' | b'{' | b'}' | b':' | b'@') {
i += 1;
Tok::Sep
} else if let Some(op) = OPS.iter().find(|op| b[i..].starts_with(op.as_bytes())) {
i += op.len();
if b.get(i) == Some(&b'=') && !matches!(*op, "==" | "!=" | "<=" | ">=") {
i += 1;
Tok::Sep // compound assignment
} else {
Tok::Op(op)
}
} else if ch == b'=' {
i += 1;
Tok::Sep
} else {
i += 1;
Tok::Prefix
};
let tok = next_token(&s, &mut i);
match tok {
Tok::Open => levels.push(vec![]),
Tok::Close => {
if levels.len() > 1 {
levels.pop();
}
}
Tok::Sep => {
if let Some(l) = levels.last_mut() {
l.clear();
@@ -148,11 +188,7 @@ fn violations(src: &str) -> Vec<(usize, String)> {
}
Tok::Op(op) if matches!(prev, Tok::Operand | Tok::Close) => {
let level = levels.last_mut().expect("level");
let mixes = |a: &str, b: &str| a != b && (BITWISE.contains(&a) || BITWISE.contains(&b));
if let Some(other) = level.iter().find(|o| mixes(o, op)) {
out.push((line, format!("mixing '{other}' and '{op}' requires parentheses")));
}
level.push(op);
note_operator(level, op, line, &mut out);
}
_ => {}
}
+34 -28
View File
@@ -470,6 +470,39 @@ fn strip_comments(s: &str) -> String {
out
}
/// Splits one simple selector (`tag.class#id`) into its tag, classes and id.
fn parse_compound(one: &str) -> (Option<String>, Vec<String>, Option<String>) {
let mut tag = None;
let mut classes = vec![];
let mut id = None;
let mut cur = String::new();
let mut kind = 't';
let flush = |kind: char, cur: &mut String, tag: &mut Option<String>, classes: &mut Vec<String>, id: &mut Option<String>| {
if !cur.is_empty() {
match kind {
'.' => classes.push(std::mem::take(cur)),
'#' => *id = Some(std::mem::take(cur)),
_ => {
if cur != "*" {
*tag = Some(std::mem::take(cur));
}
cur.clear();
}
}
}
};
for c in one.chars() {
if c == '.' || c == '#' {
flush(kind, &mut cur, &mut tag, &mut classes, &mut id);
kind = c;
} else {
cur.push(c);
}
}
flush(kind, &mut cur, &mut tag, &mut classes, &mut id);
(tag, classes, id)
}
/// Add the rules of one `<style>` sheet. Errs once the sheets exceed [`MAX_CSS_SELECTORS`] or
/// [`MAX_CSS_DECLARATIONS`] (#2140).
fn parse_css(css: &str, sheet: &mut StyleSheet) -> Result<(), Error> {
@@ -505,34 +538,7 @@ fn parse_css(css: &str, sheet: &mut StyleSheet) -> Result<(), Error> {
sheet.blocks.push(std::mem::take(&mut decls));
i
});
let mut tag = None;
let mut classes = vec![];
let mut id = None;
let mut cur = String::new();
let mut kind = 't';
let flush = |kind: char, cur: &mut String, tag: &mut Option<String>, classes: &mut Vec<String>, id: &mut Option<String>| {
if !cur.is_empty() {
match kind {
'.' => classes.push(std::mem::take(cur)),
'#' => *id = Some(std::mem::take(cur)),
_ => {
if cur != "*" {
*tag = Some(std::mem::take(cur));
}
cur.clear();
}
}
}
};
for c in one.chars() {
if c == '.' || c == '#' {
flush(kind, &mut cur, &mut tag, &mut classes, &mut id);
kind = c;
} else {
cur.push(c);
}
}
flush(kind, &mut cur, &mut tag, &mut classes, &mut id);
let (tag, classes, id) = parse_compound(one);
let specificity = (id.is_some() as u32 * 100)
.saturating_add(u32::try_from(classes.len()).unwrap_or(u32::MAX).saturating_mul(10))
.saturating_add(tag.is_some() as u32);
+91 -79
View File
@@ -6,7 +6,7 @@ use effectcraft_raster::{Image, Mask};
use kurbo::{Affine, Point, Shape as _};
use rayon::prelude::*;
use crate::{BlendMode, Doc, FillRule, Gradient, GradientKind, Group, Node, Paint, Shape, Spread};
use crate::{BlendMode, Doc, FillRule, Gradient, GradientKind, Group, Node, Paint, Shape, SoftMask, Spread};
fn mat3(a: Affine) -> Mat3 {
let c = a.as_coeffs();
@@ -77,6 +77,11 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image, budget: &mut
}
return;
}
draw_offscreen_group(g, m, o, dst, budget);
}
/// Draws a group through an offscreen buffer, then composites it onto `dst`.
fn draw_offscreen_group(g: &Group, m: Affine, o: f64, dst: &mut Image, budget: &mut SurfaceBudget) {
let only_clip = g.mask.is_none() && g.blend == BlendMode::Normal && o >= 1.0 && !g.knockout;
// Draw the children on their own (isolated) or over a copy of the backdrop, keep what
// every clip path covers and the soft mask lets through, then composite with the group's
@@ -85,65 +90,24 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image, budget: &mut
if !g.isolated && backdrop.is_none() {
return;
}
let mut tmp = match &backdrop {
Some(b) => match budget.image(dst.width, dst.height, Some(b)) {
Some(image) => image,
None => {
budget.release(b);
return;
}
},
None => match budget.image(dst.width, dst.height, None) {
Some(image) => image,
None => return,
},
let Some(mut tmp) = budget.image(dst.width, dst.height, backdrop.as_ref()) else {
release_backdrop(budget, backdrop.as_ref());
return;
};
draw_children(g, m, &mut tmp, backdrop.as_ref(), budget);
if let Some(b) = &backdrop {
if only_clip {
// A non-isolated clipping group: the backdrop where the clip excludes the children.
let cov = clip_coverage(g, m, dst.width, dst.height);
dst.data.par_iter_mut().zip(tmp.data.par_iter()).zip(cov.par_iter()).for_each(|((d, t), c)| {
for i in 0..4 {
d[i] = d[i] * (1.0 - c) + t[i] * c;
}
});
clip_onto_backdrop(g, m, dst, &tmp);
budget.release(&tmp);
drop(tmp);
if let Some(b) = &backdrop {
budget.release(b);
}
release_backdrop(budget, backdrop.as_ref());
return;
}
// Remove the backdrop's contribution (ISO 32000-1 §11.4.8): with the group's own
// alpha αg (its children alone), C = Cn + (Cn − C0)·(α0/αg − α0).
let Some(mut solo) = budget.image(dst.width, dst.height, None) else {
if !strip_backdrop(g, m, b, &mut tmp, budget) {
budget.release(&tmp);
drop(tmp);
if let Some(b) = &backdrop {
budget.release(b);
}
release_backdrop(budget, backdrop.as_ref());
return;
};
draw_children(g, m, &mut solo, None, budget);
tmp.data.par_iter_mut().zip(b.data.par_iter()).zip(solo.data.par_iter()).for_each(|((t, b0), s)| {
let ag = s[3];
if ag <= 0.0 {
*t = [0.0; 4];
return;
}
let un = |c: &[f32; 4]| if c[3] > 0.0 { [c[0] / c[3], c[1] / c[3], c[2] / c[3]] } else { [0.0; 3] };
let (cn, c0, a0) = (un(t), un(b0), b0[3]);
let k = a0 / ag - a0;
let mut out = [0.0; 4];
for i in 0..3 {
out[i] = (cn[i] + (cn[i] - c0[i]) * k).clamp(0.0, 1.0) * ag;
}
out[3] = ag;
*t = out;
});
budget.release(&solo);
drop(solo);
}
}
if !g.clip.is_empty() {
let cov = clip_coverage(g, m, dst.width, dst.height);
@@ -153,33 +117,12 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image, budget: &mut
}
});
}
if let Some(sm) = &g.mask {
let Some(mut mk) = budget.image(dst.width, dst.height, None) else {
budget.release(&tmp);
drop(tmp);
if let Some(b) = &backdrop {
budget.release(b);
}
return;
};
draw_group(&sm.content, m, 1.0, &mut mk, budget);
let bc = sm.backdrop.map(|v| v as f32);
let lum = sm.luminosity;
tmp.data.par_iter_mut().zip(mk.data.par_iter()).for_each(|(p, q)| {
let k = if lum {
// The mask group composited over its backdrop colour, then its luminosity.
let c = [0, 1, 2].map(|i| q[i] + bc[i] * (1.0 - q[3]));
0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
} else {
q[3]
};
let k = k.clamp(0.0, 1.0);
for v in p.iter_mut() {
*v *= k;
}
});
budget.release(&mk);
drop(mk);
if let Some(sm) = &g.mask
&& !apply_soft_mask(sm, m, &mut tmp, budget)
{
budget.release(&tmp);
release_backdrop(budget, backdrop.as_ref());
return;
}
if g.blend == BlendMode::Normal {
over(dst, &tmp, o as f32);
@@ -194,12 +137,81 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image, budget: &mut
});
}
budget.release(&tmp);
drop(tmp);
if let Some(b) = &backdrop {
release_backdrop(budget, backdrop.as_ref());
}
fn release_backdrop(budget: &mut SurfaceBudget, backdrop: Option<&Image>) {
if let Some(b) = backdrop {
budget.release(b);
}
}
/// A clipping group over a backdrop: the children where the clip covers, the backdrop elsewhere.
fn clip_onto_backdrop(g: &Group, m: Affine, dst: &mut Image, tmp: &Image) {
let cov = clip_coverage(g, m, dst.width, dst.height);
dst.data.par_iter_mut().zip(tmp.data.par_iter()).zip(cov.par_iter()).for_each(|((d, t), c)| {
for i in 0..4 {
d[i] = d[i] * (1.0 - c) + t[i] * c;
}
});
}
/// Remove the backdrop's contribution (ISO 32000-1 §11.4.8): with the group's own
/// alpha αg (its children alone), C = Cn + (Cn − C0)·(α0/αg − α0). Returns false when no
/// scratch buffer is available.
fn strip_backdrop(g: &Group, m: Affine, backdrop: &Image, tmp: &mut Image, budget: &mut SurfaceBudget) -> bool {
let Some(mut solo) = budget.image(tmp.width, tmp.height, None) else {
return false;
};
draw_children(g, m, &mut solo, None, budget);
tmp.data.par_iter_mut().zip(backdrop.data.par_iter()).zip(solo.data.par_iter()).for_each(|((t, b0), s)| {
let ag = s[3];
if ag <= 0.0 {
*t = [0.0; 4];
return;
}
let un = |c: &[f32; 4]| if c[3] > 0.0 { [c[0] / c[3], c[1] / c[3], c[2] / c[3]] } else { [0.0; 3] };
let (cn, c0, a0) = (un(t), un(b0), b0[3]);
let k = a0 / ag - a0;
let mut out = [0.0; 4];
for i in 0..3 {
out[i] = (cn[i] + (cn[i] - c0[i]) * k).clamp(0.0, 1.0) * ag;
}
out[3] = ag;
*t = out;
});
budget.release(&solo);
drop(solo);
true
}
/// Multiplies the group's pixels by its soft mask. Returns false when no scratch buffer is
/// available.
fn apply_soft_mask(sm: &SoftMask, m: Affine, tmp: &mut Image, budget: &mut SurfaceBudget) -> bool {
let Some(mut mk) = budget.image(tmp.width, tmp.height, None) else {
return false;
};
draw_group(&sm.content, m, 1.0, &mut mk, budget);
let bc = sm.backdrop.map(|v| v as f32);
let lum = sm.luminosity;
tmp.data.par_iter_mut().zip(mk.data.par_iter()).for_each(|(p, q)| {
let k = if lum {
// The mask group composited over its backdrop colour, then its luminosity.
let c = [0, 1, 2].map(|i| q[i] + bc[i] * (1.0 - q[3]));
0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
} else {
q[3]
};
let k = k.clamp(0.0, 1.0);
for v in p.iter_mut() {
*v *= k;
}
});
budget.release(&mk);
drop(mk);
true
}
/// The product of a group's clip-path coverages.
fn clip_coverage(g: &Group, m: Affine, w: u32, h: u32) -> Vec<f32> {
let mut out = vec![1.0f32; w as usize * h as usize];
+1 -1
View File
@@ -971,7 +971,7 @@ fn curves_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter,
let col = channel_color(ch);
p.rect_filled(Rect::from_min_max(pos2(gr.min.x, gr.max.y + 3.0), pos2(gr.max.x, gr.max.y + 8.0)), 0.0, col.gamma_multiply(0.5));
let id = egui::Id::unique(("ec-curves", euid, ch));
let (resp, edit) = fw::curves_graph(ui, gr, &cur, ch, pencil, true, id, &t);
let (resp, edit) = fw::curves_graph(ui, gr, &cur, ch, fw::CurveMode { pencil, natural: true }, id, &t);
app.auto.add(&format!("effectControls.effect.{euid}.curves.graph"), gr, "Curves");
let started = resp.drag_started() || resp.clicked();
if let Some(np) = edit.changed {
+2 -2
View File
@@ -193,7 +193,7 @@ fn rgb_curves(
}
let gr = Rect::from_min_size(pos2(x0, r.min.y + 30.0), vec2(size, size));
let gid = egui::Id::unique(("lumetri-rgb", euid, ch));
let (resp, edit) = fw::curves_graph(ui, gr, &cur, ch, false, false, gid, &t);
let (resp, edit) = fw::curves_graph(ui, gr, &cur, ch, fw::CurveMode { pencil: false, natural: false }, gid, &t);
app.auto.add(&format!("{prefix}.graph"), gr, "RGB Curves");
register_points(app, &prefix, gr, &cur.0);
let key = gesture_key(ui, gid, resp.drag_started() || resp.clicked());
@@ -713,7 +713,7 @@ fn glow_map(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, laye
}
let gr = Rect::from_min_size(pos2(x0, r.min.y + 30.0), vec2(size, size));
let gid = egui::Id::unique(("glow-map", euid, ch));
let (resp, edit) = fw::curves_graph(ui, gr, &map[ch], ch + 1, false, false, gid, &t);
let (resp, edit) = fw::curves_graph(ui, gr, &map[ch], ch + 1, fw::CurveMode { pencil: false, natural: false }, gid, &t);
app.auto.add(&format!("{prefix}.graph"), gr, "Arbitrary Map");
register_points(app, &prefix, gr, &map[ch].0);
let key = gesture_key(ui, gid, resp.drag_started() || resp.clicked());
+91 -54
View File
@@ -325,23 +325,56 @@ pub struct CurveEdit {
pub done: bool,
}
/// How the Curves graph takes input: `pencil` draws freehand, `natural` picks the natural spline.
#[derive(Clone, Copy)]
pub struct CurveMode {
pub pencil: bool,
pub natural: bool,
}
/// The Curves graph: grid, identity diagonal, the curve, its points. Click adds a point, drag
/// moves one, dragging a point off the graph (or Alt-clicking it) removes it. In pencil mode a
/// drag draws the curve freehand. `natural` draws the natural spline the Curves effect renders
/// (`Curve::parse_natural`) instead of the monotone one.
/// drag draws the curve freehand. `mode.natural` draws the natural spline the Curves effect
/// renders (`Curve::parse_natural`) instead of the monotone one.
pub fn curves_graph(
ui: &mut egui::Ui,
rect: Rect,
pts: &CurvePoints,
channel: usize,
pencil: bool,
natural: bool,
mode: CurveMode,
id: egui::Id,
t: &Tokens,
) -> (egui::Response, CurveEdit) {
let resp = ui.interact(rect.expand(6.0), id, Sense::click_and_drag());
let p = ui.painter().with_clip_rect(rect.expand(8.0));
p.rect_filled(rect, 0.0, Color32::from_rgb(0x1c, 0x1c, 0x1c));
draw_curve_grid(&p, rect, t);
let col = curve_channel_color(channel);
let spline =
if mode.natural { effectcraft_engine::effects::Curve::parse_natural(&pts.format()) } else { effectcraft_engine::effects::Curve::parse(&pts.format()) };
let eval = |x: f32| spline.as_ref().map(|c| c.eval(x)).unwrap_or(x);
let line: Vec<Pos2> = (0..=96)
.map(|i| {
let x = i as f32 / 96.0;
curve_to_screen(rect, x, eval(x).clamp(0.0, 1.0))
})
.collect();
p.add(egui::Shape::line(line, Stroke::new(1.5, col)));
let drag_id = id.with("drag");
let dragging: Option<usize> = ui.data(|d| d.get_temp(drag_id));
draw_curve_points(&p, rect, pts, col, dragging);
if mode.pencil {
let edit = curve_pencil(ui, &resp, rect, id, &eval);
return (resp, edit);
}
let mut edit = CurveEdit::default();
curve_drag(ui, &resp, rect, pts, drag_id, dragging, &mut edit);
let alt = ui.input(|i| i.modifiers.alt);
curve_click(&resp, rect, pts, alt, &mut edit);
(resp, edit)
}
fn draw_curve_grid(p: &egui::Painter, rect: Rect, t: &Tokens) {
for k in 1..4 {
let f = k as f32 / 4.0;
let x = rect.min.x + rect.width() * f;
@@ -351,37 +384,29 @@ pub fn curves_graph(
}
p.line_segment([rect.left_bottom(), rect.right_top()], Stroke::new(1.0, Color32::from_white_alpha(40)));
p.rect_stroke(rect, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Outside);
let col = match channel {
}
fn curve_channel_color(channel: usize) -> Color32 {
match channel {
1 => Color32::from_rgb(0xf0, 0x60, 0x60),
2 => Color32::from_rgb(0x60, 0xe0, 0x70),
3 => Color32::from_rgb(0x60, 0x90, 0xff),
4 => Color32::from_gray(0xb0),
_ => Color32::from_gray(0xe8),
};
let to_s = |x: f32, y: f32| pos2(rect.min.x + x * rect.width(), rect.max.y - y * rect.height());
let to_g = |s: Pos2| [((s.x - rect.min.x) / rect.width()), ((rect.max.y - s.y) / rect.height())];
let spline =
if natural { effectcraft_engine::effects::Curve::parse_natural(&pts.format()) } else { effectcraft_engine::effects::Curve::parse(&pts.format()) };
let seed = || {
(0..=64)
.map(|i| {
let x = i as f32 / 64.0;
spline.as_ref().map(|c| c.eval(x)).unwrap_or(x)
})
.collect::<Vec<f32>>()
};
let line: Vec<Pos2> = (0..=96)
.map(|i| {
let x = i as f32 / 96.0;
to_s(x, spline.as_ref().map(|c| c.eval(x)).unwrap_or(x).clamp(0.0, 1.0))
})
.collect();
p.add(egui::Shape::line(line, Stroke::new(1.5, col)));
let drag_id = id.with("drag");
let pencil_id = id.with("pencil");
let dragging: Option<usize> = ui.data(|d| d.get_temp(drag_id));
}
}
fn curve_to_screen(rect: Rect, x: f32, y: f32) -> Pos2 {
pos2(rect.min.x + x * rect.width(), rect.max.y - y * rect.height())
}
fn screen_to_curve(rect: Rect, s: Pos2) -> [f32; 2] {
[((s.x - rect.min.x) / rect.width()), ((rect.max.y - s.y) / rect.height())]
}
fn draw_curve_points(p: &egui::Painter, rect: Rect, pts: &CurvePoints, col: Color32, dragging: Option<usize>) {
for (i, q) in pts.0.iter().enumerate() {
let s = to_s(q[0], q[1]);
let s = curve_to_screen(rect, q[0], q[1]);
let r = Rect::from_center_size(s, vec2(7.0, 7.0));
if dragging == Some(i) {
p.rect_filled(r, 0.0, col);
@@ -390,33 +415,41 @@ pub fn curves_graph(
p.rect_stroke(r, 0.0, Stroke::new(1.2, col), StrokeKind::Inside);
}
}
}
/// Pencil mode: a drag overwrites the sampled curve under the pointer.
fn curve_pencil(ui: &mut egui::Ui, resp: &egui::Response, rect: Rect, id: egui::Id, eval: &dyn Fn(f32) -> f32) -> CurveEdit {
let pencil_id = id.with("pencil");
let seed = || (0..=64).map(|i| eval(i as f32 / 64.0)).collect::<Vec<f32>>();
let mut edit = CurveEdit::default();
let hit_r = 7.0 / rect.width();
let alt = ui.input(|i| i.modifiers.alt);
if pencil {
if resp.drag_started() {
ui.data_mut(|d| d.insert_temp(pencil_id, seed()));
}
if (resp.dragged() || resp.clicked())
&& let Some(pt) = resp.interact_pointer_pos()
{
let mut samples: Vec<f32> = ui.data(|d| d.get_temp(pencil_id)).unwrap_or_else(seed);
let g = to_g(pt);
let i = (g[0].clamp(0.0, 1.0) * 64.0).round() as usize;
samples[i.min(64)] = g[1].clamp(0.0, 1.0);
ui.data_mut(|d| d.insert_temp(pencil_id, samples.clone()));
edit.changed = Some(CurvePoints::from_samples(&samples));
}
if resp.drag_stopped() {
ui.data_mut(|d| d.remove::<Vec<f32>>(pencil_id));
edit.done = true;
}
return (resp, edit);
if resp.drag_started() {
ui.data_mut(|d| d.insert_temp(pencil_id, seed()));
}
if (resp.dragged() || resp.clicked())
&& let Some(pt) = resp.interact_pointer_pos()
{
let mut samples: Vec<f32> = ui.data(|d| d.get_temp(pencil_id)).unwrap_or_else(seed);
let g = screen_to_curve(rect, pt);
let i = (g[0].clamp(0.0, 1.0) * 64.0).round() as usize;
samples[i.min(64)] = g[1].clamp(0.0, 1.0);
ui.data_mut(|d| d.insert_temp(pencil_id, samples.clone()));
edit.changed = Some(CurvePoints::from_samples(&samples));
}
if resp.drag_stopped() {
ui.data_mut(|d| d.remove::<Vec<f32>>(pencil_id));
edit.done = true;
}
edit
}
/// Drag a point to move it, or drag from empty space to insert one; dropping a point well off
/// the graph removes it.
fn curve_drag(ui: &mut egui::Ui, resp: &egui::Response, rect: Rect, pts: &CurvePoints, drag_id: egui::Id, dragging: Option<usize>, edit: &mut CurveEdit) {
let hit_r = 7.0 / rect.width();
if resp.drag_started()
&& let Some(pt) = ui.input(|i| i.pointer.press_origin())
{
let g = to_g(pt);
let g = screen_to_curve(rect, pt);
let i = match pts.hit(g[0], g[1], hit_r) {
Some(i) => i,
None => {
@@ -430,7 +463,7 @@ pub fn curves_graph(
} else if resp.dragged()
&& let (Some(i), Some(pt)) = (dragging, resp.interact_pointer_pos())
{
let g = to_g(pt);
let g = screen_to_curve(rect, pt);
let mut np = pts.clone();
np.move_point(i, g[0], g[1]);
if np != *pts {
@@ -449,10 +482,15 @@ pub fn curves_graph(
ui.data_mut(|d| d.remove::<usize>(drag_id));
edit.done = true;
}
}
/// A click adds a point on empty space; Alt-clicking a point removes it.
fn curve_click(resp: &egui::Response, rect: Rect, pts: &CurvePoints, alt: bool, edit: &mut CurveEdit) {
if resp.clicked()
&& let Some(pt) = resp.interact_pointer_pos()
{
let g = to_g(pt);
let g = screen_to_curve(rect, pt);
let hit_r = 7.0 / rect.width();
let mut np = pts.clone();
match pts.hit(g[0], g[1], hit_r) {
Some(i) if alt => {
@@ -468,7 +506,6 @@ pub fn curves_graph(
}
edit.done = true;
}
(resp, edit)
}
// ---------------------------------------------------------------------------------------------