From 649d4ecdca9c5649856cd9bdcf210813e003e516 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 22:52:30 +0000 Subject: [PATCH 01/12] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- .lovable/plan.md | 69 ++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/.lovable/plan.md b/.lovable/plan.md index 46d731b..7fed05f 100644 --- a/.lovable/plan.md +++ b/.lovable/plan.md @@ -1,42 +1,47 @@ - ## Objetivo -Adicionar alternância entre câmera **Perspectiva** e **Ortográfica** no viewer principal, com **Ortográfica como padrão**, e tornar a calibração mais confiável usando a vista ortogonal (sem distorção de perspectiva, normais alinhadas com os eixos da tela). +Adicionar uma ferramenta de **corte de seção** (clipping planes) nos eixos X, Y e Z — padrão em viewers IFC/BIM. O usuário move sliders por eixo e a peça vai sendo cortada/revelada em tempo real. Implementada como **janela pop-up flutuante** sobre o viewer, com controle de opacidade da própria janela. Sem presets persistidos. -## Por que isso resolve a calibração +## UX -Em perspectiva, ao clicar numa face "de frente", a normal capturada pelo raycaster é a normal real do triângulo, mas o usuário tende a clicar em faces inclinadas achando que estão alinhadas — o que gera pares de vetores ruins. Em **ortográfica + vista alinhada ao cubo (FRENTE/TOPO/DIR)**, a face que aparece "de frente" é geometricamente perpendicular à câmera, então a chance de capturar a normal certa é muito maior. Também damos uma dica visual (snap angular) para validar o clique. +### Botões na barra `ViewerControls` +- **"Cortes"** (ícone `Scissors`) — abre/fecha a janela pop-up. O estado dos cortes é independente da janela: fechar não reseta nada. +- **Lixeira** ao lado, só fica habilitada quando há pelo menos um eixo de corte ativo. Clicar = `resetSection()` (desliga e zera os 3 eixos). Único caminho para zerar. +- Indicador visual no botão "Cortes" quando há corte ativo (badge ou variante `default`). -## Mudanças +### Pop-up (componente `SectionCutPanel`) +- Janela flutuante sobre o viewer (canto superior-direito da área 3D), arrastável pelo header. Não-modal. +- Estilo: `bg-card/X` + `backdrop-blur`, borda industrial, font-mono. +- **Header**: título "Cortes de Seção", slider de **opacidade da janela** (20%–100%, default 90%), botão fechar (X). +- **3 blocos idênticos** para X / Y / Z, cada um com controle individual: + - Switch on/off do eixo. + - Switch "Inverter" (flip do lado mantido). + - Slider do nível de corte mapeado ao bbox mundial da peça ativa naquele eixo, valor em mm. + - Mini-botões: **Min** · **Centro** · **Max** para saltar a posição. +- Botão **"Recalcular limites"** (re-amostra o bbox depois de calibração/fine-tuning). +- Sem botões de salvar/memória. +- Fechar a janela apenas oculta os controles. Reabrir mostra exatamente o estado dos cortes que o usuário deixou. -### 1. Toggle de câmera no viewer (`ModelViewer.tsx` + `ViewerControls.tsx`) -- Estado `cameraMode: 'ortho' | 'persp'` no `useModelStore` (default `'ortho'`, persistido em localStorage junto com as demais prefs visuais). -- Em `ModelViewer.tsx`, condicionar `` / `` (drei) como `makeDefault`, compartilhando posição/target com o `OrbitControls`. Ao trocar de modo, copiar `position`, `target` e calcular `zoom` ortográfico a partir da distância atual para evitar "pulo" visual. -- `OrbitControls` permanece, apenas com `enableRotate` igual; em ortho mantemos rotação livre (estilo CAD). Pan/zoom continuam funcionando. -- Botão de alternância adicionado em `ViewerControls.tsx` ao lado dos controles de render (Sólido / Bordas), com ícones (ex.: `Box` para perspectiva, `Square` para ortho) e tooltip. Atalho de teclado opcional `O`/`P`. +## Comportamento técnico -### 2. ViewCube já existente -- Continua funcionando igual, mas agora ao clicar numa face em modo ortográfico o resultado é uma vista verdadeiramente axial (sem foreshortening), o que é o comportamento esperado de CAD. - -### 3. Calibração refinada (`ViewCube.tsx` + `viewCubeBus.ts`) -- Ao iniciar a calibração, **forçar modo ortográfico** automaticamente (salvando o modo anterior para restaurar ao concluir/cancelar). -- Em cada passo "await-cube-N", após o usuário clicar a face do cubo, **animar a câmera principal** para essa vista (já temos `requestView`) — assim a próxima clique na peça é feito olhando a face de frente. -- Adicionar **snap de normal**: ao capturar a normal da face da peça, se ela estiver a <10° de um eixo principal (X/Y/Z), arredondar para esse eixo. Isso elimina ruído de faces ligeiramente chanfradas. -- Mostrar no hint: "olhando para FRENTE — clique na face frontal da peça" (texto dinâmico do passo). -- Manter o fluxo de 4 cliques + verificação opcional já aprovado. - -### 4. Persistência -- `cameraMode` salvo globalmente (preferência do app, não por peça). -- `calibrationQuat` por peça continua como já está. +- `gl.localClippingEnabled = true` no `onCreated` do ``. +- Novos campos em `useModelStore` (não persistidos): + - `sectionEnabled: { x: boolean; y: boolean; z: boolean }` + - `sectionInvert: { x: boolean; y: boolean; z: boolean }` + - `sectionLevel: { x: number; y: number; z: number }` (em metros, mundo) + - `sectionPanelOpen: boolean` + - `sectionPanelOpacity: number` (0.2–1) + - Setters por eixo + `resetSection()` (zera enabled, invert e leva level ao centro do bbox). + - Helper `hasActiveSection()` derivado de `sectionEnabled`. +- Novo componente `` montado dentro do ``: + - Mantém 3 `THREE.Plane` reutilizáveis. A cada mudança nos campos do store, atualiza `plane.normal` (com sinal flipado se `invert`) e `plane.constant = -level` (axis-aligned, mundo). + - Aplica `material.clippingPlanes = ativos` em cada `Mesh` dos grupos registrados em `modelTransforms`. Restaura `[]` ao desligar todos os eixos. +- Limites dos sliders: bbox mundial agregado dos `getAllModelLocalGroups()`. Calculado quando o painel abre e quando o usuário aciona "Recalcular limites". ## Arquivos afetados -- `src/stores/useModelStore.ts` — campo `cameraMode` + setter. -- `src/components/three/ModelViewer.tsx` — alternância de câmera, sincronização de posição/zoom. -- `src/components/ViewerControls.tsx` — botão de alternância Persp/Ortho. -- `src/components/ViewCube.tsx` — força ortho durante calibração, anima vista após cada cube-click. -- `src/components/three/viewCubeBus.ts` — snap angular da normal capturada. - -## Pergunta - -Te parece bom seguir com **ortográfica como default** e **forçar ortho durante calibração** com snap automático nas normais? Ou prefere deixar o usuário trocar manualmente e a calibração apenas "sugerir" o modo ortho via toast? +- `src/stores/useModelStore.ts` — novos campos e setters de seção + painel. +- `src/components/three/ModelViewer.tsx` — `localClippingEnabled` no GL; monta ``. +- `src/components/SectionCutPanel.tsx` (novo) — janela pop-up flutuante com 3 sliders, switches, opacidade. +- `src/components/ViewerControls.tsx` — botão "Cortes" + lixeira condicional. +- `src/pages/Viewer.tsx` — monta `` na área do viewer. From 93c693f1392c6f014d1e9dcde0920fa1186f2d7d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 22:52:40 +0000 Subject: [PATCH 02/12] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/stores/useModelStore.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index ba96846..914b38a 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -261,6 +261,21 @@ interface ModelStore { gridCalibMode: boolean; setGridCalibMode: (b: boolean) => void; + /** Section/clipping cut tool (X/Y/Z axis-aligned planes in world space). */ + sectionEnabled: { x: boolean; y: boolean; z: boolean }; + sectionInvert: { x: boolean; y: boolean; z: boolean }; + /** Cut level in meters (world coordinates) per axis. */ + sectionLevel: { x: number; y: number; z: number }; + sectionPanelOpen: boolean; + /** Opacity of the floating section panel (0.2 – 1). */ + sectionPanelOpacity: number; + setSectionEnabled: (axis: 'x' | 'y' | 'z', on: boolean) => void; + setSectionInvert: (axis: 'x' | 'y' | 'z', on: boolean) => void; + setSectionLevel: (axis: 'x' | 'y' | 'z', v: number) => void; + setSectionPanelOpen: (open: boolean) => void; + setSectionPanelOpacity: (o: number) => void; + resetSection: () => void; + wireframeColor: string; setWireframeColor: (color: string) => void; From 2754260962aee696190dd02cff974714276401c8 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 22:52:52 +0000 Subject: [PATCH 03/12] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/stores/useModelStore.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index 914b38a..6216d0c 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -556,6 +556,22 @@ export const useModelStore = create((set, get) => ({ gridCalibMode: false, setGridCalibMode: (gridCalibMode) => set({ gridCalibMode }), + sectionEnabled: { x: false, y: false, z: false }, + sectionInvert: { x: false, y: false, z: false }, + sectionLevel: { x: 0, y: 0, z: 0 }, + sectionPanelOpen: false, + sectionPanelOpacity: 0.9, + setSectionEnabled: (axis, on) => set((s) => ({ sectionEnabled: { ...s.sectionEnabled, [axis]: on } })), + setSectionInvert: (axis, on) => set((s) => ({ sectionInvert: { ...s.sectionInvert, [axis]: on } })), + setSectionLevel: (axis, v) => set((s) => ({ sectionLevel: { ...s.sectionLevel, [axis]: v } })), + setSectionPanelOpen: (sectionPanelOpen) => set({ sectionPanelOpen }), + setSectionPanelOpacity: (sectionPanelOpacity) => set({ sectionPanelOpacity }), + resetSection: () => set({ + sectionEnabled: { x: false, y: false, z: false }, + sectionInvert: { x: false, y: false, z: false }, + sectionLevel: { x: 0, y: 0, z: 0 }, + }), + wireframeColor: '#00f3ff', setWireframeColor: (wireframeColor) => set({ wireframeColor }), From eb34692fdd80d6b65e21b5aa8788a838c1b49e5b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 22:53:02 +0000 Subject: [PATCH 04/12] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 992c984..6436616 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -1283,6 +1283,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) { const isApple = /Mac|iPhone|iPad/.test(navigator.platform); const maxRatio = isApple ? 2 : 1.5; gl.setPixelRatio(Math.min(window.devicePixelRatio, maxRatio)); + gl.localClippingEnabled = true; }} > From 7efac5448b8e5fd0c2bc3d94973b228a651c7606 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 22:53:09 +0000 Subject: [PATCH 05/12] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 6436616..5c30a88 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -1303,6 +1303,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) { + From 7208e2bc56169c0800cac21786031461764a5556 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 22:53:23 +0000 Subject: [PATCH 06/12] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 5c30a88..e7a8314 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -1115,6 +1115,72 @@ function GridCalibrationHandler() { return null; } +/** Applies axis-aligned clipping planes (X / Y / Z) to every mesh inside the + * registered model groups. Listens to the section state in the store; when + * all axes are off, restores empty clipping arrays so meshes render whole. */ +function SectionClippingApplier() { + const enabled = useModelStore((s) => s.sectionEnabled); + const invert = useModelStore((s) => s.sectionInvert); + const level = useModelStore((s) => s.sectionLevel); + + const planes = useMemo(() => ({ + x: new THREE.Plane(new THREE.Vector3(1, 0, 0), 0), + y: new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), + z: new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), + }), []); + + useEffect(() => { + // Configure each plane: keep the half-space on the positive side of the + // normal. Inverting flips the normal direction. + (['x', 'y', 'z'] as const).forEach((axis) => { + const plane = planes[axis]; + const sign = invert[axis] ? -1 : 1; + plane.normal.set( + axis === 'x' ? sign : 0, + axis === 'y' ? sign : 0, + axis === 'z' ? sign : 0, + ); + // For plane equation n·p + d = 0, keeping `n·p >= level*sign_axis` means + // d = -level when normal is +axis, d = +level when normal is -axis. + plane.constant = -sign * level[axis]; + }); + + const active: THREE.Plane[] = []; + if (enabled.x) active.push(planes.x); + if (enabled.y) active.push(planes.y); + if (enabled.z) active.push(planes.z); + + const groups = getAllModelLocalGroups(); + const touched: THREE.Material[] = []; + const apply = (clipping: THREE.Plane[]) => { + for (const g of groups) { + g.traverse((obj) => { + if (!(obj instanceof THREE.Mesh)) return; + const mats = Array.isArray(obj.material) ? obj.material : [obj.material]; + for (const mat of mats) { + if (!mat) continue; + mat.clippingPlanes = clipping; + mat.clipShadows = true; + mat.needsUpdate = true; + touched.push(mat); + } + }); + } + }; + apply(active); + + return () => { + // Cleanup: clear clipping from any material we touched. + for (const mat of touched) { + mat.clippingPlanes = []; + mat.needsUpdate = true; + } + }; + }, [enabled.x, enabled.y, enabled.z, invert.x, invert.y, invert.z, level.x, level.y, level.z, planes]); + + return null; +} + /** Mouse drag handler for desktop "Posicionar" mode: translates/rotates the active model. */ function PositionDragHandler() { const { camera, gl, scene } = useThree(); From 1222d284e3a1e9f61305104299bf5ef238953a03 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 22:53:59 +0000 Subject: [PATCH 07/12] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/SectionCutPanel.tsx | 222 +++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/components/SectionCutPanel.tsx diff --git a/src/components/SectionCutPanel.tsx b/src/components/SectionCutPanel.tsx new file mode 100644 index 0000000..7cf60cb --- /dev/null +++ b/src/components/SectionCutPanel.tsx @@ -0,0 +1,222 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { X, RefreshCw, Scissors } from 'lucide-react'; +import * as THREE from 'three'; +import { Button } from '@/components/ui/button'; +import { Slider } from '@/components/ui/slider'; +import { Switch } from '@/components/ui/switch'; +import { useModelStore } from '@/stores/useModelStore'; +import { getAllModelLocalGroups } from '@/lib/modelTransforms'; + +type Axis = 'x' | 'y' | 'z'; + +interface AxisRange { min: number; max: number; } + +function computeBoundsByAxis(): Record | null { + const groups = getAllModelLocalGroups(); + if (groups.length === 0) return null; + const box = new THREE.Box3(); + let has = false; + for (const g of groups) { + g.updateWorldMatrix(true, true); + const b = new THREE.Box3().setFromObject(g); + if (Number.isFinite(b.min.x) && Number.isFinite(b.max.x)) { + if (!has) { box.copy(b); has = true; } else box.union(b); + } + } + if (!has) return null; + return { + x: { min: box.min.x, max: box.max.x }, + y: { min: box.min.y, max: box.max.y }, + z: { min: box.min.z, max: box.max.z }, + }; +} + +const AXIS_COLOR: Record = { + x: 'text-red-400', + y: 'text-emerald-400', + z: 'text-sky-400', +}; + +const AXIS_LABEL: Record = { x: 'X', y: 'Y', z: 'Z' }; + +export function SectionCutPanel() { + const open = useModelStore((s) => s.sectionPanelOpen); + const opacity = useModelStore((s) => s.sectionPanelOpacity); + const setOpen = useModelStore((s) => s.setSectionPanelOpen); + const setOpacity = useModelStore((s) => s.setSectionPanelOpacity); + + const enabled = useModelStore((s) => s.sectionEnabled); + const invert = useModelStore((s) => s.sectionInvert); + const level = useModelStore((s) => s.sectionLevel); + const setSectionEnabled = useModelStore((s) => s.setSectionEnabled); + const setSectionInvert = useModelStore((s) => s.setSectionInvert); + const setSectionLevel = useModelStore((s) => s.setSectionLevel); + + const activeModelId = useModelStore((s) => s.activeModelId); + + const [bounds, setBounds] = useState | null>(null); + + // Compute bounds when the panel opens or active model changes. + useEffect(() => { + if (!open) return; + // Defer one tick to let scene finish updating world matrices. + const id = setTimeout(() => setBounds(computeBoundsByAxis()), 50); + return () => clearTimeout(id); + }, [open, activeModelId]); + + // Drag-to-move logic. + const [pos, setPos] = useState<{ x: number; y: number }>({ x: 16, y: 16 }); + const dragRef = useRef<{ ox: number; oy: number; sx: number; sy: number } | null>(null); + useEffect(() => { + const onMove = (e: PointerEvent) => { + if (!dragRef.current) return; + setPos({ + x: Math.max(8, dragRef.current.sx + (e.clientX - dragRef.current.ox)), + y: Math.max(8, dragRef.current.sy + (e.clientY - dragRef.current.oy)), + }); + }; + const onUp = () => { dragRef.current = null; }; + window.addEventListener('pointermove', onMove); + window.addEventListener('pointerup', onUp); + return () => { + window.removeEventListener('pointermove', onMove); + window.removeEventListener('pointerup', onUp); + }; + }, []); + + const recompute = () => setBounds(computeBoundsByAxis()); + + if (!open) return null; + + return ( +
+ {/* Header (drag handle) */} +
{ + dragRef.current = { ox: e.clientX, oy: e.clientY, sx: pos.x, sy: pos.y }; + }} + > +
+ + + Cortes de Seção + +
+ +
+ + {/* Window opacity */} +
+
+ + Opacidade da janela + + {Math.round(opacity * 100)}% +
+ setOpacity(v / 100)} + /> +
+ + {/* Per-axis controls */} +
+ {(['x', 'y', 'z'] as Axis[]).map((axis) => { + const range = bounds?.[axis]; + const minMM = range ? range.min * 1000 : 0; + const maxMM = range ? range.max * 1000 : 100; + const valMM = level[axis] * 1000; + const disabled = !range; + return ( +
+
+
+ + {AXIS_LABEL[axis]} + + setSectionEnabled(axis, c)} + disabled={disabled} + /> +
+ +
+ +
+ + {disabled ? '— sem peça' : `${minMM.toFixed(0)} … ${maxMM.toFixed(0)} mm`} + + + {disabled ? '—' : `${valMM.toFixed(1)} mm`} + +
+ + setSectionLevel(axis, v / 1000)} + disabled={disabled || !enabled[axis]} + /> + +
+ + + +
+
+ ); + })} +
+ +
+ +
+
+ ); +} From e032f1f3ad1d5b5301b992568901ddfafde9c3f7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 22:54:07 +0000 Subject: [PATCH 08/12] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/ViewerControls.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ViewerControls.tsx b/src/components/ViewerControls.tsx index 741304d..7a68f3e 100644 --- a/src/components/ViewerControls.tsx +++ b/src/components/ViewerControls.tsx @@ -1,4 +1,4 @@ -import { Eye, Grid3X3, Ruler, Trash2, Camera, LayoutGrid, Palette, Box, Move3D, Undo2, MousePointerClick, EyeOff, Focus, Download, Square } from 'lucide-react'; +import { Eye, Grid3X3, Ruler, Trash2, Camera, LayoutGrid, Palette, Box, Move3D, Undo2, MousePointerClick, EyeOff, Focus, Download, Square, Scissors } from 'lucide-react'; import { Slider } from '@/components/ui/slider'; import { Button } from '@/components/ui/button'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; From bdd5a92c173bf030071240cdb7bfe63dc17e75a7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 22:54:15 +0000 Subject: [PATCH 09/12] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/ViewerControls.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/ViewerControls.tsx b/src/components/ViewerControls.tsx index 7a68f3e..7b654f2 100644 --- a/src/components/ViewerControls.tsx +++ b/src/components/ViewerControls.tsx @@ -37,7 +37,9 @@ export function ViewerControls() { hideSelectedElements, isolateSelectedElements, showAllElements, clearElementSelection, cameraMode, setCameraMode, gridCalibMode, setGridCalibMode, setGridAutoFollow, + sectionPanelOpen, setSectionPanelOpen, sectionEnabled, resetSection, } = useModelStore(); + const sectionActive = sectionEnabled.x || sectionEnabled.y || sectionEnabled.z; const activeModel = models.find(m => m.id === activeModelId); const hasSelection = selectedElementKeys.size > 0; const hasHiddenOrIsolated = hiddenElementKeys.size > 0 || isolatedElementKeys !== null; From ac30db8241495223a09feea92ad89e35d5df7e94 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 22:54:33 +0000 Subject: [PATCH 10/12] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/ViewerControls.tsx | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/components/ViewerControls.tsx b/src/components/ViewerControls.tsx index 7b654f2..25a5682 100644 --- a/src/components/ViewerControls.tsx +++ b/src/components/ViewerControls.tsx @@ -133,9 +133,38 @@ export function ViewerControls() { + {/* Section cuts (X/Y/Z clipping planes) */} +
+ + {sectionActive && ( + + )} +
+ {/* Scale presets */} +