-
Notifications
You must be signed in to change notification settings - Fork 78
Expand file tree
/
Copy pathplane-to-torus.typ
More file actions
256 lines (236 loc) · 8.68 KB
/
Copy pathplane-to-torus.typ
File metadata and controls
256 lines (236 loc) · 8.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
#import "@preview/cetz:0.5.2": canvas, draw
#import draw: content, grid, line, rect
// Size of compact annotations.
#let annotation-size = 12pt
#set page(width: auto, height: auto, margin: 10pt, fill: none)
#set text(size: 12pt)
// Each edge pair keeps its color through every stage, so it stays visible which edge of
// the square becomes which circle on the torus.
#let rim-color = rgb("#0B5FA5") // vertical edges -> the cylinder rims -> the short circle
#let seam-color = rgb("#C2570A") // horizontal edges -> the seam -> the long circle
#let surface-fill = rgb("#DFE6EF")
#let mesh-stroke = rgb("#A2AEBD") + 0.3pt
#let edge-weight = 2.2pt
// === orthographic camera, shared by both 3D panels ===
// The azimuth matters: viewed square-on, a tube's rims collapse to straight lines.
#let azimuth = 27deg
#let elevation = 20deg
#let light = (-0.42, -0.66, 0.62)
#let camera = (
-calc.sin(azimuth) * calc.cos(elevation),
-calc.cos(azimuth) * calc.cos(elevation),
calc.sin(elevation),
)
#let project((x, y, z)) = (
x * calc.cos(azimuth) - y * calc.sin(azimuth),
(x * calc.sin(azimuth) + y * calc.cos(azimuth)) * calc.sin(elevation) + z * calc.cos(elevation),
)
#let view-depth((x, y, z)) = (
(x * calc.sin(azimuth) + y * calc.cos(azimuth)) * calc.cos(elevation) - z * calc.sin(elevation)
)
#let dot(a, b) = a.zip(b).map(((p, q)) => p * q).sum()
#let place-at(point, origin) = project(point).zip(origin).map(((c, o)) => c + o)
// A tube is swept along an axis curve, which reports its center and its outward radial
// direction at each step; the tube's "up" is always z. A straight axis gives a cylinder,
// a closed ring gives a torus, and both then share the drawing and hiding code below.
#let straight-axis(span) = u => ((u * span - span / 2, 0.0, 0.0), (0.0, 1.0, 0.0))
#let ring-axis(ring) = u => {
let sweep = u * 360deg - 90deg
let (cos-s, sin-s) = (calc.cos(sweep), calc.sin(sweep))
((ring * cos-s, ring * sin-s, 0.0), (cos-s, sin-s, 0.0))
}
#let tube-point(axis, tube, u, v) = {
let ((cx, cy, cz), (rx, ry, _)) = axis(u)
let angle = v * 360deg
let reach = tube * calc.cos(angle)
(cx + reach * rx, cy + reach * ry, cz + tube * calc.sin(angle))
}
#let tube-normal(axis, u, v) = {
let (_, (rx, ry, _)) = axis(u)
let angle = v * 360deg
(calc.cos(angle) * rx, calc.cos(angle) * ry, calc.sin(angle))
}
// CeTZ has no depth buffer, so quads get painted back to front by hand. Their edges double
// as the surface mesh, which carries the square's grid onto the rolled-up shapes.
#let draw-tube(axis, tube, u-steps, v-steps, origin) = {
let quads = ()
for iu in range(u-steps) {
for iv in range(v-steps) {
let (u0, u1) = (iu / u-steps, (iu + 1) / u-steps)
let (v0, v1) = (iv / v-steps, (iv + 1) / v-steps)
let corners = ((u0, v0), (u1, v0), (u1, v1), (u0, v1)).map(((u, v)) => tube-point(
axis,
tube,
u,
v,
))
let mid = tube-normal(axis, (u0 + u1) / 2, (v0 + v1) / 2)
let lambert = calc.max(0.0, dot(mid, light))
// back faces are the inside of the tube, seen through its open rims: shading them
// down keeps the cylinder from reading as a capped solid
let lit = if dot(mid, camera) > 0 { 0.45 + 0.55 * lambert } else { 0.3 + 0.2 * lambert }
quads.push((
corners.map(view-depth).sum() / 4,
corners.map(pt => place-at(pt, origin)),
surface-fill.darken((1 - lit) * 45%),
))
}
}
for (_, pts, fill) in quads.sorted(key: quad => quad.first()).rev() {
line(..pts, close: true, fill: fill, stroke: mesh-stroke)
}
}
// Polyline along a curve on the tube, dropping the stretches turned away from the camera
// so the far side stays hidden behind the surface.
#let draw-visible(sample, normal-at, steps, origin, stroke, arrows: ()) = {
for idx in range(steps) {
let (t0, t1) = (idx / steps, (idx + 1) / steps)
if dot(normal-at((t0 + t1) / 2), camera) <= 0 { continue }
line(..(t0, t1).map(t => place-at(sample(t), origin)), stroke: stroke)
}
// chevrons repeat the square's edge markings, so orientation survives the gluing
for at in arrows {
line(
..(at, at + 0.012).map(t => place-at(sample(t), origin)),
stroke: stroke,
mark: (end: "stealth", fill: stroke.paint, scale: 0.55),
)
}
}
// Draw a tube's mesh, longitudinal seam, and circumferential rims in that order.
#let tube_panel(axis, tube, center_x, mesh, seam_steps, seam_arrows, rims, rim_arrows) = {
let origin = (center_x, 0)
draw-tube(axis, tube, ..mesh, origin)
draw-visible(
phase => tube-point(axis, tube, phase, 0.25),
phase => tube-normal(axis, phase, 0.25),
seam_steps,
origin,
seam-color + edge-weight,
arrows: seam_arrows,
)
for rim in rims {
draw-visible(
phase => tube-point(axis, tube, rim, phase),
phase => tube-normal(axis, rim, phase),
44,
origin,
rim-color + edge-weight,
arrows: rim_arrows,
)
}
}
#let caption(x, body) = content(
(x, -2.6),
text(size: 14pt, fill: rgb("#4A5560"))[#body],
anchor: "north",
)
#let step-arrow(x, body, paint) = {
line(
(x, 0),
(x + 1.45, 0),
stroke: rgb("#4A5560") + 0.9pt,
mark: (end: "stealth", fill: rgb("#4A5560"), scale: 0.55),
)
content(
(x + 0.725, 0.2),
text(size: annotation-size, fill: paint)[#body],
anchor: "south",
)
}
#canvas({
// === 1. the periodic plane ===
let cell = 1.3
for idx in range(-1, 2) {
let at = idx * cell
line((at, -1.5 * cell), (at, 1.5 * cell), stroke: rim-color.transparentize(78%) + 0.9pt)
line((-1.5 * cell, at), (1.5 * cell, at), stroke: seam-color.transparentize(78%) + 0.9pt)
}
for (from, to) in (
((-1.85 * cell, 0), (1.85 * cell, 0)),
((0, -1.85 * cell), (0, 1.85 * cell)),
) {
line(
from,
to,
stroke: rgb("#4A5560") + 0.7pt,
mark: (end: "stealth", fill: rgb("#4A5560"), scale: 0.5),
)
}
// shifting this cell by 2pi in either direction lands on a copy of itself
rect((0, 0), (cell, cell), fill: seam-color.transparentize(90%), stroke: none)
for (from, to, paint) in (
((0, 0), (cell, 0), seam-color),
((0, cell), (cell, cell), seam-color),
((0, 0), (0, cell), rim-color),
((cell, 0), (cell, cell), rim-color),
) { line(from, to, stroke: paint + 1.5pt) }
content((cell / 2, -0.22), text(size: annotation-size)[$2pi$], anchor: "north")
content((cell + 0.16, cell / 2), text(size: annotation-size)[$2pi$], anchor: "west")
caption(0, [plane with $2pi$ periodicity])
// === 2. the fundamental domain and its two identifications ===
step-arrow(2.55, [one cell], rgb("#4A5560"))
let (square-x, side) = (4.75, 2.15)
grid(
(square-x, -side / 2),
(square-x + side, side / 2),
step: side / 4,
stroke: rgb("#78828C").lighten(58%) + 0.35pt,
)
// one chevron on the horizontal pair, two on the vertical: the usual shorthand for which
// edge is glued to which, and in which direction
for (from, to, paint, chevrons) in (
((0, 0), (side, 0), seam-color, 1),
((0, side), (side, side), seam-color, 1),
((0, 0), (0, side), rim-color, 2),
((side, 0), (side, side), rim-color, 2),
) {
let at(t) = (
square-x + from.at(0) + (to.at(0) - from.at(0)) * t,
-side / 2 + from.at(1) + (to.at(1) - from.at(1)) * t,
)
line(at(0), at(1), stroke: paint + edge-weight)
for idx in range(chevrons) {
let base = 0.5 + (idx - (chevrons - 1) / 2) * 0.11
line(
at(base),
at(base + 0.012),
stroke: paint + edge-weight,
mark: (end: "stealth", fill: paint, scale: 0.6),
)
}
}
content(
(square-x - 0.24, 0),
text(size: annotation-size, fill: rim-color)[$a$],
anchor: "east",
)
content(
(square-x + side / 2, -side / 2 - 0.16),
text(size: annotation-size, fill: seam-color)[$b$],
anchor: "north",
)
caption(square-x + side / 2, [fundamental domain])
// === 3. glue the horizontal pair -> cylinder ===
step-arrow(7.6, align(center)[glue\ top & bottom], seam-color)
let cyl-x = 11.3
// The glued pair is a seam; the two vertical edges are still open rims.
tube_panel(straight-axis(2.7), .62, cyl-x, (20, 18), 24, (.46,), (0.0, 1.0), (.60, .635))
caption(cyl-x, [cylinder])
// === 4. bend it round and glue the rims -> torus ===
step-arrow(13.5, align(center)[glue\ rim to rim], rim-color)
let torus-x = 17.1
// The seam closes the long way round; the two rims fuse into one short circle.
tube_panel(ring-axis(1.32), .5, torus-x, (36, 16), 72, (.60,), (.2,), (.86, .895))
content(
(torus-x - 0.1, 1.02),
text(size: annotation-size, fill: seam-color)[$b$],
anchor: "south",
)
content(
(torus-x + 1.62, -0.5),
text(size: annotation-size, fill: rim-color)[$a$],
anchor: "west",
)
caption(torus-x, [torus])
})