Skip to main content

ItemShape

Struct ItemShape 

Source
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

Source

pub fn bounds(rect: Rect) -> Self

The item’s local AABB. This is the trait default and it allocates nothing.

Source

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.

Source

pub fn ellipse(rect: Rect) -> Self

The ellipse inscribed in rect. Closed form; never flattened for a point test.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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×.

Source

pub fn is_none(&self) -> bool

Whether this shape can never be hit (ItemShape::none).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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§

Source§

impl Clone for ItemShape

Source§

fn clone(&self) -> ItemShape

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ItemShape

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for ItemShape

Source§

fn default() -> Self

ItemShape::none — a shape that has not been declared cannot be claimed to cover anything.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
§

impl<T> Downcast for T
where T: Any,

§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert 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>

Convert 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)

Convert &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)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
§

impl<T> Downcast<T> for T

§

fn downcast(&self) -> &T

§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
§

impl<T> NoneValue for T
where T: Default,

§

type NoneType = T

§

fn null_value() -> T

The none-equivalent value.
§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> Upcast<T> for T

§

fn upcast(&self) -> Option<&T>

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more