BIMIT
Разработка плагинов

calc

Интерфейс строительного калькулятора: арифметика, сравнение и округление чисел, проверки по габаритным боксам элементов, точные расчёты по триангулированной геометрии (сетка GeometryObject), пакетные проверки наборов элементов и анализ смежности стен и помещений.

Единицы измерения. Габариты элементов (GeometryElement.bounds, obb, boundSizeX/Y/Z) — в метрах. Сетки GeometryObject, полученные с сервера (bim.getGeometryObjects, bim.getGeometryObjectsMap), — в миллиметрах. Зазор gap указывается в единицах тех данных, которые передаются в метод. В методах, которые получают элементы и их сетки вместе (intersectAll, проверки смежности), gap задаётся в метрах.

Арифметика и числа

  • area - подсчет площади прямоугольной фигуры, заданной длинами сторон
  • volume - подсчет объема параллелепипеда, заданного длинами сторон (ДЛИНАxШИРИНАxВЫСОТА)
  • areaBox - подсчет площади поверхности стен прямоугольной комнаты, заданной длинами сторон и высотой
  • volumeBox - подсчет объема стен коробки, заданной длинами сторон (по внешней стороне), высотой и толщиной стен
  • compare - сравнение значений с абсолютной погрешностью
  • compareRelative - сравнение значений с относительной погрешностью
  • trunc - округление значения
  • length - расстояние между двумя точками в пространстве

Габаритные боксы элементов

  • located - вхождение одного элемента в область другого
  • intersect - пересечение боксов или сеток двух элементов
  • getElementBounds - граничный бокс по сетке или по списку вершин
  • isExtendedAABBIntersectNeighbours - соседи, попадающие в расширенный по направлению бокс элемента
  • isExtendedAABBIntersectNeighboursDistance - то же, с расстоянием до каждого соседа
  • isValveBelongToPipe - проверка принадлежности одного элемента другому

Точные расчёты по сеткам

  • distance - минимальное расстояние между двумя сетками
  • distanceFast - минимальное расстояние между двумя сетками через AABB-дерево
  • manhattanDistance - расстояние Манхэттена между двумя сетками
  • distance2D - расстояние между сетками в плоскости XY
  • distance2DXY - расстояние между сетками в плоскости XY, 0 если пересекаются
  • checkInside - находится ли одна сетка внутри другой

Пакетные методы

  • findIntersectingPairs - все пары элементов двух наборов с пересекающимися габаритными боксами
  • intersectAll - все пары элементов двух наборов, пересекающиеся по сеткам
  • distanceAll - расстояния от элемента до набора элементов

Ориентированный бокс и выпуклая оболочка

  • getOrientedBoundingBox - ориентированный минимальный граничный бокс (OBB) элемента
  • getOrientedBoundingBoxes - OBB для набора элементов одним запросом
  • getElementConvexHull - геометрия выпуклой оболочки элемента
  • createOrientedBox - построение бокса заданных размеров в заданном направлении
  • getOBBLongestAxis - индекс самой длинной стороны OBB (X=1, Y=2, Z=3)
  • getElementDirection - главная ось элемента

Стены и помещения

  • getFaceArea - площадь грани габаритного бокса элемента
  • getContactFaceName - грань одной сетки, контактирующая с другой
  • findContactFaceName - внешняя грань контакта стены с помещением
  • isExternalAdjacentElement - является ли стена внешне смежной с помещением
  • isInternalAdjacentElement - является ли стена внутренне смежной с помещением

Арифметика и числа

area - подсчет площади прямоугольной фигуры, заданной длинами сторон
  • calc.area(double length, double width) → double

Пример использования:

def area = calc.area(6, 6)

// console.info(area) -> "INFO: 36.0"
volume - подсчет объема параллелепипеда, заданного длинами сторон (ДЛИНАxШИРИНАxВЫСОТА)
  • calc.volume(double length, double width, double height) → double

Пример использования:

def vol = calc.volume(6, 0.5, 3)

// console.info(vol) -> "INFO: 9.0"
areaBox - подсчет площади поверхности стен прямоугольной комнаты, заданной длинами сторон и высотой
  • calc.areaBox(double length, double width, double height) → double

Пример использования:

def area = calc.areaBox(6, 6, 3)

// console.info(area) -> "INFO: 72.0"
volumeBox - подсчет объема стен коробки, заданной длинами сторон (по внешней стороне), высотой и толщиной стен
  • calc.volumeBox(double length, double width, double height, double widthWall) → double

Пример использования:

def vol = calc.volumeBox(6, 6, 3, 0.5)

// console.info(vol) -> "INFO: 33.0"
compare - сравнение значений с абсолютной погрешностью
  • calc.compare(double a, double b, double tolerance) → boolean - true, если |a − b| <= tolerance

Пример использования:

if (calc.compare(el.getAreaZ(), attrArea, 0.01)) {
    // площади совпадают с точностью до 0.01
}
compareRelative - сравнение значений с относительной погрешностью
  • calc.compareRelative(double a, double b, double tolerance) → boolean - true, если |a − b| / max(|a|, |b|) <= tolerance; если одно из значений равно 0, сравнение абсолютное

Пример использования:

// расхождение не больше 5 %
if (!calc.compareRelative(geometryVolume, attrVolume, 0.05)) {
    console.warn("Объём в атрибуте отличается от геометрии")
}
trunc - округление значения
  • calc.trunc(double value, int length) → double - округление до length знаков после запятой (половина — вверх)
  • calc.trunc(double value) → double - округление до 3-го знака после запятой

Пример использования:

double ratio = calc.trunc(windowAreas / spaceArea, 3)
double area = calc.trunc(1.23456)   // 1.235
length - расстояние между двумя точками в пространстве
  • calc.length(Vector3f p1, Vector3f p2) → double

Пример использования:

double lengthX = calc.length(new Vector3f(0, 0, 0), new Vector3f(3, 4, 0))   // 5.0

Габаритные боксы элементов

Методы этого раздела сравнивают только габаритные боксы (AABB) и работают быстро, но это необходимое, а не достаточное условие пересечения. Для точной проверки используйте перегрузки с сетками GeometryObject.

located - вхождение одного элемента в область другого
  • calc.located(GeometryElement space, GeometryElement obj) → boolean - бокс obj целиком внутри бокса space
  • calc.located(GeometryElement space, GeometryElement obj, double gap) → boolean - то же с допуском gap (в единицах bounds, м)

Пример использования:

if (calc.located(space, elem, 0.01)) {
    console.info(elem.getName() + " находится в помещении " + space.getName())
}
intersect - пересечение боксов или сеток двух элементов

По габаритным боксам:

  • calc.intersect(Bounds bounds1, Bounds bounds2) → boolean - пересечение двух боксов
  • calc.intersect(double gap, Bounds bounds1, Bounds bounds2) → boolean - с увеличением границ на gap
  • calc.intersect(double gapX, double gapY, double gapZ, Bounds bounds1, Bounds bounds2) → boolean - с увеличением границ отдельно по осям
  • calc.intersect(GeometryElement element1, GeometryElement element2) → boolean - по боксам элементов
  • calc.intersect(double gap, GeometryElement element1, GeometryElement element2) → boolean - по боксам элементов с зазором
  • calc.intersect(double gapX, double gapY, double gapZ, GeometryElement element1, GeometryElement element2) → boolean - по боксам элементов с зазором по осям

По сеткам (точный метод):

  • calc.intersect(GeometryObject element1, GeometryObject element2) → boolean - пересекаются ли треугольники сеток
  • calc.intersect(double gap, GeometryObject element1, GeometryObject element2) → boolean - пересекаются или находятся ближе gap (в единицах сетки: для сеток с сервера — мм)
Сначала отсейте пары по боксам, затем проверяйте сетки. Треугольники сетки строятся один раз и кешируются на время запуска плагина, поэтому повторные проверки одной и той же сетки дешевле первой. Для двух наборов элементов используйте intersectAll.

Пример использования:

if (calc.intersect(gapX, gapY, gapZ, element, slab)) {
    def meshes = bim.getGeometryObjectsMap([element.getUuid(), slab.getUuid()])
    if (calc.intersect(10.0, meshes[element.getUuid()], meshes[slab.getUuid()])) {   // 10 мм
        console.info("Элемент пересекает плиту")
    }
}
getElementBounds - граничный бокс по сетке или по списку вершин
  • calc.getElementBounds(GeometryObject geometry) → Bounds - по вершинам сетки
  • calc.getElementBounds(double vertices) → Bounds - по массиву координат x1, y1, z1, x2, y2, z2, …

Пример использования:

def bounds = mesh.getBounds() ?: calc.getElementBounds(mesh)
isExtendedAABBIntersectNeighbours - соседи, попадающие в расширенный по направлению бокс элемента
  • calc.isExtendedAABBIntersectNeighbours(GeometryElement element, Set<GeometryElement> neighbours, double dir, double length, double width, double height) → List<GeometryElement> - бокс элемента расширяется в сторону dir на length (и на width, height поперёк); возвращаются соседи, чьи боксы пересекают расширенный бокс. Соседи без bounds пропускаются.

Пример использования:

double[] dir = [1, 0, 0]
def blocking = calc.isExtendedAABBIntersectNeighbours(valve, allElements, dir, 0.6, 0.4, 0.4)
if (!blocking.isEmpty()) {
    console.warn("Зона обслуживания арматуры занята: " + blocking*.getName())
}
isExtendedAABBIntersectNeighboursDistance - соседи в расширенном боксе с расстоянием до каждого
  • calc.isExtendedAABBIntersectNeighboursDistance(GeometryElement element, List<GeometryElement> neighbours, double dir, double length, double width, double height) → List<Double> - список той же длины, что и neighbours: для соседа, попавшего в расширенный бокс, — расстояние между боксами элемента и соседа, для остальных -1.0

Пример использования:

def distances = calc.isExtendedAABBIntersectNeighboursDistance(valve, neighbours, dir, 0.6, 0.4, 0.4)
distances.eachWithIndex { d, i ->
    if (d >= 0) console.info(neighbours[i].getName() + ": " + calc.trunc(d))
}
isValveBelongToPipe - проверка принадлежности одного элемента другому
  • calc.isValveBelongToPipe(GeometryElement valve, GeometryElement pipe) → boolean - пересекается ли габаритный бокс valve с ориентированным боксом (OBB) pipe

Пример использования:

if (calc.isValveBelongToPipe(valve, pipe)) {
    console.info(valve.getName() + " установлен на " + pipe.getName())
}

Точные расчёты по сеткам

distance - минимальное расстояние между двумя сетками
  • calc.distance(GeometryObject data1, GeometryObject data2) → double - 0.0, если сетки пересекаются, иначе минимальное расстояние между треугольниками (в единицах сетки, для сеток с сервера — мм)

Треугольники сеток кешируются, а пары треугольников отсекаются по боксам. Поэтому метод обычно быстрее distanceFast. Худший случай — одна сетка внутри другой: на сетках из тысяч треугольников расчёт занимает сотни миллисекунд.

Пример использования:

def meshes = bim.getGeometryObjectsMap([pipe.getUuid(), wall.getUuid()])
def len = calc.distance(meshes[pipe.getUuid()], meshes[wall.getUuid()])   // мм
distanceFast - минимальное расстояние между двумя сетками через AABB-дерево
  • calc.distanceFast(GeometryObject data1, GeometryObject data2) → double - 0.0, если сетки пересекаются, иначе минимальное расстояние (мм для сеток с сервера). Дерево строится один раз на сетку.

Результат может немного отличаться от distance: обе реализации приближённые на очень тонких треугольниках.

Пример использования:

if (calc.distanceFast(slabObj, elementObj) <= 400) {   // ближе 400 мм
    console.warn("Элемент слишком близко к плите")
}
manhattanDistance - расстояние Манхэттена между двумя сетками
  • calc.manhattanDistance(GeometryObject data1, GeometryObject data2) → double - минимальная сумма |dx| + |dy| + |dz| между вершинами треугольников двух сеток

Пример использования:

def manhattanDist = calc.manhattanDistance(data1, data2)
distance2D - расстояние между сетками в плоскости XY
  • calc.distance2D(GeometryObject data1, GeometryObject data2) → double - расстояние между проекциями треугольников на плоскость XY

Пример использования:

def distance = calc.distance2D(data1, data2)
distance2DXY - расстояние между сетками в плоскости XY, 0 если пересекаются
  • calc.distance2DXY(GeometryObject data1, GeometryObject data2) → double - 0.0, если проекции на XY пересекаются, иначе минимальное расстояние между ними
В проекции на XY вертикальные грани вырождаются в отрезки. Для таких треугольников distance2D и distance2DXY могут вернуть 0 у разнесённых элементов. Для элементов с вертикальными стенками проверяйте результат по боксам или используйте distance.

Пример использования:

def distance = calc.distance2DXY(data1, data2)
checkInside - находится ли одна сетка внутри другой
  • calc.checkInside(GeometryObject geometryObject1, GeometryObject geometryObject2) → boolean - сетка 1 (вложенная) внутри сетки 2 (объемлющей)
  • calc.checkInside(double gap, GeometryObject geometryObject1, GeometryObject geometryObject2) → boolean - с допуском gap (мм для сеток с сервера)
  • calc.checkInside(double gapX, double gapY, double gapZ, GeometryObject geometryObject1, GeometryObject geometryObject2) → boolean - с допуском отдельно по осям

Сначала передаётся вложенный элемент, затем объемлющий. Если одна из сеток null, метод возвращает false.

Пример использования:

def meshes = bim.getGeometryObjectsMap([element.getUuid(), space.getUuid()])
boolean inSpace = calc.checkInside(15, 15, 15, meshes[element.getUuid()], meshes[space.getUuid()])

Пакетные методы

Двойной цикл for (a in walls) for (b in pipes) if (calc.intersect(gap, a, b)) растёт как произведение размеров наборов. Пакетные методы отбирают кандидатов без перебора всех пар (сортировка по оси X) и переиспользуют кеш сеток.
findIntersectingPairs - все пары элементов двух наборов с пересекающимися габаритными боксами
  • calc.findIntersectingPairs(Collection<GeometryElement> first, Collection<GeometryElement> second, double gap) → Map<UUID, List<GeometryElement>> - идентификатор элемента из first → элементы из second, чьи боксы пересекаются с ним с учётом gap (в единицах bounds, м)

Результат совпадает с перебором через calc.intersect(gap, a, b). В карту попадают только элементы, у которых есть пересечения. Элементы без bounds пропускаются, элемент с самим собой не сопоставляется. Если передать одну и ту же коллекцию дважды, каждая пара попадёт в результат в обе стороны.

Пример использования:

def walls = bim.getGeometryElements('entity like "IfcWall"')
def slabs = bim.getGeometryElements('entity like "IfcSlab"')
calc.findIntersectingPairs(walls, slabs, 0.01).each { wallUuid, crossed ->
    console.info("${wallUuid}: ${crossed.size()} перекрытий")
}
intersectAll - все пары элементов двух наборов, пересекающиеся по сеткам
  • calc.intersectAll(double gap, Collection<GeometryElement> first, Collection<GeometryElement> second, Map<UUID, GeometryObject> meshes) → Map<UUID, List<GeometryElement>> - отбор пар по боксам (findIntersectingPairs), затем точная проверка сеток

gap задаётся в метрах, для сеток метод сам переводит его в миллиметры. meshes — сетки элементов по идентификатору, например из bim.getGeometryObjectsMap. Если у элемента нет сетки, пара остаётся в результате по итогам проверки боксов.

Пример использования:

def walls = bim.getGeometryElements('entity like "IfcWall"')
def pipes = bim.getGeometryElements('entity like "IfcPipeSegment"')
def meshes = bim.getGeometryObjectsMap(new ArrayList((walls*.uuid + pipes*.uuid).unique()))

def hits = calc.intersectAll(0.01, walls, pipes, meshes)   // зазор 10 мм
hits.each { wallUuid, crossed ->
    crossed.each { pipe -> console.info("Стена ${wallUuid} пересекает ${pipe.getName()}") }
}
distanceAll - расстояния от элемента до набора элементов
  • calc.distanceAll(GeometryElement from, Collection<GeometryElement> to, Map<UUID, GeometryObject> meshes) → Map<UUID, Double> - идентификатор элемента → расстояние по сеткам (distance), в единицах сетки (мм). Элементы без сетки в meshes пропускаются.

Пример использования:

def meshes = bim.getGeometryObjectsMap(new ArrayList(([stair] + walls)*.uuid))
def distances = calc.distanceAll(stair, walls, meshes)
def nearest = distances.min { it.value }
console.info("Ближайшая стена: ${nearest?.key}, ${nearest?.value} мм")

Ориентированный бокс и выпуклая оболочка

getOrientedBoundingBox - ориентированный минимальный граничный бокс (OBB) элемента
  • calc.getOrientedBoundingBox(UUID elementUuid) → GeometryObject - по идентификатору элемента
  • calc.getOrientedBoundingBox(GeometryElement element) → GeometryObject - по элементу
  • calc.getOrientedBoundingBox(Element element) → GeometryObject - по элементу модели (ElementExternal)

Каждый вызов — отдельный запрос к серверу. Для набора элементов используйте getOrientedBoundingBoxes.

Пример использования:

def listPoints = new ArrayList<Vector3f>()
def orientedBox = calc.getOrientedBoundingBox(stair.getUuid())
def koeff = orientedBox.getKoeff()
console.info("Коэффициент длины: " + koeff)

// превращаем бокс в набор вершин
def vertices = orientedBox.getVertices()
for (int i = 0; i < vertices.length; i += 3) {
    listPoints.add(new Vector3f(vertices[i], vertices[i + 1], vertices[i + 2]))
}

def point0 = listPoints.get(0)
def point3 = listPoints.get(3)
def point5 = listPoints.get(5)
def point6 = listPoints.get(6)

// вычисляем, какая сторона по x или по y длиннее
double lengthX = calc.trunc(calc.length(point3, point6))
double lengthY = calc.trunc(calc.length(point3, point5))
double lengthZ = calc.trunc(calc.length(point0, point3))

Обратите внимание!

На примере представлен приблизительный алгоритм получения длины, высоты и ширины. Какая сторона окажется длиной, высотой или шириной, не гарантируется — необходима проверка по точкам.

getOrientedBoundingBoxes - OBB для набора элементов одним запросом
  • calc.getOrientedBoundingBoxes(Set<UUID> elementUuids) → List<GeometryObject>

Пример использования:

def boxes = calc.getOrientedBoundingBoxes(stairs*.uuid as Set)
getElementConvexHull - геометрия выпуклой оболочки элемента
  • calc.getElementConvexHull(UUID elementUuid) → GeometryObject - по идентификатору
  • calc.getElementConvexHull(GeometryElement element) → GeometryObject - по элементу
  • calc.getElementConvexHull(Element element) → GeometryObject - по элементу модели (ElementExternal)

Метод возвращает триангулированную геометрию оболочки. Каждый вызов — отдельный запрос к серверу.

Пример использования:

def convexHull = calc.getElementConvexHull(elemUuid)
createOrientedBox - построение бокса заданных размеров в заданном направлении
  • calc.createOrientedBox(Vector3f center, Vector3f direction, double width, double depth, double height) → GeometryObject - бокс из 8 вершин и 12 треугольников с заполненными bounds

Глубина depth откладывается вдоль direction, ширина width — горизонтально поперёк него, высота height — по вертикали. Если direction вертикальный, оси подбираются автоматически. Координаты и размеры задаются в тех же единицах, что и сетки, с которыми бокс будет сравниваться (для сеток с сервера — мм). Бокс удобно использовать как зону проверки или как фигуру коллизии (figure в collision.createCollision).

Пример использования:

// зона обслуживания 1200 × 600 × 2000 мм перед оборудованием по оси +Y
double[] c = equipment.getBounds().getCenter()   // центр в метрах
def center = new Vector3f(c[0] * 1000, c[1] * 1000 + 600, c[2] * 1000)
def zone = calc.createOrientedBox(center, new Vector3f(0, 1, 0), 1200, 600, 2000)
if (calc.intersect(zone, wallMesh)) {
    console.warn("Зона обслуживания пересекается со стеной")
}
getOBBLongestAxis - индекс самой длинной стороны OBB
  • calc.getOBBLongestAxis(GeometryElement element) → int - X=1, Y=2, Z=3 (стороны OBB элемента); -1, если у элемента нет OBB

Пример использования:

int axis = calc.getOBBLongestAxis(pipe)
getElementDirection - главная ось элемента
  • calc.getElementDirection(GeometryElement element) → Vector3D - направление по OBB элемента; если OBB нет — по направлению большей грани (largeDirect и матрица) или по габаритам. null, если определить не удалось.

Пример использования:

def dir = calc.getElementDirection(wall)
if (dir != null) console.info("Направление стены: ${dir.getX()}, ${dir.getY()}, ${dir.getZ()}")

Стены и помещения

Методы раздела получают элементы (GeometryElement, габариты в метрах) и карту их сеток (Map<UUID, GeometryObject> из bim.getGeometryObjectsMap, в миллиметрах). gap задаётся в метрах. Если нет сетки помещения, OBB стены или её направления, методы возвращают false (или null).

getFaceArea - площадь грани габаритного бокса элемента
  • calc.getFaceArea(GeometryElement geometryElement, String face) → double - площадь грани "minX", "maxX", "minY", "maxY", "minZ" или "maxZ" (в м²); для другого имени — 0

Пример использования:

def face = calc.findContactFaceName(0.05, wall, space, meshes)
if (face != null) console.info("Площадь контакта: " + calc.trunc(calc.getFaceArea(wall, face)))
getContactFaceName - грань одной сетки, контактирующая с другой
  • calc.getContactFaceName(double gap, GeometryObject wallObject, GeometryObject spaceObject) → String - имя грани wallObject ("minX", "maxY" и т. п.), к которой ближе gap подходит spaceObject (gap в единицах сетки, мм); null, если контакта нет

Пример использования:

def face = calc.getContactFaceName(50, meshes[wall.getUuid()], meshes[space.getUuid()])
findContactFaceName - внешняя грань контакта стены с помещением
  • calc.findContactFaceName(double gap, GeometryElement wallElement, GeometryElement spaceElement, Map<UUID, GeometryObject> geometryObjectMap) → String - имя грани стены, которой она снаружи касается помещения; null, если стена находится внутри помещения или контакта нет (gap в метрах)

Пример использования:

def meshes = bim.getGeometryObjectsMap([wall.getUuid(), space.getUuid()])
def face = calc.findContactFaceName(0.05, wall, space, meshes)
isExternalAdjacentElement - является ли стена внешне смежной с помещением
  • calc.isExternalAdjacentElement(double gap, GeometryElement wallElement, GeometryElement spaceElement, Set<GeometryElement> openElements, Map<UUID, GeometryObject> geometryObjectMap) → boolean

openElements — проёмы, двери и окна этой стены: связанный проём подтверждает смежность. gap задаётся в метрах.

Пример использования:

def meshes = bim.getGeometryObjectsMap(new ArrayList(([space] + walls + openings)*.uuid))
def outerWalls = walls.findAll { calc.isExternalAdjacentElement(0.05, it, space, openings as Set, meshes) }
isInternalAdjacentElement - является ли стена внутренне смежной с помещением
  • calc.isInternalAdjacentElement(double gap, GeometryElement wallElement, GeometryElement spaceElement, Set<GeometryElement> openElements, Map<UUID, GeometryObject> geometryObjectMap) → boolean - стена находится внутри помещения и касается его грани

Пример использования:

def innerWalls = walls.findAll { calc.isInternalAdjacentElement(0.05, it, space, [] as Set, meshes) }
Copyright © 2026