M6: PDF export depth and REST conditional and variable-mode actions (#65)

PDF export
- Vector masks clip to the mask's outline, luminance masks are luminosity
  soft masks (alpha masks as before).
- Background blur: the backdrop the page draws under the layer is rendered,
  blurred and cut to its shape, under the layer's vector content.
- Text hyperlinks become clickable link annotations (one per line and link).
- Frames-to-PDF shares one path (`frames_pdf`): sRGB conversion for Display
  P3 files, hidden frames left out, the file name as the PDF title.
- `figcraft-cli export -o x.pdf` and `convert x.figcraft x.pdf` write the
  page's frames one PDF page each.
- Tests: mask types, backdrops, links, image deduplication, page sizes in
  points, hostile inputs; CLI end to end. Checked by rendering the PDFs back
  to PNG and diffing against the PNG export.

Prototyping
- Model: conditional actions (if / else if / else blocks), set variable mode,
  and expressions (literals, variable references, + - * /, comparisons,
  and/or/not, negate) for set-variable values and conditions; walk helpers
  for nested actions (remap on paste, colours, destination checks, library
  copies and pruning, canvas connections).
- REST import maps CONDITIONAL, SET_VARIABLE_MODE and SET_VARIABLE with
  variable references and EXPRESSION values; unsupported functions (mode
  lookups) skip the action with a warning.
- The presenter runs them in order (`figcraft_engine::proto_run`); the
  Prototype panel summarises conditionals, edits set-variable-mode, and
  `ui.set {protoEditor}` opens an interaction's editor.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BFlat
2026-10-10 10:57:58 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 8dff0089e5
commit 044cfef834
33 changed files with 2155 additions and 171 deletions
+16 -6
View File
@@ -106,13 +106,22 @@ pub fn write_doc(doc: &Document, page: NodeId, out: &str) -> CliResult<()> {
.ok_or_else(|| CliError::Failed("the page is empty: nothing to export".into()))?
.into_bytes(),
"png" | "jpg" | "jpeg" | "webp" => encode(doc, &render_page(doc, page, 1.0)?, &e)?,
"pdf" => figcraft_engine::cmd::file::export_bytes(doc, &children(doc, page), ExportFormat::Pdf, 1.0)
.map_err(|e| CliError::Failed(e.to_string()))?,
"pdf" => page_pdf(doc, page)?,
other => return Err(CliError::Usage(format!("unknown output format `.{other}` (figcraft, json, svg, png, jpg, webp, pdf)"))),
};
write(out, &bytes)
}
/// A page as PDF: its top-level frames one page each, or (without frames) everything on one page.
fn page_pdf(doc: &Document, page: NodeId) -> CliResult<Vec<u8>> {
let r = if doc.top_frames(page).iter().any(|f| doc.node(*f).is_some_and(|n| n.visible)) {
figcraft_engine::cmd::file::frames_pdf(doc, page).map(|(bytes, _)| bytes)
} else {
figcraft_engine::cmd::file::export_bytes(doc, &children(doc, page), ExportFormat::Pdf, 1.0)
};
r.map_err(|e| CliError::Failed(e.to_string()))
}
pub fn info(path: &str, as_json: bool) -> CliResult<ExitCode> {
let (doc, notes) = load_noted(path)?;
let pages: Vec<Value> = doc
@@ -183,11 +192,12 @@ pub fn export(file: &str, out: &str, format: Option<&str>, scale: f64, page: Opt
let single = image_format(&ext(out)).is_some();
let bytes_of = |ids: &[NodeId]| figcraft_engine::cmd::file::export_bytes(&doc, ids, fmt, scale).map_err(|e| CliError::Failed(e.to_string()));
if single {
let ids = match target {
Some(t) => vec![t],
None => children(&doc, page),
let bytes = match target {
Some(t) => bytes_of(&[t])?,
None if fmt == ExportFormat::Pdf => page_pdf(&doc, page)?,
None => bytes_of(&children(&doc, page))?,
};
write(out, &bytes_of(&ids)?)?;
write(out, &bytes)?;
println!("{out}");
return Ok(ExitCode::SUCCESS);
}