Skip to content

Chapter 2: Scene and Geometry Model

In Chapter 1: Project and Persistent Record Boundary, we saw how RAMAL-EBX keeps each research workspace safe and organized.

Now we will look inside a Project at the scientific model itself: the scene.

Imagine that you want to create a concrete shielding wall with a circular air channel through it. RAMAL-EBX must represent:

  • The concrete material.
  • The outer wall boundary.
  • The cylindrical channel.
  • The room, or volume, formed by those boundaries.
  • The transforms that position the geometry.
  • The PHITS settings connected to the design.
  • The editing history needed to undo changes.

This is the purpose of the Scene and Geometry Model.

The main idea

A scene is RAMAL-EBX’s editable in-memory description of a PHITS design.

A useful analogy is a digital workshop:

  • Materials are the substances.
  • Surfaces are boundaries or tools.
  • Cells are rooms or volumes.
  • Transforms position and orient objects.
  • PHITS state stores simulation settings.
  • Scene history remembers editing changes.

The model is used by many parts of RAMAL-EBX:

flowchart TD
    A[Scene State] --> B[Validation]
    A --> C[PHITS Import and Export]
    A --> D[Viewport Editing]
    A --> E[Mesh Generation]
    A --> F[Immutable Simulation Case]

The same geometry model must therefore be useful for both people and algorithms.

Geometry versus Scene

The word “geometry” means the scientific structure:

materials
surfaces
transforms
cells

The word “scene” means the geometry plus PHITS settings and editing history.

Scene_State :: struct {
    geometry:     Geometry,
    phits:        PHITS_State,
    dirty:        bool,
    undo_history: [dynamic]^Scene_State,
    redo_history: [dynamic]^Scene_State,
}

Geometry answers:

“What physical objects and regions exist?”

Scene_State answers:

“What is the complete editable design right now?”

This distinction is important. An undo snapshot should remember the design, but a frozen simulation Case should not carry unnecessary editing history.

Materials: what things are made of

A Material describes a substance used by a cell.

Material :: struct {
    id:           Material_ID,
    phits_id:     i32,
    name:         string,
    density:      f32,
    color:        [4]f32,
    composition:  string,
}

For example:

name:         Concrete
density:      -2.30
composition:  100001 0.02 8016 0.53 ...
color:        gray

The negative density follows PHITS conventions. The internal id is used by RAMAL-EBX, while phits_id is the identifier written into PHITS input.

This separation is useful:

  • RAMAL-EBX can manage its own stable identity.
  • PHITS can use the numbering it expects.

A cell refers to a material by its Material_ID:

cell.material = concrete_id

The cell does not copy the entire material. It stores a reference to the material in the geometry.

Surfaces: boundaries in space

A Surface describes a geometric boundary.

RAMAL-EBX supports several surface kinds:

Surface_Kind :: enum {
    RPP,
    SPH,
    RCC,
    TRC,
    P,
    PX,
    PY,
    PZ,
    Raw,
}

Some examples are:

  • RPP: rectangular parallelepiped.
  • SPH: sphere.
  • RCC: right circular cylinder.
  • TRC: truncated cone.
  • P: general plane.
  • PX, PY, PZ: axis-aligned planes.
  • Raw: a PHITS surface that RAMAL-EBX preserves without fully understanding.

A rectangular surface might be created like this:

surface := surface_add_rpp(
    &geometry,
    "shield",
    {0, 50, 0},
    {120, 100, 80},
)

This creates a box centered near {0, 50, 0} with the requested size.

The returned value is a Surface_ID, not the complete surface object.

surface = 1

The geometry stores the full surface internally.

Cells: volumes formed from surfaces

A surface is only a boundary. A cell gives that boundary meaning as a volume.

For example:

Inside the shield box
Outside the air channel

This creates a concrete region with a cylindrical hole.

A simple cell can be created from one surface:

cell := cell_add(
    &geometry,
    "shield_cell",
    concrete_id,
    shield_surface,
)

The resulting cell means approximately:

Inside shield_surface

A cell stores both its material and its expression:

Cell :: struct {
    id:          Cell_ID,
    material:     Material_ID,
    transform_id: i32,
    is_void:      bool,
    expression:   Cell_Expression,
}

The material says what the cell is made of. The expression says where the cell exists.

Cell expressions

A cell expression is a list of geometric conditions.

Cell_Expression_Term_Kind :: enum {
    Inside_Surface,
    Outside_Surface,
    Outside_Cell,
}

These terms support three important operations:

Term Meaning
Inside_Surface Keep points inside a surface
Outside_Surface Remove points inside a surface
Outside_Cell Remove points inside another cell

For the shielding wall with a hole:

Inside outer box
Outside cylinder

The expression can be represented as:

expression.terms = {
    {kind = .Inside_Surface,  surface = outer_box},
    {kind = .Outside_Surface, surface = air_channel},
}

The result is the part of the box that is not inside the cylinder.

This is called constructive solid geometry, or CSG.

flowchart LR
    A[Outer box] --> C[Cell expression]
    B[Inner cylinder] --> C
    C --> D[Box minus cylinder]

Nested cell complements

Cells can also exclude other cells.

Suppose one cell describes a solid shielding block and another describes an internal cavity. A third cell can use:

Inside outer region
Outside cavity cell

The Outside_Cell term creates this relationship.

Cell_Expression_Term{
    kind = .Outside_Cell,
    cell = cavity_cell,
}

This is more powerful than only subtracting surfaces. A cell can contain a complete nested expression of its own.

For example:

flowchart TD
    A[Outer cell] --> B[Inside outer surface]
    A --> C[Outside inner cell]
    C --> D[Inside cavity surface]
    C --> E[Outside smaller cell]

This allows nested complements while still keeping the model understandable.

Why cycles are rejected

A cell must not exclude itself:

Cell A excludes Cell A

That expression has no useful meaning.

Cycles are also invalid:

Cell A excludes Cell B
Cell B excludes Cell A

RAMAL-EBX checks for these relationships before accepting an expression.

if term.cell == owner_cell_id {
    return false
}

It also searches through referenced cells to detect longer cycles.

This protects later operations such as validation, point queries, and mesh generation from infinite recursion.

Transforms

A transform changes where a cell is evaluated.

For example, a cell may be defined in local coordinates but placed elsewhere in the world using a PHITS transform.

Cell :: struct {
    id:          Cell_ID,
    transform_id: i32,
    expression:  Cell_Expression,
}

When RAMAL-EBX tests a world-space point, it first converts that point into the cell’s local coordinate system.

Conceptually:

world point
    ↓ inverse transform
local point
    ↓ evaluate expression
inside or outside

Transforms are especially important when importing PHITS designs, because PHITS can describe geometry using local coordinate systems.

Scene state and editing history

The scene owns undo and redo snapshots:

Scene_State :: struct {
    geometry:     Geometry,
    phits:        PHITS_State,
    dirty:        bool,
    undo_history: [dynamic]^Scene_State,
    redo_history: [dynamic]^Scene_State,
}

When the user moves a surface:

  1. The current scene is saved as an undo snapshot.
  2. The surface is changed.
  3. The scene becomes dirty.
  4. The redo history is cleared if appropriate.
  5. The viewport displays the new geometry.

The dirty flag means the current scene differs from its saved version.

dirty = true

This is editor state. It is not part of the scientific geometry itself.

A central example: creating a shield with a channel

Let us describe the goal:

Material: Concrete
Outer surface: Box
Inner surface: Cylinder
Cell: Box minus cylinder

The high-level operation is:

sequenceDiagram
    participant User
    participant Scene
    participant Geometry
    participant Validator
    participant Viewport

    User->>Scene: Create concrete shield
    Scene->>Geometry: Add material and surfaces
    Scene->>Geometry: Build cell expression
    Geometry->>Validator: Check references and volume
    Validator-->>Scene: Expression is valid
    Scene->>Viewport: Build visible mesh

If validation succeeds, the cell can be shown, exported, and later copied into an immutable Case.

Creating the geometry step by step

First, initialize an empty geometry.

geometry_init_empty(&geometry)

This creates empty material, surface, transform, and cell arrays. It also initializes ID counters.

The geometry starts with values such as:

next material ID = 1
next surface ID  = 1
next cell ID      = 1

Next, add a material.

concrete := material_add(
    &geometry,
    "Concrete",
    -2.30,
    "100001 0.02 8016 0.53",
    {0.58, 0.62, 0.66, 1},
)

The result is a Material_ID, such as:

concrete = 1

Now add the outer box.

outer := surface_add_rpp(
    &geometry,
    "shield_outer",
    {0, 50, 0},
    {120, 100, 80},
)

Then add the cylindrical channel.

channel := surface_add_rcc(
    &geometry,
    "air_channel",
    {0, 10, 0},
    {0, 80, 0},
    12,
)

The cylinder runs along the Y direction and has radius 12.

Now build the expression.

expression := Cell_Expression{
    terms = {
        {kind = .Inside_Surface, surface = outer},
        {kind = .Outside_Surface, surface = channel},
    },
}

Finally, create the cell.

shield := cell_add_with_expression(
    &geometry,
    "concrete_shield",
    concrete,
    expression,
)

The output is a new cell ID. RAMAL-EBX now has a material-filled box with a cylindrical opening.

What validation checks

Before adding or changing a cell expression, RAMAL-EBX checks several things:

  • Every referenced surface exists.
  • Every referenced cell exists.
  • A cell does not reference itself.
  • Nested references do not form cycles.
  • A normal material cell has an inside surface.
  • The expression appears to contain a real volume.
  • The transform reference is valid.
  • The cell can be represented for export.

A simplified validation shape looks like this:

if _, ok := surface_find(geometry, term.surface); !ok {
    return false
}
if owner_cell_id != 0 && term.cell == owner_cell_id {
    return false
}

This means invalid references are rejected early, before they reach export or mesh generation.

Checking whether a point is inside a cell

Once a cell exists, RAMAL-EBX can ask:

“Is this point inside the cell?”

inside := cell_expression_contains_point(
    &geometry,
    &cell,
    {0, 50, 0},
)

For the shield example, the point might be:

{0, 50, 0}

If that point lies inside the channel, the result is false.

A point in the concrete wall might return true.

Internally, the evaluator checks every term:

Inside outer box?       yes
Outside channel?        no
Final result:           outside the cell

The expression behaves like a filter. A point must pass every condition.

Signed distance

RAMAL-EBX can also calculate a signed distance.

distance, ok := cell_expression_signed_distance(
    &geometry,
    &cell,
    point,
)

A signed distance is useful for geometry processing:

  • Negative values usually mean inside.
  • Positive values usually mean outside.
  • Values near zero are close to a boundary.

This information is used by mesh generation and section previews.

For a point near the wall boundary:

distance ≈ 0

For a point far inside the concrete:

distance < 0

For a point outside the cell:

distance > 0

Bounds and empty-volume checks

Before creating a mesh, RAMAL-EBX needs a reasonable region to sample.

It estimates bounds from the cell’s inside surfaces:

min, max, ok := cell_expression_sample_bounds(
    &geometry,
    &cell,
)

For the shield, the result is approximately the outer box bounds.

The outside cylinder term then removes part of that region.

RAMAL-EBX also checks whether the expression contains any volume at all. This catches mistakes such as:

Inside box A
Inside box B

when the boxes do not overlap.

A cell with no real volume should not be exported as a normal material region.

Diagnostics

The model provides diagnostics for cells.

Cell_Diagnostic :: struct {
    exportable:       bool,
    missing_material: bool,
    missing_surface:  bool,
    unbounded:        bool,
    empty_volume:     bool,
    outside_world:    bool,
}

A diagnostic might report:

exportable:       false
missing_surface:  true
empty_volume:     false

This tells the editor that a cell refers to a surface that no longer exists.

The same checks help the PHITS authoring pipeline described in Chapter 3: PHITS Design Authoring Pipeline.

Geometry ownership and references

The Geometry object owns the actual arrays:

Geometry :: struct {
    materials:  [dynamic]Material,
    surfaces:   [dynamic]Surface,
    transforms: [dynamic]PHITS_Transform,
    cells:      [dynamic]Cell,
}

Cells refer to materials and surfaces by IDs.

This provides a simple ownership rule:

Geometry owns the objects.
Cells store references to the objects.

For example:

Geometry.materials[0]  = Concrete
Geometry.surfaces[0]   = Outer box
Geometry.surfaces[1]   = Channel
Geometry.cells[0]      = References IDs 1 and 2

If a surface is used by a cell, RAMAL-EBX prevents accidental deletion:

if surface_used_by_cell(&geometry, id) {
    return false
}

This prevents dangling references.

Cloning a geometry

An editable scene sometimes needs a complete copy.

For example:

  • Creating an undo snapshot.
  • Preparing a temporary Case.
  • Duplicating a Design.
  • Testing a change without modifying the original.

geometry_clone creates independent arrays and copies owned strings.

copy := geometry_clone(&source_geometry)

The copy contains the same materials, surfaces, transforms, and cells, but it owns its own memory.

Changing the copy does not change the original.

This is important for immutable Cases, which are discussed in Chapter 4: Immutable Case, Job, Attempt, and Result Lifecycle.

Cloning a Scene

A scene clone copies geometry and PHITS settings, but intentionally starts with empty undo and redo history.

result.geometry = geometry_clone(&source.geometry)
result.dirty = source.dirty
result.phits.path = strings.clone(source.phits.path)

The history is not copied:

result.undo_history = make([dynamic]^Scene_State, 0)
result.redo_history = make([dynamic]^Scene_State, 0)

This is a useful design decision.

A temporary simulation Case should contain the design that will be simulated, not the editor’s entire interaction history.

Editing surfaces

A surface can often be translated or scaled directly.

surface_translate(&surface, {10, 0, 0})

For a cell, RAMAL-EBX may need to edit every surface used by its expression.

cell_translate_surfaces(
    &geometry,
    &cell,
    {10, 0, 0},
)

If the cell has a transform, the world-space movement is converted into local coordinates first.

Some expressions cannot be edited safely through simple surface manipulation:

  • Raw PHITS expressions.
  • Nested cell exclusions.
  • Raw PHITS surfaces.
  • Missing references.

The editor reports these cases instead of silently changing the wrong geometry.

Shared surfaces

Two cells may use the same surface.

For example:

Cell A: inside Box 1
Cell B: outside Box 1

Editing the shared surface would affect both cells.

RAMAL-EBX can make surfaces unique before editing:

cell_make_surfaces_unique(&geometry, &cell)

This creates copies for surfaces shared with other cells.

The result is:

Cell A -> Box 1
Cell B -> Box 1 copy

Now editing Cell A does not unexpectedly change Cell B.

Scene state and the editor

The editor keeps temporary interaction state separately from geometry.

For example, it stores:

  • Which cell is selected.
  • Which surfaces are hidden.
  • Which viewport mode is active.
  • Current gizmo operation.
  • Pending drag transformations.
  • Display preferences.
Editor_State :: struct {
    active_panel:       Editor_Panel,
    design_tab:         Editor_Design_Tab,
    viewport_display_mode: Editor_Viewport_Display_Mode,
    hidden_cells:       [256]Cell_ID,
    hidden_surfaces:    [256]Surface_ID,
}

This is not scientific geometry. It is how the user is currently looking at and editing the geometry.

That separation allows the same geometry to be used for:

  • Rendering.
  • Export.
  • Validation.
  • Case creation.

From geometry to a mesh

The geometry model is analytical: it describes shapes and expressions.

The renderer needs triangles.

RAMAL-EBX converts a cell into a CSG_Mesh:

CSG_Mesh :: struct {
    vertices: [dynamic]Render_Vertex,
    indices:  [dynamic]u32,
    bounds:   Render_Bounds,
}

The process is:

  1. Read the cell expression.
  2. Compile its surfaces and nested cells.
  3. Sample signed distances in a grid.
  4. Find where the surface crosses grid edges.
  5. Create triangles.
  6. Calculate normals and bounds.
flowchart LR
    A[Cell expression] --> B[CSG evaluator]
    B --> C[Signed-distance samples]
    C --> D[Marching Cubes]
    D --> E[Render mesh]

Simple rectangular cells may use an exact box path. More complex expressions use a sampled marching-cubes path.

This is explained further in Chapter 6: CSG Mesh and Rendering Pipeline.

Exact and sampled geometry

RAMAL-EBX uses two useful strategies.

For a simple rectangular cell:

One outer RPP
Optional rectangular cut-out
No transform

the mesh can be built directly from box faces.

For a more complex expression, RAMAL-EBX samples the signed-distance field.

mesh, ok := csg_mesh_build_cell(
    &geometry,
    &cell,
)

The output is a triangle mesh suitable for viewport rendering.

The analytical geometry remains the source of truth. The mesh is only a visual representation.

Clearing the model

Because the model owns dynamic arrays and strings, it must release them correctly.

geometry_clear(&scene.geometry)
phits_state_clear(&scene.phits)
design_history_clear(&scene.undo_history)

The clear operation:

  1. Clears each material.
  2. Clears each surface.
  3. Clears each cell expression.
  4. Deletes dynamic arrays.
  5. Resets the structure.

This prevents stale references and memory leaks when a Project or Design is closed.

A beginner’s mental model

Think of the Scene and Geometry Model as a small digital workshop:

Geometry
├── Materials: what objects are made from
├── Surfaces: boundaries in space
├── Cells: volumes built from boundaries
└── Transforms: where geometry is placed

Scene State
├── Geometry
├── PHITS settings
├── Dirty flag
└── Undo and redo history

The most important relationship is:

Materials describe substance.
Surfaces describe boundaries.
Cell expressions describe volume.
Scene state describes the editable design.

Conclusion

You learned that:

  • Geometry stores materials, surfaces, transforms, and cells.
  • Scene_State combines geometry with PHITS state and editing history.
  • Surfaces are boundaries, while cells are volumes formed from those boundaries.
  • Cell expressions support intersections, exclusions, and nested cell complements.
  • Transforms convert between local and world coordinates.
  • Validation prevents missing references, empty volumes, and cyclic cell relationships.
  • Geometry can be cloned for undo, duplication, and Case creation.
  • Analytical geometry can be converted into a renderable CSG mesh.
  • Editor state controls interaction without becoming scientific geometry.

The Scene and Geometry Model is the foundation on which RAMAL-EBX builds, validates, exports, renders, and freezes research designs.

Next, we will follow this model into the PHITS Design Authoring Pipeline.


Generated by AI Codebase Knowledge Builder