21 changed files with 566 additions and 904 deletions
@ -1,41 +0,0 @@ |
|||
|
|||
export class DpiWatcher { |
|||
static lastDpi: number; |
|||
static timerId: number; |
|||
static callback: DotNet.DotNetObjectReference; |
|||
|
|||
public static getDpi() { |
|||
return window.devicePixelRatio; |
|||
} |
|||
|
|||
public static start(callback: DotNet.DotNetObjectReference): number { |
|||
//console.info(`Starting DPI watcher with callback ${callback._id}...`);
|
|||
|
|||
DpiWatcher.lastDpi = window.devicePixelRatio; |
|||
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); |
|||
DpiWatcher.callback = callback; |
|||
|
|||
return DpiWatcher.lastDpi; |
|||
} |
|||
|
|||
public static stop() { |
|||
//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
|
|||
|
|||
window.clearInterval(DpiWatcher.timerId); |
|||
|
|||
DpiWatcher.callback = undefined; |
|||
} |
|||
|
|||
static update() { |
|||
if (!DpiWatcher.callback) |
|||
return; |
|||
|
|||
const currentDpi = window.devicePixelRatio; |
|||
const lastDpi = DpiWatcher.lastDpi; |
|||
DpiWatcher.lastDpi = currentDpi; |
|||
|
|||
if (Math.abs(lastDpi - currentDpi) > 0.001) { |
|||
DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi); |
|||
} |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
|
|||
export class InputHelper { |
|||
public static clear (inputElement: HTMLInputElement){ |
|||
inputElement.value = ""; |
|||
} |
|||
|
|||
public static focus (inputElement: HTMLInputElement){ |
|||
inputElement.focus(); |
|||
inputElement.setSelectionRange(0, 0); |
|||
} |
|||
|
|||
public static setCursor (inputElement: HTMLInputElement, kind: string) { |
|||
inputElement.style.cursor = kind; |
|||
} |
|||
|
|||
public static hide (inputElement: HTMLInputElement){ |
|||
inputElement.style.display = 'none'; |
|||
} |
|||
|
|||
public static show (inputElement: HTMLInputElement){ |
|||
inputElement.style.display = 'block'; |
|||
} |
|||
} |
|||
@ -1,261 +0,0 @@ |
|||
// aliases for emscripten
|
|||
declare let GL: any; |
|||
declare let GLctx: WebGLRenderingContext; |
|||
declare let Module: EmscriptenModule; |
|||
|
|||
// container for gl info
|
|||
type SKGLViewInfo = { |
|||
context: WebGLRenderingContext | WebGL2RenderingContext | undefined; |
|||
fboId: number; |
|||
stencil: number; |
|||
sample: number; |
|||
depth: number; |
|||
} |
|||
|
|||
// alias for a potential skia html canvas
|
|||
type SKHtmlCanvasElement = { |
|||
SKHtmlCanvas: SKHtmlCanvas |
|||
} & HTMLCanvasElement |
|||
|
|||
export class SKHtmlCanvas { |
|||
static elements: Map<string, HTMLCanvasElement>; |
|||
|
|||
htmlCanvas: HTMLCanvasElement; |
|||
glInfo: SKGLViewInfo; |
|||
renderFrameCallback: DotNet.DotNetObjectReference; |
|||
renderLoopEnabled: boolean = false; |
|||
renderLoopRequest: number = 0; |
|||
newWidth: number; |
|||
newHeight: number; |
|||
|
|||
public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo { |
|||
var view = SKHtmlCanvas.init(true, element, elementId, callback); |
|||
if (!view || !view.glInfo) |
|||
return null; |
|||
|
|||
return view.glInfo; |
|||
} |
|||
|
|||
public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): boolean { |
|||
var view = SKHtmlCanvas.init(false, element, elementId, callback); |
|||
if (!view) |
|||
return false; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKHtmlCanvas { |
|||
var htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas) { |
|||
console.error(`No canvas element was provided.`); |
|||
return null; |
|||
} |
|||
|
|||
if (!SKHtmlCanvas.elements) |
|||
SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>(); |
|||
SKHtmlCanvas.elements[elementId] = element; |
|||
|
|||
const view = new SKHtmlCanvas(useGL, element, callback); |
|||
|
|||
htmlCanvas.SKHtmlCanvas = view; |
|||
|
|||
return view; |
|||
} |
|||
|
|||
public static deinit(elementId: string) { |
|||
if (!elementId) |
|||
return; |
|||
|
|||
const element = SKHtmlCanvas.elements[elementId]; |
|||
SKHtmlCanvas.elements.delete(elementId); |
|||
|
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.deinit(); |
|||
htmlCanvas.SKHtmlCanvas = undefined; |
|||
} |
|||
|
|||
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop); |
|||
} |
|||
|
|||
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) |
|||
{ |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height); |
|||
} |
|||
|
|||
public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable); |
|||
} |
|||
|
|||
public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height); |
|||
} |
|||
|
|||
public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObjectReference) { |
|||
this.htmlCanvas = element; |
|||
this.renderFrameCallback = callback; |
|||
|
|||
if (useGL) { |
|||
const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas); |
|||
if (!ctx) { |
|||
console.error(`Failed to create WebGL context: err ${ctx}`); |
|||
return null; |
|||
} |
|||
|
|||
// make current
|
|||
GL.makeContextCurrent(ctx); |
|||
|
|||
// read values
|
|||
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); |
|||
this.glInfo = { |
|||
context: ctx, |
|||
fboId: fbo ? fbo.id : 0, |
|||
stencil: GLctx.getParameter(GLctx.STENCIL_BITS), |
|||
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
|
|||
depth: GLctx.getParameter(GLctx.DEPTH_BITS), |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public deinit() { |
|||
this.setEnableRenderLoop(false); |
|||
} |
|||
|
|||
public setCanvasSize(width: number, height: number) |
|||
{ |
|||
this.newWidth = width; |
|||
this.newHeight = height; |
|||
|
|||
if(this.htmlCanvas.width != this.newWidth) |
|||
{ |
|||
this.htmlCanvas.width = this.newWidth; |
|||
} |
|||
|
|||
if(this.htmlCanvas.height != this.newHeight) |
|||
{ |
|||
this.htmlCanvas.height = this.newHeight; |
|||
} |
|||
|
|||
if (this.glInfo) { |
|||
// make current
|
|||
GL.makeContextCurrent(this.glInfo.context); |
|||
} |
|||
} |
|||
|
|||
public requestAnimationFrame(renderLoop?: boolean) { |
|||
// optionally update the render loop
|
|||
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) |
|||
this.setEnableRenderLoop(renderLoop); |
|||
|
|||
// skip because we have a render loop
|
|||
if (this.renderLoopRequest !== 0) |
|||
return; |
|||
|
|||
// add the draw to the next frame
|
|||
this.renderLoopRequest = window.requestAnimationFrame(() => { |
|||
if (this.glInfo) { |
|||
// make current
|
|||
GL.makeContextCurrent(this.glInfo.context); |
|||
} |
|||
|
|||
if(this.htmlCanvas.width != this.newWidth) |
|||
{ |
|||
this.htmlCanvas.width = this.newWidth; |
|||
} |
|||
|
|||
if(this.htmlCanvas.height != this.newHeight) |
|||
{ |
|||
this.htmlCanvas.height = this.newHeight; |
|||
} |
|||
|
|||
this.renderFrameCallback.invokeMethod('Invoke'); |
|||
this.renderLoopRequest = 0; |
|||
|
|||
// we may want to draw the next frame
|
|||
if (this.renderLoopEnabled) |
|||
this.requestAnimationFrame(); |
|||
}); |
|||
} |
|||
|
|||
public setEnableRenderLoop(enable: boolean) { |
|||
this.renderLoopEnabled = enable; |
|||
|
|||
// either start the new frame or cancel the existing one
|
|||
if (enable) { |
|||
//console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
|
|||
this.requestAnimationFrame(); |
|||
} else if (this.renderLoopRequest !== 0) { |
|||
window.cancelAnimationFrame(this.renderLoopRequest); |
|||
this.renderLoopRequest = 0; |
|||
} |
|||
} |
|||
|
|||
public putImageData(pData: number, width: number, height: number): boolean { |
|||
if (this.glInfo || !pData || width <= 0 || width <= 0) |
|||
return false; |
|||
|
|||
var ctx = this.htmlCanvas.getContext('2d'); |
|||
if (!ctx) { |
|||
console.error(`Failed to obtain 2D canvas context.`); |
|||
return false; |
|||
} |
|||
|
|||
// make sure the canvas is scaled correctly for the drawing
|
|||
this.htmlCanvas.width = width; |
|||
this.htmlCanvas.height = height; |
|||
|
|||
// set the canvas to be the bytes
|
|||
var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4); |
|||
var imageData = new ImageData(buffer, width, height); |
|||
ctx.putImageData(imageData, 0, 0); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { |
|||
const contextAttributes = { |
|||
alpha: 1, |
|||
depth: 1, |
|||
stencil: 8, |
|||
antialias: 0, |
|||
premultipliedAlpha: 1, |
|||
preserveDrawingBuffer: 0, |
|||
preferLowPowerToHighPerformance: 0, |
|||
failIfMajorPerformanceCaveat: 0, |
|||
majorVersion: 2, |
|||
minorVersion: 0, |
|||
enableExtensionsByDefault: 1, |
|||
explicitSwapControl: 0, |
|||
renderViaOffscreenBackBuffer: 1, |
|||
}; |
|||
|
|||
let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); |
|||
if (!ctx && contextAttributes.majorVersion > 1) { |
|||
console.warn('Falling back to WebGL 1.0'); |
|||
contextAttributes.majorVersion = 1; |
|||
contextAttributes.minorVersion = 0; |
|||
ctx = GL.createContext(htmlCanvas, contextAttributes); |
|||
} |
|||
|
|||
return ctx; |
|||
} |
|||
} |
|||
@ -1,68 +0,0 @@ |
|||
|
|||
type SizeWatcherElement = { |
|||
SizeWatcher: SizeWatcherInstance; |
|||
} & HTMLElement |
|||
|
|||
type SizeWatcherInstance = { |
|||
callback: DotNet.DotNetObjectReference; |
|||
} |
|||
|
|||
export class SizeWatcher { |
|||
static observer: ResizeObserver; |
|||
static elements: Map<string, HTMLElement>; |
|||
|
|||
public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObjectReference) { |
|||
if (!element || !callback) |
|||
return; |
|||
|
|||
//console.info(`Adding size watcher observation with callback ${callback._id}...`);
|
|||
|
|||
SizeWatcher.init(); |
|||
|
|||
const watcherElement = element as SizeWatcherElement; |
|||
watcherElement.SizeWatcher = { |
|||
callback: callback |
|||
}; |
|||
|
|||
SizeWatcher.elements[elementId] = element; |
|||
SizeWatcher.observer.observe(element); |
|||
|
|||
SizeWatcher.invoke(element); |
|||
} |
|||
|
|||
public static unobserve(elementId: string) { |
|||
if (!elementId || !SizeWatcher.observer) |
|||
return; |
|||
|
|||
//console.info('Removing size watcher observation...');
|
|||
|
|||
const element = SizeWatcher.elements[elementId]; |
|||
|
|||
SizeWatcher.elements.delete(elementId); |
|||
SizeWatcher.observer.unobserve(element); |
|||
} |
|||
|
|||
static init() { |
|||
if (SizeWatcher.observer) |
|||
return; |
|||
|
|||
//console.info('Starting size watcher...');
|
|||
|
|||
SizeWatcher.elements = new Map<string, HTMLElement>(); |
|||
SizeWatcher.observer = new ResizeObserver((entries) => { |
|||
for (let entry of entries) { |
|||
SizeWatcher.invoke(entry.target); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
static invoke(element: Element) { |
|||
const watcherElement = element as SizeWatcherElement; |
|||
const instance = watcherElement.SizeWatcher; |
|||
|
|||
if (!instance || !instance.callback) |
|||
return; |
|||
|
|||
return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight); |
|||
} |
|||
} |
|||
@ -1,7 +0,0 @@ |
|||
|
|||
declare namespace DotNet { |
|||
interface DotNetObjectReference extends DotNet.DotNetObject { |
|||
_id: number; |
|||
dispose(); |
|||
} |
|||
} |
|||
@ -1,326 +0,0 @@ |
|||
// Type definitions for Emscripten 1.39.16
|
|||
// Project: https://emscripten.org
|
|||
// Definitions by: Kensuke Matsuzaki <https://github.com/zakki>
|
|||
// Periklis Tsirakidis <https://github.com/periklis>
|
|||
// Bumsik Kim <https://github.com/kbumsik>
|
|||
// Louis DeScioli <https://github.com/lourd>
|
|||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
|||
// TypeScript Version: 2.2
|
|||
|
|||
/** Other WebAssembly declarations, for compatibility with older versions of Typescript */ |
|||
declare namespace WebAssembly { |
|||
interface Module {} |
|||
} |
|||
|
|||
declare namespace Emscripten { |
|||
interface FileSystemType {} |
|||
type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER'; |
|||
|
|||
type JSType = 'number' | 'string' | 'array' | 'boolean'; |
|||
type TypeCompatibleWithC = number | string | any[] | boolean; |
|||
|
|||
type CIntType = 'i8' | 'i16' | 'i32' | 'i64'; |
|||
type CFloatType = 'float' | 'double'; |
|||
type CPointerType = 'i8*' | 'i16*' | 'i32*' | 'i64*' | 'float*' | 'double*' | '*'; |
|||
type CType = CIntType | CFloatType | CPointerType; |
|||
|
|||
type WebAssemblyImports = Array<{ |
|||
name: string; |
|||
kind: string; |
|||
}>; |
|||
|
|||
type WebAssemblyExports = Array<{ |
|||
module: string; |
|||
name: string; |
|||
kind: string; |
|||
}>; |
|||
|
|||
interface CCallOpts { |
|||
async?: boolean | undefined; |
|||
} |
|||
} |
|||
|
|||
interface EmscriptenModule { |
|||
print(str: string): void; |
|||
printErr(str: string): void; |
|||
arguments: string[]; |
|||
environment: Emscripten.EnvironmentType; |
|||
preInit: Array<{ (): void }>; |
|||
preRun: Array<{ (): void }>; |
|||
postRun: Array<{ (): void }>; |
|||
onAbort: { (what: any): void }; |
|||
onRuntimeInitialized: { (): void }; |
|||
preinitializedWebGLContext: WebGLRenderingContext; |
|||
noInitialRun: boolean; |
|||
noExitRuntime: boolean; |
|||
logReadFiles: boolean; |
|||
filePackagePrefixURL: string; |
|||
wasmBinary: ArrayBuffer; |
|||
|
|||
destroy(object: object): void; |
|||
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer; |
|||
instantiateWasm( |
|||
imports: Emscripten.WebAssemblyImports, |
|||
successCallback: (module: WebAssembly.Module) => void, |
|||
): Emscripten.WebAssemblyExports; |
|||
locateFile(url: string, scriptDirectory: string): string; |
|||
onCustomMessage(event: MessageEvent): void; |
|||
|
|||
// USE_TYPED_ARRAYS == 1
|
|||
HEAP: Int32Array; |
|||
IHEAP: Int32Array; |
|||
FHEAP: Float64Array; |
|||
|
|||
// USE_TYPED_ARRAYS == 2
|
|||
HEAP8: Int8Array; |
|||
HEAP16: Int16Array; |
|||
HEAP32: Int32Array; |
|||
HEAPU8: Uint8Array; |
|||
HEAPU16: Uint16Array; |
|||
HEAPU32: Uint32Array; |
|||
HEAPF32: Float32Array; |
|||
HEAPF64: Float64Array; |
|||
|
|||
TOTAL_STACK: number; |
|||
TOTAL_MEMORY: number; |
|||
FAST_MEMORY: number; |
|||
|
|||
addOnPreRun(cb: () => any): void; |
|||
addOnInit(cb: () => any): void; |
|||
addOnPreMain(cb: () => any): void; |
|||
addOnExit(cb: () => any): void; |
|||
addOnPostRun(cb: () => any): void; |
|||
|
|||
preloadedImages: any; |
|||
preloadedAudios: any; |
|||
|
|||
_malloc(size: number): number; |
|||
_free(ptr: number): void; |
|||
} |
|||
|
|||
/** |
|||
* A factory function is generated when setting the `MODULARIZE` build option |
|||
* to `1` in your Emscripten build. It return a Promise that resolves to an |
|||
* initialized, ready-to-call `EmscriptenModule` instance. |
|||
* |
|||
* By default, the factory function will be named `Module`. It's recommended to |
|||
* use the `EXPORT_ES6` option, in which the factory function will be the |
|||
* default export. If used without `EXPORT_ES6`, the factory function will be a |
|||
* global variable. You can rename the variable using the `EXPORT_NAME` build |
|||
* option. It's left to you to declare any global variables as needed in your |
|||
* application's types. |
|||
* @param moduleOverrides Default properties for the initialized module. |
|||
*/ |
|||
type EmscriptenModuleFactory<T extends EmscriptenModule = EmscriptenModule> = ( |
|||
moduleOverrides?: Partial<T>, |
|||
) => Promise<T>; |
|||
|
|||
declare namespace FS { |
|||
interface Lookup { |
|||
path: string; |
|||
node: FSNode; |
|||
} |
|||
|
|||
interface FSStream {} |
|||
interface FSNode {} |
|||
interface ErrnoError {} |
|||
|
|||
let ignorePermissions: boolean; |
|||
let trackingDelegate: any; |
|||
let tracking: any; |
|||
let genericErrors: any; |
|||
|
|||
//
|
|||
// paths
|
|||
//
|
|||
function lookupPath(path: string, opts: any): Lookup; |
|||
function getPath(node: FSNode): string; |
|||
|
|||
//
|
|||
// nodes
|
|||
//
|
|||
function isFile(mode: number): boolean; |
|||
function isDir(mode: number): boolean; |
|||
function isLink(mode: number): boolean; |
|||
function isChrdev(mode: number): boolean; |
|||
function isBlkdev(mode: number): boolean; |
|||
function isFIFO(mode: number): boolean; |
|||
function isSocket(mode: number): boolean; |
|||
|
|||
//
|
|||
// devices
|
|||
//
|
|||
function major(dev: number): number; |
|||
function minor(dev: number): number; |
|||
function makedev(ma: number, mi: number): number; |
|||
function registerDevice(dev: number, ops: any): void; |
|||
|
|||
//
|
|||
// core
|
|||
//
|
|||
function syncfs(populate: boolean, callback: (e: any) => any): void; |
|||
function syncfs(callback: (e: any) => any, populate?: boolean): void; |
|||
function mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any; |
|||
function unmount(mountpoint: string): void; |
|||
|
|||
function mkdir(path: string, mode?: number): any; |
|||
function mkdev(path: string, mode?: number, dev?: number): any; |
|||
function symlink(oldpath: string, newpath: string): any; |
|||
function rename(old_path: string, new_path: string): void; |
|||
function rmdir(path: string): void; |
|||
function readdir(path: string): any; |
|||
function unlink(path: string): void; |
|||
function readlink(path: string): string; |
|||
function stat(path: string, dontFollow?: boolean): any; |
|||
function lstat(path: string): any; |
|||
function chmod(path: string, mode: number, dontFollow?: boolean): void; |
|||
function lchmod(path: string, mode: number): void; |
|||
function fchmod(fd: number, mode: number): void; |
|||
function chown(path: string, uid: number, gid: number, dontFollow?: boolean): void; |
|||
function lchown(path: string, uid: number, gid: number): void; |
|||
function fchown(fd: number, uid: number, gid: number): void; |
|||
function truncate(path: string, len: number): void; |
|||
function ftruncate(fd: number, len: number): void; |
|||
function utime(path: string, atime: number, mtime: number): void; |
|||
function open(path: string, flags: string, mode?: number, fd_start?: number, fd_end?: number): FSStream; |
|||
function close(stream: FSStream): void; |
|||
function llseek(stream: FSStream, offset: number, whence: number): any; |
|||
function read(stream: FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number; |
|||
function write( |
|||
stream: FSStream, |
|||
buffer: ArrayBufferView, |
|||
offset: number, |
|||
length: number, |
|||
position?: number, |
|||
canOwn?: boolean, |
|||
): number; |
|||
function allocate(stream: FSStream, offset: number, length: number): void; |
|||
function mmap( |
|||
stream: FSStream, |
|||
buffer: ArrayBufferView, |
|||
offset: number, |
|||
length: number, |
|||
position: number, |
|||
prot: number, |
|||
flags: number, |
|||
): any; |
|||
function ioctl(stream: FSStream, cmd: any, arg: any): any; |
|||
function readFile(path: string, opts: { encoding: 'binary'; flags?: string | undefined }): Uint8Array; |
|||
function readFile(path: string, opts: { encoding: 'utf8'; flags?: string | undefined }): string; |
|||
function readFile(path: string, opts?: { flags?: string | undefined }): Uint8Array; |
|||
function writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string | undefined }): void; |
|||
|
|||
//
|
|||
// module-level FS code
|
|||
//
|
|||
function cwd(): string; |
|||
function chdir(path: string): void; |
|||
function init( |
|||
input: null | (() => number | null), |
|||
output: null | ((c: number) => any), |
|||
error: null | ((c: number) => any), |
|||
): void; |
|||
|
|||
function createLazyFile( |
|||
parent: string | FSNode, |
|||
name: string, |
|||
url: string, |
|||
canRead: boolean, |
|||
canWrite: boolean, |
|||
): FSNode; |
|||
function createPreloadedFile( |
|||
parent: string | FSNode, |
|||
name: string, |
|||
url: string, |
|||
canRead: boolean, |
|||
canWrite: boolean, |
|||
onload?: () => void, |
|||
onerror?: () => void, |
|||
dontCreateFile?: boolean, |
|||
canOwn?: boolean, |
|||
): void; |
|||
function createDataFile( |
|||
parent: string | FSNode, |
|||
name: string, |
|||
data: ArrayBufferView, |
|||
canRead: boolean, |
|||
canWrite: boolean, |
|||
canOwn: boolean, |
|||
): FSNode; |
|||
} |
|||
|
|||
declare var MEMFS: Emscripten.FileSystemType; |
|||
declare var NODEFS: Emscripten.FileSystemType; |
|||
declare var IDBFS: Emscripten.FileSystemType; |
|||
|
|||
// Below runtime function/variable declarations are exportable by
|
|||
// -s EXTRA_EXPORTED_RUNTIME_METHODS. You can extend or merge
|
|||
// EmscriptenModule interface to add runtime functions.
|
|||
//
|
|||
// For example, by using -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"
|
|||
// You can access ccall() via Module["ccall"]. In this case, you should
|
|||
// extend EmscriptenModule to pass the compiler check like the following:
|
|||
//
|
|||
// interface YourOwnEmscriptenModule extends EmscriptenModule {
|
|||
// ccall: typeof ccall;
|
|||
// }
|
|||
//
|
|||
// See: https://emscripten.org/docs/getting_started/FAQ.html#why-do-i-get-typeerror-module-something-is-not-a-function
|
|||
|
|||
declare function ccall( |
|||
ident: string, |
|||
returnType: Emscripten.JSType | null, |
|||
argTypes: Emscripten.JSType[], |
|||
args: Emscripten.TypeCompatibleWithC[], |
|||
opts?: Emscripten.CCallOpts, |
|||
): any; |
|||
declare function cwrap( |
|||
ident: string, |
|||
returnType: Emscripten.JSType | null, |
|||
argTypes: Emscripten.JSType[], |
|||
opts?: Emscripten.CCallOpts, |
|||
): (...args: any[]) => any; |
|||
|
|||
declare function setValue(ptr: number, value: any, type: Emscripten.CType, noSafe?: boolean): void; |
|||
declare function getValue(ptr: number, type: Emscripten.CType, noSafe?: boolean): number; |
|||
|
|||
declare function allocate( |
|||
slab: number[] | ArrayBufferView | number, |
|||
types: Emscripten.CType | Emscripten.CType[], |
|||
allocator: number, |
|||
ptr?: number, |
|||
): number; |
|||
|
|||
declare function stackAlloc(size: number): number; |
|||
declare function stackSave(): number; |
|||
declare function stackRestore(ptr: number): void; |
|||
|
|||
declare function UTF8ToString(ptr: number, maxBytesToRead?: number): string; |
|||
declare function stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void; |
|||
declare function lengthBytesUTF8(str: string): number; |
|||
declare function allocateUTF8(str: string): number; |
|||
declare function allocateUTF8OnStack(str: string): number; |
|||
declare function UTF16ToString(ptr: number): string; |
|||
declare function stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void; |
|||
declare function lengthBytesUTF16(str: string): number; |
|||
declare function UTF32ToString(ptr: number): string; |
|||
declare function stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void; |
|||
declare function lengthBytesUTF32(str: string): number; |
|||
|
|||
declare function intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[]; |
|||
declare function intArrayToString(array: number[]): string; |
|||
declare function writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void; |
|||
declare function writeArrayToMemory(array: number[], buffer: number): void; |
|||
declare function writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void; |
|||
|
|||
declare function addRunDependency(id: any): void; |
|||
declare function removeRunDependency(id: any): void; |
|||
|
|||
declare function addFunction(func: (...args: any[]) => any, signature?: string): number; |
|||
declare function removeFunction(funcPtr: number): void; |
|||
|
|||
declare var ALLOC_NORMAL: number; |
|||
declare var ALLOC_STACK: number; |
|||
declare var ALLOC_STATIC: number; |
|||
declare var ALLOC_DYNAMIC: number; |
|||
declare var ALLOC_NONE: number; |
|||
@ -1,14 +0,0 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"noImplicitAny": false, |
|||
"noEmitOnError": true, |
|||
"removeComments": false, |
|||
"sourceMap": true, |
|||
"target": "ES2020", |
|||
"module": "ES2020", |
|||
"outDir": "wwwroot" |
|||
}, |
|||
"exclude": [ |
|||
"node_modules" |
|||
] |
|||
} |
|||
@ -0,0 +1 @@ |
|||
|
|||
@ -0,0 +1,40 @@ |
|||
export class DpiWatcher { |
|||
static lastDpi: number; |
|||
static timerId: number; |
|||
static callback?: DotNet.DotNetObject; |
|||
|
|||
public static getDpi() { |
|||
return window.devicePixelRatio; |
|||
} |
|||
|
|||
public static start(callback: DotNet.DotNetObject): number { |
|||
//console.info(`Starting DPI watcher with callback ${callback._id}...`);
|
|||
|
|||
DpiWatcher.lastDpi = window.devicePixelRatio; |
|||
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); |
|||
DpiWatcher.callback = callback; |
|||
|
|||
return DpiWatcher.lastDpi; |
|||
} |
|||
|
|||
public static stop() { |
|||
//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
|
|||
|
|||
window.clearInterval(DpiWatcher.timerId); |
|||
|
|||
DpiWatcher.callback = undefined; |
|||
} |
|||
|
|||
static update() { |
|||
if (!DpiWatcher.callback) |
|||
return; |
|||
|
|||
const currentDpi = window.devicePixelRatio; |
|||
const lastDpi = DpiWatcher.lastDpi; |
|||
DpiWatcher.lastDpi = currentDpi; |
|||
|
|||
if (Math.abs(lastDpi - currentDpi) > 0.001) { |
|||
DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
class InnerDbConnection { |
|||
constructor(private database: IDBDatabase) { } |
|||
|
|||
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore { |
|||
const tx = this.database.transaction(store, mode); |
|||
return tx.objectStore(store); |
|||
} |
|||
|
|||
public put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> { |
|||
const os = this.openStore(store, "readwrite"); |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
const response = os.put(obj, key); |
|||
response.onsuccess = () => { |
|||
resolve(response.result); |
|||
}; |
|||
response.onerror = () => { |
|||
reject(response.error); |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public get(store: string, key: IDBValidKey): any { |
|||
const os = this.openStore(store, "readonly"); |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
const response = os.get(key); |
|||
response.onsuccess = () => { |
|||
resolve(response.result); |
|||
}; |
|||
response.onerror = () => { |
|||
reject(response.error); |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public delete(store: string, key: IDBValidKey): Promise<void> { |
|||
const os = this.openStore(store, "readwrite"); |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
const response = os.delete(key); |
|||
response.onsuccess = () => { |
|||
resolve(); |
|||
}; |
|||
response.onerror = () => { |
|||
reject(response.error); |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public close() { |
|||
this.database.close(); |
|||
} |
|||
} |
|||
|
|||
export class IndexedDbWrapper { |
|||
constructor(private databaseName: string, private objectStores: [string]) { |
|||
} |
|||
|
|||
public connect(): Promise<InnerDbConnection> { |
|||
const conn = window.indexedDB.open(this.databaseName, 1); |
|||
|
|||
conn.onupgradeneeded = event => { |
|||
const db = (<IDBRequest<IDBDatabase>>event.target).result; |
|||
this.objectStores.forEach(store => { |
|||
db.createObjectStore(store); |
|||
}); |
|||
}; |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
conn.onsuccess = event => { |
|||
resolve(new InnerDbConnection((<IDBRequest<IDBDatabase>>event.target).result)); |
|||
}; |
|||
conn.onerror = event => { |
|||
reject((<IDBRequest<IDBDatabase>>event.target).error); |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
export class InputHelper { |
|||
public static clear(inputElement: HTMLInputElement) { |
|||
inputElement.value = ""; |
|||
} |
|||
|
|||
public static focus(inputElement: HTMLInputElement) { |
|||
inputElement.focus(); |
|||
inputElement.setSelectionRange(0, 0); |
|||
} |
|||
|
|||
public static setCursor(inputElement: HTMLInputElement, kind: string) { |
|||
inputElement.style.cursor = kind; |
|||
} |
|||
|
|||
public static hide(inputElement: HTMLInputElement) { |
|||
inputElement.style.display = 'none'; |
|||
} |
|||
|
|||
public static show(inputElement: HTMLInputElement) { |
|||
inputElement.style.display = 'block'; |
|||
} |
|||
} |
|||
@ -0,0 +1,255 @@ |
|||
// aliases for emscripten
|
|||
declare let GL: any; |
|||
declare let GLctx: WebGLRenderingContext; |
|||
declare let Module: EmscriptenModule; |
|||
|
|||
// container for gl info
|
|||
type SKGLViewInfo = { |
|||
context: WebGLRenderingContext | WebGL2RenderingContext | undefined; |
|||
fboId: number; |
|||
stencil: number; |
|||
sample: number; |
|||
depth: number; |
|||
} |
|||
|
|||
// alias for a potential skia html canvas
|
|||
type SKHtmlCanvasElement = { |
|||
SKHtmlCanvas: SKHtmlCanvas | undefined |
|||
} & HTMLCanvasElement |
|||
|
|||
export class SKHtmlCanvas { |
|||
static elements: Map<string, HTMLCanvasElement>; |
|||
|
|||
htmlCanvas: HTMLCanvasElement; |
|||
glInfo?: SKGLViewInfo; |
|||
renderFrameCallback: DotNet.DotNetObject; |
|||
renderLoopEnabled: boolean = false; |
|||
renderLoopRequest: number = 0; |
|||
newWidth?: number; |
|||
newHeight?: number; |
|||
|
|||
public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKGLViewInfo | null { |
|||
var view = SKHtmlCanvas.init(true, element, elementId, callback); |
|||
if (!view || !view.glInfo) |
|||
return null; |
|||
|
|||
return view.glInfo; |
|||
} |
|||
|
|||
public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): boolean { |
|||
var view = SKHtmlCanvas.init(false, element, elementId, callback); |
|||
if (!view) |
|||
return false; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKHtmlCanvas | null { |
|||
var htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas) { |
|||
console.error(`No canvas element was provided.`); |
|||
return null; |
|||
} |
|||
|
|||
if (!SKHtmlCanvas.elements) |
|||
SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>(); |
|||
SKHtmlCanvas.elements.set(elementId, element); |
|||
|
|||
const view = new SKHtmlCanvas(useGL, element, callback); |
|||
|
|||
htmlCanvas.SKHtmlCanvas = view; |
|||
|
|||
return view; |
|||
} |
|||
|
|||
public static deinit(elementId: string) { |
|||
if (!elementId) |
|||
return; |
|||
|
|||
const element = SKHtmlCanvas.elements.get(elementId); |
|||
SKHtmlCanvas.elements.delete(elementId); |
|||
|
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.deinit(); |
|||
htmlCanvas.SKHtmlCanvas = undefined; |
|||
} |
|||
|
|||
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop); |
|||
} |
|||
|
|||
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height); |
|||
} |
|||
|
|||
public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable); |
|||
} |
|||
|
|||
public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height); |
|||
} |
|||
|
|||
public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObject) { |
|||
this.htmlCanvas = element; |
|||
this.renderFrameCallback = callback; |
|||
|
|||
if (useGL) { |
|||
const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas); |
|||
if (!ctx) { |
|||
console.error(`Failed to create WebGL context: err ${ctx}`); |
|||
return; |
|||
} |
|||
|
|||
// make current
|
|||
GL.makeContextCurrent(ctx); |
|||
|
|||
// read values
|
|||
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); |
|||
this.glInfo = { |
|||
context: ctx, |
|||
fboId: fbo ? fbo.id : 0, |
|||
stencil: GLctx.getParameter(GLctx.STENCIL_BITS), |
|||
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
|
|||
depth: GLctx.getParameter(GLctx.DEPTH_BITS), |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public deinit() { |
|||
this.setEnableRenderLoop(false); |
|||
} |
|||
|
|||
public setCanvasSize(width: number, height: number) { |
|||
this.newWidth = width; |
|||
this.newHeight = height; |
|||
|
|||
if (this.htmlCanvas.width != this.newWidth) { |
|||
this.htmlCanvas.width = this.newWidth; |
|||
} |
|||
|
|||
if (this.htmlCanvas.height != this.newHeight) { |
|||
this.htmlCanvas.height = this.newHeight; |
|||
} |
|||
|
|||
if (this.glInfo) { |
|||
// make current
|
|||
GL.makeContextCurrent(this.glInfo.context); |
|||
} |
|||
} |
|||
|
|||
public requestAnimationFrame(renderLoop?: boolean) { |
|||
// optionally update the render loop
|
|||
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) |
|||
this.setEnableRenderLoop(renderLoop); |
|||
|
|||
// skip because we have a render loop
|
|||
if (this.renderLoopRequest !== 0) |
|||
return; |
|||
|
|||
// add the draw to the next frame
|
|||
this.renderLoopRequest = window.requestAnimationFrame(() => { |
|||
if (this.glInfo) { |
|||
// make current
|
|||
GL.makeContextCurrent(this.glInfo.context); |
|||
} |
|||
|
|||
if (this.htmlCanvas.width != this.newWidth) { |
|||
this.htmlCanvas.width = this.newWidth || 0; |
|||
} |
|||
|
|||
if (this.htmlCanvas.height != this.newHeight) { |
|||
this.htmlCanvas.height = this.newHeight || 0; |
|||
} |
|||
|
|||
this.renderFrameCallback.invokeMethod('Invoke'); |
|||
this.renderLoopRequest = 0; |
|||
|
|||
// we may want to draw the next frame
|
|||
if (this.renderLoopEnabled) |
|||
this.requestAnimationFrame(); |
|||
}); |
|||
} |
|||
|
|||
public setEnableRenderLoop(enable: boolean) { |
|||
this.renderLoopEnabled = enable; |
|||
|
|||
// either start the new frame or cancel the existing one
|
|||
if (enable) { |
|||
//console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
|
|||
this.requestAnimationFrame(); |
|||
} else if (this.renderLoopRequest !== 0) { |
|||
window.cancelAnimationFrame(this.renderLoopRequest); |
|||
this.renderLoopRequest = 0; |
|||
} |
|||
} |
|||
|
|||
public putImageData(pData: number, width: number, height: number): boolean { |
|||
if (this.glInfo || !pData || width <= 0 || width <= 0) |
|||
return false; |
|||
|
|||
var ctx = this.htmlCanvas.getContext('2d'); |
|||
if (!ctx) { |
|||
console.error(`Failed to obtain 2D canvas context.`); |
|||
return false; |
|||
} |
|||
|
|||
// make sure the canvas is scaled correctly for the drawing
|
|||
this.htmlCanvas.width = width; |
|||
this.htmlCanvas.height = height; |
|||
|
|||
// set the canvas to be the bytes
|
|||
var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4); |
|||
var imageData = new ImageData(buffer, width, height); |
|||
ctx.putImageData(imageData, 0, 0); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { |
|||
const contextAttributes = { |
|||
alpha: 1, |
|||
depth: 1, |
|||
stencil: 8, |
|||
antialias: 0, |
|||
premultipliedAlpha: 1, |
|||
preserveDrawingBuffer: 0, |
|||
preferLowPowerToHighPerformance: 0, |
|||
failIfMajorPerformanceCaveat: 0, |
|||
majorVersion: 2, |
|||
minorVersion: 0, |
|||
enableExtensionsByDefault: 1, |
|||
explicitSwapControl: 0, |
|||
renderViaOffscreenBackBuffer: 1, |
|||
}; |
|||
|
|||
let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); |
|||
if (!ctx && contextAttributes.majorVersion > 1) { |
|||
console.warn('Falling back to WebGL 1.0'); |
|||
contextAttributes.majorVersion = 1; |
|||
contextAttributes.minorVersion = 0; |
|||
ctx = GL.createContext(htmlCanvas, contextAttributes); |
|||
} |
|||
|
|||
return ctx; |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
type SizeWatcherElement = { |
|||
SizeWatcher: SizeWatcherInstance; |
|||
} & HTMLElement |
|||
|
|||
type SizeWatcherInstance = { |
|||
callback: DotNet.DotNetObject; |
|||
} |
|||
|
|||
export class SizeWatcher { |
|||
static observer: ResizeObserver; |
|||
static elements: Map<string, HTMLElement>; |
|||
|
|||
public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObject) { |
|||
if (!element || !callback) |
|||
return; |
|||
|
|||
//console.info(`Adding size watcher observation with callback ${callback._id}...`);
|
|||
|
|||
SizeWatcher.init(); |
|||
|
|||
const watcherElement = element as SizeWatcherElement; |
|||
watcherElement.SizeWatcher = { |
|||
callback: callback |
|||
}; |
|||
|
|||
SizeWatcher.elements.set(elementId, element); |
|||
SizeWatcher.observer.observe(element); |
|||
|
|||
SizeWatcher.invoke(element); |
|||
} |
|||
|
|||
public static unobserve(elementId: string) { |
|||
if (!elementId || !SizeWatcher.observer) |
|||
return; |
|||
|
|||
//console.info('Removing size watcher observation...');
|
|||
|
|||
const element = SizeWatcher.elements.get(elementId)!; |
|||
|
|||
SizeWatcher.elements.delete(elementId); |
|||
SizeWatcher.observer.unobserve(element); |
|||
} |
|||
|
|||
static init() { |
|||
if (SizeWatcher.observer) |
|||
return; |
|||
|
|||
//console.info('Starting size watcher...');
|
|||
|
|||
SizeWatcher.elements = new Map<string, HTMLElement>(); |
|||
SizeWatcher.observer = new ResizeObserver((entries) => { |
|||
for (let entry of entries) { |
|||
SizeWatcher.invoke(entry.target); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
static invoke(element: Element) { |
|||
const watcherElement = element as SizeWatcherElement; |
|||
const instance = watcherElement.SizeWatcher; |
|||
|
|||
if (!instance || !instance.callback) |
|||
return; |
|||
|
|||
return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"name": "avalonia.web", |
|||
"scripts": { |
|||
"build": "tsc --build", |
|||
"clean": "tsc --build --clean" |
|||
}, |
|||
"devDependencies": { |
|||
"@types/emscripten": "^1.39.6", |
|||
"@types/wicg-file-system-access": "^2020.9.5", |
|||
"typescript": "^4.7.4", |
|||
"webpack": "^5.73.0", |
|||
"webpack-cli": "^4.10.0" |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "ES6", |
|||
"strict": true, |
|||
"sourceMap": true, |
|||
"outDir": "../wwwroot", |
|||
"removeComments": true, |
|||
"noEmitOnError": true, |
|||
"lib": [ |
|||
"dom", |
|||
"ES6", |
|||
"esnext.asynciterable" |
|||
] |
|||
}, |
|||
"exclude": [ |
|||
"node_modules" |
|||
] |
|||
} |
|||
Loading…
Reference in new issue