Repository: ManiacalLabs/BiblioPixel
Pull request #234 (2017-06-22 12:56:43)

LED becomes layout!
Came out quite neatly and I learned a huge amount.  :-)

adammhaile: Holy file change count, Batman!
damn... looking at this now.
adammhaile: in Animation, change `_layout` (used to be `_led`) to just `layout`. It's not actually protected and as noted, Animations MUST use it to do anything, so the `_` is silly. Figured we might as well while changing all this.
adammhaile: For reference, that comment was:

By many, you mean ALL of them? That's literally the only way to set your pixels.
Thank you for at least adding this in... 2 things we need to do though:

BPA should switch every anim over to _layout
Please update the wiki doc ASAP to show _layout once this is merged in.
rec: All right!  I pushed a fixup commit which should cover everything - I'll rebase before merging.
adammhaile: Sweet... just cause I'm curious, what are your steps for "Rebase". I always
see putting multiple commits into one as a squash, not a "Rebase" per
se.... so what's your commands for this?

On Thu, Jun 22, 2017 at 10:40 AM, Tom Swirly <notifications@github.com>
wrote:

> All right! I pushed a fixup commit which should cover everything - I'll
> rebase before merging.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/ManiacalLabs/BiblioPixel/pull/234#issuecomment-310400645>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AA6a6iRnu102z7drGz8LC_8cYdj22Epbks5sGnzYgaJpZM4OCQcY>
> .
>

rec: if you do git rebase -i --autosquash it automatically squashes any --fixup
commits in the rebase...

In my .gitconfig:

[rebase]
autosquash = true
autostash = true


And these two aliases:

https://github.com/rec/dotfiles/blob/master/bash/git.sh#L118-L124




On Thu, Jun 22, 2017 at 4:42 PM, Adam Haile <notifications@github.com>
wrote:

> Sweet... just cause I'm curious, what are your steps for "Rebase". I always
> see putting multiple commits into one as a squash, not a "Rebase" per
> se.... so what's your commands for this?
>
> On Thu, Jun 22, 2017 at 10:40 AM, Tom Swirly <notifications@github.com>
> wrote:
>
> > All right! I pushed a fixup commit which should cover everything - I'll
> > rebase before merging.
> >
> > —
> > You are receiving this because you commented.
> > Reply to this email directly, view it on GitHub
> > <https://github.com/ManiacalLabs/BiblioPixel/pull/
> 234#issuecomment-310400645>,
> > or mute the thread
> > <https://github.com/notifications/unsubscribe-
> auth/AA6a6iRnu102z7drGz8LC_8cYdj22Epbks5sGnzYgaJpZM4OCQcY>
> > .
>
> >
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/ManiacalLabs/BiblioPixel/pull/234#issuecomment-310401382>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAPdsl4hZ0U-DxdisQfbldeO7WGo0lILks5sGn1kgaJpZM4OCQcY>
> .
>



-- 
     /t

*https://tom.ritchford.com <https://tom.ritchford.com>*
*https://tom.swirly.com <https://tom.swirly.com>*

adammhaile: Whoa... that's awesome :) Good to know! 
when you submit that rebased version back do you have to force push or anything, or just push as normal?
rec: You'll of course have to force push as you've rewritten history.

I have all sorts of little dumb scripts for this.  Sometimes when I go away
for a while I get confused and havoc ensues.  Thank Goodness for the reflog!

gfresh <https://github.com/rec/dotfiles/blob/master/bash/git.sh> is
particularly useful - makes a fresh branch with a copy of the current
contents of ManiacalLab's dev branch.


On Thu, Jun 22, 2017 at 4:57 PM, Adam Haile <notifications@github.com>
wrote:

> Whoa... that's awesome :) Good to know!
> when you submit that rebased version back do you have to force push or
> anything, or just push as normal?
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/ManiacalLabs/BiblioPixel/pull/234#issuecomment-310405901>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAPdsoA7-LtDtxtcNp5fvbBdDv35POzMks5sGoDzgaJpZM4OCQcY>
> .
>



-- 
     /t

*https://tom.ritchford.com <https://tom.ritchford.com>*
*https://tom.swirly.com <https://tom.swirly.com>*

rec: Rebased.  :+1: this if you think it's ready to go.


Patch:
diff --git a/bibliopixel/__init__.py b/bibliopixel/__init__.py
index aa28e40..46c5480 100644
--- a/bibliopixel/__init__.py
+++ b/bibliopixel/__init__.py
@@ -1,12 +1,12 @@
-from . led import Circle, Matrix, POV, Strip, Cube
-from . layout import Rotation
-from . layout.matrix import gen_matrix
-from . layout.circle import gen_circle
-from . layout.cube import gen_cube
-from . import animation, colors, font, gamma, led, log, util
+from . layout import Circle, Matrix, POV, Strip, Cube
+from . layout.geometry import Rotation
+from . layout.geometry.matrix import gen_matrix
+from . layout.geometry.circle import gen_circle
+from . layout.geometry.cube import gen_cube
+from . import animation, colors, font, gamma, layout, log, util
 
 # These are DEPRECATED
-from . led import LEDCircle, LEDMatrix, LEDPOV, LEDStrip, LEDCube
+from . layout import LEDCircle, LEDMatrix, LEDPOV, LEDStrip, LEDCube
 
 
 def _get_version():
diff --git a/bibliopixel/animation/animation.py b/bibliopixel/animation/animation.py
index 61c16a8..e7f5583 100644
--- a/bibliopixel/animation/animation.py
+++ b/bibliopixel/animation/animation.py
@@ -7,19 +7,28 @@ from .. threads.animation_threading import AnimationThreading
 class BaseAnimation(object):
     free_run = False
 
-    def __init__(self, led):
-        self._led = led
+    def __init__(self, layout):
+        self.layout = layout
         self.internal_delay = None
 
+    @property
+    def _led(self):
+        """Many BiblioPixelAnimations use the "protected" variable _led."""
+        return self.layout
+
+    @_led.setter
+    def _led(self, layout):
+        self.layout = layout
+
     def preRun(self, amt=1):
-        self._led.all_off()
+        self.layout.all_off()
 
     def step(self, amt=1):
         raise RuntimeError("Base class step() called. This shouldn't happen")
 
     def cleanup(self):
         self.threading.stop_thread(wait=True)
-        self._led.cleanup()
+        self.layout.cleanup()
 
     def is_running(self):
         if self.threading.stop_event.isSet():
@@ -42,8 +51,8 @@ class BaseAnimation(object):
 
         stamp()
 
-        self._led.frame_render_time = timestamps[1] - timestamps[0]
-        self._led.push_to_driver()
+        self.layout.frame_render_time = timestamps[1] - timestamps[0]
+        self.layout.push_to_driver()
 
         stamp()
 
@@ -72,7 +81,7 @@ class BaseAnimation(object):
             self.sleep_time = self.internal_delay
         else:
             self.sleep_time = self.runner.sleep_time
-        self._led.animation_sleep_time = self.sleep_time or 0
+        self.layout.animation_sleep_time = self.sleep_time or 0
 
         self.preRun(self.runner.amt)
         try:
diff --git a/bibliopixel/animation/circle.py b/bibliopixel/animation/circle.py
index e7548b9..562c6c9 100644
--- a/bibliopixel/animation/circle.py
+++ b/bibliopixel/animation/circle.py
@@ -1,16 +1,17 @@
 from . animation import BaseAnimation
-from .. led import Circle
+from .. layout import Circle
 
 
 class BaseCircleAnim(BaseAnimation):
 
-    def __init__(self, led):
-        super().__init__(led)
+    def __init__(self, layout):
+        super().__init__(layout)
 
-        if not isinstance(led, Circle):
-            raise RuntimeError("Must use Circle with Circle Animations!")
+        if not isinstance(layout, Circle):
+            raise RuntimeError('Must use bibliopixel.layout.Circle with ' +
+                               'Circle Animations!')
 
-        self.rings = led.rings
-        self.ringCount = led.ringCount
-        self.lastRing = led.lastRing
-        self.ringSteps = led.ringSteps
+        self.rings = layout.rings
+        self.ringCount = layout.ringCount
+        self.lastRing = layout.lastRing
+        self.ringSteps = layout.ringSteps
diff --git a/bibliopixel/animation/cube.py b/bibliopixel/animation/cube.py
index 63968bc..090a0e1 100644
--- a/bibliopixel/animation/cube.py
+++ b/bibliopixel/animation/cube.py
@@ -1,12 +1,13 @@
 from . animation import BaseAnimation
-from .. led import Cube
+from .. layout import Cube
 
 
 class BaseCubeAnim(BaseAnimation):
 
-    def __init__(self, led):
-        super().__init__(led)
-        if not isinstance(led, Cube):  # pragma: no cover
-            raise RuntimeError("Must use Cube with Matrix Animations!")
+    def __init__(self, layout):
+        super().__init__(layout)
+        if not isinstance(layout, Cube):  # pragma: no cover
+            raise RuntimeError('Must use bibliopixel.layout.Cube with ' +
+                               'Cube Animations!')
 
-        self.x, self.y, self.z = led.x, led.y, led.z
+        self.x, self.y, self.z = layout.x, layout.y, layout.z
diff --git a/bibliopixel/animation/game.py b/bibliopixel/animation/game.py
index 32dbd6b..c7cf260 100644
--- a/bibliopixel/animation/game.py
+++ b/bibliopixel/animation/game.py
@@ -4,8 +4,8 @@ from .. util import AttributeDict
 
 class BaseGameAnim(BaseMatrixAnim):
 
-    def __init__(self, led, inputDev):
-        super().__init__(led)
+    def __init__(self, layout, inputDev):
+        super().__init__(layout)
         self._input_dev = inputDev
         self._keys = None
         self._lastKeys = None
diff --git a/bibliopixel/animation/matrix.py b/bibliopixel/animation/matrix.py
index 7f05791..a82cc97 100644
--- a/bibliopixel/animation/matrix.py
+++ b/bibliopixel/animation/matrix.py
@@ -1,16 +1,17 @@
 from . animation import BaseAnimation
-from .. led import Matrix
+from .. layout import Matrix
 
 
 class BaseMatrixAnim(BaseAnimation):
 
-    def __init__(self, led, width=0, height=0, startX=0, startY=0):
-        super().__init__(led)
-        if not isinstance(led, Matrix):  # pragma: no cover
-            raise RuntimeError("Must use Matrix with Matrix Animations!")
+    def __init__(self, layout, width=0, height=0, startX=0, startY=0):
+        super().__init__(layout)
+        if not isinstance(layout, Matrix):  # pragma: no cover
+            raise RuntimeError('Must use bibliopixel.layout.Matrix with ' +
+                               'Matrix Animations!')
 
-        self.width = width or led.width
-        self.height = height or led.height
+        self.width = width or layout.width
+        self.height = height or layout.height
 
         self.startX = startX
         self.startY = startY
diff --git a/bibliopixel/animation/off.py b/bibliopixel/animation/off.py
index 0611a5b..1e0be61 100644
--- a/bibliopixel/animation/off.py
+++ b/bibliopixel/animation/off.py
@@ -2,11 +2,11 @@ from . animation import BaseAnimation
 
 
 class OffAnim(BaseAnimation):
-    """A trivial animation that turns all LEDs off."""
+    """A trivial animation that turns all pixels in a layout off."""
 
-    def __init__(self, led, timeout=10):
-        super().__init__(led)
+    def __init__(self, layout, timeout=10):
+        super().__init__(layout)
         self.internal_delay = timeout
 
     def step(self, amt=1):
-        self._led.all_off()
+        self.layout.all_off()
diff --git a/bibliopixel/animation/receiver.py b/bibliopixel/animation/receiver.py
index 3e27ccc..1e6ebad 100644
--- a/bibliopixel/animation/receiver.py
+++ b/bibliopixel/animation/receiver.py
@@ -22,12 +22,12 @@ HOST_MAP = {
 class BaseReceiver(BaseAnimation):
     free_run = True
 
-    def __init__(self, led):
-        super().__init__(led)
-        name = type(self._led).__name__
+    def __init__(self, layout):
+        super().__init__(layout)
+        name = type(self.layout).__name__
 
         if name not in HOST_MAP:
-            raise ValueError('led must be of type' + ', '.join(HOST_MAP))
+            raise ValueError('layout must be of type' + ', '.join(HOST_MAP))
 
         self.host_type = HOST_MAP[name]
         self._hold_for_data = threading.Event()
@@ -36,7 +36,7 @@ class BaseReceiver(BaseAnimation):
         self._recv_thread_obj = None
 
     def preRun(self, amt=1):
-        self._led.all_off()
+        self.layout.all_off()
         self.start()
 
     def start(self):
diff --git a/bibliopixel/animation/sequence.py b/bibliopixel/animation/sequence.py
index 9fda217..9a6b9d3 100644
--- a/bibliopixel/animation/sequence.py
+++ b/bibliopixel/animation/sequence.py
@@ -3,7 +3,7 @@ from . import animation, runner
 
 
 class Sequence(animation.BaseAnimation):
-    def __init__(self, led, animations=None):
+    def __init__(self, layout, animations=None):
         def make_animation(a):
             if isinstance(a, str):
                 desc = {'animation': a[0]}
@@ -12,9 +12,9 @@ class Sequence(animation.BaseAnimation):
             else:
                 desc = {'animation': a[0], 'run': a[1]}
             desc = aliases.resolve_aliases(desc)
-            return project.make_animation(led=led, **desc)
+            return project.make_animation(layout=layout, **desc)
 
-        super().__init__(led)
+        super().__init__(layout)
         self.animations = [make_animation(i) for i in animations or []]
         self.index = 0
         self.internal_delay = 0  # never wait
diff --git a/bibliopixel/animation/strip.py b/bibliopixel/animation/strip.py
index 906a9c4..6a91d66 100644
--- a/bibliopixel/animation/strip.py
+++ b/bibliopixel/animation/strip.py
@@ -1,18 +1,19 @@
 from . animation import BaseAnimation
-from .. led import Strip
+from .. layout import Strip
 
 
 class BaseStripAnim(BaseAnimation):
 
-    def __init__(self, led, start=0, end=-1):
-        super().__init__(led)
+    def __init__(self, layout, start=0, end=-1):
+        super().__init__(layout)
 
-        if not isinstance(led, Strip):
-            raise RuntimeError("Must use Strip with Strip Animations!")
+        if not isinstance(layout, Strip):
+            raise RuntimeError('Must use bibliopixel.layout.Strip with ' +
+                               'Strip Animations!')
 
         self._start = max(start, 0)
         self._end = end
-        if self._end < 0 or self._end >= self._led.numLEDs:
-            self._end = self._led.numLEDs - 1
+        if self._end < 0 or self._end >= self.layout.numLEDs:
+            self._end = self.layout.numLEDs - 1
 
         self._size = self._end - self._start + 1
diff --git a/bibliopixel/drivers/SimPixel/driver.py b/bibliopixel/drivers/SimPixel/driver.py
index a135d3d..04ba5b5 100644
--- a/bibliopixel/drivers/SimPixel/driver.py
+++ b/bibliopixel/drivers/SimPixel/driver.py
@@ -12,18 +12,27 @@ Perhaps BiblioPixel is already running on your machine?
 
 class SimPixel(DriverBase):
 
-    def __init__(self, num, port=1337, layout=None, **kwds):
+    def __init__(self, num, port=1337, pixel_positions=None, **kwds):
+        """
+        Args:
+            num:  number of LEDs being visualizer.
+            port:  the port on which the SimPixel server is running.
+            pixel_positions:  the positions of the LEDs in 3-d space.
+            **kwds:  keywords passed to DriverBase.
+        """
         super().__init__(num, **kwds)
         self.port = port
-        self.layout = self.server = self.thread = None
+        self.pixel_positions = self.server = self.thread = None
         self.websocks = {}
 
-        if layout:
-            self.set_layout(layout)
+        if pixel_positions:
+            self.set_pixel_positions(pixel_positions)
 
     def __start_server(self):
         log.debug('Starting server...')
-        desc = dict(driver=self, layout=self.layout, selectInterval=0.001)
+        desc = dict(driver=self,
+                    pixel_positions=self.pixel_positions,
+                    selectInterval=0.001)
         try:
             self.server = websocket.Server(
                 '', self.port, websocket.Client, **desc)
@@ -43,11 +52,11 @@ class SimPixel(DriverBase):
             pass
         log.debug('WebSocket Server closed')
 
-    def set_layout(self, layout):
+    def set_pixel_positions(self, pixel_positions):
         if not (self.thread and self.thread.is_alive()):
-            # flatten layout
-            pl = [c for p in layout for c in p]
-            self.layout = bytearray(struct.pack('<%sh' % len(pl), *pl))
+            # Flatten list of led positions.
+            pl = [c for p in pixel_positions for c in p]
+            self.pixel_positions = bytearray(struct.pack('<%sh' % len(pl), *pl))
             self.__start_server()
 
     def add_websock(self, oid, send_pixels):
diff --git a/bibliopixel/drivers/SimPixel/websocket.py b/bibliopixel/drivers/SimPixel/websocket.py
index 8bd6a25..bc8700d 100644
--- a/bibliopixel/drivers/SimPixel/websocket.py
+++ b/bibliopixel/drivers/SimPixel/websocket.py
@@ -7,11 +7,11 @@ Server = SimpleWebSocketServer
 
 class Client(WebSocket):
 
-    def __init__(self, *args, driver, layout):
+    def __init__(self, *args, driver, pixel_positions):
         super().__init__(*args)
         self.driver = driver
         self.connected = False
-        self.layout = layout
+        self.pixel_positions = pixel_positions
         self.oid = None
         log.debug('Server started...')
 
@@ -20,7 +20,7 @@ class Client(WebSocket):
         self.connected = True
         self.oid = uuid.uuid1()
         self.driver.add_websock(self.oid, self.send_pixels)
-        self.sendMessage(bytearray([0x00, 0x00]) + self.layout)
+        self.sendMessage(bytearray([0x00, 0x00]) + self.pixel_positions)
 
     def handleClose(self):
         self.driver.remove_websock(self.oid)
diff --git a/bibliopixel/drivers/driver_base.py b/bibliopixel/drivers/driver_base.py
index 212777f..9df1ebf 100644
--- a/bibliopixel/drivers/driver_base.py
+++ b/bibliopixel/drivers/driver_base.py
@@ -28,7 +28,7 @@ class DriverBase(object):
         self.c_order = c_order
         self.perm = ChannelOrder.ORDERS.index(c_order)
 
-        self.layout = None
+        self.pixel_positions = None
 
         self.width = width
         self.height = height
@@ -39,7 +39,7 @@ class DriverBase(object):
         self._brightness = 255
         self._waiting_brightness = None
 
-    def set_layout(self, layout):
+    def set_pixel_positions(self, pixel_positions):
         pass
 
     def set_colors(self, colors, pos):
diff --git a/bibliopixel/drivers/network_receiver.py b/bibliopixel/drivers/network_receiver.py
index 0e6b25e..7728d43 100644
--- a/bibliopixel/drivers/network_receiver.py
+++ b/bibliopixel/drivers/network_receiver.py
@@ -81,13 +81,13 @@ class ThreadedDataServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
 
 class NetworkReceiver:
 
-    def __init__(self, led, port=3142, interface='0.0.0.0'):
-        self._led = led
+    def __init__(self, layout, port=3142, interface='0.0.0.0'):
+        self.layout = layout
         self.address = (interface, port)
         SocketServer.TCPServer.allow_reuse_address = True
         self._server = ThreadedDataServer(self.address, ThreadedDataHandler)
         self._server.update = self.update
-        self._server.set_brightness = self._led.set_brightness
+        self._server.set_brightness = self.layout.set_brightness
 
     def start(self, join=False):
         self._t = threading.Thread(target=self._server.serve_forever)
@@ -104,5 +104,5 @@ class NetworkReceiver:
         # self._t.join()
 
     def update(self, data):
-        self._led.setBuffer(list(data))
-        self._led.push_to_driver()
+        self.layout.setBuffer(list(data))
+        self.layout.push_to_driver()
diff --git a/bibliopixel/image.py b/bibliopixel/image.py
index dc9003b..4eea94f 100644
--- a/bibliopixel/image.py
+++ b/bibliopixel/image.py
@@ -1,6 +1,6 @@
 import glob, numbers, os, sys
 from . import colors, log
-from . led import Matrix
+from . layout import Matrix
 
 try:
     from PIL import Image, ImageSequence
@@ -54,30 +54,30 @@ def show_image(setter, width, height,
             setter(x, y, (r, g, b))
 
 
-def showImage(led, imagePath="", imageObj=None, offset=(0, 0), bgcolor=colors.Off, brightness=255):
+def showImage(layout, imagePath="", imageObj=None, offset=(0, 0), bgcolor=colors.Off, brightness=255):
     """Display an image on the matrix"""
-    if not isinstance(led, Matrix):
+    if not isinstance(layout, Matrix):
         raise RuntimeError("Must use Matrix with showImage!")
 
-    led.all_off()
+    layout.all_off()
 
-    return show_image(led.set, led.width, led.height, imagePath, imageObj,
+    return show_image(layout.set, layout.width, layout.height, imagePath, imageObj,
                       offset, bgcolor, brightness)
 
 
-def loadImage(led, imagePath="", imageObj=None, offset=(0, 0), bgcolor=colors.Off, brightness=255):
+def loadImage(layout, imagePath="", imageObj=None, offset=(0, 0), bgcolor=colors.Off, brightness=255):
     """Display an image on the matrix"""
 
-    if not isinstance(led, Matrix):
+    if not isinstance(layout, Matrix):
         raise RuntimeError("Must use Matrix with loadImage!")
 
-    texture = [[colors.Off for x in range(led.width)] for y in range(led.height)]
+    texture = [[colors.Off for x in range(layout.width)] for y in range(layout.height)]
 
     def setter(x, y, pixel):
         if y >= 0 and x >= 0:
             texture[y][x] = pixel
 
-    return show_image(setter, led.width, led.height, imagePath, imageObj,
+    return show_image(setter, layout.width, layout.height, imagePath, imageObj,
                       offset, bgcolor, brightness)
 
 
diff --git a/bibliopixel/layout/__init__.py b/bibliopixel/layout/__init__.py
index 838d30f..5316c7a 100644
--- a/bibliopixel/layout/__init__.py
+++ b/bibliopixel/layout/__init__.py
@@ -1,5 +1,16 @@
-from . rotation import Rotation, rotate_and_flip
-from . circle import calc_ring_pixel_count, calc_ring_steps, gen_circle, layout_from_rings
-from . cube import gen_cube, layout_from_cube
-from . matrix import gen_matrix, layout_from_matrix
-from . strip import gen_strip
+from . circle import Circle
+from . matrix import Matrix
+from . cube import Cube
+from . pov import POV
+from . strip import Strip
+from . geometry import Rotation
+from . geometry.matrix import gen_matrix
+from . geometry.circle import gen_circle
+from . geometry.cube import gen_cube
+
+# These are DEPRECATED
+from . circle import LEDCircle
+from . cube import LEDCube
+from . matrix import LEDMatrix
+from . pov import LEDPOV
+from . strip import LEDStrip
diff --git a/bibliopixel/layout/circle.py b/bibliopixel/layout/circle.py
index b79a516..a31fea2 100644
--- a/bibliopixel/layout/circle.py
+++ b/bibliopixel/layout/circle.py
@@ -1,83 +1,118 @@
-from .. util import pointOnCircle
-import math
-
-
-def calc_ring_pixel_count(rings):
-    num = 0
-    for r in rings:
-        num += len(r)
-    return num
-
-
-def calc_ring_steps(rings):
-    steps = []
-    for r in rings:
-        steps.append(360.0 / len(r))
-    return steps
-
-
-def gen_circle(rings=None, pixels_per=None, offset=0, invert=False):
-    if pixels_per:
-        rings = []
-        for c in pixels_per:
-            rings.append([offset, offset + c])
-            offset = offset + c
-
-    if not rings:
-        raise ValueError('Must specify rings or pixels_per')
-
-    if rings:
-        num = 0
-        out_rings = []
-        for r in rings:
-            if len(r) != 2:
-                raise ValueError('"rings" values must only be first and last index.')
-            if r[0] < r[1]:
-                indices = list(range(r[0], r[1]))
-            else:
-                indices = list(range(r[1], r[0]))[::-1]
-            out_rings.append(indices)
-            num += len(indices)
-
-        if invert:
-            out_rings = out_rings[::-1]
-
-        return (out_rings, calc_ring_steps(out_rings))
-
-
-def layout_from_rings(rings, origin=(0, 0, 0), z_diff=0, radii=None):
-    if len(origin) not in [2, 3]:
-        raise ValueError('origin must be (x,y) or (x,y,z)')
-
-    use_z = len(origin) == 3
-    if use_z:
-        ox, oy, oz = origin
-    else:
-        ox, oz = origin
-
-    num = calc_ring_pixel_count(rings)
-    steps = calc_ring_steps(rings)
-
-    points = [None] * num
-    z = 0
-
-    for i in range(len(rings)):
-        r = rings[i]
-        step = steps[i]
-        angle = 0.0
-        for p in r:
-            if radii:
-                radius = radii[i]
-            else:
-                radius = (len(r) - 1) * math.pi
-            x, y = pointOnCircle(0, 0, radius, angle)
-            if use_z:
-                points[p] = (x + ox, y + oy, z + oz)
-            else:
-                points[p] = (x + ox, y + oy)
-
-            angle += step
-
-        z += z_diff
-
-    return points
+from . layout import Layout
+from .. import data_maker
+from . geometry.circle import (
+    gen_circle, calc_ring_steps, calc_ring_pixel_count,
+    pixel_positions_from_rings)
+
+
+class Circle(Layout):
+
+    def __init__(self, drivers, rings=[], pixels_per=None,
+                 maxAngleDiff=0, rotation=0,
+                 threadedUpdate=False, brightness=255, **kwargs):
+        super().__init__(drivers, threadedUpdate, brightness,
+                         maker=kwargs.get('maker', data_maker.MAKER))
+        self.rings = rings
+        self.maxAngleDiff = maxAngleDiff
+        full_coords = False
+        for r in self.rings:
+            full_coords |= (len(r) > 2)
+
+        if not full_coords:
+            self.rings, self.ringSteps = gen_circle(
+                rings=self.rings, pixels_per=pixels_per)
+        else:
+            self.ringSteps = calc_ring_steps(self.rings)
+
+        self.ringCount = len(self.rings)
+        self.lastRing = self.ringCount - 1
+
+        self.set_pixel_positions(pixel_positions_from_rings(self.rings))
+
+        num = calc_ring_pixel_count(self.rings)
+
+        self.rotation = rotation
+
+        if self.numLEDs != num:
+            raise ValueError(
+                "Total ring LED count does not equal driver LED count!")
+
+    def __genOffsetFromAngle(self, angle, ring):
+        if ring >= self.ringCount:
+            return -1
+
+        angle = (angle + self.rotation) % 360
+        offset = int(round(angle / self.ringSteps[ring]))
+
+        # wraps it back around
+        length = len(self.rings[ring]) - 1
+
+        if offset > length:
+            offset = 0
+
+        if self.maxAngleDiff > 0:
+            calcAngle = (offset * self.ringSteps[ring]) % 360
+            diff = abs(angle - calcAngle)
+            if diff > self.maxAngleDiff:
+                return -1
+
+        return offset
+
+    def angleToPixel(self, angle, ring):
+        offset = self.__genOffsetFromAngle(angle, ring)
+        if offset < 0:
+            return offset
+
+        return self.rings[ring][offset]
+
+    # Set single pixel to Color value
+    def set(self, ring, angle, color):
+        """Set pixel to RGB color tuple"""
+        pixel = self.angleToPixel(angle, ring)
+        self._set_base(pixel, color)
+
+    def get(self, ring, angle):
+        """Get RGB color tuple of color at index pixel"""
+        pixel = self.angleToPixel(angle, ring)
+        return self._get_base(pixel)
+
+    def drawRadius(self, angle, color, startRing=0, endRing=-1):
+        if startRing < 0:
+            startRing = 0
+        if endRing < 0 or endRing > self.lastRing:
+            endRing = self.lastRing
+        for ring in range(startRing, endRing + 1):
+            self.set(ring, angle, color)
+
+    def fillRing(self, ring, color, startAngle=0, endAngle=None):
+        if endAngle is None:
+            endAngle = 359
+
+        if ring >= self.ringCount:
+            raise ValueError("Invalid ring!")
+
+        def pixelRange(ring, start=0, stop=-1):
+            r = self.rings[ring]
+            if stop == -1:
+                stop = len(r) - 1
+
+            return [r[i] for i in range(start, stop + 1)]
+
+        start = self.__genOffsetFromAngle(startAngle, ring)
+        end = self.__genOffsetFromAngle(endAngle, ring)
+
+        pixels = []
+        if start >= end:
+            pixels = pixelRange(ring, start)
+            pixels.extend(pixelRange(ring, 0, end))
+        elif start == end and startAngle > endAngle:
+            pixels = pixelRange(ring)
+        else:
+            pixels = pixelRange(ring, start, end)
+
+        for i in pixels:
+            self._set_base(i, color)
+
+
+# This is DEPRECATED
+LEDCircle = Circle
diff --git a/bibliopixel/layout/cube.py b/bibliopixel/layout/cube.py
index 96e90fc..69e11fa 100644
--- a/bibliopixel/layout/cube.py
+++ b/bibliopixel/layout/cube.py
@@ -1,39 +1,60 @@
-from . import Rotation, rotate_and_flip
-from . matrix import gen_matrix
-
-
-def gen_cube(dx, dy, dz, xy_serpentine=True, offset=0,
-             xy_rotation=Rotation.ROTATE_0, z_rotation=Rotation.ROTATE_0,
-             y_flip=False, z_flip=False):
-    result = []
-    plane_offset = offset
-    for z in range(dz):
-        plane = gen_matrix(dx, dy, serpentine=xy_serpentine,
-                           offset=plane_offset, rotation=xy_rotation,
-                           y_flip=y_flip)
-        plane_offset += (dx * dy)
-        result.append(plane)
-
-    result = rotate_and_flip(result, z_rotation, z_flip)
-
-    return result
-
-
-def layout_from_cube(coord_map):
-    num = 0
-    dx, dy, dz = (0, 0, len(coord_map))
-
-    for xy in coord_map:
-        if len(xy) > dx:
-            dy = len(xy)
-        for x in xy:
-            if len(x) > dx:
-                dx = len(x)
-            num += len(x)
-
-    result = [None] * num
-    for z in range(dz):
-        for y in range(dy):
-            for x in range(dx):
-                result[coord_map[z][y][x]] = [x, y, z]
-    return result
+import math, threading, time
+
+from .. import colors, data_maker, font, log, matrix
+from . layout import Layout
+from . geometry.cube import gen_cube, pixel_positions_from_cube
+
+
+class Cube(Layout):
+
+    def __init__(self, drivers, x, y, z, coordMap=None,
+                 threadedUpdate=False, brightness=255, **kwargs):
+        super().__init__(drivers, threadedUpdate, brightness,
+                         maker=kwargs.get('maker', data_maker.MAKER))
+
+        self.x = x
+        self.y = y
+        self.z = z
+
+        if self.x * self.y * self.z != self.numLEDs:
+            raise TypeError("(x * y * z) MUST equal the total pixel count!")
+
+        if coordMap:
+            self.cube_map = coordMap
+        else:
+            if len(self.drivers) == 1:
+                log.info('Auto generating coordinate map. Use gen_cube directly if more control needed.')
+                self.cube_map = gen_cube(x, y, z)
+            else:
+                raise TypeError("Must provide coordMap if using multiple drivers!")
+
+        self.set_pixel_positions(pixel_positions_from_cube(self.cube_map))
+
+    def get_pixel_positions(self):
+        return pixel_positions_from_cube(self.coord_map)
+
+    def set(self, x, y, z, color):
+        try:
+            pixel = self.cube_map[z][y][x]
+            self._set_base(pixel, color)
+        except IndexError:
+            pass
+
+    def get(self, x, y, z):
+        try:
+            pixel = self.cube_map[z][y][x]
+            return self._get_base(pixel)
+        except IndexError:
+            return 0, 0, 0
+
+    def setHSV(self, x, y, z, hsv):
+        color = colors.hsv2rgb(hsv)
+        self._set(x, y, z, color)
+
+    def setRGB(self, x, y, z, r, g, b):
+        color = (r, g, b)
+        self._set(x, y, z, color)
+
+
+# This is DEPRECATED
+LEDCube = Cube
diff --git a/bibliopixel/layout/geometry/__init__.py b/bibliopixel/layout/geometry/__init__.py
new file mode 100644
index 0000000..ef3522a
--- /dev/null
+++ b/bibliopixel/layout/geometry/__init__.py
@@ -0,0 +1,6 @@
+from . rotation import Rotation, rotate_and_flip
+from . circle import (calc_ring_pixel_count, calc_ring_steps, gen_circle,
+                      pixel_positions_from_rings)
+from . cube import gen_cube, pixel_positions_from_cube
+from . matrix import gen_matrix, pixel_positions_from_matrix
+from . strip import gen_strip
diff --git a/bibliopixel/layout/geometry/circle.py b/bibliopixel/layout/geometry/circle.py
new file mode 100644
index 0000000..6f59780
--- /dev/null
+++ b/bibliopixel/layout/geometry/circle.py
@@ -0,0 +1,83 @@
+from ... util import pointOnCircle
+import math
+
+
+def calc_ring_pixel_count(rings):
+    num = 0
+    for r in rings:
+        num += len(r)
+    return num
+
+
+def calc_ring_steps(rings):
+    steps = []
+    for r in rings:
+        steps.append(360.0 / len(r))
+    return steps
+
+
+def gen_circle(rings=None, pixels_per=None, offset=0, invert=False):
+    if pixels_per:
+        rings = []
+        for c in pixels_per:
+            rings.append([offset, offset + c])
+            offset = offset + c
+
+    if not rings:
+        raise ValueError('Must specify rings or pixels_per')
+
+    if rings:
+        num = 0
+        out_rings = []
+        for r in rings:
+            if len(r) != 2:
+                raise ValueError('"rings" values must only be first and last index.')
+            if r[0] < r[1]:
+                indices = list(range(r[0], r[1]))
+            else:
+                indices = list(range(r[1], r[0]))[::-1]
+            out_rings.append(indices)
+            num += len(indices)
+
+        if invert:
+            out_rings = out_rings[::-1]
+
+        return (out_rings, calc_ring_steps(out_rings))
+
+
+def pixel_positions_from_rings(rings, origin=(0, 0, 0), z_diff=0, radii=None):
+    if len(origin) not in [2, 3]:
+        raise ValueError('origin must be (x,y) or (x,y,z)')
+
+    use_z = len(origin) == 3
+    if use_z:
+        ox, oy, oz = origin
+    else:
+        ox, oz = origin
+
+    num = calc_ring_pixel_count(rings)
+    steps = calc_ring_steps(rings)
+
+    points = [None] * num
+    z = 0
+
+    for i in range(len(rings)):
+        r = rings[i]
+        step = steps[i]
+        angle = 0.0
+        for p in r:
+            if radii:
+                radius = radii[i]
+            else:
+                radius = (len(r) - 1) * math.pi
+            x, y = pointOnCircle(0, 0, radius, angle)
+            if use_z:
+                points[p] = (x + ox, y + oy, z + oz)
+            else:
+                points[p] = (x + ox, y + oy)
+
+            angle += step
+
+        z += z_diff
+
+    return points
diff --git a/bibliopixel/layout/geometry/cube.py b/bibliopixel/layout/geometry/cube.py
new file mode 100644
index 0000000..8ef7ccb
--- /dev/null
+++ b/bibliopixel/layout/geometry/cube.py
@@ -0,0 +1,39 @@
+from . import Rotation, rotate_and_flip
+from . matrix import gen_matrix
+
+
+def gen_cube(dx, dy, dz, xy_serpentine=True, offset=0,
+             xy_rotation=Rotation.ROTATE_0, z_rotation=Rotation.ROTATE_0,
+             y_flip=False, z_flip=False):
+    result = []
+    plane_offset = offset
+    for z in range(dz):
+        plane = gen_matrix(dx, dy, serpentine=xy_serpentine,
+                           offset=plane_offset, rotation=xy_rotation,
+                           y_flip=y_flip)
+        plane_offset += (dx * dy)
+        result.append(plane)
+
+    result = rotate_and_flip(result, z_rotation, z_flip)
+
+    return result
+
+
+def pixel_positions_from_cube(coord_map):
+    num = 0
+    dx, dy, dz = (0, 0, len(coord_map))
+
+    for xy in coord_map:
+        if len(xy) > dx:
+            dy = len(xy)
+        for x in xy:
+            if len(x) > dx:
+                dx = len(x)
+            num += len(x)
+
+    result = [None] * num
+    for z in range(dz):
+        for y in range(dy):
+            for x in range(dx):
+                result[coord_map[z][y][x]] = [x, y, z]
+    return result
diff --git a/bibliopixel/layout/index_ops.py b/bibliopixel/layout/geometry/index_ops.py
similarity index 100%
rename from bibliopixel/layout/index_ops.py
rename to bibliopixel/layout/geometry/index_ops.py
diff --git a/bibliopixel/layout/geometry/matrix.py b/bibliopixel/layout/geometry/matrix.py
new file mode 100644
index 0000000..680b182
--- /dev/null
+++ b/bibliopixel/layout/geometry/matrix.py
@@ -0,0 +1,77 @@
+from . import index_ops
+from . import Rotation, rotate_and_flip
+
+
+class Matrix(object):
+    """Map a matrix onto a strip of lights."""
+
+    def __init__(self, strip, columns=None, rows=None,
+                 reflect_x=False, reflect_y=False,
+                 serpentine_x=False, serpentine_y=False,
+                 transpose=False):
+        self.strip = strip
+
+        if columns:
+            if not rows:
+                rows = len(strip) // columns
+            elif rows * columns > len(strip):
+                raise IndexError('Index out of range.')
+        elif rows:
+            columns = len(strip) // rows
+        else:
+            raise ValueError('Must supply either rows or columns')
+        self.columns, self.rows = columns, rows
+
+        ops = (reflect_x and index_ops.reflect_x,
+               reflect_y and index_ops.reflect_y,
+               serpentine_x and index_ops.serpentine_x,
+               serpentine_y and index_ops.serpentine_y,
+               transpose and index_ops.transpose)
+        self.operations = list(filter(None, ops))
+
+    def _index(self, x, y):
+        for o in self.operations:
+            x, y = o(x, y, self)
+
+        if 0 <= x < self.columns and 0 <= y < self.rows:
+            return x + y * self.columns
+
+        raise IndexError('Index out of range.')
+
+    def get(self, x, y):
+        return self.strip[self._index(x, y)]
+
+    def set(self, x, y, value):
+        self.strip[self._index(x, y)] = value
+
+
+def gen_matrix(dx, dy, serpentine=True, offset=0,
+               rotation=Rotation.ROTATE_0, y_flip=False):
+    """Helper method to generate X,Y coordinate maps for strips"""
+
+    result = []
+    for y in range(dy):
+        if not serpentine or y % 2 == 0:
+            result.append([(dx * y) + x + offset for x in range(dx)])
+        else:
+            result.append([((dx * (y + 1)) - 1) - x + offset for x in range(dx)])
+
+    result = rotate_and_flip(result, rotation, y_flip)
+
+    return result
+
+
+def pixel_positions_from_matrix(coord_map):
+    max_width = 0
+    for x in coord_map:
+        if len(x) > max_width:
+            max_width = len(x)
+
+    num = len(coord_map) * max_width
+    result = [None] * num
+
+    for y in range(len(coord_map)):
+        for x in range(len(coord_map[y])):
+            result[coord_map[y][x]] = [x, y, 0]
+
+    return result
diff --git a/bibliopixel/layout/rotation.py b/bibliopixel/layout/geometry/rotation.py
similarity index 100%
rename from bibliopixel/layout/rotation.py
rename to bibliopixel/layout/geometry/rotation.py
diff --git a/bibliopixel/layout/segment.py b/bibliopixel/layout/geometry/segment.py
similarity index 100%
rename from bibliopixel/layout/segment.py
rename to bibliopixel/layout/geometry/segment.py
diff --git a/bibliopixel/layout/geometry/strip.py b/bibliopixel/layout/geometry/strip.py
new file mode 100644
index 0000000..6e09269
--- /dev/null
+++ b/bibliopixel/layout/geometry/strip.py
@@ -0,0 +1,34 @@
+import abc
+
+
+class Strip(abc.ABC):
+    """Base class for contiguous strips.  You can also use a list as a Strip."""
+
+    @abc.abstractmethod
+    def __getitem__(self, index):
+        """`index` must be an integer, not a slice."""
+        pass
+
+    @abc.abstractmethod
+    def __setitem__(self, index, value):
+        """`index` must be an integer, not a slice."""
+        pass
+
+    @abc.abstractmethod
+    def __len__(self):
+        pass
+
+
+def fill(strip, item, start=0, stop=None, step=1):
+    """Fill a portion of a strip from start to stop by step with a given item.
+    If stop is not given, it defaults to the length of the strip.
+    """
+    if stop is None:
+        stop = len(strip)
+
+    for i in range(start, stop, step):
+        strip[i] = item
+
+
+def gen_strip(num):
+    return [[x, 0, 0] for x in range(num)]
diff --git a/bibliopixel/led/led.py b/bibliopixel/layout/layout.py
similarity index 96%
rename from bibliopixel/led/led.py
rename to bibliopixel/layout/layout.py
index 17f9557..4df0afd 100644
--- a/bibliopixel/led/led.py
+++ b/bibliopixel/layout/layout.py
@@ -3,7 +3,7 @@ from .. import colors, data_maker, util
 from .. threads.update_threading import UpdateThreading
 
 
-class LEDBase(object):
+class Layout(object):
 
     def __init__(self, drivers, threadedUpdate, brightness,
                  maker=data_maker.MAKER):
@@ -28,9 +28,9 @@ class LEDBase(object):
         self.threading = UpdateThreading(threadedUpdate, self)
         self.set_brightness(brightness)
 
-    def set_layout(self, layout):
+    def set_pixel_positions(self, pixel_positions):
         for d in self.drivers:
-            d.set_layout(layout)
+            d.set_pixel_positions(pixel_positions)
 
     def update(self):
         """DEPRECATED - use self.push_to_driver()"""
@@ -50,7 +50,7 @@ class LEDBase(object):
         if pixel >= 0 and pixel < self.numLEDs:
             self._colors[pixel] = tuple(color)
 
-    def get_layout(self):
+    def get_pixel_positions(self):
         result = []
         for x in range(len(self.numLEDs)):
             result.append([x, 0, 0])
diff --git a/bibliopixel/layout/matrix.py b/bibliopixel/layout/matrix.py
index 653b02e..b1a7f0a 100644
--- a/bibliopixel/layout/matrix.py
+++ b/bibliopixel/layout/matrix.py
@@ -1,77 +1,232 @@
-from . import index_ops
-from . import Rotation, rotate_and_flip
-
-
-class Matrix(object):
-    """Map a matrix onto a strip of lights."""
-
-    def __init__(self, strip, columns=None, rows=None,
-                 reflect_x=False, reflect_y=False,
-                 serpentine_x=False, serpentine_y=False,
-                 transpose=False):
-        self.strip = strip
-
-        if columns:
-            if not rows:
-                rows = len(strip) // columns
-            elif rows * columns > len(strip):
-                raise IndexError('Index out of range.')
-        elif rows:
-            columns = len(strip) // rows
+import math, threading, time
+
+from .. import colors, data_maker, font, matrix, log
+from . layout import Layout
+from . geometry.matrix import Rotation, gen_matrix, pixel_positions_from_matrix
+
+
+class Matrix(Layout):
+
+    def __init__(self, drivers, width=0, height=0, coordMap=None,
+                 rotation=Rotation.ROTATE_0, vert_flip=False,
+                 threadedUpdate=False, brightness=255,
+                 pixelSize=(1, 1), **kwargs):
+        """Main class for matricies.
+        driver - instance that inherits from DriverBase
+        width - X axis size of matrix
+        height - Y axis size of matrix
+        coordMap - a 2D matrix defining the X,Y to strip index mapping. Not needed in most cases
+        rotation - how to rotate when generating the map. Not used if coordMap specified
+        vert_flip - flips the generated map along the Y axis. This along with rotation can achieve any orientation
+        """
+        super().__init__(drivers, threadedUpdate, brightness,
+                         maker=kwargs.get('maker', data_maker.MAKER))
+
+        if width == 0 and height == 0:
+            if len(self.drivers) != 1:
+                raise TypeError(
+                    "Must provide width and height if using multiple drivers!")
+            width = self.drivers[0].width
+            height = self.drivers[0].height
+
+        self.width = width
+        self.height = height
+        self.pixelSize = pixelSize
+        pw, ph = self.pixelSize
+
+        # if both are 0 try to assume it's a square display
+        if self.width == 0 and self.height == 0:
+            square = int(math.sqrt(self.numLEDs))
+            if (square * square) == self.numLEDs:
+                self.width = self.height = square
+            else:
+                raise TypeError(
+                    "No width or height passed but the number of LEDs is not a perfect square")
+
+        if self.width * self.height != self.numLEDs:
+            raise ValueError('width * height MUST equal total pixel count!'
+                             ' %s * %s != %s'
+                             % (self.width, self.height, self.numLEDs))
+
+        if coordMap:
+            self.matrix_map = coordMap
         else:
-            raise ValueError('Must supply either rows or columns')
-        self.columns, self.rows = columns, rows
+            if len(self.drivers) == 1:
+                log.info('Auto generating coordinate map. Use gen_matrix directly if more control needed.')
+                self.matrix_map = gen_matrix(self.width, self.height)
+            else:
+                raise TypeError(
+                    "Must provide coordMap if using multiple drivers!")
+
+        self.set_pixel_positions(pixel_positions_from_matrix(self.matrix_map))
+
+        # if 90 or 270 rotation dimensions need to be swapped so they match the
+        # matrix rotation
+        if rotation % 2 != 0:
+            w = self.width
+            h = self.height
+            self.width = h
+            self.height = w
+
+        self.texture = None
+        self.set = self._setColor
+
+        if pw < 0 or pw > self.width or ph < 0 or ph > self.height:
+            raise ValueError(
+                "pixelSize must be greater than 0 and not larger than total matrix!")
+        if self.width % pw != 0 or self.height % ph != 0:
+            raise ValueError(
+                "pixelSize must evenly divide into matrix dimensions!")
+
+        if pw == 1 and ph == 1:
+            self._set = self.__setNormal
+        else:
+            self._set = self.__setScaled
+            self.width = self.width / pw
+            self.height = self.height / ph
+            self.numLEDs = self.width * self.height
+
+        self.fonts = font.fonts
+
+    def get_pixel_positions(self):
+        return pixel_positions_from_matrix(self.coord_map)
+
+    def loadFont(self, name, height, width, data):
+        self.fonts[name] = {
+            "data": data,
+            "height": height,
+            "width": width
+        }
+
+    def setTexture(self, tex=None):
+        if tex is None:
+            self.texture = tex
+            self.set = self._setColor
+        else:
+            if not isinstance(tex, list):
+                raise ValueError("Texture must be a list!")
+            elif len(tex) != self.height:
+                raise ValueError(
+                    "Given texture is must be {} high!".format(self.height))
+            else:
+                for r in tex:
+                    if not isinstance(r, list):
+                        raise ValueError("Texture rows must be lists!")
+                    elif len(r) != self.width:
+                        raise ValueError(
+                            "Texture rows must be {} wide!".format(self.width))
+
+            self.texture = tex
+            self.set = self._setTexture
+
+    def __setNormal(self, x, y, color):
+        try:
+            pixel = self.matrix_map[y][x]
+            self._set_base(pixel, color)
+        except IndexError:
+            pass
+
+    def __setScaled(self, x, y, color):
+        sx = x * self.pixelSize[0]
+        sy = y * self.pixelSize[1]
+
+        for xs in range(sx, sx + self.pixelSize[0]):
+            for ys in range(sy, sy + self.pixelSize[1]):
+                self.__setNormal(xs, ys, color)
+
+    # Set single pixel to Color value
+    def _setColor(self, x, y, color=None):
+        try:
+            self._set(x, y, color or (0, 0, 0))
+        except IndexError:
+            pass
+
+    def _setTexture(self, x, y, color=None):
+        if x >= 0 and y >= 0:
+            try:
+                self._set(x, y, color or self.texture[y][x])
+            except IndexError:
+                pass
 
-        ops = (reflect_x and index_ops.reflect_x,
-               reflect_y and index_ops.reflect_y,
-               serpentine_x and index_ops.serpentine_x,
-               serpentine_y and index_ops.serpentine_y,
-               transpose and index_ops.transpose)
-        self.operations = list(filter(None, ops))
+    def get(self, x, y):
+        try:
+            pixel = self.matrix_map[y][x]
+            return self._get_base(pixel)
+        except IndexError:
+            return 0, 0, 0
 
-    def _index(self, x, y):
-        for o in self.operations:
-            x, y = o(x, y, self)
+    def setHSV(self, x, y, hsv):
+        color = colors.hsv2rgb(hsv)
+        self._set(x, y, color)
 
-        if 0 <= x < self.columns and 0 <= y < self.rows:
-            return x + y * self.columns
+    def setRGB(self, x, y, r, g, b):
+        color = (r, g, b)
+        self._set(x, y, color)
 
-        raise IndexError('Index out of range.')
+    ##########################################################################
+    # Drawing Functions
+    # Lovingly borrowed from Adafruit
+    # https://github.com/adafruit/Adafruit-GFX-Library/blob/master/Adafruit_GFX.cpp
+    ##########################################################################
 
-    def get(self, x, y):
-        return self.strip[self._index(x, y)]
+    def drawCircle(self, x0, y0, r, color=None):
+        """Draws a circle at point x0, y0 with radius r of the specified RGB color"""
+        matrix.draw_circle(self.set, x0, y0, r, color)
 
-    def set(self, x, y, value):
-        self.strip[self._index(x, y)] = value
+    def fillCircle(self, x0, y0, r, color=None):
+        """Draws a filled circle at point x0,y0 with radius r and specified color"""
+        matrix.fill_circle(self.set, x0, y0, r, color)
 
+    def drawLine(self, x0, y0, x1, y1, color=None, colorFunc=None, aa=False):
+        matrix.draw_line(self.set, x0, y0, x1, y1, color, colorFunc, aa)
 
-def gen_matrix(dx, dy, serpentine=True, offset=0,
-               rotation=Rotation.ROTATE_0, y_flip=False):
-    """Helper method to generate X,Y coordinate maps for strips"""
+    # Bresenham's algorithm
+    def bresenham_line(self, x0, y0, x1, y1, color=None, colorFunc=None):
+        """Draw line from point x0,y0 to x,1,y1. Will draw beyond matrix bounds."""
+        matrix.bresenham_line(self.set, x0, y0, x1, y1, color, colorFunc)
 
-    result = []
-    for y in range(dy):
-        if not serpentine or y % 2 == 0:
-            result.append([(dx * y) + x + offset for x in range(dx)])
-        else:
-            result.append([((dx * (y + 1)) - 1) - x + offset for x in range(dx)])
+    # Xiaolin Wu's Line Algorithm
+    def wu_line(self, x0, y0, x1, y1, color=None, colorFunc=None):
+        matrix.wu_line(self.set, x0, y0, x1, y1, color, colorFunc)
+
+    def drawRect(self, x, y, w, h, color=None, aa=False):
+        """Draw rectangle with top-left corner at x,y, width w and height h"""
+        matrix.draw_rect(self.set, x, y, w, h, color, aa)
+
+    def fillRect(self, x, y, w, h, color=None, aa=False):
+        """Draw solid rectangle with top-left corner at x,y, width w and height h"""
+        matrix.fill_rect(self.set, x, y, w, h, color, aa)
+
+    def fillScreen(self, color=None):
+        """Fill the matrix with the given RGB color"""
+        matrix.fill_rect(self.set, 0, 0, self.width, self.height, color)
+
+    def drawRoundRect(self, x, y, w, h, r, color=None, aa=False):
+        """Draw rectangle with top-left corner at x,y, width w, height h, and corner radius r"""
+        matrix.draw_round_rect(self.set, x, y, w, h, r, color, aa)
+
+    def fillRoundRect(self, x, y, w, h, r, color=None, aa=False):
+        """Draw solid rectangle with top-left corner at x,y, width w, height h, and corner radius r"""
+        matrix.fill_round_rect(self.set, x, y, w, h, r, color, aa)
 
-    result = rotate_and_flip(result, rotation, y_flip)
+    def drawTriangle(self, x0, y0, x1, y1, x2, y2, color=None, aa=False):
+        """Draw triangle with points x0,y0 - x1,y1 - x2,y2"""
+        matrix.draw_triangle(self.set, x0, y0, x1, y1, x2, y2, color, aa)
 
-    return result
+    def fillTriangle(self, x0, y0, x1, y1, x2, y2, color=None, aa=False):
+        """Draw solid triangle with points x0,y0 - x1,y1 - x2,y2"""
+        matrix.fill_triangle(self.set, x0, y0, x1, y1, x2, y2, color, aa)
 
+    fillTrangle = fillTriangle  # DEPRECATED!
 
-def layout_from_matrix(coord_map):
-    max_width = 0
-    for x in coord_map:
-        if len(x) > max_width:
-            max_width = len(x)
+    def drawChar(self, x, y, c, color, bg, aa=False, font=font.default_font, font_scale=1):
+        matrix.draw_char(self.fonts, self.set, self.width, self.height,
+                         x, y, c, color, bg, aa, font, font_scale)
 
-    num = len(coord_map) * max_width
-    result = [None] * num
+    def drawText(self, text, x=0, y=0, color=None, bg=colors.Off, aa=False, font=font.default_font, font_scale=1):
+        matrix.draw_text(self.fonts, self.set, text, self.width, self.height,
+                         x, y, color, bg, aa, font, font_scale)
 
-    for y in range(len(coord_map)):
-        for x in range(len(coord_map[y])):
-            result[coord_map[y][x]] = [x, y, 0]
 
-    return result
+# This is DEPRECATED
+LEDMatrix = Matrix
diff --git a/bibliopixel/led/multimap.py b/bibliopixel/layout/multimap.py
similarity index 100%
rename from bibliopixel/led/multimap.py
rename to bibliopixel/layout/multimap.py
diff --git a/bibliopixel/led/pov.py b/bibliopixel/layout/pov.py
similarity index 100%
rename from bibliopixel/led/pov.py
rename to bibliopixel/layout/pov.py
diff --git a/bibliopixel/layout/strip.py b/bibliopixel/layout/strip.py
index 6e09269..543affa 100644
--- a/bibliopixel/layout/strip.py
+++ b/bibliopixel/layout/strip.py
@@ -1,34 +1,59 @@
-import abc
-
-
-class Strip(abc.ABC):
-    """Base class for contiguous strips.  You can also use a list as a Strip."""
-
-    @abc.abstractmethod
-    def __getitem__(self, index):
-        """`index` must be an integer, not a slice."""
-        pass
-
-    @abc.abstractmethod
-    def __setitem__(self, index, value):
-        """`index` must be an integer, not a slice."""
-        pass
-
-    @abc.abstractmethod
-    def __len__(self):
-        pass
-
-
-def fill(strip, item, start=0, stop=None, step=1):
-    """Fill a portion of a strip from start to stop by step with a given item.
-    If stop is not given, it defaults to the length of the strip.
-    """
-    if stop is None:
-        stop = len(strip)
-
-    for i in range(start, stop, step):
-        strip[i] = item
-
-
-def gen_strip(num):
-    return [[x, 0, 0] for x in range(num)]
+from .. import colors, data_maker
+from . layout import Layout
+from . geometry.strip import gen_strip
+
+
+class Strip(Layout):
+
+    def __init__(self, drivers, threadedUpdate=False,
+                 brightness=255, pixelWidth=1, **kwargs):
+        super().__init__(drivers, threadedUpdate, brightness,
+                         maker=kwargs.get('maker', data_maker.MAKER))
+
+        self.pixelWidth = pixelWidth
+        if self.pixelWidth < 1 or self.pixelWidth > self.numLEDs:
+            raise ValueError(
+                "pixelWidth must be greater than 0 and less than or equal to the total LED count!")
+        if self.numLEDs % self.pixelWidth != 0:
+            raise ValueError(
+                "Total LED count must be evenly divisible by pixelWidth!")
+        if self.pixelWidth == 1:
+            self.set = self._set
+        else:
+            self.set = self._setScaled
+            self.numLEDs = self.numLEDs / self.pixelWidth
+
+        self.set_pixel_positions(gen_strip(self.numLEDs))
+
+    # Set single pixel to Color value
+    def _set(self, pixel, color):
+        """Set pixel to RGB color tuple"""
+        self._set_base(pixel, color)
+
+    def _setScaled(self, pixel, color):
+        start = pixel * self.pixelWidth
+        for p in range(start, start + self.pixelWidth):
+            self._set_base(p, color)
+
+    def get(self, pixel):
+        """Get RGB color tuple of color at index pixel"""
+        return self._get_base(pixel)
+
+    # Set single pixel to RGB value
+    def setRGB(self, pixel, r, g, b):
+        """Set single pixel using individual RGB values instead of tuple"""
+        self.set(pixel, (r, g, b))
+
+    def setHSV(self, pixel, hsv):
+        """Set single pixel to HSV tuple"""
+        color = colors.hsv2rgb(hsv)
+        self.set(pixel, color)
+
+    # turns off the desired pixel
+    def setOff(self, pixel):
+        """Set single pixel off"""
+        self.set(pixel, (0, 0, 0))
+
+
+# This is DEPRECATED
+LEDStrip = Strip
diff --git a/bibliopixel/led/__init__.py b/bibliopixel/led/__init__.py
deleted file mode 100644
index b14cadb..0000000
--- a/bibliopixel/led/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from . circle import Circle
-from . matrix import Matrix
-from . cube import Cube
-from . pov import POV
-from . strip import Strip
-from .. layout import Rotation
-from .. layout.matrix import gen_matrix
-from .. layout.circle import gen_circle
-from .. layout.cube import gen_cube
-
-# These are DEPRECATED
-from . circle import LEDCircle
-from . cube import LEDCube
-from . matrix import LEDMatrix
-from . pov import LEDPOV
-from . strip import LEDStrip
diff --git a/bibliopixel/led/circle.py b/bibliopixel/led/circle.py
deleted file mode 100644
index f6801c2..0000000
--- a/bibliopixel/led/circle.py
+++ /dev/null
@@ -1,115 +0,0 @@
-from . led import LEDBase
-from .. import data_maker
-from .. layout.circle import gen_circle, calc_ring_steps, calc_ring_pixel_count, layout_from_rings
-
-
-class Circle(LEDBase):
-
-    def __init__(self, drivers, rings=[], pixels_per=None,
-                 maxAngleDiff=0, rotation=0,
-                 threadedUpdate=False, brightness=255, **kwargs):
-        super().__init__(drivers, threadedUpdate, brightness,
-                         maker=kwargs.get('maker', data_maker.MAKER))
-        self.rings = rings
-        self.maxAngleDiff = maxAngleDiff
-        full_coords = False
-        for r in self.rings:
-            full_coords |= (len(r) > 2)
-
-        if not full_coords:
-            self.rings, self.ringSteps = gen_circle(rings=self.rings, pixels_per=pixels_per)
-        else:
-            self.ringSteps = calc_ring_steps(self.rings)
-
-        self.ringCount = len(self.rings)
-        self.lastRing = self.ringCount - 1
-
-        self.set_layout(layout_from_rings(self.rings))
-
-        num = calc_ring_pixel_count(self.rings)
-
-        self.rotation = rotation
-
-        if self.numLEDs != num:
-            raise ValueError(
-                "Total ring LED count does not equal driver LED count!")
-
-    def __genOffsetFromAngle(self, angle, ring):
-        if ring >= self.ringCount:
-            return -1
-
-        angle = (angle + self.rotation) % 360
-        offset = int(round(angle / self.ringSteps[ring]))
-
-        # wraps it back around
-        length = len(self.rings[ring]) - 1
-
-        if offset > length:
-            offset = 0
-
-        if self.maxAngleDiff > 0:
-            calcAngle = (offset * self.ringSteps[ring]) % 360
-            diff = abs(angle - calcAngle)
-            if diff > self.maxAngleDiff:
-                return -1
-
-        return offset
-
-    def angleToPixel(self, angle, ring):
-        offset = self.__genOffsetFromAngle(angle, ring)
-        if offset < 0:
-            return offset
-
-        return self.rings[ring][offset]
-
-    # Set single pixel to Color value
-    def set(self, ring, angle, color):
-        """Set pixel to RGB color tuple"""
-        pixel = self.angleToPixel(angle, ring)
-        self._set_base(pixel, color)
-
-    def get(self, ring, angle):
-        """Get RGB color tuple of color at index pixel"""
-        pixel = self.angleToPixel(angle, ring)
-        return self._get_base(pixel)
-
-    def drawRadius(self, angle, color, startRing=0, endRing=-1):
-        if startRing < 0:
-            startRing = 0
-        if endRing < 0 or endRing > self.lastRing:
-            endRing = self.lastRing
-        for ring in range(startRing, endRing + 1):
-            self.set(ring, angle, color)
-
-    def fillRing(self, ring, color, startAngle=0, endAngle=None):
-        if endAngle is None:
-            endAngle = 359
-
-        if ring >= self.ringCount:
-            raise ValueError("Invalid ring!")
-
-        def pixelRange(ring, start=0, stop=-1):
-            r = self.rings[ring]
-            if stop == -1:
-                stop = len(r) - 1
-
-            return [r[i] for i in range(start, stop + 1)]
-
-        start = self.__genOffsetFromAngle(startAngle, ring)
-        end = self.__genOffsetFromAngle(endAngle, ring)
-
-        pixels = []
-        if start >= end:
-            pixels = pixelRange(ring, start)
-            pixels.extend(pixelRange(ring, 0, end))
-        elif start == end and startAngle > endAngle:
-            pixels = pixelRange(ring)
-        else:
-            pixels = pixelRange(ring, start, end)
-
-        for i in pixels:
-            self._set_base(i, color)
-
-
-# This is DEPRECATED
-LEDCircle = Circle
diff --git a/bibliopixel/led/cube.py b/bibliopixel/led/cube.py
deleted file mode 100644
index 3c7acbf..0000000
--- a/bibliopixel/led/cube.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import math, threading, time
-
-from .. import colors, data_maker, font, matrix
-from . led import LEDBase
-from .. layout.cube import gen_cube, layout_from_cube
-from .. import log
-
-
-class Cube(LEDBase):
-
-    def __init__(self, drivers, x, y, z, coordMap=None,
-                 threadedUpdate=False, brightness=255, **kwargs):
-        super().__init__(drivers, threadedUpdate, brightness,
-                         maker=kwargs.get('maker', data_maker.MAKER))
-
-        self.x = x
-        self.y = y
-        self.z = z
-
-        if self.x * self.y * self.z != self.numLEDs:
-            raise TypeError("(x * y * z) MUST equal the total pixel count!")
-
-        if coordMap:
-            self.cube_map = coordMap
-        else:
-            if len(self.drivers) == 1:
-                log.info('Auto generating coordinate map. Use gen_cube directly if more control needed.')
-                self.cube_map = gen_cube(x, y, z)
-            else:
-                raise TypeError("Must provide coordMap if using multiple drivers!")
-
-        self.set_layout(layout_from_cube(self.cube_map))
-
-    def get_layout(self):
-        return layout_from_cube(self.coord_map)
-
-    def set(self, x, y, z, color):
-        try:
-            pixel = self.cube_map[z][y][x]
-            self._set_base(pixel, color)
-        except IndexError:
-            pass
-
-    def get(self, x, y, z):
-        try:
-            pixel = self.cube_map[z][y][x]
-            return self._get_base(pixel)
-        except IndexError:
-            return 0, 0, 0
-
-    def setHSV(self, x, y, z, hsv):
-        color = colors.hsv2rgb(hsv)
-        self._set(x, y, z, color)
-
-    def setRGB(self, x, y, z, r, g, b):
-        color = (r, g, b)
-        self._set(x, y, z, color)
-
-
-# This is DEPRECATED
-LEDCube = Cube
diff --git a/bibliopixel/led/matrix.py b/bibliopixel/led/matrix.py
deleted file mode 100644
index c476851..0000000
--- a/bibliopixel/led/matrix.py
+++ /dev/null
@@ -1,232 +0,0 @@
-import math, threading, time
-
-from .. import colors, data_maker, font, matrix, log
-from . led import LEDBase
-from .. layout.matrix import Rotation, gen_matrix, layout_from_matrix
-
-
-class Matrix(LEDBase):
-
-    def __init__(self, drivers, width=0, height=0, coordMap=None,
-                 rotation=Rotation.ROTATE_0, vert_flip=False,
-                 threadedUpdate=False, brightness=255,
-                 pixelSize=(1, 1), **kwargs):
-        """Main class for matricies.
-        driver - instance that inherits from DriverBase
-        width - X axis size of matrix
-        height - Y axis size of matrix
-        coordMap - a 2D matrix defining the X,Y to strip index mapping. Not needed in most cases
-        rotation - how to rotate when generating the map. Not used if coordMap specified
-        vert_flip - flips the generated map along the Y axis. This along with rotation can achieve any orientation
-        """
-        super().__init__(drivers, threadedUpdate, brightness,
-                         maker=kwargs.get('maker', data_maker.MAKER))
-
-        if width == 0 and height == 0:
-            if len(self.drivers) != 1:
-                raise TypeError(
-                    "Must provide width and height if using multiple drivers!")
-            width = self.drivers[0].width
-            height = self.drivers[0].height
-
-        self.width = width
-        self.height = height
-        self.pixelSize = pixelSize
-        pw, ph = self.pixelSize
-
-        # if both are 0 try to assume it's a square display
-        if self.width == 0 and self.height == 0:
-            square = int(math.sqrt(self.numLEDs))
-            if (square * square) == self.numLEDs:
-                self.width = self.height = square
-            else:
-                raise TypeError(
-                    "No width or height passed but the number of LEDs is not a perfect square")
-
-        if self.width * self.height != self.numLEDs:
-            raise ValueError('width * height MUST equal total pixel count!'
-                             ' %s * %s != %s'
-                             % (self.width, self.height, self.numLEDs))
-
-        if coordMap:
-            self.matrix_map = coordMap
-        else:
-            if len(self.drivers) == 1:
-                log.info('Auto generating coordinate map. Use gen_matrix directly if more control needed.')
-                self.matrix_map = gen_matrix(self.width, self.height)
-            else:
-                raise TypeError(
-                    "Must provide coordMap if using multiple drivers!")
-
-        self.set_layout(layout_from_matrix(self.matrix_map))
-
-        # if 90 or 270 rotation dimensions need to be swapped so they match the
-        # matrix rotation
-        if rotation % 2 != 0:
-            w = self.width
-            h = self.height
-            self.width = h
-            self.height = w
-
-        self.texture = None
-        self.set = self._setColor
-
-        if pw < 0 or pw > self.width or ph < 0 or ph > self.height:
-            raise ValueError(
-                "pixelSize must be greater than 0 and not larger than total matrix!")
-        if self.width % pw != 0 or self.height % ph != 0:
-            raise ValueError(
-                "pixelSize must evenly divide into matrix dimensions!")
-
-        if pw == 1 and ph == 1:
-            self._set = self.__setNormal
-        else:
-            self._set = self.__setScaled
-            self.width = self.width / pw
-            self.height = self.height / ph
-            self.numLEDs = self.width * self.height
-
-        self.fonts = font.fonts
-
-    def get_layout(self):
-        return layout_from_matrix(self.coord_map)
-
-    def loadFont(self, name, height, width, data):
-        self.fonts[name] = {
-            "data": data,
-            "height": height,
-            "width": width
-        }
-
-    def setTexture(self, tex=None):
-        if tex is None:
-            self.texture = tex
-            self.set = self._setColor
-        else:
-            if not isinstance(tex, list):
-                raise ValueError("Texture must be a list!")
-            elif len(tex) != self.height:
-                raise ValueError(
-                    "Given texture is must be {} high!".format(self.height))
-            else:
-                for r in tex:
-                    if not isinstance(r, list):
-                        raise ValueError("Texture rows must be lists!")
-                    elif len(r) != self.width:
-                        raise ValueError(
-                            "Texture rows must be {} wide!".format(self.width))
-
-            self.texture = tex
-            self.set = self._setTexture
-
-    def __setNormal(self, x, y, color):
-        try:
-            pixel = self.matrix_map[y][x]
-            self._set_base(pixel, color)
-        except IndexError:
-            pass
-
-    def __setScaled(self, x, y, color):
-        sx = x * self.pixelSize[0]
-        sy = y * self.pixelSize[1]
-
-        for xs in range(sx, sx + self.pixelSize[0]):
-            for ys in range(sy, sy + self.pixelSize[1]):
-                self.__setNormal(xs, ys, color)
-
-    # Set single pixel to Color value
-    def _setColor(self, x, y, color=None):
-        try:
-            self._set(x, y, color or (0, 0, 0))
-        except IndexError:
-            pass
-
-    def _setTexture(self, x, y, color=None):
-        if x >= 0 and y >= 0:
-            try:
-                self._set(x, y, color or self.texture[y][x])
-            except IndexError:
-                pass
-
-    def get(self, x, y):
-        try:
-            pixel = self.matrix_map[y][x]
-            return self._get_base(pixel)
-        except IndexError:
-            return 0, 0, 0
-
-    def setHSV(self, x, y, hsv):
-        color = colors.hsv2rgb(hsv)
-        self._set(x, y, color)
-
-    def setRGB(self, x, y, r, g, b):
-        color = (r, g, b)
-        self._set(x, y, color)
-
-    ##########################################################################
-    # Drawing Functions
-    # Lovingly borrowed from Adafruit
-    # https://github.com/adafruit/Adafruit-GFX-Library/blob/master/Adafruit_GFX.cpp
-    ##########################################################################
-
-    def drawCircle(self, x0, y0, r, color=None):
-        """Draws a circle at point x0, y0 with radius r of the specified RGB color"""
-        matrix.draw_circle(self.set, x0, y0, r, color)
-
-    def fillCircle(self, x0, y0, r, color=None):
-        """Draws a filled circle at point x0,y0 with radius r and specified color"""
-        matrix.fill_circle(self.set, x0, y0, r, color)
-
-    def drawLine(self, x0, y0, x1, y1, color=None, colorFunc=None, aa=False):
-        matrix.draw_line(self.set, x0, y0, x1, y1, color, colorFunc, aa)
-
-    # Bresenham's algorithm
-    def bresenham_line(self, x0, y0, x1, y1, color=None, colorFunc=None):
-        """Draw line from point x0,y0 to x,1,y1. Will draw beyond matrix bounds."""
-        matrix.bresenham_line(self.set, x0, y0, x1, y1, color, colorFunc)
-
-    # Xiaolin Wu's Line Algorithm
-    def wu_line(self, x0, y0, x1, y1, color=None, colorFunc=None):
-        matrix.wu_line(self.set, x0, y0, x1, y1, color, colorFunc)
-
-    def drawRect(self, x, y, w, h, color=None, aa=False):
-        """Draw rectangle with top-left corner at x,y, width w and height h"""
-        matrix.draw_rect(self.set, x, y, w, h, color, aa)
-
-    def fillRect(self, x, y, w, h, color=None, aa=False):
-        """Draw solid rectangle with top-left corner at x,y, width w and height h"""
-        matrix.fill_rect(self.set, x, y, w, h, color, aa)
-
-    def fillScreen(self, color=None):
-        """Fill the matrix with the given RGB color"""
-        matrix.fill_rect(self.set, 0, 0, self.width, self.height, color)
-
-    def drawRoundRect(self, x, y, w, h, r, color=None, aa=False):
-        """Draw rectangle with top-left corner at x,y, width w, height h, and corner radius r"""
-        matrix.draw_round_rect(self.set, x, y, w, h, r, color, aa)
-
-    def fillRoundRect(self, x, y, w, h, r, color=None, aa=False):
-        """Draw solid rectangle with top-left corner at x,y, width w, height h, and corner radius r"""
-        matrix.fill_round_rect(self.set, x, y, w, h, r, color, aa)
-
-    def drawTriangle(self, x0, y0, x1, y1, x2, y2, color=None, aa=False):
-        """Draw triangle with points x0,y0 - x1,y1 - x2,y2"""
-        matrix.draw_triangle(self.set, x0, y0, x1, y1, x2, y2, color, aa)
-
-    def fillTriangle(self, x0, y0, x1, y1, x2, y2, color=None, aa=False):
-        """Draw solid triangle with points x0,y0 - x1,y1 - x2,y2"""
-        matrix.fill_triangle(self.set, x0, y0, x1, y1, x2, y2, color, aa)
-
-    fillTrangle = fillTriangle  # DEPRECATED!
-
-    def drawChar(self, x, y, c, color, bg, aa=False, font=font.default_font, font_scale=1):
-        matrix.draw_char(self.fonts, self.set, self.width, self.height,
-                         x, y, c, color, bg, aa, font, font_scale)
-
-    def drawText(self, text, x=0, y=0, color=None, bg=colors.Off, aa=False, font=font.default_font, font_scale=1):
-        matrix.draw_text(self.fonts, self.set, text, self.width, self.height,
-                         x, y, color, bg, aa, font, font_scale)
-
-
-# This is DEPRECATED
-LEDMatrix = Matrix
diff --git a/bibliopixel/led/strip.py b/bibliopixel/led/strip.py
deleted file mode 100644
index e7c5db7..0000000
--- a/bibliopixel/led/strip.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from .. import colors, data_maker
-from . led import LEDBase
-from .. layout.strip import gen_strip
-
-
-class Strip(LEDBase):
-
-    def __init__(self, drivers, threadedUpdate=False,
-                 brightness=255, pixelWidth=1, **kwargs):
-        super().__init__(drivers, threadedUpdate, brightness,
-                         maker=kwargs.get('maker', data_maker.MAKER))
-
-        self.pixelWidth = pixelWidth
-        if self.pixelWidth < 1 or self.pixelWidth > self.numLEDs:
-            raise ValueError(
-                "pixelWidth must be greater than 0 and less than or equal to the total LED count!")
-        if self.numLEDs % self.pixelWidth != 0:
-            raise ValueError(
-                "Total LED count must be evenly divisible by pixelWidth!")
-        if self.pixelWidth == 1:
-            self.set = self._set
-        else:
-            self.set = self._setScaled
-            self.numLEDs = self.numLEDs / self.pixelWidth
-
-        self.set_layout(gen_strip(self.numLEDs))
-
-    # Set single pixel to Color value
-    def _set(self, pixel, color):
-        """Set pixel to RGB color tuple"""
-        self._set_base(pixel, color)
-
-    def _setScaled(self, pixel, color):
-        start = pixel * self.pixelWidth
-        for p in range(start, start + self.pixelWidth):
-            self._set_base(p, color)
-
-    def get(self, pixel):
-        """Get RGB color tuple of color at index pixel"""
-        return self._get_base(pixel)
-
-    # Set single pixel to RGB value
-    def setRGB(self, pixel, r, g, b):
-        """Set single pixel using individual RGB values instead of tuple"""
-        self.set(pixel, (r, g, b))
-
-    def setHSV(self, pixel, hsv):
-        """Set single pixel to HSV tuple"""
-        color = colors.hsv2rgb(hsv)
-        self.set(pixel, color)
-
-    # turns off the desired pixel
-    def setOff(self, pixel):
-        """Set single pixel off"""
-        self.set(pixel, (0, 0, 0))
-
-
-# This is DEPRECATED
-LEDStrip = Strip
diff --git a/bibliopixel/main/demo.py b/bibliopixel/main/demo.py
index 26ef580..c60a255 100644
--- a/bibliopixel/main/demo.py
+++ b/bibliopixel/main/demo.py
@@ -21,23 +21,23 @@ def make_runnable(demo, args):
 
     if 'driver' in demo:
         if not demo['driver'].get('num'):
-            if 'led' in demo and demo['led']['typename'] == 'cube':
+            if 'layout' in demo and demo['layout']['typename'] == 'cube':
                 demo['driver']['num'] = args.width * args.height * args.depth
             else:
                 demo['driver']['num'] = args.width * args.height
 
-    if 'led' in demo:
-        led = demo['led']
-        if 'width' in led:
-            led['width'] = led['width'] or args.width
-        if 'x' in led:
-            led['x'] = led['x'] or args.width
-        if 'height' in led:
-            led['height'] = led['height'] or args.height
-        if 'y' in led:
-            led['y'] = led['y'] or args.height
-        if 'z' in led:
-            led['z'] = led['z'] or args.depth
+    if 'layout' in demo:
+        layout = demo['layout']
+        if 'width' in layout:
+            layout['width'] = layout['width'] or args.width
+        if 'x' in layout:
+            layout['x'] = layout['x'] or args.width
+        if 'height' in layout:
+            layout['height'] = layout['height'] or args.height
+        if 'y' in layout:
+            layout['y'] = layout['y'] or args.height
+        if 'z' in layout:
+            layout['z'] = layout['z'] or args.depth
 
     defaults = _run.get_project_default_arguments(args)
     return project.project_to_runnable(demo, defaults)
diff --git a/bibliopixel/main/demo_table.py b/bibliopixel/main/demo_table.py
index 3fbff99..425e552 100644
--- a/bibliopixel/main/demo_table.py
+++ b/bibliopixel/main/demo_table.py
@@ -1,5 +1,6 @@
 from bibliopixel.drivers.SimPixel import SimPixel
-from bibliopixel import Circle, Matrix, layout
+from bibliopixel import Circle, Matrix
+from bibliopixel.layout import geometry
 from bibliopixel.animation import Sequence
 
 BLOOM = {
@@ -8,7 +9,7 @@ BLOOM = {
         'num': 0
     },
 
-    'led': {
+    'layout': {
         'typename': 'matrix',
         'width': 0,
         'height': 0,
@@ -26,7 +27,7 @@ MATRIX_PROJECT = {
         'num': 0
     },
 
-    'led': {
+    'layout': {
         'typename': 'matrix',
         'width': 0,
         'height': 0,
@@ -84,7 +85,7 @@ CUBE_PROJECT = {
         'num': 0
     },
 
-    'led': {
+    'layout': {
         'typename': 'cube',
         'x': 0,
         'y': 0,
@@ -117,36 +118,37 @@ CUBE_PROJECT = {
 
 def matrix(args):
     driver = SimPixel(args.width * args.height)
-    led = Matrix(driver, width=args.width, height=args.height)
+    layout = Matrix(driver, width=args.width, height=args.height)
 
-    anim = Sequence(led)
+    anim = Sequence(layout)
 
     from BiblioPixelAnimations.matrix.MatrixRain import MatrixRainBow
-    anim.add_animation(MatrixRainBow(led), amt=1, fps=20, seconds=8)
+    anim.add_animation(MatrixRainBow(layout), amt=1, fps=20, seconds=8)
 
     from BiblioPixelAnimations.matrix.Text import ScrollText
     anim.add_animation(
-        ScrollText(led, 'BiblioPixel Demo', xPos=args.width, font_scale=2),
+        ScrollText(layout, 'BiblioPixel Demo', xPos=args.width, font_scale=2),
         amt=1, fps=30, until_complete=True)
 
     from BiblioPixelAnimations.matrix.bloom import Bloom
-    anim.add_animation(Bloom(led), amt=3, fps=60, seconds=8)
+    anim.add_animation(Bloom(layout), amt=3, fps=60, seconds=8)
 
     from BiblioPixelAnimations.matrix.circlepop import CirclePop
-    anim.add_animation(CirclePop(led), amt=1, fps=30, seconds=8)
+    anim.add_animation(CirclePop(layout), amt=1, fps=30, seconds=8)
 
     return anim
 
 
 def circle(args):
     pixels_per = [1, 4, 8, 12, 18, 24, 32, 40, 52, 64]
-    rings, steps = layout.gen_circle(pixels_per=pixels_per)
-    points = layout.layout_from_rings(rings, origin=(200, 200, 0), z_diff=8)
+    rings, steps = geometry.gen_circle(pixels_per=pixels_per)
+    points = geometry.pixel_positions_from_rings(
+        rings, origin=(200, 200, 0), z_diff=8)
     driver = SimPixel(sum(pixels_per), layout=points)
-    led = Circle(driver, rings=rings, maxAngleDiff=0)
+    layout = Circle(driver, rings=rings, maxAngleDiff=0)
 
     from BiblioPixelAnimations.circle.bloom import CircleBloom
-    return CircleBloom(led)
+    return CircleBloom(layout)
 
 
 DEMO_TABLE = {
diff --git a/bibliopixel/main/help.py b/bibliopixel/main/help.py
index 1654802..451f540 100644
--- a/bibliopixel/main/help.py
+++ b/bibliopixel/main/help.py
@@ -14,7 +14,7 @@ BiblioPixel projects are in JSON.  Each project is an object with four named
 values called "sections".
 
   * driver: identifies the hardware driving the LED and its characteristics.
-  * led: represents the geometric layout of the LEDs in one or more drivers.
+  * layout: represents the geometric layout of the LEDs in one or more drivers.
   * animation: a program that changes the LEDs over time.
   * run: time settings for the animation.
 
@@ -24,7 +24,7 @@ default values.
 
 THE SETTINGS FILE
 
-The Settings File lets you reuse sections (driver, led, animation or run)
+The Settings File lets you reuse sections (driver, layout, animation or run)
 between projects.
 
 For each section, there's a list of
diff --git a/bibliopixel/main/run.py b/bibliopixel/main/run.py
index 96747d8..974ca9e 100644
--- a/bibliopixel/main/run.py
+++ b/bibliopixel/main/run.py
@@ -12,8 +12,8 @@ def add_project_default_arguments(parser):
         help='Default driver type if no driver is specified')
 
     parser.add_argument(
-        '-l', '--led', default=None,
-        help='Default LED class if no LED is specified')
+        '-l', '--layout', default=None,
+        help='Default LAYOUT class if no LAYOUT is specified')
 
     parser.add_argument(
         '-t', '--ledtype', default=None,
@@ -27,7 +27,7 @@ def add_project_default_arguments(parser):
 def get_project_default_arguments(args):
     return {
         'driver': args.driver,
-        'led': args.led,
+        'layout': args.layout,
         'animation': args.animation,
         'ledtype': args.ledtype,
     }
diff --git a/bibliopixel/matrix.py b/bibliopixel/matrix.py
index e6b8faa..3446844 100644
--- a/bibliopixel/matrix.py
+++ b/bibliopixel/matrix.py
@@ -98,7 +98,7 @@ def _fill_circle_helper(setter, x0, y0, r, cornername, delta, color=None):
 
 
 def fill_circle(setter, x0, y0, r, color=None):
-    """Draws a filled circle at point x0,y0 with radius r and specified color"""
+    """Draws a fillayout circle at point x0,y0 with radius r and specified color"""
     _draw_fast_vline(setter, x0, y0 - r, 2 * r + 1, color)
     _fill_circle_helper(setter, x0, y0, r, 3, 0, color)
 
diff --git a/bibliopixel/project/aliases.py b/bibliopixel/project/aliases.py
index b149b6c..1d4f369 100644
--- a/bibliopixel/project/aliases.py
+++ b/bibliopixel/project/aliases.py
@@ -18,12 +18,12 @@ ALIASES = {
         'pi_ws281x': 'bibliopixel.drivers.PiWS281X.PiWS281X'
     },
 
-    'led': {
-        'circle': 'bibliopixel.led.circle.Circle',
-        'cube': 'bibliopixel.led.cube.Cube',
-        'matrix': 'bibliopixel.led.matrix.Matrix',
-        'pov': 'bibliopixel.led.pov.POV',
-        'strip': 'bibliopixel.led.strip.Strip',
+    'layout': {
+        'circle': 'bibliopixel.layout.circle.Circle',
+        'cube': 'bibliopixel.layout.cube.Cube',
+        'matrix': 'bibliopixel.layout.matrix.Matrix',
+        'pov': 'bibliopixel.layout.pov.POV',
+        'strip': 'bibliopixel.layout.strip.Strip',
     },
 
     'animation': {
diff --git a/bibliopixel/project/defaults.py b/bibliopixel/project/defaults.py
index 2c3aeba..c2c0b6d 100644
--- a/bibliopixel/project/defaults.py
+++ b/bibliopixel/project/defaults.py
@@ -8,8 +8,8 @@ DEFAULTS = {
         'num': 1024
     },
 
-    'led': {
-        'typename': 'bibliopixel.led.matrix.Matrix',
+    'layout': {
+        'typename': 'bibliopixel.layout.matrix.Matrix',
         'width': 32,
         'height': 32,
     },
diff --git a/bibliopixel/project/preset_library.py b/bibliopixel/project/preset_library.py
index fe55404..e9bc487 100644
--- a/bibliopixel/project/preset_library.py
+++ b/bibliopixel/project/preset_library.py
@@ -5,7 +5,7 @@ from ..util.datafile import DataFile
 """
 The BP preset library is a JSON file with five named sections:
 
-    project, driver, led, animation, run.
+    project, driver, layout, animation, run.
 
 Each section is a dictionary that maps a name to a dictionary called a "preset".
 
diff --git a/bibliopixel/project/project.py b/bibliopixel/project/project.py
index 208f0bf..ef51eb6 100644
--- a/bibliopixel/project/project.py
+++ b/bibliopixel/project/project.py
@@ -3,13 +3,13 @@ from . import defaults
 from . importer import make_object
 from .. animation import runner
 from .. import data_maker
-from .. layout import gen_matrix
-from .. led.multimap import MultiMapBuilder
+from .. layout.geometry import gen_matrix
+from .. layout.multimap import MultiMapBuilder
 from .. util import files
 from .. drivers.serial import codes
 
 
-def make_led(driver, led, maker=None):
+def make_layout(driver, layout, maker=None):
     maker = data_maker.Maker(**(maker or {}))
     drivers = []
 
@@ -30,11 +30,11 @@ def make_led(driver, led, maker=None):
         drivers.append(make_object(**kwds))
 
     coordMap = make_drivers(maker=maker, **driver)
-    return make_object(drivers, coordMap=coordMap, maker=maker, **led)
+    return make_object(drivers, coordMap=coordMap, maker=maker, **layout)
 
 
-def make_animation(led, animation, run=None):
-    animation = make_object(led, **animation)
+def make_animation(layout, animation, run=None):
+    animation = make_object(layout, **animation)
     animation.set_runner(runner.Runner(**(run or {})))
     return animation
 
@@ -45,8 +45,8 @@ def project_to_animation(*, path=None, **project):
     kwds = defaults.apply_defaults(project)
     animation = kwds.pop('animation', {})
     run = kwds.pop('run', {})
-    led = make_led(**kwds)
-    return make_animation(led, animation, run)
+    layout = make_layout(**kwds)
+    return make_animation(layout, animation, run)
 
 
 def apply_defaults(project, defaults):
@@ -68,12 +68,12 @@ def apply_defaults(project, defaults):
 
     ledtype = defaults.get('ledtype')
     if ledtype:
-        if 'led' not in project:
-            project['led'] = {}
+        if 'layout' not in project:
+            project['layout'] = {}
 
-        if 'type' not in project['led']:
-            project['led']['type'] = ledtype
-            # The field in LED should probably be renamed "ledtype" to be
+        if 'type' not in project['layout']:
+            project['layout']['type'] = ledtype
+            # The field in LAYOUT should probably be renamed "ledtype" to be
             # consistent across the whole system.
 
 
diff --git a/bibliopixel/threads/update_threading.py b/bibliopixel/threads/update_threading.py
index b1f9437..b750ed1 100644
--- a/bibliopixel/threads/update_threading.py
+++ b/bibliopixel/threads/update_threading.py
@@ -75,13 +75,13 @@ class NoThreading(object):
     """
     """
 
-    def __init__(self, led):
-        self.led = led
+    def __init__(self, layout):
+        self.layout = layout
 
     def update_colors(self):
-        for d in self.led.drivers:
+        for d in self.layout.drivers:
             d.update_colors()
-        for d in self.led.drivers:
+        for d in self.layout.drivers:
             d.sync()
 
     def wait_for_update(self):
@@ -94,22 +94,22 @@ class NoThreading(object):
 
 
 class UseThreading(NoThreading):
-    def __init__(self, led):
-        self.led = led
-        self.update_thread = UpdateThread(led.drivers)
+    def __init__(self, layout):
+        self.layout = layout
+        self.update_thread = UpdateThread(layout.drivers)
         self.update_thread.start()
 
     def update_colors(self):
         self.update_thread.update_colors()
 
     def wait_for_update(self):
-        while all([d._thread.sending() for d in self.led.drivers]):
+        while all([d._thread.sending() for d in self.layout.drivers]):
             time.sleep(0.000001)
 
 
-def UpdateThreading(enable, led):
+def UpdateThreading(enable, layout):
     """
     UpdateThreading handles threading - and eventually multiprocessing - for
-    LEDBase.
+    Layout.
     """
-    return (UseThreading if enable else NoThreading)(led)
+    return (UseThreading if enable else NoThreading)(layout)


Test patch:
diff --git a/bibliopixel/animation/tests.py b/bibliopixel/animation/tests.py
index b8e8f06..fd4c6bd 100644
--- a/bibliopixel/animation/tests.py
+++ b/bibliopixel/animation/tests.py
@@ -5,62 +5,62 @@ from .. import colors
 
 class StripChannelTest(BaseStripAnim):
 
-    def __init__(self, led):
-        super().__init__(led)
+    def __init__(self, layout):
+        super().__init__(layout)
         self.internal_delay = 0.500
         self.colors = [colors.Red, colors.Green, colors.Blue, colors.White]
 
     def step(self, amt=1):
 
-        self._led.set(0, colors.Red)
-        self._led.set(1, colors.Green)
-        self._led.set(2, colors.Green)
-        self._led.set(3, colors.Blue)
-        self._led.set(4, colors.Blue)
-        self._led.set(5, colors.Blue)
+        self.layout.set(0, colors.Red)
+        self.layout.set(1, colors.Green)
+        self.layout.set(2, colors.Green)
+        self.layout.set(3, colors.Blue)
+        self.layout.set(4, colors.Blue)
+        self.layout.set(5, colors.Blue)
 
         color = self._step % 4
-        self._led.fill(self.colors[color], 7, 9)
+        self.layout.fill(self.colors[color], 7, 9)
 
         self._step += 1
 
 
 class MatrixChannelTest(BaseMatrixAnim):
 
-    def __init__(self, led):
-        super().__init__(led, 0, 0)
+    def __init__(self, layout):
+        super().__init__(layout, 0, 0)
         self.internal_delay = 0.500
         self.colors = [colors.Red, colors.Green, colors.Blue, colors.White]
 
     def step(self, amt=1):
 
-        self._led.drawLine(0, 0, 0, self.height - 1, colors.Red)
-        self._led.drawLine(1, 0, 1, self.height - 1, colors.Green)
-        self._led.drawLine(2, 0, 2, self.height - 1, colors.Green)
-        self._led.drawLine(3, 0, 3, self.height - 1, colors.Blue)
-        self._led.drawLine(4, 0, 4, self.height - 1, colors.Blue)
-        self._led.drawLine(5, 0, 5, self.height - 1, colors.Blue)
+        self.layout.drawLine(0, 0, 0, self.height - 1, colors.Red)
+        self.layout.drawLine(1, 0, 1, self.height - 1, colors.Green)
+        self.layout.drawLine(2, 0, 2, self.height - 1, colors.Green)
+        self.layout.drawLine(3, 0, 3, self.height - 1, colors.Blue)
+        self.layout.drawLine(4, 0, 4, self.height - 1, colors.Blue)
+        self.layout.drawLine(5, 0, 5, self.height - 1, colors.Blue)
 
         color = self._step % 4
-        self._led.fillRect(7, 0, 3, self.height, self.colors[color])
+        self.layout.fillRect(7, 0, 3, self.height, self.colors[color])
 
         self._step += 1
 
 
 class MatrixCalibrationTest(BaseMatrixAnim):
 
-    def __init__(self, led):
-        super().__init__(led, 0, 0)
+    def __init__(self, layout):
+        super().__init__(layout, 0, 0)
         self.internal_delay = 0.500
         self.colors = [colors.Red, colors.Green, colors.Green,
                        colors.Blue, colors.Blue, colors.Blue]
 
     def step(self, amt=1):
-        self._led.all_off()
+        self.layout.all_off()
         i = self._step % self.width
         for x in range(i + 1):
             c = self.colors[x % len(self.colors)]
-            self._led.drawLine(x, 0, x, i, c)
+            self.layout.drawLine(x, 0, x, i, c)
 
         self.completed = (i == (self.width - 1))
 
diff --git a/test/aliases_test.py b/test/aliases_test.py
index 788a03e..e357b2e 100644
--- a/test/aliases_test.py
+++ b/test/aliases_test.py
@@ -7,11 +7,11 @@ class AliasTest(unittest.TestCase):
         self.assertEqual(resolve_aliases({}), {})
 
     def test_simple(self):
-        self.assertEqual(resolve_aliases({'led': {}}), {'led': {}})
-        d = {'led': {'typename': 'bibliopixel.led.circle.Circle'}}
+        self.assertEqual(resolve_aliases({'layout': {}}), {'layout': {}})
+        d = {'layout': {'typename': 'bibliopixel.layout.circle.Circle'}}
         self.assertEqual(resolve_aliases(d), d)
 
     def test_resolve(self):
-        d = {'led': {'typename': 'bibliopixel.led.circle.Circle'}}
-        self.assertEqual(resolve_aliases({'led': 'circle'}), d)
-        self.assertEqual(resolve_aliases({'led': {'typename': 'circle'}}), d)
+        d = {'layout': {'typename': 'bibliopixel.layout.circle.Circle'}}
+        self.assertEqual(resolve_aliases({'layout': 'circle'}), d)
+        self.assertEqual(resolve_aliases({'layout': {'typename': 'circle'}}), d)
diff --git a/test/all_test.py b/test/all_test.py
index 9e4129f..050887d 100644
--- a/test/all_test.py
+++ b/test/all_test.py
@@ -11,7 +11,6 @@ from bibliopixel import (
     gamma,
     image,
     layout,
-    led,
     log,
     matrix,
     return_codes,
@@ -19,6 +18,8 @@ from bibliopixel import (
     util,
 )
 
+from bibliopixel.layout import geometry
+
 from bibliopixel.drivers import (
     SPI,
     # SimPixel,
diff --git a/test/dummy_test.py b/test/dummy_test.py
index 430c370..6126a4b 100644
--- a/test/dummy_test.py
+++ b/test/dummy_test.py
@@ -1,11 +1,11 @@
 import unittest
 
-from bibliopixel.led.strip import Strip
+from bibliopixel.layout.strip import Strip
 from bibliopixel.drivers.dummy_driver import Dummy
 
 
 class DummyTest(unittest.TestCase):
     def test_dummy(self):
         for driver in Dummy(32), Dummy(32, 0.01):
-            led = Strip([driver])
-            led.push_to_driver()
+            layout = Strip([driver])
+            layout.push_to_driver()
diff --git a/test/layout_matrix_test.py b/test/layout_matrix_test.py
index 9ae4828..1bbb1e3 100644
--- a/test/layout_matrix_test.py
+++ b/test/layout_matrix_test.py
@@ -1,5 +1,5 @@
 import unittest
-from bibliopixel.layout import matrix
+from bibliopixel.layout.geometry import matrix
 
 
 class MatrixTest(unittest.TestCase):
diff --git a/test/matrix_test.py b/test/matrix_test.py
index 3bb4130..390be81 100644
--- a/test/matrix_test.py
+++ b/test/matrix_test.py
@@ -1,7 +1,7 @@
 import unittest
 
 from bibliopixel import data_maker
-from bibliopixel.led import Matrix
+from bibliopixel.layout import Matrix
 from bibliopixel.drivers.driver_base import DriverBase
 
 
diff --git a/test/project.json b/test/project.json
index 6904803..d1199ee 100644
--- a/test/project.json
+++ b/test/project.json
@@ -4,8 +4,8 @@
         "num": 12
     },
 
-    "led": {
-        "typename": "bibliopixel.led.strip.Strip"
+    "layout": {
+        "typename": "bibliopixel.layout.strip.Strip"
     },
 
     "animation": {
diff --git a/test/project_test.py b/test/project_test.py
index 1dc0b4d..fd142bb 100644
--- a/test/project_test.py
+++ b/test/project_test.py
@@ -11,7 +11,7 @@ PROJECT = """
         "num": 12
     },
 
-    "led": "strip",
+    "layout": "strip",
     "animation": "strip_test",
     "run": {
         "max_steps": 2
@@ -30,7 +30,7 @@ PROJECT_MULTI = """
         "device_ids": [10, 11, 12]
     },
 
-    "led": {
+    "layout": {
         "typename": "matrix",
         "width": 128
     },
@@ -51,7 +51,7 @@ PROJECT_SHARED = """
         "num": 12
     },
 
-    "led": "strip",
+    "layout": "strip",
     "animation": "strip_test",
     "run": {
         "max_steps": 2
@@ -71,8 +71,8 @@ PROJECT_SIM = """
         "num": 12
     },
 
-    "led": {
-        "typename": "bibliopixel.led.strip.Strip"
+    "layout": {
+        "typename": "bibliopixel.layout.strip.Strip"
     },
 
     "animation": {
diff --git a/test/segment_test.py b/test/segment_test.py
index 02af980..740bb61 100644
--- a/test/segment_test.py
+++ b/test/segment_test.py
@@ -1,5 +1,5 @@
 import unittest
-from bibliopixel.layout import segment
+from bibliopixel.layout.geometry import segment
 
 
 class SegmentTest(unittest.TestCase):
diff --git a/test/serial_test.py b/test/serial_test.py
index 7410ea8..88b15b5 100644
--- a/test/serial_test.py
+++ b/test/serial_test.py
@@ -48,19 +48,19 @@ class SerialTest(unittest.TestCase):
 
         driver = Serial(
             LEDTYPE.APA102, 600, SPISpeed=4, c_order=ChannelOrder.RGB)
-        led = Strip(driver)
+        layout = Strip(driver)
 
         for b in range(7, 256, 8):
             log.info('Set brightness ' + str(b))
-            led.set_brightness(b)
+            layout.set_brightness(b)
             time.sleep(0.25)
 
         log.info('Run StripChannelTest')
-        anim = StripChannelTest(led)
+        anim = StripChannelTest(layout)
 
         anim.run(amt=1, max_steps=8)
 
-        led.all_off()
-        led.update()
+        layout.all_off()
+        layout.update()
 
         print("Test Complete!")
diff --git a/test/simpixel_project.json b/test/simpixel_project.json
index ce09683..ce40227 100644
--- a/test/simpixel_project.json
+++ b/test/simpixel_project.json
@@ -6,7 +6,7 @@
         "num": 1024
     },
 
-    "led": {
+    "layout": {
         "typename": "matrix",
         "width": 32,
         "height": 32