Source for: lesson11.py [raw]
1from fonts import adjust_widths_by_letter
2from hyphen import insert_soft_hyphens
3
4
5class Box():
6
7 def __init__(self, x=0, y=0, w=1, h=1, stretchy=False, letter="x"):
8 """Accept arguments to define our box, and store them."""
9 self.x = x
10 self.y = y
11 self.w = w
12 self.h = h
13 self.stretchy = stretchy
14 self.letter = letter
15
16 def __repr__(self):
17 return 'Box(%s, %s, %s, %s, "%s")' % (
18 self.x, self.y, self.w, self.h, self.letter
19 )
20
21
22# Three runs of "a" with spaces between them.
23# The ideal breaking point is the second space.
24text_boxes = [Box(letter="a") for a in range(72)]
25for i in (20, 50):
26 text_boxes[i].letter = " "
27 text_boxes[i].stretchy = True
28adjust_widths_by_letter(text_boxes)
29
30# A few pages all the same size
31pages = [Box(i * 35, 0, 30, 50) for i in range(10)]
32
33
34def hyphenbox():
35 b = Box(letter="-")
36 adjust_widths_by_letter([b])
37 return b
38
39
40def badness(page_width, row):
41 """Calculate how 'bad' a position to break is.
42
43 bigger is worse.
44 """
45 # Yes, this is suboptimal. It's easier to optimize working code
46 # than fixing fast code.
47 row_width = (row[-1].x + row[-1].w) - row[0].x
48 slack = page_width - row_width
49 stretchies = [b for b in row if b.stretchy]
50 if len(stretchies) > 0:
51 stretchies_width = sum(s.w for s in stretchies)
52 # More stetchy space is good. More slack is bad.
53 badness = slack / stretchies_width
54 else: # Nothing to stretch. Not good.
55 badness = 1000
56 if slack < 0:
57 # Arbitrary fudge factor, negative slack is THIS much worse
58 badness *= -2
59 return badness
60
61
62# We add a "separation" constant so you can see the boxes individually
63separation = .05
64
65
66def layout(_boxes):
67 """Layout boxes along pages.
68
69 Keep in mind that this function modifies the boxes themselves, so
70 you should be very careful about trying to call layout() more than once
71 on the same boxes.
72
73 Specifically, some spaces will become 0-width and not stretchy.
74 """
75
76 # Because we modify the box list, we will work on a copy
77 boxes = _boxes[:]
78 # We start at page 0
79 page = 0
80 # The 1st box should be placed in the correct page
81 previous = boxes.pop(0)
82 previous.x = pages[page].x
83 previous.y = pages[page].y
84 row = []
85 while boxes:
86 # We take the new 1st box
87 box = boxes.pop(0)
88 # And put it next to the other
89 box.x = previous.x + previous.w + separation
90 # At the same vertical location
91 box.y = previous.y
92
93 # Handle breaking on newlines
94 break_line = False
95 # But if it's a newline
96 if (box.letter == "\n"):
97 break_line = True
98 # Newlines take no horizontal space ever
99 box.w = 0
100 box.stretchy = False
101
102 # Or if it's too far to the right, and is a
103 # good place to break the line...
104 elif (box.x + box.w) > (
105 pages[page].x + pages[page].w
106 ) and box.letter in (
107 " ", "\xad"
108 ):
109 if box.letter == "\xad":
110 # Add a visible hyphen in the row
111 h_b = hyphenbox()
112 h_b.x = previous.x + previous.w + separation
113 h_b.y = previous.y
114 _boxes.append(h_b) # So it's drawn
115 row.append(h_b) # So it's justified
116 break_line = True
117 # We adjust the row
118 # Remove all right-margin spaces
119 while row[-1].letter == " ":
120 row.pop()
121 slack = (pages[page].x + pages[page].w) - (
122 row[-1].x + row[-1].w
123 )
124 # Get a list of all the ones that are stretchy
125 stretchies = [b for b in row if b.stretchy]
126 if not stretchies: # Nothing stretches do as before.
127 bump = slack / len(row)
128 # The 1st box gets 0 bumps, the 2nd gets 1 and so on
129 for i, b in enumerate(row):
130 b.x += bump * i
131 else:
132 bump = slack / len(stretchies)
133 # Each stretchy gets wider
134 for b in stretchies:
135 b.w += bump
136 # And we put each thing next to the previous one
137 for j, b in enumerate(row[1:], 1):
138 b.x = row[j - 1].x + row[j - 1].w + separation
139
140 if break_line:
141 # We start a new row
142 row = []
143 # We go all the way left and a little down
144 box.x = pages[page].x
145 box.y = previous.y + previous.h + separation
146
147 # But if we go too far down
148 if box.y + box.h > pages[page].y + pages[page].h:
149 # We go to the next page
150 page += 1
151 # And put the box at the top-left
152 box.x = pages[page].x
153 box.y = pages[page].y
154
155 # Put the box in the row
156 row.append(box)
157
158 # Collapse all left-margin space
159 if all(b.letter == " " for b in row):
160 box.w = 0
161 box.stretchy = False
162 box.x = pages[page].x
163
164 previous = box
165
166
167layout(text_boxes)
168
169
170import svgwrite
171
172
173def draw_boxes(boxes, fname, size, hide_boxes=False):
174 dwg = svgwrite.Drawing(fname, profile="full", size=size)
175 # Draw the pages
176 for page in pages:
177 dwg.add(
178 dwg.rect(
179 insert=(f"{page.x}cm", f"{page.y}cm"),
180 size=(f"{page.w}cm", f"{page.h}cm"),
181 fill="lightblue",
182 )
183 )
184 # Draw all the boxes
185 for box in boxes:
186 # The box color depends on its features
187 color = "green" if box.stretchy else "red"
188 # Make the colored boxes optional
189 if not hide_boxes:
190 dwg.add(
191 dwg.rect(
192 insert=(f"{box.x}cm", f"{box.y}cm"),
193 size=(f"{box.w}cm", f"{box.h}cm"),
194 fill=color,
195 )
196 )
197 # Display the letter in the box
198 if box.letter:
199 dwg.add(
200 dwg.text(
201 box.letter,
202 insert=(f"{box.x}cm", f"{box.y + box.h}cm"),
203 font_size=f"{box.h}cm",
204 font_family="Arial",
205 )
206 )
207 dwg.save()
208
209
210draw_boxes(text_boxes, "lesson11.svg", ("33cm", "5cm"), hide_boxes=True)
211