Skip to main content
Version: 1.0.7

3D Viewer

Renders an imported GLB model with live tag bindings — the colour, emissive, opacity and visibility of any node in the model can follow a tag. A 3D equivalent of a P&ID mimic.

The 3D Viewer component as it renders with its default settings.The 3D Viewer component as it renders with its default settings.

Reach for it when you need:

  • A 3D plant or machine view where components colour by state

Component name: Viewer3D · Category: Display

In the palette: Renders an imported 3D model (GLB) with live tag bindings: color, emissive, opacity and visibility of any node in the model can follow a tag.

Properties​

Any property can be bound to a tag, another component's property, or a query.

PropertyTypeDefaultDescription
modelasset""The 3D model (.glb), from the asset Library or the Uploads folder. Bindable — bind it to a tag/expression yielding a path (e.g. Models/Pumps/p1.glb) or an upload:// ref (e.g. a File Manager widget's {{ <name>.selectedItem.ref }}) to swap the model at runtime.
bindingsarray[]Tag→model bindings. Each row drives one property (color / emissive / opacity / visible) of one node. Bind each row's value to a tag, or the whole array to a Named Query.
  ↳ bindings.targetmodelNode""Node/mesh inside the model, by name or index path (e.g. 0/2/1). Pick from the model's scene tree (listed from the import manifest, then from the loaded scene), or click a mesh in the canvas.
  ↳ bindings.propertyenumcolorWhich property of the target the value drives. Options: color · emissive · state · opacity · visible · position · rotation · scale · morph.
  ↳ bindings.valueany""The driving value — bind to {{tag.<path>.value}}. Color: CSS color or 0xRRGGBB · state: normal/warning/alarm/offline (or 0-3, or a fault boolean) · opacity & morph: 0–1 (or a 0–100 percent tag) · visible: boolean/0/1 · position/rotation: [x,y,z] OFFSET from the model's rest pose (rotation in degrees) · scale: a factor, [x,y,z] or a single number.
  ↳ bindings.channelstring""Only for property = morph: the morph target's name (see the output.morphTargets output). Leave blank otherwise.
  ↳ bindings.enabledbooleantrue—
labelsarray[]Text overlays pinned to nodes — a live tag readout with a quality badge. Each row binds its own text + quality.
  ↳ labels.targetmodelNode""Node the label is pinned to. Pick from the scene tree or click a mesh.
  ↳ labels.textany""The text shown. Bind to a tag value ({{tag.<path>.value}}) for a live readout; a scalar (string/number) renders as-is.
  ↳ labels.qualityany""Drives the badge dot colour. Bind to {{tag.<path>.quality}} — good / bad / uncertain / stale (or an OPC-UA code). Blank = good.
  ↳ labels.enabledbooleantrue—
fitOnLoadbooleantrueFrame the model automatically when no camera has been set.
backgroundcolor""Canvas background color. Blank = transparent (the view shows through).
lightingobject{"preset":"studio","intensity":1,"ambient":1,"direction":[5,… (truncated)Light rig, how strong it is, and where the key light comes from. Set the direction with the edit-mode 'Set light' tool (aim the view, then Save) or type it. Every field is bindable.
  ↳ lighting.presetenumstudioBuilt-in light rig (self-hosted — no external environment downloads). Options: studio · basic · none.
  ↳ lighting.intensitynumber1Brightness of the main/key light. 1 = the preset's own value; raise it for a model that reads too dark. (three.js has no hard max — this is just the slider range; bind a tag for any value.)
  ↳ lighting.ambientnumber1Fill/ambient light — lifts the shadowed side. 1 = the preset value; 0 = only the key light (hard, high-contrast).
  ↳ lighting.directionarray[5,10,7]Where the key light shines FROM, as [x, y, z] in model space — the sun's position, not a rotation. [5, 10, 7] is over the model's right shoulder. Unlike the camera this HAS a real default, so its three slots arrive filled. Use the edit-mode 'Set light' tool to drag it into place instead of typing it.
interactivebooleantrueAllow orbit/pan/zoom in LiveView (the editor always locks the camera; use Set camera to frame it).
cameraobject{}Input: the AUTHORED default camera pose — where the camera sits (position), what it looks at (target) and how wide the lens is (fov). It is EMPTY by default, which means 'frame the model automatically'; the moment a position is filled in, that framing replaces the auto-fit. Easiest way to set it: enter the widget, click 'Set camera', aim the view, Save — that writes every field at once. (For the LIVE pose an operator orbits to at runtime, read the output.cameraView output instead.)
  ↳ camera.positionarray—Where the CAMERA sits — three numbers [x, y, z] in the model's own units (Y is up). Not an offset: it is an absolute point in the model's space, so [4, 3, 6] means 4 right, 3 up, 6 towards the viewer from the model origin. Leave it EMPTY to keep the automatic framing (fitOnLoad) — filling it in is exactly what turns the auto-fit off. Easiest way to set it: enter the widget, aim the view with 'Set camera', Save.
  ↳ camera.targetarray—The point the camera LOOKS AT — [x, y, z] in model units (three's lookAt point). It is also the pivot an operator's orbit spins around and the centre panning moves, so put it on the equipment you care about. EMPTY means wherever the automatic framing aims, which is the model's centre; [0, 0, 0] is the model ORIGIN, and for an off-centre model those are not the same place. Change it at runtime (an event action, a bound tag) and the camera FLIES to the new point rather than cutting, unless orbit.smooth is off.
  ↳ camera.fovnumber—Field of view — how WIDE the lens is, as the vertical angle it covers, in degrees. It changes perspective, not position: a narrow angle flattens the model and shows less of the scene from the same spot, a wide one exaggerates depth and fits more in. Empty keeps the scene's own 45° normal lens; 20-30 reads flat and schematic (kind to equipment drawings); 80-120 is a dramatic wide angle that bends straight pipe runs. Ignored when look.projection is orthographic (that view has no perspective to widen).
  ↳ camera.zoomnumber—Magnification for an ORTHOGRAPHIC view only (look.projection = orthographic), where fov has no meaning — higher fills more of the box. Empty (or 0) = fit the model automatically, which is almost always what you want; 'Set camera' fills in the real value when you frame one by hand. 1.0.6
orbitobject{"minDistance":0,"maxDistance":0,"enableZoom":true,"enablePa… (truncated)How the camera may be moved and how that movement FEELS: which gestures operators get (zoom / pan / rotate), how far they may zoom, how smooth and how fast each gesture is, and auto-rotate.
  ↳ orbit.minDistancenumber0Closest the operator may zoom, in model units. 0 = no limit.
  ↳ orbit.maxDistancenumber0Furthest the operator may zoom out. 0 = no limit — the usual cause of 'the model vanished'.
  ↳ orbit.enableZoombooleantrueAllow zooming in LiveView.
  ↳ orbit.enablePanbooleantrueAllow panning in LiveView (right-drag / two-finger drag). Off keeps the model centred no matter what an operator drags.
  ↳ orbit.enableRotatebooleantrueAllow orbiting in LiveView (left-drag). Off pins the viewing angle — a fixed overview that an operator cannot tumble into a useless view. 1.0.6
  ↳ orbit.smoothbooleantrueEase every camera move. Covers both what an OPERATOR does — rotate, pan and zoom glide to a stop instead of snapping — and what the VIEW does: a new camera pose (a re-aimed target/lookAt from an event, a bound tag, or an edit here) flies there instead of cutting, so nobody loses track of which way round the equipment is. Any gesture cancels a flight in progress. Costs a handful of extra frames per move; the canvas is idle in between either way. 1.0.6
  ↳ orbit.smoothnessnumber0.5How long a glide or flight lasts: 0 is almost instant, 1 is a long floaty coast. 0.5 (about 300 ms) suits a control screen; raise it for a lobby display. 1.0.6
  ↳ orbit.rotateSpeednumber1How far the model turns per pixel dragged. Below 1 is finer control for close inspection; above 1 spins faster. 1.0.6
  ↳ orbit.panSpeednumber1How far the view slides per pixel dragged. 1.0.6
  ↳ orbit.zoomSpeednumber1How much one wheel notch (or pinch) moves in or out. Lower it on a big plant model where a single notch overshoots. 1.0.6
  ↳ orbit.autoRotatebooleanfalseSlowly orbit the model — a lobby/overview display. It draws continuously, so leave it off on control screens.
  ↳ orbit.autoRotateSpeednumber2Auto-rotate speed (negative reverses).
lookobject{"projection":"perspective","toneMapping":"none","exposure":… (truncated)How the model READS on screen: projection, tone mapping + exposure, shadows/ground/grid/axes, selection highlight, and the device-pixel-ratio cap.
  ↳ look.projectionenumperspectivePerspective for a realistic view; orthographic for schematic/isometric layouts where equal sizes must read as equal. Options: perspective · orthographic.
  ↳ look.toneMappingenumnoneNone keeps authored colours exactly (the HMI-faithful default). ACES/Neutral are filmic curves that dim saturated colour — pick them only for a model lit for it. Options: none · aces · neutral.
  ↳ look.exposurenumber1Renderer exposure. 1 = unchanged; raise it for a model that reads too dark.
  ↳ look.shadowsbooleanfalseCast/receive shadows. Adds a shadow-map pass per frame — worth it for depth cues on a hero model, not for a wall of small ones.
  ↳ look.groundbooleanfalseShadow-catching ground plane at the model's base (invisible except where a shadow falls).
  ↳ look.gridbooleanfalseReference grid — spatial orientation while authoring.
  ↳ look.axesbooleanfalseX/Y/Z axis helper at the origin — handy when typing transform offsets.
  ↳ look.highlightSelectionbooleantrueDraw a box around the clicked node, so a selection is visible and not only published to selectedObject.
  ↳ look.highlightColorcolor#38bdf8Selection box colour.
  ↳ look.maxDprnumber2Device-pixel-ratio ceiling — the cheapest quality/performance dial. Lower it on 4K panels and thin clients.
animationobject{"clip":"","playing":false,"speed":1,"time":0,"loop":true}Input: animation playback CONTROL — clip name, play/pause, speed, scrub time and loop. Every field is bindable. (For the list of clip names this model offers, see the output.animationClips output.)
  ↳ animation.clipstring""Name of the clip to drive (see the output.animationClips output). Blank = no animation.
  ↳ animation.playingbooleanfalsePlay/pause. Bind it to a running/stopped tag and the model animates with the equipment.
  ↳ animation.speednumber1Playback rate (negative plays backwards). Bind a speed tag for a motion-proportional indicator.
  ↳ animation.timenumber0Scrub position in seconds, honoured while PAUSED — bind a process value here to use the clip as a position indicator (e.g. valve travel).
  ↳ animation.loopbooleantrueRepeat the clip. Off holds the last frame when it finishes.
outputobject{"selectedObject":{},"cameraView":{},"sceneNodes":[],"animat… (truncated)Read-only runtime outputs the widget publishes — bind a leaf, e.g. {{ <Viewer3D>.output.selectedObject.name }}. Leaves: selectedObject { name, path }, cameraView { position, target, fov }, sceneNodes [{ name, path }], animationClips [string], morphTargets [string].
  ↳ output.selectedObjectobject{}The node last clicked ({ name, path }) — bind {{ <Viewer3D>.output.selectedObject.name }}.
  ↳ output.cameraViewobject{}The LIVE camera pose { position, target, fov }, updated when an operator's orbit/pan gesture ends. This is the read-back counterpart of the 'camera' input (which is the AUTHORED default pose you set) — bind {{ <Viewer3D>.output.cameraView.position }} to mirror the current view to a tag or another viewer.
  ↳ output.sceneNodesarray[]The model's node list [{ name, path }] (first 200) — the binding targets available in this model.
  ↳ output.animationClipsarray[]The animation clip NAMES discovered in the loaded model — the reference list for what animation.clip accepts (distinct from the animation input, which is the playback CONTROL: clip/play/speed/time/loop).
  ↳ output.morphTargetsarray[]Morph target names found in the loaded model — the values a morph row's channel accepts.
stylestyle{}Custom CSS properties

Events​

Attach event actions to these in the Event Manager.

EventLabelGroupPayload
clickClickMouseThe DOM event.
dblclickDouble clickMouseThe DOM event.
contextmenuRight clickMouseThe DOM event.
mouseenterMouse enterMouseThe DOM event.
mouseleaveMouse leaveMouseThe DOM event.
objectClickObject click3D ObjectThe picked model node, under selectedObject ({ name, path }).
objectDoubleClickObject double click3D ObjectThe picked model node, under selectedObject.
Right-click behaves differently in the Designer

contextmenu fires your configured actions at runtime only — in LiveView and in preview. On the Designer canvas the right-click is captured to open the Event Manager, so testing it there will not run your actions. Test right-click in preview.

Notes​

The camera​

Three numbers decide the view: where the camera sits (camera.position), the point it looks at (camera.target, which is also the pivot an operator orbits around) and how wide the lens is (camera.fov). Leave camera empty and the model is framed automatically; the moment a position is filled in, that framing is replaced.

Do not type them. Enter the widget, click Set camera, aim the view, then Save — the tool writes the pose you are looking at. Clicking + on the property fills the whole tuple at its default in one click, which is the fallback for typing one in by hand.

Motion is eased by default. New in 1.0.6 orbit.smooth covers both directions at once: an operator's rotate, pan and zoom glide to a stop instead of snapping, and a new pose — a target re-aimed by an event action, a bound tag or an edit here — flies there rather than cutting, so nobody loses track of which way round the equipment is. Any gesture cancels a flight in progress, and the first camera and the auto-fit still land instantly. orbit.smoothness is how long a glide lasts: 0.5 is about 300 ms and suits a control screen; raise it for a lobby display.

orbit.enableRotate is the third gesture switch, beside zoom and pan. New in 1.0.6 Off pins the viewing angle — a fixed overview an operator cannot tumble into a useless view.

Reading what it publishes​

Everything the viewer reports back — the picked object, the current camera, the model's animation clips and morph targets — lives under one output object, so a binding reads:

{{ <Viewer3D>.output.selectedObject.name }}
{{ <Viewer3D>.output.cameraView }}
{{ <Viewer3D>.output.animationClips }}
{{ <Viewer3D>.output.morphTargets }}

They were top-level props once, and three of them mirrored an authored input by name — cameraView beside camera, animationClips beside animation — so the property grid read as though the same thing were declared twice and a binding author could not tell which one to bind from. Grouping them says which side of the widget each belongs to.

Clicking one object rather than the whole viewer​

Objects in the model are addressable parts: each can carry its own objectClick and objectDoubleClick, wired in the Event Manager under its node name. A click on a multi-primitive node is walked up to the deepest ancestor you wired, so the name you see in the model tree is the name to use. → Events on a part of a widget

Performance​

Large models are the most expensive thing you can put on a screen. Check it on the actual panel hardware, not on your laptop.

See also​