The Scene Graph Is Pascal's Shared Language

Pascal Editor's scene graph is a data model for buildings, not merely a storage format for whatever the canvas happens to display. This chapter focuses on how a flat collection of nodes can preserve architectural ownership, how schemas keep those nodes valid as the project changes, and how the core store separates document truth from later rendering. By the end, a reader should be able to inspect a small Pascal scene and understand what its relationships mean before judging whether the layout is sensible.

Imagine a simple plan: one level, four walls, a door in one wall, and a room zone inside the perimeter. A picture can show all of that at once. The graph must say more. It must tell us which level owns the walls, which wall hosts the door, which coordinate system the door uses, and which values were authored versus derived. That extra meaning is what allows the same plan to be edited, queried, migrated, and handed to another consumer.

A flat map that still knows ownership

Pascal stores scene nodes in a flat dictionary keyed by ID, with a separate list of root node IDs. At first glance, that sounds like a spreadsheet rather than a building hierarchy. The hierarchy is still there, but it is expressed through relationships: parent IDs and child arrays connect a site to its building, a building to its levels, and each level to the architectural objects it owns.

This design separates two jobs that are often confused. The dictionary answers, “Where is the node with this identity?” The hierarchy answers, “What does this node belong to, and what belongs to it?” A direct lookup is useful when an editor selects a wall or an agent receives a wall ID. Ownership is necessary when the system decides how that wall is rendered, whether it belongs in a level summary, or what should be removed when its parent is deleted.

A common node base gives every object a shared vocabulary. It carries an object marker, an identifier, a type discriminator, an optional display name, a parent link, visibility, and metadata. The type tells consumers what kind of architectural thing they are looking at. The ID makes the thing stable enough to select, reference, update, and report. The parent link gives the node a place in the model, while metadata leaves room for additional context without redefining the common contract for every kind.

The result is a graph that is flat for storage but hierarchical for interpretation:

  • A site provides the top-level context.
  • A building groups levels and their vertical order.
  • A level owns the main story geometry and rooms.
  • A wall can host openings and attached items.
  • A zone describes a room or other spatial region.
  • A slab or ceiling describes a surface that other objects may depend on.

The list is not meant to be an exhaustive catalogue. Its value is the direction of the relationships. A wall is not just a line segment in the same global bucket as a door. The door's meaning depends on its host wall, and the wall's meaning depends on the level and building around it.

classDiagram class Site class Building class Level class Wall class Door class Window class SceneGraph { nodesById rootNodeIds } class SchemaBoundary class SceneStore class History Site "1" --> "many" Building : contains Building "1" --> "many" Level : contains Level "1" --> "many" Wall : owns Wall "1" --> "many" Door : hosts Wall "1" --> "many" Window : hosts SceneGraph --> Site : stores semantic nodes SceneGraph --> SchemaBoundary : passes through SceneStore --> SceneGraph : owns document state SceneStore --> History : records changes

A flat representation also makes serialization easier. A saved graph does not need to reproduce the entire object tree inside every parent. It can carry the nodes once and preserve the connections separately. That matters for an editor that may be loaded by a browser, a headless MCP process, or another host. Each consumer can rebuild the relationships it needs from the same IDs instead of receiving a different nested interpretation.

There is a compatibility cost. Historical scenes have not always represented children identically. In particular, older site data could contain embedded child objects where newer paths expect IDs. Pascal's load and bridge code keeps fallbacks for those shapes and migrates old data rather than assuming that every document was written by today's schema. A semantic model does not remove legacy complexity; it gives that complexity a named place to be handled.

What a wall and its door mean together

The wall-and-door pair shows why relationships are more important than a list of shapes. A wall stores its start and end in the level's two-dimensional plan space. A door is a separate node, but it is parented to the wall and carries a wall reference. Its position is local to that wall: the horizontal component measures distance along the wall rather than pretending that the door is an unrelated object positioned in world coordinates.

That local frame lets the editor reason about the opening when the wall changes. A door placed halfway along a wall can be represented as a relationship plus a parameter, not just as a point that happened to line up with the wall at one moment. The renderer can use the host wall's thickness and geometry. A query can list the wall's openings. A verifier can check that the door remains within the wall's length and height. An agent can request a door at a normalized position along the wall without calculating an entire mesh transform.

The relationship also defines valid ownership. Doors and windows live under walls, while floor items belong under a level and ceiling-attached items belong under a ceiling. These are not cosmetic tree choices. They determine which context a placement check uses, which geometry system should see the child, and what a cascade delete means. If a door is stored at the wrong level, a generic node parser may still accept its fields while the building model is semantically broken.

This is the first important distinction in Pascal: a node can be structurally well-formed without being a sensible member of a scene. The graph schema can say that a door has a position and a width. Higher-level verification must ask whether its parent is a wall, whether the wall lists it as a child, and whether the opening stays within the host's resolved dimensions. Syntax and architectural meaning are related, but they are not the same check.

Schemas are part of creation, not cleanup

Pascal uses Zod schemas for node kinds. Parsing a wall or door is therefore more than converting an untyped object into a TypeScript value. It validates the input, supplies defaults where the contract permits them, and generates a properly shaped identifier when a caller creates a new node. The repository's architecture guidance explicitly prefers parsing before insertion rather than manually assembling an object and hoping a later consumer will reject it.

That order protects every boundary after creation. The scene store can rely on the node's discriminator. The renderer can dispatch to the appropriate definition. An MCP tool can return an ID that follows the same convention as a browser-created object. A plugin can consume the shared node type rather than inventing a private representation. If invalid data is admitted early, each of those consumers must implement its own defensive interpretation, and their answers can diverge.

The schema is also where Pascal preserves a distinction between “not supplied” and “supplied with a default value.” That may sound minor, but it carries design intent. An ordinary wall whose height is absent can mean that its top follows the level plane or another resolved bound. An explicit height can instead mean that the wall is intentionally shorter, raised, or otherwise independent. Replacing every absent value with a number would erase that difference before the geometry system could interpret it.

This is why the project is cautious about defaults on meaning-bearing fields. A default is appropriate when omission really means a stable ordinary value. It is dangerous when omission selects a mode or tells the system to derive a value from context. The schema must preserve the distinction, and the consuming system must resolve it using the surrounding level, slab, support, or wall relationships.

Keeping old scenes meaningful

A saved scene is not frozen in the version of the code that created it. Pascal's load path applies compatibility work before the current graph is used. New fields can be optional or receive safe defaults. A renamed or retyped field needs an explicit migration that reads the old shape and rewrites it. Structural changes—such as changing how materials or children are represented—also belong in migration logic rather than in an assumption that old JSON will somehow resemble new JSON.

The reason is practical. A field rename without migration can silently discard the value that made an old scene look or behave correctly. A structural change without migration can leave a parent pointing at children that are no longer reachable. A missing host field can allow a node to exist in the flat dictionary while the renderer never mounts it through the parent's child list. In each case, the visible symptom may appear much later than the incompatible save, which makes the original mistake difficult to diagnose.

The loader therefore performs more than a single parse. The store's scene-setting path heals certain existing corruption, applies targeted migrations, normalizes roots, removes nodes whose parents do not exist, and removes nodes that cannot be reached from the declared roots. It also handles compatibility cases in which parent links or children arrays are incomplete. These steps turn a historical document into the current canonical shape before ordinary editing resumes.

That process has a design trade-off. Centralized migration makes old scenes easier to keep alive and lets all consumers receive a common result. It also means that the load path is a substantial part of the data contract and must be maintained as carefully as the schema definitions. A new node kind is not finished when its current parser works; it is finished when the project knows what older scenes should do when that kind or one of its fields changes.

The core store owns the document, not the picture

The core scene store holds the node dictionary and roots, but it also owns collections, scene materials, installed-plugin state, the read-only lock, history, and the dirty-node set. Mutations pass through create, update, delete, and batch actions. Zundo supplies undo and redo snapshots for document changes. Hydration is treated as a lifecycle: a loaded graph is normalized, marked for downstream work, and protected against stale asynchronous normalization replacing a newer edit.

The dirty set is best understood as a rebuild request. When a wall changes, a geometry or rendering consumer may need to recompute its mesh and related dependents. Once that work is complete, the mark should clear. It is not a second source of truth about the wall. The store even guards against marking kinds that have no dirty consumer, because a permanent queue of meaningless marks would make every frame look as if work remained.

This separation explains why the same graph can outlive a particular rendering surface. The viewer may be unmounted, the browser may switch between 3D and floor plan, or the MCP bridge may run in Node without a renderer. In all three cases the document remains a graph of semantic nodes. A rendered object, a registry entry, or a pending dirty mark is a runtime consequence of that document, not a replacement for it.

Durable persistence enters through a host boundary. Some high-level repository summaries describe scene state in terms of browser persistence, while the current core store implementation and MCP design make a more precise distinction: core owns the in-memory document and its history, while the browser application and MCP/CLI storage layers provide durable save and load behavior. That distinction matters because an in-memory undo stack and a durable project revision have different lifecycles. Treating them as identical would make it difficult to explain what survives a process restart or what another client can see.

The same distinction separates graph validity from layout validity. A scene can pass node schemas while lacking a room zone, placing an opening outside its wall, or containing a wall that blocks a stair. Pascal's higher-level verification tools exist because the graph needs domain checks in addition to shape checks. The schema protects the language; verification asks whether the sentence makes architectural sense.

From data contract to visible geometry

A semantic graph earns its value only when the relationships survive contact with an actual edit. Pascal's next architectural question is therefore not whether a wall object can be stored, but how that stored wall, its host level, its openings, and its derived constraints become a coherent 3D or floor-plan result. The answer belongs to the rendering registry, dirty processing, spatial rules, and viewer/editor boundary—not to the node dictionary alone.

References