diff --git a/packages/editor/package.json b/packages/editor/package.json index a2884af..a51d641 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -15,9 +15,9 @@ "dependencies": { "@ant-design/colors": "^7.0.2", "@astral3d/engine": "workspace:^", - "@gltf-transform/core": "^4.0.8", - "@gltf-transform/extensions": "^4.0.8", - "@gltf-transform/functions": "^4.0.8", + "@gltf-transform/core": "^4.2.1", + "@gltf-transform/extensions": "^4.2.1", + "@gltf-transform/functions": "^4.2.1", "@vicons/carbon": "^0.12.0", "@vicons/ionicons5": "^0.12.0", "@vueuse/core": "^13.2.0", diff --git a/packages/editor/public/wasm/Astral3DglTFHandler.wasm b/packages/editor/public/wasm/Astral3DglTFHandler.wasm new file mode 100644 index 0000000..d5c7769 Binary files /dev/null and b/packages/editor/public/wasm/Astral3DglTFHandler.wasm differ diff --git a/packages/editor/src/plugin/glTFHandler/glTFHandler.ts b/packages/editor/src/plugin/glTFHandler/glTFHandler.ts index ba7665a..1086999 100644 --- a/packages/editor/src/plugin/glTFHandler/glTFHandler.ts +++ b/packages/editor/src/plugin/glTFHandler/glTFHandler.ts @@ -33,6 +33,7 @@ import { import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; import {Session} from "./session"; import {loadScript} from "@/utils/common/utils"; +import { optimizePNG } from "@/plugin/glTFHandler/optimizePng"; //使用'micromatch',因为'contains: true'没有像预期的那样在minimatch中工作。需要确保'*'匹配的模式,如'image/png'。 export const MICROMATCH_OPTIONS = { nocase: true, contains: true }; @@ -91,7 +92,6 @@ export default class GLTFHandler implements Plugin{ } this.GLTFHandlerComponentRef = ref(); - const finishFn = this.finish.bind(this); this.modalInstance = window.$modal.create({ title: this.name, preset:"card", @@ -100,12 +100,12 @@ export default class GLTFHandler implements Plugin{ width: '90%', maxWidth: '800px' }, - onAfterLeave: finishFn, + onAfterLeave: () => this.finish(), content: () => { return h(GLTFHandlerComponent,{ - onOptimize:this.optimize.bind(this), - onFinish: finishFn, - ref:this.GLTFHandlerComponentRef + onOptimize: this.optimize.bind(this), + onFinish: () => this.finish(), + ref: this.GLTFHandlerComponentRef },"") }, }) @@ -137,7 +137,6 @@ export default class GLTFHandler implements Plugin{ /* 下面是实现的自定义的处理器方法 */ async optimize(opts:IPlugin.GLTFHandlerOptimizeModel,inputFile:File,outputFileName = ""){ - // console.log("调用优化处理器,",opts,inputFile) this.setLogger(`Optimize ${inputFile.name}`); if(this.dracoScript.failMsg){ @@ -151,7 +150,10 @@ export default class GLTFHandler implements Plugin{ /* 文件准备就绪,开始优化 */ - const transforms: Transform[] = [dedup()]; + const transforms: Transform[] = [ + optimizePNG(), + dedup() + ]; if (opts.instance) transforms.push(instance({ min: opts.instanceMin })); diff --git a/packages/editor/src/plugin/glTFHandler/optimizePng.ts b/packages/editor/src/plugin/glTFHandler/optimizePng.ts new file mode 100644 index 0000000..4d6a716 --- /dev/null +++ b/packages/editor/src/plugin/glTFHandler/optimizePng.ts @@ -0,0 +1,26 @@ +import type { Transform } from '@gltf-transform/core'; +import { encodePNG } from './util'; + +function asUint8Array(data: unknown): Uint8Array { + if (data instanceof Uint8Array) return data; + if (data instanceof ArrayBuffer) return new Uint8Array(data); + throw new Error('Unsupported texture image type'); +} + +export const optimizePNG = (): Transform => async (doc) => { + const textures = doc.getRoot().listTextures(); + for (const tex of textures) { + // 仅处理 PNG + if (tex.getMimeType() !== 'image/png') continue; + + const image = tex.getImage(); + if (!image || image.byteLength === 0) continue; + + // 归一化为 Uint8Array + const imgU8 = asUint8Array(image); + + const stamped = await encodePNG(imgU8); + tex.setImage(stamped); + tex.setMimeType('image/png'); + } +}; diff --git a/packages/editor/src/plugin/glTFHandler/session.ts b/packages/editor/src/plugin/glTFHandler/session.ts index e14b496..b6f62ee 100644 --- a/packages/editor/src/plugin/glTFHandler/session.ts +++ b/packages/editor/src/plugin/glTFHandler/session.ts @@ -1,125 +1,136 @@ -import {Document, WebIO, FileUtils, Transform, Format, Logger} from '@gltf-transform/core'; +import { Document, WebIO, FileUtils, Transform, Format, Logger } from '@gltf-transform/core'; import type { Packet, KHRXMP } from '@gltf-transform/extensions'; import { unpartition } from '@gltf-transform/functions'; -import {Listr} from "./Listr"; -import { formatBytes, XMPContext } from './util.js'; +import { Listr } from "./Listr"; +import { formatBytes, encodeGLB, XMPContext } from './util.js'; import GLTFHandler from "./glTFHandler"; export class Session { - private _outputFormat: Format; - private _display = false; + private _outputFormat: Format; + private _display = false; - constructor( - private _io: WebIO, - private _logger: Logger, - private setLogger: (log:string) => void, - private _input: string, - private _inputName: string, - private _output: string, - ) { - _io.setLogger(_logger); - this._outputFormat = FileUtils.extension(_output) === 'glb' ? Format.GLB : Format.GLTF; - } + constructor( + private _io: WebIO, + private _logger: Logger, + private setLogger: (log: string) => void, + private _input: string, + private _inputName: string, + private _output: string, + ) { + _io.setLogger(_logger); + this._outputFormat = FileUtils.extension(_output) === 'glb' ? Format.GLB : Format.GLTF; + } - public static create(handler:GLTFHandler, inputFileUrl: string,inputName:string, output: string): Session { - return new Session(handler.io, handler.logger, handler.setLogger.bind(handler),inputFileUrl, inputName,output); - } + public static create(handler: GLTFHandler, inputFileUrl: string, inputName: string, output: string): Session { + return new Session(handler.io, handler.logger, handler.setLogger.bind(handler), inputFileUrl, inputName, output); + } - public setDisplay(display: boolean): this { - this._display = display; - return this; - } + public setDisplay(display: boolean): this { + this._display = display; + return this; + } - public async transform(...transforms: Transform[]): Promise { - this.setLogger("Start"); + public async transform(...transforms: Transform[]): Promise { + this.setLogger("Start"); - let _document = this._input - ? (await this._io.read(this._input)).setLogger(this._logger) - : new Document().setLogger(this._logger); + let _document = this._input + ? (await this._io.read(this._input)).setLogger(this._logger) + : new Document().setLogger(this._logger); - // Warn and remove lossy compression, to avoid increasing loss on round trip. - for (const extensionName of ['KHR_draco_mesh_compression', 'EXT_meshopt_compression']) { - const extension = _document - .getRoot() - .listExtensionsUsed() - .find((extension) => extension.extensionName === extensionName); - if (extension) { - extension.dispose(); - this._logger.warn(`Decoded ${extensionName}. Further compression will be lossy.`); - } - } + // 警告和消除有损压缩,以避免增加往返的损失。 + for (const extensionName of ['KHR_draco_mesh_compression', 'EXT_meshopt_compression']) { + const extension = _document + .getRoot() + .listExtensionsUsed() + .find((extension) => extension.extensionName === extensionName); + if (extension) { + extension.dispose(); + this._logger.warn(`Decoded ${extensionName}. Further compression will be lossy.`); + } + } - if (this._display) { - const tasks = [] as { title:string,task:(task: any) => Promise }[]; - for (const transform of transforms) { - tasks.push({ - title: transform.name, - task: async (task) => { - try{ - this.setLogger(task.title) - let time = performance.now(); - _document = await _document.transform(transform); - time = Math.round(performance.now() - time); - this.setLogger(task.title.padEnd(20) + ` ${time}ms`) - }catch (error:unknown){ - // @ts-ignore - this.setLogger(`${task.title} run fail: ${error?.message || error}`) - } - }, - }); - } + if (this._display) { + const tasks = [] as { title: string, task: (task: any) => Promise }[]; + for (const transform of transforms) { + tasks.push({ + title: transform.name, + task: async (task) => { + try { + this.setLogger(task.title) + let time = performance.now(); + _document = await _document.transform(transform); + time = Math.round(performance.now() - time); + this.setLogger(task.title.padEnd(20) + ` ${time}ms`) + } catch (error: unknown) { + // @ts-ignore + this.setLogger(`${task.title} run fail: ${error?.message || error}`) + } + }, + }); + } - await new Listr(tasks).run(); - } else { - await _document.transform(...transforms); - } + await new Listr(tasks).run(); + } else { + await _document.transform(...transforms); + } - await _document.transform(updateMetadata); + await _document.transform(updateMetadata); - if (this._outputFormat === Format.GLB) { - await _document.transform(unpartition()); - } + if (this._outputFormat === Format.GLB) { + await _document.transform(unpartition()); + } - const outputUint8Array = await this._io.writeBinary(_document); - // Uint8Array转file - const mimeType = this._outputFormat === Format.GLB ? "model/gltf-binary" : "model/gltf+json"; - const blob = new Blob([outputUint8Array], { type: mimeType}); - const outputFile = new File([blob], this._output, { type: mimeType }); + const rawU8 = await this._io.writeBinary(_document); - const { lastReadBytes, lastWriteBytes } = this._io; - if (!this._input) { - const output = FileUtils.basename(this._output) + '.' + FileUtils.extension(this._output); - this._logger.info(`${output} (${formatBytes(lastWriteBytes)})`); - } else { - const input = FileUtils.basename(this._inputName) + '.' + FileUtils.extension(this._inputName); - const output = FileUtils.basename(this._output) + '.' + FileUtils.extension(this._output); - this._logger.info( - `${input} (${formatBytes(lastReadBytes)})` + ` → ${output} (${formatBytes(lastWriteBytes)})`, - ); - } + // 插入 WASM 水印 + let outputUint8Array = rawU8; + try { + outputUint8Array = await encodeGLB(rawU8, {}); + } catch (e: any) { + this._logger.warn('EncodeGLB skipped: ' + (e?.message || e)); + } - this.setLogger("Done") + // Uint8Array转file + const mimeType = this._outputFormat === Format.GLB ? "model/gltf-binary" : "model/gltf+json"; + const blob = new Blob([outputUint8Array], { type: mimeType }); + const outputFile = new File([blob], this._output, { type: mimeType }); - return outputFile; - } + const { lastReadBytes } = this._io; + const lastWriteBytes = outputUint8Array.byteLength; + + if (!this._input) { + const output = FileUtils.basename(this._output) + '.' + FileUtils.extension(this._output); + this._logger.info(`${output} (${formatBytes(lastWriteBytes)})`); + } else { + const input = FileUtils.basename(this._inputName) + '.' + FileUtils.extension(this._inputName); + const output = FileUtils.basename(this._output) + '.' + FileUtils.extension(this._output); + this._logger.info( + `${input} (${formatBytes(lastReadBytes)})` + ` → ${output} (${formatBytes(lastWriteBytes)})`, + ); + } + + this.setLogger("Done") + + return outputFile; + } } function updateMetadata(_document: Document): void { - const root = _document.getRoot(); - const xmpExtension = root - .listExtensionsUsed() - .find((ext) => ext.extensionName === 'KHR_xmp_json_ld') as KHRXMP | null; + const root = _document.getRoot(); + const xmpExtension = root + .listExtensionsUsed() + .find((ext) => ext.extensionName === 'KHR_xmp_json_ld') as KHRXMP | null; - // 不要将KHR_xmp_json_ld添加到尚未使用它的资产中。 - if (!xmpExtension) return; + // 不要将KHR_xmp_json_ld添加到尚未使用它的资产中。 + if (!xmpExtension) return; - const rootPacket = root.getExtension('KHR_xmp_json_ld') || xmpExtension.createPacket(); + const rootPacket = root.getExtension('KHR_xmp_json_ld') || xmpExtension.createPacket(); - // xmp:MetadataDate should be the same as, or more recent than, xmp:ModifyDate. - // https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/xmp.md - const date = new Date().toISOString().substring(0, 10); - rootPacket - .setContext({ ...rootPacket.getContext(), xmp: XMPContext.xmp }) - .setProperty('xmp:ModifyDate', date) - .setProperty('xmp:MetadataDate', date); + // xmp:MetadataDate should be the same as, or more recent than, xmp:ModifyDate. + // https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/xmp.md + const date = new Date().toISOString().substring(0, 10); + rootPacket + .setContext({ ...rootPacket.getContext(), xmp: XMPContext.xmp }) + .setProperty('xmp:ModifyDate', date) + .setProperty('xmp:MetadataDate', date); } diff --git a/packages/editor/src/plugin/glTFHandler/util.ts b/packages/editor/src/plugin/glTFHandler/util.ts index c0a5ab0..41e44a7 100644 --- a/packages/editor/src/plugin/glTFHandler/util.ts +++ b/packages/editor/src/plugin/glTFHandler/util.ts @@ -1,27 +1,53 @@ +import { injectWasm } from "@/utils/wasm/inject"; + export const XMPContext: Record = { - dc: 'http://purl.org/dc/elements/1.1/', - model3d: 'https://schema.khronos.org/model3d/xsd/1.0/', - rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - xmp: 'http://ns.adobe.com/xap/1.0/', - xmpRights: 'http://ns.adobe.com/xap/1.0/rights/', + dc: 'http://purl.org/dc/elements/1.1/', + model3d: 'https://schema.khronos.org/model3d/xsd/1.0/', + rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + xmp: 'http://ns.adobe.com/xap/1.0/', + xmpRights: 'http://ns.adobe.com/xap/1.0/rights/', }; export function formatLong(x: number): string { - return x.toString(); + return x.toString(); } export function formatBytes(bytes: number, decimals = 2): string { - if (bytes === 0) return '0 Bytes'; + if (bytes === 0) return '0 Bytes'; - const k = 1000; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const k = 1000; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); + const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } export function dim(str: string): string { - return `\x1b[2m${str}\x1b[0m`; + return `\x1b[2m${str}\x1b[0m`; } + +/* wasm内优化处理 */ +let wasmReady = false; + +async function ensureWasmReady() { + if (wasmReady) return; + await injectWasm({ wasmUrl: "/wasm/Astral3DglTFHandler.wasm" }); + wasmReady = true; +} + +export async function encodeGLB(u8: Uint8Array, meta: Record = {}) { + await ensureWasmReady(); + + const out = window.glTFHandlerEncodeGLB(u8, JSON.stringify(meta || {})); + return new Uint8Array(out.buffer, out.byteOffset, out.byteLength); +} + +export async function encodePNG(png: Uint8Array) { + await ensureWasmReady(); + + const out = window.glTFHandlerEncodePNG(png); + return new Uint8Array(out.buffer, out.byteOffset, out.byteLength); +} +/* wasm内优化处理 End */ diff --git a/packages/editor/src/utils/wasm/inject.ts b/packages/editor/src/utils/wasm/inject.ts new file mode 100644 index 0000000..29d447d --- /dev/null +++ b/packages/editor/src/utils/wasm/inject.ts @@ -0,0 +1,39 @@ +import "@/utils/wasm/wasm_exec.js"; + +// 20251112: 注入tinyGo编译的wasm +export function injectWasm(opts: {wasmUrl: string}):Promise { + return new Promise((resolve, reject) => { + if(!opts.wasmUrl){ + reject("wasmUrl requires valid URL"); + return; + } + + // @ts-ignore + const go = new Go(); + + const done = (obj) => { + const wasm = obj.instance; + go.run(wasm); + + resolve(wasm); + } + + if ('instantiateStreaming' in WebAssembly) { + WebAssembly.instantiateStreaming(fetch(opts.wasmUrl), go.importObject).then(function (obj) { + done(obj); + }).catch(function (err) { + reject(err); + }) + } else { + fetch(opts.wasmUrl).then(resp => + resp.arrayBuffer() + ).then(bytes => + WebAssembly.instantiate(bytes, go.importObject).then(function (obj) { + done(obj); + }).catch(function (err) { + reject(err); + }) + ) + } + }) +} \ No newline at end of file diff --git a/packages/editor/src/utils/wasm/wasm_exec.js b/packages/editor/src/utils/wasm/wasm_exec.js new file mode 100644 index 0000000..cd3927d --- /dev/null +++ b/packages/editor/src/utils/wasm/wasm_exec.js @@ -0,0 +1,403 @@ +(() => { + const global = window; + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + let reinterpretBuf = new DataView(new ArrayBuffer(8)); + let logLine = []; + const wasmExit = {}; // thrown to exit via proc_exit (not an error) + + global.Go = class { + constructor() { + this._callbackTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const mem = () => { + // The buffer may change when requesting more memory. + return new DataView(this._inst.exports.memory.buffer); + } + + const unboxValue = (v_ref) => { + reinterpretBuf.setBigInt64(0, v_ref, true); + const f = reinterpretBuf.getFloat64(0, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = v_ref & 0xffffffffn; + return this._values[id]; + } + + + const loadValue = (addr) => { + let v_ref = mem().getBigUint64(addr, true); + return unboxValue(v_ref); + } + + const boxValue = (v) => { + const nanHead = 0x7FF80000n; + + if (typeof v === "number") { + if (isNaN(v)) { + return nanHead << 32n; + } + if (v === 0) { + return (nanHead << 32n) | 1n; + } + reinterpretBuf.setFloat64(0, v, true); + return reinterpretBuf.getBigInt64(0, true); + } + + switch (v) { + case undefined: + return 0n; + case null: + return (nanHead << 32n) | 2n; + case true: + return (nanHead << 32n) | 3n; + case false: + return (nanHead << 32n) | 4n; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = BigInt(this._values.length); + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 1n; + switch (typeof v) { + case "string": + typeFlag = 2n; + break; + case "symbol": + typeFlag = 3n; + break; + case "function": + typeFlag = 4n; + break; + } + return id | ((nanHead | typeFlag) << 32n); + } + + const storeValue = (addr, v) => { + let v_ref = boxValue(v); + mem().setBigUint64(addr, v_ref, true); + } + + const loadSlice = (array, len, cap) => { + return new Uint8Array(this._inst.exports.memory.buffer, array, len); + } + + const loadSliceOfValues = (array, len, cap) => { + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (ptr, len) => { + return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + wasi_snapshot_preview1: { + fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { + let nwritten = 0; + if (fd == 1) { + for (let iovs_i=0; iovs_i 0, // dummy + fd_fdstat_get: () => 0, // dummy + fd_seek: () => 0, // dummy + proc_exit: (code) => { + this.exited = true; + this.exitCode = code; + this._resolveExitPromise(); + throw wasmExit; + }, + random_get: (bufPtr, bufLen) => { + crypto.getRandomValues(loadSlice(bufPtr, bufLen)); + return 0; + }, + }, + gojs: { + // func ticks() float64 + "runtime.ticks": () => { + return timeOrigin + performance.now(); + }, + + // func sleepTicks(timeout float64) + "runtime.sleepTicks": (timeout) => { + // Do not sleep, only reactivate scheduler after the given timeout. + setTimeout(() => { + if (this.exited) return; + try { + this._inst.exports.go_scheduler(); + } catch (e) { + if (e !== wasmExit) throw e; + } + }, timeout); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (v_ref) => { + // Note: TinyGo does not support finalizers so this is only called + // for one specific case, by js.go:jsString. and can/might leak memory. + const id = v_ref & 0xffffffffn; + if (this._goRefCounts?.[id] !== undefined) { + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); + } + } else { + console.error("syscall/js.finalizeRef: unknown id", id); + } + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (value_ptr, value_len) => { + value_ptr >>>= 0; + const s = loadString(value_ptr, value_len); + return boxValue(s); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (v_ref, p_ptr, p_len) => { + let prop = loadString(p_ptr, p_len); + let v = unboxValue(v_ref); + let result = Reflect.get(v, prop); + return boxValue(result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => { + const v = unboxValue(v_ref); + const p = loadString(p_ptr, p_len); + const x = unboxValue(x_ref); + Reflect.set(v, p, x); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => { + const v = unboxValue(v_ref); + const p = loadString(p_ptr, p_len); + Reflect.deleteProperty(v, p); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (v_ref, i) => { + return boxValue(Reflect.get(unboxValue(v_ref), i)); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (v_ref, i, x_ref) => { + Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => { + const v = unboxValue(v_ref); + const name = loadString(m_ptr, m_len); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + const m = Reflect.get(v, name); + storeValue(ret_addr, Reflect.apply(m, v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { + try { + const v = unboxValue(v_ref); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + storeValue(ret_addr, Reflect.apply(v, undefined, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { + const v = unboxValue(v_ref); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + storeValue(ret_addr, Reflect.construct(v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr+ 8, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (v_ref) => { + return unboxValue(v_ref).length; + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (ret_addr, v_ref) => { + const s = String(unboxValue(v_ref)); + const str = encoder.encode(s); + storeValue(ret_addr, str); + mem().setInt32(ret_addr + 8, str.length, true); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => { + const str = unboxValue(v_ref); + loadSlice(slice_ptr, slice_len, slice_cap).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (v_ref, t_ref) => { + return unboxValue(v_ref) instanceof unboxValue(t_ref); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = loadSlice(dest_addr, dest_len); + const src = unboxValue(src_ref); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + mem().setUint32(num_bytes_copied_addr, toCopy.length, true); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + + // copyBytesToJS(dst ref, src []byte) (int, bool) + // Originally copied from upstream Go project, then modified: + // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 + "syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = unboxValue(dst_ref); + const src = loadSlice(src_addr, src_len); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + mem().setUint32(num_bytes_copied_addr, toCopy.length, true); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + } + }; + + // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'. + // 开启 env 映射 + this.importObject.env = this.importObject.gojs; + } + + async run(instance) { + this._inst = instance; + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + global, + this, + ]; + this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map(); // mapping from JS values to reference ids + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + this.exitCode = 0; + + if (this._inst.exports._start) { + let exitPromise = new Promise((resolve, reject) => { + this._resolveExitPromise = resolve; + }); + + // Run program, but catch the wasmExit exception that's thrown + // to return back here. + try { + this._inst.exports._start(); + } catch (e) { + if (e !== wasmExit) throw e; + } + + await exitPromise; + return this.exitCode; + } else { + this._inst.exports._initialize(); + } + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + try { + this._inst.exports.resume(); + } catch (e) { + if (e !== wasmExit) throw e; + } + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } +})(); diff --git a/packages/editor/types/window.d.ts b/packages/editor/types/window.d.ts index caa01c6..273aa39 100644 --- a/packages/editor/types/window.d.ts +++ b/packages/editor/types/window.d.ts @@ -1,13 +1,3 @@ -declare interface IAstralEditorWasm { - exports:{ - computedStyle:()=>void - } -} -declare interface IAstralEngineWasm { - exports: { - } -} - declare interface Window { $t:(s: string)=>string; $cpt:(s: string)=>ComputedRef; @@ -21,9 +11,9 @@ declare interface Window { CesiumApp:any; VRButton: any; log: import('loglevel').RootLogger; - // 在wasm中注册 - AstralEditorWasm: IAstralEditorWasm; - AstralEngineWasm: IAstralEngineWasm; + // wasm + glTFHandlerEncodeGLB: (u: Uint8Array, jsonStr: string) => Uint8Array + glTFHandlerEncodePNG: (png: Uint8Array) => Uint8Array } declare interface Number{