pub struct ItemShape { /* private fields */ }Expand description
What an item’s geometry is, in local item coordinates — the single source of truth for hit-test, marquee, collision and path queries.
The interior-plus-band model, the cost story and the local-vs-scene split are spelled out on the module this type lives in.
Implementations§
Source§impl ItemShape
impl ItemShape
Sourcepub fn bounds(rect: Rect) -> Self
pub fn bounds(rect: Rect) -> Self
The item’s local AABB. This is the trait default and it allocates nothing.
Sourcepub fn rounded_rect(rect: Rect, radius: f32) -> Self
pub fn rounded_rect(rect: Rect, radius: f32) -> Self
A rectangle with rounded corners — the corners are outside the shape, so a click in the transparent corner of a rounded card misses it, exactly as the pixels suggest. Closed form; never flattened for a point test.
Sourcepub fn ellipse(rect: Rect) -> Self
pub fn ellipse(rect: Rect) -> Self
The ellipse inscribed in rect. Closed form; never flattened for a
point test.
Sourcepub fn path(geometry: Rc<ShapeGeometry>) -> Self
pub fn path(geometry: Rc<ShapeGeometry>) -> Self
An arbitrary local path, filled under [FillRule::Winding].
Takes the shared handle so the flattening is paid once per item
lifetime; build the ShapeGeometry in your constructor.
Sourcepub fn from_path(path: Path) -> Self
pub fn from_path(path: Path) -> Self
ItemShape::path for a one-off path, allocating a fresh
ShapeGeometry on the spot.
Convenient for a test or a shape built once and thrown away. An item
that publishes its shape every layout pass should own an
Rc<ShapeGeometry> instead — this one re-flattens every time it is
asked.
Sourcepub fn none() -> Self
pub fn none() -> Self
Never hit, never selected, never collides.
The logical-only GroupItem case, and any pure
accessibility container: clicks fall straight through to whatever is
beneath.
Sourcepub fn filled(self, rule: FillRule) -> Self
pub fn filled(self, rule: FillRule) -> Self
Fill the interior under rule.
Pass [FillRule::EvenOdd] so a ring authored as two subpaths has a
real hole. A PathItem takes its rule from its own
fill_rule, which is the same rule it paints with — a hit-only
fill rule would be a second source of truth, which is precisely what
this type exists to abolish.
Sourcepub fn unfilled(self) -> Self
pub fn unfilled(self) -> Self
Drop the interior, leaving only the stroke band (if any).
A stroke-only connector: clicking the empty middle of its bounding box must miss, and clicking anywhere along the drawn line must hit. Has no effect on the closed forms, which are regions by construction.
Sourcepub fn stroked(self, width: f32, space: StrokeSpace) -> Self
pub fn stroked(self, width: f32, space: StrokeSpace) -> Self
Add a stroke band width wide, centred on the outline.
[StrokeSpace::Device] is a cosmetic stroke: its width is in
device pixels, so the local-coordinate band is divided by the live view
scale at test time and tracks the rendered line at any zoom.
[StrokeSpace::Logical] is already in local units and is unaffected by
zoom.
Sourcepub fn hit_stroke_width(self, width: f32) -> Self
pub fn hit_stroke_width(self, width: f32) -> Self
Replace the band’s width for hit purposes only, independent of what
is painted (Konva’s hitStrokeWidth): a 1 dp connector wire made 12 dp
grabbable. Always in local units — a hit band is a target size, not a
rendered thickness, so it does not shrink as you zoom in.
Sourcepub fn contains(&self, local_pt: Point, view_scale: f32) -> bool
pub fn contains(&self, local_pt: Point, view_scale: f32) -> bool
Whether local_pt is inside the shape.
view_scale is the live view zoom — the uniform scale of the view
transform’s linear part. It is consulted only by a
[StrokeSpace::Device] band; pass 1.0 for an item pinned in screen
space (see ItemFlags::IGNORES_TRANSFORMATIONS),
whose local coordinates are screen coordinates and which therefore
has no zoom to convert.
Sourcepub fn bounding_rect(&self) -> Rect
pub fn bounding_rect(&self) -> Rect
The shape’s own local AABB at unit view scale, inflated by any stroke band at its logical width.
This is the shape’s extent, not the rectangle the spatial index
buckets on — that stays the entry’s local_bounds, composed into a
scene AABB. Every geometry-bearing built-in derives its local_bounds
from exactly this value, and SceneItem::shape
asks an app item for the same, so at unit scale the box encloses the
shape and a broad phase on the box can never reject a point the narrow
phase would have accepted.
At other zooms that holds only for a [StrokeSpace::Logical] band
and the closed forms. A [StrokeSpace::Device] (cosmetic) band is
specified in device pixels, so its width in local coordinates is
w / view_scale and it is wider than this rectangle at any zoom
below 1×. The narrow phase is right there — the band tracks what is
painted — but the broad phase bucketed on the unit-scale box, so a
point on a cosmetic stroke zoomed far out can be rejected before the
narrow phase sees it. Use
bounding_rect_at to measure the real
extent at a zoom. This is not closed by clamping: the divergence grows
without bound as the zoom approaches zero, so the only rectangle that
would always cover it is an infinite one, and paying for the largest
band any item could ever need would bloat every cosmetic-stroke item’s
index bucket and its advertised accessibility box at every zoom. The
mitigation is that zooming out that far turns a hairline into a hair:
at 0.25× a 40 px cosmetic stroke covers 160 local units, which is a
blot on screen rather than a line anyone is aiming at.
Sourcepub fn bounding_rect_at(&self, view_scale: f32) -> Rect
pub fn bounding_rect_at(&self, view_scale: f32) -> Rect
ItemShape::bounding_rect at an explicit view zoom — the rectangle
that really does enclose everything ItemShape::contains accepts at
that zoom.
Differs from bounding_rect() only for a [StrokeSpace::Device] band,
and only away from 1×.
Sourcepub fn is_none(&self) -> bool
pub fn is_none(&self) -> bool
Whether this shape can never be hit (ItemShape::none).
Sourcepub fn intersects_region(&self, region: &SceneRegion, view_scale: f32) -> bool
pub fn intersects_region(&self, region: &SceneRegion, view_scale: f32) -> bool
Whether the shape and region share area — the Intersects…
half of ItemSelectionMode.
region must already be in this shape’s coordinate space — call
SceneRegion::to_local first. view_scale is as for
ItemShape::contains.
§Edges belong to no one
Sharing area is the rule, so two sets that meet only along their
boundaries do not intersect: a marquee laid exactly on an item’s
edge picks it up under no mode. That is what Scene::items_in_rect
has always answered (its rectangle compare is strict on all four
sides), and both paths below answer it — the one-compare fast path for
a plain box against an axis-aligned band, and the general outline
algebra for everything else. The two disagreeing is what let a rounded
tile be picked by a band its square twin was not, and made
IntersectsItemShape report a hit where IntersectsItemBoundingRect
reported none — impossible for a shape that lies inside its own box.
ItemShape::contains, by contrast, is closed: a point on the
edge of a rectangle is on the rectangle. A point has no area to share,
so the two rules are about different questions and neither follows
from the other.
The one exception is a region — or a shape — that is all boundary: a
zero-width path, which is exactly what
Scene::items_along_path builds, or
a degenerate box. An area rule would answer “no” to every such query,
so when either side has no area of its own the question becomes “does
this line touch the item?” and contact is the answer — the behaviour
that query has always had.
§How the general path witnesses an overlap
Four witnesses, any one of which proves shared area, checked in
increasing cost order: a probe point of the shape’s outline strictly
inside the region; a probe point of the region strictly inside the
shape; a pair of boundary segments that properly cross (or, with a
band, come closer than the two half-widths together); and — last —
every vertex of the shape inside the closed region, which is the only
witness that survives two outlines lying exactly on top of each other.
That last case is not exotic: it is a duplicate of a rotated item laid
over the original, which colliding_items really does get asked about,
and without it two identical rotated cards would report no collision.
A probe point is a vertex or a segment midpoint. The midpoints are load-bearing rather than belt-and-braces: two rectangles with the same top and bottom edges, overlapping in x — a marquee dragged to exactly an item’s height, which is a gesture, not a curiosity — put every vertex of each on the other’s boundary and cross nothing transversally, so the vertices alone witness nothing.
Every witness is sound (each really does imply shared area), and
together they are not quite complete: a sliver of overlap that no
probe point lands in and no segment pair crosses transversally reports
no intersection. Erring that way is deliberate — a false positive here
would let a …ItemShape query pick an item its own bounding rect does
not, which is impossible for a shape that lies inside its box, whereas
a false negative only ever agrees with the cheaper mode.
Where a probe lands exactly on a freeform outline, “strictly inside”
is decided within SHAPE_FLATTEN_TOLERANCE: a ray cast against a
polyline has no meaningful answer on the polyline itself. Against a
rectangle, the closed form every default shape and every marquee band
uses, it is exact.
Sourcepub fn contained_by_region(&self, region: &SceneRegion, view_scale: f32) -> bool
pub fn contained_by_region(&self, region: &SceneRegion, view_scale: f32) -> bool
Whether the shape lies entirely inside region — the Contains… half
of ItemSelectionMode.
region must already be in this shape’s coordinate space. A shape with
a stroke band is contained only when the band is — the whole
clickable ribbon, not merely the centreline.
§Edges are inside
Containment is of closed sets, so a shape resting exactly on the
region’s boundary is contained: an item and a band drawn at precisely
the same rectangle select under Contains…. That is what the
rectangle fast path has always answered, and the general path answers
it too — an outline lying along the region’s edge is not a crossing,
only an outline passing through it is. (Before, a collinear edge
counted as a crossing, so a rounded tile an exact-fit band contained
was rejected while its square twin was accepted.)
Note the asymmetry with ItemShape::intersects_region, and that it
is the only consistent pair: an exact-fit band both contains the item
and shares its whole area, while a band merely abutting the item
does neither.
§Two kinds of boundary
For a region with an interior, “every vertex is inside and no edge crosses the outline” settles it: an outline that never crosses the region’s boundary cannot have its inside on the other side of it.
A stroke band has no such outline. Its boundary is an offset curve the region never stores, and the outline it does store is the band’s centreline — which runs down the middle of the region rather than around it. Testing crossings against that gets both answers wrong: an item sitting squarely on a wire crosses the centreline and is reported as escaping, while a box whose four corners are all within a bent wire’s band bulges out across the inside of the bend, crosses nothing, and is reported as contained. Each item edge is therefore checked against the band itself, by an exact segment-in-band test.
A region with both is inside-the-fill or inside-the-band; an item weaving between the two, inside their union but inside neither part, is reported as not contained. That is the one deliberately conservative answer here, and it is one-sided: never a false containment.
Sourcepub fn intersects_scene_region(
&self,
region: &SceneRegion,
local_to_scene: &Transform2D,
view_scale: f32,
) -> bool
pub fn intersects_scene_region( &self, region: &SceneRegion, local_to_scene: &Transform2D, view_scale: f32, ) -> bool
Whether this shape, placed by local_to_scene, shares area with a
scene-space region — the Intersects… half of
ItemSelectionMode, with the frame chosen rather than assumed.
This is the door a region query comes in by, and the reason it exists
is spelled out on ItemShape::contained_by_scene_region.
Sourcepub fn contained_by_scene_region(
&self,
region: &SceneRegion,
local_to_scene: &Transform2D,
view_scale: f32,
) -> bool
pub fn contained_by_scene_region( &self, region: &SceneRegion, local_to_scene: &Transform2D, view_scale: f32, ) -> bool
Whether this shape, placed by local_to_scene, lies entirely inside a
scene-space region — the Contains… half of
ItemSelectionMode, with the frame chosen rather than assumed.
§Why the frame is a decision
A query has two frames to choose from and they are not
interchangeable, because a stroke band is a distance and only a
similarity — a rotation, a uniform scale, a translation, a reflection —
carries one. Under any other affine the exact image of a round band is
an elliptical one, which neither ItemShape nor SceneRegion
can hold: both store a single width.
Each side’s band is round in its own frame. The region’s is round in
scene space. The item’s is round in local space — that is not a
convention but what the renderer does, since
PathItem strokes its path in local coordinates and
the item’s transform is pushed on the canvas around it, so an
anisotropically scaled wire really is painted with an elliptical pen.
So: when the region carries no band, or the transform is a similarity,
the local frame is exact and the query takes it — one map, the same
cost it has always had. Otherwise the region cannot come down, so the
item goes up: its outline is a polyline and
[Path::transformed] is exact for every affine, so lifting it costs
nothing in accuracy.
A mapped band is then the only thing left, and it is dealt with by
not mapping one: in_scene_frame writes the item’s band
out as an explicit outline in local space, where it is round, and
lifts that. What comes back is a union of band-free parts, and a union
is contained iff every part is.
Approximating instead — scaling the band by one of the two stretches —
cannot work here, and the reason is worth stating because it looks like
it should. The two selection-mode families owe each other implications
in opposite directions: IntersectsItemShape must never exceed
IntersectsItemBoundingRect, which wants a band that under-reaches,
while ContainsItemBoundingRect must never exceed
ContainsItemShape, which wants one that over-reaches. No single
scalar satisfies both, so the only answer that keeps both is the exact
one.
Sourcepub fn to_scene_region(&self, local_to_scene: &Transform2D) -> SceneRegion
pub fn to_scene_region(&self, local_to_scene: &Transform2D) -> SceneRegion
Re-publish this shape as a SceneRegion in another frame — its own
scene transform, so a collision query can ask the rest of the scene
about it.
The outline goes through [Path::transformed], which is exact for
every affine (an arc a transform cannot carry as an arc is expanded
into the cubics the renderer paints it as), so the republished region
keeps its curves and is re-flattened at
SHAPE_FLATTEN_TOLERANCE in the target frame rather than
inheriting a polyline sampled in this one.
§A band’s width is still scaled here
A region query picks the frame that keeps each band round
(ItemShape::contained_by_scene_region); this cannot, because it
returns one SceneRegion and one region holds one band width. So
under a transform that is not a similarity — where the exact image of a
round band is elliptical — the width is scaled by the transform’s
smallest stretch: the widest uniform band that fits inside the true
image, under-reaching along the stretched axis and never claiming
ground the true band does not cover.
Writing the band out as an outline, which is what the query path does
and which is exact, does not work here: a band is a union of
overlapping capsules and reads only under [FillRule::Winding], while
the interior it would have to be unioned with keeps its author’s rule
and its author’s subpath orientations. The query path escapes that by
keeping the two apart as separate parts, and a single region has
nowhere to put a second part. The consequence is confined to
Scene::item_region and the collision
query built on it: an anisotropically scaled item carrying a stroke
collides as though its band were the narrowest the map allows.
Trait Implementations§
Auto Trait Implementations§
impl !RefUnwindSafe for ItemShape
impl !Send for ItemShape
impl !Sync for ItemShape
impl !UnwindSafe for ItemShape
impl Freeze for ItemShape
impl Unpin for ItemShape
impl UnsafeUnpin for ItemShape
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.impl<T> ErasedDestructor for Twhere
T: 'static,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more