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.
O mesmo viewer, outro renderizador
Veja como um adaptador próprio desenha a imagem e responde à troca de corte.
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:
No elemento PycomClinicalViewerNext, substitua viewportRenderer={imports.viewportRenderer} por:
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.