mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 21:48:20 -06:00
Merge pull request #2639 from storytold/land/1328
#712: color pickers get an eyedropper everywhere (#1328)
This commit is contained in:
@@ -378,6 +378,10 @@ impl EffectcraftApp {
|
||||
// Video Preview is a second viewport: only a real pass can update it.
|
||||
prefs_live::frame(self, &ctx);
|
||||
self.auto.begin_frame();
|
||||
panels::viewer_tools::clear_sample_source(&ctx);
|
||||
if let Some(rect) = crate::color_picker::take_automation(&ctx) {
|
||||
self.auto.add("colorPicker.eyedropper", rect, "Eyedropper");
|
||||
}
|
||||
let tab_had_widget_focus = ctx.egui_wants_keyboard_input();
|
||||
menus::handle_shortcuts(self, &ctx);
|
||||
if !ctx.input(|i| i.pointer.primary_down()) {
|
||||
@@ -406,6 +410,7 @@ impl EffectcraftApp {
|
||||
let header = egui::Rect::from_min_size(egui::pos2(full.min.x, top), egui::vec2(full.width(), header_h));
|
||||
header::show(self, ui, header);
|
||||
let body = egui::Rect::from_min_max(egui::pos2(full.min.x + 4.0, header.max.y + 2.0), egui::pos2(full.max.x - 4.0, full.max.y - 4.0));
|
||||
crate::color_picker::note_dialog(&ctx, self.dialog);
|
||||
self.dock_area(ui, body);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
prefs_live::dropped_files(self, ui, full);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! The colour picker every colour input opens, laid out like After Effects' Color Picker: a
|
||||
//! square and a slider for the channel chosen with the H / S / B / R / G / B buttons, the new
|
||||
//! colour over the original, the HSB and RGB values, the hex code, and OK / Cancel.
|
||||
//! colour over the original, the HSB and RGB values, the hex code with an eyedropper that samples
|
||||
//! the Composition viewer, and OK / Cancel.
|
||||
//!
|
||||
//! Colours are sRGB components (0–1), as the project stores them. Changes apply as they are made
|
||||
//! (After Effects' Preview); Cancel or Escape goes back to the colour the picker opened with.
|
||||
@@ -30,35 +31,55 @@ pub fn srgb_color_button(ui: &mut Ui, c: &mut [f32; 3]) -> Response {
|
||||
}
|
||||
|
||||
/// The colour picker as a popup at `pos`, bound to `c`, opened with [`open_popup`]. Returns true
|
||||
/// when the colour changed (Cancel, Escape and the original swatch change it back).
|
||||
/// when the colour changed (Cancel, Escape and the original swatch change it back; the eyedropper
|
||||
/// changes it to the colour it samples).
|
||||
pub fn color_popup(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, c: &mut [f32; 3]) -> bool {
|
||||
if !popup_is_open(ui, id) {
|
||||
return false;
|
||||
}
|
||||
let orig_id = id.with("original");
|
||||
let original = ui.data_mut(|d| *d.get_temp_mut_or_insert_with(orig_id, || *c));
|
||||
// Read before a sample is taken: the press that samples the viewer lands outside the picker, and
|
||||
// must not close it.
|
||||
let armed = armed_for(ui.ctx(), id);
|
||||
let mut sampled = false;
|
||||
if let Some(rgb) = take_sample(ui.ctx(), id)
|
||||
&& rgb != *c
|
||||
{
|
||||
*c = rgb;
|
||||
sampled = true;
|
||||
}
|
||||
let mut out = Outcome::default();
|
||||
let area = egui::Area::new(id.with("area")).order(egui::Order::Foreground).fixed_pos(pos + vec2(0.0, 4.0)).show(ui.ctx(), |ui| {
|
||||
egui::Frame::popup(ui.style()).show(ui, |ui| out = color_picker(ui, id, c, original));
|
||||
});
|
||||
// Escape is the picker's: consume it, so nothing later this pass takes it too.
|
||||
// Escape is the picker's: consume it, so nothing later this pass takes it too. While the
|
||||
// eyedropper is armed, Escape cancels it and the picker stays open.
|
||||
if ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) {
|
||||
out.close = Some(false);
|
||||
if armed {
|
||||
disarm(ui.ctx(), id);
|
||||
} else {
|
||||
out.close = Some(false);
|
||||
}
|
||||
}
|
||||
// A press outside closes the picker, unless it is the armed eyedropper's press on the viewer.
|
||||
let on_viewer = armed && eyedropper_point(ui.ctx()).is_some();
|
||||
// A press in the pass that first draws the picker must not close it: an Area's rect is only
|
||||
// at its requested position from its second pass on (the first pass reports it at the
|
||||
// origin), so the click that just opened the picker would be read as a press outside it and
|
||||
// the picker would flash and close in that one frame (#1476).
|
||||
let pass_id = id.with("open-pass");
|
||||
let drawn_before = ui.data(|d| d.get_temp::<u64>(pass_id)).is_some_and(|p| p < ui.ctx().cumulative_pass_nr());
|
||||
if drawn_before && pressed_outside(ui.ctx(), &area.response) {
|
||||
if drawn_before && pressed_outside(ui.ctx(), &area.response) && !on_viewer {
|
||||
out.close.get_or_insert(true);
|
||||
}
|
||||
note_overlay(ui.ctx(), area.response.rect);
|
||||
if let Some(keep) = out.close {
|
||||
if !keep && *c != original {
|
||||
*c = original;
|
||||
out.changed = true;
|
||||
}
|
||||
disarm(ui.ctx(), id);
|
||||
ui.data_mut(|d| {
|
||||
d.insert_temp(id.with("open"), false);
|
||||
d.remove::<[f32; 3]>(orig_id);
|
||||
@@ -67,6 +88,7 @@ pub fn color_popup(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, c: &mut [f32; 3])
|
||||
d.remove::<u64>(open_pass_id());
|
||||
});
|
||||
} else {
|
||||
refresh_armed(ui.ctx(), id);
|
||||
// Still open after this pass: dialogs leave Escape to the picker (see `open_recently`).
|
||||
let pass = ui.ctx().cumulative_pass_nr();
|
||||
ui.data_mut(|d| {
|
||||
@@ -74,7 +96,7 @@ pub fn color_popup(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, c: &mut [f32; 3])
|
||||
d.insert_temp(open_pass_id(), pass);
|
||||
});
|
||||
}
|
||||
out.changed
|
||||
out.changed || sampled
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the pass number in which a colour picker last stayed open, which
|
||||
@@ -92,6 +114,196 @@ pub fn open_recently(ctx: &egui::Context) -> bool {
|
||||
pass.is_some_and(|p| p.saturating_add(1) >= ctx.cumulative_pass_nr())
|
||||
}
|
||||
|
||||
/// The armed eyedropper of a picker: the picker's popup id, the pass it was last drawn in, and the
|
||||
/// colour the viewer sampled for it, if one is waiting for the picker's next draw. It lives in the
|
||||
/// egui context, since the viewer that samples has no picker in hand.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Armed {
|
||||
popup: egui::Id,
|
||||
pass: u64,
|
||||
sample: Option<[f32; 3]>,
|
||||
dialog: Option<crate::Dialog>,
|
||||
}
|
||||
|
||||
fn armed_key() -> egui::Id {
|
||||
egui::Id::unique("color-picker-eyedropper")
|
||||
}
|
||||
|
||||
fn dialog_key() -> egui::Id {
|
||||
egui::Id::unique("color-picker-dialog")
|
||||
}
|
||||
|
||||
/// Record the modal before drawing the viewer. A picker may sample behind its own dialog, but
|
||||
/// a new, unrelated modal must block it from its very first pass, before its rect is known.
|
||||
pub(crate) fn note_dialog(ctx: &egui::Context, dialog: Option<crate::Dialog>) {
|
||||
ctx.data_mut(|data| data.insert_temp(dialog_key(), dialog));
|
||||
}
|
||||
|
||||
fn current_dialog(ctx: &egui::Context) -> Option<crate::Dialog> {
|
||||
ctx.data(|data| data.get_temp::<Option<crate::Dialog>>(dialog_key())).flatten()
|
||||
}
|
||||
|
||||
/// The armed eyedropper, if its picker was drawn in this pass or the last: one whose picker stops
|
||||
/// being drawn (its dialog gone) lapses.
|
||||
fn armed_record(ctx: &egui::Context) -> Option<Armed> {
|
||||
let armed: Option<Armed> = ctx.data(|d| d.get_temp(armed_key()));
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
armed.filter(|a| a.pass.saturating_add(1) >= pass)
|
||||
}
|
||||
|
||||
fn armed_for(ctx: &egui::Context, popup: egui::Id) -> bool {
|
||||
armed_record(ctx).is_some_and(|a| a.popup == popup)
|
||||
}
|
||||
|
||||
/// Some picker's eyedropper is armed: a press on the Composition viewer samples the frame for it.
|
||||
pub fn eyedropper_armed(ctx: &egui::Context) -> bool {
|
||||
armed_record(ctx).is_some()
|
||||
}
|
||||
|
||||
fn arm(ctx: &egui::Context, popup: egui::Id) {
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
let dialog = current_dialog(ctx);
|
||||
ctx.data_mut(|d| d.insert_temp(armed_key(), Armed { popup, pass, sample: None, dialog }));
|
||||
}
|
||||
|
||||
fn disarm(ctx: &egui::Context, popup: egui::Id) {
|
||||
let armed: Option<Armed> = ctx.data(|d| d.get_temp(armed_key()));
|
||||
if armed.is_some_and(|a| a.popup == popup) {
|
||||
ctx.data_mut(|d| d.remove::<Armed>(armed_key()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps `popup`'s eyedropper armed while its picker is drawn.
|
||||
fn refresh_armed(ctx: &egui::Context, popup: egui::Id) {
|
||||
if let Some(armed) = armed_record(ctx).filter(|a| a.popup == popup) {
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
ctx.data_mut(|d| d.insert_temp(armed_key(), Armed { pass, ..armed }));
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour the viewer sampled for `popup`, once: taking it disarms the eyedropper.
|
||||
fn take_sample(ctx: &egui::Context, popup: egui::Id) -> Option<[f32; 3]> {
|
||||
let armed = armed_record(ctx).filter(|a| a.popup == popup)?;
|
||||
let sample = armed.sample?;
|
||||
disarm(ctx, popup);
|
||||
Some(sample)
|
||||
}
|
||||
|
||||
/// The viewer sampled `rgb` for the armed picker, which takes it on its next draw.
|
||||
pub fn deliver_sample(ctx: &egui::Context, rgb: [f32; 3]) {
|
||||
if let Some(armed) = armed_record(ctx) {
|
||||
ctx.data_mut(|d| d.insert_temp(armed_key(), Armed { sample: Some(rgb), ..armed }));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
fn viewer_key() -> egui::Id {
|
||||
egui::Id::unique("color-picker-viewer")
|
||||
}
|
||||
|
||||
/// The Composition viewer's rect as it was drawn this pass: where an armed eyedropper samples.
|
||||
pub fn note_viewer(ctx: &egui::Context, rect: Rect) {
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
ctx.data_mut(|d| d.insert_temp(viewer_key(), (pass, rect)));
|
||||
}
|
||||
|
||||
fn overlay_key() -> egui::Id {
|
||||
egui::Id::unique("color-picker-overlays")
|
||||
}
|
||||
|
||||
/// A popup or dialog drawn over the viewer: a press inside `rect` is not a sample.
|
||||
pub fn note_overlay(ctx: &egui::Context, rect: Rect) {
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
ctx.data_mut(|d| {
|
||||
// This pass's and the last's: the viewer may be drawn before an overlay in a pass.
|
||||
let mut rects = d.get_temp::<Vec<(u64, Rect)>>(overlay_key()).unwrap_or_default();
|
||||
rects.retain(|(p, _)| p.saturating_add(1) >= pass);
|
||||
rects.push((pass, rect));
|
||||
d.insert_temp(overlay_key(), rects);
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether `pos` is over a popup or dialog drawn this pass or the last.
|
||||
fn overlaid(ctx: &egui::Context, pos: egui::Pos2) -> bool {
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
let rects: Option<Vec<(u64, Rect)>> = ctx.data(|d| d.get_temp(overlay_key()));
|
||||
rects.unwrap_or_default().iter().any(|(p, r)| p.saturating_add(1) >= pass && r.contains(pos))
|
||||
}
|
||||
|
||||
/// Where an armed eyedropper samples: the pointer, if it is over the viewer and not over a popup or
|
||||
/// dialog. The pointer is read rather than the viewer's responses, since a modal dialog over the
|
||||
/// viewer takes its clicks.
|
||||
pub fn eyedropper_point(ctx: &egui::Context) -> Option<egui::Pos2> {
|
||||
if armed_record(ctx).is_some_and(|armed| armed.dialog != current_dialog(ctx)) {
|
||||
return None;
|
||||
}
|
||||
let viewer: Option<(u64, Rect)> = ctx.data(|d| d.get_temp(viewer_key()));
|
||||
let viewer = viewer.filter(|(p, _)| p.saturating_add(1) >= ctx.cumulative_pass_nr())?.1;
|
||||
let pos = ctx.input(|i| i.pointer.interact_pos())?;
|
||||
(viewer.contains(pos) && !overlaid(ctx, pos)).then_some(pos)
|
||||
}
|
||||
|
||||
/// The press that samples for an armed eyedropper this frame, if there is one.
|
||||
pub fn sample_press(ctx: &egui::Context) -> Option<egui::Pos2> {
|
||||
if ctx.input(|i| i.pointer.primary_pressed()) { eyedropper_point(ctx) } else { None }
|
||||
}
|
||||
|
||||
fn automation_key() -> egui::Id {
|
||||
egui::Id::unique("color-picker-eyedropper-rect")
|
||||
}
|
||||
|
||||
/// The eyedropper's rect, for the automation registry; [`take_automation`] hands it over.
|
||||
fn note_automation(ctx: &egui::Context, rect: Rect) {
|
||||
ctx.data_mut(|d| d.insert_temp(automation_key(), rect));
|
||||
}
|
||||
|
||||
/// The eyedropper rect the last picker drawing noted, once. Pickers have no registry in hand, so the
|
||||
/// app registers it each frame.
|
||||
pub fn take_automation(ctx: &egui::Context) -> Option<Rect> {
|
||||
ctx.data_mut(|d| {
|
||||
let rect = d.get_temp::<Rect>(automation_key());
|
||||
d.remove::<Rect>(automation_key());
|
||||
rect
|
||||
})
|
||||
}
|
||||
|
||||
/// The eyedropper of the hex row. A press arms it, or disarms it while armed; the next press on the
|
||||
/// Composition viewer then samples the colour under the pointer into this picker.
|
||||
fn eyedropper(ui: &mut Ui, popup: egui::Id) {
|
||||
let armed = armed_for(ui.ctx(), popup);
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::click());
|
||||
let resp = resp.on_hover_text(tr("Eyedropper: click, then click in the Composition panel"));
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, resp.enabled(), "Eyedropper"));
|
||||
let fill = if armed {
|
||||
ui.visuals().selection.bg_fill
|
||||
} else if resp.hovered() {
|
||||
ui.visuals().widgets.hovered.bg_fill
|
||||
} else {
|
||||
Color32::TRANSPARENT
|
||||
};
|
||||
ui.painter().rect_filled(rect, 2.0, fill);
|
||||
let col = if armed { ui.visuals().selection.stroke.color } else { ui.visuals().strong_text_color() };
|
||||
draw_pipette(ui.painter(), rect, col);
|
||||
if resp.clicked() {
|
||||
if armed {
|
||||
disarm(ui.ctx(), popup);
|
||||
} else {
|
||||
arm(ui.ctx(), popup);
|
||||
}
|
||||
}
|
||||
note_automation(ui.ctx(), rect);
|
||||
}
|
||||
|
||||
/// A pipette: a diagonal glass tube with a bulb at the top right and a tip at the bottom left, drawn
|
||||
/// within `rect`.
|
||||
pub fn draw_pipette(p: &egui::Painter, rect: Rect, col: Color32) {
|
||||
let r = rect.shrink(rect.width() * 0.2);
|
||||
let at = |x: f32, y: f32| pos2(r.min.x + x * r.width(), r.min.y + y * r.height());
|
||||
p.line_segment([at(0.08, 0.92), at(0.62, 0.38)], Stroke::new(2.2, col));
|
||||
p.circle_filled(at(0.76, 0.24), r.width() * 0.2, col);
|
||||
p.line_segment([at(0.5, 0.3), at(0.7, 0.5)], Stroke::new(1.4, col));
|
||||
}
|
||||
|
||||
/// What the picker did this frame.
|
||||
#[derive(Default)]
|
||||
struct Outcome {
|
||||
@@ -255,6 +467,7 @@ fn color_picker(ui: &mut Ui, id: egui::Id, c: &mut [f32; 3], original: [f32; 3])
|
||||
if hex_field(ui, id.with("hex"), &mut rgb) {
|
||||
pick = Pick { hsv: keep_hue(rgb_to_hsv(rgb), pick.hsv), rgb };
|
||||
}
|
||||
eyedropper(ui, id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -447,3 +660,125 @@ pub fn hsv_to_rgb([h, s, v]: [f32; 3]) -> [f32; 3] {
|
||||
};
|
||||
[f(5.0), f(3.0), f(1.0)]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sampling_viewer() -> egui_kittest::Harness<'static, crate::EffectcraftApp> {
|
||||
let mut session = effectcraft_engine::Session::default();
|
||||
session.execute("comp.new", serde_json::json!({"width": 320, "height": 180, "duration": 2})).unwrap();
|
||||
session.execute("layer.newSolid", serde_json::json!({"color": "#204060"})).unwrap();
|
||||
let mut harness = egui_kittest::Harness::builder().with_size(vec2(1200.0, 800.0)).build_eframe(|_| crate::EffectcraftApp::new(session));
|
||||
settle_sampling_viewer(&mut harness);
|
||||
harness
|
||||
}
|
||||
|
||||
fn settle_sampling_viewer(harness: &mut egui_kittest::Harness<'_, crate::EffectcraftApp>) {
|
||||
for _ in 0..200 {
|
||||
harness.step();
|
||||
if harness.state().frames.inflight() == 0 && harness.state().viewer_image.is_some() {
|
||||
harness.run_steps(3);
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
panic!("the synthetic viewer frame did not arrive");
|
||||
}
|
||||
|
||||
fn press_sampling_viewer(harness: &mut egui_kittest::Harness<'_, crate::EffectcraftApp>) -> Option<[f32; 3]> {
|
||||
let ctx = harness.ctx.clone();
|
||||
arm(&ctx, egui::Id::unique("test-popup"));
|
||||
let point = crate::panels::viewer::comp_to_screen(&ctx, [20.0, 20.0]).unwrap();
|
||||
harness.event(egui::Event::PointerMoved(point));
|
||||
harness.event(egui::Event::PointerButton { pos: point, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE });
|
||||
harness.step();
|
||||
let sample = ctx.data(|data| data.get_temp::<Armed>(armed_key())).and_then(|armed| armed.sample);
|
||||
harness.event(egui::Event::PointerButton { pos: point, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE });
|
||||
harness.step();
|
||||
sample
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn popup_eyedropper_samples_the_displayed_channel_and_exposure() {
|
||||
let mut harness = sampling_viewer();
|
||||
harness.state_mut().session.state.viewer.channel = effectcraft_engine::viewer::Channel::Green;
|
||||
let green = press_sampling_viewer(&mut harness).unwrap();
|
||||
assert!(green.iter().all(|value| (*value - 64.0 / 255.0).abs() < 0.01), "{green:?}");
|
||||
let options = &mut harness.state_mut().session.state.viewer;
|
||||
options.channel = effectcraft_engine::viewer::Channel::Rgb;
|
||||
options.exposure = 1.0;
|
||||
let exposed = press_sampling_viewer(&mut harness).unwrap();
|
||||
let mut expected = [[32, 64, 96, 255]];
|
||||
effectcraft_engine::viewer::display_transform(&mut expected, effectcraft_engine::viewer::Channel::Rgb, false, 1.0);
|
||||
for (actual, expected) in exposed.iter().zip(expected[0]) {
|
||||
assert!((*actual - expected as f32 / 255.0).abs() < 0.01, "{exposed:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn popup_eyedropper_samples_snapshot_and_refuses_wireframe() {
|
||||
let mut harness = sampling_viewer();
|
||||
harness.state_mut().session.execute("view.takeSnapshot", serde_json::json!({})).unwrap();
|
||||
harness.state_mut().session.execute("layer.newSolid", serde_json::json!({"color": "#a0b0c0"})).unwrap();
|
||||
settle_sampling_viewer(&mut harness);
|
||||
harness.event(egui::Event::Key { key: egui::Key::F5, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE });
|
||||
// Let the snapshot texture's discard passes finish before arming this synthetic picker.
|
||||
harness.step();
|
||||
let snapshot = press_sampling_viewer(&mut harness).unwrap();
|
||||
for (actual, expected) in snapshot.iter().zip([32, 64, 96]) {
|
||||
assert!((*actual - expected as f32 / 255.0).abs() < 0.01, "{snapshot:?}");
|
||||
}
|
||||
let options = &mut harness.state_mut().session.state.viewer;
|
||||
options.show_snapshot = false;
|
||||
options.fast_previews = effectcraft_engine::commands::viewer_cmds::FastPreviews::Wireframe;
|
||||
harness.event(egui::Event::Key { key: egui::Key::F5, physical_key: None, pressed: false, repeat: false, modifiers: egui::Modifiers::NONE });
|
||||
assert_eq!(press_sampling_viewer(&mut harness), None, "wireframe must not sample an old pixel frame");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_modal_backdrop_cannot_deliver_a_pending_sample() {
|
||||
let mut harness = sampling_viewer();
|
||||
harness.state_mut().dialog = Some(crate::Dialog::About);
|
||||
assert_eq!(press_sampling_viewer(&mut harness), None, "the first modal pass must already exclude its backdrop");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_picker_can_sample_behind_its_own_dialog_but_not_a_new_modal() {
|
||||
let ctx = egui::Context::default();
|
||||
let point = pos2(20.0, 20.0);
|
||||
let mut output = ctx.run_ui(egui::RawInput { events: vec![egui::Event::PointerMoved(point)], ..Default::default() }, |ui| {
|
||||
let ctx = ui.ctx();
|
||||
note_dialog(ctx, Some(crate::Dialog::SolidSettings));
|
||||
note_viewer(ctx, Rect::from_min_size(pos2(0.0, 0.0), vec2(320.0, 180.0)));
|
||||
note_overlay(ctx, Rect::from_min_size(pos2(100.0, 100.0), vec2(120.0, 80.0)));
|
||||
arm(ctx, egui::Id::unique("test-picker"));
|
||||
assert_eq!(eyedropper_point(ctx), Some(point));
|
||||
note_dialog(ctx, Some(crate::Dialog::About));
|
||||
assert_eq!(eyedropper_point(ctx), None);
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn armed_eyedropper_takes_one_sample_then_disarms() {
|
||||
let ctx = egui::Context::default();
|
||||
let popup = egui::Id::unique("test-picker");
|
||||
arm(&ctx, popup);
|
||||
assert!(eyedropper_armed(&ctx));
|
||||
assert_eq!(take_sample(&ctx, popup), None);
|
||||
deliver_sample(&ctx, [0.1, 0.2, 0.3]);
|
||||
assert_eq!(take_sample(&ctx, popup), Some([0.1, 0.2, 0.3]));
|
||||
assert!(!eyedropper_armed(&ctx));
|
||||
assert_eq!(take_sample(&ctx, popup), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn points_over_a_dialog_are_not_samples() {
|
||||
let ctx = egui::Context::default();
|
||||
let dialog = Rect::from_min_size(pos2(100.0, 100.0), vec2(120.0, 80.0));
|
||||
note_overlay(&ctx, dialog);
|
||||
assert!(overlaid(&ctx, pos2(150.0, 150.0)));
|
||||
assert!(!overlaid(&ctx, pos2(20.0, 20.0)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,18 +539,16 @@ impl Default for Frames {
|
||||
/// Premultiplied f32 → egui premultiplied Color32 (alpha kept, so the viewer can show the
|
||||
/// transparency grid or the comp background colour underneath).
|
||||
pub fn to_color_image(img: &effectcraft_engine::render::Image) -> egui::ColorImage {
|
||||
let px: Vec<egui::Color32> = img
|
||||
.data
|
||||
.par_iter()
|
||||
.map(|p| {
|
||||
let a = p[3].clamp(0.0, 1.0);
|
||||
let c = |v: f32| (v.clamp(0.0, a) * 255.0 + 0.5) as u8;
|
||||
egui::Color32::from_rgba_premultiplied(c(p[0]), c(p[1]), c(p[2]), (a * 255.0 + 0.5) as u8)
|
||||
})
|
||||
.collect();
|
||||
let px: Vec<egui::Color32> = img.data.par_iter().map(|p| display_pixel(*p)).collect();
|
||||
egui::ColorImage::new([img.width as usize, img.height as usize], px)
|
||||
}
|
||||
|
||||
pub(crate) fn display_pixel(pixel: [f32; 4]) -> egui::Color32 {
|
||||
let alpha = if pixel[3].is_nan() { 0.0 } else { pixel[3].clamp(0.0, 1.0) };
|
||||
let component = |value: f32| (value.clamp(0.0, alpha) * 255.0 + 0.5) as u8;
|
||||
egui::Color32::from_rgba_premultiplied(component(pixel[0]), component(pixel[1]), component(pixel[2]), (alpha * 255.0 + 0.5) as u8)
|
||||
}
|
||||
|
||||
/// The whole factor an image of `size` is averaged down by for a texture: at least `min_k`, and
|
||||
/// the smallest that fits its sides in `max_side` (frames of comps wider than the GPU's texture
|
||||
/// limit, #201).
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn converting_a_frame_with_nan_alpha_keeps_the_viewer_alive() {
|
||||
let image = effectcraft_engine::render::Image::filled(1, 1, [0.5, 0.25, 0.1, f32::NAN]);
|
||||
assert_eq!(to_color_image(&image).pixels, [egui::Color32::TRANSPARENT]);
|
||||
}
|
||||
|
||||
struct ProbeDisk {
|
||||
keys: Mutex<HashSet<u128>>,
|
||||
probes: std::sync::atomic::AtomicUsize,
|
||||
|
||||
@@ -173,6 +173,7 @@ pub(crate) fn tr_args(text: &str, args: &[&dyn Display]) -> String {
|
||||
|
||||
/// Japanese: the same rows, in the same order, as the Chinese catalogs below.
|
||||
const JAPANESE: &[(&str, &str)] = &[
|
||||
("Click to sample color", "クリックして色をサンプル"),
|
||||
("Appearance Settings…", "外観設定…"),
|
||||
("Add to favorites", "お気に入りに追加"),
|
||||
("Remove from favorites", "お気に入りから削除"),
|
||||
@@ -1222,6 +1223,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
];
|
||||
|
||||
const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("Click to sample color", "单击以取样颜色"),
|
||||
("Appearance Settings…", "外观设置…"),
|
||||
("Add to favorites", "添加到收藏"),
|
||||
("Remove from favorites", "从收藏中移除"),
|
||||
@@ -2330,6 +2332,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
];
|
||||
|
||||
const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("Click to sample color", "按一下以取樣顏色"),
|
||||
("Appearance Settings…", "外觀設定…"),
|
||||
("Add to favorites", "加入最愛"),
|
||||
("Remove from favorites", "從最愛移除"),
|
||||
@@ -3386,6 +3389,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
/// Ukrainian (uk): original translations of the same interface rows as the Chinese catalogs.
|
||||
/// Technical names, key names and substituted project content keep their original spelling.
|
||||
const UKRAINIAN: &[(&str, &str)] = &[
|
||||
("Click to sample color", "Клацніть, щоб узяти зразок кольору"),
|
||||
("Appearance Settings…", "Налаштування вигляду…"),
|
||||
("Add to favorites", "Додати до вибраного"),
|
||||
("Remove from favorites", "Видалити з вибраного"),
|
||||
@@ -3962,6 +3966,7 @@ const UKRAINIAN: &[(&str, &str)] = &[
|
||||
|
||||
// Brazilian Portuguese.
|
||||
const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("Click to sample color", "Clique para obter uma amostra de cor"),
|
||||
("Export Scene to Blender...", "Exportar cena para o Blender..."),
|
||||
(" OK ", " OK "),
|
||||
(" Delete ", " Excluir "),
|
||||
@@ -4715,6 +4720,7 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
|
||||
/// Polish (pl): original translations covering every row in the Chinese panel catalogs.
|
||||
const POLISH: &[(&str, &str)] = &[
|
||||
("Click to sample color", "Kliknij, aby pobrać próbkę koloru"),
|
||||
("Appearance Settings…", "Ustawienia wyglądu…"),
|
||||
("Add to favorites", "Dodaj do ulubionych"),
|
||||
("Remove from favorites", "Usuń z ulubionych"),
|
||||
@@ -5462,6 +5468,7 @@ const POLISH: &[(&str, &str)] = &[
|
||||
|
||||
/// Czech (cs): original translations covering every row in the Chinese panel catalogs.
|
||||
const CZECH: &[(&str, &str)] = &[
|
||||
("Click to sample color", "Kliknutím naberete barvu"),
|
||||
(" OK ", " OK "),
|
||||
(" Delete ", " Odstranit "),
|
||||
(" Open Auto-Save ", " Otevřít automatickou zálohu "),
|
||||
@@ -6018,6 +6025,7 @@ const CZECH: &[(&str, &str)] = &[
|
||||
/// order. Terms follow the Spanish menu catalog (composición, material de archivo, cola de procesamiento,
|
||||
/// ajuste preestablecido, método abreviado). Written from the English source.
|
||||
const SPANISH: &[(&str, &str)] = &[
|
||||
("Click to sample color", "Haz clic para tomar una muestra de color"),
|
||||
("Copy Source Path", "Copiar ruta de origen"),
|
||||
("Appearance Settings…", "Ajustes de apariencia…"),
|
||||
("Add to favorites", "Añadir a favoritos"),
|
||||
@@ -6576,6 +6584,7 @@ const SPANISH: &[(&str, &str)] = &[
|
||||
|
||||
/// Georgian (ka): every row of the Chinese catalogs, in the same order (the tests keep them in step).
|
||||
const GEORGIAN: &[(&str, &str)] = &[
|
||||
("Click to sample color", "დააწკაპუნეთ ფერის ნიმუშის ასაღებად"),
|
||||
("Appearance Settings…", "იერსახის პარამეტრები…"),
|
||||
("Add to favorites", "რჩეულებში დამატება"),
|
||||
("Remove from favorites", "რჩეულებიდან ამოშლა"),
|
||||
|
||||
@@ -265,6 +265,8 @@ pub(crate) fn modal_fit_placed(ctx: &egui::Context, title: &str, size: egui::Vec
|
||||
ui.add_space(10.0);
|
||||
let title_h = ui.cursor().top() - ui.min_rect().top();
|
||||
body(ui, (size.y - 36.0 - title_h).max(40.0));
|
||||
// A colour picker's eyedropper samples the viewer under the dialog, but not the dialog itself.
|
||||
crate::color_picker::note_overlay(ui.ctx(), ui.min_rect().expand(18.0));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,20 @@ fn fx_pick_session(
|
||||
ControlFlow::Continue(point_preview)
|
||||
}
|
||||
|
||||
/// Shared hint chip and sampled-color swatch for text and popup eyedroppers.
|
||||
fn color_pick_preview(app: &EffectcraftApp, painter: &egui::Painter, map: &crate::panels::viewer::ViewerMap, hint: &str, pointer: Option<egui::Pos2>) {
|
||||
let chip = Rect::from_min_size(map.area.min + vec2(10.0, 10.0), vec2(220.0, 22.0));
|
||||
painter.rect_filled(chip, 11.0, Color32::from_black_alpha(180));
|
||||
painter.text(chip.center(), Align2::CENTER_CENTER, hint, Tokens::ui(11.5), app.tokens.text);
|
||||
if let Some(pos) = pointer
|
||||
&& let Some(sample) = crate::panels::viewer_tools::sample_displayed(app, painter.ctx(), map.to_comp(pos))
|
||||
{
|
||||
let swatch = Rect::from_min_size(pos + vec2(14.0, 14.0), vec2(26.0, 18.0));
|
||||
painter.rect_filled(swatch, 2.0, Color32::from_rgb((sample[0] * 255.0) as u8, (sample[1] * 255.0) as u8, (sample[2] * 255.0) as u8));
|
||||
painter.rect_stroke(swatch, 2.0, Stroke::new(1.0, Color32::WHITE), egui::StrokeKind::Outside);
|
||||
}
|
||||
}
|
||||
|
||||
/// An armed text-colour pick: sample the shown frame on click, Esc cancels.
|
||||
fn text_color_pick(
|
||||
app: &mut EffectcraftApp,
|
||||
@@ -128,7 +142,6 @@ fn text_color_pick(
|
||||
ectx: &EvalCtx,
|
||||
actions: &mut Actions,
|
||||
) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
let Some(pick) = app.ui.text_color_pick.clone() else {
|
||||
return;
|
||||
@@ -140,25 +153,15 @@ fn text_color_pick(
|
||||
app.viewer_pixels();
|
||||
let resp = ui.interact(map.area, egui::Id::unique("viewer-text-color-pick"), Sense::click());
|
||||
app.auto.add("viewer.textColorPick", map.area, &pick.field);
|
||||
let chip = Rect::from_min_size(map.area.min + vec2(10.0, 10.0), vec2(220.0, 22.0));
|
||||
painter.rect_filled(chip, 11.0, Color32::from_black_alpha(180));
|
||||
painter.text(chip.center(), Align2::CENTER_CENTER, crate::i18n::tr_args("Click to sample text {}", &[&pick.field]), Tokens::ui(11.5), t.text);
|
||||
color_pick_preview(app, painter, map, &crate::i18n::tr_args("Click to sample text {}", &[&pick.field]), resp.hover_pos());
|
||||
if let Some(pos) = resp.hover_pos() {
|
||||
ctx.set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
let c = map.to_comp(pos);
|
||||
app.pointer_comp = Some([c[0] as f32, c[1] as f32]);
|
||||
if let Some(img) = &app.viewer_image
|
||||
&& let Some(s) = fw::sample_frame(img, crate::panels::viewer_tools::shown_region(&ctx, map.comp), c)
|
||||
{
|
||||
let sw = Rect::from_min_size(pos + vec2(14.0, 14.0), vec2(26.0, 18.0));
|
||||
painter.rect_filled(sw, 2.0, Color32::from_rgb((s[0] * 255.0) as u8, (s[1] * 255.0) as u8, (s[2] * 255.0) as u8));
|
||||
painter.rect_stroke(sw, 2.0, Stroke::new(1.0, Color32::WHITE), egui::StrokeKind::Outside);
|
||||
}
|
||||
}
|
||||
if resp.clicked()
|
||||
&& let Some(pos) = resp.interact_pointer_pos()
|
||||
&& let Some(img) = &app.viewer_image
|
||||
&& let Some(s) = fw::sample_frame(img, crate::panels::viewer_tools::shown_region(&ctx, map.comp), map.to_comp(pos))
|
||||
&& let Some(s) = crate::panels::viewer_tools::sample_displayed(app, &ctx, map.to_comp(pos))
|
||||
{
|
||||
let mut params = json!({"layer": pick.layer});
|
||||
params[&pick.field] = json!([s[0], s[1], s[2]]);
|
||||
@@ -176,6 +179,27 @@ fn text_color_pick(
|
||||
}
|
||||
}
|
||||
|
||||
/// A colour picker's eyedropper is armed (its popup stays open): the viewer shows the colour under
|
||||
/// the pointer, and the next press on it samples the frame into the picker. The press is read from
|
||||
/// the pointer, since a modal dialog over the viewer takes its clicks.
|
||||
fn popup_eyedropper(app: &mut EffectcraftApp, ui: &mut egui::Ui, painter: &egui::Painter, map: &crate::panels::viewer::ViewerMap) {
|
||||
let ctx = ui.ctx().clone();
|
||||
if !crate::color_picker::eyedropper_armed(&ctx) {
|
||||
return;
|
||||
}
|
||||
let _ = ui.interact(map.area, egui::Id::unique("viewer-popup-eyedropper"), Sense::click());
|
||||
// The sampling surface, apart from the picker's button that arms it (`colorPicker.eyedropper`).
|
||||
app.auto.add("viewer.colorPickerEyedropper", map.area, "Sample color");
|
||||
ctx.set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
app.viewer_pixels();
|
||||
color_pick_preview(app, painter, map, crate::i18n::tr("Click to sample color"), crate::color_picker::eyedropper_point(&ctx));
|
||||
if let Some(pos) = crate::color_picker::sample_press(&ctx)
|
||||
&& let Some(s) = crate::panels::viewer_tools::sample_displayed(app, &ctx, map.to_comp(pos))
|
||||
{
|
||||
crate::color_picker::deliver_sample(&ctx, [s[0], s[1], s[2]]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Effect Controls' part of the Composition viewer, called by the viewer after its own
|
||||
/// interaction (so these controls sit on top):
|
||||
///
|
||||
@@ -195,12 +219,14 @@ pub fn viewer_hook(
|
||||
) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
crate::color_picker::note_viewer(&ctx, map.area);
|
||||
let mut actions: Actions = vec![];
|
||||
let point_preview = match fx_pick_session(app, ui, painter, map, ectx, l2c, &mut actions) {
|
||||
ControlFlow::Break(()) => return,
|
||||
ControlFlow::Continue(p) => p,
|
||||
};
|
||||
text_color_pick(app, ui, painter, map, ectx, &mut actions);
|
||||
popup_eyedropper(app, ui, painter, map);
|
||||
let picking = app.ui.fx_pick.is_some() || app.ui.text_color_pick.is_some();
|
||||
let point_pick = app.ui.fx_pick.clone().filter(|p| p.kind == "point");
|
||||
if app.ui.viewer.show_layer_controls || point_pick.is_some() {
|
||||
|
||||
@@ -172,12 +172,7 @@ pub fn eyedropper_button(ui: &mut egui::Ui, rect: Rect, active: bool, id: egui::
|
||||
} else {
|
||||
t.text_dim
|
||||
};
|
||||
// A pipette: a diagonal glass tube with a bulb at the top right and a tip at the bottom left.
|
||||
let r = rect.shrink(rect.width() * 0.2);
|
||||
let at = |x: f32, y: f32| pos2(r.min.x + x * r.width(), r.min.y + y * r.height());
|
||||
p.line_segment([at(0.08, 0.92), at(0.62, 0.38)], Stroke::new(2.2, col));
|
||||
p.circle_filled(at(0.76, 0.24), r.width() * 0.2, col);
|
||||
p.line_segment([at(0.5, 0.3), at(0.7, 0.5)], Stroke::new(1.4, col));
|
||||
crate::color_picker::draw_pipette(p, rect, col);
|
||||
resp
|
||||
}
|
||||
|
||||
@@ -201,14 +196,21 @@ pub fn layer_to_comp(l2c: &Mat3, p: [f64; 2]) -> [f64; 2] {
|
||||
/// `region` ([x, y, w, h] in comp pixels: the whole comp, or the region of interest or Extended
|
||||
/// Viewer area it was rendered for), maybe at a reduced resolution. `None` outside it.
|
||||
pub fn frame_pixel(img: &egui::ColorImage, region: [f64; 4], pt: [f64; 2]) -> Option<Color32> {
|
||||
img.pixels.get(frame_pixel_index(img.size, region, pt)?).copied()
|
||||
}
|
||||
|
||||
pub(crate) fn frame_pixel_index(size: [usize; 2], region: [f64; 4], pt: [f64; 2]) -> Option<usize> {
|
||||
if size.contains(&0) {
|
||||
return None;
|
||||
}
|
||||
let [x, y, w, h] = region;
|
||||
let (fx, fy) = ((pt[0] - x) / w, (pt[1] - y) / h);
|
||||
if !(0.0..1.0).contains(&fx) || !(0.0..1.0).contains(&fy) {
|
||||
return None;
|
||||
}
|
||||
let sx = ((fx * img.size[0] as f64).floor() as usize).min(img.size[0].saturating_sub(1));
|
||||
let sy = ((fy * img.size[1] as f64).floor() as usize).min(img.size[1].saturating_sub(1));
|
||||
img.pixels.get(sy.checked_mul(img.size[0])?.checked_add(sx)?).copied()
|
||||
let sx = ((fx * size[0] as f64).floor() as usize).min(size[0].saturating_sub(1));
|
||||
let sy = ((fy * size[1] as f64).floor() as usize).min(size[1].saturating_sub(1));
|
||||
sy.checked_mul(size[0])?.checked_add(sx)
|
||||
}
|
||||
|
||||
/// Straight RGBA (0..1) of the displayed frame at comp point `pt` ([`frame_pixel`]).
|
||||
|
||||
@@ -269,7 +269,8 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
let er = Rect::from_min_size(pos2(fs.max.x + 4.0, y + 1.0), vec2(18.0, 18.0));
|
||||
let active = app.ui.text_color_pick.as_ref().is_some_and(|pick| pick.layer == target.as_ref().map_or(0, |tt| tt.layer) && pick.field == key);
|
||||
let eye = super::fx_widgets::eyedropper_button(ui, er, active, egui::Id::unique(("char-eye", key)), &t);
|
||||
let eye = super::fx_widgets::eyedropper_button(ui, er, active, egui::Id::unique(("char-eye", key)), &t)
|
||||
.on_hover_text(tr("Eyedropper: click, then click in the Composition panel"));
|
||||
app.auto.add(&format!("character.{key}.eyedropper"), er, &format!("{label} eyedropper"));
|
||||
if eye.clicked() {
|
||||
if active {
|
||||
|
||||
@@ -366,6 +366,7 @@ pub(crate) fn display_key(app: &EffectcraftApp, has_conversion: bool) -> String
|
||||
/// is set), or the snapshot while Show Snapshot is on / F5 is held, through Show Channel and the
|
||||
/// exposure. Fast Previews ▸ Wireframe draws layer outlines instead.
|
||||
pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter: &egui::Painter, comp_rect: Rect, cid: ItemId, ectx: &EvalCtx) {
|
||||
clear_sample_source(ctx);
|
||||
let opts = app.session.state.viewer.clone();
|
||||
let snap = showing_snapshot(app, ctx);
|
||||
// Settings ▸ Video ▸ Mirror on Computer Monitor off: playback goes to Video Preview only.
|
||||
@@ -393,6 +394,8 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter:
|
||||
let dc = vw::DisplayColor::of(&app.session);
|
||||
let dc_key = display_key(app, dc.is_some());
|
||||
let plain = opts.channel == Channel::Rgb && opts.exposure == 0.0 && dc.is_none();
|
||||
let display = SampleDisplay { channel: opts.channel, colorized: opts.colorized, exposure: opts.exposure, conversion: dc.clone() };
|
||||
let region = shown_region(ctx, [ectx.comp.width as f32, ectx.comp.height as f32]);
|
||||
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
|
||||
if snap {
|
||||
let Some(s) = app.session.snapshot.clone() else { return };
|
||||
@@ -415,6 +418,12 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter:
|
||||
}
|
||||
};
|
||||
painter.image(tex.id(), comp_rect, uv, Color32::WHITE);
|
||||
note_sample_source(
|
||||
ctx,
|
||||
SampleSource::Snapshot(s.image.clone()),
|
||||
[0.0, 0.0, ectx.comp.width as f64, ectx.comp.height as f64],
|
||||
(!plain).then_some(display),
|
||||
);
|
||||
painter.text(comp_rect.left_top() + vec2(6.0, 6.0), Align2::LEFT_TOP, crate::i18n::tr("Snapshot"), Tokens::ui(11.0), SNAP_COLOR);
|
||||
return;
|
||||
}
|
||||
@@ -428,6 +437,9 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter:
|
||||
ctx.request_repaint();
|
||||
}
|
||||
painter.image(id, comp_rect, uv, Color32::WHITE);
|
||||
if let Some((_, key)) = app.viewer_shown {
|
||||
note_sample_source(ctx, SampleSource::Gpu(key), region, None);
|
||||
}
|
||||
return;
|
||||
}
|
||||
comp_rect
|
||||
@@ -444,6 +456,9 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter:
|
||||
if plain {
|
||||
let uv = app.viewer_image.as_ref().map_or(uv, |src| fitted_uv(ctx, src.size, minify));
|
||||
painter.image(tex.id(), rect, uv, Color32::WHITE);
|
||||
if let Some(image) = app.viewer_image.clone() {
|
||||
note_sample_source(ctx, SampleSource::Cpu(image), region, None);
|
||||
}
|
||||
return;
|
||||
}
|
||||
rect
|
||||
@@ -470,6 +485,71 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter:
|
||||
}
|
||||
};
|
||||
painter.image(tex.id(), rect, fitted_uv(ctx, src.size, minify), Color32::WHITE);
|
||||
note_sample_source(ctx, SampleSource::Cpu(src), region, Some(display));
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum SampleSource {
|
||||
Cpu(std::sync::Arc<egui::ColorImage>),
|
||||
Gpu(crate::frames::FrameKey),
|
||||
Snapshot(std::sync::Arc<effectcraft_engine::render::Image>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SampleDisplay {
|
||||
channel: Channel,
|
||||
colorized: bool,
|
||||
exposure: f32,
|
||||
conversion: Option<vw::DisplayColor>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DisplayedSample {
|
||||
pass: u64,
|
||||
source: SampleSource,
|
||||
region: [f64; 4],
|
||||
display: Option<SampleDisplay>,
|
||||
}
|
||||
|
||||
fn sample_source_id() -> egui::Id {
|
||||
egui::Id::unique("viewer-displayed-sample-source")
|
||||
}
|
||||
|
||||
pub(crate) fn clear_sample_source(ctx: &egui::Context) {
|
||||
ctx.data_mut(|data| data.remove::<DisplayedSample>(sample_source_id()));
|
||||
}
|
||||
|
||||
fn note_sample_source(ctx: &egui::Context, source: SampleSource, region: [f64; 4], display: Option<SampleDisplay>) {
|
||||
let sample = DisplayedSample { pass: ctx.cumulative_pass_nr(), source, region, display };
|
||||
ctx.data_mut(|data| data.insert_temp(sample_source_id(), sample));
|
||||
}
|
||||
|
||||
/// Sample only a frame actually drawn this pass, with that draw's channel, exposure and
|
||||
/// display conversion. Hidden and wireframe views deliberately have no sample source.
|
||||
pub(crate) fn sample_displayed(app: &EffectcraftApp, ctx: &egui::Context, point: [f64; 2]) -> Option<[f32; 4]> {
|
||||
let sample = ctx.data(|data| data.get_temp::<DisplayedSample>(sample_source_id()))?;
|
||||
if sample.pass != ctx.cumulative_pass_nr() {
|
||||
return None;
|
||||
}
|
||||
let pixel = match sample.source {
|
||||
SampleSource::Cpu(image) => super::fx_widgets::frame_pixel(&image, sample.region, point)?,
|
||||
SampleSource::Gpu(key) => {
|
||||
if app.viewer_shown.map(|(_, shown)| shown) != Some(key) {
|
||||
return None;
|
||||
}
|
||||
super::fx_widgets::frame_pixel(app.viewer_image.as_ref()?, sample.region, point)?
|
||||
}
|
||||
SampleSource::Snapshot(image) => {
|
||||
let index = super::fx_widgets::frame_pixel_index([image.width as usize, image.height as usize], sample.region, point)?;
|
||||
crate::frames::display_pixel(*image.data.get(index)?)
|
||||
}
|
||||
};
|
||||
let image = egui::ColorImage::new([1, 1], vec![pixel]);
|
||||
let image = match sample.display {
|
||||
Some(display) => transformed(&image, display.channel, display.colorized, display.exposure, display.conversion),
|
||||
None => image,
|
||||
};
|
||||
super::fx_widgets::sample_frame(&image, [0.0, 0.0, 1.0, 1.0], [0.5, 0.5])
|
||||
}
|
||||
|
||||
/// The texture coordinates of a frame of `size` in its texture, fitted with at least `minify`
|
||||
|
||||
Reference in New Issue
Block a user