diff --git a/lib/modules-lib/src/types/index.ts b/lib/modules-lib/src/types/index.ts index 82bea25336..3e593c6e55 100644 --- a/lib/modules-lib/src/types/index.ts +++ b/lib/modules-lib/src/types/index.ts @@ -72,4 +72,7 @@ export interface ModuleSideContent { * on Source Academy frontend. */ body: (context: DebuggerContext) => JSX.Element; + + serialize: (context: DebuggerContext) => DebuggerContext; + deserialize: (context: DebuggerContext) => DebuggerContext; }; diff --git a/src/bundles/rune/src/index.ts b/src/bundles/rune/src/index.ts index 4db0c66d23..4594e51d6e 100644 --- a/src/bundles/rune/src/index.ts +++ b/src/bundles/rune/src/index.ts @@ -61,3 +61,11 @@ export { hollusion_magnitude, show } from './display'; + + +export { + serializeRune, + serializeDrawnRune, + deserializeRune, + deserializeDrawnRune +} from './rune' \ No newline at end of file diff --git a/src/bundles/rune/src/rune.ts b/src/bundles/rune/src/rune.ts index 77426d7890..ec6530df6d 100644 --- a/src/bundles/rune/src/rune.ts +++ b/src/bundles/rune/src/rune.ts @@ -419,3 +419,135 @@ export class AnimatedRune extends glAnimation implements ReplResult { public toReplString = () => ''; } + +export type SerializedRune = { + vertices: Float32Array; + colors: Float32Array | null; + transformMatrix: mat4; + hollusionDistance: number; + texture?: null; +}; + +export function serializeRune(rune: Rune): SerializedRune { + return { + vertices: rune.vertices, + colors: rune.colors, + transformMatrix: rune.transformMatrix, + hollusionDistance: rune.hollusionDistance, + texture: null + } as SerializedRune; +} + +export function deserializeRune(serializedRune: SerializedRune): Rune { + const vertices = serializedRune.vertices; + const colors = serializedRune.colors; + const transformMatrix = serializedRune.transformMatrix; + const hollusionDistance = serializedRune.hollusionDistance; + const texture = serializedRune.texture || null; + + return Rune.of({ + vertices, + colors, + transformMatrix, + hollusionDistance, + texture + }); +} + +export type SerializedDrawnRune = + | { + kind: 'normal'; + rune: SerializedRune; + isHollusion: boolean; + } + | { + kind: 'animated'; + duration: number; + fps: number; + frames: SerializedDrawnRune[]; + }; + +/** + * Serialize a DrawnRune (NormalRune) or AnimatedRune to a plain data structure. + * - Normal: serialize the contained Rune + * - Animated: precompute each frame using the animation function and serialize each frame + */ +export function serializeDrawnRune(drawn: DrawnRune | AnimatedRune): SerializedDrawnRune { + // AnimatedRune is a subclass of glAnimation and defined below in this file + if (drawn instanceof AnimatedRune) { + const duration = drawn.duration; + const fps = drawn.fps; + const totalFrames = Math.max(1, Math.round(duration * fps)); + + // Access the private func stored on AnimatedRune instance. Use any to bypass TS visibility. + const func = (drawn as any).func as (frame: number) => DrawnRune; + + const frames: SerializedDrawnRune[] = []; + for (let i = 0; i < totalFrames; i++) { + const frameDrawn = func(i); + frames.push(serializeDrawnRune(frameDrawn)); + } + + return { + kind: 'animated', + duration, + fps, + frames + }; + } + + // Normal case: any DrawnRune that is not AnimatedRune - serialize its underlying Rune + const innerRune: Rune = (drawn as any).rune as Rune; + const isHollusion = (drawn as any).isHollusion as boolean; + + return { + kind: 'normal', + rune: serializeRune(innerRune), + isHollusion + }; +} + +/** + * Deserialize a SerializedDrawnRune back into a DrawnRune (NormalRune or AnimatedRune). + */ +export function deserializeDrawnRune(serialized: SerializedDrawnRune): DrawnRune | AnimatedRune { + if (serialized.kind === 'normal') { + const rune = deserializeRune(serialized.rune); + + // Create a small subclass instance to preserve isHollusion flag and draw behaviour + class RehydratedNormalRune extends DrawnRune { + constructor(r: Rune, isH: boolean) { + super(r, isH); + } + + public draw = (canvas: HTMLCanvasElement) => { + const gl = getWebGlFromCanvas(canvas); + const cameraMatrix = mat4.create(); + drawRunesToFrameBuffer( + gl, + this.rune.flatten(), + cameraMatrix, + new Float32Array([1, 1, 1, 1]), + null, + true + ); + }; + } + + return new RehydratedNormalRune(rune, serialized.isHollusion); + } + + // animated + const { duration, fps, frames } = serialized; + + const func = (frame: number) => { + const frameIndex = frame % frames.length; + const des = deserializeDrawnRune(frames[frameIndex]); + if (des instanceof AnimatedRune) { + throw new Error('Nested animated frames are not supported when deserializing an AnimatedRune'); + } + return des as DrawnRune; + }; + + return new AnimatedRune(duration, fps, func); +} \ No newline at end of file diff --git a/src/tabs/Rune/src/index.tsx b/src/tabs/Rune/src/index.tsx index 4833340b86..7785b1afdd 100644 --- a/src/tabs/Rune/src/index.tsx +++ b/src/tabs/Rune/src/index.tsx @@ -5,10 +5,15 @@ import WebGLCanvas from '@sourceacademy/modules-lib/tabs/WebGLCanvas'; import { defineTab, getModuleState } from '@sourceacademy/modules-lib/tabs/utils'; import { glAnimation, type ModuleTab } from '@sourceacademy/modules-lib/types'; import HollusionCanvas from './hollusion_canvas'; +import { serializeDrawnRune, deserializeDrawnRune } from '@sourceacademy/bundle-rune/rune'; export const RuneTab: ModuleTab = ({ context }) => { - const { drawnRunes } = getModuleState(context, 'rune'); - const runeCanvases = drawnRunes.map((rune, i) => { + // const { drawnRunes } = getModuleState(context, 'rune'); + const deserializedDrawnRunes = context.context.moduleContexts.rune.state.deserializedDrawnRunes + console.log("deserializedDrawnRune inside Tab Render"); + console.log(deserializedDrawnRunes); + + const runeCanvases = deserializedDrawnRunes.map((rune, i) => { const elemKey = i.toString(); if (glAnimation.isAnimation(rune)) { @@ -33,9 +38,19 @@ export const RuneTab: ModuleTab = ({ context }) => { }; export default defineTab({ - toSpawn(context) { - const drawnRunes = context.context?.moduleContexts?.rune?.state?.drawnRunes; - return drawnRunes.length > 0; + serialize(debuggerContext) { + const drawnRunes = debuggerContext.context?.moduleContexts?.rune?.state?.drawnRunes ?? []; + debuggerContext.context.moduleContexts.rune.state.serializedDrawnRunes = drawnRunes.map((r: any) => serializeDrawnRune(r)); + return debuggerContext; + }, + deserialize(debuggerContext) { + const serializedDrawnRunes = debuggerContext.context?.moduleContexts?.rune?.state?.serializedDrawnRunes ?? []; + debuggerContext.context.moduleContexts.rune.state.deserializedDrawnRunes = serializedDrawnRunes.map((s: any) => deserializeDrawnRune(s)); + return debuggerContext; + }, + toSpawn(debuggerContext) { + const serializedDrawnRunes = debuggerContext.context?.moduleContexts?.rune?.state?.serializedDrawnRunes ?? []; + return serializedDrawnRunes.length > 0; }, body(context) { return ;