mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-10 12:22:22 +00:00
commit
eb83e5cc22
6 files changed
+197
-11
No files matched your search
@@ -890,7 +890,7 @@ fn ui_set_drives_the_layers_panel_artboard_filter() {
|
||||
assert_eq!(app.ui.layer_filter_by, FilterBy::Artboard);
|
||||
let bad = call(&mut app, &ctx, "ui.set", json!({"layerFilter": {"by": "artboards"}}));
|
||||
assert_eq!(bad["ok"], false, "{bad}");
|
||||
assert!(bad.to_string().contains("kind, name, effect, mode, attribute, color, smartObject or artboard"), "{bad}");
|
||||
assert!(bad.to_string().contains("kind, name, effect, mode, attribute, color, smartObject, artboard or group"), "{bad}");
|
||||
assert_eq!(app.ui.layer_filter_by, FilterBy::Artboard, "a bad value changed nothing");
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! picks one effect kind (#10880), Mode picks one blend mode, Attribute one layer attribute or
|
||||
//! its opposite (#10881), Color one layer colour label (#5898), Smart Object toggles linked,
|
||||
//! missing and embedded smart objects, Artboard lists only the artboards that hold the selected
|
||||
//! layers. The criteria live in `UiState` (`layer_filter*`), so the control channel can read and
|
||||
//! set them.
|
||||
//! layers, Group every group with its contents (#10883). The criteria live in `UiState`
|
||||
//! (`layer_filter*`), so the control channel can read and set them.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -28,6 +28,7 @@ pub enum FilterBy {
|
||||
Color,
|
||||
SmartObject,
|
||||
Artboard,
|
||||
Group,
|
||||
}
|
||||
|
||||
/// The filter criteria, copied out of `UiState` for one frame of the Layers panel.
|
||||
@@ -46,7 +47,8 @@ pub struct Filter {
|
||||
/// The linked files the Smart Object filter's Missing toggle lists
|
||||
/// ([`photocraft_engine::Session::missing_links`]).
|
||||
pub missing: HashSet<String>,
|
||||
/// The rows the Artboard filter lists ([`board_rows`]); empty lists every row.
|
||||
/// The rows the Artboard filter ([`board_rows`]) or the Group filter ([`group::listed`])
|
||||
/// lists; empty lists every row under Artboard and none under Group.
|
||||
pub board_rows: HashSet<LayerId>,
|
||||
/// The empty layers the Attribute filter's Empty and Not Empty use
|
||||
/// ([`attribute::empty_layers`]); `None` reads each layer's pixels.
|
||||
@@ -83,6 +85,9 @@ impl Filter {
|
||||
if f.by == FilterBy::Artboard {
|
||||
f.board_rows = board_rows(doc, selection);
|
||||
}
|
||||
if f.by == FilterBy::Group {
|
||||
f.board_rows = group::listed(doc);
|
||||
}
|
||||
if f.by == FilterBy::Attribute {
|
||||
f.empty = attribute::empty_layers(app, f.attribute);
|
||||
}
|
||||
@@ -99,7 +104,7 @@ impl Filter {
|
||||
}
|
||||
|
||||
/// Whether the panel is filtered. Like Photoshop, Effect, Mode, Attribute and Color always
|
||||
/// filter (by Bevel & Emboss, by Normal, by Visible and by no colour at first); Kind, Name and
|
||||
/// filter (by Bevel & Emboss, by Normal, by Visible and by no colour at first), and so does Group; Kind, Name and
|
||||
/// Smart Object only once a toggle is on or some text is typed; Artboard only while some layers
|
||||
/// are selected and each of them is in an artboard.
|
||||
pub fn active(&self) -> bool {
|
||||
@@ -109,9 +114,16 @@ impl Filter {
|
||||
FilterBy::Effect | FilterBy::Mode | FilterBy::Attribute | FilterBy::Color => true,
|
||||
FilterBy::SmartObject => !self.smart.is_empty(),
|
||||
FilterBy::Artboard => !self.board_rows.is_empty(),
|
||||
FilterBy::Group => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the panel lists the contents of closed groups: while it is filtered, except by
|
||||
/// Group, which lists whole groups and keeps each one open or closed as it is (#10883).
|
||||
pub fn show_all(&self) -> bool {
|
||||
self.active() && self.by != FilterBy::Group
|
||||
}
|
||||
|
||||
/// Whether `l` is listed under this filter.
|
||||
pub fn shows(&self, l: &Layer) -> bool {
|
||||
if !self.active() {
|
||||
@@ -133,6 +145,7 @@ impl Filter {
|
||||
})
|
||||
}
|
||||
FilterBy::Artboard => self.board_rows.contains(&l.id),
|
||||
FilterBy::Group => self.board_rows.contains(&l.id),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,12 +193,12 @@ pub const SMART: [&str; 3] = ["linked", "missing", "embedded"];
|
||||
|
||||
/// `ui.set {layerFilter: {by?, kinds?, name?, effect?, mode?, attribute?, color?, smart?}}`:
|
||||
/// `current` with the given fields replaced, or an error naming the bad field (nothing changes
|
||||
/// then). `by` is `kind`, `name`, `effect`, `mode`, `attribute`, `color`, `smartObject` or
|
||||
/// `artboard`; `kinds` a list of [`KINDS`]; `effect` an [`effect::EffectKind`] id; `mode` one of
|
||||
/// then). `by` is `kind`, `name`, `effect`, `mode`, `attribute`, `color`, `smartObject`,
|
||||
/// `artboard` or `group`; `kinds` a list of [`KINDS`]; `effect` an [`effect::EffectKind`] id; `mode` one of
|
||||
/// the layer blend modes such as `Multiply` (not Pass Through, which the Mode list doesn't offer,
|
||||
/// #10878); `attribute` an [`attribute::Attribute`] id; `color` a label colour id (`none`, `red`,
|
||||
/// … `gray`, [`LabelColor::id`]); `smart` a list of [`SMART`]. Artboard has no criteria of its
|
||||
/// own: the selected layers pick the artboards.
|
||||
/// own: the selected layers pick the artboards; nor has Group.
|
||||
pub fn patched(current: &UiState, v: &serde_json::Value) -> Result<Filter, String> {
|
||||
let Some(o) = v.as_object() else { return Err("layerFilter must be an object".into()) };
|
||||
if let Some(k) = o.keys().find(|k| !["by", "kinds", "name", "effect", "mode", "attribute", "color", "smart"].contains(&k.as_str())) {
|
||||
@@ -194,7 +207,7 @@ pub fn patched(current: &UiState, v: &serde_json::Value) -> Result<Filter, Strin
|
||||
let mut f = Filter::of(current);
|
||||
if let Some(by) = o.get("by") {
|
||||
f.by = serde_json::from_value(by.clone())
|
||||
.map_err(|_| "layerFilter.by must be kind, name, effect, mode, attribute, color, smartObject or artboard".to_string())?;
|
||||
.map_err(|_| "layerFilter.by must be kind, name, effect, mode, attribute, color, smartObject, artboard or group".to_string())?;
|
||||
}
|
||||
if let Some(kinds) = o.get("kinds") {
|
||||
let list = kinds.as_array().ok_or("layerFilter.kinds must be a list")?;
|
||||
@@ -287,6 +300,7 @@ pub fn bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
(FilterBy::Color, tl!("Color")),
|
||||
(FilterBy::SmartObject, tl!("Smart Object")),
|
||||
(FilterBy::Artboard, tl!("Artboard")),
|
||||
(FilterBy::Group, tl!("Group")),
|
||||
];
|
||||
widgets::dropdown(ui, "layer-filter-by", tl!("Kind"), &mut app.ui.layer_filter_by, &by, 64.0);
|
||||
ui.add_space(4.0);
|
||||
@@ -347,12 +361,15 @@ pub fn bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
// Artboard has no controls: the selected layers pick the artboards ([`board_rows`]).
|
||||
FilterBy::Artboard => {}
|
||||
// Group has no controls either: it lists every group ([`group::listed`]).
|
||||
FilterBy::Group => {}
|
||||
}
|
||||
});
|
||||
selection::follow(app, ui.ctx());
|
||||
}
|
||||
|
||||
pub mod effect;
|
||||
mod group;
|
||||
pub mod selection;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -369,3 +386,6 @@ mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod kind_mode_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod group_tests;
|
||||
@@ -0,0 +1,22 @@
|
||||
//! The Layers panel's Group filter (#10883): every group with its whole subtree; layers in no
|
||||
//! group are hidden. An artboard row is not a group here, but the groups inside it are.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use photocraft_doc::{Document, Layer, LayerContent, LayerId};
|
||||
|
||||
/// The ids of the layers the Group filter lists. The tree is walked once, with a stack rather
|
||||
/// than recursion, so a deep document can't overflow it.
|
||||
pub fn listed(doc: &Document) -> HashSet<LayerId> {
|
||||
let mut out = HashSet::new();
|
||||
let mut pending: Vec<(&Layer, bool)> = doc.layers.iter().map(|l| (l, false)).collect();
|
||||
while let Some((l, inside)) = pending.pop() {
|
||||
let artboard = matches!(&l.content, LayerContent::Group(g) if g.artboard.is_some());
|
||||
let inside = inside || (l.is_group() && !artboard);
|
||||
if inside {
|
||||
out.insert(l.id);
|
||||
}
|
||||
pending.extend(l.children().unwrap_or_default().iter().map(|c| (c, inside)));
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//! #10883: the Layers panel's Group filter.
|
||||
|
||||
use photocraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{Filter, FilterBy};
|
||||
use crate::PhotocraftApp;
|
||||
use crate::state::UiState;
|
||||
|
||||
fn new_session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width": 64, "height": 48})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// The app with `s` open and the Layers panel filtered by Group.
|
||||
fn app(s: Session) -> PhotocraftApp {
|
||||
let mut app = PhotocraftApp::new(s, crate::Services::default());
|
||||
app.ui.layer_filter_by = FilterBy::Group;
|
||||
app
|
||||
}
|
||||
|
||||
fn id(s: &Session, name: &str) -> u64 {
|
||||
s.active().unwrap().doc.walk().into_iter().find(|(_, _, l)| l.name == name).map(|(_, _, l)| l.id.0).unwrap_or_else(|| panic!("no layer {name}"))
|
||||
}
|
||||
|
||||
fn select(s: &mut Session, name: &str) {
|
||||
let layer = id(s, name);
|
||||
s.execute("layer.select", json!({"layer": layer})).unwrap();
|
||||
}
|
||||
|
||||
/// A new layer above the one selected (inside its group, if it is in one).
|
||||
fn add(s: &mut Session, name: &str) {
|
||||
s.execute("layer.new.layer", json!({"name": name})).unwrap();
|
||||
}
|
||||
|
||||
/// Groups the layer `name`.
|
||||
fn group(s: &mut Session, name: &str, group_name: &str) {
|
||||
select(s, name);
|
||||
s.execute("layer.groupLayers", json!({"name": group_name})).unwrap();
|
||||
}
|
||||
|
||||
fn set_expanded(s: &mut Session, name: &str, expanded: bool) {
|
||||
let layer = id(s, name);
|
||||
s.execute("layer.setExpanded", json!({"layer": layer, "expanded": expanded})).unwrap();
|
||||
}
|
||||
|
||||
/// The rows the panel draws, top to bottom, two spaces per level: the rows and the filter the
|
||||
/// Layers panel uses for the frame.
|
||||
fn listed(app: &PhotocraftApp) -> Vec<String> {
|
||||
let doc = &app.session.active().unwrap().doc;
|
||||
let f = Filter::of_app(app);
|
||||
crate::layer_tree_ui::panel_rows(app, doc).into_iter().filter(|(_, l)| f.shows(l)).map(|(depth, l)| format!("{}{}", " ".repeat(depth), l.name)).collect()
|
||||
}
|
||||
|
||||
/// The selected layers' names.
|
||||
fn selected(app: &PhotocraftApp) -> Vec<String> {
|
||||
let st = app.session.active().unwrap();
|
||||
st.selected_layers().iter().filter_map(|id| st.doc.layer(*id)).map(|l| l.name.clone()).collect()
|
||||
}
|
||||
|
||||
/// Top Pixel · Outer { Inner { Deep } } · Background.
|
||||
fn nested() -> Session {
|
||||
let mut s = new_session();
|
||||
add(&mut s, "Top Pixel");
|
||||
add(&mut s, "Deep");
|
||||
group(&mut s, "Deep", "Inner");
|
||||
group(&mut s, "Inner", "Outer");
|
||||
s.execute("layer.setExpanded", json!({"all": true, "expanded": true})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists_nested_groups_with_their_contents_and_hides_loose_layers() {
|
||||
let app = app(nested());
|
||||
assert!(Filter::of_app(&app).active(), "Group filters at once");
|
||||
assert_eq!(listed(&app), ["Outer", " Inner", " Deep"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collapsed_groups_stay_collapsed() {
|
||||
let mut app = app(nested());
|
||||
set_expanded(&mut app.session, "Inner", false);
|
||||
assert_eq!(listed(&app), ["Outer", " Inner"], "a collapsed nested group hides its children");
|
||||
set_expanded(&mut app.session, "Outer", false);
|
||||
assert_eq!(listed(&app), ["Outer"], "a collapsed group hides everything inside");
|
||||
set_expanded(&mut app.session, "Outer", true);
|
||||
assert_eq!(listed(&app), ["Outer", " Inner"], "each group keeps its own state");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_artboard_is_not_listed_but_its_groups_are() {
|
||||
let mut s = new_session();
|
||||
add(&mut s, "G child");
|
||||
group(&mut s, "G child", "Plain G");
|
||||
select(&mut s, "Background");
|
||||
add(&mut s, "A child");
|
||||
s.execute("layer.new.artboardFromLayers", json!({"name": "Artboard 1"})).unwrap();
|
||||
select(&mut s, "A child");
|
||||
add(&mut s, "GA child");
|
||||
group(&mut s, "GA child", "G in AB");
|
||||
s.execute("layer.setExpanded", json!({"all": true, "expanded": true})).unwrap();
|
||||
let mut app = app(s);
|
||||
assert_eq!(listed(&app), [" G in AB", " GA child", "Plain G", " G child"]);
|
||||
set_expanded(&mut app.session, "Artboard 1", false);
|
||||
assert_eq!(listed(&app), ["Plain G", " G child"], "a collapsed artboard hides the groups inside it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_list_does_not_depend_on_the_selection() {
|
||||
let mut app = app(nested());
|
||||
select(&mut app.session, "Deep");
|
||||
let with_deep = listed(&app);
|
||||
app.session.execute("select.deselectLayers", json!({})).unwrap();
|
||||
assert_eq!(listed(&app), with_deep, "same list with nothing selected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applying_it_keeps_a_selected_layer_in_a_group_and_deselects_a_loose_one() {
|
||||
let mut app = app(nested());
|
||||
select(&mut app.session, "Deep");
|
||||
super::selection::deselect_hidden(&mut app);
|
||||
assert_eq!(selected(&app), ["Deep"], "a listed layer stays selected");
|
||||
select(&mut app.session, "Top Pixel");
|
||||
super::selection::deselect_hidden(&mut app);
|
||||
assert!(selected(&app).is_empty(), "a layer in no group is hidden, so it is deselected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_is_set_by_name_through_the_control_channel() {
|
||||
let ui = UiState::default();
|
||||
let f = super::patched(&ui, &json!({"by": "group"})).unwrap();
|
||||
assert_eq!(f.by, FilterBy::Group);
|
||||
let err = super::patched(&ui, &json!({"by": "nope"})).unwrap_err();
|
||||
assert!(err.contains("group"), "the error lists group: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists_empty_groups_and_nothing_without_groups() {
|
||||
let mut app = app(new_session());
|
||||
assert!(listed(&app).is_empty(), "no group: nothing is listed");
|
||||
app.session.execute("layer.new.group", json!({"name": "Empty Group"})).unwrap();
|
||||
assert_eq!(listed(&app), ["Empty Group"], "an empty group is listed as a group row");
|
||||
}
|
||||
@@ -40,7 +40,7 @@ pub fn display_rows(doc: &Document, show_all: bool) -> Vec<(usize, &Layer)> {
|
||||
/// Mask session's working text, which is no layer to the user: Photoshop shows only the mask
|
||||
/// overlay while you type (#3036).
|
||||
pub fn panel_rows<'a>(app: &PhotocraftApp, doc: &'a Document) -> Vec<(usize, &'a Layer)> {
|
||||
let mut rows = display_rows(doc, crate::layer_filter_ui::Filter::of(&app.ui).active());
|
||||
let mut rows = display_rows(doc, crate::layer_filter_ui::Filter::of(&app.ui).show_all());
|
||||
if let Some(working) = app.ui.text_edit.as_ref().filter(|ed| ed.mask).map(|ed| ed.layer) {
|
||||
rows.retain(|(_, l)| l.id.0 != working);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ The transport is `apps/photocraft/src/control_server.rs`, and the handlers are i
|
||||
- `ui.set` also accepts `camTransformOnDrop` (boolean), the Content-Aware Move tool's Transform On Drop checkbox: a drop then puts a scale/rotate box on the patch (`ui.inspect` › `camTransform`: its document/revision, drag `offset` and `frame`), driven with `ui.pointer` (handles as Free Transform's; outside the box rotates), committed with `ui.key Enter` (one `paint.contentAwareMove` with `scaleX`, `scaleY`, `rotate`) or dropped with `Escape`.
|
||||
- `ui.set` also accepts `tileGrid` (boolean), a developer overlay of the active layer's storage tiles: the tile grid over the document, every stored tile filled and labelled `tx,ty`, tiles with identical pixels in a shared colour and unique ones in blue. It has no menu item and View › Extras doesn't hide it. `ui.inspect` reports it as `view.tile_grid`.
|
||||
- `ui.set` also accepts `strokeTrace` (`true`, `false`, `"stderr"` or `"off"`): turn the stroke latency trace on or off at runtime; each stroke's summary is in `ui.inspect` `perf.strokeTrace`. A CSV file of every frame is set only at launch (`PHOTOCRAFT_STROKE_TRACE=<file.csv>` / `--stroke-trace <file.csv>`): the control channel doesn't write files outside the automation write root
|
||||
- `ui.set` also accepts `layerFilter` (#5048), the Layers panel filter row: `{by?, kinds?, name?, effect?, mode?, attribute?, color?, smart?}`, any subset. `by` is `kind`, `name`, `effect`, `mode`, `attribute`, `color`, `smartObject` or `artboard` (the filter-type dropdown); `kinds` lists the Kind toggles that are on (`pixel`, `adjustment`, `type`, `shape`, `smart`); `name` matches part of a layer name, ignoring case; `effect` (#10880) is `bevelEmboss`, `stroke`, `innerShadow`, `innerGlow`, `satin`, `overlay` (Color, Gradient or Pattern Overlay), `outerGlow` or `dropShadow`, and lists the layers with that effect turned on; `mode` is one of the 27 layer blend modes (`Multiply`, `Screen`), not Pass Through, which the Mode list doesn't offer (#10878); `attribute` (#10881) is `visible`, `locked`, `empty`, `linked`, `clipped`, `layerMask`, `vectorMask`, `layerEffects` or `advancedBlending`, or one of their opposites `notVisible`, `notLocked`, `notEmpty`, `notLinked`, `notClipped`, `noLayerMask`, `noVectorMask`, `noLayerEffects`, `noAdvancedBlending`; `color` is a layer colour label id (`none`, `red`, `orange`, `yellow`, `green`, `seafoam`, `blue`, `indigo`, `magenta`, `fuchsia`, `violet`, `gray`; #5898); `smart` lists the Smart Object toggles that are on (`linked`: linked to a file, `missing`: linked to a file that was not there when the app last looked, `embedded`). Kind, Name and Smart Object filter only once a toggle is on or text is typed; Effect, Mode, Attribute and Color always filter, by Bevel & Emboss, by Normal, by Visible and by no colour at first, as in Photoshop. Artboard has no field of its own: it lists the artboards that hold the selected layers, with everything inside them (closed groups stay closed), and does not filter when nothing is selected or a selected layer is in no artboard. Fields left out keep their value; a bad one is an error and changes nothing.
|
||||
- `ui.set` also accepts `layerFilter` (#5048), the Layers panel filter row: `{by?, kinds?, name?, effect?, mode?, attribute?, color?, smart?}`, any subset. `by` is `kind`, `name`, `effect`, `mode`, `attribute`, `color`, `smartObject`, `artboard` or `group` (the filter-type dropdown); `kinds` lists the Kind toggles that are on (`pixel`, `adjustment`, `type`, `shape`, `smart`); `name` matches part of a layer name, ignoring case; `effect` (#10880) is `bevelEmboss`, `stroke`, `innerShadow`, `innerGlow`, `satin`, `overlay` (Color, Gradient or Pattern Overlay), `outerGlow` or `dropShadow`, and lists the layers with that effect turned on; `mode` is one of the 27 layer blend modes (`Multiply`, `Screen`), not Pass Through, which the Mode list doesn't offer (#10878); `attribute` (#10881) is `visible`, `locked`, `empty`, `linked`, `clipped`, `layerMask`, `vectorMask`, `layerEffects` or `advancedBlending`, or one of their opposites `notVisible`, `notLocked`, `notEmpty`, `notLinked`, `notClipped`, `noLayerMask`, `noVectorMask`, `noLayerEffects`, `noAdvancedBlending`; `color` is a layer colour label id (`none`, `red`, `orange`, `yellow`, `green`, `seafoam`, `blue`, `indigo`, `magenta`, `fuchsia`, `violet`, `gray`; #5898); `smart` lists the Smart Object toggles that are on (`linked`: linked to a file, `missing`: linked to a file that was not there when the app last looked, `embedded`). Kind, Name and Smart Object filter only once a toggle is on or text is typed; Effect, Mode, Attribute and Color always filter, by Bevel & Emboss, by Normal, by Visible and by no colour at first, as in Photoshop. Artboard has no field of its own: it lists the artboards that hold the selected layers, with everything inside them (closed groups stay closed), and does not filter when nothing is selected or a selected layer is in no artboard. Group (#10883) has no field of its own either and filters at once: it lists every group that is not an artboard with everything inside it (the groups inside an artboard too), and hides the layers that are in no group; each group stays open or closed as it is. Fields left out keep their value; a bad one is an error and changes nothing.
|
||||
- `ui.set` also accepts the Histogram panel: `histogramPanel` `{view?, channel?, showChannelsInColor?, showStatistics?, source?}` (any subset, kept with the preferences): `view` is `compact`, `expanded` or `allChannels`; `channel` is `composite`, `red`, `green`, `blue`, `cyan`, `magenta`, `yellow`, `black`, `a`, `b`, `luminosity` or `colors` (one the document's mode lacks shows as its composite); `source` is `entireImage`, `selectedLayer` or `adjustmentComposite` (Entire Image is counted in Compact View, when the document has one layer, or when no adjustment layer is selected for Adjustment Composite). `histogramRange` is `[from, to]` (levels 0..255) or `null`: the dragged tonal range whose Count and Percentile the statistics show. `histogramRefresh: true` is Uncached Refresh: the current state is recounted at cache level 1. `ui.inspect` › `histogram` reports the options, the channel and source as they apply, the channels the document offers, whether the shown histogram is `current` (counted for the document's revision, source and cache level the panel wants now), its `cacheLevel`, `maxCacheLevel` (Preferences › Performance › Cache Levels), `preview` (an adjustment's preview is counted) and `stats` (`mean`, `stdDev`, `median`, `pixels`, `level`, `count`, `percentile`, `clipped` percent at 0 and 255). Histograms are counted while the panel is shown.
|
||||
- Each painting tool keeps its own brush, as in Photoshop: `tools.setBrush` and `ui.set {brushSize}` change the active tool's brush, and switching tools swaps in that tool's own (size, hardness, mode, opacity, flow, dynamics, smoothing). A tool used for the first time starts from the brush in hand. The colours, the erase flag and the stroke seed stay shared. So `ui.set {tool: "eraser", brushSize: 12}` sets the Eraser's size, and `ui.set {tool: "brush"}` afterwards brings the Brush's own size back.
|
||||
- Brush Pose's `pose.overrideTiltX` and `pose.overrideTiltY` are independent booleans. For example, `engine.execute {"command":"tools.setBrush","params":{"pose":{"enabled":true,"tiltX":45,"overrideTiltX":true,"overrideTiltY":false}}}` fixes X at 45° while Y follows the stylus. Pose tilts are degrees (±90); the Brush Settings panel displays percentages (±100). Old stored `overrideTilt` values migrate to both axes. In command patches, `overrideTilt` sets both axes unless an explicit axis field in the same patch overrides it. `brush.get` includes both axis flags and the legacy combined readback (`overrideTilt` is true only when both flags are true). Axis-only journals omit that derived combined key so replay retains the other axis. Present flag values must be booleans; null and other types return an error.
|
||||
|
||||
Reference in new issue
Block a user