Allow different glyphs to be selected in the mesh debugger
This commit is contained in:
parent
b1cf4b9760
commit
a5b0e9bf9a
|
@ -85,7 +85,7 @@ button > svg path {
|
||||||
#pf-svg {
|
#pf-svg {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
#pf-spinner {
|
.pf-spinner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -95,6 +95,9 @@ button > svg path {
|
||||||
.pf-spinner-hidden {
|
.pf-spinner-hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
.pf-display-none {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Arrow
|
* Arrow
|
||||||
|
|
|
@ -9,6 +9,59 @@
|
||||||
<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>
|
||||||
|
<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"
|
||||||
|
class="btn btn-outline-secondary pf-toolbar-button">
|
||||||
|
Open…
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="pf-open-modal" tabindex="-1" role="dialog"
|
||||||
|
aria-labelledby="pf-open-label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="pf-open-label">Open…</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal"
|
||||||
|
aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pf-open-file-label">File</label>
|
||||||
|
<select id="pf-open-file-select"
|
||||||
|
class="form-control custom-select">
|
||||||
|
<option value="font-eb-garamond" selected>
|
||||||
|
Font: EB Garamond
|
||||||
|
</option>
|
||||||
|
<option value="font-open-sans">Font: Open Sans</option>
|
||||||
|
<option value="font-nimbus-sans">Font: Nimbus Sans</option>
|
||||||
|
<option value="svg-tiger">SVG: Ghostscript Tiger</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="pf-font-path-select-group">
|
||||||
|
<label for="pf-font-path-label">Path</label>
|
||||||
|
<select id="pf-font-path-select"
|
||||||
|
class="form-control custom-select">
|
||||||
|
<option value="65" selected>A</option>
|
||||||
|
<option value="66">B</option>
|
||||||
|
<option value="67">C</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary"
|
||||||
|
data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary" id="pf-open-ok-button">Open</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{>partials/spinner.html}}
|
{{>partials/spinner.html}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- http://tobiasahlin.com/spinkit/ -->
|
<!-- http://tobiasahlin.com/spinkit/ -->
|
||||||
<div class="sk-fading-circle pf-spinner-hidden" id="pf-spinner">
|
<div class="sk-fading-circle pf-spinner-hidden pf-spinner">
|
||||||
<div class="sk-circle1 sk-circle"></div>
|
<div class="sk-circle1 sk-circle"></div>
|
||||||
<div class="sk-circle2 sk-circle"></div>
|
<div class="sk-circle2 sk-circle"></div>
|
||||||
<div class="sk-circle3 sk-circle"></div>
|
<div class="sk-circle3 sk-circle"></div>
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {PerspectiveCamera} from "./camera";
|
||||||
import {mat4, vec2} from "gl-matrix";
|
import {mat4, vec2} from "gl-matrix";
|
||||||
import {PathfinderMeshData} from "./meshes";
|
import {PathfinderMeshData} from "./meshes";
|
||||||
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
|
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
|
||||||
import {BUILTIN_FONT_URI, ExpandedMeshData, GlyphStorage, PathfinderGlyph} from "./text";
|
import {BUILTIN_FONT_URI, ExpandedMeshData, TextFrameGlyphStorage, PathfinderGlyph} from "./text";
|
||||||
import {Hint, SimpleTextLayout, TextFrame, TextRun} from "./text";
|
import {Hint, SimpleTextLayout, TextFrame, TextRun} from "./text";
|
||||||
import {PathfinderError, assert, panic, unwrapNull} from "./utils";
|
import {PathfinderError, assert, panic, unwrapNull} from "./utils";
|
||||||
import {PathfinderDemoView, Timings} from "./view";
|
import {PathfinderDemoView, Timings} from "./view";
|
||||||
|
@ -163,7 +163,7 @@ class ThreeDController extends DemoAppController<ThreeDView> {
|
||||||
textFrames.push(new TextFrame(textRuns, font));
|
textFrames.push(new TextFrame(textRuns, font));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.glyphStorage = new GlyphStorage(this.fileData, textFrames, createGlyph, font);
|
this.glyphStorage = new TextFrameGlyphStorage(this.fileData, textFrames, font);
|
||||||
this.glyphStorage.layoutRuns();
|
this.glyphStorage.layoutRuns();
|
||||||
|
|
||||||
this.glyphStorage.partition().then((baseMeshes: PathfinderMeshData) => {
|
this.glyphStorage.partition().then((baseMeshes: PathfinderMeshData) => {
|
||||||
|
@ -191,7 +191,7 @@ class ThreeDController extends DemoAppController<ThreeDView> {
|
||||||
return FONT;
|
return FONT;
|
||||||
}
|
}
|
||||||
|
|
||||||
glyphStorage: GlyphStorage<ThreeDGlyph>;
|
glyphStorage: TextFrameGlyphStorage<ThreeDGlyph>;
|
||||||
|
|
||||||
private baseMeshes: PathfinderMeshData;
|
private baseMeshes: PathfinderMeshData;
|
||||||
private expandedMeshes: ExpandedMeshData[];
|
private expandedMeshes: ExpandedMeshData[];
|
||||||
|
|
|
@ -13,7 +13,7 @@ import * as opentype from "opentype.js";
|
||||||
|
|
||||||
import { AppController, DemoAppController } from "./app-controller";
|
import { AppController, DemoAppController } from "./app-controller";
|
||||||
import {PathfinderMeshData} from "./meshes";
|
import {PathfinderMeshData} from "./meshes";
|
||||||
import { BUILTIN_FONT_URI, GlyphStorage, PathfinderGlyph, TextFrame, TextRun, ExpandedMeshData } from "./text";
|
import { BUILTIN_FONT_URI, TextFrameGlyphStorage, PathfinderGlyph, TextFrame, TextRun, ExpandedMeshData } from "./text";
|
||||||
import { assert, unwrapNull, PathfinderError } from "./utils";
|
import { assert, unwrapNull, PathfinderError } from "./utils";
|
||||||
import { PathfinderDemoView, Timings, MonochromePathfinderView } from "./view";
|
import { PathfinderDemoView, Timings, MonochromePathfinderView } from "./view";
|
||||||
import { ShaderMap, ShaderProgramSource } from "./shader-loader";
|
import { ShaderMap, ShaderProgramSource } from "./shader-loader";
|
||||||
|
@ -68,7 +68,7 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
|
||||||
const textRun = new TextRun<BenchmarkGlyph>(STRING, [0, 0], font, createGlyph);
|
const textRun = new TextRun<BenchmarkGlyph>(STRING, [0, 0], font, createGlyph);
|
||||||
this.textRun = textRun;
|
this.textRun = textRun;
|
||||||
const textFrame = new TextFrame([textRun], font);
|
const textFrame = new TextFrame([textRun], font);
|
||||||
this.glyphStorage = new GlyphStorage(this.fileData, [textFrame], createGlyph, font);
|
this.glyphStorage = new TextFrameGlyphStorage(this.fileData, [textFrame], font);
|
||||||
|
|
||||||
this.glyphStorage.partition().then(baseMeshes => {
|
this.glyphStorage.partition().then(baseMeshes => {
|
||||||
this.baseMeshes = baseMeshes;
|
this.baseMeshes = baseMeshes;
|
||||||
|
@ -116,7 +116,7 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
|
||||||
protected readonly defaultFile: string = FONT;
|
protected readonly defaultFile: string = FONT;
|
||||||
protected readonly builtinFileURI: string = BUILTIN_FONT_URI;
|
protected readonly builtinFileURI: string = BUILTIN_FONT_URI;
|
||||||
|
|
||||||
private glyphStorage: GlyphStorage<BenchmarkGlyph>;
|
private glyphStorage: TextFrameGlyphStorage<BenchmarkGlyph>;
|
||||||
private baseMeshes: PathfinderMeshData;
|
private baseMeshes: PathfinderMeshData;
|
||||||
private expandedMeshes: ExpandedMeshData;
|
private expandedMeshes: ExpandedMeshData;
|
||||||
|
|
||||||
|
|
|
@ -17,12 +17,14 @@ 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 {BUILTIN_FONT_URI, GlyphStorage, PathfinderGlyph, TextRun, TextFrame} from "./text";
|
import {BUILTIN_FONT_URI, TextFrameGlyphStorage, PathfinderGlyph, TextRun} 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";
|
||||||
import {PathfinderView} from "./view";
|
import {PathfinderView} from "./view";
|
||||||
import * as opentype from "opentype.js";
|
import * as opentype from "opentype.js";
|
||||||
|
import {Font} from 'opentype.js';
|
||||||
|
|
||||||
const CHARACTER: string = 'r';
|
const CHARACTER: string = 'A';
|
||||||
|
|
||||||
const FONT: string = 'eb-garamond';
|
const FONT: string = 'eb-garamond';
|
||||||
|
|
||||||
|
@ -36,19 +38,77 @@ class MeshDebuggerAppController extends AppController {
|
||||||
|
|
||||||
this.view = new MeshDebuggerView(this);
|
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();
|
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 {
|
protected fileLoaded(): void {
|
||||||
const font = opentype.parse(this.fileData);
|
this.font = opentype.parse(this.fileData);
|
||||||
assert(font.isSupported(), "The font type is unsupported!");
|
assert(this.font.isSupported(), "The font type is unsupported!");
|
||||||
|
|
||||||
const createGlyph = (glyph: opentype.Glyph) => new MeshDebuggerGlyph(glyph);
|
while (this.fontPathSelect.lastChild != null)
|
||||||
const textRun = new TextRun<MeshDebuggerGlyph>(CHARACTER, [0, 0], font, createGlyph);
|
this.fontPathSelect.removeChild(this.fontPathSelect.lastChild);
|
||||||
const textFrame = new TextFrame([textRun], font);
|
|
||||||
this.glyphStorage = new GlyphStorage(this.fileData, [textFrame], createGlyph, font);
|
|
||||||
|
|
||||||
this.glyphStorage.partition().then(meshes => {
|
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.meshes = meshes;
|
||||||
this.view.attachMeshes();
|
this.view.attachMeshes();
|
||||||
})
|
})
|
||||||
|
@ -57,8 +117,14 @@ class MeshDebuggerAppController extends AppController {
|
||||||
protected readonly defaultFile: string = FONT;
|
protected readonly defaultFile: string = FONT;
|
||||||
protected readonly builtinFileURI: string = BUILTIN_FONT_URI;
|
protected readonly builtinFileURI: string = BUILTIN_FONT_URI;
|
||||||
|
|
||||||
glyphStorage: GlyphStorage<MeshDebuggerGlyph>;
|
private font: Font;
|
||||||
meshes: PathfinderMeshData;
|
|
||||||
|
meshes: PathfinderMeshData | null;
|
||||||
|
|
||||||
|
private openModal: HTMLElement;
|
||||||
|
private openFileSelect: HTMLSelectElement;
|
||||||
|
private fontPathSelectGroup: HTMLElement;
|
||||||
|
private fontPathSelect: HTMLSelectElement;
|
||||||
|
|
||||||
private view: MeshDebuggerView;
|
private view: MeshDebuggerView;
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,33 +194,24 @@ export class TextFrame<Glyph extends PathfinderGlyph> {
|
||||||
readonly origin: glmatrix.vec3;
|
readonly origin: glmatrix.vec3;
|
||||||
|
|
||||||
private readonly font: Font;
|
private readonly font: Font;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GlyphStorage<Glyph extends PathfinderGlyph> {
|
export class GlyphStorage<Glyph extends PathfinderGlyph> {
|
||||||
constructor(fontData: ArrayBuffer,
|
constructor(fontData: ArrayBuffer, glyphs: Glyph[], font?: Font) {
|
||||||
textFrames: TextFrame<Glyph>[],
|
|
||||||
createGlyph: CreateGlyphFn<Glyph>,
|
|
||||||
font?: Font) {
|
|
||||||
if (font == null) {
|
if (font == null) {
|
||||||
font = opentype.parse(fontData);
|
font = opentype.parse(fontData);
|
||||||
assert(font.isSupported(), "The font type is unsupported!");
|
assert(font.isSupported(), "The font type is unsupported!");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fontData = fontData;
|
this.fontData = fontData;
|
||||||
this.textFrames = textFrames;
|
|
||||||
this.font = font;
|
this.font = font;
|
||||||
|
|
||||||
// Determine all glyphs potentially needed.
|
// Determine all glyphs potentially needed.
|
||||||
this.uniqueGlyphs = this.allGlyphs;
|
this.uniqueGlyphs = glyphs;
|
||||||
this.uniqueGlyphs.sort((a, b) => a.index - b.index);
|
this.uniqueGlyphs.sort((a, b) => a.index - b.index);
|
||||||
this.uniqueGlyphs = _.sortedUniqBy(this.uniqueGlyphs, glyph => glyph.index);
|
this.uniqueGlyphs = _.sortedUniqBy(this.uniqueGlyphs, glyph => glyph.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
expandMeshes(meshes: PathfinderMeshData): ExpandedMeshData[] {
|
|
||||||
return this.textFrames.map(textFrame => textFrame.expandMeshes(this.uniqueGlyphs, meshes));
|
|
||||||
}
|
|
||||||
|
|
||||||
partition(): Promise<PathfinderMeshData> {
|
partition(): Promise<PathfinderMeshData> {
|
||||||
// Build the partitioning request to the server.
|
// Build the partitioning request to the server.
|
||||||
//
|
//
|
||||||
|
@ -253,6 +244,23 @@ export class GlyphStorage<Glyph extends PathfinderGlyph> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly fontData: ArrayBuffer;
|
||||||
|
readonly font: Font;
|
||||||
|
readonly uniqueGlyphs: Glyph[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextFrameGlyphStorage<Glyph extends PathfinderGlyph> extends GlyphStorage<Glyph> {
|
||||||
|
constructor(fontData: ArrayBuffer, textFrames: TextFrame<Glyph>[], font?: Font) {
|
||||||
|
const allGlyphs = _.flatMap(textFrames, textRun => textRun.allGlyphs);
|
||||||
|
super(fontData, allGlyphs, font);
|
||||||
|
|
||||||
|
this.textFrames = textFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
expandMeshes(meshes: PathfinderMeshData): ExpandedMeshData[] {
|
||||||
|
return this.textFrames.map(textFrame => textFrame.expandMeshes(this.uniqueGlyphs, meshes));
|
||||||
|
}
|
||||||
|
|
||||||
layoutRuns() {
|
layoutRuns() {
|
||||||
for (const textFrame of this.textFrames)
|
for (const textFrame of this.textFrames)
|
||||||
textFrame.runs.forEach(textRun => textRun.layout());
|
textFrame.runs.forEach(textRun => textRun.layout());
|
||||||
|
@ -262,10 +270,7 @@ export class GlyphStorage<Glyph extends PathfinderGlyph> {
|
||||||
return _.flatMap(this.textFrames, textRun => textRun.allGlyphs);
|
return _.flatMap(this.textFrames, textRun => textRun.allGlyphs);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly fontData: ArrayBuffer;
|
|
||||||
readonly font: Font;
|
|
||||||
readonly textFrames: TextFrame<Glyph>[];
|
readonly textFrames: TextFrame<Glyph>[];
|
||||||
readonly uniqueGlyphs: Glyph[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SimpleTextLayout<Glyph extends PathfinderGlyph> {
|
export class SimpleTextLayout<Glyph extends PathfinderGlyph> {
|
||||||
|
@ -279,7 +284,7 @@ export class SimpleTextLayout<Glyph extends PathfinderGlyph> {
|
||||||
});
|
});
|
||||||
this.textFrame = new TextFrame(textRuns, font);
|
this.textFrame = new TextFrame(textRuns, font);
|
||||||
|
|
||||||
this.glyphStorage = new GlyphStorage(fontData, [this.textFrame], createGlyph, font);
|
this.glyphStorage = new TextFrameGlyphStorage(fontData, [this.textFrame], font);
|
||||||
}
|
}
|
||||||
|
|
||||||
layoutRuns() {
|
layoutRuns() {
|
||||||
|
@ -291,7 +296,7 @@ export class SimpleTextLayout<Glyph extends PathfinderGlyph> {
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly textFrame: TextFrame<Glyph>;
|
readonly textFrame: TextFrame<Glyph>;
|
||||||
readonly glyphStorage: GlyphStorage<Glyph>;
|
readonly glyphStorage: TextFrameGlyphStorage<Glyph>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class PathfinderGlyph {
|
export abstract class PathfinderGlyph {
|
||||||
|
|
Loading…
Reference in New Issue