diff --git a/data/handouts/.gitignore b/data/handouts/.gitignore index eb2cf65e..66a3da3e 100644 --- a/data/handouts/.gitignore +++ b/data/handouts/.gitignore @@ -5,4 +5,5 @@ !*.en.tex !*.cs.tex !Images/ -!Images/** \ No newline at end of file +!Images/** +Images/texput.** \ No newline at end of file diff --git a/data/handouts/Images/_Export-Asy.ps1 b/data/handouts/Images/_Export-Asy.ps1 new file mode 100644 index 00000000..7233713e --- /dev/null +++ b/data/handouts/Images/_Export-Asy.ps1 @@ -0,0 +1,120 @@ +# The script below batch-renders Asymptote .asy files to PDF and SVG sitting next to each .asy. +# It is meant for the handout-figure workflow: author a .asy file (typically importing _common.asy) +# and let this script produce both a vector PDF and a plain SVG without ever opening a viewer. +# +# Per .asy file the pipeline is: +# 1. Run asy with -noView to render the .asy to a PDF next to the source. asy is run with +# -cd so `import _common;` and other relative imports resolve next to the source file. +# 2. Convert the PDF to SVG via Inkscape (matches the conversion path used by _Export-Ggb.ps1 +# so handout figures coming from either source render identically downstream). +param( + # One or more directories, .asy files, or PowerShell wildcards (e.g. 'angles-*.asy'). Default + # is the folder this script lives in, which matches the typical handout-figures layout. + [Parameter(Position = 0, ValueFromRemainingArguments = $true)] + [string[]]$Path = @(Split-Path -Parent $MyInvocation.MyCommand.Definition), + + # Path to the Asymptote executable. + [string]$AsyExe = 'C:\Program Files\Asymptote\asy.exe' +) + +# Ensure errors stop the script and suppress progress output +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +# Validate the Asymptote executable up front so we fail fast with a clear message +if (-not (Test-Path -LiteralPath $AsyExe)) { + Write-Error "Asymptote executable not found at $AsyExe" + exit 1 +} + +# Validate Inkscape is on PATH (used for converting the PDF to SVG). Prefer inkscape.exe +# (GUI subsystem) over inkscape.com (console launcher) so it doesn't attach to our terminal. +$inkscape = Get-Command inkscape -ErrorAction SilentlyContinue +if (-not $inkscape) { + Write-Error "Inkscape not found on PATH (needed for PDF -> SVG conversion)" + exit 1 +} +$inkscape = $inkscape.Source +$inkscapeExe = [System.IO.Path]::ChangeExtension($inkscape, '.exe') +if (Test-Path -LiteralPath $inkscapeExe) { $inkscape = $inkscapeExe } + +# Pipeline for one .asy file: asy PDF -> Inkscape SVG. Both outputs are written next to the +# input as .pdf / .svg. +function Convert-OneAsy { + param( + [Parameter(Mandatory = $true)][string]$AsyPath + ) + + # Determine output paths + $stem = [System.IO.Path]::GetFileNameWithoutExtension($AsyPath) + $dir = [System.IO.Path]::GetDirectoryName($AsyPath) + $finalPdf = Join-Path $dir "$stem.pdf" + $finalSvg = Join-Path $dir "$stem.svg" + + # 1. Render the .asy to PDF. -noView suppresses asy's auto-opening of the system PDF viewer. + # -f pdf forces PDF output (asy otherwise picks based on settings.eps / settings.outformat). + # -o sets the output filename stem (asy appends the format extension). + # -cd changes asy's working directory so `import _common;` resolves next to the file. + if (Test-Path -LiteralPath $finalPdf) { Remove-Item -LiteralPath $finalPdf -Force } + $asyArgs = @('-noView', '-f', 'pdf', '-o', $stem, '-cd', $dir, $AsyPath) + $proc = Start-Process -FilePath $AsyExe -ArgumentList $asyArgs -Wait -PassThru -NoNewWindow + if ($proc.ExitCode -ne 0 -or -not (Test-Path -LiteralPath $finalPdf)) { + throw "asy failed (exit $($proc.ExitCode))" + } + + # 2. Convert the PDF to plain SVG via Inkscape. The .com launcher on PATH is the console + # launcher that waits for inkscape.exe, so -Wait is enough. + if (Test-Path -LiteralPath $finalSvg) { Remove-Item -LiteralPath $finalSvg -Force } + $proc = Start-Process -FilePath $inkscape -ArgumentList @($finalPdf, '--pdf-poppler', '--export-type=svg', '--export-plain-svg', "--export-filename=$finalSvg") -Wait -PassThru + if ($proc.ExitCode -ne 0 -or -not (Test-Path -LiteralPath $finalSvg)) { + throw "Inkscape SVG conversion failed (exit $($proc.ExitCode))" + } +} + +# Resolve each entry in $Path into .asy files: a directory expands to *.asy in it, a single +# file is used as-is, anything else is treated as a glob (Get-ChildItem handles wildcards). +$asyFiles = @() +foreach ($p in $Path) { + if (-not (Test-Path -LiteralPath $p -PathType Container) -and $p -notmatch '\.asy$') { + $p = $p + '.asy' + } + if (Test-Path -LiteralPath $p -PathType Container) { + $asyFiles += Get-ChildItem -LiteralPath $p -Filter '*.asy' -File + } elseif (Test-Path -LiteralPath $p -PathType Leaf) { + $asyFiles += @(Get-Item -LiteralPath $p) + } else { + $asyFiles += Get-ChildItem -Path $p -File -ErrorAction SilentlyContinue | + Where-Object { $_.Extension -ieq '.asy' } + } +} + +# Skip files whose name starts with '_' (convention for shared modules like _common.asy) +$asyFiles = @($asyFiles | Where-Object { -not $_.Name.StartsWith('_') }) + +if ($asyFiles.Count -eq 0) { + Write-Error "No .asy files found for path: $Path" + exit 1 +} + +# Process each .asy, accumulate per-file status so one failure doesn't stop the batch +$total = $asyFiles.Count +Write-Host "Processing $total file$(if ($total -ne 1) { 's' })..." -ForegroundColor Cyan +$ok = 0; $fail = 0; $i = 0 +foreach ($asyFile in $asyFiles) { + $i++ + $prefix = "[$i/$total]" + try { + Convert-OneAsy -AsyPath $asyFile.FullName + Write-Host "$prefix [OK] $($asyFile.Name)" -ForegroundColor Green + $ok++ + } + catch { + Write-Host "$prefix [FAIL] $($asyFile.Name): $_" -ForegroundColor Yellow + $fail++ + } +} + +# Final report +Write-Host ""; Write-Host "Done. Success: $ok, Failed: $fail" -ForegroundColor Cyan; Write-Host "" + +if ($fail -gt 0) { exit 1 } else { exit 0 } diff --git a/data/handouts/Images/_common.asy b/data/handouts/Images/_common.asy new file mode 100644 index 00000000..a1afaf11 --- /dev/null +++ b/data/handouts/Images/_common.asy @@ -0,0 +1,383 @@ +// Shared setup for handout figures: imported with `import _common;`. + +// Latin Modern for label text so figures match the surrounding handout body +texpreamble("\usepackage{lmodern}\usepackage[T1]{fontenc}"); + +// Basic units +unitsize(1pt); +defaultpen(fontsize(13pt)); + +// +// Palette: 5 hue families × Light/Normal/Dark. Pick the closest hue and shade. +// AngleMark / RightAngleMark fills want a `Light*` pen so the sector reads softly. +// +pen LightBlue = rgb(0.5, 0.5, 1); +pen Blue = rgb(0, 0, 1); +pen DarkBlue = rgb(0, 0, 0.5); + +pen LightRed = rgb(1, 0.5, 0.5); +pen Red = rgb(1, 0, 0); +pen DarkRed = rgb(0.5, 0, 0); + +pen LightGreen = rgb(0.5, 1, 0.5); +pen Green = rgb(0, 0.5, 0); +pen DarkGreen = rgb(0, 0.25, 0); + +pen LightPurple = rgb(1, 0.5, 1); +pen Purple = rgb(0.5, 0, 0.5); +pen DarkPurple = rgb(0.25, 0, 0.25); + +pen LightPink = rgb(1, 0.75, 0.85); +pen Pink = rgb(1, 0.5, 0.75); +pen DarkPink = rgb(0.75, 0.25, 0.5); + +// +// Line-width tiers. NormalWidth is the default for edges and circles. +// +real ThinWidth = 0.5; +real NormalWidth = 1.0; +real ThickWidth = 1.5; + +// +// Standard pens reused by every figure. +// +pen edgePen = black + linewidth(NormalWidth); +pen vertexPen = black + linewidth(ThinWidth); + +// +// Dashed linetype for auxiliary segments — combine with a colour, e.g. `Draw(A, B, black + dashedPen)`. +// +pen dashedPen = linetype("3 3", offset=0, scale=false); + +// +// Arc-radius presets for AngleMark. Radius3 is the default; pick a different +// preset (or pass a literal) per call when a figure needs more variety. +// Reassign any of these per file if the figure scale calls for it. +// +real Radius1 = 10; +real Radius2 = 15; +real Radius3 = 20; +real Radius4 = 25; +real Radius5 = 30; + +// +// Default vertex dot radius +// +real vertexDotRadius = 2.95; + +// +// Default shift along alignDir for point labels +// +real pointLabelDistance = 3; + +// +// Default shift along alignDir for edge labels +// +real edgeLabelDistance = 0; + +// +// Default fraction of AngleMark's radius at which its label is placed. +// +real angleMarkLabelFraction = 1.5; + +// +// Length of each tick segment in the parallel-lines mark. +// +real parallelMarkLength = 7.5; + +// +// Distance between adjacent ticks in the parallel-lines mark. +// +real parallelMarkSpacing = 3; + +// +// Rotation angle of each tick relative to the segment direction. +// +real parallelMarkAngle = 80; + +// +// Geometric point constructors +// + +// +// Returns the midpoint of segment AB. +// +pair Midpoint( + pair A, + pair B) +{ + return (A + B) / 2; +} + +// +// Returns the point at distance r from O along the ray at angle angleDeg +// (measured CCW from +x). +// +pair Polar( + pair O, + real angleDeg, + real r) +{ + return O + r * dir(angleDeg); +} + +// +// Extends ray AB past B by `length` units. The returned point lies on line AB, +// on the far side of B from A, at distance `length` from B. +// +pair ExtendPast( + pair A, + pair B, + real length) +{ + return B + length * unit(B - A); +} + +// +// Returns the perpendicular projection of P onto the line through A and B. +// +pair Foot( + pair P, + pair A, + pair B) +{ + pair d = B - A; + return A + dot(P - A, d) / dot(d, d) * d; +} + +// +// Returns the reflection of P across the line through A and B. +// +pair ReflectAcross( + pair P, + pair A, + pair B) +{ + return 2 * Foot(P, A, B) - P; +} + +// +// Fills the angle sector ∠XYZ with vertex Y, sweeping CCW from ray YX to ray YZ. +// Pass a `Light*` pen for `color` so the filled sector reads softly. When `lab` +// is non-empty, "$lab$" is placed on the angular bisector at distance +// labelFraction * radius + labelOffset from Y. +// +// Used global variables: Radius3, angleMarkLabelFraction +// +void AngleMark( + pair X, + pair Y, + pair Z, + pen color, + string lab = "", + real radius = Radius3, + real labelFraction = angleMarkLabelFraction, + real labelOffset = 0, + pen labelPen = black) +{ + // Compute start/end angles (CCW from +x) + real d1 = degrees(X - Y); + real d2 = degrees(Z - Y); + + // Normalise so d2 ≥ d1 — Asymptote's arc() reverses direction (CW) when d2 < d1. + while (d2 < d1) d2 += 360; + + // Fill the wedge from Y along the arc back to Y + fill(Y -- arc(Y, radius, d1, d2) -- cycle, color); + + // Place the optional label on the angular bisector + if (lab != "") { + real mid = (d1 + d2) / 2; + real labelR = radius * labelFraction + labelOffset; + label("$" + lab + "$", Y + labelR * dir(mid), labelPen); + } +} + +// +// Draws the segment AB with the standard edge styling. Pass a solid colour +// (e.g. Blue) to tint; linewidth is added automatically. To keep extra pen +// attributes such as a linetype, pass a fully-formed pen (e.g. `black + dashedPen`). +// +// Used global variables: NormalWidth +// +void Draw( + pair A, + pair B, + pen color = black) +{ + draw(A -- B, color + linewidth(NormalWidth)); +} + +// +// Draws a circle with the same edge styling as Draw — same colour-only convention. +// +// Used global variables: NormalWidth +// +void Circle( + pair center, + real radius, + pen color = black) +{ + draw(circle(center, radius), color + linewidth(NormalWidth)); +} + +// +// Draws a single coloured-fill black-outlined dot at point P. +// +// Used global variables: vertexDotRadius, vertexPen +// +void VertexDot( + pair P, + pen fillColor = Blue, + real radius = vertexDotRadius) +{ + // Fill the disc, then outline it + fill(circle(P, radius), fillColor); + draw(circle(P, radius), vertexPen); +} + +// +// Draws the same dot at every given point. Pass `fillColor` to recolour all of them. +// +// Used global variables: vertexDotRadius +// +void VertexDots( + pair[] points, + pen fillColor = Blue, + real radius = vertexDotRadius) +{ + for (pair P : points) VertexDot(P, fillColor, radius); +} + +// +// Places "$name$" near point P with an optional compass alignment (e.g. N, SE). +// `distanceOffset` shifts the label outward along alignDir; `offset` nudges in +// absolute coords for the rare case the compass directions aren't enough. +// +// Used global variables: pointLabelDistance +// +void PointLabel( + pair P, + string name, + pair alignDir = (0, 0), + real distanceOffset = pointLabelDistance, + pair offset = (0, 0), + pen color = Blue) +{ + // Start from the anchor with the absolute offset applied + pair pos = P + offset; + + // Push outward along the compass direction, if one was given + if (alignDir != (0, 0)) pos += distanceOffset * unit(alignDir); + + // Render the LaTeX-wrapped name at that position + label("$" + name + "$", pos, alignDir, color); +} + +// +// Draws a vertex dot at P plus its "$name$" label, both in the same colour. +// Replaces the VertexDot + PointLabel pair that appears for every named point. +// +// Used global variables: pointLabelDistance +// +void LabeledDot( + pair P, + string name, + pair alignDir = (0, 0), + real distanceOffset = pointLabelDistance, + pair offset = (0, 0), + pen color = Blue) +{ + // Draw the dot, then the label, using the same colour for both + VertexDot(P, color); + PointLabel(P, name, alignDir, distanceOffset, offset, color); +} + +// +// Draws a right-angle indicator at vertex O with perpendicular rays toward A and B. +// `radius` is the distance from O to the outer corner, matching AngleMark's +// `radius` so the two scale together when sharing Radius3. Filled by default +// (pass a `Light*` pen for `color` so the L reads softly); pass filled=false +// for the bare L-shape stroke. +// +// Used global variables: Radius3, edgePen +// +void RightAngleMark( + pair A, + pair O, + pair B, + real radius = Radius3, + pen color = LightBlue, + bool filled = true) +{ + // Leg length so the outer corner sits at distance `radius` from O + real legLen = radius * sqrt(2) / 2; + + // Three corners of the right-angle quadrilateral (the fourth is O itself) + pair p1 = O + legLen * unit(A - O); + pair p2 = p1 + legLen * unit(B - O); + pair p3 = O + legLen * unit(B - O); + + // Fill the L-shape or just stroke its outline + if (filled) { + fill(O -- p1 -- p2 -- p3 -- cycle, color); + } else { + draw(p1 -- p2 -- p3, edgePen); + } +} + +// +// Places "$name$" near the midpoint of segment AB. `alignDir` (compass: N, S, +// E, W, …) both nudges the position `distanceOffset` units off the segment AND +// aligns the label box on that side, so it reads cleanly. `placement` shifts +// away from the midpoint as a parametric position in 0..1. +// +// Used global variables: edgeLabelDistance +// +void EdgeLabel( + pair A, + pair B, + string name, + pair alignDir = (0, 0), + real distanceOffset = edgeLabelDistance, + real placement = 0.5, + pen color = black) +{ + // Start from the parametric position along AB + pair pos = A + placement * (B - A); + + // Push perpendicular off the segment if a compass direction was given + if (alignDir != (0, 0)) pos += distanceOffset * unit(alignDir); + + // Render the LaTeX-wrapped name there + label("$" + name + "$", pos, alignDir, color); +} + +// +// Draws `count` short ticks along segment AB centred at parametric position +// `placement`, each rotated `parallelMarkAngle` from the segment direction. +// +// Used global variables: parallelMarkLength, parallelMarkSpacing, parallelMarkAngle +// +void ParallelMark( + pair A, + pair B, + int count = 2, + real placement = 0.5, + pen color = edgePen) +{ + // Unit vector along AB and the centre of the tick group + pair u = unit(B - A); + pair mid = A + placement * (B - A); + + // Tick direction, plus precomputed half-length and offset start + pair tickDir = rotate(parallelMarkAngle) * u; + real half = parallelMarkLength / 2; + real start = -(count - 1) / 2.0; + + // Draw each tick centred on a point spaced parallelMarkSpacing along AB + for (int i = 0; i < count; ++i) { + pair c = mid + (start + i) * parallelMarkSpacing * u; + draw((c - half * tickDir) -- (c + half * tickDir), color); + } +}