Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b19162d9a7
|
||
|
|
c89561b0f5
|
||
|
|
e3f37afe36
|
||
|
|
cabd8500ea
|
Generated
+1
-1
@@ -222,7 +222,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "enok"
|
||||
version = "0.4.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "enok"
|
||||
version = "0.4.1"
|
||||
version = "0.4.3"
|
||||
edition = "2021"
|
||||
description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)"
|
||||
license = "MIT"
|
||||
|
||||
@@ -180,6 +180,16 @@ navn = "Nye vinduer"
|
||||
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
|
||||
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*"`.
|
||||
@@ -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
|
||||
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å
|
||||
modellen alene.
|
||||
|
||||
@@ -382,7 +405,7 @@ jobs:
|
||||
"$GITHUB_SERVER_URL/klingenbergbygg/enok/releases/download/$TAG/enok"
|
||||
chmod +x enok
|
||||
./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:
|
||||
name: bygningsmodell
|
||||
path: |
|
||||
@@ -396,7 +419,7 @@ jobs:
|
||||
shell: fish {0}
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v6
|
||||
- uses: https://gitea.com/actions/download-artifact@v4
|
||||
- uses: https://gitea.com/actions/download-artifact@v3
|
||||
with:
|
||||
name: bygningsmodell
|
||||
- name: Lag rapporten
|
||||
|
||||
@@ -596,6 +596,7 @@ fn bygg(
|
||||
lufting: Lufting::default(),
|
||||
drift: Drift::default(),
|
||||
scenario: Vec::new(),
|
||||
rapport: Rapport::default(),
|
||||
};
|
||||
Ok(Importrapport { bygning, merknader })
|
||||
}
|
||||
|
||||
@@ -833,6 +833,7 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
||||
lufting: Lufting::default(),
|
||||
drift: Drift::default(),
|
||||
scenario: Vec::new(),
|
||||
rapport: Rapport::default(),
|
||||
};
|
||||
Ok(Importrapport {
|
||||
bygning,
|
||||
|
||||
@@ -215,6 +215,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
lufting: Lufting::default(),
|
||||
drift: Drift::default(),
|
||||
scenario: Vec::new(),
|
||||
rapport: Rapport::default(),
|
||||
};
|
||||
Ok(Importrapport { bygning, merknader })
|
||||
}
|
||||
|
||||
@@ -82,6 +82,26 @@ pub struct Bygning {
|
||||
pub drift: Drift,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
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)]
|
||||
|
||||
+49
-7
@@ -66,6 +66,12 @@ fn rad(celler: &[String]) -> String {
|
||||
pub fn skriv(r: &Resultat, katalog: &Path, scenarier: &[(String, Resultat)]) -> Result<()> {
|
||||
std::fs::create_dir_all(katalog)?;
|
||||
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-varighet.svg"), svg::varighetskurve(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();
|
||||
|
||||
s.push_str("#import \"stil.typ\": *\n\n");
|
||||
s.push_str(&format!(
|
||||
"#show: oppsett.with(tittel: \"{} — energirapport\", bunntekst: \"{}\")\n\n",
|
||||
e(&l.navn),
|
||||
e(&bunntekst(r))
|
||||
));
|
||||
let rap = &r.bygning.rapport;
|
||||
let mut valg = format!(
|
||||
"tittel: \"{} — energirapport\", bunntekst: \"{}\"",
|
||||
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 ─────────────────────────────────────────────────────
|
||||
if let Some(navn) = logo_navn(r) {
|
||||
s.push_str(&format!(
|
||||
"#v(1.2cm)\n#text(size: 26pt, weight: \"bold\")[Energirapport]\n\n\
|
||||
"#align(right, image(\"{}\", height: 1.7cm))\n\n#v(-0.9cm)\n",
|
||||
streng(&navn)
|
||||
));
|
||||
}
|
||||
s.push_str(&format!(
|
||||
"#v(1.2cm)\n#forsidetittel[Energirapport]\n\n\
|
||||
#v(-0.3em)\n#text(size: 15pt, fill: mid)[{}]\n\n",
|
||||
e(&l.navn)
|
||||
));
|
||||
@@ -473,7 +496,8 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
||||
if l.forutsetninger.is_empty() {
|
||||
s.push_str("Alle inndata er oppgitt i bygningsbeskrivelsen.\n\n");
|
||||
} 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 {
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
@@ -539,6 +563,24 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
||||
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 {
|
||||
let b = &r.bygning.bygning;
|
||||
let mut deler = Vec::new();
|
||||
|
||||
+45
-8
@@ -7,11 +7,32 @@
|
||||
#let lys = rgb("#f4f1ee")
|
||||
#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-body = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
|
||||
#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(
|
||||
paper: "a4",
|
||||
margin: (top: 2.4cm, bottom: 2.4cm, left: 2.2cm, right: 2.2cm),
|
||||
@@ -20,32 +41,36 @@
|
||||
grid(
|
||||
columns: (1fr, auto),
|
||||
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()],
|
||||
)
|
||||
line(length: 100%, stroke: 0.4pt + kantlinje)
|
||||
}
|
||||
},
|
||||
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)
|
||||
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]
|
||||
}
|
||||
},
|
||||
)
|
||||
set text(font: font-body, size: 10pt, fill: moerk, lang: "nb")
|
||||
set par(justify: true, leading: 0.62em)
|
||||
show heading: it => {
|
||||
set text(font: font-heading, fill: aksent)
|
||||
show heading: it => context {
|
||||
set text(font: overskriftsskrift(), fill: aksent-valgt.get())
|
||||
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")
|
||||
block(above: 1.6em, below: 0.7em)[
|
||||
#it.body
|
||||
#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")
|
||||
@@ -55,7 +80,8 @@
|
||||
}
|
||||
|
||||
// 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 }
|
||||
block(breakable: true, table(
|
||||
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.
|
||||
#let karakterrute(bokstav, verdi, farge) = {
|
||||
box(
|
||||
@@ -99,3 +135,4 @@
|
||||
stroke: (left: 2pt + aksent-lys),
|
||||
text(size: 9pt)[#innhold],
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user