From a8eb4ce148822376cb3aed05ff69bb53538f5715 Mon Sep 17 00:00:00 2001 From: Bauti Date: Sun, 29 Mar 2026 19:39:16 -0300 Subject: [PATCH 01/17] chore: scaffold calc-report module for PR [2] --- web/src/lib/engine/calc-report.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 web/src/lib/engine/calc-report.ts diff --git a/web/src/lib/engine/calc-report.ts b/web/src/lib/engine/calc-report.ts new file mode 100644 index 00000000..07616dae --- /dev/null +++ b/web/src/lib/engine/calc-report.ts @@ -0,0 +1,13 @@ +/** + * Basic Structural Calc-Book Report Generator + * + * Generates a printable HTML report covering model data, loads, and analysis results. + * Works for both 2D and 3D Basic mode analysis. + * Uses Blob URL + browser print for PDF output (same pattern as pro-report.ts). + * + * This is the general-purpose structural report — distinct from the PRO verification report + * which focuses on code-check results and reinforcement details. + */ + +// TODO: Implementation in progress +export const CALC_REPORT_VERSION = '0.1.0'; From af8d4de3dd9c7b91c08f0f1cb52fdc709a5928c4 Mon Sep 17 00:00:00 2001 From: Bauti Date: Sun, 29 Mar 2026 20:01:45 -0300 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20calc-book=20report=20v1=20?= =?UTF-8?q?=E2=80=94=20model=20data,=20loads,=20reactions,=20displacements?= =?UTF-8?q?,=20forces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete HTML report generator with 5 sections: 1. Model Data — materials, sections, nodes, elements, supports (condensed for large models) 2. Loads — load cases, combinations with factors, applied load summary 3. Reactions — per-node reactions with equilibrium check (ΣF ≈ 0) 4. Displacements — max displacement summary box + sorted table (top 20 for large models) 5. Internal Forces — force summary + per-element start/end forces Both 2D (N/V/M) and 3D (N/Vy/Vz/Mx/My/Mz) results supported. Pre-report dialog for project name, engineer, company, notes. Print-friendly CSS with page breaks. Blob URL approach (same as PRO report). Report triggered from existing PDF button in ToolbarProject. i18n keys for en/es. --- web/src/components/CalcReportDialog.svelte | 187 ++++++++ .../components/toolbar/ToolbarProject.svelte | 9 +- web/src/lib/engine/calc-report.ts | 451 +++++++++++++++++- web/src/lib/i18n/locales/en.ts | 11 + web/src/lib/i18n/locales/es.ts | 11 + 5 files changed, 662 insertions(+), 7 deletions(-) create mode 100644 web/src/components/CalcReportDialog.svelte diff --git a/web/src/components/CalcReportDialog.svelte b/web/src/components/CalcReportDialog.svelte new file mode 100644 index 00000000..9960fac0 --- /dev/null +++ b/web/src/components/CalcReportDialog.svelte @@ -0,0 +1,187 @@ + + +{#if open} + +
open = false}> + +
e.stopPropagation()}> +

{t('calcReport.title')}

+
+ + + + +
+
+ + +
+
+
+{/if} + + diff --git a/web/src/components/toolbar/ToolbarProject.svelte b/web/src/components/toolbar/ToolbarProject.svelte index 26497112..7c432d9e 100644 --- a/web/src/components/toolbar/ToolbarProject.svelte +++ b/web/src/components/toolbar/ToolbarProject.svelte @@ -1,8 +1,11 @@ + +
+ +
+
+ + +
+ {#if summary} +
+ {summary.totalMembers} members + {statusIcon('ok')} {summary.pass} + {statusIcon('warn')} {summary.warn} + {statusIcon('fail')} {summary.fail} +
+ {/if} +
+ + {#if error} +
{error}
+ {/if} + + {#if !hasResults} +
Solve the model first to run design checks.
+ {:else if designResults.length === 0 && !error} +
Select a design code and click "Run Design Check" to verify members.
+ {:else} + +
+ + + + +
+ + +
+ + + + + + + + + + + + + + {#each filteredResults as r (r.elementId)} + + + + + + + + + + {/each} + +
ElemTypeSectionGoverning CheckUtilizationStatusCombo
{r.elementId}{r.elementType}{r.sectionName}{r.governingCheck} +
+ {fmtRatio(r.utilization)} +
+
+
+
+
{statusIcon(r.status)}{r.comboName ?? '—'}
+
+ {/if} +
+ + diff --git a/web/src/components/pro/ProPanel.svelte b/web/src/components/pro/ProPanel.svelte index e411f877..045901b1 100644 --- a/web/src/components/pro/ProPanel.svelte +++ b/web/src/components/pro/ProPanel.svelte @@ -22,6 +22,7 @@ import ProLoadsTab from './ProLoadsTab.svelte'; import ProResultsTab from './ProResultsTab.svelte'; import ProVerificationTab from './ProVerificationTab.svelte'; + import ProDesignTab from './ProDesignTab.svelte'; import ProShellTab from './ProShellTab.svelte'; import ProConstraintsTab from './ProConstraintsTab.svelte'; import ProAdvancedTab from './ProAdvancedTab.svelte'; @@ -30,7 +31,7 @@ import { checkModel } from '../../lib/engine/model-diagnostics'; import { get2DDisplayNodalLoadMoment, get2DDisplayNodalLoadVertical } from '../../lib/geometry/coordinate-system'; - type ProTab = 'nodes' | 'elements' | 'shells' | 'materials' | 'sections' | 'supports' | 'constraints' | 'loads' | 'advanced' | 'results' | 'verification' | 'connections' | 'diagnostics'; + type ProTab = 'nodes' | 'elements' | 'shells' | 'materials' | 'sections' | 'supports' | 'constraints' | 'loads' | 'advanced' | 'results' | 'design' | 'verification' | 'connections' | 'diagnostics'; // Group tabs into logical categories interface TabGroup { @@ -67,6 +68,7 @@ tabs: [ { id: 'advanced' as ProTab, label: t('pro.tabAdvanced') }, { id: 'results' as ProTab, label: t('pro.tabResults') }, + { id: 'design' as ProTab, label: t('pro.tabDesign') }, { id: 'verification' as ProTab, label: t('pro.tabVerification') }, { id: 'connections' as ProTab, label: t('pro.tabConnections') }, { id: 'diagnostics' as ProTab, label: t('pro.tabDiagnostics') }, @@ -689,6 +691,8 @@ {:else if activeTab === 'results'} + {:else if activeTab === 'design'} + {:else if activeTab === 'verification'} {:else if activeTab === 'connections'} diff --git a/web/src/lib/i18n/locales/en.ts b/web/src/lib/i18n/locales/en.ts index 35b14375..d0715cad 100644 --- a/web/src/lib/i18n/locales/en.ts +++ b/web/src/lib/i18n/locales/en.ts @@ -2282,6 +2282,7 @@ const en: Record = { 'pro.tabLoads': 'Loads', 'pro.tabAdvanced': 'Advanced', 'pro.tabResults': 'Results', + 'pro.tabDesign': 'Design', 'pro.tabVerification': 'Verification', 'pro.tabConnections': 'Conn.', 'pro.tabDiagnostics': 'Diag.', diff --git a/web/src/lib/i18n/locales/es.ts b/web/src/lib/i18n/locales/es.ts index e8d181f7..939fdd6a 100644 --- a/web/src/lib/i18n/locales/es.ts +++ b/web/src/lib/i18n/locales/es.ts @@ -2282,6 +2282,7 @@ const es: Record = { 'pro.tabLoads': 'Cargas', 'pro.tabAdvanced': 'Avanzado', 'pro.tabResults': 'Resultados', + 'pro.tabDesign': 'Diseño', 'pro.tabVerification': 'Verificación', 'pro.tabConnections': 'Conex.', 'pro.tabDiagnostics': 'Diag.', From 351de9271af45b03113c42e959a4686078fe6c9d Mon Sep 17 00:00:00 2001 From: Bauti Date: Tue, 31 Mar 2026 20:36:33 -0300 Subject: [PATCH 05/17] =?UTF-8?q?fix:=20add=20manual=20dismiss=20button=20?= =?UTF-8?q?(=C3=97)=20to=20toast=20notifications?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toasts now have a small × in the top-right corner for immediate dismissal. Auto-dismiss still works as before (4s/8s timeout). The dismiss button is semi-transparent and highlights on hover. Added dismissToast(id) to uiStore. --- web/src/App.svelte | 20 ++++++++++++++++++-- web/src/lib/store/ui.svelte.ts | 6 +++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/web/src/App.svelte b/web/src/App.svelte index 73f5d4fb..b1a91299 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -668,10 +668,11 @@
{toast.message} {#if toast.actionId === 'kinematic'} - {/if} +
{/each} @@ -1385,7 +1386,8 @@ } .toast { - padding: 0.6rem 1rem; + position: relative; + padding: 0.6rem 2rem 0.6rem 1rem; border-radius: 6px; font-size: 0.85rem; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); @@ -1395,6 +1397,20 @@ flex-direction: column; gap: 0.4rem; } + .toast-dismiss { + position: absolute; + top: 4px; + right: 6px; + background: none; + border: none; + color: inherit; + opacity: 0.5; + font-size: 1rem; + line-height: 1; + cursor: pointer; + padding: 0 2px; + } + .toast-dismiss:hover { opacity: 1; } .toast-action { align-self: flex-end; diff --git a/web/src/lib/store/ui.svelte.ts b/web/src/lib/store/ui.svelte.ts index d1dbae4b..94048561 100644 --- a/web/src/lib/store/ui.svelte.ts +++ b/web/src/lib/store/ui.svelte.ts @@ -580,7 +580,11 @@ function createUIStore() { setTimeout(() => { const idx = toasts.findIndex(t => t.id === id); if (idx >= 0) toasts.splice(idx, 1); - }, actionId ? 8000 : 4000); // Longer timeout when there's an action button + }, actionId ? 8000 : 4000); + }, + dismissToast(id: number) { + const idx = toasts.findIndex(t => t.id === id); + if (idx >= 0) toasts.splice(idx, 1); }, get liveCalc() { return liveCalc; }, From e9aa8d846009fde4d8c11018917bcfc59b937c73 Mon Sep 17 00:00:00 2001 From: Bauti Date: Fri, 3 Apr 2026 18:51:31 -0300 Subject: [PATCH 06/17] feat: PRO workflow overhaul, mobile support, loads/shells editing, calc report hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calc report: - empty-report guard, truthful result provenance, analysis-only metadata - report type, result basis, and contents block Saved-project restore: - mode-aware autosave with reactive banner that hides/shows on mode switch PRO toolbar: - grouped dropdown desktop toolbar (Geometry, Properties, Conditions, Analysis) - Pan/Select tools with subtype filtering - undo/redo buttons and Ctrl/Cmd+Z/Y shortcuts - expanded Analysis dropdown labels (Connections, Diagnostics) across all locales PRO selection/delete: - select-mode filtering for click and box selection - shells picking via raycasting shellsParent - loads picking with line threshold for ArrowHelper shafts - correct highlight sync using userData.id for loads - selectMode disambiguation for overlapping shell/element IDs - row-click selection sets selectMode to match entity type - tab-entry auto-aligns selectMode - delete respects selectMode for shell/element disambiguation PRO tables: - shells editable (material, thickness) with updatePlate/updateQuad - loads editable across all 3D load types including surface3d and thermalQuad3d - load cases as editable table rows with type selector, name input, load count - combination cards with per-factor rows (factor × case name) - self-weight surfaced as display-only D row with ON/OFF toggle - Lr type support in load-case selectors - load-case row selection highlights all loads using that case - consistent row hover/selected/cursor affordance across all data tabs - selection bar separates shells from elements, shows supports and loads 3D viewport: - measure tool node snapping via direct raycast + screen-space proximity - shell selection with opacity boost (0.45 → 0.85) for legibility - load hover/selection highlight with visual feedback 7-story RC example: - English load-case names - wind combinations corrected to 1.6W (U4-U7) Mobile PRO: - upper toolbar with Pan/Undo/Redo/Results-Solve/Select - persistent Select sub-mode strip - floating Results/Solve panel (upper-left, shared with Basic via mode gate) - mode selector as styled dropdown with Beta labels - examples dialog full-width on mobile with text wrapping - floating Results/Solve box hidden in Education/PRO (Basic-only) i18n: - shells/loads status labels - PRO loads tab labels (Type, Name, Loads, ON/OFF, auto, etc.) --- web/src/App.svelte | 435 +++++++++++++++--- web/src/components/CalcReportDialog.svelte | 47 +- web/src/components/MobileResultsPanel.svelte | 22 +- web/src/components/StatusBar.svelte | 27 +- web/src/components/Toolbar.svelte | 28 +- web/src/components/Viewport3D.svelte | 325 ++++++++++--- web/src/components/pro/ProDesignTab.svelte | 2 +- web/src/components/pro/ProElementsTab.svelte | 6 +- web/src/components/pro/ProLoadsTab.svelte | 265 +++++++---- web/src/components/pro/ProNodesTab.svelte | 14 +- web/src/components/pro/ProPanel.svelte | 58 ++- web/src/components/pro/ProResultsTab.svelte | 16 +- web/src/components/pro/ProShellTab.svelte | 42 +- web/src/components/pro/ProSupportsTab.svelte | 5 +- .../components/pro/ProVerificationTab.svelte | 3 +- web/src/lib/engine/calc-report.ts | 60 ++- web/src/lib/i18n/locales/de.ts | 4 +- web/src/lib/i18n/locales/en.ts | 20 +- web/src/lib/i18n/locales/es.ts | 20 +- web/src/lib/i18n/locales/fr.ts | 4 +- web/src/lib/i18n/locales/id.ts | 4 +- web/src/lib/i18n/locales/it.ts | 4 +- web/src/lib/i18n/locales/pt.ts | 4 +- web/src/lib/i18n/locales/ru.ts | 4 +- web/src/lib/i18n/locales/tr.ts | 2 +- web/src/lib/store/file.ts | 4 + web/src/lib/store/model.svelte.ts | 25 + web/src/lib/store/ui.svelte.ts | 13 +- .../templates/fixtures/pro-edificio-7p.json | 30 +- web/src/lib/viewport3d/scene-sync.ts | 36 +- 30 files changed, 1215 insertions(+), 314 deletions(-) diff --git a/web/src/App.svelte b/web/src/App.svelte index b1a91299..e6ff08d8 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -133,7 +133,7 @@ // the target mode's model (or start empty if first visit to that mode). import type { ModelSnapshot } from './lib/store/history.svelte'; const modeSnapshots = new Map(); - let currentAppMode: AppMode = typeof window !== 'undefined' ? pathToMode(location.pathname) : 'basico'; + let currentAppMode = $state(typeof window !== 'undefined' ? pathToMode(location.pathname) : 'basico'); function switchAppMode(target: AppMode) { const prev = currentAppMode; @@ -162,6 +162,7 @@ resultsStore.showReactions = false; } else { uiStore.analysisMode = 'pro'; + uiStore.includeSelfWeight = true; resultsStore.showReactions = false; resultsStore.showConstraintForces = false; } @@ -178,22 +179,26 @@ // Derive showResults from whether results exist — no manual management needed const showResults = $derived(resultsStore.results !== null || resultsStore.results3D !== null); - let showAutosaveBanner = $state(false); let showImportDialog = $state(false); let importText = $state(''); let autosaveData = $state>(null); + /** True once the user has explicitly Restored or Discarded the pending save. */ + let autosaveDismissed = $state(false); let autosaveInterval: ReturnType | null = null; - // Keep in sync with selected locale - $effect(() => { - document.documentElement.lang = t('file.htmlLang'); + /** Banner visibility: autosave exists, mode matches, user hasn't dismissed, + * and the user hasn't started editing a different project. */ + const showAutosaveBanner = $derived.by(() => { + if (!autosaveData || autosaveDismissed) return false; + const savedMode = autosaveData.appMode ?? 'basico'; + if (savedMode !== currentAppMode) return false; + if (modelStore.nodes.size > 0 && modelStore.model.name !== autosaveData.name) return false; + return true; }); + // Keep in sync with selected locale $effect(() => { - if (!showAutosaveBanner || !autosaveData) return; - if (modelStore.nodes.size > 0 && modelStore.model.name !== autosaveData.name) { - showAutosaveBanner = false; - } + document.documentElement.lang = t('file.htmlLang'); }); function restoreAutosave() { @@ -202,12 +207,12 @@ modelStore.model.name = autosaveData.name; resultsStore.clear(); } - showAutosaveBanner = false; + autosaveDismissed = true; } function discardAutosave() { clearLocalStorage(); - showAutosaveBanner = false; + autosaveDismissed = true; } @@ -235,6 +240,78 @@ importText = ''; } + function handleProKeydown(e: KeyboardEvent) { + if (uiStore.appMode !== 'pro') return; + // Skip if focus is in an input/textarea/select + const tag = (e.target as HTMLElement)?.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; + + // Ctrl/Cmd+Z: Undo + const key = e.key.toUpperCase(); + if ((e.ctrlKey || e.metaKey) && key === 'Z' && !e.shiftKey) { + e.preventDefault(); + historyStore.undo(); + return; + } + // Ctrl/Cmd+Y or Ctrl/Cmd+Shift+Z: Redo + if ((e.ctrlKey || e.metaKey) && (key === 'Y' || (key === 'Z' && e.shiftKey))) { + e.preventDefault(); + historyStore.redo(); + return; + } + + if (e.key === 'Delete' || e.key === 'Backspace') { + if (uiStore.selectedSupports.size > 0) { + const sups = [...uiStore.selectedSupports]; + modelStore.batch(() => { for (const id of sups) modelStore.removeSupport(id); }); + uiStore.clearSelectedSupports(); + resultsStore.clear(); + return; + } + if (uiStore.selectedLoads.size > 0) { + const indices = [...uiStore.selectedLoads].sort((a, b) => b - a); + modelStore.batch(() => { + for (const idx of indices) { + const load = modelStore.loads[idx]; + if (load) modelStore.removeLoad(load.data.id); + } + }); + uiStore.clearSelectedLoads(); + resultsStore.clear(); + return; + } + if (uiStore.selectedNodes.size > 0 || uiStore.selectedElements.size > 0) { + const nodes = [...uiStore.selectedNodes]; + const elems = [...uiStore.selectedElements]; + const shellMode = uiStore.selectMode === 'shells'; + modelStore.batch(() => { + for (const id of nodes) modelStore.removeNode(id); + for (const id of elems) { + const isShell = modelStore.plates.has(id) || modelStore.quads.has(id); + const isElem = modelStore.elements.has(id); + if (isShell && isElem) { + // Ambiguous ID — use selectMode to decide + if (shellMode) { + if (modelStore.plates.has(id)) modelStore.removePlate(id); + else modelStore.removeQuad(id); + } else { + modelStore.removeElement(id); + } + } else if (isShell) { + if (modelStore.plates.has(id)) modelStore.removePlate(id); + else modelStore.removeQuad(id); + } else if (isElem) { + modelStore.removeElement(id); + } + } + }); + uiStore.clearSelection(); + resultsStore.clear(); + return; + } + } + } + function handleExportPNG() { const canvas = document.querySelector('.viewport-container canvas') as HTMLCanvasElement | null; if (canvas) downloadCanvasPNG(canvas); @@ -246,6 +323,7 @@ uiStore.analysisMode = 'edu'; } else if (currentAppMode === 'pro') { uiStore.analysisMode = 'pro'; + uiStore.includeSelfWeight = true; } else { uiStore.analysisMode = '2d'; } @@ -310,12 +388,16 @@ currentAppMode = uiStore.appMode; replaceAppUrl(currentAppMode, modelStore.model.name); autosaveData = null; - showAutosaveBanner = false; } - autosaveData = loadFromLocalStorage(); - if (!savedWorkspace && autosaveData && autosaveData.snapshot.nodes.length > 0) { - showAutosaveBanner = true; + // Load autosave data if no workspace was restored. + // Banner visibility is derived — it checks mode match, dismiss state, + // and whether the user has started editing a different project. + if (!savedWorkspace) { + const loaded = loadFromLocalStorage(); + if (loaded && loaded.snapshot.nodes.length > 0) { + autosaveData = loaded; + } } } @@ -405,6 +487,21 @@ let proExBtnEl = $state(undefined); let proSettingsOpen = $state(false); + // PRO toolbar dropdown state + type ProDropdown = null | 'select' | 'geometry' | 'properties' | 'conditions' | 'analysis'; + let openDropdown = $state(null); + + function toggleDropdown(dd: ProDropdown) { + openDropdown = openDropdown === dd ? null : dd; + } + + /** Close dropdown when clicking outside the toolbar. */ + function handleProBarClickOutside(e: MouseEvent) { + if (openDropdown && !(e.target as HTMLElement)?.closest('.pro-bar')) { + openDropdown = null; + } + } + function startProResize(e: MouseEvent) { e.preventDefault(); const startX = e.clientX; @@ -438,6 +535,8 @@ } + + {#if showLanding} {/if} @@ -449,13 +548,21 @@ Stabileo -
- - - -
+ {#if uiStore.isMobile} + + {:else} +
+ + + +
+ {/if} | @@ -505,18 +612,90 @@ {/if} {#if uiStore.appMode === 'pro' && !uiStore.isMobile} -