mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
Type is drawn as its glyph outlines under a geometry effect, in EMF and WMF export, in DXF export when it is written as outlines, through Flatten Transparency with Convert All Text to Outlines, and inside an envelope. Those outlines differed from the type the canvas draws in four ways. They had no underline or strikethrough bars. A character stroke on scaled type kept its unscaled weight. An unplaced gradient was fitted to the glyphs it painted instead of the layout box. A run's pattern tiles stayed in document space instead of scaling and turning with the type. A group's or layer's own fills and strokes also painted the glyphs of the type in it without the bars. The outlines now include each run's underline and strikethrough bars, as the canvas has drawn them since #850. Each run's path is made in text space with its unplaced gradients fixed to the layout box, then placed by the type's transform, which also scales its stroke weight and dashes and transforms its pattern tiles. The type's own fills and strokes have their unplaced gradients fixed to the bounding box of the layout box in the document, where the canvas fits them. Geometry effects that move or scale the outlines move and scale the type's gradients with them: the type's own effects, also inside an envelope and after Flatten Transparency has converted the type to outlines, and the effects of a group, symbol instance or envelope that holds the type. outline_text marks its group with a new node field, type_outlines, which is saved with the document. reshape_leaves maps the placed gradients in each marked group through the map that takes the group's bounds before the effects onto its bounds after them. A scale out of proportion stretches a radial gradient into an ellipse and turns the isolines of a diagonal linear gradient, as it does on type scaled by its own transform. Placed gradients outside those groups stay where they are.
447 lines
19 KiB
Rust
447 lines
19 KiB
Rust
//! Object-level effects on objects that aren't paths: type, placed images, symbol instances and
|
|
//! live objects (blends, envelopes, meshes, repeats).
|
|
//!
|
|
//! Geometry effects reach them through outlines: [`outline_art`] turns the object into plain paths
|
|
//! (glyph outlines, the evaluated live object, the symbol's art) and [`reshape`] runs each effect
|
|
//! over all of them as one piece, with the whole art's bounds as the effect's reference box, so a
|
|
//! warp bends a line of type as a whole. An image is clipped by its reshaped frame. Raster effects
|
|
//! (shadows, glows, blur, feather) stay on the result for the renderer and exporters.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use vectorcraft_doc::{Appearance, AppearanceItem, Effect, Node, NodeKind};
|
|
use vectorcraft_geom::{Affine, FillRule, PathData, Rect, shapes};
|
|
|
|
use crate::{GeomContext, apply_one, is_geometry, merged_params};
|
|
|
|
/// Can object-level effects apply to `n` only through [`outline_art`] / an offscreen layer (it is
|
|
/// neither a path nor a container)?
|
|
pub fn needs_outline(n: &Node) -> bool {
|
|
matches!(
|
|
n.kind,
|
|
NodeKind::Text(_)
|
|
| NodeKind::Image(_)
|
|
| NodeKind::SymbolInstance { .. }
|
|
| NodeKind::Blend { .. }
|
|
| NodeKind::Envelope { .. }
|
|
| NodeKind::Mesh(_)
|
|
| NodeKind::Repeat(_)
|
|
| NodeKind::PlacedDocument(_)
|
|
)
|
|
}
|
|
|
|
/// The glyph outlines of type `t` in text space with its underline and strikethrough bars: one
|
|
/// path per run, and all of them as one.
|
|
pub(crate) fn glyph_outlines(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath) {
|
|
let (runs, all, _) = laid_out(t);
|
|
(runs, all)
|
|
}
|
|
|
|
/// [`glyph_outlines`] and the bounds of `t`'s layout in text space.
|
|
fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath, Rect) {
|
|
let db = vectorcraft_text::FontDb::global();
|
|
let layout = vectorcraft_text::layout(db, t);
|
|
let mut runs = vec![kurbo::BezPath::new(); t.runs.len()];
|
|
let mut all = kurbo::BezPath::new();
|
|
for g in &layout.glyphs {
|
|
if let Some(r) = runs.get_mut(g.run) {
|
|
r.extend(g.outline.iter());
|
|
}
|
|
all.extend(g.outline.iter());
|
|
}
|
|
// Underline and strikethrough bars, painted as their run's type is (#847).
|
|
for (run, bar) in vectorcraft_text::decorations(&layout, db, t) {
|
|
if let Some(r) = runs.get_mut(run) {
|
|
r.extend(bar.iter());
|
|
}
|
|
all.extend(bar.iter());
|
|
}
|
|
(runs, all, layout.bounds)
|
|
}
|
|
|
|
/// Type as glyph outlines in document space: one path per run with the run's fill and stroke,
|
|
/// and (when the object has its own fills/strokes) the whole outline with the object's items below
|
|
/// the characters under them and the others over them, as the renderer paints them. Each run's
|
|
/// path is placed by the type's transform, which also scales its stroke weights and dashes and
|
|
/// transforms its pattern tiles; its unplaced gradients are first fixed to the layout's bounds in
|
|
/// text space. The object's own unplaced gradients are fixed to the bounding box of those bounds
|
|
/// in the document. The group is marked as type's outlines
|
|
/// ([`vectorcraft_doc::NodeExtras::type_outlines`]).
|
|
pub fn outline_text(n: &Node) -> Option<Node> {
|
|
let NodeKind::Text(t) = &n.kind else { return None };
|
|
let (runs, all, bounds) = laid_out(t);
|
|
let path = |bp: &kurbo::BezPath| PathData::from_bezpath(bp).transformed(t.xf);
|
|
let scaling = vectorcraft_doc::Scaling { strokes: true, patterns: true, ..Default::default() };
|
|
let mut children: Vec<Arc<Node>> = runs
|
|
.iter()
|
|
.zip(&t.runs)
|
|
.filter(|(bp, _)| !bp.elements().is_empty())
|
|
.map(|(bp, run)| {
|
|
let mut ap = run.style.appearance();
|
|
ap.pin_gradients(bounds);
|
|
let mut piece = Node::path(n.id, PathData::from_bezpath(bp), ap);
|
|
piece.transform(t.xf, scaling);
|
|
Arc::new(piece)
|
|
})
|
|
.collect();
|
|
if !n.appearance.items.is_empty() && !all.elements().is_empty() {
|
|
let (below, above) = n.appearance.split_contents();
|
|
let tb = t.xf.transform_rect_bbox(bounds);
|
|
let glyphs = |items: &[AppearanceItem]| {
|
|
let mut ap = Appearance { items: items.to_vec(), ..Default::default() };
|
|
ap.pin_gradients(tb);
|
|
Arc::new(Node::path(n.id, path(&all), ap))
|
|
};
|
|
if !below.is_empty() {
|
|
children.insert(0, glyphs(below));
|
|
}
|
|
if !above.is_empty() {
|
|
children.push(glyphs(above));
|
|
}
|
|
}
|
|
let mut g = Node::group(n.id, children);
|
|
g.extras_mut().type_outlines = true;
|
|
Some(g)
|
|
}
|
|
|
|
/// `n` as plain art that geometry effects can reshape, without its transparency and object
|
|
/// effects: type outlined ([`outline_text`]), live objects evaluated (type inside them outlined),
|
|
/// a symbol instance as `symbol_art` (the symbol's art, stained as the caller draws it) placed by
|
|
/// the instance's transform. `None` for other kinds (images, paths, containers).
|
|
pub fn outline_art(n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
|
|
let hook: &dyn Fn(&Node) -> Option<Node> = &outline_text;
|
|
let mut art = match &n.kind {
|
|
NodeKind::Text(_) => outline_text(n)?,
|
|
NodeKind::SymbolInstance { xf, .. } => {
|
|
let mut a = symbol_art?.clone();
|
|
a.transform(*xf, false);
|
|
Node::group(n.id, vec![Arc::new(a)])
|
|
}
|
|
_ if vectorcraft_doc::live::is_live(n) => vectorcraft_doc::live::expand_deep(n, Some(hook)),
|
|
_ => return None,
|
|
};
|
|
outline_nested_text(&mut art, false);
|
|
art.opacity = 1.0;
|
|
art.blend = Default::default();
|
|
art.isolate = false;
|
|
// One element: its pieces never knock each other out.
|
|
art.knockout = vectorcraft_doc::Knockout::Off;
|
|
art.extras_mut().mask = None;
|
|
art.appearance.effects.clear();
|
|
Some(art)
|
|
}
|
|
|
|
/// Replace type anywhere inside `n` by its outlines, as art that keeps the type's look
|
|
/// ([`as_art`]). Type in a clip group's clipping path (all of `n` when `clipping` is set) becomes
|
|
/// its plain outlines ([`outline_text`]) with its visibility, which is what
|
|
/// [`crate::clip_outline`] clips by.
|
|
fn outline_nested_text(n: &mut Node, clipping: bool) {
|
|
let clips = n.clips();
|
|
let Some(ch) = n.children_mut() else { return };
|
|
for (i, c) in ch.iter_mut().enumerate() {
|
|
let clipping = clipping || (clips && i == 0);
|
|
let art = match &c.kind {
|
|
NodeKind::Text(_) if clipping => outline_text(c).map(|o| Node { visible: c.visible, ..o }),
|
|
NodeKind::Text(_) => as_art(c, None),
|
|
_ => None,
|
|
};
|
|
if let Some(o) = art {
|
|
*c = Arc::new(o);
|
|
} else if c.children().is_some() {
|
|
outline_nested_text(Arc::make_mut(c), clipping);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `n` (see [`needs_outline`]) with its object-level geometry effects and its perspective applied:
|
|
/// its [`outline_art`] reshaped as one piece, or for an image the image clipped by its reshaped
|
|
/// frame. The result is a group with `n`'s id, name, visibility, transparency, opacity mask and
|
|
/// remaining (raster) effects; `None` when `n` has neither a visible geometry effect nor a
|
|
/// perspective, or has no art (a symbol instance without `symbol_art`).
|
|
pub fn reshape(n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
|
|
if (geometry_effects(n).is_empty() && n.projection().is_none()) || !needs_outline(n) {
|
|
return None;
|
|
}
|
|
as_art(n, symbol_art)
|
|
}
|
|
|
|
/// `n` (see [`needs_outline`]) as plain art for Expand Appearance: [`reshape`]d when it has
|
|
/// geometry effects, else type with fills or strokes of its own outlined, in the same form. `None`
|
|
/// when it has neither.
|
|
pub fn expand_outlined(n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
|
|
let own_paint = matches!(n.kind, NodeKind::Text(_)) && !n.appearance.items.is_empty();
|
|
if !needs_outline(n) || (geometry_effects(n).is_empty() && !own_paint) {
|
|
return None;
|
|
}
|
|
as_art(n, symbol_art)
|
|
}
|
|
|
|
/// [`reshape`] without its checks (no geometry effects: just the art).
|
|
pub(crate) fn as_art(n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
|
|
let fx = geometry_effects(n);
|
|
let mut art = match &n.kind {
|
|
NodeKind::Image(im) => {
|
|
let frame = shapes::rectangle(Rect::new(0.0, 0.0, im.width as f64, im.height as f64)).transformed(im.xf);
|
|
let mut clip = Node::path(n.id, frame, Appearance::default());
|
|
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
|
|
*clipping = true;
|
|
}
|
|
let image = Node::new(n.id, n.kind.clone());
|
|
Node::new(n.id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(image)], clip: true })
|
|
}
|
|
_ => outline_art(n, symbol_art)?,
|
|
};
|
|
reshape_leaves(&mut art, &fx);
|
|
// Type and symbols in perspective: the art projected onto their plane.
|
|
if let Some(h) = n.projection() {
|
|
project_leaves(&mut art, &h);
|
|
}
|
|
// The art is a group with `n`'s id (a clip group for an image).
|
|
let mut out = art;
|
|
out.name = n.name.clone();
|
|
out.visible = n.visible;
|
|
out.opacity = n.opacity;
|
|
out.blend = n.blend;
|
|
out.isolate = n.isolate;
|
|
// One element: its pieces never knock each other out.
|
|
out.knockout = vectorcraft_doc::Knockout::Off;
|
|
out.knockout_shape = n.knockout_shape;
|
|
out.extras_mut().mask = n.extras().mask.clone();
|
|
out.appearance.effects = n.appearance.effects.iter().filter(|e| !is_geometry(&e.id)).cloned().collect();
|
|
Some(out)
|
|
}
|
|
|
|
/// Map every path under `art` (and its gradients) through the projective map `h`, curves split
|
|
/// finely enough to follow it.
|
|
fn project_leaves(art: &mut Node, h: &vectorcraft_geom::Homography) {
|
|
let f = |p: vectorcraft_geom::Point| h.apply(p).unwrap_or(p);
|
|
let piece = art.geometric_bounds().map_or(1.0, |b| (b.width().max(b.height()) / 64.0).max(0.25));
|
|
let project = |path: &mut PathData| *path = vectorcraft_doc::live::map_nonlinear(path, piece, f);
|
|
for_each_leaf(art, &mut |leaf| {
|
|
leaf.pin_gradients();
|
|
leaf.appearance.warp_gradients(&|p| h.affine_at(p).unwrap_or(vectorcraft_geom::Affine::IDENTITY));
|
|
match &mut leaf.kind {
|
|
NodeKind::Path { path, live, .. } => {
|
|
project(path);
|
|
*live = None;
|
|
}
|
|
NodeKind::Compound { children, .. } => {
|
|
for c in children.iter_mut() {
|
|
if let NodeKind::Path { path, live, .. } = &mut Arc::make_mut(c).kind {
|
|
project(path);
|
|
*live = None;
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// The visible geometry effects of `n`'s own effect list, in order.
|
|
pub(crate) fn geometry_effects(n: &Node) -> Vec<&Effect> {
|
|
n.appearance.effects.iter().filter(|e| e.visible && is_geometry(&e.id)).collect()
|
|
}
|
|
|
|
/// Apply `fx` to every path under `art` as one piece: each effect takes the whole art's bounds as
|
|
/// its reference box and the art's seed (a random Transform moves the pieces together). Images
|
|
/// follow a Transform effect the same way ([`transform_images`]); other geometry effects leave
|
|
/// them as they are (a raster follows a non-affine map only as a mesh warp).
|
|
pub(crate) fn reshape_leaves(art: &mut Node, fx: &[&Effect]) {
|
|
let seed = art.id.0;
|
|
// The placed gradients of type's outlines in `art` (`outline_text`) move and scale with the
|
|
// pieces, from the outlines' bounds before the effects to their bounds after them.
|
|
let mut outlines = vec![];
|
|
type_outline_bounds(art, &mut outlines);
|
|
for e in fx {
|
|
let Some(bounds) = art.geometric_bounds() else { break };
|
|
let params = merged_params(&e.id, &e.params);
|
|
for_each_leaf(art, &mut |leaf| {
|
|
let Some((path, rule)) = leaf_geometry(leaf) else { return };
|
|
let out = apply_one(&e.id, ¶ms, &path, bounds, &GeomContext { seed, ..GeomContext::of(leaf) });
|
|
set_geometry(leaf, out, rule);
|
|
});
|
|
if e.id == "distort.transform" {
|
|
let (m, copies) = crate::distort::transform_map(bounds, ¶ms, seed);
|
|
transform_images(art, m, copies);
|
|
}
|
|
}
|
|
if !fx.is_empty() && outlines.iter().any(Option::is_some) {
|
|
rebase_type_outlines(art, &mut outlines.into_iter());
|
|
}
|
|
}
|
|
|
|
/// The bounds of each group of type's outlines under `n` ([`outline_text`]), `n` included, in
|
|
/// order. A group inside one of them is part of it.
|
|
fn type_outline_bounds(n: &Node, out: &mut Vec<Option<Rect>>) {
|
|
if n.extras().type_outlines {
|
|
out.push(n.geometric_bounds());
|
|
} else if let NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } = &n.kind {
|
|
for c in children {
|
|
type_outline_bounds(c, out);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Move and scale the placed gradients of each group of type's outlines under `n` from its bounds
|
|
/// in `before` ([`type_outline_bounds`] before its paths changed) to its bounds now.
|
|
fn rebase_type_outlines(n: &mut Node, before: &mut std::vec::IntoIter<Option<Rect>>) {
|
|
if !n.extras().type_outlines {
|
|
if let NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } = &mut n.kind {
|
|
for c in children.iter_mut() {
|
|
rebase_type_outlines(Arc::make_mut(c), before);
|
|
}
|
|
}
|
|
} else if let Some(Some(from)) = before.next()
|
|
&& let Some(to) = n.geometric_bounds()
|
|
&& from != to
|
|
{
|
|
for_each_leaf(n, &mut |leaf| {
|
|
if leaf.appearance.has_placed_gradient() {
|
|
follow_boxes(&mut leaf.appearance, from, to);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Map the placed gradients of `ap` from box `from` to box `to` through the map that takes one box
|
|
/// onto the other (strokes: their stroke-inflated boxes). The start and end go where
|
|
/// [`Appearance::rebase_gradients`] puts them, and a radial's ellipse and a linear's isolines are
|
|
/// mapped with them.
|
|
fn follow_boxes(ap: &mut Appearance, from: Rect, to: Rect) {
|
|
for i in &mut ap.items {
|
|
match i {
|
|
AppearanceItem::Fill(f) => {
|
|
if let vectorcraft_doc::color::Paint::Gradient(g) = &mut f.paint {
|
|
g.transform(box_map(from, to));
|
|
}
|
|
}
|
|
AppearanceItem::Stroke(s) => {
|
|
let m = box_map(s.paint_bounds(from), s.paint_bounds(to));
|
|
if let vectorcraft_doc::color::Paint::Gradient(g) = &mut s.paint {
|
|
g.transform(m);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The map that takes box `from` onto box `to` along each axis (along an axis `from` has no extent
|
|
/// on, a move from its center to `to`'s).
|
|
fn box_map(from: Rect, to: Rect) -> vectorcraft_geom::Affine {
|
|
let axis = |a0: f64, a1: f64, b0: f64, b1: f64| {
|
|
if (a1 - a0).abs() > 1e-12 {
|
|
let s = (b1 - b0) / (a1 - a0);
|
|
(s, b0 - a0 * s)
|
|
} else {
|
|
(1.0, (b0 + b1 - a0 - a1) / 2.0)
|
|
}
|
|
};
|
|
let (sx, tx) = axis(from.x0, from.x1, to.x0, to.x1);
|
|
let (sy, ty) = axis(from.y0, from.y1, to.y0, to.y1);
|
|
vectorcraft_geom::Affine::new([sx, 0.0, 0.0, sy, tx, ty])
|
|
}
|
|
|
|
/// Apply the Transform effect's map `m` to every image under `n`, as [`for_each_leaf`] does to
|
|
/// paths: the map goes into the image's placement (pixel space → document), and `copies` more
|
|
/// images follow, each mapped once more than the last. Without copies the image itself takes the
|
|
/// map, as a path does.
|
|
fn transform_images(n: &mut Node, m: Affine, copies: usize) {
|
|
transform_images_within(n, m, copies, crate::distort::MAX_COPY_IMAGES)
|
|
}
|
|
|
|
/// [`transform_images`] whose copies make at most `max` image nodes of the art, the originals
|
|
/// included: each copy adds the image once more, and a Transform after this one copies the lot
|
|
/// again, so copies stop where the result would pass `max` (#2042).
|
|
pub(crate) fn transform_images_within(n: &mut Node, m: Affine, copies: usize, max: usize) {
|
|
let mut room = max.saturating_sub(image_count(n));
|
|
copy_images(n, m, copies, &mut room);
|
|
}
|
|
|
|
/// The walk of [`transform_images`]: the map and its copies go to each image in document order,
|
|
/// each copy taking one from `room` (what the art can still grow by), until the budget is full.
|
|
fn copy_images(n: &mut Node, m: Affine, copies: usize, room: &mut usize) {
|
|
let Some(children) = n.children_mut() else { return };
|
|
for c in children.iter_mut() {
|
|
if !matches!(c.kind, NodeKind::Image(_)) {
|
|
copy_images(Arc::make_mut(c), m, copies, room);
|
|
continue;
|
|
}
|
|
let src = (**c).clone();
|
|
let placed = |xf: Affine| {
|
|
let mut one = src.clone();
|
|
if let NodeKind::Image(im) = &mut one.kind {
|
|
im.xf = xf * im.xf;
|
|
}
|
|
one
|
|
};
|
|
if copies == 0 {
|
|
*c = Arc::new(placed(m));
|
|
continue;
|
|
}
|
|
let made = copies.min(*room);
|
|
*room -= made;
|
|
// The copies become image nodes of their own, under a group with the image's id, name and
|
|
// place in the art (as the runs an outlined text gets).
|
|
let mut images: Vec<Arc<Node>> = Vec::with_capacity(made + 1);
|
|
images.push(Arc::new(placed(Affine::IDENTITY)));
|
|
let mut xf = Affine::IDENTITY;
|
|
for _ in 0..made {
|
|
xf *= m;
|
|
images.push(Arc::new(placed(xf)));
|
|
}
|
|
let mut g = Node::group(src.id, images);
|
|
g.name = src.name.clone();
|
|
*c = Arc::new(g);
|
|
}
|
|
}
|
|
|
|
/// How many image nodes `n` holds below itself.
|
|
fn image_count(n: &Node) -> usize {
|
|
n.children().into_iter().flatten().map(|c| usize::from(matches!(c.kind, NodeKind::Image(_))) + image_count(c)).sum()
|
|
}
|
|
|
|
/// `n` with type anywhere inside it outlined and live objects evaluated, so geometry effects on
|
|
/// it reach every member.
|
|
pub(crate) fn outlined_members(n: &Node) -> Node {
|
|
let hook: &dyn Fn(&Node) -> Option<Node> = &outline_text;
|
|
let mut art = vectorcraft_doc::live::expand_deep(n, Some(hook));
|
|
outline_nested_text(&mut art, false);
|
|
art
|
|
}
|
|
|
|
/// Run `f` on every path and compound path under `n` (guides skipped).
|
|
fn for_each_leaf(n: &mut Node, f: &mut dyn FnMut(&mut Node)) {
|
|
match &mut n.kind {
|
|
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => f(n),
|
|
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
|
|
for c in children.iter_mut() {
|
|
for_each_leaf(Arc::make_mut(c), f);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn leaf_geometry(n: &Node) -> Option<(PathData, FillRule)> {
|
|
match &n.kind {
|
|
NodeKind::Path { path, rule, .. } => Some((path.clone(), *rule)),
|
|
NodeKind::Compound { children, rule } => {
|
|
Some((PathData::new(children.iter().filter_map(|c| c.path_data()).flat_map(|p| p.subpaths.iter().cloned()).collect()), *rule))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Replace the geometry of path or compound `n` (a compound when `path` has several subpaths).
|
|
fn set_geometry(n: &mut Node, path: PathData, rule: FillRule) {
|
|
let clipping = matches!(n.kind, NodeKind::Path { clipping: true, .. });
|
|
n.kind = if path.subpaths.len() > 1 {
|
|
let id = n.id;
|
|
let children = path.subpaths.into_iter().map(|sp| Arc::new(Node::path(id, PathData::single(sp), Appearance::default()))).collect();
|
|
NodeKind::Compound { children, rule }
|
|
} else {
|
|
NodeKind::Path { path, rule, live: None, clipping, guide: false }
|
|
};
|
|
}
|