15
$\begingroup$

Is it possible to use a VoronoiMesh to define a TogglerBar or SetterBar type Control?

For example, I can customise the looks of a TogglerBar

Control[{{a, 0, ""},
  {1 -> Spacer[{.1, .1}], 4 -> Spacer[{.1, 20}], 
   7 -> Spacer[{.1, .1}], 2 -> Spacer[{20, .1}], 
   5 -> Spacer[{20, 20}], 8 -> Spacer[{20, .1}], 
   3 -> Spacer[{.1, .1}], 6 -> Spacer[{.1, 20}], 
   9 -> Spacer[{.1, .1}]},
  Appearance -> "Vertical" -> {3, 3}, ControlType -> TogglerBar}]

enter image description here

But this doesn't change the rectangle-shaped buttons and I don't think this is the way to go about if I want to define a VoronoiMesh with clickable and "toggleable" cells.

I want something like

enter image description here

where each cell is selected/unselected whenever I click it, adding/removing a correspondent number to a list, for example, in the case of a TogglerBar. I would like this to also work as a SetterBar.

Any ideas?

Edit 1: Thank you all for your answers. As a follow up, I'm now interested in developing a TogglerBar-type object that allows users to hold and drag the mouse to select/deselect several cells. If you have time, please take a look at it, I'm a bit a clueless on how to do this, so any hint or idea is welcome.

Edit 2: Following Lukas Lang's answer below, I also tried to vary the grid size in Manipulate

Manipulate[x,
 Control[{n, 2, 10, 1}],
 Control[{{x, 3, ""}, 
   MeshSetterBar[VoronoiMesh@RandomReal[{0, 1}, {n, 2}]]}]]

However, this doesn't seem to behave as expected. Instead, I get

enter image description here

Any idea why, and how to fix this? I tried Dynamic, but didn't work.

Edit 3: As a third and (hopefully) final edit, thanks to Lukas Lang's answer, I was able to solve the original question. Now I just need to define several toggler-type meshes of the same shape. One naive attempt is simply

Manipulate[Null, Dynamic@Grid[{
    {Control[{n, 2, 10, 1}]},
    {Control[{{x, {}, ""}, 
       MeshTogglerBar[VoronoiMesh@RandomReal[{0, 1}, {n, 2}]]}]},
    {Control[{{y, {}, ""}, 
       MeshTogglerBar[VoronoiMesh@RandomReal[{0, 1}, {n, 2}]]}]}
    }]]

enter image description here

Which naturally doesn't yield meshes with the same shape, due to the randomness in defining the points. How can I solve this? I have tried to define the mesh outside, then I lose the dynamic update of the mesh-dependent control. I would like something like the following, where I'm able to independently update similarly shaped meshes

enter image description here

$\endgroup$
  • $\begingroup$ Some sort of Manipulate implementation? $\endgroup$ – CA Trevillian Jan 10 at 15:24
  • $\begingroup$ This has such a control. It seems to be written in V8, so no mesh. The control does several things, so it's more complicated than you probably need. It uses ClickPane to handle the mouse clicks and toggling. $\endgroup$ – Michael E2 Jan 10 at 15:32
  • $\begingroup$ Apparently I also wrote this, which someone just upvoted (thanks!). You could use it to un/highlight the cell clicked I suppose. $\endgroup$ – Michael E2 Jan 10 at 15:37
  • $\begingroup$ It's somewhat frowned upon to keep updating an old Q&A with new questions, especially there is already an accepted answer. -- Anyway, all you have to do is pass the same mesh to MeshTogglerBar's first argument. $\endgroup$ – Michael E2 Jan 15 at 23:06
  • 1
    $\begingroup$ I'm not sure exactly what you want, but you could put mesh=VoronoiMesh@RandomReal[{0, 1}, {n, 2}] in the TrackingFunction for n. If you want the same mesh every time you come back to the same n, then you'll need to do something else, like define mesh[n_] := mesh[n] = VoronoiMesh@RandomReal[{0, 1}, {n, 2}] or use SeedRandom[] or meshTogglerBar[VoronoiMesh@Take[myBigListOfPoints, n]] and so forth. $\endgroup$ – Michael E2 Jan 16 at 0:39
9
$\begingroup$

Here are implementations for a MeshTogglerBar and MeshSetterBar based on my answer here (code below). Both implementations use Mouseover and EventHandler to handle detection of the polygon below the cursor for you. Compared to the NearestFunction approach, this is far more performant (since it is done by the front-end), it also works nicely for other types of meshes, where the cell below the cursor is not necessarily the one with the closest center.

TogglerBar

TogglerBar

SetterBar

SetterBar

Code

MeshTogglerBar[mesh_] := iMeshTogglerBar[#, mesh] &
Dynamic[MeshTogglerBar[mesh_]] ^:= 
 Dynamic[iMeshTogglerBar[#, mesh] &]
MeshTogglerBar[Dynamic@var_, mesh_] := 
 iMeshTogglerBar[Dynamic@var, mesh]
iMeshTogglerBar[Dynamic@var_, mesh_] := Module[
  {prims = MeshPrimitives[mesh, 2]},
  With[
   {
    active = 
     Append[dragAction]@Table[Unique["active"], Length@prims],
    n = Length@prims
    },
   DynamicModule[
    active,
    Graphics[
     {
      FaceForm@White, EdgeForm@Blue,
      MapIndexed[
       With[
         {v = active[[#2[[1]]]]},
         EventHandler[
          Style[
           Annotation[#, ""],
           TagBoxOptions -> {
             BaseStyle -> FEPrivate`Which[
               FEPrivate`SameQ[v, True],
               {Lighter@Blue, EdgeForm@{Thick, Blue}},
               FrontEnd`CurrentValue@"MouseOver",
               LightBlue,
               True,
               {}
               ]
             }
           ],
          {
           "MouseEntered" :> FEPrivate`If[
             FEPrivate`And[
              FrontEnd`CurrentValue[{"MouseButtonTest", 1}],
              FEPrivate`UnsameQ[v, dragAction]
              ],
             FEPrivate`Set[v, dragAction];
             var[[#2[[1]]]] = dragAction
             ],
           {"MouseDown", 1} :> (
             FEPrivate`Set[dragAction, FEPrivate`UnsameQ[v, True]];
             FEPrivate`Set[v, dragAction];
             var[[#2[[1]]]] = dragAction
             )
           }
          ]
         ] &,
       prims
       ]
      },
     ImageSize -> Medium
     ],
    Initialization :> (
      If[ListQ@var,
       var = TrueQ /@ PadLeft[var, n, False],
       var = ConstantArray[False, n]
       ];
      MapThread[Set, {Most@active, var}]
      )
    ]
   ]
  ]

MeshSetterBar[mesh_] := iMeshSetterBar[#, mesh] &
Dynamic[MeshSetterBar[mesh_]] ^:= Dynamic[iMeshSetterBar[#, mesh] &]
MeshSetterBar[Dynamic@var_, mesh_] := iMeshSetterBar[Dynamic@var, mesh]
iMeshSetterBar[Dynamic@var_, mesh_] :=
 DynamicModule[
  {active},
  Graphics[
   {
    FaceForm@White,
    EdgeForm@Blue,
    MapIndexed[
     EventHandler[
       Style[
        Annotation[#, ""],
        TagBoxOptions -> {
          BaseStyle -> FEPrivate`Which[
            FEPrivate`SameQ[active, #2[[1]]],
            {Lighter@Blue, EdgeForm@{Thick, Blue}},
            FrontEnd`CurrentValue@"MouseOver",
            LightBlue,
            True,
            {}
            ]
          }
        ],
       {"MouseClicked" :> (
          FEPrivate`Set[active, #2[[1]]]; var = #2[[1]]
          )
        }
       ] &,
     MeshPrimitives[mesh, 2]
     ]
    },
   ImageSize -> Medium
   ],
  Initialization :> (active =var)
  ]

SeedRandom[1]

mesh = VoronoiMesh@RandomReal[{0, 1}, {10, 2}]

Dynamic@x

MeshSetterBar[Dynamic@x, mesh]

Dynamic@x

MeshTogglerBar[Dynamic@x, mesh]

Notes

Some notes on the implementation (you can find some more in my answer linked above):

  • Since everything is handled by the front-end, these controls will have excellent performance
  • For the MeshTogglerBar, we have to generate a list of state variables (one per cell). This is because the front-end cannot manipulate lists, so each cell needs a separate variable
  • The default values of the state variables are set in the Initialization property of the DynamicModule to ensure that the values are not prematurely inserted anywhere.
  • The dynamic styling is done via TagBoxOptions -> {BaseStyle -> {...}}. This is done since we need to set the styles via an option for the front-end-only solution to work. The Annotation[...]/TagBoxOptions trick is to ensure that any type of primitive is styled, not only Polygons.
  • The controlled variables are kept separate from the DynamicModule variables used to store the state of the control. This ensures that the front-end ↔ kernel communication is kept to a minimum (i.e. only when a click has happened is the kernel variable updated).
  • For the MeshTogglerBar, we trigger on both "MouseEntered" and "MouseDown" to enable dragging over many elements to toggle them. The state of the first element is stored in dragAction, to ensure that dragging sets all elements to the same state instead of toggling them back and forth
  • The iMeshTogglerBar/iMeshSetterBar functions are there so the control can be easily used inside Manipulate:

    Manipulate[
      x,
      {{x, 3}, MeshSetterBar[mesh]}
     ]
    
  • Similarly, the Dynamic[MeshSetterBar[_]]/Dynamic[MeshTogglerBar[_]] type definitions are to ensure that the controls work inside of Manipulate when the controls depend on other variables:

    Manipulate[x,
     {n, 2, 10, 1},
     {{x, 3, ""}, MeshSetterBar[VoronoiMesh@RandomReal[{0, 1}, {n, 2}]]}
     ]
    

    The additional definition is necessary, since Manipulate wraps control specifications in Dynamic if any other manipulate variables occur in the specifications. This prevents Manipulator from seeing the Function expression, since it is not evaluated. The additional upvalue forces evaluation into something with an explicit Function in those cases.

$\endgroup$
  • $\begingroup$ Thank you! I just have one question: would it be possible to hold the mouse and select/unselect several cells just by dragging the mouse over them? $\endgroup$ – sam wolfe Jan 14 at 10:56
  • $\begingroup$ @samwolfe see the updated code - now you can hold the mouse button down and drag over other elements to set them all to the same state $\endgroup$ – Lukas Lang Jan 14 at 12:22
  • $\begingroup$ That is great! I'm just getting a message in the beginning: "MapThread: incompatible dimensions of objects at positions {2,1} and {2,2} of (...)". Once I drag the mouse over it, it's gone. Any idea how to fix this small issue? $\endgroup$ – sam wolfe Jan 14 at 12:27
  • 1
    $\begingroup$ @samwolfe Sorry, I missed a line when updating the code - should be fixed now $\endgroup$ – Lukas Lang Jan 14 at 13:42
  • $\begingroup$ Thank you. I've just added a new edit section, because Manipulate doesn't seem to be working properly if I vary the mesh size with another Control. Can you please take a quick look? $\endgroup$ – sam wolfe Jan 14 at 16:09
10
$\begingroup$
ClearAll[togglerMesh]
togglerMesh = DynamicModule[{ m = #, ids = {},
     nF = Nearest[PropertyValue[{#, 2}, MeshCellCentroid] -> "Index"]}, 
    Dynamic@EventHandler[HighlightMesh[m, Thread[{2, Flatten@ids}]], 
      "MouseClicked" :> If[MemberQ[ids, #], ids = DeleteCases[ids, #], 
          AppendTo[ids, #]] &[If[MousePosition["Graphics"] === None, {}, 
        First@nF[MousePosition["Graphics"]]]]]] &;

Examples:

SeedRandom[1]
pts = RandomReal[{-1, 1}, {20, 2}];

vm = VoronoiMesh[pts, {{-1, 1}, {-1, 1}}];
togglerMesh[vm]

enter image description here

dm = DelaunayMesh[pts, {{-1, 1}, {-1, 1}}];
togglerMesh[dm]

enter image description here

Using a hexagonal mesh (from this answer):

SeedRandom[1]
pts = Flatten[Table[{3/2 i + RandomReal[.5], 
     Sqrt[3] j + Mod[i, 2] Sqrt[3]/2 + RandomReal[.5]}, {i, 7}, {j, 7}], 1];

hexmesh = DiscretizeGraphics @ Graphics @
   Select[Length[#[[1]]] == 6 &] @ MeshPrimitives[VoronoiMesh[pts], {2, "Interior"}];

togglerMesh[hexmesh]

enter image description here

$\endgroup$
  • 1
    $\begingroup$ Nice! This more properly takes care of the "toggler" functionality. $\endgroup$ – MarcoB Jan 10 at 16:43
  • $\begingroup$ Note that the NearestFunction will return the wrong cell in some cases for non-voronoi-type meshes. Try for example to select one of the narrow cells in your DelaunyMesh example with the cursor close to the corner $\endgroup$ – Lukas Lang Jan 11 at 10:27
8
$\begingroup$
SeedRandom[421]
points = RandomReal[{-1, 1}, {10, 2}];
mesh = VoronoiMesh[points, {{-1, 1}, {-1, 1}}];

nf = Nearest[points -> Automatic];
primitives = MeshPrimitives[mesh, {2, All}];

Dynamic[
  ClickPane[
    HighlightMesh[mesh, {2, #}] & @@
      FirstPosition[
        primitives,
        SelectFirst[primitives, RegionMember[#, Extract[points, selected]] &]
      ],
    (selected = nf[#]) &
  ],
  Initialization :> (selected = {1})
]

clicking on cell selects it

@MichaelE2's answer to Clickable Bounded Diagram provided key inspiration; @PlatoManiac's code in their answer to How to find adjacent polygons of a specific polygon in a VoronoiMesh helped with the selection of the cell within which the mouse click is found

$\endgroup$
  • $\begingroup$ I'm getting an error message saying "Extract: position specification selected (...)". Any idea why? $\endgroup$ – sam wolfe Jan 10 at 16:28
  • 1
    $\begingroup$ @samwolfe It's an initialization problem when you first evaluate because selected has not yet been assigned any value. Just click any cell and it will work after that. Otherwise, add e.g. Initialization :> (selected = {1}) as an option to Dynamic. $\endgroup$ – MarcoB Jan 10 at 16:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.