Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Making a Circle Selector #742

Copy link
Copy link
Open
Open
Copy link
@CSSFrancis

Description

@CSSFrancis
Issue body actions

Hi I was interesed in making a circle selector. I've been playing around with it a little bit but was interested if you had guidance on a couple of points. Primarily when looking at the Rectangle Selector you are using a 3D object rather than a 2D one? Is there a good reason for this?

I think the code that I have (mostly) works although I need to work on the resize and the selection code. Would this be of interest? I can also abstract this to an ellipse if we just make our own custom geometry.

One other question is how is the _move_graphic function called? It would be good to include not only the deltas but the position of the click. Currently, it is hard to know if the user is dragging the circle out/in without the event position. You can kind of see the effect of this in the video where the resize is reversed depending on which part of the circle you click on.

Screen.Recording.2025-03-03.at.8.47.10.AM.mov
class CircleSelectionFeature(GraphicFeature):
    """
    **additional event attributes:**

    +----------------------+----------+------------------------------------+
    | attribute            | type     | description                        |
    +======================+==========+====================================+
    | get_selected_indices | callable | returns indices under the selector |
    +----------------------+----------+------------------------------------+
    | get_selected_data    | callable | returns data under the selector    |
    +----------------------+----------+------------------------------------+

    **info dict:**

    +-------------0--+------------+-------------------------------------------+
    |   dict key    | value type | value description                         |
    +===============+============+===========================================+
    | value        | np.ndarray | new [centerx, centery, radius, inner_radius] of selection |
    +-------------+------------+-------------------------------------------+
    | limits      | np.ndarray | new [xmin, xmax, ymin, ymax] for the ind |
    +------------+------------+-------------------------------------------+
    | size_limits| np.ndarray | new [xmin, xmax, ymin, ymax] for the ind |
    +------------+------------+-------------------------------------------+
    """

    def __init__(
        self,
        value: tuple[float, float, float, float],
        limits: tuple[float, float, float, float],
        size_limits: tuple[float, float, float, float]=None,
    ):
        super().__init__()

        self._limits = limits
        self._value = tuple(int(v) for v in value)
        self._size_limits = size_limits

    @property
    def value(self) -> np.ndarray[float]:
        """
        (centerx, centery, radius, inner_radius) of the selection, in data space
        """
        return self._value


    def set_value(self, selector, value: Sequence[float]):
        """
        Set the selection of the rectangle selector.

        Parameters
        ----------
        selector: RectangleSelector

        value: (float, float, float, float)
            new values (centerx, centery, radius, inner_radius) of the selection
        """
        if not len(value) == 4:
            raise TypeError(
                "Selection must be an array, tuple, list, or sequence in the form of `(xmin, xmax, ymin, ymax)`, "
                "where `xmin`, `xmax`, `ymin`, `ymax` are numeric values."
            )

        # convert to array
        value = np.asarray(value, dtype=np.float32)

        # clip the center values if they are outside of the selection.
        # the radius should be allowed to move beyond the limits
        value[0] = value[0].clip(self._limits[0], self._limits[1])
        value[1] = value[1].clip(self._limits[2], self._limits[3])


        x, y, radius, inner_radius = value

        # make sure that the radius is greater than 0 and the inner radius is less than the radius
        if radius <= 0 or inner_radius> radius:
            return
        radial_segments = 360

        cap = generate_cap(radius=radius, height=1, radial_segments=radial_segments, theta_start=0,
                           theta_length=np.pi * 2)

        cap_outline = generate_cap(radius=radius, height=1.01, radial_segments=radial_segments, theta_start=0,
                                   theta_length=np.pi * 2)

        positions, normals, texcoords, indices = cap

        selector.fill.geometry.positions.data[:] = positions + np.array([x, y, 0])

        positions, normals, texcoords, indices = cap_outline
        # shift the circle to the correct position

        selector.edges[0].geometry.positions.data[:] = positions[1:] + np.array([x, y, 0])


        # change the edge lines
        self._value = value

        # send changes to GPU
        selector.fill.geometry.positions.update_range()

        for edge in selector.edges:
            edge.geometry.positions.update_range()

        # send event
        if len(self._event_handlers) < 1:
            return

        event = FeatureEvent("selection", {"value": self.value})

        event.get_selected_indices = selector.get_selected_indices
        event.get_selected_data = selector.get_selected_data

        # calls any events
        self._call_event_handlers(event)
class CircleSelector(BaseSelector):

    def __init__(self,
                 center: Sequence[float],
                 radius: float,
                 limits: Sequence[float],
                 inner_radius: float = 0,
                 parent: Graphic = None,
                 resizable: bool = True,
                 fill_color=(0, 0, 0.35),
                 edge_color=(0.8, 0.6, 0),
                 vertex_color=(0.7, 0.4, 0),
                 edge_thickness: float = 4,
                 vertex_thickness: float = 8,
                 arrow_keys_modifier: str = "Shift",
                 size_limits: Sequence[float] = None,
                 name: str = None,):

        if not len(center) == 2:
                raise ValueError()

        # lots of very close to zero values etc. so round them
        center = tuple(map(round, center))
        radius = round(radius)
        inner_radius = round(inner_radius)

        self._parent = parent
        self._center = np.asarray(center)
        self._resizable = resizable
        self._limits = np.asarray(limits)
        self.size_limits = size_limits

        center = np.asarray(center)

        # world object for this will be a group
        # basic mesh for the fill area of the selector
        # line for each edge of the selector
        group = pygfx.Group()


        self._fill_color = pygfx.Color(fill_color)
        self._edge_color = pygfx.Color(edge_color)
        self._vertex_color = pygfx.Color(vertex_color)
        if radius < 0:
            raise ValueError()

        radial_segments = 360

        cap = generate_cap(radius=radius, height=1, radial_segments=radial_segments, theta_start=0,
                           theta_length=np.pi * 2)

        cap_outline = generate_cap(radius=radius, height=1.01, radial_segments=radial_segments, theta_start=0,
                                   theta_length=np.pi * 2)

        positions, normals, texcoords, indices = cap
        circle_geo = pygfx.Geometry(
            indices=indices.reshape((-1, 3)),
            positions=positions,
        )

        self.fill = pygfx.Mesh(circle_geo,
                               pygfx.MeshBasicMaterial(
                                   color=pygfx.Color(self.fill_color), pick_write=True
                               ),
                               )
        self.fill.world.position = (0, 0, -2)

        group.add(self.fill)

        positions, normals, texcoords, indices = cap_outline

        positions_outline = positions[1:]
        circle_outline_geo = pygfx.Geometry(
            indices=indices.reshape((-1, 3))[1:],
            positions=positions_outline,
        )


        outline = pygfx.Line(
            geometry= circle_outline_geo,
            material= pygfx.LineMaterial(thickness=edge_thickness, color=self.edge_color)

        )

        self.edges = (outline,)


        outline.world.z = -0.5
        group.add(outline)
        selection = np.asarray(tuple(center) + (radius, inner_radius))
        self._selection = CircleSelectionFeature(selection, limits=self._limits,
                                                         size_limits=size_limits)

        # include parent offset
        if parent is not None:
            offset = (parent.offset[0], parent.offset[1], 0)
        else:
            offset = (0, 0, 0)

        BaseSelector.__init__(
            self,
            edges=self.edges,
            fill=(self.fill,),
            hover_responsive=(*self.edges,),
            arrow_keys_modifier=arrow_keys_modifier,
            parent=parent,
            name=name,
            offset=offset,
        )

        self._set_world_object(group)

        self.selection = selection

    def _move_graphic(self, delta: np.ndarray):

        # new selection positions
        centerx_new = self.selection[0] + delta[0]
        centery_new = self.selection[1] + delta[1]

        # move entire selector if source is fill
        if self._move_info.source == self.fill:
            # set thew new bounds
            self._selection.set_value(self, (centerx_new, centery_new, self.selection[2], self.selection[3]))
            self.selection = (centerx_new, centery_new, self.selection[2], self.selection[3])
            return

        # if selector not resizable return
        if not self._resizable:
            return
        elif self._move_info.source == self.edges[0]:
            if delta[0] < 0 or delta[1] < 0:
                radius_change = -np.sqrt(delta[0]**2 + delta[1]**2)
            else:
                radius_change = np.sqrt(delta[0]**2 + delta[1]**2)

            new_radius = self.selection[2] + radius_change
            if new_radius < 0:
                return
            if self.size_limits is not None:
                if new_radius < self.size_limits[0] or new_radius > self.size_limits[1]:
                    return
            values = (self.selection[0], self.selection[1], self.selection[2]+radius_change, self.selection[3])
            self._selection.set_value(self, values)
            self.selection = values
            return

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.