#include <iostream>
#include <cstdio>
#include <vector>
#include <complex>
#include <cmath>
#include <set>
#include <algorithm>
using namespace std;
// * XY座標
#define X real()
#define Y imag()

// * 点の表現
typedef complex<double> P;

// * 許容する誤差ε
const double EPS = 1e-10;
// * Infinity
const double INF = 1e12;
// * 円周率
const double PI = acos(-1.0);
// * Infinity 2
const P INF_P(INF,INF);

// * complex<double> の順序を定義する
namespace std {
	bool operator < (const P& a, const P& b) {
		return (real(a) != real(b))? real(a) < real(b) : imag(a) < imag(b);
	}
}

// * 点座標のデバッグ出力
void print(P p) {
	cout << "point" << p << ";" << endl;
}

// * 2乗する
double sq(double x) {
	return x * x;
}

// * 2点間の距離
double dist(P p1, P p2) {
	return abs(p1 - p2);
}

// * 2つのスカラーが等しいかどうか
bool equal(double a, double b) {
	return ( fabs( a - b ) < EPS );
}
// * 2つのベクトルが等しいかどうか
bool eq_v(P a, P b) {
	return ( equal( a.X, b.X ) && equal( a.Y, b.Y ) );
}

// ベクトルaの単位ベクトルを求める
P unit(P a) {
	return a / abs(a);
}

// ベクトルaの法線ベクトルを求める
vector<P> normal(P a) {
	vector<P> vp;
	vp.push_back( a * P(0,  1) );
	vp.push_back( a * P(0, -1) );
	return vp;
}

// ベクトル a の単位法線ベクトルを求める
vector<P> normal_unit(P a) {
	vector<P> vp;
	vp.push_back( (a * P(0,  1)) / abs(a) );
	vp.push_back( (a * P(0, -1)) / abs(a) ) ;
	return vp;
}

// * 内積 (dot product) : a・b = |a||b|cosΘ
double dot(P a, P b) {
	return real( conj(a) * b );
}

// * 外積 (cross product) : a×b = |a||b|sinΘ
double cross(P a, P b) {
	return imag( conj(a) * b ) ;
}

// CCW : 反時計回り (Counter Clock Wise)
// CW : 時計回り (Clock Wise)
enum {CCW=1, CW=-1, ON=0};
// * 3点がどちら回りであるか返す, 3点が1直線に乗っているときは ON を返す
int ccw(const P &a, P b, P c) {
	b-=a, c-=a;
	double rotdir = cross(b,c);
	if( rotdir >  EPS ) return CCW;
	if( rotdir < -EPS ) return CW;
	return ON;
}

// 線分の表現
struct Segment{
	// 点 a, b を端点とする線分
	P a, b;
	// コンストラクタで初期化
	Segment(P a_, P b_) {
		// x 座標の小さいほうの点を 点 a とする. (不要なときはこの1行を削除すること!!!)
		if( b_ < a_ ) swap(a_,b_);
		a = a_; b = b_;
	}
	// 線分の長さを返す
	double length() { return abs(a-b); }
	// 中点を返す
	P mid() { return P( (a.X+b.X)/2.0 , (a.Y+b.Y)/2.0 ); }
	// 点 p が線分上にあるかどうか
	bool contain(P p) { return (abs(a-p) + abs(p-b) < abs(a-b) + EPS); }
	// 線分と点 p の距離
	double distance(P p) {
		if( dot(b-a, p-a) < EPS ) return abs(p-a);
		if( dot(a-b, p-b) < EPS ) return abs(p-b);
		return abs(cross(b-a, p-a)) / abs(b-a);
	}
	// 線分の交差判定
	bool is_intersection(const Segment& s) {
		return ( cross(b-a, s.a-a) * cross(b-a, s.b-a) < EPS ) &&
			   ( cross(s.b-s.a, a-s.a) * cross(s.b-s.a, b-s.a) < EPS );
	}
	// 線分の交差判定と交点計算
	bool intersection(Segment s, P& p) {
		bool result = is_intersection( s );
		if( result ) {
			double d1 = abs( cross(s.b-s.a, a-s.a) );
			double d2 = abs( cross(s.b-s.a, b-s.a) );
			
			if( abs(d1) < EPS && abs(d2) < EPS ) { // 2つの線分が同じ直線上にあるとき
				if( this->contain(s.a) ) {
					p = s.a;
				}else if( this->contain(s.b) ) {
					p = s.b;
				}else if( s.contain( a ) ) {
					p = a;
				}else if( s.contain( b ) ) {
					p = b;
				}else {
					p = INF_P;
				}
			}else if( abs(d1) < EPS ) {
				p = INF_P;
			}else {
				double t = d1 / (d1 + d2);
				p = a + (b-a) * t;
			}
		}else{
			p = INF_P;
		}
		return result;
	}
	// 線分と線分の距離
	double distance(Segment s) {
		if( this->is_intersection( s ) ) { // 交差するとき	
			double d1 = abs( cross(s.b-s.a, a-s.a) );
			double d2 = abs( cross(s.b-s.a, b-s.a) );
			
			if( abs(d1) < EPS && abs(d2) < EPS ) { // 2つの線分が同じ直線上にあるとき
				if( this->contain(s.a) || this->contain(s.b) ) {
					return 0.0;
				}else if( s.contain( a ) || s.contain( b ) ) {
					return 0.0;
				}
			}else{
				return 0.0;
			}
		}
		double A = this->distance(s.a);
		double B = this->distance(s.b);
		double C = s.distance( (*this).a );
		double D = s.distance( (*this).b );
		return min( min(A,B) , min(C,D) );
	}
	// デバッグ出力
	void print() { printf("line(%f,%f,%f,%f); \n", a.X, a.Y, b.X, b.Y ); }
};
// Segment の順序を定義 (x座標の小さい端点で比較)
bool operator < (const Segment& s1, const Segment& s2) { return s1.a < s2.a; }

// 直線の表現 ((*this)[0]) と ((*this)[1]) を通る直線
struct Line : public vector<P> {
	Line(const P &a, const P &b) {
		push_back(a); push_back(b);
	}
	Line(vector<P> v){
		push_back(v[0]); push_back(v[1]);
	}
	Line(){}
	// 2直線の直交判定
	bool orthogonal(const Line& l) {
		return equal( dot( (*this)[0] - (*this)[1] , l[0] - l[1] ) , 0.0 );
	}
	// 2直線の平行判定
	bool parallel(const Line& l) {
		return equal( cross( (*this)[0] - (*this)[1] , l[0] - l[1] ), 0.0 );
	}
	// 点 p が直線上に乗っているか
	bool contain(P p) {
		return equal( cross( (*this)[1] - (*this)[0] , p - (*this)[0] ) , 0.0 );
	}
	// 直線と点 p の距離
	double distance(P a) {
		P p1 = (*this)[0];
		P p2 = (*this)[1];
		return abs( cross( p2 - p1 , a - p1) ) / abs(p2 - p1);
	}
	// 直線の交差判定
	bool is_intersection(const Line& l) {
		return !equal( cross( (*this)[0] - (*this)[1], l[0] - l[1] ) , 0.0 );
	}
	// 直線の交差判定と交点計算
	bool intersection(Line l, P& p) {
		bool result = this->is_intersection( l );
		if( result ) {
			P a = (*this)[1] - (*this)[0];
			P b = l[1] - l[0];
			p = (*this)[0] + a * cross(b, l[0] - (*this)[0]) / cross(b, a);
		}else {
			p = INF_P;
		}
		return result;
	}
	// 点 (*this)[0] から 点 (*this)[1] への角度を返す [0,2π]
	double get_dir() {
		P p = (*this)[0] - (*this)[1];
		double angle = atan2( p.Y , p.X );
		if( p.X < 0 ) angle += PI;
		if( angle < 0 ) angle += 2.0 * PI;
		return angle;
	}
	// 双対変換
	P dual(const Line &l) {
		const P p = l[0], q = l[1];
		return P( imag(p - q) / real(p - q), cross(p, q) / real(q - p) );
	}
	// 点を線対称に移動させる
	P symmetry(P a) {
		double x,y;
		double x1 = (*this)[0].X;
		double x2 = (*this)[1].X;
		double y1 = (*this)[0].Y;
		double y2 = (*this)[1].Y;
		double xq = a.X;
		double yq = a.Y;
		if( x2 == x1 ) {
			x = -xq;
			y = yq;
		}else{
			double m = (y2 - y1) / (x2 - x1);
			double c = y2 - m * x2;
			x = (2 * m * yq - xq * (m * m - 1) - 2 * m * c) / (m * m + 1);
		    y = (2 * m * xq + yq * (m * m - 1) + 2 * c) / (m * m + 1);
		}
		return P(x,y);
	}
	// デバッグ出力
	void print() {
		P p1 = (*this)[0];
		P p2 = (*this)[1];
		double dx = p1.X - p2.X;
		double dy = p1.Y - p2.Y;
		::print( p1 );
		::print( p2 );
		p1.X += 100 * dx;
		p1.Y += 100 * dy;
		p2.X -= 100 * dx;
		p2.Y -= 100 * dy;
		Segment s( p1 , p2 );
		s.print();
	}
};

// 三角形の表現
struct Triangle{
private:
	// 余弦定理から 3 つの角の角度を求める (内部の関数で使用)
    double law_of_cos(double a, double b, double c) {
        return acos( (b*b + c*c - a*a) / (2.0 * b * c) );
    }
public:
	// 3 つの頂点
	P a, b, c;
    // 3 つの辺の長さ
    double edgeA, edgeB, edgeC;
    // 3 つの角の角度 (ラジアン)
    double angleA,angleB,angleC;
    
    // コンストラクタで初期化
    Triangle(P p1, P p2, P p3) {
        a = p1;
        b = p2;
        c = p3;
        edgeB = abs(c-a);
        edgeA = abs(b-c);
        edgeC = abs(a-b);
        angleA = law_of_cos( edgeA, edgeB, edgeC );
        angleB = law_of_cos( edgeB, edgeC, edgeA );
        angleC = law_of_cos( edgeC, edgeA, edgeB );
    }
    // 三角形の面積を返す (ヘロンの公式)
	double get_area() {
		double s = 0.5 * (edgeA + edgeB + edgeC);
		return sqrt( s * (s - edgeA) * (s - edgeB) * (s - edgeC) );
	}
	// 点 p が三角形の辺上にあるかどうか
	bool on_boundary(P p) {
		Segment AB(a,b), BC(b,c), CA(c,a);
		return (AB.contain( p ) || BC.contain( p ) || CA.contain( p ) );
	}
	// 点 p が三角形の内部にあるかどうか ( AOJ 0012 で検証済み )
	// 点 p が三角形の辺上 or 頂点上 にあるときは誤差で正しい結果が返ってこないので注意!!!
	bool is_inside(P p) {
		// 辺上にあるとき
		if( this->on_boundary(p) ) return true;
		
		Triangle t1( a , b , p );
		Triangle t2( b , c , p );
		Triangle t3( c , a , p );
		// 角APB + 角BPB + 角CPA が 360度なら 点 p は三角形の内部にある
		return equal( t1.angleC + t2.angleC + t3.angleC , 2.0*PI );
	}
	// デバッグ出力
	void print() {
		Segment AB(a,b);
		Segment BC(b,c);
		Segment CA(c,a);
		AB.print();
		BC.print();
		CA.print();
	}
};

// 多角形の表現 (凸多角形と仮定する)
struct Polygon{
	// 頂点の集まり
	vector<P> g;
	// コンストラクタで初期化
	Polygon(vector<P> g_){ g = g_; }
	Polygon(){}
	// 点 p が凸多角形の内部にあるかどうか
	bool is_inside(P p) {
		for(int i=2 ; i < g.size() ; i++ ) {
			Triangle t( g[0] , g[i-1] , g[i] );
			if( t.is_inside( p ) ) {
				return true;
			}
		}
		return false;
	}
	// 点 p が凸多角形の辺上にあるかどうか
	bool on_boundary(P p) {
		for(int i=0 ; i < g.size() ; i++ ) {
			Segment s( g[i] , g[(i+1) % g.size()] );
			if( s.contain( p ) ) return true;
		}
		return false;
	}
	// デバッグ用出力
	void print() {
		for(int i=0 ; i < g.size() ; i++ ) {
			Segment s( g[i] , g[(i+1) % g.size()] );
			s.print();
		}
	}
};

// n 個の点の集まりから凸包を求める
Polygon convex_full(vector<P> ps) {
	int n = ps.size();
	sort( ps.begin() , ps.end() );
	
	// 上部を求める
    vector<P> ch_u( n );
    int n_u = 0;
    for(int i = n-1 ; i >= 0 ; i-- ) {
        ch_u[n_u++] = ps[i];
        while( n_u >= 3 && ccw( ch_u[n_u-3] , ch_u[n_u-2] , ch_u[n_u-1] ) != CCW ) {
            ch_u[n_u-2] = ch_u[n_u-1];
            n_u--;
        }
    }
 
    vector<P> ch_l( n );
    int n_l = 0;
    for(int i = 0 ; i < n ; i++ ) {
        ch_l[n_l++] = ps[i];
        while( n_l >= 3 && ccw( ch_l[n_l-3] , ch_l[n_l-2] , ch_l[n_l-1]) != CCW ) {
            ch_l[n_l-2] = ch_l[n_l-1];
            n_l--;
        }
    }
 
    Polygon ch;
    for(int i=0 ; i<n_u-1 ; i++ ) ch.g.push_back(ch_u[i]);
    for(int i=0 ; i<n_l-1 ; i++ ) ch.g.push_back(ch_l[i]);
    return ch;
}

bool is_point_online(P a,P b,P c){
  return ( abs(a-c)+abs(b-c) < abs(a-b)+EPS);
}

bool isl(P a1,P a2,P b1,P b2){
	if( is_point_online(a1,a2,b1) )
		return true;
	if( is_point_online(a1,a2,b2) )
		return true;
	if( is_point_online(b1,b2,a1) )
		return true;
	if( is_point_online(b1,b2,a2) )
		return true;
	if( ( cross(a2-a1,b1-a1) * cross(a2-a1,b2-a1)<-EPS ) && ( cross(b2-b1,a1-b1)*cross(b2-b1,a2-b1)<-EPS))
		return true;
	else
		return false;
}

bool intersected(Polygon g1 , Polygon g2){
	bool flag1 = g2.is_inside( g1.g[0] ) && g2.is_inside( g1.g[1] );
	bool flag2 = g2.is_inside( g1.g[2] ) && g2.is_inside( g1.g[3] );
	if( flag1 && flag2 ){
		return true;
	}
	flag1 = g1.is_inside( g2.g[0] ) && g1.is_inside( g2.g[1] );
	flag2 = g1.is_inside( g2.g[2] ) && g1.is_inside( g2.g[3] );
	if( flag1 && flag2 ){
		return true;
	}
	
	for(int i=0 ; i < 4 ; i++ ){
		P p1 = g1.g[i];
		P p2 = g1.g[(i+1)%4];
		for(int j=0 ; j < 4 ; j++ ){
			P p3 = g2.g[i];
			P p4 = g2.g[(i+1)%4];
			if( isl(p1,p2,p3,p4) ){
				return true;
			}
		}
	}
	return false;
}

// Union-Find
struct union_find{
	vector<int> par, rank;
	
	// コンストラクタで初期化
	union_find(int n){
		par = vector<int>( n );
		rank = vector<int>( n , 0 );
		for(int i=0 ; i < n ; i++ ){
			par[i] = i;
		}
	}
	// x の root を返す
	int find(int x){
		return ( par[x] == x )? x : (par[x] = find(par[x]) ) ;
	}
	// x と y の属する集合を併合する
	void unite(int x, int y){
		x = find(x);
		y = find(y);
		if( x == y ) return;
		
		if( rank[x] < rank[y] ){
			par[x] = y;
		}else{
			par[y] = x;
			if( rank[x] == rank[y] )
				rank[x]++;
		}
	}
	// x と y が同じ集合に属するかどうか
	bool same(int x, int y){
		return ( find(x) == find(y) );
	}
};

int main(){
	int n;
	while( cin >> n , n ){
		
		for(int i_=0 ; i_ < n ; i_++ ){
			int m;
			cin >> m;
			
			vector<Polygon> v;
			for(int j_=0 ; j_ < m ; j_++ ){
				int xa, ya, xb, yb, xc, yc, xd, yd;
				cin >> xa >> ya >> xb >> yb >> xc >> yc >> xd >> yd;
				P a(xa,ya), b(xb,yb), c(xc,yc), d(xd,yd);
				vector<P> vp;
				vp.push_back( d );
				vp.push_back( c );
				vp.push_back( b );
				vp.push_back( a );
				v.push_back( Polygon(vp) );			
			}
			
			if( v.size() == 1 ){
				cout << 1 << endl;
				continue;
			}
			
			union_find uf( v.size() );
			for(int i=0 ; i < v.size() ; i++ ){
				for(int j=i+1 ; j < v.size() ; j++ ){
					// 2つの四角形がくっついていたら
					if( intersected( v[i] , v[j] ) ){
						uf.unite( i , j );
					}
				}
			}
			// 連結成分の個数を数える
			set<int> s;
			for(int i=0 ; i < v.size() ; i++ ){
				s.insert( uf.find(i) );
			}
			cout << s.size() << endl;
		}
	}
}