From 3e721d4b41a559b9717c639898f493e2c8629294 Mon Sep 17 00:00:00 2001 From: Maarten Vroegindeweij <30430941+DutchSailor@users.noreply.github.com> Date: Fri, 22 May 2026 09:13:11 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feat(core):=20#break,=20|=E2=86=92to,=20?= =?UTF-8?q?=E2=89=A4=E2=89=A5=E2=89=A1=E2=89=A0,=20vector=20index=20.(expr?= =?UTF-8?q?)/.i,=20iterative=20solvers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the engine in line with real-world CalcPAD usage: - `#break` is now a true loop-exit (BreakNode in AST, scope-flag in evaluator). The Intertek 2259 foundation calc now produces the same pile-type choices [5, 8, 4, 5] as the CalcPAD PDF, which previously iterated all 9 attempts and ended on type 9. - `expr | unit` is now interpreted as the CalcPAD target-unit operator → mathjs `expr to unit`. Eliminated the NaN cascade in `p_ground`. - `≤ ≥ ≡ ≠` symbols normalized to `<= >= == !=` for ALL expressions (was conditional-only). - Vector-index `name.(expr)` and `name.i` (single-letter index) now rewrite to `name[expr]` / `name[i]`. Fixes the 5.1 drawFloor schema where `aaa1.i` / `aaa1.(i+1)` were folded to a single identifier. - Iterative solver directives lifted into helper functions: `$Find{f(x) @ x = lo:hi}` → bisection `$Root{...}` → alias for $Find `$Solve{f(x) @ x = guess}` → Newton-Raphson `$Sup{f(x) @ x = lo:hi}` → golden-section max `$Inf{f(x) @ x = lo:hi}` → golden-section min Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/core/src/evaluator.ts | 83 +++++++++++++++++++++++++++++++++ packages/core/src/parser.ts | 84 ++++++++++++++++++++++++++++++++-- packages/core/src/types.ts | 7 +++ 3 files changed, 171 insertions(+), 3 deletions(-) diff --git a/packages/core/src/evaluator.ts b/packages/core/src/evaluator.ts index d63158a..8fe8f6f 100644 --- a/packages/core/src/evaluator.ts +++ b/packages/core/src/evaluator.ts @@ -179,6 +179,71 @@ math.import( { override: true }, ); +// ── Iterative numerical solvers (used by parser's $Find/$Solve/$Sup/$Inf +// rewrite). The `fn` argument is a mathjs user-defined function in scope; +// we call it via `Number(fn(x))` and run a robust scalar algorithm. +math.import( + { + /** Bisection root-finder for f(x) = 0 in [lo, hi]. */ + _find_root: function (fn: unknown, lo: unknown, hi: unknown) { + if (typeof fn !== 'function') return Number.NaN; + const evalAt = (x: number): number => Number((fn as (n: number) => unknown)(x)); + let a = Number(lo); let b = Number(hi); + if (a > b) [a, b] = [b, a]; + let fa = evalAt(a); let fb = evalAt(b); + if (!isFinite(fa) || !isFinite(fb)) return Number.NaN; + if (fa === 0) return a; + if (fb === 0) return b; + if (fa * fb > 0) return Number.NaN; + for (let i = 0; i < 80; i++) { + const c = (a + b) / 2; + const fc = evalAt(c); + if (Math.abs(fc) < 1e-12 || (b - a) < 1e-14) return c; + if (fa * fc < 0) { b = c; fb = fc; } else { a = c; fa = fc; } + } + return (a + b) / 2; + }, + /** Newton-Raphson root-finder for f(x) = 0 starting from `guess`. */ + _solve_newton: function (fn: unknown, guess: unknown) { + if (typeof fn !== 'function') return Number.NaN; + const evalAt = (x: number): number => Number((fn as (n: number) => unknown)(x)); + let x = Number(guess); + const h = 1e-7; + for (let i = 0; i < 60; i++) { + const f = evalAt(x); + if (!isFinite(f)) return Number.NaN; + if (Math.abs(f) < 1e-12) return x; + const fp = (evalAt(x + h) - f) / h; + if (!isFinite(fp) || Math.abs(fp) < 1e-15) break; + const dx = f / fp; + x = x - dx; + if (Math.abs(dx) < 1e-12) return x; + } + return x; + }, + /** Golden-section extremum over [lo, hi]; sign=+1 → sup, -1 → inf. */ + _extremum: function (fn: unknown, lo: unknown, hi: unknown, sign: unknown) { + if (typeof fn !== 'function') return Number.NaN; + const s = Number(sign) >= 0 ? 1 : -1; + const evalAt = (x: number): number => s * Number((fn as (n: number) => unknown)(x)); + const phi = (Math.sqrt(5) - 1) / 2; + let a = Number(lo); let b = Number(hi); + if (a > b) [a, b] = [b, a]; + let c = b - (b - a) * phi; + let d = a + (b - a) * phi; + for (let i = 0; i < 80; i++) { + if (evalAt(c) > evalAt(d)) b = d; else a = c; + c = b - (b - a) * phi; + d = a + (b - a) * phi; + if ((b - a) < 1e-14) break; + } + const xOpt = (a + b) / 2; + return Number((fn as (n: number) => unknown)(xOpt)); + }, + }, + { override: true }, +); + export interface Scope { [key: string]: unknown; } @@ -197,10 +262,16 @@ export function evaluate(nodes: AstNode[], selectValues?: SelectValues): Evaluat return evaluateNodes(nodes, scope, selectValues || {}); } +/** Sentinel key on `scope` used by `#break` to short-circuit out of a loop. */ +const BREAK_FLAG = '__break__'; + function evaluateNodes(nodes: AstNode[], scope: Scope, selectValues: SelectValues): EvaluatedNode[] { const result: EvaluatedNode[] = []; for (const node of nodes) { + // `#break` raised by a deeper node — stop evaluating siblings. The + // enclosing `repeat` case picks up the flag and exits the loop. + if (scope[BREAK_FLAG]) break; switch (node.type) { case 'heading': if (node.hidden) break; @@ -300,6 +371,12 @@ function evaluateNodes(nodes: AstNode[], scope: Scope, selectValues: SelectValue break; } + case 'break': { + // Set the break flag — the enclosing repeat case consumes it. + scope[BREAK_FLAG] = true; + break; + } + case 'repeat': { let count = 0; try { @@ -315,6 +392,12 @@ function evaluateNodes(nodes: AstNode[], scope: Scope, selectValues: SelectValue scope['_i'] = iter; const children = evaluateNodes(node.body, scope, selectValues); if (!node.hidden) result.push(...children); + if (scope[BREAK_FLAG]) { + // Iteration stopped — consume the flag so outer loops aren't + // also broken out of. + delete scope[BREAK_FLAG]; + break; + } } break; } diff --git a/packages/core/src/parser.ts b/packages/core/src/parser.ts index fed19a5..c5514d8 100644 --- a/packages/core/src/parser.ts +++ b/packages/core/src/parser.ts @@ -92,6 +92,7 @@ const BREAK_RE = /^#break\b/; // Match `$Plot{...}` even when CalcPAD has appended trailing persisted-input // data after the closing brace (e.g. `}\v2\t3`). const PLOT_RE = /^\$Plot\s*\{([^}]+)\}/; +const SOLVER_RE = /\$(Find|Root|Solve|Sup|Inf)\s*\{([^}]+)\}/g; const TRAILING_DATA_RE = /^[\s\d.eE+\-,;]+$/; // pure numeric/whitespace line // HTML wrappers CalcPAD uses around conditions / equation fragments. @@ -364,12 +365,25 @@ function foldIdentifierDots(source: string): string { // attribute values must survive unchanged. const transformOutsideQuotes = (text: string): string => { let out = text; + // CalcPAD vector index `name.(expr)` → `name[expr]`. Must precede dot-fold + // so the parenthesised index isn't accidentally read as a member access. + out = out.replace( + /(? `${name}[${expr}]`, + ); // CalcPAD vector index `name.digit` → `name[digit]`. Must precede the // identifier-cluster fold so `cc.3` isn't mistakenly read as `cc_3`. out = out.replace( /(? `${name}[${idx}]`, ); + // CalcPAD vector index by loop-variable: `name.i`, `name.j` (single + // lowercase letter) → `name[i]`. Distinguishes from dotted identifiers + // like `Cs.Cd` (uppercase / multi-char RHS) which still get folded. + out = out.replace( + /(? `${name}[${idx}]`, + ); // Identifier-cluster fold: `Cs.Cd`, `F_0.9G50%TotalWeight` → underscores. out = out.replace( /(? { + const atIdx = inner.lastIndexOf('@'); + if (atIdx === -1) return match; + const body = inner.slice(0, atIdx).trim(); + const param = inner.slice(atIdx + 1).trim(); + const eqIdx = param.indexOf('='); + if (eqIdx === -1) return match; + const varName = param.slice(0, eqIdx).trim(); + const range = param.slice(eqIdx + 1).trim(); + counter += 1; + const fnName = `__solver_${counter}`; + defs.push(`${fnName}(${varName}) = ${body}`); + if (op === 'Solve') { + return `_solve_newton(${fnName}, ${range})`; + } + const colonIdx = range.indexOf(':'); + if (colonIdx === -1) return match; + const lo = range.slice(0, colonIdx).trim(); + const hi = range.slice(colonIdx + 1).trim(); + if (op === 'Find' || op === 'Root') return `_find_root(${fnName}, ${lo}, ${hi})`; + if (op === 'Sup') return `_extremum(${fnName}, ${lo}, ${hi}, 1)`; + if (op === 'Inf') return `_extremum(${fnName}, ${lo}, ${hi}, -1)`; + return match; + }); + for (const d of defs) out.push(d); + out.push(newLine); + } + return out.join('\n'); +} + function normalizeExpression(expr: string): string { return expr .replace(/\bsqr\b/g, 'sqrt') .replace(/\blg\b/g, 'log10') .replace(/π/g, 'pi') + .replace(/≡/g, '==') + .replace(/≠/g, '!=') + .replace(/≥/g, '>=') + .replace(/≤/g, '<=') + // CalcPAD `expr|unit` = target-unit conversion → mathjs `expr to unit`. + // Runs after matrix-rewrite so `[a;b|c;d]` matrices are already + // converted to nested arrays before this point. + .replace(/\|(\s*[\p{L}\p{N}_/\s^*-]+)$/u, ' to $1') .replace(/;/g, ','); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3521ca0..b2c73a8 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -154,6 +154,12 @@ export interface GefUploadNode { hidden?: boolean; } +/** CalcPAD `#break` — early exit from the enclosing #repeat / #for loop. */ +export interface BreakNode { + type: 'break'; + hidden?: boolean; +} + export type AstNode = | HeadingNode | TextNode @@ -165,6 +171,7 @@ export type AstNode = | RepeatNode | PlotNode | SvgNode + | BreakNode | ImageNode | SelectNode | GefUploadNode; From 2368be178213554ad16e2603733a1e3fd37cc47d Mon Sep 17 00:00:00 2001 From: Maarten Vroegindeweij <30430941+DutchSailor@users.noreply.github.com> Date: Fri, 22 May 2026 09:13:22 +0200 Subject: [PATCH 2/4] style(preview): CalcPAD-look formules + geen horizontale slider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `unitPartToLatex` herschreven zonder mathjs round-trip — geen leading "1" meer voor units. `235 N/mm²` ipv `235 1 N/mm²`. - `.calc-line` heeft niet meer de grijze achtergrond + blauwe linker- streep. Foutregels behouden hun rode visuele indicator. - `.calc-preview-content` zet `overflow-x: hidden` + `overflow-wrap: anywhere`. Lange KaTeX-formules wrappen naar de volgende regel ipv een horizontale scrollbar te tonen. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/core/src/latex.ts | 31 +++++++++++-------- packages/core/src/renderer.ts | 15 ++++----- .../desktop/src/components/calc/Preview.css | 16 +++++++++- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/packages/core/src/latex.ts b/packages/core/src/latex.ts index 3c66929..5e77c39 100644 --- a/packages/core/src/latex.ts +++ b/packages/core/src/latex.ts @@ -262,20 +262,25 @@ export function resultToLatex(numStr: string, unitStr: string): string { return `${num} \\; ${unitPartToLatex(unitStr)}`; } -/** Format a unit string like "N / mm^2" as LaTeX */ +/** Format a unit string like "N / mm^2" or "kN m" directly as LaTeX. */ export function unitPartToLatex(unitStr: string): string { - // Handle compound units like "N / mm^2" or "kN m" - // Parse as a mathjs expression to get proper LaTeX - try { - // Wrap in "1 unit" so mathjs parses it as a unit expression - const node = parse(`1 ${unitStr}`); - const latex = nodeToLatex(node); - // Remove the leading "1 \;" from the result - return latex.replace(/^1\s*\\[;,]\s*/, '').replace(/^1\s+/, ''); - } catch { - // Fallback: simple text rendering - return `\\text{${unitStr.replace(/\^(\d+)/g, '}^{$1}\\text{')}}`; - } + // Direct converter — no mathjs round-trip (which prepends `1~` that's + // hard to strip reliably). Tokens are identifiers optionally followed by + // `^exp`. `/` introduces a denominator; space is implicit multiplication. + const fmtToken = (tok: string): string => { + const m = tok.match(/^([\p{L}_][\p{L}\p{N}_]*)(?:\^(-?\d+))?$/u); + if (!m) return `\\mathrm{${tok}}`; + if (!m[2]) return `\\mathrm{${m[1]}}`; + return `{\\mathrm{${m[1]}}}^{${m[2]}}`; + }; + const fmtGroup = (s: string): string => + s.trim().split(/\s+/).filter(Boolean).map(fmtToken).join('\\,'); + + const parts = unitStr.split('/').map((s) => s.trim()).filter(Boolean); + if (parts.length === 0) return ''; + if (parts.length === 1) return fmtGroup(parts[0]); + // a/b/c → a / (b·c) + return `\\frac{${fmtGroup(parts[0])}}{${parts.slice(1).map(fmtGroup).join('\\,')}}`; } /** Format a variable name as LaTeX */ diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index 85c9cc9..b525b3c 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -289,12 +289,11 @@ export const defaultStyles = ` } .calc-line { - background: #f8fafc; - border-left: 3px solid #3b82f6; - padding: 0.4em 1.2em; - margin: 0.35em 0; - border-radius: 0 6px 6px 0; - overflow-x: auto; + /* CalcPAD-stijl: geen achtergrond / linker streep — gewoon de formule. */ + padding: 0.15em 0; + margin: 0.25em 0; + /* Verbergt overflow zonder slider; KaTeX past zich via .calc-line .katex aan. */ + overflow-x: hidden; } .calc-line .katex-display { @@ -312,8 +311,10 @@ export const defaultStyles = ` } .calc-error { - border-left-color: #dc2626; + /* Fout-regels krijgen wel een lichte rood-tint zodat ze opvallen. */ background: #fef2f2; + border-left: 3px solid #dc2626; + padding-left: 0.6em; } .calc-error-msg { diff --git a/packages/desktop/src/components/calc/Preview.css b/packages/desktop/src/components/calc/Preview.css index ad6a107..201fb5b 100644 --- a/packages/desktop/src/components/calc/Preview.css +++ b/packages/desktop/src/components/calc/Preview.css @@ -11,10 +11,24 @@ flex: 1 1 0; min-height: 0; overflow-y: auto; - overflow-x: auto; + /* Geen horizontale scrollbalk — content moet wrappen of clippen. */ + overflow-x: hidden; padding: 24px 32px; font-family: var(--font-body); color: var(--theme-text); + /* Lange formules / tekst breken naar de volgende regel ipv te scrollen. */ + overflow-wrap: anywhere; + word-break: break-word; +} + +/* KaTeX display-blokken: laat ze wrappen ipv één lange regel te forceren. */ +.calc-preview-content .calc-line, +.calc-preview-content .katex-display { + max-width: 100%; +} + +.calc-preview-content .katex-display { + overflow-x: hidden; } .calc-preview-content::-webkit-scrollbar { From 3c0fc0b24d4a59098bdce05292dbc825d2b3c7a2 Mon Sep 17 00:00:00 2001 From: Maarten Vroegindeweij <30430941+DutchSailor@users.noreply.github.com> Date: Fri, 22 May 2026 09:13:39 +0200 Subject: [PATCH 3/4] feat(desktop): Project + Library secties, projectgegevens, gevelkolom, windverband MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProjectBrowser splitst nu in twee secties met section-headers: • PROJECT (boven): klikbare projectgegevens + de calc-sheets • LIBRARY (onder): Books / Standards / CalcPAD-voorbeelden Nieuwe sheets onder Project: • Projectgegevens — Eurocode-uitgangspunten (CC-klasse, K_FI, windgebied, terreincategorie, sneeuwbelasting, grondsoort, geotechnische categorie) via @select dropdowns. • Stalen gevelkolom (wind + N + kip) — alle 42 IPE/HEA, matrix- lookup van h, b, t_w, t_f, A, W_el,y, I_y. Toets buiging §6.2.5, dwarskracht §6.2.6, druk §6.2.4, kip §6.3.2.4 (vereen- voudigde slankheidsmethode met n_kipsteunen), gecombineerd §6.2.9, en BGT-verplaatsing §7.2. Schema + M/V-lijn SVG. • Verticaal windverband — trekstaaftoetsing (strip of hoeklijn). Schema is een rechthoek + kruis (XZ-aanzicht) met krachtdriehoek die de wind ontbindt in F_h, F_v en F_t,Ed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/calc/ProjectBrowser.css | 44 +++ .../src/components/calc/ProjectBrowser.tsx | 28 +- .../src/components/calc/projectTree.ts | 61 +++- packages/desktop/src/templates/index.ts | 6 + .../desktop/src/templates/projectMetadata.ts | 109 ++++++ .../desktop/src/templates/stalenGevelkolom.ts | 313 ++++++++++++++++++ .../src/templates/verticaalWindverband.ts | 226 +++++++++++++ 7 files changed, 766 insertions(+), 21 deletions(-) create mode 100644 packages/desktop/src/templates/projectMetadata.ts create mode 100644 packages/desktop/src/templates/stalenGevelkolom.ts create mode 100644 packages/desktop/src/templates/verticaalWindverband.ts diff --git a/packages/desktop/src/components/calc/ProjectBrowser.css b/packages/desktop/src/components/calc/ProjectBrowser.css index a601d0e..f35f51b 100644 --- a/packages/desktop/src/components/calc/ProjectBrowser.css +++ b/packages/desktop/src/components/calc/ProjectBrowser.css @@ -210,3 +210,47 @@ .project-browser-tree::-webkit-scrollbar-thumb:hover { background: var(--theme-accent, #D97706); } + +/* ─── Sections (Project / Library) ───────────────────────────── */ + +.tree-section { + margin: 8px 0 4px; +} + +.tree-section + .tree-section { + margin-top: 16px; + border-top: 1px solid var(--theme-border-subtle); + padding-top: 8px; +} + +.tree-section-header { + display: flex; + align-items: center; + padding: 4px 10px 6px; +} + +.tree-section-label { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.8px; + text-transform: uppercase; + color: var(--theme-accent, #D97706); + opacity: 0.85; +} + +.tree-section-children { + display: flex; + flex-direction: column; +} + +.tree-item-emphasis { + font-weight: 600; + color: var(--theme-text); + background: var(--theme-accent-soft); + border-radius: 4px; + margin: 2px 6px; +} + +.tree-item-emphasis:hover { + background: var(--theme-hover); +} diff --git a/packages/desktop/src/components/calc/ProjectBrowser.tsx b/packages/desktop/src/components/calc/ProjectBrowser.tsx index fd0b711..c5fb140 100644 --- a/packages/desktop/src/components/calc/ProjectBrowser.tsx +++ b/packages/desktop/src/components/calc/ProjectBrowser.tsx @@ -13,9 +13,30 @@ interface TreeProps { function TreeNodeView({ node, level, selectedId, onSelect }: TreeProps) { const [expanded, setExpanded] = useState( - node.kind === "category" ? !!node.defaultExpanded : false, + node.kind === "category" ? !!node.defaultExpanded : node.kind === "section", ); + if (node.kind === "section") { + return ( +
+
+ {node.label} +
+
+ {node.children.map((child) => ( + + ))} +
+
+ ); + } + if (node.kind === "category") { return (
0 ? " tree-subcategory" : ""}`}> @@ -47,14 +68,15 @@ function TreeNodeView({ node, level, selectedId, onSelect }: TreeProps) { const isSelected = selectedId === node.id; const hasTemplate = !!node.templateId; + const isEmphasis = node.kind === "item" && node.emphasis; return ( ); diff --git a/packages/desktop/src/components/calc/projectTree.ts b/packages/desktop/src/components/calc/projectTree.ts index 57c31e8..199143e 100644 --- a/packages/desktop/src/components/calc/projectTree.ts +++ b/packages/desktop/src/components/calc/projectTree.ts @@ -1,20 +1,53 @@ /** - * Project browser tree — hierarchical organization of every template - * (Eurocodes, Vandepitte books, project calcs) the user can drop into the editor. + * Project browser tree — sidebar voor de editor. + * + * Twee secties, gescheiden door een `section` node: + * • PROJECT (boven) — projectgegevens + de calc-sheets van dit project + * • LIBRARY (onder) — referentie-materiaal (boeken, NEN-EN, CalcPAD samples) * * `templateId` matches a key in `src/templates/index.ts`. */ export type TreeNode = + | { kind: "section"; id: string; label: string; children: TreeNode[] } | { kind: "category"; id: string; label: string; defaultExpanded?: boolean; children: TreeNode[]; count?: number } - | { kind: "item"; id: string; label: string; templateId?: string }; + | { kind: "item"; id: string; label: string; templateId?: string; emphasis?: boolean }; + +/** + * Calc-sheets binnen het huidige project. Voor nu hardcoded; later vervangen + * door dynamische projectstaat (persisted per project file). + */ +const projectSheets: TreeNode[] = [ + { + kind: "item", + id: "project-metadata", + label: "📋 Projectgegevens", + templateId: "project-metadata", + emphasis: true, + }, + { kind: "item", id: "sheet-stalen-gevelkolom", label: "Stalen gevelkolom (wind + N)", templateId: "stalen-gevelkolom" }, + { kind: "item", id: "sheet-verticaal-windverband", label: "Verticaal windverband", templateId: "verticaal-windverband" }, + { kind: "item", id: "sheet-paaldraagvermogen", label: "Paaldraagvermogen", templateId: "paaldraagvermogen" }, + { kind: "item", id: "sheet-stalen-ligger", label: "Stalen ligger IPE 300", templateId: "stalen-ligger" }, +]; export const projectTree: TreeNode[] = [ + { + kind: "section", + id: "project", + label: "Project", + children: projectSheets, + }, + { + kind: "section", + id: "library", + label: "Library", + children: [ { kind: "category", id: "books", label: "Books", - defaultExpanded: true, + defaultExpanded: false, count: 8, children: [ { kind: "item", id: "book-bijlage-a", label: "Constructieberekening Bijlage A" }, @@ -130,25 +163,15 @@ export const projectTree: TreeNode[] = [ }, ], }, - { - kind: "category", - id: "calculations", - label: "Calculations", - defaultExpanded: true, - count: 3, - children: [ - { kind: "item", id: "calc-calcpad-demo", label: "CalcPAD syntax demo", templateId: "calcpad-demo" }, - { kind: "item", id: "calc-paaldraagvermogen", label: "Paaldraagvermogen", templateId: "paaldraagvermogen" }, - { kind: "item", id: "calc-stalen-ligger", label: "Stalen ligger IPE 300", templateId: "stalen-ligger" }, - ], - }, { kind: "category", id: "calcpad-samples", label: "CalcPAD voorbeelden", - defaultExpanded: true, - count: 10, + defaultExpanded: false, + count: 11, children: [ + { kind: "item", id: "cpd-2259-intertek", label: "2259 Intertek units (real-world)", templateId: "cpd-2259-intertek" }, + { kind: "item", id: "cpd-calcpad-demo", label: "CalcPAD syntax demo", templateId: "calcpad-demo" }, { kind: "item", id: "cpd-quadratic", label: "Quadratic Equation", templateId: "cpd-quadratic" }, { kind: "item", id: "cpd-cubic", label: "Cubic Equation", templateId: "cpd-cubic" }, { kind: "item", id: "cpd-lissajous", label: "Lissajous Curve", templateId: "cpd-lissajous" }, @@ -161,4 +184,6 @@ export const projectTree: TreeNode[] = [ { kind: "item", id: "cpd-deep-beam", label: "Deep Beam (Elastic)", templateId: "cpd-deep-beam" }, ], }, + ], + }, ]; diff --git a/packages/desktop/src/templates/index.ts b/packages/desktop/src/templates/index.ts index f469076..ef3e0d2 100644 --- a/packages/desktop/src/templates/index.ts +++ b/packages/desktop/src/templates/index.ts @@ -1,6 +1,9 @@ import { paalExample, exampleDoc } from "./examples"; import { calcpadDemo } from "./calcpad-demo"; import { calcpadSamples } from "./calcpad-samples"; +import { projectMetadata } from "./projectMetadata"; +import { stalenGevelkolom } from "./stalenGevelkolom"; +import { verticaalWindverband } from "./verticaalWindverband"; import { ec5Buiging, ec5Afschuiving, ec5Druk, ec5DrukLoodrecht, ec5Knik, ec5Doorbuiging, ec5HoutenBalk, @@ -32,6 +35,9 @@ import { } from "./en1992"; export const templates: Record = { + "project-metadata": projectMetadata, + "stalen-gevelkolom": stalenGevelkolom, + "verticaal-windverband": verticaalWindverband, "calcpad-demo": calcpadDemo, ...calcpadSamples, "paaldraagvermogen": paalExample, diff --git a/packages/desktop/src/templates/projectMetadata.ts b/packages/desktop/src/templates/projectMetadata.ts new file mode 100644 index 0000000..ffb1708 --- /dev/null +++ b/packages/desktop/src/templates/projectMetadata.ts @@ -0,0 +1,109 @@ +/** + * Project metadata sheet — vult de Eurocode-uitgangspunten in die door alle + * berekeningen binnen het project gebruikt worden. Gevolgklasse, wind-, + * sneeuwgebied, ontwerplevensduur etc. komen voort uit NEN-EN 1990 + NB. + */ + +export const projectMetadata = `"Project metadata +'Vul hieronder de algemene projectgegevens en Eurocode-uitgangspunten in. Deze +'waarden worden in alle berekeningen binnen dit project gebruikt. + +'

1. Projectgegevens

+ +#hide +project_nummer = 0 +project_naam = "Nieuw project" +opdrachtgever = "Onbekend" +constructeur = "Onbekend" +locatie = "Onbekend" +#show + +' +' +' +' +' +' +'
Projectnummer'project_nummer'
Projectnaam'project_naam'
Opdrachtgever'opdrachtgever'
Constructeur'constructeur'
Locatie'locatie'
+ +'

2. Constructieve uitgangspunten (NEN-EN 1990 + NB)

+ +@select CC "Gevolgklasse" + CC1 — beperkte gevolgen = 1 + CC2 — middelmatige gevolgen = 2 + CC3 — grote gevolgen = 3 +@end + +@select RC "Betrouwbaarheidsklasse" + RC1 = 1 + RC2 = 2 + RC3 = 3 +@end + +@select DesignLife "Ontwerplevensduur" + 10 jaar (tijdelijk) = 10 + 25 jaar = 25 + 50 jaar (standaard) = 50 + 100 jaar (monumenten/infra) = 100 +@end + +'

KFI — gevolgklasse factor (Tabel NB.A1.1)

+#if CC ≡ 1 + K_FI = 0.9 +#else if CC ≡ 2 + K_FI = 1.0 +#else + K_FI = 1.1 +#end if + +'KFI = 'K_FI + +'

3. Klimatologische uitgangspunten (NEN-EN 1991 + NB)

+ +@select WindGebied "Windgebied (NB)" + Gebied I (kust) = 1 + Gebied II = 2 + Gebied III (binnenland) = 3 +@end + +@select Terrein "Terreincategorie" + 0 — open zee = 0 + I — meer/vlakte = 1 + II — open gebied = 2 + III — bebouwd gebied = 3 + IV — stedelijk gebied = 4 +@end + +@select SneeuwBelasting "Sneeuwbelasting (kN/m²)" + s_k = 0.56 (NL standaard) = 0.56 + s_k = 0.70 (verhoogd) = 0.70 +@end + +s_k = SneeuwBelasting kN/m^2 +'Karakteristieke sneeuwbelasting: sk = 's_k + +'

4. Geotechnische uitgangspunten (NEN 9997-1)

+ +@select GrondType "Grondsoort" + Zand, vast = 1 + Zand, los = 2 + Klei, stijf = 3 + Klei, slap = 4 + Veen = 5 + Op aanvraag = 6 +@end + +@select GeoCat "Geotechnische categorie" + GC1 — eenvoudig = 1 + GC2 — standaard = 2 + GC3 — complex = 3 +@end + +'

5. Notities

+'Hier kun je vrije notities, scope-afbakeningen en projectspecifieke +'aandachtspunten kwijt. + +'
+'Bij wijziging van bovenstaande uitgangspunten moeten alle gekoppelde +'rekensheets opnieuw worden uitgevoerd om de update mee te nemen. +`; diff --git a/packages/desktop/src/templates/stalenGevelkolom.ts b/packages/desktop/src/templates/stalenGevelkolom.ts new file mode 100644 index 0000000..3c8d3e5 --- /dev/null +++ b/packages/desktop/src/templates/stalenGevelkolom.ts @@ -0,0 +1,313 @@ +/** + * Toetsing van een stalen gevelkolom op windbelasting + normaalkracht + + * kip volgens EN 1993-1-1 §6.2 + §6.3.2.4 (simplified slenderness method). + * + * Profielen 1–18: IPE 80 t/m IPE 600 + * Profielen 19–42: HEA 100 t/m HEA 1000 + * + * Doorsnede-eigenschappen volgens "Stahl im Hochbau" / steelconstruction.info, + * eenheden hieronder in mm / mm² / mm³ / mm⁴. + */ + +export const stalenGevelkolom = `"Stalen gevelkolom — toetsing wind + N + kip + +'Toetsing van een enkelvoudig opgelegde stalen gevelkolom belast door +'winddruk + axiaal, inclusief kip via de vereenvoudigde slankheidsmethode +'(EN 1993-1-1 §6.3.2.4). + +# 1. Profielkeuze + +@select profile "Staalprofiel" + IPE 80 = 1 + IPE 100 = 2 + IPE 120 = 3 + IPE 140 = 4 + IPE 160 = 5 + IPE 180 = 6 + IPE 200 = 7 + IPE 220 = 8 + IPE 240 = 9 + IPE 270 = 10 + IPE 300 = 11 + IPE 330 = 12 + IPE 360 = 13 + IPE 400 = 14 + IPE 450 = 15 + IPE 500 = 16 + IPE 550 = 17 + IPE 600 = 18 + HEA 100 = 19 + HEA 120 = 20 + HEA 140 = 21 + HEA 160 = 22 + HEA 180 = 23 + HEA 200 = 24 + HEA 220 = 25 + HEA 240 = 26 + HEA 260 = 27 + HEA 280 = 28 + HEA 300 = 29 + HEA 320 = 30 + HEA 340 = 31 + HEA 360 = 32 + HEA 400 = 33 + HEA 450 = 34 + HEA 500 = 35 + HEA 550 = 36 + HEA 600 = 37 + HEA 650 = 38 + HEA 700 = 39 + HEA 800 = 40 + HEA 900 = 41 + HEA 1000 = 42 +@end + +@select staalkwaliteit "Staalkwaliteit" + S235 = 235 + S275 = 275 + S355 = 355 +@end + +f_y = staalkwaliteit N/mm^2 +γ_M0 = 1.0 +γ_M1 = 1.0 + +#hide +'Profielmatrix — kolomvolgorde: +' 1: id 2: h(mm) 3: b(mm) 4: t_w(mm) +' 5: t_f(mm) 6: A(mm²) 7: W_el,y(mm³) 8: I_y(mm⁴) +profiles = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; 17; 18; 19; 20; 21; 22; 23; 24; 25; 26; 27; 28; 29; 30; 31; 32; 33; 34; 35; 36; 37; 38; 39; 40; 41; 42 |80; 100; 120; 140; 160; 180; 200; 220; 240; 270; 300; 330; 360; 400; 450; 500; 550; 600; 96; 114; 133; 152; 171; 190; 210; 230; 250; 270; 290; 310; 330; 350; 390; 440; 490; 540; 590; 640; 690; 790; 890; 990 |46; 55; 64; 73; 82; 91; 100; 110; 120; 135; 150; 160; 170; 180; 190; 200; 210; 220; 100; 120; 140; 160; 180; 200; 220; 240; 260; 280; 300; 300; 300; 300; 300; 300; 300; 300; 300; 300; 300; 300; 300; 300 |3.8; 4.1; 4.4; 4.7; 5.0; 5.3; 5.6; 5.9; 6.2; 6.6; 7.1; 7.5; 8.0; 8.6; 9.4; 10.2; 11.1; 12.0; 5.0; 5.0; 5.5; 6.0; 6.0; 6.5; 7.0; 7.5; 7.5; 8.0; 8.5; 9.0; 9.5; 10.0; 11.0; 11.5; 12.0; 12.5; 13.0; 13.5; 14.5; 15.0; 16.0; 16.5 |5.2; 5.7; 6.3; 6.9; 7.4; 8.0; 8.5; 9.2; 9.8; 10.2; 10.7; 11.5; 12.7; 13.5; 14.6; 16.0; 17.2; 19.0; 8.0; 8.0; 8.5; 9.0; 9.5; 10.0; 11.0; 12.0; 12.5; 13.0; 14.0; 15.5; 16.5; 17.5; 19.0; 21.0; 23.0; 24.0; 25.0; 26.0; 27.0; 28.0; 30.0; 31.0 |764; 1030; 1320; 1640; 2010; 2390; 2850; 3340; 3910; 4590; 5380; 6260; 7270; 8450; 9880; 11600; 13400; 15600; 2120; 2530; 3140; 3880; 4530; 5380; 6430; 7680; 8680; 9730; 11300; 12400; 13300; 14300; 15900; 17800; 19800; 21200; 22600; 24200; 26000; 28600; 32000; 34700 |20000; 34200; 53000; 77300; 109000; 146000; 194000; 252000; 324000; 429000; 557000; 713000; 904000; 1156000; 1500000; 1928000; 2441000; 3069000; 73000; 106000; 156000; 220000; 294000; 389000; 515000; 675000; 836000; 1013000; 1260000; 1479000; 1678000; 1891000; 2311000; 2896000; 3550000; 4146000; 4787000; 5474000; 6241000; 7682000; 9485000; 11190000 |801000; 1710000; 3180000; 5410000; 8690000; 13170000; 19430000; 27720000; 38920000; 57900000; 83560000; 117700000; 162700000; 231300000; 337400000; 482000000; 671200000; 920800000; 3490000; 6060000; 10330000; 16730000; 25100000; 36920000; 54100000; 77630000; 104500000; 136700000; 182600000; 229300000; 276900000; 330900000; 450700000; 637200000; 869700000; 1119000000; 1412000000; 1752000000; 2153000000; 3034000000; 4221000000; 5538000000] + +h = hlookup(profiles; profile; 1; 2)*mm +b_profile = hlookup(profiles; profile; 1; 3)*mm +t_w = hlookup(profiles; profile; 1; 4)*mm +t_f = hlookup(profiles; profile; 1; 5)*mm +A = hlookup(profiles; profile; 1; 6)*mm^2 +W_el,y = hlookup(profiles; profile; 1; 7)*mm^3 +I_y = hlookup(profiles; profile; 1; 8)*mm^4 +#show + +'
Gekozen profieleigenschappen
+h +b_profile +t_w +t_f +A +W_el,y +I_y + +# 2. Geometrie en belastingbreedte + +L = ?*(m)', verdiepingshoogte (m) — kniklengte van de kolom' +b_belast = ?*(m)', belastingbreedte (m) — c.t.c. tussen kolommen' +n_kipsteunen = ?', aantal tussenliggende kipsteunen (regels/vloeren)' + +L_LT = L/(n_kipsteunen + 1)', afstand tussen kipsteunen — lengte voor kipcontrole' + +# 3. Windbelasting + +q_wind = ?*(kN/m^2)', wind-rekenwaarde uit project-uitgangspunten' + +q_line = q_wind*b_belast', lijnlast op de kolom (kN/m)' + +# 4. Optredende krachten + +N_Ed = ?*(kN)', axiale rekenwaarde — drukkracht door bovenliggende verdiepingen' +M_Ed = q_line*L^2/8', buigend moment in het midden bij gelijkmatige lijnlast' +V_Ed = q_line*L/2', dwarskracht aan de oplegging' + +# 5. Schema en M/V-lijnen + +'
5.1 Belastingsschema
+#hide +q_arrows_n = 8', aantal q-pijltjes in het schema +q_x_left = 60 +q_x_top = 40 +q_y_top = q_x_top + 0 +q_y_bottom = 360 +col_x = 200 +arrow_len = 80 +#show +' +' +' +' +' +#for i = 0 : q_arrows_n +' +' +#loop +' +' q = 'q_line' +' L = 'L' +' N = 'N_Ed' +' +' +' bbelast = 'b_belast' +'' + +'
5.2 M-lijn (buigend moment) en V-lijn (dwarskracht)
+#hide +dia_x_left = 50 +dia_x_right = 430 +dia_mid = (dia_x_left + dia_x_right)/2 +mline_y_base = 100 +mline_y_max = 50 +vline_y_base = 280 +vline_y_max = 50 +#show +' +' M-lijn +' +' +' +' +' MEd = 'M_Ed' +' parabool — max in het midden +' V-lijn +' +' +' +' +' +' +VEd = 'V_Ed' +' −VEd = 'V_Ed' +'' + +# 6. Toetsing buiging — §6.2.5 + +M_c,Rd = W_el,y*f_y/γ_M0' (elastische weerstand om y-as) +UC_M = M_Ed/M_c,Rd + +#if UC_M ≤ 1.0 + 'UCM = 'UC_M = M_Ed/M_c,Rd' ≤ 1.0 → Voldoet +#else + 'UCM = 'UC_M = M_Ed/M_c,Rd' > 1.0 → Voldoet NIET +#end if + +# 7. Toetsing dwarskracht — §6.2.6 + +A_v = A - 2*b_profile*t_f + (t_w + 2*0)*t_f', schuifvlak (vereenvoudigd voor I-profielen) +V_pl,Rd = A_v*(f_y/sqrt(3))/γ_M0 +UC_V = V_Ed/V_pl,Rd + +#if UC_V ≤ 1.0 + 'UCV = 'UC_V = V_Ed/V_pl,Rd' ≤ 1.0 → Voldoet +#else + 'UCV = 'UC_V = V_Ed/V_pl,Rd' > 1.0 → Voldoet NIET +#end if + +# 8. Toetsing druk — §6.2.4 + +N_c,Rd = A*f_y/γ_M0 +UC_N = N_Ed/N_c,Rd + +#if UC_N ≤ 1.0 + 'UCN = 'UC_N = N_Ed/N_c,Rd' ≤ 1.0 → Voldoet +#else + 'UCN = 'UC_N = N_Ed/N_c,Rd' > 1.0 → Voldoet NIET +#end if + +# 9. Kipcontrole — §6.3.2.4 (vereenvoudigde slankheidsmethode) + +'Vereenvoudigde methode voor de kniklengte tussen de kipsteunen. +'kc = 0,94 (uniform belaste, simpel opgelegde lengte tussen steunen, Tabel 6.6). +'kfl = 1,10 (verlaagde drukflens — Aanbevolen waarde NB). + +#hide +ε = sqrt(235 N/mm^2/f_y) +λ_1 = 93.9*ε +A_f = b_profile*t_f', flensoppervlak (compressie flens) +A_w_eff = (h - 2*t_f)*t_w/3', 1/3 van het web bijdragend aan de "T" +i_f,z = sqrt((b_profile^3*t_f/12)/(A_f + A_w_eff))', traagheidsstraal van flens+1/3web om z-as +k_c = 0.94 +k_fl = 1.10 +λ_c0 = 0.5 +#show + +'LLT = L / (nkipsteunen + 1) = 'L_LT' (afstand tussen kipsteunen)' + +i_f,z +λ_f = (k_c*L_LT)/(i_f,z*λ_1)', §6.3.2.4 (5) +'λ̄f = 'λ_f' (slankheid drukflens)' + +#if λ_f ≤ λ_c0 + 'λ̄f ≤ λ̄c0 = 0,5 → geen kipcontrole nodig: Mb,Rd = Mc,Rd + M_b,Rd = M_c,Rd +#else + 'λ̄f > λ̄c0 = 0,5 → kipcontrole nodig + #if h/b_profile ≤ 2 + α_LT = 0.34', buigingsknik-kromme b (Tabel 6.5, h/b ≤ 2) + #else + α_LT = 0.49', buigingsknik-kromme c (Tabel 6.5, h/b > 2) + #end if + φ_LT = 0.5*(1 + α_LT*(λ_f - 0.2) + λ_f^2) + χ_LT = 1/(φ_LT + sqrt(φ_LT^2 - λ_f^2)) + #if χ_LT > 1.0 + χ_LT = 1.0 + #end if + 'χLT = 'χ_LT + M_b,Rd = k_fl*χ_LT*W_el,y*f_y/γ_M1 +#end if + +UC_LT = M_Ed/M_b,Rd +#if UC_LT ≤ 1.0 + 'UCkip = MEd/Mb,Rd = 'UC_LT' ≤ 1.0 → Voldoet +#else + 'UCkip = MEd/Mb,Rd = 'UC_LT' > 1.0 → Voldoet NIET +#end if + +# 10. Verplaatsingstoets (BGT) — §7.2 + NEN-EN 1993-1-1 NB Tabel B.1 + +'Toetsing van horizontale verplaatsing onder karakteristieke windbelasting +'(BGT karakteristieke combinatie). Voor gevelelementen is de gangbare grens +'δ ≤ L/300; voor algemene constructies δ ≤ L/250. + +E = 210000 N/mm^2', E-modulus staal' + +q_wind,kar = q_wind/1.5', terug naar karakteristieke wind (γ_Q,wind = 1.5)' +q_kar,line = q_wind,kar*b_belast + +δ_max = 5*q_kar,line*L^4/(384*E*I_y)', maximale doorbuiging midden bij UDL' + +@select VerplGrens "Verplaatsingsgrens" + L/300 (gevel) = 300 + L/250 (algemeen) = 250 + L/200 (ruimer) = 200 +@end + +δ_lim = L/VerplGrens + +UC_δ = δ_max/δ_lim + +#if UC_δ ≤ 1.0 + 'UCδ = δmaxlim = 'UC_δ' ≤ 1.0 → Voldoet +#else + 'UCδ = δmaxlim = 'UC_δ' > 1.0 → Voldoet NIET +#end if + +# 11. Gecombineerde toetsing — §6.2.9 + §6.3.3 (vereenvoudigd, klasse 3) + +UC_combi = N_Ed/N_c,Rd + M_Ed/M_b,Rd + +#if UC_combi ≤ 1.0 + 'UCM+N+kip = 'UC_combi' ≤ 1.0 → Voldoet +#else + 'UCM+N+kip = 'UC_combi' > 1.0 → Voldoet NIET +#end if + +'
+'Vereenvoudigingen in deze toetsing: +'
    +'
  • Kniklengte = staaflengte L (geen aparte knikcontrole §6.3.1) — voor een +'gevelkolom met c.t.c. vloer is de kniklengte meestal gelijk aan de +'verdiepingshoogte.
  • +'
  • Kipcontrole via §6.3.2.4 vereenvoudigde slankheidsmethode i.p.v. de +'gedetailleerde §6.3.2.2 Mcr-berekening. Conservatief voor +'rolprofielen onder gelijkmatig belasting.
  • +'
  • Dwarskrachtreductie van de momentcapaciteit (§6.2.8) is verwaarloosd +'omdat UCV meestal < 0.5 blijft.
  • +'
  • Elastische weerstand (klasse 3) — voor klasse 1/2 kun je Wpl,y gebruiken +'in plaats van Wel,y voor circa 10–15% extra capaciteit.
  • +'
+`; diff --git a/packages/desktop/src/templates/verticaalWindverband.ts b/packages/desktop/src/templates/verticaalWindverband.ts new file mode 100644 index 0000000..8c7fb82 --- /dev/null +++ b/packages/desktop/src/templates/verticaalWindverband.ts @@ -0,0 +1,226 @@ +/** + * Toetsing van een verticaal windverband (vertical bracing) — trek-only. + * + * Conform praktijk: een windverband-diagonaal werkt als trekstaaf. Het + * profiel is meestal een strip (platte staal) of een hoeklijn. De + * "drukdiagonaal" in een X-kruis wordt verwaarloosd (slap). + * + * Toets: §6.2.3 trek + net cross-section bij boutaansluiting + §7.2 BGT. + */ + +export const verticaalWindverband = `"Verticaal windverband — trekstaaftoetsing + +'Toetsing van de trekstaaf (strip of hoeklijn) in een verticaal windverband. +'De getrokken diagonaal voert de horizontale wind-shear af naar de fundering; +'de tegenovergestelde diagonaal in een X-kruis wordt slap en niet meegerekend. +'Toets volgens EN 1993-1-1 §6.2.3 (trek, bruto + netto doorsnede). + +# 1. Profielkeuze + +@select verbandtype "Verbandtype" + Enkele diagonaal = 1 + X-kruis (trek-only) = 2 +@end + +@select profile "Profiel (id)" + Strip 30×5 (A = 150 mm²) = 1 + Strip 40×5 (A = 200 mm²) = 2 + Strip 50×5 (A = 250 mm²) = 3 + Strip 60×5 (A = 300 mm²) = 4 + Strip 60×8 (A = 480 mm²) = 5 + Strip 60×10 (A = 600 mm²) = 6 + Strip 80×6 (A = 480 mm²) = 7 + Strip 80×8 (A = 640 mm²) = 8 + Strip 80×10 (A = 800 mm²) = 9 + Strip 100×8 (A = 800 mm²) = 10 + Strip 100×10 (A = 1000 mm²) = 11 + Strip 100×12 (A = 1200 mm²) = 12 + Strip 120×10 (A = 1200 mm²) = 13 + Strip 120×12 (A = 1440 mm²) = 14 + Strip 150×12 (A = 1800 mm²) = 15 + L 30×30×3 (A = 175 mm²) = 16 + L 40×40×4 (A = 308 mm²) = 17 + L 50×50×5 (A = 480 mm²) = 18 + L 50×50×6 (A = 569 mm²) = 19 + L 60×60×6 (A = 691 mm²) = 20 + L 60×60×8 (A = 905 mm²) = 21 + L 70×70×7 (A = 940 mm²) = 22 + L 80×80×8 (A = 1230 mm²) = 23 + L 80×80×10 (A = 1510 mm²) = 24 + L 90×90×9 (A = 1550 mm²) = 25 + L 100×100×10 (A = 1920 mm²) = 26 + L 100×100×12 (A = 2270 mm²) = 27 + L 120×120×12 (A = 2750 mm²) = 28 + L 150×150×12 (A = 3480 mm²) = 29 + L 150×150×15 (A = 4300 mm²) = 30 +@end + +@select staalkwaliteit "Staalkwaliteit" + S235 = 235 + S275 = 275 + S355 = 355 +@end + +f_y = staalkwaliteit N/mm^2 +f_u = if(staalkwaliteit ≡ 235; 360; if(staalkwaliteit ≡ 275; 430; 510)) N/mm^2', treksterkte EN 10025' +γ_M0 = 1.0 +γ_M2 = 1.25', voor netto doorsnede met boutgat' + +#hide +'Profielmatrix: [id | A (mm²)] +profiles = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; 17; 18; 19; 20; 21; 22; 23; 24; 25; 26; 27; 28; 29; 30 |150; 200; 250; 300; 480; 600; 480; 640; 800; 800; 1000; 1200; 1200; 1440; 1800; 175; 308; 480; 569; 691; 940; 905; 1230; 1510; 1550; 1920; 2270; 2750; 3480; 4300] +A = hlookup(profiles; profile; 1; 2)*mm^2 +#show + +'
Gekozen profiel
+A + +# 2. Geometrie van de stabiliteitsbeun + +b = ?*(m)', breedte van de beun (h.o.h. tussen kolommen)' +h = ?*(m)', verdiepingshoogte van de beun' + +L_d = sqrt(b^2 + h^2)', diagonaal-systemlengte' +α = atan(h/b)', hoek van de diagonaal met de horizontale (rad)' +α_deg = α*180/pi +cos_α = b/L_d +sin_α = h/L_d + +# 3. Windkracht op de beun + +F_h = ?*(kN)', totale horizontale wind-rekenwaarde op deze beun (UGT)' + +# 4. Kracht in de getrokken diagonaal (ontbinding) + +#if verbandtype ≡ 1 + 'Enkele diagonaal — neemt de volledige horizontale kracht op + type_label = "enkele diagonaal" +#else + 'X-kruis trek-only — slechts één diagonaal werkt per windrichting + type_label = "X-kruis (trek-only)" +#end if + +'De horizontale wind Fh wordt langs de diagonaal ontbonden: +' Ft,Ed = Fh / cos α (langs de diagonaal) +' Fv = Ft,Ed · sin α (verticaal — drukt op kolom + fundering) + +F_t,Ed = F_h/cos_α', trekkracht in de werkende diagonaal' +F_v = F_t,Ed*sin_α', verticale reactiecomponent in de getrokken hoekpunten' + +# 5. Schema-tekening — aanzicht in XZ-vlak + +'Rechthoekige stabiliteitsbeun met kruis-windverband. Aan de top wordt +'Fh aangebracht (rood). De getrokken diagonaal (groen, dik) voert +'de kracht af; de tegenovergestelde diagonaal (lichtgrijs gestippeld) wordt +'slap. Rechts naast het kruis is het krachtdriehoekje getoond met de +'ontbinding Ft = √(Fh² + Fv²). + +#hide +sw = 260', breedte van het frame in svg-pixels +sh = 200', hoogte +x0 = 70 +x1 = x0 + sw +y0 = 40 +y1 = y0 + sh +mid_x = (x0 + x1)/2 +mid_y = (y0 + y1)/2 +'Krachtdriehoek rechts van de beun +tri_x = x1 + 50 +tri_y = mid_y +tri_w = 70 +tri_h = tri_w*sh/sw +#show +' +' +' +' +' +' +' +' +' +' Ft,Ed +' slap +' +' +' Fh +' b = 'b' +' h = 'h' +' type: 'type_label' · α = 'α_deg'° +' +' +' +' Fh +' +' +' Fv +' +' +' Ft,Ed +' +' Krachtdriehoek +' +' +' +' X +' +' +' Z +' +'' + +'Krachtwaarden uit de ontbinding: +' Fh = 'F_h' (toegepaste wind, horizontaal) +' Ft,Ed = Fh / cos α = 'F_t,Ed' +' Fv = Ft,Ed · sin α = 'F_v' + +# 6. Trekcontrole — §6.2.3 (bruto doorsnede) + +N_pl,Rd = A*f_y/γ_M0 +UC_t = F_t,Ed/N_pl,Rd + +#if UC_t ≤ 1.0 + 'UCt,bruto = Ft,Ed/Npl,Rd = 'UC_t' ≤ 1.0 → Voldoet +#else + 'UCt,bruto = Ft,Ed/Npl,Rd = 'UC_t' > 1.0 → Voldoet NIET +#end if + +# 7. Netto doorsnede bij boutaansluiting — §6.2.3 (2) + +'Verlaag de bruto-doorsnede met boutgaten. Bij excentrische aansluiting +'van een hoeklijn aan één been past §3.10.3 (effectieve netto doorsnede) +'extra reductiefactoren — niet in deze sheet. + +d_bout = ?*(mm)', boutgatdiameter (0 = geen bouten / volledig gelast)' +n_bouten = ?', aantal boutgaten in dezelfde dwarsdoorsnede' +t_aansluit = ?*(mm)', dikte op de aansluiting (strip-dikte of hoeklijn-been)' + +A_net = A - n_bouten*d_bout*t_aansluit +N_u,Rd = 0.9*A_net*f_u/γ_M2', §6.2.3 (2) netto-doorsnede met γ_M2' + +#if A_net ≤ 0 + 'Aantal/diameter bouten te groot: Anet ≤ 0 + UC_net = 9.99 +#else + UC_net = F_t,Ed/N_u,Rd + A_net + N_u,Rd + #if UC_net ≤ 1.0 + 'UCt,netto = Ft,Ed/Nu,Rd = 'UC_net' ≤ 1.0 → Voldoet + #else + 'UCt,netto = Ft,Ed/Nu,Rd = 'UC_net' > 1.0 → Voldoet NIET + #end if +#end if + +'
+'Vereenvoudigingen en aandachtspunten: +'
    +'
  • Trek-only ontwerp — drukcontrole en BGT-verplaatsing zijn bewust niet +'opgenomen. Geldig voor strippen en niet-stijve hoeklijnen die onder druk +'slap worden.
  • +'
  • Bij hoeklijn aangesloten op één been geldt §3.10.3 — effectieve netto +'doorsnede (β-factoren). Sheet rekent met volle Anet.
  • +'
  • Verbindingen niet getoetst — bouten op afschuiving, blok-shear, +'lasdoorsneden uit EN 1993-1-8.
  • +'
+`; From 8eff3dfae9a6ccdd10993605845ba4ac340bb144 Mon Sep 17 00:00:00 2001 From: Maarten Vroegindeweij <30430941+DutchSailor@users.noreply.github.com> Date: Fri, 22 May 2026 09:13:48 +0200 Subject: [PATCH 4/4] feat(desktop): persist source + load case values, bump to 0.1.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit documentStore (source + filePath) en loadCaseStore (cases + activeId + valuesByCase) hydrateren nu uit de Tauri-store bij app-boot en auto-saven debounced (250–500 ms) bij elke wijziging. Open je calc na herstart en je dropdown-/prompt-keuzes staan er weer. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/desktop/package.json | 2 +- packages/desktop/src-tauri/Cargo.toml | 2 +- packages/desktop/src-tauri/tauri.conf.json | 2 +- packages/desktop/src/store/documentStore.ts | 28 +++++++++++++++ packages/desktop/src/store/loadCaseStore.ts | 39 +++++++++++++++++++++ 5 files changed, 70 insertions(+), 3 deletions(-) diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 590a9b2..0611900 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@openaec/calculations-studio", "private": true, - "version": "0.1.3", + "version": "0.1.4", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 89336a0..6ba42d1 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "open-calculations-studio" -version = "0.1.3" +version = "0.1.4" description = "Open Calculations Studio - lightweight CalcPAD alternative for Eurocode verifications" authors = ["OpenAEC Contributors"] edition = "2021" diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json index 58321f3..ca38736 100644 --- a/packages/desktop/src-tauri/tauri.conf.json +++ b/packages/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Open Calculations Studio", - "version": "0.1.3", + "version": "0.1.4", "identifier": "studio.opencalculations.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/packages/desktop/src/store/documentStore.ts b/packages/desktop/src/store/documentStore.ts index ab499b3..de07343 100644 --- a/packages/desktop/src/store/documentStore.ts +++ b/packages/desktop/src/store/documentStore.ts @@ -1,6 +1,14 @@ import { create } from "zustand"; import type { SelectValues } from "@ifc-calc/core"; import { paalExample } from "../templates/examples"; +import { getSetting, setSetting } from "../store"; + +const STORE_KEY = "documentState"; + +interface PersistedDoc { + source: string; + filePath: string | null; +} interface DocumentState { source: string; @@ -27,3 +35,23 @@ export const useDocumentStore = create((set) => ({ markSaved: (filePath) => set({ filePath, dirty: false }), })); + +// Persistence — hydrate source + filePath, then auto-save (debounced) on +// each change. Survives app restart so the user picks up where they left off. +let docPersistTimer: ReturnType | null = null; +function scheduleDocPersist(snapshot: PersistedDoc) { + if (docPersistTimer) clearTimeout(docPersistTimer); + docPersistTimer = setTimeout(() => { + void setSetting(STORE_KEY, snapshot); + docPersistTimer = null; + }, 500); +} + +void getSetting(STORE_KEY, null).then((saved) => { + if (saved && typeof saved.source === "string") { + useDocumentStore.setState({ source: saved.source, filePath: saved.filePath ?? null, dirty: false }); + } + useDocumentStore.subscribe((s) => + scheduleDocPersist({ source: s.source, filePath: s.filePath }), + ); +}); diff --git a/packages/desktop/src/store/loadCaseStore.ts b/packages/desktop/src/store/loadCaseStore.ts index 6de4e6c..4787c29 100644 --- a/packages/desktop/src/store/loadCaseStore.ts +++ b/packages/desktop/src/store/loadCaseStore.ts @@ -1,5 +1,14 @@ import { create } from "zustand"; import type { SelectValues } from "@ifc-calc/core"; +import { getSetting, setSetting } from "../store"; + +const STORE_KEY = "loadCaseState"; + +interface Persisted { + cases: LoadCase[]; + activeId: string; + valuesByCase: Record; +} /** * Per-document `belastingsgeval` (load case) state. Each load case stores its @@ -38,6 +47,17 @@ interface LoadCaseState { setActiveValue: (name: string, value: string) => void; } +// Debounced persistence — coalesce rapid writes (e.g. while user types in a +// prompt input) into a single Tauri store update. +let persistTimer: ReturnType | null = null; +function schedulePersist(snapshot: Persisted) { + if (persistTimer) clearTimeout(persistTimer); + persistTimer = setTimeout(() => { + void setSetting(STORE_KEY, snapshot); + persistTimer = null; + }, 250); +} + export const useLoadCaseStore = create((set, get) => ({ cases: defaultCases, activeId: defaultCases[0].id, @@ -88,3 +108,22 @@ export const useLoadCaseStore = create((set, get) => ({ }; }), })); + +// Persistence — hydrate from Tauri store on first import, then auto-save on +// every mutation. Values survive app restart. +void getSetting(STORE_KEY, null).then((saved) => { + if (saved && saved.cases && saved.cases.length > 0) { + useLoadCaseStore.setState({ + cases: saved.cases, + activeId: saved.activeId ?? saved.cases[0].id, + valuesByCase: saved.valuesByCase ?? {}, + }); + } + useLoadCaseStore.subscribe((s) => + schedulePersist({ + cases: s.cases, + activeId: s.activeId, + valuesByCase: s.valuesByCase, + }), + ); +});