Merge pull request #3292 from dotCooCoo/fix/command-integers-refuse-values-past-usize

Commands: an index of 2^32 or more no longer wraps around on 32-bit builds
This commit is contained in:
BFlat
2026-10-22 00:01:15 +07:00
committed by GitHub
51 changed files with 368 additions and 130 deletions
+37 -6
View File
@@ -60,7 +60,9 @@ pub fn convert_radii_relative(live: &mut LiveShape, relative: bool) {
}
}
match live {
LiveShape::Rectangle { radii_relative: rel, .. } | LiveShape::Polygon { radii_relative: rel, .. } | LiveShape::Path { radii_relative: rel, .. } => {
LiveShape::Rectangle { radii_relative: rel, .. }
| LiveShape::Polygon { radii_relative: rel, .. }
| LiveShape::Path { radii_relative: rel, .. } => {
*rel = relative;
}
LiveShape::Ellipse { .. } | LiveShape::Line { .. } => {}
@@ -290,7 +292,14 @@ mod tests {
#[test]
fn rectangle_corners_map_to_their_anchors() {
let l = LiveShape::Rectangle { w: 100.0, h: 50.0, radii: [0.0, 10.0, 0.0, 0.0], kinds: Default::default(), radii_relative: false, xf: Affine::IDENTITY };
let l = LiveShape::Rectangle {
w: 100.0,
h: 50.0,
radii: [0.0, 10.0, 0.0, 0.0],
kinds: Default::default(),
radii_relative: false,
xf: Affine::IDENTITY,
};
let path = l.to_path();
let c = LiveCorners::new(&path, Some(&l)).unwrap();
// Anchors: top-left, the top-right corner's two, bottom-right, bottom-left.
@@ -333,7 +342,8 @@ mod tests {
/// A polygon's corners are cut in the document and survive a change of sides when shared.
#[test]
fn polygon_corners_follow_their_sides() {
let mut l = LiveShape::Polygon { radius: 50.0, sides: 6, xf: Affine::translate((100.0, 100.0)), radii: vec![], kinds: vec![], radii_relative: false };
let mut l =
LiveShape::Polygon { radius: 50.0, sides: 6, xf: Affine::translate((100.0, 100.0)), radii: vec![], kinds: vec![], radii_relative: false };
let (mut path, mut live) = (l.to_path(), Some(Box::new(l.clone())));
set_corners(&mut path, &mut live, &set(&[0, 1, 2, 3, 4, 5]), Some(8.0), None);
assert_eq!(path.anchor_count(), 12);
@@ -355,11 +365,25 @@ mod tests {
#[test]
fn relative_radii_scale_with_rectangle_size() {
let l = LiveShape::Rectangle { w: 100.0, h: 100.0, radii: [0.25, 0.25, 0.25, 0.25], kinds: Default::default(), radii_relative: true, xf: Affine::IDENTITY };
let l = LiveShape::Rectangle {
w: 100.0,
h: 100.0,
radii: [0.25, 0.25, 0.25, 0.25],
kinds: Default::default(),
radii_relative: true,
xf: Affine::IDENTITY,
};
let path = l.to_path();
let c = LiveCorners::new(&path, Some(&l)).unwrap();
let r0 = c.drawn_radius(0);
let l2 = LiveShape::Rectangle { w: 200.0, h: 200.0, radii: [0.25, 0.25, 0.25, 0.25], kinds: Default::default(), radii_relative: true, xf: Affine::IDENTITY };
let l2 = LiveShape::Rectangle {
w: 200.0,
h: 200.0,
radii: [0.25, 0.25, 0.25, 0.25],
kinds: Default::default(),
radii_relative: true,
xf: Affine::IDENTITY,
};
let path2 = l2.to_path();
let c2 = LiveCorners::new(&path2, Some(&l2)).unwrap();
assert!((c2.drawn_radius(0) / r0 - 2.0).abs() < 0.05, "{r0} → {}", c2.drawn_radius(0));
@@ -367,7 +391,14 @@ mod tests {
#[test]
fn convert_between_relative_and_absolute_radii() {
let mut l = LiveShape::Rectangle { w: 100.0, h: 50.0, radii: [10.0, 0.0, 0.0, 0.0], kinds: Default::default(), radii_relative: false, xf: Affine::IDENTITY };
let mut l = LiveShape::Rectangle {
w: 100.0,
h: 50.0,
radii: [10.0, 0.0, 0.0, 0.0],
kinds: Default::default(),
radii_relative: false,
xf: Affine::IDENTITY,
};
let before = LiveCorners::new(&l.to_path(), Some(&l)).unwrap().drawn_radius(0);
convert_radii_relative(&mut l, true);
assert!(l.radii_relative());
+19 -3
View File
@@ -146,7 +146,9 @@ fn is_false(b: &bool) -> bool {
impl LiveShape {
pub fn radii_relative(&self) -> bool {
match self {
LiveShape::Rectangle { radii_relative, .. } | LiveShape::Polygon { radii_relative, .. } | LiveShape::Path { radii_relative, .. } => *radii_relative,
LiveShape::Rectangle { radii_relative, .. } | LiveShape::Polygon { radii_relative, .. } | LiveShape::Path { radii_relative, .. } => {
*radii_relative
}
LiveShape::Ellipse { .. } | LiveShape::Line { .. } => false,
}
}
@@ -2205,14 +2207,28 @@ mod tests {
#[test]
fn rect_live_shape_roundtrip() {
let l = LiveShape::Rectangle { w: 10.0, h: 20.0, radii: [0.0; 4], kinds: Default::default(), radii_relative: false, xf: Affine::translate((5.0, 5.0)) };
let l = LiveShape::Rectangle {
w: 10.0,
h: 20.0,
radii: [0.0; 4],
kinds: Default::default(),
radii_relative: false,
xf: Affine::translate((5.0, 5.0)),
};
assert_eq!(l.to_path().bounds(), Some(Rect::new(5.0, 5.0, 15.0, 25.0)));
assert_eq!(l.label(), "Rectangle");
}
#[test]
fn corner_kinds_save_only_when_some_corner_isnt_round() {
let mut l = LiveShape::Rectangle { w: 100.0, h: 50.0, radii: [0.0, 10.0, 0.0, 0.0], kinds: Default::default(), radii_relative: false, xf: Affine::IDENTITY };
let mut l = LiveShape::Rectangle {
w: 100.0,
h: 50.0,
radii: [0.0, 10.0, 0.0, 0.0],
kinds: Default::default(),
radii_relative: false,
xf: Affine::IDENTITY,
};
// Older files read as round.
let json = serde_json::to_value(&l).unwrap();
assert!(json.get("kinds").is_none(), "{json}");
+1 -5
View File
@@ -176,11 +176,7 @@ fn hard_mix_face(path: PathData, covering: &[usize], shapes: &[po::Shape], sourc
painted = true;
}
}
let fill = if painted {
Paint::solid(Color::cmyk(cmyk[0], cmyk[1], cmyk[2], cmyk[3]))
} else {
Paint::None
};
let fill = if painted { Paint::solid(Color::cmyk(cmyk[0], cmyk[1], cmyk[2], cmyk[3])) } else { Paint::None };
let top = covering.last().and_then(|&si| sources.get(shapes[si].key as usize));
let mut n = Node::path(top.map_or(NodeId(0), |s| s.id), path, Appearance::basic(fill, Paint::None, 0.0));
if let Some(src) = top {
+1 -5
View File
@@ -463,11 +463,7 @@ pub(super) fn dry_brush(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpa
*a /= SAMPLES as f32;
}
let a = acc[3].clamp(0.0, 1.0);
let c = if a > 1e-6 {
[0, 1, 2].map(|i| (acc[i] / a).clamp(0.0, 1.0))
} else {
straight(*p)
};
let c = if a > 1e-6 { [0, 1, 2].map(|i| (acc[i] / a).clamp(0.0, 1.0)) } else { straight(*p) };
let edge = edges[k].min(1.0);
let dark = (edge_gain * edge).clamp(0.0, 0.75);
let grain = bristle * (2.0 * value_noise(SALT_DRY, q.x / (5.0 * space.px), q.y / (5.0 * space.px)) - 1.0) as f32;
+1 -4
View File
@@ -1446,10 +1446,7 @@ fn texturizer_draws_a_loaded_texture_as_relief() {
assert_ne!(relief, run(json!({"texture": builtin, "relief": 20})), "{builtin}");
}
assert_eq!(run(json!({"texture": "loaded", "textureImage": rings, "relief": 0})), flat(w, mid));
assert_eq!(
fx("texture.texturizer", json!({"texture": "LOADED", "textureImage": "not a texture"})),
fx("texture.texturizer", json!({}))
);
assert_eq!(fx("texture.texturizer", json!({"texture": "LOADED", "textureImage": "not a texture"})), fx("texture.texturizer", json!({})));
}
/// The Texture filters' patterns lie in document space around the object's centre: the same at
+4 -3
View File
@@ -218,7 +218,7 @@ pub(crate) fn item_target_at(s: &Session, p: &Value, key: &str, cmd: &str) -> Re
Some(Value::Null) => Ok(ItemTarget::Top),
Some(v) => v
.as_u64()
.map(|i| ItemTarget::Item { index: i as usize, explicit: true })
.map(|i| ItemTarget::Item { index: index(i), explicit: true })
.ok_or_else(|| bad(cmd, format!("`{key}` must be an appearance item index (paint order) or null"))),
// The active item is a row of the first selected object's own stack.
None if p.get("ids").is_some() || p.get("id").is_some() || targets_contents(p)? => Ok(ItemTarget::Top),
@@ -383,8 +383,9 @@ pub(crate) fn edits_stroke(s: &Session, p: &Value, item: ItemTarget, default: bo
Ok(ids.first().and_then(|id| d.node(*id)?.appearance.items.get(index)).map_or(default, |it| !it.is_fill()))
}
/// The whole number of 0 or more that `key` holds, read with [`index`].
pub(crate) fn index_param(p: &Value, key: &str, cmd: &str) -> Result<usize> {
p.get(key).and_then(Value::as_u64).map(|i| i as usize).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
p.get(key).and_then(Value::as_u64).map(index).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
}
fn set_active_item(s: &mut Session, p: &Value) -> Result<Value> {
@@ -603,7 +604,7 @@ fn item_indices(p: &Value, cmd: &str) -> Result<Vec<usize>> {
let mut v = match p.get("indices") {
Some(a) => a
.as_array()
.and_then(|a| a.iter().map(|x| x.as_u64().map(|i| i as usize)).collect::<Option<Vec<_>>>())
.and_then(|a| a.iter().map(|x| x.as_u64().map(index)).collect::<Option<Vec<_>>>())
.filter(|v| !v.is_empty())
.ok_or_else(|| bad(cmd, "`indices` must be a non-empty array of item indices"))?,
None => vec![index_param(p, "index", cmd)?],
+2 -1
View File
@@ -211,8 +211,9 @@ fn within_input_bound(q: Point) -> bool {
q.x.is_finite() && q.y.is_finite() && q.x.abs() < MAX_INPUT && q.y.abs() < MAX_INPUT
}
/// The whole number of 0 or more that `key` holds, read with [`index`].
fn usize_req(p: &Value, key: &str, cmd: &str) -> Result<usize> {
p.get(key).and_then(Value::as_u64).map(|v| v as usize).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
p.get(key).and_then(Value::as_u64).map(index).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
}
/// The whole number of 0 or more that `key` holds, or `None` when `key` is absent or null. Any other
+2 -2
View File
@@ -171,7 +171,7 @@ pub fn specs() -> Vec<CommandSpec> {
}
fn file_close(s: &mut Session, p: &Value) -> Result<Value> {
let i = p.get("index").and_then(Value::as_u64).map(|v| v as usize).or(s.active_index()).ok_or(EngineError::NoDocument)?;
let i = p.get("index").and_then(Value::as_u64).map(index).or(s.active_index()).ok_or(EngineError::NoDocument)?;
if !s.close_document(i) {
return Err(bad("file.close", "no such document"));
}
@@ -179,7 +179,7 @@ fn file_close(s: &mut Session, p: &Value) -> Result<Value> {
}
fn doc_activate(s: &mut Session, p: &Value) -> Result<Value> {
let i = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad("document.activate", "missing index"))? as usize;
let i = p.get("index").and_then(Value::as_u64).map(index).ok_or_else(|| bad("document.activate", "missing index"))?;
if s.set_active(i) { ok() } else { Err(bad("document.activate", "no such document")) }
}
+2 -2
View File
@@ -227,7 +227,7 @@ fn edit<R>(s: &mut Session, p: &Value, cmd: &str, mut f: impl FnMut(&mut Freefor
/// The `index` param, else the selected point.
fn point_index(s: &Session, p: &Value, cmd: &str) -> Result<usize> {
match p.get("index") {
Some(v) => v.as_u64().map(|i| i as usize).ok_or_else(|| bad(cmd, "`index` must be a point index")),
Some(v) => v.as_u64().map(index).ok_or_else(|| bad(cmd, "`index` must be a point index")),
None => s.selected_freeform_point().ok_or_else(|| bad(cmd, "no point selected (give `index`)")),
}
}
@@ -318,7 +318,7 @@ fn select_point(s: &mut Session, p: &Value) -> Result<Value> {
None => return Err(bad(C, "missing `index` (a point index, or null to clear)")),
Some(Value::Null) => None,
Some(v) => {
let i = v.as_u64().ok_or_else(|| bad(C, "`index` must be a whole number or null"))? as usize;
let i = v.as_u64().map(index).ok_or_else(|| bad(C, "`index` must be a whole number or null"))?;
let (f, _) = s.proxy_freeform(!s.fill_active).ok_or_else(|| bad(C, NOT_FREEFORM))?;
check_index(&f, i).map_err(|e| bad(C, e))?;
Some(i)
+1 -1
View File
@@ -60,7 +60,7 @@ fn select_stop(s: &mut Session, p: &Value) -> Result<Value> {
None => return Err(bad(C, "missing `index` (a stop index, or null to clear)")),
Some(Value::Null) => None,
Some(v) => {
let i = v.as_u64().ok_or_else(|| bad(C, "`index` must be a whole number or null"))? as usize;
let i = v.as_u64().map(index).ok_or_else(|| bad(C, "`index` must be a whole number or null"))?;
let n = active_gradient(s).ok_or_else(|| bad(C, "the active paint is not a gradient"))?.gradient.stops.len();
if i >= n {
return Err(bad(C, format!("no stop {i} (the gradient has {n})")));
+2 -2
View File
@@ -289,7 +289,7 @@ pub(crate) fn make_layer(d: &mut Document, name: Option<&str>) -> Node {
pub(super) fn layer_color_param(v: &Value) -> std::result::Result<LayerColor, String> {
match v {
Value::Number(n) => match n.as_u64() {
Some(i) if (i as usize) < LAYER_COLORS.len() => Ok(LayerColor::Preset(i as u8)),
Some(i) if index(i) < LAYER_COLORS.len() => Ok(LayerColor::Preset(i as u8)),
_ => Err(format!("a colour index is 0 to {}, not {n}", LAYER_COLORS.len() - 1)),
},
Value::String(s) => {
@@ -781,7 +781,7 @@ fn move_node(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "node.move";
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?;
let parent = id_param(p, "parent");
let index = p.get("index").and_then(Value::as_u64).unwrap_or(u64::MAX).min(usize::MAX as u64) as usize;
let index = p.get("index").and_then(Value::as_u64).map_or(usize::MAX, index);
let d = &s.doc()?.doc;
let n = d.node(id).ok_or(EngineError::NoNode(id))?;
if !accepts(d, parent, n) {
+1 -1
View File
@@ -1088,7 +1088,7 @@ fn move_group(s: &mut Session, p: &Value) -> Result<Value> {
let (name, at) = s.libraries.change(&id, |lib| {
let i = group_at(lib, group, C)?;
let g = lib.groups.remove(i);
let at = usize::try_from(to).unwrap_or(usize::MAX).min(lib.groups.len());
let at = index(to).min(lib.groups.len());
let name = g.name.clone();
lib.groups.insert(at, g);
Ok((name, at))
+8 -1
View File
@@ -899,7 +899,14 @@ pub(crate) fn detect_shape(path: &PathData) -> Option<LiveShape> {
}
let rot = Affine::rotate(e0.y.atan2(e0.x));
let flip = if e0.cross(e1) < 0.0 { Affine::scale_non_uniform(1.0, -1.0) } else { Affine::IDENTITY };
return Some(LiveShape::Rectangle { w, h, radii: [0.0; 4], kinds: Default::default(), radii_relative: false, xf: Affine::translate(a[0].p.to_vec2()) * rot * flip });
return Some(LiveShape::Rectangle {
w,
h,
radii: [0.0; 4],
kinds: Default::default(),
radii_relative: false,
xf: Affine::translate(a[0].p.to_vec2()) * rot * flip,
});
}
let b = path.bounds()?;
let c = b.center();
+4
View File
@@ -341,6 +341,10 @@ pub(crate) fn id_param(p: &Value, key: &str) -> Option<NodeId> {
pub(crate) fn ids_param(p: &Value, key: &str) -> Option<Vec<NodeId>> {
p.get(key).and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(NodeId).collect())
}
/// `v` as an index, a count or a byte offset: past `usize::MAX` reads as `usize::MAX`, past the end of any list.
pub(crate) fn index(v: u64) -> usize {
usize::try_from(v).unwrap_or(usize::MAX)
}
/// Validate all explicit targets before a command changes the document or selection.
pub(crate) fn checked_ids_param(s: &Session, p: &Value, key: &str, command: &str) -> Result<Vec<NodeId>> {
let values =
+3 -3
View File
@@ -279,7 +279,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; 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))",
"{x?, y?, width?, height?, reference?: 0..8 (9-point grid; a number past 8 is read as 8), 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
),
@@ -1791,7 +1791,7 @@ fn bounds_transform(s: &Session, p: &Value, ids: Vec<NodeId>, least: f64) -> Res
let margins = |b: Rect| [g.x0 - b.x0, g.y0 - b.y0, b.x1 - g.x1, b.y1 - g.y1];
let (m, all) = (margins(vs), margins(v));
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 refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4).min(8) as usize;
let rp = vectorcraft_geom::reference_point(v, refi);
let (w, h) = wanted_size(p, v);
// The geometric scale that gives the visual size: margins grow by the strokes' scale `f`. It is
@@ -1954,7 +1954,7 @@ fn wanted_size(p: &Value, b: Rect) -> (f64, f64) {
/// `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 refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4).min(8) 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.
+1 -1
View File
@@ -55,7 +55,7 @@ pub fn specs() -> Vec<CommandSpec> {
fn artboard_reorder(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "artboard.reorder";
let i = artboard_index(&s.doc()?.doc, p, C)?;
let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(C, "missing to"))? as usize;
let to = p.get("to").and_then(Value::as_u64).map(index).ok_or_else(|| bad(C, "missing to"))?;
s.edit("Reorder Artboards", |d, _| {
if i >= d.artboards.len() {
return Err(EngineError::Other("no such artboard".into()));
+3 -3
View File
@@ -226,7 +226,7 @@ pub(crate) fn transform_anchors(s: &mut Session, label: &str, xf: vectorcraft_ge
fn set_handle(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.setHandle", "missing id"))?;
let si = usize_opt(p, "subpath", "path.setHandle")?.unwrap_or(0);
let ai = p.get("anchor").and_then(Value::as_u64).ok_or_else(|| bad("path.setHandle", "missing anchor"))? as usize;
let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.setHandle", "missing anchor"))?;
let out = str_param(p, "which") != Some("in");
let pos = Point::new(f64_req(p, "x", "path.setHandle")?, f64_req(p, "y", "path.setHandle")?);
let independent = bool_or(p, "independent", false);
@@ -244,7 +244,7 @@ fn set_handle(s: &mut Session, p: &Value) -> Result<Value> {
fn move_anchor(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.moveAnchor", "missing id"))?;
let si = usize_opt(p, "subpath", "path.moveAnchor")?.unwrap_or(0);
let ai = p.get("anchor").and_then(Value::as_u64).ok_or_else(|| bad("path.moveAnchor", "missing anchor"))? as usize;
let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.moveAnchor", "missing anchor"))?;
let pos = Point::new(f64_req(p, "x", "path.moveAnchor")?, f64_req(p, "y", "path.moveAnchor")?);
s.edit("Move", |d, _| {
let path = path_mut(d, id)?;
@@ -772,7 +772,7 @@ fn reshape(s: &mut Session, p: &Value) -> Result<Value> {
fn insert_anchor(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.insertAnchor", "missing id"))?;
let si = usize_opt(p, "subpath", "path.insertAnchor")?.unwrap_or(0);
let seg = p.get("segment").and_then(Value::as_u64).ok_or_else(|| bad("path.insertAnchor", "missing segment"))? as usize;
let seg = p.get("segment").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.insertAnchor", "missing segment"))?;
let t = f64_or(p, "t", 0.5).clamp(0.0, 1.0);
let idx = s.edit("Add Anchor Point", |d, _| {
let path = path_mut(d, id)?;
+1 -1
View File
@@ -96,7 +96,7 @@ fn set_origin(s: &mut Session, p: &Value) -> Result<Value> {
let regions = vectorcraft_pdf::print_regions(doc, &set).map_err(|e| pdf_error(SET, e))?;
let picked = match p.get("artboard").filter(|v| !v.is_null()) {
Some(v) => {
let i = v.as_u64().ok_or_else(|| bad(SET, "artboard must be a 0-based index"))? as usize;
let i = v.as_u64().map(index).ok_or_else(|| bad(SET, "artboard must be a 0-based index"))?;
regions.iter().find(|(a, _)| a.is_none() || *a == Some(i)).ok_or_else(|| bad(SET, format!("artboard {i} doesn't print")))?
}
None => region_holding(&regions, at).or(regions.first()).ok_or_else(|| bad(SET, "nothing prints"))?,
+3 -3
View File
@@ -156,11 +156,11 @@ fn reduce(s: &mut Session, p: &Value) -> Result<Value> {
}
}
let unit_colors = |g: &[&Used]| g.iter().map(|u| u.color).collect::<Vec<_>>();
let rows: Vec<Vec<&Used>> = match p.get("colors").and_then(Value::as_u64) {
Some(n) if (n as usize) < units.len() => {
let rows: Vec<Vec<&Used>> = match p.get("colors").and_then(Value::as_u64).map(index) {
Some(n) if n < units.len() => {
let keys: Vec<Color> = units.iter().map(|g| method.key_color(&unit_colors(g)).unwrap_or_default()).collect();
let weights: Vec<f32> = units.iter().map(|g| g.iter().map(|u| u.count as f32).sum()).collect();
cluster(&keys, &weights, n as usize).into_iter().map(|ix| ix.into_iter().flat_map(|i| units[i].iter().copied()).collect()).collect()
cluster(&keys, &weights, n).into_iter().map(|ix| ix.into_iter().flat_map(|i| units[i].iter().copied()).collect()).collect()
}
_ => units,
};
+2 -2
View File
@@ -266,7 +266,7 @@ fn all(s: &mut Session, _: &Value) -> Result<Value> {
fn all_on_artboard(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc()?;
let i = p.get("artboard").and_then(Value::as_u64).unwrap_or(0) as usize;
let i = p.get("artboard").and_then(Value::as_u64).map_or(0, index);
let r = st.doc.artboards.get(i).map(|a| a.rect).ok_or_else(|| bad("select.allOnArtboard", "no such artboard"))?;
// Symbol instances are measured by their art.
let symbols = st.doc.symbol_boxes();
@@ -644,7 +644,7 @@ fn same_attribute(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc()?;
let refn = st.selection.objects.first().and_then(|id| st.doc.node(*id)).ok_or_else(|| bad(cmd, "nothing selected"))?;
let ap = &refn.appearance;
let item = p.get("item").and_then(Value::as_u64).map(|i| i as usize).or(s.appearance_item());
let item = p.get("item").and_then(Value::as_u64).map(index).or(s.appearance_item());
let attr = match (item.and_then(|i| ap.items.get(i)), ap.effects.first()) {
(Some(it), _) => Attribute::Item(it.clone()),
(None, Some(e)) => Attribute::Effect(e.clone()),
+1 -1
View File
@@ -409,7 +409,7 @@ fn splits(lo: f64, hi: f64, count: Option<u64>, size: Option<f64>) -> std::resul
(None, Some(_)) => return Err("a slice size must be a positive number of points".into()),
(None, None) => 1,
};
if n == 0 || n as usize > MAX_PIECES {
if n == 0 || index(n) > MAX_PIECES {
return Err(format!("divide into 1 to {MAX_PIECES} slices"));
}
let step = match (count, size) {
+1 -1
View File
@@ -547,7 +547,7 @@ fn style_move(s: &mut Session, p: &Value) -> Result<Value> {
let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(cmd, "missing `to` (an index)"))?;
let d = &s.doc()?.doc;
let from = style_index(d, name)?;
let to = (to as usize).min(d.graphic_styles.len() - 1);
let to = index(to).min(d.graphic_styles.len() - 1);
if from == to {
return ok();
}
+1 -1
View File
@@ -364,7 +364,7 @@ fn insert_before<T>(list: &mut Vec<T>, items: Vec<T>, anchor: Option<&str>, name
fn swatch_move(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "swatch.move";
let names = names_param(p, C)?;
let to = p.get("to").and_then(Value::as_u64).map(|v| v as usize);
let to = p.get("to").and_then(Value::as_u64).map(index);
let group = str_param(p, "group").map(str::to_string);
let moved = names.len();
s.edit("Move Swatch", |d, _| {
+2 -1
View File
@@ -174,8 +174,9 @@ pub(crate) fn set_plain_text(d: &mut vectorcraft_doc::Document, id: NodeId, text
Ok(())
}
/// The byte offset that `k` holds, read with [`index`].
fn byte_param(p: &Value, k: &str) -> Option<usize> {
p.get(k).and_then(Value::as_u64).map(|v| v as usize)
p.get(k).and_then(Value::as_u64).map(index)
}
fn range_of(p: &Value, len: usize) -> (usize, usize) {
+5 -8
View File
@@ -343,10 +343,10 @@ fn selection_attrs(s: &Session, p: &Value, kind: Kind) -> Result<Option<Map<Stri
None => text_targets(s, &json!({}), "").ok().and_then(|v| v.first().copied()),
};
let Some(NodeKind::Text(t)) = id.and_then(|i| d.node(i)).map(|n| &n.kind) else { return Ok(None) };
let at = p.get("start").and_then(Value::as_u64).map_or(0, |v| usize::try_from(v).unwrap_or(usize::MAX));
let at = p.get("start").and_then(Value::as_u64).map_or(0, index);
Ok(Some(match kind {
Kind::Char => {
let style = match p.get("end").and_then(Value::as_u64).map(|v| usize::try_from(v).unwrap_or(usize::MAX)) {
let style = match p.get("end").and_then(Value::as_u64).map(index) {
Some(end) => vectorcraft_text::edit::insertion_style(&t.runs, at.min(end), at.max(end)),
None => vectorcraft_text::edit::style_at(&t.runs, at),
};
@@ -487,12 +487,9 @@ fn apply(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
match kind {
Kind::Char => {
let len = vectorcraft_text::edit::runs_len(&t.runs);
let (a, b) = match range {
Some(_) => {
let g = |k: &str, def: usize| p.get(k).and_then(Value::as_u64).map_or(def, |v| (v as usize).min(len));
(g("start", 0), g("end", len))
}
None => (0, len),
let (a, b) = match (range, text_range) {
(Some(_), Some(r)) => r.bytes(t),
_ => (0, len),
};
let mut err = None;
vectorcraft_text::edit::style_range(&mut t.runs, a.min(b), a.max(b), |st| match char_fn(st) {
+1 -3
View File
@@ -532,9 +532,7 @@ impl TextRange {
let get = |k: &str| -> Result<Option<usize>> {
match p.get(k) {
None | Some(Value::Null) => Ok(None),
Some(v) => {
v.as_u64().map(|n| Some(usize::try_from(n).unwrap_or(usize::MAX))).ok_or_else(|| bad(cmd, format!("`{k}` must be a byte offset")))
}
Some(v) => v.as_u64().map(|n| Some(index(n))).ok_or_else(|| bad(cmd, format!("`{k}` must be a byte offset"))),
}
};
let (start, end) = (get("start")?, get("end")?);
+1 -1
View File
@@ -345,7 +345,7 @@ fn sample_color(s: &mut Session, p: &Value) -> Result<Value> {
let (id, mut q) = match p.get("stop") {
None | Some(Value::Null) => (if stroke { "paint.setStroke" } else { "paint.setFill" }, json!({ "color": c })),
Some(v) => {
let i = v.as_u64().ok_or_else(|| bad(C, "`stop` must be a stop index"))? as usize;
let i = v.as_u64().map(index).ok_or_else(|| bad(C, "`stop` must be a stop index"))?;
let color = super::paint::new_color(s, p, color_value(c).ok_or_else(|| bad(C, format!("bad color {c}")))?);
let (fill, stroke_paint) = s.proxy_paints();
let Paint::Gradient(g) = (if stroke { stroke_paint } else { fill }) else {
+39
View File
@@ -1084,6 +1084,45 @@ fn set_bounds_acts_on_direct_selected_anchors() {
assert_eq!(points(&s), [(10.0, 0.0), (140.0, 20.0), (60.0, 100.0), (10.0, 100.0)]);
}
/// `object.setBounds` reads a `reference` past 8 as 8, the bottom-right point, as
/// `object.transformEach` does, for a path selected whole and for direct-selected anchors. Before,
/// 9 read as the bottom-left point, 2^32 as the bottom center, and a 32-bit build read 2^32 as the
/// top-left point.
#[test]
fn set_bounds_reads_a_reference_past_8_as_8() {
for reference in [9, 1u64 << 32] {
for anchors in [false, true] {
let mut s = session();
let square = json!({"anchors": [{"x": 0, "y": 0}, {"x": 100, "y": 0}, {"x": 100, "y": 100}, {"x": 0, "y": 100}], "closed": true});
let id = NodeId(s.execute("path.create", &square).unwrap()["id"].as_u64().unwrap());
if anchors {
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 0], [0, 1], [0, 2], [0, 3]]})).unwrap();
} else {
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
}
s.execute("object.setBounds", &json!({"width": 50, "height": 50, "reference": reference})).unwrap();
let NodeKind::Path { path, .. } = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("a path") };
let points: Vec<(f64, f64)> = path.subpaths[0].anchors.iter().map(|a| (a.p.x.round(), a.p.y.round())).collect();
assert_eq!(points, [(50.0, 50.0), (100.0, 50.0), (100.0, 100.0), (50.0, 100.0)], "reference {reference}, anchors {anchors}");
}
}
}
/// `cmd::index` keeps a number that fits in a `usize` and reads a larger one (2^32 or more on a
/// 32-bit build) as `usize::MAX`.
#[test]
fn index_reads_a_number_past_usize_max_as_usize_max() {
let past_u32 = u64::from(u32::MAX) + 1;
#[cfg(target_pointer_width = "64")]
let expected = 1usize << 32;
#[cfg(not(target_pointer_width = "64"))]
let expected = usize::MAX;
assert_eq!(cmd::index(0), 0);
assert_eq!(cmd::index(7), 7);
assert_eq!(cmd::index(past_u32), expected);
assert_eq!(cmd::index(u64::MAX), usize::MAX);
}
/// Arrange commands that leave the stacking order as it is record no undo step: Bring to Front and
/// Bring Forward on the front objects, Send to Back and Send Backward on the back ones, and Send to
/// Current Layer on objects already on top of the current layer.
+44
View File
@@ -974,6 +974,50 @@ fn split_refuses_a_subpath_anchor_or_segment_that_is_not_a_whole_number() {
}
}
/// A subpath, anchor or segment of 2^32 + k fails, where k names one that exists, and the document
/// and the undo history stay as they were. A 32-bit build read the number as k.
#[test]
fn path_commands_refuse_a_subpath_anchor_or_segment_past_u32() {
let mut s = session();
let id = two_subpaths(&mut s);
let at_anchor = json!({"id": id.0, "anchor": 1});
let at_segment = json!({"id": id.0, "segment": 0, "t": 0.5});
let curvature_move = json!({"id": id.0, "op": "move", "anchor": 1, "x": 200, "y": 150});
let curvature_insert = json!({"id": id.0, "op": "insert", "segment": 0, "t": 0.5});
let convert = json!({"id": id.0, "anchor": 1, "to": "corner"});
let reshape = json!({"id": id.0, "segment": 0, "t": 0.5, "dx": 0, "dy": 20});
let handle = json!({"id": id.0, "anchor": 0, "which": "out", "x": 140, "y": 250});
let moved = json!({"id": id.0, "anchor": 0, "x": 140, "y": 250});
for (cmd, p, key) in [
("path.curvatureEdit", &curvature_move, "subpath"),
("path.curvatureEdit", &curvature_move, "anchor"),
("path.curvatureEdit", &curvature_insert, "segment"),
("path.removeAnchor", &at_anchor, "subpath"),
("path.removeAnchor", &at_anchor, "anchor"),
("path.convertAnchor", &convert, "subpath"),
("path.convertAnchor", &convert, "anchor"),
("path.reshapeSegment", &reshape, "subpath"),
("path.reshapeSegment", &reshape, "segment"),
("path.split", &at_anchor, "anchor"),
("path.split", &at_segment, "segment"),
("path.setHandle", &handle, "subpath"),
("path.setHandle", &handle, "anchor"),
("path.moveAnchor", &moved, "subpath"),
("path.moveAnchor", &moved, "anchor"),
("path.insertAnchor", &at_segment, "subpath"),
("path.insertAnchor", &at_segment, "segment"),
] {
let mut p = p.clone();
let k = if key == "subpath" { 1 } else { p[key].as_u64().unwrap() };
p[key] = json!((1u64 << 32) + k);
let before = s.doc().unwrap().doc.clone();
let undo = undo_steps(&s);
assert!(s.execute(cmd, &p).is_err(), "{cmd} {p} succeeded");
assert!(s.doc().unwrap().doc == before, "{cmd} {p} changed the document");
assert_eq!(undo_steps(&s), undo, "{cmd} {p} left an undo step");
}
}
#[test]
fn set_handle_refuses_a_subpath_that_is_not_a_whole_number() {
let mut s = session();
+21
View File
@@ -262,6 +262,27 @@ fn point_commands_need_a_freeform_gradient() {
assert!(s.execute("paint.freeform.addPoint", &json!({"at": [1, 1]})).is_err());
}
/// The point and line commands refuse a point, line or segment of 2^32 + k, where k names one that
/// exists, and the points and the selected point stay as they were. A 32-bit build read the number
/// as k.
#[test]
fn point_commands_refuse_an_index_past_u32() {
let (mut s, id) = freeform_rect();
run(&mut s, "paint.freeform.addLine", json!({"points": [0, 1]}));
let (before, selected) = (points(&s, id), s.selected_freeform_point());
let past = |k: u64| (1u64 << 32) + k;
for (cmd, p) in [
("paint.freeform.setPoint", json!({"index": past(1), "color": "#000000"})),
("paint.freeform.deletePoint", json!({"index": past(1)})),
("paint.freeform.selectPoint", json!({"index": past(1)})),
("paint.freeform.splitLine", json!({"line": past(0), "segment": 0})),
("paint.freeform.splitLine", json!({"line": 0, "segment": past(0)})),
] {
assert!(s.execute(cmd, &p).is_err(), "{cmd} {p}");
assert_eq!((points(&s, id), s.selected_freeform_point()), (before.clone(), selected), "{cmd} {p}");
}
}
#[test]
fn selection_follows_the_object_and_the_proxy() {
let (mut s, id) = freeform_rect();
+2
View File
@@ -288,6 +288,8 @@ fn select_stop_is_validated_and_shared() {
assert_eq!(s.execute("gradient.selectStop", &json!({"index": 1})).unwrap(), json!({"index": 1}));
assert_eq!(s.selected_stop(), Some(1));
assert!(s.execute("gradient.selectStop", &json!({"index": 2})).is_err(), "two stops");
// A 32-bit build read 2^32 + 1 as stop 1.
assert!(s.execute("gradient.selectStop", &json!({"index": (1u64 << 32) + 1})).is_err(), "past u32");
assert!(s.execute("gradient.selectStop", &json!({"index": "a"})).is_err());
assert!(s.execute("gradient.selectStop", &json!({})).is_err());
s.execute("gradient.selectStop", &json!({"index": null})).unwrap();
+2
View File
@@ -38,6 +38,8 @@ fn sample_color_with_a_stop_recolours_that_stop_in_one_undo_step() {
s.execute("paint.sampleColor", &json!({"color": "#ff0000", "stop": 1})).unwrap();
assert_eq!(stop_hexes(&s, id, false), ["#ffffff", "#ff0000"]);
assert!(s.execute("paint.sampleColor", &json!({"color": "#ff0000", "stop": 2})).unwrap_err().to_string().contains("no stop 2"));
// A 32-bit build read 2^32 + 1 as stop 1.
assert!(s.execute("paint.sampleColor", &json!({"color": "#00ff00", "stop": (1u64 << 32) + 1})).is_err());
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(stop_hexes(&s, id, false), ["#ffffff", "#000000"]);
// The stroke's gradient through `stroke`, the fill left alone.
+2
View File
@@ -349,6 +349,8 @@ fn layer_options_validate_and_template_locks_and_dims() {
p["id"] = json!(layer.0);
assert!(s.execute("layer.setProps", &p).is_err(), "{bad}");
}
// A 32-bit build read color 2^32 + 4 as preset 4.
assert!(s.execute("layer.setProps", &json!({"id": layer.0, "color": (1u64 << 32) + 4})).is_err());
let color = |s: &Session| match node(s, layer).kind {
NodeKind::Layer { color, .. } => color,
_ => panic!("not a layer"),
+23
View File
@@ -1515,6 +1515,17 @@ fn select_all_on_artboard_takes_lines_on_its_right_and_bottom_edges() {
assert_eq!(on(1), vec![right, shared]);
}
/// All on Active Artboard refuses artboard 2^32 and selects nothing. A 32-bit build read it as
/// artboard 0.
#[test]
fn select_all_on_artboard_refuses_an_artboard_past_u32() {
let mut s = session();
rect(&mut s, 10.0, 10.0, 20.0, 20.0);
s.execute("select.none", &json!({})).unwrap();
assert!(s.execute("select.allOnArtboard", &json!({"artboard": 1u64 << 32})).is_err());
assert!(selected(&s).is_empty());
}
#[test]
fn save_and_recall_selection() {
let mut s = session();
@@ -2699,6 +2710,18 @@ fn close_all_documents() {
assert!(s.execute("file.closeAll", &json!({})).is_err());
}
/// Close and Activate refuse a document index of 2^32, and both documents stay open with the second
/// one active. A 32-bit build read the index as 0.
#[test]
fn close_and_activate_refuse_a_document_index_past_u32() {
let mut s = session();
s.execute("file.new", &json!({})).unwrap();
for cmd in ["document.activate", "file.close"] {
assert!(s.execute(cmd, &json!({"index": 1u64 << 32})).is_err(), "{cmd}");
assert_eq!((s.documents().len(), s.active_index()), (2, Some(1)), "{cmd}");
}
}
#[test]
fn document_color_mode_converts_colours() {
let mut s = session();
+36
View File
@@ -81,6 +81,31 @@ fn appearance_duplicate_and_move() {
assert!(s.execute("appearance.duplicateItem", &json!({"index": 99})).is_err());
}
/// The appearance and effect commands given an item index of 2^32 + k, where k names one of the
/// object's items, leave the object's appearance and the undo history as they were, as for any index
/// past the last item: `appearance.removeItem` removes nothing, and the others fail. A 32-bit build
/// read the index as k.
#[test]
fn appearance_commands_leave_an_item_past_u32_alone() {
let mut s = session();
let id = rect(&mut s);
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
let before = node(&s, id).appearance;
let undo = s.doc().unwrap().history.undo.len();
let past = |k: u64| (1u64 << 32) + k;
for (cmd, p, fails) in [
("appearance.removeItem", json!({"index": past(0)}), false),
("appearance.removeItem", json!({"indices": [past(0)]}), false),
("appearance.duplicateItem", json!({"index": past(0)}), true),
("appearance.moveItem", json!({"from": past(1), "to": 0}), true),
("effect.apply", json!({"effect": "distort.roughen", "item": past(1)}), true),
] {
assert_eq!(s.execute(cmd, &p).is_err(), fails, "{cmd} {p}");
assert_eq!(node(&s, id).appearance, before, "{cmd} {p}");
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "{cmd} {p}");
}
}
#[test]
fn artboards_reorder_and_duplicate() {
let mut s = session();
@@ -155,6 +180,17 @@ fn artboard_color_round_trips_through_the_native_format() {
assert_eq!(back.artboards, s.doc().unwrap().doc.artboards);
}
/// An artboard moved to place 2^32 goes last. A 32-bit build read the place as 0 and moved it to the
/// front.
#[test]
fn artboard_reorder_past_u32_goes_last() {
let mut s = session();
s.execute("artboard.new", &json!({"name": "Second"})).unwrap();
s.execute("artboard.new", &json!({"name": "Third"})).unwrap();
s.execute("artboard.reorder", &json!({"index": 1, "to": 1u64 << 32})).unwrap();
assert_eq!(s.doc().unwrap().doc.artboards.last().map(|a| a.name.as_str()), Some("Second"));
}
/// The artboards' left edges and names.
fn boards(s: &Session) -> Vec<(f64, String)> {
s.doc().unwrap().doc.artboards.iter().map(|a| (a.rect.x0, a.name.clone())).collect()
+2
View File
@@ -144,6 +144,8 @@ fn tiles_start_at_the_tile_origin_and_cover_the_art() {
// Bad input is refused.
assert!(s.execute("print.tiling.set", &json!({})).is_err());
assert!(s.execute("print.tiling.set", &json!({"origin": [1, 2], "artboard": 7})).is_err());
// A 32-bit build read 2^32 as artboard 0.
assert!(s.execute("print.tiling.set", &json!({"origin": [1, 2], "artboard": 1u64 << 32})).is_err());
assert!(s.execute("print.tiling.set", &json!({"origin": [1e9, 2]})).is_err());
}
+2
View File
@@ -72,6 +72,8 @@ fn five_colours_reduce_to_two() {
assert_eq!(run(&mut s, "recolor.colors", json!({}))["colors"].as_array().unwrap().len(), 5);
// Without a count every colour is its own row.
assert_eq!(run(&mut s, "recolor.reduce", json!({}))["map"].as_array().unwrap().len(), 5);
// A count larger than the number of colors also keeps every color in its own row. A 32-bit build read 2^32 + 2 as 2.
assert_eq!(run(&mut s, "recolor.reduce", json!({"colors": (1u64 << 32) + 2}))["map"].as_array().unwrap().len(), 5);
}
#[test]
+3
View File
@@ -140,6 +140,9 @@ fn divide_two_by_three_and_by_size() {
assert_eq!(hs, [25.0, 25.0, 10.0]);
assert!(s.execute("object.slice.divide", &json!({"rows": 0})).is_err());
assert!(s.execute("object.slice.divide", &json!({"columns": 5000})).is_err());
// A 32-bit build read 2^32 + 2 as 2, passed the check and went on to list 2^32 + 3 cuts.
let e = s.execute("object.slice.divide", &json!({"rows": (1u64 << 32) + 2})).unwrap_err().to_string();
assert!(e.contains("divide into 1 to"), "{e}");
assert!(s.execute("object.slice.divide", &json!({})).is_err(), "nothing to divide by");
}
+10
View File
@@ -102,6 +102,16 @@ fn move_reorders_and_undoes_in_one_step() {
assert!(s.execute("graphicStyle.move", &json!({"name": first})).is_err());
}
/// A graphic style moved to place 2^32 goes last. A 32-bit build read the place as 0 and moved it
/// to the front.
#[test]
fn move_past_u32_goes_last() {
let mut s = session();
let second = names(&s)[1].clone();
run(&mut s, "graphicStyle.move", json!({"name": second, "to": 1u64 << 32}));
assert_eq!(names(&s).last().unwrap(), &second);
}
#[test]
fn override_character_color_clears_the_run_fill() {
let mut s = session();
+4
View File
@@ -250,6 +250,10 @@ fn select_same_appearance_attribute() {
run(&mut s, "select.set", json!({"ids": [a.0]}));
run(&mut s, "select.same.appearanceAttribute", json!({"item": 1}));
assert_eq!(s.doc().unwrap().selection.objects, vec![a, b, c]);
// An item past the last one leaves the shared effect. A 32-bit build read 2^32 + 1 as item 1.
run(&mut s, "select.set", json!({"ids": [a.0]}));
run(&mut s, "select.same.appearanceAttribute", json!({"item": (1u64 << 32) + 1}));
assert_eq!(s.doc().unwrap().selection.objects, vec![a, b]);
}
#[test]
+11
View File
@@ -56,6 +56,17 @@ fn move_between_groups_is_one_undo_step() {
assert_eq!(names(&s, None).last().map(String::as_str), Some("White"));
}
/// A swatch or color group moved to place 2^32 goes last. A 32-bit build read the place as 0 and
/// moved it to the front.
#[test]
fn move_past_u32_goes_last() {
let mut s = session();
run(&mut s, "swatch.move", json!({"name": "White", "to": 1u64 << 32}));
assert_eq!(names(&s, None).last().map(String::as_str), Some("White"));
run(&mut s, "swatch.move", json!({"name": "Grays", "to": 1u64 << 32}));
assert_eq!(doc(&s).swatch_groups.last().map(|g| g.name.as_str()), Some("Grays"));
}
#[test]
fn move_reorders_groups_and_refuses_bad_moves() {
let mut s = session();
+16
View File
@@ -55,6 +55,9 @@ fn edit_range_replaces_and_undoes() {
assert_eq!(obj(&s, id).plain_text(), "Hello!");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(obj(&s, id).plain_text(), "Hello there");
// An offset of 2^32 is past the end too. A 32-bit build read it as 0 and inserted at the start.
s.execute("text.editRange", &json!({"id": id.0, "start": 1u64 << 32, "end": 1u64 << 32, "insert": "!"})).unwrap();
assert_eq!(obj(&s, id).plain_text(), "Hello there!");
assert!(s.execute("text.editRange", &json!({"start": 0})).is_err(), "id required");
let rect = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
assert!(s.execute("text.editRange", &json!({"id": rect["id"], "insert": "x"})).is_err(), "not text");
@@ -84,6 +87,17 @@ fn set_range_style_splits_runs() {
assert!(s.execute("text.setRangeStyle", &json!({"id": id.0, "size": -1})).is_err());
}
/// A character style applied from byte 2^32 to 2^32 + 5 styles no character, as a range past the
/// end does. A 32-bit build read the range as bytes 0 to 5 and styled "Hello".
#[test]
fn char_style_range_past_u32_styles_nothing() {
let mut s = session();
let id = text(&mut s, "Hello world");
s.execute("charStyle.new", &json!({"name": "Big", "attrs": {"size": 40.0}})).unwrap();
s.execute("charStyle.apply", &json!({"name": "Big", "id": id.0, "start": 1u64 << 32, "end": (1u64 << 32) + 5})).unwrap();
assert!(obj(&s, id).runs.iter().all(|r| r.style.size == 20.0 && r.style.style_name.is_none()), "{:?}", obj(&s, id).runs);
}
#[test]
fn set_range_style_defaults_to_all_text_and_get_range() {
let mut s = session();
@@ -147,6 +161,8 @@ fn touch_type_moves_one_character_and_leaves_the_others_where_they_are() {
assert!(near(moved_by(&before, &glyph_pens(&s, id), 0), (0.0, 2.0)));
assert!(s.execute("text.touchType", &json!({"id": id.0, "start": 1})).is_err(), "nothing to change");
assert!(s.execute("text.touchType", &json!({"id": id.0, "start": 9, "dy": 1})).is_err(), "no character there");
// A 32-bit build read 2^32 + 1 as character 1.
assert!(s.execute("text.touchType", &json!({"id": id.0, "start": (1u64 << 32) + 1, "dy": 1})).is_err(), "no character there");
}
/// Raising, scaling or rotating a character leaves it and the characters after it where they
+8 -1
View File
@@ -30,7 +30,14 @@ fn rect(d: &mut Document, (x, y, w, h): (f64, f64, f64, f64), radius: f64) -> No
Appearance::basic(fill, Paint::solid(Color::rgb8(0, 0, 255)), 2.0),
);
if let NodeKind::Path { live, .. } = &mut n.kind {
*live = Some(Box::new(LiveShape::Rectangle { w, h, radii: [radius; 4], kinds: Default::default(), radii_relative: false, xf: Affine::translate((x, y)) }));
*live = Some(Box::new(LiveShape::Rectangle {
w,
h,
radii: [radius; 4],
kinds: Default::default(),
radii_relative: false,
xf: Affine::translate((x, y)),
}));
}
n
}
+10 -2
View File
@@ -485,7 +485,14 @@ mod tests {
#[test]
fn widgets_of_several_selected_paths_drag_together() {
let (mut d, a) = live_rect(0.0, Affine::translate((100.0, 100.0)));
let live = LiveShape::Rectangle { w: 100.0, h: 100.0, radii: [0.0; 4], kinds: Default::default(), radii_relative: false, xf: Affine::translate((300.0, 100.0)) };
let live = LiveShape::Rectangle {
w: 100.0,
h: 100.0,
radii: [0.0; 4],
kinds: Default::default(),
radii_relative: false,
xf: Affine::translate((300.0, 100.0)),
};
let b = d.alloc_id();
let mut n = Node::path(b, live.to_path(), Appearance::default_art());
if let NodeKind::Path { live: slot, .. } = &mut n.kind {
@@ -792,7 +799,8 @@ mod tests {
/// cuts outline in red.
#[test]
fn a_live_polygon_rounds_with_either_tool() {
let live = LiveShape::Polygon { radius: 80.0, sides: 6, xf: Affine::translate((200.0, 200.0)), radii: vec![], kinds: vec![], radii_relative: false };
let live =
LiveShape::Polygon { radius: 80.0, sides: 6, xf: Affine::translate((200.0, 200.0)), radii: vec![], kinds: vec![], radii_relative: false };
let (d, id) = doc_with(live.to_path(), Some(live));
let s = selected(id);
let p = paint();
+1 -5
View File
@@ -231,11 +231,7 @@ fn glass_fields(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, id:
// The stored texture is shown as its size, not as a text field.
let stored = d.fields.remove(TEXTURE_IMAGE);
let size = stored.as_ref().and_then(Value::as_str).and_then(texture).map(|t| t.size());
let menu = if id == TEXTURIZER {
texturizer_textures(size.is_some())
} else {
glass_textures(size.is_some())
};
let menu = if id == TEXTURIZER { texturizer_textures(size.is_some()) } else { glass_textures(size.is_some()) };
let doc = vectorcraft_effects::effect_info(id).map(|e| e.params).unwrap_or_default();
let unit = app.session.general_unit();
let pick = |k: &str| if k == "texture" { Some(menu) } else { choices(id, k) };
+3 -11
View File
@@ -725,7 +725,6 @@ mod tests {
fn arrow(&self) -> egui::Pos2 {
egui::pos2(SCREEN.x - 10.0, HEADER / 2.0)
}
}
fn icon_panel_count() -> usize {
@@ -842,11 +841,7 @@ mod tests {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
assert!(app.ui.docking.is_none());
app.run(
"window.panel.move",
json!({ "panel": "libraries", "anchor": "properties", "before": "properties" }),
)
.unwrap();
app.run("window.panel.move", json!({ "panel": "libraries", "anchor": "properties", "before": "properties" })).unwrap();
let order: Vec<_> = floating::docked_tabs(&app.ui).map(|t| t.info().0).collect();
assert_eq!(order, ["libraries", "properties", "layers"]);
let saved: crate::state::UiState = serde_json::from_slice(&serde_json::to_vec(&app.ui).unwrap()).unwrap();
@@ -855,11 +850,8 @@ mod tests {
#[test]
fn partial_dock_tab_order_appends_tabs_missing_from_preferences() {
let ui = crate::state::UiState {
dock_tab: DockTab::Layers,
dock_tab_order: vec![DockTab::Layers, DockTab::Properties],
..Default::default()
};
let ui =
crate::state::UiState { dock_tab: DockTab::Layers, dock_tab_order: vec![DockTab::Layers, DockTab::Properties], ..Default::default() };
let order = floating::resolved_dock_tab_order(&ui);
assert_eq!(order.len(), DockTab::ALL.len());
assert_eq!(order[..2], [DockTab::Layers, DockTab::Properties]);
+1 -3
View File
@@ -1295,9 +1295,7 @@ mod tests {
h.drag(h.icons()[2].center(), pos2(500.0, 250.0));
h.drag(h.icons()[0].center(), h.shared_tab("swatches").center());
assert_eq!(h.groups(), [["swatches", "color"]]);
h.app
.run("window.panel.move", json!({ "panel": "color", "anchor": "swatches", "before": "swatches" }))
.unwrap();
h.app.run("window.panel.move", json!({ "panel": "color", "anchor": "swatches", "before": "swatches" })).unwrap();
assert_eq!(h.groups(), [["color", "swatches"]]);
}
+6 -26
View File
@@ -167,12 +167,7 @@ fn legacy_strip_reorder_target(
/// Legacy workspaces (no saved `docking` layout): reorder tabs in the dock strip or a floating
/// group without promoting to a shared layout.
fn legacy_same_strip_reorder(
app: &mut VectorcraftApp,
panel: &str,
before: &str,
target: LegacyStripReorder,
) -> Result<Value, String> {
fn legacy_same_strip_reorder(app: &mut VectorcraftApp, panel: &str, before: &str, target: LegacyStripReorder) -> Result<Value, String> {
use crate::state::DockTab;
match target {
LegacyStripReorder::DockStrip => {
@@ -852,8 +847,7 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
return app.ui.docking.is_some();
}
craft_ui::docking::cancel_drag::<String>(ui.ctx(), egui::Id::new("vectorcraft-panel-docking"));
ui.ctx()
.data_mut(|data| data.remove::<LegacyDrag>(egui::Id::new("vectorcraft-panel-docking").with("legacy-origin")));
ui.ctx().data_mut(|data| data.remove::<LegacyDrag>(egui::Id::new("vectorcraft-panel-docking").with("legacy-origin")));
}
for action in output.actions {
if tab_drop
@@ -947,13 +941,7 @@ pub(crate) fn legacy_tab(app: &mut VectorcraftApp, ui: &mut egui::Ui, panel: &st
legacy_tab_in_strip(app, ui, panel, response, None);
}
pub(crate) fn legacy_tab_in_strip(
app: &mut VectorcraftApp,
ui: &mut egui::Ui,
panel: &str,
response: &egui::Response,
strip: Option<egui::Rect>,
) {
pub(crate) fn legacy_tab_in_strip(app: &mut VectorcraftApp, ui: &mut egui::Ui, panel: &str, response: &egui::Response, strip: Option<egui::Rect>) {
let Some(panel) = normalize(panel) else { return };
response.context_menu(|ui| {
if ui.button(tl!("Float Panel")).clicked() {
@@ -977,19 +965,11 @@ pub(crate) fn legacy_tab_in_strip(
}
});
});
let inside_strip = strip.is_some_and(|strip| {
ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| strip.expand(4.0).contains(p))
});
let inside_strip = strip.is_some_and(|strip| ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| strip.expand(4.0).contains(p)));
let area = egui::Id::new("vectorcraft-panel-docking");
let begun = ui.ctx().data(|d| d.get_temp::<LegacyDrag>(area.with("legacy-origin")).is_some());
let begin = if strip.is_some() {
!begun && (response.drag_started() || (!inside_strip && response.dragged()))
} else {
response.drag_started()
};
if begin
&& let Some(id) = normalize(panel)
{
let begin = if strip.is_some() { !begun && (response.drag_started() || (!inside_strip && response.dragged())) } else { response.drag_started() };
if begin && let Some(id) = normalize(panel) {
let mut layout = app.ui.docking.clone().unwrap_or_else(|| legacy(app));
let bounds = ui.max_rect();
let source = egui::Rect::from_min_size(
+1 -11
View File
@@ -88,17 +88,7 @@ fn effect_menu_lists_the_artistic_filters_before_blur() {
let labels: Vec<&str> = artistic.iter().map(|e| e.label.as_str()).collect();
assert_eq!(
labels,
[
"Cutout…",
"Dry Brush…",
"Film Grain…",
"Neon Glow…",
"Plastic Wrap…",
"Poster Edges…",
"Smudge Stick…",
"Sponge…",
"Watercolor…"
]
["Cutout…", "Dry Brush…", "Film Grain…", "Neon Glow…", "Plastic Wrap…", "Poster Edges…", "Smudge Stick…", "Sponge…", "Watercolor…"]
);
assert!(artistic.iter().all(|e| e.command.as_deref() == Some("effect.dialog")));
assert_eq!(artistic[1].params, json!({"effect": "artistic.dryBrush"}));
+1 -6
View File
@@ -142,12 +142,7 @@ pub fn sync_touch_workspace(app: &mut VectorcraftApp, touch_screen: bool) {
}
}
} else if app.ui.touch_workspace_restore.is_some() && app.ui.workspace == TOUCH {
let back = app
.ui
.touch_workspace_restore
.take()
.filter(|n| find(&app.ui, n).is_some())
.unwrap_or_else(|| ESSENTIALS.to_string());
let back = app.ui.touch_workspace_restore.take().filter(|n| find(&app.ui, n).is_some()).unwrap_or_else(|| ESSENTIALS.to_string());
if let Some(w) = find(&app.ui, &back) {
apply(&mut app.ui, &w);
}
+10
View File
@@ -942,6 +942,16 @@ missing one does.
`` `columns` must be a number, not "4" ``. Each message names the key that was given, and the command then changes
nothing.
A whole number that is larger than the build's `usize` can hold (4294967295 on a 32-bit build, such as the
`wasm32-unknown-unknown` web build) counts as larger than any index, place, byte offset or count. An index, such as
a `subpath`, `anchor`, `segment`, document `index`, `artboard`, appearance `item` or `index`, gradient `stop` or
freeform point `index`, names nothing, and the command does what it does for any other index past the end: most
commands fail and change nothing, and `appearance.removeItem` removes nothing. A place (`to` in `swatch.move`,
`artboard.reorder`, `graphicStyle.move`, `appearance.moveItem`, `appearance.duplicateItem` and `effect.move`) puts
the item last. A text `start` or `end` is past the end of the text. `object.slice.divide` refuses such a `rows` or
`columns`, `recolor.reduce` keeps each color in its own row for such a `colors`, and `layer.setProps` refuses such a
`color`. `object.setBounds` reads a `reference` past 8 as 8, as `object.transformEach` does.
The blend, envelope and gradient mesh commands refuse a string or another value that is not a number with the same
message, as in `` `steps` must be a number, not "8" ``, and change nothing: `steps`, `distance` and `value` in
`object.blend.make` and `object.blend.options`; `bend`, `h`, `v`, `horizontalDistortion` and `verticalDistortion` in