mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 02:16:56 -06:00
chore: resolve SonarCloud maintainability issues (#1241)
* chore: resolve SonarCloud maintainability issues - Define SHA256_FMT constant in packaging script to remove duplicated string literal (shelldre:S1192) - Remove redundant 'static lifetime annotations from const items (rust:S8863) - Replace closures with method and function pointers (rust:S1612) - Extract helper functions in synthetic document generation, bezier polyline flattening, and component restoration to reduce cognitive complexity (rust:S3776) * fix(packaging): use verify_sha256 helper function to avoid shellcheck SC2059 * fix(engine): remove duplicate closing brace in large_document * fix(engine): take &mut Document in restore_id_map; keep 'static on associated consts restore_id_map allocates ids, so it needs a mutable document. Elided lifetimes in associated consts trip elided_lifetimes_in_associated_constant, so those annotations are restored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Assign positional parameters to locals in sha256 verification --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
This commit is contained in:
co-authored by
Claude Opus 5.5
Dylan Taylor
parent
c668e21759
commit
a1a842af9a
@@ -1,5 +1,6 @@
|
|||||||
//! The file subcommands: info, export, convert, render and run.
|
//! The file subcommands: info, export, convert, render and run.
|
||||||
|
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ fn load_noted(path: &str) -> CliResult<(Document, Vec<String>)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ext(path: &str) -> String {
|
fn ext(path: &str) -> String {
|
||||||
Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase()
|
Path::new(path).extension().and_then(OsStr::to_str).unwrap_or("").to_ascii_lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn image_format(e: &str) -> Option<ExportFormat> {
|
fn image_format(e: &str) -> Option<ExportFormat> {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::store::Store;
|
|||||||
use crate::test_util::{TempDir, config};
|
use crate::test_util::{TempDir, config};
|
||||||
|
|
||||||
fn args(s: &[&str]) -> Vec<String> {
|
fn args(s: &[&str]) -> Vec<String> {
|
||||||
s.iter().map(|a| a.to_string()).collect()
|
s.iter().map(ToString::to_string).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(dir: &std::path::Path, command: Command) -> Result<String, String> {
|
fn run(dir: &std::path::Path, command: Command) -> Result<String, String> {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
//! store replays such a log anyway and reports any change the snapshot lacked. A log ends at its
|
//! store replays such a log anyway and reports any change the snapshot lacked. A log ends at its
|
||||||
//! first damaged record (a write cut short).
|
//! first damaged record (a write cut short).
|
||||||
|
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -49,7 +50,7 @@ pub fn free_bytes(path: &Path) -> Option<u64> {
|
|||||||
|
|
||||||
/// `path` with `.tmp` added: where its next version is written first.
|
/// `path` with `.tmp` added: where its next version is written first.
|
||||||
pub fn temp_of(path: &Path) -> PathBuf {
|
pub fn temp_of(path: &Path) -> PathBuf {
|
||||||
let mut name = path.file_name().map(|n| n.to_os_string()).unwrap_or_default();
|
let mut name = path.file_name().map(OsStr::to_os_string).unwrap_or_default();
|
||||||
name.push(".tmp");
|
name.push(".tmp");
|
||||||
path.with_file_name(name)
|
path.with_file_name(name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use crate::args::{Args, file_url_path, is_invite, parse};
|
use crate::args::{Args, file_url_path, is_invite, parse};
|
||||||
|
|
||||||
fn args(a: &[&str]) -> Vec<String> {
|
fn args(a: &[&str]) -> Vec<String> {
|
||||||
a.iter().map(|s| s.to_string()).collect()
|
a.iter().map(ToString::to_string).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ impl BlendMode {
|
|||||||
/// The mode named by its label or CSS keyword, ignoring case, spaces and dashes. Labels come
|
/// The mode named by its label or CSS keyword, ignoring case, spaces and dashes. Labels come
|
||||||
/// first: "normal" is Normal, not pass-through (whose CSS keyword is also `normal`).
|
/// first: "normal" is Normal, not pass-through (whose CSS keyword is also `normal`).
|
||||||
pub fn from_name(s: &str) -> Option<Self> {
|
pub fn from_name(s: &str) -> Option<Self> {
|
||||||
let key: String = s.chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>().to_ascii_lowercase();
|
let key: String = s.chars().filter(char::is_ascii_alphanumeric).collect::<String>().to_ascii_lowercase();
|
||||||
Self::ALL
|
Self::ALL
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|m| m.label().replace(' ', "").to_ascii_lowercase() == key)
|
.find(|m| m.label().replace(' ', "").to_ascii_lowercase() == key)
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ impl WidthPreset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_id(s: &str) -> Option<WidthPreset> {
|
pub fn from_id(s: &str) -> Option<WidthPreset> {
|
||||||
let s: String = s.chars().filter(|c| c.is_ascii_alphanumeric()).collect();
|
let s: String = s.chars().filter(char::is_ascii_alphanumeric).collect();
|
||||||
WidthPreset::ALL
|
WidthPreset::ALL
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|p| p.id().eq_ignore_ascii_case(&s) || p.label().replace(' ', "").eq_ignore_ascii_case(&s))
|
.find(|p| p.id().eq_ignore_ascii_case(&s) || p.label().replace(' ', "").eq_ignore_ascii_case(&s))
|
||||||
|
|||||||
@@ -786,6 +786,38 @@ fn push_changes(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
Ok(json!({"layers": pushed}))
|
Ok(json!({"layers": pushed}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn restore_prop_defs(doc: &Document, props: &BTreeMap<String, Value>) -> BTreeMap<String, PropDef> {
|
||||||
|
let mut defs = BTreeMap::new();
|
||||||
|
for (k, v) in props {
|
||||||
|
let def = match v {
|
||||||
|
Value::Bool(b) => PropDef::Boolean { default: *b },
|
||||||
|
Value::String(t) => PropDef::Text { default: t.clone() },
|
||||||
|
other => match id_of(other) {
|
||||||
|
Some(c) if doc.contains(c) => PropDef::InstanceSwap { default: c, preferred: Vec::new() },
|
||||||
|
_ => continue,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
defs.insert(k.clone(), def);
|
||||||
|
}
|
||||||
|
defs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_id_map(doc: &mut Document, i: NodeId, component: NodeId, sub: &[NodeId]) -> BTreeMap<NodeId, NodeId> {
|
||||||
|
let mut map: BTreeMap<NodeId, NodeId> = BTreeMap::new();
|
||||||
|
let mut used: BTreeSet<NodeId> = BTreeSet::new();
|
||||||
|
map.insert(i, component);
|
||||||
|
used.insert(component);
|
||||||
|
for d in sub.iter().filter(|d| **d != i) {
|
||||||
|
let src = doc.node(*d).and_then(|n| n.source);
|
||||||
|
let nid = match src {
|
||||||
|
Some(s) if !doc.contains(s) && used.insert(s) => s,
|
||||||
|
_ => doc.alloc_id(),
|
||||||
|
};
|
||||||
|
map.insert(*d, nid);
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
fn restore_main(s: &mut Session, p: &Value) -> Result<Value> {
|
fn restore_main(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let cmd = "component.restoreMain";
|
let cmd = "component.restoreMain";
|
||||||
let i = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select an instance"))?;
|
let i = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select an instance"))?;
|
||||||
@@ -804,35 +836,13 @@ fn restore_main(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
// The main comes back with its old id (other instances point to it) and its layers with the
|
// The main comes back with its old id (other instances point to it) and its layers with the
|
||||||
// ids their copies mirror, so every instance re-links by source.
|
// ids their copies mirror, so every instance re-links by source.
|
||||||
let sub = doc.subtree(i);
|
let sub = doc.subtree(i);
|
||||||
let mut map: BTreeMap<NodeId, NodeId> = BTreeMap::new();
|
let map = restore_id_map(doc, i, component, &sub);
|
||||||
let mut used: BTreeSet<NodeId> = BTreeSet::new();
|
|
||||||
map.insert(i, component);
|
|
||||||
used.insert(component);
|
|
||||||
for d in sub.iter().filter(|d| **d != i) {
|
|
||||||
let src = doc.node(*d).and_then(|n| n.source);
|
|
||||||
let nid = match src {
|
|
||||||
Some(s) if !doc.contains(s) && used.insert(s) => s,
|
|
||||||
_ => doc.alloc_id(),
|
|
||||||
};
|
|
||||||
map.insert(*d, nid);
|
|
||||||
}
|
|
||||||
let mut nodes = Vec::new();
|
let mut nodes = Vec::new();
|
||||||
let mut nested = Vec::new();
|
let mut nested = Vec::new();
|
||||||
for d in &sub {
|
for d in &sub {
|
||||||
let Some(mut n) = doc.node(*d).cloned() else { continue };
|
let Some(mut n) = doc.node(*d).cloned() else { continue };
|
||||||
if *d == i {
|
if *d == i {
|
||||||
let mut defs = BTreeMap::new();
|
let defs = restore_prop_defs(doc, &props);
|
||||||
for (k, v) in &props {
|
|
||||||
let def = match v {
|
|
||||||
Value::Bool(b) => PropDef::Boolean { default: *b },
|
|
||||||
Value::String(t) => PropDef::Text { default: t.clone() },
|
|
||||||
other => match id_of(other) {
|
|
||||||
Some(c) if doc.contains(c) => PropDef::InstanceSwap { default: c, preferred: Vec::new() },
|
|
||||||
_ => continue,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
defs.insert(k.clone(), def);
|
|
||||||
}
|
|
||||||
n.kind = NodeKind::Component { description: String::new(), props: defs };
|
n.kind = NodeKind::Component { description: String::new(), props: defs };
|
||||||
n.source = None;
|
n.source = None;
|
||||||
n.overrides.clear();
|
n.overrides.clear();
|
||||||
|
|||||||
@@ -488,11 +488,11 @@ pub(crate) fn write(path: &str, _bytes: &[u8]) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ext(path: &str) -> String {
|
fn ext(path: &str) -> String {
|
||||||
std::path::Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase()
|
std::path::Path::new(path).extension().and_then(std::ffi::OsStr::to_str).unwrap_or("").to_ascii_lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_stem(path: &str) -> String {
|
fn file_stem(path: &str) -> String {
|
||||||
std::path::Path::new(path).file_stem().and_then(|e| e.to_str()).unwrap_or("Untitled").to_string()
|
std::path::Path::new(path).file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Untitled").to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The counts the import report shows for a freshly loaded document.
|
/// The counts the import report shows for a freshly loaded document.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub fn run<T>(cmd: &str, f: impl FnOnce() -> Result<T>) -> Result<T> {
|
|||||||
Err(payload) => {
|
Err(payload) => {
|
||||||
let msg = payload
|
let msg = payload
|
||||||
.downcast_ref::<&str>()
|
.downcast_ref::<&str>()
|
||||||
.map(|s| s.to_string())
|
.map(ToString::to_string)
|
||||||
.or_else(|| payload.downcast_ref::<String>().cloned())
|
.or_else(|| payload.downcast_ref::<String>().cloned())
|
||||||
.unwrap_or_else(|| "panic".to_string());
|
.unwrap_or_else(|| "panic".to_string());
|
||||||
log::error!("command `{cmd}` panicked: {msg}");
|
log::error!("command `{cmd}` panicked: {msg}");
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ pub fn publish(doc: &Document, prev: Option<&PublishedLibrary>, sel: &Selection,
|
|||||||
.map(|v| v.id.as_str())
|
.map(|v| v.id.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
let (bound, mode_colls) = used_variables(doc, &units);
|
let (bound, mode_colls) = used_variables(doc, &units);
|
||||||
let var_ids = alias_closure(&doc.variables, offered_vars.iter().map(|v| v.to_string()).chain(bound));
|
let var_ids = alias_closure(&doc.variables, offered_vars.iter().map(ToString::to_string).chain(bound));
|
||||||
snap.variables.variables = doc.variables.variables.iter().filter(|v| var_ids.contains(&v.id)).cloned().collect();
|
snap.variables.variables = doc.variables.variables.iter().filter(|v| var_ids.contains(&v.id)).cloned().collect();
|
||||||
let coll_ids: BTreeSet<&str> = snap
|
let coll_ids: BTreeSet<&str> = snap
|
||||||
.variables
|
.variables
|
||||||
|
|||||||
@@ -28,6 +28,53 @@ fn fill(doc: &mut Document, id: NodeId, c: Color) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_screen_header(doc: &mut Document, screen: NodeId, s: usize) -> DocResult<()> {
|
||||||
|
let header = doc.create(screen, NodeKind::Frame, "Header", 0.0, 0.0, SCREEN_W, 40.0)?;
|
||||||
|
if let Some(n) = doc.node_mut(header) {
|
||||||
|
n.layout = Some(AutoLayout { gap: 8.0, padding: [8.0; 4], ..AutoLayout::default() });
|
||||||
|
n.fills = vec![Paint::solid(Color::from_rgb8_const(0xF5, 0xF5, 0xF5))];
|
||||||
|
}
|
||||||
|
doc.create(header, text(&format!("Screen {}", s + 1)), "Title", 0.0, 0.0, 120.0, 16.0)?;
|
||||||
|
for k in 0..2 {
|
||||||
|
let icon = doc.create(header, ellipse(), &format!("Icon {}", k + 1), 0.0, 0.0, 20.0, 20.0)?;
|
||||||
|
fill(doc, icon, Color::from_rgb8_const(0x99, 0x99, 0x99));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_card_group(doc: &mut Document, screen: NodeId) -> DocResult<()> {
|
||||||
|
let card = doc.create(screen, NodeKind::Group, "Card", 12.0, 52.0, 160.0, 64.0)?;
|
||||||
|
// Children are in the group's space; normalising fits the group around them.
|
||||||
|
let cbg = doc.create(card, NodeKind::Rectangle, "Card background", 0.0, 0.0, 160.0, 64.0)?;
|
||||||
|
fill(doc, cbg, Color::from_rgb8_const(0xEE, 0xF4, 0xFF));
|
||||||
|
doc.create(card, ellipse(), "Avatar", 8.0, 8.0, 32.0, 32.0)?;
|
||||||
|
doc.create(card, text("Ada Lovelace"), "Name", 48.0, 8.0, 100.0, 16.0)?;
|
||||||
|
doc.create(card, text("Analyst"), "Role", 48.0, 28.0, 100.0, 16.0)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_screen_grid(doc: &mut Document, screen: NodeId, s: usize, rest: usize, made: &mut usize) -> DocResult<()> {
|
||||||
|
for k in 0..rest {
|
||||||
|
let (gx, gy) = (184.0 + (k % 8) as f64 * 21.0, 52.0 + (k / 8) as f64 * 34.0);
|
||||||
|
let hue = ((s * PER_SCREEN + k) % 360) as f32 / 360.0;
|
||||||
|
// The grid starts with a rectangle so even the smallest workload has one.
|
||||||
|
let kind = (k + 2) % 5;
|
||||||
|
let id = match kind {
|
||||||
|
0 => doc.create(screen, text(&format!("Item {k}")), "Text", gx, gy, 20.0, 14.0)?,
|
||||||
|
1 | 3 => doc.create(screen, ellipse(), "Ellipse", gx, gy, 18.0, 18.0)?,
|
||||||
|
_ => doc.create(screen, NodeKind::Rectangle, "Rectangle", gx, gy, 18.0, 18.0)?,
|
||||||
|
};
|
||||||
|
if kind != 0 {
|
||||||
|
fill(doc, id, Color::new(hue, 0.5, 1.0 - hue, 1.0));
|
||||||
|
if let Some(n) = doc.node_mut(id) {
|
||||||
|
n.corners.radii = [3.0; 4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*made += 1;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// A page of about `layers` layers (pages not counted), normalised like an opened file. Returns
|
/// A page of about `layers` layers (pages not counted), normalised like an opened file. Returns
|
||||||
/// the document and its page.
|
/// the document and its page.
|
||||||
pub fn large_document(layers: usize) -> DocResult<(Document, NodeId)> {
|
pub fn large_document(layers: usize) -> DocResult<(Document, NodeId)> {
|
||||||
@@ -53,28 +100,13 @@ pub fn large_document(layers: usize) -> DocResult<(Document, NodeId)> {
|
|||||||
let budget = |made: usize| layers.saturating_sub(made);
|
let budget = |made: usize| layers.saturating_sub(made);
|
||||||
// Auto layout header: title + two icons (4 layers).
|
// Auto layout header: title + two icons (4 layers).
|
||||||
if budget(made) >= 4 {
|
if budget(made) >= 4 {
|
||||||
let header = doc.create(screen, NodeKind::Frame, "Header", 0.0, 0.0, SCREEN_W, 40.0)?;
|
add_screen_header(&mut doc, screen, s)?;
|
||||||
if let Some(n) = doc.node_mut(header) {
|
|
||||||
n.layout = Some(AutoLayout { gap: 8.0, padding: [8.0; 4], ..AutoLayout::default() });
|
|
||||||
n.fills = vec![Paint::solid(Color::from_rgb8_const(0xF5, 0xF5, 0xF5))];
|
|
||||||
}
|
|
||||||
doc.create(header, text(&format!("Screen {}", s + 1)), "Title", 0.0, 0.0, 120.0, 16.0)?;
|
|
||||||
for k in 0..2 {
|
|
||||||
let icon = doc.create(header, ellipse(), &format!("Icon {}", k + 1), 0.0, 0.0, 20.0, 20.0)?;
|
|
||||||
fill(&mut doc, icon, Color::from_rgb8_const(0x99, 0x99, 0x99));
|
|
||||||
}
|
|
||||||
made += 4;
|
made += 4;
|
||||||
}
|
}
|
||||||
// A card group (5 layers).
|
// A card group (5 layers).
|
||||||
// The card and the instances leave one layer for the grid, which starts with a rectangle.
|
// The card and the instances leave one layer for the grid, which starts with a rectangle.
|
||||||
if budget(made) > 5 {
|
if budget(made) > 5 {
|
||||||
let card = doc.create(screen, NodeKind::Group, "Card", 12.0, 52.0, 160.0, 64.0)?;
|
add_card_group(&mut doc, screen)?;
|
||||||
// Children are in the group's space; normalising fits the group around them.
|
|
||||||
let cbg = doc.create(card, NodeKind::Rectangle, "Card background", 0.0, 0.0, 160.0, 64.0)?;
|
|
||||||
fill(&mut doc, cbg, Color::from_rgb8_const(0xEE, 0xF4, 0xFF));
|
|
||||||
doc.create(card, ellipse(), "Avatar", 8.0, 8.0, 32.0, 32.0)?;
|
|
||||||
doc.create(card, text("Ada Lovelace"), "Name", 48.0, 8.0, 100.0, 16.0)?;
|
|
||||||
doc.create(card, text("Analyst"), "Role", 48.0, 28.0, 100.0, 16.0)?;
|
|
||||||
made += 5;
|
made += 5;
|
||||||
}
|
}
|
||||||
// Three button instances (3 layers each).
|
// Three button instances (3 layers each).
|
||||||
@@ -87,24 +119,7 @@ pub fn large_document(layers: usize) -> DocResult<(Document, NodeId)> {
|
|||||||
}
|
}
|
||||||
// Fill the rest of the screen with a grid of shapes and text.
|
// Fill the rest of the screen with a grid of shapes and text.
|
||||||
let rest = PER_SCREEN.saturating_sub(4 + 5 + 9 + 1).min(budget(made));
|
let rest = PER_SCREEN.saturating_sub(4 + 5 + 9 + 1).min(budget(made));
|
||||||
for k in 0..rest {
|
add_screen_grid(&mut doc, screen, s, rest, &mut made)?;
|
||||||
let (gx, gy) = (184.0 + (k % 8) as f64 * 21.0, 52.0 + (k / 8) as f64 * 34.0);
|
|
||||||
let hue = ((s * PER_SCREEN + k) % 360) as f32 / 360.0;
|
|
||||||
// The grid starts with a rectangle so even the smallest workload has one.
|
|
||||||
let kind = (k + 2) % 5;
|
|
||||||
let id = match kind {
|
|
||||||
0 => doc.create(screen, text(&format!("Item {k}")), "Text", gx, gy, 20.0, 14.0)?,
|
|
||||||
1 | 3 => doc.create(screen, ellipse(), "Ellipse", gx, gy, 18.0, 18.0)?,
|
|
||||||
_ => doc.create(screen, NodeKind::Rectangle, "Rectangle", gx, gy, 18.0, 18.0)?,
|
|
||||||
};
|
|
||||||
if kind != 0 {
|
|
||||||
fill(&mut doc, id, Color::new(hue, 0.5, 1.0 - hue, 1.0));
|
|
||||||
if let Some(n) = doc.node_mut(id) {
|
|
||||||
n.corners.radii = [3.0; 4];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
made += 1;
|
|
||||||
}
|
|
||||||
if budget(made) == 0 {
|
if budget(made) == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ impl Kit {
|
|||||||
pub fn variables(&mut self, collection: &str, modes: &[&str], vars: Vec<(&str, &str, Vec<Value>)>) -> Result<()> {
|
pub fn variables(&mut self, collection: &str, modes: &[&str], vars: Vec<(&str, &str, Vec<Value>)>) -> Result<()> {
|
||||||
self.run("variable.createCollection", json!({"name": collection, "modes": modes}))?;
|
self.run("variable.createCollection", json!({"name": collection, "modes": modes}))?;
|
||||||
for (name, kind, values) in vars {
|
for (name, kind, values) in vars {
|
||||||
let values: Map<String, Value> = modes.iter().map(|m| m.to_string()).zip(values).collect();
|
let values: Map<String, Value> = modes.iter().map(ToString::to_string).zip(values).collect();
|
||||||
self.run("variable.create", json!({"collection": collection, "name": name, "type": kind, "values": values}))?;
|
self.run("variable.create", json!({"collection": collection, "name": name, "type": kind, "values": values}))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+22
-19
@@ -91,6 +91,27 @@ pub fn polylines(path: &BezPath) -> Option<Vec<Polyline>> {
|
|||||||
polylines_with_limit(path, MAX_SAMPLES)
|
polylines_with_limit(path, MAX_SAMPLES)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten_curve_segment(el: PathEl, seg_start: Point, limit: usize, count: &mut usize, cur: &mut Polyline) -> bool {
|
||||||
|
let mut over = false;
|
||||||
|
kurbo::flatten([PathEl::MoveTo(seg_start), el], FLATTEN_TOL, |fel| {
|
||||||
|
if over {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let PathEl::LineTo(p) = fel {
|
||||||
|
*count += 1;
|
||||||
|
if *count > limit {
|
||||||
|
over = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if cur.points.is_empty() {
|
||||||
|
cur.push(seg_start);
|
||||||
|
}
|
||||||
|
cur.push(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
!over
|
||||||
|
}
|
||||||
|
|
||||||
/// [`polylines`] with an explicit sample budget, so tests can use a small one.
|
/// [`polylines`] with an explicit sample budget, so tests can use a small one.
|
||||||
///
|
///
|
||||||
/// The budget is enforced *while* flattening, not after it (issue #204): the input element
|
/// The budget is enforced *while* flattening, not after it (issue #204): the input element
|
||||||
@@ -141,25 +162,7 @@ fn polylines_with_limit(path: &BezPath, limit: usize) -> Option<Vec<Polyline>> {
|
|||||||
PathEl::QuadTo(_, end) | PathEl::CurveTo(_, _, end) => {
|
PathEl::QuadTo(_, end) | PathEl::CurveTo(_, _, end) => {
|
||||||
// Flatten this one segment on its own, so an over-budget curve aborts
|
// Flatten this one segment on its own, so an over-budget curve aborts
|
||||||
// here instead of after the whole path has been flattened.
|
// here instead of after the whole path has been flattened.
|
||||||
let seg_start = pen;
|
if !flatten_curve_segment(*el, pen, limit, &mut count, &mut cur) {
|
||||||
let mut over = false;
|
|
||||||
kurbo::flatten([PathEl::MoveTo(seg_start), *el], FLATTEN_TOL, |fel| {
|
|
||||||
if over {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let PathEl::LineTo(p) = fel {
|
|
||||||
count += 1;
|
|
||||||
if count > limit {
|
|
||||||
over = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if cur.points.is_empty() {
|
|
||||||
cur.push(seg_start);
|
|
||||||
}
|
|
||||||
cur.push(p);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if over {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
pen = end;
|
pen = end;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ fn error(id: Value, code: i64, message: impl Into<String>) -> Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
|
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
|
||||||
payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into())
|
payload.downcast_ref::<&str>().map(ToString::to_string).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
|
|||||||
@@ -812,7 +812,7 @@ fn scan_dir(dir: &std::path::Path, depth: usize, out: &mut BTreeMap<String, (Str
|
|||||||
if !meta.is_file() {
|
if !meta.is_file() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let ext = path.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).unwrap_or_default();
|
let ext = path.extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase).unwrap_or_default();
|
||||||
if !matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc") {
|
if !matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ mod windows_qa_tests;
|
|||||||
mod windows_tests;
|
mod windows_tests;
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
@@ -1175,7 +1176,7 @@ impl FigApp {
|
|||||||
pub fn save_as_dialog(&mut self) {
|
pub fn save_as_dialog(&mut self) {
|
||||||
let Ok(name) = self.session.doc().map(|d| d.name.clone()) else { return };
|
let Ok(name) = self.session.doc().map(|d| d.name.clone()) else { return };
|
||||||
self.pick_save(i18n::tr("Save as"), &format!("{name}.figcraft"), &["figcraft", "fig"], |app, mut p| {
|
self.pick_save(i18n::tr("Save as"), &format!("{name}.figcraft"), &["figcraft", "fig"], |app, mut p| {
|
||||||
if p.extension().is_none_or(|e| e.is_empty()) {
|
if p.extension().is_none_or(OsStr::is_empty) {
|
||||||
if !p.set_extension("figcraft") {
|
if !p.set_extension("figcraft") {
|
||||||
app.toast(i18n::tr("Use the .figcraft or .fig extension."));
|
app.toast(i18n::tr("Use the .figcraft or .fig extension."));
|
||||||
return;
|
return;
|
||||||
@@ -1192,7 +1193,7 @@ impl FigApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if !p.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("figcraft") || e.eq_ignore_ascii_case("fig")) {
|
} else if !p.extension().and_then(OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("figcraft") || e.eq_ignore_ascii_case("fig")) {
|
||||||
app.toast(i18n::tr("Use the .figcraft or .fig extension."));
|
app.toast(i18n::tr("Use the .figcraft or .fig extension."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1211,7 +1212,7 @@ impl FigApp {
|
|||||||
.docs
|
.docs
|
||||||
.get(tab)
|
.get(tab)
|
||||||
.and_then(|d| d.path.as_deref())
|
.and_then(|d| d.path.as_deref())
|
||||||
.is_some_and(|path| !Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("fig")));
|
.is_some_and(|path| !Path::new(path).extension().and_then(OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("fig")));
|
||||||
if native_path {
|
if native_path {
|
||||||
if self.run("file.save", json!({})).is_some() {
|
if self.run("file.save", json!({})).is_some() {
|
||||||
self.toast(i18n::tr("Saved"));
|
self.toast(i18n::tr("Saved"));
|
||||||
@@ -1870,13 +1871,13 @@ pub(crate) const OPEN_EXTENSIONS: &[&str] = &["figcraft", "fig", "json", "svg",
|
|||||||
|
|
||||||
/// Whether `path` names a file type that opens as a document (the extension, in any case).
|
/// Whether `path` names a file type that opens as a document (the extension, in any case).
|
||||||
pub(crate) fn opens(path: &Path) -> bool {
|
pub(crate) fn opens(path: &Path) -> bool {
|
||||||
path.extension().and_then(|e| e.to_str()).is_some_and(|e| OPEN_EXTENSIONS.iter().any(|x| x.eq_ignore_ascii_case(e)))
|
path.extension().and_then(OsStr::to_str).is_some_and(|e| OPEN_EXTENSIONS.iter().any(|x| x.eq_ignore_ascii_case(e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether `path` is a whole document (`.figcraft`, `.fig`, file JSON) rather than an image or SVG:
|
/// Whether `path` is a whole document (`.figcraft`, `.fig`, file JSON) rather than an image or SVG:
|
||||||
/// dropped on the canvas it opens as a file of its own instead of adding its pages to the open one.
|
/// dropped on the canvas it opens as a file of its own instead of adding its pages to the open one.
|
||||||
pub(crate) fn is_document(path: &Path) -> bool {
|
pub(crate) fn is_document(path: &Path) -> bool {
|
||||||
path.extension().and_then(|e| e.to_str()).is_some_and(|e| ["figcraft", "fig", "json"].iter().any(|x| x.eq_ignore_ascii_case(e)))
|
path.extension().and_then(OsStr::to_str).is_some_and(|e| ["figcraft", "fig", "json"].iter().any(|x| x.eq_ignore_ascii_case(e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the contents of dropped file `name` that has no file on disk to read by path.
|
/// Reads the contents of dropped file `name` that has no file on disk to read by path.
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ pub fn guard(app: &mut FigApp, ctx: &Context, f: impl FnOnce(&mut FigApp)) {
|
|||||||
let roots = app.automation_roots.clone();
|
let roots = app.automation_roots.clone();
|
||||||
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| figcraft_engine::file_access::confined(roots.as_ref(), || f(&mut *app))));
|
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| figcraft_engine::file_access::confined(roots.as_ref(), || f(&mut *app))));
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
let msg = e.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| e.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into());
|
let msg = e.downcast_ref::<&str>().map(ToString::to_string).or_else(|| e.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into());
|
||||||
log::error!("UI panic: {msg}");
|
log::error!("UI panic: {msg}");
|
||||||
app.toast(format!("Internal error (please report): {msg}"));
|
app.toast(format!("Internal error (please report): {msg}"));
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ set -euo pipefail
|
|||||||
HERE="$ROOT/packaging/linux"
|
HERE="$ROOT/packaging/linux"
|
||||||
APP_ID=ai.storyteller.figcraft
|
APP_ID=ai.storyteller.figcraft
|
||||||
|
|
||||||
|
verify_sha256() {
|
||||||
|
local expected="$1"
|
||||||
|
local file="$2"
|
||||||
|
printf '%s %s\n' "$expected" "$file" | sha256sum -c -
|
||||||
|
}
|
||||||
|
|
||||||
SKIP_BUILD=0
|
SKIP_BUILD=0
|
||||||
FORMATS="appimage deb rpm tar"
|
FORMATS="appimage deb rpm tar"
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
@@ -145,21 +151,21 @@ if has appimage; then
|
|||||||
download="$TOOL.download"
|
download="$TOOL.download"
|
||||||
curl --proto '=https' -fsSL -o "$download" \
|
curl --proto '=https' -fsSL -o "$download" \
|
||||||
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage"
|
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage"
|
||||||
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c -
|
verify_sha256 "$APPIMAGETOOL_SHA256" "$download"
|
||||||
chmod 755 "$download"
|
chmod 755 "$download"
|
||||||
mv "$download" "$TOOL"
|
mv "$download" "$TOOL"
|
||||||
fi
|
fi
|
||||||
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c -
|
verify_sha256 "$APPIMAGETOOL_SHA256" "$TOOL"
|
||||||
fi
|
fi
|
||||||
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH"
|
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH"
|
||||||
if [[ ! -f "$RUNTIME" ]]; then
|
if [[ ! -f "$RUNTIME" ]]; then
|
||||||
download="$RUNTIME.download"
|
download="$RUNTIME.download"
|
||||||
curl --proto '=https' -fsSL -o "$download" \
|
curl --proto '=https' -fsSL -o "$download" \
|
||||||
"https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH"
|
"https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH"
|
||||||
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c -
|
verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$download"
|
||||||
mv "$download" "$RUNTIME"
|
mv "$download" "$RUNTIME"
|
||||||
fi
|
fi
|
||||||
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c -
|
verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME"
|
||||||
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
|
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
|
||||||
# relative, e.g. target/agent-<name>).
|
# relative, e.g. target/agent-<name>).
|
||||||
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
|
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ fn cargo() -> Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn root() -> std::path::PathBuf {
|
fn root() -> std::path::PathBuf {
|
||||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().map(|p| p.to_path_buf()).unwrap_or_default()
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().map(std::path::Path::to_path_buf).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
|
|||||||
Reference in New Issue
Block a user