@twinfinity/scene-visuals
v6.1.2-ci.31615-beta
Published
Managed GreasedLine and 3D visuals
Readme
@twinfinity/scene-visuals
Babylon.js drawing primitives shared by Twinfinity apps: managed GreasedLine meshes, hover
highlight, snap-preview symbols, text labels, and renderers for section geometry and plane
sections. App-agnostic — it knows Babylon and @twinfinity/geometry, not the BIM model.
@twinfinity/interaction builds its visual feedback on the first three.
- ManagedLine
- HighlightManager
- Snap previews
- Text labels
- Section geometry
- Plane sections
- Colour and orientation helpers
Positions are Vec3 ([x, y, z]) from @twinfinity/geometry; line inputs are flat coordinate
rows ([x1, y1, z1, x2, y2, z2, …], one row per polyline), which is what GreasedLine consumes.
ManagedLine
The one way to create, update and dispose a GreasedLine here — it owns the mesh lifecycle so nothing leaks.
import { ManagedLine } from '@twinfinity/scene-visuals';
const line = new ManagedLine(scene, {
name: 'distance-1',
color: Color3.Red(),
width: 4, // default 5
sizeAttenuation: true, // default true: width shrinks with distance; false = width in scene units
renderingGroupId: 0,
isPickable: false, // default false
dashCount: 10,
dashRatio: 0.5 // omit both for a solid line
});
line.setSegments([
[0, 0, 0, 1, 0, 0],
[1, 0, 0, 1, 1, 0]
]); // creates the mesh lazily on first call
line.setStyle(Color3.Blue(), 6);
line.setRenderingGroupId(2); // render on top
line.hide();
line.show();
line.dispose();setSegments replaces all polylines; call it again to move the line rather than rebuilding it.
HighlightManager
One reused ManagedLine for "the thing under the pointer". highlightSegments(rows, options?)
replaces the current highlight; clear() hides it; updateConfig changes the defaults;
setRenderGroupId moves it between groups; dispose frees the mesh. color, width and
renderingGroupId in options apply to that call only; the next call without them is back on the
defaults and on the group setRenderGroupId last set.
const highlight = new HighlightManager(scene, { defaultColor: Color3.Yellow(), defaultWidth: 3 });
highlight.highlightSegments([[0, 0, 0, 1, 0, 0]], { color: Color3.Green(), renderingGroupId: 2 });
highlight.clear();Snap previews
SnapPreview draws one screen-sized symbol at a world position: SnapPreviewStyle.Circle,
Triangle, Square, Angle90, T or X. It always renders in group 2
(SNAP_PREVIEW_RENDERING_GROUP_ID) unless renderingGroupId says otherwise, so it is never hidden
by geometry.
SnapPreviewManager is what you normally use: showSnap(position, options) moves the existing
symbol when only the position changed and rebuilds it only when the style, colour, size,
thickness or rendering group changes; showSnap(undefined, …) and clear() remove it.
const previews = new SnapPreviewManager(scene);
previews.showSnap([1, 2, 3], {
style: SnapPreviewStyle.Square,
color: Color3.Red(),
size: 20, // px, default 10
thickness: 5, // px, default 5
renderingGroupId: 2 // optional; default SNAP_PREVIEW_RENDERING_GROUP_ID
});
previews.clear();
previews.dispose();The manager takes an optional factory (position, options, scene) => SnapPreviewLike as its
second argument, so tests can run the reuse logic without creating a real preview mesh.
Text labels
import { createTextPlane, placeTextPlane } from '@twinfinity/scene-visuals';
const label = createTextPlane(scene, '2.350 m', {
fontSizePx: 40, // default 40; multi-line text splits on \n
marginPx: 10, // default 10
fontFamily: 'Arial', // default 'Arial'
textColor: '#ffffff', // default '#ffffff'
backgroundColor: 'rgba(0,0,0,0.4)', // default 'rgba(0,0,0,0.4)'
textureScale: 512, // texture px per world unit; default 512
renderingGroupId: 2 // default 0
});
placeTextPlane(label, midpoint, [0, 0.1, 0]); // position + optional offset
label.dispose();The plane is a full billboard (BILLBOARDMODE_ALL), unlit and double-sided, so it always faces
the camera. computeOrientation (below) is for labels that should not billboard.
Section geometry
renderSectionGeometry(geometry, scene, options?) draws a computed section as one GreasedLine
mesh per enabled category. There are nine categories, the keys of SectionRenderStyles, and all
are enabled by default: cutLines, visibleLines, hiddenLines, silhouettesVisible,
silhouettesHidden, featureEdges, stairArrows, outlineVisible and outlineHidden. To draw
fewer, set enabled: false on each one you don't want. The result exposes only dispose(); the
meshes are not handed back.
import { renderSectionGeometry } from '@twinfinity/scene-visuals';
const result = renderSectionGeometry(geometry, scene, {
styles: {
cutLines: { color: Color3.FromHexString('#dc2626'), width: 10 },
visibleLines: { color: Color3.Black(), width: 5 },
hiddenLines: { color: Color3.FromHexString('#6b7280'), width: 4, dashCount: 10, dashRatio: 0.5 },
featureEdges: { enabled: false }
},
namePrefix: 'drawing-view-slab',
renderingGroupId: 2,
widthScale: 1
});
result.dispose();SectionRenderOptions has exactly those four fields. Dash settings are flat on each
SectionLineStyle (dashCount, dashRatio); there is no nested dashPattern, no isPickable,
and no per-category mesh on the result. The usual update pattern is dispose-then-render:
let current: SectionRenderResult | null = null;
async function updateDrawing() {
const geo = await section.compute(products);
current?.dispose();
current = renderSectionGeometry(geo, scene, options);
}Plane sections
PlaneSectionVisual<TProduct> manages the visuals of a live plane section (PlaneSectionResult):
per-product line buckets and optional fill meshes, with colour and material caches.
setLineOptions / setFillOptions restyle, setLineRenderGroupId / setFillRenderGroupId move
them between groups, getProductVisual(product) / forEachProductVisual reach the per-product
meshes, and dispose() frees everything. Options: parent, name, plane, line
(enabled, width, renderingGroupId, color, namePrefix), fill (enabled,
renderingGroupId, color, opacity, offset, namePrefix) and productColorResolver.
A color is a PlaneSectionColorOption: a fixed Color3, a (product) => Color3 | undefined
resolver, or undefined to fall back to productColorResolver.
Colour and orientation helpers
sampleProductColorRgba(product)reads a representative colour and alpha from the product's mesh style metadata, not from Babylon materials: the first mesh whosestyle.surfaceColoris non-zero (RGBA, 0–255), else the first mesh's surface RGB with alpha 1;sampleProductColoris the RGB-only variant. Both are typicalproductColorResolverimplementations.resolveColorOption(option, product)turns aPlaneSectionColorOptioninto aColor3for one product.computeOrientation(inputs)chooses right/up/forward axes for a non-billboarded 3D label so it reads upright from the current camera. Inputs are the label's base axes, a baseline up, the camera vectors, and the previousOrientationState; theORIENTATION_CONFIGhysteresis keeps the label from flipping while the camera hovers near a threshold.
