From d9378c64fbb604e21f01cdc8a04b473587bfd090 Mon Sep 17 00:00:00 2001 From: Sourabh Jaiswal Date: Tue, 3 Oct 2023 10:09:27 +0800 Subject: [PATCH 1/7] add curves --- src/bundles/plotly/curve_functions.ts | 93 +++++++++-------- src/bundles/plotly/functions.ts | 140 ++++++++++++++------------ src/bundles/plotly/index.ts | 14 +-- src/tabs/Plotly/index.tsx | 2 +- 4 files changed, 136 insertions(+), 113 deletions(-) diff --git a/src/bundles/plotly/curve_functions.ts b/src/bundles/plotly/curve_functions.ts index 1f1d8286e..cc707a3b4 100644 --- a/src/bundles/plotly/curve_functions.ts +++ b/src/bundles/plotly/curve_functions.ts @@ -1,8 +1,8 @@ -import Plotly, { type Data, type Layout } from 'plotly.js-dist'; -import { type Curve, CurvePlot, type Point } from './plotly'; +import Plotly, { type Data, type Layout } from "plotly.js-dist"; +import { type Curve, CurvePlot, type Point } from "./plotly"; export function x_of(pt: Point): number { - return pt.x; + return pt.x; } /** @@ -17,7 +17,7 @@ export function x_of(pt: Point): number { * ``` */ export function y_of(pt: Point): number { - return pt.y; + return pt.y; } /** @@ -32,7 +32,7 @@ export function y_of(pt: Point): number { * ``` */ export function z_of(pt: Point): number { - return pt.z; + return pt.z; } /** @@ -47,7 +47,7 @@ export function z_of(pt: Point): number { * ``` */ export function r_of(pt: Point): number { - return pt.color.r * 255; + return pt.color[0] ?? 0 * 255; } /** @@ -62,7 +62,7 @@ export function r_of(pt: Point): number { * ``` */ export function g_of(pt: Point): number { - return pt.color.g * 255; + return pt.color[1] ?? 0 * 255; } /** @@ -77,47 +77,52 @@ export function g_of(pt: Point): number { * ``` */ export function b_of(pt: Point): number { - return pt.color.b * 255; + return pt.color[2] ?? 0 * 255; } export function generatePlot( - type: string, - numPoints: number, - config: Data, - layout: Partial, - is_colored: boolean, - func: Curve, + type: string, + numPoints: number, + config: Data, + layout: Partial, + is_colored: boolean, + func: Curve ): CurvePlot { - let x_s: number[] = []; - let y_s: number[] = []; - let z_s: number[] = []; - let color_s: string[] = []; - for (let i = 0; i <= numPoints; i += 1) { - const point = func(i / numPoints); - x_s.push(x_of(point)); - y_s.push(y_of(point)); - z_s.push(z_of(point)); - color_s.push(`rgb(${r_of(point)},${g_of(point)},${b_of(point)})`); - } - const plotlyData: Data = { - x: x_s, - y: y_s, - z: z_s, - marker: { - size: 2, - color: color_s, - }, - }; - return new CurvePlot( - draw_new_curve, - { - ...plotlyData, - ...config, - type, - } as Data, - layout, - ); + let x_s: number[] = []; + let y_s: number[] = []; + let z_s: number[] = []; + let color_s: string[] = []; + for (let i = 0; i <= numPoints; i += 1) { + const point = func(i / numPoints); + x_s.push(x_of(point)); + y_s.push(y_of(point)); + z_s.push(z_of(point)); + color_s.push(`rgb(${r_of(point)},${g_of(point)},${b_of(point)})`); + } + + + const plotlyData: Data = { + x: x_s, + y: y_s, + z: z_s, + marker: { + size: 2, + color: color_s, + }, + line: { + color: color_s, + }, + }; + return new CurvePlot( + draw_new_curve, + { + ...plotlyData, + ...config, + type, + } as Data, + layout + ); } function draw_new_curve(divId: string, data: Data, layout: Partial) { - Plotly.newPlot(divId, [data], layout); + Plotly.newPlot(divId, [data], layout); } diff --git a/src/bundles/plotly/functions.ts b/src/bundles/plotly/functions.ts index 3f3d10fd3..758d6c75d 100644 --- a/src/bundles/plotly/functions.ts +++ b/src/bundles/plotly/functions.ts @@ -3,20 +3,20 @@ * @module plotly */ -import context from 'js-slang/context'; -import Plotly, { type Data, type Layout } from 'plotly.js-dist'; +import context from "js-slang/context"; +import Plotly, { type Data, type Layout } from "plotly.js-dist"; import { - type Curve, - type CurvePlot, - type CurvePlotFunction, - DrawnPlot, - type ListOfPairs, -} from './plotly'; -import { generatePlot } from './curve_functions'; + type Curve, + type CurvePlot, + type CurvePlotFunction, + DrawnPlot, + type ListOfPairs, +} from "./plotly"; +import { generatePlot } from "./curve_functions"; const drawnPlots: (DrawnPlot | CurvePlot)[] = []; context.moduleContexts.plotly.state = { - drawnPlots, + drawnPlots, }; /** @@ -118,10 +118,9 @@ context.moduleContexts.plotly.state = { * among the fields mentioned above */ export function new_plot(data: ListOfPairs): void { - drawnPlots.push(new DrawnPlot(draw_new_plot, data)); + drawnPlots.push(new DrawnPlot(draw_new_plot, data)); } - /** * Adds a new plotly plot to the context which will be rendered in the Plotly Tabs * @example @@ -233,17 +232,16 @@ export function new_plot(data: ListOfPairs): void { * @param data The data as an array of json objects having some or all of the given fields */ export function new_plot_json(data: any): void { - drawnPlots.push(new DrawnPlot(draw_new_plot_json, data)); + drawnPlots.push(new DrawnPlot(draw_new_plot_json, data)); } - /** * @param data The data which plotly will use * @param divId The id of the div element on which the plot will be displayed */ function draw_new_plot(data: ListOfPairs, divId: string) { - const plotlyData = convert_to_plotly_data(data); - Plotly.newPlot(divId, [plotlyData]); + const plotlyData = convert_to_plotly_data(data); + Plotly.newPlot(divId, [plotlyData]); } /** @@ -252,7 +250,7 @@ function draw_new_plot(data: ListOfPairs, divId: string) { * @param divId The id of the div element on which the plot will be displayed */ function draw_new_plot_json(data: any, divId: string) { - Plotly.newPlot(divId, data); + Plotly.newPlot(divId, data); } /** @@ -260,11 +258,11 @@ function draw_new_plot_json(data: any, divId: string) { * @returns The converted data that can be used by the plotly.js function */ function convert_to_plotly_data(data: ListOfPairs): Data { - let convertedData: Data = {}; - if (Array.isArray(data) && data.length === 2) { - add_fields_to_data(convertedData, data); - } - return convertedData; + let convertedData: Data = {}; + if (Array.isArray(data) && data.length === 2) { + add_fields_to_data(convertedData, data); + } + return convertedData; } /** @@ -273,37 +271,37 @@ function convert_to_plotly_data(data: ListOfPairs): Data { */ function add_fields_to_data(convertedData: Data, data: ListOfPairs) { - if (Array.isArray(data) && data.length === 2 && data[0].length === 2) { - const field = data[0][0]; - const value = data[0][1]; - convertedData[field] = value; - add_fields_to_data(convertedData, data[1]); - } + if (Array.isArray(data) && data.length === 2 && data[0].length === 2) { + const field = data[0][0]; + const value = data[0][1]; + convertedData[field] = value; + add_fields_to_data(convertedData, data[1]); + } } function createPlotFunction( - type: string, - config: Data, - layout: Partial, - is_colored: boolean = false, + type: string, + config: Data, + layout: Partial, + is_colored: boolean = false ): (numPoints: number) => CurvePlotFunction { - return (numPoints: number) => { - const func = (curveFunction: Curve) => { - const plotDrawn = generatePlot( - type, - numPoints, - config, - layout, - is_colored, - curveFunction, - ); + return (numPoints: number) => { + const func = (curveFunction: Curve) => { + const plotDrawn = generatePlot( + type, + numPoints, + config, + layout, + is_colored, + curveFunction + ); - drawnPlots.push(plotDrawn); - return plotDrawn; - }; + drawnPlots.push(plotDrawn); + return plotDrawn; + }; - return func; - }; + return func; + }; } /** @@ -320,23 +318,41 @@ function createPlotFunction( * ``` */ export const draw_connected_2d = createPlotFunction( - 'scatter', - { mode: 'lines' }, - { - xaxis: { visible: false }, - yaxis: { - visible: false, - scaleanchor: 'x', + "scattergl", + { + mode: "lines", }, - }, - + { + xaxis: { visible: false }, + yaxis: { + visible: false, + scaleanchor: "x", + }, + }, + true ); -export const draw_3D_points = createPlotFunction( - 'scatter3d', - { mode: 'markers' }, - { +export const draw_connected_3d = createPlotFunction( + "scatter3d", + { mode: "lines" }, + {}, + true +); - }, - true, +export const draw_points_2d = createPlotFunction( + "scatter", + { mode: "markers" }, + { + xaxis: { visible: false }, + yaxis: { + visible: false, + scaleanchor: "x", + }, + }, + true +); +export const draw_points_3d = createPlotFunction( + "scatter3d", + { mode: "markers" }, + {} ); diff --git a/src/bundles/plotly/index.ts b/src/bundles/plotly/index.ts index d3e2b1a3c..51473df3c 100644 --- a/src/bundles/plotly/index.ts +++ b/src/bundles/plotly/index.ts @@ -4,10 +4,12 @@ */ export { - new_plot, - new_plot_json, - draw_connected_2d, - draw_3D_points, -} from './functions'; + new_plot, + new_plot_json, + draw_connected_2d, + draw_connected_3d, + draw_points_2d, + draw_points_3d, +} from "./functions"; -export { draw_sound_2d } from './sound_functions'; +// export { draw_sound_2d } from './sound_functions'; diff --git a/src/tabs/Plotly/index.tsx b/src/tabs/Plotly/index.tsx index cd8424efc..17e6b6919 100644 --- a/src/tabs/Plotly/index.tsx +++ b/src/tabs/Plotly/index.tsx @@ -55,7 +55,7 @@ class Plotly extends React.Component { drawnPlots.map((drawnPlot: any, id:number) => { const divId = `plotDiv${id}`; return ( -
+
this.handleOpen(drawnPlot)}>Click here to open Modal
Date: Tue, 17 Oct 2023 01:26:47 +0800 Subject: [PATCH 2/7] fix draw_sound_2d --- src/bundles/plotly/curve_functions.ts | 3 +- src/bundles/plotly/functions.ts | 118 +++++++++++++++++++++++++- src/bundles/plotly/index.ts | 3 +- src/bundles/plotly/plotly.ts | 82 +++++++++--------- src/bundles/plotly/sound_functions.ts | 106 +++++++---------------- 5 files changed, 188 insertions(+), 124 deletions(-) diff --git a/src/bundles/plotly/curve_functions.ts b/src/bundles/plotly/curve_functions.ts index cc707a3b4..00f711c09 100644 --- a/src/bundles/plotly/curve_functions.ts +++ b/src/bundles/plotly/curve_functions.ts @@ -99,7 +99,6 @@ export function generatePlot( color_s.push(`rgb(${r_of(point)},${g_of(point)},${b_of(point)})`); } - const plotlyData: Data = { x: x_s, y: y_s, @@ -124,5 +123,5 @@ export function generatePlot( } function draw_new_curve(divId: string, data: Data, layout: Partial) { - Plotly.newPlot(divId, [data], layout); + Plotly.react(divId, [data], layout); } diff --git a/src/bundles/plotly/functions.ts b/src/bundles/plotly/functions.ts index 758d6c75d..7c99c4021 100644 --- a/src/bundles/plotly/functions.ts +++ b/src/bundles/plotly/functions.ts @@ -7,14 +7,17 @@ import context from "js-slang/context"; import Plotly, { type Data, type Layout } from "plotly.js-dist"; import { type Curve, - type CurvePlot, + CurvePlot, type CurvePlotFunction, DrawnPlot, type ListOfPairs, + type Sound, } from "./plotly"; import { generatePlot } from "./curve_functions"; +import { get_duration, get_wave, is_sound } from "./sound_functions"; + +let drawnPlots: (DrawnPlot | CurvePlot)[] = []; -const drawnPlots: (DrawnPlot | CurvePlot)[] = []; context.moduleContexts.plotly.state = { drawnPlots, }; @@ -307,14 +310,13 @@ function createPlotFunction( /** * Returns a function that turns a given Curve into a Drawing, by sampling the * Curve at `num` sample points and connecting each pair with a line. - * The parts between (0,0) and (1,1) of the resulting Drawing are shown in the window. * * @param num determines the number of points, lower than 65535, to be sampled. * Including 0 and 1, there are `num + 1` evenly spaced sample points * @return function of type Curve → Drawing * @example * ``` - * draw_connected(100)(t => make_point(t, t)); + * draw_connected_2d(100)(t => make_point(t, t)); * ``` */ export const draw_connected_2d = createPlotFunction( @@ -332,6 +334,18 @@ export const draw_connected_2d = createPlotFunction( true ); +/** + * Returns a function that turns a given 3D Curve into a Drawing, by sampling the + * 3D Curve at `num` sample points and connecting each pair with a line. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type 3D Curve → Drawing + * @example + * ``` + * draw_connected_3d(100)(t => make_point(t, t)); + * ``` + */ export const draw_connected_3d = createPlotFunction( "scatter3d", { mode: "lines" }, @@ -339,6 +353,18 @@ export const draw_connected_3d = createPlotFunction( true ); +/** + * Returns a function that turns a given Curve into a Drawing, by sampling the + * Curve at num sample points. The Drawing consists of isolated points, and does not connect them. + * When a program evaluates to a Drawing, the Source system displays it graphically, in a window, + * + * * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type 2D Curve → Drawing + * @example + * ``` + * draw_points_2d(100)(t => make_point(t, t)); + */ export const draw_points_2d = createPlotFunction( "scatter", { mode: "markers" }, @@ -351,8 +377,92 @@ export const draw_points_2d = createPlotFunction( }, true ); + +/** + * Returns a function that turns a given 3D Curve into a Drawing, by sampling the + * 3D Curve at num sample points. The Drawing consists of isolated points, and does not connect them. + * When a program evaluates to a Drawing, the Source system displays it graphically, in a window, + * + * * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type 3D Curve → Drawing + * @example + * ``` + * draw_points_3d(100)(t => make_point(t, t)); + */ export const draw_points_3d = createPlotFunction( "scatter3d", { mode: "markers" }, {} ); + +/** + * Visualizes the sound on a 2d line graph + * @param sound the sound which is to be visualized on plotly + */ +export const draw_sound_2d = (sound: Sound) => { + const FS: number = 44100; // Output sample rate + if (!is_sound(sound)) { + throw new Error( + `draw_sound_2d is expecting sound, but encountered ${sound}` + ); + // If a sound is already displayed, terminate execution. + } else if (get_duration(sound) < 0) { + throw new Error("draw_sound_2d: duration of sound is negative"); + } else { + // Instantiate audio context if it has not been instantiated. + + // Create mono buffer + const channel: number[] = []; + const time_stamps: number[] = []; + const len = Math.ceil(FS * get_duration(sound)); + + const wave = get_wave(sound); + for (let i = 0; i < len; i += 1) { + time_stamps[i] = i / FS; + channel[i] = wave(i / FS); + } + + let x_s: number[] = []; + let y_s: number[] = []; + + for (let i = 0; i < channel.length; i += 1) { + x_s.push(time_stamps[i]); + y_s.push(channel[i]); + } + + const plotlyData: Data = { + x: x_s, + y: y_s, + }; + const plot = new CurvePlot( + draw_new_curve, + { + ...plotlyData, + type: "scattergl", + mode: "lines", + line: { width: 0.5 }, + } as Data, + { + xaxis: { + type: "linear", + title: "Time", + anchor: "y", + position: 0, + rangeslider: { visible: true }, + }, + yaxis: { + type: "linear", + visible: false, + }, + bargap: 0.2, + barmode: "stack", + } + ); + if (drawnPlots) drawnPlots.push(plot); + } +}; + +function draw_new_curve(divId: string, data: Data, layout: Partial) { + Plotly.react(divId, [data], layout); +} diff --git a/src/bundles/plotly/index.ts b/src/bundles/plotly/index.ts index 51473df3c..862dd9b9b 100644 --- a/src/bundles/plotly/index.ts +++ b/src/bundles/plotly/index.ts @@ -10,6 +10,5 @@ export { draw_connected_3d, draw_points_2d, draw_points_3d, + draw_sound_2d, } from "./functions"; - -// export { draw_sound_2d } from './sound_functions'; diff --git a/src/bundles/plotly/plotly.ts b/src/bundles/plotly/plotly.ts index d475bcf2b..407de3459 100644 --- a/src/bundles/plotly/plotly.ts +++ b/src/bundles/plotly/plotly.ts @@ -1,59 +1,63 @@ -import { type Data, type Layout } from 'plotly.js-dist'; -import { type ReplResult } from '../../typings/type_helpers'; +import { type Data, type Layout } from "plotly.js-dist"; +import { type ReplResult } from "../../typings/type_helpers"; +import type { Pair } from "js-slang/dist/stdlib/list"; /** * Represents plots with a draw method attached */ export class DrawnPlot implements ReplResult { - drawFn: any; - data: ListOfPairs; - constructor(drawFn: any, data: ListOfPairs) { - this.drawFn = drawFn; - this.data = data; - } - - public toReplString = () => ''; - - public draw = (divId: string) => { - this.drawFn(this.data, divId); - }; + drawFn: any; + data: ListOfPairs; + constructor(drawFn: any, data: ListOfPairs) { + this.drawFn = drawFn; + this.data = data; + } + + public toReplString = () => ""; + + public draw = (divId: string) => { + this.drawFn(this.data, divId); + }; } export class CurvePlot implements ReplResult { - plotlyDrawFn: any; - data: Data; - layout: Partial; - constructor(plotlyDrawFn: any, data: Data, layout: Partial) { - this.plotlyDrawFn = plotlyDrawFn; - this.data = data; - this.layout = layout; - } - public toReplString = () => ''; - - public draw = (divId: string) => { - this.plotlyDrawFn(divId, this.data, this.layout); - }; + plotlyDrawFn: any; + data: Data; + layout: Partial; + constructor(plotlyDrawFn: any, data: Data, layout: Partial) { + this.plotlyDrawFn = plotlyDrawFn; + this.data = data; + this.layout = layout; + } + public toReplString = () => ""; + + public draw = (divId: string) => { + this.plotlyDrawFn(divId, this.data, this.layout); + }; } export type ListOfPairs = (ListOfPairs | any)[] | null; export type Data2d = number[]; -export type Color = { r: number, g: number, b: number }; +export type Color = { r: number; g: number; b: number }; export type DataTransformer = (c: Data2d[]) => Data2d[]; -export type CurvePlotFunction = ((func: Curve) => CurvePlot); +export type CurvePlotFunction = (func: Curve) => CurvePlot; -export type Curve = ((n: number) => Point); +export type Curve = (n: number) => Point; export type CurveTransformer = (c: Curve) => Curve; - /** Encapsulates 3D point with RGB values. */ export class Point implements ReplResult { - constructor( - public readonly x: number, - public readonly y: number, - public readonly z: number, - public readonly color: Color, - ) {} - - public toReplString = () => `(${this.x}, ${this.y}, ${this.z}, Color: ${this.color})`; + constructor( + public readonly x: number, + public readonly y: number, + public readonly z: number, + public readonly color: Color + ) {} + + public toReplString = () => + `(${this.x}, ${this.y}, ${this.z}, Color: ${this.color})`; } + +export type Wave = (...t: any) => number; +export type Sound = Pair; diff --git a/src/bundles/plotly/sound_functions.ts b/src/bundles/plotly/sound_functions.ts index 9949d71f7..413288413 100644 --- a/src/bundles/plotly/sound_functions.ts +++ b/src/bundles/plotly/sound_functions.ts @@ -1,81 +1,33 @@ -import context from 'js-slang/context'; -import Plotly, { type Data, type Layout } from 'plotly.js-dist'; -import { get_duration, get_wave, is_sound } from '../sound'; -import { type Sound } from '../sound/types'; -import { CurvePlot, type DrawnPlot } from './plotly'; - -const FS: number = 44100; // Output sample rate - -const drawnPlots: (DrawnPlot | CurvePlot)[] = []; -context.moduleContexts.plotly.state = { - drawnPlots, -}; - +import { + head, + tail, + is_pair, +} from "js-slang/dist/stdlib/list"; +import { type Sound, type Wave } from "../sound/types"; +export function is_sound(x: any): x is Sound { + return ( + is_pair(x) && + typeof get_wave(x) === "function" && + typeof get_duration(x) === "number" + ); +} /** - * Visualizes the sound on a 2d line graph - * @param sound the sound which is to be visualized on plotly + * Accesses the wave function of a given Sound. + * + * @param sound given Sound + * @return the wave function of the Sound + * @example get_wave(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns t => Math_sin(2 * Math_PI * 440 * t) */ -export function draw_sound_2d(sound: Sound) { - if (!is_sound(sound)) { - throw new Error(`draw_sound_2d is expecting sound, but encountered ${sound}`); - // If a sound is already displayed, terminate execution. - } else if (get_duration(sound) < 0) { - throw new Error('draw_sound_2d: duration of sound is negative'); - } else { - // Instantiate audio context if it has not been instantiated. - - // Create mono buffer - const channel: number[] = []; - const time_stamps: number[] = []; - const len = Math.ceil(FS * get_duration(sound)); - - - const wave = get_wave(sound); - for (let i = 0; i < len; i += 1) { - time_stamps[i] = i / FS; - channel[i] = wave(i / FS); - } - - let x_s: number[] = []; - let y_s: number[] = []; - - for (let i = 0; i < channel.length; i += 1) { - x_s.push(time_stamps[i]); - y_s.push(channel[i]); - } - - const plotlyData: Data = { - x: x_s, - y: y_s, - }; - const plot = new CurvePlot( - draw_new_curve, - { - ...plotlyData, - type: 'scattergl', - mode: 'lines', - line: { width: 0.5 }, - } as Data, - { - xaxis: { - type: 'linear', - title: 'Time', - anchor: 'y', - position: 0, - rangeslider: { visible: true }, - }, - yaxis: { - type: 'linear', - visible: false, - }, - bargap: 0.2, - barmode: 'stack', - }, - ); - drawnPlots.push(plot); - } +export function get_wave(sound: Sound): Wave { + return head(sound); } - -function draw_new_curve(divId: string, data: Data, layout: Partial) { - Plotly.newPlot(divId, [data], layout); +/** + * Accesses the duration of a given Sound. + * + * @param sound given Sound + * @return the duration of the Sound + * @example get_duration(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns 5 + */ +export function get_duration(sound: Sound): number { + return tail(sound); } From abd04323b6dd6ea7d0705de359baf45f3b17633b Mon Sep 17 00:00:00 2001 From: Sourabh Jaiswal Date: Tue, 24 Oct 2023 03:27:51 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=9A=A8=20fix:=20linter=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bundles/csg/functions.ts | 1772 ++++++++++++------------- src/bundles/plotly/curve_functions.ts | 94 +- src/bundles/plotly/functions.ts | 260 ++-- src/bundles/plotly/index.ts | 16 +- src/bundles/plotly/plotly.ts | 73 +- src/bundles/plotly/sound_functions.ts | 24 +- src/tabs/Plotly/index.tsx | 5 +- 7 files changed, 1123 insertions(+), 1121 deletions(-) diff --git a/src/bundles/csg/functions.ts b/src/bundles/csg/functions.ts index 41b600930..277409534 100644 --- a/src/bundles/csg/functions.ts +++ b/src/bundles/csg/functions.ts @@ -1,886 +1,886 @@ -/** - * The CSG module enables working with Constructive Solid Geometry in the Source - * Academy. Users are able to program colored 3D models and interact with them - * in a tab. - * - * The main objects in use are called Shapes. Users can create, operate on, - * transform, and finally render these Shapes. - * - * There are also Groups, which contain Shapes, but can also contain other - * nested Groups. Groups allow many Shapes to be transformed in tandem, as - * opposed to having to call transform functions on each Shape individually. - * - * An object that is either a Shape or a Group is called an Operable. Operables - * as a whole are stateless, which means that passing them into functions does - * not modify the original Operable; instead, the newly created Operable is - * returned. Therefore, it is safe to reuse existing Operables after passing - * them into functions, as they remain immutable. - * - * When you are done modeling your Operables, pass them to one of the CSG - * rendering functions to have them displayed in a tab. - * - * When rendering, you may optionally render with a grid and/or axes displayed, - * depending on the rendering function used. The grid appears on the XY-plane - * with white lines every 1 unit of distance, and slightly fainter lines every - * 0.25 units of distance. The axes for x, y, and z are coloured red, green, and - * blue respectively. The positive z direction is upwards from the flat plane - * (right-handed coordinate system). - * - * ```js - * // Sample usage - * import { - * silver, crimson, cyan, - * cube, cone, sphere, - * intersect, union, scale, translate, - * render_grid_axes - * } from "csg"; - * - * const base = intersect( - * scale(cube(silver), 1, 1, 0.3), - * scale(cone(crimson), 1, 1, 3) - * ); - * const snowglobe = union( - * translate(sphere(cyan), 0, 0, 0.22), - * base - * ); - * render_grid_axes(snowglobe); - * ``` - * - * More samples can be found at: https://github.com/source-academy/modules/tree/master/src/bundles/csg/samples - * - * @module csg - * @author Joel Leow - * @author Liu Muchen - * @author Ng Yin Joe - * @author Yu Chenbo - */ - - - -/* [Imports] */ -import { primitives } from '@jscad/modeling'; -import { colorize as colorSolid } from '@jscad/modeling/src/colors'; -import { - measureBoundingBox, - type BoundingBox, -} from '@jscad/modeling/src/measurements'; -import { - intersect as _intersect, - subtract as _subtract, - union as _union, -} from '@jscad/modeling/src/operations/booleans'; -import { extrudeLinear } from '@jscad/modeling/src/operations/extrusions'; -import { serialize } from '@jscad/stl-serializer'; -import { - head, - list, - tail, - type List, - is_list, -} from 'js-slang/dist/stdlib/list'; -import save from 'save-file'; -import { Core } from './core.js'; -import type { Solid } from './jscad/types.js'; -import { - Group, - Shape, - hexToColor, - type Operable, - type RenderGroup, - centerPrimitive, -} from './utilities'; -import { degreesToRadians } from '../../common/utilities.js'; - - - -/* [Main] */ -/* NOTE - These functions involving calls (not merely types) to js-slang make this file - only usable in bundles. DO NOT import this file in tabs or the build will - fail. Something about the node modules that building them involves causes - esbuild to attempt but fail to include Node-specific APIs (eg fs, os, https) - in the output that's meant for a browser environment (you can't use those in - the browser since they are Node-only). This is why we keep these functions - here instead of in utilities.ts. - - When a user passes in a List, we convert it to arrays here so that the rest of - the underlying code is free to operate with arrays. -*/ -export function listToArray(l: List): Operable[] { - let operables: Operable[] = []; - while (l !== null) { - let operable: Operable = head(l); - operables.push(operable); - l = tail(l); - } - return operables; -} - -export function arrayToList(array: Operable[]): List { - return list(...array); -} - - - -/* [Exports] */ - -// [Variables - Colors] - -/** - * A hex color code for black (#000000). - * - * @category Colors - */ -export const black: string = '#000000'; - -/** - * A hex color code for dark blue (#0000AA). - * - * @category Colors - */ -export const navy: string = '#0000AA'; - -/** - * A hex color code for green (#00AA00). - * - * @category Colors - */ -export const green: string = '#00AA00'; - -/** - * A hex color code for dark cyan (#00AAAA). - * - * @category Colors - */ -export const teal: string = '#00AAAA'; - -/** - * A hex color code for dark red (#AA0000). - * - * @category Colors - */ -export const crimson: string = '#AA0000'; - -/** - * A hex color code for purple (#AA00AA). - * - * @category Colors - */ -export const purple: string = '#AA00AA'; - -/** - * A hex color code for orange (#FFAA00). - * - * @category Colors - */ -export const orange: string = '#FFAA00'; - -/** - * A hex color code for light gray (#AAAAAA). - * - * @category Colors - */ -export const silver: string = '#AAAAAA'; - -/** - * A hex color code for dark gray (#555555). - * - * @category Colors - */ -export const gray: string = '#555555'; - -/** - * A hex color code for blue (#5555FF). - * - * @category Colors - */ -export const blue: string = '#5555FF'; - -/** - * A hex color code for light green (#55FF55). - * - * @category Colors - */ -export const lime: string = '#55FF55'; - -/** - * A hex color code for cyan (#55FFFF). - * - * @category Colors - */ -export const cyan: string = '#55FFFF'; - -/** - * A hex color code for light red (#FF5555). - * - * @category Colors - */ -export const rose: string = '#FF5555'; - -/** - * A hex color code for pink (#FF55FF). - * - * @category Colors - */ -export const pink: string = '#FF55FF'; - -/** - * A hex color code for yellow (#FFFF55). - * - * @category Colors - */ -export const yellow: string = '#FFFF55'; - -/** - * A hex color code for white (#FFFFFF). - * - * @category Colors - */ -export const white: string = '#FFFFFF'; - -// [Functions - Primitives] - -/** - * Returns a cube Shape in the specified color. - * - * - Side length: 1 - * - Center: (0.5, 0.5, 0.5) - * - * @param hex hex color code - * - * @category Primitives - */ -export function cube(hex: string): Shape { - let solid: Solid = primitives.cube({ size: 1 }); - let shape: Shape = new Shape( - colorSolid( - hexToColor(hex), - solid, - ), - ); - return centerPrimitive(shape); -} - -/** - * Returns a rounded cube Shape in the specified color. - * - * - Side length: 1 - * - Center: (0.5, 0.5, 0.5) - * - * @param hex hex color code - * - * @category Primitives - */ -export function rounded_cube(hex: string): Shape { - let solid: Solid = primitives.roundedCuboid({ size: [1, 1, 1] }); - let shape: Shape = new Shape( - colorSolid( - hexToColor(hex), - solid, - ), - ); - return centerPrimitive(shape); -} - -/** - * Returns an upright cylinder Shape in the specified color. - * - * - Height: 1 - * - Radius: 0.5 - * - Center: (0.5, 0.5, 0.5) - * - * @param hex hex color code - * - * @category Primitives - */ -export function cylinder(hex: string): Shape { - let solid: Solid = primitives.cylinder({ - height: 1, - radius: 0.5, - }); - let shape: Shape = new Shape( - colorSolid( - hexToColor(hex), - solid, - ), - ); - return centerPrimitive(shape); -} - -/** - * Returns a rounded, upright cylinder Shape in the specified color. - * - * - Height: 1 - * - Radius: 0.5 - * - Center: (0.5, 0.5, 0.5) - * - * @param hex hex color code - * - * @category Primitives - */ -export function rounded_cylinder(hex: string): Shape { - let solid: Solid = primitives.roundedCylinder({ - height: 1, - radius: 0.5, - }); - let shape: Shape = new Shape( - colorSolid( - hexToColor(hex), - solid, - ), - ); - return centerPrimitive(shape); -} - -/** - * Returns a sphere Shape in the specified color. - * - * - Radius: 0.5 - * - Center: (0.5, 0.5, 0.5) - * - * @param hex hex color code - * - * @category Primitives - */ -export function sphere(hex: string): Shape { - let solid: Solid = primitives.sphere({ radius: 0.5 }); - let shape: Shape = new Shape( - colorSolid( - hexToColor(hex), - solid, - ), - ); - return centerPrimitive(shape); -} - -/** - * Returns a geodesic sphere Shape in the specified color. - * - * - Radius: 0.5 - * - Center: Floating at (0.5, 0.5, 0.5) - * - * @param hex hex color code - * - * @category Primitives - */ -export function geodesic_sphere(hex: string): Shape { - let solid: Solid = primitives.geodesicSphere({ radius: 0.5 }); - let shape: Shape = new Shape( - colorSolid( - hexToColor(hex), - solid, - ), - ); - return centerPrimitive(shape); -} - -/** - * Returns a square pyramid Shape in the specified color. - * - * - Height: 1 - * - Base length: 1 - * - Center: (0.5, 0.5, 0.5) - * - * @param hex hex color code - * - * @category Primitives - */ -export function pyramid(hex: string): Shape { - let pythagorasSide: number = Math.sqrt(2); // sqrt(1^2 + 1^2) - let radius = pythagorasSide / 2; - let solid: Solid = primitives.cylinderElliptic({ - height: 1, - // Base starting radius - startRadius: [radius, radius], - // Radius by the time the top is reached - endRadius: [0, 0], - segments: 4, - }); - let shape: Shape = new Shape( - colorSolid( - hexToColor(hex), - solid, - ), - ); - shape = rotate(shape, 0, 0, degreesToRadians(45)) as Shape; - return centerPrimitive(shape); -} - -/** - * Returns a cone Shape in the specified color. - * - * - Height: 1 - * - Radius: 0.5 - * - Center: (0.5, 0.5, 0.5) - * - * @param hex hex color code - * - * @category Primitives - */ -export function cone(hex: string): Shape { - let solid: Solid = primitives.cylinderElliptic({ - height: 1, - startRadius: [0.5, 0.5], - endRadius: [0, 0], - }); - let shape: Shape = new Shape( - colorSolid( - hexToColor(hex), - solid, - ), - ); - return centerPrimitive(shape); -} - -/** - * Returns an upright triangular prism Shape in the specified color. - * - * - Height: 1 - * - Side length: 1 - * - Center: (0.5, 0.5, 0.5) - * - * @param hex hex color code - * - * @category Primitives - */ -export function prism(hex: string): Shape { - let solid: Solid = extrudeLinear( - { height: 1 }, - primitives.triangle(), - ); - let shape: Shape = new Shape( - colorSolid( - hexToColor(hex), - solid, - ), - ); - shape = rotate(shape, 0, 0, degreesToRadians(-90)) as Shape; - return centerPrimitive(shape); -} - -/** - * Returns an upright extruded star Shape in the specified color. - * - * - Height: 1 - * - Center: (0.5, 0.5, 0.5) - * - * @param hex hex color code - * - * @category Primitives - */ -export function star(hex: string): Shape { - let solid: Solid = extrudeLinear( - { height: 1 }, - primitives.star({ outerRadius: 0.5 }), - ); - let shape: Shape = new Shape( - colorSolid( - hexToColor(hex), - solid, - ), - ); - return centerPrimitive(shape); -} - -/** - * Returns a torus (donut) Shape in the specified color. - * - * - Inner radius: 0.15 (ring is 0.3 thick) - * - Total radius: 0.5 (from the centre of the hole to "outside") - * - Center: Floating at (0.5, 0.5, 0.5) - * - * @param hex hex color code - * - * @category Primitives - */ -export function torus(hex: string): Shape { - let solid: Solid = primitives.torus({ - innerRadius: 0.15, - outerRadius: 0.35, - }); - let shape: Shape = new Shape( - colorSolid( - hexToColor(hex), - solid, - ), - ); - return centerPrimitive(shape); -} - -// [Functions - Operations] - -/** - * Returns the union of the two specified Shapes. - * - * @param first first Shape - * @param second second Shape - * @returns unioned Shape - * - * @category Operations - */ -export function union(first: Shape, second: Shape): Shape { - if (!is_shape(first) || !is_shape(second)) { - throw new Error('Failed to union, only Shapes can be operated on'); - } - - let solid: Solid = _union(first.solid, second.solid); - return new Shape(solid); -} - -/** - * Subtracts the second Shape from the first Shape, returning the resultant - * Shape. - * - * @param target target Shape to be subtracted from - * @param subtractedShape Shape to remove from the first Shape - * @returns subtracted Shape - * - * @category Operations - */ -export function subtract(target: Shape, subtractedShape: Shape): Shape { - if (!is_shape(target) || !is_shape(subtractedShape)) { - throw new Error('Failed to subtract, only Shapes can be operated on'); - } - - let solid: Solid = _subtract(target.solid, subtractedShape.solid); - return new Shape(solid); -} - -/** - * Returns the intersection of the two specified Shapes. - * - * @param first first Shape - * @param second second Shape - * @returns intersected Shape - * - * @category Operations - */ -export function intersect(first: Shape, second: Shape): Shape { - if (!is_shape(first) || !is_shape(second)) { - throw new Error('Failed to intersect, only Shapes can be operated on'); - } - - let solid: Solid = _intersect(first.solid, second.solid); - return new Shape(solid); -} - -// [Functions - Transformations] - -/** - * Translates (moves) the specified Operable in the x, y, and z directions using - * the specified offsets. - * - * @param operable Shape or Group - * @param xOffset x offset - * @param yOffset y offset - * @param zOffset z offset - * @returns translated Shape - * - * @category Transformations - */ -export function translate( - operable: Operable, - xOffset: number, - yOffset: number, - zOffset: number, -): Operable { - return operable.translate([xOffset, yOffset, zOffset]); -} - -/** - * Sequentially rotates the specified Operable about the x, y, and z axes using - * the specified angles, in radians (i.e. 2π represents 360°). - * - * The order of rotation is: x, y, then z axis. The order of rotation can affect - * the result, so you may wish to make multiple separate calls to rotate() if - * you require a specific order of rotation. - * - * @param operable Shape or Group - * @param xAngle x angle in radians - * @param yAngle y angle in radians - * @param zAngle z angle in radians - * @returns rotated Shape - * - * @category Transformations - */ -export function rotate( - operable: Operable, - xAngle: number, - yAngle: number, - zAngle: number, -): Operable { - return operable.rotate([xAngle, yAngle, zAngle]); -} - -/** - * Scales the specified Operable in the x, y, and z directions using the - * specified factors. Scaling is done about the origin (0, 0, 0). - * - * For example, a factor of 0.5 results in a smaller Shape, while a factor of 2 - * results in a larger Shape. A factor of 1 results in the original Shape. - * Factors must be greater than 0. - * - * @param operable Shape or Group - * @param xFactor x scaling factor - * @param yFactor y scaling factor - * @param zFactor z scaling factor - * @returns scaled Shape - * - * @category Transformations - */ -export function scale( - operable: Operable, - xFactor: number, - yFactor: number, - zFactor: number, -): Operable { - if (xFactor <= 0 || yFactor <= 0 || zFactor <= 0) { - // JSCAD library does not allow factors <= 0 - throw new Error('Scaling factor must be greater than 0'); - } - - return operable.scale([xFactor, yFactor, zFactor]); -} - -// [Functions - Utilities] - -/** - * Groups the specified list of Operables together. Groups can contain a mix of - * Shapes and other nested Groups. - * - * Groups cannot be operated on, but can be transformed together. I.e. a call - * like `intersect(group_a, group_b)` is not allowed, but a call like - * `scale(group, 5, 5, 5)` is. - * - * @param operables list of Shapes and/or Groups - * @returns new Group - * - * @category Utilities - */ -export function group(operables: List): Group { - if (!is_list(operables)) { - throw new Error('Only lists of Operables can be grouped'); - } - - return new Group(listToArray(operables)); -} - -/** - * Ungroups the specified Group, returning the list of Shapes and/or nested - * Groups contained within. - * - * @param g Group to ungroup - * @returns ungrouped list of Shapes and/or Groups - * - * @category Utilities - */ -export function ungroup(g: Group): List { - if (!is_group(g)) { - throw new Error('Only Groups can be ungrouped'); - } - - return arrayToList(g.ungroup()); -} - -/** - * Checks if the given parameter is a Shape. - * - * @param parameter parameter to check - * @returns whether parameter is a Shape - * - * @category Utilities - */ -export function is_shape(parameter: unknown): boolean { - return parameter instanceof Shape; -} - -/** - * Checks if the given parameter is a Group. - * - * @param parameter parameter to check - * @returns whether parameter is a Group - * - * @category Utilities - */ -export function is_group(parameter: unknown): boolean { - return parameter instanceof Group; -} - -/** - * Returns a function of type (string, string) → number, for getting the - * specified Shape's bounding box coordinates. - * - * Its first parameter must be "x", "y", or "z", indicating the coordinate axis. - * - * Its second parameter must be "min" or "max", indicating the minimum or - * maximum bounding box coordinate respectively. - * - * For example, if a sphere of radius 0.5 is centred at (0.5, 0.5, 0.5), its - * minimum bounding coordinates will be (0, 0, 0), and its maximum bounding - * coordinates will be (1, 1, 1). - * - * ```js - * // Sample usage - * const getter_function = bounding_box(sphere(silver)); - * display(getter_function("y", "max")); // Displays 1, the maximum y coordinate - * ``` - * - * @param shape Shape to measure - * @returns bounding box getter function - * - * @category Utilities - */ -export function bounding_box( - shape: Shape, -): (axis: string, minMax: string) => number { - let bounds: BoundingBox = measureBoundingBox(shape.solid); - - return (axis: string, minMax: string): number => { - let j: number; - if (axis === 'x') j = 0; - else if (axis === 'y') j = 1; - else if (axis === 'z') j = 2; - else { - throw new Error( - `Bounding box getter function expected "x", "y", or "z" as first parameter, but got ${axis}`, - ); - } - - let i: number; - if (minMax === 'min') i = 0; - else if (minMax === 'max') i = 1; - else { - throw new Error( - `Bounding box getter function expected "min" or "max" as second parameter, but got ${minMax}`, - ); - } - - return bounds[i][j]; - }; -} - -/** - * Returns a hex color code representing the specified RGB values. - * - * @param redValue red value of the color - * @param greenValue green value of the color - * @param blueValue blue value of the color - * @returns hex color code - * - * @category Utilities - */ -export function rgb( - redValue: number, - greenValue: number, - blueValue: number, -): string { - if ( - redValue < 0 - || redValue > 255 - || greenValue < 0 - || greenValue > 255 - || blueValue < 0 - || blueValue > 255 - ) { - throw new Error('RGB values must be between 0 and 255 (inclusive)'); - } - - return `#${redValue.toString(16)}${greenValue.toString(16)} - ${blueValue.toString(16)}`; -} - -/** - * Exports the specified Shape as an STL file, downloaded to your device. - * - * The file can be used for purposes such as 3D printing. - * - * @param shape Shape to export - * - * @category Utilities - */ -export async function download_shape_stl(shape: Shape): Promise { - if (!is_shape(shape)) { - throw new Error('Failed to export, only Shapes can be converted to STL'); - } - - await save( - new Blob(serialize({ binary: true }, shape.solid)), - 'Source Academy CSG Shape.stl', - ); -} - -// [Functions - Rendering] - -/** - * Renders the specified Operable. - * - * @param operable Shape or Group to render - * - * @category Rendering - */ -export function render(operable: Operable): RenderGroup { - if (!(operable instanceof Shape || operable instanceof Group)) { - throw new Error('Only Operables can be rendered'); - } - - operable.store(); - - // Trigger a new render group for use with subsequent renders. - // Render group is returned for REPL text only; do not document - return Core.getRenderGroupManager() - .nextRenderGroup(); -} - -/** - * Renders the specified Operable, along with a grid. - * - * @param operable Shape or Group to render - * - * @category Rendering - */ -export function render_grid(operable: Operable): RenderGroup { - if (!(operable instanceof Shape || operable instanceof Group)) { - throw new Error('Only Operables can be rendered'); - } - - operable.store(); - - return Core.getRenderGroupManager() - .nextRenderGroup(true); -} - -/** - * Renders the specified Operable, along with z, y, and z axes. - * - * @param operable Shape or Group to render - * - * @category Rendering - */ -export function render_axes(operable: Operable): RenderGroup { - if (!(operable instanceof Shape || operable instanceof Group)) { - throw new Error('Only Operables can be rendered'); - } - - operable.store(); - - return Core.getRenderGroupManager() - .nextRenderGroup(undefined, true); -} - -/** - * Renders the specified Operable, along with both a grid and axes. - * - * @param operable Shape or Group to render - * - * @category Rendering - */ -export function render_grid_axes(operable: Operable): RenderGroup { - if (!(operable instanceof Shape || operable instanceof Group)) { - throw new Error('Only Operables can be rendered'); - } - - operable.store(); - - return Core.getRenderGroupManager() - .nextRenderGroup(true, true); -} +/** + * The CSG module enables working with Constructive Solid Geometry in the Source + * Academy. Users are able to program colored 3D models and interact with them + * in a tab. + * + * The main objects in use are called Shapes. Users can create, operate on, + * transform, and finally render these Shapes. + * + * There are also Groups, which contain Shapes, but can also contain other + * nested Groups. Groups allow many Shapes to be transformed in tandem, as + * opposed to having to call transform functions on each Shape individually. + * + * An object that is either a Shape or a Group is called an Operable. Operables + * as a whole are stateless, which means that passing them into functions does + * not modify the original Operable; instead, the newly created Operable is + * returned. Therefore, it is safe to reuse existing Operables after passing + * them into functions, as they remain immutable. + * + * When you are done modeling your Operables, pass them to one of the CSG + * rendering functions to have them displayed in a tab. + * + * When rendering, you may optionally render with a grid and/or axes displayed, + * depending on the rendering function used. The grid appears on the XY-plane + * with white lines every 1 unit of distance, and slightly fainter lines every + * 0.25 units of distance. The axes for x, y, and z are coloured red, green, and + * blue respectively. The positive z direction is upwards from the flat plane + * (right-handed coordinate system). + * + * ```js + * // Sample usage + * import { + * silver, crimson, cyan, + * cube, cone, sphere, + * intersect, union, scale, translate, + * render_grid_axes + * } from "csg"; + * + * const base = intersect( + * scale(cube(silver), 1, 1, 0.3), + * scale(cone(crimson), 1, 1, 3) + * ); + * const snowglobe = union( + * translate(sphere(cyan), 0, 0, 0.22), + * base + * ); + * render_grid_axes(snowglobe); + * ``` + * + * More samples can be found at: https://github.com/source-academy/modules/tree/master/src/bundles/csg/samples + * + * @module csg + * @author Joel Leow + * @author Liu Muchen + * @author Ng Yin Joe + * @author Yu Chenbo + */ + + + +/* [Imports] */ +import { primitives } from '@jscad/modeling'; +import { colorize as colorSolid } from '@jscad/modeling/src/colors'; +import { + measureBoundingBox, + type BoundingBox, +} from '@jscad/modeling/src/measurements'; +import { + intersect as _intersect, + subtract as _subtract, + union as _union, +} from '@jscad/modeling/src/operations/booleans'; +import { extrudeLinear } from '@jscad/modeling/src/operations/extrusions'; +import { serialize } from '@jscad/stl-serializer'; +import { + head, + list, + tail, + type List, + is_list, +} from 'js-slang/dist/stdlib/list'; +import save from 'save-file'; +import { Core } from './core.js'; +import type { Solid } from './jscad/types.js'; +import { + Group, + Shape, + hexToColor, + type Operable, + type RenderGroup, + centerPrimitive, +} from './utilities'; +import { degreesToRadians } from '../../common/utilities.js'; + + + +/* [Main] */ +/* NOTE + These functions involving calls (not merely types) to js-slang make this file + only usable in bundles. DO NOT import this file in tabs or the build will + fail. Something about the node modules that building them involves causes + esbuild to attempt but fail to include Node-specific APIs (eg fs, os, https) + in the output that's meant for a browser environment (you can't use those in + the browser since they are Node-only). This is why we keep these functions + here instead of in utilities.ts. + + When a user passes in a List, we convert it to arrays here so that the rest of + the underlying code is free to operate with arrays. +*/ +export function listToArray(l: List): Operable[] { + let operables: Operable[] = []; + while (l !== null) { + let operable: Operable = head(l); + operables.push(operable); + l = tail(l); + } + return operables; +} + +export function arrayToList(array: Operable[]): List { + return list(...array); +} + + + +/* [Exports] */ + +// [Variables - Colors] + +/** + * A hex color code for black (#000000). + * + * @category Colors + */ +export const black: string = '#000000'; + +/** + * A hex color code for dark blue (#0000AA). + * + * @category Colors + */ +export const navy: string = '#0000AA'; + +/** + * A hex color code for green (#00AA00). + * + * @category Colors + */ +export const green: string = '#00AA00'; + +/** + * A hex color code for dark cyan (#00AAAA). + * + * @category Colors + */ +export const teal: string = '#00AAAA'; + +/** + * A hex color code for dark red (#AA0000). + * + * @category Colors + */ +export const crimson: string = '#AA0000'; + +/** + * A hex color code for purple (#AA00AA). + * + * @category Colors + */ +export const purple: string = '#AA00AA'; + +/** + * A hex color code for orange (#FFAA00). + * + * @category Colors + */ +export const orange: string = '#FFAA00'; + +/** + * A hex color code for light gray (#AAAAAA). + * + * @category Colors + */ +export const silver: string = '#AAAAAA'; + +/** + * A hex color code for dark gray (#555555). + * + * @category Colors + */ +export const gray: string = '#555555'; + +/** + * A hex color code for blue (#5555FF). + * + * @category Colors + */ +export const blue: string = '#5555FF'; + +/** + * A hex color code for light green (#55FF55). + * + * @category Colors + */ +export const lime: string = '#55FF55'; + +/** + * A hex color code for cyan (#55FFFF). + * + * @category Colors + */ +export const cyan: string = '#55FFFF'; + +/** + * A hex color code for light red (#FF5555). + * + * @category Colors + */ +export const rose: string = '#FF5555'; + +/** + * A hex color code for pink (#FF55FF). + * + * @category Colors + */ +export const pink: string = '#FF55FF'; + +/** + * A hex color code for yellow (#FFFF55). + * + * @category Colors + */ +export const yellow: string = '#FFFF55'; + +/** + * A hex color code for white (#FFFFFF). + * + * @category Colors + */ +export const white: string = '#FFFFFF'; + +// [Functions - Primitives] + +/** + * Returns a cube Shape in the specified color. + * + * - Side length: 1 + * - Center: (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitives + */ +export function cube(hex: string): Shape { + let solid: Solid = primitives.cube({ size: 1 }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); +} + +/** + * Returns a rounded cube Shape in the specified color. + * + * - Side length: 1 + * - Center: (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitives + */ +export function rounded_cube(hex: string): Shape { + let solid: Solid = primitives.roundedCuboid({ size: [1, 1, 1] }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); +} + +/** + * Returns an upright cylinder Shape in the specified color. + * + * - Height: 1 + * - Radius: 0.5 + * - Center: (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitives + */ +export function cylinder(hex: string): Shape { + let solid: Solid = primitives.cylinder({ + height: 1, + radius: 0.5, + }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); +} + +/** + * Returns a rounded, upright cylinder Shape in the specified color. + * + * - Height: 1 + * - Radius: 0.5 + * - Center: (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitives + */ +export function rounded_cylinder(hex: string): Shape { + let solid: Solid = primitives.roundedCylinder({ + height: 1, + radius: 0.5, + }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); +} + +/** + * Returns a sphere Shape in the specified color. + * + * - Radius: 0.5 + * - Center: (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitives + */ +export function sphere(hex: string): Shape { + let solid: Solid = primitives.sphere({ radius: 0.5 }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); +} + +/** + * Returns a geodesic sphere Shape in the specified color. + * + * - Radius: 0.5 + * - Center: Floating at (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitives + */ +export function geodesic_sphere(hex: string): Shape { + let solid: Solid = primitives.geodesicSphere({ radius: 0.5 }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); +} + +/** + * Returns a square pyramid Shape in the specified color. + * + * - Height: 1 + * - Base length: 1 + * - Center: (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitives + */ +export function pyramid(hex: string): Shape { + let pythagorasSide: number = Math.sqrt(2); // sqrt(1^2 + 1^2) + let radius = pythagorasSide / 2; + let solid: Solid = primitives.cylinderElliptic({ + height: 1, + // Base starting radius + startRadius: [radius, radius], + // Radius by the time the top is reached + endRadius: [0, 0], + segments: 4, + }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + shape = rotate(shape, 0, 0, degreesToRadians(45)) as Shape; + return centerPrimitive(shape); +} + +/** + * Returns a cone Shape in the specified color. + * + * - Height: 1 + * - Radius: 0.5 + * - Center: (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitives + */ +export function cone(hex: string): Shape { + let solid: Solid = primitives.cylinderElliptic({ + height: 1, + startRadius: [0.5, 0.5], + endRadius: [0, 0], + }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); +} + +/** + * Returns an upright triangular prism Shape in the specified color. + * + * - Height: 1 + * - Side length: 1 + * - Center: (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitives + */ +export function prism(hex: string): Shape { + let solid: Solid = extrudeLinear( + { height: 1 }, + primitives.triangle(), + ); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + shape = rotate(shape, 0, 0, degreesToRadians(-90)) as Shape; + return centerPrimitive(shape); +} + +/** + * Returns an upright extruded star Shape in the specified color. + * + * - Height: 1 + * - Center: (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitives + */ +export function star(hex: string): Shape { + let solid: Solid = extrudeLinear( + { height: 1 }, + primitives.star({ outerRadius: 0.5 }), + ); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); +} + +/** + * Returns a torus (donut) Shape in the specified color. + * + * - Inner radius: 0.15 (ring is 0.3 thick) + * - Total radius: 0.5 (from the centre of the hole to "outside") + * - Center: Floating at (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitives + */ +export function torus(hex: string): Shape { + let solid: Solid = primitives.torus({ + innerRadius: 0.15, + outerRadius: 0.35, + }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); +} + +// [Functions - Operations] + +/** + * Returns the union of the two specified Shapes. + * + * @param first first Shape + * @param second second Shape + * @returns unioned Shape + * + * @category Operations + */ +export function union(first: Shape, second: Shape): Shape { + if (!is_shape(first) || !is_shape(second)) { + throw new Error('Failed to union, only Shapes can be operated on'); + } + + let solid: Solid = _union(first.solid, second.solid); + return new Shape(solid); +} + +/** + * Subtracts the second Shape from the first Shape, returning the resultant + * Shape. + * + * @param target target Shape to be subtracted from + * @param subtractedShape Shape to remove from the first Shape + * @returns subtracted Shape + * + * @category Operations + */ +export function subtract(target: Shape, subtractedShape: Shape): Shape { + if (!is_shape(target) || !is_shape(subtractedShape)) { + throw new Error('Failed to subtract, only Shapes can be operated on'); + } + + let solid: Solid = _subtract(target.solid, subtractedShape.solid); + return new Shape(solid); +} + +/** + * Returns the intersection of the two specified Shapes. + * + * @param first first Shape + * @param second second Shape + * @returns intersected Shape + * + * @category Operations + */ +export function intersect(first: Shape, second: Shape): Shape { + if (!is_shape(first) || !is_shape(second)) { + throw new Error('Failed to intersect, only Shapes can be operated on'); + } + + let solid: Solid = _intersect(first.solid, second.solid); + return new Shape(solid); +} + +// [Functions - Transformations] + +/** + * Translates (moves) the specified Operable in the x, y, and z directions using + * the specified offsets. + * + * @param operable Shape or Group + * @param xOffset x offset + * @param yOffset y offset + * @param zOffset z offset + * @returns translated Shape + * + * @category Transformations + */ +export function translate( + operable: Operable, + xOffset: number, + yOffset: number, + zOffset: number, +): Operable { + return operable.translate([xOffset, yOffset, zOffset]); +} + +/** + * Sequentially rotates the specified Operable about the x, y, and z axes using + * the specified angles, in radians (i.e. 2π represents 360°). + * + * The order of rotation is: x, y, then z axis. The order of rotation can affect + * the result, so you may wish to make multiple separate calls to rotate() if + * you require a specific order of rotation. + * + * @param operable Shape or Group + * @param xAngle x angle in radians + * @param yAngle y angle in radians + * @param zAngle z angle in radians + * @returns rotated Shape + * + * @category Transformations + */ +export function rotate( + operable: Operable, + xAngle: number, + yAngle: number, + zAngle: number, +): Operable { + return operable.rotate([xAngle, yAngle, zAngle]); +} + +/** + * Scales the specified Operable in the x, y, and z directions using the + * specified factors. Scaling is done about the origin (0, 0, 0). + * + * For example, a factor of 0.5 results in a smaller Shape, while a factor of 2 + * results in a larger Shape. A factor of 1 results in the original Shape. + * Factors must be greater than 0. + * + * @param operable Shape or Group + * @param xFactor x scaling factor + * @param yFactor y scaling factor + * @param zFactor z scaling factor + * @returns scaled Shape + * + * @category Transformations + */ +export function scale( + operable: Operable, + xFactor: number, + yFactor: number, + zFactor: number, +): Operable { + if (xFactor <= 0 || yFactor <= 0 || zFactor <= 0) { + // JSCAD library does not allow factors <= 0 + throw new Error('Scaling factor must be greater than 0'); + } + + return operable.scale([xFactor, yFactor, zFactor]); +} + +// [Functions - Utilities] + +/** + * Groups the specified list of Operables together. Groups can contain a mix of + * Shapes and other nested Groups. + * + * Groups cannot be operated on, but can be transformed together. I.e. a call + * like `intersect(group_a, group_b)` is not allowed, but a call like + * `scale(group, 5, 5, 5)` is. + * + * @param operables list of Shapes and/or Groups + * @returns new Group + * + * @category Utilities + */ +export function group(operables: List): Group { + if (!is_list(operables)) { + throw new Error('Only lists of Operables can be grouped'); + } + + return new Group(listToArray(operables)); +} + +/** + * Ungroups the specified Group, returning the list of Shapes and/or nested + * Groups contained within. + * + * @param g Group to ungroup + * @returns ungrouped list of Shapes and/or Groups + * + * @category Utilities + */ +export function ungroup(g: Group): List { + if (!is_group(g)) { + throw new Error('Only Groups can be ungrouped'); + } + + return arrayToList(g.ungroup()); +} + +/** + * Checks if the given parameter is a Shape. + * + * @param parameter parameter to check + * @returns whether parameter is a Shape + * + * @category Utilities + */ +export function is_shape(parameter: unknown): boolean { + return parameter instanceof Shape; +} + +/** + * Checks if the given parameter is a Group. + * + * @param parameter parameter to check + * @returns whether parameter is a Group + * + * @category Utilities + */ +export function is_group(parameter: unknown): boolean { + return parameter instanceof Group; +} + +/** + * Returns a function of type (string, string) → number, for getting the + * specified Shape's bounding box coordinates. + * + * Its first parameter must be "x", "y", or "z", indicating the coordinate axis. + * + * Its second parameter must be "min" or "max", indicating the minimum or + * maximum bounding box coordinate respectively. + * + * For example, if a sphere of radius 0.5 is centred at (0.5, 0.5, 0.5), its + * minimum bounding coordinates will be (0, 0, 0), and its maximum bounding + * coordinates will be (1, 1, 1). + * + * ```js + * // Sample usage + * const getter_function = bounding_box(sphere(silver)); + * display(getter_function("y", "max")); // Displays 1, the maximum y coordinate + * ``` + * + * @param shape Shape to measure + * @returns bounding box getter function + * + * @category Utilities + */ +export function bounding_box( + shape: Shape, +): (axis: string, minMax: string) => number { + let bounds: BoundingBox = measureBoundingBox(shape.solid); + + return (axis: string, minMax: string): number => { + let j: number; + if (axis === 'x') j = 0; + else if (axis === 'y') j = 1; + else if (axis === 'z') j = 2; + else { + throw new Error( + `Bounding box getter function expected "x", "y", or "z" as first parameter, but got ${axis}`, + ); + } + + let i: number; + if (minMax === 'min') i = 0; + else if (minMax === 'max') i = 1; + else { + throw new Error( + `Bounding box getter function expected "min" or "max" as second parameter, but got ${minMax}`, + ); + } + + return bounds[i][j]; + }; +} + +/** + * Returns a hex color code representing the specified RGB values. + * + * @param redValue red value of the color + * @param greenValue green value of the color + * @param blueValue blue value of the color + * @returns hex color code + * + * @category Utilities + */ +export function rgb( + redValue: number, + greenValue: number, + blueValue: number, +): string { + if ( + redValue < 0 + || redValue > 255 + || greenValue < 0 + || greenValue > 255 + || blueValue < 0 + || blueValue > 255 + ) { + throw new Error('RGB values must be between 0 and 255 (inclusive)'); + } + + return `#${redValue.toString(16)}${greenValue.toString(16)} + ${blueValue.toString(16)}`; +} + +/** + * Exports the specified Shape as an STL file, downloaded to your device. + * + * The file can be used for purposes such as 3D printing. + * + * @param shape Shape to export + * + * @category Utilities + */ +export async function download_shape_stl(shape: Shape): Promise { + if (!is_shape(shape)) { + throw new Error('Failed to export, only Shapes can be converted to STL'); + } + + await save( + new Blob(serialize({ binary: true }, shape.solid)), + 'Source Academy CSG Shape.stl', + ); +} + +// [Functions - Rendering] + +/** + * Renders the specified Operable. + * + * @param operable Shape or Group to render + * + * @category Rendering + */ +export function render(operable: Operable): RenderGroup { + if (!(operable instanceof Shape || operable instanceof Group)) { + throw new Error('Only Operables can be rendered'); + } + + operable.store(); + + // Trigger a new render group for use with subsequent renders. + // Render group is returned for REPL text only; do not document + return Core.getRenderGroupManager() + .nextRenderGroup(); +} + +/** + * Renders the specified Operable, along with a grid. + * + * @param operable Shape or Group to render + * + * @category Rendering + */ +export function render_grid(operable: Operable): RenderGroup { + if (!(operable instanceof Shape || operable instanceof Group)) { + throw new Error('Only Operables can be rendered'); + } + + operable.store(); + + return Core.getRenderGroupManager() + .nextRenderGroup(true); +} + +/** + * Renders the specified Operable, along with z, y, and z axes. + * + * @param operable Shape or Group to render + * + * @category Rendering + */ +export function render_axes(operable: Operable): RenderGroup { + if (!(operable instanceof Shape || operable instanceof Group)) { + throw new Error('Only Operables can be rendered'); + } + + operable.store(); + + return Core.getRenderGroupManager() + .nextRenderGroup(undefined, true); +} + +/** + * Renders the specified Operable, along with both a grid and axes. + * + * @param operable Shape or Group to render + * + * @category Rendering + */ +export function render_grid_axes(operable: Operable): RenderGroup { + if (!(operable instanceof Shape || operable instanceof Group)) { + throw new Error('Only Operables can be rendered'); + } + + operable.store(); + + return Core.getRenderGroupManager() + .nextRenderGroup(true, true); +} diff --git a/src/bundles/plotly/curve_functions.ts b/src/bundles/plotly/curve_functions.ts index 00f711c09..eb5abfd4e 100644 --- a/src/bundles/plotly/curve_functions.ts +++ b/src/bundles/plotly/curve_functions.ts @@ -1,8 +1,8 @@ -import Plotly, { type Data, type Layout } from "plotly.js-dist"; -import { type Curve, CurvePlot, type Point } from "./plotly"; +import Plotly, { type Data, type Layout } from 'plotly.js-dist'; +import { type Curve, CurvePlot, type Point } from './plotly'; export function x_of(pt: Point): number { - return pt.x; + return pt.x; } /** @@ -17,7 +17,7 @@ export function x_of(pt: Point): number { * ``` */ export function y_of(pt: Point): number { - return pt.y; + return pt.y; } /** @@ -32,7 +32,7 @@ export function y_of(pt: Point): number { * ``` */ export function z_of(pt: Point): number { - return pt.z; + return pt.z; } /** @@ -47,7 +47,7 @@ export function z_of(pt: Point): number { * ``` */ export function r_of(pt: Point): number { - return pt.color[0] ?? 0 * 255; + return pt.color[0] ?? 0 * 255; } /** @@ -62,7 +62,7 @@ export function r_of(pt: Point): number { * ``` */ export function g_of(pt: Point): number { - return pt.color[1] ?? 0 * 255; + return pt.color[1] ?? 0 * 255; } /** @@ -77,51 +77,51 @@ export function g_of(pt: Point): number { * ``` */ export function b_of(pt: Point): number { - return pt.color[2] ?? 0 * 255; + return pt.color[2] ?? 0 * 255; } export function generatePlot( - type: string, - numPoints: number, - config: Data, - layout: Partial, - is_colored: boolean, - func: Curve + type: string, + numPoints: number, + config: Data, + layout: Partial, + is_colored: boolean, + func: Curve, ): CurvePlot { - let x_s: number[] = []; - let y_s: number[] = []; - let z_s: number[] = []; - let color_s: string[] = []; - for (let i = 0; i <= numPoints; i += 1) { - const point = func(i / numPoints); - x_s.push(x_of(point)); - y_s.push(y_of(point)); - z_s.push(z_of(point)); - color_s.push(`rgb(${r_of(point)},${g_of(point)},${b_of(point)})`); - } + let x_s: number[] = []; + let y_s: number[] = []; + let z_s: number[] = []; + let color_s: string[] = []; + for (let i = 0; i <= numPoints; i += 1) { + const point = func(i / numPoints); + x_s.push(x_of(point)); + y_s.push(y_of(point)); + z_s.push(z_of(point)); + color_s.push(`rgb(${r_of(point)},${g_of(point)},${b_of(point)})`); + } - const plotlyData: Data = { - x: x_s, - y: y_s, - z: z_s, - marker: { - size: 2, - color: color_s, - }, - line: { - color: color_s, - }, - }; - return new CurvePlot( - draw_new_curve, - { - ...plotlyData, - ...config, - type, - } as Data, - layout - ); + const plotlyData: Data = { + x: x_s, + y: y_s, + z: z_s, + marker: { + size: 2, + color: color_s, + }, + line: { + color: color_s, + }, + }; + return new CurvePlot( + draw_new_curve, + { + ...plotlyData, + ...config, + type, + } as Data, + layout, + ); } function draw_new_curve(divId: string, data: Data, layout: Partial) { - Plotly.react(divId, [data], layout); + Plotly.react(divId, [data], layout); } diff --git a/src/bundles/plotly/functions.ts b/src/bundles/plotly/functions.ts index 7c99c4021..a5578c47d 100644 --- a/src/bundles/plotly/functions.ts +++ b/src/bundles/plotly/functions.ts @@ -3,23 +3,23 @@ * @module plotly */ -import context from "js-slang/context"; -import Plotly, { type Data, type Layout } from "plotly.js-dist"; +import context from 'js-slang/context'; +import Plotly, { type Data, type Layout } from 'plotly.js-dist'; import { - type Curve, - CurvePlot, - type CurvePlotFunction, - DrawnPlot, - type ListOfPairs, - type Sound, -} from "./plotly"; -import { generatePlot } from "./curve_functions"; -import { get_duration, get_wave, is_sound } from "./sound_functions"; + type Curve, + CurvePlot, + type CurvePlotFunction, + DrawnPlot, + type ListOfPairs, + type Sound, +} from './plotly'; +import { generatePlot } from './curve_functions'; +import { get_duration, get_wave, is_sound } from './sound_functions'; let drawnPlots: (DrawnPlot | CurvePlot)[] = []; context.moduleContexts.plotly.state = { - drawnPlots, + drawnPlots, }; /** @@ -121,7 +121,7 @@ context.moduleContexts.plotly.state = { * among the fields mentioned above */ export function new_plot(data: ListOfPairs): void { - drawnPlots.push(new DrawnPlot(draw_new_plot, data)); + drawnPlots.push(new DrawnPlot(draw_new_plot, data)); } /** @@ -235,7 +235,7 @@ export function new_plot(data: ListOfPairs): void { * @param data The data as an array of json objects having some or all of the given fields */ export function new_plot_json(data: any): void { - drawnPlots.push(new DrawnPlot(draw_new_plot_json, data)); + drawnPlots.push(new DrawnPlot(draw_new_plot_json, data)); } /** @@ -243,8 +243,8 @@ export function new_plot_json(data: any): void { * @param divId The id of the div element on which the plot will be displayed */ function draw_new_plot(data: ListOfPairs, divId: string) { - const plotlyData = convert_to_plotly_data(data); - Plotly.newPlot(divId, [plotlyData]); + const plotlyData = convert_to_plotly_data(data); + Plotly.newPlot(divId, [plotlyData]); } /** @@ -253,7 +253,7 @@ function draw_new_plot(data: ListOfPairs, divId: string) { * @param divId The id of the div element on which the plot will be displayed */ function draw_new_plot_json(data: any, divId: string) { - Plotly.newPlot(divId, data); + Plotly.newPlot(divId, data); } /** @@ -261,11 +261,11 @@ function draw_new_plot_json(data: any, divId: string) { * @returns The converted data that can be used by the plotly.js function */ function convert_to_plotly_data(data: ListOfPairs): Data { - let convertedData: Data = {}; - if (Array.isArray(data) && data.length === 2) { - add_fields_to_data(convertedData, data); - } - return convertedData; + let convertedData: Data = {}; + if (Array.isArray(data) && data.length === 2) { + add_fields_to_data(convertedData, data); + } + return convertedData; } /** @@ -274,37 +274,37 @@ function convert_to_plotly_data(data: ListOfPairs): Data { */ function add_fields_to_data(convertedData: Data, data: ListOfPairs) { - if (Array.isArray(data) && data.length === 2 && data[0].length === 2) { - const field = data[0][0]; - const value = data[0][1]; - convertedData[field] = value; - add_fields_to_data(convertedData, data[1]); - } + if (Array.isArray(data) && data.length === 2 && data[0].length === 2) { + const field = data[0][0]; + const value = data[0][1]; + convertedData[field] = value; + add_fields_to_data(convertedData, data[1]); + } } function createPlotFunction( - type: string, - config: Data, - layout: Partial, - is_colored: boolean = false + type: string, + config: Data, + layout: Partial, + is_colored: boolean = false, ): (numPoints: number) => CurvePlotFunction { - return (numPoints: number) => { - const func = (curveFunction: Curve) => { - const plotDrawn = generatePlot( - type, - numPoints, - config, - layout, - is_colored, - curveFunction - ); + return (numPoints: number) => { + const func = (curveFunction: Curve) => { + const plotDrawn = generatePlot( + type, + numPoints, + config, + layout, + is_colored, + curveFunction, + ); - drawnPlots.push(plotDrawn); - return plotDrawn; - }; - - return func; + drawnPlots.push(plotDrawn); + return plotDrawn; }; + + return func; + }; } /** @@ -320,18 +320,18 @@ function createPlotFunction( * ``` */ export const draw_connected_2d = createPlotFunction( - "scattergl", - { - mode: "lines", + 'scattergl', + { + mode: 'lines', + }, + { + xaxis: { visible: false }, + yaxis: { + visible: false, + scaleanchor: 'x', }, - { - xaxis: { visible: false }, - yaxis: { - visible: false, - scaleanchor: "x", - }, - }, - true + }, + true, ); /** @@ -347,10 +347,10 @@ export const draw_connected_2d = createPlotFunction( * ``` */ export const draw_connected_3d = createPlotFunction( - "scatter3d", - { mode: "lines" }, - {}, - true + 'scatter3d', + { mode: 'lines' }, + {}, + true, ); /** @@ -366,16 +366,16 @@ export const draw_connected_3d = createPlotFunction( * draw_points_2d(100)(t => make_point(t, t)); */ export const draw_points_2d = createPlotFunction( - "scatter", - { mode: "markers" }, - { - xaxis: { visible: false }, - yaxis: { - visible: false, - scaleanchor: "x", - }, + 'scatter', + { mode: 'markers' }, + { + xaxis: { visible: false }, + yaxis: { + visible: false, + scaleanchor: 'x', }, - true + }, + true, ); /** @@ -391,9 +391,9 @@ export const draw_points_2d = createPlotFunction( * draw_points_3d(100)(t => make_point(t, t)); */ export const draw_points_3d = createPlotFunction( - "scatter3d", - { mode: "markers" }, - {} + 'scatter3d', + { mode: 'markers' }, + {}, ); /** @@ -401,68 +401,68 @@ export const draw_points_3d = createPlotFunction( * @param sound the sound which is to be visualized on plotly */ export const draw_sound_2d = (sound: Sound) => { - const FS: number = 44100; // Output sample rate - if (!is_sound(sound)) { - throw new Error( - `draw_sound_2d is expecting sound, but encountered ${sound}` - ); - // If a sound is already displayed, terminate execution. - } else if (get_duration(sound) < 0) { - throw new Error("draw_sound_2d: duration of sound is negative"); - } else { - // Instantiate audio context if it has not been instantiated. - - // Create mono buffer - const channel: number[] = []; - const time_stamps: number[] = []; - const len = Math.ceil(FS * get_duration(sound)); + const FS: number = 44100; // Output sample rate + if (!is_sound(sound)) { + throw new Error( + `draw_sound_2d is expecting sound, but encountered ${sound}`, + ); + // If a sound is already displayed, terminate execution. + } else if (get_duration(sound) < 0) { + throw new Error('draw_sound_2d: duration of sound is negative'); + } else { + // Instantiate audio context if it has not been instantiated. - const wave = get_wave(sound); - for (let i = 0; i < len; i += 1) { - time_stamps[i] = i / FS; - channel[i] = wave(i / FS); - } + // Create mono buffer + const channel: number[] = []; + const time_stamps: number[] = []; + const len = Math.ceil(FS * get_duration(sound)); - let x_s: number[] = []; - let y_s: number[] = []; + const wave = get_wave(sound); + for (let i = 0; i < len; i += 1) { + time_stamps[i] = i / FS; + channel[i] = wave(i / FS); + } - for (let i = 0; i < channel.length; i += 1) { - x_s.push(time_stamps[i]); - y_s.push(channel[i]); - } + let x_s: number[] = []; + let y_s: number[] = []; - const plotlyData: Data = { - x: x_s, - y: y_s, - }; - const plot = new CurvePlot( - draw_new_curve, - { - ...plotlyData, - type: "scattergl", - mode: "lines", - line: { width: 0.5 }, - } as Data, - { - xaxis: { - type: "linear", - title: "Time", - anchor: "y", - position: 0, - rangeslider: { visible: true }, - }, - yaxis: { - type: "linear", - visible: false, - }, - bargap: 0.2, - barmode: "stack", - } - ); - if (drawnPlots) drawnPlots.push(plot); + for (let i = 0; i < channel.length; i += 1) { + x_s.push(time_stamps[i]); + y_s.push(channel[i]); } + + const plotlyData: Data = { + x: x_s, + y: y_s, + }; + const plot = new CurvePlot( + draw_new_curve, + { + ...plotlyData, + type: 'scattergl', + mode: 'lines', + line: { width: 0.5 }, + } as Data, + { + xaxis: { + type: 'linear', + title: 'Time', + anchor: 'y', + position: 0, + rangeslider: { visible: true }, + }, + yaxis: { + type: 'linear', + visible: false, + }, + bargap: 0.2, + barmode: 'stack', + }, + ); + if (drawnPlots) drawnPlots.push(plot); + } }; function draw_new_curve(divId: string, data: Data, layout: Partial) { - Plotly.react(divId, [data], layout); + Plotly.react(divId, [data], layout); } diff --git a/src/bundles/plotly/index.ts b/src/bundles/plotly/index.ts index 862dd9b9b..2f05feb0d 100644 --- a/src/bundles/plotly/index.ts +++ b/src/bundles/plotly/index.ts @@ -4,11 +4,11 @@ */ export { - new_plot, - new_plot_json, - draw_connected_2d, - draw_connected_3d, - draw_points_2d, - draw_points_3d, - draw_sound_2d, -} from "./functions"; + new_plot, + new_plot_json, + draw_connected_2d, + draw_connected_3d, + draw_points_2d, + draw_points_3d, + draw_sound_2d, +} from './functions'; diff --git a/src/bundles/plotly/plotly.ts b/src/bundles/plotly/plotly.ts index 407de3459..2b03cc630 100644 --- a/src/bundles/plotly/plotly.ts +++ b/src/bundles/plotly/plotly.ts @@ -1,39 +1,39 @@ -import { type Data, type Layout } from "plotly.js-dist"; -import { type ReplResult } from "../../typings/type_helpers"; -import type { Pair } from "js-slang/dist/stdlib/list"; +import { type Data, type Layout } from 'plotly.js-dist'; +import { type ReplResult } from '../../typings/type_helpers'; +import type { Pair } from 'js-slang/dist/stdlib/list'; /** * Represents plots with a draw method attached */ export class DrawnPlot implements ReplResult { - drawFn: any; - data: ListOfPairs; - constructor(drawFn: any, data: ListOfPairs) { - this.drawFn = drawFn; - this.data = data; - } - - public toReplString = () => ""; - - public draw = (divId: string) => { - this.drawFn(this.data, divId); - }; + drawFn: any; + data: ListOfPairs; + constructor(drawFn: any, data: ListOfPairs) { + this.drawFn = drawFn; + this.data = data; + } + + public toReplString = () => ''; + + public draw = (divId: string) => { + this.drawFn(this.data, divId); + }; } export class CurvePlot implements ReplResult { - plotlyDrawFn: any; - data: Data; - layout: Partial; - constructor(plotlyDrawFn: any, data: Data, layout: Partial) { - this.plotlyDrawFn = plotlyDrawFn; - this.data = data; - this.layout = layout; - } - public toReplString = () => ""; - - public draw = (divId: string) => { - this.plotlyDrawFn(divId, this.data, this.layout); - }; + plotlyDrawFn: any; + data: Data; + layout: Partial; + constructor(plotlyDrawFn: any, data: Data, layout: Partial) { + this.plotlyDrawFn = plotlyDrawFn; + this.data = data; + this.layout = layout; + } + public toReplString = () => ''; + + public draw = (divId: string) => { + this.plotlyDrawFn(divId, this.data, this.layout); + }; } export type ListOfPairs = (ListOfPairs | any)[] | null; @@ -48,15 +48,14 @@ export type CurveTransformer = (c: Curve) => Curve; /** Encapsulates 3D point with RGB values. */ export class Point implements ReplResult { - constructor( - public readonly x: number, - public readonly y: number, - public readonly z: number, - public readonly color: Color - ) {} - - public toReplString = () => - `(${this.x}, ${this.y}, ${this.z}, Color: ${this.color})`; + constructor( + public readonly x: number, + public readonly y: number, + public readonly z: number, + public readonly color: Color, + ) {} + + public toReplString = () => `(${this.x}, ${this.y}, ${this.z}, Color: ${this.color})`; } export type Wave = (...t: any) => number; diff --git a/src/bundles/plotly/sound_functions.ts b/src/bundles/plotly/sound_functions.ts index 413288413..a56162594 100644 --- a/src/bundles/plotly/sound_functions.ts +++ b/src/bundles/plotly/sound_functions.ts @@ -1,15 +1,15 @@ import { - head, - tail, - is_pair, -} from "js-slang/dist/stdlib/list"; -import { type Sound, type Wave } from "../sound/types"; + head, + tail, + is_pair, +} from 'js-slang/dist/stdlib/list'; +import { type Sound, type Wave } from '../sound/types'; export function is_sound(x: any): x is Sound { - return ( - is_pair(x) && - typeof get_wave(x) === "function" && - typeof get_duration(x) === "number" - ); + return ( + is_pair(x) + && typeof get_wave(x) === 'function' + && typeof get_duration(x) === 'number' + ); } /** * Accesses the wave function of a given Sound. @@ -19,7 +19,7 @@ export function is_sound(x: any): x is Sound { * @example get_wave(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns t => Math_sin(2 * Math_PI * 440 * t) */ export function get_wave(sound: Sound): Wave { - return head(sound); + return head(sound); } /** * Accesses the duration of a given Sound. @@ -29,5 +29,5 @@ export function get_wave(sound: Sound): Wave { * @example get_duration(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns 5 */ export function get_duration(sound: Sound): number { - return tail(sound); + return tail(sound); } diff --git a/src/tabs/Plotly/index.tsx b/src/tabs/Plotly/index.tsx index 17e6b6919..67584b274 100644 --- a/src/tabs/Plotly/index.tsx +++ b/src/tabs/Plotly/index.tsx @@ -55,7 +55,10 @@ class Plotly extends React.Component { drawnPlots.map((drawnPlot: any, id:number) => { const divId = `plotDiv${id}`; return ( -
+
this.handleOpen(drawnPlot)}>Click here to open Modal
Date: Tue, 24 Oct 2023 03:57:45 +0800 Subject: [PATCH 4/7] :bug: fix Sound import --- src/bundles/plotly/functions.ts | 2 +- src/bundles/plotly/sound_functions.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bundles/plotly/functions.ts b/src/bundles/plotly/functions.ts index a5578c47d..ea9fbf46b 100644 --- a/src/bundles/plotly/functions.ts +++ b/src/bundles/plotly/functions.ts @@ -11,10 +11,10 @@ import { type CurvePlotFunction, DrawnPlot, type ListOfPairs, - type Sound, } from './plotly'; import { generatePlot } from './curve_functions'; import { get_duration, get_wave, is_sound } from './sound_functions'; +import { Sound } from '../sound/types'; let drawnPlots: (DrawnPlot | CurvePlot)[] = []; diff --git a/src/bundles/plotly/sound_functions.ts b/src/bundles/plotly/sound_functions.ts index a56162594..aaa37334b 100644 --- a/src/bundles/plotly/sound_functions.ts +++ b/src/bundles/plotly/sound_functions.ts @@ -31,3 +31,5 @@ export function get_wave(sound: Sound): Wave { export function get_duration(sound: Sound): number { return tail(sound); } + +export type Sound; From 7b163bb15a6acbb592cc632a9c9a8ac4318f418c Mon Sep 17 00:00:00 2001 From: Sourabh Jaiswal Date: Thu, 16 Nov 2023 16:02:49 +0800 Subject: [PATCH 5/7] fix merge issues --- src/bundles/game/functions.ts | 2236 ++++++++++++------------- src/bundles/game/index.ts | 118 +- src/bundles/game/types.ts | 124 +- src/bundles/plotly/curve_functions.ts | 252 +-- src/bundles/plotly/functions.ts | 936 +++++------ src/bundles/plotly/index.ts | 26 +- src/bundles/plotly/plotly.ts | 122 +- src/bundles/plotly/sound_functions.ts | 68 +- src/bundles/repl/config.ts | 20 +- src/bundles/repl/functions.ts | 238 +-- src/bundles/repl/index.ts | 92 +- src/bundles/repl/programmable_repl.ts | 524 +++--- src/tabs/Plotly/index.tsx | 176 +- src/tabs/Repl/index.tsx | 358 ++-- 14 files changed, 2644 insertions(+), 2646 deletions(-) diff --git a/src/bundles/game/functions.ts b/src/bundles/game/functions.ts index ed7c6cd4b..6a3eae63c 100644 --- a/src/bundles/game/functions.ts +++ b/src/bundles/game/functions.ts @@ -1,1118 +1,1118 @@ -/** - * Game library that translates Phaser 3 API into Source. - * - * More in-depth explanation of the Phaser 3 API can be found at - * Phaser 3 documentation itself. - * - * For Phaser 3 API Documentation, check: - * https://photonstorm.github.io/phaser3-docs/ - * - * @module game - * @author Anthony Halim - * @author Chi Xu - * @author Chong Sia Tiffany - * @author Gokul Rajiv - */ - -/* eslint-disable consistent-return, @typescript-eslint/default-param-last, @typescript-eslint/no-shadow, @typescript-eslint/no-unused-vars */ -import { - type GameObject, - type ObjectConfig, - type RawContainer, - type RawGameElement, - type RawGameObject, - type RawInputObject, - defaultGameParams, -} from './types'; - -import context from 'js-slang/context'; -import { type List, head, tail, is_pair, accumulate } from 'js-slang/dist/stdlib/list'; - -if (!context.moduleContexts.game.state) { - context.moduleContexts.game.state = defaultGameParams; -} - -const { - preloadImageMap, - preloadSoundMap, - preloadSpritesheetMap, - remotePath, - screenSize, - createAward, -} = context.moduleContexts.game.state; - -// Listener ObjectTypes -enum ListenerTypes { - InputPlugin = 'input_plugin', - KeyboardKeyType = 'keyboard_key', -} - -const ListnerTypes = Object.values(ListenerTypes); - -// Object ObjectTypes -enum ObjectTypes { - ImageType = 'image', - TextType = 'text', - RectType = 'rect', - EllipseType = 'ellipse', - ContainerType = 'container', - AwardType = 'award', -} - -const ObjTypes = Object.values(ObjectTypes); - -const nullFn = () => {}; - -const mandatory = (obj, errMsg: string) => { - if (!obj) { - throw_error(errMsg); - } - return obj; -}; - -const scene = () => mandatory(context.moduleContexts.game.state.scene, 'No scene found!'); - -// ============================================================================= -// Module's Private Functions -// ============================================================================= - -/** @hidden */ -function get_obj( - obj: GameObject, -): RawGameObject | RawInputObject | RawContainer { - return obj.object!; -} - -/** @hidden */ -function get_game_obj(obj: GameObject): RawGameObject | RawContainer { - return obj.object as RawGameObject | RawContainer; -} - -/** @hidden */ -function get_input_obj(obj: GameObject): RawInputObject { - return obj.object as RawInputObject; -} - -/** @hidden */ -function get_container(obj: GameObject): RawContainer { - return obj.object as RawContainer; -} - -/** - * Checks whether the given game object is of the enquired type. - * If the given obj is undefined, will also return false. - * - * @param obj the game object - * @param type enquired type - * @returns if game object is of enquired type - * @hidden - */ -function is_type(obj: GameObject, type: string): boolean { - return obj !== undefined && obj.type === type && obj.object !== undefined; -} - -/** - * Checks whether the given game object is any of the enquired ObjectTypes - * - * @param obj the game object - * @param ObjectTypes enquired ObjectTypes - * @returns if game object is of any of the enquired ObjectTypes - * @hidden - */ -function is_any_type(obj: GameObject, types: string[]): boolean { - for (let i = 0; i < types.length; ++i) { - if (is_type(obj, types[i])) return true; - } - return false; -} - -/** - * Set a game object to the given type. - * Mutates the object. - * - * @param object the game object - * @param type type to set - * @returns typed game object - * @hidden - */ -function set_type( - object: RawGameObject | RawInputObject | RawContainer, - type: string, -): GameObject { - return { - type, - object, - }; -} - -/** - * Throw a console error, including the function caller name. - * - * @param {string} message error message - * @hidden - */ -function throw_error(message: string) { - // eslint-disable-next-line no-caller - throw new Error(`${arguments.callee.caller.name}: ${message}`); -} - -// ============================================================================= -// Module's Exposed Functions -// ============================================================================= - -// HELPER - -/** - * Prepend the given asset key with the remote path (S3 path). - * - * @param asset_key - * @returns prepended path - */ -export function prepend_remote_url(asset_key: string): string { - return remotePath(asset_key); -} - -/** - * Transforms the given list of pairs into an object config. The list follows - * the format of list(pair(key1, value1), pair(key2, value2), ...). - * - * e.g list(pair("alpha", 0), pair("duration", 1000)) - * - * @param lst the list to be turned into object config. - * @returns object config - */ -export function create_config(lst: List): ObjectConfig { - const config = {}; - accumulate((xs: [any, any], _) => { - if (!is_pair(xs)) { - throw_error('config element is not a pair!'); - } - config[head(xs)] = tail(xs); - }, null, lst); - return config; -} - -/** - * Create text config object, can be used to stylise text object. - * - * font_family: for available font_family, see: - * https://developer.mozilla.org/en-US/docs/Web/CSS/font-family#Valid_family_names - * - * align: must be either 'left', 'right', 'center', or 'justify' - * - * For more details about text config, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Types.GameObjects.Text.html#.TextStyle - * - * @param font_family font to be used - * @param font_size size of font, must be appended with 'px' e.g. '16px' - * @param color colour of font, in hex e.g. '#fff' - * @param stroke colour of stroke, in hex e.g. '#fff' - * @param stroke_thickness thickness of stroke - * @param align text alignment - * @returns text config - */ -export function create_text_config( - font_family: string = 'Courier', - font_size: string = '16px', - color: string = '#fff', - stroke: string = '#fff', - stroke_thickness: number = 0, - align: string = 'left', -): ObjectConfig { - return { - fontFamily: font_family, - fontSize: font_size, - color, - stroke, - strokeThickness: stroke_thickness, - align, - }; -} - -/** - * Create interactive config object, can be used to configure interactive settings. - * - * For more details about interactive config object, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Input.html#.InputConfiguration - * - * @param draggable object will be set draggable - * @param use_hand_cursor if true, pointer will be set to 'pointer' when a pointer is over it - * @param pixel_perfect pixel perfect function will be set for the hit area. Only works for texture based object - * @param alpha_tolerance if pixel_perfect is set, this is the alpha tolerance threshold value used in the callback - * @returns interactive config - */ -export function create_interactive_config( - draggable: boolean = false, - use_hand_cursor: boolean = false, - pixel_perfect: boolean = false, - alpha_tolerance: number = 1, -): ObjectConfig { - return { - draggable, - useHandCursor: use_hand_cursor, - pixelPerfect: pixel_perfect, - alphaTolerance: alpha_tolerance, - }; -} - -/** - * Create sound config object, can be used to configure sound settings. - * - * For more details about sound config object, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Sound.html#.SoundConfig - * - * @param mute whether the sound should be muted or not - * @param volume value between 0(silence) and 1(full volume) - * @param rate the speed at which the sound is played - * @param detune detuning of the sound, in cents - * @param seek position of playback for the sound, in seconds - * @param loop whether or not the sound should loop - * @param delay time, in seconds, that elapse before the sound actually starts - * @returns sound config - */ -export function create_sound_config( - mute: boolean = false, - volume: number = 1, - rate: number = 1, - detune: number = 0, - seek: number = 0, - loop: boolean = false, - delay: number = 0, -): ObjectConfig { - return { - mute, - volume, - rate, - detune, - seek, - loop, - delay, - }; -} - -/** - * Create tween config object, can be used to configure tween settings. - * - * For more details about tween config object, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Tweens.html#.TweenBuilderConfig - * - * @param target_prop target to tween, e.g. x, y, alpha - * @param target_value the property value to tween to - * @param delay time in ms/frames before tween will start - * @param duration duration of tween in ms/frames, exclude yoyos or repeats - * @param ease ease function to use, e.g. 'Power0', 'Power1', 'Power2' - * @param on_complete function to execute when tween completes - * @param yoyo if set to true, once tween complete, reverses the values incrementally to get back to the starting tween values - * @param loop number of times the tween should loop, or -1 to loop indefinitely - * @param loop_delay The time the tween will pause before starting either a yoyo or returning to the start for a repeat - * @param on_loop function to execute each time the tween loops - * @returns tween config - */ -export function create_tween_config( - target_prop: string = 'x', - target_value: string | number = 0, - delay: number = 0, - duration: number = 1000, - ease: Function | string = 'Power0', - on_complete: Function = nullFn, - yoyo: boolean = false, - loop: number = 0, - loop_delay: number = 0, - on_loop: Function = nullFn, -): ObjectConfig { - return { - [target_prop]: target_value, - delay, - duration, - ease, - onComplete: on_complete, - yoyo, - loop, - loopDelay: loop_delay, - onLoop: on_loop, - }; -} - -/** - * Create anims config, can be used to configure anims - * - * For more details about the config object, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Animations.html#.Animation - * - * @param anims_key key that the animation will be associated with - * @param anim_frames data used to generate the frames for animation - * @param frame_rate frame rate of playback in frames per second - * @param duration how long the animation should play in seconds. - * If null, will be derived from frame_rate - * @param repeat number of times to repeat the animation, -1 for infinity - * @param yoyo should the animation yoyo (reverse back down to the start) - * @param show_on_start should the sprite be visible when the anims start? - * @param hide_on_complete should the sprite be not visible when the anims finish? - * @returns animation config - */ -export function create_anim_config( - anims_key: string, - anim_frames: ObjectConfig[], - frame_rate: number = 24, - duration: any = null, - repeat: number = -1, - yoyo: boolean = false, - show_on_start: boolean = true, - hide_on_complete: boolean = false, -): ObjectConfig { - return { - key: anims_key, - frames: anim_frames, - frameRate: frame_rate, - duration, - repeat, - yoyo, - showOnStart: show_on_start, - hideOnComplete: hide_on_complete, - }; -} - -/** - * Create animation frame config, can be used to configure a specific frame - * within an animation. - * - * The key should refer to an image that is already loaded. - * To make frame_config from spritesheet based on its frames, - * use create_anim_spritesheet_frame_configs instead. - * - * @param key key that is associated with the sprite at this frame - * @param duration duration, in ms, of this frame of the animation - * @param visible should the parent object be visible during this frame? - * @returns animation frame config - */ -export function create_anim_frame_config( - key: string, - duration: number = 0, - visible: boolean = true, -): ObjectConfig { - return { - key, - duration, - visible, - }; -} - -/** - * Create list of animation frame config, can be used directly as part of - * anim_config's `frames` parameter. - * - * This function will generate list of frame configs based on the - * spritesheet_config attached to the associated spritesheet. - * This function requires that the given key is a spritesheet key - * i.e. a key associated with loaded spritesheet, loaded in using - * load_spritesheet function. - * - * Will return empty frame configs if key is not associated with - * a spritesheet. - * - * @param key key associated with spritesheet - * @returns animation frame configs - */ -export function create_anim_spritesheet_frame_configs( - key: string, -): ObjectConfig[] | undefined { - if (preloadSpritesheetMap.get(key)) { - const configArr = scene().anims.generateFrameNumbers(key, {}); - return configArr; - } - throw_error(`${key} is not associated with any spritesheet`); -} - -/** - * Create spritesheet config, can be used to configure the frames within the - * spritesheet. Can be used as config at load_spritesheet. - * - * @param frame_width width of frame in pixels - * @param frame_height height of frame in pixels - * @param start_frame first frame to start parsing from - * @param margin margin in the image; this is the space around the edge of the frames - * @param spacing the spacing between each frame in the image - * @returns spritesheet config - */ -export function create_spritesheet_config( - frame_width: number, - frame_height: number, - start_frame: number = 0, - margin: number = 0, - spacing: number = 0, -): ObjectConfig { - return { - frameWidth: frame_width, - frameHeight: frame_height, - startFrame: start_frame, - margin, - spacing, - }; -} - -// SCREEN - -/** - * Get in-game screen width. - * - * @return screen width - */ -export function get_screen_width(): number { - return screenSize.x; -} - -/** - * Get in-game screen height. - * - * @return screen height - */ -export function get_screen_height(): number { - return screenSize.y; -} - -/** - * Get game screen display width (accounting window size). - * - * @return screen display width - */ -export function get_screen_display_width(): number { - return scene().scale.displaySize.width; -} - -/** - * Get game screen display height (accounting window size). - * - * @return screen display height - */ -export function get_screen_display_height(): number { - return scene().scale.displaySize.height; -} - -// LOAD - -/** - * Load the image asset into the scene for use. All images - * must be loaded before used in create_image. - * - * @param key key to be associated with the image - * @param url path to the image - */ -export function load_image(key: string, url: string) { - preloadImageMap.set(key, url); -} - -/** - * Load the sound asset into the scene for use. All sound - * must be loaded before used in play_sound. - * - * @param key key to be associated with the sound - * @param url path to the sound - */ -export function load_sound(key: string, url: string) { - preloadSoundMap.set(key, url); -} - -/** - * Load the spritesheet into the scene for use. All spritesheet must - * be loaded before used in create_image. - * - * @param key key associated with the spritesheet - * @param url path to the sound - * @param spritesheet_config config to determines frames within the spritesheet - */ -export function load_spritesheet( - key: string, - url: string, - spritesheet_config: ObjectConfig, -) { - preloadSpritesheetMap.set(key, [url, spritesheet_config]); -} - -// ADD - -/** - * Add the object to the scene. Only objects added to the scene - * will appear. - * - * @param obj game object to be added - */ -export function add(obj: GameObject): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - scene().add.existing(get_game_obj(obj)); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); -} - -// SOUND - -/** - * Play the sound associated with the key. - * Throws error if key is non-existent. - * - * @param key key to the sound to be played - * @param config sound config to be used - */ -export function play_sound(key: string, config: ObjectConfig = {}): void { - if (preloadSoundMap.get(key)) { - scene().sound.play(key, config); - } else { - throw_error(`${key} is not associated with any sound`); - } -} - -// ANIMS - -/** - * Create a new animation and add it to the available animations. - * Animations are global i.e. once created, it can be used anytime, anywhere. - * - * NOTE: Anims DO NOT need to be added into the scene to be used. - * It is automatically added to the scene when it is created. - * - * Will return true if the animation key is valid - * (key is specified within the anim_config); false if the key - * is already in use. - * - * @param anim_config - * @returns true if animation is successfully created, false otherwise - */ -export function create_anim(anim_config: ObjectConfig): boolean { - const anims = scene().anims.create(anim_config); - return typeof anims !== 'boolean'; -} - -/** - * Start playing the given animation on image game object. - * - * @param image image game object - * @param anims_key key associated with an animation - */ -export function play_anim_on_image( - image: GameObject, - anims_key: string, -): GameObject | undefined { - if (is_type(image, ObjectTypes.ImageType)) { - (get_obj(image) as Phaser.GameObjects.Sprite).play(anims_key); - return image; - } - throw_error(`${image} is not of type ${ObjectTypes.ImageType}`); -} - -// IMAGE - -/** - * Create an image using the key associated with a loaded image. - * If key is not associated with any loaded image, throws error. - * - * 0, 0 is located at the top, left hand side. - * - * @param x x position of the image. 0 is at the left side - * @param y y position of the image. 0 is at the top side - * @param asset_key key to loaded image - * @returns image game object - */ -export function create_image( - x: number, - y: number, - asset_key: string, -): GameObject | undefined { - if ( - preloadImageMap.get(asset_key) - || preloadSpritesheetMap.get(asset_key) - ) { - const image = new Phaser.GameObjects.Sprite(scene(), x, y, asset_key); - return set_type(image, ObjectTypes.ImageType); - } - throw_error(`${asset_key} is not associated with any image`); -} - -// AWARD - -/** - * Create an award using the key associated with the award. - * The award key can be obtained from the Awards Hall or - * Awards menu, after attaining the award. - * - * Valid award will have an on-hover VERIFIED tag to distinguish - * it from images created by create_image. - * - * If student does not possess the award, this function will - * return a untagged, default image. - * - * @param x x position of the image. 0 is at the left side - * @param y y position of the image. 0 is at the top side - * @param award_key key for award - * @returns award game object - */ -export function create_award(x: number, y: number, award_key: string): GameObject { - return set_type(createAward(x, y, award_key), ObjectTypes.AwardType); -} - -// TEXT - -/** - * Create a text object. - * - * 0, 0 is located at the top, left hand side. - * - * @param x x position of the text - * @param y y position of the text - * @param text text to be shown - * @param config text configuration to be used - * @returns text game object - */ -export function create_text( - x: number, - y: number, - text: string, - config: ObjectConfig = {}, -): GameObject { - const txt = new Phaser.GameObjects.Text(scene(), x, y, text, config); - return set_type(txt, ObjectTypes.TextType); -} - -// RECTANGLE - -/** - * Create a rectangle object. - * - * 0, 0 is located at the top, left hand side. - * - * @param x x coordinate of the top, left corner posiiton - * @param y y coordinate of the top, left corner position - * @param width width of rectangle - * @param height height of rectangle - * @param fill colour fill, in hext e.g 0xffffff - * @param alpha value between 0 and 1 to denote alpha - * @returns rectangle object - */ -export function create_rect( - x: number, - y: number, - width: number, - height: number, - fill: number = 0, - alpha: number = 1, -): GameObject { - const rect = new Phaser.GameObjects.Rectangle( - scene(), - x, - y, - width, - height, - fill, - alpha, - ); - return set_type(rect, ObjectTypes.RectType); -} - -// ELLIPSE - -/** - * Create an ellipse object. - * - * @param x x coordinate of the centre of ellipse - * @param y y coordinate of the centre of ellipse - * @param width width of ellipse - * @param height height of ellipse - * @param fill colour fill, in hext e.g 0xffffff - * @param alpha value between 0 and 1 to denote alpha - * @returns ellipse object - */ -export function create_ellipse( - x: number, - y: number, - width: number, - height: number, - fill: number = 0, - alpha: number = 1, -): GameObject { - const ellipse = new Phaser.GameObjects.Ellipse( - scene(), - x, - y, - width, - height, - fill, - alpha, - ); - return set_type(ellipse, ObjectTypes.EllipseType); -} - -// CONTAINER - -/** - * Create a container object. Container is able to contain any other game object, - * and the positions of contained game object will be relative to the container. - * - * Rendering the container as visible or invisible will also affect the contained - * game object. - * - * Container can also contain another container. - * - * 0, 0 is located at the top, left hand side. - * - * For more details about container object, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.GameObjects.Container.html - * - * @param x x position of the container - * @param y y position of the container - * @returns container object - */ -export function create_container(x: number, y: number): GameObject { - const cont = new Phaser.GameObjects.Container(scene(), x, y); - return set_type(cont, ObjectTypes.ContainerType); -} - -/** - * Add the given game object to the container. - * Mutates the container. - * - * @param container container object - * @param obj game object to add to the container - * @returns container object - */ -export function add_to_container( - container: GameObject, - obj: GameObject, -): GameObject | undefined { - if ( - is_type(container, ObjectTypes.ContainerType) - && is_any_type(obj, ObjTypes) - ) { - get_container(container) - .add(get_game_obj(obj)); - return container; - } - throw_error( - `${obj} is not of type ${ObjTypes} or ${container} is not of type ${ObjectTypes.ContainerType}`, - ); -} - -// OBJECT - -/** - * Destroy the given game object. Destroyed game object - * is removed from the scene, and all of its listeners - * is also removed. - * - * @param obj game object itself - */ -export function destroy_obj(obj: GameObject) { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj) - .destroy(); - } else { - throw_error(`${obj} is not of type ${ObjTypes}`); - } -} - -/** - * Set the display size of the object. - * Mutate the object. - * - * @param obj object to be set - * @param x new display width size - * @param y new display height size - * @returns game object itself - */ -export function set_display_size( - obj: GameObject, - x: number, - y: number, -): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj) - .setDisplaySize(x, y); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); -} - -/** - * Set the alpha of the object. - * Mutate the object. - * - * @param obj object to be set - * @param alpha new alpha - * @returns game object itself - */ -export function set_alpha(obj: GameObject, alpha: number): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj) - .setAlpha(alpha); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); -} - -/** - * Set the interactivity of the object. - * Mutate the object. - * - * Rectangle and Ellipse are not able to receive configs, only boolean - * i.e. set_interactive(rect, true); set_interactive(ellipse, false) - * - * @param obj object to be set - * @param config interactive config to be used - * @returns game object itself - */ -export function set_interactive( - obj: GameObject, - config: ObjectConfig = {}, -): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj) - .setInteractive(config); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); -} - -/** - * Set the origin in which all position related will be relative to. - * In other words, the anchor of the object. - * Mutate the object. - * - * @param obj object to be set - * @param x new anchor x coordinate, between value 0 to 1. - * @param y new anchor y coordinate, between value 0 to 1. - * @returns game object itself - */ -export function set_origin( - obj: GameObject, - x: number, - y: number, -): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - (get_game_obj(obj) as RawGameObject).setOrigin(x, y); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); -} - -/** - * Set the position of the game object - * Mutate the object - * - * @param obj object to be set - * @param x new x position - * @param y new y position - * @returns game object itself - */ -export function set_position( - obj: GameObject, - x: number, - y: number, -): GameObject | undefined { - if (obj && is_any_type(obj, ObjTypes)) { - get_game_obj(obj) - .setPosition(x, y); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); -} - -/** - * Set the scale of the object. - * Mutate the object. - * - * @param obj object to be set - * @param x new x scale - * @param y new y scale - * @returns game object itself - */ -export function set_scale( - obj: GameObject, - x: number, - y: number, -): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj) - .setScale(x, y); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); -} - -/** - * Set the rotation of the object. - * Mutate the object. - * - * @param obj object to be set - * @param rad the rotation, in radians - * @returns game object itself - */ -export function set_rotation(obj: GameObject, rad: number): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj) - .setRotation(rad); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); -} - -/** - * Sets the horizontal and flipped state of the object. - * Mutate the object. - * - * @param obj game object itself - * @param x to flip in the horizontal state - * @param y to flip in the vertical state - * @returns game object itself - */ -export function set_flip( - obj: GameObject, - x: boolean, - y: boolean, -): GameObject | undefined { - const GameElementType = [ObjectTypes.ImageType, ObjectTypes.TextType]; - if (is_any_type(obj, GameElementType)) { - (get_obj(obj) as RawGameElement).setFlip(x, y); - return obj; - } - throw_error(`${obj} is not of type ${GameElementType}`); -} - -/** - * Creates a tween to the object and plays it. - * Mutate the object. - * - * @param obj object to be added to - * @param config tween config - * @returns game object itself - */ -export async function add_tween( - obj: GameObject, - config: ObjectConfig = {}, -): Promise { - if (is_any_type(obj, ObjTypes)) { - scene().tweens.add({ - targets: get_game_obj(obj), - ...config, - }); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); -} - -// LISTENER - -/** - * Attach a listener to the object. The callback will be executed - * when the event is emitted. - * Mutate the object. - * - * For all available events, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Input.Events.html - * - * @param obj object to be added to - * @param event the event name - * @param callback listener function, executed on event - * @returns listener game object - */ -export function add_listener( - obj: GameObject, - event: string, - callback: Function, -): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - const listener = get_game_obj(obj) - .addListener(event, callback); - return set_type(listener, ListenerTypes.InputPlugin); - } - throw_error(`${obj} is not of type ${ObjTypes}`); -} - -/** - * Attach a listener to the object. The callback will be executed - * when the event is emitted. - * Mutate the object. - * - * For all available events, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Input.Events.html - * - * For list of keycodes, see: - * https://github.com/photonstorm/phaser/blob/v3.22.0/src/input/keyboard/keys/KeyCodes.js - * - * @param key keyboard key to trigger listener - * @param event the event name - * @param callback listener function, executed on event - * @returns listener game object - */ -export function add_keyboard_listener( - key: string | number, - event: string, - callback: Function, -): GameObject { - const keyObj = scene().input.keyboard.addKey(key); - const keyboardListener = keyObj.addListener(event, callback); - return set_type(keyboardListener, ListenerTypes.KeyboardKeyType); -} - -/** - * Deactivate and remove listener. - * - * @param listener - * @returns if successful - */ -export function remove_listener(listener: GameObject): boolean { - if (is_any_type(listener, ListnerTypes)) { - get_input_obj(listener) - .removeAllListeners(); - return true; - } - return false; -} - -const gameFunctions = [ - add, - add_listener, - add_keyboard_listener, - add_to_container, - add_tween, - create_anim, - create_anim_config, - create_anim_frame_config, - create_anim_spritesheet_frame_configs, - create_award, - create_config, - create_container, - create_ellipse, - create_image, - create_interactive_config, - create_rect, - create_text, - create_text_config, - create_tween_config, - create_sound_config, - create_spritesheet_config, - destroy_obj, - get_screen_width, - get_screen_height, - get_screen_display_width, - get_screen_display_height, - load_image, - load_sound, - load_spritesheet, - play_anim_on_image, - play_sound, - prepend_remote_url, - remove_listener, - set_alpha, - set_display_size, - set_flip, - set_interactive, - set_origin, - set_position, - set_rotation, - set_scale, -]; - -// Inject minArgsNeeded to allow module varargs -// Remove if module varargs is fixed on js-slang side -gameFunctions.forEach((fn) => { - const dummy = fn as any; - dummy.minArgsNeeded = fn.length; -}); +/** + * Game library that translates Phaser 3 API into Source. + * + * More in-depth explanation of the Phaser 3 API can be found at + * Phaser 3 documentation itself. + * + * For Phaser 3 API Documentation, check: + * https://photonstorm.github.io/phaser3-docs/ + * + * @module game + * @author Anthony Halim + * @author Chi Xu + * @author Chong Sia Tiffany + * @author Gokul Rajiv + */ + +/* eslint-disable consistent-return, @typescript-eslint/default-param-last, @typescript-eslint/no-shadow, @typescript-eslint/no-unused-vars */ +import { + type GameObject, + type ObjectConfig, + type RawContainer, + type RawGameElement, + type RawGameObject, + type RawInputObject, + defaultGameParams, +} from './types'; + +import context from 'js-slang/context'; +import { type List, head, tail, is_pair, accumulate } from 'js-slang/dist/stdlib/list'; + +if (!context.moduleContexts.game.state) { + context.moduleContexts.game.state = defaultGameParams; +} + +const { + preloadImageMap, + preloadSoundMap, + preloadSpritesheetMap, + remotePath, + screenSize, + createAward, +} = context.moduleContexts.game.state; + +// Listener ObjectTypes +enum ListenerTypes { + InputPlugin = 'input_plugin', + KeyboardKeyType = 'keyboard_key', +} + +const ListnerTypes = Object.values(ListenerTypes); + +// Object ObjectTypes +enum ObjectTypes { + ImageType = 'image', + TextType = 'text', + RectType = 'rect', + EllipseType = 'ellipse', + ContainerType = 'container', + AwardType = 'award', +} + +const ObjTypes = Object.values(ObjectTypes); + +const nullFn = () => {}; + +const mandatory = (obj, errMsg: string) => { + if (!obj) { + throw_error(errMsg); + } + return obj; +}; + +const scene = () => mandatory(context.moduleContexts.game.state.scene, 'No scene found!'); + +// ============================================================================= +// Module's Private Functions +// ============================================================================= + +/** @hidden */ +function get_obj( + obj: GameObject, +): RawGameObject | RawInputObject | RawContainer { + return obj.object!; +} + +/** @hidden */ +function get_game_obj(obj: GameObject): RawGameObject | RawContainer { + return obj.object as RawGameObject | RawContainer; +} + +/** @hidden */ +function get_input_obj(obj: GameObject): RawInputObject { + return obj.object as RawInputObject; +} + +/** @hidden */ +function get_container(obj: GameObject): RawContainer { + return obj.object as RawContainer; +} + +/** + * Checks whether the given game object is of the enquired type. + * If the given obj is undefined, will also return false. + * + * @param obj the game object + * @param type enquired type + * @returns if game object is of enquired type + * @hidden + */ +function is_type(obj: GameObject, type: string): boolean { + return obj !== undefined && obj.type === type && obj.object !== undefined; +} + +/** + * Checks whether the given game object is any of the enquired ObjectTypes + * + * @param obj the game object + * @param ObjectTypes enquired ObjectTypes + * @returns if game object is of any of the enquired ObjectTypes + * @hidden + */ +function is_any_type(obj: GameObject, types: string[]): boolean { + for (let i = 0; i < types.length; ++i) { + if (is_type(obj, types[i])) return true; + } + return false; +} + +/** + * Set a game object to the given type. + * Mutates the object. + * + * @param object the game object + * @param type type to set + * @returns typed game object + * @hidden + */ +function set_type( + object: RawGameObject | RawInputObject | RawContainer, + type: string, +): GameObject { + return { + type, + object, + }; +} + +/** + * Throw a console error, including the function caller name. + * + * @param {string} message error message + * @hidden + */ +function throw_error(message: string) { + // eslint-disable-next-line no-caller + throw new Error(`${arguments.callee.caller.name}: ${message}`); +} + +// ============================================================================= +// Module's Exposed Functions +// ============================================================================= + +// HELPER + +/** + * Prepend the given asset key with the remote path (S3 path). + * + * @param asset_key + * @returns prepended path + */ +export function prepend_remote_url(asset_key: string): string { + return remotePath(asset_key); +} + +/** + * Transforms the given list of pairs into an object config. The list follows + * the format of list(pair(key1, value1), pair(key2, value2), ...). + * + * e.g list(pair("alpha", 0), pair("duration", 1000)) + * + * @param lst the list to be turned into object config. + * @returns object config + */ +export function create_config(lst: List): ObjectConfig { + const config = {}; + accumulate((xs: [any, any], _) => { + if (!is_pair(xs)) { + throw_error('config element is not a pair!'); + } + config[head(xs)] = tail(xs); + }, null, lst); + return config; +} + +/** + * Create text config object, can be used to stylise text object. + * + * font_family: for available font_family, see: + * https://developer.mozilla.org/en-US/docs/Web/CSS/font-family#Valid_family_names + * + * align: must be either 'left', 'right', 'center', or 'justify' + * + * For more details about text config, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Types.GameObjects.Text.html#.TextStyle + * + * @param font_family font to be used + * @param font_size size of font, must be appended with 'px' e.g. '16px' + * @param color colour of font, in hex e.g. '#fff' + * @param stroke colour of stroke, in hex e.g. '#fff' + * @param stroke_thickness thickness of stroke + * @param align text alignment + * @returns text config + */ +export function create_text_config( + font_family: string = 'Courier', + font_size: string = '16px', + color: string = '#fff', + stroke: string = '#fff', + stroke_thickness: number = 0, + align: string = 'left', +): ObjectConfig { + return { + fontFamily: font_family, + fontSize: font_size, + color, + stroke, + strokeThickness: stroke_thickness, + align, + }; +} + +/** + * Create interactive config object, can be used to configure interactive settings. + * + * For more details about interactive config object, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Input.html#.InputConfiguration + * + * @param draggable object will be set draggable + * @param use_hand_cursor if true, pointer will be set to 'pointer' when a pointer is over it + * @param pixel_perfect pixel perfect function will be set for the hit area. Only works for texture based object + * @param alpha_tolerance if pixel_perfect is set, this is the alpha tolerance threshold value used in the callback + * @returns interactive config + */ +export function create_interactive_config( + draggable: boolean = false, + use_hand_cursor: boolean = false, + pixel_perfect: boolean = false, + alpha_tolerance: number = 1, +): ObjectConfig { + return { + draggable, + useHandCursor: use_hand_cursor, + pixelPerfect: pixel_perfect, + alphaTolerance: alpha_tolerance, + }; +} + +/** + * Create sound config object, can be used to configure sound settings. + * + * For more details about sound config object, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Sound.html#.SoundConfig + * + * @param mute whether the sound should be muted or not + * @param volume value between 0(silence) and 1(full volume) + * @param rate the speed at which the sound is played + * @param detune detuning of the sound, in cents + * @param seek position of playback for the sound, in seconds + * @param loop whether or not the sound should loop + * @param delay time, in seconds, that elapse before the sound actually starts + * @returns sound config + */ +export function create_sound_config( + mute: boolean = false, + volume: number = 1, + rate: number = 1, + detune: number = 0, + seek: number = 0, + loop: boolean = false, + delay: number = 0, +): ObjectConfig { + return { + mute, + volume, + rate, + detune, + seek, + loop, + delay, + }; +} + +/** + * Create tween config object, can be used to configure tween settings. + * + * For more details about tween config object, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Tweens.html#.TweenBuilderConfig + * + * @param target_prop target to tween, e.g. x, y, alpha + * @param target_value the property value to tween to + * @param delay time in ms/frames before tween will start + * @param duration duration of tween in ms/frames, exclude yoyos or repeats + * @param ease ease function to use, e.g. 'Power0', 'Power1', 'Power2' + * @param on_complete function to execute when tween completes + * @param yoyo if set to true, once tween complete, reverses the values incrementally to get back to the starting tween values + * @param loop number of times the tween should loop, or -1 to loop indefinitely + * @param loop_delay The time the tween will pause before starting either a yoyo or returning to the start for a repeat + * @param on_loop function to execute each time the tween loops + * @returns tween config + */ +export function create_tween_config( + target_prop: string = 'x', + target_value: string | number = 0, + delay: number = 0, + duration: number = 1000, + ease: Function | string = 'Power0', + on_complete: Function = nullFn, + yoyo: boolean = false, + loop: number = 0, + loop_delay: number = 0, + on_loop: Function = nullFn, +): ObjectConfig { + return { + [target_prop]: target_value, + delay, + duration, + ease, + onComplete: on_complete, + yoyo, + loop, + loopDelay: loop_delay, + onLoop: on_loop, + }; +} + +/** + * Create anims config, can be used to configure anims + * + * For more details about the config object, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Animations.html#.Animation + * + * @param anims_key key that the animation will be associated with + * @param anim_frames data used to generate the frames for animation + * @param frame_rate frame rate of playback in frames per second + * @param duration how long the animation should play in seconds. + * If null, will be derived from frame_rate + * @param repeat number of times to repeat the animation, -1 for infinity + * @param yoyo should the animation yoyo (reverse back down to the start) + * @param show_on_start should the sprite be visible when the anims start? + * @param hide_on_complete should the sprite be not visible when the anims finish? + * @returns animation config + */ +export function create_anim_config( + anims_key: string, + anim_frames: ObjectConfig[], + frame_rate: number = 24, + duration: any = null, + repeat: number = -1, + yoyo: boolean = false, + show_on_start: boolean = true, + hide_on_complete: boolean = false, +): ObjectConfig { + return { + key: anims_key, + frames: anim_frames, + frameRate: frame_rate, + duration, + repeat, + yoyo, + showOnStart: show_on_start, + hideOnComplete: hide_on_complete, + }; +} + +/** + * Create animation frame config, can be used to configure a specific frame + * within an animation. + * + * The key should refer to an image that is already loaded. + * To make frame_config from spritesheet based on its frames, + * use create_anim_spritesheet_frame_configs instead. + * + * @param key key that is associated with the sprite at this frame + * @param duration duration, in ms, of this frame of the animation + * @param visible should the parent object be visible during this frame? + * @returns animation frame config + */ +export function create_anim_frame_config( + key: string, + duration: number = 0, + visible: boolean = true, +): ObjectConfig { + return { + key, + duration, + visible, + }; +} + +/** + * Create list of animation frame config, can be used directly as part of + * anim_config's `frames` parameter. + * + * This function will generate list of frame configs based on the + * spritesheet_config attached to the associated spritesheet. + * This function requires that the given key is a spritesheet key + * i.e. a key associated with loaded spritesheet, loaded in using + * load_spritesheet function. + * + * Will return empty frame configs if key is not associated with + * a spritesheet. + * + * @param key key associated with spritesheet + * @returns animation frame configs + */ +export function create_anim_spritesheet_frame_configs( + key: string, +): ObjectConfig[] | undefined { + if (preloadSpritesheetMap.get(key)) { + const configArr = scene().anims.generateFrameNumbers(key, {}); + return configArr; + } + throw_error(`${key} is not associated with any spritesheet`); +} + +/** + * Create spritesheet config, can be used to configure the frames within the + * spritesheet. Can be used as config at load_spritesheet. + * + * @param frame_width width of frame in pixels + * @param frame_height height of frame in pixels + * @param start_frame first frame to start parsing from + * @param margin margin in the image; this is the space around the edge of the frames + * @param spacing the spacing between each frame in the image + * @returns spritesheet config + */ +export function create_spritesheet_config( + frame_width: number, + frame_height: number, + start_frame: number = 0, + margin: number = 0, + spacing: number = 0, +): ObjectConfig { + return { + frameWidth: frame_width, + frameHeight: frame_height, + startFrame: start_frame, + margin, + spacing, + }; +} + +// SCREEN + +/** + * Get in-game screen width. + * + * @return screen width + */ +export function get_screen_width(): number { + return screenSize.x; +} + +/** + * Get in-game screen height. + * + * @return screen height + */ +export function get_screen_height(): number { + return screenSize.y; +} + +/** + * Get game screen display width (accounting window size). + * + * @return screen display width + */ +export function get_screen_display_width(): number { + return scene().scale.displaySize.width; +} + +/** + * Get game screen display height (accounting window size). + * + * @return screen display height + */ +export function get_screen_display_height(): number { + return scene().scale.displaySize.height; +} + +// LOAD + +/** + * Load the image asset into the scene for use. All images + * must be loaded before used in create_image. + * + * @param key key to be associated with the image + * @param url path to the image + */ +export function load_image(key: string, url: string) { + preloadImageMap.set(key, url); +} + +/** + * Load the sound asset into the scene for use. All sound + * must be loaded before used in play_sound. + * + * @param key key to be associated with the sound + * @param url path to the sound + */ +export function load_sound(key: string, url: string) { + preloadSoundMap.set(key, url); +} + +/** + * Load the spritesheet into the scene for use. All spritesheet must + * be loaded before used in create_image. + * + * @param key key associated with the spritesheet + * @param url path to the sound + * @param spritesheet_config config to determines frames within the spritesheet + */ +export function load_spritesheet( + key: string, + url: string, + spritesheet_config: ObjectConfig, +) { + preloadSpritesheetMap.set(key, [url, spritesheet_config]); +} + +// ADD + +/** + * Add the object to the scene. Only objects added to the scene + * will appear. + * + * @param obj game object to be added + */ +export function add(obj: GameObject): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + scene().add.existing(get_game_obj(obj)); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); +} + +// SOUND + +/** + * Play the sound associated with the key. + * Throws error if key is non-existent. + * + * @param key key to the sound to be played + * @param config sound config to be used + */ +export function play_sound(key: string, config: ObjectConfig = {}): void { + if (preloadSoundMap.get(key)) { + scene().sound.play(key, config); + } else { + throw_error(`${key} is not associated with any sound`); + } +} + +// ANIMS + +/** + * Create a new animation and add it to the available animations. + * Animations are global i.e. once created, it can be used anytime, anywhere. + * + * NOTE: Anims DO NOT need to be added into the scene to be used. + * It is automatically added to the scene when it is created. + * + * Will return true if the animation key is valid + * (key is specified within the anim_config); false if the key + * is already in use. + * + * @param anim_config + * @returns true if animation is successfully created, false otherwise + */ +export function create_anim(anim_config: ObjectConfig): boolean { + const anims = scene().anims.create(anim_config); + return typeof anims !== 'boolean'; +} + +/** + * Start playing the given animation on image game object. + * + * @param image image game object + * @param anims_key key associated with an animation + */ +export function play_anim_on_image( + image: GameObject, + anims_key: string, +): GameObject | undefined { + if (is_type(image, ObjectTypes.ImageType)) { + (get_obj(image) as Phaser.GameObjects.Sprite).play(anims_key); + return image; + } + throw_error(`${image} is not of type ${ObjectTypes.ImageType}`); +} + +// IMAGE + +/** + * Create an image using the key associated with a loaded image. + * If key is not associated with any loaded image, throws error. + * + * 0, 0 is located at the top, left hand side. + * + * @param x x position of the image. 0 is at the left side + * @param y y position of the image. 0 is at the top side + * @param asset_key key to loaded image + * @returns image game object + */ +export function create_image( + x: number, + y: number, + asset_key: string, +): GameObject | undefined { + if ( + preloadImageMap.get(asset_key) + || preloadSpritesheetMap.get(asset_key) + ) { + const image = new Phaser.GameObjects.Sprite(scene(), x, y, asset_key); + return set_type(image, ObjectTypes.ImageType); + } + throw_error(`${asset_key} is not associated with any image`); +} + +// AWARD + +/** + * Create an award using the key associated with the award. + * The award key can be obtained from the Awards Hall or + * Awards menu, after attaining the award. + * + * Valid award will have an on-hover VERIFIED tag to distinguish + * it from images created by create_image. + * + * If student does not possess the award, this function will + * return a untagged, default image. + * + * @param x x position of the image. 0 is at the left side + * @param y y position of the image. 0 is at the top side + * @param award_key key for award + * @returns award game object + */ +export function create_award(x: number, y: number, award_key: string): GameObject { + return set_type(createAward(x, y, award_key), ObjectTypes.AwardType); +} + +// TEXT + +/** + * Create a text object. + * + * 0, 0 is located at the top, left hand side. + * + * @param x x position of the text + * @param y y position of the text + * @param text text to be shown + * @param config text configuration to be used + * @returns text game object + */ +export function create_text( + x: number, + y: number, + text: string, + config: ObjectConfig = {}, +): GameObject { + const txt = new Phaser.GameObjects.Text(scene(), x, y, text, config); + return set_type(txt, ObjectTypes.TextType); +} + +// RECTANGLE + +/** + * Create a rectangle object. + * + * 0, 0 is located at the top, left hand side. + * + * @param x x coordinate of the top, left corner posiiton + * @param y y coordinate of the top, left corner position + * @param width width of rectangle + * @param height height of rectangle + * @param fill colour fill, in hext e.g 0xffffff + * @param alpha value between 0 and 1 to denote alpha + * @returns rectangle object + */ +export function create_rect( + x: number, + y: number, + width: number, + height: number, + fill: number = 0, + alpha: number = 1, +): GameObject { + const rect = new Phaser.GameObjects.Rectangle( + scene(), + x, + y, + width, + height, + fill, + alpha, + ); + return set_type(rect, ObjectTypes.RectType); +} + +// ELLIPSE + +/** + * Create an ellipse object. + * + * @param x x coordinate of the centre of ellipse + * @param y y coordinate of the centre of ellipse + * @param width width of ellipse + * @param height height of ellipse + * @param fill colour fill, in hext e.g 0xffffff + * @param alpha value between 0 and 1 to denote alpha + * @returns ellipse object + */ +export function create_ellipse( + x: number, + y: number, + width: number, + height: number, + fill: number = 0, + alpha: number = 1, +): GameObject { + const ellipse = new Phaser.GameObjects.Ellipse( + scene(), + x, + y, + width, + height, + fill, + alpha, + ); + return set_type(ellipse, ObjectTypes.EllipseType); +} + +// CONTAINER + +/** + * Create a container object. Container is able to contain any other game object, + * and the positions of contained game object will be relative to the container. + * + * Rendering the container as visible or invisible will also affect the contained + * game object. + * + * Container can also contain another container. + * + * 0, 0 is located at the top, left hand side. + * + * For more details about container object, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.GameObjects.Container.html + * + * @param x x position of the container + * @param y y position of the container + * @returns container object + */ +export function create_container(x: number, y: number): GameObject { + const cont = new Phaser.GameObjects.Container(scene(), x, y); + return set_type(cont, ObjectTypes.ContainerType); +} + +/** + * Add the given game object to the container. + * Mutates the container. + * + * @param container container object + * @param obj game object to add to the container + * @returns container object + */ +export function add_to_container( + container: GameObject, + obj: GameObject, +): GameObject | undefined { + if ( + is_type(container, ObjectTypes.ContainerType) + && is_any_type(obj, ObjTypes) + ) { + get_container(container) + .add(get_game_obj(obj)); + return container; + } + throw_error( + `${obj} is not of type ${ObjTypes} or ${container} is not of type ${ObjectTypes.ContainerType}`, + ); +} + +// OBJECT + +/** + * Destroy the given game object. Destroyed game object + * is removed from the scene, and all of its listeners + * is also removed. + * + * @param obj game object itself + */ +export function destroy_obj(obj: GameObject) { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .destroy(); + } else { + throw_error(`${obj} is not of type ${ObjTypes}`); + } +} + +/** + * Set the display size of the object. + * Mutate the object. + * + * @param obj object to be set + * @param x new display width size + * @param y new display height size + * @returns game object itself + */ +export function set_display_size( + obj: GameObject, + x: number, + y: number, +): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setDisplaySize(x, y); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); +} + +/** + * Set the alpha of the object. + * Mutate the object. + * + * @param obj object to be set + * @param alpha new alpha + * @returns game object itself + */ +export function set_alpha(obj: GameObject, alpha: number): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setAlpha(alpha); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); +} + +/** + * Set the interactivity of the object. + * Mutate the object. + * + * Rectangle and Ellipse are not able to receive configs, only boolean + * i.e. set_interactive(rect, true); set_interactive(ellipse, false) + * + * @param obj object to be set + * @param config interactive config to be used + * @returns game object itself + */ +export function set_interactive( + obj: GameObject, + config: ObjectConfig = {}, +): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setInteractive(config); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); +} + +/** + * Set the origin in which all position related will be relative to. + * In other words, the anchor of the object. + * Mutate the object. + * + * @param obj object to be set + * @param x new anchor x coordinate, between value 0 to 1. + * @param y new anchor y coordinate, between value 0 to 1. + * @returns game object itself + */ +export function set_origin( + obj: GameObject, + x: number, + y: number, +): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + (get_game_obj(obj) as RawGameObject).setOrigin(x, y); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); +} + +/** + * Set the position of the game object + * Mutate the object + * + * @param obj object to be set + * @param x new x position + * @param y new y position + * @returns game object itself + */ +export function set_position( + obj: GameObject, + x: number, + y: number, +): GameObject | undefined { + if (obj && is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setPosition(x, y); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); +} + +/** + * Set the scale of the object. + * Mutate the object. + * + * @param obj object to be set + * @param x new x scale + * @param y new y scale + * @returns game object itself + */ +export function set_scale( + obj: GameObject, + x: number, + y: number, +): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setScale(x, y); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); +} + +/** + * Set the rotation of the object. + * Mutate the object. + * + * @param obj object to be set + * @param rad the rotation, in radians + * @returns game object itself + */ +export function set_rotation(obj: GameObject, rad: number): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setRotation(rad); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); +} + +/** + * Sets the horizontal and flipped state of the object. + * Mutate the object. + * + * @param obj game object itself + * @param x to flip in the horizontal state + * @param y to flip in the vertical state + * @returns game object itself + */ +export function set_flip( + obj: GameObject, + x: boolean, + y: boolean, +): GameObject | undefined { + const GameElementType = [ObjectTypes.ImageType, ObjectTypes.TextType]; + if (is_any_type(obj, GameElementType)) { + (get_obj(obj) as RawGameElement).setFlip(x, y); + return obj; + } + throw_error(`${obj} is not of type ${GameElementType}`); +} + +/** + * Creates a tween to the object and plays it. + * Mutate the object. + * + * @param obj object to be added to + * @param config tween config + * @returns game object itself + */ +export async function add_tween( + obj: GameObject, + config: ObjectConfig = {}, +): Promise { + if (is_any_type(obj, ObjTypes)) { + scene().tweens.add({ + targets: get_game_obj(obj), + ...config, + }); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); +} + +// LISTENER + +/** + * Attach a listener to the object. The callback will be executed + * when the event is emitted. + * Mutate the object. + * + * For all available events, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Input.Events.html + * + * @param obj object to be added to + * @param event the event name + * @param callback listener function, executed on event + * @returns listener game object + */ +export function add_listener( + obj: GameObject, + event: string, + callback: Function, +): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + const listener = get_game_obj(obj) + .addListener(event, callback); + return set_type(listener, ListenerTypes.InputPlugin); + } + throw_error(`${obj} is not of type ${ObjTypes}`); +} + +/** + * Attach a listener to the object. The callback will be executed + * when the event is emitted. + * Mutate the object. + * + * For all available events, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Input.Events.html + * + * For list of keycodes, see: + * https://github.com/photonstorm/phaser/blob/v3.22.0/src/input/keyboard/keys/KeyCodes.js + * + * @param key keyboard key to trigger listener + * @param event the event name + * @param callback listener function, executed on event + * @returns listener game object + */ +export function add_keyboard_listener( + key: string | number, + event: string, + callback: Function, +): GameObject { + const keyObj = scene().input.keyboard.addKey(key); + const keyboardListener = keyObj.addListener(event, callback); + return set_type(keyboardListener, ListenerTypes.KeyboardKeyType); +} + +/** + * Deactivate and remove listener. + * + * @param listener + * @returns if successful + */ +export function remove_listener(listener: GameObject): boolean { + if (is_any_type(listener, ListnerTypes)) { + get_input_obj(listener) + .removeAllListeners(); + return true; + } + return false; +} + +const gameFunctions = [ + add, + add_listener, + add_keyboard_listener, + add_to_container, + add_tween, + create_anim, + create_anim_config, + create_anim_frame_config, + create_anim_spritesheet_frame_configs, + create_award, + create_config, + create_container, + create_ellipse, + create_image, + create_interactive_config, + create_rect, + create_text, + create_text_config, + create_tween_config, + create_sound_config, + create_spritesheet_config, + destroy_obj, + get_screen_width, + get_screen_height, + get_screen_display_width, + get_screen_display_height, + load_image, + load_sound, + load_spritesheet, + play_anim_on_image, + play_sound, + prepend_remote_url, + remove_listener, + set_alpha, + set_display_size, + set_flip, + set_interactive, + set_origin, + set_position, + set_rotation, + set_scale, +]; + +// Inject minArgsNeeded to allow module varargs +// Remove if module varargs is fixed on js-slang side +gameFunctions.forEach((fn) => { + const dummy = fn as any; + dummy.minArgsNeeded = fn.length; +}); diff --git a/src/bundles/game/index.ts b/src/bundles/game/index.ts index 1faf72013..a4ab431d2 100644 --- a/src/bundles/game/index.ts +++ b/src/bundles/game/index.ts @@ -1,59 +1,59 @@ -/** - * Game library that translates Phaser 3 API into Source. - * - * More in-depth explanation of the Phaser 3 API can be found at - * Phaser 3 documentation itself. - * - * For Phaser 3 API Documentation, check: - * https://photonstorm.github.io/phaser3-docs/ - * - * @module game - * @author Anthony Halim - * @author Chi Xu - * @author Chong Sia Tiffany - * @author Gokul Rajiv - */ - -export { - add, - add_listener, - add_keyboard_listener, - add_to_container, - add_tween, - create_anim, - create_anim_config, - create_anim_frame_config, - create_anim_spritesheet_frame_configs, - create_award, - create_config, - create_container, - create_ellipse, - create_image, - create_interactive_config, - create_rect, - create_text, - create_text_config, - create_tween_config, - create_sound_config, - create_spritesheet_config, - destroy_obj, - get_screen_width, - get_screen_height, - get_screen_display_width, - get_screen_display_height, - load_image, - load_sound, - load_spritesheet, - play_anim_on_image, - play_sound, - prepend_remote_url, - remove_listener, - set_alpha, - set_display_size, - set_flip, - set_interactive, - set_origin, - set_position, - set_rotation, - set_scale, -} from './functions'; +/** + * Game library that translates Phaser 3 API into Source. + * + * More in-depth explanation of the Phaser 3 API can be found at + * Phaser 3 documentation itself. + * + * For Phaser 3 API Documentation, check: + * https://photonstorm.github.io/phaser3-docs/ + * + * @module game + * @author Anthony Halim + * @author Chi Xu + * @author Chong Sia Tiffany + * @author Gokul Rajiv + */ + +export { + add, + add_listener, + add_keyboard_listener, + add_to_container, + add_tween, + create_anim, + create_anim_config, + create_anim_frame_config, + create_anim_spritesheet_frame_configs, + create_award, + create_config, + create_container, + create_ellipse, + create_image, + create_interactive_config, + create_rect, + create_text, + create_text_config, + create_tween_config, + create_sound_config, + create_spritesheet_config, + destroy_obj, + get_screen_width, + get_screen_height, + get_screen_display_width, + get_screen_display_height, + load_image, + load_sound, + load_spritesheet, + play_anim_on_image, + play_sound, + prepend_remote_url, + remove_listener, + set_alpha, + set_display_size, + set_flip, + set_interactive, + set_origin, + set_position, + set_rotation, + set_scale, +} from './functions'; diff --git a/src/bundles/game/types.ts b/src/bundles/game/types.ts index 6f821e994..68902f7d0 100644 --- a/src/bundles/game/types.ts +++ b/src/bundles/game/types.ts @@ -1,62 +1,62 @@ -import * as Phaser from 'phaser'; - -export type ObjectConfig = { [attr: string]: any }; - -export type RawGameElement = - | Phaser.GameObjects.Sprite - | Phaser.GameObjects.Text; - -export type RawGameShape = - | Phaser.GameObjects.Rectangle - | Phaser.GameObjects.Ellipse; - -export type RawGameObject = RawGameElement | RawGameShape; - -export type RawContainer = Phaser.GameObjects.Container; - -export type RawInputObject = - | Phaser.Input.InputPlugin - | Phaser.Input.Keyboard.Key; - -export type GameObject = { - type: string; - object: RawGameObject | RawInputObject | RawContainer | undefined; -}; - -export type GameParams = { - scene: Phaser.Scene | undefined; - preloadImageMap: Map; - preloadSoundMap: Map; - preloadSpritesheetMap: Map; - lifecycleFuncs: { - preload: () => void; - create: () => void; - update: () => void; - }; - renderPreview: boolean; - remotePath: (path: string) => string; - screenSize: { x: number; y: number }; - createAward: (x: number, y: number, key: string) => Phaser.GameObjects.Sprite; -}; - -export const sourceAcademyAssets = 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com'; - -// Scene needs to be set when available! -export const defaultGameParams: GameParams = { - scene: undefined, - preloadImageMap: new Map(), - preloadSoundMap: new Map(), - preloadSpritesheetMap: new Map(), - lifecycleFuncs: { - preload() {}, - create() {}, - update() {}, - }, - renderPreview: false, - remotePath: (path: string) => sourceAcademyAssets + (path[0] === '/' ? '' : '/') + path, - screenSize: { - x: 1920, - y: 1080, - }, - createAward: (x: number, y: number, key: string) => new Phaser.GameObjects.Sprite(defaultGameParams.scene!, x, y, key), -}; +import * as Phaser from 'phaser'; + +export type ObjectConfig = { [attr: string]: any }; + +export type RawGameElement = + | Phaser.GameObjects.Sprite + | Phaser.GameObjects.Text; + +export type RawGameShape = + | Phaser.GameObjects.Rectangle + | Phaser.GameObjects.Ellipse; + +export type RawGameObject = RawGameElement | RawGameShape; + +export type RawContainer = Phaser.GameObjects.Container; + +export type RawInputObject = + | Phaser.Input.InputPlugin + | Phaser.Input.Keyboard.Key; + +export type GameObject = { + type: string; + object: RawGameObject | RawInputObject | RawContainer | undefined; +}; + +export type GameParams = { + scene: Phaser.Scene | undefined; + preloadImageMap: Map; + preloadSoundMap: Map; + preloadSpritesheetMap: Map; + lifecycleFuncs: { + preload: () => void; + create: () => void; + update: () => void; + }; + renderPreview: boolean; + remotePath: (path: string) => string; + screenSize: { x: number; y: number }; + createAward: (x: number, y: number, key: string) => Phaser.GameObjects.Sprite; +}; + +export const sourceAcademyAssets = 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com'; + +// Scene needs to be set when available! +export const defaultGameParams: GameParams = { + scene: undefined, + preloadImageMap: new Map(), + preloadSoundMap: new Map(), + preloadSpritesheetMap: new Map(), + lifecycleFuncs: { + preload() {}, + create() {}, + update() {}, + }, + renderPreview: false, + remotePath: (path: string) => sourceAcademyAssets + (path[0] === '/' ? '' : '/') + path, + screenSize: { + x: 1920, + y: 1080, + }, + createAward: (x: number, y: number, key: string) => new Phaser.GameObjects.Sprite(defaultGameParams.scene!, x, y, key), +}; diff --git a/src/bundles/plotly/curve_functions.ts b/src/bundles/plotly/curve_functions.ts index 09792cb67..e25fcc9ab 100644 --- a/src/bundles/plotly/curve_functions.ts +++ b/src/bundles/plotly/curve_functions.ts @@ -1,127 +1,127 @@ -import Plotly, { type Data, type Layout } from 'plotly.js-dist'; -import { type Curve, CurvePlot, type Point } from './plotly'; - -export function x_of(pt: Point): number { - return pt.x; -} - -/** - * Retrieves the y-coordinate of a given Point. - * - * @param p given point - * @returns y-coordinate of the Point - * @example - * ``` - * const point = make_color_point(1, 2, 3, 50, 100, 150); - * y_of(point); // Returns 2 - * ``` - */ -export function y_of(pt: Point): number { - return pt.y; -} - -/** - * Retrieves the z-coordinate of a given Point. - * - * @param p given point - * @returns z-coordinate of the Point - * @example - * ``` - * const point = make_color_point(1, 2, 3, 50, 100, 150); - * z_of(point); // Returns 3 - * ``` - */ -export function z_of(pt: Point): number { - return pt.z; -} - -/** - * Retrieves the red component of a given Point. - * - * @param p given point - * @returns Red component of the Point - * @example - * ``` - * const point = make_color_point(1, 2, 3, 50, 100, 150); - * r_of(point); // Returns 50 - * ``` - */ -export function r_of(pt: Point): number { - return pt.color[0] ?? 0 * 255; -} - -/** - * Retrieves the green component of a given Point. - * - * @param p given point - * @returns Green component of the Point - * @example - * ``` - * const point = make_color_point(1, 2, 3, 50, 100, 150); - * g_of(point); // Returns 100 - * ``` - */ -export function g_of(pt: Point): number { - return pt.color[1] ?? 0 * 255; -} - -/** - * Retrieves the blue component of a given Point. - * - * @param p given point - * @returns Blue component of the Point - * @example - * ``` - * const point = make_color_point(1, 2, 3, 50, 100, 150); - * b_of(point); // Returns 150 - * ``` - */ -export function b_of(pt: Point): number { - return pt.color[2] ?? 0 * 255; -} -export function generatePlot( - type: string, - numPoints: number, - config: Data, - layout: Partial, - is_colored: boolean, - func: Curve, -): CurvePlot { - let x_s: number[] = []; - let y_s: number[] = []; - let z_s: number[] = []; - let color_s: string[] = []; - for (let i = 0; i <= numPoints; i += 1) { - const point = func(i / numPoints); - x_s.push(x_of(point)); - y_s.push(y_of(point)); - z_s.push(z_of(point)); - color_s.push(`rgb(${r_of(point)},${g_of(point)},${b_of(point)})`); - } - - const plotlyData: Data = { - x: x_s, - y: y_s, - z: z_s, - marker: { - size: 2, - color: color_s, - }, - line: { - color: color_s, - }, - }; - return new CurvePlot( - draw_new_curve, - { - ...plotlyData, - ...config, - type, - } as Data, - layout, - ); -} - -function draw_new_curve(divId: string, data: Data, layout: Partial) { - Plotly.react(divId, [data], layout); +import Plotly, { type Data, type Layout } from 'plotly.js-dist'; +import { type Curve, CurvePlot, type Point } from './plotly'; + +export function x_of(pt: Point): number { + return pt.x; +} + +/** + * Retrieves the y-coordinate of a given Point. + * + * @param p given point + * @returns y-coordinate of the Point + * @example + * ``` + * const point = make_color_point(1, 2, 3, 50, 100, 150); + * y_of(point); // Returns 2 + * ``` + */ +export function y_of(pt: Point): number { + return pt.y; +} + +/** + * Retrieves the z-coordinate of a given Point. + * + * @param p given point + * @returns z-coordinate of the Point + * @example + * ``` + * const point = make_color_point(1, 2, 3, 50, 100, 150); + * z_of(point); // Returns 3 + * ``` + */ +export function z_of(pt: Point): number { + return pt.z; +} + +/** + * Retrieves the red component of a given Point. + * + * @param p given point + * @returns Red component of the Point + * @example + * ``` + * const point = make_color_point(1, 2, 3, 50, 100, 150); + * r_of(point); // Returns 50 + * ``` + */ +export function r_of(pt: Point): number { + return pt.color[0] ?? 0 * 255; +} + +/** + * Retrieves the green component of a given Point. + * + * @param p given point + * @returns Green component of the Point + * @example + * ``` + * const point = make_color_point(1, 2, 3, 50, 100, 150); + * g_of(point); // Returns 100 + * ``` + */ +export function g_of(pt: Point): number { + return pt.color[1] ?? 0 * 255; +} + +/** + * Retrieves the blue component of a given Point. + * + * @param p given point + * @returns Blue component of the Point + * @example + * ``` + * const point = make_color_point(1, 2, 3, 50, 100, 150); + * b_of(point); // Returns 150 + * ``` + */ +export function b_of(pt: Point): number { + return pt.color[2] ?? 0 * 255; +} +export function generatePlot( + type: string, + numPoints: number, + config: Data, + layout: Partial, + is_colored: boolean, + func: Curve, +): CurvePlot { + let x_s: number[] = []; + let y_s: number[] = []; + let z_s: number[] = []; + let color_s: string[] = []; + for (let i = 0; i <= numPoints; i += 1) { + const point = func(i / numPoints); + x_s.push(x_of(point)); + y_s.push(y_of(point)); + z_s.push(z_of(point)); + color_s.push(`rgb(${r_of(point)},${g_of(point)},${b_of(point)})`); + } + + const plotlyData: Data = { + x: x_s, + y: y_s, + z: z_s, + marker: { + size: 2, + color: color_s, + }, + line: { + color: color_s, + }, + }; + return new CurvePlot( + draw_new_curve, + { + ...plotlyData, + ...config, + type, + } as Data, + layout, + ); +} + +function draw_new_curve(divId: string, data: Data, layout: Partial) { + Plotly.react(divId, [data], layout); } \ No newline at end of file diff --git a/src/bundles/plotly/functions.ts b/src/bundles/plotly/functions.ts index ea9fbf46b..d1e5d1381 100644 --- a/src/bundles/plotly/functions.ts +++ b/src/bundles/plotly/functions.ts @@ -1,468 +1,468 @@ -/** - * The module `plotly` provides functions for drawing plots using the plotly.js library. - * @module plotly - */ - -import context from 'js-slang/context'; -import Plotly, { type Data, type Layout } from 'plotly.js-dist'; -import { - type Curve, - CurvePlot, - type CurvePlotFunction, - DrawnPlot, - type ListOfPairs, -} from './plotly'; -import { generatePlot } from './curve_functions'; -import { get_duration, get_wave, is_sound } from './sound_functions'; -import { Sound } from '../sound/types'; - -let drawnPlots: (DrawnPlot | CurvePlot)[] = []; - -context.moduleContexts.plotly.state = { - drawnPlots, -}; - -/** - * Adds a new plotly plot to the context which will be rendered in the Plotly Tabs - * @example - * ```typescript - * const z1 = [ - * [8.83,8.89,8.81,8.87,8.9,8.87], - * [8.89,8.94,8.85,8.94,8.96,8.92], - * [8.84,8.9,8.82,8.92,8.93,8.91], - * [8.79,8.85,8.79,8.9,8.94,8.92], - * [8.79,8.88,8.81,8.9,8.95,8.92], - * [8.8,8.82,8.78,8.91,8.94,8.92], - * [8.75,8.78,8.77,8.91,8.95,8.92], - * [8.8,8.8,8.77,8.91,8.95,8.94], - * [8.74,8.81,8.76,8.93,8.98,8.99], - * [8.89,8.99,8.92,9.1,9.13,9.11], - * [8.97,8.97,8.91,9.09,9.11,9.11], - * [9.04,9.08,9.05,9.25,9.28,9.27], - * [9,9.01,9,9.2,9.23,9.2], - * [8.99,8.99,8.98,9.18,9.2,9.19], - * [8.93,8.97,8.97,9.18,9.2,9.18] - * ]; - * new_plot(list(pair("z", z1), pair("type", "surface"))) // creates a surface plot in Plotly Tab - * ``` - * - * - * @Types - * ``` typescript - * // The data format for input [{field_name}, value] from among the following fields - * data = { - * type: PlotType; - * x: Datum[] | Datum[][]; - * y: Datum[] | Datum[][]; - * z: Datum[] | Datum[][] | Datum[][][]; - * mode: - * | 'lines' - * | 'markers' - * | 'text' - * | 'lines+markers' - * | 'text+markers' - * | 'text+lines' - * | 'text+lines+markers' - * } - * - * - * type Datum = string | number | Date | null; - * type PlotType = - * | 'bar' - * | 'barpolar' - * | 'box' - * | 'candlestick' - * | 'carpet' - * | 'choropleth' - * | 'choroplethmapbox' - * | 'cone' - * | 'contour' - * | 'contourcarpet' - * | 'densitymapbox' - * | 'funnel' - * | 'funnelarea' - * | 'heatmap' - * | 'heatmapgl' - * | 'histogram' - * | 'histogram2d' - * | 'histogram2dcontour' - * | 'image' - * | 'indicator' - * | 'isosurface' - * | 'mesh3d' - * | 'ohlc' - * | 'parcats' - * | 'parcoords' - * | 'pie' - * | 'pointcloud' - * | 'sankey' - * | 'scatter' - * | 'scatter3d' - * | 'scattercarpet' - * | 'scattergeo' - * | 'scattergl' - * | 'scattermapbox' - * | 'scatterpolar' - * | 'scatterpolargl' - * | 'scatterternary' - * | 'splom' - * | 'streamtube' - * | 'sunburst' - * | 'surface' - * | 'table' - * | 'treemap' - * | 'violin' - * | 'volume' - * | 'waterfall'; - * - * ``` - * @param data The data in the form of list of pair, with the first term in the pair is - * the name of the field as a string and the second term is the value of the field - * among the fields mentioned above - */ -export function new_plot(data: ListOfPairs): void { - drawnPlots.push(new DrawnPlot(draw_new_plot, data)); -} - -/** - * Adds a new plotly plot to the context which will be rendered in the Plotly Tabs - * @example - * ```typescript - * - * const z1 = [ - * [8.83,8.89,8.81,8.87,8.9,8.87], - * [8.89,8.94,8.85,8.94,8.96,8.92], - * [8.84,8.9,8.82,8.92,8.93,8.91], - * [8.79,8.85,8.79,8.9,8.94,8.92], - * [8.79,8.88,8.81,8.9,8.95,8.92], - * [8.8,8.82,8.78,8.91,8.94,8.92], - * [8.75,8.78,8.77,8.91,8.95,8.92], - * [8.8,8.8,8.77,8.91,8.95,8.94], - * [8.74,8.81,8.76,8.93,8.98,8.99], - * [8.89,8.99,8.92,9.1,9.13,9.11], - * [8.97,8.97,8.91,9.09,9.11,9.11], - * [9.04,9.08,9.05,9.25,9.28,9.27], - * [9,9.01,9,9.2,9.23,9.2], - * [8.99,8.99,8.98,9.18,9.2,9.19], - * [8.93,8.97,8.97,9.18,9.2,9.18] - * ]; - * - * let z2 = []; - * for (var i=0;i, - is_colored: boolean = false, -): (numPoints: number) => CurvePlotFunction { - return (numPoints: number) => { - const func = (curveFunction: Curve) => { - const plotDrawn = generatePlot( - type, - numPoints, - config, - layout, - is_colored, - curveFunction, - ); - - drawnPlots.push(plotDrawn); - return plotDrawn; - }; - - return func; - }; -} - -/** - * Returns a function that turns a given Curve into a Drawing, by sampling the - * Curve at `num` sample points and connecting each pair with a line. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_connected_2d(100)(t => make_point(t, t)); - * ``` - */ -export const draw_connected_2d = createPlotFunction( - 'scattergl', - { - mode: 'lines', - }, - { - xaxis: { visible: false }, - yaxis: { - visible: false, - scaleanchor: 'x', - }, - }, - true, -); - -/** - * Returns a function that turns a given 3D Curve into a Drawing, by sampling the - * 3D Curve at `num` sample points and connecting each pair with a line. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type 3D Curve → Drawing - * @example - * ``` - * draw_connected_3d(100)(t => make_point(t, t)); - * ``` - */ -export const draw_connected_3d = createPlotFunction( - 'scatter3d', - { mode: 'lines' }, - {}, - true, -); - -/** - * Returns a function that turns a given Curve into a Drawing, by sampling the - * Curve at num sample points. The Drawing consists of isolated points, and does not connect them. - * When a program evaluates to a Drawing, the Source system displays it graphically, in a window, - * - * * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type 2D Curve → Drawing - * @example - * ``` - * draw_points_2d(100)(t => make_point(t, t)); - */ -export const draw_points_2d = createPlotFunction( - 'scatter', - { mode: 'markers' }, - { - xaxis: { visible: false }, - yaxis: { - visible: false, - scaleanchor: 'x', - }, - }, - true, -); - -/** - * Returns a function that turns a given 3D Curve into a Drawing, by sampling the - * 3D Curve at num sample points. The Drawing consists of isolated points, and does not connect them. - * When a program evaluates to a Drawing, the Source system displays it graphically, in a window, - * - * * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type 3D Curve → Drawing - * @example - * ``` - * draw_points_3d(100)(t => make_point(t, t)); - */ -export const draw_points_3d = createPlotFunction( - 'scatter3d', - { mode: 'markers' }, - {}, -); - -/** - * Visualizes the sound on a 2d line graph - * @param sound the sound which is to be visualized on plotly - */ -export const draw_sound_2d = (sound: Sound) => { - const FS: number = 44100; // Output sample rate - if (!is_sound(sound)) { - throw new Error( - `draw_sound_2d is expecting sound, but encountered ${sound}`, - ); - // If a sound is already displayed, terminate execution. - } else if (get_duration(sound) < 0) { - throw new Error('draw_sound_2d: duration of sound is negative'); - } else { - // Instantiate audio context if it has not been instantiated. - - // Create mono buffer - const channel: number[] = []; - const time_stamps: number[] = []; - const len = Math.ceil(FS * get_duration(sound)); - - const wave = get_wave(sound); - for (let i = 0; i < len; i += 1) { - time_stamps[i] = i / FS; - channel[i] = wave(i / FS); - } - - let x_s: number[] = []; - let y_s: number[] = []; - - for (let i = 0; i < channel.length; i += 1) { - x_s.push(time_stamps[i]); - y_s.push(channel[i]); - } - - const plotlyData: Data = { - x: x_s, - y: y_s, - }; - const plot = new CurvePlot( - draw_new_curve, - { - ...plotlyData, - type: 'scattergl', - mode: 'lines', - line: { width: 0.5 }, - } as Data, - { - xaxis: { - type: 'linear', - title: 'Time', - anchor: 'y', - position: 0, - rangeslider: { visible: true }, - }, - yaxis: { - type: 'linear', - visible: false, - }, - bargap: 0.2, - barmode: 'stack', - }, - ); - if (drawnPlots) drawnPlots.push(plot); - } -}; - -function draw_new_curve(divId: string, data: Data, layout: Partial) { - Plotly.react(divId, [data], layout); -} +/** + * The module `plotly` provides functions for drawing plots using the plotly.js library. + * @module plotly + */ + +import context from 'js-slang/context'; +import Plotly, { type Data, type Layout } from 'plotly.js-dist'; +import { + type Curve, + CurvePlot, + type CurvePlotFunction, + DrawnPlot, + type ListOfPairs, +} from './plotly'; +import { generatePlot } from './curve_functions'; +import { get_duration, get_wave, is_sound } from './sound_functions'; +import { type Sound } from '../sound/types'; + +let drawnPlots: (DrawnPlot | CurvePlot)[] = []; + +context.moduleContexts.plotly.state = { + drawnPlots, +}; + +/** + * Adds a new plotly plot to the context which will be rendered in the Plotly Tabs + * @example + * ```typescript + * const z1 = [ + * [8.83,8.89,8.81,8.87,8.9,8.87], + * [8.89,8.94,8.85,8.94,8.96,8.92], + * [8.84,8.9,8.82,8.92,8.93,8.91], + * [8.79,8.85,8.79,8.9,8.94,8.92], + * [8.79,8.88,8.81,8.9,8.95,8.92], + * [8.8,8.82,8.78,8.91,8.94,8.92], + * [8.75,8.78,8.77,8.91,8.95,8.92], + * [8.8,8.8,8.77,8.91,8.95,8.94], + * [8.74,8.81,8.76,8.93,8.98,8.99], + * [8.89,8.99,8.92,9.1,9.13,9.11], + * [8.97,8.97,8.91,9.09,9.11,9.11], + * [9.04,9.08,9.05,9.25,9.28,9.27], + * [9,9.01,9,9.2,9.23,9.2], + * [8.99,8.99,8.98,9.18,9.2,9.19], + * [8.93,8.97,8.97,9.18,9.2,9.18] + * ]; + * new_plot(list(pair("z", z1), pair("type", "surface"))) // creates a surface plot in Plotly Tab + * ``` + * + * + * @Types + * ``` typescript + * // The data format for input [{field_name}, value] from among the following fields + * data = { + * type: PlotType; + * x: Datum[] | Datum[][]; + * y: Datum[] | Datum[][]; + * z: Datum[] | Datum[][] | Datum[][][]; + * mode: + * | 'lines' + * | 'markers' + * | 'text' + * | 'lines+markers' + * | 'text+markers' + * | 'text+lines' + * | 'text+lines+markers' + * } + * + * + * type Datum = string | number | Date | null; + * type PlotType = + * | 'bar' + * | 'barpolar' + * | 'box' + * | 'candlestick' + * | 'carpet' + * | 'choropleth' + * | 'choroplethmapbox' + * | 'cone' + * | 'contour' + * | 'contourcarpet' + * | 'densitymapbox' + * | 'funnel' + * | 'funnelarea' + * | 'heatmap' + * | 'heatmapgl' + * | 'histogram' + * | 'histogram2d' + * | 'histogram2dcontour' + * | 'image' + * | 'indicator' + * | 'isosurface' + * | 'mesh3d' + * | 'ohlc' + * | 'parcats' + * | 'parcoords' + * | 'pie' + * | 'pointcloud' + * | 'sankey' + * | 'scatter' + * | 'scatter3d' + * | 'scattercarpet' + * | 'scattergeo' + * | 'scattergl' + * | 'scattermapbox' + * | 'scatterpolar' + * | 'scatterpolargl' + * | 'scatterternary' + * | 'splom' + * | 'streamtube' + * | 'sunburst' + * | 'surface' + * | 'table' + * | 'treemap' + * | 'violin' + * | 'volume' + * | 'waterfall'; + * + * ``` + * @param data The data in the form of list of pair, with the first term in the pair is + * the name of the field as a string and the second term is the value of the field + * among the fields mentioned above + */ +export function new_plot(data: ListOfPairs): void { + drawnPlots.push(new DrawnPlot(draw_new_plot, data)); +} + +/** + * Adds a new plotly plot to the context which will be rendered in the Plotly Tabs + * @example + * ```typescript + * + * const z1 = [ + * [8.83,8.89,8.81,8.87,8.9,8.87], + * [8.89,8.94,8.85,8.94,8.96,8.92], + * [8.84,8.9,8.82,8.92,8.93,8.91], + * [8.79,8.85,8.79,8.9,8.94,8.92], + * [8.79,8.88,8.81,8.9,8.95,8.92], + * [8.8,8.82,8.78,8.91,8.94,8.92], + * [8.75,8.78,8.77,8.91,8.95,8.92], + * [8.8,8.8,8.77,8.91,8.95,8.94], + * [8.74,8.81,8.76,8.93,8.98,8.99], + * [8.89,8.99,8.92,9.1,9.13,9.11], + * [8.97,8.97,8.91,9.09,9.11,9.11], + * [9.04,9.08,9.05,9.25,9.28,9.27], + * [9,9.01,9,9.2,9.23,9.2], + * [8.99,8.99,8.98,9.18,9.2,9.19], + * [8.93,8.97,8.97,9.18,9.2,9.18] + * ]; + * + * let z2 = []; + * for (var i=0;i, + is_colored: boolean = false, +): (numPoints: number) => CurvePlotFunction { + return (numPoints: number) => { + const func = (curveFunction: Curve) => { + const plotDrawn = generatePlot( + type, + numPoints, + config, + layout, + is_colored, + curveFunction, + ); + + drawnPlots.push(plotDrawn); + return plotDrawn; + }; + + return func; + }; +} + +/** + * Returns a function that turns a given Curve into a Drawing, by sampling the + * Curve at `num` sample points and connecting each pair with a line. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_connected_2d(100)(t => make_point(t, t)); + * ``` + */ +export const draw_connected_2d = createPlotFunction( + 'scattergl', + { + mode: 'lines', + }, + { + xaxis: { visible: false }, + yaxis: { + visible: false, + scaleanchor: 'x', + }, + }, + true, +); + +/** + * Returns a function that turns a given 3D Curve into a Drawing, by sampling the + * 3D Curve at `num` sample points and connecting each pair with a line. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type 3D Curve → Drawing + * @example + * ``` + * draw_connected_3d(100)(t => make_point(t, t)); + * ``` + */ +export const draw_connected_3d = createPlotFunction( + 'scatter3d', + { mode: 'lines' }, + {}, + true, +); + +/** + * Returns a function that turns a given Curve into a Drawing, by sampling the + * Curve at num sample points. The Drawing consists of isolated points, and does not connect them. + * When a program evaluates to a Drawing, the Source system displays it graphically, in a window, + * + * * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type 2D Curve → Drawing + * @example + * ``` + * draw_points_2d(100)(t => make_point(t, t)); + */ +export const draw_points_2d = createPlotFunction( + 'scatter', + { mode: 'markers' }, + { + xaxis: { visible: false }, + yaxis: { + visible: false, + scaleanchor: 'x', + }, + }, + true, +); + +/** + * Returns a function that turns a given 3D Curve into a Drawing, by sampling the + * 3D Curve at num sample points. The Drawing consists of isolated points, and does not connect them. + * When a program evaluates to a Drawing, the Source system displays it graphically, in a window, + * + * * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type 3D Curve → Drawing + * @example + * ``` + * draw_points_3d(100)(t => make_point(t, t)); + */ +export const draw_points_3d = createPlotFunction( + 'scatter3d', + { mode: 'markers' }, + {}, +); + +/** + * Visualizes the sound on a 2d line graph + * @param sound the sound which is to be visualized on plotly + */ +export const draw_sound_2d = (sound: Sound) => { + const FS: number = 44100; // Output sample rate + if (!is_sound(sound)) { + throw new Error( + `draw_sound_2d is expecting sound, but encountered ${sound}`, + ); + // If a sound is already displayed, terminate execution. + } else if (get_duration(sound) < 0) { + throw new Error('draw_sound_2d: duration of sound is negative'); + } else { + // Instantiate audio context if it has not been instantiated. + + // Create mono buffer + const channel: number[] = []; + const time_stamps: number[] = []; + const len = Math.ceil(FS * get_duration(sound)); + + const wave = get_wave(sound); + for (let i = 0; i < len; i += 1) { + time_stamps[i] = i / FS; + channel[i] = wave(i / FS); + } + + let x_s: number[] = []; + let y_s: number[] = []; + + for (let i = 0; i < channel.length; i += 1) { + x_s.push(time_stamps[i]); + y_s.push(channel[i]); + } + + const plotlyData: Data = { + x: x_s, + y: y_s, + }; + const plot = new CurvePlot( + draw_new_curve, + { + ...plotlyData, + type: 'scattergl', + mode: 'lines', + line: { width: 0.5 }, + } as Data, + { + xaxis: { + type: 'linear', + title: 'Time', + anchor: 'y', + position: 0, + rangeslider: { visible: true }, + }, + yaxis: { + type: 'linear', + visible: false, + }, + bargap: 0.2, + barmode: 'stack', + }, + ); + if (drawnPlots) drawnPlots.push(plot); + } +}; + +function draw_new_curve(divId: string, data: Data, layout: Partial) { + Plotly.react(divId, [data], layout); +} diff --git a/src/bundles/plotly/index.ts b/src/bundles/plotly/index.ts index b97142f13..bb1ca049b 100644 --- a/src/bundles/plotly/index.ts +++ b/src/bundles/plotly/index.ts @@ -1,14 +1,14 @@ -/** - * Bundle for Source Academy Plotly repository - * @author Sourabh Raj Jaiswal - */ - -export { - new_plot, - new_plot_json, - draw_connected_2d, - draw_connected_3d, - draw_points_2d, - draw_points_3d, - draw_sound_2d, +/** + * Bundle for Source Academy Plotly repository + * @author Sourabh Raj Jaiswal + */ + +export { + new_plot, + new_plot_json, + draw_connected_2d, + draw_connected_3d, + draw_points_2d, + draw_points_3d, + draw_sound_2d, } from './functions'; \ No newline at end of file diff --git a/src/bundles/plotly/plotly.ts b/src/bundles/plotly/plotly.ts index c218804fd..3ad00f06b 100644 --- a/src/bundles/plotly/plotly.ts +++ b/src/bundles/plotly/plotly.ts @@ -1,62 +1,62 @@ -import { type Data, type Layout } from 'plotly.js-dist'; -import { type ReplResult } from '../../typings/type_helpers'; -import type { Pair } from 'js-slang/dist/stdlib/list'; - -/** - * Represents plots with a draw method attached - */ -export class DrawnPlot implements ReplResult { - drawFn: any; - data: ListOfPairs; - constructor(drawFn: any, data: ListOfPairs) { - this.drawFn = drawFn; - this.data = data; - } - - public toReplString = () => ''; - - public draw = (divId: string) => { - this.drawFn(this.data, divId); - }; -} - -export class CurvePlot implements ReplResult { - plotlyDrawFn: any; - data: Data; - layout: Partial; - constructor(plotlyDrawFn: any, data: Data, layout: Partial) { - this.plotlyDrawFn = plotlyDrawFn; - this.data = data; - this.layout = layout; - } - public toReplString = () => ''; - - public draw = (divId: string) => { - this.plotlyDrawFn(divId, this.data, this.layout); - }; -} - -export type ListOfPairs = (ListOfPairs | any)[] | null; -export type Data2d = number[]; -export type Color = { r: number; g: number; b: number }; - -export type DataTransformer = (c: Data2d[]) => Data2d[]; -export type CurvePlotFunction = (func: Curve) => CurvePlot; - -export type Curve = (n: number) => Point; -export type CurveTransformer = (c: Curve) => Curve; - -/** Encapsulates 3D point with RGB values. */ -export class Point implements ReplResult { - constructor( - public readonly x: number, - public readonly y: number, - public readonly z: number, - public readonly color: Color, - ) {} - - public toReplString = () => `(${this.x}, ${this.y}, ${this.z}, Color: ${this.color})`; -} - -export type Wave = (...t: any) => number; +import { type Data, type Layout } from 'plotly.js-dist'; +import { type ReplResult } from '../../typings/type_helpers'; +import type { Pair } from 'js-slang/dist/stdlib/list'; + +/** + * Represents plots with a draw method attached + */ +export class DrawnPlot implements ReplResult { + drawFn: any; + data: ListOfPairs; + constructor(drawFn: any, data: ListOfPairs) { + this.drawFn = drawFn; + this.data = data; + } + + public toReplString = () => ''; + + public draw = (divId: string) => { + this.drawFn(this.data, divId); + }; +} + +export class CurvePlot implements ReplResult { + plotlyDrawFn: any; + data: Data; + layout: Partial; + constructor(plotlyDrawFn: any, data: Data, layout: Partial) { + this.plotlyDrawFn = plotlyDrawFn; + this.data = data; + this.layout = layout; + } + public toReplString = () => ''; + + public draw = (divId: string) => { + this.plotlyDrawFn(divId, this.data, this.layout); + }; +} + +export type ListOfPairs = (ListOfPairs | any)[] | null; +export type Data2d = number[]; +export type Color = { r: number; g: number; b: number }; + +export type DataTransformer = (c: Data2d[]) => Data2d[]; +export type CurvePlotFunction = (func: Curve) => CurvePlot; + +export type Curve = (n: number) => Point; +export type CurveTransformer = (c: Curve) => Curve; + +/** Encapsulates 3D point with RGB values. */ +export class Point implements ReplResult { + constructor( + public readonly x: number, + public readonly y: number, + public readonly z: number, + public readonly color: Color, + ) {} + + public toReplString = () => `(${this.x}, ${this.y}, ${this.z}, Color: ${this.color})`; +} + +export type Wave = (...t: any) => number; export type Sound = Pair; \ No newline at end of file diff --git a/src/bundles/plotly/sound_functions.ts b/src/bundles/plotly/sound_functions.ts index 830d34284..6d727eef4 100644 --- a/src/bundles/plotly/sound_functions.ts +++ b/src/bundles/plotly/sound_functions.ts @@ -1,35 +1,33 @@ -import { - head, - tail, - is_pair, -} from 'js-slang/dist/stdlib/list'; -import { type Sound, type Wave } from '../sound/types'; -export function is_sound(x: any): x is Sound { - return ( - is_pair(x) - && typeof get_wave(x) === 'function' - && typeof get_duration(x) === 'number' - ); -} -/** - * Accesses the wave function of a given Sound. - * - * @param sound given Sound - * @return the wave function of the Sound - * @example get_wave(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns t => Math_sin(2 * Math_PI * 440 * t) - */ -export function get_wave(sound: Sound): Wave { - return head(sound); -} -/** - * Accesses the duration of a given Sound. - * - * @param sound given Sound - * @return the duration of the Sound - * @example get_duration(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns 5 - */ -export function get_duration(sound: Sound): number { - return tail(sound); -} - -export type Sound; \ No newline at end of file +import { + head, + tail, + is_pair, +} from 'js-slang/dist/stdlib/list'; +import { type Sound, type Wave } from '../sound/types'; +export function is_sound(x: any): x is Sound { + return ( + is_pair(x) + && typeof get_wave(x) === 'function' + && typeof get_duration(x) === 'number' + ); +} +/** + * Accesses the wave function of a given Sound. + * + * @param sound given Sound + * @return the wave function of the Sound + * @example get_wave(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns t => Math_sin(2 * Math_PI * 440 * t) + */ +export function get_wave(sound: Sound): Wave { + return head(sound); +} +/** + * Accesses the duration of a given Sound. + * + * @param sound given Sound + * @return the duration of the Sound + * @example get_duration(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns 5 + */ +export function get_duration(sound: Sound): number { + return tail(sound); +} diff --git a/src/bundles/repl/config.ts b/src/bundles/repl/config.ts index e873007b1..9512fcdd8 100644 --- a/src/bundles/repl/config.ts +++ b/src/bundles/repl/config.ts @@ -1,10 +1,10 @@ -export const COLOR_REPL_DISPLAY_DEFAULT = 'cyan'; -export const COLOR_RUN_CODE_RESULT = 'white'; -export const COLOR_ERROR_MESSAGE = 'red'; -export const FONT_MESSAGE = { - fontFamily: 'Inconsolata, Consolas, monospace', - fontSize: '16px', - fontWeight: 'normal', -}; -export const DEFAULT_EDITOR_HEIGHT = 375; -export const MINIMUM_EDITOR_HEIGHT = 40; +export const COLOR_REPL_DISPLAY_DEFAULT = 'cyan'; +export const COLOR_RUN_CODE_RESULT = 'white'; +export const COLOR_ERROR_MESSAGE = 'red'; +export const FONT_MESSAGE = { + fontFamily: 'Inconsolata, Consolas, monospace', + fontSize: '16px', + fontWeight: 'normal', +}; +export const DEFAULT_EDITOR_HEIGHT = 375; +export const MINIMUM_EDITOR_HEIGHT = 40; diff --git a/src/bundles/repl/functions.ts b/src/bundles/repl/functions.ts index 38680d0d7..914ceea70 100644 --- a/src/bundles/repl/functions.ts +++ b/src/bundles/repl/functions.ts @@ -1,119 +1,119 @@ -/** - * Functions for Programmable REPL - * @module repl - * @author Wang Zihan - */ - -import context from 'js-slang/context'; -import { ProgrammableRepl } from './programmable_repl'; -import { COLOR_REPL_DISPLAY_DEFAULT } from './config'; - -const INSTANCE = new ProgrammableRepl(); -context.moduleContexts.repl.state = INSTANCE; -/** - * Setup the programmable REPL with given evaulator's entrance function - * - * The function should take one parameter as the code from the module's editor, for example: - * ```js - * function parse_and_evaluate(code) { - * // ... - * } - * ``` - * @param {evalFunc} evalFunc - evaulator entrance function - * - * @category Main - */ -export function set_evaluator(evalFunc: Function) { - if (!(evalFunc instanceof Function)) { - const typeName = typeof (evalFunc); - throw new Error(`Wrong parameter type "${typeName}' in function "set_evaluator". It supposed to be a function and it's the entrance function of your metacircular evaulator.`); - } - INSTANCE.evalFunction = evalFunc; - return { - toReplString: () => '', - }; -} - - -/** - * Display message in Programmable Repl Tab - * If you give a pair as the parameter, it will use the given pair to generate rich text and use rich text display mode to display the string in Programmable Repl Tab with undefined return value (see module description for more information). - * If you give other things as the parameter, it will simply display the toString value of the parameter in Programmable Repl Tab and returns the displayed string itself. - * - * **Rich Text Display** - * - First you need to `import { repl_display } from "repl";` - * - Format: pair(pair("string",style),style)... - * - Examples: - * - * ```js - * // A large italic underlined "Hello World" - * repl_display(pair(pair(pair(pair("Hello World", "underline"), "italic"), "bold"), "gigantic")); - * - * // A large italic underlined "Hello World" in blue - * repl_display(pair(pair(pair(pair(pair("Hello World", "underline"),"italic"), "bold"), "gigantic"), "clrt#0000ff")); - * - * // A large italic underlined "Hello World" with orange foreground and purple background - * repl_display(pair(pair(pair(pair(pair(pair("Hello World", "underline"), "italic"), "bold"), "gigantic"), "clrb#A000A0"),"clrt#ff9700")); - * ``` - * - * - Coloring: - * - `clrt` stands for text color, `clrb` stands for background color. The color string are in hexadecimal begin with "#" and followed by 6 hexadecimal digits. - * - Example: `pair("123","clrt#ff0000")` will produce a red "123"; `pair("456","clrb#00ff00")` will produce a green "456". - * - Besides coloring, the following styles are also supported: - * - `bold`: Make the text bold. - * - `italic`: Make the text italic. - * - `small`: Make the text in small size. - * - `medium`: Make the text in medium size. - * - `large`: Make the text in large size. - * - `gigantic`: Make the text in very large size. - * - `underline`: Underline the text. - * - Note that if you apply the conflicting attributes together, only one conflicted style will take effect and other conflicting styles will be discarded, like "pair(pair(pair("123", small), medium), large) " (Set conflicting font size for the same text) - * - Also note that for safety matters, certain words and characters are not allowed to appear under rich text display mode. - * - * @param {content} the content you want to display - * @category Main - */ -export function repl_display(content: any) : any { - if (INSTANCE.richDisplayInternal(content) === 'not_rich_text_pair') { - INSTANCE.pushOutputString(content.toString(), COLOR_REPL_DISPLAY_DEFAULT, 'plaintext');// students may set the value of the parameter "str" to types other than a string (for example "repl_display(1)" ). So here I need to first convert the parameter "str" into a string before preceding. - return content; - } - return undefined; -} - - -/** - * Set Programmable Repl editor background image with a customized image URL - * @param {img_url} the url to the new background image - * @param {background_color_alpha} the alpha (transparency) of the original background color that covers on your background image [0 ~ 1]. Recommended value is 0.5 . - * - * @category Main - */ -export function set_background_image(img_url: string, background_color_alpha: number) : void { - INSTANCE.customizedEditorProps.backgroundImageUrl = img_url; - INSTANCE.customizedEditorProps.backgroundColorAlpha = background_color_alpha; -} - - -/** - * Set Programmable Repl editor font size - * @param {font_size_px} font size (in pixel) - * - * @category Main - */ -export function set_font_size(font_size_px: number) { - INSTANCE.customizedEditorProps.fontSize = parseInt(font_size_px.toString());// The TypeScript type checker will throw an error as "parseInt" in TypeScript only accepts one string as parameter. -} - -/** - * When use this function as the entrance function in the parameter of "set_evaluator", the Programmable Repl will directly use the default js-slang interpreter to run your program in Programmable Repl editor. Do not directly call this function in your own code. - * @param {program} Do not directly set this parameter in your code. - * @param {safeKey} A parameter that is designed to prevent student from directly calling this function in Source language. - * - * @category Main - */ -export function default_js_slang(_program: string) : any { - throw new Error('Invaild Call: Function "default_js_slang" can not be directly called by user\'s code in editor. You should use it as the parameter of the function "set_evaluator"'); - // When the function is normally called by set_evaluator function, safeKey is set to "document.body", which has a type "Element". - // Students can not create objects and use HTML Elements in Source due to limitations and rules in Source, so they can't set the safeKey to a HTML Element, thus they can't use this function in Source. -} +/** + * Functions for Programmable REPL + * @module repl + * @author Wang Zihan + */ + +import context from 'js-slang/context'; +import { ProgrammableRepl } from './programmable_repl'; +import { COLOR_REPL_DISPLAY_DEFAULT } from './config'; + +const INSTANCE = new ProgrammableRepl(); +context.moduleContexts.repl.state = INSTANCE; +/** + * Setup the programmable REPL with given evaulator's entrance function + * + * The function should take one parameter as the code from the module's editor, for example: + * ```js + * function parse_and_evaluate(code) { + * // ... + * } + * ``` + * @param {evalFunc} evalFunc - evaulator entrance function + * + * @category Main + */ +export function set_evaluator(evalFunc: Function) { + if (!(evalFunc instanceof Function)) { + const typeName = typeof (evalFunc); + throw new Error(`Wrong parameter type "${typeName}' in function "set_evaluator". It supposed to be a function and it's the entrance function of your metacircular evaulator.`); + } + INSTANCE.evalFunction = evalFunc; + return { + toReplString: () => '', + }; +} + + +/** + * Display message in Programmable Repl Tab + * If you give a pair as the parameter, it will use the given pair to generate rich text and use rich text display mode to display the string in Programmable Repl Tab with undefined return value (see module description for more information). + * If you give other things as the parameter, it will simply display the toString value of the parameter in Programmable Repl Tab and returns the displayed string itself. + * + * **Rich Text Display** + * - First you need to `import { repl_display } from "repl";` + * - Format: pair(pair("string",style),style)... + * - Examples: + * + * ```js + * // A large italic underlined "Hello World" + * repl_display(pair(pair(pair(pair("Hello World", "underline"), "italic"), "bold"), "gigantic")); + * + * // A large italic underlined "Hello World" in blue + * repl_display(pair(pair(pair(pair(pair("Hello World", "underline"),"italic"), "bold"), "gigantic"), "clrt#0000ff")); + * + * // A large italic underlined "Hello World" with orange foreground and purple background + * repl_display(pair(pair(pair(pair(pair(pair("Hello World", "underline"), "italic"), "bold"), "gigantic"), "clrb#A000A0"),"clrt#ff9700")); + * ``` + * + * - Coloring: + * - `clrt` stands for text color, `clrb` stands for background color. The color string are in hexadecimal begin with "#" and followed by 6 hexadecimal digits. + * - Example: `pair("123","clrt#ff0000")` will produce a red "123"; `pair("456","clrb#00ff00")` will produce a green "456". + * - Besides coloring, the following styles are also supported: + * - `bold`: Make the text bold. + * - `italic`: Make the text italic. + * - `small`: Make the text in small size. + * - `medium`: Make the text in medium size. + * - `large`: Make the text in large size. + * - `gigantic`: Make the text in very large size. + * - `underline`: Underline the text. + * - Note that if you apply the conflicting attributes together, only one conflicted style will take effect and other conflicting styles will be discarded, like "pair(pair(pair("123", small), medium), large) " (Set conflicting font size for the same text) + * - Also note that for safety matters, certain words and characters are not allowed to appear under rich text display mode. + * + * @param {content} the content you want to display + * @category Main + */ +export function repl_display(content: any) : any { + if (INSTANCE.richDisplayInternal(content) === 'not_rich_text_pair') { + INSTANCE.pushOutputString(content.toString(), COLOR_REPL_DISPLAY_DEFAULT, 'plaintext');// students may set the value of the parameter "str" to types other than a string (for example "repl_display(1)" ). So here I need to first convert the parameter "str" into a string before preceding. + return content; + } + return undefined; +} + + +/** + * Set Programmable Repl editor background image with a customized image URL + * @param {img_url} the url to the new background image + * @param {background_color_alpha} the alpha (transparency) of the original background color that covers on your background image [0 ~ 1]. Recommended value is 0.5 . + * + * @category Main + */ +export function set_background_image(img_url: string, background_color_alpha: number) : void { + INSTANCE.customizedEditorProps.backgroundImageUrl = img_url; + INSTANCE.customizedEditorProps.backgroundColorAlpha = background_color_alpha; +} + + +/** + * Set Programmable Repl editor font size + * @param {font_size_px} font size (in pixel) + * + * @category Main + */ +export function set_font_size(font_size_px: number) { + INSTANCE.customizedEditorProps.fontSize = parseInt(font_size_px.toString());// The TypeScript type checker will throw an error as "parseInt" in TypeScript only accepts one string as parameter. +} + +/** + * When use this function as the entrance function in the parameter of "set_evaluator", the Programmable Repl will directly use the default js-slang interpreter to run your program in Programmable Repl editor. Do not directly call this function in your own code. + * @param {program} Do not directly set this parameter in your code. + * @param {safeKey} A parameter that is designed to prevent student from directly calling this function in Source language. + * + * @category Main + */ +export function default_js_slang(_program: string) : any { + throw new Error('Invaild Call: Function "default_js_slang" can not be directly called by user\'s code in editor. You should use it as the parameter of the function "set_evaluator"'); + // When the function is normally called by set_evaluator function, safeKey is set to "document.body", which has a type "Element". + // Students can not create objects and use HTML Elements in Source due to limitations and rules in Source, so they can't set the safeKey to a HTML Element, thus they can't use this function in Source. +} diff --git a/src/bundles/repl/index.ts b/src/bundles/repl/index.ts index ec02024a2..456cccf5f 100644 --- a/src/bundles/repl/index.ts +++ b/src/bundles/repl/index.ts @@ -1,46 +1,46 @@ -/** - * ## Example of usage: - * ### Use with metacircular evaluator: - * ```js - * import { set_evaluator, repl_display } from "repl"; - * - * const primitive_functions = list( - * // (omitted other primitive functions) - * list("display", repl_display), // Here change this from "display" to "repl_display" to let the display result goes to the repl tab. - * // (omitted other primitive functions) - * } - * - * function parse_and_evaluate(code){ - * // (your metacircular evaluator entry function) - * } - * - * set_evaluator(parse_and_evaluate); // This can invoke the repl with your metacircular evaluator's evaluation entry - * ``` - * - * ### Use with Source Academy's builtin js-slang - * ```js - * import { set_evaluator, default_js_slang, repl_display } from "repl"; // Here you also need to import "repl_display" along with "set_evaluator" and "default_js_slang". - * - * set_evaluator(default_js_slang); // This can invoke the repl with Source Academy's builtin js-slang evaluation entry - * ``` - * (Note that you can't directly call "default_js_slang" in your own code. It should only be used as the parameter of "set_evaluator") - * - * - * ### Customize Editor Appearance - * ```js - * import { set_background_image, set_font_size } from "repl"; - * set_background_image("https://www.some_image_website.xyz/your_favorite_image.png"); // Set the background image of the editor in repl tab - * set_font_size(20.5); // Set the font size of the editor in repl tab - * ``` - * - * @module repl - * @author Wang Zihan -*/ - -export { - set_evaluator, - repl_display, - set_background_image, - set_font_size, - default_js_slang, -} from './functions'; +/** + * ## Example of usage: + * ### Use with metacircular evaluator: + * ```js + * import { set_evaluator, repl_display } from "repl"; + * + * const primitive_functions = list( + * // (omitted other primitive functions) + * list("display", repl_display), // Here change this from "display" to "repl_display" to let the display result goes to the repl tab. + * // (omitted other primitive functions) + * } + * + * function parse_and_evaluate(code){ + * // (your metacircular evaluator entry function) + * } + * + * set_evaluator(parse_and_evaluate); // This can invoke the repl with your metacircular evaluator's evaluation entry + * ``` + * + * ### Use with Source Academy's builtin js-slang + * ```js + * import { set_evaluator, default_js_slang, repl_display } from "repl"; // Here you also need to import "repl_display" along with "set_evaluator" and "default_js_slang". + * + * set_evaluator(default_js_slang); // This can invoke the repl with Source Academy's builtin js-slang evaluation entry + * ``` + * (Note that you can't directly call "default_js_slang" in your own code. It should only be used as the parameter of "set_evaluator") + * + * + * ### Customize Editor Appearance + * ```js + * import { set_background_image, set_font_size } from "repl"; + * set_background_image("https://www.some_image_website.xyz/your_favorite_image.png"); // Set the background image of the editor in repl tab + * set_font_size(20.5); // Set the font size of the editor in repl tab + * ``` + * + * @module repl + * @author Wang Zihan +*/ + +export { + set_evaluator, + repl_display, + set_background_image, + set_font_size, + default_js_slang, +} from './functions'; diff --git a/src/bundles/repl/programmable_repl.ts b/src/bundles/repl/programmable_repl.ts index 707b2ecc0..77fa17972 100644 --- a/src/bundles/repl/programmable_repl.ts +++ b/src/bundles/repl/programmable_repl.ts @@ -1,262 +1,262 @@ -/** - * Source Academy Programmable REPL module - * @module repl - * @author Wang Zihan - */ - - -import context from 'js-slang/context'; -import { default_js_slang } from './functions'; -import { runFilesInContext, type IOptions } from 'js-slang'; -import { COLOR_RUN_CODE_RESULT, COLOR_ERROR_MESSAGE, DEFAULT_EDITOR_HEIGHT } from './config'; - -export class ProgrammableRepl { - public evalFunction: Function; - public userCodeInEditor: string; - public outputStrings: any[]; - private _editorInstance; - private _tabReactComponent: any; - // I store editorHeight value separately in here although it is already stored in the module's Tab React component state because I need to keep the editor height - // when the Tab component is re-mounted due to the user drags the area between the module's Tab and Source Academy's original REPL to resize the module's Tab height. - public editorHeight : number; - - public customizedEditorProps = { - backgroundImageUrl: 'no-background-image', - backgroundColorAlpha: 1, - fontSize: 17, - }; - - constructor() { - this.evalFunction = (_placeholder) => this.easterEggFunction(); - this.userCodeInEditor = this.getSavedEditorContent(); - this.outputStrings = []; - this._editorInstance = null;// To be set when calling "SetEditorInstance" in the ProgrammableRepl Tab React Component render function. - this.editorHeight = DEFAULT_EDITOR_HEIGHT; - developmentLog(this); - } - - InvokeREPL_Internal(evalFunc: Function) { - this.evalFunction = evalFunc; - } - - runCode() { - this.outputStrings = []; - let retVal: any; - try { - if (Object.is(this.evalFunction, default_js_slang)) { - retVal = this.runInJsSlang(this.userCodeInEditor); - } else { - retVal = this.evalFunction(this.userCodeInEditor); - } - } catch (exception: any) { - developmentLog(exception); - // If the exception has a start line of -1 and an undefined error property, then this exception is most likely to be "incorrect number of arguments" caused by incorrect number of parameters in the evaluator entry function provided by students with set_evaluator. - if (exception.location.start.line === -1 && exception.error === undefined) { - this.pushOutputString('Error: Unable to use your evaluator to run the code. Does your evaluator entry function contain and only contain exactly one parameter?', COLOR_ERROR_MESSAGE); - } else { - this.pushOutputString(`Line ${exception.location.start.line.toString()}: ${exception.error?.message}`, COLOR_ERROR_MESSAGE); - } - this.reRenderTab(); - return; - } - if (typeof (retVal) === 'string') { - retVal = `"${retVal}"`; - } - // Here must use plain text output mode because retVal contains strings from the users. - this.pushOutputString(retVal, COLOR_RUN_CODE_RESULT); - this.reRenderTab(); - developmentLog('RunCode finished'); - } - - updateUserCode(code) { - this.userCodeInEditor = code; - } - - // Rich text output method allow output strings to have html tags and css styles. - pushOutputString(content : string, textColor : string, outputMethod : string = 'plaintext') { - let tmp = { - content: content === undefined ? 'undefined' : content === null ? 'null' : content, - color: textColor, - outputMethod, - }; - this.outputStrings.push(tmp); - } - - setEditorInstance(instance: any) { - if (instance === undefined) return; // It seems that when calling this function in gui->render->ref, the React internal calls this function for multiple times (at least two times) , and in at least one call the parameter 'instance' is set to 'undefined'. If I don't add this if statement, the program will throw a runtime error when rendering tab. - this._editorInstance = instance; - this._editorInstance.on('guttermousedown', (e) => { - const breakpointLine = e.getDocumentPosition().row; - developmentLog(breakpointLine); - }); - - this._editorInstance.setOptions({ fontSize: `${this.customizedEditorProps.fontSize.toString()}pt` }); - } - - richDisplayInternal(pair_rich_text) { - developmentLog(pair_rich_text); - const head = (pair) => pair[0]; - const tail = (pair) => pair[1]; - const is_pair = (obj) => obj instanceof Array && obj.length === 2; - if (!is_pair(pair_rich_text)) return 'not_rich_text_pair'; - function checkColorStringValidity(htmlColor:string) { - if (htmlColor.length !== 7) return false; - if (htmlColor[0] !== '#') return false; - for (let i = 1; i < 7; i++) { - const char = htmlColor[i]; - developmentLog(` ${char}`); - if (!((char >= '0' && char <= '9') || (char >= 'A' && char <= 'F') || (char >= 'a' && char <= 'f'))) { - return false; - } - } - return true; - } - function recursiveHelper(thisInstance, param): string { - if (typeof (param) === 'string') { - // There MUST be a safe check on users' strings, because users may insert something that can be interpreted as executable JavaScript code when outputing rich text. - const safeCheckResult = thisInstance.userStringSafeCheck(param); - if (safeCheckResult !== 'safe') { - throw new Error(`For safety matters, the character/word ${safeCheckResult} is not allowed in rich text output. Please remove it or use plain text output mode and try again.`); - } - developmentLog(head(param)); - return `">${param}`; - } - if (!is_pair(param)) { - throw new Error(`Unexpected data type ${typeof (param)} when processing rich text. It should be a pair.`); - } else { - const pairStyleToCssStyle : { [pairStyle : string] : string } = { - bold: 'font-weight:bold;', - italic: 'font-style:italic;', - small: 'font-size: 14px;', - medium: 'font-size: 20px;', - large: 'font-size: 25px;', - gigantic: 'font-size: 50px;', - underline: 'text-decoration: underline;', - }; - if (typeof (tail(param)) !== 'string') { - throw new Error(`The tail in style pair should always be a string, but got ${typeof (tail(param))}.`); - } - let style = ''; - if (tail(param) - .substring(0, 3) === 'clr') { - let prefix = ''; - if (tail(param)[3] === 't') prefix = 'color:'; - else if (tail(param)[3] === 'b') prefix = 'background-color:'; - else throw new Error('Error when decoding rich text color data'); - const colorHex = tail(param) - .substring(4); - if (!checkColorStringValidity(colorHex)) { - throw new Error(`Invalid html color string ${colorHex}. It should start with # and followed by 6 characters representing a hex number.`); - } - style = `${prefix + colorHex};`; - } else { - style = pairStyleToCssStyle[tail(param)]; - if (style === undefined) { - throw new Error(`Found undefined style ${tail(param)} during processing rich text.`); - } - } - return style + recursiveHelper(thisInstance, head(param)); - } - } - this.pushOutputString(`', 'script', 'javascript', 'eval', 'document', 'window', 'console', 'location']; - for (let word of forbiddenWords) { - if (tmp.indexOf(word) !== -1) { - return word; - } - } - return 'safe'; - } - - /* - Directly invoking Source Academy's builtin js-slang runner. - Needs hard-coded support from js-slang part for the "sourceRunner" function and "backupContext" property in the content object for this to work. - */ - runInJsSlang(code: string): string { - developmentLog('js-slang context:'); - // console.log(context); - const options : Partial = { - originalMaxExecTime: 1000, - scheduler: 'preemptive', - stepLimit: 1000, - throwInfiniteLoops: true, - useSubst: false, - }; - context.prelude = 'const display=(x)=>repl_display(x);'; - context.errors = []; // Here if I don't manually clear the "errors" array in context, the remaining errors from the last evaluation will stop the function "preprocessFileImports" in preprocessor.ts of js-slang thus stop the whole evaluation. - const sourceFile : Record = { - '/ReplModuleUserCode.js': code, - }; - - runFilesInContext(sourceFile, '/ReplModuleUserCode.js', context, options) - .then((evalResult) => { - if (evalResult.status === 'suspended' || evalResult.status === 'suspended-ec-eval') { - throw new Error('This should not happen'); - } - if (evalResult.status !== 'error') { - this.pushOutputString('js-slang program finished with value:', COLOR_RUN_CODE_RESULT); - // Here must use plain text output mode because evalResult.value contains strings from the users. - this.pushOutputString(evalResult.value === undefined ? 'undefined' : evalResult.value.toString(), COLOR_RUN_CODE_RESULT); - } else { - const errors = context.errors; - console.log(errors); - const errorCount = errors.length; - for (let i = 0; i < errorCount; i++) { - const error = errors[i]; - if (error.explain() - .indexOf('Name repl_display not declared.') !== -1) { - this.pushOutputString('[Error] It seems that you haven\'t import the function "repl_display" correctly when calling "set_evaluator" in Source Academy\'s main editor.', COLOR_ERROR_MESSAGE); - } else this.pushOutputString(`Line ${error.location.start.line}: ${error.type} Error: ${error.explain()} (${error.elaborate()})`, COLOR_ERROR_MESSAGE); - } - } - this.reRenderTab(); - }); - - return 'Async run in js-slang'; - } - - setTabReactComponentInstance(tab : any) { - this._tabReactComponent = tab; - } - - private reRenderTab() { - this._tabReactComponent.setState({});// Forces the tab React Component to re-render using setState - } - - saveEditorContent() { - localStorage.setItem('programmable_repl_saved_editor_code', this.userCodeInEditor.toString()); - this.pushOutputString('Saved', 'lightgreen'); - this.pushOutputString('The saved code is stored locally in your browser. You may lose the saved code if you clear browser data or use another device.', 'gray', 'richtext'); - this.reRenderTab(); - } - - private getSavedEditorContent() { - let savedContent = localStorage.getItem('programmable_repl_saved_editor_code'); - if (savedContent === null) return ''; - return savedContent; - } - - // Small Easter Egg that doesn't affect module functionality and normal user experience :) - // Please don't modify these text! Thanks! :) - private easterEggFunction() { - this.pushOutputString('[Author (Wang Zihan)] ❤I love Keqing and Ganyu.❤', 'pink', 'richtext'); - this.pushOutputString('Showing my love to my favorite girls through a SA module, is that the so-called "romance of a programmer"?', 'gray', 'richtext'); - this.pushOutputString('❤❤❤❤❤', 'pink'); - this.pushOutputString('
', 'white', 'richtext'); - this.pushOutputString('If you see this, please check whether you have called set_evaluator function with the correct parameter before using the Programmable Repl Tab.', 'yellow', 'richtext'); - return 'Easter Egg!'; - } -} - -// Comment all the codes inside this function before merging the code to github as production version. -// Because console.log() can expose the sandboxed VM location to students thus may cause security concerns. -function developmentLog(_content) { - // console.log(`[Programmable Repl Log] ${_content}`); -} +/** + * Source Academy Programmable REPL module + * @module repl + * @author Wang Zihan + */ + + +import context from 'js-slang/context'; +import { default_js_slang } from './functions'; +import { runFilesInContext, type IOptions } from 'js-slang'; +import { COLOR_RUN_CODE_RESULT, COLOR_ERROR_MESSAGE, DEFAULT_EDITOR_HEIGHT } from './config'; + +export class ProgrammableRepl { + public evalFunction: Function; + public userCodeInEditor: string; + public outputStrings: any[]; + private _editorInstance; + private _tabReactComponent: any; + // I store editorHeight value separately in here although it is already stored in the module's Tab React component state because I need to keep the editor height + // when the Tab component is re-mounted due to the user drags the area between the module's Tab and Source Academy's original REPL to resize the module's Tab height. + public editorHeight : number; + + public customizedEditorProps = { + backgroundImageUrl: 'no-background-image', + backgroundColorAlpha: 1, + fontSize: 17, + }; + + constructor() { + this.evalFunction = (_placeholder) => this.easterEggFunction(); + this.userCodeInEditor = this.getSavedEditorContent(); + this.outputStrings = []; + this._editorInstance = null;// To be set when calling "SetEditorInstance" in the ProgrammableRepl Tab React Component render function. + this.editorHeight = DEFAULT_EDITOR_HEIGHT; + developmentLog(this); + } + + InvokeREPL_Internal(evalFunc: Function) { + this.evalFunction = evalFunc; + } + + runCode() { + this.outputStrings = []; + let retVal: any; + try { + if (Object.is(this.evalFunction, default_js_slang)) { + retVal = this.runInJsSlang(this.userCodeInEditor); + } else { + retVal = this.evalFunction(this.userCodeInEditor); + } + } catch (exception: any) { + developmentLog(exception); + // If the exception has a start line of -1 and an undefined error property, then this exception is most likely to be "incorrect number of arguments" caused by incorrect number of parameters in the evaluator entry function provided by students with set_evaluator. + if (exception.location.start.line === -1 && exception.error === undefined) { + this.pushOutputString('Error: Unable to use your evaluator to run the code. Does your evaluator entry function contain and only contain exactly one parameter?', COLOR_ERROR_MESSAGE); + } else { + this.pushOutputString(`Line ${exception.location.start.line.toString()}: ${exception.error?.message}`, COLOR_ERROR_MESSAGE); + } + this.reRenderTab(); + return; + } + if (typeof (retVal) === 'string') { + retVal = `"${retVal}"`; + } + // Here must use plain text output mode because retVal contains strings from the users. + this.pushOutputString(retVal, COLOR_RUN_CODE_RESULT); + this.reRenderTab(); + developmentLog('RunCode finished'); + } + + updateUserCode(code) { + this.userCodeInEditor = code; + } + + // Rich text output method allow output strings to have html tags and css styles. + pushOutputString(content : string, textColor : string, outputMethod : string = 'plaintext') { + let tmp = { + content: content === undefined ? 'undefined' : content === null ? 'null' : content, + color: textColor, + outputMethod, + }; + this.outputStrings.push(tmp); + } + + setEditorInstance(instance: any) { + if (instance === undefined) return; // It seems that when calling this function in gui->render->ref, the React internal calls this function for multiple times (at least two times) , and in at least one call the parameter 'instance' is set to 'undefined'. If I don't add this if statement, the program will throw a runtime error when rendering tab. + this._editorInstance = instance; + this._editorInstance.on('guttermousedown', (e) => { + const breakpointLine = e.getDocumentPosition().row; + developmentLog(breakpointLine); + }); + + this._editorInstance.setOptions({ fontSize: `${this.customizedEditorProps.fontSize.toString()}pt` }); + } + + richDisplayInternal(pair_rich_text) { + developmentLog(pair_rich_text); + const head = (pair) => pair[0]; + const tail = (pair) => pair[1]; + const is_pair = (obj) => obj instanceof Array && obj.length === 2; + if (!is_pair(pair_rich_text)) return 'not_rich_text_pair'; + function checkColorStringValidity(htmlColor:string) { + if (htmlColor.length !== 7) return false; + if (htmlColor[0] !== '#') return false; + for (let i = 1; i < 7; i++) { + const char = htmlColor[i]; + developmentLog(` ${char}`); + if (!((char >= '0' && char <= '9') || (char >= 'A' && char <= 'F') || (char >= 'a' && char <= 'f'))) { + return false; + } + } + return true; + } + function recursiveHelper(thisInstance, param): string { + if (typeof (param) === 'string') { + // There MUST be a safe check on users' strings, because users may insert something that can be interpreted as executable JavaScript code when outputing rich text. + const safeCheckResult = thisInstance.userStringSafeCheck(param); + if (safeCheckResult !== 'safe') { + throw new Error(`For safety matters, the character/word ${safeCheckResult} is not allowed in rich text output. Please remove it or use plain text output mode and try again.`); + } + developmentLog(head(param)); + return `">${param}
`; + } + if (!is_pair(param)) { + throw new Error(`Unexpected data type ${typeof (param)} when processing rich text. It should be a pair.`); + } else { + const pairStyleToCssStyle : { [pairStyle : string] : string } = { + bold: 'font-weight:bold;', + italic: 'font-style:italic;', + small: 'font-size: 14px;', + medium: 'font-size: 20px;', + large: 'font-size: 25px;', + gigantic: 'font-size: 50px;', + underline: 'text-decoration: underline;', + }; + if (typeof (tail(param)) !== 'string') { + throw new Error(`The tail in style pair should always be a string, but got ${typeof (tail(param))}.`); + } + let style = ''; + if (tail(param) + .substring(0, 3) === 'clr') { + let prefix = ''; + if (tail(param)[3] === 't') prefix = 'color:'; + else if (tail(param)[3] === 'b') prefix = 'background-color:'; + else throw new Error('Error when decoding rich text color data'); + const colorHex = tail(param) + .substring(4); + if (!checkColorStringValidity(colorHex)) { + throw new Error(`Invalid html color string ${colorHex}. It should start with # and followed by 6 characters representing a hex number.`); + } + style = `${prefix + colorHex};`; + } else { + style = pairStyleToCssStyle[tail(param)]; + if (style === undefined) { + throw new Error(`Found undefined style ${tail(param)} during processing rich text.`); + } + } + return style + recursiveHelper(thisInstance, head(param)); + } + } + this.pushOutputString(`', 'script', 'javascript', 'eval', 'document', 'window', 'console', 'location']; + for (let word of forbiddenWords) { + if (tmp.indexOf(word) !== -1) { + return word; + } + } + return 'safe'; + } + + /* + Directly invoking Source Academy's builtin js-slang runner. + Needs hard-coded support from js-slang part for the "sourceRunner" function and "backupContext" property in the content object for this to work. + */ + runInJsSlang(code: string): string { + developmentLog('js-slang context:'); + // console.log(context); + const options : Partial = { + originalMaxExecTime: 1000, + scheduler: 'preemptive', + stepLimit: 1000, + throwInfiniteLoops: true, + useSubst: false, + }; + context.prelude = 'const display=(x)=>repl_display(x);'; + context.errors = []; // Here if I don't manually clear the "errors" array in context, the remaining errors from the last evaluation will stop the function "preprocessFileImports" in preprocessor.ts of js-slang thus stop the whole evaluation. + const sourceFile : Record = { + '/ReplModuleUserCode.js': code, + }; + + runFilesInContext(sourceFile, '/ReplModuleUserCode.js', context, options) + .then((evalResult) => { + if (evalResult.status === 'suspended' || evalResult.status === 'suspended-ec-eval') { + throw new Error('This should not happen'); + } + if (evalResult.status !== 'error') { + this.pushOutputString('js-slang program finished with value:', COLOR_RUN_CODE_RESULT); + // Here must use plain text output mode because evalResult.value contains strings from the users. + this.pushOutputString(evalResult.value === undefined ? 'undefined' : evalResult.value.toString(), COLOR_RUN_CODE_RESULT); + } else { + const errors = context.errors; + console.log(errors); + const errorCount = errors.length; + for (let i = 0; i < errorCount; i++) { + const error = errors[i]; + if (error.explain() + .indexOf('Name repl_display not declared.') !== -1) { + this.pushOutputString('[Error] It seems that you haven\'t import the function "repl_display" correctly when calling "set_evaluator" in Source Academy\'s main editor.', COLOR_ERROR_MESSAGE); + } else this.pushOutputString(`Line ${error.location.start.line}: ${error.type} Error: ${error.explain()} (${error.elaborate()})`, COLOR_ERROR_MESSAGE); + } + } + this.reRenderTab(); + }); + + return 'Async run in js-slang'; + } + + setTabReactComponentInstance(tab : any) { + this._tabReactComponent = tab; + } + + private reRenderTab() { + this._tabReactComponent.setState({});// Forces the tab React Component to re-render using setState + } + + saveEditorContent() { + localStorage.setItem('programmable_repl_saved_editor_code', this.userCodeInEditor.toString()); + this.pushOutputString('Saved', 'lightgreen'); + this.pushOutputString('The saved code is stored locally in your browser. You may lose the saved code if you clear browser data or use another device.', 'gray', 'richtext'); + this.reRenderTab(); + } + + private getSavedEditorContent() { + let savedContent = localStorage.getItem('programmable_repl_saved_editor_code'); + if (savedContent === null) return ''; + return savedContent; + } + + // Small Easter Egg that doesn't affect module functionality and normal user experience :) + // Please don't modify these text! Thanks! :) + private easterEggFunction() { + this.pushOutputString('[Author (Wang Zihan)] ❤I love Keqing and Ganyu.❤', 'pink', 'richtext'); + this.pushOutputString('Showing my love to my favorite girls through a SA module, is that the so-called "romance of a programmer"?', 'gray', 'richtext'); + this.pushOutputString('❤❤❤❤❤', 'pink'); + this.pushOutputString('
', 'white', 'richtext'); + this.pushOutputString('If you see this, please check whether you have called set_evaluator function with the correct parameter before using the Programmable Repl Tab.', 'yellow', 'richtext'); + return 'Easter Egg!'; + } +} + +// Comment all the codes inside this function before merging the code to github as production version. +// Because console.log() can expose the sandboxed VM location to students thus may cause security concerns. +function developmentLog(_content) { + // console.log(`[Programmable Repl Log] ${_content}`); +} diff --git a/src/tabs/Plotly/index.tsx b/src/tabs/Plotly/index.tsx index afd79e29f..a1da0d286 100644 --- a/src/tabs/Plotly/index.tsx +++ b/src/tabs/Plotly/index.tsx @@ -1,88 +1,88 @@ -import React from 'react'; -import { type DrawnPlot } from '../../bundles/plotly/plotly'; -import { type DebuggerContext } from '../../typings/type_helpers'; -import Modal from '../common/modal_div'; - -type Props = { - children?: never - className?: string - debuggerContext: any -}; - -type State = { - modalOpen: boolean - selectedPlot: any | null -}; - -class Plotly extends React.Component { - constructor(props: Props) { - super(props); - this.state = { - modalOpen: false, - selectedPlot: null, - }; - } - - handleOpen = (selectedPlot: DrawnPlot) => { - this.setState({ - modalOpen: true, - selectedPlot, - }); - }; - - public render() { - const { context: { moduleContexts: { plotly: { state: { drawnPlots } } } } } = this.props.debuggerContext; - - return ( -
- this.setState({ modalOpen: false })} - > -
{ - if (this.state.selectedPlot) { - this.state.selectedPlot.draw('modalDiv'); - } - }} - style={{ height: '80vh' }} - >
-
- { - drawnPlots.map((drawnPlot: any, id:number) => { - const divId = `plotDiv${id}`; - return ( -
-
this.handleOpen(drawnPlot)}>Click here to open Modal
-
{ - drawnPlot.draw(divId); - }} - >
-
- ); - }) - } - -
- ); - } -} - -export default { - toSpawn(context: DebuggerContext) { - const drawnPlots = context.context?.moduleContexts?.plotly.state.drawnPlots; - return drawnPlots.length > 0; - }, - body: (debuggerContext: any) => , - label: 'Plotly Test Tab', - iconName: 'scatter-plot', -}; \ No newline at end of file +import React from 'react'; +import { type DrawnPlot } from '../../bundles/plotly/plotly'; +import { type DebuggerContext } from '../../typings/type_helpers'; +import Modal from '../common/ModalDiv' + +type Props = { + children?: never + className?: string + debuggerContext: any +}; + +type State = { + modalOpen: boolean + selectedPlot: any | null +}; + +class Plotly extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + modalOpen: false, + selectedPlot: null, + }; + } + + handleOpen = (selectedPlot: DrawnPlot) => { + this.setState({ + modalOpen: true, + selectedPlot, + }); + }; + + public render() { + const { context: { moduleContexts: { plotly: { state: { drawnPlots } } } } } = this.props.debuggerContext; + + return ( +
+ this.setState({ modalOpen: false })} + > +
{ + if (this.state.selectedPlot) { + this.state.selectedPlot.draw('modalDiv'); + } + }} + style={{ height: '80vh' }} + >
+
+ { + drawnPlots.map((drawnPlot: any, id:number) => { + const divId = `plotDiv${id}`; + return ( +
+
this.handleOpen(drawnPlot)}>Click here to open Modal
+
{ + drawnPlot.draw(divId); + }} + >
+
+ ); + }) + } + +
+ ); + } +} + +export default { + toSpawn(context: DebuggerContext) { + const drawnPlots = context.context?.moduleContexts?.plotly.state.drawnPlots; + return drawnPlots.length > 0; + }, + body: (debuggerContext: any) => , + label: 'Plotly Test Tab', + iconName: 'scatter-plot', +}; diff --git a/src/tabs/Repl/index.tsx b/src/tabs/Repl/index.tsx index ee5e09463..df66ec9d1 100644 --- a/src/tabs/Repl/index.tsx +++ b/src/tabs/Repl/index.tsx @@ -1,179 +1,179 @@ -/** - * Tab for Source Academy Programmable REPL module - * @module repl - * @author Wang Zihan - */ - -import React from 'react'; -import type { DebuggerContext } from '../../typings/type_helpers'; -import { Button } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import type { ProgrammableRepl } from '../../bundles/repl/programmable_repl'; -import { FONT_MESSAGE, MINIMUM_EDITOR_HEIGHT } from '../../bundles/repl/config'; -// If I use import for AceEditor it will cause runtime error and crash Source Academy when spawning tab in the new module building system. -// import AceEditor from 'react-ace'; -const AceEditor = require('react-ace').default; - -import 'ace-builds/src-noconflict/mode-javascript'; -import 'ace-builds/src-noconflict/theme-twilight'; -import 'ace-builds/src-noconflict/ext-language_tools'; - -type Props = { - programmableReplInstance: ProgrammableRepl; -}; - -type State = { - editorHeight: number, - isDraggingDragBar: boolean, -}; - -const BOX_PADDING_VALUE = 4; - -class ProgrammableReplGUI extends React.Component { - public replInstance : ProgrammableRepl; - private editorAreaRect; - private editorInstance; - constructor(data: Props) { - super(data); - this.replInstance = data.programmableReplInstance; - this.replInstance.setTabReactComponentInstance(this); - this.state = { - editorHeight: this.replInstance.editorHeight, - isDraggingDragBar: false, - }; - } - private dragBarOnMouseDown = (e) => { - e.preventDefault(); - this.setState({ isDraggingDragBar: true }); - }; - private onMouseMove = (e) => { - if (this.state.isDraggingDragBar) { - const height = Math.max(e.clientY - this.editorAreaRect.top - BOX_PADDING_VALUE * 2, MINIMUM_EDITOR_HEIGHT); - this.replInstance.editorHeight = height; - this.setState({ editorHeight: height }); - this.editorInstance.resize(); - } - }; - private onMouseUp = (_e) => { - this.setState({ isDraggingDragBar: false }); - }; - componentDidMount() { - document.addEventListener('mousemove', this.onMouseMove); - document.addEventListener('mouseup', this.onMouseUp); - } - componentWillUnmount() { - document.removeEventListener('mousemove', this.onMouseMove); - document.removeEventListener('mouseup', this.onMouseUp); - } - public render() { - const { editorHeight } = this.state; - const outputDivs : JSX.Element[] = []; - const outputStringCount = this.replInstance.outputStrings.length; - for (let i = 0; i < outputStringCount; i++) { - const str = this.replInstance.outputStrings[i]; - if (str.outputMethod === 'richtext') { - if (str.color === '') { - outputDivs.push(
); - } else { - outputDivs.push(
); - } - } else if (str.color === '') { - outputDivs.push(
{ str.content }
); - } else { - outputDivs.push(
{ str.content }
); - } - } - return ( -
-