This website uses cookies for analytics and to improve provided services. By choosing I Accept, you consent to our use of them and other tracking technologies according to our Privacy Policy

Building a 3D Editor with Vue.js: Designing a Reactive Entity System

Being a frontend developer working on dozens of Web projects, it's fairly easy to dissolve into the numbing monotony of dashboards, tables, and photo galleries. But every now and then, an unusual project appears, offering up an opportunity to face new challenges and a whole new level of problems to solve. Last year, we were approached by a client who asked us to build a 3D interior design app for the Web. Leading such a project was truly a unique experience and a breath of fresh air. In a series of blog posts, I would like to share with you some knowledge acquired in the course of developing that particular application.

The general idea was to create something similar to Homestyler, Wanaplan, or Homebyme. Our client prepared a list of insights regarding those applications—what they liked, what could still be improved, and what features should be rewritten from scratch. All of these insights combined contributed to the vision for the new product.

The Challenge

The shape of the planned application was defined by the following requirements:

  • Web application—offering end users easy access (similar to all other services by the same client).
  • 3D workspace—computer graphics and attendant 3D rendering engines have become a true blessing for designers and architects, as they allow them to produce highly immersive representation of their creations. The same, however, is now available to the average Joe, particularly when it comes to playing with their virtual flat and decorating it as they wish. These semi-professional capabilities are the key driver of such products, the main selling point of many a service, and, naturally, the core feature of our application.
  • 2D workspace—although a 3D view is perfectly suited for intuitive item placement and material visualization, it nevertheless still lacks precision and often leads to poor space planning. A traditional CAD-like view for floor plans is an obligatory addition to the UI. Obviously, both views should allow for similar operations and be synchronized all the time.
  • Workspace save & load—once the plan is created, the user has to have the possibility of saving it for further use at any point; otherwise, the time spent in the app by a user would be lost. Once implemented, such a feature could easily be used for prepopulating plans or promotional demonstration of fully arranged interiors.
  • History tracking—it’s hard to imagine any usable editor without the option of allowing you to correct your mistakes with a magical “Undo” button. Nobody wants to make frustration a part of the product experience, so this was an obvious point on the list.
  • External assets—all product data was planned to be provided via a REST API in order to allow complete control over content provided inside the editor (descriptions, prices, providers, thumbnails, and 3D models).

The Technological Side

All in all, from the list outlined above emerges the image of a complicated and a rather non-trivial piece of software. Some of the points above carry considerable weight on their own, while others only add complexity to an already big pile of trouble.

Thanks to the evolution of the modern browser, however, we had the option to steer clear of third-party extensions offering 3D accelerated graphics solutions. A few years ago, the only widely supported technology would be Flash, but nowadays modern Web browsers provide the WebGL API, with multiple great engines also emerging from the JS community. After some research, we ultimately decided to go with BabylonJS. The 2D view conundrum could have been solved with many different approaches, but our first bet was on SVG—and it has proven to work just fine.

Other requirements were more technology agnostic, but they inevitably foreshadowed careful architecture design planning and a well-thought out implementation. Two issues stood out as the most problematic—workspace state sharing and history tracking. These two had to become the core of the editor’s architecture, since introducing them at a later stage of development would have been too expensive and very bug-prone.

The Solution

After turning it over in my mind for quite some time, I eventually concluded that a proper state sharing solution would solve the state tracking problem, thus making history manipulation an easy task. Having no previous experience with editor building but some interest in game development, my thoughts veered toward one particular pattern that seemed to suit this problem well—an Entity System.

Below, you’ll find a brief outline of the idea of an Entity System from the perspective of our planned features, but for a more in-depth look into the concept, I recommend some further investigation thereof online. Starting with the Wikipedia entry, the Github article, or the whole dedicated wiki platform. The Entity System is a programming pattern that gathers all objects (entities) existing within a certain space (world, scene, level) and allows for a flexible composition of different behaviors and traits by applying components to entities. Each Entity is described by its Components, each Component being a certain property (or a group of properties) and an associated value, with no logic inside. The actual interpretation of the components and their values is performed by Systems. Each system processes entities within the scope of a certain component (or components) that are of interest to it, ignoring everything else.

How is that useful for the editor case? Proper use of components facilitates the creation of a data-driven architecture for an application. User interaction with the editor is nothing more than populating a workspace with predefined objects and tinkering with their properties. All we have to do, therefore, is to define a list of all properties that describe items occurring in the user workspace and implement proper systems supporting the desired logic, behaviors, and mechanics. All interaction with the items—whether initiated by the user or conducted programmatically—would ultimately boil down to component manipulation.

Turns out that modeling the workspace state with entities and components fulfills previously described requirements. Both 2D and 3D workspaces can be viewed as a function of said state, producing a different outcome and offering the user different ways of interaction in each case, while being populated with data from a single source solves the issue of synchronization.

Designing such a setup simplifies the process of saving and loading of a user’s work, essentially stripping it down to state serialization and storage. Another advantage of the proposed approach is that with a single location for state description, it’s easier to track all changes applied in the workspace. Restrict those changes with a set of rules and deterministic behaviors and voilà, history tracking is here!

Reactivity to the Rescue

So far so good, the plan looked right on paper and we were ready to begin implementation. As our editor’s runtime platform would be the browser and the whole application was supposed to be a Web app anyway, we wanted to use some sort of modern tool to facilitate development.

When it comes to building Web software, my first choice is Vue.js. Built on top of a powerful reactivity engine, it makes data management easy, even when dealing with big and complex creations. As it turned out, this project has clearly proven that it is possible to harness the power of Vue.js for much more advanced features than just UI controls.


Work with the Monterail team—unquestionably the best Vue experts on the market. We’ve delivered over 15 Vue projects, authored open source libraries, organized the first VueConf, and authored State of Vue.js report. Let’s take your app from ok to exceptional.


Before diving into implementation details—a short introduction to reactivity and the way Vue incorporates it. The Wikipedia definition of reactive programming states:

In computing, reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change. This means that it becomes possible to express static (e.g. arrays) or dynamic (e.g. event emitters) data streams with ease via the employed programming language(s), and that an inferred dependency within the associated execution model exists, which facilitates the automatic propagation of the change involved with data flow.

The key passage from this definition is “automatic propagation of the change involved with data flow.” It means that any time we declare a data dependency by combining, transforming or consuming values, the system detects and stores these dependencies, reusing them at the right time whenever a relevant value is changed. Ultimately, the main goal of the entity system is to store scene (items) data and propagate state changes to both 2D and 3D workspaces.

The Vue.js reactivity model is based on JS objects—each value placed in the Vue.js component data space becomes reactive thanks to being wrapped in a getter and setter. The getter registers who consumes the requested value (building the dependency list), while the setter informs those consumers about a new value (triggering all dependencies). This way, there is no need for testing data propagation—the only place related to the changed value (or its derivative) will eventually be called. For more details on this system, check out the Vue.js documentation.

VirtualDOM

The reason for this reactivity chit-chat is the fact that the 3D engine has its own internal state and our goal is to connect it with the central application state. Since constant full-state synchronization would be costly, we can trick Vue.js to optimize it for us. It internally uses a mechanism called Virtual DOM to keep the DOM up to date with the component state by minimizing the number of DOM changes. We will use it to generate synchronization calls to the 3D engine, according to a relevant part of the state, by building DOM representations of 3D entities and optimizing its changes with the Virtual DOM.

You'll find the code sandbox for the implementation presented in a cool way here. 

Let’s base our entity description on a JS object. Any time we want to add a component to entity, we just have to define a new property:


{
  id: 'abc123',
  selected: true,
  position: {x: 0, y: 0, z: 100},
  rotation: {x: 0, y: 1.125, z: 0.764},
  item: 'af528c0'
}

Now, our scene is described by an array of such entities. New objects are appended each time a user adds an element to a workspace.

We’re in the main component of our app, let’s define data using the entities array:


<script>
  import Entity from "./components/Entity";
  export default {
    name: "App",
    components: { Entity },
    data() {
      return {
        entities: [
          // place entity objects here
        ]
      }
    }
  };
</script>

We will get to the Entity component shortly. Once entities are defined in reactive state space (the object returned by datafunction is made reactive by Vue.js), we can refer to them in a template:


<template>
<div id="app">
<ul>
<Entity v-for="entity in entities" :key="entity.id" :entity="entity"></Entity>
</ul>
</div>
</template>

The Entity component should accept an entity object as a prop and then list components in a similar way:


<script>
import PropComponent from "./PropComponent";
import { isComponent } from "./compUtils";

export default {
name: "Entity",
components: { PropComponent },
props: {
entity: {
type: Object,
required: true
}
},
computed: {
components() {
return Object.keys(this.entity).filter(key => isComponent(key));
}
},
mounted() {
console.log("Created DOM node for entity", this.entity);
},
beforeDestroy () {
console.log(`DOM node for entity ${this.entity.id} is being removed`);
}
};
</script>

Watch out for PropComponent—naming Vue's component as a Componentwould suit the Entity/Component idea, but that name is reserved for Vue’s dynamic components. The components list is built from the entity object keys list (and non-component util properties, like id, should be ignored). The template will be quite simple—v-foriterating through the components list, passing data to PropComponent with props:



<template>
<li> id: {{entity.id}}
<ul>
<PropComponent v-for="component in components" :key="component" :id="entity.id" :comp="component" :value="entity[component]"></PropComponent>
</ul>
</li>
</template>

Now, for the final part of our implementation—PropComponent:


<script>
export default {
name: "PropComponent",
props: {
comp: {
type: String,
required: true
},
id: {
required: true
},
value: {
required: true
}
},
mounted () {
console.log(`Created DOM node for component ${this.comp}`)
},
watch: {
value: {
handler (newVal) {
console.log(`New value of ${this.id}-${this.comp} is `, newVal)
}
}
},
beforeDestroy () {
console.log(`DOM node for component ${this.comp} is being removed`)
}
};
</script>

The value watcher is essential part. It will be called with a new value any time a component is changed inside the entity. Since the watcher provides only value, idand compare passed in props. Together, this data will be used for workspace updates like modifying item position, selection, or other properties. mounted and beforeDestroy methods will make it easier to build a proper value synchronization lifecycle in the future. In our case, it was 3D scene object addition or removal. Open CodeSandbox and play with the sample implementation.

Additionally, I added a simple text editor that allows for editing the entities list in JSON form. When you examine the App component for the computed property state, you will see that changes will apply only once the text representation is a valid JSON file.

Any time you add an entry for a new entity, add a component property or change component value, you will be informed about it via the console. The only oddity is that with any change to state, components containing an object as a value will be falsely flagged as having new value. This particular issue is the result of replacing the whole state with a new object instance returned by JSON.parse()

Vue.js will compare the new state tree against its older iterations, treating primitive values (numbers, strings) as “no changes” if values are the same, but flagging every object value that appeared since the reference change. To observe reactive behavior in a more natural environment, open the sandbox app and play with the entities using Vue.js dev tools. Modify, add, and remove components in the existing entities or add new entity entries (first select the App component in the application tree, then locate the entities array in the state section). Each time you modify the state, a dedicated console log should be fired off, describing that exact change.

Displaying all the structure of the entities and components is not necessary. The system requires components to be rendered in order to utilize the VirtualDOM trick, resulting in an opportunity to build a simple entities inspector at the same time. Eventually, this whole mechanism can be hidden from the end user with a simple display: none CSS property, leaving the DOM structure working under the hood.

The Outcome

 The resulting application now offered both 3D and 2D designing, planning, and arranging your dream interior. Much of it was achievable due to the dev team’s choice of Vue.js for the backbone of the app.

Vue.js has once again proven that it’s capable of handling even very complex projects. It allowed us to build an app with an unusual architecture and turned out to be highly flexible in the process. It’s possible to use Vue in myriad ways, many of which are far removed from what we would expect as the more obvious ones. In our project, using Vue.js as a library for building the UI was in fact the solution to the data flow trouble.

Building a 3D editor is a long journey, but a solid basis will make it much easier. In upcoming posts in the series, I will be explain how to tap the Vue.js ecosystem to solve other complex problems, so stay tuned.

One more thing—if Vue.js sounds like something you could use for your next project, my colleagues published the State of Vue.js report, so if you’d like to make an educated decision on what framework you’d like to work with, the report will definitely help you make one. Short on time? Check out the key takeaways. 

vueteam Brief Summary of the Vue.js Core Team Sprint and Their Visit at Monterail
Easyship case study Easyship case study: How They Switched From AngularJS to Vue and Increased Website Performance by 37%
Cooleaf Working with Legacy Code: Cooleaf Refactor Case Study