mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 05:46:46 -06:00
@@ -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()?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user