Merge main into #1460

This commit is contained in:
bflatastic
2026-10-29 10:38:06 +07:00
289 changed files with 10322 additions and 4115 deletions
+16 -2
View File
@@ -1,12 +1,12 @@
//! Arrange: align, distribute, tidy, z-order, flips, rotations and numeric transforms.
use figcraft_doc::*;
use figcraft_doc::{Document, EffectKind, LetterSpacing, LineHeight, Node, NodeId, NodeKind, Sizing, TextAutoResize, TextStyle};
use figcraft_geom::{Affine, Point, Rect};
use serde_json::{Value, json};
use super::edit::{roots, z_sorted};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{boolean, get, num, num_or, req_id, req_num, req_targets, string};
use crate::{Result, Session, bad, spec};
pub fn specs() -> Vec<CommandSpec> {
@@ -405,6 +405,11 @@ fn turn_pivot(s: &Session, ids: &[NodeId]) -> Result<Option<Point>> {
pub fn scale_node(doc: &mut Document, id: NodeId, k: f64) -> Result<()> {
let k = if k.is_finite() && k > 0.0 { k } else { return Ok(()) };
let sub = doc.subtree(id);
// The layers below `id` scale whatever their constraints, so where they are layers of an
// instance their new size and place are overrides. The edit's own diff takes geometry that
// changed with an ancestor's size for what constraints give, so the next rebuild of the
// instance would put them back at their main's size (#922).
let in_instance: Vec<bool> = sub.iter().enumerate().map(|(depth, i)| depth > 0 && crate::components::inside_instance(doc, *i)).collect();
let tiles = |paints: &mut Vec<figcraft_color::Paint>| {
for p in paints {
if let figcraft_color::PaintKind::Image(img) = &mut p.kind
@@ -416,6 +421,7 @@ pub fn scale_node(doc: &mut Document, id: NodeId, k: f64) -> Result<()> {
};
for (depth, i) in sub.iter().enumerate() {
let n = doc.get_mut(*i)?;
let (w0, h0, t0) = (n.width, n.height, n.transform);
tiles(&mut n.fills);
tiles(&mut n.strokes);
let cl = &mut n.child_layout;
@@ -511,6 +517,14 @@ pub fn scale_node(doc: &mut Document, id: NodeId, k: f64) -> Result<()> {
let c = n.transform.as_coeffs();
n.transform = Affine::new([c[0], c[1], c[2], c[3], c[4] * k, c[5] * k]);
}
if in_instance.get(depth).copied().unwrap_or(false) {
if n.width != w0 || n.height != h0 {
n.overrides.insert("size".into());
}
if n.transform != t0 {
n.overrides.insert("position".into());
}
}
}
Ok(())
}
+2 -2
View File
@@ -4,11 +4,11 @@
//! message). `author` is who is acting (default: the name from Preferences, `pref.userName`): only
//! a message's author edits or deletes it, and only the thread's author deletes the thread.
use figcraft_doc::*;
use figcraft_doc::{Comment, Document, Message, Node, NodeId, NodeKind, geom};
use serde_json::{Value, json};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{boolean, get, id, id_of, num, req_index, req_num, req_string, string};
use crate::{Session, bad, spec};
/// The name comments are signed with until the user sets theirs.
+8 -3
View File
@@ -2,13 +2,18 @@
use std::collections::{BTreeMap, BTreeSet};
use figcraft_doc::*;
use figcraft_doc::{Document, Node, NodeId, NodeKind, PropDef, VariableType, VariableValue};
use serde_json::{Value, json};
use super::edit::{roots, z_sorted};
use super::{CommandSpec, ok};
use crate::components::*;
use crate::params::*;
use crate::components::{
OVERRIDE_GROUPS, PROP_BINDING, SLOT, copy_group, create_instance, create_instance_at, default_props, default_variant, exposed_copies,
find_variant, inside_instance, instances_of, is_slot, keep_text_overrides, owning_component, owning_instance, prop_default_value,
rebase_instance, root_instance, set_boolean_default, slot_edited, slot_name, slots_of, takes_value, unlink_layers, variant_conflicts,
variant_name, variant_options, variant_props, variant_set,
};
use crate::params::{boolean, get, id, id_of, num, req_id, req_num, req_string, req_targets, string, targets};
use crate::{EngineError, Result, Session, bad, spec};
mod instance;
+6 -1
View File
@@ -1,5 +1,10 @@
use super::props::{Owner, add_prop_for, delete_prop, prop_defs};
use super::*;
use super::{
BTreeMap, BTreeSet, Document, EngineError, NodeId, NodeKind, OVERRIDE_GROUPS, PROP_BINDING, PropDef, Result, SLOT, Session, Value, bad, boolean,
copy_group, create_instance_at, default_props, get, id_of, inside_instance, is_slot, json, keep_text_overrides, main_of, ok, owning_component,
owning_instance, rebase_instance, req_id, req_num, req_targets, root_instance, slot_edited, slot_name, slots_of, string, targets, unlink_layers,
variant_options,
};
pub(super) fn detach(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "component.detach";
+6 -1
View File
@@ -1,6 +1,11 @@
use super::instance::preferred_arg;
use super::variants::{fit_set, wrap_in_set};
use super::*;
use super::{
BTreeMap, Document, NodeId, NodeKind, PROP_BINDING, PropDef, Result, SLOT, Session, Value, VariableType, VariableValue, bad, boolean,
default_variant, find_variant, get, id, id_of, inside_instance, instances_of, json, main_of, ok, owning_component, prop_default_value,
req_string, req_targets, set_boolean_default, slot_edited, slots_of, string, takes_value, targets, variant_conflicts, variant_name,
variant_options, variant_props, variant_set,
};
/// Whether instance `id` has a component property `name` that a `kind` variable can drive (a
/// boolean property takes a boolean variable, a text or variant property a string one).
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
BTreeMap, BTreeSet, Document, EngineError, Node, NodeId, NodeKind, Result, Session, Value, bad, inside_instance, json, owning_instance,
req_targets, roots, set_name_from, string, targets, variant_name, variant_options, variant_props, variant_set, z_sorted,
};
/// Wrap components in a new component set (named after the first) placed around them.
pub(super) fn wrap_in_set(cmd: &str, doc: &mut Document, ids: &[NodeId]) -> Result<NodeId> {
+2 -2
View File
@@ -1,12 +1,12 @@
//! Creating nodes.
use figcraft_color::{ImageScaleMode, Paint};
use figcraft_doc::*;
use figcraft_doc::{Document, Node, NodeId, NodeKind, StrokeCap, TextAutoResize, TextData, VectorNetwork};
use figcraft_geom::Point;
use serde_json::{Value, json};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{boolean, f64_of, get, id, num, num_or, opt_id, req_targets, string};
use crate::{Result, Session, bad, spec};
/// Frame presets shown by the Frame tool (category, name, width, height).
+5 -2
View File
@@ -2,12 +2,15 @@
use std::collections::{BTreeMap, BTreeSet};
use figcraft_doc::*;
use figcraft_doc::{Document, Effect, ExportSetting, ImageData, LayoutGrid, Node, NodeId, NodeKind};
use figcraft_geom::Affine;
use serde_json::{Map, Value, json};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{
blend, boolean, color, get, id, id_of, num, num_or, opt_index, paint_of, parse, req_id, req_index, req_num, req_string, req_targets, string,
targets,
};
use crate::{BatchStep, EngineError, Result, Session, bad, spec};
mod clipboard;
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Affine, BTreeMap, BTreeSet, Document, EngineError, ImageData, Node, NodeId, NodeKind, Result, Session, Value, bad, delete, id, json, num, ok,
req_targets, roots, string, targets, z_sorted,
};
/// The copied form of the selection.
pub fn copy_to_clipboard(s: &mut Session, cmd: &str, ids: &[NodeId]) -> Result<()> {
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
BTreeMap, Effect, EngineError, Map, Node, NodeId, NodeKind, Result, Session, Value, bad, blend, boolean, color, get, json, num, ok, opt_index,
paint_of, parse, req_id, req_index, req_string, req_targets, roots, string, targets, z_sorted,
};
pub(super) fn batch_rename(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "object.batchRename";
+3 -1
View File
@@ -1,4 +1,6 @@
use super::*;
use super::{
Affine, BTreeMap, BTreeSet, Node, NodeId, Result, Session, Value, bad, boolean, json, num, num_or, ok, req_num, req_string, req_targets, roots,
};
pub(super) fn layers_at(s: &mut Session, p: &Value) -> Result<Value> {
let (x, y) = (req_num("select.layersAt", p, "x")?, req_num("select.layersAt", p, "y")?);
+5 -2
View File
@@ -3,12 +3,15 @@
use std::sync::Arc;
use base64::Engine as _;
use figcraft_doc::*;
use figcraft_doc::{
BOARD_PAGE, Document, ExportFormat, ExportSetting, ExportSize, FileKind, LineHeight, Node, NodeId, NodeKind, PrintSettings, StyleKind, TextStyle,
VdCompat,
};
use figcraft_format::Version;
use serde_json::{Value, json};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{bool_of, boolean, get, id, num, num_or, req_index, req_string, string, targets};
use crate::{DocState, EngineError, Result, Session, bad, spec};
mod export;
+2 -2
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{DocState, Document, EngineError, LineHeight, NodeId, NodeKind, Result, StyleKind, TextStyle, Value, bad, get, json};
/// The recovery copy at `path` as a document with unsaved changes and no path.
pub(crate) fn recovered(path: &str) -> Result<DocState> {
@@ -351,7 +351,7 @@ fn link_instances(doc: &mut Document) {
/// Largest image file read for a REST API file JSON import.
#[cfg(not(target_arch = "wasm32"))]
const MAX_IMAGE_FILE: u64 = 256 * 1024 * 1024;
const MAX_IMAGE_FILE: u64 = figcraft_doc::ImageData::MAX_BYTES;
/// Image bytes for a REST API file JSON's `imageRef`: `images/<imageRef>[.png|.jpg|…]` next to
/// the file.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Arc, DocState, Document, EngineError, Result, Session, Value, Version, bad, get, json, req_index, string};
pub(super) fn version_json(index: usize, v: &Version) -> Value {
let mut j = json!({"id": v.id, "index": index, "name": v.name, "description": v.description, "time": v.time});
+1 -1
View File
@@ -5,7 +5,7 @@ use figcraft_doc::{Document, ImagePreset};
use serde_json::{Value, json};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{f64_of, get, num, req_index, req_string, req_targets, string, targets};
use crate::{Result, Session, bad, spec};
pub fn specs() -> Vec<CommandSpec> {
+1 -1
View File
@@ -6,7 +6,7 @@ use serde_json::Value;
use super::props::sizing;
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{get, num, req_num, req_targets, string};
use crate::{Result, Session, bad, spec};
pub fn specs() -> Vec<CommandSpec> {
+6 -3
View File
@@ -5,16 +5,19 @@
use std::collections::BTreeSet;
use std::sync::Arc;
use figcraft_doc::*;
use figcraft_doc::{Collection, Document, ImportedComponent, ImportedRecord, LibraryLink, LibraryManifest, NodeId, PublishedLibrary, VariableId};
use serde_json::{Value, json};
use figcraft_collab::message::{LibraryReply, LibraryReq};
use super::CommandSpec;
use crate::library::*;
use crate::library::{
Pending, Selection, absolute_path, accept_updates, asset_root, default_selection, find_link, import, import_styles, import_variables,
is_remote_variable, pending, prune, publish, publishable, published_collections, published_variables,
};
use crate::library_server::{archive, is_server_link, read_archive, shown_path};
use crate::live::{NetReply, NetRequest, PublishOptions};
use crate::params::*;
use crate::params::{boolean, get, id_of, num, req_string, string};
use crate::{EngineError, Result, Session, bad, spec};
/// Longest library description kept (chars).
+2 -2
View File
@@ -6,11 +6,11 @@
//! Every list is stored bottom first: index 0 paints (or applies) first, the last one is on top;
//! the Design panel shows them in reverse, the topmost row first.
use figcraft_doc::*;
use figcraft_doc::{Document, LayoutGrid, Node, NodeId};
use serde_json::Value;
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{req_index, req_targets};
use crate::{Result, Session, spec};
pub fn specs() -> Vec<CommandSpec> {
+7 -5
View File
@@ -1,11 +1,11 @@
//! Pages.
use figcraft_doc::proto::Flow;
use figcraft_doc::*;
use figcraft_doc::{Guide, NodeId, NodeKind};
use serde_json::{Value, json};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{boolean, color, id, req_index, req_num, req_string, string};
use crate::{Session, bad, spec};
/// After a page edit: the file keeps a page that isn't a divider, and the canvas shows one (the
@@ -33,6 +33,7 @@ fn page_target(s: &Session, cmd: &str, p: &Value) -> crate::Result<NodeId> {
Ok(target)
}
/// The page and ruler guide commands.
pub fn specs() -> Vec<CommandSpec> {
vec![
spec!("page.add", "Add page", "{name?} (a name starting with a hyphen adds a divider)", true, |s, p| {
@@ -106,8 +107,10 @@ pub fn specs() -> Vec<CommandSpec> {
copies.extend(doc.subtree(*c).into_iter().zip(doc.subtree(*copy)));
}
// The copy keeps the page's colour, flows (pointing at the copied frames), prototype settings and guides.
if let NodeKind::Page { background, flows, prototype, guides } = doc.get(src)?.kind.clone()
&& let NodeKind::Page { background: b, flows: f, prototype: pr, guides: g } = &mut doc.get_mut(np)?.kind
let guides = doc.get(src)?.guides.clone();
doc.get_mut(np)?.guides = guides;
if let NodeKind::Page { background, flows, prototype } = doc.get(src)?.kind.clone()
&& let NodeKind::Page { background: b, flows: f, prototype: pr } = &mut doc.get_mut(np)?.kind
{
*b = background;
*f = flows
@@ -115,7 +118,6 @@ pub fn specs() -> Vec<CommandSpec> {
.filter_map(|fl| copies.iter().find(|(old, _)| *old == fl.start).map(|(_, new)| Flow { start: *new, ..fl }))
.collect();
*pr = prototype;
*g = guides;
}
// Keep the new page right after the source.
if let Some(si) = doc.pages.iter().position(|x| *x == src) {
File diff suppressed because it is too large Load Diff
+8 -4
View File
@@ -1,11 +1,14 @@
//! Prototype commands: interactions, flows, settings, presenting.
use figcraft_doc::proto::*;
use figcraft_doc::*;
use figcraft_doc::proto::{
Action, DeviceRotation, Easing, Expr, Flow, Interaction, MAX_DELAY_MS, MAX_EXPR_DEPTH, OverlayPosition, PrototypeSettings, Transition, Trigger,
walk_mut,
};
use figcraft_doc::{Document, NodeId, NodeKind, VariableValue, Variables};
use serde_json::{Value, json};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{boolean, color, f64_of, get, id, num, parse, req_id, req_num, req_targets, string};
use crate::{Result, Session, bad, spec};
pub fn specs() -> Vec<CommandSpec> {
@@ -158,7 +161,7 @@ pub fn specs() -> Vec<CommandSpec> {
spec!(
"proto.linkNotice",
"Notice before leaving for a link",
"{show? (default true; false skips it)} whether a presented prototype's Open link to a website first says that it leaves the prototype, with Open link and Cancel; kept with the preferences → {shown}",
"{show? (default true; false skips it)} whether a presented prototype's Open link the viewer clicked (or fired by a key) first says that it leaves the prototype, with Open link and Cancel (a link a delay or hover fired always does); kept with the preferences → {shown}",
false,
|s, p| {
let show = boolean(p, "show").unwrap_or(true);
@@ -498,6 +501,7 @@ fn set(s: &mut Session, p: &Value) -> Result<Value> {
mod tests {
use super::*;
use crate::params::id_of;
use figcraft_doc::proto::{Direction, Overflow, ScrollPosition};
fn frame(s: &mut Session, x: f64) -> NodeId {
let r = s.execute("node.create", &json!({"type": "frame", "x": x, "y": 0, "width": 100, "height": 200})).unwrap();
+3 -3
View File
@@ -10,12 +10,12 @@
use std::collections::BTreeMap;
use figcraft_doc::proto::*;
use figcraft_doc::*;
use figcraft_doc::proto::{Action, ConditionalBlock, Expr, ExprOp, MAX_ACTION_DEPTH, walk};
use figcraft_doc::{Variable, VariableType, VariableValue, Variables};
use serde_json::{Value, json};
use super::CommandSpec;
use crate::params::*;
use crate::params::{bool_of, f64_of, get, id, parse, req_index, req_targets, string};
use crate::{Result, Session, bad, spec};
/// Most actions in one list (an interaction's, or a block's).
+1 -1
View File
@@ -7,7 +7,7 @@ use serde_json::{Value, json};
use super::CommandSpec;
use crate::devinspect::{dev_items, has_status};
use crate::params::*;
use crate::params::{boolean, get, req_targets, string, targets};
use crate::{Result, Session, bad, spec};
/// Longest change reason kept.
+5 -2
View File
@@ -1,13 +1,16 @@
//! Structure: groups, frames, masks, boolean groups, flatten, outline stroke, auto layout, pages.
use figcraft_color::{Color, Paint};
use figcraft_doc::*;
use figcraft_doc::{
AutoLayout, BoolOp, Corners, Document, LayoutMode, Node, NodeId, NodeKind, Region, Segment, Sizing, StrokeAlign, StrokeJoin, StrokeProps,
VectorNetwork, WindingRule, proto,
};
use figcraft_geom::{Affine, BezPath, Rect, Shape};
use serde_json::{Value, json};
use super::edit::{roots, z_sorted};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{f64_of, get, id_of, num_or, req_id, req_index, req_num, req_targets, string, targets};
use crate::{EngineError, Result, Session, bad, spec};
/// The layer structure commands: group, frame, mask, boolean operations, flatten, auto layout
+5 -2
View File
@@ -3,11 +3,14 @@
use std::collections::BTreeMap;
use figcraft_color::{Color, Paint, PaintKind};
use figcraft_doc::*;
use figcraft_doc::{
Collection, CollectionId, Document, Effect, EffectKind, Mode, ModeId, Node, NodeId, NodeKind, Style, StyleId, StyleKind, TextData, TextStyle,
Variable, VariableId, VariableType, VariableValue, Variables, trim_num,
};
use serde_json::{Value, json};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{color_of, f64_of, get, id, num, paints_of, req_string, req_targets, string, targets};
use crate::{EngineError, Result, Session, bad, spec};
pub fn specs() -> Vec<CommandSpec> {
+5 -2
View File
@@ -1,11 +1,14 @@
//! Text menu and text editing commands.
use figcraft_doc::*;
use figcraft_doc::{
LetterSpacing, LineHeight, ListStyle, NodeId, NodeKind, TextAlignH, TextAutoResize, TextCase, TextData, TextDecoration, TextPath, TextStyle,
VariableId,
};
use serde_json::{Map, Value, json};
use super::props::{styled_name, weight_name};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{boolean, get, id, num, req_id, req_num, req_string, req_targets, string, targets};
use crate::{Result, Session, bad, spec, textedit};
fn finite(v: f64) -> f64 {
+5 -2
View File
@@ -4,12 +4,15 @@
use std::collections::BTreeSet;
use figcraft_doc::*;
use figcraft_doc::{
CODE_PLATFORMS, Collection, CollectionId, Document, ModeId, NodeId, NodeKind, Variable, VariableId, VariableType, VariableValue, Variables,
normalize_scopes, scope_known,
};
use serde_json::{Value, json};
use super::lists::BoundList;
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{boolean, get, num, req_string, string};
use crate::{Result, Session, bad, spec};
pub fn specs() -> Vec<CommandSpec> {
+2 -2
View File
@@ -4,13 +4,13 @@
use std::collections::{BTreeMap, BTreeSet};
use figcraft_doc::*;
use figcraft_doc::{Collection, CollectionId, Document, Mode, ModeId, Variable, VariableId, VariableValue, Variables, trim_num};
use serde_json::{Value, json};
use super::CommandSpec;
use crate::codegen::{css_var_name, css_var_of};
use crate::library::{is_remote_collection, local_collections};
use crate::params::*;
use crate::params::{get, string};
use crate::{EngineError, Result, Session, bad, spec};
pub fn specs() -> Vec<CommandSpec> {
+2 -2
View File
@@ -1,10 +1,10 @@
//! View menu, preferences and tools. View state is per document and drivable like everything else.
use figcraft_doc::*;
use figcraft_doc::NodeId;
use serde_json::{Value, json};
use super::{CommandSpec, ok};
use crate::params::*;
use crate::params::{boolean, get, num, num_or, req_index, req_num, req_string, string};
use crate::tools::ToolId;
use crate::{EngineError, Result, Session, ViewState, bad, spec};