diff --git a/boxes/__init__.py b/boxes/__init__.py index 321be3a1..fb291cf3 100755 --- a/boxes/__init__.py +++ b/boxes/__init__.py @@ -354,7 +354,9 @@ def verify(self, assembly): """Run any plausibility checks or other verification that can be run, and raise on error""" - for ((partname_a, edge_a), (partname_b, edge_b)) in assembly._antiparallel_edges: + for alignment in assembly._alignments: + partname_a, edge_a, partname_b, edge_b = alignment + part_a = [p for p in self.surface.parts if p.name == partname_a] part_b = [p for p in self.surface.parts if p.name == partname_b] if len(part_a) != 1: diff --git a/boxes/assembly.py b/boxes/assembly.py index 1d39b6d0..fe843284 100644 --- a/boxes/assembly.py +++ b/boxes/assembly.py @@ -1,7 +1,25 @@ +from typing import NamedTuple + type Part = str type Edge = int type Tree = tuple[(Part, Edge), list[(Edge, Tree)]] +class Alignment(NamedTuple): + """Base class for alignments two pieces (precisely: oriented edges on + pieces) can have to each other""" + part_a: str + edge_a: int # migth be any, really? + part_b: str + edge_b: int + +class AntiparallelEdge(Alignment): + """The edges match by turning one 180° and starting it where the other edge + ends""" + +class Stacked(Alignment): + """Two parts are on top of each other. In the printed / laser-cut + coordinates, part A is the lower and part B the upper part.""" + class Assembly: """An assembly describes how parts of a generator belong together. @@ -9,7 +27,7 @@ class Assembly: """ def __init__(self): self._base = None - self._antiparallel_edges = [] + self._alignments = [] def base(self, part, edge): self._base = (part, edge) @@ -21,7 +39,16 @@ def antiparallel_edge(self, part_a, edge_a, part_b, edge_b): Antiparallel edges are the typical case in boxes scripts: Parts usually all follow the same direction (counterclockwise), and boxes are usually built so that the same side of the material faces out.""" - self._antiparallel_edges.append(((part_a, edge_a), (part_b, edge_b))) + self._alignments.append(AntiparallelEdge(part_a, edge_a, part_b, edge_b)) + + def stacked(self, part_a, edge_a, part_b, edge_b): + """Note that part A stacked on part B. + + When parts are identical in size, it does not matter which edge is + which; if not, there needs to be some edge that matches. The assembly + module can currently not express that parts are stacked + center-to-center.""" + self._alignments.append(Stacked(part_a, edge_a, part_b, edge_b)) def tree(self) -> Tree: """Greedily assemble a tree out of all connected parts starting from @@ -32,7 +59,12 @@ def tree(self) -> Tree: def tree_from(self, start, exclude) -> tuple[(Tree, set[Part])]: children = [] covered_parts = exclude | {start[0]} - for ((a, ae), (b, be)) in self._antiparallel_edges: + for alignment in self._alignments: + # We don't care about their precise relation -- what matters + # here is that once we know where one is, we know where the + # other is too. But we pass it on for later users. + a, ae, b, be = alignment + child_tree = None if a == start[0] and b not in covered_parts: child_tree, new_covered_parts = tree_from(self, (b, be), covered_parts) @@ -42,7 +74,7 @@ def tree_from(self, start, exclude) -> tuple[(Tree, set[Part])]: child_tree_startingedge = be if child_tree is not None: covered_parts |= new_covered_parts - children.append((child_tree_startingedge, child_tree)) + children.append((child_tree_startingedge, alignment, child_tree)) return ((start, children), covered_parts) @@ -54,8 +86,8 @@ def explain(self, *, _tree=None, _indent=0): ((part, edge), children) = _tree print(" "*_indent, f"Painting part {part} starting at its edge {edge}") - for (parentedge, child) in children: - print(" "*_indent, f"at its edge {parentedge}:") + for (parentedge, alignment, child) in children: + print(" "*_indent, f"at its edge {parentedge} as {type(alignment).__name__}:") # FIXME for some this may be asymmetric self.explain(_tree=child, _indent=_indent + 1) def openscad(self, file, *, box, _tree=None, _indent=0): @@ -72,7 +104,7 @@ def openscad(self, file, *, box, _tree=None, _indent=0): (parent_part,) = [p for p in box.surface.parts if p.name == part] print(f'{i}reverse_{part}_{edge}() {{ part("{part}"); ', file=file); - for (parent_edge, child) in children: + for (parent_edge, alignment, child) in children: # We could inline the lengths and translations; currently they are # generated independently and probably that's a good thing because # it's useful for other interactions with the visualizations as @@ -91,25 +123,36 @@ def openscad(self, file, *, box, _tree=None, _indent=0): print(f'{i} forward_{part}_{parent_edge}() // Shift coordinates to the correct edge of the parent', file=file) - parent_positive = getattr(parent_edge_kind, "positive", None) - child_positive = getattr(child_edge_kind, "positive", None) - from . import edges - if isinstance(parent_edge_kind, edges.FingerJointEdge) and parent_positive == True and isinstance(child_edge_kind, edges.FingerHoleEdge): - print(f'{i} translate([0, 0, -2*3]) rotate([-fold_angle, 0, 0]) // Fold up', file=file) # FIXME thickness and extract parameters - elif isinstance(parent_edge_kind, edges.FingerJointEdge) and parent_positive == True and isinstance(child_edge_kind, edges.StackableEdge): - print(f'{i} translate([0, 0, -2*3 - 8]) rotate([-fold_angle, 0, 0]) // Fold up', file=file) # FIXME thickness and extract parameters, worse: the 8 are just a guess - elif parent_positive == False and child_positive == True: - print(f'{i} rotate([-fold_angle, 0, 0]) translate([0, 0, 3]) // Fold up', file=file) # FIXME thickness - elif parent_positive == True and child_positive == False: - # It'd be great to autoamte that as inverse-of the other directin - print(f'{i} translate([0, 0, -3]) rotate([-fold_angle, 0, 0]) // Fold up', file=file) # FIXME thickness + if isinstance(alignment, AntiparallelEdge): + parent_positive = getattr(parent_edge_kind, "positive", None) + child_positive = getattr(child_edge_kind, "positive", None) + from . import edges + if isinstance(parent_edge_kind, edges.FingerJointEdge) and parent_positive == True and isinstance(child_edge_kind, edges.FingerHoleEdge): + print(f'{i} translate([0, 0, thickness]) rotate([fold_angle, 0, 0]) translate([0, 0, thickness]) // Fold away from surface', file=file) # FIXME extract parameters + elif isinstance(parent_edge_kind, edges.FingerJointEdge) and parent_positive == True and isinstance(child_edge_kind, edges.StackableEdge): + print(f'{i} translate([0, 0, thickness + 6]) rotate([fold_angle, 0, 0]) translate([0, 0, thickness]) // Fold away from surface', file=file) # FIXME extract parameters, worse: the 6 are just a guess + elif parent_positive == False and child_positive == True: + print(f'{i} translate([0, 0, -thickness]) rotate([fold_angle, 0, 0]) // Fold away from surface', file=file) + elif parent_positive == True and child_positive == False: + # It'd be great to autoamte that as inverse-of the other directin + print(f'{i} rotate([fold_angle, 0, 0]) translate([0, 0, thickness]) // Fold away from surface', file=file) + else: + print(f"Warning: No rules describe how to assemble a {parent_edge_kind} and a {child_edge_kind}. Coarsely rotating; children are marked in transparent red.") + print(f"{i} #", file=file) + print(f'{i} rotate([fold_angle, 0, 0])', file=file) + + print(f'{i} translate([length_{part}_{parent_edge}, 0, 0]) rotate([0, 0, 180]) // Edge is antiparallel', file=file) + + elif isinstance(alignment, Stacked): + if alignment.part_a == part: + print(f'{i} translate([0, 0, thickness])', file=file) + elif alignment.part_b == part: + print(f'{i} translate([0, 0, -thickness])', file=file) + else: + raise RuntimeError("Stacked alignment is oriented, but neither part is in alignment") else: - print(f"Warning: No rules describe how to assemble a {parent_edge_kind} and a {child_edge_kind}. Coarsely rotating; children are marked in transparent red.") - print(f"{i} #", file=file) - print(f'{i} rotate([-fold_angle, 0, 0])', file=file) - - print(f'{i} translate([length_{part}_{parent_edge}, 0, 0]) rotate([0, 0, 180]) // Edge is antiparallel', file=file) - + print(f"Warning: No rules describe how to assemble a {type(alignment).__name__} alignment, overlyaing it") + print(f'{i} #', file=file) self.openscad(file=file, box=box, _tree=child, _indent=_indent + 1) print(f"{i}}}", file=file) diff --git a/boxes/drawing.py b/boxes/drawing.py index cd0b417e..65ad172a 100644 --- a/boxes/drawing.py +++ b/boxes/drawing.py @@ -136,7 +136,7 @@ def __init__(self, name) -> None: self.oriented_markers = {} def __repr__(self): - return "<%s %s at %#x>" % (type(self).__name__, repr(self.name) if self.name else "(unnamed)", id(self)) + return "<{} {} at {:#x}>".format(type(self).__name__, repr(self.name) if self.name else "(unnamed)", id(self)) def place_oriented_marker(self, coords, label, length, extra_metadata): self.oriented_markers[label] = (coords, length, extra_metadata) @@ -562,8 +562,15 @@ def finish(self, inner_corners="loop"): self._add_metadata(svg) - for (i, part), posneg in itertools.product(enumerate(self.parts), ["pos", "neg"]): - if not part.pathes: + for (i, part), posneg in itertools.product(enumerate(self.parts), ["pos", "neg", "decorative"]): + if posneg == "pos": + path_criterion = lambda path: path.params['rgb'] == (0, 0, 0) + if posneg == "neg": + path_criterion = lambda path: path.params['rgb'] == (0, 0, 1) + if posneg == "decorative": + path_criterion = lambda path: path.params['rgb'] not in ((0, 0, 0), (0, 0, 1)) + filtered_paths = [p for p in part.pathes if path_criterion(p)] + if not filtered_paths: continue g = ET.SubElement(svg, "g", id=f"p-{i}-{posneg}", style="fill:none;stroke-linecap:round;stroke-linejoin:round;") @@ -573,11 +580,7 @@ def finish(self, inner_corners="loop"): ET.SubElement(g, "title").text = part.name g.text = "\n " g.tail = "\n" - for path in part.pathes: - if posneg == "pos" and path.params['rgb'] == (0, 0, 1): - continue - if posneg == "neg" and path.params['rgb'] == (0, 0, 0): - continue + for path in filtered_paths: p = [] x, y = 0, 0 start = None diff --git a/boxes/generators/doublewall.py b/boxes/generators/doublewall.py index 90ed5de3..70b782b8 100644 --- a/boxes/generators/doublewall.py +++ b/boxes/generators/doublewall.py @@ -176,6 +176,26 @@ def cutout_bottom1(): self.rectangularWall(y, h, edges=[w2, f2, f2, f2], label="left2", move="down", bedBolts=[bottombolts, None, None, None]) self.rectangularWall(y, h, edges=[E2, f2, "f", f2], label="left3", move="down") + def assemble(self, assembly): + assembly.base("top1", 0) + + assembly.antiparallel_edge('top1', 1, 'right3', 2) + assembly.antiparallel_edge('top1', 3, 'left3', 2) + assembly.antiparallel_edge('left3', 1, 'front1', 3) + assembly.antiparallel_edge('left3', 3, 'back1', 1) + assembly.antiparallel_edge('left2', 0, 'bottom1', 1) + + assembly.stacked("left1", 3, "left2", 3) + assembly.stacked("left2", 3, "left3", 3) + + assembly.stacked("right1", 3, "right2", 3) + assembly.stacked("right2", 3, "right3", 3) + + assembly.stacked("top1", 0, "top2", 0) + assembly.stacked("bottom1", 0, "bottom2", 0) + + assembly.stacked("front1", 1, "front2", 1) + assembly.stacked("back1", 1, "back2", 1) # class FingerJointEdge # ... how do we make sure this is compatible with both walls? diff --git a/scripts/boxes b/scripts/boxes index be02725c..16f3b83a 100755 --- a/scripts/boxes +++ b/scripts/boxes @@ -71,6 +71,7 @@ def run_generator(name, args): box.verify(assembly) assembly.explain() with open("assembly.scad", "w") as assemblyfile: + print(f"thickness = {box.thickness};", file=assemblyfile); for i, part in enumerate(box.surface.parts): for (label, (coords, length, extra_metadata)) in part.oriented_markers.items(): a, b, c, d, e, f, g, h, i = coords