testqbsp: add more detail_illusionary tests

This commit is contained in:
Eric Wasylishen 2022-05-01 21:44:58 -06:00
parent 59684a1c5d
commit 3a30891476
5 changed files with 267 additions and 20 deletions

View File

@ -319,21 +319,28 @@ static bool EdgePlanes_PointInside(const std::vector<qplane3d> &edgeplanes, cons
return true;
}
static const mface_t *BSP_FindFaceAtPoint_r(
const mbsp_t *bsp, const int nodenum, const qvec3d &point, const qvec3d &wantedNormal)
/**
* pass 0,0,0 for wantedNormal to disable the normal check
*/
static void BSP_FindFaceAtPoint_r(
const mbsp_t *bsp, const int nodenum, const qvec3d &point, const qvec3d &wantedNormal, std::vector<const mface_t *> &result)
{
if (nodenum < 0) {
// we're only interested in nodes, since faces are owned by nodes.
return nullptr;
return;
}
const bsp2_dnode_t *node = &bsp->dnodes[nodenum];
const vec_t dist = bsp->dplanes[node->planenum].distance_to_fast(point);
if (dist > 0.1)
return BSP_FindFaceAtPoint_r(bsp, node->children[0], point, wantedNormal);
if (dist < -0.1)
return BSP_FindFaceAtPoint_r(bsp, node->children[1], point, wantedNormal);
if (dist > 0.1) {
BSP_FindFaceAtPoint_r(bsp, node->children[0], point, wantedNormal, result);
return;
}
if (dist < -0.1) {
BSP_FindFaceAtPoint_r(bsp, node->children[1], point, wantedNormal, result);
return;
}
// Point is close to this node plane. Check all faces on the plane.
for (int i = 0; i < node->numfaces; i++) {
@ -341,9 +348,11 @@ static const mface_t *BSP_FindFaceAtPoint_r(
// First check if it's facing the right way
qvec3d faceNormal = Face_Normal(bsp, face);
if (qv::dot(faceNormal, wantedNormal) < 0) {
// Opposite, so not the right face.
continue;
if (wantedNormal != qvec3d{0, 0, 0}) {
if (qv::dot(faceNormal, wantedNormal) < 0) {
// Opposite, so not the right face.
continue;
}
}
// Next test if it's within the boundaries of the face
@ -352,23 +361,33 @@ static const mface_t *BSP_FindFaceAtPoint_r(
// Found a match?
if (insideFace) {
return face;
result.push_back(face);
}
}
// No match found on this plane. Check both sides of the tree.
const mface_t *side0Match = BSP_FindFaceAtPoint_r(bsp, node->children[0], point, wantedNormal);
if (side0Match != nullptr) {
return side0Match;
} else {
return BSP_FindFaceAtPoint_r(bsp, node->children[1], point, wantedNormal);
}
BSP_FindFaceAtPoint_r(bsp, node->children[0], point, wantedNormal, result);
BSP_FindFaceAtPoint_r(bsp, node->children[1], point, wantedNormal, result);
}
std::vector<const mface_t *> BSP_FindFacesAtPoint(
const mbsp_t *bsp, const dmodelh2_t *model, const qvec3d &point, const qvec3d &wantedNormal)
{
std::vector<const mface_t *> result;
BSP_FindFaceAtPoint_r(bsp, model->headnode[0], point, wantedNormal, result);
return result;
}
const mface_t *BSP_FindFaceAtPoint(
const mbsp_t *bsp, const dmodelh2_t *model, const qvec3d &point, const qvec3d &wantedNormal)
{
return BSP_FindFaceAtPoint_r(bsp, model->headnode[0], point, wantedNormal);
std::vector<const mface_t *> result;
BSP_FindFaceAtPoint_r(bsp, model->headnode[0], point, wantedNormal, result);
if (result.empty()) {
return nullptr;
}
return result[0];
}
static const bsp2_dnode_t *BSP_FindNodeAtPoint_r(

View File

@ -21,10 +21,11 @@
#include <common/bspfile.hh>
#include <common/mathlib.hh>
#include <string>
#include <common/qvec.hh>
#include <string>
#include <vector>
const dmodelh2_t *BSP_GetWorldModel(const mbsp_t *bsp);
int Face_GetNum(const mbsp_t *bsp, const mface_t *f);
@ -51,6 +52,9 @@ int Face_ContentsOrSurfaceFlags(
const dmodelh2_t *BSP_DModelForModelString(const mbsp_t *bsp, const std::string &submodel_str);
bool Light_PointInSolid(const mbsp_t *bsp, const dmodelh2_t *model, const qvec3d &point);
bool Light_PointInWorld(const mbsp_t *bsp, const qvec3d &point);
std::vector<const mface_t *> BSP_FindFacesAtPoint(
const mbsp_t *bsp, const dmodelh2_t *model, const qvec3d &point, const qvec3d &wantedNormal);
/**
* Searches for a face touching a point and facing a certain way.
* Sometimes (water, sky?) there will be 2 overlapping candidates facing opposite ways, the provided normal

View File

@ -510,6 +510,45 @@ TEST(testmaps_q1, noclipfaces_mirrorinside)
}
}
TEST(testmaps_q1, detail_illusionary_intersecting)
{
const mbsp_t bsp = LoadTestmap("qbsp_detail_illusionary_intersecting.map");
EXPECT_FALSE(map.leakfile);
// sides: 3*4 = 12
// top: 3
// bottom: 3
EXPECT_EQ(bsp.dfaces.size(), 18);
for (auto &face : bsp.dfaces) {
EXPECT_STREQ("{trigger", Face_TextureName(&bsp, &face));
}
// top of cross
EXPECT_EQ(1, BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-58, -50, 120), qvec3d(0, 0, 1)).size());
// interior face that should be clipped away
EXPECT_EQ(0, BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-58, -52, 116), qvec3d(0, -1, 0)).size());
}
TEST(testmaps_q1, detail_illusionary_noclipfaces_intersecting)
{
const mbsp_t bsp = LoadTestmap("qbsp_detail_illusionary_noclipfaces_intersecting.map");
EXPECT_FALSE(map.leakfile);
for (auto &face : bsp.dfaces) {
EXPECT_STREQ("{trigger", Face_TextureName(&bsp, &face));
}
// top of cross has 2 faces Z-fighting, because we disabled clipping
EXPECT_EQ(2, BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-58, -50, 120), qvec3d(0, 0, 1)).size());
// interior face not clipped away
EXPECT_EQ(1, BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-58, -52, 116), qvec3d(0, -1, 0)).size());
}
TEST(testmaps_q1, detail_doesnt_seal)
{
const mbsp_t bsp = LoadTestmap("qbsp_detail_doesnt_seal.map");

View File

@ -0,0 +1,92 @@
// Game: Quake
// Format: Valve
// entity 0
{
"mapversion" "220"
"classname" "worldspawn"
"wad" "deprecated/free_wad.wad;deprecated/fence.wad;deprecated/origin.wad;deprecated/hintskip.wad"
"_wateralpha" "0.5"
"_tb_def" "builtin:Quake.fgd"
"message" "Intersecting detail illusionary. Intersection should be clipped away"
// brush 0
{
( 204 0 232 ) ( 204 -192 232 ) ( 204 0 32 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 208 -192 32 ) ( 204 -192 32 ) ( 208 -192 232 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 208 0 32 ) ( 204 0 32 ) ( 208 -192 32 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 208 -192 232 ) ( 204 -192 232 ) ( 208 0 232 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 208 0 232 ) ( 204 0 232 ) ( 208 0 32 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 208 -192 232 ) ( 208 0 232 ) ( 208 -192 32 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 1
{
( -176 -192 32 ) ( -176 0 32 ) ( -176 -192 232 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -176 -192 232 ) ( -172 -192 232 ) ( -176 -192 32 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -176 -192 32 ) ( -172 -192 32 ) ( -176 0 32 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -176 0 232 ) ( -172 0 232 ) ( -176 -192 232 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -176 0 32 ) ( -172 0 32 ) ( -176 0 232 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -172 -192 32 ) ( -172 -192 232 ) ( -172 0 32 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 2
{
( -172 0 232 ) ( -172 -4 232 ) ( -172 0 32 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -4 232 ) ( 204 -4 32 ) ( -172 -4 232 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -172 0 32 ) ( -172 -4 32 ) ( 204 0 32 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 204 0 232 ) ( 204 -4 232 ) ( -172 0 232 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 204 0 232 ) ( -172 0 232 ) ( 204 0 32 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 0 32 ) ( 204 -4 32 ) ( 204 0 232 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 3
{
( -172 -192 32 ) ( -172 -188 32 ) ( -172 -192 232 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -192 32 ) ( -172 -192 32 ) ( 204 -192 232 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -192 32 ) ( 204 -188 32 ) ( -172 -192 32 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -172 -192 232 ) ( -172 -188 232 ) ( 204 -192 232 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -172 -188 32 ) ( 204 -188 32 ) ( -172 -188 232 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -192 232 ) ( 204 -188 232 ) ( 204 -192 32 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 4
{
( -172 -188 232 ) ( -172 -188 228 ) ( -172 -4 232 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -188 232 ) ( 204 -188 228 ) ( -172 -188 232 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -4 228 ) ( -172 -4 228 ) ( 204 -188 228 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 204 -4 232 ) ( 204 -188 232 ) ( -172 -4 232 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -172 -4 232 ) ( -172 -4 228 ) ( 204 -4 232 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -4 232 ) ( 204 -4 228 ) ( 204 -188 232 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 5
{
( -172 -4 32 ) ( -172 -4 36 ) ( -172 -188 32 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -172 -188 32 ) ( -172 -188 36 ) ( 204 -188 32 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -172 -4 32 ) ( -172 -188 32 ) ( 204 -4 32 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -172 -188 36 ) ( -172 -4 36 ) ( 204 -188 36 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 204 -4 32 ) ( 204 -4 36 ) ( -172 -4 32 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -188 32 ) ( 204 -188 36 ) ( 204 -4 32 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
}
// entity 1
{
"classname" "info_player_start"
"origin" "-120 -80 120"
}
// entity 2
{
"classname" "func_detail_illusionary"
// brush 0
{
( -60 -36 120 ) ( -60 -68 120 ) ( -60 -68 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ 0 1.0000000000000002 0 0 ] 0 1 1
( -60 -68 120 ) ( -56 -68 120 ) ( -56 -68 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ -1.0000000000000002 0 0 0 ] 0 1 1
( -56 -68 72 ) ( -56 -36 72 ) ( -60 -36 72 ) {trigger [ -1.0000000000000002 0 0 12 ] [ 0 1.0000000000000002 0 0 ] 0 1 1
( -60 -36 120 ) ( -56 -36 120 ) ( -56 -68 120 ) {trigger [ 1.0000000000000002 0 0 52 ] [ 0 1.0000000000000002 0 0 ] 0 1 1
( -56 -36 72 ) ( -56 -36 120 ) ( -60 -36 120 ) {trigger [ 0 0 1.0000000000000002 0 ] [ 1.0000000000000002 0 0 24 ] 0 1 1
( -56 -68 120 ) ( -56 -36 120 ) ( -56 -36 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ 0 1.0000000000000002 0 0 ] 0 1 1
}
// brush 1
{
( -72 -48 120 ) ( -72 -52 120 ) ( -72 -52 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ 0 1.0000000000000002 0 20 ] 0 1 1
( -68 -52 120 ) ( -28 -52 120 ) ( -28 -52 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ -1.0000000000000002 0 0 24 ] 0 1 1
( -28 -52 72 ) ( -28 -48 72 ) ( -68 -48 72 ) {trigger [ 0 -1.0000000000000002 0 20 ] [ -1.0000000000000002 0 0 24 ] 0 1 1
( -68 -48 120 ) ( -28 -48 120 ) ( -28 -52 120 ) {trigger [ 0 1.0000000000000002 0 44 ] [ -1.0000000000000002 0 0 24 ] 0 1 1
( -28 -48 72 ) ( -28 -48 120 ) ( -68 -48 120 ) {trigger [ 0 0 1.0000000000000002 0 ] [ -1.0000000000000002 0 0 24 ] 0 1 1
( -28 -52 120 ) ( -28 -48 120 ) ( -28 -48 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ 0 -1.0000000000000002 0 0 ] 0 1 1
}
}

View File

@ -0,0 +1,93 @@
// Game: Quake
// Format: Valve
// entity 0
{
"mapversion" "220"
"classname" "worldspawn"
"wad" "deprecated/free_wad.wad;deprecated/fence.wad;deprecated/origin.wad;deprecated/hintskip.wad"
"_wateralpha" "0.5"
"_tb_def" "builtin:Quake.fgd"
"message" "Intersecting detail illusionary with _noclipfaces. Should have 2 z-fighting faces where they intersect"
// brush 0
{
( 204 0 232 ) ( 204 -192 232 ) ( 204 0 32 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 208 -192 32 ) ( 204 -192 32 ) ( 208 -192 232 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 208 0 32 ) ( 204 0 32 ) ( 208 -192 32 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 208 -192 232 ) ( 204 -192 232 ) ( 208 0 232 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 208 0 232 ) ( 204 0 232 ) ( 208 0 32 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 208 -192 232 ) ( 208 0 232 ) ( 208 -192 32 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 1
{
( -176 -192 32 ) ( -176 0 32 ) ( -176 -192 232 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -176 -192 232 ) ( -172 -192 232 ) ( -176 -192 32 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -176 -192 32 ) ( -172 -192 32 ) ( -176 0 32 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -176 0 232 ) ( -172 0 232 ) ( -176 -192 232 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -176 0 32 ) ( -172 0 32 ) ( -176 0 232 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -172 -192 32 ) ( -172 -192 232 ) ( -172 0 32 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 2
{
( -172 0 232 ) ( -172 -4 232 ) ( -172 0 32 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -4 232 ) ( 204 -4 32 ) ( -172 -4 232 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -172 0 32 ) ( -172 -4 32 ) ( 204 0 32 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 204 0 232 ) ( 204 -4 232 ) ( -172 0 232 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 204 0 232 ) ( -172 0 232 ) ( 204 0 32 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 0 32 ) ( 204 -4 32 ) ( 204 0 232 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 3
{
( -172 -192 32 ) ( -172 -188 32 ) ( -172 -192 232 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -192 32 ) ( -172 -192 32 ) ( 204 -192 232 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -192 32 ) ( 204 -188 32 ) ( -172 -192 32 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -172 -192 232 ) ( -172 -188 232 ) ( 204 -192 232 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -172 -188 32 ) ( 204 -188 32 ) ( -172 -188 232 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -192 232 ) ( 204 -188 232 ) ( 204 -192 32 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 4
{
( -172 -188 232 ) ( -172 -188 228 ) ( -172 -4 232 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -188 232 ) ( 204 -188 228 ) ( -172 -188 232 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -4 228 ) ( -172 -4 228 ) ( 204 -188 228 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 204 -4 232 ) ( 204 -188 232 ) ( -172 -4 232 ) skip [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -172 -4 232 ) ( -172 -4 228 ) ( 204 -4 232 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -4 232 ) ( 204 -4 228 ) ( 204 -188 232 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 5
{
( -172 -4 32 ) ( -172 -4 36 ) ( -172 -188 32 ) skip [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -172 -188 32 ) ( -172 -188 36 ) ( 204 -188 32 ) skip [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -172 -4 32 ) ( -172 -188 32 ) ( 204 -4 32 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -172 -188 36 ) ( -172 -4 36 ) ( 204 -188 36 ) skip [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 204 -4 32 ) ( 204 -4 36 ) ( -172 -4 32 ) skip [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 204 -188 32 ) ( 204 -188 36 ) ( 204 -4 32 ) skip [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
}
// entity 1
{
"classname" "info_player_start"
"origin" "-120 -80 120"
}
// entity 2
{
"classname" "func_detail_illusionary"
"_noclipfaces" "1"
// brush 0
{
( -60 -36 120 ) ( -60 -68 120 ) ( -60 -68 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ 0 1.0000000000000002 0 0 ] 0 1 1
( -60 -68 120 ) ( -56 -68 120 ) ( -56 -68 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ -1.0000000000000002 0 0 0 ] 0 1 1
( -56 -68 72 ) ( -56 -36 72 ) ( -60 -36 72 ) {trigger [ -1.0000000000000002 0 0 12 ] [ 0 1.0000000000000002 0 0 ] 0 1 1
( -60 -36 120 ) ( -56 -36 120 ) ( -56 -68 120 ) {trigger [ 1.0000000000000002 0 0 52 ] [ 0 1.0000000000000002 0 0 ] 0 1 1
( -56 -36 72 ) ( -56 -36 120 ) ( -60 -36 120 ) {trigger [ 0 0 1.0000000000000002 0 ] [ 1.0000000000000002 0 0 24 ] 0 1 1
( -56 -68 120 ) ( -56 -36 120 ) ( -56 -36 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ 0 1.0000000000000002 0 0 ] 0 1 1
}
// brush 1
{
( -72 -48 120 ) ( -72 -52 120 ) ( -72 -52 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ 0 1.0000000000000002 0 20 ] 0 1 1
( -68 -52 120 ) ( -28 -52 120 ) ( -28 -52 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ -1.0000000000000002 0 0 24 ] 0 1 1
( -28 -52 72 ) ( -28 -48 72 ) ( -68 -48 72 ) {trigger [ 0 -1.0000000000000002 0 20 ] [ -1.0000000000000002 0 0 24 ] 0 1 1
( -68 -48 120 ) ( -28 -48 120 ) ( -28 -52 120 ) {trigger [ 0 1.0000000000000002 0 44 ] [ -1.0000000000000002 0 0 24 ] 0 1 1
( -28 -48 72 ) ( -28 -48 120 ) ( -68 -48 120 ) {trigger [ 0 0 1.0000000000000002 0 ] [ -1.0000000000000002 0 0 24 ] 0 1 1
( -28 -52 120 ) ( -28 -48 120 ) ( -28 -48 72 ) {trigger [ 0 0 1.0000000000000002 0 ] [ 0 -1.0000000000000002 0 0 ] 0 1 1
}
}