public static BspNode AddNode(BspNode inParent, BspNode.EBspLocation inLocation, CSGFace inFace, int inFlags) { BspNode newNode = new BspNode(); newNode.plane = inFace.GetPlane(); newNode.face = inFace; newNode.front = newNode.back = newNode.planar = null; newNode.flags = inFlags; if (inLocation == BspNode.EBspLocation.BspLocation_Front) { // check that front node is null inParent.front = newNode; } else if (inLocation == BspNode.EBspLocation.BspLocation_Back) { // TODO: check that back node is null inParent.back = newNode; } else if (inLocation == BspNode.EBspLocation.BspLocation_Planar) { // go to the last planar node BspNode lastPlanar = inParent; while (lastPlanar.planar != null) { lastPlanar = lastPlanar.planar; } // add planar node lastPlanar.planar = newNode; } return(newNode); }
/// <summary> /// Imports the sub mesh. /// </summary> /// <param name='inMesh'> /// In mesh. /// </param> /// <param name='inSubMesh'> /// In sub mesh. /// </param> /// <param name='inMaterial'> /// In material. /// </param> private void ImportSubMesh(Mesh inMesh, int inSubMesh, Material inMaterial) { // int[] tris = inMesh.GetTriangles(inSubMesh); for (int i = 0; i < tris.Length; i += 3) { CSGFace newFace = new CSGFace(); // copy triangle newFace.vertices = new Vector3[3]; newFace.vertices[0] = inMesh.vertices[tris[i + 0]]; newFace.vertices[1] = inMesh.vertices[tris[i + 1]]; newFace.vertices[2] = inMesh.vertices[tris[i + 2]]; // copy triangle uv newFace.uv = new Vector2[3]; newFace.uv[0] = inMesh.uv[tris[i + 0]]; newFace.uv[1] = inMesh.uv[tris[i + 1]]; newFace.uv[2] = inMesh.uv[tris[i + 2]]; // set material newFace.material = inMaterial; // add to face list faces.Add(newFace); } }
/// <summary> /// Optimize this instance. /// </summary> public void Optimize() { // CSGFace.MergeCoplanars(faces); // TransferFacesToMesh(); }
/// <summary> /// Single Mesh importing, do not support Sub Meshes. /// </summary> /// <param name='inMesh'> /// In mesh. /// </param> /// <param name='inMaterial'> /// In material. /// </param> private void ImportMesh(Mesh inMesh, Material inMaterial) { // ONLY SUPPORTS TRIANGLES AT THE MOMENT faces = new List <CSGFace>(); for (int i = 0; i < inMesh.triangles.Length; i += 3) { CSGFace newFace = new CSGFace(); // copy triangle newFace.vertices = new Vector3[3]; newFace.vertices[0] = inMesh.vertices[inMesh.triangles[i + 0]]; newFace.vertices[1] = inMesh.vertices[inMesh.triangles[i + 1]]; newFace.vertices[2] = inMesh.vertices[inMesh.triangles[i + 2]]; // copy triangle uv newFace.uv = new Vector2[3]; newFace.uv[0] = inMesh.uv[inMesh.triangles[i + 0]]; newFace.uv[1] = inMesh.uv[inMesh.triangles[i + 1]]; newFace.uv[2] = inMesh.uv[inMesh.triangles[i + 2]]; // set material newFace.material = inMaterial; // add to face list faces.Add(newFace); } }
public void AddDeferredFace(CSGFace inFace) { // add to deferred faces DeferredFace defFace; defFace.face = inFace; defFace.node = currentNode; deferredFaces.Add(defFace); }
// void RouteOper(BspNode inNode, CSGFace inFace, EPolySide inSide, OperationInfo info) { if (processState == EProcessState.Process_Master) { csgVisitor.ProcessMaster(this, inFace, inSide, info); } else { csgVisitor.ProcessSlave(this, inFace, inSide, info); } }
// TODO public void ProcessMaster(CsgOperation inOperation, CSGFace inFace, CsgOperation.EPolySide inSide, CsgOperation.OperationInfo info) { switch (inSide) { case CsgOperation.EPolySide.PolySide_Outside: case CsgOperation.EPolySide.PolySide_Planar_Outside: case CsgOperation.EPolySide.PolySide_CoPlanar_Outside: case CsgOperation.EPolySide.PolySide_CoPlanar_Inside: break; case CsgOperation.EPolySide.PolySide_Inside: case CsgOperation.EPolySide.PolySide_Planar_Inside: // add to deferred faces inOperation.AddDeferredFace(inFace); break; } }
public object Clone() { // create new Clone CSGFace clone = new CSGFace(); // copy vertices clone.vertices = new Vector3[vertices.Length]; System.Array.Copy(vertices, clone.vertices, vertices.Length); // copy uvs clone.uv = new Vector2[uv.Length]; System.Array.Copy(uv, clone.uv, uv.Length); // copy material clone.material = material; // FIXME: clone flags??? clone.flags = 0; return(clone); }
// public void Perform(ECsgOperation inOper, CSGObject inMaster, CSGObject inSlave) { // we are processing our slave faces processState = EProcessState.Process_Slave; // process faces against master tree PerformFaces(inMaster.rootNode, inSlave.faces); // process face from master tree processState = EProcessState.Process_Master; // perform master faces on slave bsp tree PerformTree(inMaster.rootNode, inSlave.rootNode); // check if how do we need to process generated faces if (inOper == ECsgOperation.CsgOper_Additive || inOper == ECsgOperation.CsgOper_Subtractive) { // add deferred faces to master tree... for (int i = 0; i < deferredFaces.Count; i++) { CSGFace defFace = ((DeferredFace)deferredFaces[i]).face; BspNode startNode = ((DeferredFace)deferredFaces[i]).node; // testing startNode = inMaster.rootNode; // add node to master tree BspGen.AddNodeRecursive(startNode, defFace, BspNode.BspFlags_IsNew); } } else { // clear old faces list inMaster.faces.Clear(); // copy created faces for (int i = 0; i < deferredFaces.Count; i++) { inMaster.faces.Add(deferredFaces[i].face); } } // clear deferred faces deferredFaces.Clear(); }
public void ProcessSlave(CsgOperation inOperation, CSGFace inFace, CsgOperation.EPolySide inSide, CsgOperation.OperationInfo info) { switch (inSide) { case CsgOperation.EPolySide.PolySide_Outside: case CsgOperation.EPolySide.PolySide_Planar_Outside: case CsgOperation.EPolySide.PolySide_CoPlanar_Outside: case CsgOperation.EPolySide.PolySide_CoPlanar_Inside: break; case CsgOperation.EPolySide.PolySide_Inside: case CsgOperation.EPolySide.PolySide_Planar_Inside: // clone face CSGFace newFace = (CSGFace)inFace.Clone(); newFace.Reverse(); // add to deferred faces inOperation.AddDeferredFace(newFace); break; } }
public void ProcessMaster(CsgOperation inOperation, CSGFace inFace, CsgOperation.EPolySide inSide, CsgOperation.OperationInfo info) { switch (inSide) { case CsgOperation.EPolySide.PolySide_Outside: case CsgOperation.EPolySide.PolySide_Planar_Outside: case CsgOperation.EPolySide.PolySide_CoPlanar_Inside: // add cutted polygons if ((inFace.flags & CSGFace.FaceFlags_WasCutted) != 0) { inOperation.AddPlanarFace(inFace); } break; case CsgOperation.EPolySide.PolySide_Inside: case CsgOperation.EPolySide.PolySide_Planar_Inside: case CsgOperation.EPolySide.PolySide_CoPlanar_Outside: // discard node inOperation.MarkNodeAsDestroyed(); break; } }
private void PerformNode(BspNode inNode, CSGFace inFace, int nodeSide, OperationInfo info) { while (inNode != null) { CSGFace.EPlaneSide side = inFace.Side(inNode.plane); switch (side) { case CSGFace.EPlaneSide.Side_Front: // nodeSide = nodeSide | (inNode.IsCsg() ? 1 : 0); // leaf node if (inNode.front == null) { // set operation infos info.leafNode = inNode; info.leafLocation = BspNode.EBspLocation.BspLocation_Front; // we are done, process face ProcessFace(inFace, SIDE_Outside, info); } // get to next front node (if any) inNode = inNode.front; break; case CSGFace.EPlaneSide.Side_Back: int backSide = inNode.IsCsg() ? 0 : 1; // nodeSide = nodeSide & backSide; // leaf node if (inNode.back == null) { // set leaf infos info.leafNode = inNode; info.leafLocation = BspNode.EBspLocation.BspLocation_Back; // we are done, process face ProcessFace(inFace, SIDE_Inside, info); } // get to next front node (if any) inNode = inNode.back; break; case CSGFace.EPlaneSide.Side_Split: // split face and process front and back CSGFace frontFace, backFace; // inFace.Split(inNode.plane, out frontFace, out backFace); // TODO: set polygon cutted flags frontFace.flags |= CSGFace.FaceFlags_WasCutted; backFace.flags |= CSGFace.FaceFlags_WasCutted; // front node is a leaf node if (inNode.front == null) { // info.leafNode = inNode; info.leafLocation = BspNode.EBspLocation.BspLocation_Front; ProcessFace(frontFace, SIDE_Outside, info); } else { PerformNode(inNode.front, frontFace, nodeSide, info); } // Prcess back node with back face if (inNode.back == null) { // info.leafNode = inNode; info.leafLocation = BspNode.EBspLocation.BspLocation_Back; ProcessFace(backFace, SIDE_Inside, info); } else { // process back node with new face PerformNode(inNode.back, backFace, nodeSide, info); } // stop loop inNode = null; break; case CSGFace.EPlaneSide.Side_Planar: BspNode front, back; if (info.wasPlanar == true) { Debug.Log("Reentering Planar Nodes!"); } // set operation infos info.wasPlanar = true; info.backNode = null; info.processingBack = false; if (Vector3.Dot(inFace.GetPlane().normal, inNode.plane.normal) >= 0.0f) { // same order as we face in the same order front = inNode.front; back = inNode.back; // we are for now outside (as we are looking outside) info.planarSide = SIDE_Outside; } else { // reverse order as we are facing in the opposite direction front = inNode.back; back = inNode.front; // we are now inside as we are looking to the inside info.planarSide = SIDE_Inside; } // we are leaf node (coplanar face) if (front == null && back == null) { // set leaf stuff info.leafNode = inNode; info.leafLocation = BspNode.EBspLocation.BspLocation_Planar; // process node info.processingBack = true; // process face ProcessFace(inFace, InverseSide(info.planarSide), info); // stop loop inNode = null; } else if (front == null && back != null) { // only back nodes info.processingBack = true; // process back inNode = back; } else { // tread like we were on front side (maybe we do have a back node) info.processingBack = false; // remember back node info.backNode = back; // process front inNode = front; } break; } } }
// Split this Face with given Plane public bool Split(Plane inPlane, out CSGFace outFront, out CSGFace outBack) { outFront = new CSGFace(); outBack = new CSGFace(); float[] distance = new float[vertices.Length + 1]; EPlaneSide[] side = new EPlaneSide[vertices.Length + 1]; for (int i = 0; i < vertices.Length; ++i) { distance[i] = inPlane.GetDistanceToPoint(vertices[i]); side[i] = Side(inPlane, vertices[i]); } distance[vertices.Length] = distance[0]; side[vertices.Length] = side[0]; for (int i = 0; i < vertices.Length; ++i) { // if we lie on plane, add them to both if (side[i] == EPlaneSide.Side_Planar) { outFront.AddVertex(vertices[i], uv[i]); outBack.AddVertex(vertices[i], uv[i]); // nothing todo with this vertex continue; } // if we are on the front, add it to front face if (side[i] == EPlaneSide.Side_Front) { outFront.AddVertex(vertices[i], uv[i]); } // if we are on the back, add it to the back side else if (side[i] == EPlaneSide.Side_Back) { outBack.AddVertex(vertices[i], uv[i]); } // check if the next vertex is planar or on the same side, then we do not split if (side[i + 1] == EPlaneSide.Side_Planar || side[i] == side[i + 1]) { continue; } // create split point Vector3 nextVector = vertices[(i + 1) % vertices.Length]; Vector2 nextUV = uv[(i + 1) % uv.Length]; Vector3 newVector, newUV; // if we were on the front if (side[i] == EPlaneSide.Side_Front) { float t = distance[i] / (distance[i] - distance[i + 1]); newVector = vertices[i] + t * (nextVector - vertices[i]); newUV = uv[i] + t * (nextUV - uv[i]); } else // back side... { float t = distance[i + 1] / (distance[i + 1] - distance[i]); newVector = nextVector + t * (vertices[i] - nextVector); newUV = nextUV + t * (uv[i] - nextUV); } // split points are added // add to front outFront.AddVertex(newVector, newUV); // add to back outBack.AddVertex(newVector, newUV); } // Debugging checks if (outFront.vertices.Length < 3 || outBack.vertices.Length < 3) { Debug.Log("Degenerate Faces"); } // todo... outFront.material = material; // outBack.material = material; return(true); }
// bool FindSplitter(List <CSGFace> inFaces, out CSGFace outFace) { int increase = 1; int bestValue = 9999999; // reset out face... outFace = null; // setup optimization... switch (bspOptm) { case BspOptm_Worse: increase = Mathf.Max(1, inFaces.Count / 24); break; case BspOptm_Average: increase = Mathf.Max(1, inFaces.Count / 12); break; case BspOptm_Best: default: increase = 1; break; } // find best splitter plane for (int i = 0; i < inFaces.Count; i += increase) { // statistics int numSplits = 0, numFront = 0, numBack = 0, numPlanar = 0; // CSGFace splitterFace = inFaces[i] as CSGFace; // Plane splitterPlane = splitterFace.GetPlane(); // sort all faces to side where it lies... for (int j = 0; j < inFaces.Count; ++j) { CSGFace.EPlaneSide side = (inFaces[j] as CSGFace).Side(splitterPlane); switch (side) { case CSGFace.EPlaneSide.Side_Front: numFront++; break; case CSGFace.EPlaneSide.Side_Back: numBack++; break; case CSGFace.EPlaneSide.Side_Planar: numPlanar++; break; case CSGFace.EPlaneSide.Side_Split: numSplits++; break; default: //ERROR Debug.DebugBreak(); break; } } // int val = numSplits * 5 + Mathf.Abs(numFront - numBack) + numPlanar; if (val < bestValue) { bestValue = val; outFace = splitterFace; } } // if we have a face found, return true return(outFace != null); }
/* Intersection * * * // Master Faces against Slave Tree * private void AdditiveMaster( Face inFace, EPolySide inSide, OperationInfo info ) * { * Debug.Log( inSide ); * * switch( inSide ) * { * case EPolySide.PolySide_Outside: * case EPolySide.PolySide_Planar_Front: * // discard original node * currentNode.flags |= BspNode.BspFlags_IsDestroyed; * break; * case EPolySide.PolySide_Inside: * case EPolySide.PolySide_Planar_Back: * case EPolySide.PolySide_CoPlanar_Back: * case EPolySide.PolySide_CoPlanar_Front: * // add cutted polygons * if( (inFace.flags & Face.FaceFlags_WasCutted) != 0 ) * BspGen.AddNode( currentNode, BspNode.EBspLocation.BspLocation_Planar, inFace, BspNode.BspFlags_IsNew ); * break; * * } * } * * // Slave Polygons against Master Tree * private void AdditiveSlave( Face inFace, EPolySide inSide, OperationInfo info ) * { * * // Debug.Log( inSide ); * * switch( inSide ) * { * case EPolySide.PolySide_Outside: * case EPolySide.PolySide_CoPlanar_Front: * case EPolySide.PolySide_Planar_Front: * break; * case EPolySide.PolySide_Inside: * case EPolySide.PolySide_Planar_Back: * case EPolySide.PolySide_CoPlanar_Back: * * // add to deferred faces * DeferredFace defFace; * defFace.face = inFace; * defFace.node = currentNode; * deferredFaces.Add( defFace ); * break; * * } * } */ // Callback for Visitor public void AddPlanarFace(CSGFace inFace) { BspGen.AddNode(currentNode, BspNode.EBspLocation.BspLocation_Planar, inFace, BspNode.BspFlags_IsNew); }
public static BspNode AddNodeRecursive(BspNode inNode, CSGFace inFace, int inFlags) { while (inNode != null) { CSGFace.EPlaneSide planeSide = inFace.Side(inNode.plane); switch (planeSide) { case CSGFace.EPlaneSide.Side_Front: if (inNode.front == null) { return(AddNode(inNode, BspNode.EBspLocation.BspLocation_Front, inFace, inFlags)); } inNode = inNode.front; break; case CSGFace.EPlaneSide.Side_Back: if (inNode.back == null) { return(AddNode(inNode, BspNode.EBspLocation.BspLocation_Back, inFace, inFlags)); } inNode = inNode.back; break; case CSGFace.EPlaneSide.Side_Planar: return(AddNode(inNode, BspNode.EBspLocation.BspLocation_Planar, inFace, inFlags)); case CSGFace.EPlaneSide.Side_Split: CSGFace frontFace, backFace; inFace.Split(inNode.plane, out frontFace, out backFace); if (inNode.front == null) { AddNode(inNode, BspNode.EBspLocation.BspLocation_Front, frontFace, inFlags); } else { AddNodeRecursive(inNode.front, frontFace, inFlags); } if (inNode.back == null) { AddNode(inNode, BspNode.EBspLocation.BspLocation_Back, inFace, inFlags); } else { AddNodeRecursive(inNode.back, backFace, inFlags); } inNode = null; break; } } // happens when face get splitted... return(null); }
// private void ProcessFace(CSGFace inFace, int inNodeSide, OperationInfo info) { EPolySide polySide; // never on a planar node, really easy if (info.wasPlanar == false) { // set polyside polySide = (inNodeSide == SIDE_Outside) ? EPolySide.PolySide_Outside : EPolySide.PolySide_Inside; // RouteOper(null, inFace, polySide, info); } else if (info.processingBack) { // if (inNodeSide == info.planarSide) { polySide = (inNodeSide == SIDE_Inside) ? EPolySide.PolySide_Planar_Inside : EPolySide.PolySide_Planar_Outside; } else { polySide = (info.planarSide == SIDE_Inside) ? EPolySide.PolySide_CoPlanar_Inside : EPolySide.PolySide_CoPlanar_Outside; } RouteOper(null, inFace, polySide, info); } else { // int backNodeSide = InverseSide(info.planarSide); // info.planarSide = inNodeSide; // back node is empty if (info.backNode == null) { // back tree is empty inNodeSide = backNodeSide; // back node is empty if (inNodeSide == info.planarSide) { polySide = (inNodeSide == SIDE_Inside) ? EPolySide.PolySide_Planar_Inside : EPolySide.PolySide_Planar_Outside; } else { polySide = (info.planarSide == SIDE_Inside) ? EPolySide.PolySide_CoPlanar_Inside : EPolySide.PolySide_CoPlanar_Outside; } RouteOper(null, inFace, polySide, info); } else { info.processingBack = true; // TODO: conversion inNodeSide = backNodeSide; // PerformNode(info.backNode, inFace, inNodeSide, info); } } }
// void Partition(BspNode inNode, List <CSGFace> inFaces) { List <CSGFace> frontFaces = new List <CSGFace>(); List <CSGFace> backFaces = new List <CSGFace>(); CSGFace nodeFace; // find best splitter plane for this bool ret = FindSplitter(inFaces, out nodeFace); // return has to be true!!! if (ret == false) { Debug.DebugBreak(); Debug.Log("Error processing Mesh!"); return; } // setup node inNode.front = null; inNode.back = null; inNode.planar = null; inNode.face = nodeFace; inNode.plane = nodeFace.GetPlane(); // split remaining faces into lists for (int i = 0; i < inFaces.Count; ++i) { // get face CSGFace face = inFaces[i] as CSGFace; // do not process our self if (face == nodeFace) { continue; } CSGFace.EPlaneSide side = face.Side(inNode.plane); switch (side) { case CSGFace.EPlaneSide.Side_Front: frontFaces.Add(face); break; case CSGFace.EPlaneSide.Side_Back: backFaces.Add(face); break; case CSGFace.EPlaneSide.Side_Planar: // get last planar node BspNode lastPlanar = inNode; while (lastPlanar.planar != null) { lastPlanar = lastPlanar.planar; } // create new planar node BspNode planar = lastPlanar.planar = new BspNode(); // setup planar node planar.front = null; planar.back = null; planar.planar = null; planar.face = face; planar.plane = face.GetPlane(); break; case CSGFace.EPlaneSide.Side_Split: // TODO... CSGFace front, back; // split face into two parts... ret = face.Split(inNode.plane, out front, out back); if (ret == false) { Debug.DebugBreak(); } // add to front and back frontFaces.Add(front); backFaces.Add(back); break; } } // optimizing a bit, clear in array list as we do not need it any more inFaces.Clear(); // process front faces if (frontFaces.Count > 0) { inNode.front = new BspNode(); // partition front faces Partition(inNode.front, frontFaces); } // process back faces if (backFaces.Count > 0) { inNode.back = new BspNode(); // partition back faces Partition(inNode.back, backFaces); } }
public static void MergeCoplanars(BspNode inNode) { // proceed front nodes if (inNode.front != null) { MergeCoplanars(inNode.front); } // proceed back nodes if (inNode.back != null) { MergeCoplanars(inNode.back); } // first try to merge other co planar nodes if (inNode.planar != null) { MergeCoplanars(inNode.planar); } // bool tryToMerge = (inNode.flags & BspNode.BspFlags_IsDestroyed) == 0; while (tryToMerge) { // get planar node BspNode planarNode = inNode.planar; // assume we are done tryToMerge = false; // get through all planar nodes while (planarNode != null) { // if we are destroyed, proceed to next if ((planarNode.flags & BspNode.BspFlags_IsDestroyed) != 0) { // proceed to next planarNode = planarNode.planar; continue; } CSGFace thisFace = inNode.face; CSGFace otherFace = planarNode.face; // are we facing the same direction if (Vector3.Dot(thisFace.GetPlane().normal, otherFace.GetPlane().normal) > 0.995f) { // result CSGFace merged; // if (thisFace.Merge(otherFace, out merged)) { // replace this face with merged thisFace = merged; // set other node as destroyed planarNode.flags |= BspNode.BspFlags_IsDestroyed; // retry to merge tryToMerge = true; } } // proceed to next planarNode = planarNode.planar; } } }
public bool Merge(CSGFace inOther, out CSGFace outFace) { outFace = null; // do not share same material if (material != inOther.material || vertices.Length < 1) { return(false); } Vector3 p1, p2; Vector3 p3, p4; int i = 0, j = 0; // just to fix compiler error p1 = vertices[0]; p2 = vertices[1 % vertices.Length]; // check if we share an edge for (i = 0; i < vertices.Length; ++i) { // get edge p1 = vertices[i]; p2 = vertices[(i + 1) % vertices.Length]; // go through all edges of other face for (j = 0; j < inOther.vertices.Length; ++j) { // get other edge p3 = inOther.vertices[j]; p4 = inOther.vertices[(j + 1) % inOther.vertices.Length]; // check if we are sharing an edge if (p1.Equals(p4) && p2.Equals(p3)) { break; } } // found edge if (j < inOther.vertices.Length) { break; } } // no edge found if (i == vertices.Length) { return(false); } // ... Vector3 back = vertices[(i + vertices.Length - 1) % vertices.Length]; Vector3 delta = p1 - back; Vector3 normal = Vector3.Cross(GetPlane().normal, delta); normal.Normalize(); back = inOther.vertices[(j + 2) % inOther.vertices.Length]; delta = back - p1; float dot = Vector3.Dot(delta, normal); // not a convex polygon if (dot > GlobalSettings.Epsilonf) { return(false); } // if they are co linear bool keep1 = (dot < -GlobalSettings.Epsilonf); // ... back = vertices[(i + 2) % vertices.Length]; delta = back - p2; normal = Vector3.Cross(GetPlane().normal, delta); normal.Normalize(); back = inOther.vertices[(j + inOther.vertices.Length - 1) % inOther.vertices.Length]; delta = back - p2; dot = Vector3.Dot(delta, normal); // not convex if (dot > GlobalSettings.Epsilonf) { return(false); } bool keep2 = (dot < -GlobalSettings.Epsilonf); bool keep = false; // create out face outFace = new CSGFace(); outFace.flags = flags; outFace.material = material; // copy vertices from this for (int k = (i + 1) % vertices.Length; k != i; k = (k + 1) % vertices.Length) { if (!keep && k == (i + 1) % vertices.Length && !keep2) { continue; } // copy vector outFace.AddVertex(vertices[k], uv[k]); } // copy vertices from other for (int k = (j + 1) % inOther.vertices.Length; k != j; k = (k + 1) % inOther.vertices.Length) { if (!keep && k == (j + 1) % inOther.vertices.Length && !keep1) { continue; } outFace.AddVertex(inOther.vertices[k], inOther.uv[k]); } return(true); }