diff --git a/data/jurisdictions-adoption.json b/data/jurisdictions-adoption.json index 34c7456..6112d3f 100644 --- a/data/jurisdictions-adoption.json +++ b/data/jurisdictions-adoption.json @@ -1,6 +1,6 @@ { - "version": "2026-08-13", - "disclaimer": "Drafting aid, not engineering. Typical/approximate values — verify with the local authority having jurisdiction.", + "version": "2026-08-15", + "disclaimer": "Drafting aid, not engineering. Typical/approximate values — verify with the local authority having jurisdiction. Canadian entries (CA-*) describe National Building Code of Canada adoption, not IRC adoption; ircBase is null for all of them.", "sources": [ "https://up.codes/viewer/alabama/irc-2015", "https://up.codes/codes/alabama", @@ -67,7 +67,11 @@ "https://up.codes/codes/washington", "https://up.codes/viewer/west_virginia/irc-2018", "https://up.codes/codes/wisconsin", - "https://up.codes/codes/wyoming" + "https://up.codes/codes/wyoming", + "https://www.ontario.ca/page/building-code-updates", + "https://www.saskatchewan.ca/business/housing-development-construction-and-property-management/building-and-technical-standards", + "https://www.gov.mb.ca/labour/its/bldg_codes/2020_construction_codes_adoption.html", + "https://dhicanada.ca/codes/provincial-codes-matrix" ], "states": { "AL": { @@ -650,6 +654,199 @@ "High snow loads and frost depths in mountain jurisdictions" ], "note": "ircBase 2018 is representative; many rural counties have no residential code." + }, + "CA-ON-S": { + "name": "Canada · Ontario — South (GTA / Golden Horseshoe)", + "residentialCode": "2024 Ontario Building Code (O. Reg. 163/24), adopting NBC 2020 plus the Ontario Amendments — effective 2025-01-01", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "Ontario adopts NBC 2020 by reference plus a published amendment document; the 2024 harmonization removed roughly 1,730 technical variations from the national codes. Part 9 wood framing is largely unamended.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)", + "MMAH Supplementary Standard SB-1 (climatic and seismic data by municipality) — governs snow load and frost depth, not NBC Appendix C", + "MMAH Supplementary Standard SB-7 (joist, rafter, beam and lintel span tables, by species group)" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-ON-E": { + "name": "Canada · Ontario — East (Ottawa Valley / Kingston)", + "residentialCode": "2024 Ontario Building Code (O. Reg. 163/24), adopting NBC 2020 plus the Ontario Amendments — effective 2025-01-01", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "Same code as the rest of Ontario; this entry differs only in climatic and seismic data (SB-1) for the Ottawa Valley.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)", + "MMAH Supplementary Standard SB-1 (climatic and seismic data by municipality)", + "MMAH Supplementary Standard SB-7 (span tables)" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-ON-N": { + "name": "Canada · Ontario — North (Sudbury / Thunder Bay)", + "residentialCode": "2024 Ontario Building Code (O. Reg. 163/24), adopting NBC 2020 plus the Ontario Amendments — effective 2025-01-01", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "Same code as the rest of Ontario; this entry differs only in climatic data (SB-1) for northern Ontario.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)", + "MMAH Supplementary Standard SB-1 (climatic and seismic data by municipality)", + "MMAH Supplementary Standard SB-7 (span tables)" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-BC": { + "name": "Canada · British Columbia", + "residentialCode": "BC Building Code 2024 (NBC 2020 base), in force 2024", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "BC publishes its own code book on the NBC base, with substantial provincial amendments. Vancouver enforces its own separate Vancouver Building By-law.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)", + "BC Energy Step Code — mandatory performance tiers that commonly drive thicker or double-stud exterior walls", + "High seismic: coastal BC exceeds the NBC 9.23.13 prescriptive bracing threshold, requiring Part 4 engineered lateral design" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-AB": { + "name": "Canada · Alberta", + "residentialCode": "Alberta Building Code 2023 (NBC 2020 base), in force 2023", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "Alberta publishes its own code book on the NBC base with modest provincial amendments; Part 9 framing is close to the national text.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-SK": { + "name": "Canada · Saskatchewan", + "residentialCode": "National Building Code of Canada 2020 (Division B Part 9), adopted 2024-01-01", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "Direct adoption under The Construction Codes Act; Saskatchewan skipped NBC 2015 entirely, moving from NBC 2010 to NBC 2020. Among the closest jurisdictions to the unmodified national code.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-MB": { + "name": "Canada · Manitoba", + "residentialCode": "Manitoba Building Code (M.R. 78/2023), adopting NBC 2020 — effective 2024-01-01", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "NBC 2020 with Manitoba amendments, which the province has been actively reducing under its Canadian Free Trade Agreement commitments. Manitoba also skipped NBC 2015.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-QC": { + "name": "Canada · Quebec", + "residentialCode": "Construction Code, Chapter I – Building (NBC 2015 base), in force since 2022", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "Quebec is one full code cycle behind the rest of Canada: the Construction Code still sits on the NBC 2015 base, with Regie du batiment du Quebec (RBQ) amendments. Official code text is French.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)", + "RBQ provincial amendments", + "Novoclimat energy program (voluntary)" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines. Quebec is on the NBC 2015 base, not NBC 2020 — verify clause numbering before citing." + }, + "CA-NB": { + "name": "Canada · New Brunswick", + "residentialCode": "National Building Code of Canada 2020 (Division B Part 9), adopted the 2020 code cycle", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "Direct adoption of the national model code with minimal provincial amendment; municipalities enforce.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-NS": { + "name": "Canada · Nova Scotia", + "residentialCode": "National Building Code of Canada 2020 (Division B Part 9), adopted the 2020 code cycle", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "Direct adoption of the national model code with minimal provincial amendment; municipalities enforce.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)", + "Elevated Atlantic wind exposure — post-tropical systems drive roof-uplift connection detailing" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-PE": { + "name": "Canada · Prince Edward Island", + "residentialCode": "National Building Code of Canada 2020 (Division B Part 9), adopted the 2020 code cycle", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "Direct adoption of the national model code; permit administration and enforcement are municipal and are not uniform province-wide.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-NL": { + "name": "Canada · Newfoundland and Labrador", + "residentialCode": "National Building Code of Canada 2020 (Division B Part 9), adopted the 2020 code cycle", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "Direct adoption of the national model code with minimal provincial amendment; municipalities enforce.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)", + "Extreme North Atlantic wind and snow exposure", + "Permafrost in northern Labrador" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-YT": { + "name": "Canada · Yukon", + "residentialCode": "National Building Code of Canada 2020 (Division B Part 9), adopted the 2020 code cycle", + "ircBase": null, + "adoptionLevel": "province", + "amendmentFlavor": "Direct adoption of the national model code with minimal provincial amendment; municipalities enforce.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)", + "Discontinuous permafrost — pile and space-frame foundation practice" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-NT": { + "name": "Canada · Northwest Territories", + "residentialCode": "National Building Code of Canada 2020 (Division B Part 9), adopted the 2020 code cycle", + "ircBase": null, + "adoptionLevel": "territory", + "amendmentFlavor": "Direct adoption of the national model code with minimal provincial amendment; municipalities enforce.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)", + "Permafrost — pile and space-frame foundation practice" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-NU": { + "name": "Canada · Nunavut", + "residentialCode": "National Building Code of Canada 2020 (Division B Part 9), adopted the 2020 code cycle", + "ircBase": null, + "adoptionLevel": "territory", + "amendmentFlavor": "Direct adoption of the national model code with minimal provincial amendment; municipalities enforce.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)", + "Continuous permafrost — pile and space-frame foundation practice", + "Extreme Arctic wind and drifting snow" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines." + }, + "CA-GEN": { + "name": "Canada · Generic (NBC 2020)", + "residentialCode": "National Building Code of Canada 2020 (Division B Part 9)", + "ircBase": null, + "adoptionLevel": "national model code", + "amendmentFlavor": "The unamended national model code. Nine of thirteen provinces and territories adopt it essentially as written; Ontario, British Columbia, Alberta and Quebec publish their own variants.", + "specialRegimes": [ + "NBC Division B Part 9 — Housing and Small Buildings (buildings up to 3 storeys and 600 m2)" + ], + "note": "NOT an IRC adoption. Framing rules come from NBC Division B Part 9 (9.23 Wood-Frame Construction); inference engines must not assume IRC section numbers or IRC braced-wall-panel methods here. NBC 9.23.13 selects lateral bracing from Sa(0.2) and hourly wind pressure rather than braced-wall lines. This is the fallback for a Canadian locale with an unknown province — pick a province where it is known. NBC 2025 was published 2025-12-22 but no province had adopted it as of 2026-08." } } } diff --git a/data/jurisdictions-climate.json b/data/jurisdictions-climate.json index 0730aba..e9463c8 100644 --- a/data/jurisdictions-climate.json +++ b/data/jurisdictions-climate.json @@ -1,6 +1,6 @@ { - "version": "2026-08-13", - "disclaimer": "Typical state-level values — site-specific design values (IRC Table R301.2(1), filled in by the local jurisdiction) govern; verify with the AHJ. Entries AL through MO + DC were reconstructed on 2026-08-13 after the climate-a research output was lost; see per-entry \"caveat\".", + "version": "2026-08-15", + "disclaimer": "Typical state-level values — site-specific design values (IRC Table R301.2(1), filled in by the local jurisdiction) govern; verify with the AHJ. Entries AL through MO + DC were reconstructed on 2026-08-13 after the climate-a research output was lost; see per-entry \"caveat\". Canadian provincial and territorial entries (CA-*) were added 2026-08-15 from NBC 2020 Appendix C and Ontario SB-1; see the per-entry \"caveat\".", "sources": [ "https://ascehazardtool.org/", "https://hazards.atcouncil.org/", @@ -14,7 +14,10 @@ "https://www.wbdg.org/", "https://floridabuilding.org/", "https://www.dli.mn.gov/business/codes-and-laws", - "https://www.mass.gov/orgs/board-of-building-regulations-and-standards" + "https://www.mass.gov/orgs/board-of-building-regulations-and-standards", + "https://nrc.canada.ca/en/certifications-evaluations-standards/codes-canada/codes-canada-publications/national-building-code-canada-2020", + "https://www.seismescanada.rncan.gc.ca/hazard-alea/interpolat/nbc-cnb-en.php", + "https://www.ontario.ca/page/building-code-updates" ], "states": { "AL": { @@ -1011,6 +1014,321 @@ "hvhz": false, "deepFrostFootings": true } + }, + "CA-ON-S": { + "name": "Canada · Ontario — South (GTA / Golden Horseshoe)", + "frostLineIn": 47, + "frostLineNote": "OBC 9.12.2.2 + SB-1: 1200 mm (47 in) typical for Toronto, Hamilton and Niagara.", + "groundSnowLoadPsf": 23, + "snowNote": "SB-1 Toronto Ss = 1.1 kPa, Sr = 0.4 kPa (~23 psf ground snow). Windsor and Niagara slightly lower; the Georgian Bay snowbelt is far higher.", + "ultimateWindMph": 110, + "windNote": "Hourly wind pressure q(1/50) ~0.44-0.52 kPa; no hurricane or wind-borne-debris regime.", + "seismicSdc": "A", + "seismicNote": "Sa(0.2) is well below the NBC 9.23.13 prescriptive threshold of 0.70, so prescriptive lateral bracing applies.", + "termiteRisk": "slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern." + }, + "CA-ON-E": { + "name": "Canada · Ontario — East (Ottawa Valley / Kingston)", + "frostLineIn": 71, + "frostLineNote": "SB-1: 1800 mm (71 in) for Ottawa and much of eastern Ontario.", + "groundSnowLoadPsf": 52, + "snowNote": "SB-1 Ottawa Ss = 2.5 kPa, Sr = 0.4 kPa (~52 psf) — more than double the GTA.", + "ultimateWindMph": 110, + "windNote": "Comparable to southern Ontario; no coastal regime.", + "seismicSdc": "C", + "seismicNote": "The western Quebec / Ottawa Valley zone is the second-highest seismic hazard in Canada; Sa(0.2) approaches the NBC 9.23.13 prescriptive threshold and engineered lateral design may be required.", + "termiteRisk": "slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern." + }, + "CA-ON-N": { + "name": "Canada · Ontario — North (Sudbury / Thunder Bay)", + "frostLineIn": 83, + "frostLineNote": "SB-1: 1800 mm (71 in) at Sudbury, up to 2100 mm (83 in) at Thunder Bay and further north.", + "groundSnowLoadPsf": 60, + "snowNote": "Ss typically 2.5-3.5 kPa (~52-73 psf); lake-effect belts higher.", + "ultimateWindMph": 110, + "seismicSdc": "A", + "seismicNote": "Low hazard across the Canadian Shield.", + "termiteRisk": "slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern." + }, + "CA-BC": { + "name": "Canada · British Columbia", + "frostLineIn": 18, + "frostLineNote": "Lower Mainland and Vancouver Island ~450 mm (18 in) in a mild marine climate. The Interior (Kamloops, Prince George) runs 1200-1800 mm — override for interior sites.", + "groundSnowLoadPsf": 35, + "snowNote": "Coastal Ss ~1.5-2.0 kPa (~31-42 psf); interior and mountain sites far higher.", + "ultimateWindMph": 115, + "windNote": "Exposed coastal sites higher; no hurricane regime.", + "seismicSdc": "D", + "seismicNote": "Cascadia subduction zone — the highest seismic hazard in Canada. Coastal BC exceeds the NBC 9.23.13 prescriptive bracing threshold, so Part 4 engineered lateral design is normal practice.", + "termiteRisk": "moderate to heavy", + "weatheringPotential": "moderate", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": true, + "hvhz": false, + "deepFrostFootings": false + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern. BC also enforces the BC Energy Step Code, which commonly drives thicker or double-stud exterior walls than this profile assumes." + }, + "CA-AB": { + "name": "Canada · Alberta", + "frostLineIn": 72, + "frostLineNote": "Typically 1500-2400 mm (59-94 in); the Calgary-Edmonton corridor commonly ~1800 mm (71 in).", + "groundSnowLoadPsf": 27, + "snowNote": "Ss ~1.1-1.6 kPa (~23-33 psf) for the Calgary-Edmonton corridor; foothills and mountain parks much higher.", + "ultimateWindMph": 115, + "windNote": "The chinook belt and foothills see high gusts.", + "seismicSdc": "A", + "seismicNote": "Low hazard; the southwestern foothills are moderate.", + "termiteRisk": "none to slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern." + }, + "CA-SK": { + "name": "Canada · Saskatchewan", + "frostLineIn": 84, + "frostLineNote": "Prairie frost penetration 1800-2400 mm (71-94 in) — deeper than any US state tabulates. Regina and Saskatoon commonly 2100 mm (83 in).", + "groundSnowLoadPsf": 31, + "snowNote": "Ss ~1.4-1.6 kPa (~29-33 psf); dry cold with moderate accumulation.", + "ultimateWindMph": 115, + "windNote": "Sustained open-prairie exposure.", + "seismicSdc": "A", + "seismicNote": "Among the lowest seismic hazards in North America.", + "termiteRisk": "none to slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern." + }, + "CA-MB": { + "name": "Canada · Manitoba", + "frostLineIn": 78, + "frostLineNote": "Prairie frost penetration 1800-2400 mm (71-94 in); Winnipeg commonly ~2000 mm (78 in).", + "groundSnowLoadPsf": 42, + "snowNote": "Ss ~1.9-2.2 kPa (~40-46 psf) for Winnipeg and the Red River Valley.", + "ultimateWindMph": 115, + "windNote": "Sustained open-prairie exposure.", + "seismicSdc": "A", + "seismicNote": "Very low hazard.", + "termiteRisk": "none to slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern." + }, + "CA-QC": { + "name": "Canada · Quebec", + "frostLineIn": 59, + "frostLineNote": "Montreal ~1500 mm (59 in); Quebec City and north 1500-2000 mm.", + "groundSnowLoadPsf": 54, + "snowNote": "Montreal Ss ~2.6 kPa (~54 psf); Quebec City and the north 3.0-4.5 kPa.", + "ultimateWindMph": 110, + "seismicSdc": "C", + "seismicNote": "Charlevoix and western Quebec are elevated-hazard zones; Montreal is moderate.", + "termiteRisk": "slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern." + }, + "CA-NB": { + "name": "Canada · New Brunswick", + "frostLineIn": 55, + "frostLineNote": "Typically 1200-1500 mm (47-59 in).", + "groundSnowLoadPsf": 60, + "snowNote": "Ss ~2.5-3.5 kPa (~52-73 psf).", + "ultimateWindMph": 120, + "windNote": "Post-tropical storm exposure inland of the Bay of Fundy.", + "seismicSdc": "B", + "seismicNote": "Low to moderate.", + "termiteRisk": "slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern." + }, + "CA-NS": { + "name": "Canada · Nova Scotia", + "frostLineIn": 47, + "frostLineNote": "Typically ~1200 mm (47 in); milder maritime ground temperatures.", + "groundSnowLoadPsf": 52, + "snowNote": "Ss ~2.0-3.0 kPa (~42-63 psf).", + "ultimateWindMph": 130, + "windNote": "Atlantic post-tropical systems (Juan 2003, Fiona 2022) drive roof-uplift detailing.", + "seismicSdc": "A", + "seismicNote": "Low hazard.", + "termiteRisk": "slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": true, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern." + }, + "CA-PE": { + "name": "Canada · Prince Edward Island", + "frostLineIn": 47, + "frostLineNote": "Typically ~1200 mm (47 in).", + "groundSnowLoadPsf": 52, + "snowNote": "Ss ~2.5 kPa (~52 psf).", + "ultimateWindMph": 130, + "windNote": "Among the highest sustained wind exposures in Atlantic Canada.", + "seismicSdc": "A", + "seismicNote": "Low hazard.", + "termiteRisk": "slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": true, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern." + }, + "CA-NL": { + "name": "Canada · Newfoundland and Labrador", + "frostLineIn": 59, + "frostLineNote": "Island 1200-1500 mm; Labrador far deeper, with permafrost in the far north.", + "groundSnowLoadPsf": 73, + "snowNote": "Ss ~2.5-4.5 kPa (~52-94 psf) — among the highest in Canada.", + "ultimateWindMph": 140, + "windNote": "Extreme North Atlantic wind exposure.", + "seismicSdc": "A", + "seismicNote": "Low hazard.", + "termiteRisk": "none to slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": true, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern." + }, + "CA-YT": { + "name": "Canada · Yukon", + "frostLineIn": 96, + "frostLineNote": "Discontinuous permafrost; conventional frost-depth footings are often inappropriate. PERMAFROST: a spread-footing depth is not a meaningful output here — piles, adfreeze piles or insulated space-frame foundations are the norm. Design the foundation explicitly.", + "groundSnowLoadPsf": 42, + "snowNote": "Ss ~1.0-2.5 kPa (~21-52 psf).", + "ultimateWindMph": 110, + "seismicSdc": "C", + "seismicNote": "Southwestern Yukon is a moderate-to-high hazard zone.", + "termiteRisk": "none", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern. PERMAFROST: spread-footing depth is not a meaningful output here — piles, adfreeze piles or insulated space-frame foundations are the norm. Design the foundation explicitly." + }, + "CA-NT": { + "name": "Canada · Northwest Territories", + "frostLineIn": 96, + "frostLineNote": "Continuous and discontinuous permafrost. PERMAFROST: a spread-footing depth is not a meaningful output here — piles, adfreeze piles or insulated space-frame foundations are the norm. Design the foundation explicitly.", + "groundSnowLoadPsf": 52, + "snowNote": "Ss ~1.5-3.0 kPa (~31-63 psf).", + "ultimateWindMph": 115, + "seismicSdc": "A", + "seismicNote": "Low hazard.", + "termiteRisk": "none", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern. PERMAFROST: spread-footing depth is not a meaningful output here — piles, adfreeze piles or insulated space-frame foundations are the norm. Design the foundation explicitly." + }, + "CA-NU": { + "name": "Canada · Nunavut", + "frostLineIn": 96, + "frostLineNote": "Continuous permafrost. PERMAFROST: a spread-footing depth is not a meaningful output here — piles, adfreeze piles or insulated space-frame foundations are the norm. Design the foundation explicitly.", + "groundSnowLoadPsf": 63, + "snowNote": "Ss ~1.5-3.5 kPa (~31-73 psf) with extreme drifting.", + "ultimateWindMph": 140, + "windNote": "Extreme Arctic wind exposure.", + "seismicSdc": "A", + "seismicNote": "Low hazard.", + "termiteRisk": "none", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern. PERMAFROST: spread-footing depth is not a meaningful output here — piles, adfreeze piles or insulated space-frame foundations are the norm. Design the foundation explicitly." + }, + "CA-GEN": { + "name": "Canada · Generic (NBC 2020)", + "frostLineIn": 48, + "frostLineNote": "Conservative national placeholder. Canadian frost depths run from ~450 mm on the BC coast to 2400 mm on the Prairies — pick a province for a usable value.", + "groundSnowLoadPsf": 40, + "snowNote": "Conservative national placeholder; Ss varies roughly 1.0-4.5 kPa across Canada.", + "ultimateWindMph": 115, + "seismicSdc": "B", + "seismicNote": "Generic. Coastal BC is far higher; the Prairies far lower.", + "termiteRisk": "slight", + "weatheringPotential": "severe", + "flags": { + "hurricaneTies": false, + "seismicHoldDowns": false, + "hvhz": false, + "deepFrostFootings": true + }, + "caveat": "Canadian entry (added 2026-08-15): typical provincial values derived from NBC 2020 Division B Appendix C, and Ontario MMAH Supplementary Standard SB-1 for the Ontario rows. Site-specific values from the authority having jurisdiction govern. This is the fallback for a Canadian locale with an unknown province — always pick a province where it is known." } } } diff --git a/data/wall-assemblies.json b/data/wall-assemblies.json index 707e8c2..af82ad2 100644 --- a/data/wall-assemblies.json +++ b/data/wall-assemblies.json @@ -210,9 +210,13 @@ "OH": "vinyl", "OK": "brickVeneer", "OR": "fiberCement", "PA": "vinyl", "RI": "vinyl", "SC": "fiberCement", "SD": "vinyl", "TN": "brickVeneer", "TX": "brickVeneer", "UT": "stucco", "VA": "fiberCement", "VT": "vinyl", "WA": "fiberCement", "WI": "vinyl", "WV": "vinyl", - "WY": "vinyl" + "WY": "vinyl", + "CA-ON-S": "brickVeneer", "CA-ON-E": "brickVeneer", "CA-ON-N": "vinyl", "CA-BC": "fiberCement", + "CA-AB": "vinyl", "CA-SK": "vinyl", "CA-MB": "vinyl", "CA-QC": "brickVeneer", "CA-NB": "vinyl", + "CA-NS": "vinyl", "CA-PE": "vinyl", "CA-NL": "vinyl", "CA-YT": "vinyl", "CA-NT": "vinyl", + "CA-NU": "vinyl", "CA-GEN": "vinyl" }, - "defaultCladdingCitation": "Regional defaults keyed to 2021 IRC R703.6/.8/.10/.11 + data/jurisdictions-climate.json (termiteRisk, weatheringPotential, flags) and jurisdictions-adoption.json; FL default is stucco-on-CMU variant", + "defaultCladdingCitation": "Regional defaults keyed to 2021 IRC R703.6/.8/.10/.11 + data/jurisdictions-climate.json (termiteRisk, weatheringPotential, flags) and jurisdictions-adoption.json; FL default is stucco-on-CMU variant. Canadian entries (CA-*) reflect dominant provincial single-family practice — brick veneer is common on Ontario and Quebec street elevations, vinyl elsewhere.", "insulationByClimateZone": { "1": { "value": "R13", "compliance2021": ["R-13 cavity", "R-0 + R-10ci"], "compliance2018": ["R-13 cavity"], "studDepthIn": 3.5, "battThicknessIn": 3.5, "citation": "2021 IECC Table R402.1.3 / IRC N1102.1.3 (2018: Table R402.1.2)" }, "2": { "value": "R13", "compliance2021": ["R-13 cavity", "R-0 + R-10ci"], "compliance2018": ["R-13 cavity"], "studDepthIn": 3.5, "battThicknessIn": 3.5, "citation": "2021 IECC Table R402.1.3 / IRC N1102.1.3" }, @@ -262,9 +266,13 @@ "ND": "6A-7", "OH": "5A (4A south)", "OK": "3A (4B panhandle)", "OR": "4C west (5B east)", "PA": "5A (4A SE)", "RI": "5A", "SC": "3A (2A coast)", "SD": "5A-6A", "TN": "4A (3A west)", "TX": "2A-3A (3B/4B west)", "UT": "5B (3B St. George)", "VT": "6A", "VA": "4A", - "WA": "4C west (5B east)", "WV": "4A-5A", "WI": "6A (5A south)", "WY": "6B (7 mtn)" + "WA": "4C west (5B east)", "WV": "4A-5A", "WI": "6A (5A south)", "WY": "6B (7 mtn)", + "CA-ON-S": "5A", "CA-ON-E": "6A", "CA-ON-N": "7 (6A Sudbury)", "CA-BC": "4C (5B-6B interior)", + "CA-AB": "7 (6B south)", "CA-SK": "7", "CA-MB": "7 (6A south)", "CA-QC": "6A (7 north)", + "CA-NB": "6A", "CA-NS": "6A", "CA-PE": "6A", "CA-NL": "6A (7 Labrador)", + "CA-YT": "7 (8 north)", "CA-NT": "8", "CA-NU": "8", "CA-GEN": "6A" }, - "stateClimateZoneCitation": "2021 IECC Figure R301.1 / county tables — dominant residential zone per state; split states (parenthesized) need a 'zone varies by county — confirm with AHJ' warning. jurisdictions-climate.json lacks an ieccZone field; overlay generator writes ieccZone + ieccZoneRange from this map.", + "stateClimateZoneCitation": "2021 IECC Figure R301.1 / county tables — dominant residential zone per state; split states (parenthesized) need a 'zone varies by county — confirm with AHJ' warning. jurisdictions-climate.json lacks an ieccZone field; overlay generator writes ieccZone + ieccZoneRange from this map. Canadian entries (CA-*) map each province to its closest ASHRAE/IECC-equivalent zone: Canada uses NBC/NECB heating-degree-day data rather than IECC zones, so these are approximate and exist so the shared insulation/vapour-retarder tables resolve. Note NBC 9.25 requires a vapour barrier on the warm side throughout Canada, which is stricter than the IECC vapor-retarder class implied by these zones.", "overlays": { "hvhz": { "trigger": "flags.hvhz === true (jurisdictions-climate.json)", diff --git a/src/framing/compute.ts b/src/framing/compute.ts index acad63f..8cf0645 100644 --- a/src/framing/compute.ts +++ b/src/framing/compute.ts @@ -74,7 +74,7 @@ export function computeLevel( return result } -function computeLevelUncached( +export function computeLevelUncached( nodes: Record>, config: FramingNode, ): ComputeResult { diff --git a/src/framing/renderer.tsx b/src/framing/renderer.tsx index 75f932f..e07671c 100644 --- a/src/framing/renderer.tsx +++ b/src/framing/renderer.tsx @@ -336,43 +336,12 @@ export const FramingRenderer = ({ node }: { node: FramingNode }) => { } } - // Auto-switch the host to its most revealing wall mode while the X-ray is - // on (round-13 user feedback). 'down' — host walls fully hidden — not - // 'cutaway': the host's cutaway needs per-face exterior tags the scene - // data doesn't carry, so it painted every wall with its dot-stipple film - // (quality rounds 1-2). With the host shells gone, Bones' own assembly - // layers ARE the walls, and the per-face camera culling below gives the - // true dollhouse: near faces open, far drywall is the backdrop. - // Restores the previous mode on unmount UNLESS the user changed it since. - useEffect(() => { - if (node.seeThrough === false) return - // Dynamic import: the viewer package drags browser-only deps that must - // never evaluate under bun test (this effect only runs in the host). - let previous: string | undefined - let restore: (() => void) | undefined - let cancelled = false - import('@pascal-app/viewer').then(({ useViewer }) => { - if (cancelled) return - const viewer = useViewer.getState() as unknown as { - wallMode?: string - setWallMode?: (mode: string) => void - } - if (!viewer.setWallMode || viewer.wallMode === 'down') return - previous = viewer.wallMode - viewer.setWallMode('down') - restore = () => { - const now = useViewer.getState() as unknown as { - wallMode?: string - setWallMode?: (m: string) => void - } - if (now.wallMode === 'down' && previous && now.setWallMode) now.setWallMode(previous) - } - }) - return () => { - cancelled = true - restore?.() - } - }, [node.seeThrough]) + // NOTE: the wall-mode takeover ('down' while X-raying) used to live here, + // keyed to this renderer's lifetime — which is the NODE's lifetime, so + // leaving the Bones panel (or merely loading a scene that contained an + // X-ray node) left the host's walls hidden, persisted across reloads. + // It moved to the panel (see ../view-takeover.ts), whose mount/unmount is + // the lifetime the intent actually has. This renderer only draws. // Dollhouse cut (round 13): assembly-layer buckets carry their face // normal — hide the stacks whose face points TOWARD the camera so you diff --git a/src/index.ts b/src/index.ts index 6db5dd0..8a3daac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,3 +52,28 @@ export const bonesHostPanel: PluginHostPanel = { export { lumberDefinition } from './definition' export { LumberNode } from './schema' export { LUMBER_CROSS_SECTIONS, LUMBER_SIZES, lumberBoxDims } from './lumber' + +// Headless engine surface. The derivation pipeline is pure (no React, no +// three, no stores) — a host estimator can compute the same members and +// quantities the panel shows, per level, without mounting anything. +// `computeLevelUncached` exists so a multi-level rollup loop doesn't thrash +// the 1-deep render memo that the panel and 3D renderer share. +export { + computeLevel, + computeLevelUncached, + type ComputeResult, + wallConstruction, +} from './framing/compute' +export { FramingNode } from './framing/schema' +export { + computeTakeoff, + cutList, + cutListCsv, + takeoffCsv, + type CutRow, + type TakeoffAreas, + type TakeoffRow, +} from './engines/takeoff' +export { extractRoofs } from './engines/roof-framing' +export { extractLevels, type LevelSlice } from './core/wall-model' +export type { BonesSystem, Fixture, FixtureKind, Member, MemberRole } from './core/types' diff --git a/src/jurisdiction/canada.test.ts b/src/jurisdiction/canada.test.ts new file mode 100644 index 0000000..548602e --- /dev/null +++ b/src/jurisdiction/canada.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from 'bun:test' +import { DEFAULT_SPEC } from '../core/spec' +import { INCH } from '../core/units' +import { applyJurisdiction, jurisdictionOptions, profileFor } from './profiles' + +/** + * Canada rides the same data path as the US states: two JSON rows merged by + * `profileFor`, then mapped onto the FramingSpec by `applyJurisdiction`. These + * tests pin the parts that would silently regress — a missing data row + * degrades to the INTL fallback rather than throwing, so "it still renders" + * is not evidence that a province actually landed. + */ + +const CANADA = [ + 'CA-ON-S', + 'CA-ON-E', + 'CA-ON-N', + 'CA-BC', + 'CA-AB', + 'CA-SK', + 'CA-MB', + 'CA-QC', + 'CA-NB', + 'CA-NS', + 'CA-PE', + 'CA-NL', + 'CA-YT', + 'CA-NT', + 'CA-NU', + 'CA-GEN', +] as const + +const footingInches = (code: string): number => + applyJurisdiction(DEFAULT_SPEC, profileFor(code)).footingDepth / INCH + +describe('Canadian jurisdictions', () => { + it('every province resolves to real data, not the INTL fallback', () => { + for (const code of CANADA) { + const p = profileFor(code) + // The fallback returns `{...INTL_PROFILE, code, name: code}` — so a name + // equal to the bare code means the data row is missing. + expect(p.name).not.toBe(code) + expect(p.name.startsWith('Canada')).toBe(true) + expect(p.residentialCode).not.toBe('IRC (edition unverified)') + } + }) + + it('cites the National Building Code, not the IRC', () => { + expect(profileFor('CA-SK').residentialCode).toContain('National Building Code') + expect(profileFor('CA-ON-S').residentialCode).toContain('Ontario Building Code') + expect(profileFor('CA-QC').residentialCode).toContain('2015') + }) + + it('no Canadian code collides with a US state code', () => { + const codes = jurisdictionOptions().map((o) => o.code) + expect(new Set(codes).size).toBe(codes.length) + // The obvious trap: bare 'CA' is California and must stay California. + expect(profileFor('CA').name).toBe('California') + }) + + it('southern Ontario frames like the GTA: shallow-ish frost, no ties, 2x6 rafters', () => { + const spec = applyJurisdiction(DEFAULT_SPEC, profileFor('CA-ON-S')) + expect(footingInches('CA-ON-S')).toBeCloseTo(47, 5) + expect(spec.hurricaneTies).toBe(false) + expect(spec.seismicHoldDowns).toBe(false) + expect(spec.rafterSize).toBe(DEFAULT_SPEC.rafterSize) + }) + + it('the Ottawa Valley digs deeper and bumps the rafter for snow', () => { + const spec = applyJurisdiction(DEFAULT_SPEC, profileFor('CA-ON-E')) + expect(footingInches('CA-ON-E')).toBeCloseTo(71, 5) + expect(spec.rafterSize).toBe('2x8') + }) + + it('prairie frost depths exceed every US state', () => { + // The reason Canada needed real rows instead of a US proxy: the deepest + // US frost line in the data is 60in, and Saskatchewan runs deeper. + expect(footingInches('CA-SK')).toBeCloseTo(84, 5) + expect(footingInches('CA-MB')).toBeCloseTo(78, 5) + expect(footingInches('CA-SK')).toBeGreaterThan(footingInches('ND')) + }) + + it('coastal BC turns on seismic hold-downs and tightens anchor bolts', () => { + const spec = applyJurisdiction(DEFAULT_SPEC, profileFor('CA-BC')) + expect(spec.seismicHoldDowns).toBe(true) + expect(spec.anchorBoltSpacing).toBeCloseTo(4 * 12 * INCH, 5) + }) + + it('Atlantic Canada gets uplift ties, the interior does not', () => { + expect(applyJurisdiction(DEFAULT_SPEC, profileFor('CA-NS')).hurricaneTies).toBe(true) + expect(applyJurisdiction(DEFAULT_SPEC, profileFor('CA-NL')).hurricaneTies).toBe(true) + expect(applyJurisdiction(DEFAULT_SPEC, profileFor('CA-SK')).hurricaneTies).toBe(false) + }) + + it('exterior walls stay framed everywhere in Canada', () => { + for (const code of CANADA) { + expect(profileFor(code).exteriorWallDefault).toBe('framed') + } + }) + + it('permafrost territories carry an explicit warning in their notes', () => { + for (const code of ['CA-YT', 'CA-NT', 'CA-NU']) { + expect(profileFor(code).notes.join(' ')).toContain('PERMAFROST') + } + }) + + it('the dropdown still leads with INTL and now carries Canada', () => { + const options = jurisdictionOptions() + expect(options[0]?.code).toBe('INTL') + expect(options.filter((o) => o.code.startsWith('CA-')).length).toBe(CANADA.length) + }) +}) diff --git a/src/jurisdiction/guess.ts b/src/jurisdiction/guess.ts index 4a8e44d..58e9e4a 100644 --- a/src/jurisdiction/guess.ts +++ b/src/jurisdiction/guess.ts @@ -5,8 +5,8 @@ * * 1. `Intl.DateTimeFormat().resolvedOptions().timeZone` — IANA zone ids name * actual cities (`America/Denver`, `America/Indiana/Indianapolis`), which - * maps to a US state for every US zone. - * 2. `navigator.language` region subtag — a non-US locale (fr-FR, de-DE…) + * maps to a US state (or Canadian province) for every US/CA zone. + * 2. `navigator.language` region subtag — a non-US/CA locale (fr-FR, de-DE…) * falls back to the INTL profile. * * This is a SUGGESTION rendered in the panel ("guessed from your browser") — @@ -44,6 +44,38 @@ const TZ_STATE: Record = { 'America/Nome': 'AK', 'America/Adak': 'AK', 'Pacific/Honolulu': 'HI', + + // Canada. Ontario is split three ways (the frost line runs 1200mm in the + // GTA to 2100mm at Thunder Bay), so the zone picks the region it names. + // Caveat: modern tzdata folds Montreal into America/Toronto, so Quebec + // users land on southern Ontario and have to pick their province by hand. + 'America/Toronto': 'CA-ON-S', + 'America/Nipigon': 'CA-ON-N', + 'America/Thunder_Bay': 'CA-ON-N', + 'America/Atikokan': 'CA-ON-N', + 'America/Rainy_River': 'CA-ON-N', + 'America/Blanc-Sablon': 'CA-QC', + 'America/Winnipeg': 'CA-MB', + 'America/Regina': 'CA-SK', + 'America/Swift_Current': 'CA-SK', + 'America/Edmonton': 'CA-AB', + 'America/Vancouver': 'CA-BC', + 'America/Dawson_Creek': 'CA-BC', + 'America/Fort_Nelson': 'CA-BC', + 'America/Creston': 'CA-BC', + 'America/Halifax': 'CA-NS', + 'America/Glace_Bay': 'CA-NS', + 'America/Moncton': 'CA-NB', + 'America/St_Johns': 'CA-NL', + 'America/Goose_Bay': 'CA-NL', + 'America/Whitehorse': 'CA-YT', + 'America/Dawson': 'CA-YT', + 'America/Yellowknife': 'CA-NT', + 'America/Inuvik': 'CA-NT', + 'America/Iqaluit': 'CA-NU', + 'America/Rankin_Inlet': 'CA-NU', + 'America/Cambridge_Bay': 'CA-NU', + 'America/Resolute': 'CA-NU', } export type JurisdictionGuess = { @@ -61,6 +93,9 @@ export function guessJurisdiction(): JurisdictionGuess { typeof navigator !== 'undefined' ? (navigator.language ?? navigator.languages?.[0]) : '' const region = lang?.split('-')[1]?.toUpperCase() if (region === 'US') return { code: 'TX', reason: `US locale (${lang}), unknown state` } + if (region === 'CA') { + return { code: 'CA-GEN', reason: `Canadian locale (${lang}), unknown province` } + } if (region) return { code: 'INTL', reason: `non-US locale (${lang})` } } catch { // SSR or restricted environment — fall through. diff --git a/src/panel.tsx b/src/panel.tsx index d270518..70a8642 100644 --- a/src/panel.tsx +++ b/src/panel.tsx @@ -13,6 +13,7 @@ import { guessJurisdiction } from './jurisdiction/guess' import { jurisdictionOptions, profileFor } from './jurisdiction/profiles' import { LUMBER_CROSS_SECTIONS, LUMBER_SIZES, type LumberSize } from './lumber' import { useBonesStore } from './store' +import { createWallModeTakeover, type WallModeViewer } from './view-takeover' const LUMBER_KIND: string = 'bones:lumber' const FRAMING_KIND: string = 'bones:framing' @@ -40,6 +41,19 @@ export default function BonesPanel() { (n) => (n.type as string) === FRAMING_KIND && n.parentId === activeLevelId, ) as (FramingNode & { id: string }) | undefined }) + // While this panel is OPEN with a live X-ray, hide the host's wall shells + // so the skeleton reads as the building; restore on leave. Panel-owned on + // purpose — see view-takeover.ts for why the renderer must not do this. + const takeoverActive = Boolean(framingNode) && framingNode?.seeThrough !== false + useEffect(() => { + if (!takeoverActive) return + const takeover = createWallModeTakeover( + () => useViewer.getState() as unknown as WallModeViewer, + ) + takeover.engage() + return () => takeover.release() + }, [takeoverActive]) + // ONE derivation per scene edit, shared by the X-Ray status line and the // takeoff — the renderer runs its own (also once). Reviewer advisory r1. const nodes = useScene((s) => s.nodes) @@ -52,9 +66,9 @@ export default function BonesPanel() {
-

Bones

+

Structural Framing

- Alpha + Beta

@@ -551,7 +565,7 @@ function ExportPlansButton({ className="flex w-full flex-col items-center gap-0.5 rounded-lg bg-primary px-3 py-2.5 font-semibold text-primary-foreground text-sm shadow-sm transition-transform hover:scale-[1.02] active:scale-[0.99]" onClick={() => { const sheets = buildPlanSet(result.members, result.fixtures, { - projectName: document.title.split('—')[0]?.trim() || 'Pascal project', + projectName: document.title.split('—')[0]?.trim() || 'Project', levelName, // resolved state code — raw 'AUTO' printed on sheets (quality C1) jurisdiction: result.jurisdiction, diff --git a/src/plans/plan-set.ts b/src/plans/plan-set.ts index b945c6e..3a1b172 100644 --- a/src/plans/plan-set.ts +++ b/src/plans/plan-set.ts @@ -217,7 +217,7 @@ function chrome( ${esc(clip(title, 44))} - ${esc(clip(`${opts.projectName ?? 'Pascal project'} — ${opts.levelName ?? 'Level'}`, 66))} + ${esc(clip(`${opts.projectName ?? 'Project'} — ${opts.levelName ?? 'Level'}`, 66))} ${esc(line1)} ${line1b ? `${esc(line1b)}` : ''} ${esc(line2)} @@ -803,7 +803,7 @@ function coverSheet(members: Member[], opts: PlanSetOptions, index: string[]): P ) const f = fitSegs(segs) if (!f) return null - const title = opts.projectName ?? 'Pascal project' + const title = opts.projectName ?? 'Project' const lines = [ `${opts.levelName ?? 'Level'} — full construction set`, [opts.jurisdiction, opts.codeName].filter(Boolean).join(' · '), @@ -942,7 +942,7 @@ export function planSetHtml(sheets: PlanSheet[], opts: PlanSetOptions = {}): str .map((s) => `

${s.svg}
`) .join('\n') return ` -${esc(opts.projectName ?? 'Pascal')} — Full plans (LOD 400) +${esc(opts.projectName ?? 'Project')} — Full plans (LOD 400)