mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
PDF.W1: reconcile shared writer with current main
This commit is contained in:
@@ -265,7 +265,7 @@ fn undo_group_end(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// under an interaction that would then preview on, or cancel to, the document from before (#1719).
|
||||
/// A typing session also ends in the Type tool, whose caret and marked text (IME) must follow the
|
||||
/// document.
|
||||
fn settle_interaction(s: &mut Session) -> Result<()> {
|
||||
pub(crate) fn settle_interaction(s: &mut Session) -> Result<()> {
|
||||
let typing = typing_in_progress(s);
|
||||
s.commit_interaction()?;
|
||||
if typing {
|
||||
|
||||
@@ -13,14 +13,22 @@ pub(super) fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// What an error rolls back besides the documents: session-level state a step may change
|
||||
// (paint defaults, drawing mode, clipboard, the Untitled-N count).
|
||||
let saved = (s.paint.clone(), s.fill_active, s.draw_mode, s.draw_inside, s.clipboard.clone(), s.untitled_counter);
|
||||
let (start, active): (Vec<u64>, _) = (s.docs.iter().map(|d| d.uid).collect(), s.active);
|
||||
// A drag or typing still in progress is a step of its own, as before Undo: the batch's steps
|
||||
// would otherwise run inside it and take its baseline as theirs (#1697).
|
||||
if s.active().is_some() {
|
||||
super::super::edit::settle_interaction(s)?;
|
||||
}
|
||||
// The documents as they are now, whole: what a failed step brings back. Their art, history,
|
||||
// file, format and saved state share their data with the open ones until a step changes them.
|
||||
let start = s.docs.clone();
|
||||
let active = s.active;
|
||||
s.batch_stash = Some(vec![]);
|
||||
let r = run_steps(s, &label, artboard, &mut cmds);
|
||||
let stash = s.batch_stash.take().unwrap_or_default();
|
||||
let results = match r {
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
roll_back(s, &start, stash);
|
||||
roll_back(s, start, &stash);
|
||||
s.active = active;
|
||||
(s.paint, s.fill_active, s.draw_mode, s.draw_inside, s.clipboard, s.untitled_counter) = saved;
|
||||
s.pending_paint = None;
|
||||
@@ -81,22 +89,17 @@ fn run_steps(s: &mut Session, label: &str, artboard: Option<u64>, cmds: &mut [Va
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// The documents as they were before the batch (`start`: their uids in order): the ones it opened
|
||||
/// close, the ones it closed or replaced (`stash`) come back, and each one's interaction is undone.
|
||||
fn roll_back(s: &mut Session, start: &[u64], mut stash: Vec<DocState>) {
|
||||
let mut open = std::mem::take(&mut s.docs);
|
||||
for &uid in start {
|
||||
// The newest revision the document reached, so the restored one redraws.
|
||||
let newest = open.iter().chain(&stash).filter(|d| d.uid == uid).map(|d| d.revision).max().unwrap_or_default();
|
||||
// Its state before the batch: the first one a step closed or replaced, else the open one.
|
||||
let st = match stash.iter().position(|d| d.uid == uid) {
|
||||
Some(i) => Some(stash.remove(i)),
|
||||
None => open.iter().position(|d| d.uid == uid).map(|i| open.remove(i)),
|
||||
};
|
||||
if let Some(mut st) = st {
|
||||
st.undo_interaction();
|
||||
st.revision = newest.saturating_add(1);
|
||||
s.docs.push(st);
|
||||
}
|
||||
/// The documents as they were before the batch (`start`), whatever the steps did to them: opened
|
||||
/// ones close, closed or replaced ones (`stash`) come back, and each has its art, selection,
|
||||
/// history, file, format and saved state again (#1697: undoing the batch's interaction alone left
|
||||
/// a Save As step's new file and an Undo step's history in place). Files the steps wrote stay
|
||||
/// written.
|
||||
fn roll_back(s: &mut Session, start: Vec<DocState>, stash: &[DocState]) {
|
||||
let open = std::mem::take(&mut s.docs);
|
||||
s.docs = start;
|
||||
for st in &mut s.docs {
|
||||
// Past the newest revision the document reached, so the restored one redraws.
|
||||
let newest = open.iter().chain(stash).filter(|d| d.uid == st.uid).map(|d| d.revision).max().unwrap_or(st.revision);
|
||||
st.revision = newest.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,12 +273,17 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
/// own): rasters stay linked, SVG files become art, missing ones placeholders (see
|
||||
/// [`vectorcraft_svg::ImportOptions`]).
|
||||
pub(crate) fn import_svg(text: &str, folder: Option<&str>) -> Result<(Document, Vec<String>)> {
|
||||
// The importer reads on a thread of its own (for its stack): what may be read is what may be
|
||||
// read here.
|
||||
let scope = crate::file_access::ScopeHandle::capture();
|
||||
let read = |path: &str| {
|
||||
// Only files (not folders or devices) are read.
|
||||
file_stamp(path)?;
|
||||
let bytes = read_file(path).ok()?;
|
||||
let link = crate::cmd::links::link_info(path, &bytes);
|
||||
Some((bytes, link))
|
||||
scope.clone().run(|| {
|
||||
// Only files (not folders or devices) are read.
|
||||
file_stamp(path)?;
|
||||
let bytes = read_file(path).ok()?;
|
||||
let link = crate::cmd::links::link_info(path, &bytes);
|
||||
Some((bytes, link))
|
||||
})
|
||||
};
|
||||
vectorcraft_svg::import_with(text, &vectorcraft_svg::ImportOptions { folder, read: Some(&read) }).map_err(err)
|
||||
}
|
||||
@@ -419,7 +424,7 @@ pub(super) fn new_from_template(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// The most memory decoding one image may take: a 60 in print sheet at 300 ppi (18000 × 6600 px)
|
||||
/// is ~475 MB as RGBA, past the decoder's 512 MB default once it is converted. The web app keeps
|
||||
/// that default: wasm32 has 4 GB to address and aborts when an allocation fails.
|
||||
pub(crate) const MAX_RASTER_ALLOC: u64 = if cfg!(target_arch = "wasm32") { 512 << 20 } else { 2 << 30 };
|
||||
pub(crate) use vectorcraft_doc::MAX_RASTER_ALLOC;
|
||||
|
||||
/// What to say of encoded image `bytes` that need more than [`MAX_RASTER_ALLOC`] to decode (its
|
||||
/// header says so; the pixels aren't read): the renderer, which has the same allowance, draws
|
||||
|
||||
@@ -457,6 +457,7 @@ pub(crate) fn unique_name(base: &str, taken: impl Fn(&str) -> bool) -> String {
|
||||
(2..).map(|i| format!("{base} {i}")).find(|n| !taken(n)).unwrap_or_else(|| base.to_string())
|
||||
}
|
||||
|
||||
pub(crate) use edit::{settle_interaction, typing_in_progress};
|
||||
pub(crate) use vectorcraft_doc::range::parse_range;
|
||||
|
||||
/// A date param in Unix seconds that defaults to the clock: `None` when absent (now, see
|
||||
|
||||
@@ -202,7 +202,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Set Bounds",
|
||||
[],
|
||||
None,
|
||||
"{x?, y?, width?, height?, reference?: 0..8 (9-point grid), proportional?, strokes?, corners?, copy?: bool, patterns?: bool (Transform Patterns), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size), objects?: bool (Transform Objects, as object.move)} (Transform panel; width/height and the reference point follow the bounding box, rotated with rotated objects; x, y are page coordinates; with Use Preview Bounds the values measure the visual bounds; strokes/corners as object.scale)",
|
||||
"{x?, y?, width?, height?, reference?: 0..8 (9-point grid), proportional?, strokes?, corners?, copy?: bool, patterns?: bool (Transform Patterns), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size), objects?: bool (Transform Objects, as object.move)} (Transform panel; width/height and the reference point follow the bounding box, rotated with rotated objects; x, y are page coordinates; with Use Preview Bounds the values measure the visual bounds; strokes/corners as object.scale; with anchor points selected (Direct Selection) and no copy, the values are the box around those points, square to the page, and only they move or scale, as with object.move (#1824))",
|
||||
has_selection,
|
||||
set_bounds
|
||||
),
|
||||
@@ -1400,6 +1400,13 @@ fn distribute_spacing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn set_bounds(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = selected_roots(s)?;
|
||||
let st = s.doc()?;
|
||||
if transforms_objects(p)
|
||||
&& !bool_or(p, "copy", false)
|
||||
&& let Some(b) = st.selection.anchor_bounds(&st.doc, s.prefs.use_preview_bounds)
|
||||
{
|
||||
return set_anchor_bounds(s, p, ids, b);
|
||||
}
|
||||
let d = &s.doc()?.doc;
|
||||
// Sizes are measured square to the bounding box, which a rotation turns with the objects:
|
||||
// the math below runs in its frame.
|
||||
@@ -1419,15 +1426,7 @@ fn set_bounds(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let fixed = [all[0] - m[0], all[1] - m[1], all[2] - m[2], all[3] - m[3]];
|
||||
let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4) as usize;
|
||||
let rp = vectorcraft_geom::reference_point(v, refi);
|
||||
let mut w = f64_or(p, "width", v.width());
|
||||
let mut h = f64_or(p, "height", v.height());
|
||||
if bool_or(p, "proportional", false) {
|
||||
if p.get("width").is_some() && v.width() > 0.0 {
|
||||
h = v.height() * w / v.width();
|
||||
} else if p.get("height").is_some() && v.height() > 0.0 {
|
||||
w = v.width() * h / v.height();
|
||||
}
|
||||
}
|
||||
let (w, h) = wanted_size(p, v);
|
||||
// The geometric scale that gives the visual size: margins grow by the strokes' scale `f`. It is
|
||||
// exact for proportional scales of one path; [`Spans::refit`] checks it, and solves for the
|
||||
// scales where it misses.
|
||||
@@ -1477,6 +1476,35 @@ fn set_bounds(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
apply_transform(s, "Transform", ids, frame.conjugate(Affine::translate(to - new_rp) * scale), p, false)
|
||||
}
|
||||
|
||||
/// The `width` and `height` `object.setBounds` asks for a box now `b`: the one not given stays, or
|
||||
/// with `proportional` follows the one given.
|
||||
fn wanted_size(p: &Value, b: Rect) -> (f64, f64) {
|
||||
let mut w = f64_or(p, "width", b.width());
|
||||
let mut h = f64_or(p, "height", b.height());
|
||||
if bool_or(p, "proportional", false) {
|
||||
if p.get("width").is_some() && b.width() > 0.0 {
|
||||
h = b.height() * w / b.width();
|
||||
} else if p.get("height").is_some() && b.height() > 0.0 {
|
||||
w = b.width() * h / b.height();
|
||||
}
|
||||
}
|
||||
(w, h)
|
||||
}
|
||||
|
||||
/// `object.setBounds` with anchors selected with Direct Selection (#1824): the values are those of
|
||||
/// `b`, the box around the anchors (and the objects selected whole) square to the page, and only
|
||||
/// the anchors, their handles with them, and those objects move or scale.
|
||||
fn set_anchor_bounds(s: &mut Session, p: &Value, ids: Vec<NodeId>, b: Rect) -> Result<Value> {
|
||||
let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4) as usize;
|
||||
let rp = vectorcraft_geom::reference_point(b, refi);
|
||||
let (w, h) = wanted_size(p, b);
|
||||
// A single anchor, or anchors in a line, have no size to scale on that side.
|
||||
let ratio = |size: f64, now: f64| if now > 1e-9 { size / now } else { 1.0 };
|
||||
let scale = about(rp, Affine::scale_non_uniform(ratio(w, b.width()), ratio(h, b.height())));
|
||||
let to = Point::new(f64_or(p, "x", rp.x), f64_or(p, "y", rp.y));
|
||||
apply_transform(s, "Transform", ids, Affine::translate(to - rp) * scale, p, true)
|
||||
}
|
||||
|
||||
/// The visual box of `ids` square to `frame`, as [`Document::oriented_bounds`] measures it, with
|
||||
/// symbol instances measured without the strokes in their art
|
||||
/// ([`vectorcraft_doc::SymbolBoxes::without_art_strokes`]).
|
||||
|
||||
@@ -594,7 +594,7 @@ pub(crate) fn outlined_stroke(
|
||||
st: &StrokeLayer,
|
||||
) -> Option<Node> {
|
||||
if let Some(b) = st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name)) {
|
||||
let mut pieces = vectorcraft_brush::stroke_pieces(b, &path.to_bezpath(), st);
|
||||
let mut pieces = vectorcraft_brush::stroke_pieces(d, b, &path.to_bezpath(), st);
|
||||
let art = match pieces.len() {
|
||||
0 => return None,
|
||||
1 => pieces.remove(0),
|
||||
|
||||
@@ -90,9 +90,13 @@ fn map_of(src: &[Arc<Node>]) -> Result<BuilderMap> {
|
||||
}
|
||||
let arrangement = po::shape_builder(&shapes, true);
|
||||
let bounded = |pieces: Vec<po::Shape>| pieces.into_iter().filter_map(|p| p.path.bounds().map(|b| (p, b))).collect();
|
||||
// A Shaper Group's faces are what its shapes fill and what its open paths enclose: the gaps
|
||||
// closed paths enclose are the Shape Builder's only.
|
||||
let open = po::has_open_path(&shapes);
|
||||
let regions = arrangement
|
||||
.regions
|
||||
.into_iter()
|
||||
.filter(|r| open || !r.sources.is_empty())
|
||||
.filter_map(|r| {
|
||||
let bounds = r.path.bounds()?;
|
||||
let path = r.path.to_bezpath();
|
||||
|
||||
@@ -113,4 +113,38 @@ mod tests {
|
||||
s.execute("text.tabs.clear", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("text.tabs.get", &json!({})).unwrap()["stops"], json!([]));
|
||||
}
|
||||
|
||||
/// #1877: a leader set the way the Tabs panel sets it fills the tab's gap with its dots.
|
||||
#[test]
|
||||
fn tab_leader_fills_the_gap_with_its_characters() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 20, "text": "AAA\t20", "size": 12})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.tabs.set", &json!({"stops": [{"position": 200, "align": "left", "leader": ".", "alignOn": "."}]})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let Some(vectorcraft_doc::NodeKind::Text(t)) = st.doc.node(vectorcraft_doc::NodeId(id)).map(|n| &n.kind) else { panic!() };
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
|
||||
let x_of = |b: usize| lay.glyphs.iter().find(|g| g.byte == b).unwrap().origin.x;
|
||||
let (from, to) = (x_of(3), x_of(4));
|
||||
assert!(to - from > 150.0, "{from}..{to}");
|
||||
// Contours drawn inside the gap: only the leader's dots can be there.
|
||||
let mut dots = 0;
|
||||
for g in &lay.glyphs {
|
||||
let mut contours: Vec<kurbo::BezPath> = vec![];
|
||||
for el in g.outline.elements() {
|
||||
match (el, contours.last_mut()) {
|
||||
(kurbo::PathEl::MoveTo(_), _) | (_, None) => contours.push(kurbo::BezPath::from_vec(vec![*el])),
|
||||
(_, Some(c)) => c.push(*el),
|
||||
}
|
||||
}
|
||||
for c in contours {
|
||||
let r = kurbo::Shape::bounding_box(&c);
|
||||
if r.x0 > from - 1e-6 && r.x1 < to + 1e-6 {
|
||||
assert!(r.width() < 3.0 && r.height() < 3.0, "not a dot: {r:?}");
|
||||
dots += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(dots > 30, "{dots} dots in a {} pt gap", to - from);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user