mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
Merge PR #333
This commit is contained in:
6 files changed
+123
-64
No files matched your search
@@ -317,20 +317,6 @@ impl Session {
|
||||
journal.snapshot(&self.catalog)?;
|
||||
}
|
||||
let mut settings = SettingsLoad::default();
|
||||
// presets
|
||||
if let Some(f) = settings.read::<PresetsFile>(files.as_mut(), "presets.json") {
|
||||
for p in &mut self.presets {
|
||||
p.favorite = p.builtin && f.favorites.contains(&p.id) || (!p.builtin && p.favorite);
|
||||
}
|
||||
for u in f.user {
|
||||
if !self.presets.iter().any(|p| p.id == u.id) {
|
||||
self.presets.push(u);
|
||||
}
|
||||
}
|
||||
let known = |id: &String| crate::presets::profile(id).is_some();
|
||||
self.profile_favorites = f.profile_favorites.into_iter().filter(known).collect();
|
||||
self.profile_recent = f.profile_recent.into_iter().filter(known).take(crate::presets::RECENT_PROFILES).collect();
|
||||
}
|
||||
// preferences
|
||||
let prefs = settings.read::<PrefsFile>(files.as_mut(), "prefs.json").unwrap_or_default();
|
||||
self.xmp = prefs.xmp;
|
||||
@@ -352,6 +338,20 @@ impl Session {
|
||||
if let Some(d) = &self.smart_previews_dir {
|
||||
self.media.smart_dir = Some(d.clone());
|
||||
}
|
||||
// presets
|
||||
if let Some(f) = settings.read::<PresetsFile>(files.as_mut(), "presets.json") {
|
||||
for p in &mut self.presets {
|
||||
p.favorite = p.builtin && f.favorites.contains(&p.id) || (!p.builtin && p.favorite);
|
||||
}
|
||||
for u in f.user {
|
||||
if !self.presets.iter().any(|p| p.id == u.id) {
|
||||
self.presets.push(u);
|
||||
}
|
||||
}
|
||||
let known = |id: &String| crate::presets::profile(id).is_some() || self.lut_profiles.iter().any(|p| &p.id == id);
|
||||
self.profile_favorites = f.profile_favorites.into_iter().filter(known).collect();
|
||||
self.profile_recent = f.profile_recent.into_iter().filter(known).take(crate::presets::RECENT_PROFILES).collect();
|
||||
}
|
||||
// view state
|
||||
if let Some(v) = settings.read::<ViewFile>(files.as_mut(), "view.json") {
|
||||
self.source = v.source;
|
||||
|
||||
@@ -446,6 +446,17 @@ pub fn profile(id: &str) -> Option<&'static ProfileInfo> {
|
||||
pub const RECENT_PROFILES: usize = 5;
|
||||
|
||||
impl Session {
|
||||
/// A profile by id, looking up built-in profiles first and imported LUT profiles second.
|
||||
pub fn profile_info(&self, id: &str) -> Option<(&str, &str)> {
|
||||
if let Some(p) = profile(id) {
|
||||
return Some((p.name, p.group));
|
||||
}
|
||||
self.lut_profiles
|
||||
.iter()
|
||||
.find(|p| p.id == id)
|
||||
.map(|p| (p.name.as_str(), p.group.as_str()))
|
||||
}
|
||||
|
||||
/// Remember `id` as the most recently applied profile.
|
||||
pub fn note_profile_used(&mut self, id: &str) {
|
||||
self.profile_recent.retain(|p| p != id);
|
||||
|
||||
@@ -544,13 +544,17 @@ fn cube_luts_become_profiles() {
|
||||
mean(&warm, 2)
|
||||
);
|
||||
s.execute("develop.profile", &json!({"id": id, "amount": 0})).unwrap();
|
||||
assert_eq!(s.render_now(photo, 48, 32).unwrap().image.data, base.data, "Amount 0 = the photo as it was");
|
||||
assert_eq!(s.profile_info(&id), Some(("Warm Test", "Film Looks")));
|
||||
s.execute("profile.favorite", &json!({"id": id})).unwrap();
|
||||
assert!(s.profile_favorites.contains(&id));
|
||||
// the library keeps it
|
||||
drop(s);
|
||||
lightcraft_pipeline::lut::unregister(&id);
|
||||
let mut s = Session::new().with_fs();
|
||||
s.open_library(&lib, false).unwrap();
|
||||
assert!(s.lut_profiles.iter().any(|p| p.id == id));
|
||||
assert!(s.profile_favorites.contains(&id), "imported LUT favorite survives reopen");
|
||||
assert_eq!(s.profile_info(&id), Some(("Warm Test", "Film Looks")));
|
||||
assert!(lightcraft_pipeline::lut::get(&id).is_some(), "registered again on open");
|
||||
s.execute("profile.deleteImported", &json!({"id": id})).unwrap();
|
||||
assert!(lightcraft_pipeline::lut::get(&id).is_none());
|
||||
|
||||
@@ -139,7 +139,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 14, bottom: 14 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Profile")).font(t.font(13.0)).color(t.text_dim));
|
||||
let name = lightcraft_engine::presets::profile(&d.profile.id).map(|p| p.name).unwrap_or("Color");
|
||||
let name = app.session.profile_info(&d.profile.id).map(|(name, _)| name).unwrap_or("Color");
|
||||
let r = crate::widgets::dropdown(ui, "profile", crate::i18n::tr(name), t.font(15.0), t.text_label);
|
||||
egui::Popup::menu(&r).show(|ui| profile_menu(app, ui, &d));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
@@ -345,36 +345,37 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
/// The profile dropdown: Favorites, Recent, one submenu per group, then favourite toggle and
|
||||
/// Browse….
|
||||
fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings) {
|
||||
use lightcraft_engine::presets::{PROFILES, profile, profile_groups};
|
||||
use lightcraft_engine::presets::{PROFILES, profile_groups};
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.set_min_width(200.0);
|
||||
let cur = d.profile.id.as_str();
|
||||
// (clicked, hovered)
|
||||
let mut pick: (Option<&'static str>, Option<&'static str>) = (None, None);
|
||||
let item = |ui: &mut egui::Ui,
|
||||
key: &str,
|
||||
p: &'static lightcraft_engine::presets::ProfileInfo,
|
||||
pick: &mut (Option<&'static str>, Option<&'static str>)| {
|
||||
let r = ui.selectable_label(p.id == cur, crate::i18n::tr(p.name));
|
||||
register(ui.ctx(), format!("profileMenu:{key}:{}", p.id), r.rect);
|
||||
let mut pick: (Option<String>, Option<String>) = (None, None);
|
||||
let mut item = |ui: &mut egui::Ui,
|
||||
key: &str,
|
||||
id: &str,
|
||||
name: &str,
|
||||
pick: &mut (Option<String>, Option<String>)| {
|
||||
let r = ui.selectable_label(id == cur, crate::i18n::tr(name));
|
||||
register(ui.ctx(), format!("profileMenu:{key}:{id}"), r.rect);
|
||||
if r.clicked() {
|
||||
pick.0 = Some(p.id);
|
||||
pick.0 = Some(id.to_string());
|
||||
}
|
||||
if r.hovered() {
|
||||
pick.1 = Some(p.id);
|
||||
pick.1 = Some(id.to_string());
|
||||
}
|
||||
};
|
||||
let heading = |ui: &mut egui::Ui, s: &str| {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr(s)).font(t.semibold(11.5)).color(t.text_dim));
|
||||
};
|
||||
for (title, key, ids) in [("Favorites", "fav", app.session.profile_favorites.clone()), ("Recent", "recent", app.session.profile_recent.clone())] {
|
||||
let list: Vec<_> = ids.iter().filter_map(|id| profile(id)).collect();
|
||||
let list: Vec<_> = ids.iter().filter_map(|id| app.session.profile_info(id).map(|(name, _)| (id.as_str(), name))).collect();
|
||||
if list.is_empty() {
|
||||
continue;
|
||||
}
|
||||
heading(ui, title);
|
||||
for p in list {
|
||||
item(ui, key, p, &mut pick);
|
||||
for (id, name) in list {
|
||||
item(ui, key, id, name, &mut pick);
|
||||
}
|
||||
ui.separator();
|
||||
}
|
||||
@@ -382,23 +383,36 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
||||
let r = ui.menu_button(crate::i18n::tr(g), |ui| {
|
||||
ui.set_min_width(170.0);
|
||||
for p in PROFILES.iter().filter(|p| p.group == g) {
|
||||
item(ui, "group", p, &mut pick);
|
||||
item(ui, "group", p.id, p.name, &mut pick);
|
||||
}
|
||||
});
|
||||
register(ui.ctx(), format!("profileMenu:groupMenu:{g}"), r.response.rect);
|
||||
}
|
||||
// imported LUT profile groups
|
||||
let mut lut_groups: Vec<String> = app.session.lut_profiles.iter().map(|p| p.group.clone()).collect();
|
||||
lut_groups.sort_unstable();
|
||||
lut_groups.dedup();
|
||||
for g in &lut_groups {
|
||||
let r = ui.menu_button(crate::i18n::tr(g), |ui| {
|
||||
ui.set_min_width(170.0);
|
||||
for p in app.session.lut_profiles.iter().filter(|p| &p.group == g) {
|
||||
item(ui, "group", &p.id, &p.name, &mut pick);
|
||||
}
|
||||
});
|
||||
register(ui.ctx(), format!("profileMenu:groupMenu:{g}"), r.response.rect);
|
||||
}
|
||||
ui.separator();
|
||||
if let Some(p) = profile(cur) {
|
||||
let fav = app.session.profile_favorites.iter().any(|f| f == p.id);
|
||||
if let Some((name, _)) = app.session.profile_info(cur) {
|
||||
let fav = app.session.profile_favorites.iter().any(|f| f == cur);
|
||||
let label = if fav {
|
||||
crate::i18n::tr_format!("Remove “{}” from Favorites", crate::i18n::tr(p.name))
|
||||
crate::i18n::tr_format!("Remove “{}” from Favorites", crate::i18n::tr(name))
|
||||
} else {
|
||||
crate::i18n::tr_format!("Add “{}” to Favorites", crate::i18n::tr(p.name))
|
||||
crate::i18n::tr_format!("Add “{}” to Favorites", crate::i18n::tr(name))
|
||||
};
|
||||
let r = ui.button(label);
|
||||
register(ui.ctx(), "profileMenu:toggleFavorite", r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("profile.favorite", json!({"id": p.id}));
|
||||
let _ = app.run("profile.favorite", json!({"id": cur}));
|
||||
}
|
||||
}
|
||||
let r = ui.button(crate::i18n::tr("Browse…"));
|
||||
@@ -407,13 +421,14 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
||||
let _ = app.run("panel.profiles", json!({}));
|
||||
}
|
||||
// resting on a profile previews it in the loupe
|
||||
if let Some(p) = pick.1.and_then(profile)
|
||||
&& p.id != cur
|
||||
if let Some(hovered_id) = &pick.1
|
||||
&& hovered_id != cur
|
||||
&& let Some((name, _)) = app.session.profile_info(hovered_id)
|
||||
{
|
||||
let mut s = d.clone();
|
||||
s.profile.id = p.id.to_string();
|
||||
s.profile.id = hovered_id.clone();
|
||||
s.profile.amount = 100.0;
|
||||
app.hover_preview = Some(crate::HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(p.name)), settings: s });
|
||||
app.hover_preview = Some(crate::HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(name)), settings: s });
|
||||
}
|
||||
if let Some(id) = pick.0 {
|
||||
let _ = app.run("develop.profile", json!({"id": id}));
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2};
|
||||
use lightcraft_catalog::PhotoId;
|
||||
use lightcraft_develop::DevelopSettings;
|
||||
use lightcraft_engine::presets::{PROFILES, ProfileInfo, profile, profile_groups};
|
||||
use lightcraft_engine::presets::{PROFILES, profile_groups};
|
||||
use serde_json::json;
|
||||
|
||||
use super::presets::cover_uv;
|
||||
@@ -33,7 +33,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
divider(ui);
|
||||
// the applied profile and its amount
|
||||
let (cr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::hover());
|
||||
let name = profile(&d.profile.id).map(|p| p.name).unwrap_or("Color");
|
||||
let name = app.session.profile_info(&d.profile.id).map(|(name, _)| name).unwrap_or("Color");
|
||||
ui.painter().text(pos2(cr.left() + 24.0, cr.center().y), Align2::LEFT_CENTER, crate::i18n::tr("Profile"), t.font(13.0), t.text_dim);
|
||||
ui.painter().text(pos2(cr.left() + 84.0, cr.center().y), Align2::LEFT_CENTER, crate::i18n::tr(name), t.font(14.0), t.text_label);
|
||||
if d.profile.id != "lc.color" {
|
||||
@@ -41,13 +41,32 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
divider(ui);
|
||||
let mut sections: Vec<(&str, &str, Vec<&'static ProfileInfo>)> = Vec::new();
|
||||
let favs: Vec<&'static ProfileInfo> = app.session.profile_favorites.iter().filter_map(|f| profile(f)).collect();
|
||||
let mut sections: Vec<(&str, &str, Vec<(String, String, String)>)> = Vec::new();
|
||||
let favs: Vec<(String, String, String)> = app
|
||||
.session
|
||||
.profile_favorites
|
||||
.iter()
|
||||
.filter_map(|f| app.session.profile_info(f).map(|(name, group)| (f.clone(), name.to_string(), group.to_string())))
|
||||
.collect();
|
||||
if !favs.is_empty() {
|
||||
sections.push(("Favorites", "fav", favs));
|
||||
}
|
||||
for g in profile_groups() {
|
||||
sections.push((g, "", PROFILES.iter().filter(|p| p.group == g).collect()));
|
||||
sections.push((g, "", PROFILES.iter().filter(|p| p.group == g).map(|p| (p.id.to_string(), p.name.to_string(), p.group.to_string())).collect()));
|
||||
}
|
||||
// imported LUT profile groups
|
||||
let mut lut_groups: Vec<String> = app.session.lut_profiles.iter().map(|p| p.group.clone()).collect();
|
||||
lut_groups.sort_unstable();
|
||||
lut_groups.dedup();
|
||||
for g in &lut_groups {
|
||||
let profiles = app
|
||||
.session
|
||||
.lut_profiles
|
||||
.iter()
|
||||
.filter(|p| &p.group == g)
|
||||
.map(|p| (p.id.clone(), p.name.clone(), p.group.clone()))
|
||||
.collect();
|
||||
sections.push((g.as_str(), "", profiles));
|
||||
}
|
||||
for (title, tag, list) in sections {
|
||||
let open_id = egui::Id::new(("profile-group", title));
|
||||
@@ -87,34 +106,44 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
if app.hover_preview.is_none()
|
||||
&& let Some((saved_id, saved_profile_id)) = ui.data(|m| m.get_temp::<(PhotoId, String)>(last_hover_id))
|
||||
&& saved_id == id
|
||||
&& let Some(p) = profile(&saved_profile_id)
|
||||
&& let Some((p_name, _)) = app.session.profile_info(&saved_profile_id)
|
||||
{
|
||||
let s = with_profile(&d, p);
|
||||
app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(p.name)), settings: s });
|
||||
let s = with_profile_id(&d, &saved_profile_id);
|
||||
app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(p_name)), settings: s });
|
||||
}
|
||||
} else {
|
||||
ui.data_mut(|m| m.remove::<(PhotoId, String)>(last_hover_id));
|
||||
}
|
||||
}
|
||||
|
||||
/// `d` with profile `p` (at 100 % unless it is the applied one).
|
||||
fn with_profile(d: &DevelopSettings, p: &ProfileInfo) -> DevelopSettings {
|
||||
/// `d` with profile `profile_id` (at 100 % unless it is the applied one).
|
||||
fn with_profile_id(d: &DevelopSettings, profile_id: &str) -> DevelopSettings {
|
||||
let mut s = d.clone();
|
||||
if s.profile.id != p.id {
|
||||
s.profile.id = p.id.to_string();
|
||||
if s.profile.id != profile_id {
|
||||
s.profile.id = profile_id.to_string();
|
||||
s.profile.amount = 100.0;
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn cell_ui(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, id: PhotoId, tag: &str, p: &'static ProfileInfo, cell: Rect, th: f32) {
|
||||
fn cell_ui(
|
||||
app: &mut LightcraftApp,
|
||||
ui: &mut egui::Ui,
|
||||
d: &DevelopSettings,
|
||||
id: PhotoId,
|
||||
tag: &str,
|
||||
p: &(String, String, String),
|
||||
cell: Rect,
|
||||
th: f32,
|
||||
) {
|
||||
let (p_id, p_name, p_group) = (p.0.as_str(), p.1.as_str(), p.2.as_str());
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let key = if tag.is_empty() { format!("profileCell:{}", p.id) } else { format!("profileCell:{tag}:{}", p.id) };
|
||||
let key = if tag.is_empty() { format!("profileCell:{p_id}") } else { format!("profileCell:{tag}:{p_id}") };
|
||||
let resp = ui.interact(cell, egui::Id::new(&key), Sense::click());
|
||||
register(ui.ctx(), key, cell);
|
||||
let thumb = Rect::from_min_size(cell.min, vec2(cell.width(), th));
|
||||
let s = with_profile(d, p);
|
||||
let s = with_profile_id(d, p_id);
|
||||
let painter = ui.painter();
|
||||
painter.rect_filled(thumb, 3.0, t.canvas);
|
||||
if let Some(job) = app.session.variant_job(id, &s, THUMB_EDGE)
|
||||
@@ -122,33 +151,33 @@ fn cell_ui(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, id:
|
||||
{
|
||||
painter.image(tex.tex.id(), thumb, cover_uv(thumb, tex.size), Color32::WHITE);
|
||||
}
|
||||
let applied = d.profile.id == p.id;
|
||||
let applied = d.profile.id == p_id;
|
||||
if applied {
|
||||
painter.rect_stroke(thumb.expand(1.0), 4.0, Stroke::new(2.0, t.pick), egui::StrokeKind::Outside);
|
||||
} else if resp.hovered() {
|
||||
painter.rect_stroke(thumb, 3.0, Stroke::new(1.0, t.text_dim), egui::StrokeKind::Outside);
|
||||
}
|
||||
let name_color = if applied { t.text } else { t.text_label };
|
||||
painter.text(pos2(cell.left() + 2.0, thumb.bottom() + 12.0), Align2::LEFT_CENTER, crate::i18n::tr(p.name), t.font(12.0), name_color);
|
||||
painter.text(pos2(cell.left() + 2.0, thumb.bottom() + 12.0), Align2::LEFT_CENTER, crate::i18n::tr(p_name), t.font(12.0), name_color);
|
||||
// favourite star (always shown when set, on hover otherwise)
|
||||
let fav = app.session.profile_favorites.iter().any(|f| f == p.id);
|
||||
let fav = app.session.profile_favorites.iter().any(|f| f == p_id);
|
||||
let star = Rect::from_center_size(pos2(thumb.right() - 13.0, thumb.top() + 13.0), vec2(20.0, 20.0));
|
||||
register(ui.ctx(), format!("profileStar:{}{}", if tag.is_empty() { "" } else { "fav:" }, p.id), star);
|
||||
register(ui.ctx(), format!("profileStar:{}{}", if tag.is_empty() { "" } else { "fav:" }, p_id), star);
|
||||
let over_star = resp.hover_pos().is_some_and(|q| star.contains(q));
|
||||
if fav || resp.hovered() {
|
||||
painter.circle_filled(star.center(), 10.0, Color32::from_black_alpha(110));
|
||||
paint(painter, star.shrink(4.0), if fav { Icon::StarFilled } else { Icon::Star }, if fav || over_star { t.star } else { Color32::WHITE });
|
||||
}
|
||||
if resp.hovered() && !applied && !over_star {
|
||||
app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(p.name)), settings: s.clone() });
|
||||
ui.data_mut(|m| m.insert_temp(egui::Id::new("last-profile-hover"), (id, p.id.to_string())));
|
||||
app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(p_name)), settings: s.clone() });
|
||||
ui.data_mut(|m| m.insert_temp(egui::Id::new("last-profile-hover"), (id, p_id.to_string())));
|
||||
}
|
||||
let resp = resp.on_hover_text(format!("{} ({})", crate::i18n::tr(p.name), crate::i18n::tr(p.group)));
|
||||
let resp = resp.on_hover_text(format!("{} ({})", crate::i18n::tr(p_name), crate::i18n::tr(p_group)));
|
||||
if resp.clicked() {
|
||||
if resp.interact_pointer_pos().is_some_and(|q| star.contains(q)) {
|
||||
let _ = app.run("profile.favorite", json!({"id": p.id}));
|
||||
let _ = app.run("profile.favorite", json!({"id": p_id}));
|
||||
} else if !applied {
|
||||
let _ = app.run("develop.profile", json!({"id": p.id, "amount": 100}));
|
||||
let _ = app.run("develop.profile", json!({"id": p_id, "amount": 100}));
|
||||
ui.data_mut(|m| m.remove::<(PhotoId, String)>(egui::Id::new("last-profile-hover")));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -291,7 +291,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| LR-PROF-LEGACY | Legacy profiles | P2 | ⬜ | | |
|
||||
| LR-PROF-NONRAW | Profiles for non-raw files | P0 | ✅ | `cmd:develop.profile` | same looks apply to JPEG/TIFF |
|
||||
| LR-PROF-AMOUNT | Profile amount | P1 | ✅ | `ctl:profile.amount` | |
|
||||
| LR-PROF-IMPORT | Import profiles | P1 | 🟡 | `cmd:profile.import`, `cmd:profile.deleteImported`, `crates/pipeline/src/lut.rs`, `crates/engine/src/cmd/lut_profiles.rs` | `.cube` 3D LUTs (files, folders, zips) become creative profiles with Amount, grouped in the profile browser, kept with the library; rendered on the CPU; Adobe profile formats deliberately unsupported |
|
||||
| LR-PROF-IMPORT | Import profiles | P1 | ✅ | `cmd:profile.import`, `cmd:profile.deleteImported`, `crates/pipeline/src/lut.rs`, `crates/engine/src/cmd/lut_profiles.rs`, `crates/ui-egui/src/panels/edit.rs`, `crates/ui-egui/src/panels/profiles.rs` | `.cube` 3D LUTs (files, folders, zips) become creative profiles with Amount, grouped in the profile browser, kept with the library; rendered on the CPU; Adobe profile formats deliberately unsupported |
|
||||
|
||||
## H. Crop & rotate (CROP)
|
||||
|
||||
|
||||
Reference in new issue
Block a user