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
2022-03-28 22:00:12 +02:00
class NotchSettings ( edges . Settings ) :
2022-03-30 23:27:29 +02:00
""" Settings for Notches on the Dividers """
2022-03-28 22:00:12 +02:00
absolute_params = {
" upper_radius " : 1 ,
" lower_radius " : 8 ,
" depth " : 15 ,
}
class SlotSettings ( edges . Settings ) :
2022-03-30 23:27:29 +02:00
""" Settings for Divider Slots
2022-03-28 22:00:12 +02:00
Values :
* absolute
* depth : 20 : depth of the slot in mm
* angle : 0 : angle at which slots are generated , in degrees . 0 ° is vertical .
* radius : 2 : radius of the slot entrance in mm
2022-03-30 23:27:29 +02:00
* extra_slack : 0.2 : extra slack ( in addition to thickness and kerf ) to help insert dividers in mm """
2022-03-28 22:00:12 +02:00
absolute_params = {
" depth " : 20 ,
" angle " : 0 ,
" radius " : 2 ,
" extra_slack " : 0.2 ,
}
class DividerSettings ( edges . Settings ) :
""" Settings for Dividers
Values :
* absolute_params
2022-03-30 23:27:29 +02:00
* bottom_margin : 0 : margin between box ' s bottom and divider ' s in mm
2022-03-28 22:00:12 +02:00
* relative ( in multiples of thickness )
2022-03-30 23:27:29 +02:00
* play : 0.05 : play to avoid them clamping onto the walls ( in multiples of thickness )
2022-03-28 22:00:12 +02:00
"""
absolute_params = {
" bottom_margin " : 0 ,
}
relative_params = {
" play " : 0.05 ,
}
2019-08-03 19:06:23 +02:00
class DividerTray ( Boxes ) :
""" Divider tray - rows and dividers """
2022-09-23 17:00:06 +02: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.
There are 3 different sets of dividers rendered : One single divider spanning across all columns and two sets with one divider per column - one set with tabs of a full wall thickness and one set with tabs of half wall thickness that can go side by side . You will likely need to cut each of the dividers you want multiple times .
"""
2022-03-21 17:23:29 +01:00
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 )
2022-06-16 18:06:58 +02:00
self . addSettingsArgs ( edges . HandleEdgeSettings )
2019-08-03 19:06:23 +02:00
self . buildArgParser ( " sx " , " sy " , " h " , " outside " )
2022-03-28 22:00:12 +02:00
self . addSettingsArgs ( SlotSettings )
self . addSettingsArgs ( NotchSettings )
self . addSettingsArgs ( DividerSettings )
2022-03-27 21:53:39 +02:00
self . argparser . add_argument (
" --notches_in_wall " ,
type = boolarg ,
default = True ,
help = " generate the same notches on the walls that are on the dividers " ,
)
2019-08-03 19:06:23 +02:00
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
)
2022-06-16 18:06:58 +02:00
self . argparser . add_argument (
" --handle " , type = boolarg , default = False , help = " add handle to the bottom " ,
)
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
2022-07-12 22:10:59 +02:00
# We need to adjust height before slot generation
if self . outside :
if self . bottom :
self . h - = self . thickness
else :
2021-01-28 19:14:05 +01:00
# If the parameter 'h' is the inner height of the content itself,
# then the actual tray height needs to be adjusted with the angle
2022-03-28 22:00:12 +02:00
self . h = self . h * math . cos ( math . radians ( self . Slot_angle ) )
2021-01-28 19:14:05 +01:00
2020-09-19 23:47:21 +02:00
slot_descriptions = SlotDescriptionsGenerator ( ) . generate_all_same_angles (
self . sy ,
self . thickness ,
2022-03-28 22:00:12 +02:00
self . Slot_extra_slack ,
self . Slot_depth ,
2020-09-19 23:47:21 +02:00
self . h ,
2022-03-28 22:00:12 +02:00
self . Slot_angle ,
self . Slot_radius ,
2020-09-19 23:47:21 +02:00
)
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 "
2022-06-16 18:06:58 +02:00
bottom_edge = lambda with_wall , with_handle : ( " f " if with_handle else " F " ) if with_wall else " e "
2022-03-27 21:53:39 +02:00
upper_edge = (
DividerNotchesEdge (
self ,
list ( reversed ( self . sx ) ) ,
)
if self . notches_in_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
[
2022-06-16 18:06:58 +02:00
bottom_edge ( self . bottom , _ and self . handle ) ,
2020-09-19 23:47:21 +02:00
side_edge ( self . right_wall ) ,
2022-03-27 21:53:39 +02:00
upper_edge ,
2020-09-19 23:47:21 +02:00
side_edge ( self . left_wall ) ,
] ,
2020-02-16 07:20:15 +01:00
callback = [ partial ( self . generate_finger_holes , self . h ) ] ,
2022-06-16 17:42:59 +02:00
move = " up " , label = " Front " if _ else " Back " ,
2019-08-03 19:06:23 +02:00
)
# 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 (
2022-06-16 17:42:59 +02:00
side_wall_length , self . h , [ be , " f " , se , " f " ] , move = " up " , label = " Sidepiece " + str ( _ + 1 )
2019-08-03 19:06:23 +02:00
)
# Switch to right side of the file
self . ctx . restore ( )
self . rectangularWall (
2022-06-16 17:42:59 +02:00
max ( facing_wall_length , side_wall_length ) , self . h , " ffff " , move = " right only " , label = " invisible "
2019-08-03 19:06:23 +02:00
)
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 " ,
2022-06-16 18:06:58 +02:00
" Y " if self . handle else " f " ,
2020-09-19 23:47:21 +02:00
" f " if self . left_wall else " e " ,
] ,
callback = [ partial ( self . generate_finger_holes , side_wall_length ) ] ,
2022-06-16 17:42:59 +02:00
move = " up " , label = " Bottom " ,
2020-09-19 23:47:21 +02:00
)
2020-02-16 07:20:15 +01:00
2019-08-03 19:06:23 +02:00
# Dividers
divider_height = (
# h, with angle adjustement
2022-03-28 22:00:12 +02:00
self . h / math . cos ( math . radians ( self . Slot_angle ) )
2019-08-03 19:06:23 +02:00
# removing what exceeds in the width of the divider
2022-03-28 22:00:12 +02:00
- self . thickness * math . tan ( math . radians ( self . Slot_angle ) )
2019-08-03 19:06:23 +02:00
# with margin
2022-03-28 22:00:12 +02:00
- self . Divider_bottom_margin
2019-08-03 19:06:23 +02:00
)
2022-03-21 17:46:43 +01:00
self . generate_divider (
self . sx , divider_height , " up " ,
first_tab_width = self . thickness if self . left_wall else 0 ,
second_tab_width = self . thickness if self . right_wall else 0
)
2022-09-23 17:00:06 +02:00
for tabs in [ self . thickness , self . thickness / 2 ] :
with self . saved_context ( ) :
for i , length in enumerate ( self . sx ) :
self . generate_divider (
[ length ] ,
divider_height ,
" right " ,
first_tab_width = tabs if self . left_wall or i > 0 else 0 ,
second_tab_width = tabs if self . right_wall or i < ( len ( self . sx ) - 1 ) else 0 ,
)
self . generate_divider ( self . sx , divider_height , " up only " )
2019-08-03 19:06:23 +02:00
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 (
2022-03-28 22:00:12 +02:00
self . h / math . cos ( math . radians ( self . Slot_angle ) )
2019-08-03 19:06:23 +02:00
)
)
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
2022-03-21 17:46:43 +01:00
def generate_divider (
self , widths , height , move , first_tab_width = 0 , second_tab_width = 0 ) :
total_width = sum ( widths ) + ( len ( widths ) - 1 ) * self . thickness + first_tab_width + second_tab_width
2019-08-03 19:06:23 +02:00
if self . move ( total_width , height , move , True ) :
return
# Upper edge with a finger notch
2022-03-28 22:00:12 +02:00
play = self . Divider_play
2019-08-03 19:06:23 +02:00
# Upper: first tab width
2022-03-21 17:53:30 +01:00
self . edge ( first_tab_width - play )
2019-08-03 19:06:23 +02:00
2022-03-21 17:46:43 +01:00
for nr , width in enumerate ( widths ) :
if nr > 0 :
self . edge ( self . thickness )
2022-03-27 21:53:39 +02:00
DividerNotchesEdge (
self ,
[ width ] ,
) ( width )
2019-08-03 19:06:23 +02:00
2020-09-19 23:47:21 +02:00
self . polyline (
# Upper: second tab width if needed
2022-03-21 17:53:30 +01:00
second_tab_width - play ,
2020-09-19 23:47:21 +02:00
# First side, with tab depth only if there is 2 walls
90 ,
2022-03-28 22:00:12 +02:00
self . Slot_depth ,
2020-09-19 23:47:21 +02:00
90 ,
second_tab_width ,
- 90 ,
2022-03-28 22:00:12 +02:00
height - self . Slot_depth ,
90 ,
)
2022-03-21 17:46:43 +01:00
# Lower edge
for width in reversed ( widths [ 1 : ] ) :
2022-03-28 22:00:12 +02:00
self . polyline (
width - 2 * play ,
90 ,
height - self . Slot_depth ,
- 90 ,
self . thickness + 2 * play ,
- 90 ,
height - self . Slot_depth ,
90 ,
)
2022-03-21 17:46:43 +01:00
self . polyline (
# Second side tab
2022-03-21 17:53:30 +01:00
widths [ 0 ] - 2 * play ,
2020-09-19 23:47:21 +02:00
90 ,
2022-03-28 22:00:12 +02:00
height - self . Slot_depth ,
2020-09-19 23:47:21 +02:00
- 90 ,
2022-03-21 17:46:43 +01:00
first_tab_width ,
2020-09-19 23:47:21 +02:00
90 ,
2022-03-28 22:00:12 +02:00
self . Slot_depth ,
2020-09-19 23:47:21 +02:00
)
2019-08-03 19:06:23 +02:00
# Move for next piece
2022-06-16 17:42:59 +02:00
self . move ( total_width , height , move , label = " Divider " )
2019-08-03 19:06:23 +02:00
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
2022-03-27 21:53:39 +02:00
class DividerNotchesEdge ( edges . BaseEdge ) :
""" Edge with multiple notches for easier access to dividers """
description = " Edge with multiple notches for easier access to dividers "
2022-03-28 22:00:12 +02:00
def __init__ ( self , boxes , sx ) :
2022-03-27 21:53:39 +02:00
2022-04-09 20:39:00 +02:00
super ( ) . __init__ ( boxes , None )
2022-03-27 21:53:39 +02:00
self . sx = sx
def __call__ ( self , _ , * * kw ) :
first = True
for width in self . sx :
if first :
first = False
else :
self . edge ( self . thickness )
self . edge_with_notch ( width )
def edge_with_notch ( self , width ) :
2022-03-28 22:00:12 +02:00
# width (with notch if possible)
upper_third = (
width - 2 * self . Notch_upper_radius - 2 * self . Notch_lower_radius
) / 3
2022-03-27 21:53:39 +02:00
if upper_third > 0 :
2022-03-28 22:00:12 +02:00
straightHeight = (
self . Notch_depth - self . Notch_upper_radius - self . Notch_lower_radius
)
2022-03-27 21:53:39 +02:00
self . polyline (
upper_third ,
2022-03-28 22:00:12 +02:00
( 90 , self . Notch_upper_radius ) ,
straightHeight ,
( - 90 , self . Notch_lower_radius ) ,
2022-03-27 21:53:39 +02:00
upper_third ,
2022-03-28 22:00:12 +02:00
( - 90 , self . Notch_lower_radius ) ,
straightHeight ,
( 90 , self . Notch_upper_radius ) ,
2022-03-27 21:53:39 +02:00
upper_third ,
)
else :
# if there isn't enough room for the radius, we don't use it
self . edge ( width )
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 ) :
2022-04-09 20:39:00 +02:00
super ( ) . __init__ ( boxes , None )
2019-08-03 19:06:23 +02:00
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 ( ) )