aframevr / aframe

:a: Web framework for building virtual reality experiences.
https://aframe.io/
MIT License
16.58k stars 3.94k forks source link

Component/attribute inheritance and precedence for primitives #5484

Open mrxz opened 6 months ago

mrxz commented 6 months ago

Description:

The inheritance and precedences of values for a primitive is currently as follows:

This is handled during entity and component initialization, but later updates can easily 'break' things. For example a resetProperty completely ignores mixins. Clobbering a multi-prop component wipes primitive defaults and mappings, yet retains mixins. There's a difference between initializing a primitive with a mixin and adding that mixin later. Some of these are fixed in #5474, but a lot of this logic for primitives lives outside of components. Effectively the precedence is:

Especially the way mixins are handled causes unintuitive inconsistencies like the fact that a mixin at init time on a primitive can result in a different outcome compared to that mixin being added post initialization.

However, there are benefits to the current behaviour, despite its inconsistencies. Because it's all combined into the AttrValue of a component, it allows the user to "subtract" from it. Take the following example:

// HTML
// <a-camera></a-camera>
cameraEl.removeAttribute('wasd-controls'); // Gets rid of the wasd-controls

Which is also documented and also an answer on StackOverflow. So it's a given that users do depend on this behaviour.

At the same time users might be surprised why the following behaves inconsistently:

// HTML
// <a-mixin id="blue" material="color: blue"></a-mixin>
// <a-box></a-box>
boxEl.setAttribute('mixin', 'blue');
// <a-image src="url(some-image.jpg)"></a-image>
imageEl.setAttribute('mixin', 'blue');

Only the box turns blue, while the image doesn't get "tinted" blue.

Personally I would be for enforcing the full inheritance at all times. This should avoid many inconsistent and unitive situations. But it won't allow any "subtracting", as in, the primitive can't be reduced beyond what it is by default. That would be a breaking change in behaviour, though.

dmarcos commented 6 months ago

A bit of a tangent. From the beginning I've considered primitives a liability and a source of complexity and headaches. It makes markup inconsistent in a model where we have tags that map to entities and their attributes to components. I definitively see the appeal of primitives for beginners <a-camera> <a-box>... it makes A-Frame very welcoming and concept of entity / components can be introduced later. Main reason I kept them around. Wonder if we should think about deprecating the public API (registerPrimitive) and keep only those relevant to beginners as a form of alias to the expanded entity + components.

We could rename primitive to alias to convey they're just a shorthand of the "real" thing. Maybe we wouldn't need to full support for mixins, arbitrary mappings... logic can be more ad-hoc for just the "aliases" targeting beginners.

mrxz commented 6 months ago

Primitives have a nice benefit in terms of DX and readability, which extend beyond just beginner use. With only <a-entity> and its components, you'd have to infer its semantic purpose or name/label it accordingly. With editor based ECS engines, the outline will generally have the names followed by icons for specific components making it easy to see at a glance. The verbosity and structure of HTML makes this a lot harder. For example, compare the following:

<a-entity checkpoint geometry="primitive: cylinder; height: 1.5; radius: 0.2" material="color: green"></a-entity>
<a-entity checkpoint="final: true" geometry="primitive: cylinder; height: 1.5; radius: 0.2" material="color: purple" move-back-and-forth></a-entity>
<a-checkpoint color="green"></a-checkpoint>
<a-checkpoint final="true" color="purple" move-back-and-forth></a-checkpoint>

Of course there are multiple ways to achieve similar result. Mixins can be used or a component can be created that bootstraps the other components. Though, when working on a larger project, I find defining primitives to be a really nice way to make a sort of 'DSL' of tags.

The usefulness quickly drops the more you do programmatically instead of declaratively, but I don't see this as an argument against it. It's akin to how <div> tags can be made to behave like <img>, <button> or <a>.

Code complexity can be greatly reduced if the default values slot into the already existing inheritance behaviour. Having components without corresponding attribute is already the case for mixins, so dropping primitives won't help there. Only the attribute mappings are specific for primitives, but that is already handle in the primitive internally.

dmarcos commented 6 months ago

In practice when writing HTML or instantiating entities with JS you don't deal with the component expansion. The checkpoin component initializes the others at setup. A more accurate example of the dev experience would be:

<a-entity checkpoint="color: green"></a-entity>
vs
<a-checkpoint color="green"></a-checkpoint>

or in JS

var entityEl = document.createElement('a-entity');
entittyEl.setAttribute('checkpoint', {color: green});

vs 

var entityEl = document.createElement('a-checkpoint');
entittyEl.setAttribute('color', 'green');

The difference is pretty marginal if any at the expense of breaking the model consistency and introducing ambiguity (only <a-entities> in your scene graph with attributes corresponding to components). Also can be confusing when combining with other components because properties might be mapped to the primitive attributes resulting in collisions and unexpected results. Finally, usage of primitives is not very spread out past the built-in ones.

mrxz commented 6 months ago

In practice when writing HTML or instantiating entities with JS you don't deal with the component expansion. The checkpoin component initializes the others at setup.

That's one of the alternative approaches I mentioned, but it isn't without its drawbacks. If the component naively sets the other components, it won't properly handle mixins or conflicting component values on the entity. Exposing all properties of the underlying components in its schema is tedious and couples the bootstrapping component tighter to other components than needed. All in all it would be very tricky to correctly implement the lifecycle hooks (init, update, remove), and everyone creating such components would have to do so.

This inheritance order is arguable a neat feature of primitives. The goal of this issue is to see if we can/should expand this behaviour beyond just initialization. That would allow users to add and remove attributes on primitives while retaining the base/default components and values of those primitives, which I think is generally more intuitive and expected behaviour.

The difference is pretty marginal if any at the expense of breaking the model consistency and introducing ambiguity (only in your scene graph with attributes corresponding to components). Also can be confusing when combining with other components because properties might be mapped to the primitive attributes resulting in collisions and unexpected results.

While I do agree that having attributes map to either components or properties is less consistent and a potential source of confusion (/ learning curve), the problem of collisions could easily be addressed by having attribute mapping collisions be handled the same way as component name collisions, that is, by throwing an error. Since primitives are instances of AEntity I don't really see why only having the <a-entity> tag would be inherently better. Assuming no collisions, they behave as such, so no code should have to care about the actual tag. And personally I find the readability increases if the tags reflect the semantics of the entity.

Finally, usage of primitives is not very spread out past the built-in ones.

Is that really the case? Both AR-JS and 8thwall provide primitives. Virtually all sky and ocean/water implementations offer a primitive. And GUI libraries like aframe-gui and aframe-material-collection do as well for their UI elements.

Primitives are ideal for those use-cases. Many users creating experiences instead of libraries will likely find no need to write their own primitives. But that doesn't mean they don't benefit from consuming primitives (built-in ones or third party).

dmarcos commented 6 months ago

I think there's not really a difference recommending <a-entity xxx> instead of <a-xxx></a-xxx>. Very marginal benefit to the user of primitives if any. Deprecating results in simpler more maintainable code, removes one layer of abstraction, less concepts to learn, less ambiguity and space for confusion. Makes also things more explicit even if sometimes more verbose. It's easier to understand what's going on.