Object manipulation
Object navigation changes an object’s position and orientation while the application retains control of the edit: which object is affected, when changes are accepted, and what undo restores.
The demos edit one cube at a time. Their integrations use dynamic application tags to make object interactions available while the edit is active.
Try it
Section titled “Try it”- Click an object to select it. Camera navigation can use that selection for its pivot.
- Double-click or press Enter to start editing.
- Left-drag translates; middle/right-drag rotates; the wheel moves in depth.
- With Rotatrix connected, its ball rotates the object; Shift translates it. The profile’s camera controls remain available.
- Enter accepts; Escape restores the starting pose; U undoes an accepted edit.
Native controls work independently of OpenAxis. The integration announces object interaction tags only while an edit is active.
Object adapter setup
Section titled “Object adapter setup”The object adapter connects NavigationSession to the application’s object
read and write APIs. Its context identifies an edit operation, so pending input
cannot affect a later edit, even if both edits involve the same object.
The integration supplies this adapter and an object observation callback to the
same NavigationSession that handles the camera. Application events report
native object movement and changes to the active edit.
MyOpenAxisIntegration.start() passes object_adapter=object_adapter and object_observation=object_adapter.read. Native events call native_object_changed() and context_changed().
class MyObjectAdapter: def __init__(self, app): self.app = app
def capture_context(self): return self.app.operation if self.app.alive else None
def is_current(self, context): return self.app.alive and self.app.operation is context
def read(self, context): pose = self.app.get_object_pose(context.index) return ObjectPose(t=pose.position,r=pose.rotation)
def begin_query(self, context): return MyObjectCapture(self.read(context),self.app.object_bounds(context.index))
def apply_object(self, context, desired, navigation, pivot): self.app.set_object_pose(context.index,MyObjectPose(desired.t,desired.r)) return WriteResult(True,self.read(context))
def show_pivot(self, context, point): self.app.set_pivot_marker(point, object_marker=True)MyOpenAxisIntegration has already constructed adapter, objects and
scheduler. Its constructor supplies the object adapter and observation callback
to the existing session. Native event wiring follows concurrent input
and dynamic tags:
session = new NavigationSession(client, adapter, scheduler, observation: adapter.Read, objectAdapter: objects, objectObservation: objects.Read, diagnostics: diagnostics);View full implementation: MyOpenAxisIntegration.MyOpenAxisIntegration →
objects is a MyObjectAdapter(app). The NavigationSession uses its captured edit identity
to read and write the same native object throughout a gesture:
public sealed class MyObjectAdapter(MyApplication app) : INavigationObjectAdapter{ public object? CaptureContext() => app.Alive ? app.Operation : null; public bool IsCurrent(object context) => app.Alive && ReferenceEquals(context, app.Operation); public ObjectPoseValue? Read(object context) => app.GetObject(((MyApplication.Edit)context).Index); public INavigationObjectCapture BeginQuery(object context) => new MyObjectCapture(Read(context)!.Value, app.Bounds(((MyApplication.Edit)context).Index)); public ObjectWriteResult ApplyObject(object context, ObjectPoseValue desired, NavigationState? state, Vec3? pivot) { app.SetObject(((MyApplication.Edit)context).Index, desired); return new ObjectWriteResult(true, Read(context)); } public void ShowPivot(object context, Vec3? point) => app.ShowObjectPivot(point);}The constructor passes objectAdapter(app) and an objectObservation callback to the camera session. start() connects native object changes and operation invalidation. The adapter binds the exact MyEdit instance, which the host must replace for every edit.
export class MyObjectAdapter implements NavigationObjectAdapter<MyEdit> { constructor(private app: MyApplication) {} captureContext() { return this.app.alive ? this.app.edit : undefined; } isCurrent(edit: MyEdit) { return this.app.alive && this.app.edit === edit; } read(edit: MyEdit) { return structuredClone(this.app.readObject(edit)); } beginQuery(edit: MyEdit) { return new MyObjectCapture( this.read(edit), structuredClone(this.app.objectBounds(edit)), ); } applyPose(edit: MyEdit, desired: ObjectPoseValue) { const success = this.app.writeObject(edit, desired); return { success, realizedPose: success ? this.read(edit) : undefined }; } showPivot(_edit: MyEdit, point: Vec3 | undefined) { this.app.showPivot(point, true); }}
export class MyObjectCapture { constructor( private pose: ObjectPoseValue, private bounds: Bounds, ) {} initialObservation() { return structuredClone(this.pose); } resolve(name: string) { return name === "object.pose" ? structuredClone(this.pose) : name === "object.bounds" ? structuredClone(this.bounds) : UNAVAILABLE; }}The integration passes a separate MyObjectAdapter through options.object_adapter. It captures the explicit edit target and operation generation; camera/object acknowledgement state remains independent.
struct MyObjectAdapter : NavigationObjectAdapter { MyApplication &app; explicit MyObjectAdapter(MyApplication &application) : app(application) {} NavigationContext capture_context() override { if (app.editing < 0) return {}; return MyNavigationContext{&app, app.generation, app.editing}; } bool is_current(const NavigationContext &context) override { const auto &captured = std::any_cast<const MyNavigationContext &>(context); return captured.application == &app && captured.generation == app.generation && captured.object >= 0 && captured.object == app.editing; } std::unique_ptr<NavigationCapture> begin_query(const NavigationContext &context) override { if (!is_current(context)) return {}; return std::make_unique<MyObjectCapture>(app, std::any_cast<const MyNavigationContext &>(context).object); } std::optional<Pose> read_object(const NavigationContext &context) { if (!is_current(context)) return {}; return app.meshes.at(std::any_cast<const MyNavigationContext &>(context).object).pose; } WriteResult apply_pose(const NavigationContext &context, const NavigationPose &pose, const Value &, std::optional<Vec3>) override { if (!is_current(context)) return {}; auto &target = app.meshes.at(std::any_cast<const MyNavigationContext &>(context).object); target.pose = pose; return {true, target.pose}; } void show_pivot(const NavigationContext &context, std::optional<Vec3> point) override { if (!point || is_current(context)) app.object_pivot = point; }};Each edit creates a new stable operation identity, even for the same cube. The adapter checks that identity before output can be applied. Return actual pose readback so native changes and constraints can be reconciled. See comparison precision for choosing tolerances.
Object queries
Section titled “Object queries”Rotatrix needs the object’s current pose and world-space bounds to calculate its
movement. The adapter creates a query capture from those application values;
NavigationSession asks its resolver for the requested facts.
class MyObjectCapture: def __init__(self, pose, bounds): self.pose, self.bounds = pose, bounds
def initial_object_observation(self): return self.pose
def resolve(self, name): if name == 'object.pose': return self.pose.value() if name == 'object.bounds': return bounds_value(self.bounds) return UNAVAILABLEpublic sealed class MyObjectCapture(ObjectPoseValue pose, Rect3D bounds) : INavigationObjectCapture{ public ObjectPoseValue? InitialObjectObservation() => pose; public object? Resolve(string name) => name switch { "object.pose" => new Dictionary<string, object> { ["t"] = Values.Vector(pose.Position), ["r"] = Values.Vector(pose.RotationVector) }, "object.bounds" => Values.Bounds(bounds), _ => NavigationQuery.Unavailable, };}In beginQuery above, pose and world bounds are captured for that edit. initialObservation() returns a detached pose; resolve answers object.pose or object.bounds and returns UNAVAILABLE for other names. The host must include the object transform in its bounds.
MyObjectCapture supplies the captured object pose and bounds for the original edit target. Selection bounds remain a separate camera-query fact. JSON null marks unavailable facts.
Camera queries and picking also use the updated geometry. Camera and object streams retain separate pose/correction state in the shared session.
Operation lifetime
Section titled “Operation lifetime”The application owns accept, cancel and undo. A gesture can end while the edit remains active; multiple gestures can contribute to one edit. Accept saves the starting pose for undo, cancel restores it, and shutdown cancels an unfinished edit.
MyObjectOperation contains the target index and starting pose. The native application retains this operation until accept/cancel; MyObjectAdapter only binds it for SDK work. Inspect application.py for the transaction and undo handlers.
The application’s BeginEdit() creates an edit identity and saves the starting
pose. FinishEdit() accepts or cancels it, then invokes ContextChanged so the
integration invalidates the old binding and refreshes the interaction tags.
The runnable MyApplication owns its edit transaction. On begin, create a fresh MyEdit containing the target and a detached initialPose, then emit operation. Accept records undo state; cancel restores initialPose. Both clear app.edit and emit operation. Undo is a separate native operation. Session gesture cleanup only releases navigation; it does not accept or cancel an edit. Cancel unfinished edits during host shutdown.
The C++ application stores the edit target and starting pose. begin_edit, finish_edit and undo_edit own the native transaction and increment its generation. MyObjectAdapter::capture_context() captures that generation and the edit target. Gesture end does not accept the edit; Enter/left-click accepts, Escape/right-click cancels, and U undoes.
An integration binds its application’s own transaction and target. Constraints are reported through actual pose readback; see adapter contracts.
Verify
Section titled “Verify”Alternate object edits and camera gestures, then accept or cancel while input is active. Check undo, transformed picking/bounds, and a second edit of the same cube. Old output must leave a later operation untouched.
For automated commands and coverage, use the demo READMEs: Python, C#, TypeScript, and C++.