xingbofeng / xingbofeng.github.io

counterxing的博客
https://xingbofeng.github.io
175 stars 18 forks source link

cornerstone源码解析(二)——enable #26

Open xingbofeng opened 6 years ago

xingbofeng commented 6 years ago

cornerstone enable API

在每次loadImage之前都需要进行enable,至于enable到底做了哪些事情,对外来说确实是不可见的。

创建canvas节点

创建canvas节点前,首先判断这个element是否存在,如果存在,继续看是否option是否有webgloption。之后创建节点,并置为enabled element的子节点。

if (element === undefined) {
  throw new Error('enable: parameter element cannot be undefined');
}

// If this enabled element has the option set for WebGL, we should
// Check if this device actually supports it
// 如果设定webgl选项,判定浏览器是否支持webgl
if (options && options.renderer && options.renderer.toLowerCase() === 'webgl') {
  if (webGL.renderer.isWebGLAvailable()) {
    // If WebGL is available on the device, initialize the renderer
    // And return the renderCanvas from the WebGL rendering path
    webGL.renderer.initRenderer();
    options.renderer = 'webgl';
  } else {
    // If WebGL is not available on this device, we will fall back
    // To using the Canvas renderer
    console.error('WebGL not available, falling back to Canvas renderer');
    delete options.renderer;
  }
}

const canvas = document.createElement('canvas');

element.appendChild(canvas);

初始化enabledElement对象

enabledElement的作用是在cornerstone内部缓存与当前element相关的信息,包含是否需要刷新,image对象,viewport对象,elementcanvas节点等。

const enabledElement = {
  element,
  canvas,
  image: undefined, // Will be set once image is loaded
  invalid: false, // True if image needs to be drawn, false if not
  needsRedraw: true,
  options,
  layers: [],
  data: {},
  renderingTools: {}
};

全局cornerstone存储enabledElement列表

enabledElements.js提供了多个工具函数,用于获取到已经enable过的enabledElement对象:

首先会把所有enable过的enabledElement放在一个Array内:

const enabledElements = [];

在初始化的时候,会调用addEnabledElement(enabledElement)方法,把enabledElement放入数组:

addEnabledElement(enabledElement);

addEnabledElement(enabledElement)方法也很简单:

export function addEnabledElement (enabledElement) {
  if (enabledElement === undefined) {
    throw new Error('getEnabledElement: enabledElement element must not be undefined');
  }

  enabledElements.push(enabledElement);
}

关于根据对应的element查找对应的enabledElement对象的方法:

/**
 * Retrieves a Cornerstone Enabled Element object
 *
 * @param {HTMLElement} element An HTML Element enabled for Cornerstone
 *
 * @returns {EnabledElement} A Cornerstone Enabled Element
 */
export function getEnabledElement (element) {
  if (element === undefined) {
    throw new Error('getEnabledElement: parameter element must not be undefined');
  }
  for (let i = 0; i < enabledElements.length; i++) {
    if (enabledElements[i].element === element) {
      return enabledElements[i];
    }
  }

  throw new Error('element not enabled');
}
/**
 * Adds a Cornerstone Enabled Element object to the central store of enabledElements
 *
 * @param {string} imageId A Cornerstone Image ID
 * @returns {EnabledElement[]} An Array of Cornerstone enabledElement Objects
 */
export function getEnabledElementsByImageId (imageId) {
  const ees = [];

  enabledElements.forEach(function (enabledElement) {
    if (enabledElement.image && enabledElement.image.imageId === imageId) {
      ees.push(enabledElement);
    }
  });

  return ees;
}

/**
 * Retrieve all of the currently enabled Cornerstone elements
 *
 * @return {EnabledElement[]} An Array of Cornerstone enabledElement Objects
 */
export function getEnabledElements () {
  return enabledElements;
}

其实在这里发现每次查找都经历过一个循环,是否可以考虑更改为key-value。以imageIdkey

enabledElements初始化之后,会触发一次resize(),这时会设定canvas节点的宽高,这就是为何建议把element宽高定死的原因了:

/**
 * This module is responsible for enabling an element to display images with cornerstone
 *
 * @param {HTMLElement} element The DOM element enabled for Cornerstone
 * @param {HTMLElement} canvas The Canvas DOM element within the DOM element enabled for Cornerstone
 * @returns {void}
 */
function setCanvasSize (element, canvas) {
  // The device pixel ratio is 1.0 for normal displays and > 1.0
  // For high DPI displays like Retina
  /*

    This functionality is disabled due to buggy behavior on systems with mixed DPI's.  If the canvas
    is created on a display with high DPI (e.g. 2.0) and then the browser window is dragged to
    a different display with a different DPI (e.g. 1.0), the canvas is not recreated so the pageToPixel
    produces incorrect results.  I couldn't find any way to determine when the DPI changed other than
    by polling which is not very clean.  If anyone has any ideas here, please let me know, but for now
    we will disable this functionality.  We may want
    to add a mechanism to optionally enable this functionality if we can determine it is safe to do
    so (e.g. iPad or iPhone or perhaps enumerate the displays on the system.  I am choosing
    to be cautious here since I would rather not have bug reports or safety issues related to this
    scenario.

    var devicePixelRatio = window.devicePixelRatio;
    if(devicePixelRatio === undefined) {
        devicePixelRatio = 1.0;
    }
    */

  // Avoid setting the same value because it flashes the canvas with IE and Edge
  if (canvas.width !== element.clientWidth) {
    canvas.width = element.clientWidth;
    canvas.style.width = `${element.clientWidth}px`;
  }
  // Avoid setting the same value because it flashes the canvas with IE and Edge
  if (canvas.height !== element.clientHeight) {
    canvas.height = element.clientHeight;
    canvas.style.height = `${element.clientHeight}px`;
  }
}

/**
 * Resizes an enabled element and optionally fits the image to window
 *
 * @param {HTMLElement} element The DOM element enabled for Cornerstone
 * @param {Boolean} fitViewportToWindow true to refit, false to leave viewport parameters as they are
 * @returns {void}
 */
export default function (element, fitViewportToWindow) {

  const enabledElement = getEnabledElement(element);
  // 每次更改element,重新设定canvas的宽高,不然这时候dicom图片可能显示不完全
  setCanvasSize(element, enabledElement.canvas);

  const eventData = {
    element
  };
  // 发出cornerstoneelementresized事件
  triggerEvent(element, 'cornerstoneelementresized', eventData);
  // 必须保证有image对象
  if (enabledElement.image === undefined) {
    return;
  }
  // 是否要重设viewport
  if (fitViewportToWindow === true) {
    fitToWindow(element);
  } else {
    updateImage(element);
  }
}

如果resize()设定第二个参数fitViewportToWindowtrue,则会触发重设viewport的计算,如果没有设定(一般来说是宽高未改变的情况),这时不需要重设viewport对象。而在fitToWindow()方法里面则是显式调用过fitToWindow(element)

/**
 * Retrieves the current image dimensions given an enabled element
 *
 * @param {EnabledElement} enabledElement The Cornerstone Enabled Element
 * @return {{width, height}} The Image dimensions
 */
function getImageSize (enabledElement) {
  // 正立或倒立,宽就是宽,高就是高
  if (enabledElement.viewport.rotation === 0 || enabledElement.viewport.rotation === 180) {
    return {
      width: enabledElement.image.width,
      height: enabledElement.image.height
    };
  }
  // 如果旋转过90度,则宽高互换
  return {
    width: enabledElement.image.height,
    height: enabledElement.image.width
  };

}

/**
 * Adjusts an image's scale and translation so the image is centered and all pixels
 * in the image are viewable.
 *
 * @param {HTMLElement} element The Cornerstone element to update
 * @returns {void}
 */
export default function (element) {
  // fitToWindow主要就是做对于viewport的重新设定
  const enabledElement = getEnabledElement(element);
  const imageSize = getImageSize(enabledElement);

  const verticalScale = enabledElement.canvas.height / imageSize.height;
  const horizontalScale = enabledElement.canvas.width / imageSize.width;

  // The new scale is the minimum of the horizontal and vertical scale values
  enabledElement.viewport.scale = Math.min(horizontalScale, verticalScale);

  enabledElement.viewport.translation.x = 0;
  enabledElement.viewport.translation.y = 0;
  updateImage(element);
}

updateImage()方法则是核心,这里初看很简单,重点在下面:

/**
 * Forces the image to be updated/redrawn for the specified enabled element
 * @param {HTMLElement} element An HTML Element enabled for Cornerstone
 * @param {Boolean} [invalidated=false] Whether or not the image pixel data has been changed, necessitating a redraw
 *
 * @returns {void}
 */
export default function (element, invalidated = false) {
  const enabledElement = getEnabledElement(element);

  if (enabledElement.image === undefined && !enabledElement.layers.length) {
    throw new Error('updateImage: image has not been loaded yet');
  }

  drawImage(enabledElement, invalidated);
}

/**
 * Internal API function to draw an image to a given enabled element
 *
 * @param {EnabledElement} enabledElement The Cornerstone Enabled Element to redraw
 * @param {Boolean} [invalidated] - true if pixel data has been invalidated and cached rendering should not be used
 * @returns {void}
 */
export default function (enabledElement, invalidated = false) {
  enabledElement.needsRedraw = true;
  if (invalidated) {
    enabledElement.invalid = true;
  }
}

重点是drawImage(enabledElement, invalidated)方法的needsRedrawinvalidated,这里简单的把这两个参数设定为true,是为了在下面的draw方法使用。

初始化事件,轮询是否需要更新图片

  /**
   * Draw the image immediately
   *
   * @param {DOMHighResTimeStamp} timestamp The current time for when requestAnimationFrame starts to fire callbacks
   * @returns {void}
   */
  function draw (timestamp) {
    if (enabledElement.canvas === undefined) {
      return;
    }

    const eventDetails = {
      enabledElement,
      timestamp // 这个时间戳是requestAnimationFrame的默认参
    };
    // 发出cornerstoneprerender事件
    triggerEvent(enabledElement.element, 'cornerstoneprerender', eventDetails);
    // 相当于是轮询操作是否需要重绘
    if (enabledElement.needsRedraw && hasImageOrLayers(enabledElement)) {
      drawImageSync(enabledElement, enabledElement.invalid);
    }
    // draw会一直触发
    requestAnimationFrame(draw);
  }

  draw();

这是一个永远在轮询的操作,或许这就是cornerstone为何在运转太久不刷新页面之后会卡死的原因了。

// 相当于是轮询操作是否需要重绘
if (enabledElement.needsRedraw && hasImageOrLayers(enabledElement)) {
  drawImageSync(enabledElement, enabledElement.invalid);
}

一旦我们调用updateImage(),提示需要重绘,则会在下一次轮询到draw()触发的时候调用drawImageSync(enabledElement, enabledElement.invalid)方法进行画图。

思考:两点优化