+ A posição desliza o corte por todo o corpo carregado (não só pelo
+ centro) e a inclinação permite cortes oblíquos a partir de
+ qualquer um dos 3 planos anatômicos de base.
+
)}
diff --git a/frontend/src/components/viewer/Scene.tsx b/frontend/src/components/viewer/Scene.tsx
index fbc0d63..8fbba46 100644
--- a/frontend/src/components/viewer/Scene.tsx
+++ b/frontend/src/components/viewer/Scene.tsx
@@ -6,22 +6,47 @@ import { useAtlasStore, type BodyRegion, type ClipAxis } from "../../store/useAt
import { SystemModel } from "./SystemModel.js";
import type { Structure } from "../../lib/api.js";
-const AXIS_CONFIG: Record = {
- sagittal: { normal: [1, 0, 0], min: -0.4, max: 0.4 },
- coronal: { normal: [0, 0, 1], min: -0.3, max: 0.3 },
- transverse: { normal: [0, -1, 0], min: -1.8, max: 0 },
+// Normais de referência em anatomia: sagital separa esq./dir., coronal
+// (= frontal) separa ant./post., transversal separa sup./inf.
+const AXIS_NORMAL: Record = {
+ sagittal: [1, 0, 0],
+ coronal: [0, 0, 1],
+ transverse: [0, -1, 0],
};
+// Corte oblíquo = pega a normal de um dos 3 planos-base e inclina em torno
+// dos outros dois eixos (em graus) — cobre qualquer ângulo, não só os 3
+// puros. A posição do corte é sempre relativa ao bounding box REAL do
+// conteúdo carregado (não a um intervalo fixo chutado), então o corte
+// atravessa a parte do corpo certa não importa a escala/origem dos dados.
function useClippingPlanes(): THREE.Plane[] {
- const { enabled, axis, position } = useAtlasStore((s) => s.clipping);
+ const { enabled, axis, position, tiltX, tiltZ } = useAtlasStore((s) => s.clipping);
+ const contentBox = useAtlasStore((s) => s.contentBox);
+
return useMemo(() => {
- if (!enabled) return [];
- const cfg = AXIS_CONFIG[axis];
- const t = (position + 1) / 2; // normalize -1..1 -> 0..1
- const constant = cfg.min + t * (cfg.max - cfg.min);
- const normal = new THREE.Vector3(...cfg.normal);
- return [new THREE.Plane(normal, -constant)];
- }, [enabled, axis, position]);
+ if (!enabled || !contentBox) return [];
+
+ const normal = new THREE.Vector3(...AXIS_NORMAL[axis]);
+ const euler = new THREE.Euler(
+ THREE.MathUtils.degToRad(tiltX),
+ 0,
+ THREE.MathUtils.degToRad(tiltZ),
+ );
+ normal.applyEuler(euler).normalize();
+
+ const box = new THREE.Box3(
+ new THREE.Vector3(...contentBox.min),
+ new THREE.Vector3(...contentBox.max),
+ );
+ const center = box.getCenter(new THREE.Vector3());
+ const size = box.getSize(new THREE.Vector3());
+ const halfExtent =
+ (Math.abs(normal.x) * size.x + Math.abs(normal.y) * size.y + Math.abs(normal.z) * size.z) / 2;
+
+ const pointOnPlane = center.clone().addScaledVector(normal, position * halfExtent);
+ const constant = -normal.dot(pointOnPlane);
+ return [new THREE.Plane(normal, constant)];
+ }, [enabled, axis, position, tiltX, tiltZ, contentBox]);
}
function isFiniteVec3(v: THREE.Vector3): boolean {
@@ -77,6 +102,7 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
const { scene, camera } = useThree();
const focusRequest = useAtlasStore((s) => s.focusRequest);
const regionFocusRequest = useAtlasStore((s) => s.regionFocusRequest);
+ const setContentBox = useAtlasStore((s) => s.setContentBox);
// Malhas reais (Z-Anatomy) não seguem a mesma escala/origem que os
// modelos esquemáticos antigos (podem vir em cm, ou centralizadas em
@@ -88,6 +114,10 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
const id = requestAnimationFrame(() => {
const box = computeVisibleBoundingBox(scene);
if (box.isEmpty()) return;
+ setContentBox({
+ min: [box.min.x, box.min.y, box.min.z],
+ max: [box.max.x, box.max.y, box.max.z],
+ });
const size = box.getSize(new THREE.Vector3()).length();
// Ajusta os planos de corte da câmera ao tamanho real do conteúdo —
// um "far" fixo pequeno cortaria (tornaria invisível) um modelo bem
@@ -103,7 +133,7 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
});
});
return () => cancelAnimationFrame(id);
- }, [modelFiles, scene, camera]);
+ }, [modelFiles, scene, camera, setContentBox]);
useEffect(() => {
if (!focusRequest || !controlsRef.current) return;
diff --git a/frontend/src/store/useAtlasStore.ts b/frontend/src/store/useAtlasStore.ts
index b559d73..7205fa7 100644
--- a/frontend/src/store/useAtlasStore.ts
+++ b/frontend/src/store/useAtlasStore.ts
@@ -26,10 +26,20 @@ interface AtlasState {
regionFocusRequest: { region: BodyRegion; nonce: number } | null;
focusOnRegion: (region: BodyRegion) => void;
- clipping: { enabled: boolean; axis: ClipAxis; position: number };
+ // tiltX/tiltZ (graus) inclinam o plano de corte a partir do eixo base —
+ // é assim que se obtém um corte oblíquo em vez de só sagital/coronal/
+ // transversal puros.
+ clipping: { enabled: boolean; axis: ClipAxis; position: number; tiltX: number; tiltZ: number };
setClippingEnabled: (enabled: boolean) => void;
setClipAxis: (axis: ClipAxis) => void;
setClipPosition: (position: number) => void;
+ setClipTilt: (tiltX: number, tiltZ: number) => void;
+
+ // Bounding box real do conteúdo carregado (calculado a partir das malhas
+ // visíveis) — o plano de corte usa isso pra saber onde fica "o corpo",
+ // já que os dados reais do Z-Anatomy não têm escala/origem previsível.
+ contentBox: { min: [number, number, number]; max: [number, number, number] } | null;
+ setContentBox: (box: { min: [number, number, number]; max: [number, number, number] } | null) => void;
}
export const useAtlasStore = create((set) => ({
@@ -56,8 +66,12 @@ export const useAtlasStore = create((set) => ({
regionFocusRequest: { region, nonce: (state.regionFocusRequest?.nonce ?? 0) + 1 },
})),
- clipping: { enabled: false, axis: "sagittal", position: 0 },
+ clipping: { enabled: false, axis: "sagittal", position: 0, tiltX: 0, tiltZ: 0 },
setClippingEnabled: (enabled) => set((state) => ({ clipping: { ...state.clipping, enabled } })),
setClipAxis: (axis) => set((state) => ({ clipping: { ...state.clipping, axis } })),
setClipPosition: (position) => set((state) => ({ clipping: { ...state.clipping, position } })),
+ setClipTilt: (tiltX, tiltZ) => set((state) => ({ clipping: { ...state.clipping, tiltX, tiltZ } })),
+
+ contentBox: null,
+ setContentBox: (contentBox) => set({ contentBox }),
}));