feat: unified 0 and 90 degree PDF envelope and category descriptors

This commit is contained in:
2026-07-08 19:52:34 +00:00
commit 9fece3f174
170 changed files with 27177 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
import { create } from 'zustand';
import {
calculateGlobalWindData,
type TerrainCategory,
type StructureClass,
} from '../lib/wind-kernel';
import { computeCpiSimplified, clampCpi, type PermeabilityCase } from '../lib/internal-pressure';
export interface GlobalWindState {
// Entradas (Inputs)
v0: number;
s1: number;
s3: number;
s3Group: 1 | 2 | 3 | 4 | 5;
terrainCategory: TerrainCategory;
largestDimension: number;
heightZ: number;
// Saídas (Calculados)
structureClass: StructureClass;
s2: number;
vk: number;
q: number;
// Pressão interna e Vento
windAngle: 0 | 90;
permeabilityCase: PermeabilityCase;
cpiRatio: number;
cpi: number;
// Ações
updateCalculations: () => void;
setV0: (val: number) => void;
setS1: (val: number) => void;
setS3: (val: number) => void;
setS3Group: (group: 1 | 2 | 3 | 4 | 5) => void;
setTerrainCategory: (cat: TerrainCategory) => void;
setDimensions: (largestDim: number, z: number) => void;
setWindAngle: (angle: 0 | 90) => void;
setPermeabilityCase: (c: PermeabilityCase) => void;
setCpiRatio: (r: number) => void;
setCpiManual: (c: number) => void;
}
function calcCpi(permeabilityCase: PermeabilityCase, ratio: number, windAngle: 0 | 90): number {
if (permeabilityCase === 'airtight') return 0; // Just as an example, computeCpiSimplified handles it
const value = computeCpiSimplified({
case: permeabilityCase,
ratio,
windAngle,
});
return clampCpi(value);
}
export const useWindStore = create<GlobalWindState>((set, get) => {
const recalc = (state: GlobalWindState): Partial<GlobalWindState> => {
const calc = calculateGlobalWindData(
state.v0,
state.s1,
state.s3,
state.terrainCategory,
state.largestDimension,
state.heightZ,
);
return {
structureClass: calc.structClass,
s2: calc.s2,
vk: calc.vk,
q: calc.q,
cpi: calcCpi(state.permeabilityCase, state.cpiRatio, state.windAngle),
};
};
return {
v0: 40,
s1: 1.0,
s3: 1.0,
s3Group: 3,
terrainCategory: 'II',
largestDimension: 30,
heightZ: 10,
structureClass: 'B',
s2: 1.06,
vk: 42.4,
q: 1.1024,
windAngle: 0,
permeabilityCase: 'four-equally-permeable',
cpiRatio: 1.0,
cpi: calcCpi('four-equally-permeable', 1.0, 0),
updateCalculations: () => set((state) => recalc(state)),
setV0: (val) => {
set({ v0: val });
get().updateCalculations();
},
setS1: (val) => {
set({ s1: val });
get().updateCalculations();
},
setS3: (val) => {
set({ s3: val });
get().updateCalculations();
},
setS3Group: (group) => {
const newS3 = group === 1 ? 1.10 : group === 2 ? 1.08 : group === 3 ? 1.0 : group === 4 ? 0.95 : 0.83;
set({ s3Group: group, s3: newS3 });
get().updateCalculations();
},
setTerrainCategory: (cat) => {
set({ terrainCategory: cat });
get().updateCalculations();
},
setDimensions: (largestDim, z) => {
set({ largestDimension: largestDim, heightZ: z });
get().updateCalculations();
},
setWindAngle: (angle) => {
set({ windAngle: angle });
get().updateCalculations();
},
setPermeabilityCase: (c) => {
set({ permeabilityCase: c });
get().updateCalculations();
},
setCpiRatio: (r) => {
set({ cpiRatio: r });
get().updateCalculations();
},
setCpiManual: (c) => {
set({ cpi: clampCpi(c) });
// Not calling updateCalculations to avoid overriding it immediately if permeabilityCase applies,
// but if the user overrides manually we might want to switch permeabilityCase to 'custom' ?
// Wait, there is no 'custom'. We just set the value.
},
};
});
+70
View File
@@ -0,0 +1,70 @@
/**
* Store global para captura de canvas 3D (M9.3)
*
* O componente 3DViewer registra seu canvas via `registerCanvas`.
* O painel de captura usa `captureCanvas` para extrair uma imagem.
*
* Funciona com qualquer viewer 3D baseado em R3F (galpão, cilindro,
* abóbada, cúpula, etc.) — basta chamar `registerCanvas(canvas)` quando
* o canvas monta e `unregisterCanvas()` quando desmonta.
*/
import { create } from 'zustand';
import { captureCanvasImage, canvasToDataURL } from '../lib/canvas-capture';
interface CaptureState {
/** Canvas 3D ativo registrado pelo viewer atual */
canvas: HTMLCanvasElement | null;
/** Imagem capturada em data URL (PNG). Limpa após uso. */
capturedImage: string | null;
/** Timestamp da última captura */
capturedAt: number | null;
/** Largura preferida do PNG exportado (px). 0 = original */
targetWidth: number;
/** Qualidade JPEG (01) se formato = jpeg */
jpegQuality: number;
/** Formato de saída */
format: 'png' | 'jpeg';
registerCanvas: (canvas: HTMLCanvasElement | null) => void;
unregisterCanvas: () => void;
setTargetWidth: (w: number) => void;
setFormat: (f: 'png' | 'jpeg') => void;
setJpegQuality: (q: number) => void;
capture: () => Promise<string | null>;
clearCaptured: () => void;
}
export const useCaptureStore = create<CaptureState>((set, get) => ({
canvas: null,
capturedImage: null,
capturedAt: null,
targetWidth: 1600,
jpegQuality: 0.92,
format: 'png',
registerCanvas: (canvas) => set({ canvas }),
unregisterCanvas: () => set({ canvas: null }),
setTargetWidth: (w) => set({ targetWidth: Math.max(0, Math.floor(w)) }),
setFormat: (f) => set({ format: f }),
setJpegQuality: (q) => set({ jpegQuality: Math.max(0, Math.min(1, q)) }),
capture: async () => {
const { canvas, format, jpegQuality, targetWidth } = get();
if (!canvas) return null;
try {
const dataUrl = canvasToDataURL(canvas, format, jpegQuality);
const resized = targetWidth > 0
? await captureCanvasImage(canvas, { format, quality: jpegQuality, maxWidth: targetWidth })
: dataUrl;
set({ capturedImage: resized, capturedAt: Date.now() });
return resized;
} catch (err) {
console.error('[capture] Falha ao capturar canvas:', err);
return null;
}
},
clearCaptured: () => set({ capturedImage: null, capturedAt: null }),
}));
+67
View File
@@ -0,0 +1,67 @@
import { create } from 'zustand';
import { getWallCpeOfficial as getWallCpe, getRoofCpeOfficial as getRoofCpe } from '../lib/coefficients';
import { useWindStore } from './appStore';
import type { WallCoefficients, RoofCoefficients } from '../lib/coefficients';
interface GalpaoState {
// Dimensões do Galpão Retangular
width: number;
length: number;
height: number;
roofPitch: number;
// Saídas calculadas (Cpe)
wallCpe: WallCoefficients;
roofCpe: RoofCoefficients;
// Ações
setWidth: (val: number) => void;
setLength: (val: number) => void;
setHeight: (val: number) => void;
setRoofPitch: (val: number) => void;
updateCoefficients: () => void;
}
const initWidth = 15;
const initLength = 30;
const initHeight = 6;
const initPitch = 10;
const initAngle: 0 | 90 = 0;
export const useGalpaoStore = create<GalpaoState>((set, get) => ({
width: initWidth,
length: initLength,
height: initHeight,
roofPitch: initPitch,
wallCpe: getWallCpe(initLength, initWidth, initHeight, initAngle),
roofCpe: getRoofCpe(initLength, initWidth, initHeight, initPitch, initAngle),
updateCoefficients: () => {
const { length, width, height, roofPitch } = get();
const windAngle = useWindStore.getState().windAngle;
set({
wallCpe: getWallCpe(length, width, height, windAngle),
roofCpe: getRoofCpe(length, width, height, roofPitch, windAngle),
});
},
setWidth: (val) => {
set({ width: val });
get().updateCoefficients();
useWindStore.getState().setDimensions(Math.max(val, get().length), get().height);
},
setLength: (val) => {
set({ length: val });
get().updateCoefficients();
useWindStore.getState().setDimensions(Math.max(get().width, val), get().height);
},
setHeight: (val) => {
set({ height: val });
get().updateCoefficients();
useWindStore.getState().setDimensions(Math.max(get().width, get().length), val);
},
setRoofPitch: (val) => {
set({ roofPitch: val });
get().updateCoefficients();
},
}));
+56
View File
@@ -0,0 +1,56 @@
/**
* Store global de i18n — M9.8
*
* Zustand store para o locale ativo, com persistência em localStorage.
*/
import { create } from 'zustand';
import {
t as tPure,
loadStoredLocale,
saveStoredLocale,
type Locale,
DEFAULT_LOCALE,
} from '../lib/i18n';
interface I18nState {
locale: Locale;
setLocale: (l: Locale) => void;
}
/**
* Função de tradução com escopo do store.
* Use-a dentro de componentes: `const { t } = useI18n();`
*/
export function useI18n() {
const locale = useI18nStore((s) => s.locale);
const setLocale = useI18nStore((s) => s.setLocale);
const tt = (key: string, params?: Record<string, string | number>) =>
tPure(key, locale, params);
return { t: tt, locale, setLocale };
}
export const useI18nStore = create<I18nState>((set) => ({
locale: loadStoredLocale(),
setLocale: (l: Locale) => {
saveStoredLocale(l);
set({ locale: l });
},
}));
/**
* Função utilitária para tradução fora de componentes (utils, hooks, etc.).
* Retorna a tradução baseada no locale atual do store.
*/
export function tNow(key: string, params?: Record<string, string | number>): string {
const locale = useI18nStore.getState().locale;
return tPure(key, locale, params);
}
// Garante que o módulo carrega o locale padrão se nada estiver armazenado
if (typeof window !== 'undefined') {
const current = useI18nStore.getState().locale;
if (current !== DEFAULT_LOCALE && !window.localStorage.getItem('ventoapp.locale')) {
saveStoredLocale(DEFAULT_LOCALE);
}
}