<NME> frontend.ts
<BEF> /**
 * @license
 * Copyright 2016 Google Inc.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {ChunkState, LayerChunkProgressInfo} from 'neuroglancer/chunk_manager/base';
import {Chunk, ChunkManager, ChunkSource} from 'neuroglancer/chunk_manager/frontend';
import {LayerView, VisibleLayerInfo} from 'neuroglancer/layer';
import {PerspectivePanel} from 'neuroglancer/perspective_view/panel';
import {PerspectiveViewRenderContext, PerspectiveViewRenderLayer} from 'neuroglancer/perspective_view/render_layer';
import {RenderLayer, ThreeDimensionalRenderLayerAttachmentState, update3dRenderLayerAttachment} from 'neuroglancer/renderlayer';
import {getObjectColor, registerRedrawWhenSegmentationDisplayState3DChanged, SegmentationDisplayState3D, SegmentationLayerSharedObject} from 'neuroglancer/segmentation_display_state/frontend';
import {SKELETON_LAYER_RPC_ID, VertexAttributeInfo} from 'neuroglancer/skeleton/base';
import {SliceViewPanelRenderContext, SliceViewPanelRenderLayer} from 'neuroglancer/sliceview/panel';
import {TrackableValue, WatchableValue, WatchableValueInterface} from 'neuroglancer/trackable_value';
import {DataType} from 'neuroglancer/util/data_type';
import {Borrowed, RefCounted} from 'neuroglancer/util/disposable';
import {mat4, vec3} from 'neuroglancer/util/geom';
import {verifyString} from 'neuroglancer/util/json';
import {NullarySignal} from 'neuroglancer/util/signal';
import {Buffer} from 'neuroglancer/webgl/buffer';
import {CircleShader} from 'neuroglancer/webgl/circles';
import glsl_COLORMAPS from 'neuroglancer/webgl/colormaps.glsl';
import {defineCircleShader, drawCircles, initializeCircleShader} from 'neuroglancer/webgl/circles';
import {glsl_COLORMAPS} from 'neuroglancer/webgl/colormaps';
import {GL} from 'neuroglancer/webgl/context';
import {makeTrackableFragmentMain, parameterizedEmitterDependentShaderGetter, shaderCodeWithLineDirective, WatchableShaderError} from 'neuroglancer/webgl/dynamic_shader';
import {defineLineShader, drawLines, initializeLineShader} from 'neuroglancer/webgl/lines';
import {ShaderBuilder, ShaderProgram, ShaderSamplerType} from 'neuroglancer/webgl/shader';
import {addControlsToBuilder, getFallbackBuilderState, parseShaderUiControls, setControlsInShader, ShaderControlsBuilderState, ShaderControlState} from 'neuroglancer/webgl/shader_ui_controls';
import {computeTextureFormat, getSamplerPrefixForDataType, OneDimensionalTextureAccessHelper, setOneDimensionalTextureData, TextureFormat} from 'neuroglancer/webgl/texture_access';
import {defineVertexId, VertexIdHelper} from 'neuroglancer/webgl/vertex_id';

const tempMat2 = mat4.create();

const DEFAULT_FRAGMENT_MAIN = `void main() {
  emitDefault();
}
`;

interface VertexAttributeRenderInfo extends VertexAttributeInfo {
  name: string;
  webglDataType: number;
  glslDataType: string;
}

const vertexAttributeSamplerSymbols: Symbol[] = [];

const vertexPositionTextureFormat = computeTextureFormat(new TextureFormat(), DataType.FLOAT32, 3);

class RenderHelper extends RefCounted {
  private textureAccessHelper = new OneDimensionalTextureAccessHelper('vertexData');
  private vertexIdHelper = this.registerDisposer(VertexIdHelper.get(this.gl));
  get vertexAttributes(): VertexAttributeRenderInfo[] {
    return this.base.vertexAttributes;
  }

  defineCommonShader(builder: ShaderBuilder) {
    defineVertexId(builder);
    builder.addUniform('highp vec4', 'uColor');
    builder.addUniform('highp mat4', 'uProjection');
    builder.addUniform('highp uint', 'uPickID');
  }

  edgeShaderGetter = parameterizedEmitterDependentShaderGetter(this, this.gl, {
    memoizeKey:
        {type: 'skeleton/SkeletonShaderManager/edge', vertexAttributes: this.vertexAttributes},
    fallbackParameters: this.base.fallbackShaderParameters,
    parameters: this.base.displayState.skeletonRenderingOptions.shaderControlState.builderState,
  edgeShaderGetter = parameterizedEmitterDependentShaderGetter(
      this, this.gl,
      {type: 'skeleton/SkeletonShaderManager/edge', vertexAttributes: this.vertexAttributes},
      this.base.fallbackFragmentMain, this.base.displayState.fragmentMain,
      this.base.displayState.shaderError, (builder: ShaderBuilder, fragmentMain: string) => {
        this.defineAttributeAccess(builder);
        this.lineShader.defineShader(builder);
          this.defineAttributeAccess(builder);
          defineLineShader(builder);
          builder.addAttribute('highp uvec2', 'aVertexIndex');
          builder.addUniform('highp float', 'uLineWidth');
          let vertexMain = `
highp vec3 vertexA = readAttribute0(aVertexIndex.x);
highp vec3 vertexB = readAttribute0(aVertexIndex.y);
emitLine(uProjection, vertexA, vertexB, uLineWidth);
highp uint lineEndpointIndex = getLineEndpointIndex();
highp uint vertexIndex = aVertexIndex.x * lineEndpointIndex + aVertexIndex.y * (1u - lineEndpointIndex);
`;

          builder.addFragmentCode(`
vec4 segmentColor() {
  return uColor;
}
void emitRGB(vec3 color) {
  emit(vec4(color * uColor.a, uColor.a * getLineAlpha() * ${
              this.getCrossSectionFadeFactor()}), uPickID);
}
void emitDefault() {
  emit(vec4(uColor.rgb, uColor.a * getLineAlpha() * ${this.getCrossSectionFadeFactor()}), uPickID);
}
`);
          builder.addFragmentCode(glsl_COLORMAPS);
          const {vertexAttributes} = this;
          const numAttributes = vertexAttributes.length;
          for (let i = 1; i < numAttributes; ++i) {
            const info = vertexAttributes[i];
            builder.addVarying(`highp ${info.glslDataType}`, `vCustom${i}`);
            vertexMain += `vCustom${i} = readAttribute${i}(vertexIndex);\n`;
            builder.addFragmentCode(`#define ${info.name} vCustom${i}\n`);
          }
          builder.setVertexMain(vertexMain);
          addControlsToBuilder(shaderBuilderState, builder);
  nodeShaderGetter = parameterizedEmitterDependentShaderGetter(
      this, this.gl,
      {type: 'skeleton/SkeletonShaderManager/node', vertexAttributes: this.vertexAttributes},
      this.base.fallbackFragmentMain, this.base.displayState.fragmentMain,
      this.base.displayState.shaderError, (builder: ShaderBuilder, fragmentMain: string) => {
        this.defineAttributeAccess(builder);
        this.circleShader.defineShader(builder, /*crossSectionFade=*/ this.targetIsSliceView);
        {type: 'skeleton/SkeletonShaderManager/node', vertexAttributes: this.vertexAttributes},
    fallbackParameters: this.base.fallbackShaderParameters,
    parameters: this.base.displayState.skeletonRenderingOptions.shaderControlState.builderState,
    shaderError: this.base.displayState.shaderError,
    defineShader:
        (builder: ShaderBuilder, shaderBuilderState: ShaderControlsBuilderState) => {
          if (shaderBuilderState.parseResult.errors.length !== 0) {
            throw new Error('Invalid UI control specification');
vec4 segmentColor() {
  return uColor;
}
void emitRGB(vec3 color) {
  vec4 borderColor = vec4(0.0, 0.0, 0.0, 1.0);
  emit(getCircleColor(vec4(color, 1.0), borderColor), uPickID);
}
void emitDefault() {
  vec4 borderColor = vec4(0.0, 0.0, 0.0, 1.0);
  emit(getCircleColor(uColor, borderColor), uPickID);
}
`);
        builder.addFragmentCode(glsl_COLORMAPS);
void emitRGBA(vec4 color) {
  vec4 borderColor = color;
  emit(getCircleColor(color, borderColor), uPickID);
}
void emitRGB(vec3 color) {
  emitRGBA(vec4(color, 1.0));
}
void emitDefault() {
  emitRGBA(uColor);
}
`);
          builder.addFragmentCode(glsl_COLORMAPS);
          const {vertexAttributes} = this;
          const numAttributes = vertexAttributes.length;
          for (let i = 1; i < numAttributes; ++i) {
            const info = vertexAttributes[i];
            builder.addVarying(`highp ${info.glslDataType}`, `vCustom${i}`);
            vertexMain += `vCustom${i} = readAttribute${i}(vertexIndex);\n`;
            builder.addFragmentCode(`#define ${info.name} vCustom${i}\n`);
          }
          builder.setVertexMain(vertexMain);
          addControlsToBuilder(shaderBuilderState, builder);
          builder.setFragmentMainFunction(
              shaderCodeWithLineDirective(shaderBuilderState.parseResult.code));
        },
  });

  get gl(): GL {
    return this.base.gl;
  }

  constructor(public base: SkeletonLayer, public targetIsSliceView: boolean) {
    super();
  }

  defineAttributeAccess(builder: ShaderBuilder) {
    const {textureAccessHelper} = this;
    textureAccessHelper.defineShader(builder);
    const numAttributes = this.vertexAttributes.length;
    for (let j = vertexAttributeSamplerSymbols.length; j < numAttributes; ++j) {
      vertexAttributeSamplerSymbols[j] = Symbol(`SkeletonShader.vertexAttributeTextureUnit${j}`);
    }
    this.vertexAttributes.forEach((info, i) => {
      builder.addTextureSampler(
          `${getSamplerPrefixForDataType(info.dataType)}sampler2D` as ShaderSamplerType,
          `uVertexAttributeSampler${i}`, vertexAttributeSamplerSymbols[i]);
      builder.addVertexCode(textureAccessHelper.getAccessor(
          `readAttribute${i}`, `uVertexAttributeSampler${i}`, info.dataType, info.numComponents));
    });
  }

  getCrossSectionFadeFactor() {
    if (this.targetIsSliceView) {
      return `(clamp(1.0 - 2.0 * abs(0.5 - gl_FragCoord.z), 0.0, 1.0))`;
    } else {
      return `(1.0)`;
    }
  }

  beginLayer(
      gl: GL, shader: ShaderProgram,
  drawSkeleton(
      gl: GL, edgeShader: ShaderProgram, nodeShader: ShaderProgram|null,
      skeletonChunk: SkeletonChunk, renderContext: {viewportWidth: number, viewportHeight: number},
    lineWidth: number) {
    const {vertexAttributes} = this;
    const numAttributes = vertexAttributes.length;
    const {vertexAttributeTextures} = skeletonChunk;
    gl.uniform4fv(shader.uniform('uColor'), color);
  }

  setPickID(gl: GL, shader: ShaderProgram, pickID: number) {
    gl.uniform1ui(shader.uniform('uPickID'), pickID);
  }

  drawSkeleton(
      gl: GL, edgeShader: ShaderProgram, nodeShader: ShaderProgram|null,
      skeletonChunk: SkeletonChunk, projectionParameters: {width: number, height: number}) {
    const {vertexAttributes} = this;
    const numAttributes = vertexAttributes.length;
    const {vertexAttributeTextures} = skeletonChunk;
    for (let i = 0; i < numAttributes; ++i) {
      const textureUnit = WebGL2RenderingContext.TEXTURE0 +
          edgeShader.textureUnit(vertexAttributeSamplerSymbols[i]);
      gl.activeTexture(textureUnit);
      gl.bindTexture(WebGL2RenderingContext.TEXTURE_2D, vertexAttributeTextures[i]);
    }

    // Draw edges
    {
      edgeShader.bind();
      const aVertexIndex = edgeShader.attribute('aVertexIndex');
      this.textureAccessHelper.setupTextureLayout(gl, nodeShader, skeletonChunk);
      this.circleShader.draw(
          nodeShader, renderContext, {
            interiorRadiusInPixels: 5,
            borderWidthInPixels: 0,
            featherWidthInPixels: this.targetIsSliceView ? 1.0 : 0.0,
          },
    }

    if (nodeShader !== null) {
      nodeShader.bind();
      initializeCircleShader(
          nodeShader, projectionParameters,
          {featherWidthInPixels: this.targetIsSliceView ? 1.0 : 0.0});
      drawCircles(nodeShader.gl, 2, skeletonChunk.numVertices);
    }
  }

  endLayer(gl: GL, shader: ShaderProgram) {
    const {vertexAttributes} = this;
  }
}

export interface SkeletonLayerDisplayState extends SegmentationDisplayState3D {
  shaderError: WatchableShaderError;
  fragmentMain: TrackableValue<string>;
  showSkeletonNodes: WatchableValueInterface<boolean>;
}

export class SkeletonLayer extends RefCounted {
      if (v !== undefined) return obj;
    }
    return undefined;
  }
}

export interface SkeletonLayerDisplayState extends SegmentationDisplayState3D {
  shaderError: WatchableShaderError;
  skeletonRenderingOptions: SkeletonRenderingOptions;
}

export class SkeletonLayer extends RefCounted {
  layerChunkProgressInfo = new LayerChunkProgressInfo();
  redrawNeeded = new NullarySignal();

    registerRedrawWhenSegmentationDisplayState3DChanged(displayState, this);
    this.displayState.shaderError.value = undefined;
    this.registerDisposer(displayState.fragmentMain.changed.add(() => {
      this.displayState.shaderError.value = undefined;
      this.redrawNeeded.dispatch();
    }));
    this.registerDisposer(displayState.showSkeletonNodes.changed.add(this.redrawNeeded.dispatch));
    let sharedObject = this.sharedObject =
        this.registerDisposer(new SegmentationLayerSharedObject(chunkManager, displayState));
    sharedObject.RPC_TYPE_ID = SKELETON_LAYER_RPC_ID;
      public displayState: SkeletonLayerDisplayState) {
    super();

    registerRedrawWhenSegmentationDisplayState3DChanged(displayState, this);
    this.displayState.shaderError.value = undefined;
    const {skeletonRenderingOptions: renderingOptions} = displayState;
    this.registerDisposer(renderingOptions.shader.changed.add(() => {
      this.displayState.shaderError.value = undefined;
      this.redrawNeeded.dispatch();
    }));
    let sharedObject = this.sharedObject = this.registerDisposer(
        new SegmentationLayerSharedObject(chunkManager, displayState, this.layerChunkProgressInfo));
    sharedObject.RPC_TYPE_ID = SKELETON_LAYER_RPC_ID;
    sharedObject.initializeCounterpartWithChunkManager({
      'source': source.addCounterpartRef(),
    });

    const vertexAttributes = this.vertexAttributes = [vertexPositionAttribute];

    for (let [name, info] of source.vertexAttributes) {

  draw(
      renderContext: SliceViewPanelRenderContext|PerspectiveViewRenderContext, layer: RenderLayer,
      renderHelper: RenderHelper, lineWidth?: number) {
    if (lineWidth === undefined) {
      lineWidth = renderContext.emitColor ? 1 : 5;
    }
    let {gl, source, displayState} = this;
    let alpha = Math.min(1.0, displayState.objectAlpha.value);
  get gl() {
      // Skip drawing.
      return;
    }
    const showSkeletonNodes = this.displayState.showSkeletonNodes.value;

    const edgeShader = renderHelper.edgeShaderGetter(renderContext.emitter);
    const nodeShader = renderHelper.nodeShaderGetter(renderContext.emitter);
    }
    const modelMatrix = update3dRenderLayerAttachment(
        displayState.transform.value, renderContext.projectionParameters.displayDimensionRenderInfo,
        attachment);
    if (modelMatrix === undefined) return;
    let pointDiameter: number;
    if (renderOptions.mode.value === SkeletonRenderMode.LINES_AND_POINTS) {
      pointDiameter = Math.max(5, lineWidth * 2);
    } else {
      pointDiameter = lineWidth;
    }

    const edgeShaderResult = renderHelper.edgeShaderGetter(renderContext.emitter);
    const nodeShaderResult = renderHelper.nodeShaderGetter(renderContext.emitter);
    const {shader: edgeShader, parameters: edgeShaderParameters} = edgeShaderResult;
    const {shader: nodeShader, parameters: nodeShaderParameters} = nodeShaderResult;
    if (edgeShader === null || nodeShader === null) {
      // Shader error, skip drawing.
      return;
    }

    const {shaderControlState} = this.displayState.skeletonRenderingOptions;

    edgeShader.bind();
    renderHelper.beginLayer(gl, edgeShader, renderContext, modelMatrix);
    setControlsInShader(
        gl, edgeShader, shaderControlState, edgeShaderParameters.parseResult.controls);
    gl.uniform1f(edgeShader.uniform('uLineWidth'), lineWidth!);

    nodeShader.bind();
    renderHelper.beginLayer(gl, nodeShader, renderContext, modelMatrix);
    gl.uniform1f(nodeShader.uniform('uNodeDiameter'), pointDiameter);
    setControlsInShader(
        gl, nodeShader, shaderControlState, nodeShaderParameters.parseResult.controls);

    const skeletons = source.chunks;

    forEachVisibleSegmentToDraw(
        displayState, layer, renderContext.emitColor,
        renderContext.emitPickID ? renderContext.pickIDs : undefined,
        (objectId, color, pickIndex) => {
        renderHelper.setPickID(gl, nodeShader, pickIDs.registerUint64(layer, objectId));
      }
      renderHelper.drawSkeleton(
          gl, edgeShader, showSkeletonNodes ? nodeShader : null, skeleton, renderContext, lineWidth!
      );
    });
    renderHelper.endLayer(gl, edgeShader);
  }
            renderHelper.setColor(gl, edgeShader, <vec3><Float32Array>color);

export class PerspectiveViewSkeletonLayer extends PerspectiveViewRenderLayer {
  private renderHelper = this.registerDisposer(new RenderHelper(this.base, false));

  constructor(public base: SkeletonLayer) {
    super();
    this.registerDisposer(base);
    this.registerDisposer(base.redrawNeeded.add(() => {
      this.redrawNeeded.dispatch();
    }));
    this.setReady(true);
    this.registerDisposer(base.visibility.add(this.visibility));
  }
  isReady() {
    const {source, displayState} = this;
    if (displayState.objectAlpha.value <= 0.0) {
      // Skip drawing.
      return true;
    }
  }

  draw(renderContext: PerspectiveViewRenderContext) {
    this.base.draw(renderContext, this, this.renderHelper);
  }
}

export class SliceViewPanelSkeletonLayer extends SliceViewPanelRenderLayer {
  private renderHelper = this.registerDisposer(new RenderHelper(this.base, true));

  constructor(public base: SkeletonLayer) {
    super();
    this.registerDisposer(base);
    this.registerDisposer(base.redrawNeeded.add(() => {
      this.redrawNeeded.dispatch();
    }));
    this.setReady(true);
    this.registerDisposer(base.visibility.add(this.visibility));
  }
  constructor(public base: SkeletonLayer) {
    super();
  }

  draw(renderContext: SliceViewPanelRenderContext) {
    this.base.draw(renderContext, this, this.renderHelper, 1);
  }
}

  }
  get gl() {
    return this.base.gl;
  }

  get isTransparent() {
    return this.base.displayState.objectAlpha.value < 1.0;
  }

  draw(
      renderContext: PerspectiveViewRenderContext,
      attachment: VisibleLayerInfo<PerspectivePanel, ThreeDimensionalRenderLayerAttachmentState>) {
    if (!renderContext.emitColor && renderContext.alreadyEmittedPickID) {
      // No need for a separate pick ID pass.
      return;
    }
    this.base.draw(renderContext, this, this.renderHelper, this.renderOptions, attachment);
  }

  isReady() {
    return this.base.isReady();
  }
}

export class SliceViewPanelSkeletonLayer extends SliceViewPanelRenderLayer {
  private renderHelper = this.registerDisposer(new RenderHelper(this.base, true));
  private renderOptions = this.base.displayState.skeletonRenderingOptions.params2d;
  constructor(public base: SkeletonLayer) {
    super();
    this.layerChunkProgressInfo = base.layerChunkProgressInfo;
    this.registerDisposer(base);
    const {renderOptions} = this;
    this.registerDisposer(renderOptions.mode.changed.add(this.redrawNeeded.dispatch));
    this.registerDisposer(renderOptions.lineWidth.changed.add(this.redrawNeeded.dispatch));
    this.registerDisposer(base.redrawNeeded.add(this.redrawNeeded.dispatch));
    this.registerDisposer(base.visibility.add(this.visibility));
  }
  get gl() {
    return this.base.gl;
  }

  draw(
      renderContext: SliceViewPanelRenderContext,
      attachment: VisibleLayerInfo<SliceViewPanel, ThreeDimensionalRenderLayerAttachmentState>) {
    this.base.draw(renderContext, this, this.renderHelper, this.renderOptions, attachment);
  }

  isReady() {
    return this.base.isReady();
  }
}

function getWebglDataType(dataType: DataType) {
  switch (dataType) {
    case DataType.FLOAT32:
      return WebGL2RenderingContext.FLOAT;
    default:
      throw new Error(`Data type not supported by WebGL: ${DataType[dataType]}`);
  }
}

const vertexPositionAttribute: VertexAttributeRenderInfo = {
  dataType: DataType.FLOAT32,
  numComponents: 3,
  name: '',
  webglDataType: WebGL2RenderingContext.FLOAT,
  glslDataType: 'vec3',
};

export class SkeletonChunk extends Chunk {
  source: SkeletonSource;
  vertexAttributes: Uint8Array;
  indices: Uint32Array;
  indexBuffer: Buffer;
  numIndices: number;
  numVertices: number;
  vertexAttributeOffsets: Uint32Array;
  vertexAttributeTextures: (WebGLTexture|null)[];

  constructor(source: SkeletonSource, x: any) {
    super(source);
    this.vertexAttributes = x['vertexAttributes'];
    let indices = this.indices = x['indices'];
    this.numVertices = x['numVertices'];
    this.vertexAttributeOffsets = x['vertexAttributeOffsets'];
    this.numIndices = indices.length;
  }

  copyToGPU(gl: GL) {
    super.copyToGPU(gl);
    const {attributeTextureFormats} = this.source;
    const {vertexAttributes, vertexAttributeOffsets} = this;
    const vertexAttributeTextures: (WebGLTexture|null)[] = this.vertexAttributeTextures = [];
    for (let i = 0, numAttributes = vertexAttributeOffsets.length; i < numAttributes; ++i) {
      const texture = gl.createTexture();
      gl.bindTexture(WebGL2RenderingContext.TEXTURE_2D, texture);
      setOneDimensionalTextureData(
          gl, attributeTextureFormats[i],
          vertexAttributes.subarray(
              vertexAttributeOffsets[i],
              i + 1 !== numAttributes ? vertexAttributeOffsets[i + 1] : vertexAttributes.length));
      vertexAttributeTextures[i] = texture;
    }
    gl.bindTexture(WebGL2RenderingContext.TEXTURE_2D, null);
    this.indexBuffer = Buffer.fromData(
        gl, this.indices, WebGL2RenderingContext.ARRAY_BUFFER, WebGL2RenderingContext.STATIC_DRAW);
  }

  freeGPUMemory(gl: GL) {
    super.freeGPUMemory(gl);
    const {vertexAttributeTextures} = this;
    for (const texture of vertexAttributeTextures) {
      gl.deleteTexture(texture);
    }
    vertexAttributeTextures.length = 0;
    this.indexBuffer.dispose();
  }
}

const emptyVertexAttributes = new Map<string, VertexAttributeInfo>();

function getAttributeTextureFormats(vertexAttributes: Map<string, VertexAttributeInfo>):
    TextureFormat[] {
  const attributeTextureFormats: TextureFormat[] = [vertexPositionTextureFormat];
  for (const info of vertexAttributes.values()) {
    attributeTextureFormats.push(
        computeTextureFormat(new TextureFormat(), info.dataType, info.numComponents));
  }
  return attributeTextureFormats;
}

export interface SkeletonSourceOptions {}

export class SkeletonSource extends ChunkSource {
  private attributeTextureFormats_?: TextureFormat[];

  get attributeTextureFormats() {
    let attributeTextureFormats = this.attributeTextureFormats_;
    if (attributeTextureFormats === undefined) {
      attributeTextureFormats = this.attributeTextureFormats_ =
          getAttributeTextureFormats(this.vertexAttributes);
    }
    return attributeTextureFormats;
  }

  chunks: Map<string, SkeletonChunk>;
  getChunk(x: any) {
    return new SkeletonChunk(this, x);
  }

  constructor(chunkManager: Borrowed<ChunkManager>, options: SkeletonSourceOptions) {
    super(chunkManager, options);
  }

  get vertexAttributes(): Map<string, VertexAttributeInfo> {
    return emptyVertexAttributes;
  }
}

<MSG> feat(skeleton): add skeleton rendering mode/line width options

<DFF> @@ -23,12 +23,14 @@ import {forEachVisibleSegment, getObjectKey} from 'neuroglancer/segmentation_dis
 import {getObjectColor, registerRedrawWhenSegmentationDisplayState3DChanged, SegmentationDisplayState3D, SegmentationLayerSharedObject} from 'neuroglancer/segmentation_display_state/frontend';
 import {SKELETON_LAYER_RPC_ID, VertexAttributeInfo} from 'neuroglancer/skeleton/base';
 import {SliceViewPanelRenderContext, SliceViewPanelRenderLayer} from 'neuroglancer/sliceview/panel';
-import {TrackableValue, WatchableValue, WatchableValueInterface} from 'neuroglancer/trackable_value';
+import {TrackableValue, WatchableValue} from 'neuroglancer/trackable_value';
 import {DataType} from 'neuroglancer/util/data_type';
 import {Borrowed, RefCounted} from 'neuroglancer/util/disposable';
 import {mat4, vec3} from 'neuroglancer/util/geom';
-import {verifyString} from 'neuroglancer/util/json';
+import {verifyFinitePositiveFloat, verifyString} from 'neuroglancer/util/json';
 import {NullarySignal} from 'neuroglancer/util/signal';
+import {CompoundTrackable, Trackable} from 'neuroglancer/util/trackable';
+import {TrackableEnum} from 'neuroglancer/util/trackable_enum';
 import {Buffer} from 'neuroglancer/webgl/buffer';
 import {CircleShader} from 'neuroglancer/webgl/circles';
 import glsl_COLORMAPS from 'neuroglancer/webgl/colormaps.glsl';
@@ -81,7 +83,7 @@ class RenderHelper extends RefCounted {
   edgeShaderGetter = parameterizedEmitterDependentShaderGetter(
       this, this.gl,
       {type: 'skeleton/SkeletonShaderManager/edge', vertexAttributes: this.vertexAttributes},
-      this.base.fallbackFragmentMain, this.base.displayState.fragmentMain,
+      this.base.fallbackFragmentMain, this.base.displayState.skeletonRenderingOptions.shader,
       this.base.displayState.shaderError, (builder: ShaderBuilder, fragmentMain: string) => {
         this.defineAttributeAccess(builder);
         this.lineShader.defineShader(builder);
@@ -123,7 +125,7 @@ void emitDefault() {
   nodeShaderGetter = parameterizedEmitterDependentShaderGetter(
       this, this.gl,
       {type: 'skeleton/SkeletonShaderManager/node', vertexAttributes: this.vertexAttributes},
-      this.base.fallbackFragmentMain, this.base.displayState.fragmentMain,
+      this.base.fallbackFragmentMain, this.base.displayState.skeletonRenderingOptions.shader,
       this.base.displayState.shaderError, (builder: ShaderBuilder, fragmentMain: string) => {
         this.defineAttributeAccess(builder);
         this.circleShader.defineShader(builder, /*crossSectionFade=*/ this.targetIsSliceView);
@@ -138,13 +140,15 @@ emitCircle(uProjection * vec4(vertexPosition, 1.0));
 vec4 segmentColor() {
   return uColor;
 }
+void emitRGBA(vec4 color) {
+  vec4 borderColor = color;
+  emit(getCircleColor(color, borderColor), uPickID);
+}
 void emitRGB(vec3 color) {
-  vec4 borderColor = vec4(0.0, 0.0, 0.0, 1.0);
-  emit(getCircleColor(vec4(color, 1.0), borderColor), uPickID);
+  emitRGBA(vec4(color, 1.0));
 }
 void emitDefault() {
-  vec4 borderColor = vec4(0.0, 0.0, 0.0, 1.0);
-  emit(getCircleColor(uColor, borderColor), uPickID);
+  emitRGBA(uColor);
 }
 `);
         builder.addFragmentCode(glsl_COLORMAPS);
@@ -212,7 +216,8 @@ void emitDefault() {
   drawSkeleton(
       gl: GL, edgeShader: ShaderProgram, nodeShader: ShaderProgram|null,
       skeletonChunk: SkeletonChunk, renderContext: {viewportWidth: number, viewportHeight: number},
-    lineWidth: number) {
+    lineWidth: number,
+    pointDiameter: number) {
     const {vertexAttributes} = this;
     const numAttributes = vertexAttributes.length;
     const {vertexAttributeTextures} = skeletonChunk;
@@ -243,7 +248,7 @@ void emitDefault() {
       this.textureAccessHelper.setupTextureLayout(gl, nodeShader, skeletonChunk);
       this.circleShader.draw(
           nodeShader, renderContext, {
-            interiorRadiusInPixels: 5,
+            interiorRadiusInPixels: pointDiameter / 2,
             borderWidthInPixels: 0,
             featherWidthInPixels: this.targetIsSliceView ? 1.0 : 0.0,
           },
@@ -263,10 +268,72 @@ void emitDefault() {
   }
 }
 
+export enum SkeletonRenderMode {
+  LINES,
+  LINES_AND_POINTS,
+}
+
+export class TrackableSkeletonRenderMode extends TrackableEnum<SkeletonRenderMode> {
+  constructor(value: SkeletonRenderMode, defaultValue: SkeletonRenderMode = value) {
+    super(SkeletonRenderMode, value, defaultValue);
+  }
+}
+
+export class TrackableSkeletonLineWidth extends TrackableValue<number> {
+  constructor(value: number, defaultValue: number = value) {
+    super(value, verifyFinitePositiveFloat, defaultValue);
+  }
+}
+
+export interface ViewSpecificSkeletonRenderingOptions {
+  mode: TrackableSkeletonRenderMode;
+  lineWidth: TrackableSkeletonLineWidth;
+}
+
+export class SkeletonRenderingOptions implements Trackable {
+  private compound = new CompoundTrackable();
+  get changed() {
+    return this.compound.changed;
+  }
+
+  shader = getTrackableFragmentMain();
+  params2d: ViewSpecificSkeletonRenderingOptions = {
+    mode: new TrackableSkeletonRenderMode(SkeletonRenderMode.LINES_AND_POINTS),
+    lineWidth: new TrackableSkeletonLineWidth(5),
+  };
+  params3d: ViewSpecificSkeletonRenderingOptions = {
+    mode: new TrackableSkeletonRenderMode(SkeletonRenderMode.LINES),
+    lineWidth: new TrackableSkeletonLineWidth(2),
+  };
+
+  constructor() {
+    const {compound} = this;
+    compound.add('shader', this.shader);
+    compound.add('mode2d', this.params2d.mode);
+    compound.add('lineWidth2d', this.params2d.lineWidth);
+    compound.add('mode3d', this.params3d.mode);
+    compound.add('lineWidth3d', this.params3d.lineWidth);
+  }
+
+  reset() {
+    this.compound.reset();
+  }
+
+  restoreState(obj: any) {
+    if (obj === undefined) return;
+    this.compound.restoreState(obj);
+  }
+
+  toJSON(): any {
+    const obj = this.compound.toJSON();
+    for (const _ in obj) return obj;
+    return undefined;
+  }
+}
+
 export interface SkeletonLayerDisplayState extends SegmentationDisplayState3D {
   shaderError: WatchableShaderError;
-  fragmentMain: TrackableValue<string>;
-  showSkeletonNodes: WatchableValueInterface<boolean>;
+  skeletonRenderingOptions: SkeletonRenderingOptions;
 }
 
 export class SkeletonLayer extends RefCounted {
@@ -287,11 +354,11 @@ export class SkeletonLayer extends RefCounted {
 
     registerRedrawWhenSegmentationDisplayState3DChanged(displayState, this);
     this.displayState.shaderError.value = undefined;
-    this.registerDisposer(displayState.fragmentMain.changed.add(() => {
+    const {skeletonRenderingOptions: renderingOptions} = displayState;
+    this.registerDisposer(renderingOptions.shader.changed.add(() => {
       this.displayState.shaderError.value = undefined;
       this.redrawNeeded.dispatch();
     }));
-    this.registerDisposer(displayState.showSkeletonNodes.changed.add(this.redrawNeeded.dispatch));
     let sharedObject = this.sharedObject =
         this.registerDisposer(new SegmentationLayerSharedObject(chunkManager, displayState));
     sharedObject.RPC_TYPE_ID = SKELETON_LAYER_RPC_ID;
@@ -318,9 +385,10 @@ export class SkeletonLayer extends RefCounted {
 
   draw(
       renderContext: SliceViewPanelRenderContext|PerspectiveViewRenderContext, layer: RenderLayer,
-      renderHelper: RenderHelper, lineWidth?: number) {
-    if (lineWidth === undefined) {
-      lineWidth = renderContext.emitColor ? 1 : 5;
+      renderHelper: RenderHelper, renderOptions: ViewSpecificSkeletonRenderingOptions) {
+    let lineWidth = renderOptions.lineWidth.value;
+    if (!renderContext.emitColor && renderHelper.targetIsSliceView) {
+      lineWidth = Math.max(lineWidth, 5);
     }
     let {gl, source, displayState} = this;
     let alpha = Math.min(1.0, displayState.objectAlpha.value);
@@ -328,7 +396,12 @@ export class SkeletonLayer extends RefCounted {
       // Skip drawing.
       return;
     }
-    const showSkeletonNodes = this.displayState.showSkeletonNodes.value;
+    let pointDiameter: number;
+    if (renderOptions.mode.value === SkeletonRenderMode.LINES_AND_POINTS) {
+      pointDiameter = Math.max(10, lineWidth * 2);
+    } else {
+      pointDiameter = lineWidth;
+    }
 
     const edgeShader = renderHelper.edgeShaderGetter(renderContext.emitter);
     const nodeShader = renderHelper.nodeShaderGetter(renderContext.emitter);
@@ -376,8 +449,7 @@ export class SkeletonLayer extends RefCounted {
         renderHelper.setPickID(gl, nodeShader, pickIDs.registerUint64(layer, objectId));
       }
       renderHelper.drawSkeleton(
-          gl, edgeShader, showSkeletonNodes ? nodeShader : null, skeleton, renderContext, lineWidth!
-      );
+          gl, edgeShader, nodeShader, skeleton, renderContext, lineWidth!, pointDiameter);
     });
     renderHelper.endLayer(gl, edgeShader);
   }
@@ -385,13 +457,15 @@ export class SkeletonLayer extends RefCounted {
 
 export class PerspectiveViewSkeletonLayer extends PerspectiveViewRenderLayer {
   private renderHelper = this.registerDisposer(new RenderHelper(this.base, false));
+  private renderOptions = this.base.displayState.skeletonRenderingOptions.params3d;
 
   constructor(public base: SkeletonLayer) {
     super();
     this.registerDisposer(base);
-    this.registerDisposer(base.redrawNeeded.add(() => {
-      this.redrawNeeded.dispatch();
-    }));
+    this.registerDisposer(base.redrawNeeded.add(this.redrawNeeded.dispatch));
+    const {renderOptions} = this;
+    this.registerDisposer(renderOptions.mode.changed.add(this.redrawNeeded.dispatch));
+    this.registerDisposer(renderOptions.lineWidth.changed.add(this.redrawNeeded.dispatch));
     this.setReady(true);
     this.registerDisposer(base.visibility.add(this.visibility));
   }
@@ -404,19 +478,20 @@ export class PerspectiveViewSkeletonLayer extends PerspectiveViewRenderLayer {
   }
 
   draw(renderContext: PerspectiveViewRenderContext) {
-    this.base.draw(renderContext, this, this.renderHelper);
+    this.base.draw(renderContext, this, this.renderHelper, this.renderOptions);
   }
 }
 
 export class SliceViewPanelSkeletonLayer extends SliceViewPanelRenderLayer {
   private renderHelper = this.registerDisposer(new RenderHelper(this.base, true));
-
+  private renderOptions = this.base.displayState.skeletonRenderingOptions.params2d;
   constructor(public base: SkeletonLayer) {
     super();
     this.registerDisposer(base);
-    this.registerDisposer(base.redrawNeeded.add(() => {
-      this.redrawNeeded.dispatch();
-    }));
+    const {renderOptions} = this;
+    this.registerDisposer(renderOptions.mode.changed.add(this.redrawNeeded.dispatch));
+    this.registerDisposer(renderOptions.lineWidth.changed.add(this.redrawNeeded.dispatch));
+    this.registerDisposer(base.redrawNeeded.add(this.redrawNeeded.dispatch));
     this.setReady(true);
     this.registerDisposer(base.visibility.add(this.visibility));
   }
@@ -425,7 +500,7 @@ export class SliceViewPanelSkeletonLayer extends SliceViewPanelRenderLayer {
   }
 
   draw(renderContext: SliceViewPanelRenderContext) {
-    this.base.draw(renderContext, this, this.renderHelper, 1);
+    this.base.draw(renderContext, this, this.renderHelper, this.renderOptions);
   }
 }
 

feat(skeleton): add skeleton rendering mode/line width options