Extract geometry command helpers

This commit is contained in:
Dylan M. Taylor
2026-10-28 16:44:11 +00:00
parent 90ef99cb2c
commit 7043ef6f3d
47 changed files with 1178 additions and 776 deletions
+4 -1
View File
@@ -27,7 +27,10 @@ use super::draw2::replace_with_pieces;
use super::edit::{outermost, selected_roots};
use super::paint::paint_from;
use super::pathops::{Beside, shape_node};
use super::*;
use super::{
CommandSpec, EngineError, Result, Session, always, bad, check_numbers, cmd, color_value, f64_or, finite_numbers, has_doc, id_param, ids_param,
number_or, point_param, str_param, switch_or,
};
mod live_paint;
mod shape_builder;
@@ -1,4 +1,8 @@
use super::*;
use super::{
Appearance, Arc, Beside, Document, EDGE_NAME, EngineError, FACE_NAME, GapOptions, GapSize, LIVE_PAINT_NAME, Node, NodeId, Paint, PathData, Point,
Rect, Result, SOURCES_NAME, Selection, Session, SubPath, Value, b, bad, build_children, edge_near, face_at, finite, id_param, ids_json,
is_live_paint, json, number_or, paint_from, po, point_param, repaint_from, replace_roots, root_ids, source_nodes, source_shapes, switch_or,
};
/// `options` as stored on a Live Paint group: none for the defaults.
fn stored_gap_options(options: GapOptions) -> Option<Box<GapOptions>> {
@@ -1,4 +1,8 @@
use super::*;
use super::{
Beside, BuilderMap, EngineError, FillRule, Node, NodeId, PathData, Point, Rect, Result, Session, SubPath, Touched, Value, b, bad, check_numbers,
f64_or, fill_param, finite, gap_param, ids_json, json, marquee_param, po, points_param, remnant, replace_roots, replace_with_pieces, root_ids,
shape_node, switch_or,
};
pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "shapeBuilder.merge";
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Appearance, AppearanceItem, Arc, Beside, Color, Document, EngineError, LineCap, LineJoin, LiveShape, Node, NodeId, NodeKind, Paint, Point,
Result, Session, StrokeLayer, TRACE_NAME, TraceView, Value, bad, fill_only, ids_json, json, root_ids, shape_node, str_param, switch_or, tr,
};
// ---------- Image Trace ----------
+4 -1
View File
@@ -14,7 +14,10 @@ use vectorcraft_tools::distort::perspective::{self as persp, PerspectiveGrid, Pl
use vectorcraft_tools::distort::{PinSet, arap, collect_points, mesh_for, project_node, stroke_owner, warp_from_rest, warp_node_with};
use super::edit::{duplicate_in, outermost, selected_roots};
use super::*;
use super::{
CommandSpec, Result, Session, bad, check_numbers, check_switches, cmd, f64_or, f64_req, finite_numbers, has_doc, has_selection, id_param,
ids_param, matrix_param, number_or, ok, point_indices, point_param, str_param, switch_or, targets,
};
use crate::EngineError;
mod perspective;
@@ -1,4 +1,8 @@
use super::*;
use super::{
Affine, Arc, Document, EngineError, Homography, NodeId, NodeKind, PerspectiveGrid, Plane, Point, Result, Session, Value, bad, check_numbers,
check_switches, collect_points, duplicate_in, finite_numbers, id_param, ids_param, json, matrix_param, number_or, ok, outermost, persp,
point_param, project_node, selected_roots, str_param, switch_or,
};
// ---------- perspective grid ----------
+4 -1
View File
@@ -17,7 +17,10 @@ use vectorcraft_tools::draw2::{eraser_area, eraser_tip};
use super::create::{add_look, insert_look};
use super::newart::NewArt;
use super::pathops::{Beside, remove_each};
use super::*;
use super::{
CommandSpec, Result, Session, bad, check_numbers, cmd, f64_or, has_anchors, has_doc, has_selection, id_param, index, number_or, ok, str_param,
switch_or,
};
use crate::EngineError;
mod anchors;
+4 -1
View File
@@ -1,5 +1,8 @@
use super::common::{MAX_INPUT, path_kind, path_mut, point_entry, usize_opt, usize_req, within_input_bound};
use super::*;
use super::{
Anchor, BTreeSet, Beside, Color, Document, EngineError, Node, NodeId, NodeKind, Paint, PathData, Point, Result, Selection, Session, Value, Vec2,
add_look, bad, check_numbers, f64_or, id_param, json, number_or, ok, po, str_param, switch_or,
};
// ---------- Curvature ----------
+1 -1
View File
@@ -2,7 +2,7 @@ use super::anchors::{convert_anchor, curvature, curvature_edit, remove_anchor, r
use super::cut::{erase_region, erase_segments, knife, split};
use super::freehand::{blob, freehand};
use super::smooth::{join_scrub, smooth_region};
use super::*;
use super::{CommandSpec, cmd, has_anchors, has_doc, has_selection};
pub fn specs() -> Vec<CommandSpec> {
vec![
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Appearance, Arc, Beside, Cap, Color, Document, EngineError, FillRule, Join, Node, NodeId, NodeKind, Paint, PathData, Point, PressureProfile,
Rect, Result, Selection, Session, SubPath, Value, Vec2, bad, index, json, po,
};
// ---------- helpers ----------
+4 -1
View File
@@ -1,7 +1,10 @@
use super::common::{
dist_to_polyline, ids_json, path_mut, path_of, points_param, poly_bounds, replace_with_pieces, sibling_with, target_paths, usize_opt,
};
use super::*;
use super::{
Anchor, AnchorKind, Beside, BezPath, BoolOp, Cap, EngineError, FillRule, Join, NodeId, ParamCurve, PathData, Point, Result, Session, SubPath,
Value, bad, eraser_area, eraser_tip, f64_or, fill_contains, id_param, json, number_or, po,
};
/// Split a filled path into connected pieces (each outer contour with its holes).
pub(in super::super) fn components(pd: &PathData) -> Vec<PathData> {
+4 -1
View File
@@ -2,7 +2,10 @@ use super::common::{
brush_region, path_kind, path_mut, path_of, points_param, poly_bounds, samples_param, stroke_paint, stroke_pressure, subpath_len, target_paths,
within_input_bound,
};
use super::*;
use super::{
Anchor, Appearance, BTreeSet, BoolOp, Color, EngineError, FillRule, NewArt, NodeId, NodeKind, Paint, PathData, Point, PressureProfile, Result,
Selection, Session, SimplifyOptions, SubPath, Value, bad, id_param, insert_look, json, number_or, po, remove_each, str_param, switch_or,
};
// ---------- Pencil / Paintbrush ----------
+4 -1
View File
@@ -1,5 +1,8 @@
use super::common::{dist_to_polyline, ids_json, path_mut, path_of, points_param, poly_bounds, target_paths};
use super::*;
use super::{
Anchor, BezPath, CubicBez, NodeId, ParamCurve, ParamCurveNearest, PathData, Point, Result, Session, SimplifyOptions, SubPath, Value, json,
number_or, po,
};
// ---------- Smooth ----------
+1 -1
View File
@@ -38,7 +38,7 @@ use vectorcraft_render::effects;
use super::edit::roots_of;
use super::menucmds::{MAX_PIXELS, isolated_doc, unique_key};
use super::pathops::{node_path, outline_strokes, outline_strokes_under, separate, shape_node};
use super::*;
use super::{CommandSpec, EngineError, Result, Session, always, bad, bool_or, cmd, has_doc, str_param, targets, unique_name};
mod analysis;
mod apply;
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Arc, BTreeSet, Document, Effect, ImageBlob, Knockout, Node, NodeKind, Rect, effects, outline_strokes};
// ---------- plain art ----------
+5 -1
View File
@@ -1,7 +1,11 @@
use super::analysis::{Scratch, outlined_text, overlapping, reach};
use super::compose::{Builder, CLEAR, Composer, Elem, Fill, Kind, WHITE};
use super::options::FlattenOptions;
use super::*;
use super::{
Affine, Appearance, AppearanceItem, Arc, BlendMode, Color, ColorMode, Document, EngineError, FillLayer, FillRule, ImageBlob, ImageObject, Inks,
MAX_PIXELS, Node, NodeId, NodeKind, Paint, PathData, Rect, Result, effects, isolated_doc, outline_strokes_under, planes_cmyk, po, separate,
shape_node, shapes, unique_key,
};
// ---------- plan ----------
+1 -1
View File
@@ -1,7 +1,7 @@
use super::options::flatten;
use super::presets::{presets_delete, presets_export, presets_import, presets_list, presets_save};
use super::report::preview;
use super::*;
use super::{CommandSpec, always, cmd, has_doc};
pub fn specs() -> Vec<CommandSpec> {
vec![
+1 -1
View File
@@ -1,5 +1,5 @@
use super::analysis::{has_effects, masked, reach};
use super::*;
use super::{BlendMode, Color, FillRule, Knockout, Node, NodeKind, Paint, PathData, Rect, cmyk_planes, composite, effects, node_path, po, shapes};
// ---------- the compositor ----------
+1 -1
View File
@@ -2,7 +2,7 @@ use super::analysis::draws_image;
use super::apply::{apply, kept_work, plan};
use super::presets::{FlattenerPreset, saved_preset};
use super::report::target_roots;
use super::*;
use super::{BTreeSet, Deserialize, EngineError, Node, Result, Serialize, Session, Value, bad, json, str_param};
/// Object → Flatten Transparency settings: a preset's, adjusted by the command's params.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+1 -1
View File
@@ -1,5 +1,5 @@
use super::options::FlattenOptions;
use super::*;
use super::{Deserialize, Result, Serialize, Session, Value, bad, bool_or, json, str_param, unique_name};
// ---------- presets ----------
+4 -1
View File
@@ -1,6 +1,9 @@
use super::apply::{kept_work, opaque, plan, stroked};
use super::options::FlattenOptions;
use super::*;
use super::{
Appearance, Color, Deserialize, Document, Node, NodeId, NodeKind, Paint, PathData, Rect, Result, Serialize, Session, Value, bad, isolated_doc,
json, roots_of, shape_node, str_param, targets,
};
/// The top-level objects among `ids` (or the selection), in paint order: layers stand for their
/// contents; hidden and locked objects and clipping paths are left alone.
+1 -1
View File
@@ -10,7 +10,7 @@ use vectorcraft_geom::{Affine, Rect};
use super::appearance::{edit_items, edits_stroke, index_param, item_target};
use super::gradient::{StopOwner, item_paint_bounds, run_paint_mut};
use super::*;
use super::{CommandSpec, Result, Session, bad, cmd, color_value, has_doc, index, number_or, point_indices, point_param, str_param, switch_or};
pub fn specs() -> Vec<CommandSpec> {
vec![
+1 -1
View File
@@ -17,7 +17,7 @@ use vectorcraft_doc::{
use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Rect, shapes};
use super::typecmd::refresh_bounds;
use super::*;
use super::{CommandSpec, EngineError, Result, Session, bad, bool_or, cmd, f64_or, f64_req, has_doc, has_selection, id_param, ok, str_param};
mod axis;
mod columns;
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::Value;
pub(super) const LABEL_SIZE: f64 = 9.0;
+4 -1
View File
@@ -4,7 +4,10 @@ use super::edit::{regenerate, spec_of, target};
use super::generate::generate_with;
use super::markers::{design_frame, fit_affine};
use super::series::{picked_series, reselect_series};
use super::*;
use super::{
Affine, Arc, ColumnDesign, ColumnType, Document, FillRule, Fractions, GraphSpec, Node, NodeKind, Point, Rect, Result, Session, TextObject, Value,
bad, json, refresh_bounds, shapes,
};
/// Most objects a graph's column designs may add (each column, repeated copy and legend swatch draws a copy).
const COLUMN_BUDGET: usize = 200_000;
+1 -1
View File
@@ -3,7 +3,7 @@ use super::data::set_data;
use super::designs::design;
use super::edit::{create, set_type};
use super::markers::set_marker;
use super::*;
use super::{CommandSpec, cmd, has_doc, has_selection};
pub fn specs() -> Vec<CommandSpec> {
vec![
+1 -1
View File
@@ -1,6 +1,6 @@
use super::edit::{regenerate, spec_of, target};
use super::series::{switch_xy, transpose};
use super::*;
use super::{Document, EngineError, GraphKind, GraphSpec, NodeId, Result, Session, Value, bad, bool_or, json, str_param};
// ---------- data ----------
+1 -1
View File
@@ -1,5 +1,5 @@
use super::edit::regenerate;
use super::*;
use super::{Arc, Document, GraphDesign, GraphSpec, Result, Session, Value, bad, json, ok, str_param};
/// The document's graph design names, in order.
pub(super) fn design_names(d: &Document) -> Vec<String> {
+59 -52
View File
@@ -2,7 +2,10 @@ use super::axis::{MAX_TICK_MARKS, count_param, one_line};
use super::data::apply_data;
use super::generate::{children_bounds, generate};
use super::series::{picked_series, reselect_series};
use super::*;
use super::{
Affine, Document, EngineError, GraphKind, GraphSpec, Node, NodeId, Rect, Result, Session, TickLength, Value, ValueAxisSide, bad, bool_or, f64_or,
f64_req, id_param, json, str_param,
};
/// Regenerate graph `id` from `spec` (adjusting the plot rect for any move/scale since the last
/// generation, which is recorded in the spec's `placed` bounds).
@@ -74,6 +77,60 @@ pub(super) fn spec_of(s: &Session, id: NodeId) -> Result<GraphSpec> {
s.doc()?.doc.node(id).and_then(|n| n.extras().graph.as_deref().cloned()).ok_or(EngineError::NoNode(id))
}
fn query_type_options(spec: &GraphSpec, count: usize, picked: &[usize]) -> Value {
// With series picked, the type and value axis they share, so the dialog's OK keeps them.
let kinds: Vec<GraphKind> = picked.iter().map(|i| spec.series_kind(*i)).collect();
let kind = kinds.first().copied().filter(|k| kinds.iter().all(|x| x == k)).unwrap_or(spec.kind);
// Series on both axes give no value axis (null), so OK leaves each where it is.
let sides: Vec<bool> = picked.iter().map(|i| spec.right_series.binary_search(i).is_ok()).collect();
let value_axis = match sides.first() {
None => Some(spec.value_axis.id()),
Some(r) if sides.iter().all(|x| x == r) => Some(if *r { "right" } else { "left" }),
Some(_) => None,
};
let mut fields = json!({
"type": kind.id(), "columnWidth": spec.column_width, "clusterWidth": spec.cluster_width, "legend": spec.legend,
"markPoints": spec.mark_points, "connectPoints": spec.connect_points, "edgeToEdge": spec.edge_to_edge, "ticks": spec.ticks,
"axisMin": spec.axis_min, "axisMax": spec.axis_max, "valueAxis": value_axis, "separateScales": spec.separate_scales,
"rightTicks": spec.right_ticks, "rightAxisMin": spec.right_axis_min, "rightAxisMax": spec.right_axis_max,
"tickLength": spec.tick_length.id(), "tickMarks": spec.tick_marks.clamp(1, MAX_TICK_MARKS), "rightTickLength": spec.right_tick_length.id(),
"rightTickMarks": spec.right_tick_marks.clamp(1, MAX_TICK_MARKS), "categoryTickLength": spec.category_tick_length.id(),
"categoryTickMarks": spec.category_tick_marks.clamp(1, MAX_TICK_MARKS), "ticksBetweenLabels": spec.ticks_between_labels,
"prefix": one_line(&spec.prefix), "suffix": one_line(&spec.suffix), "rightPrefix": one_line(&spec.right_prefix),
"rightSuffix": one_line(&spec.right_suffix), "dropShadow": spec.drop_shadow, "legendAcrossTop": spec.legend_across_top,
"firstRowInFront": spec.first_row_in_front, "firstColumnInFront": spec.first_column_in_front, "filledLines": spec.filled_lines,
});
// Only the options this graph's types use: overlap order for columns and bars, filled lines for lines.
let kinds: Vec<GraphKind> = (0..count.max(1)).map(|i| spec.series_kind(i)).collect();
let has = |ks: &[GraphKind]| kinds.iter().any(|k| ks.contains(k));
if let Some(o) = fields.as_object_mut() {
if !has(&[GraphKind::Column, GraphKind::StackedColumn, GraphKind::Bar, GraphKind::StackedBar]) {
o.remove("firstRowInFront");
o.remove("firstColumnInFront");
}
if !has(&[GraphKind::Line, GraphKind::Scatter, GraphKind::Radar]) {
o.remove("filledLines");
}
}
// Pie graphs: Legend, Position and Sort (their Legend stands for the legend checkbox).
if let Some(o) = fields.as_object_mut().filter(|_| spec.kind == GraphKind::Pie) {
o.remove("legend");
o.insert(
"pieLegend".into(),
json!(if !spec.legend {
"none"
} else if spec.pie_legend_in_wedges {
"wedges"
} else {
"standard"
}),
);
o.insert("piePosition".into(), json!(spec.pie_position.id()));
o.insert("pieSort".into(), json!(spec.pie_sort.id()));
}
fields
}
pub(super) fn set_type(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "graph.setType";
let id = target(s, p, C)?;
@@ -127,57 +184,7 @@ pub(super) fn set_type(s: &mut Session, p: &Value) -> Result<Value> {
"pieSort",
];
if !keys.iter().any(|k| p.get(*k).is_some()) {
// With series picked, the type and value axis they share, so the dialog's OK keeps them.
let kinds: Vec<GraphKind> = picked.iter().map(|i| spec.series_kind(*i)).collect();
let kind = kinds.first().copied().filter(|k| kinds.iter().all(|x| x == k)).unwrap_or(spec.kind);
// Series on both axes give no value axis (null), so OK leaves each where it is.
let sides: Vec<bool> = picked.iter().map(|i| spec.right_series.binary_search(i).is_ok()).collect();
let value_axis = match sides.first() {
None => Some(spec.value_axis.id()),
Some(r) if sides.iter().all(|x| x == r) => Some(if *r { "right" } else { "left" }),
Some(_) => None,
};
let mut fields = json!({
"type": kind.id(), "columnWidth": spec.column_width, "clusterWidth": spec.cluster_width, "legend": spec.legend,
"markPoints": spec.mark_points, "connectPoints": spec.connect_points, "edgeToEdge": spec.edge_to_edge, "ticks": spec.ticks,
"axisMin": spec.axis_min, "axisMax": spec.axis_max, "valueAxis": value_axis, "separateScales": spec.separate_scales,
"rightTicks": spec.right_ticks, "rightAxisMin": spec.right_axis_min, "rightAxisMax": spec.right_axis_max,
"tickLength": spec.tick_length.id(), "tickMarks": spec.tick_marks.clamp(1, MAX_TICK_MARKS), "rightTickLength": spec.right_tick_length.id(),
"rightTickMarks": spec.right_tick_marks.clamp(1, MAX_TICK_MARKS), "categoryTickLength": spec.category_tick_length.id(),
"categoryTickMarks": spec.category_tick_marks.clamp(1, MAX_TICK_MARKS), "ticksBetweenLabels": spec.ticks_between_labels,
"prefix": one_line(&spec.prefix), "suffix": one_line(&spec.suffix), "rightPrefix": one_line(&spec.right_prefix),
"rightSuffix": one_line(&spec.right_suffix), "dropShadow": spec.drop_shadow, "legendAcrossTop": spec.legend_across_top,
"firstRowInFront": spec.first_row_in_front, "firstColumnInFront": spec.first_column_in_front, "filledLines": spec.filled_lines,
});
// Only the options this graph's types use: overlap order for columns and bars, filled lines for lines.
let kinds: Vec<GraphKind> = (0..count.max(1)).map(|i| spec.series_kind(i)).collect();
let has = |ks: &[GraphKind]| kinds.iter().any(|k| ks.contains(k));
if let Some(o) = fields.as_object_mut() {
if !has(&[GraphKind::Column, GraphKind::StackedColumn, GraphKind::Bar, GraphKind::StackedBar]) {
o.remove("firstRowInFront");
o.remove("firstColumnInFront");
}
if !has(&[GraphKind::Line, GraphKind::Scatter, GraphKind::Radar]) {
o.remove("filledLines");
}
}
// Pie graphs: Legend, Position and Sort (their Legend stands for the legend checkbox).
if let Some(o) = fields.as_object_mut().filter(|_| spec.kind == GraphKind::Pie) {
o.remove("legend");
o.insert(
"pieLegend".into(),
json!(if !spec.legend {
"none"
} else if spec.pie_legend_in_wedges {
"wedges"
} else {
"standard"
}),
);
o.insert("piePosition".into(), json!(spec.pie_position.id()));
o.insert("pieSort".into(), json!(spec.pie_sort.id()));
}
return Ok(fields);
return Ok(query_type_options(&spec, count, &picked));
}
// Series picked with Group Selection stay selected through the regeneration (their groups are new nodes).
let reselect: Vec<u32> = if p.get("seriesIndexes").is_none() { picked.iter().filter_map(|i| u32::try_from(*i).ok()).collect() } else { vec![] };
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -2,7 +2,10 @@ use super::axis::LABEL_SIZE;
use super::columns::{Allowance, ColumnArt};
use super::markers::fit_design;
use super::series::{default_series_paint, series_marks};
use super::*;
use super::{
Affine, Appearance, Arc, BezPath, CharStyle, Color, Document, GraphSpec, Justify, Node, NodeId, NodeKind, Paint, PathData, Point, Rect,
TextObject, refresh_bounds, shapes,
};
pub(super) struct Gen<'a> {
pub(super) d: &'a mut Document,
+1 -1
View File
@@ -1,7 +1,7 @@
use super::designs::design_names;
use super::edit::{regenerate, spec_of, target};
use super::series::{picked_series, reselect_series};
use super::*;
use super::{Affine, Arc, Document, GraphSpec, Node, NodeKind, Rect, Result, Session, Value, bad, json};
/// Most objects a graph's marker designs may add (each data point and legend swatch draws a copy of its design).
pub(super) const MARKER_BUDGET: usize = 200_000;
+1 -1
View File
@@ -1,5 +1,5 @@
use super::labels::{Role, label_style};
use super::*;
use super::{CharStyle, Color, Document, GraphKind, GraphSpec, HashMap, Node, NodeId, NodeKind, Paint, Result, Session, Value, bad};
// ---------- generation ----------
+4 -1
View File
@@ -14,7 +14,10 @@ use vectorcraft_render::effects;
use super::edit::{duplicate_in, outermost, selected_roots, selected_roots_to_transform};
use super::opacitymask::percent;
use super::*;
use super::{
CommandSpec, Result, Session, bad, bool_or, check_numbers, check_switches, cmd, f64_or, f64_req, has_doc, has_multi, has_selection,
has_selection_or_guides, id_param, ids_param, matrix_param, ok, point_param, str_param, targets,
};
use crate::{EngineError, LastTransform};
mod align;
+4 -1
View File
@@ -1,5 +1,8 @@
use super::transform::transform_patterns;
use super::*;
use super::{
Affine, Document, EngineError, NodeId, OrientedBox, Point, Rect, Result, Scaling, Session, Value, Vec2, bad, check_numbers, check_switches,
effects, has_multi, ok, selected_roots, str_param,
};
impl Session {
/// The bounds of `ids` the Transform panel, the bounding box, Align and Distribute measure:
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Document, EngineError, Node, NodeId, NodeKind, Result, Session, Value, bad, id_param, json, ok, selected_roots};
pub(super) enum Arrange {
Front,
+4 -1
View File
@@ -1,6 +1,9 @@
use super::spans::Spans;
use super::transform::{TRANSFORM_SWITCHES, about, apply_transform, scaling_options, transforms_objects};
use super::*;
use super::{
Affine, Document, EngineError, NodeId, OrientedBox, Point, Rect, Result, Scaling, Session, Value, bool_or, check_numbers, check_switches,
effects, f64_or, json, selected_roots_to_transform,
};
pub(super) fn set_bounds(s: &mut Session, p: &Value) -> Result<Value> {
match bounds_transform(s, p, selected_roots_to_transform(s)?, 0.0)? {
+1 -1
View File
@@ -5,7 +5,7 @@ use super::compound::{clip_edit, clip_make, clip_release, compound_make, compoun
use super::props::set_props;
use super::shape::{expand_shape, set_live_shape};
use super::transform::{move_cmd, nudge, reflect, rotate, scale, shear, transform, transform_again};
use super::*;
use super::{CommandSpec, cmd, has_doc, has_multi, has_selection, has_selection_or_guides};
pub fn specs() -> Vec<CommandSpec> {
vec![
+3 -1
View File
@@ -1,4 +1,6 @@
use super::*;
use super::{
Appearance, Arc, Document, EngineError, FillRule, Node, NodeId, NodeKind, Paint, PathData, Result, Session, Value, json, ok, selected_roots,
};
pub(super) fn compound_make(s: &mut Session, _: &Value) -> Result<Value> {
let ids = selected_roots(s)?;
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{BlendMode, EngineError, Knockout, Node, Result, Session, Value, bad, check_numbers, check_switches, ok, percent, str_param, targets};
/// The most data entries `object.setProps` takes at once.
const MAX_DATA: usize = 256;
+4 -1
View File
@@ -1,5 +1,8 @@
use super::spans::Span;
use super::*;
use super::{
BTreeSet, CornerKind, EngineError, LiveCorners, LiveShape, NodeId, NodeKind, Result, Session, Value, bad, check_numbers, check_switches,
convert_radii_relative, ok, selected_roots, set_corners, targets,
};
/// The largest scale of 0 or more at which `spans`, with the margins that scale grown by `k`,
/// reach across `size` together. Returns 1 when their geometric boxes reach 1e-9 or less across
+1 -1
View File
@@ -1,5 +1,5 @@
use super::shape::scale_across;
use super::*;
use super::{Affine, Document, Node, NodeId, NodeKind, OrientedBox, Point, Rect};
/// One axis of an object that [`set_bounds`] sizes, square to its frame: the geometric box runs
/// from `lo` to `hi`, and the visual box reaches past each end (before, after) by a margin that
+5 -1
View File
@@ -1,5 +1,9 @@
use super::align::choice;
use super::*;
use super::{
Affine, Arc, Document, EngineError, LastTransform, Node, NodeId, NodeKind, Point, Result, Scaling, Session, Value, Vec2, bad, bool_or,
check_numbers, check_switches, duplicate_in, effects, f64_or, f64_req, ids_param, json, matrix_param, ok, outermost, point_param,
selected_roots_to_transform,
};
fn origin_of(s: &Session, p: &Value, ids: &[NodeId]) -> Result<Point> {
if let Some(o) = point_param(p, "origin") {
+4 -1
View File
@@ -9,7 +9,10 @@ use vectorcraft_geom::{Anchor, AnchorKind, PathData, Point, SubPath, Vec2};
use super::create::anchor_from_json;
use super::draw2::usize_opt;
use super::pathops::Beside;
use super::*;
use super::{
CommandSpec, Result, Session, bad, check_numbers, check_switches, cmd, f64_or, f64_req, finite_numbers, has_anchors, has_doc, has_selection,
id_param, ids_param, index, number_or, ok, str_param, switch_or, targets,
};
use crate::EngineError;
pub fn specs() -> Vec<CommandSpec> {
+1 -1
View File
@@ -14,7 +14,7 @@ use vectorcraft_pathops as po;
use vectorcraft_pathops::{BoolOp, PathfinderOp};
use super::edit::selected_roots;
use super::*;
use super::{CommandSpec, EngineError, Result, Session, always, bad, bool_or, cmd, has_doc, has_multi, has_selection, str_param};
pub fn specs() -> Vec<CommandSpec> {
vec![
+4 -1
View File
@@ -8,7 +8,10 @@ use vectorcraft_doc::{Node, NodeKind, Scaling};
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
use super::edit::{outermost, selected_roots};
use super::*;
use super::{
CommandSpec, NodeId, Result, Session, always, bad, bool_or, check_numbers, check_switches, checked_ids_param, cmd, color_value, f64_or,
find_command, finite_numbers, has_doc, index,
};
use crate::EngineError;
pub fn specs() -> Vec<CommandSpec> {
+54 -50
View File
@@ -303,59 +303,63 @@ fn another_path_join_keeps_snapped_target_when_raw_pointer_misses() {
fn snapped_only_continue(preselected: bool) {
for first in [true, false] {
let mut s = session();
let mut prefs = s.prefs.clone();
prefs.gridline_every = 100.0;
prefs.grid_subdivisions = 1;
s.apply_prefs(prefs);
assert_eq!(s.doc().unwrap().doc.grid.spacing, 100.0);
assert_eq!(s.doc().unwrap().doc.grid.subdivisions, 1);
let id = line(&mut s, (100.0, 300.0), (200.0, 300.0));
let original = node(&s, id).clone();
s.execute("select.set", &json!({"ids": if preselected { vec![id.0] } else { vec![] }})).unwrap();
let v = ViewInfo { snap_to_grid: true, ..view() };
s.select_tool("pen", v).unwrap();
let at = if first { (63.0, 303.0) } else { (237.0, 303.0) };
let end = if first { Point::new(100.0, 300.0) } else { Point::new(200.0, 300.0) };
assert_eq!(s.cursor(end, Mods::default(), v), Cursor::PenContinue, "the endpoint is eligible");
assert_eq!(s.cursor(Point::new(at.0, at.1), Mods::default(), v), Cursor::Pen, "the raw pointer misses both ends");
let journal_start = s.journal.len();
let before_continue = s.doc().unwrap().history.undo.len();
gesture(&mut s, v, &[at], Mods::default());
assert_eq!(path_ids(&s), vec![id], "snapped-only continuation must keep one original path: selected={preselected}, first={first}");
assert_eq!(s.doc().unwrap().selection.objects, vec![id]);
let expected = if first { [(200.0, 300.0), (100.0, 300.0)] } else { [(100.0, 300.0), (200.0, 300.0)] };
assert_eq!(points(&s, id), expected, "continue from the snapped end: selected={preselected}, first={first}");
assert!(!s.journal.since(journal_start).iter().any(|(command, _)| command == "path.create"));
assert!(!s.in_interaction() && !s.tool_busy());
assert_eq!(s.doc().unwrap().history.undo.len(), before_continue + usize::from(first));
let continued = node(&s, id).clone();
if !first {
assert_eq!(continued, original, "continuing from the last end does not edit the path");
}
let before_append = s.doc().unwrap().history.undo.len();
let next = if first { (0.0, 300.0) } else { (300.0, 300.0) };
gesture(&mut s, v, &[next], Mods::default());
assert_eq!(path_ids(&s), vec![id]);
assert_eq!(s.doc().unwrap().selection.objects, vec![id]);
assert_eq!(points(&s, id), [expected[0], expected[1], next]);
assert!(!s.journal.since(journal_start).iter().any(|(command, _)| command == "path.create"));
assert!(!s.in_interaction() && !s.tool_busy());
assert_eq!(s.doc().unwrap().history.undo.len(), before_append + 1);
let extended = node(&s, id).clone();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(path_ids(&s), vec![id]);
assert_eq!(node(&s, id), &continued);
assert_eq!(s.doc().unwrap().history.undo.len(), before_append);
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(path_ids(&s), vec![id]);
assert_eq!(node(&s, id), &extended);
assert_eq!(s.doc().unwrap().history.undo.len(), before_append + 1);
snapped_only_case(preselected, first);
}
}
fn snapped_only_case(preselected: bool, first: bool) {
let mut s = session();
let mut prefs = s.prefs.clone();
prefs.gridline_every = 100.0;
prefs.grid_subdivisions = 1;
s.apply_prefs(prefs);
assert_eq!(s.doc().unwrap().doc.grid.spacing, 100.0);
assert_eq!(s.doc().unwrap().doc.grid.subdivisions, 1);
let id = line(&mut s, (100.0, 300.0), (200.0, 300.0));
let original = node(&s, id).clone();
s.execute("select.set", &json!({"ids": if preselected { vec![id.0] } else { vec![] }})).unwrap();
let v = ViewInfo { snap_to_grid: true, ..view() };
s.select_tool("pen", v).unwrap();
let at = if first { (63.0, 303.0) } else { (237.0, 303.0) };
let end = if first { Point::new(100.0, 300.0) } else { Point::new(200.0, 300.0) };
assert_eq!(s.cursor(end, Mods::default(), v), Cursor::PenContinue, "the endpoint is eligible");
assert_eq!(s.cursor(Point::new(at.0, at.1), Mods::default(), v), Cursor::Pen, "the raw pointer misses both ends");
let journal_start = s.journal.len();
let before_continue = s.doc().unwrap().history.undo.len();
gesture(&mut s, v, &[at], Mods::default());
assert_eq!(path_ids(&s), vec![id], "snapped-only continuation must keep one original path: selected={preselected}, first={first}");
assert_eq!(s.doc().unwrap().selection.objects, vec![id]);
let expected = if first { [(200.0, 300.0), (100.0, 300.0)] } else { [(100.0, 300.0), (200.0, 300.0)] };
assert_eq!(points(&s, id), expected, "continue from the snapped end: selected={preselected}, first={first}");
assert!(!s.journal.since(journal_start).iter().any(|(command, _)| command == "path.create"));
assert!(!s.in_interaction() && !s.tool_busy());
assert_eq!(s.doc().unwrap().history.undo.len(), before_continue + usize::from(first));
let continued = node(&s, id).clone();
if !first {
assert_eq!(continued, original, "continuing from the last end does not edit the path");
}
let before_append = s.doc().unwrap().history.undo.len();
let next = if first { (0.0, 300.0) } else { (300.0, 300.0) };
gesture(&mut s, v, &[next], Mods::default());
assert_eq!(path_ids(&s), vec![id]);
assert_eq!(s.doc().unwrap().selection.objects, vec![id]);
assert_eq!(points(&s, id), [expected[0], expected[1], next]);
assert!(!s.journal.since(journal_start).iter().any(|(command, _)| command == "path.create"));
assert!(!s.in_interaction() && !s.tool_busy());
assert_eq!(s.doc().unwrap().history.undo.len(), before_append + 1);
let extended = node(&s, id).clone();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(path_ids(&s), vec![id]);
assert_eq!(node(&s, id), &continued);
assert_eq!(s.doc().unwrap().history.undo.len(), before_append);
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(path_ids(&s), vec![id]);
assert_eq!(node(&s, id), &extended);
assert_eq!(s.doc().unwrap().history.undo.len(), before_append + 1);
}
#[test]
fn selected_snapped_only_endpoint_continues_when_raw_pointer_misses() {
snapped_only_continue(true);