Viewport3D
Headless 3D spatial editing model. Provides objects, transforms (Euler angles), parent-child hierarchy, and spatial queries — without any renderer, camera, or projection.
Overview
flowchart TD
subgraph V3D[Viewport3D]
SCENE[Scene<br/>objects + next_id]
OBJ[Object3D<br/>id + transform + parent]
TF[Transform3D<br/>translation + rotation + scale]
V3[Vec3<br/>x + y + z]
end
V3D -->|operations| OPS[Viewport3DOperation]
OPS -->|apply| OUT[Viewport3DOutcome]
Key Types
| Type |
Fields |
Purpose |
Scene |
objects: HashMap<ObjectId3D, Object3D>, next_id: u64 |
Container owning all 3D objects |
Object3D |
id, transform: Transform3D, parent: Option<ObjectId3D> |
3D spatial entity with optional parent |
Transform3D |
translation: Vec3, rotation: Vec3, scale: Vec3 |
Decomposed 3D transform |
Vec3 |
x: f64, y: f64, z: f64 |
3D vector |
ObjectId3D |
u64 (opaque) |
Stable, opaque identifier |
Viewport3DError |
ObjectNotFound, CannotBeOwnParent, ParentNotFound |
Error type |
Viewport3DOperation |
8 variants |
Value-type operation intent |
Viewport3DOutcome |
8 variants |
Result with undo data |
Scene Methods
Object management
| Method |
Returns |
Description |
create_object(transform) |
ObjectId3D |
Create a new root object |
insert_object(object) |
() |
Restore an object with its original id (for undo) |
remove_object(id) |
Result<Object3D, Viewport3DError> |
Remove an object |
clear() |
() |
Remove all objects (id counter preserved) |
Access
| Method |
Returns |
Description |
get(id) |
Option<&Object3D> |
Read access |
get_mut(id) |
Option<&mut Object3D> |
Mutable access |
contains(id) |
bool |
Existence check |
len() |
usize |
Object count |
is_empty() |
bool |
Whether scene is empty |
iter() |
impl Iterator<Item = &Object3D> |
Iterate all objects |
| Method |
Returns |
Description |
set_transform(id, transform) |
Result<Transform3D, Viewport3DError> |
Replace whole transform, returns previous |
set_translation(id, translation) |
Result<Transform3D, Viewport3DError> |
Set translation only, preserve rotation/scale |
set_rotation(id, rotation) |
Result<Transform3D, Viewport3DError> |
Set rotation only, preserve translation/scale |
set_scale(id, scale) |
Result<Transform3D, Viewport3DError> |
Set scale only, preserve translation/rotation |
Hierarchy
| Method |
Returns |
Description |
set_parent(id, parent) |
Result<(), Viewport3DError> |
Set parent (None = root), rejects cycles |
world_transform(id) |
Result<Transform3D, Viewport3DError> |
Compose transforms up the parent chain |
children(id) |
Vec<&Object3D> |
Direct children of an object |
root_objects() |
Vec<&Object3D> |
All root objects (parent == None) |
Spatial queries
| Method |
Returns |
Description |
objects_in_bounds(min, max) |
Vec<&Object3D> |
Objects whose translation falls in 3D bounds |
Parent-Child Hierarchy
Objects can have a parent for transform inheritance:
flowchart TD
ROOT["Root Object<br/>translation: (10, 0, 0)"]
ROOT --> CHILD["Child Object<br/>translation: (5, 0, 0)"]
CHILD --> GRANDCHILD["Grandchild Object<br/>translation: (1, 0, 0)"]
set_parent(id, parent) establishes the hierarchy. It rejects:
- Self-parenting:
id == parent → CannotBeOwnParent
- Cycles:
parent is a descendant of id → CannotBeOwnParent
- Missing parent:
parent doesn't exist → ParentNotFound
world_transform(id) composes transforms by walking up the parent chain:
let world = scene.world_transform(grandchild)?;
// world.translation = root.translation + child.translation + grandchild.translation
// = (10, 0, 0) + (5, 0, 0) + (1, 0, 0) = (16, 0, 0)
Known limitation
world_transform currently composes translations only. Proper 3D transform composition requires matrix multiplication (including rotation and scale), which is rendering-adjacent and intentionally excluded. A Domain Engine that needs full 3D transform composition implements it externally.
Vec3 Math
| Method |
Returns |
Description |
dot(other) |
f64 |
Dot product |
cross(other) |
Vec3 |
Cross product |
length() |
f64 |
Euclidean magnitude |
normalised() |
Vec3 |
Unit vector (zero if length is zero) |
lerp(other, t) |
Vec3 |
Linear interpolation (t=0 = self, t=1 = other) |
distance_to(other) |
f64 |
Euclidean distance to other |
Operators: Add, Sub, Mul<f64>, AddAssign, SubAssign, MulAssign.
| Method |
Description |
identity() |
No translation, no rotation, unit scale |
from_translation(v) |
Translation only |
from_rotation(v) |
Rotation only (Euler angles: pitch, yaw, roll in radians) |
from_scale(v) |
Scale only |
with_translation(v) |
Builder: set translation |
with_rotation(v) |
Builder: set rotation |
with_scale(v) |
Builder: set scale |
Rotation is stored as Euler angles (Vec3 of pitch, yaw, roll in radians). The foundation does not provide quaternion support. A Domain Engine that needs quaternion interpolation (to avoid gimbal lock) adds it externally.
Operations
| Operation |
Outcome |
Undo |
Create { transform } |
Created { id } |
Remove the created object |
Move { id, translation } |
Moved { previous } |
Restore previous transform |
Rotate { id, rotation } |
Rotated { previous } |
Restore previous transform |
Scale { id, scale } |
Scaled { previous } |
Restore previous transform |
Transform { id, transform } |
Transformed { previous } |
Restore previous transform |
Remove { id } |
Removed { object } |
Re-insert via insert_object |
SetParent { id, parent } |
ParentSet { previous_parent } |
Restore previous parent |
Clear |
Cleared { count } |
(limited — no stored objects to restore) |
Example
use app_shell::domain_foundation::environments::viewport_3d::*;
let mut scene = Scene::new();
// Create hierarchy
let root = scene.create_object(Transform3D::from_translation(Vec3::new(10.0, 0.0, 0.0)));
let child = scene.create_object(Transform3D::from_translation(Vec3::new(5.0, 0.0, 0.0)));
let grandchild = scene.create_object(Transform3D::from_translation(Vec3::new(1.0, 0.0, 0.0)));
scene.set_parent(child, Some(root)).unwrap();
scene.set_parent(grandchild, Some(child)).unwrap();
// World transform composes translations
let world = scene.world_transform(grandchild).unwrap();
assert_eq!(world.translation, Vec3::new(16.0, 0.0, 0.0));
// Children and roots
assert_eq!(scene.children(root).len(), 1);
assert_eq!(scene.root_objects().len(), 1); // only root
// Cycle detection
let err = scene.set_parent(root, Some(grandchild)).unwrap_err();
assert!(matches!(err, Viewport3DError::CannotBeOwnParent { .. }));
// Spatial query
let found = scene.objects_in_bounds(
Vec3::new(-10.0, -10.0, -10.0),
Vec3::new(20.0, 10.0, 10.0),
);
assert_eq!(found.len(), 3); // all three objects
// Vec3 math
let a = Vec3::new(0.0, 0.0, 0.0);
let b = Vec3::new(3.0, 4.0, 0.0);
assert!((a.distance_to(b) - 5.0).abs() < 1e-10);
What Viewport3D Does NOT Own
| Concern |
Owner |
| Camera, projection, frustum, viewport |
UI Layer / Domain Engine |
| Rendering (mesh, material, shader, lighting) |
UI Layer |
| Quaternion rotation |
Domain Engine (if needed) |
| Full matrix transform composition |
Domain Engine |
| Bounding volume hierarchy (BVH) |
Domain Engine / External |
| Animation / skeletal transforms |
Domain Engine |
| Scene graph (beyond parent-child) |
Domain Engine |
The foundation provides spatial state (position, rotation, scale, hierarchy). The UI provides visual presentation (camera, rendering, lighting).
Next Steps