# Task 003 — Presentation: footing.typ + compile + Typst metadata tests ## Goal Present the checked numbers in a printable Typst sheet that also derives the Blavatnik gravity loads inline, emits queryable metadata, and compiles to PDF. ## Background Architecture: `footing.typ` imports `../../lib/sheet.typ` and `json("results.json")`. It must not recompute capacities; it only presents `results.json` values via `calc-line` and `check` boxes. Load determination (roof DL/LL, tributary area, column self weight) is derived in Typst and reconciled to Python's checked `Ps`/`Pu` by the test suite — mirroring `wood-joist` and `steel-beam` where `` is queried. The `check`, `calc-line`, and `calculation-sheet` helpers are defined in `lib/sheet.typ` (worksheets root). Signatures used here: `check(label, demand, capacity, unit:, ok:)`, `calc-line(content, note)`, and `calculation-sheet.with(title:, project:, prepared-by:)`. ## Files to Modify - `calcs/concentric-footing/footing.typ` — create. - `calcs/concentric-footing/generated/footing.pdf` — create via `typst compile --root .`. - `calcs/concentric-footing/test_concentric_footing.py` — append two Typst query tests (do not rewrite existing tests); create `generated/` dir if needed. ## Implementation ### 1. footing.typ structure Follow `calcs/wood-joist/beam.typ` as template: - Header: ``` #import "../../lib/sheet.typ": calc-line, calculation-sheet, check #let data = json("results.json") #let n = data.values #let checks = data.checks #let round(value, digits: 2) = calc.round(value, digits: digits) #show: calculation-sheet.with(title: "Concentric Footing Analysis", project: data.project, prepared-by: data.prepared_by) = Concentric Footing Analysis Square spread footing under concentric axial load, ACI 318-19. Numbers come from `calc.py`; this sheet only presents them. Typst derives gravity loads below; checked Python demands are reconciled by the test suite. ``` - Add a simple footing sketch using Typst `box`/`rect`/`line` drawing (no external image needed). Example: centered square footing with column on top, label `Bf` and `Df`. Wrap in `#figure(..., caption: [Footing plan and section: #n.Bf_ft ft × #n.Bf_ft ft × #n.Df_in in, d=#n.d_in in.])`. Keep geometry text as `calc-line` below. - Section `== Loads Determination`: Derive Blavatnik loads in Typst (visible arithmetic): ``` #let DLr = 10 // psf #let LLr = 20 // psf #let Br = 18.9 // ft #let Lr = 27.5 // ft #let Ar = Br * Lr // ft2 #let bc = 14 // in #let Lc = 14 // ft #let gamma_c = 145 // pcf #let Wc_kip = bc * bc / 144 * Lc * gamma_c / 1000 // kip column weight #let Ps_typst = (DLr + LLr) * Ar / 1000 + Wc_kip // kip #let Pu_typst = 1.2*(DLr*Ar/1000 + Wc_kip) + 1.6*LLr*Ar/1000 #metadata((Ps_kip: Ps_typst, Pu_kip: Pu_typst, Ar_ft2: Ar, Wc_kip: Wc_kip)) ``` Then emit `calc-line` rows: - `Ar = Br·Lr = #round(Ar,2) ft2` (tributary area) - `Wc = bc·bc·Lc·γc = #round(Wc_kip,2) kip` (column self weight) - `Ps = (DLr+LLr)·Ar + Wc = #round(Ps_typst,2) kip (Typst-derived)` - `Ps_checked = #round(n.Ps_kip,2) kip (Python-checked)` - `Pu = 1.2(DLr·Ar+Wc)+1.6·LLr·Ar = #round(Pu_typst,2) kip (Typst-derived)` - `Pu_checked = #round(n.Pu_kip,2) kip` - Also show `qu = Pu/Af = #round(n.qu_psf,1) psf` for context but do not recompute Af. - Section `== Geometry and Materials`: `calc-line` for Bf, Df, cover, d, Af, c, bp, fc, fy, lambda, N, rebar_size, As, rho. Example: `calc-line([$B_f = #n.Bf_ft " ft"$, $A_f = #n.Af_ft2 " ft"^2$], [Footing plan area])` etc. Show As1, As, rho, rho_min. - Section `== Soil Bearing`: `calc-line([$q = P_s/A_f = #round(n.q_psf,1) " psf"$], [Acting service pressure])` `calc-line([$q_a = #n.qa_psf " psf"$], [Allowable])` Then `#check("Soil bearing", checks.soil_bearing.demand, checks.soil_bearing.capacity, unit: "psf", ok: checks.soil_bearing.ok)` Also show `qu` as info line. - Section `== One-Way Shear`: `calc-line([$L_1 = (B_f - c)/2 - d = #round(n.L1_in,2) " in"$], [Cantilever beyond d])` `calc-line([$V_u = q_u B_f L_1 = #round(n.Vu_one_way_kip,2) " kip"$], [Demand at d])` `calc-line([$V_c = 2 lambda sqrt(f'_c) B_f d = #round(n.Vc_one_way_kip,1) " kip"$], [ACI 22.5])` `calc-line([$phi V_c = #round(n.phiVc_one_way_kip,1) " kip"$], [phi=0.75])` `#check("One-way shear", checks.one_way_shear.demand, checks.one_way_shear.capacity, unit: "kip", ok: checks.one_way_shear.ok)` - Section `== Two-Way Shear (Punching)`: `calc-line([$b_o = 4(c+d)= #n.bo_in " in"$], [Critical perimeter, d/2])` `calc-line([$v_c = min(4,2+4/beta,2+alpha d/b_o) lambda sqrt(f'_c)= #round(n.vc_psi,1) " psi"$], [ACI 22.6])` `calc-line([$V_c = v_c b_o d = #round(n.Vc_two_way_kip,1) " kip"$], [])` `calc-line([$V_u = q_u(A_f - (c+d)^2)= #round(n.Vu_two_way_kip,1) " kip"$], [Punch demand])` `#check("Two-way shear", checks.two_way_shear.demand, checks.two_way_shear.capacity, unit: "kip", ok: checks.two_way_shear.ok)` - Section `== Flexure`: `calc-line([$L_c = (B_f - c)/2 = #round(n.Lc_in,1) " in"$], [Cantilever])` `calc-line([$M_u = q_u B_f L_c^2/2 = #round(n.Mu_kipft,2) " kip·ft"$], [])` `calc-line([$a = A_s f_y/(0.85 f'_c B_f)= #round(n.a_in,3) " in"$], [Whitney block])` `calc-line([$M_n = A_s f_y(d -a/2)= #round(n.Mn_kipft,1) " kip·ft"$], [])` `calc-line([$rho = A_s/(B_f d)= #round(n.rho,4)$], [vs rho_min 0.0018])` `#check("Flexure", checks.flexure.demand, checks.flexure.capacity, unit: "kip·ft", ok: checks.flexure.ok)` `#check("Minimum steel", checks.minimum_steel.demand, checks.minimum_steel.capacity, unit: "", ok: checks.minimum_steel.ok)` — for rho, show ratio but check helper expects demand/capacity; use demand=rho_min capacity=rho; label accordingly. - Section `== Concrete Bearing`: `calc-line([$A_1 = b_p^2 = #round(n.A1_in2,1) " in"^2$, $A_2 = B_f^2 = #round(n.A2_in2,1) " in"^2$], [Plate and footing])` `calc-line([$sqrt(A_2/A_1)= #round(n.sqrt_ratio,2)$, capped at 2], [])` `calc-line([$B_n = 0.85 f'_c A_1 sqrt(...)= #round(n.Bn_kip,1) " kip"$], [ACI 22.8])` `calc-line([$phi B_n = #round(n.phiBn_kip,1) " kip"$], [phi=0.65])` `#check("Concrete bearing", checks.bearing.demand, checks.bearing.capacity, unit: "kip", ok: checks.bearing.ok)` - Section `== Scope And Limitations`: Text stating square footing only, interior concentric axial only, gross pressure, no moment/overturning/sliding, no settlement, d from cover to centroid, bo correction noted (reference 68in corrected to ACI 92in), plate vs column clarification, no deflection/crack/development, final design by engineer. - Footer metadata for reconciliation: ``` #metadata((soil_util: checks.soil_bearing.demand/checks.soil_bearing.capacity, one_way_util: checks.one_way_shear.demand/checks.one_way_shear.capacity, two_way_util: checks.two_way_shear.demand/checks.two_way_shear.capacity, flexure_util: checks.flexure.demand/checks.flexure.capacity, bearing_util: checks.bearing.demand/checks.bearing.capacity, q_psf: n.q_psf, qu_psf: n.qu_psf)) ``` Ensure all `n.*` keys match Task 001 values dict exactly. ### 2. Compile From `worksheets/` run: ``` python calcs/concentric-footing/calc.py typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf ``` Verify PDF exists and >50KB. ### 3. Append Typst query tests Append to `calcs/concentric-footing/test_concentric_footing.py` two new tests (do not delete existing 10): - `test_typst_compiles_and_presents_python_numbers(result)` — runs `calc.py` with `subprocess.run([sys.executable, str(HERE/"calc.py")], check=True, cwd=HERE.parents[1])`, then `typst compile ...` check pdf exists, then `typst query` for `` via `typst eval "query()"`? Actually use `typst eval` or `typst query` command as in wood-joist: `subprocess.run(["typst","query", ...])`? Wood-joist uses `typst compile` and `typst eval query`. Use same pattern as wood-joist test: ``` subprocess.run(["typst","compile","--root",".",...], check=True, cwd=HERE.parents[1]) query = subprocess.run(["typst","query","..."]?) ``` Check wood-joist: it runs `["typst","eval","query()","--root",...,"--in","calcs/wood-joist/beam.typ","--format","json"]`. Mirror that: `["typst","query", str(HERE/"footing.typ"), "", "--root", "."]` may vary; adapt to whatever works but assert exactly one entry and that flexure_util approx equals Python f. Specifically query the published metadata and assert `meta["flexure_util"] approx values["Mu"]/values["phiMn"]` etc for soil, one-way, two-way, bearing. - `test_typst_load_demands_match_checked_inputs(result)` — queries `` and asserts `derived Ps_kip approx values["Ps_kip"]` within 0.1 kip (allow rounding of column weight 18.36 vs 18.4) and same for `Pu_kip`. Implementation must be robust: if `typst query` syntax differs, use `typst compile` + `typst eval` as in wood-joist. Ensure tests pass on the CI image where typst is installed. If `generated/` does not exist, create it with `Path(...).mkdir(parents=True, exist_ok=True)`. ## Acceptance Criteria 1. `footing.typ` imports only `lib/sheet.typ` and `results.json`; no arithmetic beyond load derivation and rounding. 2. Every `check` box reflects `checks.*.ok` from JSON; D/C printed as demand/capacity. 3. `typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf` succeeds and PDF exists. 4. Two appended pytest tests pass, reconciling Typst-derived Ps/Pu to Python-checked Ps/Pu and Typst-presented utils to Python values. 5. All prior 10 tests still pass (total 12). 6. No existing calc files modified; `generated/` is git-ignored regenerable. ## Tests Builder runs: ``` python calcs/concentric-footing/calc.py typst compile --root . calcs/concentric-footing/footing.typ calcs/concentric-footing/generated/footing.pdf python -m pytest calcs/concentric-footing/test_concentric_footing.py -v ``` Expected: 12 passed, PDF exists. ## Dependencies Tasks 001, 002 must be DONE.