2026-03-11 12:56 PM - edited 2026-03-11 01:47 PM
According to the Archicad 29 API documentation, the methods OpeningGeometry::{GetVectorX | GetVectorY} should return the x and y base vectors of an opening's local coordinate system, while OpeningGeometry::GetExtrusionDirection should return its direction vector.
Based on these descriptions, I assumed that these vectors form a local right-handed orthonormal coordinate system whose origin is given by OpeningGeometry::GetAnchorPoint.
I ran several tests using rectangular openings placed in straight or polygonal vertical walls (API_WallTypeID::APIWtyp_Normal and API_WallTypeID::APIWtyp_Poly) with the Aligned constraint (Constraint::Aligned). In some cases, the vectors indeed form an orthonormal basis, but in many others they do not. This is unexpected, because in earlier APIs the local coordinate system was always orthonormal and could be reliably obtained from API_OpeningType::extrusionGeometryData.frame.axis{X, Y, Z} and API_OpeningType::extrusionGeometryData.frame.basePoint, where frame was an API_Plane3D. Moreover, frame.axisX was parallel with the owner wall surface under this constraint.
Since API_OpeningType is deprecated and no longer available starting with the Archicad 29 API, we now rely on the classes ACAPI::Element::{Opening | OpeningDefault | OpeningExtrusionParameters | OpeningFloorPlanParameters | OpeningGeometry} and their modifier classes to maintain compatibility with Archicad 29+.
My question is: Is the non‑orthonormality of the returned vectors intentional, or is this a bug in the current API? If intentional, what is the correct way to reconstruct a stable local coordinate system for openings?
I also experimented with Gram–Schmidt orthogonalization, but the resulting right/up axes are not guaranteed to align with the vertical owner wall of the rectangular opening, which makes the approach unsuitable for my use case.
For reference, here is the code I used to retrieve and orthonormalize the vectors:
// Cartesian<3> corresponds to 3D coordinates that ensure vector arithmetic operations
struct CoordinateSystem {
Cartesian<3> right = Cartesian<3> (1.0, 0.0, 0.0);
Cartesian<3> up = Cartesian<3> (0.0, 1.0, 0.0);
Cartesian<3> forward = Cartesian<3> (0.0, 0.0, 1.0);
};
CoordinateSystem RetrieveOpeningLocalCoordinateSystem (const ACAPI::Element::Opening& opening) const
{
using namespace ACAPI;
using namespace ACAPI::Element;
const OpeningGeometry openingGeometry = opening.GetOpeningGeometry ();
const API_Vector3D axisX = openingGeometry.GetVectorX ();
const API_Vector3D axisY = openingGeometry.GetVectorY ();
const API_Vector3D axisZ = openingGeometry.GetExtrusionDirection ();
CoordinateSystem result;
Cartesian<3>& i = result.right;
Cartesian<3>& j = result.up;
Cartesian<3>& k = result.forward;
i[0] = axisX.x;
i[1] = axisX.y;
i[2] = axisX.z;
j[0] = axisY.x;
j[1] = axisY.y;
j[2] = axisY.z;
k[0] = axisZ.x;
k[1] = axisZ.y;
k[2] = axisZ.z;
i.normalize ();
j -= (i * j) * i; // operator * denotes the dot-product of two vectors
j.normalize ();
k -= (i * k) * i;
k -= (j * k) * j;
k.normalize ();
// validating right-handedness (operator ^ denotes the cross product of two vectors)
if ((i ^ j) * k < 0.0) {
// flip one axis if needed
i = -i;
}
return result;
}
Does anyone know the intended behavior of these vectors in Archicad 29, or how to correctly derive a consistent local frame for openings?
Solved! Go to Solution.
2026-03-29 05:56 PM
Thanks for the update, Tamás. I appreciate you checking with the developer and confirming that it is a known issue. The explanation about the local‑to‑global transformation makes sense, and it is helpful to know the root cause is already identified.
I will keep an eye out for the fix. If anything changes on your side or if a temporary workaround becomes available, please let me know. In the meantime, I will adjust my workflow as best as I can around the current behavior, based on the proposed correction approach I outlined in one my previous posts.
Thanks again for following up on this.
2026-03-11 02:53 PM
Hi,
I've asked a developer to look into it.
Regards,
Tamás
2026-03-11 07:07 PM
Thanks, Tamás — I appreciate you checking with the developer. Please, keep me posted when you have any updates.
2026-03-11 07:53 PM - edited 2026-03-11 10:37 PM
To help with debugging, I have prepared a small project file together with a short animation that demonstrates several polygonal walls with openings whose local coordinate systems appear inconsistent. Alongside the file, the following minimal code snippet can be used to inspect the vectors returned by OpeningGeometry for every opening in the project.
const API_Token token = ACAPI_GetToken ();
GS::Array<API_Guid> openingGuids;
ACAPI_Element_GetElemList (API_ElemTypeID::API_OpeningID, &openingGuids);
for (const API_Guid& guid : openingGuids) {
Result<Opening> openingResult = Opening::Get ({ guid, token });
if (openingResult.IsErr ()) {
return openingResult.UnwrapErr ().kind;
}
const Opening& opening = *openingResult;
const OpeningGeometry openingGeometry = opening.GetOpeningGeometry ();
const API_Vector3D axisX = openingGeometry.GetVectorX ();
const API_Vector3D axisY = openingGeometry.GetVectorY ();
const API_Vector3D axisZ = openingGeometry.GetExtrusionDirection ();
const API_Coord3D anchorPoint = openingGeometry.GetAnchorPoint ();
// ...
// e.g., for the opening associated with {C39194AE-0419-43ED-9ADB-C8BD87B81FF2},
// one should obtain the vectors
//
// axisX {x=3.8654745331095803 y=40.173165234921683 z=0.0000000000000000 }
// axisY {x=3.9493251449335793 y=41.169643571312768 z=1.0000000000000000 }
// axisZ {x=2.9528468085424957 y=41.253494183136766 z=0.0000000000000000 }
//
// which do not form an orthonormal coordinate system...
}Thank you for taking the time to look into this. I appreciate the effort, and I would be grateful for any updates when available.
2026-03-12 12:28 PM - edited 2026-03-12 06:31 PM
Concerning the local coordinate system of openings, I would like to propose a correction to the Archicad 29 API, based on the technical details and source code presented below.
The function RetrieveOpeningLocalCoordinateSystem constructs a stable, right‑handed orthonormal local coordinate system for openings placed in different types of walls. At this stage, the correctness of the coordinate system provided by ACAPI::Element::OpeningGeometry has not been verified for other types of owner elements that can also host openings. These elements may exhibit the same geometric inconsistencies observed in walls. If that turns out to be the case, the algorithmic steps shown here — or an analogue adapted to those element types — could help guide corrections in the Archicad API (version 29 and possibly earlier).
When the opening is placed in a wall, the function refines the local right axis so that it aligns with the wall's actual geometric direction:
Currently, the up axis is forced to world‑Z under the assumption that walls are vertical, and the forward axis is reconstructed via cross product to maintain an orthonormal, right‑handed coordinate frame.
If the opening's parent element is not a wall, the function currently returns the coordinate system derived directly from the opening's own geometry without applying any correction. This separation is intentional: other structural elements capable of hosting openings have not yet been evaluated for similar issues. Should they exhibit the same inconsistencies, the correction logic used for walls may need to be extended to them as well.
For walls, this approach yields a consistent, wall‑aligned local coordinate system for openings, as illustrated by the animation below (before and after correction).
2.1 Relevant API-includes, helper type-definitions and functions
// ...
#include "ACAPI/Element/Opening/Opening.hpp"
#include "ACAPI/Element/Opening/OpeningDefault.hpp"
#include "ACAPI/Element/Opening/OpeningExtrusionParameters.hpp"
#include "ACAPI/Element/Opening/OpeningFloorPlanParameters.hpp"
#include "ACAPI/Element/Opening/OpeningGeometry.hpp"
#include "Geometry/Circle2DData.h"
#include "Geometry/CircleArc2D.hpp"
#include "OnExit.hpp"
// ...
using Cartesian2 = Geometry::Vector2d;
using Cartesian3 = Geometry::Vector3D;
using Point2 = Geometry::Point2<double>;
using Circle2 = Geometry::Circle2D;
using CircleArc2 = Geometry::CircleArc2D;
struct CoordinateSystem {
// center of the coordinate system...
Cartesian3 origin;
// unit orthonormal axes of the right-handed coordinate system...
Cartesian3 right = Cartesian3 (1.0, 0.0, 0.0);
Cartesian3 forward = Cartesian3 (0.0, 1.0, 0.0);
Cartesian3 up = Cartesian3 (0.0, 0.0, 1.0);
};
Cartesian2 ConvertAPICoordToCartesian (const API_Coord& coordinate)
{
return {coordinate.x, coordinate.y};
}
Cartesian3 ConvertAPICoordToCartesian (const API_Coord3D& coordinate)
{
return {coordinate.x, coordinate.y, coordinate.z};
}
Point2 ConvertCartesianToPoint (const Cartesian2& cartesian)
{
return {cartesian.x, cartesian.y};
}
Cartesian2 ConvertPointToCartesian (const Point2& point)
{
return {point.x, point.y};
}
Cartesian2 ProjectOrthogonallyAlongZAxis (const Cartesian3& cartesian)
{
return {cartesian.x, cartesian.y};
}
GS::ErrCode CreateArrow (const API_Coord& start, const API_Coord& end, short penIndex)
{
API_Element apiElement{};
apiElement.header.type = API_LineID;
GSErrCode err = ACAPI_Element_GetDefaults (&apiElement, nullptr);
if (err != NoError) {
return err;
}
API_LineType& line = apiElement.line;
line.begC = start;
line.endC = end;
line.linePen.penIndex = penIndex;
line.endArrowData.arrowPen = penIndex;
line.endArrowData.arrowType = API_ArrowID::APIArr_SlashLine15;
line.endArrowData.arrowSize = 1.0;
line.endArrowData.arrowVisibility = true;
return ACAPI_Element_Create (&apiElement, nullptr);
}
2.2 Proposed algorithm
std::optional<CoordinateSystem> RetrieveOpeningLocalCoordinateSystem (const ACAPI::Element::Opening& opening)
{
using namespace ACAPI;
using namespace ACAPI::Element;
CoordinateSystem result;
// Step 1:
// Initialize the coordinate system directly from OpeningGeometry.
// This is the raw, API-provided local coordinate system, which may be incorrect
// for certain wall configurations (as illustrated in the "before correction" image).
const OpeningGeometry openingGeometry = opening.GetOpeningGeometry ();
result.origin = ConvertAPICoordToCartesian (openingGeometry.GetAnchorPoint ());
result.right = ConvertAPICoordToCartesian (openingGeometry.GetVectorX ());
result.up = ConvertAPICoordToCartesian (openingGeometry.GetVectorY ());
result.forward = ConvertAPICoordToCartesian (openingGeometry.GetExtrusionDirection ());
// Step 2:
// Retrieve the parent element of the opening.
// Only walls have been verified so far; other owner types may require similar corrections.
Result<UniqueID> parentUniqueIdResult = opening.GetParentElement ();
if (parentUniqueIdResult.IsErr ()) {
return std::nullopt;
}
const UniqueID& parentUniqueID = *parentUniqueIdResult;
API_Element ownerElement{};
ownerElement.header.guid = GSGuid2APIGuid (parentUniqueID.GetGuid ());
GS::ErrCode err = ACAPI_Element_Get (&ownerElement);
if (err != GS::NoError) {
return std::nullopt;
}
// Step 3:
// If the opening is not hosted by a wall, return the uncorrected OpeningGeometry-based system.
// Other structural elements may exhibit the same issues as walls, but they have not yet been evaluated.
// If they do, the correction logic below may need to be extended to them.
if (ownerElement.header.type.typeID != API_ElemTypeID::API_WallID) {
return result;
}
const API_WallType& wall = ownerElement.wall;
// Step 4:
// Correct the right-axis depending on the wall type.
// This is the core of the "after correction" behavior shown in the animation.
switch (ownerElement.wall.type) {
// Step 4A:
// Normal and trapezoid walls: use the baseline direction directly.
// This produces a stable and correct right-axis for straight walls.
default:
case API_WallTypeID::APIWtyp_Normal:
case API_WallTypeID::APIWtyp_Trapez:
{
result.right.x = wall.endC.x - wall.begC.x;
result.right.y = wall.endC.y - wall.begC.y;
result.right.z = 0.0;
result.right.NormalizeVector ();
break;
}
// Step 4B:
// Polygonal walls: determine the closest wall segment or arc to the opening's anchor point.
// This corrects the right-axis for curved or segmented walls.
case API_WallTypeID::APIWtyp_Poly:
{
if (!wall.head.hasMemo || wall.poly.nCoords < 2) {
return std::nullopt;
}
API_ElementMemo wallMemo{};
GS::OnExit wallMemoDisposer ([&] () { ACAPI_DisposeElemMemoHdls (&wallMemo); });
err = ACAPI_Element_GetMemo (wall.head.guid, &wallMemo);
if (err != GS::NoError || wallMemo.coords == nullptr) {
return std::nullopt;
}
// Step 5:
// Project the opening's anchor point onto the XY plane.
// This allows comparison with the 2D wall polygon.
const Cartesian2 projectedAnchorPoint = ProjectOrthogonallyAlongZAxis (result.origin);
constexpr double infinity = std::numeric_limits<double>::max ();
double minimumDistance = infinity;
// Step 6:
// Iterate all polygon edges and find the closest segment direction.
for (GS::Int32 i = wall.poly.nCoords - 1, j = 1; j < wall.poly.nCoords; i = j++) {
const API_Coord& currentCoordinate = (*wallMemo.coords)[i];
const API_Coord& nextCoordinate = (*wallMemo.coords)[j];
const Cartesian2 start (currentCoordinate.x, currentCoordinate.y);
const Cartesian2 end (nextCoordinate.x, nextCoordinate.y);
Cartesian2 unitDirection = end;
unitDirection -= start;
unitDirection.Normalize ();
const Cartesian2 unitNormal = unitDirection.GetNormalVector ();
Cartesian2 deviationVector = projectedAnchorPoint;
deviationVector -= start;
const double coordinate = (projectedAnchorPoint - start) * unitDirection;
const double endCoordinate = (end - start) * unitDirection;
// Skip if projection falls outside the segment.
if (coordinate < 0.0 || coordinate > endCoordinate) {
continue;
}
const double distance = fabs (deviationVector * unitNormal);
// Update closest direction.
if (distance < minimumDistance) {
minimumDistance = distance;
result.right[0] = unitDirection[0];
result.right[1] = unitDirection[1];
result.right[2] = 0.0;
}
}
// Step 7:
// If the wall contains arcs, refine the direction using radial projection.
if (wall.poly.nArcs >= 1 && wallMemo.parcs != nullptr) {
double squaredMinimumDistance = (minimumDistance != infinity) ? minimumDistance * minimumDistance : infinity;
for (GS::Int32 i = 0; i < wall.poly.nArcs; ++i) {
const API_PolyArc& polyArc = (*wallMemo.parcs)[i];
const Cartesian2 start = ConvertAPICoordToCartesian ((*wallMemo.coords)[polyArc.begIndex]);
const Cartesian2 end = ConvertAPICoordToCartesian ((*wallMemo.coords)[polyArc.endIndex]);
const CircleArc2 circleArc = CircleArc2::CreateFromBeginEnd (ConvertCartesianToPoint (start), ConvertCartesianToPoint (end), polyArc.arcAngle);
const Point2 center = circleArc.GetOrigo ();
const double radius = circleArc.GetRadius ();
Cartesian2 unitRadialDirection = projectedAnchorPoint;
unitRadialDirection -= ConvertPointToCartesian (center);
unitRadialDirection.Normalize ();
const Circle2 circle (center, radius);
const GS::Array<Point2> intersections = CalcCircleLineIntersections (circle, center, unitRadialDirection);
for (const Point2& intersection : intersections) {
Cartesian2 deviationVector = projectedAnchorPoint;
deviationVector -= ConvertPointToCartesian (intersection);
const double squaredDistance = deviationVector.GetLengthSqr ();
if (squaredMinimumDistance > squaredDistance) {
squaredMinimumDistance = squaredDistance;
result.right[0] = unitRadialDirection[1];
result.right[1] = -unitRadialDirection[0];
result.right[2] = 0.0;
}
}
}
}
break;
}
}
// Step 8:
// Assume walls are vertical, i.e., force up-axis to world Z.
// This choice also works for slanted wall as well. If necessary, it can also be set based on the slant direction.
result.up[0] = 0.0;
result.up[1] = 0.0;
result.up[2] = 1.0;
// Step 9:
// Recompute forward-axis to maintain a right-handed orthonormal system.
result.forward = result.up ^ result.right;
result.forward.NormalizeVector ();
// Step 10:
// Return the corrected coordinate system.
return result;
}
2.3 Test/visualization
The function shown below is a small diagnostic tool used to visualize the corrected local coordinate systems of openings in the floor plan. It retrieves every opening in the project, computes its refined local coordinate system using RetrieveOpeningLocalCoordinateSystem (), and then creates two arrows at the opening's anchor point:
This visualization is the basis of the "before" and "after" images included in the animation above. It demonstrates how the proposed correction improves the accuracy and consistency of the local coordinate system returned by the Archicad 29 API for openings placed in walls. The tool does not modify the model; it only draws temporary arrows so that the corrected axes can be inspected directly in the plan view.
GS::ErrCode TestLocalCoordinateSystemOfOpenings ()
{
// Token required for ACAPI::Element::Opening::Get
const API_Token token = ACAPI_GetToken ();
using namespace ACAPI;
using namespace ACAPI::Element;
// Collect all openings in the project.
// These will be used to visualize their corrected local coordinate systems.
GS::Array<API_Guid> openingGuids;
GS::ErrCode err = ACAPI_Element_GetElemList (API_ElemTypeID::API_OpeningID, &openingGuids);
if (err != GS::NoError) {
return err;
}
// Pen colors for drawing the axes:
// blue: corrected right-axis (wall segment direction)
// red: corrected forward-axis (extrusion direction)
const short bluePenIndex = 6;
const short redPenIndex = 20;
// Wrap drawing operations in an undoable command.
err = ACAPI_CallUndoableCommand ("Draw local coordinate systems", [&] () -> GS::ErrCode {
for (const API_Guid& guid : openingGuids) {
// Retrieve the opening element.
Result<Opening> openingResult = Opening::Get ({guid, token});
if (openingResult.IsErr ()) {
return openingResult.UnwrapErr ().kind;
}
const Opening& opening = *openingResult;
// Compute the corrected local coordinate system.
// This is the function whose behavior is illustrated in the
// "before" and "after" animation in the forum post.
std::optional<CoordinateSystem> optionalCoordinateSystem =
RetrieveOpeningLocalCoordinateSystem (opening);
if (!optionalCoordinateSystem.has_value ()) {
return GS::Error;
}
const CoordinateSystem& coordinateSystem = *optionalCoordinateSystem;
// Extract origin and axes.
const Cartesian3& o = coordinateSystem.origin;
const Cartesian3& r = coordinateSystem.right;
const Cartesian3& f = coordinateSystem.forward;
// Compute endpoints of the drawn axes.
Cartesian3 wallSegmentDirectionEnd = o;
wallSegmentDirectionEnd += r;
Cartesian3 extrusionDirectionEnd = o;
extrusionDirectionEnd += f;
// Create the corrected right-axis (blue).
GS::ErrCode err = CreateArrow ({o[0], o[1]}, {wallSegmentDirectionEnd[0], wallSegmentDirectionEnd[1]}, bluePenIndex);
if (err != GS::NoError) {
return err;
}
// Create the corrected forward-axis (red).
err = CreateArrow ({o[0], o[1]}, {extrusionDirectionEnd[0], extrusionDirectionEnd[1]}, redPenIndex);
if (err != GS::NoError) {
return err;
}
}
return GS::NoError;
});
return err;
}
2026-03-23 08:12 AM
Hi,
Thank you for your input and examples, I'll pass it along to the developer who is looking into it now.
Regards,
Tamás
2026-03-27 04:07 PM
Hi,
A developer looked into the problem:
Its not an intended change, we already know of this bug because of another case, it's on our backlog.
The problem is with the new local to global coordinate system conversion. To my understanding the error is caused by using point based calculation instead of vector based, missing correction based on the translation of the origin.
Unfortunately they don't seem to have a workaround yet. When it gets fixed, I'll get back to you.
Regards,
Tamás
2026-03-29 05:56 PM
Thanks for the update, Tamás. I appreciate you checking with the developer and confirming that it is a known issue. The explanation about the local‑to‑global transformation makes sense, and it is helpful to know the root cause is already identified.
I will keep an eye out for the fix. If anything changes on your side or if a temporary workaround becomes available, please let me know. In the meantime, I will adjust my workflow as best as I can around the current behavior, based on the proposed correction approach I outlined in one my previous posts.
Thanks again for following up on this.