@thecodecodex/tools 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ // Encargos do empregado CLT em 2026.
2
+ // INSS: Portaria Interministerial MPS/MF nº 13/2026 (teto R$ 8.475,55).
3
+ // IRRF: tabela progressiva mensal + redução da Lei nº 15.270/2025, conforme
4
+ // https://www.gov.br/receitafederal/pt-br/assuntos/meu-imposto-de-renda/tabelas/2026
5
+
6
+ export interface Bracket {
7
+ upTo: number
8
+ rate: number
9
+ }
10
+
11
+ export const INSS_2026: Bracket[] = [
12
+ { upTo: 1621.0, rate: 0.075 },
13
+ { upTo: 2902.84, rate: 0.09 },
14
+ { upTo: 4354.27, rate: 0.12 },
15
+ { upTo: 8475.55, rate: 0.14 },
16
+ ]
17
+
18
+ // Tabela progressiva mensal: alíquota e parcela a deduzir por faixa da BASE DE CÁLCULO
19
+ export const IRRF_2026 = [
20
+ { upTo: 2428.8, rate: 0, deduction: 0 },
21
+ { upTo: 2826.65, rate: 0.075, deduction: 182.16 },
22
+ { upTo: 3751.05, rate: 0.15, deduction: 394.16 },
23
+ { upTo: 4664.68, rate: 0.225, deduction: 675.49 },
24
+ { upTo: Infinity, rate: 0.275, deduction: 908.73 },
25
+ ] as const
26
+
27
+ export const IRRF_DEPENDENT_2026 = 189.59
28
+ // Desconto simplificado mensal: substitui as deduções legais quando for maior
29
+ export const IRRF_SIMPLIFIED_2026 = 607.2
30
+
31
+ // Redução (Lei 15.270/2025): calculada sobre os RENDIMENTOS TRIBUTÁVEIS (salário bruto), não sobre a base
32
+ export const REDUCTION_2026 = { exemptUpTo: 5000, max: 312.89, phaseOutUpTo: 7350, constant: 978.62, factor: 0.133145 }
33
+
34
+ // Arredonda centavos "para cima no meio" como a folha (121,575 → 121,58); toFixed(10) remove o ruído binário (121,57499…)
35
+ const round2 = (value: number) => Math.round(Number(value.toFixed(10)) * 100) / 100
36
+
37
+ // INSS progressivo: cada alíquota incide só sobre a parcela dentro da sua faixa
38
+ export function inss(gross: number, table: Bracket[] = INSS_2026): number {
39
+ let total = 0
40
+ let floor = 0
41
+ for (const { upTo, rate } of table) {
42
+ if (gross <= floor) break
43
+ total += (Math.min(gross, upTo) - floor) * rate
44
+ floor = upTo
45
+ }
46
+ return round2(total)
47
+ }
48
+
49
+ export function irrfTable(base: number): number {
50
+ if (base <= 0) return 0
51
+ const bracket = IRRF_2026.find((b) => base <= b.upTo) ?? IRRF_2026[IRRF_2026.length - 1]
52
+ return round2(Math.max(0, base * (bracket?.rate ?? 0) - (bracket?.deduction ?? 0)))
53
+ }
54
+
55
+ export function irrfReduction(taxableIncome: number, taxBeforeReduction: number): number {
56
+ const { exemptUpTo, max, phaseOutUpTo, constant, factor } = REDUCTION_2026
57
+ if (taxableIncome <= exemptUpTo) return Math.min(max, taxBeforeReduction)
58
+ if (taxableIncome <= phaseOutUpTo) return round2(Math.min(Math.max(0, constant - factor * taxableIncome), taxBeforeReduction))
59
+ return 0
60
+ }
61
+
62
+ export interface IrrfInput {
63
+ // rendimentos tributáveis do mês (salário bruto)
64
+ taxableIncome: number
65
+ // INSS descontado no mês
66
+ socialSecurity: number
67
+ dependents?: number
68
+ alimony?: number
69
+ }
70
+
71
+ export interface IrrfResult {
72
+ base: number
73
+ deduction: "legal" | "simplified"
74
+ deductionAmount: number
75
+ taxBeforeReduction: number
76
+ reduction: number
77
+ tax: number
78
+ }
79
+
80
+ /**
81
+ * IRRF mensal 2026:
82
+ * 1. deduções legais (INSS + dependentes + pensão) ou desconto simplificado, o que for maior;
83
+ * 2. tabela progressiva sobre a base;
84
+ * 3. redução da Lei 15.270 calculada sobre o rendimento bruto, limitada ao imposto apurado.
85
+ * Também vale para o 13º (tributação exclusiva): a redução se aplica a ele por força do art. 3º-A, §3º, da Lei 9.250/1995,
86
+ * incluído pela Lei 15.270/2025. As férias são rendimento do mês e seguem a regra mensal normal.
87
+ */
88
+ export function irrf({ taxableIncome, socialSecurity, dependents = 0, alimony = 0 }: IrrfInput): IrrfResult {
89
+ const legal = socialSecurity + dependents * IRRF_DEPENDENT_2026 + alimony
90
+ const simplified = legal < IRRF_SIMPLIFIED_2026
91
+ const deductionAmount = simplified ? IRRF_SIMPLIFIED_2026 : legal
92
+ const base = round2(Math.max(0, taxableIncome - deductionAmount))
93
+ const taxBeforeReduction = irrfTable(base)
94
+ const reduction = irrfReduction(taxableIncome, taxBeforeReduction)
95
+ return { base, deduction: simplified ? "simplified" : "legal", deductionAmount: round2(deductionAmount), taxBeforeReduction, reduction, tax: round2(Math.max(0, taxBeforeReduction - reduction)) }
96
+ }
97
+
98
+ // Atalho para o caso comum: salário bruto → INSS e IRRF do mês
99
+ export function payrollTaxes(gross: number, options: { dependents?: number, alimony?: number } = {}) {
100
+ const socialSecurity = inss(gross)
101
+ const income = irrf({ taxableIncome: gross, socialSecurity, ...options })
102
+ return { inss: socialSecurity, irrf: income.tax, detail: income }
103
+ }
104
+
105
+ export const FGTS_RATE = 0.08
106
+ // Desconto máximo do vale-transporte: 6% do salário-base (Lei 7.418/85), limitado ao custo do benefício
107
+ export const TRANSPORT_VOUCHER_RATE = 0.06
108
+
109
+ export interface CltOptions {
110
+ dependents?: number
111
+ // pensão alimentícia judicial: deduz do IR e sai do líquido
112
+ alimony?: number
113
+ // custo mensal do vale-transporte; o empregado paga até 6% do salário
114
+ transportVoucher?: number
115
+ // outros descontos em folha (coparticipação de plano, empréstimo…)
116
+ otherDiscounts?: number
117
+ // benefícios mensais pagos pela empresa e não descontados (VR/VA, parte do plano de saúde, auxílios)
118
+ monthlyBenefits?: number
119
+ // PLR, bônus ou RSUs líquidos no ano
120
+ annualBonus?: number
121
+ includeFgts?: boolean
122
+ }
123
+
124
+ export interface CltMonth {
125
+ gross: number
126
+ inss: number
127
+ irrf: IrrfResult
128
+ transportDiscount: number
129
+ alimony: number
130
+ otherDiscounts: number
131
+ net: number
132
+ }
133
+
134
+ export function cltMonth(gross: number, options: CltOptions = {}): CltMonth {
135
+ const { dependents = 0, alimony = 0, transportVoucher = 0, otherDiscounts = 0 } = options
136
+ const socialSecurity = inss(gross)
137
+ const income = irrf({ taxableIncome: gross, socialSecurity, dependents, alimony })
138
+ const transportDiscount = round2(Math.min(transportVoucher, gross * TRANSPORT_VOUCHER_RATE))
139
+ const net = round2(gross - socialSecurity - income.tax - transportDiscount - alimony - otherDiscounts)
140
+ return { gross, inss: socialSecurity, irrf: income, transportDiscount, alimony, otherDiscounts, net }
141
+ }
142
+
143
+ export interface CltYear {
144
+ month: CltMonth
145
+ // 13º líquido (tributação exclusiva, mesma tabela e redução)
146
+ net13th: number
147
+ // ganho líquido de férias: (salário + 1/3) tributados juntos, menos o salário líquido do mês que ele substitui
148
+ vacationBonusNet: number
149
+ fgtsYear: number
150
+ // benefícios no ano: mensais × 12 + parte do VT paga pela empresa
151
+ benefitsYear: number
152
+ bonusYear: number
153
+ // tudo que entra no ano (salários líquidos, 13º, 1/3 de férias, FGTS, benefícios, bônus)
154
+ annualTotal: number
155
+ monthlyEquivalent: number
156
+ }
157
+
158
+ export function cltYear(gross: number, options: CltOptions = {}): CltYear {
159
+ const { dependents = 0, monthlyBenefits = 0, annualBonus = 0, includeFgts = true, transportVoucher = 0 } = options
160
+ const month = cltMonth(gross, options)
161
+
162
+ const tax13th = payrollTaxes(gross, { dependents })
163
+ const net13th = round2(gross - tax13th.inss - tax13th.irrf)
164
+
165
+ // no mês de férias o salário vem com o terço constitucional e é tributado junto
166
+ const vacation = cltMonth(gross + gross / 3, options)
167
+ const vacationBonusNet = round2(vacation.net - month.net)
168
+
169
+ const fgtsYear = includeFgts ? round2(gross * FGTS_RATE * (12 + 1 + 1 / 3)) : 0
170
+ const benefitsYear = round2(monthlyBenefits * 12 + (transportVoucher - month.transportDiscount) * 11)
171
+ const annualTotal = round2(month.net * 12 + net13th + vacationBonusNet + fgtsYear + benefitsYear + annualBonus)
172
+ return { month, net13th, vacationBonusNet, fgtsYear, benefitsYear, bonusYear: annualBonus, annualTotal, monthlyEquivalent: round2(annualTotal / 12) }
173
+ }
174
+
175
+ // Menor salário bruto cujo pacote anual atinge a meta (busca binária, precisão de centavos)
176
+ export function grossForAnnualTotal(target: number, options: CltOptions = {}): number {
177
+ if (cltYear(0, options).annualTotal >= target) return 0
178
+ let low = 0
179
+ let high = 1_000_000
180
+ while (high - low > 0.01) {
181
+ const mid = (low + high) / 2
182
+ if (cltYear(mid, options).annualTotal >= target) high = mid
183
+ else low = mid
184
+ }
185
+ // menor valor em centavos que atinge a meta (evita 12.000,01 quando 12.000,00 já basta)
186
+ const cents = Math.floor(high * 100) / 100
187
+ return cltYear(cents, options).annualTotal >= target ? cents : Math.round((cents + 0.01) * 100) / 100
188
+ }
189
+
@@ -0,0 +1,78 @@
1
+ // Juros compostos com aportes mensais, IR regressivo da renda fixa e correção pela inflação.
2
+ // Taxas anuais são EFETIVAS (como CDI, poupança e Tesouro são cotados): mensal = (1 + anual)^(1/12) − 1.
3
+
4
+ export interface CompoundInput {
5
+ initial: number
6
+ monthly: number
7
+ months: number
8
+ // taxa anual efetiva, ex.: 0.1 = 10% a.a.
9
+ annualRate: number
10
+ // inflação anual esperada, para o valor real (poder de compra de hoje)
11
+ annualInflation?: number
12
+ // aplica o IR regressivo sobre o rendimento no resgate (CDB, Tesouro…); poupança é isenta
13
+ taxed?: boolean
14
+ }
15
+
16
+ export interface YearRow {
17
+ year: number
18
+ contributed: number
19
+ interest: number
20
+ balance: number
21
+ }
22
+
23
+ export interface CompoundResult {
24
+ rows: YearRow[]
25
+ contributed: number
26
+ interest: number
27
+ grossBalance: number
28
+ incomeTax: number
29
+ netBalance: number
30
+ // saldo líquido em reais de hoje
31
+ realBalance: number
32
+ // renda mensal sustentável pela regra dos 4% ao ano, em reais de hoje
33
+ monthlyIncome4: number
34
+ }
35
+
36
+ export const monthlyRate = (annual: number) => Math.pow(1 + annual, 1 / 12) - 1
37
+
38
+ // Tabela regressiva da renda fixa (Lei 11.033/2004), pelo prazo de cada aporte
39
+ export function regressiveRate(monthsHeld: number): number {
40
+ if (monthsHeld <= 6) return 0.225
41
+ if (monthsHeld <= 12) return 0.2
42
+ if (monthsHeld <= 24) return 0.175
43
+ return 0.15
44
+ }
45
+
46
+ export function simulate({ initial, monthly, months, annualRate, annualInflation = 0, taxed = false }: CompoundInput): CompoundResult {
47
+ const r = monthlyRate(annualRate)
48
+ const n = Math.max(0, Math.round(months))
49
+ let balance = initial
50
+ let contributed = initial
51
+ const rows: YearRow[] = []
52
+ // aporte no fim de cada mês, depois do rendimento do mês
53
+ for (let m = 1; m <= n; m++) {
54
+ balance = balance * (1 + r) + monthly
55
+ contributed += monthly
56
+ if (m % 12 === 0 || m === n) rows.push({ year: Math.ceil(m / 12), contributed, interest: balance - contributed, balance })
57
+ }
58
+
59
+ // IR: cada aporte tem o próprio prazo; o ganho de cada um paga a alíquota do seu prazo
60
+ let incomeTax = 0
61
+ if (taxed) {
62
+ const lots = [{ amount: initial, held: n }, ...Array.from({ length: n }, (_, k) => ({ amount: monthly, held: n - (k + 1) }))]
63
+ for (const { amount, held } of lots) incomeTax += amount * (Math.pow(1 + r, held) - 1) * regressiveRate(held)
64
+ }
65
+
66
+ const netBalance = balance - incomeTax
67
+ const realBalance = netBalance / Math.pow(1 + annualInflation, n / 12)
68
+ return {
69
+ rows,
70
+ contributed,
71
+ interest: balance - contributed,
72
+ grossBalance: balance,
73
+ incomeTax,
74
+ netBalance,
75
+ realBalance,
76
+ monthlyIncome4: (realBalance * 0.04) / 12,
77
+ }
78
+ }
@@ -0,0 +1,146 @@
1
+ import { INSS_2026, irrf } from "./clt"
2
+
3
+ // Tributação de PJ de serviços (2026). Fontes: LC 123/2006 (Anexos III e V do Simples), Res. CGSN 140/2018,
4
+ // Lei 15.270/2025 (dividendos), DAS-MEI com salário mínimo de R$ 1.621.
5
+
6
+ export const MINIMUM_WAGE_2026 = 1621
7
+ export const MEI = { annualLimit: 81_000, monthlyDas: 86.05 } // 5% do salário mínimo + R$ 5 de ISS
8
+ // Faixas por receita bruta dos últimos 12 meses (RBT12): alíquota nominal e parcela a deduzir
9
+ // cppShare: parte do DAS que é contribuição previdenciária patronal (repartição do Anexo III)
10
+ export const SIMPLES_ANEXO_III = [
11
+ { upTo: 180_000, rate: 0.06, deduction: 0, cppShare: 0.434 },
12
+ { upTo: 360_000, rate: 0.112, deduction: 9_360, cppShare: 0.434 },
13
+ { upTo: 720_000, rate: 0.135, deduction: 17_640, cppShare: 0.434 },
14
+ { upTo: 1_800_000, rate: 0.16, deduction: 35_640, cppShare: 0.434 },
15
+ { upTo: 3_600_000, rate: 0.21, deduction: 125_640, cppShare: 0.434 },
16
+ { upTo: 4_800_000, rate: 0.33, deduction: 648_000, cppShare: 0.305 },
17
+ ]
18
+ export const SIMPLES_ANEXO_V = [
19
+ { upTo: 180_000, rate: 0.155, deduction: 0 },
20
+ { upTo: 360_000, rate: 0.18, deduction: 4_500 },
21
+ { upTo: 720_000, rate: 0.195, deduction: 9_900 },
22
+ { upTo: 1_800_000, rate: 0.205, deduction: 17_100 },
23
+ { upTo: 3_600_000, rate: 0.23, deduction: 62_100 },
24
+ { upTo: 4_800_000, rate: 0.305, deduction: 540_000 },
25
+ ]
26
+ export const SIMPLES_LIMIT = 4_800_000
27
+ // Fator R: folha de 12 meses ÷ RBT12 ≥ 28% leva atividades do Anexo V para o Anexo III.
28
+ // A folha inclui o pró-labore E a CPP efetivamente recolhida (LC 123, art. 18, §24) — no Simples, a CPP vem dentro do DAS.
29
+ export const FATOR_R = 0.28
30
+ // Lucro Presumido, serviços: presunção de 32% para IRPJ e CSLL
31
+ export const PRESUMIDO = { presumption: 0.32, irpj: 0.15, irpjSurcharge: 0.1, surchargeMonthlyThreshold: 20_000, csll: 0.09, pis: 0.0065, cofins: 0.03, cpp: 0.2 }
32
+ // Sócio: INSS de 11% sobre o pró-labore, limitado ao teto
33
+ export const PRO_LABORE_INSS_RATE = 0.11
34
+ // Dividendos (Lei 15.270/2025): acima de R$ 50 mil/mês da mesma empresa para a mesma pessoa, 10% sobre o total
35
+ export const DIVIDENDS = { monthlyExemption: 50_000, rate: 0.1 }
36
+
37
+ type Bracket = { upTo: number, rate: number, deduction: number }
38
+
39
+ export function simplesEffectiveRate(rbt12: number, table: Bracket[]): number {
40
+ if (rbt12 <= 0) return table[0]?.rate ?? 0
41
+ const bracket = table.find((b) => rbt12 <= b.upTo) ?? table[table.length - 1]
42
+ return bracket ? (rbt12 * bracket.rate - bracket.deduction) / rbt12 : 0
43
+ }
44
+
45
+ export function simplesCppShare(rbt12: number): number {
46
+ return (SIMPLES_ANEXO_III.find((b) => rbt12 <= b.upTo) ?? SIMPLES_ANEXO_III[SIMPLES_ANEXO_III.length - 1])?.cppShare ?? 0
47
+ }
48
+
49
+ // Cooperado de cooperativa de trabalho: contribuinte individual, 20% retidos pela cooperativa até o teto
50
+ // (a alíquota de 11% acabou com o RE 595.838/STF, que derrubou a contribuição de 15% do tomador)
51
+ export const COOPERATIVE_INSS_RATE = 0.2
52
+ export function cooperativeInss(remuneration: number): number {
53
+ const ceiling = INSS_2026[INSS_2026.length - 1]?.upTo ?? Infinity
54
+ return Math.min(remuneration, ceiling) * COOPERATIVE_INSS_RATE
55
+ }
56
+
57
+ export function proLaboreInss(proLabore: number): number {
58
+ const ceiling = INSS_2026[INSS_2026.length - 1]?.upTo ?? Infinity
59
+ return Math.min(proLabore, ceiling) * PRO_LABORE_INSS_RATE
60
+ }
61
+
62
+ export type Regime = "mei" | "simples-iii" | "simples-v" | "presumido"
63
+
64
+ export interface PjInput {
65
+ monthlyRevenue: number
66
+ // pró-labore mensal; no Anexo III ele é ajustado para atingir o Fator R
67
+ proLabore: number
68
+ // ISS do município (Lucro Presumido)
69
+ issRate: number
70
+ accountant: number
71
+ otherExpenses: number
72
+ }
73
+
74
+ export interface RegimeResult {
75
+ regime: Regime
76
+ allowed: boolean
77
+ // tributos da empresa (DAS ou IRPJ+CSLL+PIS+COFINS+ISS+CPP)
78
+ companyTaxes: number
79
+ proLabore: number
80
+ proLaboreInss: number
81
+ proLaboreIrrf: number
82
+ dividendsTax: number
83
+ totalTaxes: number
84
+ effectiveRate: number
85
+ // o que sobra para o sócio no mês: faturamento − tributos − contador − despesas
86
+ net: number
87
+ // Simples: alíquota efetiva do DAS
88
+ simplesRate?: number
89
+ }
90
+
91
+ function partner(proLabore: number) {
92
+ const inss = proLaboreInss(proLabore)
93
+ return { inss, irrf: irrf({ taxableIncome: proLabore, socialSecurity: inss }).tax }
94
+ }
95
+
96
+ function finish(regime: Regime, input: PjInput, companyTaxes: number, proLabore: number, allowed = true, simplesRate?: number): RegimeResult {
97
+ const { inss, irrf: irrfTax } = partner(proLabore)
98
+ const profit = input.monthlyRevenue - companyTaxes - proLabore - input.accountant - input.otherExpenses
99
+ const dividendsTax = profit > DIVIDENDS.monthlyExemption ? profit * DIVIDENDS.rate : 0
100
+ const totalTaxes = companyTaxes + inss + irrfTax + dividendsTax
101
+ const result: RegimeResult = {
102
+ regime,
103
+ allowed,
104
+ companyTaxes,
105
+ proLabore,
106
+ proLaboreInss: inss,
107
+ proLaboreIrrf: irrfTax,
108
+ dividendsTax,
109
+ totalTaxes,
110
+ effectiveRate: input.monthlyRevenue > 0 ? totalTaxes / input.monthlyRevenue : 0,
111
+ net: input.monthlyRevenue - totalTaxes - input.accountant - input.otherExpenses,
112
+ }
113
+ if (simplesRate !== undefined) result.simplesRate = simplesRate
114
+ return result
115
+ }
116
+
117
+ export function compareRegimes(input: PjInput): RegimeResult[] {
118
+ const revenue = input.monthlyRevenue
119
+ const rbt12 = revenue * 12
120
+ const minimumProLabore = Math.max(input.proLabore, MINIMUM_WAGE_2026)
121
+
122
+ // MEI: DAS fixo, sem pró-labore obrigatório; a maioria das atividades de TI não é permitida
123
+ const mei = finish("mei", { ...input, accountant: 0 }, MEI.monthlyDas, 0, rbt12 <= MEI.annualLimit)
124
+
125
+ // Anexo III via Fator R: pró-labore + CPP do DAS precisam somar 28% do faturamento
126
+ const rateIII = simplesEffectiveRate(rbt12, SIMPLES_ANEXO_III)
127
+ const fatorRProLabore = Math.max(minimumProLabore, revenue * FATOR_R - revenue * rateIII * simplesCppShare(rbt12))
128
+ const simplesIII = finish("simples-iii", input, revenue * rateIII, fatorRProLabore, rbt12 <= SIMPLES_LIMIT, rateIII)
129
+
130
+ // Anexo V: sem Fator R, pró-labore mínimo
131
+ const rateV = simplesEffectiveRate(rbt12, SIMPLES_ANEXO_V)
132
+ const simplesV = finish("simples-v", input, revenue * rateV, minimumProLabore, rbt12 <= SIMPLES_LIMIT, rateV)
133
+
134
+ // Lucro Presumido: tributos sobre o faturamento + CPP de 20% sobre o pró-labore (fora do Simples)
135
+ const p = PRESUMIDO
136
+ const presumedProfit = revenue * p.presumption
137
+ const irpj = presumedProfit * p.irpj + Math.max(0, presumedProfit - p.surchargeMonthlyThreshold) * p.irpjSurcharge
138
+ const presumidoTaxes = irpj + presumedProfit * p.csll + revenue * (p.pis + p.cofins + input.issRate) + minimumProLabore * p.cpp
139
+ const presumido = finish("presumido", input, presumidoTaxes, minimumProLabore)
140
+
141
+ return [mei, simplesIII, simplesV, presumido]
142
+ }
143
+
144
+ export function bestRegime(results: RegimeResult[]): RegimeResult | undefined {
145
+ return results.filter((r) => r.allowed).reduce<RegimeResult | undefined>((best, r) => (!best || r.net > best.net ? r : best), undefined)
146
+ }
package/src/fov.ts ADDED
@@ -0,0 +1,145 @@
1
+ // Calculadora de FOV para simuladores — porte da lógica de https://github.com/dinex86/FOV-Calculator (MIT)
2
+ // Ideia: o campo de visão "correto" é o ângulo que a tela realmente ocupa na sua visão,
3
+ // assim os objetos no jogo aparecem em escala real.
4
+
5
+ export interface Setup {
6
+ aspect: [number, number]
7
+ // diagonal de UMA tela, em polegadas
8
+ diagonalIn: number
9
+ // distância dos olhos até a tela, em cm
10
+ distanceCm: number
11
+ screens: 1 | 3
12
+ // espessura da moldura (bezel) de cada tela, em mm — só conta com 3 telas
13
+ bezelMm: number
14
+ curved: boolean
15
+ // raio da curvatura em mm (ex.: "1800R" = 1800)
16
+ radiusMm: number
17
+ }
18
+
19
+ export const DEFAULT_SETUP: Setup = { aspect: [16, 9], diagonalIn: 27, distanceCm: 60, screens: 1, bezelMm: 0, curved: false, radiusMm: 1800 }
20
+
21
+ export const ASPECTS: [number, number][] = [[16, 9], [16, 10], [21, 9], [24, 10], [32, 9], [32, 10], [5, 4], [4, 3]]
22
+
23
+ const DEG = 180 / Math.PI
24
+
25
+ export function screenSizeCm(diagonalIn: number, [x, y]: [number, number]) {
26
+ const diagonalCm = diagonalIn * 2.54
27
+ const unit = Math.sqrt((diagonalCm * diagonalCm) / (x * x + y * y))
28
+ return { width: x * unit, height: y * unit }
29
+ }
30
+
31
+ function flatAngle(widthCm: number, distanceCm: number): number {
32
+ return Math.atan2(widthCm / 2, distanceCm) * 2
33
+ }
34
+
35
+ // Tela curva: usa a corda entre as bordas da tela e a "profundidade" da curva
36
+ function curvedAngle(widthCm: number, radiusMm: number, distanceCm: number): number {
37
+ const radius = radiusMm / 10
38
+ const arcAngle = widthCm / radius
39
+ const depth = radius * (1 - Math.cos(arcAngle / 2))
40
+ const halfChord = Math.sqrt(2 * radius * depth - depth * depth)
41
+ return 2 * Math.atan2(halfChord, distanceCm - depth)
42
+ }
43
+
44
+ export interface Angles {
45
+ // FOV horizontal de UMA tela, em graus
46
+ hFovSingle: number
47
+ // FOV horizontal total (todas as telas), em graus
48
+ hFov: number
49
+ // FOV vertical, em graus
50
+ vFov: number
51
+ // ângulo para girar cada tela lateral (igual ao hFOV de uma tela)
52
+ sideScreenAngle: number
53
+ widthCm: number
54
+ heightCm: number
55
+ }
56
+
57
+ export function calculateAngles(setup: Setup): Angles {
58
+ const [x, y] = setup.aspect
59
+ const { width, height } = screenSizeCm(setup.diagonalIn, setup.aspect)
60
+ // a moldura entre telas "rouba" campo de visão: soma 2 bezels (em cm) à largura
61
+ const effectiveWidth = width + (setup.screens > 1 ? (setup.bezelMm / 10) * 2 : 0)
62
+ const h = setup.curved ? curvedAngle(effectiveWidth, setup.radiusMm, setup.distanceCm) : flatAngle(effectiveWidth, setup.distanceCm)
63
+ const v = 2 * Math.atan2(Math.tan(h / 2) * y, x)
64
+ return { hFovSingle: h * DEG, hFov: h * DEG * setup.screens, vFov: v * DEG, sideScreenAngle: h * DEG, widthCm: width, heightCm: height }
65
+ }
66
+
67
+ type Kind = "hfov" | "hfov-rad" | "hfov-step" | "vfov" | "vfov-multiplier" | "vfov-step"
68
+
69
+ interface GameConfig {
70
+ name: string
71
+ kind: Kind
72
+ min?: number
73
+ max?: number
74
+ decimals: number
75
+ factor?: number
76
+ // jogos com slider relativo: valor = round((fov − base) / incremento) × passo
77
+ base?: number
78
+ increment?: number
79
+ step?: number
80
+ }
81
+
82
+ // Valores e limites de cada jogo conforme o projeto original
83
+ export const GAMES: GameConfig[] = [
84
+ { name: "Assetto Corsa / Assetto Corsa Competizione", kind: "vfov", min: 10, max: 120, decimals: 1 },
85
+ { name: "rFactor 1 & 2, GSC, GSCE, SCE, AMS (ISI)", kind: "vfov", min: 10, max: 100, decimals: 0 },
86
+ { name: "DiRT Rally 1/2, GRID Autosport", kind: "vfov", min: 10, max: 115, decimals: 0, factor: 2 },
87
+ { name: "RaceRoom Racing Experience", kind: "hfov", min: 35, max: 180, decimals: 1 },
88
+ { name: "Project CARS 1/2", kind: "hfov", min: 35, max: 180, decimals: 0 },
89
+ { name: "Euro & American Truck Simulator", kind: "hfov", min: 35, max: 180, decimals: 0 },
90
+ { name: "F1 2016–2018", kind: "hfov-step", min: -1, max: 1, decimals: 2, base: 77, increment: 2, step: 0.05 },
91
+ { name: "F1 2019–2020", kind: "hfov-step", min: -10, max: 10, decimals: 1, base: 77, increment: 2, step: 0.1 },
92
+ { name: "F1 2021+", kind: "hfov-step", min: -20, max: 20, decimals: 0, base: 77, increment: 2, step: 1 },
93
+ { name: "EA Sports WRC", kind: "vfov-step", min: 0, max: 100, decimals: 0, base: 18, increment: 0.5, step: 1 },
94
+ { name: "Richard Burns Rally", kind: "hfov-rad", min: 10, max: 180, decimals: 6 },
95
+ { name: "GTR2", kind: "vfov-multiplier", min: 0.5, max: 1.5, decimals: 1, base: 58 },
96
+ { name: "Race 07", kind: "vfov-multiplier", min: 0.4, max: 1.5, decimals: 1, base: 58 },
97
+ ]
98
+
99
+ export type GameUnit = "°" | "rad" | "x" | ""
100
+
101
+ export interface GameResult {
102
+ name: string
103
+ kind: Kind
104
+ value: string
105
+ unit: GameUnit
106
+ // true quando o valor ideal está fora do que o jogo permite e foi limitado
107
+ clamped: boolean
108
+ }
109
+
110
+ export function gameSettings(setup: Setup, angles: Angles = calculateAngles(setup)): GameResult[] {
111
+ return GAMES.map((game) => {
112
+ let value: number
113
+ let unit: GameUnit = "°"
114
+ switch (game.kind) {
115
+ case "hfov":
116
+ case "hfov-step":
117
+ value = angles.hFov
118
+ break
119
+ case "hfov-rad": {
120
+ // RBR usa o FOV horizontal de uma tela 4:3 com a mesma altura
121
+ const [x, y] = setup.aspect
122
+ const width4by3 = ((angles.widthCm + (setup.screens > 1 ? (setup.bezelMm / 10) * 2 : 0)) / x) * y / 3 * 4
123
+ value = flatAngle(width4by3, setup.distanceCm) * DEG
124
+ unit = "rad"
125
+ break
126
+ }
127
+ default:
128
+ value = angles.vFov
129
+ }
130
+ value *= game.factor ?? 1
131
+ if (game.kind === "vfov-multiplier") {
132
+ value /= game.base ?? 1
133
+ unit = "x"
134
+ }
135
+ if (game.kind === "hfov-step" || game.kind === "vfov-step") {
136
+ value = Math.round((value - (game.base ?? 0)) / (game.increment ?? 1)) * (game.step ?? 1)
137
+ unit = ""
138
+ }
139
+ const limited = Math.min(Math.max(value, game.min ?? -Infinity), game.max ?? Infinity)
140
+ const clamped = Math.abs(limited - value) > 1e-9
141
+ const final = unit === "rad" ? limited * (Math.PI / 180) : limited
142
+ const scale = 10 ** game.decimals
143
+ return { name: game.name, kind: game.kind, value: (Math.round(final * scale) / scale).toFixed(game.decimals), unit, clamped }
144
+ })
145
+ }
package/src/hash.ts ADDED
@@ -0,0 +1,13 @@
1
+ // Hashes via Web Crypto (nativo do navegador e do Node). MD5 não existe na Web Crypto por ser inseguro.
2
+ export const ALGORITHMS = ["SHA-1", "SHA-256", "SHA-384", "SHA-512"] as const
3
+ export type Algorithm = (typeof ALGORITHMS)[number]
4
+
5
+ export async function hash(text: string, algorithm: Algorithm): Promise<string> {
6
+ const digest = await crypto.subtle.digest(algorithm, new TextEncoder().encode(text))
7
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("")
8
+ }
9
+
10
+ export async function hashAll(text: string): Promise<Record<Algorithm, string>> {
11
+ const values = await Promise.all(ALGORITHMS.map((algorithm) => hash(text, algorithm)))
12
+ return Object.fromEntries(ALGORITHMS.map((algorithm, i) => [algorithm, values[i] ?? ""])) as Record<Algorithm, string>
13
+ }