mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 05:46:46 -06:00
* M6: FigJam .jam files open like .fig files (#1342) A .jam is the same archive as a .fig (its canvas header reads fig-jam.), which the .fig reader already took; File > Open and its filter, drag and drop, file.import and the command line now take .jam too, and the home screen shows its thumbnail. FigJam layers come in as the .fig reader makes them (stickies and shapes with text as frames with their generated layers, other FigJam layers as frames or vectors with a note, unknown types left out with a note). Tests use a synthetic fig-jam. canvas and a .jam made from our own export. * M12: a FigJam .jam file opens as a board (#1342) The .fig reader marks a canvas with FigJam's fig-jam. header as FileKind::Board, so a .jam opens with the board toolbar, the dot grid and the board panels and saves as a board; a .fig stays a design file, and importing a .jam into an open file leaves its kind alone. Its objects stay ordinary layers (frames and vectors with the existing import notes). Builds on the .jam opening branch (#1586).
This commit is contained in:
+3
-1
File diff suppressed because one or more lines are too long
@@ -183,7 +183,7 @@ pub(super) fn import(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
"figcraft" | "json" | "fig" | "sketch" | "penpot" => {
|
||||
"figcraft" | "json" | "fig" | "jam" | "sketch" | "penpot" => {
|
||||
let resolver = path.as_deref().map(rest_images);
|
||||
let images: figcraft_format::rest::ImageResolver = match &resolver {
|
||||
Some(r) => r,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//! FigJam `.jam` files (#1342) open and import like `.fig` files: File ▸ Open (`file.open`), the
|
||||
//! command line (the same command) and File ▸ Import / drag and drop (`file.import`). The file is
|
||||
//! made here from our own `.fig` export with the canvas header FigJam writes (`fig-jam.`).
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// A `.jam` holding one page with a rectangle named `Card`, written to a temporary file.
|
||||
fn jam_file(tag: &str) -> std::path::PathBuf {
|
||||
let mut doc = figcraft_doc::Document::new("Synthetic board");
|
||||
let page = doc.first_page().unwrap();
|
||||
doc.create(page, figcraft_doc::NodeKind::Rectangle, "Card", 10.0, 10.0, 80.0, 40.0).unwrap();
|
||||
let (fig, _) = figcraft_format::fig::export(&doc, None).unwrap();
|
||||
let mut from = zip::ZipArchive::new(std::io::Cursor::new(fig)).unwrap();
|
||||
let mut to = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
for i in 0..from.len() {
|
||||
let mut member = from.by_index(i).unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
member.read_to_end(&mut bytes).unwrap();
|
||||
if member.name() == "canvas.fig" {
|
||||
assert!(bytes.starts_with(b"fig-kiwi"));
|
||||
bytes.splice(..8, *b"fig-jam.");
|
||||
}
|
||||
to.start_file(member.name(), zip::write::SimpleFileOptions::default()).unwrap();
|
||||
to.write_all(&bytes).unwrap();
|
||||
}
|
||||
let path = std::env::temp_dir().join(format!("figcraft-jam-{tag}-{}.jam", std::process::id()));
|
||||
std::fs::write(&path, to.finish().unwrap().into_inner()).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
fn cards(s: &Session) -> usize {
|
||||
s.doc().unwrap().nodes().filter(|n| n.name == "Card").count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_jam_file_opens_and_imports_like_a_fig() {
|
||||
let path = jam_file("open");
|
||||
let mut s = Session::new();
|
||||
s.execute("file.open", &json!({"path": path})).unwrap();
|
||||
assert_eq!(cards(&s), 1, "opened");
|
||||
assert!(crate::board::is_board(s.doc().unwrap()), "a whiteboard opens as a board");
|
||||
// Imported into a design file, it adds its pages to it (not an image of its bytes), which
|
||||
// stays a design file.
|
||||
s.execute("doc.switch", &json!({"index": 0})).unwrap();
|
||||
let before = s.doc().unwrap().pages.len();
|
||||
s.execute("file.import", &json!({"path": path})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().pages.len(), before + 1);
|
||||
assert_eq!(cards(&s), 1);
|
||||
assert!(!crate::board::is_board(s.doc().unwrap()));
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
@@ -351,6 +351,8 @@ mod interaction_rollback_tests;
|
||||
#[cfg(test)]
|
||||
mod invite_links_tests;
|
||||
#[cfg(test)]
|
||||
mod jam_open_tests;
|
||||
#[cfg(test)]
|
||||
mod keymap_tests;
|
||||
#[cfg(test)]
|
||||
mod library_path_tests;
|
||||
|
||||
@@ -51,7 +51,7 @@ use std::collections::BTreeMap;
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
|
||||
use figcraft_doc::Document;
|
||||
use figcraft_doc::{Document, FileKind};
|
||||
|
||||
pub use export::export;
|
||||
|
||||
@@ -81,6 +81,9 @@ pub(crate) fn layer_budget(packed: usize) -> usize {
|
||||
(packed / PACKED_BYTES_PER_LAYER).clamp(crate::rest::MAX_NODES, MAX_FIG_NODES)
|
||||
}
|
||||
|
||||
/// The canvas header of a FigJam whiteboard (a `.jam`); a `.fig` has `fig-kiwi`.
|
||||
pub const JAM_HEADER: &[u8] = b"fig-jam.";
|
||||
|
||||
/// Whether `bytes` look like a `.fig` file: a zip holding `canvas.fig`, or a bare canvas.
|
||||
pub fn is_fig(bytes: &[u8]) -> bool {
|
||||
bytes.starts_with(b"fig-") || (bytes.starts_with(b"PK") && zip_has_canvas(bytes))
|
||||
@@ -102,6 +105,8 @@ pub fn import(bytes: &[u8], name: &str) -> Result<(Document, Vec<String>), Forma
|
||||
// The budgets follow the compressed canvas, which the file itself bounds (its zip member may
|
||||
// inflate too).
|
||||
let packed = archive.canvas.len().min(bytes.len());
|
||||
// FigJam writes its whiteboards with this header: they open as boards.
|
||||
let board = archive.canvas.starts_with(JAM_HEADER);
|
||||
// Each stage's input is dropped as soon as the next one is built: in a large file every one
|
||||
// of them takes gigabytes, and holding them all at once runs out of memory.
|
||||
let (schema, message) = decode_canvas(&std::mem::take(&mut archive.canvas))?;
|
||||
@@ -115,7 +120,10 @@ pub fn import(bytes: &[u8], name: &str) -> Result<(Document, Vec<String>), Forma
|
||||
let embedded = embedded_copies(&msg);
|
||||
drop(msg);
|
||||
let images = |r: &str| archive.image(r).or_else(|| embedded.get(r).map(|b| b.to_vec()));
|
||||
let (doc, more) = crate::rest::import_with_networks(&json, &images, networks, max_nodes)?;
|
||||
let (mut doc, more) = crate::rest::import_with_networks(&json, &images, networks, max_nodes)?;
|
||||
if board {
|
||||
doc.kind = FileKind::Board;
|
||||
}
|
||||
// Both stages stop at the same cap and say so in the same words: one note (#981).
|
||||
for w in more.into_iter().filter_map(map::adapt_warning) {
|
||||
let base = w.rsplit_once(" (×").map_or(w.as_str(), |(b, _)| b);
|
||||
|
||||
@@ -4,7 +4,12 @@ use super::*;
|
||||
|
||||
/// The synthetic FigJam schema adds generated layout and text alignment records.
|
||||
fn board_schema() -> Schema {
|
||||
Schema::parse(&SCHEMA.replace(
|
||||
board_schema_of(SCHEMA)
|
||||
}
|
||||
|
||||
/// [`board_schema`] over another base schema.
|
||||
fn board_schema_of(base: &str) -> Schema {
|
||||
Schema::parse(&base.replace(
|
||||
"\nmessage Blob ",
|
||||
", GeneratedData nodeGenerationData, GeneratedData derivedImmutableFrameData, string textAlignHorizontal, string textAlignVertical\nmessage GeneratedData NodeChange[] overrides, uint version\nmessage Blob ",
|
||||
))
|
||||
@@ -97,6 +102,7 @@ fn imported(layer: Kv<'static>) -> (Document, Vec<String>) {
|
||||
fn generated_stickies_and_shapes_keep_their_background_text_and_author() {
|
||||
for ty in ["STICKY", "SHAPE_WITH_TEXT"] {
|
||||
let (doc, warnings) = imported(board_node(ty));
|
||||
assert_eq!(doc.kind, figcraft_doc::FileKind::Design, "the same layers in a .fig stay a design file");
|
||||
let frame = named(&doc, "Board object");
|
||||
assert!(matches!(frame.kind, NodeKind::Frame));
|
||||
assert_eq!(frame.opacity, 0.5);
|
||||
@@ -417,3 +423,33 @@ fn outer_sticky_body_survives_alongside_the_generated_author() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A FigJam `.jam` file (#1342) is the same archive as a `.fig` with a `fig-jam.` header on its
|
||||
/// canvas (synthetic here, never a real file): it imports the same way, as a board, notes and
|
||||
/// shapes with text as their frames, a table as its stand-in, and a layer type the importer doesn't know is
|
||||
/// left out with a note instead of failing the file.
|
||||
#[test]
|
||||
fn a_jam_file_imports_like_a_fig() {
|
||||
let schema = board_schema_of(&SCHEMA.replace("CONNECTOR TABLE", "CONNECTOR TABLE SYNTHETIC_JAM_ONLY"));
|
||||
let layers = vec![
|
||||
node(0, None, "DOCUMENT", "Synthetic board", vec![]),
|
||||
node(1, Some((0, "!")), "CANVAS", "Page", vec![]),
|
||||
board_node("STICKY"),
|
||||
node(3, Some((1, "\"")), "TABLE", "Synthetic table", vec![("size", xy(200.0, 100.0)), ("transform", at(400.0, 0.0))]),
|
||||
node(4, Some((1, "#")), "SYNTHETIC_JAM_ONLY", "Unknown object", vec![("size", xy(50.0, 50.0)), ("transform", at(0.0, 400.0))]),
|
||||
];
|
||||
let msg = o(vec![("nodeChanges", arr(layers)), ("blobs", arr(vec![o(vec![("bytes", Kv::Bytes(triangle_blob()))])]))]);
|
||||
let mut jam = canvas(&schema, &msg);
|
||||
jam.splice(..8, *b"fig-jam.");
|
||||
for bytes in [jam.clone(), zip_file(&jam)] {
|
||||
assert!(is_fig(&bytes), "read as the same archive");
|
||||
let (doc, warnings) = import(&bytes, "Synthetic board").unwrap();
|
||||
assert_eq!(doc.kind, figcraft_doc::FileKind::Board, "a FigJam whiteboard opens as a board");
|
||||
let note = named(&doc, "Board object");
|
||||
assert!(matches!(note.kind, NodeKind::Frame) && note.children.len() == 3, "the sticky with its background, note and author");
|
||||
assert!(matches!(named(&doc, "Synthetic table").kind, NodeKind::Frame));
|
||||
assert!(doc.nodes().all(|n| n.name != "Unknown object"), "left out");
|
||||
assert!(warnings.iter().any(|w| w.contains("table layers import as")), "{warnings:?}");
|
||||
assert!(warnings.iter().any(|w| w.contains("SYNTHETIC_JAM_ONLY")), "a note says what was left out: {warnings:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,8 @@ impl FileKind {
|
||||
let ext = path.extension().map(|e| e.to_string_lossy().to_ascii_lowercase()).unwrap_or_default();
|
||||
match ext.as_str() {
|
||||
"figcraft" => FileKind::FigCraft,
|
||||
"fig" => FileKind::Fig,
|
||||
// A FigJam `.jam` is the same archive.
|
||||
"fig" | "jam" => FileKind::Fig,
|
||||
"sketch" => FileKind::Sketch,
|
||||
"svg" => FileKind::Svg,
|
||||
"png" | "jpg" | "jpeg" | "webp" | "gif" | "bmp" | "tif" | "tiff" => FileKind::Image,
|
||||
|
||||
@@ -459,7 +459,7 @@ status = "draft — needs native review"
|
||||
"Draw" = "رسم"
|
||||
"Draw a frame" = "رسم إطار"
|
||||
"Drawing tools" = "أدوات الرسم"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "أفلت ملف .figcraft أو .fig أو .sketch أو .json أو .svg أو ملف صورة."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "أفلت ملف .figcraft أو .fig أو .jam أو .sketch أو .json أو .svg أو ملف صورة."
|
||||
"Drop to import" = "أفلت للاستيراد"
|
||||
"Drop to open" = "أفلت للفتح"
|
||||
"Drop shadow" = "ظل خارجي"
|
||||
|
||||
@@ -437,7 +437,7 @@ status = "draft — needs native review"
|
||||
"Draw" = "আঁকুন"
|
||||
"Draw a frame" = "একটি ফ্রেম আঁকুন"
|
||||
"Drawing tools" = "অঙ্কন টুলস"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "একটি .figcraft, .fig, .sketch, .json, .svg বা ছবির ফাইল ড্রপ করুন।"
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "একটি .figcraft, .fig, .jam, .sketch, .json, .svg বা ছবির ফাইল ড্রপ করুন।"
|
||||
"Drop to import" = "ইম্পোর্ট করতে ড্রপ করুন"
|
||||
"Drop to open" = "খুলতে ড্রপ করুন"
|
||||
"Drop shadow" = "ড্রপ শ্যাডো"
|
||||
|
||||
@@ -420,7 +420,7 @@ status = "draft — needs native review"
|
||||
"Drag the stroke to add a width point" = "Ziehen Sie an der Kontur, um einen Breitenpunkt hinzuzufügen"
|
||||
"Draw" = "Zeichnen"
|
||||
"Drawing tools" = "Zeichenwerkzeuge"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Legen Sie eine .figcraft-, .fig-, .sketch-, .json-, .svg- oder Bilddatei ab."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Legen Sie eine .figcraft-, .fig-, .jam-, .sketch-, .json-, .svg- oder Bilddatei ab."
|
||||
"Drop to import" = "Zum Importieren ablegen"
|
||||
"Drop to open" = "Zum Öffnen ablegen"
|
||||
"Drop shadow" = "Schlagschatten"
|
||||
|
||||
@@ -461,7 +461,7 @@ name = "English"
|
||||
"Draw" = "Draw"
|
||||
"Draw a frame" = "Draw a frame"
|
||||
"Drawing tools" = "Drawing tools"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Drop a .figcraft, .fig, .sketch, .json, .svg or image file."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file."
|
||||
"Drop to import" = "Drop to import"
|
||||
"Drop to open" = "Drop to open"
|
||||
"Drop shadow" = "Drop shadow"
|
||||
|
||||
@@ -455,7 +455,7 @@ status = "draft — needs native review"
|
||||
"Draw" = "Dibujar"
|
||||
"Draw a frame" = "Dibuja un marco"
|
||||
"Drawing tools" = "Herramientas de dibujo"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Suelta un archivo .figcraft, .fig, .sketch, .json, .svg o de imagen."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Suelta un archivo .figcraft, .fig, .jam, .sketch, .json, .svg o de imagen."
|
||||
"Drop to import" = "Suelta para importar"
|
||||
"Drop to open" = "Suelta para abrir"
|
||||
"Drop shadow" = "Sombra paralela"
|
||||
|
||||
@@ -439,7 +439,7 @@ status = "draft — needs native review"
|
||||
"Draw" = "Piirrä"
|
||||
"Draw a frame" = "Piirrä kehys"
|
||||
"Drawing tools" = "Piirtotyökalut"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Pudota .figcraft-, .fig-, .sketch-, .json-, .svg- tai kuvatiedosto."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Pudota .figcraft-, .fig-, .jam-, .sketch-, .json-, .svg- tai kuvatiedosto."
|
||||
"Drop shadow" = "Heittovarjo"
|
||||
"Drop to import" = "Tuo pudottamalla"
|
||||
"Drop to open" = "Avaa pudottamalla"
|
||||
|
||||
@@ -438,7 +438,7 @@ status = "draft — needs native review"
|
||||
"Draw" = "Dessin"
|
||||
"Draw a frame" = "Tracer un cadre"
|
||||
"Drawing tools" = "Outils de dessin"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Déposez un fichier .figcraft, .fig, .sketch, .json, .svg ou une image."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Déposez un fichier .figcraft, .fig, .jam, .sketch, .json, .svg ou une image."
|
||||
"Drop shadow" = "Ombre portée"
|
||||
"Drop to import" = "Déposer pour importer"
|
||||
"Drop to open" = "Déposer pour ouvrir"
|
||||
|
||||
@@ -418,7 +418,7 @@ status = "draft — needs native review"
|
||||
"Draw" = "Disegna"
|
||||
"Draw a frame" = "Disegna un frame"
|
||||
"Drawing tools" = "Strumenti di disegno"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Rilascia un file .figcraft, .fig, .sketch, .json, .svg o un'immagine."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Rilascia un file .figcraft, .fig, .jam, .sketch, .json, .svg o un'immagine."
|
||||
"Drop to import" = "Rilascia per importare"
|
||||
"Drop to open" = "Rilascia per aprire"
|
||||
"Drop shadow" = "Ombra esterna"
|
||||
|
||||
@@ -443,7 +443,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
|
||||
"Draw" = "ხატვა"
|
||||
"Draw a frame" = "დახატეთ ჩარჩო"
|
||||
"Drawing tools" = "ხატვის ხელსაწყოები"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "ჩააგდეთ .figcraft, .fig, .sketch, .json, .svg ან გამოსახულების ფაილი."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "ჩააგდეთ .figcraft, .fig, .jam, .sketch, .json, .svg ან გამოსახულების ფაილი."
|
||||
"Drop to import" = "ჩააგდეთ იმპორტისთვის"
|
||||
"Drop to open" = "ჩააგდეთ გასახსნელად"
|
||||
"Drop shadow" = "ჩრდილი"
|
||||
|
||||
@@ -1751,7 +1751,7 @@ status = "draft — needs native review"
|
||||
"Dec" = "12월"
|
||||
"Design system starter" = "디자인 시스템 시작 키트"
|
||||
"Draw a frame" = "프레임 그리기"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = ".figcraft, .fig, .sketch, .json, .svg 또는 이미지 파일을 놓으세요."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = ".figcraft, .fig, .jam, .sketch, .json, .svg 또는 이미지 파일을 놓으세요."
|
||||
"Drop to import" = "놓아서 가져오기"
|
||||
"Drop to open" = "놓아서 열기"
|
||||
"Edited {when}" = "{when} 편집"
|
||||
|
||||
@@ -439,7 +439,7 @@ status = "draft — needs native review"
|
||||
"Draw" = "Tekenen"
|
||||
"Draw a frame" = "Teken een frame"
|
||||
"Drawing tools" = "Tekengereedschap"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Laat een .figcraft-, .fig-, .sketch-, .json-, .svg- of afbeeldingsbestand los."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Laat een .figcraft-, .fig-, .jam-, .sketch-, .json-, .svg- of afbeeldingsbestand los."
|
||||
"Drop shadow" = "Slagschaduw"
|
||||
"Drop to import" = "Loslaten om te importeren"
|
||||
"Drop to open" = "Loslaten om te openen"
|
||||
|
||||
@@ -438,7 +438,7 @@ name = "Polski"
|
||||
"Draw" = "Rysuj"
|
||||
"Draw a frame" = "Narysuj ramkę"
|
||||
"Drawing tools" = "Narzędzia rysowania"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Upuść plik .figcraft, .fig, .sketch, .json, .svg lub plik obrazu."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Upuść plik .figcraft, .fig, .jam, .sketch, .json, .svg lub plik obrazu."
|
||||
"Drop to import" = "Upuść, aby zaimportować"
|
||||
"Drop to open" = "Upuść, aby otworzyć"
|
||||
"Drop shadow" = "Cień"
|
||||
|
||||
@@ -1748,7 +1748,7 @@ status = "draft — needs native review"
|
||||
"Dec" = "dez."
|
||||
"Design system starter" = "Ponto de partida para sistema de design"
|
||||
"Draw a frame" = "Desenhe um quadro"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Solte um arquivo .figcraft, .fig, .sketch, .json, .svg ou uma imagem."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Solte um arquivo .figcraft, .fig, .jam, .sketch, .json, .svg ou uma imagem."
|
||||
"Drop to import" = "Solte para importar"
|
||||
"Drop to open" = "Solte para abrir"
|
||||
"Edited {when}" = "Editado {when}"
|
||||
|
||||
@@ -438,7 +438,7 @@ name = "Русский"
|
||||
"Draw" = "Рисование"
|
||||
"Draw a frame" = "Нарисовать Frame"
|
||||
"Drawing tools" = "Инструменты рисования"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Перетащите файл .figcraft, .fig, .sketch, .json, .svg или изображение."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Перетащите файл .figcraft, .fig, .jam, .sketch, .json, .svg или изображение."
|
||||
"Drop shadow" = "Внешняя тень"
|
||||
"Drop to import" = "Отпустите, чтобы импортировать"
|
||||
"Drop to open" = "Отпустите, чтобы открыть"
|
||||
|
||||
@@ -453,7 +453,7 @@ name = "Svenska"
|
||||
"Draw" = "Rita"
|
||||
"Draw a frame" = "Rita en frame"
|
||||
"Drawing tools" = "Ritverktyg"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Släpp en .figcraft-, .fig-, .sketch-, .json- eller .svg-fil eller en bildfil."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Släpp en .figcraft-, .fig-, .jam-, .sketch-, .json- eller .svg-fil eller en bildfil."
|
||||
"Drop to import" = "Släpp för att importera"
|
||||
"Drop to open" = "Släpp för att öppna"
|
||||
"Drop shadow" = "Slagskugga"
|
||||
|
||||
@@ -437,7 +437,7 @@ status = "draft — needs native review"
|
||||
"Draw" = "วาด"
|
||||
"Draw a frame" = "วาดเฟรม"
|
||||
"Drawing tools" = "เครื่องมือวาด"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "วางไฟล์ .figcraft, .fig, .sketch, .json, .svg หรือรูปภาพ"
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "วางไฟล์ .figcraft, .fig, .jam, .sketch, .json, .svg หรือรูปภาพ"
|
||||
"Drop to import" = "วางเพื่อนำเข้า"
|
||||
"Drop to open" = "วางเพื่อเปิด"
|
||||
"Drop shadow" = "เงาตกกระทบ"
|
||||
|
||||
@@ -419,7 +419,7 @@ status = "draft — needs native review"
|
||||
"Draw" = "Vẽ"
|
||||
"Draw a frame" = "Vẽ khung"
|
||||
"Drawing tools" = "Công cụ vẽ"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Thả tệp .figcraft, .fig, .sketch, .json, .svg hoặc hình ảnh vào đây."
|
||||
"Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file." = "Thả tệp .figcraft, .fig, .jam, .sketch, .json, .svg hoặc hình ảnh vào đây."
|
||||
"Drop shadow" = "Đổ bóng"
|
||||
"Drop to import" = "Thả để nhập"
|
||||
"Drop to open" = "Thả để mở"
|
||||
|
||||
@@ -75,7 +75,7 @@ impl FigApp {
|
||||
let exts: &[&str] = if place {
|
||||
&["png", "jpg", "jpeg", "webp", "gif", "tif", "tiff"]
|
||||
} else {
|
||||
&["penpot", "svg", "png", "jpg", "jpeg", "webp", "gif", "tif", "tiff", "figcraft", "fig", "sketch"]
|
||||
&["penpot", "svg", "png", "jpg", "jpeg", "webp", "gif", "tif", "tiff", "figcraft", "fig", "jam", "sketch"]
|
||||
};
|
||||
let title = if place { i18n::tr("Place image") } else { i18n::tr("Import") };
|
||||
self.pick_file(title, exts, true, |app, file| {
|
||||
|
||||
@@ -183,7 +183,7 @@ fn dropped(app: &mut FigApp, ctx: &egui::Context) {
|
||||
let files = ctx.input(|i| i.raw.dropped_files.clone());
|
||||
for f in files {
|
||||
if !crate::opens(f.path()) {
|
||||
app.toast(tr("Drop a .figcraft, .fig, .sketch, .json, .svg or image file."));
|
||||
app.toast(tr("Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file."));
|
||||
continue;
|
||||
}
|
||||
crate::pending_open::open_dropped(app, &f);
|
||||
|
||||
@@ -898,7 +898,7 @@ fn dropping_an_unsupported_file_on_the_home_screen_opens_nothing() {
|
||||
drop_file(&mut u, &path);
|
||||
assert!(u.app.session.docs.is_empty());
|
||||
assert!(u.app.ui.home);
|
||||
assert_eq!(u.app.ui.toast.as_ref().map(|t| t.0.as_str()), Some("Drop a .figcraft, .fig, .sketch, .json, .svg or image file."));
|
||||
assert_eq!(u.app.ui.toast.as_ref().map(|t| t.0.as_str()), Some("Drop a .figcraft, .fig, .jam, .sketch, .json, .svg or image file."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -907,3 +907,13 @@ fn sketch_files_open_and_drop_as_documents() {
|
||||
assert!(crate::opens(p));
|
||||
assert!(crate::is_document(p));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_jam_file_is_a_document_open_and_drops_take() {
|
||||
// FigJam `.jam` files (#1342) open like `.fig` files, from File ▸ Open, a drop or the
|
||||
// command line, and a drop on the canvas opens one as a file of its own.
|
||||
for name in ["Board.jam", "BOARD.JAM"] {
|
||||
let path = std::path::Path::new(name);
|
||||
assert!(crate::open::opens(path) && crate::open::is_document(path), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,19 @@ use super::{OsStr, Path, theme};
|
||||
|
||||
/// The file types File ▸ Open and a drop on the home screen open.
|
||||
pub(crate) const OPEN_EXTENSIONS: &[&str] =
|
||||
&["figcraft", "fig", "penpot", "sketch", "json", "svg", "png", "jpg", "jpeg", "webp", "gif", "tif", "tiff"];
|
||||
&["figcraft", "fig", "jam", "penpot", "sketch", "json", "svg", "png", "jpg", "jpeg", "webp", "gif", "tif", "tiff"];
|
||||
|
||||
/// Whether `path` names a file type that opens as a document (the extension, in any case).
|
||||
pub(crate) fn opens(path: &Path) -> bool {
|
||||
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`, `.penpot`, `.sketch`, file JSON) rather than an image or SVG:
|
||||
/// Whether `path` is a whole document (`.figcraft`, `.fig`, `.jam`, `.penpot`, `.sketch`, 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.
|
||||
pub(crate) fn is_document(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.is_some_and(|e| ["figcraft", "fig", "penpot", "sketch", "json"].iter().any(|x| x.eq_ignore_ascii_case(e)))
|
||||
.is_some_and(|e| ["figcraft", "fig", "jam", "penpot", "sketch", "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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Boards parity
|
||||
|
||||
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-29 · **Change:** minor (board grid settings: dots, lines, graph paper or none, spacing, size and contrast; the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); templates and tables; facilitation: a shared timer and voting sessions; the dot grid, dragging a connector's end to re-attach it, curved connectors, elbows that go around what they join) · **Target:** the incumbent's whiteboard product (behaviour from its public help centre)
|
||||
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-30 · **Change:** minor (a FigJam `.jam` file opens as a board (#1342); board grid settings: dots, lines, graph paper or none, spacing, size and contrast; the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); templates and tables; facilitation: a shared timer and voting sessions; the dot grid, dragging a connector's end to re-attach it, curved connectors, elbows that go around what they join) · **Target:** the incumbent's whiteboard product (behaviour from its public help centre)
|
||||
|
||||
**Breadth ~47%, ready ~29% (estimated, erring low).** A board is a FigCraft file of its own kind
|
||||
(`file.newBoard`) with a dot grid on the page, sticky notes, shapes with text, connectors that
|
||||
@@ -57,12 +57,13 @@ all end with the same geometry.
|
||||
| Audio and huddles | missing | |
|
||||
| AI features (summaries, sorting stickies, generating boards) | missing | |
|
||||
| Grid on the page | done | the canvas render draws it after the page colour and before the layers (`figcraft_render::Dots`), so objects cover it. Dots by default, or lines, graph paper (a strong line every 2–12 cells over fainter ones, like Japanese section paper) or none; spacing (4–512), mark size and contrast are settings kept with the preferences (`board.grid`; Preferences ▸ Board grid). Zoomed out it thins out (dots and lines by halves, graph paper down to its strong lines) until the marks are at least 14 points apart (10 for lines), the marks the next step drops fading out first; dots stay about a point and a half across on screen (1 to 3 points), scaled by the size setting. Dark marks on a light page, light ones on a dark page, from the theme's ink; written straight into the image, no allocation per mark, a crossing of two lines blended once; never in exports. FigCraft's own settings: the incumbent's board grid has none |
|
||||
| `.jam` import and export | missing | see [`file-format-parity.md`](file-format-parity.md) |
|
||||
| `.jam` import and export | **partial**: a `.jam` opens as a board, best effort (#1342): its stickies and shapes with text come in as frames with their generated layers and its connectors as vectors, not as native board objects; no export | see [`file-format-parity.md`](file-format-parity.md) |
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | M12, #1342: a FigJam `.jam` file opens as a board, our whiteboard file kind, instead of a design file: the `.fig` reader marks a canvas with FigJam's `fig-jam.` header as `FileKind::Board`, so the file opens with the board toolbar, the dot grid and the board panels, and saves as a board. A `.fig` stays a design file, and `file.import` of a `.jam` adds its pages to the open file without changing its kind. Its stickies and shapes with text are still frames with their generated layers and its connectors vectors, not native board objects. Tests: `board_import_tests.rs` (the kind, `.jam` against `.fig`), `jam_open_tests.rs` (open as a board, import into a design file). |
|
||||
| 2026-10-29 | minor | M12: board grid settings (`board.grid`): dots, lines, graph paper or none, with spacing, size and contrast, in Preferences; the default grid looks as before. Numbers unchanged (beyond the incumbent's behaviour) |
|
||||
| 2026-10-29 | minor | M12, #1387 (connector tool): while a connector is dragged out with the connector tool, the canvas shows the connector itself as it will be made (its straight, elbow or curved route, line, weight and arrowhead, built by the same code as `board.connector`), not a thin line; the object under the pointer is outlined with its four attach points (the middles of its sides), the one the end goes to ringed; within 10 px of one the end keeps that side (`fromSide`/`toSide`, and `side` when re-attaching). Hovering with the tool shows the attach points too. Escape drops the preview; releasing makes the connector in one undo step. Tests: `board_connector_tests`. Numbers unchanged |
|
||||
| 2026-10-24 | minor | M12: the dot grid on board pages (a render pass under the layers), dragging a selected connector's end onto another object or empty canvas (`board.reattach`), curved connectors, and elbows that go around the objects they join. Breadth ~35% → ~37%, ready ~20% → ~21%, ~30–60 h → ~28–56 h |
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -358,7 +358,7 @@ a later step can use `{"$step": 0, "field": "ids/0"}` to target the first match.
|
||||
| `select {ids}` | Replace the selection (`[]` deselects). |
|
||||
| `export {ids, format, scale \| size, path?, bleed?, cropMarks?, bleedMarks?, cmyk?, contentsOnly?, includeBoundingBox?, svgOutlineText?, svgIdAttribute?, svgSimplifyStroke?, vdCompat?}` | png, jpg, svg, webp, pdf, xml (an Android Vector Drawable: `svgIdAttribute` writes `android:name`, `vdCompat` is `bestEffort` (default: approximated with a warning), `exact` (left out with a warning) or `ignore` (no warnings: overlapping layer opacity dropped, even-odd fills kept as `fillType="evenOdd"`, diamond gradients left out), and the result lists `warnings` for what was changed or left out), or `fig`: the whole file as `.fig`, experimental (see Exporting `.fig` files below); `size` is `2x`, `512w` or `512h`. The image covers what the layers paint (shadows towards their offset, strokes, unclipped children, a mask group's mask; hidden layers take no room; a slice is its area with everything under it, or only the frame or group it sits in while `contentsOnly` is on); `contentsOnly: false` also draws every other layer showing in that area, `includeBoundingBox` exports the layers' own boxes; SVG: `svgOutlineText: false` keeps `<text>`, `svgIdAttribute` writes unique ids from layer names, `svgSimplifyStroke: false` writes every stroke as an outline. The same options go in export settings (`export.add` / `export.update`). PDF: frames one page each (sized in points), anything else together on one page; print options: `bleed` (points or a length like `"3mm"`, `"0.125in"`), crop and bleed marks, CMYK colour (see Exports below), also taken by `file.export`, `file.exportLayers`, `file.exportFramesPdf` and stored per export setting by `export.add` / `export.update`; `run_command` `file.exportFramesPdf {page?, path?}` writes every top-level frame of a page. Without `path`, images come back as MCP image content, SVG as text, XML as text followed by `{format, bytes, warnings}`. Layers' own export settings: `run_command` with `export.add` / `export.update {index, size?, suffix?, format?}` / `export.remove` and `file.exportLayers {ids?, dir?, assets?}` (every layer once per setting, named `<layer><suffix>.<format>`, a slash in a layer name making a folder; `assets: true` exports the Inspect tab's assets instead: icons as SVG, images as PNG). |
|
||||
| `screenshot {ids?, scale?, maxSize?, window?}` | PNG of the page (with background) or of layers, scaled to fit `maxSize` (default 1600). `window: true` captures the app window in front when attached, a window id (see Windows below) that window. |
|
||||
| `open_file {path, variables?, reload?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.fig` (best effort, see below), `.sketch` (Sketch 48 and newer, best effort), `.svg` (see Importing SVG below) or an image; save as `.figcraft`. `open_file` returns `opened` (for a file opened anew, a `summary` of `name`, `pages`, `layers` and `images`, and `warnings` or `repairs`; a file that was already open gives its `index`, with `stale: true` when the file changed on disk since that tab read or saved it; `reload: true` reads it again into the tab, refused while the tab has unsaved changes) and the document's `document` outline. |
|
||||
| `open_file {path, variables?, reload?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.fig` or FigJam `.jam` (best effort, see below), `.sketch` (Sketch 48 and newer, best effort), `.svg` (see Importing SVG below) or an image; save as `.figcraft`. `open_file` returns `opened` (for a file opened anew, a `summary` of `name`, `pages`, `layers` and `images`, and `warnings` or `repairs`; a file that was already open gives its `index`, with `stale: true` when the file changed on disk since that tab read or saved it; `reload: true` reads it again into the tab, refused while the tab has unsaved changes) and the document's `document` outline. |
|
||||
| `undo` / `redo` | Step through history. |
|
||||
| `pointer {kind, x, y, to?, steps?, tool?, shift, alt, cmd, pressure?, pressureTo?}` | Mouse gestures in page coordinates with the current tool: `click`, `double`, `drag`, `down`, `move`, `up`, `hover`. A dragged layer drops into the frame or auto layout slot under the pointer, except one inside a group, a boolean group or an instance (outside its slots), which only moves. `pressure` (0–1) is pen pressure; a drag goes from `pressure` to `pressureTo` (see Brushes and pen pressure). |
|
||||
| `key {key, shift, alt, cmd}` | A shortcut or key as if typed on the canvas (every default shortcut, tool keys, Escape, Delete, arrows, number keys for opacity, arrows that pan the view when nothing is selected; while editing text, the text shortcuts and then text editing keys). Keys are names like `A`, `0`, `Enter`, `ArrowLeft`, `=`, `/`, or egui's (`Plus`, `Questionmark`); `cmd` is Cmd on macOS and Ctrl elsewhere. |
|
||||
|
||||
+3
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user