Skip to content

Commit

Permalink
Added support for begin/end blocks, with repeat and matrix block proc…
Browse files Browse the repository at this point in the history
…essors
  • Loading branch information
abey79 committed Nov 9, 2019
1 parent b6c5f3b commit 4c7858c
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 11 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
.DS_Store
/.idea
__pycache__
*.egg-info
*.egg-info
/pip-wheel-metadata
5 changes: 2 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@
## Filters

- geometry: mask with rectangle, circle, etc.
- (MVP) matrix: static, dynamic

## Output

- (MVP) png
- (MVP) svg option
- scale to phyisical size
- scale to physical size
- output format (A4, A3, letter, etc.)
- axidraw api
- AxiDraw api

## hatched

Expand Down
1 change: 1 addition & 0 deletions vpype/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .vpype import cli, processor, generator

# register all commands
from .blocks import *
from .generators import *
from .hatch import *
from .transforms import *
Expand Down
6 changes: 3 additions & 3 deletions vpype/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .vpype import cli
# from .vpype import cli
import vpype

if __name__ == '__main__':
cli()
vpype.cli()
44 changes: 44 additions & 0 deletions vpype/blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Tuple

import click
from shapely import affinity

from .vpype import cli, block_processor, BlockProcessor, execute_processors, merge_mls


@cli.command("matrix")
@click.option("-n", "--number", nargs=2, default=(2, 2), type=int)
@click.option("-d", "--delta", nargs=2, default=(100, 100), type=float)
@block_processor
class MatrixBlockProcessor(BlockProcessor):
"""
Arrange generated geometries on a cartesian matrix
"""

def __init__(self, number: Tuple[int, int], delta: Tuple[float, float]):
self.number = number
self.delta = delta

def process(self, processors):
mls_arr = []
for i in range(self.number[0]):
for j in range(self.number[1]):
mls = execute_processors(processors)
mls_arr.append(affinity.translate(mls, self.delta[0] * i, self.delta[1] * j))

return merge_mls(mls_arr)


@cli.command("repeat")
@click.option("-n", "--number", default=1, type=int)
@block_processor
class RepeatBlockProcessor(BlockProcessor):
"""
Overlap generated geometries on top of each other.
"""

def __init__(self, number: int):
self.number = number

def process(self, processors):
return merge_mls([execute_processors(processors) for _ in range(self.number)])
144 changes: 140 additions & 4 deletions vpype/vpype.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import logging
from functools import update_wrapper
from typing import Iterable

import click
from shapely.geometry import MultiLineString


@click.group(chain=True)
@click.option("-v", "--verbose", "verbose", count=True)
@click.option("-v", "--verbose", count=True)
def cli(verbose):
logging.basicConfig()
if verbose == 0:
Expand All @@ -20,9 +21,100 @@ def cli(verbose):
# noinspection PyShadowingNames,PyUnusedLocal
@cli.resultcallback()
def process_pipeline(processors, verbose):
execute_processors(processors)


def execute_processors(processors) -> MultiLineString:
"""
Execute a sequence of processors to generate a MultiLineString. For block handling, we use
a recursive approach. Only top-level blocks are extracted and processed by block
processors, which, in turn, call this function again.
:param processors: iterable of processors
:return:
"""
# create a stack with a base frame, which is empty and has no block processor
outer_processors = list()
top_level_processors = list()
block = None
nested_count = 0
expect_block = False

for proc in processors:
if isinstance(proc, BlockProcessor):
if expect_block:
expect_block = False
# if we in a top level block, we save the block processor
# (nested block are ignored for the time being)
if nested_count == 1:
block = proc
else:
top_level_processors.append(proc)
else:
raise click.ClickException("A block command must always follow 'begin'")
elif expect_block:
raise click.ClickException("A block command must always follow 'begin'")
elif isinstance(proc, BeginBlock):
# entering a block
nested_count += 1
expect_block = True

if nested_count > 1:
top_level_processors.append(proc)
elif isinstance(proc, EndBlock):
if nested_count < 1:
raise click.ClickException(
"A 'end' command has no corresponding 'begin' command"
)

nested_count -= 1

if nested_count == 0:
# we're closing a top level block, let's process it
block_mls = block.process(top_level_processors)

# Create a placeholder processor that will add the block's result to the
# current frame. The placeholder_processor is a closure, so we need to make
# a closure-building function. Failing that, the closure would refer directly
# to the block_mls variable above, which might be overwritten by a subsequent
# top-level block
# noinspection PyShadowingNames
def build_placeholder_processor(block_mls):
def placeholder_processor(input_mls):
return merge_mls([input_mls, block_mls])
return placeholder_processor

outer_processors.append(build_placeholder_processor(block_mls))

# reset the top level processor list
top_level_processors = list()
else:
top_level_processors.append(proc)
else:
# this is a 'normal' processor, we can just add it to the top of the stack
if nested_count == 0:
outer_processors.append(proc)
else:
top_level_processors.append(proc)

# at this stage, the stack must have a single frame, otherwise we're missing end commands
if nested_count > 0:
raise click.ClickException("An 'end' command is missing")

# the (only) frame's processors should now be flat and can be chain-called
mls = MultiLineString([])
for processor in processors:
mls = processor(mls)
for proc in outer_processors:
mls = proc(mls)
return mls


def merge_mls(mls_arr: Iterable[MultiLineString]) -> MultiLineString:
"""
Merge multiple MultiLineString into one.
:param mls_arr: iterable of MultiLineString
:return: merged MultiLineString
"""

return MultiLineString([ls for mls in mls_arr for ls in mls])


def processor(f):
Expand All @@ -48,7 +140,51 @@ def generator(f):
@processor
def new_func(mls: MultiLineString, *args, **kwargs):
ls_arr = [ls for ls in mls]
ls_arr += [ls for ls in f(*args, **kwargs) ]
ls_arr += [ls for ls in f(*args, **kwargs)]
return MultiLineString(ls_arr)

return update_wrapper(new_func, f)


def block_processor(c):
"""
Create an instance of the block processor class
"""

def new_func(*args, **kwargs):
return c(*args, **kwargs)

return update_wrapper(new_func, c)


class BeginBlock:
pass


@cli.command()
def begin():
return BeginBlock()


class EndBlock:
pass


@cli.command()
def end():
return EndBlock()


class BlockProcessor:
"""
Base class for all block processors. Although it does nothing, block processors must
sub-class :class:`BlockProcessor` to be recognized as such.
"""

def process(self, processors) -> MultiLineString:
"""
Generate the compound geometries based on the provided processors. Sub-class must
override this function in their implementation.
:param processors: list of processors
:return: compound geometries
"""

0 comments on commit 4c7858c

Please sign in to comment.