Documentation
An early-stage IDE for building digital twin projects — import a real asset’s 3D model, wire its live telemetry (MQTT, REST APIs, weather data, or simulated sensors) through visual node graphs, drive its material/environment/animation state from that data, and lay out on-screen or in-scene dashboards around it, all inside one Next.js/react-three-fiber application.
- 1. What this application is, and where it’s headed
- 2. Digital twin readiness — what it can and can’t do today
- 3. Overall layout & navigation
- 4. The Scene tab (3D viewport)
- 5. The Material tab
- 6. The UV tab
- 7. The Animation tab & the Timeline panel
- 8. The Sensors tab
- 9. The Charts tab
- 10. Theming
- 11. Not-yet-implemented areas (feature-level)
- 12. Technology summary
- 13. Pricing
1. What this application is, and where it’s headed
Digital Twin Studio is being built as an IDE for constructing digital twin projects — a single tool where someone can bring in the 3D model of a real physical asset (a machine, a vehicle, a piece of equipment, a room), wire that model up to live data coming from the real world, and author how the digital twin should look, move, and display information in response to that data, all visually, without writing code. It’s early-stage: the pieces built so far are the authoring/visualization layer of that vision — modeling, live-data wiring, and data-driven appearance/behavior/dashboards — plus project persistence under a real account (save, reopen, per-user storage), with the remaining platform layer a real production digital-twin tool eventually needs (historical time-series data, deployment, real-time multi-user collaboration) still ahead. Section 2 below lays that out plainly.
Everything already built resolves against one live, shared THREE.js scene held in React context (SceneContext). No panel keeps its own private copy of a mesh, material, or animation clip — each tab is handed a small locator (a mesh id, a material index, a sensor id) and reads the live object straight out of that shared scene on every render. This is what lets, for example, a live sensor value wired into a material’s color, an animation’s active clip, and a dashboard chart all update in lock-step from the exact same one piece of incoming data, with no separate synchronization step anywhere.
Six of the app’s tabs have real functionality behind them today: Scene, Material, UV, Animation, Sensors, and Charts. Six more (Particles, Relations, Physics, Actuators, Analytics, Pulse) are visible in the top menu as clearly-marked “Coming Soon” placeholders — disabled, with a small padlock badge — reserved for future work but not yet implemented. Several of those names (Physics, Actuators, Analytics) map directly onto capability a full digital-twin platform needs — see Section 2.
2. Digital twin readiness — what it can and can’t do today
Read this section as a straight, working assessment against the actual goal, not a marketing pitch — where the project sits today, and what’s between here and a tool a team could use to build and run a real production digital twin.
What it already does
| Capability | Where |
|---|---|
| Bring in the real asset’s actual geometry as the twin’s visual representation | Scene tab — .gltf/.glb/.obj/.fbx import |
| Wire in live external data without writing code — a real MQTT broker, a general REST/JSON API, live weather data, or a simulated sensor for prototyping before real hardware/telemetry exists | Sensor tab, node graphs app-wide |
| Drive the twin’s visual state directly from that live data — material color/parameters, background/weather effects, which animation clip plays and how fast, chart values — all through the same wire-it-up node-graph pattern, including threshold/conditional logic | Material, Environment/Weather, Animation graphs; Conditional Router node |
| Publish data or commands back out to a broker — a real, if low-level, “twin → physical world” outbound channel | MQTT Output node (usable in any graph) |
| Show live data as a dashboard, either as a 2D screen overlay or literally attached to and tracking a specific part of the 3D asset | Charts tab, Attach |
| Hand-author custom motion for any part of the model (a valve opening, a wheel turning, a door swinging) without needing a pre-baked animation file | Timeline panel — Animate mode |
| Run several independent twinned components/assets in the same scene, each with its own animation/playback state, never interfering with each other | Per-import animation state (SceneContext) |
| Watch a sensor’s value scroll by over time within the current session | Timeline panel — Graph mode |
| Save a project to a real account and reopen it exactly as it was left — scene, graph wiring, charts, sensors, and even each viewport mode’s own camera position/zoom/angle, restored automatically | Studio File → Save / Save As, the project picker (My Projects), per-user storage on MongoDB/GridFS |
| Duplicate a selected object either as a fully independent copy, or as a live instance whose material and animation state stays mirrored across every instance sharing the same parent — even after saving and reopening the project | Scene Outliner / viewport — Ctrl+C (Duplicate), Ctrl+I (Duplicate Instance) |
What it can’t do yet
| Gap | Why it matters for a digital twin |
|---|---|
| No version history. Saving overwrites a project’s one current saved state — there’s no way to browse or roll back to an earlier version. | Real engineering artifacts are usually recoverable to a prior point in time, not just the latest save. |
| No historical data store. Sensor/timeline history is in-memory for the current session only — nothing is written anywhere durable. | Production digital twins live and die on trend analysis over real time spans (days/months), not just “what happened since I opened the tab.” |
| No real-time multi-user collaboration. Each project belongs to one account, with a per-account cap on how many projects and how much storage it can hold — there’s no way for two people to open and edit the same twin together, or share one with a teammate. | Twins are usually built and monitored by a team, not one person working alone. |
| No deployment/publish path. There’s no way to ship the finished twin as a standalone viewer or app for someone else to just open and watch — it only runs inside this editor. | The end goal of building a twin is usually to hand it to operators/stakeholders, not keep it in the authoring tool. |
| No physics/simulation engine. The Physics tab is a placeholder. | Behavior here is data-driven and visual, not physically simulated — no collision, stress, fluid, or dynamics modeling of the real asset. |
| No formal alerting. Conditional logic exists in the graphs, but there’s no built-in “notify someone when X happens” (email/SMS/webhook/push). | Monitoring a real asset usually means someone needs to be told when something’s wrong, not just see a chart change. |
| No structured actuator/command layer. Outbound MQTT publish exists at the node level, but there’s no dedicated UI for defining actuators, command acknowledgement, or safety interlocks — and the Actuators tab itself is still a placeholder. | Real control loops need more guarantees than “a wire publishes a value.” |
| No BIM/IFC import, despite a supporting library already being a project dependency — only glTF/OBJ/FBX are wired into the import pipeline today. | Facility/building-scale digital twins very often start from an IFC model, not a hand-modeled mesh. |
| No secrets management. Broker/API credentials are typed directly into node fields. | Not something to point at production infrastructure as-is. |
None of this is a dead end — it’s the honest gap between “the authoring tool works” and “a team can run a real digital twin on this,” and closing it (historical/time-series data, deployment, real-time multi-user collaboration, and eventually the Physics/Actuators/Analytics tabs already reserved in the UI) is the intended direction for this project, not a scope it’s avoiding.
3. Overall layout & navigation
Top bar
A single menu row across the top: File / Window / Edit / Help menus, the tab strip (Scene, Material, UV, Animation, Sensors, Charts, plus the six disabled placeholder tabs), and a right-hand icon row (Back, Settings — both decorative — an Info button, and a real browser-fullscreen toggle).
- Window → Theme — a hover-flyout picker between four themes: Dark, Light, Sci-Fi, and Midnight. Switching applies instantly, app-wide, via CSS custom properties.
- Info opens a small modal with live FPS, JS heap / storage usage, and a quick project census (how many objects, meshes, charts, and sensors currently exist).
Tab click behavior
| Action | Result |
|---|---|
| Click Scene | Full-screen 3D viewport, no secondary panel. |
| Plain click on any other real tab | That tab’s content takes the full screen; Scene is hidden. |
| Shift+click on any other real tab | Split view — Scene on the left, that tab’s content on the right, with a draggable divider between them. |
Material, UV, Animation, and Sensors panels stay mounted at all times in the background regardless of which tab is showing — switching tabs only toggles visibility, so each one’s own node graph, camera state, or per-item cache is never lost just from flipping back and forth.
Scene Outliner (left panel, tree view)
A collapsible tree — Scene › Layer 1 › Objects / Sensors / Actuators / Simulation — mirroring a typical 3D editor’s scene-graph panel:
- Objects lists one row per imported model, expandable to show its individual meshes underneath. Clicking a row selects that whole object (or, expanded, a specific mesh); each row has its own visibility (eye) toggle.
- Shift+click on a top-level object row, a specific mesh row, or a 3D chart row adds it to a shared multi-select group — several objects and/or meshes (even across different imported models) can then be moved/rotated/scaled together with one shared gizmo.
- Sensors lists every sensor created in the Sensor tab, live from the shared sensor registry.
- Charts lists every chart, split into 2D and 3D sub-trees, with Domain charts shown as expandable parents of their child charts.
- Actuators and Simulation are present for the tree’s shape but have no feature behind them yet.
Projects: save, limits & your dashboard
Studio access itself requires a signed-in, purchased (or admin) account — File → Save / Save As writes the current scene, graph wiring, charts, sensors, and each viewport mode’s own camera position/zoom/angle to that account’s project, so reopening it later restores the exact same working state rather than a blank scene. The project picker (My Projects) lists every project on the account and is where new ones are created and old ones deleted.
Each account is capped at 5 projects and 2GB of combined storage (model files plus other project assets, tracked server-side) — the picker and the model-import flow both warn before either limit would be hit rather than failing silently. A “My Dashboard” button on the project picker opens a personal page showing project count and storage used against those caps, per-project size and last-opened time, a login/project-open activity log, and this account’s own purchase details.
4. The Scene tab (3D viewport)
Importing models
Models are imported via a file picker supporting .gltf, .glb, .obj, and .fbx. Every import is additive — importing a second model adds it alongside the first rather than replacing it, each staggered slightly along X so they don’t land exactly on top of each other. GLTF and FBX files bring their own baked-in animation clips (THREE.AnimationClip), automatically available in the Animation tab; plain OBJ files have none.
Camera & viewport
- Perspective / Orthographic toggle.
- Preset view directions (front, back, top, etc.) via a small viewport toolbar, plus grid and axes visibility toggles.
- Orbit/zoom/pan camera controls (mouse-driven, standard orbit-camera behavior).
Selecting & transforming objects
A bottom-center floating toolbar holds the transform tools:
- Select — an independent on/off toggle. While on, clicking a mesh selects it and shows a transform gizmo; off, viewport clicks select nothing and the gizmo is hidden (existing selection is left alone either way).
- Move / Rotate / Scale — a mutually-exclusive trio choosing which gizmo mode is active. Keyboard shortcuts: E (select), M, R, S.
- Local / Global origin toggle — picks whether Rotate/Scale act along the selected object’s own current orientation (Local) or fixed world axes (Global, the default). When Local is active on a whole imported object (a parent Group with no meaningful rotation of its own), the reference orientation/origin used for the visual indicator is taken from that object’s own first child mesh instead.
- A small floating origin marker (a fixed-screen-size dot) shows exactly where the active selection’s pivot point sits — useful since an imported mesh’s pivot is often not at its visual center (e.g. a wheel’s own hub).
A single click on a mesh selects its whole parent import (so the gizmo moves the entire object together); a double-click drills down to that one specific child mesh. Shift-clicking two or more top-level objects, meshes, or 3D chart panels groups them under one shared gizmo, reparenting them into a temporary pivot group for the duration of the drag and back afterward.
Duplicating objects
A selected parent object can be duplicated two ways, from the right-click context menu or its keyboard shortcut: Duplicate (Ctrl+C) makes a fully independent copy with its own material and animation state; Duplicate Instance (Ctrl+I) makes a live-linked copy instead — editing the material or animation of any one instance immediately applies the same change to every other instance sharing that same parent, including ones added later. Both add the new object as its own entry in the Scene Outliner. Instance links are recorded on the saved project and reconnected automatically on reopen, so they survive a save/reload rather than only lasting the current session.
Effects & environment
The viewport also renders whatever the Material tab’s Environment/Weather graphs resolve to (background/HDR lighting), plus optional Rain, Snow, and Fog particle/effects driven by the Weather graph’s own output values.
“Attach” — pinning a chart panel to a mesh
From the Charts tab’s outliner, a chart (or Domain) can be armed for “Attach,” then a click in the Scene picks the reference mesh: the chart’s own HUD panel is resized and repositioned to float just above that mesh, proportional to its size, and — every single frame — re-solved against that mesh’s current world transform, so it keeps tracking a moving or animating mesh live. The sizing math measures the mesh’s own size in its local coordinate space rather than a re-measured world-space bounding box, so the panel’s size stays stable even while the mesh is rotating (an axis-aligned world box would otherwise visibly “pulse” in size as a non-symmetric shape turns).
5. The Material tab
A node-graph editor (built on React Flow) for the currently selected mesh’s material — resolved live from the shared scene, not a copy, so every edit applies immediately to the actual THREE.js material object.
Material types
Switchable at the Principled node: MeshBasicMaterial, MeshToonMaterial, MeshStandardMaterial, and MeshPhysicalMaterial — each exposing whichever parameters that THREE.js material class actually supports.
Node library
| Node | Purpose |
|---|---|
| Principled / Output | The material’s own parameters, and the fixed sink every graph resolves into. |
| Color | A flat RGB color value. |
| Image Texture | An uploaded or generated texture map. |
| Value / Slider | A single tunable number, or a live external value if something’s wired into it. |
| Math | Two-input arithmetic (add, multiply, etc.). |
| Color Ramp | Gradient lookup by a 0–1 factor. |
| Random Number Generator | Ticks on an interval, several selectable distributions, runs in the background even when this graph isn’t the one showing. |
| Sensor | References a sensor created in the Sensor tab, live. |
| Conditional Router | Switches between two inputs by a comparison. |
| Fetch Weather / API Fetch / MQTT Fetch / MQTT Output | The same live-data nodes the Sensor tab offers, usable in any graph. |
Every parameter row that supports it can be driven by a wire instead of its own typed-in value — the same “fall back to typed value, live value when connected” convention used throughout every graph in this app.
Environment & Weather sub-tabs
A small Material/Environment/Weather sub-toolbar switches the same panel between three independent graphs:
- Environment — resolves the scene’s background/HDR (an uploaded HDR, one fetched from Poly Haven, or a flat color) into a fixed Environment Output node.
- Weather — the same shell with weather-specific parameters (Rain intensity/angle/direction, Fog, and others still unwired), sharing several output fields with Environment (Show Background, Blur, Flat Ground, Exposure). Only one of Environment/Weather actually reaches the live scene at a time (an Env/Weather toggle decides which); the other keeps evaluating in the background.
Both graphs are otherwise scene-independent — built once, not rebuilt per selected mesh — and offer the identical generic node library described above, plus HDR-specific source nodes.
6. The UV tab
Shows the UV layout for the currently selected mesh’s material, over whichever texture slot is chosen — Color, Normal, Bump, Metalness, or Roughness (only the slots the current material type actually supports appear). If the selected mesh has no UVs yet, a box-projection is auto-generated. UVs are editable directly on the canvas; a “Create Texture” action can generate a blank texture and wire it straight into the Material graph’s corresponding node.
7. The Animation tab & the Timeline panel
Animation in this app has two connected halves: a node graph for picking/playing existing clips (the Animation tab), and a full keyframe authoring tool for creating brand new ones (the Timeline panel’s Animate mode, docked at the bottom of the Scene view).
Animation tab (clip graph)
Mirrors the Material tab’s Principled/Output pattern: an “Animation Clips” node lists every THREE.AnimationClip found on the currently selected imported object (from its original GLTF/FBX file, plus any hand-authored clips saved from the Timeline — see below), each with its own output socket, wired into a single “Animation Output” node with a Play/Pause button.
- Sensor → Animation node — picks a clip based on threshold bands over a numeric input (typically a live Sensor), so the active animation can switch automatically as a sensor’s value crosses a boundary — re-evaluated continuously, not just captured once.
- Animation FPS node — a speed multiplier (1 = normal, 2 = double speed, etc.) wired into Output’s own “Speed” socket, controlling THREE.AnimationAction.timeScale for whichever clip is playing. Its own number field can either be typed in directly or driven live by anything wired into its own input (a Sensor, Math, API fetch, …) — the same field shows both.
Playback state (which clip, playing/paused, speed) is tracked per imported object — each import gets its own independent THREE.AnimationMixer, so switching which object’s graph is showing never disturbs a different object’s own already-playing animation, and two different objects can genuinely animate independently and simultaneously.
Timeline panel — Animate mode (keyframe authoring)
The Timeline panel at the bottom of the Scene view has a Graph/Animate toggle. Graph mode (the default) is a live sensor-value grapher — pick sensors into tracks and watch their history scroll by. Animate mode turns the exact same transport (play, pause, step, zoom, scrub) into a real keyframe editor:
- The target object continuously follows whatever’s selected in the Scene Outliner or viewport — clicking a mesh’s parent targets the whole object, double-clicking targets that one specific mesh.
- A mesh can own any number of independently-named animations (“Walk”, “Idle”, …), created via a “+” button with an in-app name prompt — each with its own separate keyframe list, picked from a “Pick an animation” dropdown.
- Moving the playhead and clicking + Key snapshots the target’s current live position/rotation/scale as a keyframe at that time; dragging a keyframe’s diamond on the timeline retimes it; keyframes can be jumped between or deleted.
- Scrubbing between keyframes live-previews the interpolated pose (linear position/scale, spherical rotation) written straight onto the live 3D object — and every object with its own authored keyframes keeps animating simultaneously, not just whichever one is currently being edited.
- Save turns the current animation’s keyframes into a genuine THREE.AnimationClip — attached live to the object (so it immediately shows up as a clip in the Animation tab’s own graph, kept continuously in sync as you keep editing) — and also downloads the raw keyframes as a portable .json file. Upload loads a previously-downloaded file back in as a new named animation on the current target.
Timeline panel — Graph mode
Add tracks, assign one or more sensors to each with their own colors, and watch their live history scroll across a scrolling, zoomable timeline — independent of the “Play All Random” concept elsewhere (this is purely about navigating the timeline, not whether sensors are generating). Range (Start/End), Loop, Auto-scroll, and a current-frame readout round out the transport, shared with Animate mode.
8. The Sensors tab
A left sidebar lists multiple independently-named “sensors,” each with its own small node graph, styled to match the Scene Outliner’s own tree. Every sensor keeps generating in the background continuously — not just the one currently shown — since other graphs throughout the app can reference any sensor’s live value at any time.
Sensor node types
- Random Number Generator — several selectable statistical distributions, with interval/duration timing controls (regenerate every N seconds, optionally freezing after a total duration).
- Sensor — references another sensor’s live output (so sensors can build on each other).
- Fetch Weather — live readings from the Open-Meteo API (temperature and other variables) for a chosen or custom city/coordinate.
- API Fetch — a general external JSON API call on an interval, with a JSON-path selector to pull out a specific field (or every key of an object, fanned out as multiple outputs).
- MQTT Fetch / MQTT Output — subscribes to (or publishes onto) an MQTT broker over WebSockets, push-based rather than polled.
- Math, Conditional, Trigonometry, Map Range, and similar generic numeric-processing nodes.
Every sensor’s current reading is published to a shared, global registry — this is the actual source of truth a Sensor-reference node anywhere else in the app (Material, Environment, Weather, Animation graphs) reads from live.
9. The Charts tab
A screen-layout positioning tool — not a full charting library UI in itself, but a way to place data-driven boxes (“charts”) either as flat 2D overlays on the viewport, or as real objects positioned in the 3D scene.
Editing
- “+ Add Chart” drops an editable, draggable/resizable placeholder box onto a centered 1920×1080 “stage” standing in for the target screen. A Grid/Free toggle switches whether dragging/resizing snaps to a visible grid.
- A per-box action bar (Edit / Pick / Delete) appears above the selected box; Pick lets you choose which sensor(s) feed it.
- Domain charts can hold child charts, nested one level deep, rendered together as one composite panel (a child never gets its own independent 2D/3D placement — it always follows its domain’s).
Chart types
Rendered via Chart.js (bar, line, area, pie, scatter, bubble, polar, radar) for graph types, plus a dedicated Text type that shows a mapped sensor’s live value directly (just the number/text, not which sensor it came from) rather than a plotted graph.
2D vs 3D
Each top-level chart (or Domain) is independently either a flat screen overlay (2D — ChartsOverlay.js, mirrored proportionally from the same 1920×1080 stage) or a real object living in the 3D scene (3D — ChartsObjects3D.js), complete with its own position/rotation/scale, draggable with the same transform gizmo as any other selected object, and optionally set to always face the camera (billboard). A chart’s own Style panel controls background/border shape, decoration, opacity, font, and — per a recent adjustment — a font “Size Scale” slider now spanning 0–100 (previously capped at 2).
Attach
See “Attach” under the Scene tab section above — a 3D chart (or Domain) can be pinned to follow a specific mesh in the scene, live, every frame.
10. Theming
Four selectable themes — Dark, Light, Sci-Fi, and Midnight — applied instantly app-wide via CSS custom properties, chosen from Window → Theme in the top bar and persisted to local storage.
11. Not-yet-implemented areas (feature-level)
Section 2 covered the platform-level gaps toward a real digital twin (persistence, historical data, deployment, and so on). This is the narrower, feature-level list — clearly marked, not hidden or half-built — of what’s missing inside the editor itself. Each of the following top-bar tabs is a real, disabled button with a “Coming Soon” tooltip and a small padlock badge:
Within otherwise-working features, a handful of individual fields are also present in the UI but not yet wired to anything live — for example several Weather Output parameters (Rain Amount/Duration/Frequency, Effective Rainfall, Humidity, Cloud, Snow, Wind) currently have no effect on the scene beyond Rain intensity/angle/direction and Fog, which are already connected.
12. Technology summary
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (Turbopack), React 19 |
| 3D rendering | three.js, @react-three/fiber, @react-three/drei |
| Node graphs | React Flow (one independent graph instance per: Material, Environment, Weather, Animation, Sensors, and each individual sensor) |
| Charting | Chart.js |
| Live data sources | Open-Meteo weather API, arbitrary JSON REST APIs, MQTT over WebSockets |
| Model formats | .gltf / .glb, .obj, .fbx |
| State architecture | One shared React context (SceneContext) holding the live THREE.js scene graph, imported objects, chart list, and per-import animation/playback state; every panel reads/writes it directly rather than keeping its own copy |
13. Pricing
Standard Version — limited to 100 customers in our initial launch.