feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -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.
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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 (0–1) 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 }),
|
||||
}));
|
||||
@@ -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();
|
||||
},
|
||||
}));
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user