Back to Blogs
#devlog#ai#javascript#showdev

Devlog: I Built a 3D Library in Three.js Without a Level Editor — So I Made My Own

Updated 3D Environment for Library Reading DEV Articles A lot of you...

Updated 6 min read
Cover for Devlog: I Built a 3D Library in Three.js Without a Level Editor — So I Made My Own

Updated 3D Environment for Library

New Dev Setting

Reading DEV Articles

reading articles

A lot of you wonderful people enjoyed my last post on Oni, so heres an update on what I've been working on.

I've been spending the last few days building the DEV Library inside Oniria, and somewhere along the way the project stopped feeling like "a cool Three.js scene" and started feeling like I was building my own tiny game-engine workflow.

The idea behind the library is pretty simple:

DEV articles become physical books.

Instead of opening DEV.to and scrolling through cards, you walk through rooms, browse shelves, pull out an article, read it, and return to the exact place you found it.

But actually constructing that space turned out to be one of the most interesting problems I've hit so far.

Earlier versions of Oniria leaned much harder into a cyber-world aesthetic: glowing structures, highways, towering architecture, and DEV articles scattered through something closer to a futuristic city. It looked interesting, but the more I built it, the more I realized the environment wasn't helping me understand the information inside it.

Old Design

Articles were technically spatial, but the space itself didn't explain why anything was where it was. Eventually I started asking a simpler question: if thousands of articles were going to become physical objects, what kind of place would naturally organize them? The answer was obvious — a library. That shift changed the project from decorating a cyber-world with content into designing an actual information architecture: rooms became categories, shelves became collections, books became articles, and the building itself could communicate how DEV is organized.

That decision also created a completely different engineering problem. I was no longer placing a handful of futuristic landmarks; I was trying to build an entire navigable library — rooms, shelves, paths, furniture, signs, lighting, and hundreds of precise spatial relationships — almost entirely through Three.js code, without a traditional live level editor.

That progression looked like this:

cyber city
    ↓
looked cool, but lacked informational meaning
    ↓
"what spatial metaphor actually fits articles?"
    ↓
library
    ↓
rooms = information architecture
    ↓
now we need to physically build it
    ↓
wait... we don't have a level editor
    ↓
build our own tools
Enter fullscreen mode Exit fullscreen mode

The problem: I wasn't using a level editor

Before building the in-world authoring tools, most object placement happened directly in Three.js. All by code. No 3D Editor. Ha, take that, purist!

I wrote a small helper that clones one of the loaded GLB assets, then applies its world position, scale, and rotation:

const placeAsset = (
  template: THREE.Group,
  x: number,
  y: number,
  z: number,
  scale = 1,
  rotationY = 0,
  rotationX = 0,
) => {
  const instance = template.clone(true)

  instance.position.x += x
  instance.position.y += y
  instance.position.z += z

  instance.scale.multiplyScalar(scale)

  instance.rotation.y += rotationY
  instance.rotation.x += rotationX

  group.add(instance)

  return instance
}
Enter fullscreen mode Exit fullscreen mode

That made placing something fairly straightforward:

const table = placeAsset(
  readingTable,
  -16.2,   // x
  0.05,    // y
  -31.8,   // z
  1,       // scale
  Math.PI / 2,
)
Enter fullscreen mode Exit fullscreen mode

But moving it still meant editing numbers:

table.position.x += 0.8
table.position.z -= 1.2
table.rotation.y += Math.PI / 4
Enter fullscreen mode Exit fullscreen mode

Or changing the original call:

const table = placeAsset(
  readingTable,
  -15.4,
  0.05,
  -33.0,
  1,
  Math.PI * 0.75,
)
Enter fullscreen mode Exit fullscreen mode

Then:

save
↓
reload
↓
walk back across the library
↓
look at it
↓
"nope"
↓
change the numbers again
Enter fullscreen mode Exit fullscreen mode

That was basically my level editor. 😭 Below is an actual snippet of what I was working with:

old level editor

And once the environment grew beyond a handful of props, it became painfully obvious that this wasn't going to scale.

With a normal interface, I can change some CSS, refresh the browser, and immediately see whether a card needs another 16px of margin.

In a 3D world, changing:

position: [-16.2, 0, -31.8]
Enter fullscreen mode Exit fullscreen mode

might mean walking across an entire room just to discover that a table is clipping through a bookshelf.

bugs

And at this point the library wasn't just a few props.

I was placing:

  • walls and architectural pieces
  • real and decorative bookshelves
  • tables and chairs
  • rugs
  • plants
  • pavilions
  • signs
  • room entrances
  • shelf rows
  • collision boundaries
  • lights
  • eventually, empty spaces reserved for shelves that don't even exist yet

The first version of my workflow was basically:

change coordinates
      ↓
save
      ↓
reload the world
      ↓
walk to the room
      ↓
inspect the object
      ↓
realize it's wrong
      ↓
repeat
Enter fullscreen mode Exit fullscreen mode

It worked.

It was also slowly driving me insane.

The obvious answer would have been to move the whole project into a traditional level editor.

But I didn't really want another application to become the source of truth for the library.

The architecture was already deeply connected to code:

DEV data
   ↓
district
   ↓
room
   ↓
shelf slot
   ↓
Three.js placement
Enter fullscreen mode Exit fullscreen mode

Also, the Three.js editor was really freaking annoying to use.

three.js editor

At this point I realized that moving assets this way would either take forever or it would never be perfect to my liking. There had to be an easier way — and then I had an idea. Why don't we make the location of each shelf predetermined by a layout I create? Essentially a blueprint — then have Codex place the individual assets at each coord/pin that I place. So that became my next goal.

I needed to see coordinates inside the world

So instead I started building a tiny authoring system directly into Oniria.

The basic idea was simple:

If I'm standing somewhere in the world and think, "this object should go here," I should be able to record it here.

I added an in-world layout marker tool.

Now I could walk into a room, find the location visually, and drop a pin.

A marker could store something like:

{
  "label": "R1-P01",
  "roomSlot": 1,
  "districtId": "latest",
  "x": 16.154,
  "y": 0,
  "z": 5.369,
  "yaw": 1.57
}
Enter fullscreen mode Exit fullscreen mode

Suddenly I wasn't trying to mentally convert a 3D room into numbers.

I could use the room itself to produce the numbers.

The workflow became:

walk through the space
        ↓
find the location visually
        ↓
drop a marker
        ↓
capture position + rotation
        ↓
turn the marker into real architecture
Enter fullscreen mode Exit fullscreen mode

That sounds like a small change.

It completely changed how I could build the library.

using an editor

The world became my editor

For example, when I wanted to add an object to a particular room, I could physically stand where it belonged and create a marker.

Then that surveyed position could go directly back into the scene:

const surveyedPlacement = {
  x: 16.154,
  y: 0,
  z: 5.369,
  yaw: Math.PI / 2,
}

const instance = placeAsset(
  someLibraryAsset,
  surveyedPlacement.x,
  surveyedPlacement.y,
  surveyedPlacement.z,
  1,
  surveyedPlacement.yaw,
)
Enter fullscreen mode Exit fullscreen mode

I started using the same workflow for things like:

"This shelf belongs here."

"This pavilion needs to move farther from the path."

"These two signs are colliding."

"This light needs to sit over this exact area."

"This decorative bookcase should sit between these DEV shelves."
Enter fullscreen mode Exit fullscreen mode

new layout yay

Persisting the layout

I also didn't want those markers to disappear every time the browser refreshed. So the markers became Sanity documents.

A marker now has a persistent identity:

type LibraryLayoutMarker = {
  id: string
  label: string
  roomSlot: number
  districtId?: string

  x: number
  y: number
  z: number

  yaw?: number
  width?: number
  depth?: number
}
Enter fullscreen mode Exit fullscreen mode

That means a layout session can survive reloads.

I can place several markers, come back later, and continue working from the same spatial notes.

I can also export them.

For example:

{
  "format": "oniria-library-layout-pins-v1",
  "markers": [
    {
      "label": "R1-P01",
      "roomSlot": 1,
      "x": 16.154,
      "y": 0,
      "z": 5.369
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

At this point I had accidentally built something halfway between a debugging tool, a survey tool, and a very small level editor.

And it solved a real problem immediately. Yay!

What started as a frustrating layout problem ended up teaching me something bigger about the project: if Oniria is going to treat the web as a place instead of a page, then the tools for building that place matter just as much as the content inside it. The DEV Library now has its own spatial logic, persistent layout markers, searchable shelves, dynamic slots, and an architecture that can evolve without losing its sense of place. I'm still building it room by room, but that's also the part I've started to enjoy most — every time the world gets difficult to work with, I end up building a new piece of the system that makes Oniria feel a little more real.

oniria