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 - является ли стена внутренне смежной с помещением
Арифметика и числа
- calc.area(double length, double width) →
double
Пример использования:
def area = calc.area(6, 6)
// console.info(area) -> "INFO: 36.0"
- calc.volume(double length, double width, double height) →
double
Пример использования:
def vol = calc.volume(6, 0.5, 3)
// console.info(vol) -> "INFO: 9.0"
- calc.areaBox(double length, double width, double height) →
double
Пример использования:
def area = calc.areaBox(6, 6, 3)
// console.info(area) -> "INFO: 72.0"
- 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"
- calc.compare(double a, double b, double tolerance) →
boolean-true, если|a − b| <= tolerance
Пример использования:
if (calc.compare(el.getAreaZ(), attrArea, 0.01)) {
// площади совпадают с точностью до 0.01
}
- 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("Объём в атрибуте отличается от геометрии")
}
- 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
- 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.
- 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())
}
По габаритным боксам:
- 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(в единицах сетки: для сеток с сервера — мм)
Пример использования:
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("Элемент пересекает плиту")
}
}
- calc.getElementBounds(GeometryObject geometry) →
Bounds- по вершинам сетки - calc.getElementBounds(double vertices) →
Bounds- по массиву координатx1, y1, z1, x2, y2, z2, …
Пример использования:
def bounds = mesh.getBounds() ?: calc.getElementBounds(mesh)
- 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())
}
- 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))
}
- calc.isValveBelongToPipe(GeometryElement valve, GeometryElement pipe) →
boolean- пересекается ли габаритный боксvalveс ориентированным боксом (OBB)pipe
Пример использования:
if (calc.isValveBelongToPipe(valve, pipe)) {
console.info(valve.getName() + " установлен на " + pipe.getName())
}
Точные расчёты по сеткам
- 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()]) // мм
- calc.distanceFast(GeometryObject data1, GeometryObject data2) →
double-0.0, если сетки пересекаются, иначе минимальное расстояние (мм для сеток с сервера). Дерево строится один раз на сетку.
Результат может немного отличаться от distance: обе реализации приближённые на очень тонких треугольниках.
Пример использования:
if (calc.distanceFast(slabObj, elementObj) <= 400) { // ближе 400 мм
console.warn("Элемент слишком близко к плите")
}
- calc.manhattanDistance(GeometryObject data1, GeometryObject data2) →
double- минимальная сумма|dx| + |dy| + |dz|между вершинами треугольников двух сеток
Пример использования:
def manhattanDist = calc.manhattanDistance(data1, data2)
- calc.distance2D(GeometryObject data1, GeometryObject data2) →
double- расстояние между проекциями треугольников на плоскость XY
Пример использования:
def distance = calc.distance2D(data1, data2)
- calc.distance2DXY(GeometryObject data1, GeometryObject data2) →
double-0.0, если проекции на XY пересекаются, иначе минимальное расстояние между ними
distance2D и distance2DXY могут вернуть 0 у разнесённых элементов. Для элементов с вертикальными стенками проверяйте результат по боксам или используйте distance.Пример использования:
def distance = calc.distance2DXY(data1, data2)
- 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) и переиспользуют кеш сеток.- 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()} перекрытий")
}
- 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()}") }
}
- 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} мм")
Ориентированный бокс и выпуклая оболочка
- 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))

Обратите внимание!
На примере представлен приблизительный алгоритм получения длины, высоты и ширины. Какая сторона окажется длиной, высотой или шириной, не гарантируется — необходима проверка по точкам.
- calc.getOrientedBoundingBoxes(Set<UUID> elementUuids) →
List<GeometryObject>
Пример использования:
def boxes = calc.getOrientedBoundingBoxes(stairs*.uuid as Set)
- calc.getElementConvexHull(UUID elementUuid) →
GeometryObject- по идентификатору - calc.getElementConvexHull(GeometryElement element) →
GeometryObject- по элементу - calc.getElementConvexHull(Element element) →
GeometryObject- по элементу модели (ElementExternal)
Метод возвращает триангулированную геометрию оболочки. Каждый вызов — отдельный запрос к серверу.
Пример использования:
def convexHull = calc.getElementConvexHull(elemUuid)
- 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("Зона обслуживания пересекается со стеной")
}
- calc.getOBBLongestAxis(GeometryElement element) →
int- X=1, Y=2, Z=3 (стороны OBB элемента);-1, если у элемента нет OBB
Пример использования:
int axis = calc.getOBBLongestAxis(pipe)
- 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).
- 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)))
- 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()])
- 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)
- 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) }
- 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) }

