Skip to content

docs: Refactor the translator and add TGSL mode #1529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/typegpu-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@astrojs/starlight": "^0.35.1",
"@astrojs/starlight-tailwind": "^4.0.1",
"@astrojs/tailwind": "^6.0.2",
"@babel/standalone": "^7.27.0",
"@loaders.gl/core": "^4.3.4",
"@loaders.gl/obj": "^4.3.4",
"@monaco-editor/react": "^4.7.0",
Expand Down
132 changes: 132 additions & 0 deletions apps/typegpu-docs/src/components/translator/TranslatorApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Editor } from '@monaco-editor/react';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useEffect, useId } from 'react';
import { TranslatorHeader } from './components/TranslatorHeader.tsx';
import { TRANSLATOR_MODES } from './lib/constants.ts';
import {
editableEditorOptions,
LANGUAGE_MAP,
readOnlyEditorOptions,
setupMonacoEditor,
} from './lib/editorConfig.ts';
import {
editorLoadingAtom,
formatAtom,
initializeAtom,
modeAtom,
outputAtom,
tgslCodeAtom,
wgslCodeAtom,
} from './lib/translatorStore.ts';
import { useAutoCompile } from './lib/useAutoCompile.ts';

interface EditorSectionProps {
id: string;
title: string;
language: string;
value: string;
onChange?: (value: string) => void;
readOnly?: boolean;
onMount?: () => void;
}

function EditorSection({
id,
title,
language,
value,
onChange,
readOnly,
onMount,
}: EditorSectionProps) {
return (
<section className='flex min-h-[24rem] flex-col lg:min-h-0'>
<h2
id={id}
className='mb-2 font-medium text-gray-300 text-sm'
>
{title}
</h2>
<Editor
language={language}
value={value}
onChange={onChange ? (v) => onChange(v || '') : undefined}
theme='vs-dark'
beforeMount={language === 'typescript' ? setupMonacoEditor : undefined}
onMount={onMount}
loading={
<div className='flex items-center justify-center text-gray-400'>
Loading editor...
</div>
}
options={readOnly ? readOnlyEditorOptions : editableEditorOptions}
/>
</section>
);
}

export default function TranslatorApp() {
const mode = useAtomValue(modeAtom);
const format = useAtomValue(formatAtom);
const output = useAtomValue(outputAtom);
const [tgslCode, setTgslCode] = useAtom(tgslCodeAtom);
const [wgslCode, setWgslCode] = useAtom(wgslCodeAtom);
const setEditorLoaded = useSetAtom(editorLoadingAtom);
const initialize = useSetAtom(initializeAtom);

const tgslInputLabelId = useId();
const wgslInputLabelId = useId();
const compiledOutputLabelId = useId();

useAutoCompile();

useEffect(() => {
initialize();
}, [initialize]);

const handleEditorLoaded = () => setEditorLoaded(false);

const isTgslMode = mode === TRANSLATOR_MODES.TGSL;
const gridCols = isTgslMode
? 'grid-cols-1 lg:grid-cols-3'
: 'grid-cols-1 lg:grid-cols-2';

return (
<div className='flex flex-1 flex-col overflow-hidden'>
<TranslatorHeader />

<main
className={`grid flex-1 gap-4 overflow-y-auto p-4 ${gridCols}`}
>
{isTgslMode && (
<EditorSection
id={tgslInputLabelId}
title='TypeScript Shader Code (TGSL):'
language='typescript'
value={tgslCode}
onChange={setTgslCode}
onMount={handleEditorLoaded}
/>
)}

<EditorSection
id={wgslInputLabelId}
title={isTgslMode ? 'Generated WGSL:' : 'WGSL Shader Code:'}
language='wgsl'
value={wgslCode}
onChange={!isTgslMode ? setWgslCode : undefined}
readOnly={isTgslMode}
onMount={!isTgslMode ? handleEditorLoaded : undefined}
/>

<EditorSection
id={compiledOutputLabelId}
title={`Compiled Output (${format.toUpperCase()}):`}
language={LANGUAGE_MAP[format] || 'plaintext'}
value={output || '// Compiled output will appear here...'}
readOnly
/>
</main>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { useId } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import {
canCompileAtom,
canConvertTgslAtom,
clearOutputOnFormatChangeAtom,
clearOutputOnModeChangeAtom,
compileAtom,
convertTgslToWgslAtom,
formatAtom,
formatsAtom,
modeAtom,
statusAtom,
} from '../lib/translatorStore.ts';
import { TRANSLATOR_MODES, type TranslatorMode } from '../lib/constants.ts';

const STATUS_CONFIG = {
initializing: { color: 'text-violet-400', text: 'Initializing…' },
ready: { color: 'text-emerald-400', text: 'Ready to compile!' },
compiling: { color: 'text-violet-400', text: 'Compiling…' },
success: { color: 'text-emerald-400', text: 'Compilation successful!' },
error: { color: 'text-red-400', text: '' },
} as const;

export function TranslatorHeader() {
const mode = useAtomValue(modeAtom);
const format = useAtomValue(formatAtom);
const { state: status, error: errorMessage } = useAtomValue(statusAtom);
const formats = useAtomValue(formatsAtom);
const canCompile = useAtomValue(canCompileAtom);
const canConvertTgsl = useAtomValue(canConvertTgslAtom);

const setMode = useSetAtom(clearOutputOnModeChangeAtom);
const setFormat = useSetAtom(clearOutputOnFormatChangeAtom);
const handleTgslToWgsl = useSetAtom(convertTgslToWgslAtom);
const handleCompile = useSetAtom(compileAtom);

const modeSelectId = useId();
const formatSelectId = useId();

const statusConfig = STATUS_CONFIG[status];
const statusText = status === 'error'
? errorMessage || 'An unexpected error happened.'
: statusConfig.text;

return (
<header className='border-b px-2 pb-2'>
<div className='mb-4 grid grid-cols-[1fr_auto] items-center gap-4'>
<h1 className='font-bold text-xl'>
{mode === TRANSLATOR_MODES.TGSL ? 'TGSL' : 'WGSL'} Translator
</h1>

<div className='rounded border bg-gray-800 p-1'>
<div
className={`max-h-20 overflow-y-auto text-sm ${statusConfig.color}`}
>
{statusText}
</div>
</div>
</div>

<div className='grid min-w-3xs grid-cols-2 items-center gap-4 sm:grid-cols-[auto_auto_1fr_auto]'>
<div className='grid grid-cols-[auto_1fr] items-center gap-2'>
<label htmlFor={modeSelectId} className='text-sm'>
Mode:
</label>
<select
id={modeSelectId}
value={mode}
onChange={(e) => setMode(e.target.value as TranslatorMode)}
className='rounded border bg-gray-700 p-2 text-white'
>
<option value={TRANSLATOR_MODES.WGSL}>WGSL</option>
<option value={TRANSLATOR_MODES.TGSL}>TGSL</option>
</select>
</div>

<div className='grid grid-cols-[auto_1fr] items-center gap-2'>
<label htmlFor={formatSelectId} className='text-sm'>
Target:
</label>
<select
id={formatSelectId}
value={format}
onChange={(e) => setFormat(e.target.value)}
disabled={!formats.length}
className='rounded border bg-gray-700 p-2 text-white disabled:opacity-50'
title={!formats.length
? 'Loading available formats...'
: 'Select target format'}
>
{formats.map((f) => (
<option key={f} value={f}>
{f.toUpperCase()}
</option>
))}
</select>
</div>

<div className='col-span-2 grid grid-flow-col gap-2 sm:col-span-1 sm:col-start-4'>
{mode === TRANSLATOR_MODES.TGSL && (
<button
type='button'
onClick={() => handleTgslToWgsl()}
disabled={!canConvertTgsl}
className='rounded bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-500 disabled:opacity-50'
>
{status === 'compiling' ? 'Converting…' : 'Convert'}
</button>
)}

<button
type='button'
onClick={() => handleCompile()}
disabled={!canCompile}
className='rounded bg-purple-600 px-4 py-2 text-white hover:bg-purple-500 disabled:opacity-50'
>
{status === 'compiling' ? 'Compiling…' : 'Compile Now'}
</button>
</div>
</div>
</header>
);
}
69 changes: 69 additions & 0 deletions apps/typegpu-docs/src/components/translator/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export const TRANSLATOR_MODES = {
WGSL: 'wgsl',
TGSL: 'tgsl',
} as const;

export type TranslatorMode =
typeof TRANSLATOR_MODES[keyof typeof TRANSLATOR_MODES];

export const DEFAULT_WGSL = `@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {
let pos = array<vec2<f32>, 3>(
vec2<f32>(-0.5, -0.5),
vec2<f32>( 0.5, -0.5),
vec2<f32>( 0.0, 0.5)
);
return vec4<f32>(pos[vertex_index], 0.0, 1.0);
}

@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}`;

export const DEFAULT_TGSL = `import tgpu from 'typegpu';
import * as d from 'typegpu/data';
import * as std from 'typegpu/std';

const Particle = d.struct({
position: d.vec3f,
velocity: d.vec3f,
});

const SystemData = d.struct({
particles: d.arrayOf(Particle, 100),
gravity: d.vec3f,
deltaTime: d.f32,
});

const layout = tgpu.bindGroupLayout({
systemData: { storage: SystemData },
});

export const updateParicle = tgpu.fn([Particle, d.vec3f, d.f32], Particle)(
(particle, gravity, deltaTime) => {
const newVelocity = std.mul(
particle.velocity,
std.mul(gravity, deltaTime),
);
const newPosition = std.add(
particle.position,
std.mul(newVelocity, deltaTime),
);
return Particle({
position: newPosition,
velocity: newVelocity,
});
},
);

export const main = tgpu.fn([])(() => {
for (let i = 0; i < layout.$.systemData.particles.length; i++) {
const particle = layout.$.systemData.particles[i];
layout.$.systemData.particles[i] = updateParicle(
particle,
layout.$.systemData.gravity,
layout.$.systemData.deltaTime,
);
}
});`;
Loading