“Labels” can be a useful tool to annotate different aspects of a model. Ideally, these labels are front facing and support properly managing their display relative to the model and other labels. For example, the application could change the order of the labels or the user could change the orientation of the camera to bring the desired label(s) into view. Additionally, these labels could take many forms from a simple text label, to a graph showing information about a specific part of a model or even a spreadsheet type of display. Each label could have a leader line for clarity.
This capability is support in Visualize (HPS) using subwindows. First released in HOOPS Visualize Desktop 2026.7.0 — HOOPS Visualize Desktop Documentation
New interface options in HPS enhance control over label subwindows attached to 3D models:
Automatic Camera Tracking: Subwindows no longer require manual application updates during camera movements; position and orientation sync automatically.
Natural Z-Sorting: Subwindows can now be depth-sorted naturally alongside model geometry while retaining the ability to set priority overrides for specialized annotation displays.
The example below illustrates many of these techniques.
Adding the following code the CHPSView.cpp of the HPS MFC Sandbox divides the canvas into four high level subwindows to show different examples.
static constexpr float Pi = 3.14159265f;
// Most label windows need to override a lot of possible settings from the view or model.
// In real use a style would probably be better
static void clean_window(SegmentKey& current)
{
current.GetSubwindowControl().SetBackground(Subwindow::Background::SolidColor).SetBorder(Subwindow::Border::Inset);
current.GetMaterialMappingControl().SetWindowColor(RGBColor::White()).SetWindowContrastColor(RGBColor::Black());
current.GetCameraControl().SetProjection(Camera::Projection::Stretched);
current.GetTransformMaskControl().SetModellingMatrixEverything(true);
current.GetMaterialMappingControl().SetTextColor(RGBColor::Black());
current.GetTextAttributeControl().SetSize(24, Text::SizeUnits::Points);
}
// At least one partner is interested in labels resembling spreadsheets, so let's make something like a simple table.
static void spreadsheet(SegmentKey& parent, Point const& position, int language)
{
UTF8 strings[2][3] = { {"Stop", "Go", "Go very fast"}, {"Arrêt", "Allez", "Allez très vite"} };
RGBColor colors[3] = { {1, 0, 0}, {0, 1, 0}, {1, 1, 0} };
SegmentKey current = parent.Subsegment("");
{
current.GetSubwindowControl().SetSubwindow(position, { -135, 135, -45, 45 });
clean_window(current);
current.GetTextAttributeControl().SetAlignment(Text::Alignment::CenterLeft);
for (int i = 0; i < 3; ++i) {
current.InsertText({ 0.9f, 0.666f - i * 0.66667f, 0 }, strings[language][i]);
current.InsertLine({ -1, 0.33333f - i * 0.66667f, 0 }, { 1, 0.33333f - i * 0.66667f, 0 });
SegmentKey light = current.Subsegment();
{
light.GetSubwindowControl().SetSubwindow({ 0.777f, 1, 0.33333f - i * 0.66667f, 1 - i * 0.66667f });
light.GetMaterialMappingControl().SetWindowColor(colors[i]);
}
}
}
}
// Since we can draw arbitrary things, we can include things like graphs. Animation of them is left as an exercise...
static void graph(SegmentKey& parent, Point const& position, bool background)
{
SegmentKey current = parent.Subsegment();
current.GetSubwindowControl().SetSubwindow(position, { -200, 200, -50, 50 });
clean_window(current);
if (!background)
current.GetSubwindowControl().SetBackground(Subwindow::Background::Transparent).SetBorder(Subwindow::Border::None);
SegmentKey localized = current.Subsegment("localize");
{
localized.GetModellingMatrixControl().Scale(0.9, 0.75, 1).Translate(0, -0.25, 0);
SegmentKey axis = localized.Subsegment("axis");
{
axis.GetMaterialMappingControl().SetLineColor(RGBColor::Black());
axis.InsertLine({ -1, 0, 1 }, { 1, 0, 1 });
axis.InsertLine({ -1, -1, 1 }, { -1, 1, 1 });
}
SegmentKey graph = localized.Subsegment("graph");
{
graph.GetMaterialMappingControl().SetLineColor(background ? RGBColor(0, 0.6, 0) : RGBColor(0.6, 0, 0));
graph.GetLineAttributeControl().SetWeight(2);
PointArray points;
points.reserve(100);
for (int i = 0; i < 100; ++i) {
float const x = -1 + 0.02f * i;
float y = cos(0.1f * i * Pi);
if (background)
y *= sin((0.012f * i * Pi + 1));
else
y *= exp(-0.025f * i);
float const z = 0;
points.emplace_back(x, y, z);
}
graph.InsertLine(points);
}
}
current.InsertText(Point(0, 0.5, 0), "Graph");
}
// Of course, simple text labels are possible too, without some of the depth-fighting issues of text/backgrounds
static void label(SegmentKey& parent, Point const& position, char const* label)
{
SegmentKey current = parent.Subsegment();
{
current.GetSubwindowControl().SetSubwindow(position, { -30, 30, -30, 30 });
clean_window(current);
current.GetSubwindowControl().SetBorder(Subwindow::Border::None);
current.GetMaterialMappingControl().SetTextColor(RGBColor(1, 0, 0));
current.InsertText(Point::Origin(), label);
}
}
// Simple "model" combining several label types
static void model(SegmentKey& current)
{
SegmentKey model = current.Subsegment("model");
{
spreadsheet(model, { 0.1f, 0.05f, -0.5f }, 0);
spreadsheet(model, { -0.1f, -0.05f, 0.5f }, 1);
graph(model, { -0.5f, 0.5f, 0 }, true);
graph(model, { 0.5f, -0.5f, 0 }, false);
label(model, { 0.8f, 0, 0 }, "X");
label(model, { 0, 0.8f, 0 }, "Y");
}
}
// Multi-text label format for some more examples
static void one_label(SegmentKey& current, Point const& position, IntRectangle const& offsets, char const* label)
{
current.GetSubwindowControl().SetSubwindow(position, offsets);
clean_window(current);
current.GetTextAttributeControl().SetAlignment(Text::Alignment::BottomCenter);
current.InsertText({ 0, 0, 0 }, label);
SegmentKey offsets_segment = current.Subsegment();
{
char offsets_label[256];
sprintf_s(offsets_label, 256, "(%d,%d,%d,%d)", offsets.left, offsets.right, offsets.bottom, offsets.top);
offsets_segment.GetTextAttributeControl().SetSize(12, Text::SizeUnits::Points);
offsets_segment.InsertText({ 0, -0.95, 0 }, offsets_label);
}
}
// Set of labels showing that they can be "aligned"
static void wheel(SegmentKey& parent)
{
SegmentKey wheel = parent.Subsegment("wheel");
{
wheel.GetMaterialMappingControl().SetGeometryColor(RGBColor(1, 0, 0));
// HC_Set_Marker_Symbol("(*)");
wheel.InsertLine({ -1, 0, 0 }, { 1, 0, 0 });
wheel.InsertLine({ 0, -1, 0 }, { 0, 1, 0 });
for (int i = 0; i < 9; ++i) {
float const angle = i * Pi / 4;
float x = 0.5f * cos(angle);
float y = 0.5f * sin(angle);
float const z = 0;
if (i == 8)
x = y = 0;
wheel.InsertMarker({ x, y, 0 });
char buffer[64];
sprintf_s(buffer, 64, "%d", i);
SegmentKey current = wheel.Subsegment(buffer);
{
int l, r, b, t;
if (x < -0.1f)
l = -100;
else if (x > 0.1f)
l = 0;
else
l = -50;
r = l + 100;
if (y < -0.1f)
b = -50;
else if (y > 0.1f)
b = 0;
else
b = -25;
t = b + 50;
one_label(current, { x, y, z }, { l, r, b, t }, buffer);
}
}
}
}
// similar set of labels, with text-like leader attachment
static void leaders(SegmentKey& parent)
{
SegmentKey wheel = parent.Subsegment("leader wheel");
{
for (int i = 0; i < 8; ++i) {
float const angle = i * Pi / 4;
float x = 0.5f * cos(angle);
float y = 0.5f * sin(angle);
float const z = 0;
char buffer[64];
sprintf_s(buffer, 64, "%d", i);
SegmentKey current = wheel.Subsegment(buffer);
{
int l, r, b, t;
if (x < -0.1f)
l = -100;
else if (x > 0.1f)
l = 0;
else
l = -50;
r = l + 100;
if (y < -0.1f)
b = -50;
else if (y > 0.1f)
b = 0;
else
b = -25;
t = b + 50;
SegmentKey window = current.Subsegment("window");
{
one_label(window, { x, y, z }, { l, r, b, t }, buffer);
}
SegmentKey leader = current.Subsegment("leader");
{
// a place for shapes and patterns
PortfolioKey portfolio = Database::CreatePortfolio();
leader.GetPortfolioControl().Push(portfolio);
// Shape can be defined precicely in pixels by ignoring the text box width and height completely,
auto center = []() { // provides a shorter version of this class object by value
return ShapeCoordinate(0, 0);
};
// and using the desired values as scaling on a 1-pixel margin setting.
float const ll = float(l); // Note: leader attachments are placed 1/4 of the way along each edge,
float const rr = float(r); // 1/4 of the way clockwise from the corners, just to show that they
float const bb = float(b); // can be anywhere. The leader start dots make it clear where they
float const tt = float(t); // attach (i.e. not going to some point under the window).
float const qw = 0.25f * (rr - ll); // quarter of width and height for placement
float const qh = 0.25f * (tt - bb);
ShapePointArray points = {
ShapePoint(center().SetMargins(ll), center().SetMargins(bb)),
ShapePoint(center().SetMargins(ll), center().SetMargins(tt)),
ShapePoint(center().SetMargins(rr), center().SetMargins(tt)),
ShapePoint(center().SetMargins(rr), center().SetMargins(bb)),
};
PolygonShapeElement box(points);
AnchorShapeElement a1(ShapePoint(center().SetMargins(ll), center().SetMargins(tt - qh)));
AnchorShapeElement a2(ShapePoint(center().SetMargins(rr - qw), center().SetMargins(tt)));
AnchorShapeElement a3(ShapePoint(center().SetMargins(rr), center().SetMargins(bb + qh)));
AnchorShapeElement a4(ShapePoint(center().SetMargins(ll + qw), center().SetMargins(bb)));
ShapeElementArray shape_elements = { box, a1, a2, a3, a4 };
ShapeKit mybox;
mybox.SetElements(shape_elements);
portfolio.DefineShape("mybox", mybox);
// defining the pattern
portfolio.DefineGlyph("triangle",
GlyphKit::GetDefault(Glyph::Default::SolidTriangleRight).SetOffset(GlyphPoint(127, 0)));
portfolio.DefineGlyph("dot",
GlyphKit::GetDefault(Glyph::Default::SolidCircle));
GlyphLinePatternElement dot_pattern;
dot_pattern.SetSource("dot");
dot_pattern.SetSize(9, LinePattern::SizeUnits::Pixels);
GlyphLinePatternElement triangle_pattern;
triangle_pattern.SetSource("triangle");
LinePatternParallelKit line_pattern;
line_pattern.SetStartCap(dot_pattern).SetEndCap(triangle_pattern);
LinePatternKit arrow = LinePatternKit::GetDefault(LinePattern::Default::Solid);
arrow.SetParallel(line_pattern);
portfolio.DefineLinePattern("leader_line_arrow", arrow);
// use the things we defined
leader.GetTextAttributeControl().SetBackground("mybox").SetBackgroundMargins(1, Text::MarginUnits::Pixels);
// don't actually draw the background, just use it for leaders
leader.GetVisibilityControl().SetFaces(false).SetEdges(false);
leader.GetLineAttributeControl().SetPattern("leader_line_arrow");
TextKey text = leader.InsertText({ x, y, z }, "");
text.SetLeaderLine({ 0.2f * x, 0.2f * y, z });
}
}
}
}
}
void CHPSView::OnUserCode4()
{
HPS::WindowKey window = _canvas.GetWindowKey();
window.GetMaterialMappingControl().SetWindowColor(RGBColor(0.8, 0.8, 0.8)).SetWindowContrastColor(RGBColor(0.5, 0.5, 1));
window.GetSubwindowControl().SetBackground(Subwindow::Background::GradientTopToBottom);
window.GetDrawingAttributeControl().SetWorldHandedness(Drawing::Handedness::Left);
window.GetTextAttributeControl().SetFont("Arial").SetAlignment(Text::Alignment::Center);
HPS::SegmentKey modelKey = _canvas.GetFrontView().GetAttachedModel().GetSegmentKey();
modelKey.GetVisibilityControl().SetLines(true).SetMarkers(true);
SegmentKey current;
SubwindowKit swk;
current = modelKey.Subsegment("initial");
HPS::Rectangle rect1 = { -1, 0, 0, 1 };
current.ShowSubwindow(swk);
swk.SetSubwindowSorting();
swk.SetSubwindow(rect1);
current.SetSubwindow(swk);
model(current);
current = modelKey.Subsegment("orbit");
HPS::Rectangle rect2 = { 0, 1, 0, 1 };
current.ShowSubwindow(swk);
swk.SetSubwindowSorting();
swk.SetSubwindow(rect2);
current.SetSubwindow(swk);
model(current);
current.GetCameraControl().Orbit(180, 0); // spreadsheets will reorder when seen from the back
current = modelKey.Subsegment("wheel");
HPS::Rectangle rect3 = { 0, 1, -1, 0 };
current.ShowSubwindow(swk);
swk.SetSubwindowSorting();
swk.SetSubwindow(rect3);
current.SetSubwindow(swk);
wheel(current);
current = modelKey.Subsegment("leaders");
HPS::Rectangle rect4 = { -1, 0, -1, 0 };
current.ShowSubwindow(swk);
swk.SetSubwindowSorting();
swk.SetSubwindow(rect4);
current.SetSubwindow(swk);
leaders(current);
_canvas.Update();
}
