mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 11:26:50 -06:00
Merge pull request #1355 from storytold/fix/r3-followups
Review fixes for #1184, #919 and #1134
This commit is contained in:
+3
-2
@@ -6,8 +6,9 @@ CRAFT_FONTS_DIR = { value = "../craft-fonts", relative = true }
|
||||
# The Windows UI Automation adapter can be asked about a node that disappeared in the
|
||||
# same frame. On MinGW, converting that HRESULT through windows-result's thread COM
|
||||
# error-info path has crashed in GetErrorInfo. Keep the HRESULT without that extra
|
||||
# COM lookup; this leaves the AccessKit tree and screen-reader support enabled.
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
# COM lookup; this leaves the AccessKit tree and screen-reader support enabled. Every Windows GNU
|
||||
# target (x86_64, i686, the gnullvm ones) gets it, as the check in apps/figcraft/src/main.rs asks.
|
||||
[target.'cfg(all(windows, target_env = "gnu"))']
|
||||
rustflags = ["--cfg=windows_slim_errors"]
|
||||
|
||||
[alias]
|
||||
|
||||
+1
-5
@@ -1,10 +1,5 @@
|
||||
# FigCraft Roadmap
|
||||
|
||||
M9 / M7.4 server memory follow-up: read-only disconnects retain their durable client-id journal
|
||||
without rebuilding the image-bearing snapshot. Snapshot encoding appends into its header buffer,
|
||||
avoiding a second full payload copy while preserving the existing disk format. See
|
||||
[`docs/server-memory-retention.md`](docs/server-memory-retention.md).
|
||||
|
||||
**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents)
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
@@ -341,6 +336,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-25 | minor | egui 0.37 capture lifecycle: screenshots with hidden or unknown visibility queue, request a hidden paint, and expire after 60 seconds if no capture arrives (headless-tested contract). Logic-only edits, saves and inspection continue while hidden. Native visible GUI and forced hidden-window paint, browser and Wayland behavior remain unqualified; parity percentages are unchanged. |
|
||||
| 2026-10-24 | minor | Server memory (#1134): when the last person leaves a file nobody edited, figcraft-server keeps the client ids it issued in the log instead of rewriting the snapshot (compaction still follows `compact_after`/`compact_mb` and edits), and a snapshot is encoded straight into its file buffer, without a second copy of the body; the file format is unchanged. Numbers unchanged |
|
||||
| 2026-10-24 | minor | Dev Mode tail (M6, gap 16): Measurements kept on the canvas (gap 16): `devMode.addMeasurement {from?, to?, side?}` places one between two layers, along one side of a layer or (neither) to the layer around it, stored on the layer it starts from (saved in `.figcraft`, synced, undoable; old files open without them); Dev Mode draws them as labelled redlines that follow the layers, the Inspect tab lists them (those of the selected layer, or every one on the page), places and removes them; `devMode.removeMeasurement` and `devMode.measurements` remove and list them; a copy keeps those to layers copied with it. The layers panel is read-only in Dev Mode: rows select and expand, but don't rename, reorder, hide or lock. Dev Mode 97% → 98% breadth, 76% → 78% ready, 2–4 h → 1–2 h; headline numbers unchanged at this rounding |
|
||||
| 2026-10-24 | minor | Boards (M12): a file kind of its own (`file.newBoard`, saved in `.figcraft`, carried to live-session joiners) with sticky notes, shapes with text, connectors that re-route when what they join moves (an engine recompute, the same for local edits, undo and others' edits), marker, highlighter and eraser, stamps with the placer's name, a board toolbar and a contextual bar; prototyping and Dev Mode refuse on boards; design files and boards have their own marks on tabs and file rows. Boards row added outside the weighted numbers; headline numbers unchanged at this rounding |
|
||||
| 2026-10-15 | minor | Auto-save (#305, #296): Preferences ▸ Save files automatically (`pref.autoSave`, off by default; desktop only, the browser has no files to save to) saves each open file that has a path 3 s after its last change, so steady editing doesn't save every few seconds, and never during a gesture, a live text preview or while a version is shown. The file is encoded on a thread of its own and written as of that moment (edits made meanwhile stay unsaved); a failed save says why in a toast and is tried again a minute later, and version history checkpoints keep coming with it on. Numbers unchanged |
|
||||
|
||||
@@ -135,7 +135,7 @@ pub fn encode_snapshot(generation: u64, state: &SessionState) -> Result<Vec<u8>,
|
||||
out.extend_from_slice(&0u32.to_le_bytes());
|
||||
let mut out = message::encode_into(state, out).map_err(|e| e.to_string())?;
|
||||
let sum = crc(&out[SNAP_HEADER..]);
|
||||
out[13..SNAP_HEADER].copy_from_slice(&sum.to_le_bytes());
|
||||
out[SNAP_HEADER - 4..SNAP_HEADER].copy_from_slice(&sum.to_le_bytes());
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
|
||||
@@ -253,3 +253,23 @@ fn stroke_only_text_operands_use_glyphs_instead_of_the_text_box() {
|
||||
text.characters = " ".into();
|
||||
assert!(outline(&d, b).elements().is_empty(), "spaces have no stroked glyphs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded_side_borders_follow_their_corners_in_booleans() {
|
||||
use figcraft_doc::StrokeAlign;
|
||||
use figcraft_geom::Point;
|
||||
let mut d = Document::new("rounded borders");
|
||||
let page = d.first_page().unwrap();
|
||||
let b = d.create(page, NodeKind::BooleanOp { op: BoolOp::Union }, "B", 0.0, 0.0, 100.0, 100.0).unwrap();
|
||||
let id = d.create(b, NodeKind::Rectangle, "operand", 0.0, 0.0, 100.0, 100.0).unwrap();
|
||||
let n = d.get_mut(id).unwrap();
|
||||
n.fills.clear();
|
||||
n.strokes = vec![Paint::solid(Color::BLACK)];
|
||||
n.stroke.align = StrokeAlign::Inside;
|
||||
n.stroke.sides = Some([8.0; 4]);
|
||||
n.corners.radii = [30.0; 4];
|
||||
let p = outline(&d, b);
|
||||
assert_ne!(p.winding(Point::new(50.0, 4.0)), 0, "top border");
|
||||
assert_eq!(p.winding(Point::new(3.0, 3.0)), 0, "the rounded corner leaves the square's tip out");
|
||||
assert_eq!(p.winding(Point::new(50.0, 50.0)), 0, "hollow inside");
|
||||
}
|
||||
|
||||
+26
-21
@@ -156,19 +156,26 @@ pub(crate) fn hash_path(p: &BezPath, h: &mut impl Hasher) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-side strokes on rectangles and frames (a ring under non-zero winding too), if `n` has them.
|
||||
fn side_strokes(n: &Node) -> Option<Arc<BezPath>> {
|
||||
let props = &n.stroke;
|
||||
let sides = props.sides?;
|
||||
if !matches!(n.kind, NodeKind::Rectangle | NodeKind::Frame | NodeKind::Component { .. } | NodeKind::Instance { .. }) {
|
||||
return None;
|
||||
}
|
||||
let c = &n.corners;
|
||||
Some(Arc::new(if c.uniform() == Some(0.0) {
|
||||
figcraft_pathops::rect_side_strokes(n.width, n.height, sides, props.align)
|
||||
} else {
|
||||
figcraft_pathops::rounded_side_strokes(n.width, n.height, c.radii, c.smoothing, sides, props.align)
|
||||
}))
|
||||
}
|
||||
|
||||
/// The stroke area of a node in local coordinates.
|
||||
pub fn stroke_area(n: &Node, path: &BezPath, closed: bool) -> Arc<BezPath> {
|
||||
let props = &n.stroke;
|
||||
// Per-side strokes on rectangles/frames.
|
||||
if let Some(sides) = props.sides
|
||||
&& matches!(n.kind, NodeKind::Rectangle | NodeKind::Frame | NodeKind::Component { .. } | NodeKind::Instance { .. })
|
||||
{
|
||||
let c = &n.corners;
|
||||
return Arc::new(if c.uniform() == Some(0.0) {
|
||||
figcraft_pathops::rect_side_strokes(n.width, n.height, sides, props.align)
|
||||
} else {
|
||||
figcraft_pathops::rounded_side_strokes(n.width, n.height, c.radii, c.smoothing, sides, props.align)
|
||||
});
|
||||
if let Some(p) = side_strokes(n) {
|
||||
return p;
|
||||
}
|
||||
// Rect/ellipse inside/outside strokes without booleans: an even-odd ring of two outlines.
|
||||
if props.dashes.is_empty() && props.profile().is_none() && props.brush.is_none() && closed && props.align != StrokeAlign::Center {
|
||||
@@ -212,15 +219,15 @@ pub fn stroke_area(n: &Node, path: &BezPath, closed: bool) -> Arc<BezPath> {
|
||||
cached_stroke_outline(n, path, closed)
|
||||
}
|
||||
|
||||
/// A non-zero stroke outline, shared by painting and boolean operands.
|
||||
/// The stroke a boolean operand contributes: like [`stroke_area`], but never an even-odd ring
|
||||
/// (boolean folding uses non-zero winding).
|
||||
fn operand_stroke(n: &Node, path: &BezPath, closed: bool) -> Arc<BezPath> {
|
||||
side_strokes(n).unwrap_or_else(|| cached_stroke_outline(n, path, closed))
|
||||
}
|
||||
|
||||
/// A non-zero stroke outline (cached), shared by painting and boolean operands.
|
||||
fn cached_stroke_outline(n: &Node, path: &BezPath, closed: bool) -> Arc<BezPath> {
|
||||
let props = &n.stroke;
|
||||
if let Some(sides) = props.sides
|
||||
&& matches!(n.kind, NodeKind::Rectangle | NodeKind::Frame | NodeKind::Component { .. } | NodeKind::Instance { .. })
|
||||
&& n.corners.uniform() == Some(0.0)
|
||||
{
|
||||
return Arc::new(figcraft_pathops::rect_side_strokes(n.width, n.height, sides, props.align));
|
||||
}
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
hash_path(path, &mut h);
|
||||
(props.weight.to_bits(), props.align as u8, props.cap_start as u8, props.cap_end as u8, props.join as u8, props.miter_limit.to_bits(), closed)
|
||||
@@ -334,7 +341,7 @@ fn subtree_outline_at(doc: &Document, n: &Node, depth: usize, stroke_only: bool)
|
||||
if n.fills.iter().any(|f| f.visible) {
|
||||
out.extend(p.elements().iter().copied());
|
||||
} else if stroke_only && n.has_visible_stroke() {
|
||||
out.extend(cached_stroke_outline(n, &p, true).elements().iter().copied());
|
||||
out.extend(operand_stroke(n, &p, true).elements().iter().copied());
|
||||
}
|
||||
}
|
||||
for c in n.children.iter().filter_map(|c| doc.node(*c)) {
|
||||
@@ -352,9 +359,7 @@ fn subtree_outline_at(doc: &Document, n: &Node, depth: usize, stroke_only: bool)
|
||||
let (path, _) = node_outline_at(doc, n, depth)?;
|
||||
let path = if matches!(n.kind, NodeKind::Text(_)) { path } else { figcraft_doc::geometry::stroke_geometry(n).unwrap_or(path) };
|
||||
let closed = figcraft_doc::geometry::is_closed(n) || matches!(n.kind, NodeKind::BooleanOp { .. });
|
||||
// Boolean folding uses non-zero winding; the canvas stroke cache also contains
|
||||
// even-odd rectangle rings, so use the winding-correct stroke outline here.
|
||||
Some(((*cached_stroke_outline(n, &path, closed)).clone(), FillRule::Winding))
|
||||
Some(((*operand_stroke(n, &path, closed)).clone(), FillRule::Winding))
|
||||
}
|
||||
_ => node_outline_at(doc, n, depth),
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Server memory follow-up
|
||||
|
||||
Joining a persisted session issues a client id and makes its journal dirty, even if nobody edits
|
||||
the document. Previously, the last client leaving caused a full snapshot rewrite for that id.
|
||||
For image-heavy documents this creates large temporary allocations and unnecessary disk writes;
|
||||
an allocator can retain the freed memory after the room closes.
|
||||
|
||||
On idle, the server now flushes the journal and only compacts when the document sequence is newer
|
||||
than the stored snapshot, or when flushing failed and a snapshot must be attempted as a fallback.
|
||||
Issued ids and refusals remain durable in the journal. Existing size/count compaction thresholds
|
||||
remain in effect, and edited documents still compact when the last participant leaves.
|
||||
|
||||
Snapshot encoding also appends the versioned postcard body directly to the snapshot header's
|
||||
buffer, then fills the checksum. It no longer holds a separate complete encoded body alongside
|
||||
the finished snapshot. The bytes, generation, checksum and file format are unchanged.
|
||||
|
||||
Regression coverage uses synthetic documents: six read-only sessions across store reopenings
|
||||
must preserve increasing client ids without rewriting the snapshot; exact legacy framing and
|
||||
checksum must match; the binary message body must be unchanged when appended to a prefix.
|
||||
Existing crash recovery, damaged-input and edit compaction tests continue to apply.
|
||||
|
||||
This reduces unnecessary temporary allocations. It does not promise that the operating system
|
||||
immediately reclaims every freed allocation, or that all large-document memory costs are solved.
|
||||
+2
-2
@@ -336,8 +336,8 @@ the address it came from and when it was last opened (for expiry).
|
||||
beside it, flushed, then renamed over the old one. The log is only ever appended to, and synced
|
||||
before the server tells anyone about the change.
|
||||
- **Compaction** writes the file's state as the next snapshot and starts an empty log, after
|
||||
`compact_after` entries or `compact_mb` of log, when the last person leaves, and when the server
|
||||
stops. A log left over from a compaction cut short is replayed over the snapshot, which already
|
||||
`compact_after` entries or `compact_mb` of log, when the last person leaves a file that was
|
||||
edited (the ids of people who only joined stay in the log), and when the server stops. A log left over from a compaction cut short is replayed over the snapshot, which already
|
||||
holds it all, so nothing changes. If a damaged generation field made a current log look like such
|
||||
a leftover, its edits the snapshot lacks are recovered the same way and the damage is reported.
|
||||
- **Damage is skipped and reported, never deleted.** At startup a folder whose description, links
|
||||
|
||||
Reference in New Issue
Block a user