Pular para conteúdo

Tipos de componente

Uma extensão do EdgePy Viewer pode ser uma ferramenta de interface, um renderizador, um resolvedor de arquivos ou um processamento Python. Nesta trilha, usamos componente para falar dessas partes que você pode acrescentar ou substituir. Algumas são escritas em React, mas há outras formas de estender o viewer.

Escolha pelo que você quer mudar

Quero… Tipo de extensão Contrato ou ponto de composição Onde começar
Acrescentar uma ferramenta com interface própria Painel React ViewerCustomPanel → customPanels Meu primeiro componente
Inserir um botão em uma área existente Ação React toolbarActions, railActions, studyPanelActions, inspectorPanelActions Propriedades de PycomClinicalViewerNext
Apresentar imagens com outra biblioteca ou técnica Renderizador ClinicalViewportRenderer → viewportRenderer packages/react-viewer/src/ports/viewportRenderer.ts
Obter arquivos de outra fonte Resolvedor de assets ClinicalViewportAssetResolver → assetResolver Mesmo arquivo de portas do viewport
Processar pixels em Python Script ou agente Python Contexto e entradas do PyScript Lab Como usar o editor PyScript
Acrescentar leitura ou interpretação DICOM Adaptador de leitura Portas e adaptadores em packages/dicom Confira metadata.ts e a composição da importação
Receber estudos de outro sistema Integração host HostAssetManifest e comandos de estudo Como integrar com Django

Os quatro primeiros casos usam propriedades do viewer. Leitores DICOM, agentes e integrações host são conectados nos fluxos específicos da aplicação. Cada tipo de extensão tem seu próprio ponto de integração.

Um painel e um renderizador têm papéis diferentes

Um painel apresenta uma ferramenta ao lado da imagem. Pode ler o contexto ativo e pedir pixels, mas a renderização principal continua a cargo do renderer.

Um renderizador assume a área de imagem. Recebe um elemento HTML, o estudo, séries, assets resolvidos e callbacks. Ele monta sua visualização nesse elemento e fornece operações que o viewer poderá chamar.

App.tsx
  ├── customPanels=[meuPainel]          → acrescenta uma ferramenta
  └── viewportRenderer=meuRenderizador → escolhe quem desenha a imagem

Você pode trocar o renderizador e continuar usando o mesmo painel, desde que ele ofereça as operações necessárias. O histograma usa getCurrentImageData() e beginRectangularRoi(). Já o painel Olá funciona sem chamar o renderer.

Como criar outro renderizador

O contrato exige um id e um método render(context). Para uma implementação com recursos próprios, retorne um handle com destroy() para a limpeza. As demais operações são opcionais:

Operação do handle Quando implementar
destroy() Liberar canvas, listeners, recursos gráficos e tarefas associados à instância
update(context) Aplicar mudanças de janela, camadas e outras configurações recebidas
setSliceIndex(index) Permitir navegação de cortes pelo viewer
getCurrentImageData() Permitir análises que precisam dos pixels do corte
beginRectangularRoi(options) Selecionar uma região diretamente na imagem principal e informar suas coordenadas
resetView() Restaurar a apresentação inicial
setTemporaryOverlay(overlay) Apresentar um resultado temporário produzido por outra ferramenta

O laboratório desta trilha usa um renderizador sintético próprio, em vez do Cornerstone. Ele desenha um phantom de 256 × 256 pixels e uma matriz de referência 4 × 4, informa o corte ativo e entrega os pixels pelo mesmo contrato usado pelo histograma. Assim podemos experimentar sem baixar dados clínicos.

No laboratório, use Referência 4 × 4 e Imagem de exemplo para alternar entre os cortes, mantendo a interface do viewer. Abra Histograma no laboratório da próxima etapa para selecionar uma região diretamente nessa imagem.

Matriz numérica desenhada pelo renderizador sintético dentro do EdgePy Viewer.

O mesmo viewer, outro renderizador
Veja como um adaptador próprio desenha a imagem e responde à troca de corte.

Explorar o renderizador ↗

Abrir o renderizador em uma nova aba

Código completo do renderizador didático
import { bindRectangularRoi, type ClinicalViewportRenderer, type RectangularRoiBinding } from "@pycom-next/react-viewer";
import { createExampleImage } from "./histogram/exampleImage";
import { imageRange } from "./histogram/histogram";

// Phantom e matriz de referência no viewport principal, sem arquivos DICOM.
export const syntheticRenderer: ClinicalViewportRenderer = {
  id: "tutorial-synthetic",
  render(context) {
    const images = [createExampleImage(), {
      width: 4, height: 4, sliceIndex: 1, imageId: "tutorial-reference",
      values: Float32Array.from({ length: 16 }, (_, i) => i),
    }];
    let sliceIndex = 0;
    let disposed = false;
    let selection: RectangularRoiBinding | undefined;
    const surface = document.createElement("div");
    surface.style.cssText = "position:relative;margin:auto;flex:none";
    const canvas = document.createElement("canvas");
    canvas.setAttribute("aria-label", "Imagem principal do renderizador sintético");
    canvas.style.cssText = "width:100%;height:100%;display:block;image-rendering:pixelated";
    surface.append(canvas);
    context.element.append(surface);

    function draw() {
      const image = images[sliceIndex]!;
      const ctx = canvas.getContext("2d");
      if (!ctx) return;
      canvas.width = image.width;
      canvas.height = image.height;
      const [min, max] = imageRange(image.values);
      const pixels = ctx.createImageData(image.width, image.height);
      image.values.forEach((value, i) => {
        const gray = Math.round((value - min) * 255 / (max - min));
        pixels.data.set([gray, gray, gray, 255], i * 4);
      });
      ctx.putImageData(pixels, 0, 0);
      context.onViewportState?.({ sliceIndex, sliceCount: images.length });
      context.onStatus?.("Renderizador sintético · valores sem unidade física");
    }
    const resize = new ResizeObserver(() => {
      const size = Math.max(1, Math.min(context.element.clientWidth, context.element.clientHeight, 600));
      surface.style.width = `${size}px`;
      surface.style.height = `${size}px`;
      selection?.redraw();
    });
    resize.observe(context.element);
    draw();
    return {
      destroy() { disposed = true; selection?.invalidate(); resize.disconnect(); surface.remove(); },
      setSliceIndex(next) {
        if (disposed || !Number.isFinite(next)) return false;
        const index = Math.max(0, Math.min(images.length - 1, Math.round(next)));
        if (index !== sliceIndex) { selection?.invalidate(); sliceIndex = index; draw(); }
        return true;
      },
      async getCurrentImageData() {
        if (disposed) return undefined;
        const image = images[sliceIndex]!;
        return { ...image, values: image.values.slice(), seriesUid: context.activeSeries?.uid };
      },
      beginRectangularRoi(options) {
        const isCurrent = () => !disposed && options.image.imageId === images[sliceIndex]!.imageId && options.image.sliceIndex === sliceIndex;
        if (!isCurrent()) return undefined;
        selection?.invalidate();
        const { width, height } = options.image;
        selection = bindRectangularRoi({ ...options, element: surface, isCurrent,
          canvasToImage: ([x, y]) => [x / surface.clientWidth * width, y / surface.clientHeight * height],
          imageToCanvas: ([x, y]) => [x / width * surface.clientWidth, y / height * surface.clientHeight],
        });
        return selection;
      },
    };
  },
};

Na sua cópia da v0.3.0 do repositório público, extraia o ZIP de exemplos, conforme Comece aqui. O renderizador usa também histogram/exampleImage.ts e histogram/histogram.ts, incluídos nos exemplos. A capacidade beginRectangularRoi() já faz parte dessa versão do viewer.

Em apps/viewer-demo/src/App.tsx, acrescente o import:

import { syntheticRenderer } from "./syntheticRenderer";

No elemento PycomClinicalViewerNext, substitua viewportRenderer={imports.viewportRenderer} por:

viewportRenderer={syntheticRenderer}

Esse renderer sempre desenha as matrizes do exemplo, mesmo que você importe outro estudo. Depois de experimentar, restaure viewportRenderer={imports.viewportRenderer} para voltar ao Cornerstone e seguir os exercícios com imagens reais.

Um renderer de produção precisa lidar com geometria, mudanças de contexto, redimensionamento, erros de carregamento e liberação de recursos. O exemplo sintético cobre desenho, troca entre dois cortes e leitura de pixels. Quando oferecer getCurrentImageData(), retorne uma cópia coerente do corte com suas dimensões. Os painéis consumidores também precisam lidar com renderizadores que não oferecem essa leitura.

Onde fica a extensibilidade?

O contrato permite escolher a implementação na aplicação. O TypeScript confere se ela tem a estrutura esperada. Os testes e a interação com o viewer mostram se o comportamento está correto. Ao avaliar uma extensão, registre os contratos usados e as mudanças que foram necessárias fora deles.

Anterior: Comece aqui · Próximo: Meu primeiro componente.