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"> <body class="pf-unscrollable">
{{>partials/navbar.html isTool=true}} {{>partials/navbar.html isTool=true}}
<canvas id="pf-canvas" width="400" height="300"></canvas> <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 class="fixed-bottom mb-3 d-flex justify-content-end align-items-end pf-pointer-events-none">
<div id="pf-toolbar"> <div id="pf-toolbar">
<button id="pf-open-button" type="button" <button id="pf-open-button" type="button"

View File

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

View File

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

View File

@ -56,7 +56,7 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
const runBenchmarkButton = unwrapNull(document.getElementById('pf-run-benchmark-button')); const runBenchmarkButton = unwrapNull(document.getElementById('pf-run-benchmark-button'));
runBenchmarkButton.addEventListener('click', () => this.runBenchmark(), false); runBenchmarkButton.addEventListener('click', () => this.runBenchmark(), false);
this.loadInitialFile(); this.loadInitialFile(this.builtinFileURI);
} }
protected fileLoaded(): void { 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_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_RIGHT_VERTEX_OFFSET} from "./meshes";
import {B_QUAD_LOWER_CONTROL_POINT_VERTEX_OFFSET, PathfinderMeshData} 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 {BUILTIN_FONT_URI, TextFrameGlyphStorage, PathfinderGlyph, TextRun} from "./text";
import {GlyphStorage, TextFrame} from "./text"; import {GlyphStorage, TextFrame} from "./text";
import {unwrapNull, UINT32_SIZE, UINT32_MAX, assert} from "./utils"; 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_LABEL_OFFSET: glmatrix.vec2 = glmatrix.vec2.fromValues(12.0, 12.0);
const POINT_RADIUS: number = 2.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 { class MeshDebuggerAppController extends AppController {
start() { start() {
super.start(); super.start();
this.fileType = 'font';
this.view = new MeshDebuggerView(this); this.view = new MeshDebuggerView(this);
this.openModal = unwrapNull(document.getElementById('pf-open-modal')); 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')); const openOKButton = unwrapNull(document.getElementById('pf-open-ok-button'));
openOKButton.addEventListener('click', () => this.loadPath(), false); openOKButton.addEventListener('click', () => this.loadPath(), false);
this.loadInitialFile(); this.loadInitialFile(BUILTIN_FONT_URI);
} }
private showOpenDialog(): void { private showOpenDialog(): void {
@ -67,57 +78,83 @@ class MeshDebuggerAppController extends AppController {
this.fontPathSelectGroup.classList.add('pf-display-none'); this.fontPathSelectGroup.classList.add('pf-display-none');
if (optionValue.startsWith('font-')) { const results = unwrapNull(/^([a-z]+)-(.*)$/.exec(optionValue));
this.fetchFile(optionValue.substr('font-'.length)); this.fileType = results[1] as FileType;
} else if (optionValue.startsWith('svg-')) { this.fetchFile(results[2], BUILTIN_URIS[this.fileType]);
// TODO(pcwalton)
}
} }
protected fileLoaded(): void { protected fileLoaded(): void {
this.font = opentype.parse(this.fileData);
assert(this.font.isSupported(), "The font type is unsupported!");
while (this.fontPathSelect.lastChild != null) while (this.fontPathSelect.lastChild != null)
this.fontPathSelect.removeChild(this.fontPathSelect.lastChild); this.fontPathSelect.removeChild(this.fontPathSelect.lastChild);
this.fontPathSelectGroup.classList.remove('pf-display-none'); 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++) { for (let glyphIndex = 1; glyphIndex < glyphCount; glyphIndex++) {
const newOption = document.createElement('option'); const newOption = document.createElement('option');
newOption.value = "" + glyphIndex; newOption.value = "" + glyphIndex;
const glyphName = this.font.glyphIndexToName(glyphIndex); const glyphName = this.file.glyphIndexToName(glyphIndex);
newOption.appendChild(document.createTextNode(glyphName)); newOption.appendChild(document.createTextNode(glyphName));
this.fontPathSelect.appendChild(newOption); this.fontPathSelect.appendChild(newOption);
} }
// Automatically load a path if this is the initial pageload. // Automatically load a path if this is the initial pageload.
if (this.meshes == null) 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) { protected loadPath(opentypeGlyph?: opentype.Glyph | null) {
window.jQuery(this.openModal).modal('hide'); window.jQuery(this.openModal).modal('hide');
if (opentypeGlyph == null) { let partitionable: Partitionable | null = null;
const glyphIndex = parseInt(this.fontPathSelect.selectedOptions[0].value);
opentypeGlyph = this.font.glyphs.get(glyphIndex); 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); partitionable.partition().then(meshes => {
const glyphStorage = new GlyphStorage(this.fileData, [glyph], this.font);
glyphStorage.partition().then(meshes => {
this.meshes = meshes; this.meshes = meshes;
this.view.attachMeshes(); this.view.attachMeshes();
}) })
} }
protected readonly defaultFile: string = FONT; 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; 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'; type BufferType = 'ARRAY_BUFFER' | 'ELEMENT_ARRAY_BUFFER';
export interface Partitionable {
partition(): Promise<PathfinderMeshData>;
}
export interface Meshes<T> { export interface Meshes<T> {
readonly bQuads: T; readonly bQuads: T;
readonly bVertexPositions: T; readonly bVertexPositions: T;

View File

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

View File

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

View File

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