Merge main into #1026

# Conflicts:
#	docs/translating.md
This commit is contained in:
bflatastic
2026-10-26 10:15:28 +07:00
28 changed files with 3309 additions and 86 deletions
+28 -1
View File
@@ -83,7 +83,7 @@ pub fn specs() -> Vec<CommandSpec> {
spec!(
"image.place",
"Place image",
"{data (base64) | path, x?, y?, width?, height?, parent?, mode? (fill|fit|crop|tile)} → {id, image, width, height, imageProfile (its embedded colour profile; null: taken as sRGB)}",
"{data (base64) | path, x?, y?, width?, height?, parent?, target? (an existing layer: the image is added on top of its fills instead of making a layer), mode? (fill|fit|crop|tile)} → {id, image, width, height, imageProfile (its embedded colour profile; null: taken as sRGB)}",
true,
place_image
),
@@ -394,9 +394,28 @@ fn read_path(path: &str) -> Result<Vec<u8>> {
Err(crate::EngineError::Other(format!("{path}: files can't be read in the browser")))
}
/// Layers that can show an image inside their own outline.
pub fn accepts_image_fill(kind: &NodeKind) -> bool {
kind.is_shape() || kind.is_frame_like() || matches!(kind, NodeKind::Text(_) | NodeKind::BooleanOp { .. })
}
/// Add an image fill on top of the layer's fills: the others keep their place and variable
/// bindings; a linked fill style no longer describes the list, so the link goes.
fn add_image_fill(node: &mut Node, image: String, mode: ImageScaleMode) {
node.fills.push(Paint::image(image, mode));
node.styles.fill = None;
}
/// Place an image as a rectangle with an image fill (Figma's image layer).
pub fn place_image(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "image.place";
let target = opt_id(cmd, p, "target")?;
if let Some(id) = target {
let node = s.doc()?.node(id).ok_or(crate::EngineError::NoNode(id))?;
if !accepts_image_fill(&node.kind) {
return Err(bad(cmd, "`target` must be a layer that can have a fill"));
}
}
let bytes = if let Some(b64) = string(p, "data") {
use base64::Engine as _;
let b64 = b64.split_once(',').map(|(_, d)| d.to_string()).unwrap_or(b64);
@@ -412,6 +431,14 @@ pub fn place_image(s: &mut Session, p: &Value) -> Result<Value> {
"tile" => ImageScaleMode::Tile,
_ => ImageScaleMode::Fill,
};
if let Some(id) = target {
let doc = s.doc_mut()?;
let (hash, _, _) = figcraft_format::add_image(doc, bytes).map_err(|e| bad(cmd, e.to_string()))?;
let profile = figcraft_render::images::profile_name(&hash, doc.images.get(&hash));
let node = doc.get_mut(id)?;
add_image_fill(node, hash.clone(), mode);
return Ok(json!({"id": id, "image": hash, "width": node.width, "height": node.height, "imageProfile": profile}));
}
let page = s.page()?;
let parent = crate::params::opt_id(cmd, p, "parent")?.unwrap_or(page);
let ds = s.ds_mut()?;
+9 -6
View File
@@ -456,16 +456,19 @@ fn outline_stroke(s: &mut Session, p: &Value) -> Result<Value> {
continue;
}
let closed = figcraft_doc::geometry::is_closed(&n);
let Some(geom) = (match &n.kind {
NodeKind::Text(_) | NodeKind::BooleanOp { .. } => figcraft_render::node_outline(doc, &n).map(|(p, _)| p),
_ => figcraft_doc::geometry::stroke_geometry(&n),
let Some((area, rule)) = (match &n.kind {
NodeKind::Text(_) => figcraft_render::node_outline(doc, &n)
.map(|(p, _)| (figcraft_render::draw::stroke_area(&n, &p, closed), figcraft_render::FillRule::Winding)),
_ => figcraft_render::stroke_fill(doc, &n),
}) else {
continue;
};
let area = figcraft_render::draw::stroke_area(&n, &geom, closed);
let mut path = (*area).clone();
// Union the pieces so the outline is one clean shape.
if let Ok(u) = figcraft_pathops::simplify(&path) {
// Simplify non-zero strokes into even-odd contours. Inside/outside rings already use
// even-odd: interpreting their same-direction contours as non-zero would fill the hole.
if rule == figcraft_render::FillRule::Winding
&& let Ok(u) = figcraft_pathops::simplify(&path)
{
path = u;
}
let bb = path.bounding_box();
+2 -2
View File
@@ -1009,8 +1009,8 @@ fn bound_field(n: &Node, field: &str) -> Option<String> {
let cl = &n.child_layout;
let pad = |i: usize| layout.and_then(|l| l.padding.get(i).copied());
Some(match field {
"width" => format!("{}", n.width),
"height" => format!("{}", n.height),
"width" | "w" => format!("{}", n.width),
"height" | "h" => format!("{}", n.height),
"minWidth" => format!("{:?}", cl.min_w),
"maxWidth" => format!("{:?}", cl.max_w),
"minHeight" => format!("{:?}", cl.min_h),