# this contains cone, spherecone, and cylinder code translated and modified from Barbier and
# Galin 2004's example code
# see /u/ramcd/spatial/experiments/one_time_tests/2012-06-28/cone.cpp
# on 7 june 2012, the original code was available online at
#   http://jgt.akpeters.com/papers/BarbierGalin04/Cone-Sphere.zip

import bisect
cimport cython

cdef extern from "math.h":
    double sqrt(double)
    double fabs(double)


# I'm manually doing all maxes and mins here because I thought it would help
# eliminate round-off issues at the surface. it doesn't

#cdef double max(double a, double b):
#    return a if a > b else b

#cdef double min(double a, double b):
#    return a if a < b else b

cdef class Complement:
    def __init__(self, obj):
        self.obj = obj
    def __repr__(self):
        return 'Complement(%r)' % self.obj
    def distance(self, px, py, pz):
        return -self.obj.distance(px, py, pz)
    def starting_points(self, xs, ys, zs):
        return self.obj.starting_points(xs, ys, zs)
    property primitives:
        def __get__(self): return [self.obj]


cdef class Union:
    cdef list objects
    def __init__(self, list objects):
        self.objects = objects
    def __repr__(self):
        return 'Union(%r)' % self.objects
    @cython.boundscheck(False)
    @cython.wraparound(False)
    cpdef double distance(self, px, py, pz):
        # CTNG:uniondist
        dists = [obj.distance(px, py, pz) for obj in self.objects]
        cdef double d, d2
        d = dists[0]
        for d2 in dists:
            if d2 < d: d = d2
        return d
    cpdef list starting_points(self, xs, ys, zs):
        return sum([c.starting_points(xs, ys, zs) for c in self.objects], [])
    property primitives:
        def __get__(self): return sum([obj.primitives for obj in self.objects], [])


cdef class Intersection:
    cdef list objects
    def __init__(self, list objects):
        self.objects = objects
    def __repr__(self):
        return 'Intersection(%r)' % self.objects
    @cython.boundscheck(False)
    @cython.wraparound(False)        
    cpdef double distance(self, px, py, pz):
        # CTNG: intdist
        dists = [obj.distance(px, py, pz) for obj in self.objects]
        cdef double d, d2
        d = dists[0]
        for d2 in dists:
            if d2 > d: d = d2
        return d
    cpdef list starting_points(self, xs, ys, zs):
        return sum([c.starting_points(xs, ys, zs) for c in self.objects], [])
    property primitives:
        def __get__(self): return sum([obj.primitives for obj in self.objects], [])



cdef class Plane:
    cdef double d, mul, nx, ny, nz, px, py, pz
    def __init__(self, double px, double py, double pz, double nx, double ny, double nz):
        """(px, py, pz) -- a point; (nx, ny, nz) -- the normal vector"""
        self.d = - (nx * px + ny * py + nz * pz)
        self.mul = 1. / sqrt(nx ** 2 + ny ** 2 + nz ** 2)
        self.nx = nx
        self.ny = ny
        self.nz = nz
        self.px = px
        self.py = py
        self.pz = pz
    property primitives:
        def __get__(self): return []
    def __repr__(self):
        return 'Plane(%g, %g, %g, %g, %g, %g)' % (self.px, self.py, self.pz, self.nx, self.ny, self.nz)
    cpdef double distance(self, double x, double y, double z):
        return (self.nx * x + self.ny * y + self.nz * z + self.d) * self.mul
    cpdef list starting_points(self, xs, ys, zs):
        return [(bisect.bisect_left(xs, self.px), bisect.bisect_left(ys, self.py), bisect.bisect_left(zs, self.pz))]


cdef class Sphere:
    cdef double x, y, z, r, _xlo, _xhi, _ylo, _yhi, _zlo, _zhi
    cdef list clips
    property primitives:
        def __get__(self): return [self]

    def __init__(self, double x, double y, double z, double r):
        self.x, self.y, self.z, self.r = x, y, z, r
        self._xlo, self._xhi = x - r, x + r
        self._ylo, self._yhi = y - r, y + r
        self._zlo, self._zhi = z - r, z + r
        self.clips = []
    def __repr__(self):
        if self.clips:
            return 'Sphere(%g, %g, %g, %g; clips=%r)' % (self.x, self.y, self.z, self.r, self.clips)
        else:
            return 'Sphere(%g, %g, %g, %g)' % (self.x, self.y, self.z, self.r)
    property xlo:
        def __get__(self): return self._xlo
    property xhi:
        def __get__(self): return self._xhi
    property ylo:
        def __get__(self): return self._ylo
    property yhi:
        def __get__(self): return self._yhi
    property zlo:
        def __get__(self): return self._zlo
    property zhi:
        def __get__(self): return self._zhi
    cpdef double distance(self, double x, double y, double z):
        d = sqrt((x - self.x) ** 2 + (y - self.y) ** 2 + (z - self.z) ** 2) - self.r
        old_d = d
        for clip in self.clips:
            d = max(d, clip.distance(x, y, z))
        return d
    def starting_points(self, xs, ys, zs):
        #for theta in numpy.arange(0, 2 * numpy.pi, 10):
        # TODO: this only works right if the entire object is inside the domain
        return sum([c.starting_points(xs, ys, zs) for c in self.clips], [(bisect.bisect_left(xs, self.x - self.r), bisect.bisect_left(ys, self.y), bisect.bisect_left(zs, self.z))])
    cpdef bint overlaps_x(self, double lo, double hi):
        return lo <= self._xhi and hi >= self._xlo
    cpdef bint overlaps_y(self, double lo, double hi):
        return lo <= self._yhi and hi >= self._ylo
    cpdef bint overlaps_z(self, double lo, double hi):
        return lo <= self._zhi and hi >= self._zlo
    def set_clip(self, clips):
        self.clips = clips
    def get_clip(self):
        return self.clips
        


cdef class Cylinder:
    cdef double cx, cy, cz, r, rr, axisx, axisy, axisz, x0, y0, z0, x1, y1, z1, h
    cdef double length, _xlo, _xhi, _ylo, _yhi, _zlo, _zhi
    cdef list neighbors, clips, neighbor_regions
    def __repr__(self):
        if self.clips:
            return 'Cylinder(%g, %g, %g, %g, %g, %g, %g; clips=%r)' % (self.x0, self.y0, self.z0, self.x1, self.y1, self.z1, self.r, self.clips)
        else:
            return 'Cylinder(%g, %g, %g, %g, %g, %g, %g)' % (self.x0, self.y0, self.z0, self.x1, self.y1, self.z1, self.r)
    property xlo:
        def __get__(self): return self._xlo
    property xhi:
        def __get__(self): return self._xhi
    property ylo:
        def __get__(self): return self._ylo
    property yhi:
        def __get__(self): return self._yhi
    property zlo:
        def __get__(self): return self._zlo
    property zhi:
        def __get__(self): return self._zhi

    def __init__(self, double x0, double y0, double z0, double x1, double y1, double z1, double r):
        self.cx, self.cy, self.cz = (x0 + x1) * 0.5, (y0 + y1) * 0.5, (z0 + z1) * 0.5
        self.x0, self.y0, self.z0, self.x1, self.y1, self.z1 = x0, y0, z0, x1, y1, z1
        self.rr = r * r
        self.r = r
        self.axisx, self.axisy, self.axisz = (x1 - x0, y1 - y0, z1 - z0)
        self.length = sqrt(self.axisx ** 2 + self.axisy ** 2 + self.axisz ** 2)
        # normalize the axis
        self.axisx /= self.length
        self.axisy /= self.length
        self.axisz /= self.length
        
        
        self.h = self.length * 0.5
        
        self._xlo = min(x0 - r, x1 - r)
        self._xhi = max(x0 + r, x1 + r)
        self._ylo = min(y0 - r, y1 - r)
        self._yhi = max(y0 + r, y1 + r)
        self._zlo = min(z0 - r, z1 - r)
        self._zhi = max(z0 + r, z1 + r)
        self.neighbors = []
        self.clips = []
        self.neighbor_regions = []
    
    property axislength:
        def __get__(self):
            return self.length

    property _x0:
        def __get__(self):
            return self.x0

    property _y0:
        def __get__(self):
            return self.y0
    property _z0:
        def __get__(self):
            return self.z0
    property _r0:
        def __get__(self):
            return self.r
            
    property _x1:
        def __get__(self):
            return self.x1

    property _y1:
        def __get__(self):
            return self.y1
    property _z1:
        def __get__(self):
            return self.z1
    property _r1:
        def __get__(self):
            return self.r
    property primitives:
        def __get__(self): return [self]

    def get_clip(self):
        return self.clips


    def set_clip(self, clips):
        self.clips = clips
    
    def axis(self):
        return (self.axisx, self.axisy, self.axisz)

    def set_neighbors(self, neighbors, neighbor_regions):
        self.neighbors = neighbors
        self.neighbor_regions = neighbor_regions
    
    cpdef within_core(self, double px, double py, double pz):
        cdef double nx, ny, nz, y, yy, xx
        nx, ny, nz = px - self.cx, py - self.cy, pz - self.cz
        y = abs(self.axisx * nx + self.axisy * ny + self.axisz * nz)
        return y < self.h
        

    def starting_points(self, xs, ys, zs):
        # TODO: this only works right if the entire object is inside the domain
        return sum([c.starting_points(xs, ys, zs) for c in self.clips],
                    [(bisect.bisect_left(xs, self.x0), bisect.bisect_left(ys, self.y0), bisect.bisect_left(zs, self.z0)),
                     (bisect.bisect_left(xs, self.x1), bisect.bisect_left(ys, self.y1), bisect.bisect_left(zs, self.z1))])


    cpdef double distance(self, double px, double py, double pz):
        # CTNG:cyldist
        cdef double nx, ny, nz, y, yy, xx, d
        nx, ny, nz = px - self.cx, py - self.cy, pz - self.cz
        y = abs(self.axisx * nx + self.axisy * ny + self.axisz * nz)
        yy = y * y
        xx = nx * nx + ny * ny + nz * nz - yy
        if y < self.h:
            # this was wrong in my version from 2012-06-28, since did not
            # handle end-caps being closest
            d = max(y - self.h, sqrt(xx) - self.r)
        else:
            y -= self.h
            if xx < self.rr:
                d = y
            else:
                yy = y * y
                x = sqrt(xx) - self.r
                d = sqrt(yy + x * x)
                
        for clip in self.clips:
            d = max(d, clip.distance(px, py, pz))
        return d




        
    cpdef bint overlaps_x(self, double lo, double hi):
        return lo <= self._xhi and hi >= self._xlo
    cpdef bint overlaps_y(self, double lo, double hi):
        return lo <= self._yhi and hi >= self._ylo
    cpdef bint overlaps_z(self, double lo, double hi):
        return lo <= self._zhi and hi >= self._zlo



cdef class SphereCone:
    cdef double x0, y0, z0, r0, x1, y1, z1, r1, rra, rrb, axisx, axisy, axisz, conelength, side1, side2
    cdef double length, _xlo, _xhi, _ylo, _yhi, _zlo, _zhi, ha, hb, hra, hrb
    cdef list clips
    property primitives:
        def __get__(self): return [self]

    def __repr__(self):
        return 'SphereCone(%g, %g, %g, %g, %g, %g, %g, %g)' % (self.x0, self.y0, self.z0, self.r0, self.x1, self.y1, self.z1, self.r1)
        
    def set_clip(self, clips):
        self.clips = clips
    def get_clip(self):
        return self.clips


    def __init__(self, double x0, double y0, double z0, double r0, double x1, double y1, double z1, double r1):
        if r1 > r0:
            x0, y0, z0, r0, x1, y1, z1, r1 = x1, y1, z1, r1, x0, y0, z0, r0
            
        self.x0, self.y0, self.z0, self.r0, self.x1, self.y1, self.z1, self.r1 = x0, y0, z0, r0, x1, y1, z1, r1
        
        self.rra, self.rrb = r0 * r0, r1 * r1
        self.axisx, self.axisy, self.axisz = (x1 - x0, y1 - y0, z1 - z0)
        self.length = sqrt(self.axisx ** 2 + self.axisy ** 2 + self.axisz ** 2)
        # normalize the axis
        self.axisx /= self.length
        self.axisy /= self.length
        self.axisz /= self.length
        """
        rab = r0 - r1
        s = sqrt(self.length ** 2 - rab ** 2)
        self.ha = self.r0 * rab / self.length
        self.hb = self.r1 * rab / self.length
        self.hra = self.r0 * s / self.length
        self.hrb = self.r1 * s / self.length
        self.side1 = - self.ha / self.r0
        self.side2 = self.hra / self.r0
        self.conelength = self.length * self.hra / self.r0
        """
        self.r0 = r0
        self.r1 = r1
        self.rrb = r1 * r1
        self.rra = r0 * r0
        r0b = r0 - r1
        s = sqrt(self.length * self.length - r0b * r0b)
        self.ha = self.r0 * r0b / self.length
        self.hb = self.r1 * r0b / self.length
        self.hra = self.r0 * s / self.length
        self.hrb = self.r1 * s / self.length
        self.side1 = -self.ha / self.r0
        self.side2 = self.hra / self.r0
        self.conelength = self.length * self.hra / self.r0
        
        self._xlo = min(x0 - r0, x1 - r1)
        self._xhi = max(x0 + r0, x1 + r1)
        self._ylo = min(y0 - r0, y1 - r1)
        self._yhi = max(y0 + r0, y1 + r1)
        self._zlo = min(z0 - r0, z1 - r1)
        self._zhi = max(z0 + r0, z1 + r1)
        self.clips = []

    def starting_points(self, xs, ys, zs):
        # TODO: this only works right if the entire object is inside the domain
        return sum([c.starting_points(xs, ys, zs) for c in self.clips], [(bisect.bisect_left(xs, self.x0 - self.axisx * self.r0), bisect.bisect_left(ys, self.y0 - self.axisy * self.r0), bisect.bisect_left(zs, self.z0 - self.axisz * self.r0))])


    cpdef double distance(self, double px, double py, double pz):
        cdef double nx, ny, nz, y, yy, xx, ry, rx, nn, x, d
        nx, ny, nz = px - self.x0, py - self.y0, pz - self.z0
        y = nx * self.axisx + ny * self.axisy + nz * self.axisz
        nn = nx * nx + ny * ny + nz * nz

        yy = y * y
        xx = nn - yy
        # in principle, xx >= 0, however roundoff errors may cause trouble
        if xx < 0: xx = 0
        x = sqrt(xx)
        ry = x * self.side1 + y * self.side2
        if ry < 0:
            d = sqrt(nn) - self.r0
        elif ry > self.conelength:
            y = y - self.length
            yy = y * y
            nn = xx + yy
            d = sqrt(nn) - self.r1
        else:
            rx = x * self.side2 - y * self.side1
            d = rx - self.r0
            
        for clip in self.clips:
            d = max(d, clip.distance(px, py, pz))
        return d


        
    cpdef bint overlaps_x(self, double lo, double hi):
        return lo <= self._xhi and hi >= self._xlo
    cpdef bint overlaps_y(self, double lo, double hi):
        return lo <= self._yhi and hi >= self._ylo
    cpdef bint overlaps_z(self, double lo, double hi):
        return lo <= self._zhi and hi >= self._zlo



cdef class Cone:
    cdef double x0, y0, z0, r0, x1, y1, z1, r1, rra, rrb, axisx, axisy, axisz, conelength, side1, side2
    cdef double length, _xlo, _xhi, _ylo, _yhi, _zlo, _zhi, cx, cy, cz, h
    cdef list neighbors, clips, neighbor_regions
    cdef bint reversed
    property xlo:
        def __get__(self): return self._xlo
    property xhi:
        def __get__(self): return self._xhi
    property ylo:
        def __get__(self): return self._ylo
    property yhi:
        def __get__(self): return self._yhi
    property zlo:
        def __get__(self): return self._zlo
    property zhi:
        def __get__(self): return self._zhi

    def __repr__(self):
        cdef list order
        if self.reversed:
            order = [self.x1, self.y1, self.z1, self.r1, self.x0, self.y0, self.z0, self.r0]
        else:
            order = [self.x0, self.y0, self.z0, self.r0, self.x1, self.y1, self.z1, self.r1]
        if self.clips:
            return 'Cone(%g, %g, %g, %g, %g, %g, %g, %g; clips=%r)' % tuple(order + [self.clips])
        else:
            return 'Cone(%g, %g, %g, %g, %g, %g, %g, %g)' % tuple(order)
    
    property _x0:
        def __get__(self):
            return self.x1 if self.reversed else self.x0

    property _y0:
        def __get__(self):
            return self.y1 if self.reversed else self.y0
    property _z0:
        def __get__(self):
            return self.z1 if self.reversed else self.z0
    property _r0:
        def __get__(self):
            return self.r1 if self.reversed else self.r0
            
    property _x1:
        def __get__(self):
            return self.x0 if self.reversed else self.x1

    property _y1:
        def __get__(self):
            return self.y0 if self.reversed else self.y1
    property _z1:
        def __get__(self):
            return self.z0 if self.reversed else self.z1
    property _r1:
        def __get__(self):
            return self.r0 if self.reversed else self.r1

    property axislength:
        def __get__(self):
            return self.length
    property primitives:
        def __get__(self): return [self]


    def __init__(self, double x0, double y0, double z0, double r0, double x1, double y1, double z1, double r1):
        if r1 > r0:
            x0, y0, z0, r0, x1, y1, z1, r1 = x1, y1, z1, r1, x0, y0, z0, r0
            self.reversed = True
        else:
            self.reversed = False
            
        self.x0, self.y0, self.z0, self.r0, self.x1, self.y1, self.z1, self.r1 = x0, y0, z0, r0, x1, y1, z1, r1
        
        if r0 < 0:
            raise Exception('At least one Cone radius must be positive')
        if r1 < 0:
            axisx, axisy, axisz = (x1 - x0, y1 - y0, z1 - z0)
            length = sqrt(axisx ** 2 + axisy ** 2 + axisz ** 2)
            axisx /= length; axisy /= length; axisz /= length
            f = r1 / (r1 - r0)
            x1 -= f * axisx; y1 -= f * axisy; z1 -= f * axisz; r1 = 0
        
        self.rra, self.rrb = r0 * r0, r1 * r1
        self.axisx, self.axisy, self.axisz = (x1 - x0, y1 - y0, z1 - z0)
        self.length = sqrt(self.axisx ** 2 + self.axisy ** 2 + self.axisz ** 2)
        # normalize the axis
        self.axisx /= self.length
        self.axisy /= self.length
        self.axisz /= self.length
        
        self.conelength = sqrt((r1 - r0) ** 2 + self.length ** 2)
        self.side1 = (r1 - r0) / self.conelength
        self.side2 = self.length / self.conelength
        
        cdef double rmax = max(r0, r1)
        
        self._xlo = min(x0 - rmax, x1 - rmax)
        self._xhi = max(x0 + rmax, x1 + rmax)
        self._ylo = min(y0 - rmax, y1 - rmax)
        self._yhi = max(y0 + rmax, y1 + rmax)
        self._zlo = min(z0 - rmax, z1 - rmax)
        self._zhi = max(z0 + rmax, z1 + rmax)
        self.neighbors = []
        self.cx, self.cy, self.cz = (x0 + x1) * 0.5, (y0 + y1) * 0.5, (z0 + z1) * 0.5
        self.h = self.length * .5
        self.clips = []
        self.neighbor_regions = []
        
    def set_clip(self, clips):
        self.clips = clips
    def get_clip(self):
        return self.clips

    cpdef within_core(self, double px, double py, double pz):
        cdef double nx, ny, nz, y
        nx, ny, nz = px - self.cx, py - self.cy, pz - self.cz
        y = abs(self.axisx * nx + self.axisy * ny + self.axisz * nz)
        return y < self.h
        

    def axis(self):
        if self.reversed:
            return -self.axisx, -self.axisy, -self.axisz
        else:
            return (self.axisx, self.axisy, self.axisz)

    def starting_points(self, xs, ys, zs):
        # TODO: this only works right if the entire object is inside the domain
        return sum([c.starting_points(xs, ys, zs) for c in self.clips],
                    [(bisect.bisect_left(xs, self.x0), bisect.bisect_left(ys, self.y0), bisect.bisect_left(zs, self.z0)),
                     (bisect.bisect_left(xs, self.x1), bisect.bisect_left(ys, self.y1), bisect.bisect_left(zs, self.z1))])

    cpdef double distance(self, double px, double py, double pz):
        # CTNG:frustumdist
        cdef double nx, ny, nz, y, yy, xx, ry, rx, d
        nx, ny, nz = px - self.x0, py - self.y0, pz - self.z0
        y = nx * self.axisx + ny * self.axisy + nz * self.axisz
        yy = y * y
        xx = nx * nx + ny * ny + nz * nz - yy
        # in principle, xx >= 0, however roundoff errors may cause trouble
        if xx < 0: xx = 0

        if y < 0:
            # always nonnegative distance in this case
            if xx < self.rra:
                d = -y
            else:
                x = sqrt(xx) - self.r0
                d = sqrt(x * x + yy)
        elif xx < self.rrb and y > self.length:
            d = y - self.length
        else:
            x = sqrt(xx) - self.r0
            # y >= 0 always at this point (and if outside, not in the cylinder extending through the small end face)
            ry = x * self.side1 + y * self.side2
            if ry < 0:
                # if ry < 0 (and y > 0 from above), then outside the cone
                d = sqrt(x * x + yy)
            else:
                rx = x * self.side2 - y * self.side1
                if ry > self.conelength and y > self.length:
                    ry -= self.conelength
                    d = sqrt(rx * rx + ry * ry)
                else:
                    d = rx
                    if d < 0:
                        # end faces could be closer than the cone itself
                        d = max(rx, y - self.length)



        for clip in self.clips:
            d = max(d, clip.distance(px, py, pz))
        return d


        
    cpdef bint overlaps_x(self, double lo, double hi):
        return lo <= self._xhi and hi >= self._xlo
    cpdef bint overlaps_y(self, double lo, double hi):
        return lo <= self._yhi and hi >= self._ylo
    cpdef bint overlaps_z(self, double lo, double hi):
        return lo <= self._zhi and hi >= self._zlo


cdef class SkewCone:
    cdef double x0, y0, z0, r0, x1, y1, z1, r1, rra, rrb, axisx, axisy, axisz, conelength, side1, side2
    cdef double length, _xlo, _xhi, _ylo, _yhi, _zlo, _zhi, sx, sy, sz, planed
    property xlo:
        def __get__(self): return self._xlo
    property xhi:
        def __get__(self): return self._xhi
    property ylo:
        def __get__(self): return self._ylo
    property yhi:
        def __get__(self): return self._yhi
    property zlo:
        def __get__(self): return self._zlo
    property zhi:
        def __get__(self): return self._zhi
    property primitives:
        def __get__(self): return [self]

    def get_clip(self):
        # TODO: change this if ever expand to allow clipping
        return []


    def __init__(self, double x0, double y0, double z0, double r0, double x1, double y1, double z1, double r1, double x2, double y2, double z2):
        """(x2, y2, z2) denotes point to skew (x1, y1, z1) to"""


        if r1 > r0:
            x0, y0, z0, r0, x1, y1, z1, r1, x2, y2, z2 = x2, y2, z2, r1, x2 - (x1 - x0), y2 - (y1 - y0), z2 - (z1 - z0), r0, x0, y0, z0
            
        self.x0, self.y0, self.z0, self.r0, self.x1, self.y1, self.z1, self.r1 = x0, y0, z0, r0, x1, y1, z1, r1
        self.rra, self.rrb = r0 * r0, r1 * r1
        self.axisx, self.axisy, self.axisz = (x1 - x0, y1 - y0, z1 - z0)
        
        self.length = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2 + (z1 - z0) ** 2)
        # skew data
        self.sx = (x2 - x1) / self.length
        self.sy = (y2 - y1) / self.length
        self.sz = (z2 - z1) / self.length
                
        # normalize the axis
        self.axisx /= self.length
        self.axisy /= self.length
        self.axisz /= self.length
        
        self.planed = - (self.axisx * x0 + self.axisy * y0 + self.axisz * z0)
        
        self.conelength = sqrt((r1 - r0) ** 2 + self.length ** 2)
        self.side1 = (r1 - r0) / self.conelength
        self.side2 = self.length / self.conelength
        cdef double rmax = max(r0, r1)
        
        self._xlo = min(x0 - rmax, x2 - rmax)
        self._xhi = max(x0 + rmax, x2 + rmax)
        self._ylo = min(y0 - rmax, y2 - rmax)
        self._yhi = max(y0 + rmax, y2 + rmax)
        self._zlo = min(z0 - rmax, z2 - rmax)
        self._zhi = max(z0 + rmax, z2 + rmax)



    def starting_points(self, xs, ys, zs):
        # TODO: this only works right if the entire object is inside the domain
        return [(bisect.bisect_left(xs, self.x0), bisect.bisect_left(ys, self.y0), bisect.bisect_left(zs, self.z0))]
        #return sum([c.starting_points(xs, ys, zs) for c in self.clips], [(bisect.bisect_left(xs, self.x0), bisect.bisect_left(ys, self.y0), bisect.bisect_left(zs, self.z0))])
    
    # TODO: allow clipping?
    cpdef double distance(self, double px, double py, double pz):
        # CTNG:shearfrustdist
        cdef double nx, ny, nz, y, yy, xx, ry, rx, dist
        
        # compute signed distance of point to plane (note: axis vector has length 1)        
        dist = px * self.axisx + py * self.axisy + pz * self.axisz + self.planed

        # deskew
        px -= dist * self.sx
        py -= dist * self.sy
        pz -= dist * self.sz        
        
        nx, ny, nz = px - self.x0, py - self.y0, pz - self.z0
        y = nx * self.axisx + ny * self.axisy + nz * self.axisz
        yy = y * y
        xx = nx * nx + ny * ny + nz * nz - yy
        # in principle, xx >= 0, however roundoff errors may cause trouble
        if xx < 0: xx = 0

        if y < 0:
            if xx < self.rra:
                return -y
            else:
                x = sqrt(xx) - self.r0
                return sqrt(x * x + yy)
        elif xx < self.rrb and y > self.length:
            return y - self.length
        else:
            x = sqrt(xx) - self.r0
            ry = x * self.side1 + y * self.side2
            if ry < 0:
                return sqrt(x * x + yy)
            else:
                rx = x * self.side2 - y * self.side1
                if ry > self.conelength and y > self.length:
                    ry -= self.conelength
                    return sqrt(rx * rx + ry * ry)
                else:
                    if rx >= 0:
                        return rx
                    else:
                        return max(rx, y - self.length)


        
    cpdef bint overlaps_x(self, double lo, double hi):
        return lo <= self._xhi and hi >= self._xlo
    cpdef bint overlaps_y(self, double lo, double hi):
        return lo <= self._yhi and hi >= self._ylo
    cpdef bint overlaps_z(self, double lo, double hi):
        return lo <= self._zhi and hi >= self._zlo