mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 11:16:25 +00:00
A top-object envelope gives each piece of a side a share of the content in proportion to its control polygon's length. When anchors split a curve without changing its shape, the pieces' polygons don't divide the side where the curve's own parameter does, so the envelope bent its contents differently although its outline was the same: Add Anchor Points on a top object moved the art, and a top object whose .ai mesh saves each side's curve split into thirds drew its type up to about half a point off its drawn art, which follows the whole curve's parameter. Coons::from_top_object now checks whether a side's pieces, from corner to corner, are all pieces of one cubic. The pieces' spans of that curve's parameter come from the handle lengths either side of each split, the curve from the outer ends and handles, and every piece must lie within 0.02 pt of the curve's piece over its span (each control point). Such a side runs along that curve, by its parameter. Any other side keeps the polygon shares, including a side only partly made of one curve and a smooth side drawn in many pieces that isn't one curve as a whole. Corners found from the outline can still move when anchors are added, as before. Gradient meshes made from a path are unchanged. Existing top-object envelopes with such sides redraw. a_split_curve_moves_the_top_object_share_to_its_new_anchor, which pinned the earlier behaviour, becomes a_split_curve_keeps_the_top_object_patch. Tests: a_split_curve_keeps_the_top_object_patch, a_top_object_joins_a_split_curve_saved_with_rounded_coordinates, a_top_object_keeps_smoothly_joined_curves_apart, a_top_object_side_partly_of_one_curve_keeps_the_polygon_shares and a_top_object_side_of_many_arcs_stays_on_them (vectorcraft-doc), envelope_top_object_keeps_its_look_when_anchors_are_added (vectorcraft-engine) and a_top_object_whose_top_is_one_curve_split_in_thirds_opens_live (vectorcraft-eps). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
317 lines
19 KiB
Rust
317 lines
19 KiB
Rust
use super::blend::{
|
||
blend_create_spine, blend_expand, blend_info, blend_make, blend_options, blend_release, blend_replace_spine, blend_reverse_spine,
|
||
blend_reverse_stack, blend_set_easing, spine_add_anchor, spine_convert_anchor, spine_move_anchor, spine_remove_anchor, spine_reshape_segment,
|
||
};
|
||
use super::common::{has_blend, has_envelope, has_mesh};
|
||
use super::envelope::{
|
||
env_edit_contents, env_expand, env_info, env_make_mesh, env_make_top, env_make_warp, env_options, env_release, env_reset_mesh, env_reset_warp,
|
||
env_set_mesh_point,
|
||
};
|
||
use super::mesh::{mesh_add_line, mesh_create, mesh_delete_point, mesh_expand, mesh_move_point, mesh_move_points, mesh_set_color};
|
||
use super::{CommandSpec, always, cmd, has_doc, has_multi, has_selection};
|
||
|
||
pub fn specs() -> Vec<CommandSpec> {
|
||
vec![
|
||
// ---------- Blend ----------
|
||
cmd!(
|
||
"object.blend.make",
|
||
"Make",
|
||
["Object", "Blend"],
|
||
Some("Cmd+Alt+B"),
|
||
"{ids?, steps?: n | distance?: pt | smooth?: bool, orientation?: page|path (default: the Blend Options set with nothing selected, else smooth colour and page), starts?: [anchor index | null, …] (per object of `ids`, else of the selection in paint order: the anchor of its first subpath the blend starts from; an open path's last anchor runs it the other way), spacing?: smooth|steps|distance (with value), value?: n, easingType?: easeIn|easeOut|easeInOut|easeOutIn, ramp?: pct (0..100), colorShift?: pct (-100..100; 0: even), createSpine?: auto|always|never (default: the mode object.blend.createSpine sets; never: a blend without a spine, which takes no easing)} blend the selected objects (paint order) into a live blend; a blend among them takes the others in as more key objects (keeping its options, name, transparency and whether it has a spine) instead of nesting → {id}",
|
||
has_doc,
|
||
blend_make
|
||
),
|
||
cmd!(
|
||
"object.blend.release",
|
||
"Release",
|
||
["Object", "Blend"],
|
||
Some("Cmd+Alt+Shift+B"),
|
||
"{} release the selected blends: the key objects come back and the spine (if the blend has one) stays as a path with no fill or stroke, below them; a blend with a name, opacity, blend mode, isolation, opacity mask or appearance of its own comes back as a group keeping them (and its knockout) → {ids (the keys), spines: [ids], groups: [ids]}",
|
||
has_blend,
|
||
blend_release
|
||
),
|
||
cmd!(
|
||
"object.blend.options",
|
||
"Blend Options…",
|
||
["Object", "Blend"],
|
||
None,
|
||
"{spacing?: smooth|steps|distance, value?: n, steps?: n, distance?: pt, orientation?: page|path, smooth?: bool, easingType?: easeIn|easeOut|easeInOut|easeOutIn, ramp?: pct, colorShift?: pct} set the options of the selected blends; easingType chooses the distribution, ramp sets its strength (0..100; 0 = even), colorShift (-100..100; 0 = even) shifts colours independently of shape/position easing (omitted ones keep their value; out of range is an error); with no blend selected, the options new blends start with (spacing and orientation only; a tool setting, not an undo step) → {defaults?: true}",
|
||
always,
|
||
blend_options
|
||
),
|
||
cmd!(
|
||
"object.blend.createSpine",
|
||
"Create Spine",
|
||
[],
|
||
None,
|
||
"{mode?: auto|always|never} the Blend panel's Create Spine: whether new blends get a spine (auto and always: the straight lines between the key centers, as before; never: no spine to show, edit, reverse or release, and no easing, until Replace Spine gives one); with no `mode`, reads it; blends already made keep theirs (a tool setting kept with the preferences, not an undo step) → {mode}",
|
||
always,
|
||
blend_create_spine
|
||
),
|
||
cmd!(
|
||
"object.blend.setEasing",
|
||
"Blend Easing",
|
||
[],
|
||
None,
|
||
"{id? (default: the first selected blend), easingType?: easeIn|easeOut|easeInOut|easeOutIn, ramp?: pct (0..100), colorShift?: pct (-100..100)} set one blend's easing and color shift, as the blend widgets on the canvas do; omitted ones keep their value. easingType and ramp need a blend whose steps follow a spine (a replaced or edited spine, or Align to Path); a blend made without a spine takes them once Replace Spine has given it one; out of range, a hidden or locked blend and a blend with no spine are errors. A value that changes nothing records no undo step → {id, easingType, ramp, colorShift, changed}",
|
||
has_doc,
|
||
blend_set_easing
|
||
),
|
||
cmd!(
|
||
"object.blend.expand",
|
||
"Expand",
|
||
["Object", "Blend"],
|
||
None,
|
||
"{} replace the selected blends by groups of their keys and steps, each keeping the blend's id, name, transparency (knockout and isolation included), opacity mask and appearance → {ids}",
|
||
has_blend,
|
||
blend_expand
|
||
),
|
||
cmd!(
|
||
"object.blend.replaceSpine",
|
||
"Replace Spine",
|
||
["Object", "Blend"],
|
||
None,
|
||
"{} use the selected path as the spine of the selected blend (the path is consumed); a blend made without a spine gets this one",
|
||
has_blend,
|
||
blend_replace_spine
|
||
),
|
||
cmd!(
|
||
"object.blend.reverseSpine",
|
||
"Reverse Spine",
|
||
["Object", "Blend"],
|
||
None,
|
||
"{} reverse the order of the key objects along the spine; an error when a selected blend has no spine",
|
||
has_blend,
|
||
blend_reverse_spine
|
||
),
|
||
cmd!(
|
||
query "object.blend.info",
|
||
"Blend Info",
|
||
[],
|
||
None,
|
||
"{} the options of the first selected blend, else those new blends start with → {target: blend|defaults, id?, spacing: smooth|steps|distance, steps, distance, orientation: page|path, easingType?: easeIn|easeOut|easeInOut|easeOutIn, ramp?: pct, colorShift?: pct, keys?: [ids], starts?: [anchor|null], hasSpine?: false for a blend made without a spine (no `spine` then), spine?: {anchors: [{x, y, in: [x, y], out: [x, y]}], closed, keyAnchors: [anchor each key sits on] | null (keys spread evenly: a spine from Replace Spine), explicit: false for the straight lines between the key centres}}",
|
||
always,
|
||
blend_info
|
||
),
|
||
cmd!(
|
||
"object.blend.spine.moveAnchor",
|
||
"Move Spine Point",
|
||
[],
|
||
None,
|
||
"{id? (default: the selected blend), anchor: index, x, y, with?: [index…], handle?: in|out, independent?: bool} move a point of the blend's spine (its handles along), and the points `with` names by as much, or with `handle` place that handle's end at (x, y) (a smooth point keeps the other handle in line unless `independent`). A key object sitting on a moved point moves with it. The first edit turns the straight spine into a path",
|
||
has_doc,
|
||
spine_move_anchor
|
||
),
|
||
cmd!(
|
||
"object.blend.spine.convertAnchor",
|
||
"Convert Anchor Point",
|
||
[],
|
||
None,
|
||
"{id? (default: the selected blend), anchor: index, to: smooth|corner, x?, y?} convert a blend spine point: dragging a corner creates symmetric smooth handles, clicking a point with handles retracts both. Without x/y, smooth uses the neighbouring spine points",
|
||
has_doc,
|
||
spine_convert_anchor
|
||
),
|
||
cmd!(
|
||
"object.blend.spine.reshapeSegment",
|
||
"Reshape Spine Segment",
|
||
[],
|
||
None,
|
||
"{id? (default: the selected blend), segment: index, t: 0..1, dx, dy, perpendicular?: bool} bend a segment of the blend's spine so its point at t moves by (dx, dy), as path.reshapeSegment bends a path's (perpendicular too): a straight spine turns into a curve (#1111)",
|
||
has_doc,
|
||
spine_reshape_segment
|
||
),
|
||
cmd!(
|
||
"object.blend.spine.addAnchor",
|
||
"Add Spine Point",
|
||
[],
|
||
None,
|
||
"{id? (default: the selected blend), x, y} add a point to the blend's spine where it passes nearest (x, y) → {anchor}",
|
||
has_doc,
|
||
spine_add_anchor
|
||
),
|
||
cmd!(
|
||
"object.blend.spine.removeAnchor",
|
||
"Delete Spine Point",
|
||
[],
|
||
None,
|
||
"{id? (default: the selected blend), anchor: index} delete a point of the blend's spine that no key object sits on",
|
||
has_doc,
|
||
spine_remove_anchor
|
||
),
|
||
cmd!(
|
||
"object.blend.reverseFrontToBack",
|
||
"Reverse Front to Back",
|
||
["Object", "Blend"],
|
||
None,
|
||
"{} reverse the stacking order of the selected blends",
|
||
has_blend,
|
||
blend_reverse_stack
|
||
),
|
||
// ---------- Envelope ----------
|
||
cmd!(
|
||
"object.envelope.makeWithWarp",
|
||
"Make with Warp…",
|
||
["Object", "Envelope Distort"],
|
||
Some("Cmd+Alt+Shift+W"),
|
||
"{ids?, style?: arc|arcLower|arcUpper|arch|bulge|shellLower|shellUpper|flag|wave|fish|rise|fisheye|inflate|squeeze|twist (arc), bend?: % -100..100 (50), h?: horizontal distortion % -100..100 (0), v?: vertical distortion % (0), horizontal?: bool (true) | orientation?: horizontal|vertical, horizontalDistortion?: as h, verticalDistortion?: as v, version?: 0|1 (1: each style bends the bounds as one Bézier patch; 0: VectorCraft's earlier map)} envelope the selected objects with a warp (Envelope Options from object.envelope.options with nothing selected; type maps from its glyph outline bounds) → {id}",
|
||
has_selection,
|
||
env_make_warp
|
||
),
|
||
cmd!(
|
||
"object.envelope.makeWithMesh",
|
||
"Make with Mesh…",
|
||
["Object", "Envelope Distort"],
|
||
Some("Cmd+Alt+M"),
|
||
"{ids?, rows?: 1..50 (4), cols?: 1..50 (4), columns?: as cols} envelope the selected objects with a mesh of rows × cols patches over their bounds (type measured by its glyph outlines) → {id}",
|
||
has_selection,
|
||
env_make_mesh
|
||
),
|
||
cmd!(
|
||
"object.envelope.makeWithTopObject",
|
||
"Make with Top Object",
|
||
["Object", "Envelope Distort"],
|
||
Some("Cmd+Alt+C"),
|
||
"{ids?} the topmost selected path becomes the envelope of the others: its outline splits into four sides at its corners, and each piece of a side takes a share of the content in proportion to its control polygon's length, and a side whose pieces are all one curve split at anchors follows that curve's parameter (type maps from its glyph outline bounds) → {id}",
|
||
has_multi,
|
||
env_make_top
|
||
),
|
||
cmd!(
|
||
"object.envelope.resetWithWarp",
|
||
"Reset with Warp…",
|
||
["Object", "Envelope Distort"],
|
||
None,
|
||
"{style?, bend?, h?, v?, horizontal? | orientation? (as makeWithWarp; unset: the envelope's current warp, else arc 50 %), horizontalDistortion?: as h, verticalDistortion?: as v, version?: as makeWithWarp (unset: a warp envelope's own, else 1)} make the selected envelopes warp envelopes, keeping their content",
|
||
has_envelope,
|
||
env_reset_warp
|
||
),
|
||
cmd!(
|
||
"object.envelope.resetWithMesh",
|
||
"Reset with Mesh…",
|
||
["Object", "Envelope Distort"],
|
||
None,
|
||
"{rows?: 1..50, cols?: 1..50 (unset: a mesh envelope's own, else 4), maintainShape?: bool (true: the mesh follows the current envelope shape; false: a flat grid over the content, type measured by its glyph outlines), columns?: as cols} make the selected envelopes mesh envelopes, keeping their content",
|
||
has_envelope,
|
||
env_reset_mesh
|
||
),
|
||
cmd!(
|
||
query "object.envelope.info",
|
||
"Envelope Info",
|
||
[],
|
||
None,
|
||
"{} the selected envelope's settings, or with none selected the defaults new envelopes get → {id: n|null, type: warp|mesh|topObject, style, bend, h, v, horizontal, version (a warp's map: 1, each style as one Bézier patch; 0, the earlier map), rows, cols, fidelity, antiAlias, preserveShape: clippingMask|transparency, distortAppearance, distortLinearGradients, distortPatternFills, boundsMargin?: [top, bottom, left, right] (shares of the content's height and width, reported only when not zero: with distortAppearance, how far the box the content maps from reaches past its geometric bounds), editing}",
|
||
has_doc,
|
||
env_info
|
||
),
|
||
cmd!(
|
||
"object.envelope.release",
|
||
"Release",
|
||
["Object", "Envelope Distort"],
|
||
None,
|
||
"{} release the selected envelopes into their content and their shape (a grey gradient mesh for warp and mesh envelopes, the path for a top-object envelope; the content keeps the envelope's opacity, blend mode and opacity mask) → {ids}",
|
||
has_envelope,
|
||
env_release
|
||
),
|
||
cmd!(
|
||
"object.envelope.options",
|
||
"Envelope Options…",
|
||
["Object", "Envelope Distort"],
|
||
None,
|
||
"{fidelity?: 0..100, antiAlias?: bool, preserveShape?: clippingMask|transparency, distortAppearance?: bool, distortLinearGradients?: bool, distortPatternFills?: bool (the last two work with distortAppearance; on an envelope with boundsMargin, distortAppearance also switches the content between the box with the margins and its geometric box), style?, bend?, h?, v?, horizontal? (warp envelopes), horizontalDistortion?: as h, verticalDistortion?: as v, orientation?: horizontal|vertical, version?: 0|1 (warp envelopes' map, as makeWithWarp)} set the selected envelopes' options; with no envelope selected, the options new envelopes get (see object.envelope.info)",
|
||
has_doc,
|
||
env_options
|
||
),
|
||
cmd!(
|
||
"object.envelope.expand",
|
||
"Expand",
|
||
["Object", "Envelope Distort"],
|
||
None,
|
||
"{} replace the selected envelopes by groups of their distorted content (type outlined), keeping name, opacity, blend mode, isolation, knockout and opacity mask → {ids}",
|
||
has_envelope,
|
||
env_expand
|
||
),
|
||
cmd!(
|
||
"object.envelope.editContents",
|
||
"Edit Contents",
|
||
["Object", "Envelope Distort"],
|
||
None,
|
||
"{editing?: bool} toggle Edit Contents / Edit Envelope; leaving Edit Contents measures an envelope's boundsMargin again when the content's strokes reach differently than when Edit Contents began → {editing}",
|
||
has_envelope,
|
||
env_edit_contents
|
||
),
|
||
cmd!(
|
||
"object.envelope.setMeshPoint",
|
||
"Move Envelope Mesh Point",
|
||
[],
|
||
None,
|
||
"{id, index, x, y, handle?: 0 right|1 left|2 down|3 up} move a point of a mesh or warp envelope (its handles follow), or with `handle` place that handle end at (x, y); a warp's points are those of its grid (4×4 patches, or 8×8 or 16×16 where 4×4 draws more than 0.1% of its diagonal away from the warp), and its first edit makes it a mesh envelope on that grid in the same undo step (a warp no such grid follows is refused: Reset with Mesh first); object.mesh.movePoint, movePoints, addLine and deletePoint edit mesh and warp envelopes too",
|
||
has_doc,
|
||
env_set_mesh_point
|
||
),
|
||
// ---------- Gradient mesh ----------
|
||
cmd!(
|
||
"object.mesh.create",
|
||
"Create Gradient Mesh…",
|
||
["Object"],
|
||
None,
|
||
"{ids?, rows?: n (4), cols?: n (4), appearance?: flat|center|edge, highlight?: % (100), at?: [x,y], tolerance?: pt (0), columns?: as cols} convert filled paths into gradient meshes in their fill colour (a gradient fill: the colour it paints at each point), lightened per `appearance` (with `at`: 1×1 mesh plus lines through that point, or through the nearest point of the edge when `at` is outside within `tolerance`); an embedded image becomes a mesh over its rectangle (its transform kept) whose points take the average colour and opacity of the picture around them (a linked image is refused: embed it first) → {ids, index?}",
|
||
has_selection,
|
||
mesh_create
|
||
),
|
||
cmd!(
|
||
"object.mesh.setPointColor",
|
||
"Set Mesh Point Color",
|
||
[],
|
||
None,
|
||
"{id, index | points: [index…] | items: [{id, index | points}…], color? (#rrggbbaa and #rgba are refused), keepModel?: bool (as paint.setFill), opacity?: 0..1} colour and/or opacity of gradient mesh points (one undo step; an index out of range changes nothing, and so does a call that changes no point, with no undo step)",
|
||
has_doc,
|
||
mesh_set_color
|
||
),
|
||
cmd!(
|
||
"object.mesh.movePoint",
|
||
"Move Mesh Point",
|
||
[],
|
||
None,
|
||
"{id, index, x, y, handle?: 0 right|1 left|2 down|3 up} move a point of a gradient mesh or a mesh or warp envelope (its handles follow; a warp becomes a mesh envelope, see object.envelope.setMeshPoint), or with `handle` place that handle end at (x, y)",
|
||
has_doc,
|
||
mesh_move_point
|
||
),
|
||
cmd!(
|
||
"object.mesh.movePoints",
|
||
"Move Mesh Points",
|
||
[],
|
||
None,
|
||
"{id, points: [index…] | items: [{id, index | points}…], dx, dy} move points of gradient meshes or mesh or warp envelopes by (dx, dy), their handles with them (warps become mesh envelopes, see object.envelope.setMeshPoint; one undo step; an index out of range changes nothing, and so does a call that moves no point, with no undo step)",
|
||
has_doc,
|
||
mesh_move_points
|
||
),
|
||
cmd!(
|
||
"object.mesh.addLine",
|
||
"Add Mesh Line",
|
||
[],
|
||
None,
|
||
"{id, x, y, color? (#rrggbbaa and #rgba are refused), keepModel?: bool (as paint.setFill), tolerance?: pt (0)} add a row and a column through (x, y), inside or on the edge, to a gradient mesh (new point in `color`) or a mesh or warp envelope (a warp becomes a mesh envelope, see object.envelope.setMeshPoint); a point outside within `tolerance` of the edge adds them through the nearest point of the edge; at a point that is already a mesh point it adds no line, and records no undo step unless `color` changes a gradient mesh's point (an envelope's points have no color) → {index}",
|
||
has_doc,
|
||
mesh_add_line
|
||
),
|
||
cmd!(
|
||
"object.mesh.deletePoint",
|
||
"Delete Mesh Point",
|
||
[],
|
||
None,
|
||
"{id, index} delete the mesh lines through a point of a gradient mesh or a mesh or warp envelope (a warp becomes a mesh envelope, see object.envelope.setMeshPoint)",
|
||
has_doc,
|
||
mesh_delete_point
|
||
),
|
||
cmd!(
|
||
"object.mesh.expand",
|
||
"Expand Gradient Mesh",
|
||
["Object"],
|
||
None,
|
||
"{} replace the selected meshes by groups of flat-coloured pieces → {ids}",
|
||
has_mesh,
|
||
mesh_expand
|
||
),
|
||
]
|
||
}
|