// pathfinder/client/src/mesh-debugger.ts // // Copyright © 2017 The Pathfinder Project Developers. // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. import * as glmatrix from 'gl-matrix'; import {AppController} from "./app-controller"; import {OrthographicCamera} from "./camera"; import {B_QUAD_SIZE, B_QUAD_UPPER_LEFT_VERTEX_OFFSET} from "./meshes"; import {B_QUAD_UPPER_RIGHT_VERTEX_OFFSET} from "./meshes"; import {B_QUAD_UPPER_CONTROL_POINT_VERTEX_OFFSET, B_QUAD_LOWER_LEFT_VERTEX_OFFSET} from "./meshes"; import {B_QUAD_LOWER_RIGHT_VERTEX_OFFSET} from "./meshes"; import {B_QUAD_LOWER_CONTROL_POINT_VERTEX_OFFSET, PathfinderMeshData} from "./meshes"; import {BUILTIN_FONT_URI, TextFrameGlyphStorage, PathfinderGlyph, TextRun} from "./text"; import {GlyphStorage, TextFrame} from "./text"; import {unwrapNull, UINT32_SIZE, UINT32_MAX, assert} from "./utils"; import {PathfinderView} from "./view"; import * as opentype from "opentype.js"; import {Font} from 'opentype.js'; const CHARACTER: string = 'A'; const FONT: string = 'eb-garamond'; const POINT_LABEL_FONT: string = "12px sans-serif"; const POINT_LABEL_OFFSET: glmatrix.vec2 = glmatrix.vec2.fromValues(12.0, 12.0); const POINT_RADIUS: number = 2.0; class MeshDebuggerAppController extends AppController { start() { super.start(); this.view = new MeshDebuggerView(this); this.openModal = unwrapNull(document.getElementById('pf-open-modal')); this.fontPathSelectGroup = unwrapNull(document.getElementById('pf-font-path-select-group')); this.fontPathSelect = unwrapNull(document.getElementById('pf-font-path-select')) as HTMLSelectElement; this.openFileSelect = unwrapNull(document.getElementById('pf-open-file-select')) as HTMLSelectElement; this.openFileSelect.addEventListener('change', () => this.openSelectedFile(), false); const openButton = unwrapNull(document.getElementById('pf-open-button')); openButton.addEventListener('click', () => this.showOpenDialog(), false); const openOKButton = unwrapNull(document.getElementById('pf-open-ok-button')); openOKButton.addEventListener('click', () => this.loadPath(), false); this.loadInitialFile(); } private showOpenDialog(): void { window.jQuery(this.openModal).modal(); } private openSelectedFile(): void { const selectedOption = this.openFileSelect.selectedOptions[0] as HTMLOptionElement; const optionValue = selectedOption.value; this.fontPathSelectGroup.classList.add('pf-display-none'); if (optionValue.startsWith('font-')) { this.fetchFile(optionValue.substr('font-'.length)); } else if (optionValue.startsWith('svg-')) { // TODO(pcwalton) } } protected fileLoaded(): void { this.font = opentype.parse(this.fileData); assert(this.font.isSupported(), "The font type is unsupported!"); while (this.fontPathSelect.lastChild != null) this.fontPathSelect.removeChild(this.fontPathSelect.lastChild); this.fontPathSelectGroup.classList.remove('pf-display-none'); const glyphCount = this.font.numGlyphs; for (let glyphIndex = 1; glyphIndex < glyphCount; glyphIndex++) { const newOption = document.createElement('option'); newOption.value = "" + glyphIndex; const glyphName = this.font.glyphIndexToName(glyphIndex); newOption.appendChild(document.createTextNode(glyphName)); this.fontPathSelect.appendChild(newOption); } // Automatically load a path if this is the initial pageload. if (this.meshes == null) this.loadPath(this.font.charToGlyph(CHARACTER)); } protected loadPath(opentypeGlyph?: opentype.Glyph | null) { window.jQuery(this.openModal).modal('hide'); if (opentypeGlyph == null) { const glyphIndex = parseInt(this.fontPathSelect.selectedOptions[0].value); opentypeGlyph = this.font.glyphs.get(glyphIndex); } const glyph = new MeshDebuggerGlyph(opentypeGlyph); const glyphStorage = new GlyphStorage(this.fileData, [glyph], this.font); glyphStorage.partition().then(meshes => { this.meshes = meshes; this.view.attachMeshes(); }) } protected readonly defaultFile: string = FONT; protected readonly builtinFileURI: string = BUILTIN_FONT_URI; private font: Font; meshes: PathfinderMeshData | null; private openModal: HTMLElement; private openFileSelect: HTMLSelectElement; private fontPathSelectGroup: HTMLElement; private fontPathSelect: HTMLSelectElement; private view: MeshDebuggerView; } class MeshDebuggerView extends PathfinderView { constructor(appController: MeshDebuggerAppController) { super(); this.appController = appController; this.camera = new OrthographicCamera(this.canvas); this.scale = 1.0; } attachMeshes() { this.setDirty(); } redraw() { super.redraw(); const meshes = this.appController.meshes; if (meshes == null) return; const context = unwrapNull(this.canvas.getContext('2d')); context.clearRect(0, 0, this.canvas.width, this.canvas.height); context.save(); context.translate(this.camera.translation[0], this.canvas.height - this.camera.translation[1]); context.scale(this.scale, this.scale); context.font = POINT_LABEL_FONT; const bQuads = new Uint32Array(meshes.bQuads); const positions = new Float32Array(meshes.bVertexPositions); const markedVertices: boolean[] = []; for (let bQuadIndex = 0; bQuadIndex < meshes.bQuadCount; bQuadIndex++) { const bQuadStartOffset = (B_QUAD_SIZE * bQuadIndex) / UINT32_SIZE; const upperLeftIndex = bQuads[bQuadStartOffset + B_QUAD_UPPER_LEFT_VERTEX_OFFSET / UINT32_SIZE]; const upperRightIndex = bQuads[bQuadStartOffset + B_QUAD_UPPER_RIGHT_VERTEX_OFFSET / UINT32_SIZE]; const upperControlPointIndex = bQuads[bQuadStartOffset + B_QUAD_UPPER_CONTROL_POINT_VERTEX_OFFSET / UINT32_SIZE]; const lowerLeftIndex = bQuads[bQuadStartOffset + B_QUAD_LOWER_LEFT_VERTEX_OFFSET / UINT32_SIZE]; const lowerRightIndex = bQuads[bQuadStartOffset + B_QUAD_LOWER_RIGHT_VERTEX_OFFSET / UINT32_SIZE]; const lowerControlPointIndex = bQuads[bQuadStartOffset + B_QUAD_LOWER_CONTROL_POINT_VERTEX_OFFSET / UINT32_SIZE]; const upperLeftPosition = unwrapNull(getPosition(positions, upperLeftIndex)); const upperRightPosition = unwrapNull(getPosition(positions, upperRightIndex)); const upperControlPointPosition = getPosition(positions, upperControlPointIndex); const lowerLeftPosition = unwrapNull(getPosition(positions, lowerLeftIndex)); const lowerRightPosition = unwrapNull(getPosition(positions, lowerRightIndex)); const lowerControlPointPosition = getPosition(positions, lowerControlPointIndex); drawVertexIfNecessary(context, markedVertices, upperLeftIndex, upperLeftPosition); drawVertexIfNecessary(context, markedVertices, upperRightIndex, upperRightPosition); drawVertexIfNecessary(context, markedVertices, lowerLeftIndex, lowerLeftPosition); drawVertexIfNecessary(context, markedVertices, lowerRightIndex, lowerRightPosition); context.beginPath(); context.moveTo(upperLeftPosition[0], upperLeftPosition[1]); if (upperControlPointPosition != null) { context.quadraticCurveTo(upperControlPointPosition[0], upperControlPointPosition[1], upperRightPosition[0], upperRightPosition[1]); } else { context.lineTo(upperRightPosition[0], upperRightPosition[1]); } context.lineTo(lowerRightPosition[0], lowerRightPosition[1]); if (lowerControlPointPosition != null) { context.quadraticCurveTo(lowerControlPointPosition[0], lowerControlPointPosition[1], lowerLeftPosition[0], lowerLeftPosition[1]); } else { context.lineTo(lowerLeftPosition[0], lowerLeftPosition[1]); } context.closePath(); context.stroke(); } context.restore(); } protected scale: number; private appController: MeshDebuggerAppController; camera: OrthographicCamera; } class MeshDebuggerGlyph extends PathfinderGlyph {} function getPosition(positions: Float32Array, vertexIndex: number): Float32Array | null { if (vertexIndex == UINT32_MAX) return null; return new Float32Array([positions[vertexIndex * 2 + 0], -positions[vertexIndex * 2 + 1]]); } function drawVertexIfNecessary(context: CanvasRenderingContext2D, markedVertices: boolean[], vertexIndex: number, position: Float32Array) { if (markedVertices[vertexIndex] != null) return; markedVertices[vertexIndex] = true; context.beginPath(); context.moveTo(position[0], position[1]); context.arc(position[0], position[1], POINT_RADIUS, 0, 2.0 * Math.PI); context.fill(); context.fillText("" + vertexIndex, position[0] + POINT_LABEL_OFFSET[0], position[1] + POINT_LABEL_OFFSET[1]); } function main() { const controller = new MeshDebuggerAppController; window.addEventListener('load', () => controller.start(), false); } main();