Layers: sublayers, groups and objects are clickable rows in the panel

Rows at every depth highlight on click (Shift range, Ctrl toggle) and make
their layer current; selection squares select a row's art and drag the
selection to another row; eye and lock on every row with drag-down-the-column
in one undo step and Ctrl-click for Outline; Alt-click triangles; drag and drop
rows above, below or into layers and groups with a drop line (Alt copies);
Delete key; the full panel menu, Layer Options and Panel Options dialogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BFlat
2026-10-07 22:29:15 +07:00
co-authored by Claude Opus 5.5
parent 27a1a48176
commit 7eb3f25f1f
7 changed files with 1314 additions and 189 deletions
+283
View File
@@ -0,0 +1,283 @@
//! Layer Options (double-click a layer row, or ≡ › Options for Selection…) and the same dialog for
//! a new layer or sublayer (≡ › New Layer…, Alt-click Create New Layer). Objects' rows get Name,
//! Show and Lock only.
//!
//! Fields: `ids` (the rows edited; none for a new layer), `mode` (`edit`, `new` or `newSublayer`),
//! `layer` (the rows are layers), `name`, `color` (a preset's name such as "Light Blue", or
//! `#rrggbb`), `template`, `locked`, `visible`, `printable`, `preview`, `dimImages` and
//! `dimPercent`. OK runs `layer.setProps`, `layer.new` or `layer.newSublayer`.
use serde_json::{Value, json};
use vectorcraft_doc::{LAYER_COLORS, LayerColor, Node, NodeId, NodeKind};
use super::swatch_options::{grid, label};
use super::{DialogSpec, form, run_and_close};
use crate::state::Dialog;
use crate::{VectorcraftApp, widgets};
/// The dialog kind of Layer Options.
pub const KIND: &str = "layerOptions";
pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, min_width: 320.0, ..DialogSpec::FORM };
fn heading(d: &Dialog) -> String {
if d.bool("layer") || d.str("mode") != "edit" { tl!("Layer Options").into() } else { tl!("Options").into() }
}
/// A layer colour as the dialog shows it: a preset's name, else `#rrggbb`.
fn color_field(c: LayerColor) -> String {
match c {
LayerColor::Preset(i) => LAYER_COLORS.get(i as usize).map_or("Light Blue", |(n, _)| *n).to_string(),
LayerColor::Custom([r, g, b]) => format!("#{r:02x}{g:02x}{b:02x}"),
}
}
/// The fields for `n` (a layer or any other row).
fn fields_of(n: &Node) -> serde_json::Map<String, Value> {
let mut f = serde_json::Map::new();
f.insert("name".into(), json!(n.display_name()));
f.insert("visible".into(), json!(n.visible));
f.insert("locked".into(), json!(n.locked));
f.insert("layer".into(), json!(n.is_layer()));
if let NodeKind::Layer { color, template, printable, preview, dim_images, .. } = &n.kind {
f.insert("color".into(), json!(color_field(*color)));
f.insert("template".into(), json!(template));
f.insert("printable".into(), json!(printable));
f.insert("preview".into(), json!(preview));
f.insert("dimImages".into(), json!(dim_images.is_some()));
f.insert("dimPercent".into(), json!(dim_images.unwrap_or(50)));
}
f
}
/// `ui.layerOptions {ids?|id?}`: open Layer Options for rows (default: the highlighted rows, else
/// the current layer), filled in from the first.
pub fn open(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let st = app.session.active().ok_or("no document open")?;
let ids: Vec<NodeId> = match (p.get("ids").and_then(Value::as_array), p.get("id").and_then(Value::as_u64)) {
(Some(a), _) => a.iter().filter_map(Value::as_u64).map(NodeId).collect(),
(None, Some(id)) => vec![NodeId(id)],
(None, None) => {
let rows = st.highlighted_rows();
if rows.is_empty() { st.current_layer().into_iter().collect() } else { rows }
}
};
let first = ids.first().and_then(|id| st.doc.node(*id)).ok_or("no such layer or object")?;
let mut f = fields_of(first);
f.insert("ids".into(), json!(ids.iter().map(|i| i.0).collect::<Vec<_>>()));
f.insert("mode".into(), json!("edit"));
app.ui.dialog = Some(Dialog { kind: KIND.into(), fields: f });
Ok(Value::Null)
}
/// `ui.newLayer {sublayer?}`: Layer Options for a new layer (or sublayer), with the name and
/// colour it would get.
pub fn open_new(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let st = app.session.active().ok_or("no document open")?;
let mut layers = vec![];
st.doc.walk(|n| {
if n.is_layer() {
layers.push(n.display_name());
}
});
let mut i = layers.len() + 1;
while layers.contains(&format!("Layer {i}")) {
i += 1;
}
let sublayer = p.get("sublayer").and_then(Value::as_bool).unwrap_or(false);
let color = LayerColor::Preset((layers.len() % LAYER_COLORS.len()) as u8);
let fields = json!({
"mode": if sublayer { "newSublayer" } else { "new" }, "layer": true, "name": format!("Layer {i}"), "color": color_field(color),
"template": false, "locked": false, "visible": true, "printable": true, "preview": true, "dimImages": false, "dimPercent": 50,
});
app.ui.dialog = Some(Dialog::new(KIND, fields));
Ok(Value::Null)
}
fn toggle(ui: &mut egui::Ui, d: &mut Dialog, key: &str, text: &str, enabled: bool) -> bool {
let v = d.bool(key);
let changed = widgets::check(ui, text, v, enabled);
if changed {
d.fields.insert(key.into(), json!(!v));
}
changed
}
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
let layer = d.bool("layer");
let many = d.fields.get("ids").and_then(Value::as_array).is_some_and(|a| a.len() > 1);
grid(ui, |ui| {
if !many {
label(ui, tl!("Name:"));
form::text(ui, d, "name", 190.0);
ui.end_row();
}
if layer {
label(ui, tl!("Color:"));
ui.horizontal(|ui| {
let cur = d.str("color");
let names: Vec<&str> = LAYER_COLORS.iter().map(|(n, _)| *n).chain(["Custom"]).collect();
let shown = if LAYER_COLORS.iter().any(|(n, _)| n.eq_ignore_ascii_case(&cur)) { cur.clone() } else { "Custom".into() };
if let Some((n, _)) = widgets::dropdown(ui, "layer-color", &shown, &names, 140.0).and_then(|i| LAYER_COLORS.get(i)) {
d.fields.insert("color".into(), json!(n));
}
let [r, g, b] = LAYER_COLORS
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(&cur))
.map(|(_, c)| *c)
.or_else(|| {
vectorcraft_color::Color::from_hex(&cur).map(|c| {
let [r, g, b, _] = c.to_rgba8(1.0);
[r, g, b]
})
})
.unwrap_or(LAYER_COLORS[0].1);
let mut rgb = [r, g, b];
if ui.color_edit_button_srgb(&mut rgb).changed() {
d.fields.insert("color".into(), json!(format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2])));
}
});
ui.end_row();
}
});
ui.add_space(8.0);
if layer {
ui.horizontal(|ui| {
ui.vertical(|ui| {
// A template is locked and dims its images; turning it off frees both.
if toggle(ui, d, "template", tl!("Template"), true) {
let on = d.bool("template");
d.fields.insert("locked".into(), json!(on));
d.fields.insert("dimImages".into(), json!(on));
}
toggle(ui, d, "visible", tl!("Show"), true);
toggle(ui, d, "preview", tl!("Preview"), true);
ui.horizontal(|ui| {
toggle(ui, d, "dimImages", tl!("Dim Images to:"), true);
let mut v = d.f64("dimPercent", 50.0);
if ui.add_enabled(d.bool("dimImages"), egui::DragValue::new(&mut v).range(0.0..=100.0).suffix("%")).changed() {
d.fields.insert("dimPercent".into(), json!(v.round()));
}
});
});
ui.add_space(24.0);
ui.vertical(|ui| {
let template = d.bool("template");
toggle(ui, d, "locked", tl!("Lock"), !template);
toggle(ui, d, "printable", tl!("Print"), !template);
});
});
} else {
toggle(ui, d, "visible", tl!("Show"), true);
toggle(ui, d, "locked", tl!("Lock"), true);
}
false
}
/// The command and params OK runs.
pub(crate) fn params(d: &Dialog) -> (&'static str, Value) {
let mut p = serde_json::Map::new();
let many = d.fields.get("ids").and_then(Value::as_array).is_some_and(|a| a.len() > 1);
if !many {
p.insert("name".into(), json!(d.str("name")));
}
p.insert("visible".into(), json!(d.bool("visible")));
p.insert("locked".into(), json!(d.bool("locked")));
if d.bool("layer") {
p.insert("color".into(), json!(d.str("color")));
p.insert("template".into(), json!(d.bool("template")));
p.insert("printable".into(), json!(d.bool("printable")));
p.insert("preview".into(), json!(d.bool("preview")));
p.insert("dimImages".into(), if d.bool("dimImages") { json!(d.f64("dimPercent", 50.0).clamp(0.0, 100.0)) } else { json!(false) });
}
match d.str("mode").as_str() {
"new" => ("layer.new", Value::Object(p)),
"newSublayer" => ("layer.newSublayer", Value::Object(p)),
_ => {
p.insert("ids".into(), d.fields.get("ids").cloned().unwrap_or(json!([])));
("layer.setProps", Value::Object(p))
}
}
}
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let (cmd, p) = params(d);
run_and_close(app, cmd, p)
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_engine::Session;
fn app() -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
app
}
fn draw(app: &mut VectorcraftApp) {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let mut out = ctx.run_ui(Default::default(), |ui| super::super::show(app, ui.ctx()));
out.textures_delta.clear();
}
fn set(app: &mut VectorcraftApp, k: &str, v: Value) {
app.ui.dialog.as_mut().unwrap().fields.insert(k.into(), v);
}
#[test]
fn layer_options_edit_the_layer_and_ok_is_one_undo_step() {
let mut app = app();
let layer = app.session.doc().unwrap().doc.layers[0].id;
app.run("ui.layerOptions", json!({})).unwrap();
let d = app.ui.dialog.clone().unwrap();
assert_eq!((d.kind.as_str(), d.str("name"), d.str("color")), (KIND, "Layer 1".to_string(), "Light Blue".to_string()));
draw(&mut app);
set(&mut app, "name", json!("Sky"));
set(&mut app, "color", json!("#00ff00"));
set(&mut app, "dimImages", json!(true));
set(&mut app, "dimPercent", json!(30));
set(&mut app, "preview", json!(false));
let undo = app.session.doc().unwrap().history.undo.len();
super::super::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
let n = app.session.doc().unwrap().doc.node(layer).unwrap().clone();
assert_eq!(n.display_name(), "Sky");
assert!(matches!(n.kind, NodeKind::Layer { color: LayerColor::Custom([0, 255, 0]), dim_images: Some(30), preview: false, .. }));
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1);
}
#[test]
fn new_layer_options_make_a_layer_and_objects_get_name_show_and_lock() {
let mut app = app();
app.run("ui.newLayer", json!({})).unwrap();
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "Layer 2");
set(&mut app, "name", json!("Ink"));
set(&mut app, "template", json!(true));
set(&mut app, "locked", json!(true));
draw(&mut app);
super::super::confirm(&mut app).unwrap();
let d = &app.session.doc().unwrap().doc;
let top = d.layers.last().unwrap();
assert_eq!(top.display_name(), "Ink");
assert!(top.is_template() && top.locked);
// An object's row: Name, Show and Lock.
let r = app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10}));
assert!(r.is_ok());
app.session.execute("layer.setCurrent", &json!({"id": app.session.doc().unwrap().doc.layers[0].id.0})).unwrap();
let rect = app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
app.run("ui.layerOptions", json!({"id": rect})).unwrap();
assert!(!app.ui.dialog.as_ref().unwrap().bool("layer"));
draw(&mut app);
set(&mut app, "name", json!("Box"));
set(&mut app, "locked", json!(true));
let (cmd, p) = params(app.ui.dialog.as_ref().unwrap());
assert_eq!(cmd, "layer.setProps");
assert!(p.get("template").is_none());
super::super::confirm(&mut app).unwrap();
let n = app.session.doc().unwrap().doc.node(NodeId(rect)).unwrap().clone();
assert_eq!((n.display_name(), n.locked), ("Box".to_string(), true));
}
}
@@ -0,0 +1,117 @@
//! Layers panel › Panel Options…: Show Layers Only, the row size and which rows show thumbnails.
//!
//! Fields: `layersOnly`, `rowSize` (`small`, `medium`, `large` or `other`), `otherSize` (points,
//! 12–100, for `other`), `thumbLayers`, `thumbGroups` and `thumbObjects`. OK applies them to the
//! panel (kept with the UI state).
use serde_json::{Value, json};
use super::DialogSpec;
use super::swatch_options::{grid, label};
use crate::panels::layers::{PanelOptions, ROW_LARGE, ROW_MEDIUM, ROW_SMALL};
use crate::state::Dialog;
use crate::{VectorcraftApp, widgets};
/// The dialog kind of the Layers Panel Options.
pub const KIND: &str = "layersPanelOptions";
pub(super) const SPEC: DialogSpec =
DialogSpec { heading: |_| tl!("Layers Panel Options").into(), body, confirm, min_width: 300.0, ..DialogSpec::FORM };
const SIZES: [(&str, &str, f32); 3] = [("small", "Small", ROW_SMALL), ("medium", "Medium", ROW_MEDIUM), ("large", "Large", ROW_LARGE)];
/// `ui.layersPanelOptions`: open the dialog on the panel's options.
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
let o = &app.ui.layers_panel;
let size = SIZES.iter().find(|s| (s.2 - o.row()).abs() < 0.01).map_or("other", |s| s.0);
let fields = json!({
"layersOnly": o.layers_only, "rowSize": size, "otherSize": o.row(),
"thumbLayers": o.thumb_layers, "thumbGroups": o.thumb_groups, "thumbObjects": o.thumb_objects,
});
app.ui.dialog = Some(Dialog::new(KIND, fields));
Ok(Value::Null)
}
fn check(ui: &mut egui::Ui, d: &mut Dialog, key: &str, text: &str) {
let v = d.bool(key);
if widgets::check(ui, text, v, true) {
d.fields.insert(key.into(), json!(!v));
}
}
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
check(ui, d, "layersOnly", tl!("Show Layers Only"));
ui.add_space(8.0);
grid(ui, |ui| {
label(ui, tl!("Row Size:"));
ui.horizontal(|ui| {
let cur = d.str("rowSize");
let labels: Vec<&str> = SIZES.iter().map(|s| s.1).chain(["Other"]).collect();
let shown = SIZES.iter().find(|s| s.0 == cur).map_or("Other", |s| s.1);
if let Some(i) = widgets::dropdown(ui, "layers-row-size", shown, &labels, 110.0) {
d.fields.insert("rowSize".into(), json!(SIZES.get(i).map_or("other", |s| s.0)));
}
let mut v = d.f64("otherSize", f64::from(ROW_MEDIUM));
if ui.add_enabled(d.str("rowSize") == "other", egui::DragValue::new(&mut v).range(12.0..=100.0).suffix(" pt")).changed() {
d.fields.insert("otherSize".into(), json!(v.round()));
}
});
ui.end_row();
});
ui.add_space(8.0);
label(ui, tl!("Thumbnails:"));
check(ui, d, "thumbLayers", tl!("Layers"));
check(ui, d, "thumbGroups", tl!("Group"));
check(ui, d, "thumbObjects", tl!("Object"));
false
}
/// The options the fields describe.
pub(crate) fn options(d: &Dialog) -> PanelOptions {
let size = d.str("rowSize");
let row_size = SIZES.iter().find(|s| s.0 == size).map_or_else(|| (d.f64("otherSize", f64::from(ROW_MEDIUM)) as f32).clamp(12.0, 100.0), |s| s.2);
PanelOptions {
layers_only: d.bool("layersOnly"),
row_size: if row_size.is_finite() { row_size } else { ROW_MEDIUM },
thumb_layers: d.bool("thumbLayers"),
thumb_groups: d.bool("thumbGroups"),
thumb_objects: d.bool("thumbObjects"),
}
}
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
app.ui.layers_panel = options(d);
app.ui.dialog = None;
Ok(serde_json::to_value(&app.ui.layers_panel).unwrap_or_default())
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_engine::Session;
#[test]
fn panel_options_round_trip_and_apply() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
app.run("ui.layersPanelOptions", json!({})).unwrap();
assert_eq!(app.ui.dialog.as_ref().unwrap().str("rowSize"), "medium");
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let mut out = ctx.run_ui(Default::default(), |ui| super::super::show(&mut app, ui.ctx()));
out.textures_delta.clear();
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("layersOnly".into(), json!(true));
d.fields.insert("rowSize".into(), json!("other"));
d.fields.insert("otherSize".into(), json!(500));
d.fields.insert("thumbObjects".into(), json!(false));
super::super::confirm(&mut app).unwrap();
let o = &app.ui.layers_panel;
assert!(o.layers_only && !o.thumb_objects && o.thumb_layers);
assert_eq!(o.row(), 100.0, "kept to 100 pt");
app.run("ui.layersPanelOptions", json!({})).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("rowSize".into(), json!("large"));
super::super::confirm(&mut app).unwrap();
assert_eq!(app.ui.layers_panel.row(), ROW_LARGE);
}
}
+4
View File
@@ -33,6 +33,8 @@ mod gradient_stop;
pub mod graphic_style_options;
pub mod halftone;
pub mod import_pdf;
pub mod layer_options;
pub mod layers_panel_options;
pub mod liquify;
pub mod missing_links;
pub mod new_color_group;
@@ -267,6 +269,8 @@ registry! {
PerspectiveGridOptions: [perspective_options::KIND] => perspective_options::SPEC,
BlendOptions: [blend_options::KIND] => blend_options::SPEC,
PerspectivePlane: [perspective_plane::KIND] => perspective_plane::SPEC,
LayerOptions: [layer_options::KIND] => layer_options::SPEC,
LayersPanelOptions: [layers_panel_options::KIND] => layers_panel_options::SPEC,
}
/// The button labels the shared dialog frame can show (OK, discard and the fixed Cancel/Close),
+23
View File
@@ -602,6 +602,7 @@
Difference 差異化
Differences from {preset}: 與{preset}的差異:
Dim Copies to: 將拷貝調暗至:
Dim Images to: 影像變暗至:
Dimensions 尺寸
Direct Selection Tool 直接選取工具
Direction Handles 控制把手
@@ -697,6 +698,7 @@
Duplicate Layer 製作圖層複本
Duplicate Paragraph Style 製作段落樣式複本
Duplicate Selected Item 製作所選項目的複本
Duplicate Selection 製作選取範圍複本
Duplicate Slice 製作切片複本
Duplicate Swatch 製作色票複本
Duplicate Symbol 製作符號複本
@@ -817,6 +819,7 @@
Expand Compound Shape 展開複合形狀
Expand Gradient Mesh 展開漸層網格
Expand Gradient To 展開漸層為
Expand Layers Panel Rows 展開圖層面板列
Expand Options 展開選項
Expand Panels 展開面板
Expand Repeat 展開重複
@@ -924,6 +927,7 @@
Flare Tool Options 光暈工具選項
Flat Image 平面影像
Flatness: 平滑度:
Flatten Artwork 將圖稿平面化
Flatten Transparency 透明度平面化
Flatten Transparency… 透明度平面化…
Flattener preset: 平面化預設集:
@@ -1070,6 +1074,7 @@
Hide Hidden Characters 隱藏隱藏字元
Hide Note 隱藏註解
Hide Options 隱藏選項
Hide Others 隱藏其他
Hide Print Tiling 隱藏列印分頁
Hide Rulers 隱藏尺標
Hide Slices 隱藏切片
@@ -1082,6 +1087,7 @@
High Performance (discrete) 高效能 (獨立顯示卡)
Highlight Alternate Glyphs 醒目提示替代字符
Highlight anchors on mouse over 滑鼠移過時醒目提示錨點
Highlight Rows 反白顯示列
Highlight Substituted Fonts 醒目提示被取代的字體
Highlight Substituted Glyphs 醒目提示被取代的字符
History 步驟記錄
@@ -1222,8 +1228,10 @@
Latin-1 Supplement 拉丁文-1 補充
Layer 圖層
Layer Names 圖層名稱
Layer Options 圖層選項
Layer Options… 圖層選項…
Layers 圖層
Layers Panel Options 圖層面板選項
Layout 版面配置
Layout: 版面配置:
Leader: 前導字元:
@@ -1307,6 +1315,7 @@
Lock (⌘2) 鎖定 (⌘2)
Lock Grid 鎖定格點
Lock Guides 鎖定參考線
Lock Others 鎖定其他
Lock Slices 鎖定切片
Lock Station Point 鎖定基準點
Lossless (lossy WebP isn't available yet) 無失真 (尚未提供有失真的 WebP)
@@ -1371,6 +1380,7 @@
Merge Graphic Styles 合併繪圖樣式
Merge Graphic Styles… 合併繪圖樣式…
Merge Layers 合併圖層
Merge Selected 合併選取的項目
Merge Swatches 合併色票
Mesh Tool 網格工具
Message: 訊息:
@@ -1423,6 +1433,7 @@
Move Mesh Point 移動網格點
Move Plane 移動平面
Move Plane to Match Object 移動平面以符合物件
Move Rows 移動列
Move Slices 移動切片
Move Spine Point 移動脊線點
Move Swatch 移動色票
@@ -1459,6 +1470,7 @@
New Graphic Style 新增繪圖樣式
New Graphic Style (Alt-click to name it) 新增繪圖樣式 (Alt+點擊可命名)
New Graphic Style… 新增繪圖樣式…
New Layer Options… 新圖層選項…
New Layer… 新增圖層…
New Opacity Masks Are Clipping 新的不透明度遮色片為剪裁
New Opacity Masks Are Inverted 新的不透明度遮色片為反轉
@@ -1627,6 +1639,7 @@
Optimize for fast web view 最佳化以便快速網頁檢視
Optimized 最佳化
Options 選項
Options for Selection… 選取範圍的選項…
Options: 選項:
Options… 選項…
or more spaces with a tab 個以上的空格取代為定位字元
@@ -1646,6 +1659,7 @@
Out Tangent Handles 外切線控制把手
Outer Glow… 外光暈…
Outline 外框
Outline Others 其他以外框檢視
Outline Stroke 外框筆畫
Outline Text 外框文字
Outlines and Rules 外框與規則線
@@ -1815,6 +1829,7 @@
Presets: 預設集:
Press 按下
Preview 預覽
Preview All Layers 預覽所有圖層
Preview in Browser 在瀏覽器中預覽
Preview in Browser needs the desktop app (the web downloads files instead) 在瀏覽器中預覽需要桌面版應用程式 (網頁版會改為下載檔案)
Preview Mode 預覽模式
@@ -1962,6 +1977,8 @@
Release Guides 釋放參考線
Release Opacity Mask 釋放不透明度遮色片
Release Selection 釋放選取範圍
Release to Layers (Build) 釋放至圖層 (累加)
Release to Layers (Sequence) 釋放至圖層 (順序)
Release with Perspective 釋放透視
Relink 重新連結
Relink Linked Files to the Document 將連結檔案重新連結至文件
@@ -2073,6 +2090,7 @@
Rounded Rectangle 圓角矩形
Rounded Rectangle Tool 圓角矩形工具
Rounded Rectangle… 圓角矩形…
Row Size: 列尺寸:
Rows: 列數:
Rule: 規則線:
Rulers 尺標
@@ -2206,6 +2224,7 @@
Select an image to trace 選取要描圖的影像
Select Anchors 選取錨點
Select Appearance Item 選取外觀項目
Select Art 選取圖稿
Select art and choose Object › Pattern › Make. 選取圖稿,然後選擇「物件 › 圖樣 › 製作」。
Select art and click New Symbol to make one. 選取圖稿並按一下「新增符號」即可建立。
Select art and use New Brush to make one. 選取圖稿,然後使用「新增筆刷」來製作。
@@ -2280,6 +2299,7 @@
Show Active Plane Widget 顯示作用中平面 Widget
Show All 全部顯示
Show All Hidden Attributes 顯示所有隱藏屬性
Show All Layers 顯示所有圖層
Show All Plates 顯示所有色版
Show all plates 顯示所有色版
Show All Swatches 顯示所有色票
@@ -2308,6 +2328,7 @@
Show Images in Outline Mode 在外框模式中顯示影像
Show in Folder 在資料夾中顯示
Show Indic Options 顯示印度文字選項
Show Layers Only 僅顯示圖層
Show Link Info 顯示連結資訊
Show List View 顯示清單檢視
Show Mesh 顯示網格
@@ -2557,6 +2578,7 @@
Three Point Perspective Preset 5 三點透視預設集 5
Threshold 臨界值
Thumbnail View 縮圖檢視
Thumbnails: 縮圖:
Tile 並排顯示
Tile Edge Color 拼貼邊緣顏色
Tile Edge Color… 拼貼邊緣顏色…
@@ -2672,6 +2694,7 @@
Unlink the mask 取消連結遮色片
Unlock 解除鎖定
Unlock All 全部解除鎖定
Unlock All Layers 解除鎖定所有圖層
Unlock Grid 解除鎖定格點
Unlock Guides 解除鎖定參考線
Unnamed Objects 未命名物件
Can't render this file because it contains an unexpected character in line 6 and column 47.
+28
View File
@@ -305,6 +305,30 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{names: [two or more styles]} open Graphic Style Options (dialog `graphicStyleOptions`, field `name`) to name the style OK merges from them (graphicStyle.merge)",
),
(
"ui.layerOptions",
"Options for Selection…",
"",
"{ids?|id?} open Layer Options for Layers panel rows (default: the highlighted rows, else the current layer), filled in from the first: dialog `layerOptions`, fields name, color (a preset name such as \"Light Blue\" or #rrggbb), template, locked, visible, printable, preview, dimImages (bool), dimPercent (0–100); an object's row has name, visible and locked. OK runs layer.setProps (one undo step)",
),
(
"ui.newLayer",
"New Layer Options…",
"",
"{sublayer?: bool} open Layer Options for a new layer (or a sublayer of the current layer), named and coloured as it would be: dialog `layerOptions` with mode new|newSublayer; OK runs layer.new or layer.newSublayer with the fields",
),
(
"ui.layersPanelOptions",
"Panel Options…",
"",
"{} open the Layers panel's Panel Options: dialog `layersPanelOptions`, fields layersOnly, rowSize (small|medium|large|other), otherSize (12–100 pt), thumbLayers, thumbGroups, thumbObjects; OK applies them (kept with the UI state)",
),
(
"ui.layersExpand",
"Expand Layers Panel Rows",
"",
"{ids?: [id…] (default: every row that holds others), open?: bool (default true)} open or close rows of the Layers panel, as clicking their triangles does → {count}",
),
(
"ui.tileEdgeColor",
"Tile Edge Color…",
@@ -1022,6 +1046,10 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
crate::dialogs::graphic_style_options::open_merge(app, names.unwrap_or_default())
}
"ui.tileEdgeColor" => crate::dialogs::tile_edge_color::open(app),
"ui.layerOptions" => crate::dialogs::layer_options::open(app, p),
"ui.newLayer" => crate::dialogs::layer_options::open_new(app, p),
"ui.layersPanelOptions" => crate::dialogs::layers_panel_options::open(app),
"ui.layersExpand" => crate::panels::layers::expand(app, p),
"ui.flattenTransparencyDialog" => {
crate::dialogs::flatten::open(app);
Ok(Value::Null)
File diff suppressed because it is too large Load Diff
+8
View File
@@ -339,6 +339,12 @@ pub struct UiState {
/// becoming active, leaves it.
#[serde(skip)]
pub home: Option<(Option<u64>, usize)>,
/// Layers panel › Panel Options… (row size, thumbnails, Show Layers Only).
#[serde(default)]
pub layers_panel: crate::panels::layers::PanelOptions,
/// The Layers panel's open rows, per open document (`DocState::uid` → node ids).
#[serde(skip)]
pub layers_expanded: std::collections::HashMap<u64, std::collections::HashSet<u64>>,
/// The desktop window's size, position and maximized state, saved when the app quits and
/// restored at the next launch (the desktop host reads and writes it; none on the web).
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -422,6 +428,8 @@ impl Default for UiState {
eps_options: Value::Null,
dxf_import: Value::Null,
home: None,
layers_panel: Default::default(),
layers_expanded: Default::default(),
window: None,
}
}