4 Commits
Author SHA1 Message Date
blandClaude Opus 5 b19162d9a7 Sett forsidetittelen i overskriftsskriften og aksentfargen
Release enok / bygg (release) Successful in 1m13s
test / test (push) Successful in 1m22s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-20 22:08:21 +02:00
blandClaude Opus 5 c89561b0f5 Gi rapporten plass til kildene, og la den følge egne dokumenter
Release enok / bygg (release) Successful in 1m12s
test / test (push) Successful in 1m16s
Kolonnen «Kilde» i forutsetningstabellen fikk for liten plass, fordi de to første kolonnene
tok det de ville. Kolonnene har nå faste bredder, og tabellen leses på én linje per rad.

En ny [rapport]-tabell i bygningsbeskrivelsen setter logo, aksentfarge, avsenderlinje og
skrift for overskrifter, slik at energirapporten kan se ut som firmaets øvrige rapporter.
Logoen kopieres til utdatakatalogen; mangler filen, lages rapporten uten den.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-20 22:02:36 +02:00
blandClaude Opus 5 e3f37afe36 Rett artefakt-versjonen i eksempelet
test / test (push) Successful in 1m16s
Gitea melder seg som GitHub Enterprise, og artefakt-actionene i v4 nekter å kjøre der.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-20 20:27:13 +02:00
blandClaude Opus 5 cabd8500ea Beskriv hvordan importen tåler feil i eksporten
test / test (push) Successful in 1m17s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-20 20:22:55 +02:00
9 changed files with 144 additions and 19 deletions
Generated
+1 -1
View File
@@ -222,7 +222,7 @@ dependencies = [
[[package]] [[package]]
name = "enok" name = "enok"
version = "0.4.1" version = "0.4.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "enok" name = "enok"
version = "0.4.1" version = "0.4.3"
edition = "2021" edition = "2021"
description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)" description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)"
license = "MIT" license = "MIT"
+25 -2
View File
@@ -180,6 +180,16 @@ navn = "Nye vinduer"
endringer.vindu = [{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 }] endringer.vindu = [{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 }]
``` ```
Rapporten kan følge husets eller firmaets egne dokumenter:
```toml
[rapport]
logo = "logo.svg" # vises øverst på forsiden
farge = "#880000" # aksentfarge i overskrifter og tabeller
avsender = "Firma · Adresse · Org.nr. 000 000 000"
skrift_overskrift = "Futura Now Headline"
```
Scenarier flettes inn i bygningsbeskrivelsen: tabeller flettes rekursivt, elementer i lister Scenarier flettes inn i bygningsbeskrivelsen: tabeller flettes rekursivt, elementer i lister
matches på `navn` eller `type`, nye elementer legges til, og `slett = true` fjerner et element. matches på `navn` eller `type`, nye elementer legges til, og `slett = true` fjerner et element.
Et navn med `*` treffer alle elementer det passer på, for eksempel `navn = "Yttervegg*"`. Et navn med `*` treffer alle elementer det passer på, for eksempel `navn = "Yttervegg*"`.
@@ -310,6 +320,19 @@ Mangler en bygningsdel U-verdi i modellen, sier importen hvilke det gjelder, og
bruker en typisk verdi for byggeåret. Det gjelder også vinduer og dører, som i de fleste bruker en typisk verdi for byggeåret. Det gjelder også vinduer og dører, som i de fleste
eksportene ikke har U-verdi. eksportene ikke har U-verdi.
Importen tåler noen vanlige feil i eksporten:
- **Deler som ligger dobbelt.** Legger eksporten den samme veggen eller det samme dekket i
flere etasjer, telles den én gang. Av kopiene brukes den som hører til den etasjen høyden
tilsier, og vinduene i de andre finner fortsatt veggen sin.
- **Feil etasje.** Hva som er gulv mot grunnen, dekke over kjelleren eller etasjeskiller inne
i bygningen, avgjøres av høyden geometrien har, ikke av hvilken etasje delen er lagt i.
- **Terrengmodell utenfor bakkenivå.** Ligger terrenget over gulvet i første etasje, brukes
det ikke, og importen sier fra. Oppgi da `over_terreng` på kjellerveggene.
Importen sier også hvilke deler som ikke er regnet med i klimaskjermen, som innervegger og
etasjeskillere, slik at det er lett å se om noe mangler.
Med de åtte feltene på plass er importen fra IFC nær nok komplett til at beregningen står på Med de åtte feltene på plass er importen fra IFC nær nok komplett til at beregningen står på
modellen alene. modellen alene.
@@ -382,7 +405,7 @@ jobs:
"$GITHUB_SERVER_URL/klingenbergbygg/enok/releases/download/$TAG/enok" "$GITHUB_SERVER_URL/klingenbergbygg/enok/releases/download/$TAG/enok"
chmod +x enok chmod +x enok
./enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml ./enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml
- uses: https://gitea.com/actions/upload-artifact@v4 - uses: https://gitea.com/actions/upload-artifact@v3
with: with:
name: bygningsmodell name: bygningsmodell
path: | path: |
@@ -396,7 +419,7 @@ jobs:
shell: fish {0} shell: fish {0}
steps: steps:
- uses: https://gitea.com/actions/checkout@v6 - uses: https://gitea.com/actions/checkout@v6
- uses: https://gitea.com/actions/download-artifact@v4 - uses: https://gitea.com/actions/download-artifact@v3
with: with:
name: bygningsmodell name: bygningsmodell
- name: Lag rapporten - name: Lag rapporten
+1
View File
@@ -596,6 +596,7 @@ fn bygg(
lufting: Lufting::default(), lufting: Lufting::default(),
drift: Drift::default(), drift: Drift::default(),
scenario: Vec::new(), scenario: Vec::new(),
rapport: Rapport::default(),
}; };
Ok(Importrapport { bygning, merknader }) Ok(Importrapport { bygning, merknader })
} }
+1
View File
@@ -833,6 +833,7 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
lufting: Lufting::default(), lufting: Lufting::default(),
drift: Drift::default(), drift: Drift::default(),
scenario: Vec::new(), scenario: Vec::new(),
rapport: Rapport::default(),
}; };
Ok(Importrapport { Ok(Importrapport {
bygning, bygning,
+1
View File
@@ -215,6 +215,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
lufting: Lufting::default(), lufting: Lufting::default(),
drift: Drift::default(), drift: Drift::default(),
scenario: Vec::new(), scenario: Vec::new(),
rapport: Rapport::default(),
}; };
Ok(Importrapport { bygning, merknader }) Ok(Importrapport { bygning, merknader })
} }
+20
View File
@@ -82,6 +82,26 @@ pub struct Bygning {
pub drift: Drift, pub drift: Drift,
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub scenario: Vec<Scenario>, pub scenario: Vec<Scenario>,
#[serde(default)]
pub rapport: Rapport,
}
/// Utseendet på rapporten, slik at den kan følge firmaets øvrige dokumenter.
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct Rapport {
/// Logo på forsiden. Filen kopieres til utdatakatalogen ved siden av rapporten.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logo: Option<PathBuf>,
/// Aksentfarge, for eksempel «#880000». Utelatt: enoks egen.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub farge: Option<String>,
/// Avsenderlinje nederst på hver side, for eksempel firma og organisasjonsnummer.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub avsender: Option<String>,
/// Skrift for overskrifter. Utelatt: enoks egen.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skrift_overskrift: Option<String>,
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
+49 -7
View File
@@ -66,6 +66,12 @@ fn rad(celler: &[String]) -> String {
pub fn skriv(r: &Resultat, katalog: &Path, scenarier: &[(String, Resultat)]) -> Result<()> { pub fn skriv(r: &Resultat, katalog: &Path, scenarier: &[(String, Resultat)]) -> Result<()> {
std::fs::create_dir_all(katalog)?; std::fs::create_dir_all(katalog)?;
std::fs::write(katalog.join("stil.typ"), STIL)?; std::fs::write(katalog.join("stil.typ"), STIL)?;
// Logoen følger med, slik at rapporten kan kompileres for seg selv senere. Mangler
// filen, lages rapporten uten den.
if let (Some(logo), Some(navn)) = (&r.bygning.rapport.logo, logo_navn(r)) {
std::fs::copy(logo, katalog.join(navn))
.with_context(|| format!("kopierer logoen {}", logo.display()))?;
}
std::fs::write(katalog.join("fig-maaned.svg"), svg::maanedsdiagram(r))?; std::fs::write(katalog.join("fig-maaned.svg"), svg::maanedsdiagram(r))?;
std::fs::write(katalog.join("fig-varighet.svg"), svg::varighetskurve(r))?; std::fs::write(katalog.join("fig-varighet.svg"), svg::varighetskurve(r))?;
std::fs::write(katalog.join("fig-karakter.svg"), svg::karakterstige(r))?; std::fs::write(katalog.join("fig-karakter.svg"), svg::karakterstige(r))?;
@@ -100,15 +106,32 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
let mut s = String::new(); let mut s = String::new();
s.push_str("#import \"stil.typ\": *\n\n"); s.push_str("#import \"stil.typ\": *\n\n");
s.push_str(&format!( let rap = &r.bygning.rapport;
"#show: oppsett.with(tittel: \"{} — energirapport\", bunntekst: \"{}\")\n\n", let mut valg = format!(
e(&l.navn), "tittel: \"{} — energirapport\", bunntekst: \"{}\"",
e(&bunntekst(r)) streng(&l.navn),
)); streng(&bunntekst(r))
);
if let Some(a) = &rap.avsender {
valg.push_str(&format!(", avsender: \"{}\"", streng(a)));
}
if let Some(f) = &rap.farge {
valg.push_str(&format!(", farge: rgb(\"{}\")", streng(f)));
}
if let Some(sk) = &rap.skrift_overskrift {
valg.push_str(&format!(", skrift: \"{}\"", streng(sk)));
}
s.push_str(&format!("#show: oppsett.with({valg})\n\n"));
// ── forside ───────────────────────────────────────────────────── // ── forside ─────────────────────────────────────────────────────
if let Some(navn) = logo_navn(r) {
s.push_str(&format!(
"#align(right, image(\"{}\", height: 1.7cm))\n\n#v(-0.9cm)\n",
streng(&navn)
));
}
s.push_str(&format!( s.push_str(&format!(
"#v(1.2cm)\n#text(size: 26pt, weight: \"bold\")[Energirapport]\n\n\ "#v(1.2cm)\n#forsidetittel[Energirapport]\n\n\
#v(-0.3em)\n#text(size: 15pt, fill: mid)[{}]\n\n", #v(-0.3em)\n#text(size: 15pt, fill: mid)[{}]\n\n",
e(&l.navn) e(&l.navn)
)); ));
@@ -473,7 +496,8 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
if l.forutsetninger.is_empty() { if l.forutsetninger.is_empty() {
s.push_str("Alle inndata er oppgitt i bygningsbeskrivelsen.\n\n"); s.push_str("Alle inndata er oppgitt i bygningsbeskrivelsen.\n\n");
} else { } else {
s.push_str("#tabell(\n kolonner: (auto, auto, 1fr),\n justering: (left, left, left),\n overskrift: (\"Felt\", \"Verdi\", \"Kilde\"),\n rader: (\n"); // Faste bredder, ellers spiser de to første kolonnene plassen kilden trenger.
s.push_str("#tabell(\n kolonner: (1.15fr, 0.85fr, 1.6fr),\n justering: (left, left, left),\n overskrift: (\"Felt\", \"Verdi\", \"Kilde\"),\n rader: (\n");
for f in &l.forutsetninger { for f in &l.forutsetninger {
s.push_str(&format!( s.push_str(&format!(
" {}\n", " {}\n",
@@ -539,6 +563,24 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
s s
} }
/// Tekst i en Typst-strengliteral: bare omvendt skråstrek og anførselstegn skal ut.
fn streng(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
/// Filnavnet på logoen, når den er oppgitt og finnes.
fn logo_navn(r: &Resultat) -> Option<String> {
let logo = r.bygning.rapport.logo.as_ref()?;
if !logo.exists() {
eprintln!(
" ! rapport.logo: fant ikke {}; rapporten lages uten logo.",
logo.display()
);
return None;
}
Some(logo.file_name()?.to_str()?.to_string())
}
fn bunntekst(r: &Resultat) -> String { fn bunntekst(r: &Resultat) -> String {
let b = &r.bygning.bygning; let b = &r.bygning.bygning;
let mut deler = Vec::new(); let mut deler = Vec::new();
+45 -8
View File
@@ -7,11 +7,32 @@
#let lys = rgb("#f4f1ee") #let lys = rgb("#f4f1ee")
#let kantlinje = rgb("#ddd4cc") #let kantlinje = rgb("#ddd4cc")
// Aksentfarge og overskriftsskrift kan settes av rapporten, slik at den kan følge
// firmaets øvrige dokumenter. Verdiene under er enoks egne.
#let aksent-valgt = state("aksent-valgt", aksent)
#let skrift-valgt = state("skrift-valgt", none)
#let font-heading = ("Libertinus Serif", "DejaVu Serif", "Noto Serif") #let font-heading = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
#let font-body = ("Libertinus Serif", "DejaVu Serif", "Noto Serif") #let font-body = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
#let font-tall = ("DejaVu Sans Mono", "Liberation Mono") #let font-tall = ("DejaVu Sans Mono", "Liberation Mono")
#let oppsett(tittel: "", undertittel: "", bunntekst: "", doc) = { /// Skriften rapporten har bedt om, ellers enoks egen.
#let overskriftsskrift() = {
let valgt = skrift-valgt.get()
if valgt == none { font-heading } else { (valgt,) + font-heading }
}
#let oppsett(
tittel: "",
undertittel: "",
bunntekst: "",
avsender: "",
farge: none,
skrift: none,
doc,
) = {
aksent-valgt.update(if farge == none { aksent } else { farge })
skrift-valgt.update(skrift)
set page( set page(
paper: "a4", paper: "a4",
margin: (top: 2.4cm, bottom: 2.4cm, left: 2.2cm, right: 2.2cm), margin: (top: 2.4cm, bottom: 2.4cm, left: 2.2cm, right: 2.2cm),
@@ -20,32 +41,36 @@
grid( grid(
columns: (1fr, auto), columns: (1fr, auto),
align: (left + horizon, right + horizon), align: (left + horizon, right + horizon),
text(font: font-heading, size: 8pt, fill: mid)[#tittel], text(font: overskriftsskrift(), size: 8pt, fill: mid)[#tittel],
text(font: font-body, size: 8pt, fill: mid)[#counter(page).display()], text(font: font-body, size: 8pt, fill: mid)[#counter(page).display()],
) )
line(length: 100%, stroke: 0.4pt + kantlinje) line(length: 100%, stroke: 0.4pt + kantlinje)
} }
}, },
footer: context { footer: context {
if counter(page).get().first() > 1 and bunntekst != "" { if counter(page).get().first() > 1 and (bunntekst != "" or avsender != "") {
line(length: 100%, stroke: 0.4pt + kantlinje) line(length: 100%, stroke: 0.4pt + kantlinje)
v(3pt) v(3pt)
if avsender != "" {
text(font: font-body, size: 7.5pt, fill: mid)[#avsender]
linebreak()
}
text(font: font-body, size: 7.5pt, fill: mid)[#bunntekst] text(font: font-body, size: 7.5pt, fill: mid)[#bunntekst]
} }
}, },
) )
set text(font: font-body, size: 10pt, fill: moerk, lang: "nb") set text(font: font-body, size: 10pt, fill: moerk, lang: "nb")
set par(justify: true, leading: 0.62em) set par(justify: true, leading: 0.62em)
show heading: it => { show heading: it => context {
set text(font: font-heading, fill: aksent) set text(font: overskriftsskrift(), fill: aksent-valgt.get())
block(above: 1.2em, below: 0.6em, it) block(above: 1.2em, below: 0.6em, it)
} }
show heading.where(level: 1): it => { show heading.where(level: 1): it => context {
set text(size: 15pt, weight: "bold") set text(size: 15pt, weight: "bold")
block(above: 1.6em, below: 0.7em)[ block(above: 1.6em, below: 0.7em)[
#it.body #it.body
#v(-0.45em) #v(-0.45em)
#line(length: 100%, stroke: 1pt + aksent) #line(length: 100%, stroke: 1pt + aksent-valgt.get())
] ]
} }
show heading.where(level: 2): set text(size: 11.5pt, weight: "bold") show heading.where(level: 2): set text(size: 11.5pt, weight: "bold")
@@ -55,7 +80,8 @@
} }
// Tabell med stripete rader og linje under overskriften. // Tabell med stripete rader og linje under overskriften.
#let tabell(kolonner: (), overskrift: (), rader: (), justering: none, sum-rader: ()) = { #let tabell(kolonner: (), overskrift: (), rader: (), justering: none, sum-rader: ()) = context {
let aksent = aksent-valgt.get()
let j = if justering == none { kolonner.map(_ => right) } else { justering } let j = if justering == none { kolonner.map(_ => right) } else { justering }
block(breakable: true, table( block(breakable: true, table(
columns: kolonner, columns: kolonner,
@@ -71,6 +97,16 @@
)) ))
} }
// Tittelen på forsiden, i samme skrift og farge som overskriftene.
#let forsidetittel(tekst) = context {
text(
font: overskriftsskrift(),
size: 26pt,
weight: "bold",
fill: aksent-valgt.get(),
)[#tekst]
}
// Stor karakterrute på forsiden. // Stor karakterrute på forsiden.
#let karakterrute(bokstav, verdi, farge) = { #let karakterrute(bokstav, verdi, farge) = {
box( box(
@@ -99,3 +135,4 @@
stroke: (left: 2pt + aksent-lys), stroke: (left: 2pt + aksent-lys),
text(size: 9pt)[#innhold], text(size: 9pt)[#innhold],
) )