Implement bare-bones support for debugging SVG meshes

This commit is contained in:
Patrick Walton 2017-09-19 20:19:53 -07:00
parent e7a6861846
commit 0e9a59088c
9 changed files with 105 additions and 59 deletions

View File

@ -9,6 +9,7 @@
<body class="pf-unscrollable">
{{>partials/navbar.html isTool=true}}
<canvas id="pf-canvas" width="400" height="300"></canvas>
<svg id="pf-svg" xmlns="http://www.w3.org/2000/svg" width="400" height="300"></svg>
<div class="fixed-bottom mb-3 d-flex justify-content-end align-items-end pf-pointer-events-none">
<div id="pf-toolbar">
<button id="pf-open-button" type="button"

View File

@ -101,7 +101,7 @@ class ThreeDController extends DemoAppController<ThreeDView> {
.then(response => response.json())
.then(textData => this.parseTextData(textData));
this.loadInitialFile();
this.loadInitialFile(this.builtinFileURI);
}
private parseTextData(textData: any): MonumentSide[] {

View File

@ -18,19 +18,19 @@ export abstract class AppController {
const canvas = document.getElementById('pf-canvas') as HTMLCanvasElement;
}
protected loadInitialFile() {
protected loadInitialFile(builtinFileURI: string) {
const selectFileElement = document.getElementById('pf-select-file') as
(HTMLSelectElement | null);
if (selectFileElement != null) {
const selectedOption = selectFileElement.selectedOptions[0] as HTMLOptionElement;
this.fetchFile(selectedOption.value);
this.fetchFile(selectedOption.value, builtinFileURI);
} else {
this.fetchFile(this.defaultFile);
this.fetchFile(this.defaultFile, builtinFileURI);
}
}
protected fetchFile(file: string) {
window.fetch(`${this.builtinFileURI}/${file}`)
protected fetchFile(file: string, builtinFileURI: string) {
window.fetch(`${builtinFileURI}/${file}`)
.then(response => response.arrayBuffer())
.then(data => {
this.fileData = data;
@ -47,7 +47,6 @@ export abstract class AppController {
protected abstract fileLoaded(): void;
protected abstract get defaultFile(): string;
protected abstract get builtinFileURI(): string;
}
export abstract class DemoAppController<View extends PathfinderDemoView> extends AppController {
@ -221,11 +220,13 @@ export abstract class DemoAppController<View extends PathfinderDemoView> extends
selectFileElement.removeChild(placeholder);
// Fetch the file.
this.fetchFile(selectedOption.value);
this.fetchFile(selectedOption.value, this.builtinFileURI);
}
protected abstract createView(): View;
protected abstract readonly builtinFileURI: string;
view: Promise<View>;
protected filePickerElement: HTMLInputElement | null;

View File

@ -56,7 +56,7 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
const runBenchmarkButton = unwrapNull(document.getElementById('pf-run-benchmark-button'));
runBenchmarkButton.addEventListener('click', () => this.runBenchmark(), false);
this.loadInitialFile();
this.loadInitialFile(this.builtinFileURI);
}
protected fileLoaded(): void {

View File

@ -17,6 +17,8 @@ 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 {Partitionable} from "./meshes";
import { SVGLoader, BUILTIN_SVG_URI } from './svg-loader';
import {BUILTIN_FONT_URI, TextFrameGlyphStorage, PathfinderGlyph, TextRun} from "./text";
import {GlyphStorage, TextFrame} from "./text";
import {unwrapNull, UINT32_SIZE, UINT32_MAX, assert} from "./utils";
@ -32,10 +34,19 @@ 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;
const BUILTIN_URIS = {
font: BUILTIN_FONT_URI,
svg: BUILTIN_SVG_URI,
};
type FileType = 'font' | 'svg';
class MeshDebuggerAppController extends AppController {
start() {
super.start();
this.fileType = 'font';
this.view = new MeshDebuggerView(this);
this.openModal = unwrapNull(document.getElementById('pf-open-modal'));
@ -54,7 +65,7 @@ class MeshDebuggerAppController extends AppController {
const openOKButton = unwrapNull(document.getElementById('pf-open-ok-button'));
openOKButton.addEventListener('click', () => this.loadPath(), false);
this.loadInitialFile();
this.loadInitialFile(BUILTIN_FONT_URI);
}
private showOpenDialog(): void {
@ -67,57 +78,83 @@ class MeshDebuggerAppController extends AppController {
this.fontPathSelectGroup.classList.add('pf-display-none');
if (optionValue.startsWith('font-')) {
this.fetchFile(optionValue.substr('font-'.length));
} else if (optionValue.startsWith('svg-')) {
// TODO(pcwalton)
}
const results = unwrapNull(/^([a-z]+)-(.*)$/.exec(optionValue));
this.fileType = results[1] as FileType;
this.fetchFile(results[2], BUILTIN_URIS[this.fileType]);
}
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;
if (this.fileType === 'font')
this.fontLoaded();
else if (this.fileType === 'svg')
this.svgLoaded();
}
private fontLoaded(): void {
this.file = opentype.parse(this.fileData);
assert(this.file.isSupported(), "The font type is unsupported!");
const glyphCount = this.file.numGlyphs;
for (let glyphIndex = 1; glyphIndex < glyphCount; glyphIndex++) {
const newOption = document.createElement('option');
newOption.value = "" + glyphIndex;
const glyphName = this.font.glyphIndexToName(glyphIndex);
const glyphName = this.file.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));
this.loadPath(this.file.charToGlyph(CHARACTER));
}
private svgLoaded(): void {
this.file = new SVGLoader;
this.file.loadFile(this.fileData);
const pathCount = this.file.pathInstances.length;
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) {
const newOption = document.createElement('option');
newOption.value = "" + pathIndex;
newOption.appendChild(document.createTextNode(`Path ${pathIndex}`));
this.fontPathSelect.appendChild(newOption);
}
}
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);
let partitionable: Partitionable | null = null;
if (this.file instanceof opentype.Font) {
if (opentypeGlyph == null) {
const glyphIndex = parseInt(this.fontPathSelect.selectedOptions[0].value);
opentypeGlyph = this.file.glyphs.get(glyphIndex);
}
const glyph = new MeshDebuggerGlyph(opentypeGlyph);
partitionable = new GlyphStorage(this.fileData, [glyph], this.file);
} else if (this.file instanceof SVGLoader) {
partitionable = this.file;
} else {
return;
}
const glyph = new MeshDebuggerGlyph(opentypeGlyph);
const glyphStorage = new GlyphStorage(this.fileData, [glyph], this.font);
glyphStorage.partition().then(meshes => {
partitionable.partition().then(meshes => {
this.meshes = meshes;
this.view.attachMeshes();
})
}
protected readonly defaultFile: string = FONT;
protected readonly builtinFileURI: string = BUILTIN_FONT_URI;
private font: Font;
private file: Font | SVGLoader | null;
private fileType: FileType;
meshes: PathfinderMeshData | null;

View File

@ -37,6 +37,10 @@ export const B_QUAD_LOWER_INDICES_OFFSET: number = B_QUAD_LOWER_LEFT_VERTEX_OFFS
type BufferType = 'ARRAY_BUFFER' | 'ELEMENT_ARRAY_BUFFER';
export interface Partitionable {
partition(): Promise<PathfinderMeshData>;
}
export interface Meshes<T> {
readonly bQuads: T;
readonly bVertexPositions: T;

View File

@ -10,7 +10,6 @@
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import 'path-data-polyfill.js';
import {DemoAppController} from './app-controller';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
@ -18,7 +17,7 @@ import {OrthographicCamera} from "./camera";
import {ECAAStrategy, ECAAMulticolorStrategy} from "./ecaa-strategy";
import {PathfinderMeshData} from "./meshes";
import {ShaderMap, ShaderProgramSource} from './shader-loader';
import {SVGLoader} from './svg-loader';
import { SVGLoader, BUILTIN_SVG_URI } from './svg-loader';
import {panic, unwrapNull} from './utils';
import {PathfinderDemoView, Timings} from './view';
import SSAAStrategy from "./ssaa-strategy";
@ -28,8 +27,6 @@ const parseColor = require('parse-color');
const SVG_NS: string = "http://www.w3.org/2000/svg";
const BUILTIN_SVG_URI: string = "/svg/demo";
const DEFAULT_FILE: string = 'tiger';
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
@ -38,17 +35,6 @@ const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
ecaa: ECAAMulticolorStrategy,
};
declare class SVGPathSegment {
type: string;
values: number[];
}
declare global {
interface SVGPathElement {
getPathData(settings: any): SVGPathSegment[];
}
}
interface AntialiasingStrategyTable {
none: typeof NoAAStrategy;
ssaa: typeof SSAAStrategy;
@ -61,11 +47,12 @@ class SVGDemoController extends DemoAppController<SVGDemoView> {
this.loader = new SVGLoader;
this.loadInitialFile();
this.loadInitialFile(this.builtinFileURI);
}
protected fileLoaded() {
this.loader.loadFile(this.fileData).then(meshes => {
this.loader.loadFile(this.fileData);
this.loader.partition().then(meshes => {
this.meshes = meshes;
this.meshesReceived();
})
@ -77,9 +64,7 @@ class SVGDemoController extends DemoAppController<SVGDemoView> {
unwrapNull(this.shaderSources));
}
protected get builtinFileURI(): string {
return BUILTIN_SVG_URI;
}
protected readonly builtinFileURI: string = BUILTIN_SVG_URI;
protected get defaultFile(): string {
return DEFAULT_FILE;

View File

@ -11,37 +11,52 @@
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import 'path-data-polyfill.js';
import {panic, unwrapNull} from "./utils";
import {PathfinderMeshData} from "./meshes";
import {PathfinderMeshData, Partitionable} from "./meshes";
export const BUILTIN_SVG_URI: string = "/svg/demo";
const PARTITION_SVG_PATHS_ENDPOINT_URL: string = "/partition-svg-paths";
/// The minimum size of a stroke.
const HAIRLINE_STROKE_WIDTH: number = 0.25;
declare class SVGPathSegment {
type: string;
values: number[];
}
declare global {
interface SVGPathElement {
getPathData(settings: any): SVGPathSegment[];
}
}
export interface PathInstance {
element: SVGPathElement;
stroke: number | 'fill';
}
export class SVGLoader {
export class SVGLoader implements Partitionable {
constructor() {
this.svg = unwrapNull(document.getElementById('pf-svg')) as Element as SVGSVGElement;
this.pathInstances = [];
this.paths = [];
this.bounds = glmatrix.vec4.create();
}
loadFile(fileData: ArrayBuffer): Promise<PathfinderMeshData> {
loadFile(fileData: ArrayBuffer) {
this.fileData = fileData;
const decoder = new (window as any).TextDecoder('utf-8');
const fileStringData = decoder.decode(new DataView(this.fileData));
const svgDocument = (new DOMParser).parseFromString(fileStringData, 'image/svg+xml');
const svgElement = svgDocument.documentElement as Element as SVGSVGElement;
return this.attachSVG(svgElement);
this.attachSVG(svgElement);
}
private attachSVG(svgElement: SVGSVGElement): Promise<PathfinderMeshData> {
private attachSVG(svgElement: SVGSVGElement) {
// Clear out the current document.
let kid;
while ((kid = this.svg.firstChild) != null)
@ -76,8 +91,8 @@ export class SVGLoader {
}
}
const request: any = { paths: [] };
let minX = 0, minY = 0, maxX = 0, maxY = 0;
this.paths = [];
// Extract, normalize, and transform the path data.
for (const instance of this.pathInstances) {
@ -112,16 +127,18 @@ export class SVGLoader {
else
kind = { Stroke: Math.max(HAIRLINE_STROKE_WIDTH, instance.stroke) };
request.paths.push({ segments: segments, kind: kind });
this.paths.push({ segments: segments, kind: kind });
}
this.bounds = glmatrix.vec4.clone([minX, minY, maxX, maxY]);
}
partition(): Promise<PathfinderMeshData> {
// Make the request.
return window.fetch(PARTITION_SVG_PATHS_ENDPOINT_URL, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(request),
body: JSON.stringify({ paths: this.paths }),
}).then(response => response.text()).then(responseText => {
const response = JSON.parse(responseText);
if (!('Ok' in response))
@ -135,5 +152,6 @@ export class SVGLoader {
private fileData: ArrayBuffer;
pathInstances: PathInstance[];
private paths: any[];
bounds: glmatrix.vec4;
}

View File

@ -142,7 +142,7 @@ class TextDemoController extends DemoAppController<TextDemoView> {
const editTextOkButton = unwrapNull(document.getElementById('pf-edit-text-ok-button'));
editTextOkButton.addEventListener('click', () => this.updateText(), false);
this.loadInitialFile();
this.loadInitialFile(this.builtinFileURI);
}
showTextEditor() {