Skip to content

Commit

Permalink
More WIP 3D export
Browse files Browse the repository at this point in the history
... which enables DoubleWall to correctly render the assembly

Changes include:

* turning direction changed: Positive (default) fold angle for
  antiparallel edges now makes aligned parts bend "down" from the
  engraved side / makes that side the outside.
* added support for flat-on-flat mounted parts
* exporting/using the thickness
  • Loading branch information
chrysn committed Apr 13, 2024
1 parent b8809c2 commit 2a48201
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 34 deletions.
4 changes: 3 additions & 1 deletion boxes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
93 changes: 68 additions & 25 deletions boxes/assembly.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
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.
It is populated by calling base and antiparallel_edge methods.
"""
def __init__(self):
self._base = None
self._antiparallel_edges = []
self._alignments = []

def base(self, part, edge):
self._base = (part, edge)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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)
19 changes: 11 additions & 8 deletions boxes/drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;")
Expand All @@ -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
Expand Down
20 changes: 20 additions & 0 deletions boxes/generators/doublewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?
1 change: 1 addition & 0 deletions scripts/boxes
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 2a48201

Please sign in to comment.