Not sure if you'll find this answer useful because it's going to be fairly general, and you may have to figure out how to adapt these ideas to React Native, but here are a few thoughts.
Don't try to find some universal commonality in the details of how shapes are manipulated, because these are all very different. You can't base your design on that.
I presume what you want, architecturally, is the ability to relatively easily extend the application with new shapes - without having to change things all over the codebase.
So, on a high level, think of it like this - for each shape, you need (1) a representation of that shape + (2) a UI for manipulating that shape. So what you want when adding a new shape is, ideally, to just define (1) & (2) - perhaps as a couple of new classes - and plug them into the existing application, without doing much else.
Another aspect that's helpful to consider, design-wise, is that your shapes play two distinct roles in the application.
One role is that of a drawable (or renderable). I.e., something that can be drawn (or, rather, knows how to draw itself). When the application needs to draw something, it doesn't need to know what it is, what shape it has, what are its logical properties. In the simplest case, it just needs to know that it has a draw()
method (it can be asked to draw itself). Or in a more flexible alternative, a draw(graphicsContext)
method - so that it can be told onto which surface it should draw itself (e.g., if you have several drawing surfaces, like the main canvas, and a preview of the shape in a shape picker). The graphicsContext
parameter here is the object on which you can call primitive draw methods (I'm thinking of something like the HTML canvas object here). Or, if you are using SVG, draw()
(or render()
) could produce the appropriate SVG markup. In any case, it would be some variation of that idea.
This means that, to the component that governs the drawing, all these objects can look the same. It just needs a list of IDrawable
-s.
The other role is that of an object being manipulated by a particular shape manipulation UI. In this case, each shape looks distinct, and supports a distinct set of methods. The question here is, when you detect a click on a particular shape, how do you determine the type of the shape, in order to instantiate the correct user interface.
One way to do it is to somehow query for the shape type (you could use some inbuilt type querying mechanism, or simply have a type-discriminating property on the shape), and then type cast as necessary.
But, you could do it in a different way. Let's think this through. You have the "canvas" - the workspace where the shapes are shown. The shapes need to be drawable and clickable/dragable. You have the UI area (say, a separate <div>) where you can inject the UI for manipulating the shape. The application doesn't need to know what the UI is, it just needs to be able to get it from somewhare, so that it can inject it and show it. What if the shape, when clicked, was able to provide this UI object?
So now you have something like this. The "shapes" that the application works with at a higher level are not necessarily your "raw" shape classes. Instead, it's a small component - an object that contains an underlying reference to the actual shape, can draw itself by drawing the shape, and can provide on-demand UI for manipulating the shape, since it knows what the type of the shape is. This kind of setup also enables you to "embellish" the shape when you draw it - e.g., you may want to indicate that the shape is active/selected, or put manipulation handles around it, etc.
The provided shape manipulation UI object is also self contained - a small smart control. It has the reference to the underlying concrete shape (basically, the shape is its state) - typed appropriately. The provider could inject the shape instance.
This could be summarized like this - it's by no means "the one true solution", but it's something to get you started thinking in a different direction:
Shape classes / data structures:
- each represents a certain shape
- aren't part of a hierachy
For each shape:
High level application:
- has a list (or maybe a tree) of Shape Drawable components representing the state of the canvas
- delegates drawing to them
- when one of them is clicked, asks it to provide the manipulation UI for it; injects that UI at the appropriate place
Adding a new kind of shape to the application would consist of defining the three components (shape, shape drawable, shape manipulation UI), wiring them up, and plugging them in.
Your shape drawables could also act as prototype instances for the shapes. E.g., if you need to show a menu/toolbar letting the user know what shapes are possible to choose from, you can populate the menu with different shape drawables. The menu/toolbar doesn't need to know what shapes they are, again, it just need to know how to draw them and to detect clicks. When the user selects one, you can clone it and place it on the canvas (cloning from a prototypical instance like this is the idea behind the prototype pattern). Basically, in this scenario, when adding support for a new kind of shape, after wiring up together, you'd only need to plug in the shape drawable as an extra menu/toolbar item.
Finally, you can create composite shapes by composing shape drawables - see the composite pattern. At the basic level, you can provide some simple controls to manipulate the composite as a whole (e.g., resizing). For things that are more involved, you'll have to figure out how to do things like grouping/ungrouping, or isolating the composite shape to manipulate its internals before exiting the isolation mode and placing it back on the main canvas, etc.
P.S. Don't get too attached to the names I used ("Shape Drawable", "IShapeManipulatorProvider", ...), it's just something that I came up with when writing this answer - if you come up with nicer names that are better suited to your application, by all means, use those.