M4: live boolean editing, arc fields, shape builder on strokes, one-sided widths (#111)

Boolean operands are editable in place with live results (per-operand operations, cached prefixes so a drag recomputes from the edited operand up); a Design panel Arc section; the shape builder takes strokes and open paths as outlined areas; variable width points can be wider on one side.
This commit is contained in:
BFlat
2026-10-11 10:04:25 +07:00
committed by GitHub
parent 1444754f66
commit 08d91869cb
47 changed files with 2328 additions and 326 deletions
+36
View File
@@ -158,6 +158,7 @@ fn measure(table: &mut Table, col: usize, layers: usize) -> CliResult<()> {
Point::new(bounds.x0 + bounds.width() * fx, bounds.y0 + bounds.height() * fy)
};
table.put(col, "hit test (µs each)", mean(hits, |i| _ = doc.hit_test(page, pt(i), 1.0)) * 1000.0);
measure_boolean_drag(table, col);
let mut s = Session::with_document(doc);
let doc = s.doc().map_err(failed)?.clone();
@@ -240,6 +241,41 @@ fn measure(table: &mut Table, col: usize, layers: usize) -> CliResult<()> {
Ok(())
}
/// Live boolean editing: one drag frame of an operand of a boolean group of 24 overlapping
/// circles, i.e. the group's result recomputed with the operand somewhere new (each frame a new
/// position, as in a real drag). The top operand recombines one step; the bottom one, all 23.
fn measure_boolean_drag(table: &mut Table, col: usize) {
let mut d = Document::new("booleans");
let Some(page) = d.first_page() else { return };
let Ok(b) = d.create(page, NodeKind::BooleanOp { op: figcraft_doc::BoolOp::Union }, "Union", 0.0, 0.0, 0.0, 0.0) else { return };
let kids: Vec<NodeId> = (0..24)
.filter_map(|i| {
let a = f64::from(i) * std::f64::consts::TAU / 24.0;
d.create(
b,
NodeKind::Ellipse { start: 0.0, sweep: std::f64::consts::TAU, inner: 0.0 },
"Circle",
200.0 + 120.0 * a.cos(),
200.0 + 120.0 * a.sin(),
90.0,
90.0,
)
.ok()
})
.collect();
let _ = d.fit_to_children(b);
let frame = |which: usize, k: usize| {
let mut moved = d.clone();
if let Some(n) = kids.get(which).and_then(|id| moved.node_mut(*id)) {
// Distinct offsets per run and column, so no frame is served from the cache.
n.transform = figcraft_doc::geom::Affine::translate((0.37 * (k + 1) as f64 + 1000.0 * col as f64, 0.11 * k as f64)) * n.transform;
}
moved.node(b).and_then(|n| figcraft_render::node_outline(&moved, n)).is_some()
};
table.put(col, "boolean of 24: drag frame, top operand", mean(20, |k| _ = frame(23, k)));
table.put(col, "boolean of 24: drag frame, bottom operand", mean(5, |k| _ = frame(0, 100 + k)));
}
/// Time the hot paths for each document size in `sizes` (layers) and, with the synthetic design
/// system, for each instance count in `instances`; print the tables.
pub fn run(sizes: &[usize], instances: &[usize]) -> CliResult<ExitCode> {