2019-08-03 19:06:23 +02:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2020-09-19 23:47:21 +02:00
from functools import partial
2019-08-03 19:06:23 +02:00
from boxes import Boxes , edges , boolarg
import math
2020-09-19 23:47:21 +02:00
2019-08-03 19:06:23 +02:00
class DividerTray ( Boxes ) :
""" Divider tray - rows and dividers """
2022-03-21 17:23:29 +01:00
description = " Adding ' 0: ' at the start of the sy parameter adds a slot at the very back. Adding ' :0 ' at the end of sy adds a slot meeting the bottom at the very front. This is especially useful if slot angle is set above zero. "
2019-08-03 19:06:23 +02:00
ui_group = " Tray "
def __init__ ( self ) :
Boxes . __init__ ( self )
2020-07-09 00:19:23 +02:00
self . addSettingsArgs ( edges . FingerJointSettings )
2019-08-03 19:06:23 +02:00
self . buildArgParser ( " sx " , " sy " , " h " , " outside " )
self . argparser . add_argument (
" --slot_depth " , type = float , default = 20 , help = " depth of the slot in mm "
)
self . argparser . add_argument (
" --slot_angle " ,
type = float ,
default = 0 ,
help = " angle at which slots are generated, in degrees. 0° is vertical. " ,
)
self . argparser . add_argument (
" --slot_radius " ,
type = float ,
default = 2 ,
help = " radius of the slot entrance in mm " ,
)
self . argparser . add_argument (
" --slot_extra_slack " ,
type = float ,
default = 0.2 ,
help = " extra slack (in addition to thickness and kerf) for slot width to help insert dividers " ,
)
self . argparser . add_argument (
" --divider_bottom_margin " ,
type = float ,
default = 0 ,
help = " margin between box ' s bottom and divider ' s " ,
)
self . argparser . add_argument (
" --divider_upper_notch_radius " ,
type = float ,
default = 1 ,
help = " divider ' s notch ' s upper radius " ,
)
self . argparser . add_argument (
" --divider_lower_notch_radius " ,
type = float ,
default = 8 ,
help = " divider ' s notch ' s lower radius " ,
)
self . argparser . add_argument (
" --divider_notch_depth " ,
type = float ,
default = 15 ,
help = " divider ' s notch ' s depth " ,
)
self . argparser . add_argument (
" --left_wall " ,
type = boolarg ,
default = True ,
help = " generate wall on the left side " ,
)
self . argparser . add_argument (
" --right_wall " ,
type = boolarg ,
default = True ,
help = " generate wall on the right side " ,
)
2020-02-16 07:20:15 +01:00
self . argparser . add_argument (
2020-09-19 23:47:21 +02:00
" --bottom " , type = boolarg , default = False , help = " generate wall on the bottom " ,
2020-02-16 07:20:15 +01:00
)
2019-08-03 19:06:23 +02:00
def render ( self ) :
side_walls_number = len ( self . sx ) - 1 + sum ( [ self . left_wall , self . right_wall ] )
2020-02-01 15:17:18 +01:00
if side_walls_number == 0 :
raise ValueError ( " You need at least one side wall to generate this tray " )
2019-08-03 19:06:23 +02:00
2021-01-28 19:14:05 +01:00
# If measures are inside, we need to adjust height before slot generation
if not self . outside :
# If the parameter 'h' is the inner height of the content itself,
# then the actual tray height needs to be adjusted with the angle
self . h = self . h * math . cos ( math . radians ( self . slot_angle ) )
2020-09-19 23:47:21 +02:00
slot_descriptions = SlotDescriptionsGenerator ( ) . generate_all_same_angles (
self . sy ,
self . thickness ,
self . slot_extra_slack ,
self . slot_depth ,
self . h ,
self . slot_angle ,
self . slot_radius ,
)
2019-08-03 19:06:23 +02:00
2021-01-28 19:14:05 +01:00
# If measures are outside, we need to readjust slots afterwards
2019-08-03 19:06:23 +02:00
if self . outside :
self . sx = self . adjustSize ( self . sx , self . left_wall , self . right_wall )
side_wall_target_length = sum ( self . sy ) - 2 * self . thickness
slot_descriptions . adjust_to_target_length ( side_wall_target_length )
self . ctx . save ( )
# Facing walls (outer) with finger holes to support side walls
facing_wall_length = sum ( self . sx ) + self . thickness * ( len ( self . sx ) - 1 )
side_edge = lambda with_wall : " F " if with_wall else " e "
2020-02-16 07:20:15 +01:00
bottom_edge = lambda with_wall : " F " if with_wall else " e "
2019-08-03 19:06:23 +02:00
for _ in range ( 2 ) :
self . rectangularWall (
facing_wall_length ,
self . h ,
2020-09-19 23:47:21 +02:00
[
bottom_edge ( self . bottom ) ,
side_edge ( self . right_wall ) ,
" e " ,
side_edge ( self . left_wall ) ,
] ,
2020-02-16 07:20:15 +01:00
callback = [ partial ( self . generate_finger_holes , self . h ) ] ,
2019-08-03 19:06:23 +02:00
move = " up " ,
)
# Side walls (outer & inner) with slots to support dividers
side_wall_length = slot_descriptions . total_length ( )
for _ in range ( side_walls_number ) :
2020-05-22 10:25:55 +02:00
if _ < side_walls_number - ( len ( self . sx ) - 1 ) :
be = " F " if self . bottom else " e "
else :
be = " f " if self . bottom else " e "
2019-08-03 19:06:23 +02:00
se = DividerSlotsEdge ( self , slot_descriptions . descriptions )
self . rectangularWall (
2020-05-22 10:25:55 +02:00
side_wall_length , self . h , [ be , " f " , se , " f " ] , move = " up "
2019-08-03 19:06:23 +02:00
)
# Switch to right side of the file
self . ctx . restore ( )
self . rectangularWall (
max ( facing_wall_length , side_wall_length ) , self . h , " ffff " , move = " right only "
)
2020-02-16 07:20:15 +01:00
# Bottom piece.
if self . bottom :
2020-09-19 23:47:21 +02:00
self . rectangularWall (
facing_wall_length ,
side_wall_length ,
[
" f " ,
" f " if self . right_wall else " e " ,
" f " ,
" f " if self . left_wall else " e " ,
] ,
callback = [ partial ( self . generate_finger_holes , side_wall_length ) ] ,
move = " up " ,
)
2020-02-16 07:20:15 +01:00
2019-08-03 19:06:23 +02:00
# Dividers
divider_height = (
# h, with angle adjustement
self . h / math . cos ( math . radians ( self . slot_angle ) )
# removing what exceeds in the width of the divider
- self . thickness * math . tan ( math . radians ( self . slot_angle ) )
# with margin
- self . divider_bottom_margin
)
for i , length in enumerate ( self . sx ) :
is_first_wall = i == 0
is_last_wall = i == len ( self . sx ) - 1
self . generate_divider (
length ,
divider_height ,
" up " ,
only_one_wall = ( is_first_wall and not self . left_wall )
or ( is_last_wall and not self . right_wall ) ,
)
if self . debug :
debug_info = [ " Debug " ]
debug_info . append (
" Slot_edge_outer_length: {0:.2f} " . format (
slot_descriptions . total_length ( ) + 2 * self . thickness
)
)
debug_info . append (
" Slot_edge_inner_lengths: {0} " . format (
str . join (
" | " ,
[
" {0:.2f} " . format ( e . usefull_length ( ) )
for e in slot_descriptions . get_straigth_edges ( )
] ,
)
)
)
debug_info . append (
" Face_edge_outer_length: {0:.2f} " . format (
facing_wall_length
+ self . thickness * sum ( [ self . left_wall , self . right_wall ] )
)
)
debug_info . append (
" Face_edge_inner_lengths: {0} " . format (
str . join ( " | " , [ " {0:.2f} " . format ( e ) for e in self . sx ] )
)
)
debug_info . append ( " Tray_height: {0:.2f} " . format ( self . h ) )
debug_info . append (
" Content_height: {0:.2f} " . format (
self . h / math . cos ( math . radians ( self . slot_angle ) )
)
)
self . text ( str . join ( " \n " , debug_info ) , x = 5 , y = 5 , align = " bottom left " )
2020-02-16 07:20:15 +01:00
def generate_finger_holes ( self , length ) :
2019-08-03 19:06:23 +02:00
posx = - 0.5 * self . thickness
for x in self . sx [ : - 1 ] :
posx + = x + self . thickness
2020-02-16 07:20:15 +01:00
self . fingerHolesAt ( posx , 0 , length )
2019-08-03 19:06:23 +02:00
def generate_divider ( self , width , height , move , only_one_wall = False ) :
second_tab_width = 0 if only_one_wall else self . thickness
total_width = width + self . thickness + second_tab_width
if self . move ( total_width , height , move , True ) :
return
# Upper edge with a finger notch
upper_radius = self . divider_upper_notch_radius
lower_radius = self . divider_lower_notch_radius
upper_third = ( width - 2 * upper_radius - 2 * lower_radius ) / 3
# Upper: first tab width
self . edge ( self . thickness )
# Upper: divider width (with notch if possible)
if upper_third > 0 :
2020-09-19 23:47:21 +02:00
self . polyline (
upper_third ,
( 90 , upper_radius ) ,
self . divider_notch_depth - upper_radius - lower_radius ,
( - 90 , lower_radius ) ,
upper_third ,
( - 90 , lower_radius ) ,
self . divider_notch_depth - upper_radius - lower_radius ,
( 90 , upper_radius ) ,
upper_third ,
)
2019-08-03 19:06:23 +02:00
else :
# if there isn't enough room for the radius, we don't use it
self . edge ( width )
2020-09-19 23:47:21 +02:00
self . polyline (
# Upper: second tab width if needed
second_tab_width ,
# First side, with tab depth only if there is 2 walls
90 ,
self . slot_depth ,
90 ,
second_tab_width ,
- 90 ,
height - self . slot_depth ,
# Lower edge
90 ,
width ,
# Second side, always a tab
90 ,
height - self . slot_depth ,
- 90 ,
self . thickness ,
90 ,
self . slot_depth ,
)
2019-08-03 19:06:23 +02:00
# Move for next piece
self . move ( total_width , height , move )
class SlottedEdgeDescriptions :
def __init__ ( self ) :
self . descriptions = [ ]
def add ( self , description ) :
self . descriptions . append ( description )
def get_straigth_edges ( self ) :
return [ x for x in self . descriptions if isinstance ( x , StraightEdgeDescription ) ]
def get_last_edge ( self ) :
return self . descriptions [ - 1 ]
def adjust_to_target_length ( self , target_length ) :
actual_length = sum ( [ d . tracing_length ( ) for d in self . descriptions ] )
compensation = actual_length - target_length
compensation_ratio = compensation / sum (
[ d . asked_length for d in self . get_straigth_edges ( ) ]
)
for edge in self . get_straigth_edges ( ) :
edge . outside_ratio = 1 - compensation_ratio
def total_length ( self ) :
return sum ( [ x . tracing_length ( ) for x in self . descriptions ] )
class StraightEdgeDescription :
def __init__ (
self ,
asked_length ,
round_edge_compensation = 0 ,
outside_ratio = 1 ,
angle_compensation = 0 ,
) :
self . asked_length = asked_length
self . round_edge_compensation = round_edge_compensation
self . outside_ratio = outside_ratio
self . angle_compensation = angle_compensation
def __repr__ ( self ) :
return (
" StraightEdgeDescription( {0} , round_edge_compensation= {1} , angle_compensation= {2} , outside_ratio= {3} ) "
) . format (
self . asked_length ,
self . round_edge_compensation ,
self . angle_compensation ,
self . outside_ratio ,
)
def tracing_length ( self ) :
"""
How much length should take tracing this straight edge
"""
return (
( self . asked_length * self . outside_ratio )
- self . round_edge_compensation
+ self . angle_compensation
)
def usefull_length ( self ) :
"""
Part of the length which might be used by the content of the tray
"""
return self . asked_length * self . outside_ratio
class Memoizer ( dict ) :
def __init__ ( self , computation ) :
self . computation = computation
def __missing__ ( self , key ) :
res = self [ key ] = self . computation ( key )
return res
class SlotDescription :
_div_by_cos_cache = Memoizer ( lambda a : 1 / math . cos ( math . radians ( a ) ) )
_tan_cache = Memoizer ( lambda a : math . tan ( math . radians ( a ) ) )
def __init__ (
self , width , depth = 20 , angle = 0 , radius = 0 , start_radius = None , end_radius = None
) :
self . depth = depth
self . width = width
self . start_radius = radius if start_radius == None else start_radius
self . end_radius = radius if end_radius == None else end_radius
self . angle = angle
def __repr__ ( self ) :
return " SlotDescription( {0} , depth= {1} , angle= {2} , start_radius= {3} , end_radius= {4} ) " . format (
self . width , self . depth , self . angle , self . start_radius , self . end_radius
)
def _div_by_cos ( self ) :
return SlotDescription . _div_by_cos_cache [ self . angle ]
def _tan ( self ) :
return SlotDescription . _tan_cache [ self . angle ]
def angle_corrected_width ( self ) :
"""
returns how much width is the slot when measured horizontally , since the angle makes it bigger .
It ' s the same as the slot entrance width when radius is 0°.
"""
return self . width * self . _div_by_cos ( )
def round_edge_start_correction ( self ) :
"""
returns by how much we need to stop tracing our straight lines at the start of the slot
in order to do a curve line instead
"""
return self . start_radius * ( self . _div_by_cos ( ) - self . _tan ( ) )
def round_edge_end_correction ( self ) :
"""
returns by how much we need to stop tracing our straight lines at the end of the slot
in order to do a curve line instead
"""
return self . end_radius * ( self . _div_by_cos ( ) + self . _tan ( ) )
def _depth_angle_correction ( self ) :
"""
The angle makes one side of the slot deeper than the other .
"""
extra_depth = self . width * self . _tan ( )
return extra_depth
def corrected_start_depth ( self ) :
"""
Returns the depth of the straigth part of the slot starting side
"""
extra_depth = self . _depth_angle_correction ( )
return self . depth + max ( 0 , extra_depth ) - self . round_edge_start_correction ( )
def corrected_end_depth ( self ) :
"""
Returns the depth of the straigth part of the slot ending side
"""
extra_depth = self . _depth_angle_correction ( )
return self . depth + max ( 0 , - extra_depth ) - self . round_edge_end_correction ( )
def tracing_length ( self ) :
"""
How much length this slot takes on an edge
"""
return (
self . round_edge_start_correction ( )
+ self . angle_corrected_width ( )
+ self . round_edge_end_correction ( )
)
2020-09-19 23:47:21 +02:00
class SlotDescriptionsGenerator :
def generate_all_same_angles (
self , sections , thickness , extra_slack , depth , height , angle , radius = 2 ,
) :
width = thickness + extra_slack
descriptions = SlottedEdgeDescriptions ( )
# Special case: if first slot start at 0, then radius is 0
first_correction = 0
current_section = 0
if sections [ 0 ] == 0 :
slot = SlotDescription (
width , depth = depth , angle = angle , start_radius = 0 , end_radius = radius ,
)
descriptions . add ( slot )
first_correction = slot . round_edge_end_correction ( )
current_section + = 1
first_length = sections [ current_section ]
current_section + = 1
descriptions . add (
StraightEdgeDescription (
first_length , round_edge_compensation = first_correction
)
)
for l in sections [ current_section : ] :
slot = SlotDescription ( width , depth = depth , angle = angle , radius = radius , )
# Fix previous edge length
previous_edge = descriptions . get_last_edge ( )
previous_edge . round_edge_compensation + = slot . round_edge_start_correction ( )
# Add this slot
descriptions . add ( slot )
# Add the straigth edge after this slot
descriptions . add (
StraightEdgeDescription ( l , slot . round_edge_end_correction ( ) )
)
# We need to add extra space for the divider (or the actual content)
# to slide all the way down to the bottom of the tray in spite of walls
end_length = height * math . tan ( math . radians ( angle ) )
descriptions . get_last_edge ( ) . angle_compensation + = end_length
return descriptions
2019-08-03 19:06:23 +02:00
class DividerSlotsEdge ( edges . BaseEdge ) :
""" Edge with multiple angled rounded slots for dividers """
description = " Edge with multiple angled rounded slots for dividers "
def __init__ ( self , boxes , descriptions ) :
super ( DividerSlotsEdge , self ) . __init__ ( boxes , None )
self . descriptions = descriptions
def __call__ ( self , length , * * kw ) :
self . ctx . save ( )
for description in self . descriptions :
if isinstance ( description , SlotDescription ) :
self . do_slot ( description )
elif isinstance ( description , StraightEdgeDescription ) :
self . do_straight_edge ( description )
# rounding errors might accumulates :
# restore context and redo the move straight
self . ctx . restore ( )
self . moveTo ( length )
def do_straight_edge ( self , straight_edge ) :
self . edge ( straight_edge . tracing_length ( ) )
def do_slot ( self , slot ) :
self . ctx . save ( )
2020-09-19 23:47:21 +02:00
self . polyline (
0 ,
( 90 - slot . angle , slot . start_radius ) ,
slot . corrected_start_depth ( ) ,
- 90 ,
slot . width ,
- 90 ,
slot . corrected_end_depth ( ) ,
( 90 + slot . angle , slot . end_radius ) ,
)
2019-08-03 19:06:23 +02:00
# rounding errors might accumulates :
# restore context and redo the move straight
self . ctx . restore ( )
self . moveTo ( slot . tracing_length ( ) )