From 138999f0164934fc02c2c16ffeec0fc5a03ad319 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Tue, 9 Jul 2024 15:27:05 +0200 Subject: [PATCH 01/14] Started working on the code-editor --- apps/wigsill-examples/package.json | 2 + apps/wigsill-examples/src/CodeEditor.tsx | 11 + apps/wigsill-examples/src/Home.tsx | 2 + .../src/examples/BasicTriangleExample.tsx | 192 ++++++++++++++++++ .../examples/CameraThresholdingExample.tsx | 4 +- apps/wigsill-examples/src/examples/index.ts | 7 +- pnpm-lock.yaml | 36 ++++ 7 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 apps/wigsill-examples/src/CodeEditor.tsx create mode 100644 apps/wigsill-examples/src/examples/BasicTriangleExample.tsx diff --git a/apps/wigsill-examples/package.json b/apps/wigsill-examples/package.json index 5e4fd2646..1a1610aaa 100644 --- a/apps/wigsill-examples/package.json +++ b/apps/wigsill-examples/package.json @@ -10,10 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "@monaco-editor/react": "^4.6.0", "classnames": "^2.5.1", "dat.gui": "^0.7.9", "jotai": "^2.8.4", "jotai-location": "^0.5.5", + "monaco-editor": "^0.50.0", "react": "^18.3.1", "react-dom": "^18.3.1", "typed-binary": "^4.0.0", diff --git a/apps/wigsill-examples/src/CodeEditor.tsx b/apps/wigsill-examples/src/CodeEditor.tsx new file mode 100644 index 000000000..4b465cc83 --- /dev/null +++ b/apps/wigsill-examples/src/CodeEditor.tsx @@ -0,0 +1,11 @@ +import Editor from '@monaco-editor/react'; + +export function CodeEditor() { + return ( + + ); +} diff --git a/apps/wigsill-examples/src/Home.tsx b/apps/wigsill-examples/src/Home.tsx index 93ae0c11b..328f0ee32 100644 --- a/apps/wigsill-examples/src/Home.tsx +++ b/apps/wigsill-examples/src/Home.tsx @@ -1,10 +1,12 @@ import { CodeResolver } from './CodeResolver'; +import { CodeEditor } from './CodeEditor'; export function Home() { return (

Edit `sampleShader.ts` and see the change in resolved code.

+
); } diff --git a/apps/wigsill-examples/src/examples/BasicTriangleExample.tsx b/apps/wigsill-examples/src/examples/BasicTriangleExample.tsx new file mode 100644 index 000000000..104cc173e --- /dev/null +++ b/apps/wigsill-examples/src/examples/BasicTriangleExample.tsx @@ -0,0 +1,192 @@ +import * as dat from 'dat.gui'; +import { WGSLRuntime } from 'wigsill'; +import { ProgramBuilder, makeArena, u32, wgsl } from 'wigsill'; + +import { useExampleWithCanvas } from '../common/useExampleWithCanvas'; +import { useEffect } from 'react'; + +async function init(gui: dat.GUI, canvas: HTMLCanvasElement) { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter!.requestDevice(); + const runtime = new WGSLRuntime(device); + + const xSpanData = wgsl.memory(u32).alias('x-span'); + const ySpanData = wgsl.memory(u32).alias('y-span'); + + const mainArena = makeArena({ + bufferBindingType: 'uniform', + memoryEntries: [xSpanData, ySpanData], + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, + }); + + const context = canvas.getContext('webgpu') as GPUCanvasContext; + + const devicePixelRatio = window.devicePixelRatio; + canvas.width = canvas.clientWidth * devicePixelRatio; + canvas.height = canvas.clientHeight * devicePixelRatio; + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + + context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', + }); + + const mainCode = wgsl` +struct VertexOutput { + @builtin(position) pos: vec4f, + @location(0) uv: vec2f, +} + +@vertex +fn main_vert( + @builtin(vertex_index) VertexIndex: u32 +) -> VertexOutput { + var pos = array( + vec2(0.5, 0.5), // top-right + vec2(-0.5, 0.5), // top-left + vec2(0.5, -0.5), // bottom-right + vec2(-0.5, -0.5) // bottom-left + ); + + var uv = array( + vec2(1., 1.), // top-right + vec2(0., 1.), // top-left + vec2(1., 0.), // bottom-right + vec2(0., 0.) // bottom-left + ); + + var output: VertexOutput; + output.pos = vec4f(pos[VertexIndex], 0.0, 1.0); + output.uv = uv[VertexIndex]; + return output; +} + +@fragment +fn main_frag( + @builtin(position) Position: vec4f, + @location(0) uv: vec2f, +) -> @location(0) vec4f { + let red = floor(uv.x * f32(${xSpanData})) / f32(${xSpanData}); + let green = floor(uv.y * f32(${ySpanData})) / f32(${ySpanData}); + return vec4(red, green, 0.5, 1.0); +} + `; + + const program = new ProgramBuilder(runtime, mainCode).build({ + bindingGroup: 0, + shaderStage: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + arenas: [mainArena], + }); + + const shaderModule = device.createShaderModule({ + code: program.code, + }); + + const pipeline = device.createRenderPipeline({ + layout: device.createPipelineLayout({ + bindGroupLayouts: [program.bindGroupLayout], + }), + vertex: { + module: shaderModule, + entryPoint: 'main_vert', + }, + fragment: { + module: shaderModule, + entryPoint: 'main_frag', + targets: [ + { + format: presentationFormat, + }, + ], + }, + primitive: { + topology: 'triangle-strip', + }, + }); + + /// UI + + const state = { + xSpan: 16, + ySpan: 16, + }; + + xSpanData.write(runtime, state.xSpan); + ySpanData.write(runtime, state.ySpan); + + gui.add(state, 'xSpan', 1, 16).onChange(() => { + xSpanData.write(runtime, state.xSpan); + }); + gui.add(state, 'ySpan', 1, 16).onChange(() => { + ySpanData.write(runtime, state.ySpan); + }); + + let running = true; + + function frame() { + const commandEncoder = device.createCommandEncoder(); + const textureView = context.getCurrentTexture().createView(); + + const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: textureView, + clearValue: [0, 0, 0, 1], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }; + + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(pipeline); + passEncoder.setBindGroup(0, program.bindGroup); + passEncoder.draw(4); + passEncoder.end(); + + device.queue.submit([commandEncoder.finish()]); + + if (running) { + requestAnimationFrame(frame); + } + } + + requestAnimationFrame(frame); + + return { + dispose() { + running = false; + }, + }; +} + +export function BasicTriangleExample() { + const canvasRef = useExampleWithCanvas(init); + + useEffect(() => { + import('wigsill').then((wigsill) => { + const require = (moduleKey: string) => { + if (moduleKey === 'wigsill') { + return wigsill; + } + throw new Error(`Module ${moduleKey} not found.`); + }; + + const mod = Function(` +return async (require) => { + const { wgsl } = require('wigsill'); + return 'bruh'; +}; +`); + + const result: Promise = mod()(require); + + result.then((value) => { + console.log(value); + }); + }, []); + }); + + return ; +} diff --git a/apps/wigsill-examples/src/examples/CameraThresholdingExample.tsx b/apps/wigsill-examples/src/examples/CameraThresholdingExample.tsx index 0473b8192..354d0e955 100644 --- a/apps/wigsill-examples/src/examples/CameraThresholdingExample.tsx +++ b/apps/wigsill-examples/src/examples/CameraThresholdingExample.tsx @@ -1,7 +1,8 @@ import * as dat from 'dat.gui'; -import { useExampleWithCanvas } from '../common/useExampleWithCanvas'; import { createRef, RefObject } from 'react'; +import { useExampleWithCanvas } from '../common/useExampleWithCanvas'; + function init(videoRef: RefObject) { return async function (gui: dat.GUI, canvas: HTMLCanvasElement) { const adapter = await navigator.gpu.requestAdapter(); @@ -200,6 +201,7 @@ fn frag_main(@location(0) fragUV : vec2f) -> @location(0) vec4f { export function CameraThresholdingExample() { const videoRef: RefObject = createRef(); const canvasRef = useExampleWithCanvas(init(videoRef)); + // const canvasRef = useExampleWithCanvas(useCallback(() => init(videoRef), [])); const [width, height] = [500, 375]; return ( diff --git a/apps/wigsill-examples/src/examples/index.ts b/apps/wigsill-examples/src/examples/index.ts index 798c91c38..0901b6ef2 100644 --- a/apps/wigsill-examples/src/examples/index.ts +++ b/apps/wigsill-examples/src/examples/index.ts @@ -1,7 +1,12 @@ -import { CameraThresholdingExample } from './CameraThresholdingExample'; +import { BasicTriangleExample } from './BasicTriangleExample'; import { GradientTilesExample } from './GradientTilesExample'; +import { CameraThresholdingExample } from './CameraThresholdingExample'; export const examples = { + 'basic-triangle': { + label: 'Basic Triangle', + component: BasicTriangleExample, + }, 'gradient-tiles': { label: 'Gradient tiles', component: GradientTilesExample, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 623eac9f8..f292f735b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: apps/wigsill-examples: dependencies: + '@monaco-editor/react': + specifier: ^4.6.0 + version: 4.6.0(monaco-editor@0.50.0)(react-dom@18.3.1)(react@18.3.1) classnames: specifier: ^2.5.1 version: 2.5.1 @@ -71,6 +74,9 @@ importers: jotai-location: specifier: ^0.5.5 version: 0.5.5(jotai@2.8.4) + monaco-editor: + specifier: ^0.50.0 + version: 0.50.0 react: specifier: ^18.3.1 version: 18.3.1 @@ -909,6 +915,28 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@monaco-editor/loader@1.4.0(monaco-editor@0.50.0): + resolution: {integrity: sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==} + peerDependencies: + monaco-editor: '>= 0.21.0 < 1' + dependencies: + monaco-editor: 0.50.0 + state-local: 1.0.7 + dev: false + + /@monaco-editor/react@4.6.0(monaco-editor@0.50.0)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@monaco-editor/loader': 1.4.0(monaco-editor@0.50.0) + monaco-editor: 0.50.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3521,6 +3549,10 @@ packages: ufo: 1.4.0 dev: true + /monaco-editor@0.50.0: + resolution: {integrity: sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==} + dev: false + /mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -4165,6 +4197,10 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true + /state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + dev: false + /std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: true From 0c275da35343b3a8ff70d6c6664ef33f98ecb4bd Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Wed, 10 Jul 2024 10:55:23 +0200 Subject: [PATCH 02/14] Added more code parsing. --- apps/wigsill-examples/package.json | 7 +- apps/wigsill-examples/src/App.tsx | 10 +- apps/wigsill-examples/src/CodeEditor.tsx | 95 +++++++- .../src/ExampleView/ExampleView.tsx | 22 ++ .../src/ExampleView/examples.ts | 34 +++ .../src/ExampleView/parseExampleCode.test.ts | 203 ++++++++++++++++++ .../src/ExampleView/parseExampleCode.ts | 198 +++++++++++++++++ .../wigsill-examples/src/ExampleView/types.ts | 11 + apps/wigsill-examples/src/Home.tsx | 2 - .../src/common/exampleState.ts | 1 + .../src/examples/BasicTriangleExample.tsx | 24 --- .../src/examples/basic-triangle.js | 18 ++ apps/wigsill-examples/src/examples/index.ts | 18 -- apps/wigsill-examples/src/main.tsx | 12 +- pnpm-lock.yaml | 28 ++- 15 files changed, 620 insertions(+), 63 deletions(-) create mode 100644 apps/wigsill-examples/src/ExampleView/ExampleView.tsx create mode 100644 apps/wigsill-examples/src/ExampleView/examples.ts create mode 100644 apps/wigsill-examples/src/ExampleView/parseExampleCode.test.ts create mode 100644 apps/wigsill-examples/src/ExampleView/parseExampleCode.ts create mode 100644 apps/wigsill-examples/src/ExampleView/types.ts create mode 100644 apps/wigsill-examples/src/examples/basic-triangle.js delete mode 100644 apps/wigsill-examples/src/examples/index.ts diff --git a/apps/wigsill-examples/package.json b/apps/wigsill-examples/package.json index 1a1610aaa..837ce1139 100644 --- a/apps/wigsill-examples/package.json +++ b/apps/wigsill-examples/package.json @@ -18,8 +18,10 @@ "monaco-editor": "^0.50.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "remeda": "^2.3.0", "typed-binary": "^4.0.0", - "wigsill": "workspace:*" + "wigsill": "workspace:*", + "zod": "^3.23.8" }, "devDependencies": { "@types/dat.gui": "^0.7.13", @@ -36,6 +38,7 @@ "postcss": "^8.4.39", "tailwindcss": "^3.4.4", "typescript": "^5.2.2", - "vite": "^5.3.1" + "vite": "^5.3.1", + "vitest": "0.33.0" } } diff --git a/apps/wigsill-examples/src/App.tsx b/apps/wigsill-examples/src/App.tsx index 32a0eacae..4c86674e2 100644 --- a/apps/wigsill-examples/src/App.tsx +++ b/apps/wigsill-examples/src/App.tsx @@ -4,8 +4,9 @@ import { useAtom } from 'jotai/react'; import { currentExampleAtom } from './router'; import { ExampleLink } from './common/ExampleLink'; -import { examples } from './examples'; +import { examples } from './ExampleView/examples'; import { ExampleNotFound } from './ExampleNotFound'; +import { ExampleView } from './ExampleView/ExampleView'; import { Home } from './Home'; function App() { @@ -17,10 +18,7 @@ function App() { } if (currentExample in examples) { - const Example = - examples[currentExample as keyof typeof examples].component; - - return ; + return ; } return ; @@ -42,7 +40,7 @@ function App() {
{Object.entries(examples).map(([key, example]) => ( - {example.label} + {example.metadata.title} ))} diff --git a/apps/wigsill-examples/src/CodeEditor.tsx b/apps/wigsill-examples/src/CodeEditor.tsx index 4b465cc83..d2200882a 100644 --- a/apps/wigsill-examples/src/CodeEditor.tsx +++ b/apps/wigsill-examples/src/CodeEditor.tsx @@ -1,11 +1,100 @@ +import { GUI } from 'dat.gui'; import Editor from '@monaco-editor/react'; +import { useCallback, useEffect, useRef } from 'react'; + +import useEvent from './common/useEvent'; +import { ExampleState } from './common/exampleState'; + +type Props = { + code: string; + onCodeChange: (value: string) => unknown; +}; + +function useLayout() { + const defineLayout = useCallback(() => { + console.log(`Layout defined`); + }, []); + + return [null, defineLayout] as const; +} + +async function executeExample( + exampleCode: string, + defineLayout: () => void, +): Promise { + const wigsill = await import('wigsill'); + + const require = (moduleKey: string) => { + if (moduleKey === 'wigsill') { + return wigsill; + } + throw new Error(`Module ${moduleKey} not found.`); + }; + + const mod = Function(` +return async (require) => { +${exampleCode} +}; +`); + + const result: Promise = mod()(require); + + console.log(await result); + + return { + dispose: () => {}, + }; +} + +function useExample Promise>( + exampleCode: string, +) { + const exampleRef = useRef(null); + const [_layout, defineLayout] = useLayout(); + + useEffect(() => { + let cancelled = false; + + console.log('MAKE'); + const gui = new GUI({ closeOnTop: true }); + gui.hide(); + + executeExample(exampleCode, defineLayout).then((example) => { + if (cancelled) { + // Another instance was started in the meantime. + example.dispose(); + return; + } + + // Success + exampleRef.current = example; + gui.show(); + }); + + return () => { + console.log('BREAK'); + exampleRef.current?.dispose(); + cancelled = true; + gui.destroy(); + }; + }, [exampleCode, defineLayout]); +} + +export function CodeEditor(props: Props) { + const { code, onCodeChange } = props; + + const handleChange = useEvent((value: string | undefined) => { + onCodeChange(value ?? ''); + }); + + useExample(code); -export function CodeEditor() { return ( ); } diff --git a/apps/wigsill-examples/src/ExampleView/ExampleView.tsx b/apps/wigsill-examples/src/ExampleView/ExampleView.tsx new file mode 100644 index 000000000..c5de47bf1 --- /dev/null +++ b/apps/wigsill-examples/src/ExampleView/ExampleView.tsx @@ -0,0 +1,22 @@ +import { CodeEditor } from '../CodeEditor'; +import useEvent from '../common/useEvent'; +import { Example } from '../examples'; + +type Props = { + example: Example; +}; + +export function ExampleView({ example }: Props) { + const { code: initialCode, metadata } = example; + + const handleCodeChange = useEvent(() => { + // TODO + }); + + return ( + <> +

Hello {metadata.title}

+ + + ); +} diff --git a/apps/wigsill-examples/src/ExampleView/examples.ts b/apps/wigsill-examples/src/ExampleView/examples.ts new file mode 100644 index 000000000..45aa7c8c2 --- /dev/null +++ b/apps/wigsill-examples/src/ExampleView/examples.ts @@ -0,0 +1,34 @@ +const rawExamples: Record = import.meta.glob( + '../examples/**/*.js', + { + query: 'raw', + eager: true, + import: 'default', + }, +); + +import { mapValues } from 'remeda'; +import { ExampleMetadata } from './types'; + +export const examples = mapValues(rawExamples, (code) => { + // extracting metadata from the first comment + let metadata: ExampleMetadata = { + title: '', + }; + + try { + const snippet = code.substring(code.indexOf('/*') + 2, code.indexOf('*/')); + metadata = ExampleMetadata.parse(JSON.parse(snippet)); + } catch (err) { + console.error( + `Malformed example, expected metadata json at the beginning. Reason: ${err}`, + ); + } + + // Turning `import Default, { one, two } from ''module` statements into `const { default: Default, one, two } = await _import('')` + + return { + metadata, + code: code.substring(code.indexOf('*/') + 2), + }; +}); diff --git a/apps/wigsill-examples/src/ExampleView/parseExampleCode.test.ts b/apps/wigsill-examples/src/ExampleView/parseExampleCode.test.ts new file mode 100644 index 000000000..04b58c493 --- /dev/null +++ b/apps/wigsill-examples/src/ExampleView/parseExampleCode.test.ts @@ -0,0 +1,203 @@ +import { describe, expect, it } from 'vitest'; +import { + parseImportStatement as parse, + tokenizeImportStatement as tokenize, +} from './parseExampleCode'; + +describe('tokenizeImportStatement', () => { + it('tokenizes an inline import', () => { + const code = `import "module"`; + const expected = ['import', { literal: 'module' }]; + expect(tokenize(code)).toEqual(expected); + }); + + it('tokenizes a list of named imports', () => { + const code = `import { one, two } from "module"`; + const expected = [ + 'import', + '{', + { id: 'one' }, + ',', + { id: 'two' }, + '}', + 'from', + { literal: 'module' }, + ]; + expect(tokenize(code)).toEqual(expected); + }); + + it('tokenizes a list of named imports (non-standard whitespace)', () => { + const code = `import{ one,two } from "module";`; + const expected = [ + 'import', + '{', + { id: 'one' }, + ',', + { id: 'two' }, + '}', + 'from', + { literal: 'module' }, + ';', + ]; + expect(tokenize(code)).toEqual(expected); + }); + + it('tokenizes default and a list of named imports', () => { + const code = `import Module, { one, two } from "module"`; + const expected = [ + 'import', + { id: 'Module' }, + ',', + '{', + { id: 'one' }, + ',', + { id: 'two' }, + '}', + 'from', + { literal: 'module' }, + ]; + expect(tokenize(code)).toEqual(expected); + }); + + it('tokenizes default import', () => { + const code = `import Module from "module"`; + const expected = [ + 'import', + { id: 'Module' }, + 'from', + { literal: 'module' }, + ]; + expect(tokenize(code)).toEqual(expected); + }); + + it('tokenizes alias of all', () => { + const code = `import * as All from "module"`; + const expected = [ + 'import', + '*', + 'as', + { id: 'All' }, + 'from', + { literal: 'module' }, + ]; + expect(tokenize(code)).toEqual(expected); + }); + + it('tokenizes alias of named imports', () => { + const code = `import { + one as One, + two as Two, + three + } from 'module'`; + + const expected = [ + 'import', + '{', + { id: 'one' }, + 'as', + { id: 'One' }, + ',', + { id: 'two' }, + 'as', + { id: 'Two' }, + ',', + { id: 'three' }, + '}', + 'from', + { literal: 'module' }, + ]; + expect(tokenize(code)).toEqual(expected); + }); +}); + +describe('parseImportStatement', () => { + it('parses named imports', () => { + const expected = { + allAlias: null, + defaultAlias: null, + namedImports: { one: 'one', two: 'two' }, + moduleName: 'module_name', + }; + + expect(parse(tokenize(`import { one, two } from "module_name"`))).toEqual( + expected, + ); + expect(parse(tokenize(`import{one,two} from 'module_name'`))).toEqual( + expected, + ); + expect(parse(tokenize(`import {one, two } from "module_name";`))).toEqual( + expected, + ); + expect(parse(tokenize(`import { one, two } from 'module_name';`))).toEqual( + expected, + ); + }); + + it('parses default import', () => { + const expected = { + allAlias: null, + defaultAlias: 'Module', + namedImports: {}, + moduleName: 'module_name', + }; + + expect(parse(tokenize(`import Module from "module_name"`))).toEqual( + expected, + ); + expect(parse(tokenize(`import Module from 'module_name'`))).toEqual( + expected, + ); + expect(parse(tokenize(`import Module from "module_name";`))).toEqual( + expected, + ); + expect(parse(tokenize(`import Module from 'module_name';`))).toEqual( + expected, + ); + }); + + it('parses default and named imports', () => { + const expected = { + allAlias: null, + defaultAlias: 'Module', + namedImports: { one: 'one', two: 'Two' }, + moduleName: 'module_name', + }; + + expect( + parse(tokenize(`import Module, { one, two as Two } from "module_name"`)), + ).toEqual(expected); + expect( + parse(tokenize(`import Module, { one, two as Two } from 'module_name'`)), + ).toEqual(expected); + expect( + parse( + tokenize(`import Module, { one, two as Two } from "module_name";`), + ), + ).toEqual(expected); + expect( + parse(tokenize(`import Module, { one, two as Two } from 'module_name';`)), + ).toEqual(expected); + }); + + it('parses all alias', () => { + const expected = { + allAlias: 'All', + defaultAlias: null, + namedImports: {}, + moduleName: 'module_name', + }; + + expect(parse(tokenize(`import * as All from "module_name"`))).toEqual( + expected, + ); + expect(parse(tokenize(`import * as All from 'module_name'`))).toEqual( + expected, + ); + expect(parse(tokenize(`import * as All from "module_name";`))).toEqual( + expected, + ); + expect(parse(tokenize(`import * as All from 'module_name';`))).toEqual( + expected, + ); + }); +}); diff --git a/apps/wigsill-examples/src/ExampleView/parseExampleCode.ts b/apps/wigsill-examples/src/ExampleView/parseExampleCode.ts new file mode 100644 index 000000000..bce390131 --- /dev/null +++ b/apps/wigsill-examples/src/ExampleView/parseExampleCode.ts @@ -0,0 +1,198 @@ +import { mapToObj, pipe } from 'remeda'; +import { ExampleMetadata } from './types'; + +const SyntaxTokens = ['import', 'from', '{', '}', ',', ';', '*', 'as'] as const; + +type TokenPosition = { start: number; end: number }; +type IdentifierToken = TokenPosition & { id: string }; +type LiteralToken = TokenPosition & { literal: string }; + +type ImportStatementToken = + | (TokenPosition & { value: (typeof SyntaxTokens)[number] }) + | IdentifierToken + | LiteralToken; + +function isIdentifier(token: unknown): token is IdentifierToken { + return !!token && typeof token === 'object' && 'id' in token; +} + +function isLiteral(token: unknown): token is LiteralToken { + return !!token && typeof token === 'object' && 'literal' in token; +} + +export const tokenizeImportStatement = ( + str: string, +): ImportStatementToken[] => { + const tokens: ImportStatementToken[] = []; + let left = str.trimStart(); + + let lastLength = left.length; + + while (left.length > 0) { + // syntax elements + let matchedWithSyntax = false; + for (const syntax of SyntaxTokens) { + if (left.startsWith(syntax)) { + const start = 0; + const end = 0; + + tokens.push({ start, end, value: syntax }); + left = left.substring(syntax.length).trimStart(); + matchedWithSyntax = true; + break; + } + } + + if (matchedWithSyntax) { + continue; + } + + // string literals + if (left.startsWith(`"`)) { + const end = left.indexOf(`"`, 1); + const literal = left.substring(1, end); + tokens.push({ literal }); + left = left.substring(end + 1).trimStart(); + } else if (left.startsWith(`'`)) { + const end = left.indexOf(`'`, 1); + const literal = left.substring(1, end); + tokens.push({ literal }); + left = left.substring(end + 1).trimStart(); + } else { + const identifier = /\w+/.exec(left)?.[0] ?? ''; + + if (identifier.length === 0) { + break; + } + + tokens.push({ id: identifier }); + left = left.substring(identifier.length).trimStart(); + } + + if (left.length === lastLength) { + // No progress, break it off + break; + } + lastLength = left.length; + } + + return tokens; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function splitBy( + separator: TSep, +): (array: (TElem | TSep)[]) => TElem[][] { + return (array: (TElem | TSep)[]) => { + const groups: TElem[][] = []; + + array.forEach((x) => { + if (groups.length === 0) { + if (x === separator) { + // Do nothing + } else { + groups.push([x as TElem]); + } + } else { + if (x === separator) { + groups.push([]); + } else { + groups[groups.length - 1].push(x as TElem); + } + } + }); + + return groups; + }; +} + +export const parseImportStatement = (tokens: ImportStatementToken[]) => { + const allAlias = + tokens[1] === '*' && isIdentifier(tokens[3]) ? tokens[3].id : null; + + const defaultAlias: string | null = isIdentifier(tokens[1]) + ? tokens[1].id + : null; + + const moduleNameIdx = tokens.findIndex(isLiteral); + const moduleName: string = pipe(tokens[moduleNameIdx], (token) => { + if (!token) { + throw new Error(`Missing module name in import statement.`); + } + return (token as LiteralToken).literal; + }); + + const namedImports: Record = pipe( + // all tokens between { ... } + tokens.includes('{') + ? tokens.slice(tokens.indexOf('{') + 1, tokens.indexOf('}')) + : [], + // collapsing `#0 as #1` into { #0: #1 }, and if no aliasing is done, { #0: #0 } + splitBy(',' as const), + mapToObj((list) => { + if (list.length === 0) { + throw new Error(`Invalid named import`); + } + + const named = list[0]; + if (!isIdentifier(named)) { + throw new Error(`Expected identifier as named import.`); + } + + // aliased + if (list.length === 3) { + const alias = list[2]; + if (!isIdentifier(alias)) { + throw new Error(`Expected identifier as alias to named import.`); + } + return [named.id, alias.id]; + } + + return [named.id, named.id]; + }), + ); + + // Removing all used tokens. + for (let i = 0; i < moduleNameIdx; ++i) { + tokens.shift(); + } + + if (tokens[0] === ';') { + tokens.shift(); + } + + return { + allAlias, + defaultAlias, + namedImports, + moduleName, + }; +}; + +function parseExampleCode(rawCode: string) { + // extracting metadata from the first comment + let metadata: ExampleMetadata = { + title: '', + }; + + try { + const snippet = rawCode.substring( + rawCode.indexOf('/*') + 2, + rawCode.indexOf('*/'), + ); + metadata = ExampleMetadata.parse(JSON.parse(snippet)); + } catch (err) { + console.error( + `Malformed example, expected metadata json at the beginning. Reason: ${err}`, + ); + } + + // Turning: + // `import Default, { one, two } from ''module` statements into + // `const { default: Default, one, two } = await _import('module')` + + return { + metadata, + code: code.substring(code.indexOf('*/') + 2), + }; +} diff --git a/apps/wigsill-examples/src/ExampleView/types.ts b/apps/wigsill-examples/src/ExampleView/types.ts new file mode 100644 index 000000000..1ba1854d5 --- /dev/null +++ b/apps/wigsill-examples/src/ExampleView/types.ts @@ -0,0 +1,11 @@ +import z from 'zod'; + +export type ExampleMetadata = z.infer; +export const ExampleMetadata = z.object({ + title: z.string(), +}); + +export type Example = { + code: string; + metadata: ExampleMetadata; +}; diff --git a/apps/wigsill-examples/src/Home.tsx b/apps/wigsill-examples/src/Home.tsx index 328f0ee32..93ae0c11b 100644 --- a/apps/wigsill-examples/src/Home.tsx +++ b/apps/wigsill-examples/src/Home.tsx @@ -1,12 +1,10 @@ import { CodeResolver } from './CodeResolver'; -import { CodeEditor } from './CodeEditor'; export function Home() { return (

Edit `sampleShader.ts` and see the change in resolved code.

-
); } diff --git a/apps/wigsill-examples/src/common/exampleState.ts b/apps/wigsill-examples/src/common/exampleState.ts index 3a2cb8280..cfc09081f 100644 --- a/apps/wigsill-examples/src/common/exampleState.ts +++ b/apps/wigsill-examples/src/common/exampleState.ts @@ -1,3 +1,4 @@ export type ExampleState = { + eachFrame?: () => void; dispose: () => void; }; diff --git a/apps/wigsill-examples/src/examples/BasicTriangleExample.tsx b/apps/wigsill-examples/src/examples/BasicTriangleExample.tsx index 104cc173e..74129b705 100644 --- a/apps/wigsill-examples/src/examples/BasicTriangleExample.tsx +++ b/apps/wigsill-examples/src/examples/BasicTriangleExample.tsx @@ -164,29 +164,5 @@ fn main_frag( export function BasicTriangleExample() { const canvasRef = useExampleWithCanvas(init); - useEffect(() => { - import('wigsill').then((wigsill) => { - const require = (moduleKey: string) => { - if (moduleKey === 'wigsill') { - return wigsill; - } - throw new Error(`Module ${moduleKey} not found.`); - }; - - const mod = Function(` -return async (require) => { - const { wgsl } = require('wigsill'); - return 'bruh'; -}; -`); - - const result: Promise = mod()(require); - - result.then((value) => { - console.log(value); - }); - }, []); - }); - return ; } diff --git a/apps/wigsill-examples/src/examples/basic-triangle.js b/apps/wigsill-examples/src/examples/basic-triangle.js new file mode 100644 index 000000000..a4404ab25 --- /dev/null +++ b/apps/wigsill-examples/src/examples/basic-triangle.js @@ -0,0 +1,18 @@ +/* +{ + "title": "Basic Triangle" +} +*/ + +import { wgsl } from 'wigsill'; +import { defineLayout } from '@wigsill/example-toolkit'; + +const some = wgsl.fn()`() -> f32 { + return 1. + 2.; +}`; + +return { + eachFrame: () => {}, + + dispose() {}, +}; diff --git a/apps/wigsill-examples/src/examples/index.ts b/apps/wigsill-examples/src/examples/index.ts deleted file mode 100644 index 0901b6ef2..000000000 --- a/apps/wigsill-examples/src/examples/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BasicTriangleExample } from './BasicTriangleExample'; -import { GradientTilesExample } from './GradientTilesExample'; -import { CameraThresholdingExample } from './CameraThresholdingExample'; - -export const examples = { - 'basic-triangle': { - label: 'Basic Triangle', - component: BasicTriangleExample, - }, - 'gradient-tiles': { - label: 'Gradient tiles', - component: GradientTilesExample, - }, - 'camera-thresholding': { - label: 'Camera thresholding', - component: CameraThresholdingExample, - }, -}; diff --git a/apps/wigsill-examples/src/main.tsx b/apps/wigsill-examples/src/main.tsx index 3d7150da8..f9bb4b34e 100644 --- a/apps/wigsill-examples/src/main.tsx +++ b/apps/wigsill-examples/src/main.tsx @@ -1,10 +1,10 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import './index.css' +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.tsx'; +import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - + , , -) +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f292f735b..9d88966ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,12 +83,18 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + remeda: + specifier: ^2.3.0 + version: 2.3.0 typed-binary: specifier: ^4.0.0 version: 4.0.0 wigsill: specifier: workspace:* version: link:../../packages/wigsill + zod: + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@types/dat.gui': specifier: ^0.7.13 @@ -135,6 +141,9 @@ importers: vite: specifier: ^5.3.1 version: 5.3.3(@types/node@20.11.19) + vitest: + specifier: 0.33.0 + version: 0.33.0(@vitest/ui@0.33.0)(jsdom@24.0.0) packages/wigsill: devDependencies: @@ -3954,6 +3963,12 @@ packages: set-function-name: 2.0.1 dev: true + /remeda@2.3.0: + resolution: {integrity: sha512-J9djcCyFL9D1ZGnfiNJ6MMhcUT8BmNCT+dj2SApdrTNz+CxqW1dScPVaDCopivHTShiYXh4sRSPYKMzlGrgyQw==} + dependencies: + type-fest: 4.21.0 + dev: false + /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: true @@ -4523,6 +4538,11 @@ packages: engines: {node: '>=10'} dev: true + /type-fest@4.21.0: + resolution: {integrity: sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==} + engines: {node: '>=16'} + dev: false + /typed-array-buffer@1.0.1: resolution: {integrity: sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ==} engines: {node: '>= 0.4'} @@ -4638,7 +4658,7 @@ packages: debug: 4.3.4 mlly: 1.5.0 pathe: 1.1.2 - picocolors: 1.0.0 + picocolors: 1.0.1 vite: 4.5.2(@types/node@20.11.19) transitivePeerDependencies: - '@types/node' @@ -4772,7 +4792,7 @@ packages: local-pkg: 0.4.3 magic-string: 0.30.7 pathe: 1.1.2 - picocolors: 1.0.0 + picocolors: 1.0.1 std-env: 3.7.0 strip-literal: 1.3.0 tinybench: 2.6.0 @@ -4934,3 +4954,7 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true + + /zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + dev: false From 41970a67ed87ad3d1227edc973f0f48916ed9f3d Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Wed, 10 Jul 2024 12:40:01 +0200 Subject: [PATCH 03/14] Parsing import statements into async function calls. --- apps/wigsill-examples/package.json | 4 + apps/wigsill-examples/src/CodeEditor.tsx | 33 +--- .../src/ExampleView/ExampleView.tsx | 18 +- .../src/ExampleView/examples.ts | 33 +--- .../src/ExampleView/parseExampleCode.ts | 179 +----------------- apps/wigsill-examples/src/exampleRunner.ts | 97 ++++++++++ .../src/examples/basic-triangle.js | 12 +- pnpm-lock.yaml | 84 ++++++++ 8 files changed, 218 insertions(+), 242 deletions(-) create mode 100644 apps/wigsill-examples/src/exampleRunner.ts diff --git a/apps/wigsill-examples/package.json b/apps/wigsill-examples/package.json index 837ce1139..8d1f79d1a 100644 --- a/apps/wigsill-examples/package.json +++ b/apps/wigsill-examples/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@babel/standalone": "^7.24.7", "@monaco-editor/react": "^4.6.0", "classnames": "^2.5.1", "dat.gui": "^0.7.9", @@ -24,6 +25,9 @@ "zod": "^3.23.8" }, "devDependencies": { + "@types/babel__standalone": "^7.1.7", + "@types/babel__template": "^7.4.4", + "@types/babel__traverse": "^7.20.6", "@types/dat.gui": "^0.7.13", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", diff --git a/apps/wigsill-examples/src/CodeEditor.tsx b/apps/wigsill-examples/src/CodeEditor.tsx index d2200882a..6cea100bb 100644 --- a/apps/wigsill-examples/src/CodeEditor.tsx +++ b/apps/wigsill-examples/src/CodeEditor.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef } from 'react'; import useEvent from './common/useEvent'; import { ExampleState } from './common/exampleState'; +import { executeExample } from './exampleRunner'; type Props = { code: string; @@ -18,37 +19,7 @@ function useLayout() { return [null, defineLayout] as const; } -async function executeExample( - exampleCode: string, - defineLayout: () => void, -): Promise { - const wigsill = await import('wigsill'); - - const require = (moduleKey: string) => { - if (moduleKey === 'wigsill') { - return wigsill; - } - throw new Error(`Module ${moduleKey} not found.`); - }; - - const mod = Function(` -return async (require) => { -${exampleCode} -}; -`); - - const result: Promise = mod()(require); - - console.log(await result); - - return { - dispose: () => {}, - }; -} - -function useExample Promise>( - exampleCode: string, -) { +function useExample(exampleCode: string) { const exampleRef = useRef(null); const [_layout, defineLayout] = useLayout(); diff --git a/apps/wigsill-examples/src/ExampleView/ExampleView.tsx b/apps/wigsill-examples/src/ExampleView/ExampleView.tsx index c5de47bf1..2bcd7ef6a 100644 --- a/apps/wigsill-examples/src/ExampleView/ExampleView.tsx +++ b/apps/wigsill-examples/src/ExampleView/ExampleView.tsx @@ -1,6 +1,8 @@ -import { CodeEditor } from '../CodeEditor'; import useEvent from '../common/useEvent'; -import { Example } from '../examples'; +import { CodeEditor } from '../CodeEditor'; +import type { Example } from './types'; +import { useMemo, useState } from 'react'; +import { debounce } from 'remeda'; type Props = { example: Example; @@ -8,15 +10,21 @@ type Props = { export function ExampleView({ example }: Props) { const { code: initialCode, metadata } = example; + const [code, setCode] = useState(initialCode); + + const setCodeDebouncer = useMemo( + () => debounce(setCode, { waitMs: 500 }), + [setCode], + ); - const handleCodeChange = useEvent(() => { - // TODO + const handleCodeChange = useEvent((newCode: string) => { + setCodeDebouncer.call(newCode); }); return ( <>

Hello {metadata.title}

- + ); } diff --git a/apps/wigsill-examples/src/ExampleView/examples.ts b/apps/wigsill-examples/src/ExampleView/examples.ts index 45aa7c8c2..6fd76acb2 100644 --- a/apps/wigsill-examples/src/ExampleView/examples.ts +++ b/apps/wigsill-examples/src/ExampleView/examples.ts @@ -7,28 +7,13 @@ const rawExamples: Record = import.meta.glob( }, ); -import { mapValues } from 'remeda'; -import { ExampleMetadata } from './types'; +import { mapKeys, mapValues, pipe } from 'remeda'; +import { parseExampleCode } from './parseExampleCode'; -export const examples = mapValues(rawExamples, (code) => { - // extracting metadata from the first comment - let metadata: ExampleMetadata = { - title: '', - }; - - try { - const snippet = code.substring(code.indexOf('/*') + 2, code.indexOf('*/')); - metadata = ExampleMetadata.parse(JSON.parse(snippet)); - } catch (err) { - console.error( - `Malformed example, expected metadata json at the beginning. Reason: ${err}`, - ); - } - - // Turning `import Default, { one, two } from ''module` statements into `const { default: Default, one, two } = await _import('')` - - return { - metadata, - code: code.substring(code.indexOf('*/') + 2), - }; -}); +export const examples = pipe( + rawExamples, + mapKeys((key) => { + return key.replace('^../examples/', ''); + }), + mapValues(parseExampleCode), +); diff --git a/apps/wigsill-examples/src/ExampleView/parseExampleCode.ts b/apps/wigsill-examples/src/ExampleView/parseExampleCode.ts index bce390131..865ff0449 100644 --- a/apps/wigsill-examples/src/ExampleView/parseExampleCode.ts +++ b/apps/wigsill-examples/src/ExampleView/parseExampleCode.ts @@ -1,175 +1,6 @@ -import { mapToObj, pipe } from 'remeda'; -import { ExampleMetadata } from './types'; +import { Example, ExampleMetadata } from './types'; -const SyntaxTokens = ['import', 'from', '{', '}', ',', ';', '*', 'as'] as const; - -type TokenPosition = { start: number; end: number }; -type IdentifierToken = TokenPosition & { id: string }; -type LiteralToken = TokenPosition & { literal: string }; - -type ImportStatementToken = - | (TokenPosition & { value: (typeof SyntaxTokens)[number] }) - | IdentifierToken - | LiteralToken; - -function isIdentifier(token: unknown): token is IdentifierToken { - return !!token && typeof token === 'object' && 'id' in token; -} - -function isLiteral(token: unknown): token is LiteralToken { - return !!token && typeof token === 'object' && 'literal' in token; -} - -export const tokenizeImportStatement = ( - str: string, -): ImportStatementToken[] => { - const tokens: ImportStatementToken[] = []; - let left = str.trimStart(); - - let lastLength = left.length; - - while (left.length > 0) { - // syntax elements - let matchedWithSyntax = false; - for (const syntax of SyntaxTokens) { - if (left.startsWith(syntax)) { - const start = 0; - const end = 0; - - tokens.push({ start, end, value: syntax }); - left = left.substring(syntax.length).trimStart(); - matchedWithSyntax = true; - break; - } - } - - if (matchedWithSyntax) { - continue; - } - - // string literals - if (left.startsWith(`"`)) { - const end = left.indexOf(`"`, 1); - const literal = left.substring(1, end); - tokens.push({ literal }); - left = left.substring(end + 1).trimStart(); - } else if (left.startsWith(`'`)) { - const end = left.indexOf(`'`, 1); - const literal = left.substring(1, end); - tokens.push({ literal }); - left = left.substring(end + 1).trimStart(); - } else { - const identifier = /\w+/.exec(left)?.[0] ?? ''; - - if (identifier.length === 0) { - break; - } - - tokens.push({ id: identifier }); - left = left.substring(identifier.length).trimStart(); - } - - if (left.length === lastLength) { - // No progress, break it off - break; - } - lastLength = left.length; - } - - return tokens; -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function splitBy( - separator: TSep, -): (array: (TElem | TSep)[]) => TElem[][] { - return (array: (TElem | TSep)[]) => { - const groups: TElem[][] = []; - - array.forEach((x) => { - if (groups.length === 0) { - if (x === separator) { - // Do nothing - } else { - groups.push([x as TElem]); - } - } else { - if (x === separator) { - groups.push([]); - } else { - groups[groups.length - 1].push(x as TElem); - } - } - }); - - return groups; - }; -} - -export const parseImportStatement = (tokens: ImportStatementToken[]) => { - const allAlias = - tokens[1] === '*' && isIdentifier(tokens[3]) ? tokens[3].id : null; - - const defaultAlias: string | null = isIdentifier(tokens[1]) - ? tokens[1].id - : null; - - const moduleNameIdx = tokens.findIndex(isLiteral); - const moduleName: string = pipe(tokens[moduleNameIdx], (token) => { - if (!token) { - throw new Error(`Missing module name in import statement.`); - } - return (token as LiteralToken).literal; - }); - - const namedImports: Record = pipe( - // all tokens between { ... } - tokens.includes('{') - ? tokens.slice(tokens.indexOf('{') + 1, tokens.indexOf('}')) - : [], - // collapsing `#0 as #1` into { #0: #1 }, and if no aliasing is done, { #0: #0 } - splitBy(',' as const), - mapToObj((list) => { - if (list.length === 0) { - throw new Error(`Invalid named import`); - } - - const named = list[0]; - if (!isIdentifier(named)) { - throw new Error(`Expected identifier as named import.`); - } - - // aliased - if (list.length === 3) { - const alias = list[2]; - if (!isIdentifier(alias)) { - throw new Error(`Expected identifier as alias to named import.`); - } - return [named.id, alias.id]; - } - - return [named.id, named.id]; - }), - ); - - // Removing all used tokens. - for (let i = 0; i < moduleNameIdx; ++i) { - tokens.shift(); - } - - if (tokens[0] === ';') { - tokens.shift(); - } - - return { - allAlias, - defaultAlias, - namedImports, - moduleName, - }; -}; - -function parseExampleCode(rawCode: string) { +export function parseExampleCode(rawCode: string): Example { // extracting metadata from the first comment let metadata: ExampleMetadata = { title: '', @@ -187,12 +18,10 @@ function parseExampleCode(rawCode: string) { ); } - // Turning: - // `import Default, { one, two } from ''module` statements into - // `const { default: Default, one, two } = await _import('module')` + rawCode = rawCode.slice(rawCode.indexOf('*/') + 2); return { metadata, - code: code.substring(code.indexOf('*/') + 2), + code: rawCode.slice(rawCode.indexOf('*/') + 2), }; } diff --git a/apps/wigsill-examples/src/exampleRunner.ts b/apps/wigsill-examples/src/exampleRunner.ts new file mode 100644 index 000000000..d027a052e --- /dev/null +++ b/apps/wigsill-examples/src/exampleRunner.ts @@ -0,0 +1,97 @@ +import * as Babel from '@babel/standalone'; +import { filter, isNonNull, map, pipe } from 'remeda'; +import type { TraverseOptions } from '@babel/traverse'; +import type TemplateGenerator from '@babel/template'; + +import { ExampleState } from './common/exampleState'; + +// NOTE: @babel/standalone does expose internal packages, as specified in the docs, but the +// typing for @babel/standalone does not expose them. +const template = ( + Babel as unknown as { packages: { template: typeof TemplateGenerator } } +).packages.template; + +/** + * A custom babel plugin for turning: + * + * `import Default, { one, two } from ''module` + * into + * `const { default: Default, one, two } = await _import('module')` + */ +const staticToDynamicImports = { + visitor: { + ImportDeclaration(path) { + const moduleName = path.node.source.value; + + const imports = pipe( + path.node.specifiers, + map((imp) => { + if (imp.type === 'ImportDefaultSpecifier') { + return ['default', imp.local.name] as const; + } + + if (imp.type === 'ImportSpecifier') { + return [ + imp.imported.type === 'Identifier' + ? imp.imported.name + : imp.imported.value, + imp.local.name, + ] as const; + } + + // Ignoring namespace imports + return null; + }), + filter(isNonNull), + ); + + path.replaceWith( + template.statement.ast( + `const { ${imports.map((imp) => (imp[0] === imp[1] ? imp[0] : `${imp[0]}: ${imp[1]}`)).join(',')} } = await _import('${moduleName}');`, + ), + ); + }, + } satisfies TraverseOptions, +}; + +export async function executeExample( + exampleCode: string, + defineLayout: () => void, +): Promise { + /** + * Simulated imports from within the sandbox, making only a subset of + * modules available. + */ + const _import = async (moduleKey: string) => { + if (moduleKey === 'wigsill') { + return await import('wigsill'); + } + throw new Error(`Module ${moduleKey} is not available in the sandbox.`); + }; + + const transformedCode = (() => { + const output = Babel.transform(exampleCode, { + compact: false, + retainLines: true, + plugins: [staticToDynamicImports], + }); + + console.log(output.code); + + return output.code; + })(); + + const mod = Function(` +return async (_import) => { +${transformedCode} +}; +`); + + const result: Promise = mod()(_import); + + console.log(await result); + + return { + dispose: () => {}, + }; +} diff --git a/apps/wigsill-examples/src/examples/basic-triangle.js b/apps/wigsill-examples/src/examples/basic-triangle.js index a4404ab25..5091c8c4b 100644 --- a/apps/wigsill-examples/src/examples/basic-triangle.js +++ b/apps/wigsill-examples/src/examples/basic-triangle.js @@ -4,15 +4,13 @@ } */ -import { wgsl } from 'wigsill'; -import { defineLayout } from '@wigsill/example-toolkit'; +import Default, { wgsl as wigsill } from 'wigsill'; +import { defineLayout, onCleanup } from '@wigsill/example-toolkit'; const some = wgsl.fn()`() -> f32 { return 1. + 2.; }`; -return { - eachFrame: () => {}, - - dispose() {}, -}; +onCleanup(() => { + console.log(`All cleaned up`); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d88966ef..434a1d372 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: apps/wigsill-examples: dependencies: + '@babel/standalone': + specifier: ^7.24.7 + version: 7.24.7 '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.50.0)(react-dom@18.3.1)(react@18.3.1) @@ -96,6 +99,15 @@ importers: specifier: ^3.23.8 version: 3.23.8 devDependencies: + '@types/babel__standalone': + specifier: ^7.1.7 + version: 7.1.7 + '@types/babel__template': + specifier: ^7.4.4 + version: 7.4.4 + '@types/babel__traverse': + specifier: ^7.20.6 + version: 7.20.6 '@types/dat.gui': specifier: ^0.7.13 version: 0.7.13 @@ -180,6 +192,38 @@ packages: '@jridgewell/trace-mapping': 0.3.22 dev: true + /@babel/helper-string-parser@7.24.7: + resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.24.7: + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/parser@7.24.7: + resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/standalone@7.24.7: + resolution: {integrity: sha512-QRIRMJ2KTeN+vt4l9OjYlxDVXEpcor1Z6V7OeYzeBOw6Q8ew9oMTHjzTx8s6ClsZO7wVf6JgTRutihatN6K0yA==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/types@7.24.7: + resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + dev: true + /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true @@ -1344,6 +1388,41 @@ packages: '@swc/counter': 0.1.3 dev: true + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + dev: true + + /@types/babel__generator@7.6.8: + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@types/babel__standalone@7.1.7: + resolution: {integrity: sha512-4RUJX9nWrP/emaZDzxo/+RYW8zzLJTXWJyp2k78HufG459HCz754hhmSymt3VFOU6/Wy+IZqfPvToHfLuGOr7w==} + dependencies: + '@types/babel__core': 7.20.5 + dev: true + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + dev: true + + /@types/babel__traverse@7.20.6: + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + dependencies: + '@babel/types': 7.24.7 + dev: true + /@types/chai-subset@1.3.5: resolution: {integrity: sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==} dependencies: @@ -4407,6 +4486,11 @@ packages: engines: {node: '>=14.0.0'} dev: true + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} From 1accdee4cd744a250f12db51fea938b332b9a4fc Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Wed, 10 Jul 2024 13:04:02 +0200 Subject: [PATCH 04/14] Better sandbox support --- apps/wigsill-examples/src/App.tsx | 4 +++- apps/wigsill-examples/src/CodeEditor.tsx | 2 -- .../src/ExampleView/ExampleView.tsx | 5 ++-- .../src/ExampleView/examples.ts | 11 ++++++--- apps/wigsill-examples/src/exampleRunner.ts | 23 ++++++++++++++++++- .../src/examples/basic-triangle.js | 12 ++++++---- 6 files changed, 44 insertions(+), 13 deletions(-) diff --git a/apps/wigsill-examples/src/App.tsx b/apps/wigsill-examples/src/App.tsx index 4c86674e2..27b0e7d1d 100644 --- a/apps/wigsill-examples/src/App.tsx +++ b/apps/wigsill-examples/src/App.tsx @@ -18,7 +18,9 @@ function App() { } if (currentExample in examples) { - return ; + return ( + + ); } return ; diff --git a/apps/wigsill-examples/src/CodeEditor.tsx b/apps/wigsill-examples/src/CodeEditor.tsx index 6cea100bb..4dba62a56 100644 --- a/apps/wigsill-examples/src/CodeEditor.tsx +++ b/apps/wigsill-examples/src/CodeEditor.tsx @@ -26,7 +26,6 @@ function useExample(exampleCode: string) { useEffect(() => { let cancelled = false; - console.log('MAKE'); const gui = new GUI({ closeOnTop: true }); gui.hide(); @@ -43,7 +42,6 @@ function useExample(exampleCode: string) { }); return () => { - console.log('BREAK'); exampleRef.current?.dispose(); cancelled = true; gui.destroy(); diff --git a/apps/wigsill-examples/src/ExampleView/ExampleView.tsx b/apps/wigsill-examples/src/ExampleView/ExampleView.tsx index 2bcd7ef6a..4cc082367 100644 --- a/apps/wigsill-examples/src/ExampleView/ExampleView.tsx +++ b/apps/wigsill-examples/src/ExampleView/ExampleView.tsx @@ -1,8 +1,9 @@ +import { debounce } from 'remeda'; +import { useMemo, useState } from 'react'; + import useEvent from '../common/useEvent'; import { CodeEditor } from '../CodeEditor'; import type { Example } from './types'; -import { useMemo, useState } from 'react'; -import { debounce } from 'remeda'; type Props = { example: Example; diff --git a/apps/wigsill-examples/src/ExampleView/examples.ts b/apps/wigsill-examples/src/ExampleView/examples.ts index 6fd76acb2..5321d5350 100644 --- a/apps/wigsill-examples/src/ExampleView/examples.ts +++ b/apps/wigsill-examples/src/ExampleView/examples.ts @@ -12,8 +12,13 @@ import { parseExampleCode } from './parseExampleCode'; export const examples = pipe( rawExamples, - mapKeys((key) => { - return key.replace('^../examples/', ''); - }), + mapKeys((key) => + pipe( + key, + (key) => key.replace(/^..\/examples\//, ''), // remove parent folder + (key) => key.replace(/.js$/, ''), // remove extension + (key) => key.replace(/\//, '--'), // / -> -- + ), + ), mapValues(parseExampleCode), ); diff --git a/apps/wigsill-examples/src/exampleRunner.ts b/apps/wigsill-examples/src/exampleRunner.ts index d027a052e..9d7532325 100644 --- a/apps/wigsill-examples/src/exampleRunner.ts +++ b/apps/wigsill-examples/src/exampleRunner.ts @@ -58,6 +58,8 @@ export async function executeExample( exampleCode: string, defineLayout: () => void, ): Promise { + const cleanupCallbacks: (() => unknown)[] = []; + /** * Simulated imports from within the sandbox, making only a subset of * modules available. @@ -66,6 +68,23 @@ export async function executeExample( if (moduleKey === 'wigsill') { return await import('wigsill'); } + if (moduleKey === '@wigsill/example-toolkit') { + return { + onCleanup(callback: () => unknown) { + cleanupCallbacks.push(callback); + }, + onFrame(callback: () => unknown) { + let handle = 0; + const runner = () => { + callback(); + handle = requestAnimationFrame(runner); + }; + runner(); + + cleanupCallbacks.push(() => cancelAnimationFrame(handle)); + }, + }; + } throw new Error(`Module ${moduleKey} is not available in the sandbox.`); }; @@ -92,6 +111,8 @@ ${transformedCode} console.log(await result); return { - dispose: () => {}, + dispose: () => { + cleanupCallbacks.forEach((cb) => cb()); + }, }; } diff --git a/apps/wigsill-examples/src/examples/basic-triangle.js b/apps/wigsill-examples/src/examples/basic-triangle.js index 5091c8c4b..c3314bf64 100644 --- a/apps/wigsill-examples/src/examples/basic-triangle.js +++ b/apps/wigsill-examples/src/examples/basic-triangle.js @@ -1,16 +1,20 @@ /* { - "title": "Basic Triangle" + "title": "Basic Triangles" } */ -import Default, { wgsl as wigsill } from 'wigsill'; -import { defineLayout, onCleanup } from '@wigsill/example-toolkit'; +import { wgsl } from 'wigsill'; +import { defineLayout, onCleanup, onFrame } from '@wigsill/example-toolkit'; -const some = wgsl.fn()`() -> f32 { +wgsl.fn()`() -> f32 { return 1. + 2.; }`; +onFrame(() => { + console.log('Hello from basic triangle!'); +}); + onCleanup(() => { console.log(`All cleaned up`); }); From 51e4d8d00a2a9de2df72e7e0ffabb2d49518db9c Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Wed, 10 Jul 2024 13:12:17 +0200 Subject: [PATCH 05/14] Restructure --- .../examples/BasicTriangleExample.tsx | 0 .../examples/CameraThresholdingExample.tsx | 0 .../examples/GradientTilesExample.tsx | 0 .../{src => }/examples/basic-triangle.js | 5 +- apps/wigsill-examples/src/App.tsx | 4 +- apps/wigsill-examples/src/CodeEditor.tsx | 4 +- .../src/ExampleView/parseExampleCode.test.ts | 203 ------------------ .../wigsill-examples/src/common/useExample.ts | 35 --- .../src/common/useExampleWithCanvas.ts | 42 ---- .../{ExampleView => example}/ExampleView.tsx | 2 +- .../src/{ => example}/exampleRunner.ts | 2 +- .../src/{common => example}/exampleState.ts | 0 .../src/{ExampleView => example}/examples.ts | 10 +- .../parseExampleCode.ts | 0 .../src/{ExampleView => example}/types.ts | 0 15 files changed, 15 insertions(+), 292 deletions(-) rename apps/wigsill-examples/{src => }/examples/BasicTriangleExample.tsx (100%) rename apps/wigsill-examples/{src => }/examples/CameraThresholdingExample.tsx (100%) rename apps/wigsill-examples/{src => }/examples/GradientTilesExample.tsx (100%) rename apps/wigsill-examples/{src => }/examples/basic-triangle.js (64%) delete mode 100644 apps/wigsill-examples/src/ExampleView/parseExampleCode.test.ts delete mode 100644 apps/wigsill-examples/src/common/useExample.ts delete mode 100644 apps/wigsill-examples/src/common/useExampleWithCanvas.ts rename apps/wigsill-examples/src/{ExampleView => example}/ExampleView.tsx (93%) rename apps/wigsill-examples/src/{ => example}/exampleRunner.ts (98%) rename apps/wigsill-examples/src/{common => example}/exampleState.ts (100%) rename apps/wigsill-examples/src/{ExampleView => example}/examples.ts (81%) rename apps/wigsill-examples/src/{ExampleView => example}/parseExampleCode.ts (100%) rename apps/wigsill-examples/src/{ExampleView => example}/types.ts (100%) diff --git a/apps/wigsill-examples/src/examples/BasicTriangleExample.tsx b/apps/wigsill-examples/examples/BasicTriangleExample.tsx similarity index 100% rename from apps/wigsill-examples/src/examples/BasicTriangleExample.tsx rename to apps/wigsill-examples/examples/BasicTriangleExample.tsx diff --git a/apps/wigsill-examples/src/examples/CameraThresholdingExample.tsx b/apps/wigsill-examples/examples/CameraThresholdingExample.tsx similarity index 100% rename from apps/wigsill-examples/src/examples/CameraThresholdingExample.tsx rename to apps/wigsill-examples/examples/CameraThresholdingExample.tsx diff --git a/apps/wigsill-examples/src/examples/GradientTilesExample.tsx b/apps/wigsill-examples/examples/GradientTilesExample.tsx similarity index 100% rename from apps/wigsill-examples/src/examples/GradientTilesExample.tsx rename to apps/wigsill-examples/examples/GradientTilesExample.tsx diff --git a/apps/wigsill-examples/src/examples/basic-triangle.js b/apps/wigsill-examples/examples/basic-triangle.js similarity index 64% rename from apps/wigsill-examples/src/examples/basic-triangle.js rename to apps/wigsill-examples/examples/basic-triangle.js index c3314bf64..36a369e94 100644 --- a/apps/wigsill-examples/src/examples/basic-triangle.js +++ b/apps/wigsill-examples/examples/basic-triangle.js @@ -5,7 +5,10 @@ */ import { wgsl } from 'wigsill'; -import { defineLayout, onCleanup, onFrame } from '@wigsill/example-toolkit'; +import { addCanvas, onCleanup, onFrame } from '@wigsill/example-toolkit'; + +const canvas = await addCanvas(); +console.log(canvas); wgsl.fn()`() -> f32 { return 1. + 2.; diff --git a/apps/wigsill-examples/src/App.tsx b/apps/wigsill-examples/src/App.tsx index 27b0e7d1d..c1ae742a6 100644 --- a/apps/wigsill-examples/src/App.tsx +++ b/apps/wigsill-examples/src/App.tsx @@ -4,9 +4,9 @@ import { useAtom } from 'jotai/react'; import { currentExampleAtom } from './router'; import { ExampleLink } from './common/ExampleLink'; -import { examples } from './ExampleView/examples'; import { ExampleNotFound } from './ExampleNotFound'; -import { ExampleView } from './ExampleView/ExampleView'; +import { ExampleView } from './example/ExampleView'; +import { examples } from './example/examples'; import { Home } from './Home'; function App() { diff --git a/apps/wigsill-examples/src/CodeEditor.tsx b/apps/wigsill-examples/src/CodeEditor.tsx index 4dba62a56..19ac9646a 100644 --- a/apps/wigsill-examples/src/CodeEditor.tsx +++ b/apps/wigsill-examples/src/CodeEditor.tsx @@ -3,8 +3,8 @@ import Editor from '@monaco-editor/react'; import { useCallback, useEffect, useRef } from 'react'; import useEvent from './common/useEvent'; -import { ExampleState } from './common/exampleState'; -import { executeExample } from './exampleRunner'; +import { ExampleState } from './example/exampleState'; +import { executeExample } from './example/exampleRunner'; type Props = { code: string; diff --git a/apps/wigsill-examples/src/ExampleView/parseExampleCode.test.ts b/apps/wigsill-examples/src/ExampleView/parseExampleCode.test.ts deleted file mode 100644 index 04b58c493..000000000 --- a/apps/wigsill-examples/src/ExampleView/parseExampleCode.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - parseImportStatement as parse, - tokenizeImportStatement as tokenize, -} from './parseExampleCode'; - -describe('tokenizeImportStatement', () => { - it('tokenizes an inline import', () => { - const code = `import "module"`; - const expected = ['import', { literal: 'module' }]; - expect(tokenize(code)).toEqual(expected); - }); - - it('tokenizes a list of named imports', () => { - const code = `import { one, two } from "module"`; - const expected = [ - 'import', - '{', - { id: 'one' }, - ',', - { id: 'two' }, - '}', - 'from', - { literal: 'module' }, - ]; - expect(tokenize(code)).toEqual(expected); - }); - - it('tokenizes a list of named imports (non-standard whitespace)', () => { - const code = `import{ one,two } from "module";`; - const expected = [ - 'import', - '{', - { id: 'one' }, - ',', - { id: 'two' }, - '}', - 'from', - { literal: 'module' }, - ';', - ]; - expect(tokenize(code)).toEqual(expected); - }); - - it('tokenizes default and a list of named imports', () => { - const code = `import Module, { one, two } from "module"`; - const expected = [ - 'import', - { id: 'Module' }, - ',', - '{', - { id: 'one' }, - ',', - { id: 'two' }, - '}', - 'from', - { literal: 'module' }, - ]; - expect(tokenize(code)).toEqual(expected); - }); - - it('tokenizes default import', () => { - const code = `import Module from "module"`; - const expected = [ - 'import', - { id: 'Module' }, - 'from', - { literal: 'module' }, - ]; - expect(tokenize(code)).toEqual(expected); - }); - - it('tokenizes alias of all', () => { - const code = `import * as All from "module"`; - const expected = [ - 'import', - '*', - 'as', - { id: 'All' }, - 'from', - { literal: 'module' }, - ]; - expect(tokenize(code)).toEqual(expected); - }); - - it('tokenizes alias of named imports', () => { - const code = `import { - one as One, - two as Two, - three - } from 'module'`; - - const expected = [ - 'import', - '{', - { id: 'one' }, - 'as', - { id: 'One' }, - ',', - { id: 'two' }, - 'as', - { id: 'Two' }, - ',', - { id: 'three' }, - '}', - 'from', - { literal: 'module' }, - ]; - expect(tokenize(code)).toEqual(expected); - }); -}); - -describe('parseImportStatement', () => { - it('parses named imports', () => { - const expected = { - allAlias: null, - defaultAlias: null, - namedImports: { one: 'one', two: 'two' }, - moduleName: 'module_name', - }; - - expect(parse(tokenize(`import { one, two } from "module_name"`))).toEqual( - expected, - ); - expect(parse(tokenize(`import{one,two} from 'module_name'`))).toEqual( - expected, - ); - expect(parse(tokenize(`import {one, two } from "module_name";`))).toEqual( - expected, - ); - expect(parse(tokenize(`import { one, two } from 'module_name';`))).toEqual( - expected, - ); - }); - - it('parses default import', () => { - const expected = { - allAlias: null, - defaultAlias: 'Module', - namedImports: {}, - moduleName: 'module_name', - }; - - expect(parse(tokenize(`import Module from "module_name"`))).toEqual( - expected, - ); - expect(parse(tokenize(`import Module from 'module_name'`))).toEqual( - expected, - ); - expect(parse(tokenize(`import Module from "module_name";`))).toEqual( - expected, - ); - expect(parse(tokenize(`import Module from 'module_name';`))).toEqual( - expected, - ); - }); - - it('parses default and named imports', () => { - const expected = { - allAlias: null, - defaultAlias: 'Module', - namedImports: { one: 'one', two: 'Two' }, - moduleName: 'module_name', - }; - - expect( - parse(tokenize(`import Module, { one, two as Two } from "module_name"`)), - ).toEqual(expected); - expect( - parse(tokenize(`import Module, { one, two as Two } from 'module_name'`)), - ).toEqual(expected); - expect( - parse( - tokenize(`import Module, { one, two as Two } from "module_name";`), - ), - ).toEqual(expected); - expect( - parse(tokenize(`import Module, { one, two as Two } from 'module_name';`)), - ).toEqual(expected); - }); - - it('parses all alias', () => { - const expected = { - allAlias: 'All', - defaultAlias: null, - namedImports: {}, - moduleName: 'module_name', - }; - - expect(parse(tokenize(`import * as All from "module_name"`))).toEqual( - expected, - ); - expect(parse(tokenize(`import * as All from 'module_name'`))).toEqual( - expected, - ); - expect(parse(tokenize(`import * as All from "module_name";`))).toEqual( - expected, - ); - expect(parse(tokenize(`import * as All from 'module_name';`))).toEqual( - expected, - ); - }); -}); diff --git a/apps/wigsill-examples/src/common/useExample.ts b/apps/wigsill-examples/src/common/useExample.ts deleted file mode 100644 index 4be8faadf..000000000 --- a/apps/wigsill-examples/src/common/useExample.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { GUI } from 'dat.gui'; -import { useEffect, useRef } from 'react'; - -import { ExampleState } from './exampleState'; - -export function useExample Promise>( - initExampleFn: T, -) { - const exampleRef = useRef(null); - - useEffect(() => { - let cancelled = false; - - const gui = new GUI({ closeOnTop: true }); - gui.hide(); - - initExampleFn(gui).then((example) => { - if (cancelled) { - // Another instance was started in the meantime. - example.dispose(); - return; - } - - // Success - exampleRef.current = example; - gui.show(); - }); - - return () => { - exampleRef.current?.dispose(); - gui.destroy(); - cancelled = true; - }; - }, [initExampleFn]); -} diff --git a/apps/wigsill-examples/src/common/useExampleWithCanvas.ts b/apps/wigsill-examples/src/common/useExampleWithCanvas.ts deleted file mode 100644 index a96612fd5..000000000 --- a/apps/wigsill-examples/src/common/useExampleWithCanvas.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { GUI } from 'dat.gui'; -import { useEffect, useRef } from 'react'; - -import type { ExampleState } from './exampleState'; - -export function useExampleWithCanvas< - T extends (gui: dat.GUI, canvas: HTMLCanvasElement) => Promise, ->(initExampleFn: T) { - const canvasRef = useRef(null); - const exampleRef = useRef(null); - - useEffect(() => { - if (!canvasRef.current) { - return; - } - - let cancelled = false; - - const gui = new GUI({ closeOnTop: true }); - gui.hide(); - - initExampleFn(gui, canvasRef.current).then((example) => { - if (cancelled) { - // Another instance was started in the meantime. - example.dispose(); - return; - } - - // Success - exampleRef.current = example; - gui.show(); - }); - - return () => { - exampleRef.current?.dispose(); - gui.destroy(); - cancelled = true; - }; - }, [initExampleFn]); - - return canvasRef; -} diff --git a/apps/wigsill-examples/src/ExampleView/ExampleView.tsx b/apps/wigsill-examples/src/example/ExampleView.tsx similarity index 93% rename from apps/wigsill-examples/src/ExampleView/ExampleView.tsx rename to apps/wigsill-examples/src/example/ExampleView.tsx index 4cc082367..c576e2e94 100644 --- a/apps/wigsill-examples/src/ExampleView/ExampleView.tsx +++ b/apps/wigsill-examples/src/example/ExampleView.tsx @@ -3,7 +3,7 @@ import { useMemo, useState } from 'react'; import useEvent from '../common/useEvent'; import { CodeEditor } from '../CodeEditor'; -import type { Example } from './types'; +import type { Example } from '../example/types'; type Props = { example: Example; diff --git a/apps/wigsill-examples/src/exampleRunner.ts b/apps/wigsill-examples/src/example/exampleRunner.ts similarity index 98% rename from apps/wigsill-examples/src/exampleRunner.ts rename to apps/wigsill-examples/src/example/exampleRunner.ts index 9d7532325..c8d6ec05e 100644 --- a/apps/wigsill-examples/src/exampleRunner.ts +++ b/apps/wigsill-examples/src/example/exampleRunner.ts @@ -3,7 +3,7 @@ import { filter, isNonNull, map, pipe } from 'remeda'; import type { TraverseOptions } from '@babel/traverse'; import type TemplateGenerator from '@babel/template'; -import { ExampleState } from './common/exampleState'; +import { ExampleState } from './exampleState'; // NOTE: @babel/standalone does expose internal packages, as specified in the docs, but the // typing for @babel/standalone does not expose them. diff --git a/apps/wigsill-examples/src/common/exampleState.ts b/apps/wigsill-examples/src/example/exampleState.ts similarity index 100% rename from apps/wigsill-examples/src/common/exampleState.ts rename to apps/wigsill-examples/src/example/exampleState.ts diff --git a/apps/wigsill-examples/src/ExampleView/examples.ts b/apps/wigsill-examples/src/example/examples.ts similarity index 81% rename from apps/wigsill-examples/src/ExampleView/examples.ts rename to apps/wigsill-examples/src/example/examples.ts index 5321d5350..a71dc8490 100644 --- a/apps/wigsill-examples/src/ExampleView/examples.ts +++ b/apps/wigsill-examples/src/example/examples.ts @@ -1,5 +1,8 @@ +import { mapKeys, mapValues, pipe } from 'remeda'; +import { parseExampleCode } from './parseExampleCode'; + const rawExamples: Record = import.meta.glob( - '../examples/**/*.js', + '../../examples/**/*.js', { query: 'raw', eager: true, @@ -7,15 +10,12 @@ const rawExamples: Record = import.meta.glob( }, ); -import { mapKeys, mapValues, pipe } from 'remeda'; -import { parseExampleCode } from './parseExampleCode'; - export const examples = pipe( rawExamples, mapKeys((key) => pipe( key, - (key) => key.replace(/^..\/examples\//, ''), // remove parent folder + (key) => key.replace(/^..\/..\/examples\//, ''), // remove parent folder (key) => key.replace(/.js$/, ''), // remove extension (key) => key.replace(/\//, '--'), // / -> -- ), diff --git a/apps/wigsill-examples/src/ExampleView/parseExampleCode.ts b/apps/wigsill-examples/src/example/parseExampleCode.ts similarity index 100% rename from apps/wigsill-examples/src/ExampleView/parseExampleCode.ts rename to apps/wigsill-examples/src/example/parseExampleCode.ts diff --git a/apps/wigsill-examples/src/ExampleView/types.ts b/apps/wigsill-examples/src/example/types.ts similarity index 100% rename from apps/wigsill-examples/src/ExampleView/types.ts rename to apps/wigsill-examples/src/example/types.ts From 1d7c7ba6bf7209acb3dd53c23391729c26971f01 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Wed, 10 Jul 2024 14:17:35 +0200 Subject: [PATCH 06/14] Create layout on demand of the example code. --- .../examples/GradientTilesExample.tsx | 2 +- .../examples/basic-triangle.js | 4 +- .../examples/gradient-tiles.js | 140 ++++++++++++++++++ apps/wigsill-examples/src/CodeEditor.tsx | 51 +------ apps/wigsill-examples/src/common/Canvas.tsx | 45 ++++++ .../src/example/ExampleView.tsx | 55 ++++++- .../src/example/exampleRunner.ts | 4 +- apps/wigsill-examples/src/example/layout.ts | 58 ++++++++ apps/wigsill-examples/src/main.tsx | 2 +- 9 files changed, 304 insertions(+), 57 deletions(-) create mode 100644 apps/wigsill-examples/examples/gradient-tiles.js create mode 100644 apps/wigsill-examples/src/common/Canvas.tsx create mode 100644 apps/wigsill-examples/src/example/layout.ts diff --git a/apps/wigsill-examples/examples/GradientTilesExample.tsx b/apps/wigsill-examples/examples/GradientTilesExample.tsx index fc619e688..7c3074b32 100644 --- a/apps/wigsill-examples/examples/GradientTilesExample.tsx +++ b/apps/wigsill-examples/examples/GradientTilesExample.tsx @@ -163,5 +163,5 @@ fn main_frag( export function GradientTilesExample() { const canvasRef = useExampleWithCanvas(init); - return ; + return ; } diff --git a/apps/wigsill-examples/examples/basic-triangle.js b/apps/wigsill-examples/examples/basic-triangle.js index 36a369e94..32ab31934 100644 --- a/apps/wigsill-examples/examples/basic-triangle.js +++ b/apps/wigsill-examples/examples/basic-triangle.js @@ -5,9 +5,9 @@ */ import { wgsl } from 'wigsill'; -import { addCanvas, onCleanup, onFrame } from '@wigsill/example-toolkit'; +import { addElement, onCleanup, onFrame } from '@wigsill/example-toolkit'; -const canvas = await addCanvas(); +const canvas = await addElement('canvas'); console.log(canvas); wgsl.fn()`() -> f32 { diff --git a/apps/wigsill-examples/examples/gradient-tiles.js b/apps/wigsill-examples/examples/gradient-tiles.js new file mode 100644 index 000000000..aaa9c3c20 --- /dev/null +++ b/apps/wigsill-examples/examples/gradient-tiles.js @@ -0,0 +1,140 @@ +/* +{ + "title": "Gradient Tiles" +} +*/ + +import { makeArena, ProgramBuilder, u32, wgsl, WGSLRuntime } from 'wigsill'; +import { addElement, onCleanup, onFrame } from '@wigsill/example-toolkit'; + +const adapter = await navigator.gpu.requestAdapter(); +const device = await adapter.requestDevice(); +const runtime = new WGSLRuntime(device); + +const xSpanData = wgsl.memory(u32).alias('x-span'); +const ySpanData = wgsl.memory(u32).alias('y-span'); + +const mainArena = makeArena({ + bufferBindingType: 'uniform', + memoryEntries: [xSpanData, ySpanData], + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, +}); + +const canvas = await addElement('canvas'); + +const context = canvas.getContext('webgpu'); + +const devicePixelRatio = window.devicePixelRatio; +canvas.width = canvas.clientWidth * devicePixelRatio; +canvas.height = canvas.clientHeight * devicePixelRatio; +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + +context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', +}); + +const mainCode = wgsl` +struct VertexOutput { + @builtin(position) pos: vec4f, + @location(0) uv: vec2f, +} + +@vertex +fn main_vert( + @builtin(vertex_index) VertexIndex: u32 +) -> VertexOutput { + var pos = array( + vec2(0.5, 0.5), // top-right + vec2(-0.5, 0.5), // top-left + vec2(0.5, -0.5), // bottom-right + vec2(-0.5, -0.5) // bottom-left + ); + + var uv = array( + vec2(1., 1.), // top-right + vec2(0., 1.), // top-left + vec2(1., 0.), // bottom-right + vec2(0., 0.) // bottom-left + ); + + var output: VertexOutput; + output.pos = vec4f(pos[VertexIndex], 0.0, 1.0); + output.uv = uv[VertexIndex]; + return output; +} + +@fragment +fn main_frag( + @builtin(position) Position: vec4f, + @location(0) uv: vec2f, +) -> @location(0) vec4f { + let red = floor(uv.x * f32(${xSpanData})) / f32(${xSpanData}); + let green = floor(uv.y * f32(${ySpanData})) / f32(${ySpanData}); + return vec4(red, green, 0.5, 1.0); +} + `; + +const program = new ProgramBuilder(runtime, mainCode).build({ + bindingGroup: 0, + shaderStage: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + arenas: [mainArena], +}); + +const shaderModule = device.createShaderModule({ + code: program.code, +}); + +const pipeline = device.createRenderPipeline({ + layout: device.createPipelineLayout({ + bindGroupLayouts: [program.bindGroupLayout], + }), + vertex: { + module: shaderModule, + entryPoint: 'main_vert', + }, + fragment: { + module: shaderModule, + entryPoint: 'main_frag', + targets: [ + { + format: presentationFormat, + }, + ], + }, + primitive: { + topology: 'triangle-strip', + }, +}); + +xSpanData.write(runtime, 16); +ySpanData.write(runtime, 16); + +onFrame(() => { + const commandEncoder = device.createCommandEncoder(); + const textureView = context.getCurrentTexture().createView(); + + const renderPassDescriptor = { + colorAttachments: [ + { + view: textureView, + clearValue: [0, 0, 0, 1], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }; + + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(pipeline); + passEncoder.setBindGroup(0, program.bindGroup); + passEncoder.draw(4); + passEncoder.end(); + + device.queue.submit([commandEncoder.finish()]); +}); + +onCleanup(() => { + console.log(`All cleaned up`); +}); diff --git a/apps/wigsill-examples/src/CodeEditor.tsx b/apps/wigsill-examples/src/CodeEditor.tsx index 19ac9646a..84585dbac 100644 --- a/apps/wigsill-examples/src/CodeEditor.tsx +++ b/apps/wigsill-examples/src/CodeEditor.tsx @@ -1,54 +1,12 @@ -import { GUI } from 'dat.gui'; import Editor from '@monaco-editor/react'; -import { useCallback, useEffect, useRef } from 'react'; import useEvent from './common/useEvent'; -import { ExampleState } from './example/exampleState'; -import { executeExample } from './example/exampleRunner'; type Props = { code: string; onCodeChange: (value: string) => unknown; }; -function useLayout() { - const defineLayout = useCallback(() => { - console.log(`Layout defined`); - }, []); - - return [null, defineLayout] as const; -} - -function useExample(exampleCode: string) { - const exampleRef = useRef(null); - const [_layout, defineLayout] = useLayout(); - - useEffect(() => { - let cancelled = false; - - const gui = new GUI({ closeOnTop: true }); - gui.hide(); - - executeExample(exampleCode, defineLayout).then((example) => { - if (cancelled) { - // Another instance was started in the meantime. - example.dispose(); - return; - } - - // Success - exampleRef.current = example; - gui.show(); - }); - - return () => { - exampleRef.current?.dispose(); - cancelled = true; - gui.destroy(); - }; - }, [exampleCode, defineLayout]); -} - export function CodeEditor(props: Props) { const { code, onCodeChange } = props; @@ -56,14 +14,7 @@ export function CodeEditor(props: Props) { onCodeChange(value ?? ''); }); - useExample(code); - return ( - + ); } diff --git a/apps/wigsill-examples/src/common/Canvas.tsx b/apps/wigsill-examples/src/common/Canvas.tsx new file mode 100644 index 000000000..76356eabd --- /dev/null +++ b/apps/wigsill-examples/src/common/Canvas.tsx @@ -0,0 +1,45 @@ +import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; +import useEvent from './useEvent'; + +type Props = { + width?: number; + height?: number; +}; + +export const Canvas = forwardRef((_props, ref) => { + const innerRef = useRef(null); + + useImperativeHandle(ref, () => innerRef.current!); + + const onResize = useEvent(() => { + if (!innerRef.current) { + return; + } + + const canvas = innerRef.current; + const rect: DOMRect | null = + canvas.parentNode && 'getBoundingClientRect' in canvas.parentNode + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + (canvas.parentNode as any).getBoundingClientRect() + : null; + + if (rect) { + canvas.width = rect.width; + canvas.height = rect.height; + } + }); + + useEffect(() => { + onResize(); + window.addEventListener('resize', onResize); + return () => { + window.removeEventListener('resize', onResize); + }; + }, [onResize]); + + return ( +
+ +
+ ); +}); diff --git a/apps/wigsill-examples/src/example/ExampleView.tsx b/apps/wigsill-examples/src/example/ExampleView.tsx index c576e2e94..f87f0fc41 100644 --- a/apps/wigsill-examples/src/example/ExampleView.tsx +++ b/apps/wigsill-examples/src/example/ExampleView.tsx @@ -1,14 +1,54 @@ +import { GUI } from 'dat.gui'; import { debounce } from 'remeda'; -import { useMemo, useState } from 'react'; +import { useMemo, useState, useEffect, useRef } from 'react'; import useEvent from '../common/useEvent'; import { CodeEditor } from '../CodeEditor'; import type { Example } from '../example/types'; +import { ExampleState } from './exampleState'; +import { executeExample } from './exampleRunner'; +import { useLayout } from './layout'; +import { Canvas } from '../common/Canvas'; type Props = { example: Example; }; +function useExample(exampleCode: string) { + const exampleRef = useRef(null); + const { def, addElement, setRef } = useLayout(); + + useEffect(() => { + let cancelled = false; + + const gui = new GUI({ closeOnTop: true }); + gui.hide(); + + executeExample(exampleCode, addElement).then((example) => { + if (cancelled) { + // Another instance was started in the meantime. + example.dispose(); + return; + } + + // Success + exampleRef.current = example; + gui.show(); + }); + + return () => { + exampleRef.current?.dispose(); + cancelled = true; + gui.destroy(); + }; + }, [exampleCode, addElement]); + + return { + def, + setRef, + }; +} + export function ExampleView({ example }: Props) { const { code: initialCode, metadata } = example; const [code, setCode] = useState(initialCode); @@ -22,10 +62,21 @@ export function ExampleView({ example }: Props) { setCodeDebouncer.call(newCode); }); + const { def, setRef } = useExample(code); + return ( <>

Hello {metadata.title}

- +
+ {def.elements.map((_element, index) => { + return setRef(index, canvas)} />; + })} +
+
+
+ +
+
); } diff --git a/apps/wigsill-examples/src/example/exampleRunner.ts b/apps/wigsill-examples/src/example/exampleRunner.ts index c8d6ec05e..89a2dcd53 100644 --- a/apps/wigsill-examples/src/example/exampleRunner.ts +++ b/apps/wigsill-examples/src/example/exampleRunner.ts @@ -4,6 +4,7 @@ import type { TraverseOptions } from '@babel/traverse'; import type TemplateGenerator from '@babel/template'; import { ExampleState } from './exampleState'; +import { AddElement } from './layout'; // NOTE: @babel/standalone does expose internal packages, as specified in the docs, but the // typing for @babel/standalone does not expose them. @@ -56,7 +57,7 @@ const staticToDynamicImports = { export async function executeExample( exampleCode: string, - defineLayout: () => void, + addElement: AddElement, ): Promise { const cleanupCallbacks: (() => unknown)[] = []; @@ -83,6 +84,7 @@ export async function executeExample( cleanupCallbacks.push(() => cancelAnimationFrame(handle)); }, + addElement, }; } throw new Error(`Module ${moduleKey} is not available in the sandbox.`); diff --git a/apps/wigsill-examples/src/example/layout.ts b/apps/wigsill-examples/src/example/layout.ts new file mode 100644 index 000000000..0d3cd1fb5 --- /dev/null +++ b/apps/wigsill-examples/src/example/layout.ts @@ -0,0 +1,58 @@ +import { useCallback, useRef, useState } from 'react'; +import useEvent from '../common/useEvent'; + +export type CanvasDef = { + type: 'canvas'; + width?: number; + height?: number; +}; + +export type LayoutDef = { + elements: CanvasDef[]; +}; + +export type AddElement = ( + type: 'canvas', + options: Omit, +) => Promise; + +export function useLayout(): { + def: LayoutDef; + addElement: AddElement; + setRef: (index: number, element: unknown) => void; +} { + const [def, setDef] = useState({ elements: [] }); + const elementResolves = useRef(new Map void>()); + + const addElement: AddElement = useEvent( + (type: CanvasDef['type'], options: Omit) => { + const index = def.elements.length; + + if (type === 'canvas') { + setDef({ + elements: [...def.elements, { ...options, type: 'canvas' }], + }); + + return new Promise((resolve) => { + elementResolves.current.set(index, resolve as () => void); + }); + } else { + throw new Error(`Tried to add unsupported layout element: ${type}`); + } + }, + ); + + const setRef = useCallback((index: number, element: unknown) => { + const resolve = elementResolves.current.get(index); + if (!resolve) { + throw new Error(`Tried to resolve non-existent layout element`); + } + resolve(element); + }, []); + + return { + def, + addElement, + setRef, + }; +} diff --git a/apps/wigsill-examples/src/main.tsx b/apps/wigsill-examples/src/main.tsx index f9bb4b34e..9bb419d3d 100644 --- a/apps/wigsill-examples/src/main.tsx +++ b/apps/wigsill-examples/src/main.tsx @@ -5,6 +5,6 @@ import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - , + , ); From 4b7ca8b568c6b74901ca37e14bc4a1c170eac56c Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Wed, 10 Jul 2024 14:48:30 +0200 Subject: [PATCH 07/14] Fixed layout issues. --- apps/wigsill-examples/src/common/Canvas.tsx | 30 +++++++----- .../src/example/ExampleView.tsx | 14 +++--- .../src/example/exampleRunner.ts | 6 +-- apps/wigsill-examples/src/example/layout.ts | 46 ++++++++++++++++++- 4 files changed, 73 insertions(+), 23 deletions(-) diff --git a/apps/wigsill-examples/src/common/Canvas.tsx b/apps/wigsill-examples/src/common/Canvas.tsx index 76356eabd..73f1d4831 100644 --- a/apps/wigsill-examples/src/common/Canvas.tsx +++ b/apps/wigsill-examples/src/common/Canvas.tsx @@ -8,29 +8,33 @@ type Props = { export const Canvas = forwardRef((_props, ref) => { const innerRef = useRef(null); + const containerRef = useRef(null); useImperativeHandle(ref, () => innerRef.current!); const onResize = useEvent(() => { - if (!innerRef.current) { + const canvas = innerRef.current; + const container = containerRef.current; + + if (!canvas || !container) { return; } - const canvas = innerRef.current; - const rect: DOMRect | null = - canvas.parentNode && 'getBoundingClientRect' in canvas.parentNode - ? // eslint-disable-next-line @typescript-eslint/no-explicit-any - (canvas.parentNode as any).getBoundingClientRect() - : null; - - if (rect) { - canvas.width = rect.width; - canvas.height = rect.height; + const width = container.clientWidth; + const height = container.clientHeight; + + if (width && height) { + canvas.width = width; + canvas.height = height; } }); useEffect(() => { onResize(); + // Size is wrong when loading the page zoomed-in, so we reset the size a bit after mounting. + setTimeout(() => { + onResize(); + }, 1); window.addEventListener('resize', onResize); return () => { window.removeEventListener('resize', onResize); @@ -38,7 +42,9 @@ export const Canvas = forwardRef((_props, ref) => { }, [onResize]); return ( -
+
); diff --git a/apps/wigsill-examples/src/example/ExampleView.tsx b/apps/wigsill-examples/src/example/ExampleView.tsx index f87f0fc41..d900f9dda 100644 --- a/apps/wigsill-examples/src/example/ExampleView.tsx +++ b/apps/wigsill-examples/src/example/ExampleView.tsx @@ -16,7 +16,7 @@ type Props = { function useExample(exampleCode: string) { const exampleRef = useRef(null); - const { def, addElement, setRef } = useLayout(); + const { def, createLayout, dispose: deleteLayout, setRef } = useLayout(); useEffect(() => { let cancelled = false; @@ -24,7 +24,9 @@ function useExample(exampleCode: string) { const gui = new GUI({ closeOnTop: true }); gui.hide(); - executeExample(exampleCode, addElement).then((example) => { + const layout = createLayout(); + + executeExample(exampleCode, layout).then((example) => { if (cancelled) { // Another instance was started in the meantime. example.dispose(); @@ -39,9 +41,10 @@ function useExample(exampleCode: string) { return () => { exampleRef.current?.dispose(); cancelled = true; + deleteLayout(); gui.destroy(); }; - }, [exampleCode, addElement]); + }, [exampleCode, createLayout, deleteLayout]); return { def, @@ -50,7 +53,7 @@ function useExample(exampleCode: string) { } export function ExampleView({ example }: Props) { - const { code: initialCode, metadata } = example; + const { code: initialCode } = example; const [code, setCode] = useState(initialCode); const setCodeDebouncer = useMemo( @@ -66,8 +69,7 @@ export function ExampleView({ example }: Props) { return ( <> -

Hello {metadata.title}

-
+
{def.elements.map((_element, index) => { return setRef(index, canvas)} />; })} diff --git a/apps/wigsill-examples/src/example/exampleRunner.ts b/apps/wigsill-examples/src/example/exampleRunner.ts index 89a2dcd53..24c6cb5f5 100644 --- a/apps/wigsill-examples/src/example/exampleRunner.ts +++ b/apps/wigsill-examples/src/example/exampleRunner.ts @@ -4,7 +4,7 @@ import type { TraverseOptions } from '@babel/traverse'; import type TemplateGenerator from '@babel/template'; import { ExampleState } from './exampleState'; -import { AddElement } from './layout'; +import { LayoutInstance } from './layout'; // NOTE: @babel/standalone does expose internal packages, as specified in the docs, but the // typing for @babel/standalone does not expose them. @@ -57,7 +57,7 @@ const staticToDynamicImports = { export async function executeExample( exampleCode: string, - addElement: AddElement, + layout: LayoutInstance, ): Promise { const cleanupCallbacks: (() => unknown)[] = []; @@ -84,7 +84,7 @@ export async function executeExample( cleanupCallbacks.push(() => cancelAnimationFrame(handle)); }, - addElement, + addElement: layout.addElement, }; } throw new Error(`Module ${moduleKey} is not available in the sandbox.`); diff --git a/apps/wigsill-examples/src/example/layout.ts b/apps/wigsill-examples/src/example/layout.ts index 0d3cd1fb5..e6f780e73 100644 --- a/apps/wigsill-examples/src/example/layout.ts +++ b/apps/wigsill-examples/src/example/layout.ts @@ -16,16 +16,32 @@ export type AddElement = ( options: Omit, ) => Promise; +/** + * One per example instance. + */ +export type LayoutInstance = { + addElement: AddElement; + active: boolean; + dispose: () => void; +}; + export function useLayout(): { def: LayoutDef; - addElement: AddElement; + createLayout: () => LayoutInstance; + dispose: () => void; setRef: (index: number, element: unknown) => void; } { const [def, setDef] = useState({ elements: [] }); const elementResolves = useRef(new Map void>()); + const instanceRef = useRef(null); const addElement: AddElement = useEvent( (type: CanvasDef['type'], options: Omit) => { + if (!instanceRef.current) { + // No instance is active. + throw new Error(`No layout is active`); + } + const index = def.elements.length; if (type === 'canvas') { @@ -42,6 +58,31 @@ export function useLayout(): { }, ); + const dispose = useCallback(() => { + if (!instanceRef.current) { + return; + } + + instanceRef.current.active = false; + instanceRef.current = null; + }, []); + + const createLayout = useCallback(() => { + // Discarding the old one, if it still exists. + dispose(); + + setDef({ elements: [] }); + + const newInstance: LayoutInstance = { + active: true, + addElement, + dispose, + }; + + instanceRef.current = newInstance; + return newInstance; + }, [dispose, addElement]); + const setRef = useCallback((index: number, element: unknown) => { const resolve = elementResolves.current.get(index); if (!resolve) { @@ -52,7 +93,8 @@ export function useLayout(): { return { def, - addElement, + dispose, + createLayout, setRef, }; } From 46702b35309e46eba4e093f6a5eb5f615f4a8cbb Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Wed, 10 Jul 2024 16:13:31 +0200 Subject: [PATCH 08/14] Implemented basic video support in example layout. --- .../examples/BasicTriangleExample.tsx | 168 ------------- .../examples/CameraThresholdingExample.tsx | 223 ------------------ .../examples/GradientTilesExample.tsx | 167 ------------- .../examples/basic-triangle.js | 133 ++++++++++- .../examples/camera-thresholding.js | 185 +++++++++++++++ apps/wigsill-examples/src/common/Video.tsx | 21 ++ .../src/example/ExampleView.tsx | 13 +- apps/wigsill-examples/src/example/layout.ts | 22 +- 8 files changed, 361 insertions(+), 571 deletions(-) delete mode 100644 apps/wigsill-examples/examples/BasicTriangleExample.tsx delete mode 100644 apps/wigsill-examples/examples/CameraThresholdingExample.tsx delete mode 100644 apps/wigsill-examples/examples/GradientTilesExample.tsx create mode 100644 apps/wigsill-examples/examples/camera-thresholding.js create mode 100644 apps/wigsill-examples/src/common/Video.tsx diff --git a/apps/wigsill-examples/examples/BasicTriangleExample.tsx b/apps/wigsill-examples/examples/BasicTriangleExample.tsx deleted file mode 100644 index 74129b705..000000000 --- a/apps/wigsill-examples/examples/BasicTriangleExample.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import * as dat from 'dat.gui'; -import { WGSLRuntime } from 'wigsill'; -import { ProgramBuilder, makeArena, u32, wgsl } from 'wigsill'; - -import { useExampleWithCanvas } from '../common/useExampleWithCanvas'; -import { useEffect } from 'react'; - -async function init(gui: dat.GUI, canvas: HTMLCanvasElement) { - const adapter = await navigator.gpu.requestAdapter(); - const device = await adapter!.requestDevice(); - const runtime = new WGSLRuntime(device); - - const xSpanData = wgsl.memory(u32).alias('x-span'); - const ySpanData = wgsl.memory(u32).alias('y-span'); - - const mainArena = makeArena({ - bufferBindingType: 'uniform', - memoryEntries: [xSpanData, ySpanData], - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, - }); - - const context = canvas.getContext('webgpu') as GPUCanvasContext; - - const devicePixelRatio = window.devicePixelRatio; - canvas.width = canvas.clientWidth * devicePixelRatio; - canvas.height = canvas.clientHeight * devicePixelRatio; - const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); - - context.configure({ - device, - format: presentationFormat, - alphaMode: 'premultiplied', - }); - - const mainCode = wgsl` -struct VertexOutput { - @builtin(position) pos: vec4f, - @location(0) uv: vec2f, -} - -@vertex -fn main_vert( - @builtin(vertex_index) VertexIndex: u32 -) -> VertexOutput { - var pos = array( - vec2(0.5, 0.5), // top-right - vec2(-0.5, 0.5), // top-left - vec2(0.5, -0.5), // bottom-right - vec2(-0.5, -0.5) // bottom-left - ); - - var uv = array( - vec2(1., 1.), // top-right - vec2(0., 1.), // top-left - vec2(1., 0.), // bottom-right - vec2(0., 0.) // bottom-left - ); - - var output: VertexOutput; - output.pos = vec4f(pos[VertexIndex], 0.0, 1.0); - output.uv = uv[VertexIndex]; - return output; -} - -@fragment -fn main_frag( - @builtin(position) Position: vec4f, - @location(0) uv: vec2f, -) -> @location(0) vec4f { - let red = floor(uv.x * f32(${xSpanData})) / f32(${xSpanData}); - let green = floor(uv.y * f32(${ySpanData})) / f32(${ySpanData}); - return vec4(red, green, 0.5, 1.0); -} - `; - - const program = new ProgramBuilder(runtime, mainCode).build({ - bindingGroup: 0, - shaderStage: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, - arenas: [mainArena], - }); - - const shaderModule = device.createShaderModule({ - code: program.code, - }); - - const pipeline = device.createRenderPipeline({ - layout: device.createPipelineLayout({ - bindGroupLayouts: [program.bindGroupLayout], - }), - vertex: { - module: shaderModule, - entryPoint: 'main_vert', - }, - fragment: { - module: shaderModule, - entryPoint: 'main_frag', - targets: [ - { - format: presentationFormat, - }, - ], - }, - primitive: { - topology: 'triangle-strip', - }, - }); - - /// UI - - const state = { - xSpan: 16, - ySpan: 16, - }; - - xSpanData.write(runtime, state.xSpan); - ySpanData.write(runtime, state.ySpan); - - gui.add(state, 'xSpan', 1, 16).onChange(() => { - xSpanData.write(runtime, state.xSpan); - }); - gui.add(state, 'ySpan', 1, 16).onChange(() => { - ySpanData.write(runtime, state.ySpan); - }); - - let running = true; - - function frame() { - const commandEncoder = device.createCommandEncoder(); - const textureView = context.getCurrentTexture().createView(); - - const renderPassDescriptor: GPURenderPassDescriptor = { - colorAttachments: [ - { - view: textureView, - clearValue: [0, 0, 0, 1], - loadOp: 'clear', - storeOp: 'store', - }, - ], - }; - - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); - passEncoder.setPipeline(pipeline); - passEncoder.setBindGroup(0, program.bindGroup); - passEncoder.draw(4); - passEncoder.end(); - - device.queue.submit([commandEncoder.finish()]); - - if (running) { - requestAnimationFrame(frame); - } - } - - requestAnimationFrame(frame); - - return { - dispose() { - running = false; - }, - }; -} - -export function BasicTriangleExample() { - const canvasRef = useExampleWithCanvas(init); - - return ; -} diff --git a/apps/wigsill-examples/examples/CameraThresholdingExample.tsx b/apps/wigsill-examples/examples/CameraThresholdingExample.tsx deleted file mode 100644 index 354d0e955..000000000 --- a/apps/wigsill-examples/examples/CameraThresholdingExample.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import * as dat from 'dat.gui'; -import { createRef, RefObject } from 'react'; - -import { useExampleWithCanvas } from '../common/useExampleWithCanvas'; - -function init(videoRef: RefObject) { - return async function (gui: dat.GUI, canvas: HTMLCanvasElement) { - const adapter = await navigator.gpu.requestAdapter(); - const device = await adapter!.requestDevice(); - - const shaderCode = ` -@group(0) @binding(0) var mySampler : sampler; -@group(0) @binding(1) var myTexture : texture_2d; -@group(0) @binding(2) var threshold : f32; - -struct VertexOutput { - @builtin(position) Position : vec4f, - @location(0) fragUV : vec2f, -} - -@vertex -fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { - const pos = array( - vec2( 1.0, 1.0), - vec2( 1.0, -1.0), - vec2(-1.0, -1.0), - vec2( 1.0, 1.0), - vec2(-1.0, -1.0), - vec2(-1.0, 1.0), - ); - - const uv = array( - vec2(1.0, 0.0), - vec2(1.0, 1.0), - vec2(0.0, 1.0), - vec2(1.0, 0.0), - vec2(0.0, 1.0), - vec2(0.0, 0.0), - ); - - var output : VertexOutput; - output.Position = vec4(pos[VertexIndex], 0.0, 1.0); - output.fragUV = uv[VertexIndex]; - return output; -} - -@fragment -fn frag_main(@location(0) fragUV : vec2f) -> @location(0) vec4f { - var color = textureSample(myTexture, mySampler, fragUV); - let grey = 0.299*color.r + 0.587*color.g + 0.114*color.b; - - if grey < threshold { - return vec4f(0, 0, 0, 1); - } - return vec4f(1); -} -`; - - if (navigator.mediaDevices.getUserMedia && videoRef.current) { - videoRef.current.srcObject = await navigator.mediaDevices.getUserMedia({ - video: true, - }); - } - - const context = canvas.getContext('webgpu') as GPUCanvasContext; - const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); - - context.configure({ - device, - format: presentationFormat, - alphaMode: 'premultiplied', - }); - - const renderPipeline = device.createRenderPipeline({ - layout: 'auto', - vertex: { - module: device.createShaderModule({ - code: shaderCode, - }), - }, - fragment: { - module: device.createShaderModule({ - code: shaderCode, - }), - targets: [ - { - format: presentationFormat, - }, - ], - }, - primitive: { - topology: 'triangle-list', - }, - }); - - const sampler = device.createSampler({ - magFilter: 'linear', - minFilter: 'linear', - }); - - const paramsBuffer = device.createBuffer({ - size: 4, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, - }); - - const defaultThreshold = 0.4; - device.queue.writeBuffer( - paramsBuffer, - 0, - new Float32Array([defaultThreshold]), - ); - - const resultTexture = device.createTexture({ - size: [canvas.width, canvas.height, 1], - format: 'rgba8unorm', - usage: - GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT, - }); - - const bindGroup = device.createBindGroup({ - layout: renderPipeline.getBindGroupLayout(0), - entries: [ - { - binding: 0, - resource: sampler, - }, - { - binding: 1, - resource: resultTexture.createView(), - }, - { - binding: 2, - resource: { - buffer: paramsBuffer, - }, - }, - ], - }); - - // UI - - const state = { - threshold: defaultThreshold, - }; - - gui.add(state, 'threshold', 0, 1, 0.1).onChange(() => { - device.queue.writeBuffer( - paramsBuffer, - 0, - new Float32Array([state.threshold]), - ); - }); - - let running = true; - - function frame() { - const commandEncoder = device.createCommandEncoder(); - - if (videoRef.current && videoRef.current.currentTime > 0) { - device.queue.copyExternalImageToTexture( - { source: videoRef.current }, - { texture: resultTexture }, - [canvas.width, canvas.height], - ); - } - - const passEncoder = commandEncoder.beginRenderPass({ - colorAttachments: [ - { - view: context.getCurrentTexture().createView(), - clearValue: [0, 0, 0, 1], - loadOp: 'clear', - storeOp: 'store', - }, - ], - }); - - passEncoder.setPipeline(renderPipeline); - passEncoder.setBindGroup(0, bindGroup); - passEncoder.draw(6); - passEncoder.end(); - device.queue.submit([commandEncoder.finish()]); - - if (running) { - requestAnimationFrame(frame); - } - } - - requestAnimationFrame(frame); - - return { - dispose() { - running = false; - }, - }; - }; -} - -export function CameraThresholdingExample() { - const videoRef: RefObject = createRef(); - const canvasRef = useExampleWithCanvas(init(videoRef)); - // const canvasRef = useExampleWithCanvas(useCallback(() => init(videoRef), [])); - const [width, height] = [500, 375]; - - return ( -
-
- -
- -
- -
-
- ); -} diff --git a/apps/wigsill-examples/examples/GradientTilesExample.tsx b/apps/wigsill-examples/examples/GradientTilesExample.tsx deleted file mode 100644 index 7c3074b32..000000000 --- a/apps/wigsill-examples/examples/GradientTilesExample.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import * as dat from 'dat.gui'; -import { WGSLRuntime } from 'wigsill'; -import { ProgramBuilder, makeArena, u32, wgsl } from 'wigsill'; - -import { useExampleWithCanvas } from '../common/useExampleWithCanvas'; - -async function init(gui: dat.GUI, canvas: HTMLCanvasElement) { - const adapter = await navigator.gpu.requestAdapter(); - const device = await adapter!.requestDevice(); - const runtime = new WGSLRuntime(device); - - const xSpanData = wgsl.memory(u32).alias('x-span'); - const ySpanData = wgsl.memory(u32).alias('y-span'); - - const mainArena = makeArena({ - bufferBindingType: 'uniform', - memoryEntries: [xSpanData, ySpanData], - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, - }); - - const context = canvas.getContext('webgpu') as GPUCanvasContext; - - const devicePixelRatio = window.devicePixelRatio; - canvas.width = canvas.clientWidth * devicePixelRatio; - canvas.height = canvas.clientHeight * devicePixelRatio; - const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); - - context.configure({ - device, - format: presentationFormat, - alphaMode: 'premultiplied', - }); - - const mainCode = wgsl` -struct VertexOutput { - @builtin(position) pos: vec4f, - @location(0) uv: vec2f, -} - -@vertex -fn main_vert( - @builtin(vertex_index) VertexIndex: u32 -) -> VertexOutput { - var pos = array( - vec2(0.5, 0.5), // top-right - vec2(-0.5, 0.5), // top-left - vec2(0.5, -0.5), // bottom-right - vec2(-0.5, -0.5) // bottom-left - ); - - var uv = array( - vec2(1., 1.), // top-right - vec2(0., 1.), // top-left - vec2(1., 0.), // bottom-right - vec2(0., 0.) // bottom-left - ); - - var output: VertexOutput; - output.pos = vec4f(pos[VertexIndex], 0.0, 1.0); - output.uv = uv[VertexIndex]; - return output; -} - -@fragment -fn main_frag( - @builtin(position) Position: vec4f, - @location(0) uv: vec2f, -) -> @location(0) vec4f { - let red = floor(uv.x * f32(${xSpanData})) / f32(${xSpanData}); - let green = floor(uv.y * f32(${ySpanData})) / f32(${ySpanData}); - return vec4(red, green, 0.5, 1.0); -} - `; - - const program = new ProgramBuilder(runtime, mainCode).build({ - bindingGroup: 0, - shaderStage: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, - arenas: [mainArena], - }); - - const shaderModule = device.createShaderModule({ - code: program.code, - }); - - const pipeline = device.createRenderPipeline({ - layout: device.createPipelineLayout({ - bindGroupLayouts: [program.bindGroupLayout], - }), - vertex: { - module: shaderModule, - entryPoint: 'main_vert', - }, - fragment: { - module: shaderModule, - entryPoint: 'main_frag', - targets: [ - { - format: presentationFormat, - }, - ], - }, - primitive: { - topology: 'triangle-strip', - }, - }); - - /// UI - - const state = { - xSpan: 16, - ySpan: 16, - }; - - xSpanData.write(runtime, state.xSpan); - ySpanData.write(runtime, state.ySpan); - - gui.add(state, 'xSpan', 1, 16).onChange(() => { - xSpanData.write(runtime, state.xSpan); - }); - gui.add(state, 'ySpan', 1, 16).onChange(() => { - ySpanData.write(runtime, state.ySpan); - }); - - let running = true; - - function frame() { - const commandEncoder = device.createCommandEncoder(); - const textureView = context.getCurrentTexture().createView(); - - const renderPassDescriptor: GPURenderPassDescriptor = { - colorAttachments: [ - { - view: textureView, - clearValue: [0, 0, 0, 1], - loadOp: 'clear', - storeOp: 'store', - }, - ], - }; - - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); - passEncoder.setPipeline(pipeline); - passEncoder.setBindGroup(0, program.bindGroup); - passEncoder.draw(4); - passEncoder.end(); - - device.queue.submit([commandEncoder.finish()]); - - if (running) { - requestAnimationFrame(frame); - } - } - - requestAnimationFrame(frame); - - return { - dispose() { - running = false; - }, - }; -} - -export function GradientTilesExample() { - const canvasRef = useExampleWithCanvas(init); - - return ; -} diff --git a/apps/wigsill-examples/examples/basic-triangle.js b/apps/wigsill-examples/examples/basic-triangle.js index 32ab31934..a246495c4 100644 --- a/apps/wigsill-examples/examples/basic-triangle.js +++ b/apps/wigsill-examples/examples/basic-triangle.js @@ -1,21 +1,140 @@ /* { - "title": "Basic Triangles" + "title": "Basic triangle" } */ -import { wgsl } from 'wigsill'; +import { makeArena, ProgramBuilder, u32, wgsl, WGSLRuntime } from 'wigsill'; import { addElement, onCleanup, onFrame } from '@wigsill/example-toolkit'; +// Layout +const video = await addElement('video'); const canvas = await addElement('canvas'); -console.log(canvas); -wgsl.fn()`() -> f32 { - return 1. + 2.; -}`; +const adapter = await navigator.gpu.requestAdapter(); +const device = await adapter.requestDevice(); +const runtime = new WGSLRuntime(device); + +const xSpanData = wgsl.memory(u32).alias('x-span'); +const ySpanData = wgsl.memory(u32).alias('y-span'); + +const mainArena = makeArena({ + bufferBindingType: 'uniform', + memoryEntries: [xSpanData, ySpanData], + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, +}); + +const context = canvas.getContext('webgpu'); + +const devicePixelRatio = window.devicePixelRatio; +canvas.width = canvas.clientWidth * devicePixelRatio; +canvas.height = canvas.clientHeight * devicePixelRatio; +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + +context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', +}); + +const mainCode = wgsl` +struct VertexOutput { + @builtin(position) pos: vec4f, + @location(0) uv: vec2f, +} + +@vertex +fn main_vert( + @builtin(vertex_index) VertexIndex: u32 +) -> VertexOutput { + var pos = array( + vec2(0.5, 0.5), // top-right + vec2(-0.5, 0.5), // top-left + vec2(0.5, -0.5), // bottom-right + vec2(-0.5, -0.5) // bottom-left + ); + + var uv = array( + vec2(1., 1.), // top-right + vec2(0., 1.), // top-left + vec2(1., 0.), // bottom-right + vec2(0., 0.) // bottom-left + ); + + var output: VertexOutput; + output.pos = vec4f(pos[VertexIndex], 0.0, 1.0); + output.uv = uv[VertexIndex]; + return output; +} + +@fragment +fn main_frag( + @builtin(position) Position: vec4f, + @location(0) uv: vec2f, +) -> @location(0) vec4f { + let red = floor(uv.x * f32(${xSpanData})) / f32(${xSpanData}); + let green = floor(uv.y * f32(${ySpanData})) / f32(${ySpanData}); + return vec4(red, green, 0.5, 1.0); +} + `; + +const program = new ProgramBuilder(runtime, mainCode).build({ + bindingGroup: 0, + shaderStage: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + arenas: [mainArena], +}); + +const shaderModule = device.createShaderModule({ + code: program.code, +}); + +const pipeline = device.createRenderPipeline({ + layout: device.createPipelineLayout({ + bindGroupLayouts: [program.bindGroupLayout], + }), + vertex: { + module: shaderModule, + entryPoint: 'main_vert', + }, + fragment: { + module: shaderModule, + entryPoint: 'main_frag', + targets: [ + { + format: presentationFormat, + }, + ], + }, + primitive: { + topology: 'triangle-strip', + }, +}); + +xSpanData.write(runtime, 16); +ySpanData.write(runtime, 16); onFrame(() => { - console.log('Hello from basic triangle!'); + const commandEncoder = device.createCommandEncoder(); + const textureView = context.getCurrentTexture().createView(); + + const renderPassDescriptor = { + colorAttachments: [ + { + view: textureView, + clearValue: [0, 0, 0, 1], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }; + + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(pipeline); + passEncoder.setBindGroup(0, program.bindGroup); + passEncoder.draw(4); + passEncoder.end(); + + device.queue.submit([commandEncoder.finish()]); }); onCleanup(() => { diff --git a/apps/wigsill-examples/examples/camera-thresholding.js b/apps/wigsill-examples/examples/camera-thresholding.js new file mode 100644 index 000000000..7e262fce5 --- /dev/null +++ b/apps/wigsill-examples/examples/camera-thresholding.js @@ -0,0 +1,185 @@ +/* +{ + "title": "Camera thresholding" +} +*/ + +import { makeArena, ProgramBuilder, u32, wgsl, WGSLRuntime } from 'wigsill'; +import { addElement, onCleanup, onFrame } from '@wigsill/example-toolkit'; + +// Layout +const video = await addElement('video'); +const canvas = await addElement('canvas'); + +const adapter = await navigator.gpu.requestAdapter(); +const device = await adapter.requestDevice(); + +const shaderCode = ` +@group(0) @binding(0) var mySampler : sampler; +@group(0) @binding(1) var myTexture : texture_2d; +@group(0) @binding(2) var threshold : f32; + +struct VertexOutput { +@builtin(position) Position : vec4f, +@location(0) fragUV : vec2f, +} + +@vertex +fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { +const pos = array( +vec2( 1.0, 1.0), +vec2( 1.0, -1.0), +vec2(-1.0, -1.0), +vec2( 1.0, 1.0), +vec2(-1.0, -1.0), +vec2(-1.0, 1.0), +); + +const uv = array( +vec2(1.0, 0.0), +vec2(1.0, 1.0), +vec2(0.0, 1.0), +vec2(1.0, 0.0), +vec2(0.0, 1.0), +vec2(0.0, 0.0), +); + +var output : VertexOutput; +output.Position = vec4(pos[VertexIndex], 0.0, 1.0); +output.fragUV = uv[VertexIndex]; +return output; +} + +@fragment +fn frag_main(@location(0) fragUV : vec2f) -> @location(0) vec4f { +var color = textureSample(myTexture, mySampler, fragUV); +let grey = 0.299*color.r + 0.587*color.g + 0.114*color.b; + +if grey < threshold { +return vec4f(0, 0, 0, 1); +} +return vec4f(1); +} +`; + +if (navigator.mediaDevices.getUserMedia) { + video.srcObject = await navigator.mediaDevices.getUserMedia({ + video: true, + }); +} + +const context = canvas.getContext('webgpu'); +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + +context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', +}); + +const renderPipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: device.createShaderModule({ + code: shaderCode, + }), + }, + fragment: { + module: device.createShaderModule({ + code: shaderCode, + }), + targets: [ + { + format: presentationFormat, + }, + ], + }, + primitive: { + topology: 'triangle-list', + }, +}); + +const sampler = device.createSampler({ + magFilter: 'linear', + minFilter: 'linear', +}); + +const paramsBuffer = device.createBuffer({ + size: 4, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, +}); + +const defaultThreshold = 0.4; +device.queue.writeBuffer(paramsBuffer, 0, new Float32Array([defaultThreshold])); + +const resultTexture = device.createTexture({ + size: [canvas.width, canvas.height, 1], + format: 'rgba8unorm', + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, +}); + +const bindGroup = device.createBindGroup({ + layout: renderPipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: sampler, + }, + { + binding: 1, + resource: resultTexture.createView(), + }, + { + binding: 2, + resource: { + buffer: paramsBuffer, + }, + }, + ], +}); + +// UI + +// const state = { +// threshold: defaultThreshold, +// }; + +// gui.add(state, 'threshold', 0, 1, 0.1).onChange(() => { +// device.queue.writeBuffer( +// paramsBuffer, +// 0, +// new Float32Array([state.threshold]), +// ); +// }); + +onFrame(() => { + const commandEncoder = device.createCommandEncoder(); + + if (video.currentTime > 0) { + device.queue.copyExternalImageToTexture( + { source: video }, + { texture: resultTexture }, + [canvas.width, canvas.height], + ); + } + + const passEncoder = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: context.getCurrentTexture().createView(), + clearValue: [0, 0, 0, 1], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + + passEncoder.setPipeline(renderPipeline); + passEncoder.setBindGroup(0, bindGroup); + passEncoder.draw(6); + passEncoder.end(); + device.queue.submit([commandEncoder.finish()]); +}); diff --git a/apps/wigsill-examples/src/common/Video.tsx b/apps/wigsill-examples/src/common/Video.tsx new file mode 100644 index 000000000..3195f3902 --- /dev/null +++ b/apps/wigsill-examples/src/common/Video.tsx @@ -0,0 +1,21 @@ +import { forwardRef, useImperativeHandle, useRef } from 'react'; + +type Props = { + width?: number; + height?: number; +}; + +export const Video = forwardRef((_props, ref) => { + const innerRef = useRef(null); + const containerRef = useRef(null); + + useImperativeHandle(ref, () => innerRef.current!); + + return ( +
+
+ ); +}); diff --git a/apps/wigsill-examples/src/example/ExampleView.tsx b/apps/wigsill-examples/src/example/ExampleView.tsx index d900f9dda..4d34d875a 100644 --- a/apps/wigsill-examples/src/example/ExampleView.tsx +++ b/apps/wigsill-examples/src/example/ExampleView.tsx @@ -9,6 +9,7 @@ import { ExampleState } from './exampleState'; import { executeExample } from './exampleRunner'; import { useLayout } from './layout'; import { Canvas } from '../common/Canvas'; +import { Video } from '../common/Video'; type Props = { example: Example; @@ -70,8 +71,16 @@ export function ExampleView({ example }: Props) { return ( <>
- {def.elements.map((_element, index) => { - return setRef(index, canvas)} />; + {def.elements.map((element, index) => { + if (element.type === 'canvas') { + return ( + setRef(index, canvas)} /> + ); + } else if (element.type === 'video') { + return
diff --git a/apps/wigsill-examples/src/example/layout.ts b/apps/wigsill-examples/src/example/layout.ts index e6f780e73..96084c2e2 100644 --- a/apps/wigsill-examples/src/example/layout.ts +++ b/apps/wigsill-examples/src/example/layout.ts @@ -7,13 +7,19 @@ export type CanvasDef = { height?: number; }; +export type VideoDef = { + type: 'video'; +}; + +export type ElementDef = CanvasDef | VideoDef; + export type LayoutDef = { - elements: CanvasDef[]; + elements: ElementDef[]; }; export type AddElement = ( - type: 'canvas', - options: Omit, + type: ElementDef['type'], + options: Omit, ) => Promise; /** @@ -36,7 +42,7 @@ export function useLayout(): { const instanceRef = useRef(null); const addElement: AddElement = useEvent( - (type: CanvasDef['type'], options: Omit) => { + (type: ElementDef['type'], options: Omit) => { if (!instanceRef.current) { // No instance is active. throw new Error(`No layout is active`); @@ -52,6 +58,14 @@ export function useLayout(): { return new Promise((resolve) => { elementResolves.current.set(index, resolve as () => void); }); + } else if (type === 'video') { + setDef({ + elements: [...def.elements, { ...options, type: 'video' }], + }); + + return new Promise((resolve) => { + elementResolves.current.set(index, resolve as () => void); + }); } else { throw new Error(`Tried to add unsupported layout element: ${type}`); } From 07d3479e1f3e9710c5a16d86fb7c6e42aa1c7e14 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Wed, 10 Jul 2024 16:42:35 +0200 Subject: [PATCH 09/14] Parametrized gradient tiles again. --- .../examples/basic-triangle.js | 142 ------------------ .../examples/camera-thresholding.js | 15 +- .../examples/gradient-tiles.js | 16 +- .../src/example/ExampleView.tsx | 15 +- .../src/example/exampleRunner.ts | 36 ++++- 5 files changed, 52 insertions(+), 172 deletions(-) delete mode 100644 apps/wigsill-examples/examples/basic-triangle.js diff --git a/apps/wigsill-examples/examples/basic-triangle.js b/apps/wigsill-examples/examples/basic-triangle.js deleted file mode 100644 index a246495c4..000000000 --- a/apps/wigsill-examples/examples/basic-triangle.js +++ /dev/null @@ -1,142 +0,0 @@ -/* -{ - "title": "Basic triangle" -} -*/ - -import { makeArena, ProgramBuilder, u32, wgsl, WGSLRuntime } from 'wigsill'; -import { addElement, onCleanup, onFrame } from '@wigsill/example-toolkit'; - -// Layout -const video = await addElement('video'); -const canvas = await addElement('canvas'); - -const adapter = await navigator.gpu.requestAdapter(); -const device = await adapter.requestDevice(); -const runtime = new WGSLRuntime(device); - -const xSpanData = wgsl.memory(u32).alias('x-span'); -const ySpanData = wgsl.memory(u32).alias('y-span'); - -const mainArena = makeArena({ - bufferBindingType: 'uniform', - memoryEntries: [xSpanData, ySpanData], - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, -}); - -const context = canvas.getContext('webgpu'); - -const devicePixelRatio = window.devicePixelRatio; -canvas.width = canvas.clientWidth * devicePixelRatio; -canvas.height = canvas.clientHeight * devicePixelRatio; -const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); - -context.configure({ - device, - format: presentationFormat, - alphaMode: 'premultiplied', -}); - -const mainCode = wgsl` -struct VertexOutput { - @builtin(position) pos: vec4f, - @location(0) uv: vec2f, -} - -@vertex -fn main_vert( - @builtin(vertex_index) VertexIndex: u32 -) -> VertexOutput { - var pos = array( - vec2(0.5, 0.5), // top-right - vec2(-0.5, 0.5), // top-left - vec2(0.5, -0.5), // bottom-right - vec2(-0.5, -0.5) // bottom-left - ); - - var uv = array( - vec2(1., 1.), // top-right - vec2(0., 1.), // top-left - vec2(1., 0.), // bottom-right - vec2(0., 0.) // bottom-left - ); - - var output: VertexOutput; - output.pos = vec4f(pos[VertexIndex], 0.0, 1.0); - output.uv = uv[VertexIndex]; - return output; -} - -@fragment -fn main_frag( - @builtin(position) Position: vec4f, - @location(0) uv: vec2f, -) -> @location(0) vec4f { - let red = floor(uv.x * f32(${xSpanData})) / f32(${xSpanData}); - let green = floor(uv.y * f32(${ySpanData})) / f32(${ySpanData}); - return vec4(red, green, 0.5, 1.0); -} - `; - -const program = new ProgramBuilder(runtime, mainCode).build({ - bindingGroup: 0, - shaderStage: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, - arenas: [mainArena], -}); - -const shaderModule = device.createShaderModule({ - code: program.code, -}); - -const pipeline = device.createRenderPipeline({ - layout: device.createPipelineLayout({ - bindGroupLayouts: [program.bindGroupLayout], - }), - vertex: { - module: shaderModule, - entryPoint: 'main_vert', - }, - fragment: { - module: shaderModule, - entryPoint: 'main_frag', - targets: [ - { - format: presentationFormat, - }, - ], - }, - primitive: { - topology: 'triangle-strip', - }, -}); - -xSpanData.write(runtime, 16); -ySpanData.write(runtime, 16); - -onFrame(() => { - const commandEncoder = device.createCommandEncoder(); - const textureView = context.getCurrentTexture().createView(); - - const renderPassDescriptor = { - colorAttachments: [ - { - view: textureView, - clearValue: [0, 0, 0, 1], - loadOp: 'clear', - storeOp: 'store', - }, - ], - }; - - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); - passEncoder.setPipeline(pipeline); - passEncoder.setBindGroup(0, program.bindGroup); - passEncoder.draw(4); - passEncoder.end(); - - device.queue.submit([commandEncoder.finish()]); -}); - -onCleanup(() => { - console.log(`All cleaned up`); -}); diff --git a/apps/wigsill-examples/examples/camera-thresholding.js b/apps/wigsill-examples/examples/camera-thresholding.js index 0527294b7..287464d2d 100644 --- a/apps/wigsill-examples/examples/camera-thresholding.js +++ b/apps/wigsill-examples/examples/camera-thresholding.js @@ -5,7 +5,7 @@ */ import { f32, makeArena, ProgramBuilder, wgsl, WGSLRuntime } from 'wigsill'; -import { addElement, onFrame } from '@wigsill/example-toolkit'; +import { addElement, addParameter, onFrame } from '@wigsill/example-toolkit'; // Layout const video = await addElement('video', { width: 500, height: 375 }); @@ -139,18 +139,11 @@ const sampler = device.createSampler({ minFilter: 'linear', }); -const defaultThreshold = 0.4; -thresholdData.write(runtime, defaultThreshold); - // UI -// const state = { -// threshold: defaultThreshold, -// }; - -// gui.add(state, 'threshold', 0, 1, 0.1).onChange(() => { -// thresholdData.write(runtime, state.threshold); -// }); +addParameter('threshold', { initial: 0.4, min: 0, max: 1 }, (threshold) => + thresholdData.write(runtime, threshold), +); onFrame(() => { if (!(video.currentTime > 0)) { diff --git a/apps/wigsill-examples/examples/gradient-tiles.js b/apps/wigsill-examples/examples/gradient-tiles.js index aaa9c3c20..4e0c0e126 100644 --- a/apps/wigsill-examples/examples/gradient-tiles.js +++ b/apps/wigsill-examples/examples/gradient-tiles.js @@ -5,7 +5,12 @@ */ import { makeArena, ProgramBuilder, u32, wgsl, WGSLRuntime } from 'wigsill'; -import { addElement, onCleanup, onFrame } from '@wigsill/example-toolkit'; +import { + addElement, + addParameter, + onCleanup, + onFrame, +} from '@wigsill/example-toolkit'; const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); @@ -108,8 +113,13 @@ const pipeline = device.createRenderPipeline({ }, }); -xSpanData.write(runtime, 16); -ySpanData.write(runtime, 16); +addParameter('x-span', { initial: 16, min: 1, max: 16, step: 1 }, (xSpan) => + xSpanData.write(runtime, xSpan), +); + +addParameter('y-span', { initial: 16, min: 1, max: 16, step: 1 }, (ySpan) => + ySpanData.write(runtime, ySpan), +); onFrame(() => { const commandEncoder = device.createCommandEncoder(); diff --git a/apps/wigsill-examples/src/example/ExampleView.tsx b/apps/wigsill-examples/src/example/ExampleView.tsx index 33fe4f8d4..675b95d95 100644 --- a/apps/wigsill-examples/src/example/ExampleView.tsx +++ b/apps/wigsill-examples/src/example/ExampleView.tsx @@ -1,4 +1,3 @@ -import { GUI } from 'dat.gui'; import { debounce } from 'remeda'; import { useMemo, useState, useEffect, useRef } from 'react'; @@ -17,17 +16,12 @@ type Props = { function useExample(exampleCode: string) { const exampleRef = useRef(null); - const { def, createLayout, dispose: deleteLayout, setRef } = useLayout(); + const { def, createLayout, setRef } = useLayout(); useEffect(() => { let cancelled = false; - const gui = new GUI({ closeOnTop: true }); - gui.hide(); - - const layout = createLayout(); - - executeExample(exampleCode, layout).then((example) => { + executeExample(exampleCode, createLayout).then((example) => { if (cancelled) { // Another instance was started in the meantime. example.dispose(); @@ -36,16 +30,13 @@ function useExample(exampleCode: string) { // Success exampleRef.current = example; - gui.show(); }); return () => { exampleRef.current?.dispose(); cancelled = true; - deleteLayout(); - gui.destroy(); }; - }, [exampleCode, createLayout, deleteLayout]); + }, [exampleCode, createLayout]); return { def, diff --git a/apps/wigsill-examples/src/example/exampleRunner.ts b/apps/wigsill-examples/src/example/exampleRunner.ts index 24c6cb5f5..f0837ca84 100644 --- a/apps/wigsill-examples/src/example/exampleRunner.ts +++ b/apps/wigsill-examples/src/example/exampleRunner.ts @@ -1,3 +1,4 @@ +import { GUI } from 'dat.gui'; import * as Babel from '@babel/standalone'; import { filter, isNonNull, map, pipe } from 'remeda'; import type { TraverseOptions } from '@babel/traverse'; @@ -5,7 +6,6 @@ import type TemplateGenerator from '@babel/template'; import { ExampleState } from './exampleState'; import { LayoutInstance } from './layout'; - // NOTE: @babel/standalone does expose internal packages, as specified in the docs, but the // typing for @babel/standalone does not expose them. const template = ( @@ -57,10 +57,34 @@ const staticToDynamicImports = { export async function executeExample( exampleCode: string, - layout: LayoutInstance, + createLayout: () => LayoutInstance, ): Promise { const cleanupCallbacks: (() => unknown)[] = []; + const layout = createLayout(); + const gui = new GUI({ closeOnTop: true }); + gui.hide(); + + function addParameter( + label: string, + options: { + initial: number; + min?: number; + max?: number; + step?: number; + }, + onChange: (newValue: number) => void, + ) { + const temp = { [label]: options.initial }; + + gui + .add(temp, label, options.min, options.max, options.step) + .onChange((value) => onChange(value)); + + // Eager run to initialize the values. + onChange(options.initial); + } + /** * Simulated imports from within the sandbox, making only a subset of * modules available. @@ -85,6 +109,7 @@ export async function executeExample( cleanupCallbacks.push(() => cancelAnimationFrame(handle)); }, addElement: layout.addElement, + addParameter, }; } throw new Error(`Module ${moduleKey} is not available in the sandbox.`); @@ -108,13 +133,16 @@ ${transformedCode} }; `); - const result: Promise = mod()(_import); + // Running the code + await mod()(_import); - console.log(await result); + gui.show(); return { dispose: () => { cleanupCallbacks.forEach((cb) => cb()); + layout.dispose(); + gui.destroy(); }, }; } From c1dbf2cc7c1c1a7dc46edf6c19aa2b7bd9b99922 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Wed, 10 Jul 2024 18:11:37 +0200 Subject: [PATCH 10/14] Better error handling. --- .../examples/camera-thresholding.js | 6 +- apps/wigsill-examples/src/common/Video.tsx | 24 ++-- .../src/example/ExampleView.tsx | 10 +- apps/wigsill-examples/src/example/errors.ts | 8 ++ .../src/example/exampleRunner.ts | 13 +- apps/wigsill-examples/src/example/layout.ts | 125 ++++++++++++------ 6 files changed, 119 insertions(+), 67 deletions(-) create mode 100644 apps/wigsill-examples/src/example/errors.ts diff --git a/apps/wigsill-examples/examples/camera-thresholding.js b/apps/wigsill-examples/examples/camera-thresholding.js index 287464d2d..a712fd0d5 100644 --- a/apps/wigsill-examples/examples/camera-thresholding.js +++ b/apps/wigsill-examples/examples/camera-thresholding.js @@ -8,8 +8,10 @@ import { f32, makeArena, ProgramBuilder, wgsl, WGSLRuntime } from 'wigsill'; import { addElement, addParameter, onFrame } from '@wigsill/example-toolkit'; // Layout -const video = await addElement('video', { width: 500, height: 375 }); -const canvas = await addElement('canvas', { width: 500, height: 375 }); +const [video, canvas] = await Promise.all([ + addElement('video', { width: 500, height: 375 }), + addElement('canvas', { width: 500, height: 375 }), +]); const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); diff --git a/apps/wigsill-examples/src/common/Video.tsx b/apps/wigsill-examples/src/common/Video.tsx index ed0d28c5a..42f22c8c9 100644 --- a/apps/wigsill-examples/src/common/Video.tsx +++ b/apps/wigsill-examples/src/common/Video.tsx @@ -1,5 +1,5 @@ import cs from 'classnames'; -import { forwardRef, useImperativeHandle, useRef } from 'react'; +import { forwardRef } from 'react'; type Props = { width?: number; @@ -9,26 +9,24 @@ type Props = { export const Video = forwardRef((props, ref) => { const { width, height } = props; - const innerRef = useRef(null); - const containerRef = useRef(null); - - useImperativeHandle(ref, () => innerRef.current!); - return (
+ 'relative overflow-hidden bg-black', + width && height ? 'flex-initial' : 'self-stretch flex-1', + )}>
); diff --git a/apps/wigsill-examples/src/example/ExampleView.tsx b/apps/wigsill-examples/src/example/ExampleView.tsx index 675b95d95..7163ec9ef 100644 --- a/apps/wigsill-examples/src/example/ExampleView.tsx +++ b/apps/wigsill-examples/src/example/ExampleView.tsx @@ -62,12 +62,12 @@ export function ExampleView({ example }: Props) { return ( <>
- {def.elements.map((element, index) => { + {def.elements.map((element) => { if (element.type === 'canvas') { return ( setRef(index, canvas)} + key={element.key} + ref={(canvas) => setRef(element.key, canvas)} width={element.width} height={element.height} /> @@ -75,8 +75,8 @@ export function ExampleView({ example }: Props) { } else if (element.type === 'video') { return (