← Cycles
Qmap · c11

Grid Layout Engine

archivedcreated 2026-03-23· last activity 2026-08-15

New in this cycle: Virtual-grid layout engine — recursive bin packing decoupled from rendering

Why This Cycle Exists

Previous layout engines (c5-c10) evolved organically, mixing concerns:

  • Layout math (positions, bounds)
  • Three.js coordinate conversion
  • Collision resolution
  • Label width estimation

This created tight coupling and made the code hard to reason about.

C11 takes it back to basics: build a clean, pure layout engine that:

  1. Works in virtual grid units (not pixels or Three.js coordinates)
  2. Supports recursive nesting (groups of groups)
  3. Calculates size inside-out (deepest items first)
  4. Has a clear separation from the rendering layer

Architecture

layout/
├── LayoutItem.js      # Base class: id, width, height, x, y, children
├── ActorItem.js       # Leaf node (1x1 grid unit)
├── GroupItem.js       # Container with bin packing
└── LayoutEngine.js    # Tree builder + computation

view/
└── ViewOrchestrator.js  # Grid → Three.js translation

Key Design Decisions

  1. Uniform actor sizes: All actors are 1x1 grid units (simplifies bin packing)
  2. Groups of groups: Multi-level nesting supported
  3. Shelf bin packing: Children arranged in rows, sorted by height
  4. Grid scale: 1 grid unit = 1.5 Three.js units (configurable)

Data Flow

actors + groupBy config
    ↓
LayoutEngine.buildTree()  → nested LayoutItems
    ↓
LayoutEngine.compute()    → inside-out size calculation
    ↓
flat positions in grid units
    ↓
ViewOrchestrator          → Three.js world coordinates
    ↓
positioned meshes & rugs

Test Battery

Seven scenarios for validating layout behavior at different scales:

ScenarioActorsNestingPurpose
Tiny31-levelBasic sanity check
Small131-levelSimple multi-group
Medium301-levelModerate scale
Nested182-levelGroups within groups
Unbalanced251-levelExtreme size variance (1 vs 15 items)
Large1001-levelPerformance stress test
Deep Nested303-levelDeep hierarchy

See shared/testScenarios.js for implementations.

Learnings

Three.js Object Removal Pattern

Problem: When switching scenarios, old actors remained visible even though actorMeshes was cleared.

Root cause: SceneManager wrapper methods (sceneManager.value.add/remove) don't always work consistently with Vue's reactivity system. Objects were being added but removal failed silently.

Solution: Use parent-based removal instead of trying to remove from a specific scene reference:

// Instead of this (unreliable):
sceneManager.value.remove(entity.group)

// Use this (reliable):
entity.group?.parent?.remove(entity.group)

This pattern works regardless of how the object was added, since it removes from whatever parent the object actually has.

SceneManager Access Pattern

When you need the actual Three.js scene:

function getThreeScene() {
  const sm = sceneManager.value
  return sm?.getScene?.() || sm?.scene || sm
}

This handles both the SceneManager wrapper and direct scene references.

Depth-Based Rug Coloring

Goal: Nested rugs should have different colors to visually distinguish hierarchy levels.

Implementation:

  1. Color scheme stored in shared/colorScheme.js with rugDepth.fills[] array
  2. getRugColorByDepth(depth, type) helper returns color by nesting level
  3. GroupRug accepts depth_level parameter and applies colors in constructor

Key insight - Root offset: The root group has depth=0 but is never rendered (skipped in ViewOrchestrator). First visible groups have item.depth=1. Subtract 1 so visible rugs start at color index 0:

const depthLevel = Math.max(0, item.depth - 1)

Nested Rug Z-Fighting

Problem: Sibling rugs at the same nesting level showed parent's color instead of their own. Console logs showed correct depth/color values, but visuals didn't match.

Root cause: All rugs were positioned at the same Y height (0.01), causing z-fighting. Three.js couldn't determine which surface to render on top.

Solution: Stack deeper rugs slightly higher and use render order:

// In GroupRug._createPlane():
this.plane.position.y = 0.01 + this.depthLevel * 0.001;
this.plane.renderOrder = this.depthLevel;

// In GroupRug._createBorder():
const y = 0.02 + this.depthLevel * 0.001;
this.border.renderOrder = this.depthLevel + 10;

Child rugs now render properly on top of parent rugs.

What's Next

Once the core layout engine is proven:

  • Apply this pattern retroactively to c9/c10
  • Add morph animations between layouts
  • Explore alternative packing strategies

Visual record

11 captures — heroes, page slices, and full-page renders. Click to view.

Skewed Regions (the default stress case)
Skewed Regions (the default stress case)
Tiny — the engine at minimum
Tiny — the engine at minimum
Medium
Medium
Nested containers
Nested containers
Deep Nested — recursion visible
Deep Nested — recursion visible
Microservices topology
Microservices topology
Enterprise scale
Enterprise scale
Massive — the packing holds
Massive — the packing holds
Fractal 3x3x3x3 — pure structure
Fractal 3x3x3x3 — pure structure
Org Chaos — the honest enterprise
Org Chaos — the honest enterprise
c11 legacy

Artifacts

/experiments/c11.grid-layout/ (local dev)

Qpoint Brand Style Guide