🚀 Auto-deploy: BrainWind atualizado em 15/07/2026 11:22:37
This commit is contained in:
+50
-27
@@ -14,8 +14,15 @@ import PiperackModule from './pages/PiperackModule';
|
||||
import {
|
||||
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
||||
Square, Layers, BarChart3, Activity, Warehouse,
|
||||
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen
|
||||
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen, MoreVertical, Info
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -220,16 +227,48 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
{effectiveTheme === 'dark' ? <Sun className="h-4.5 w-4.5" /> : <Moon className="h-4.5 w-4.5" />}
|
||||
</Button>
|
||||
<Link
|
||||
to="/settings"
|
||||
className={cn(
|
||||
'h-8 w-8 flex items-center justify-center text-muted-foreground hover:text-foreground shrink-0 rounded-full border border-border/40 bg-background/50',
|
||||
location.pathname === '/settings' && 'text-primary border-primary/20 bg-primary/5'
|
||||
)}
|
||||
title={t('nav_settings')}
|
||||
>
|
||||
<Settings className="h-4.5 w-4.5" />
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'h-8 w-8 text-muted-foreground hover:text-foreground shrink-0 rounded-full border border-border/40 bg-background/50',
|
||||
(location.pathname === '/settings' || location.pathname === '/arquivos') && 'text-primary border-primary/20 bg-primary/5'
|
||||
)}
|
||||
>
|
||||
<MoreVertical className="h-4.5 w-4.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<GlobalWindSettingsModal trigger={
|
||||
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()} className="cursor-pointer gap-2">
|
||||
<Settings2 className="w-4 h-4" /> Parâmetros do Vento
|
||||
</DropdownMenuItem>
|
||||
} />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings?tab=gerenciador" className="cursor-pointer gap-2">
|
||||
<FolderOpen className="w-4 h-4" /> Arquivos
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings" className="cursor-pointer gap-2">
|
||||
<Settings className="w-4 h-4" /> Configurações
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings?tab=glossary" className="cursor-pointer gap-2">
|
||||
<BookOpen className="w-4 h-4" /> Glossário
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<TermsOfUseDialog trigger={
|
||||
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()} className="cursor-pointer gap-2">
|
||||
<Info className="w-4 h-4" /> Termos de Uso
|
||||
</DropdownMenuItem>
|
||||
} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex-1 overflow-auto">{children}</div>
|
||||
@@ -280,22 +319,6 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Parâmetros do Vento (Fixo na direita) */}
|
||||
<GlobalWindSettingsModal
|
||||
trigger={
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center h-full px-3 text-xs transition-colors shrink-0 border-l bg-card z-20 shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.15)] cursor-pointer text-muted-foreground hover:text-primary',
|
||||
)}
|
||||
>
|
||||
<div className="p-1 rounded-full">
|
||||
<Settings2 className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<span className="scale-90 text-[10px] text-primary font-medium">Vento</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,24 +6,27 @@ import { useState } from 'react';
|
||||
|
||||
interface TermsOfUseDialogProps {
|
||||
isCollapsed?: boolean;
|
||||
trigger?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function TermsOfUseDialog({ isCollapsed = false }: TermsOfUseDialogProps) {
|
||||
export function TermsOfUseDialog({ isCollapsed = false, trigger }: TermsOfUseDialogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
title={isCollapsed ? 'Termos de Uso e Responsabilidade' : undefined}
|
||||
>
|
||||
<Info className="w-5 h-5 shrink-0" />
|
||||
{!isCollapsed && <span>Termos de Uso</span>}
|
||||
</button>
|
||||
{trigger ? trigger : (
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
title={isCollapsed ? 'Termos de Uso e Responsabilidade' : undefined}
|
||||
>
|
||||
<Info className="w-5 h-5 shrink-0" />
|
||||
{!isCollapsed && <span>Termos de Uso</span>}
|
||||
</button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-w-2xl max-h-[85vh] p-0 flex flex-col">
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import * as React from "react"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
@@ -82,7 +82,7 @@ describe('M9.7 — parseProjectJson', () => {
|
||||
|
||||
describe('M9.7 — detectFormat', () => {
|
||||
it('Detecta SavedProject', () => {
|
||||
expect(detectFormat({ module: 'galpao', inputs: {} })).toBe('saved-project');
|
||||
expect(detectFormat({ type: 'projeto', module: 'galpao', inputs: {} })).toBe('saved-project');
|
||||
});
|
||||
|
||||
it('Detecta snapshot do windStore', () => {
|
||||
@@ -106,7 +106,7 @@ describe('M9.7 — validateSavedProject', () => {
|
||||
it('Aceita SavedProject válido', () => {
|
||||
const project = {
|
||||
name: 'Galpão Teste',
|
||||
module: 'galpao',
|
||||
type: 'projeto', module: 'galpao',
|
||||
inputs: {},
|
||||
createdAt: 1000,
|
||||
updatedAt: 2000,
|
||||
@@ -117,28 +117,28 @@ describe('M9.7 — validateSavedProject', () => {
|
||||
});
|
||||
|
||||
it('Rejeita projeto sem name', () => {
|
||||
const project = { module: 'galpao', inputs: {} };
|
||||
const project = { type: 'projeto', module: 'galpao', inputs: {} };
|
||||
const v = validateSavedProject(project);
|
||||
expect(v.ok).toBe(false);
|
||||
expect(v.errors.some((e) => e.includes('name'))).toBe(true);
|
||||
});
|
||||
|
||||
it('Rejeita módulo inválido', () => {
|
||||
const project = { name: 'X', module: 'invalido', inputs: {} };
|
||||
const project = { name: 'X', type: 'projeto', module: 'invalido', inputs: {} };
|
||||
const v = validateSavedProject(project);
|
||||
expect(v.ok).toBe(false);
|
||||
expect(v.errors.some((e) => e.includes('module'))).toBe(true);
|
||||
});
|
||||
|
||||
it('Rejeita inputs não-objeto', () => {
|
||||
const project = { name: 'X', module: 'galpao', inputs: 'não-objeto' };
|
||||
const project = { name: 'X', type: 'projeto', module: 'galpao', inputs: 'não-objeto' };
|
||||
const v = validateSavedProject(project);
|
||||
expect(v.ok).toBe(false);
|
||||
expect(v.errors.some((e) => e.includes('inputs'))).toBe(true);
|
||||
});
|
||||
|
||||
it('Emite warning se timestamps faltarem', () => {
|
||||
const project = { name: 'X', module: 'galpao', inputs: {} };
|
||||
const project = { name: 'X', type: 'projeto', module: 'galpao', inputs: {} };
|
||||
const v = validateSavedProject(project);
|
||||
expect(v.warnings.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -177,7 +177,7 @@ describe('M9.7 — applySavedProject', () => {
|
||||
it('Aplica wind.v0 corretamente', () => {
|
||||
const project: SavedProject = {
|
||||
name: 'Teste',
|
||||
module: 'galpao',
|
||||
type: 'projeto', module: 'galpao',
|
||||
inputs: { wind: { v0: 50 } },
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
@@ -190,7 +190,7 @@ describe('M9.7 — applySavedProject', () => {
|
||||
it('Aplica múltiplos campos do windStore', () => {
|
||||
const project: SavedProject = {
|
||||
name: 'Teste',
|
||||
module: 'galpao',
|
||||
type: 'projeto', module: 'galpao',
|
||||
inputs: {
|
||||
wind: {
|
||||
v0: 45,
|
||||
@@ -215,7 +215,7 @@ describe('M9.7 — applySavedProject', () => {
|
||||
it('Aplica galpaoStore quando module=galpao', () => {
|
||||
const project: SavedProject = {
|
||||
name: 'Galpão',
|
||||
module: 'galpao',
|
||||
type: 'projeto', module: 'galpao',
|
||||
inputs: {
|
||||
galpao: {
|
||||
width: 20,
|
||||
@@ -243,7 +243,7 @@ describe('M9.7 — applySavedProject', () => {
|
||||
it('Não aplica galpaoStore quando module ≠ galpao', () => {
|
||||
const project: SavedProject = {
|
||||
name: 'Cilindro',
|
||||
module: 'cilindro',
|
||||
type: 'projeto', module: 'cilindro',
|
||||
inputs: { galpao: { width: 20 } },
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
@@ -255,7 +255,7 @@ describe('M9.7 — applySavedProject', () => {
|
||||
it('Adiciona warning para categoria inválida', () => {
|
||||
const project: SavedProject = {
|
||||
name: 'Teste',
|
||||
module: 'galpao',
|
||||
type: 'projeto', module: 'galpao',
|
||||
inputs: { wind: { terrainCategory: 'INVALID' } },
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
@@ -281,7 +281,7 @@ describe('M9.7 — importProjectFromText (orquestrador)', () => {
|
||||
it('Roundtrip: export → import preserva campos principais', () => {
|
||||
const project: SavedProject = {
|
||||
name: 'Roundtrip',
|
||||
module: 'galpao',
|
||||
type: 'projeto', module: 'galpao',
|
||||
inputs: {
|
||||
wind: { v0: 45, s1: 1, s3Group: 2 },
|
||||
galpao: { width: 18, length: 35, height: 7 },
|
||||
|
||||
Reference in New Issue
Block a user