pathfinder/demo/client/src/mesh-debugger.ts

264 lines
10 KiB
TypeScript
Raw Normal View History

2017-09-01 21:11:44 -04:00
// pathfinder/client/src/mesh-debugger.ts
//
// Copyright © 2017 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
2017-09-02 01:29:05 -04:00
import * as glmatrix from 'gl-matrix';
2017-09-01 21:11:44 -04:00
import {AppController} from "./app-controller";
import {OrthographicCamera} from "./camera";
2017-09-02 01:29:05 -04:00
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";
2017-09-07 19:13:55 -04:00
import {unwrapNull, UINT32_SIZE, UINT32_MAX, assert} from "./utils";
2017-09-02 01:29:05 -04:00
import {PathfinderView} from "./view";
import * as opentype from "opentype.js";
import {Font} from 'opentype.js';
2017-09-02 01:29:05 -04:00
const CHARACTER: string = 'A';
2017-09-02 01:29:05 -04:00
const FONT: string = 'eb-garamond';
2017-09-01 21:11:44 -04:00
const POINT_LABEL_FONT: string = "12px sans-serif";
2017-09-02 01:29:05 -04:00
const POINT_LABEL_OFFSET: glmatrix.vec2 = glmatrix.vec2.fromValues(12.0, 12.0);
const POINT_RADIUS: number = 2.0;
2017-09-01 21:11:44 -04:00
class MeshDebuggerAppController extends AppController {
start() {
super.start();
2017-09-02 01:29:05 -04:00
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);
2017-09-01 21:11:44 -04:00
this.loadInitialFile();
}
2017-09-02 01:29:05 -04:00
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)
}
}
2017-09-01 21:11:44 -04:00
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);
}
2017-09-02 01:29:05 -04:00
const glyph = new MeshDebuggerGlyph(opentypeGlyph);
const glyphStorage = new GlyphStorage(this.fileData, [glyph], this.font);
glyphStorage.partition().then(meshes => {
2017-09-02 01:29:05 -04:00
this.meshes = meshes;
this.view.attachMeshes();
})
2017-09-01 21:11:44 -04:00
}
protected readonly defaultFile: string = FONT;
protected readonly builtinFileURI: string = BUILTIN_FONT_URI;
2017-09-02 01:29:05 -04:00
private font: Font;
meshes: PathfinderMeshData | null;
private openModal: HTMLElement;
private openFileSelect: HTMLSelectElement;
private fontPathSelectGroup: HTMLElement;
private fontPathSelect: HTMLSelectElement;
2017-09-02 01:29:05 -04:00
private view: MeshDebuggerView;
}
class MeshDebuggerView extends PathfinderView {
constructor(appController: MeshDebuggerAppController) {
super();
this.appController = appController;
this.camera = new OrthographicCamera(this.canvas);
2017-09-02 01:29:05 -04:00
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]);
2017-09-02 01:29:05 -04:00
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;
2017-09-02 01:29:05 -04:00
}
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]);
2017-09-01 21:11:44 -04:00
}
function main() {
2017-09-02 01:29:05 -04:00
const controller = new MeshDebuggerAppController;
window.addEventListener('load', () => controller.start(), false);
2017-09-01 21:11:44 -04:00
}
main();