diff --git a/package.json b/package.json index 7801c19a4..6d9a0a775 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "@types/eslint": "^8.4.10", "@types/estree": "^1.0.0", "@types/jest": "^27.4.1", - "@types/lodash": "^4.14.191", "@types/node": "^17.0.23", "@types/plotly.js-dist": "npm:@types/plotly.js", "@types/react": "^17.0.43", @@ -87,7 +86,7 @@ "@blueprintjs/popover2": "^1.4.3", "@box2d/core": "^0.10.0", "@box2d/debug-draw": "^0.10.0", - "@jscad/modeling": "^2.9.5", + "@jscad/modeling": "2.9.6", "@jscad/regl-renderer": "^2.6.1", "@jscad/stl-serializer": "^2.1.13", "ace-builds": "^1.4.14", @@ -95,7 +94,6 @@ "dayjs": "^1.10.4", "gl-matrix": "^3.3.0", "js-slang": "^1.0.20", - "lodash": "^4.17.21", "patch-package": "^6.5.1", "phaser": "^3.54.0", "plotly.js-dist": "^2.17.1", @@ -115,4 +113,4 @@ "scripts/src/jest.config.js" ] } -} \ No newline at end of file +} diff --git a/scripts/bin/build/buildUtils.js b/scripts/bin/build/buildUtils.js index ebdf88301..96ed5fca7 100644 --- a/scripts/bin/build/buildUtils.js +++ b/scripts/bin/build/buildUtils.js @@ -165,60 +165,70 @@ export const retrieveBundles = async (manifestFile, modules) => { return knownBundles; }; /** - * Function to determine which bundles and tabs to build based on the user's input. + * Determines which bundles and tabs to build based on the user's input. * - * @param modules - * - Pass `null` to indicate that the user did not specify any modules. This - * will add all bundles currently registered in the manifest - * - Pass `[]` to indicate not to add any modules - * - Pass an array of strings to manually specify modules to process - * @param tabOpts - * - Pass `null` to indicate that the user did not specify any tabs. This - * will add all tabs currently registered in the manifest - * - Pass `[]` to indicate not to add any tabs - * - Pass an array of strings to manually specify tabs to process - * @param addTabs If `true`, then all tabs of selected bundles will be added to - * the list of tabs to build. + * If no modules and no tabs are specified, it is assumed the user wants to + * build everything. + * + * If modules but no tabs are specified, it is assumed the user only wants to + * build those bundles (and possibly those modules' tabs based on + * shouldAddModuleTabs). + * + * If tabs but no modules are specified, it is assumed the user only wants to + * build those tabs. + * + * If both modules and tabs are specified, both of the above apply and are + * combined. + * + * @param modules module names specified by the user + * @param tabOptions tab names specified by the user + * @param shouldAddModuleTabs whether to also automatically include the tabs of + * specified modules */ -export const retrieveBundlesAndTabs = async (manifestFile, modules, tabOpts, addTabs = true) => { +export const retrieveBundlesAndTabs = async (manifestFile, modules, tabOptions, shouldAddModuleTabs = true) => { const manifest = await retrieveManifest(manifestFile); const knownBundles = Object.keys(manifest); - const knownTabs = Object.values(manifest) + const knownTabs = Object + .values(manifest) .flatMap((x) => x.tabs); - let bundles; - let tabs; - if (modules !== null) { - // Some modules were specified + let bundles = []; + let tabs = []; + function addSpecificModules() { + // If unknown modules were specified, error const unknownModules = modules.filter((m) => !knownBundles.includes(m)); if (unknownModules.length > 0) { throw new Error(`Unknown modules: ${unknownModules.join(', ')}`); } - bundles = modules; - if (addTabs) { - // If a bundle is being rebuilt, add its tabs - tabs = modules.flatMap((bundle) => manifest[bundle].tabs); - } - else { - tabs = []; + bundles = bundles.concat(modules); + if (shouldAddModuleTabs) { + // Add the modules' tabs too + tabs = [...tabs, ...modules.flatMap((bundle) => manifest[bundle].tabs)]; } } - else { - // No modules were specified - bundles = knownBundles; - tabs = []; - } - if (tabOpts !== null) { - // Tabs were specified - const unknownTabs = tabOpts.filter((t) => !knownTabs.includes(t)); + function addSpecificTabs() { + // If unknown tabs were specified, error + const unknownTabs = tabOptions.filter((t) => !knownTabs.includes(t)); if (unknownTabs.length > 0) { throw new Error(`Unknown tabs: ${unknownTabs.join(', ')}`); } - tabs = tabs.concat(tabOpts); + tabs = tabs.concat(tabOptions); } - else { - // No tabs were specified + function addAllBundles() { + bundles = bundles.concat(knownBundles); + } + function addAllTabs() { tabs = tabs.concat(knownTabs); } + if (modules === null && tabOptions === null) { + addAllBundles(); + addAllTabs(); + } + else { + if (modules !== null) + addSpecificModules(); + if (tabOptions !== null) + addSpecificTabs(); + } return { bundles: [...new Set(bundles)], tabs: [...new Set(tabs)], diff --git a/scripts/bin/build/dev.js b/scripts/bin/build/dev.js index 490b317da..6dd38dbde 100644 --- a/scripts/bin/build/dev.js +++ b/scripts/bin/build/dev.js @@ -21,9 +21,9 @@ const waitForQuit = () => new Promise((resolve, reject) => { }); const getBundleContext = ({ srcDir, outDir }, bundles, app) => esbuild({ ...bundleOptions, + entryPoints: bundles.map(bundleNameExpander(srcDir)), outbase: outDir, outdir: outDir, - entryPoints: bundles.map(bundleNameExpander(srcDir)), plugins: [{ name: 'Bundle Compiler', async setup(pluginBuild) { @@ -55,9 +55,9 @@ const getBundleContext = ({ srcDir, outDir }, bundles, app) => esbuild({ }); const getTabContext = ({ srcDir, outDir }, tabs) => esbuild({ ...tabOptions, + entryPoints: tabs.map(tabNameExpander(srcDir)), outbase: outDir, outdir: outDir, - entryPoints: tabs.map(tabNameExpander(srcDir)), external: ['react*', 'react-dom'], plugins: [{ name: 'Tab Compiler', diff --git a/scripts/bin/build/modules/index.js b/scripts/bin/build/modules/index.js index 5d31dc21c..5b2012647 100644 --- a/scripts/bin/build/modules/index.js +++ b/scripts/bin/build/modules/index.js @@ -28,7 +28,7 @@ const getBuildModulesCommand = () => createBuildCommand('modules', true) .description('Build modules and their tabs') .action(async (modules, { manifest, ...opts }) => { const [assets] = await Promise.all([ - retrieveBundlesAndTabs(manifest, modules, []), + retrieveBundlesAndTabs(manifest, modules, null), createOutDir(opts.outDir), ]); await prebuild(opts, assets); diff --git a/scripts/src/build/__tests__/buildUtils.test.ts b/scripts/src/build/__tests__/buildUtils.test.ts index 24c4dbb9f..260284e7d 100644 --- a/scripts/src/build/__tests__/buildUtils.test.ts +++ b/scripts/src/build/__tests__/buildUtils.test.ts @@ -23,7 +23,7 @@ describe('Test retrieveBundlesAndTabs', () => { .toEqual(expect.arrayContaining(['tab0'])); }); - it('should return only tabs when an empty array is passed for modules', async () => { + it('should return nothing when an empty array is passed for modules', async () => { const result = await retrieveBundlesAndTabs('', [], null); expect(result.bundles) @@ -31,7 +31,7 @@ describe('Test retrieveBundlesAndTabs', () => { expect(result.modulesSpecified) .toBe(true); expect(result.tabs) - .toEqual(expect.arrayContaining(['tab0', 'tab1'])); + .toEqual([]); }); it('should return tabs from the specified modules, and concatenate specified tabs', async () => { diff --git a/scripts/src/build/buildUtils.ts b/scripts/src/build/buildUtils.ts index 3c14ab555..84a97ed67 100644 --- a/scripts/src/build/buildUtils.ts +++ b/scripts/src/build/buildUtils.ts @@ -190,69 +190,79 @@ export const retrieveBundles = async (manifestFile: string, modules: string[] | }; /** - * Function to determine which bundles and tabs to build based on the user's input. + * Determines which bundles and tabs to build based on the user's input. * - * @param modules - * - Pass `null` to indicate that the user did not specify any modules. This - * will add all bundles currently registered in the manifest - * - Pass `[]` to indicate not to add any modules - * - Pass an array of strings to manually specify modules to process - * @param tabOpts - * - Pass `null` to indicate that the user did not specify any tabs. This - * will add all tabs currently registered in the manifest - * - Pass `[]` to indicate not to add any tabs - * - Pass an array of strings to manually specify tabs to process - * @param addTabs If `true`, then all tabs of selected bundles will be added to - * the list of tabs to build. + * If no modules and no tabs are specified, it is assumed the user wants to + * build everything. + * + * If modules but no tabs are specified, it is assumed the user only wants to + * build those bundles (and possibly those modules' tabs based on + * shouldAddModuleTabs). + * + * If tabs but no modules are specified, it is assumed the user only wants to + * build those tabs. + * + * If both modules and tabs are specified, both of the above apply and are + * combined. + * + * @param modules module names specified by the user + * @param tabOptions tab names specified by the user + * @param shouldAddModuleTabs whether to also automatically include the tabs of + * specified modules */ export const retrieveBundlesAndTabs = async ( manifestFile: string, modules: string[] | null, - tabOpts: string[] | null, - addTabs: boolean = true, + tabOptions: string[] | null, + shouldAddModuleTabs: boolean = true, ) => { const manifest = await retrieveManifest(manifestFile); const knownBundles = Object.keys(manifest); - const knownTabs = Object.values(manifest) + const knownTabs = Object + .values(manifest) .flatMap((x) => x.tabs); - let bundles: string[]; - let tabs: string[]; + let bundles: string[] = []; + let tabs: string[] = []; - if (modules !== null) { - // Some modules were specified + function addSpecificModules() { + // If unknown modules were specified, error const unknownModules = modules.filter((m) => !knownBundles.includes(m)); - if (unknownModules.length > 0) { throw new Error(`Unknown modules: ${unknownModules.join(', ')}`); } - bundles = modules; - if (addTabs) { - // If a bundle is being rebuilt, add its tabs - tabs = modules.flatMap((bundle) => manifest[bundle].tabs); - } else { - tabs = []; + bundles = bundles.concat(modules); + + if (shouldAddModuleTabs) { + // Add the modules' tabs too + tabs = [...tabs, ...modules.flatMap((bundle) => manifest[bundle].tabs)]; } - } else { - // No modules were specified - bundles = knownBundles; - tabs = []; } - - if (tabOpts !== null) { - // Tabs were specified - const unknownTabs = tabOpts.filter((t) => !knownTabs.includes(t)); - + function addSpecificTabs() { + // If unknown tabs were specified, error + const unknownTabs = tabOptions.filter((t) => !knownTabs.includes(t)); if (unknownTabs.length > 0) { throw new Error(`Unknown tabs: ${unknownTabs.join(', ')}`); } - tabs = tabs.concat(tabOpts); - } else { - // No tabs were specified + + tabs = tabs.concat(tabOptions); + } + function addAllBundles() { + bundles = bundles.concat(knownBundles); + } + function addAllTabs() { tabs = tabs.concat(knownTabs); } + if (modules === null && tabOptions === null) { + addAllBundles(); + addAllTabs(); + } else { + if (modules !== null) addSpecificModules(); + if (tabOptions !== null) addSpecificTabs(); + } + return { bundles: [...new Set(bundles)], tabs: [...new Set(tabs)], diff --git a/scripts/src/build/dev.ts b/scripts/src/build/dev.ts index 8824a33c1..e3c812ec8 100644 --- a/scripts/src/build/dev.ts +++ b/scripts/src/build/dev.ts @@ -36,9 +36,9 @@ const waitForQuit = () => new Promise((resolve, reject) => { type ContextOptions = Record<'srcDir' | 'outDir', string>; const getBundleContext = ({ srcDir, outDir }: ContextOptions, bundles: string[], app?: Application) => esbuild({ ...bundleOptions, + entryPoints: bundles.map(bundleNameExpander(srcDir)), outbase: outDir, outdir: outDir, - entryPoints: bundles.map(bundleNameExpander(srcDir)), plugins: [{ name: 'Bundle Compiler', async setup(pluginBuild) { @@ -74,9 +74,9 @@ const getBundleContext = ({ srcDir, outDir }: ContextOptions, bundles: string[], const getTabContext = ({ srcDir, outDir }: ContextOptions, tabs: string[]) => esbuild({ ...tabOptions, + entryPoints: tabs.map(tabNameExpander(srcDir)), outbase: outDir, outdir: outDir, - entryPoints: tabs.map(tabNameExpander(srcDir)), external: ['react*', 'react-dom'], plugins: [{ name: 'Tab Compiler', diff --git a/scripts/src/build/modules/index.ts b/scripts/src/build/modules/index.ts index edef979d1..7c183e79e 100644 --- a/scripts/src/build/modules/index.ts +++ b/scripts/src/build/modules/index.ts @@ -44,7 +44,7 @@ const getBuildModulesCommand = () => createBuildCommand('modules', true) .description('Build modules and their tabs') .action(async (modules: string[] | null, { manifest, ...opts }: BuildCommandInputs & LintCommandInputs) => { const [assets] = await Promise.all([ - retrieveBundlesAndTabs(manifest, modules, []), + retrieveBundlesAndTabs(manifest, modules, null), createOutDir(opts.outDir), ]); diff --git a/src/bundles/csg/constants.ts b/src/bundles/csg/constants.ts index 1d66072be..ba56bb87a 100644 --- a/src/bundles/csg/constants.ts +++ b/src/bundles/csg/constants.ts @@ -1,29 +1,7 @@ -/* [Imports] */ -import { IconSize } from '@blueprintjs/core'; - /* [Exports] */ -//NOTE Silver is in here to avoid circular dependencies, instead of in -// functions.ts with the other colour strings -export const SILVER: string = '#AAAAAA'; -export const DEFAULT_COLOR: string = SILVER; - -// Values extracted from the styling of the frontend -export const SA_TAB_BUTTON_WIDTH: string = '40px'; -export const SA_TAB_ICON_SIZE: number = IconSize.LARGE; - -export const BP_TOOLTIP_PADDING: string = '10px 12px'; -export const BP_TAB_BUTTON_MARGIN: string = '20px'; -export const BP_TAB_PANEL_MARGIN: string = '20px'; -export const BP_BORDER_RADIUS: string = '3px'; -export const STANDARD_MARGIN: string = '10px'; - -export const BP_TEXT_COLOR: string = '#F5F8FA'; -export const BP_TOOLTIP_BACKGROUND_COLOR: string = '#E1E8ED'; -export const BP_ICON_COLOR: string = '#A7B6C2'; -export const ACE_GUTTER_TEXT_COLOR: string = '#8091A0'; -export const ACE_GUTTER_BACKGROUND_COLOR: string = '#34495E'; -export const BP_TOOLTIP_TEXT_COLOR: string = '#394B59'; +// Renderer default colour. Bright aquamarine makes bugs easier to spot +export const DEFAULT_COLOR = '#55ffaa'; // Renderer grid constants export const MAIN_TICKS: number = 1; diff --git a/src/bundles/csg/functions.ts b/src/bundles/csg/functions.ts index d8bb3e1b4..41b600930 100644 --- a/src/bundles/csg/functions.ts +++ b/src/bundles/csg/functions.ts @@ -1,649 +1,886 @@ -/** - * The module `csg` provides functions for drawing Constructive Solid Geometry (CSG) called `Shape`. - * - * A *Shape* is defined by its polygons and vertices. - * - * @module csg - * @author Liu Muchen - * @author Joel Leow - */ - -/* [Imports] */ -import { primitives } from '@jscad/modeling'; -import { colorize as _colorize } 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 { align } from '@jscad/modeling/src/operations/transforms'; -import { serialize } from '@jscad/stl-serializer'; -import save from 'save-file'; -import { SILVER } from './constants.js'; -import { Core } from './core.js'; -import type { Solid } from './jscad/types.js'; -import { type List } from './types'; -import { - Group, - Shape, - hexToColor, - type Entity, - type RenderGroup, -} from './utilities'; - -/** - * Center the provided shape with the middle base of the shape at (0, 0, 0). - * - * @param {Shape} shape - The shape to be centered - * @returns {Shape} The shape that is centered - */ -function shapeSetOrigin(shape: Shape) { - let newSolid: Solid = align({ modes: ['min', 'min', 'min'] }, shape.solid); - return new Shape(newSolid); -} - -/* [Exports] */ - -// [Variables - Primitive shapes] -/** - * Primitive Shape of a cube. - * - * @category Primitive - */ -const primitiveCube: Shape = shapeSetOrigin( - new Shape(primitives.cube({ size: 1 })), -); - -/** - * Returns a Shape of a cube of a set colour or the default colour when - * colour information is omitted. - * - * @param {string} hex A hex colour code - */ -export function cube(hex: string): Shape { - const shape: Shape = primitiveCube; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} - -/** - * Primitive Shape of a sphere. - * - * @category Primitive - */ -const primitiveSphere: Shape = shapeSetOrigin( - new Shape(primitives.sphere({ radius: 0.5 })), -); - -/** - * Returns a Shape of a sphere of a set colour. - * - * @param {string} hex A hex colour code - */ -export function sphere(hex: string): Shape { - const shape: Shape = primitiveSphere; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} - -/** - * Primitive Shape of a cylinder. - * - * @category Primitive - */ -const primitiveCylinder: Shape = shapeSetOrigin( - new Shape( - primitives.cylinder({ - radius: 0.5, - height: 1, - }), - ), -); - -/** - * Returns a Shape of a cylinder of a set colour. - * - * @param {string} hex A hex colour code - */ -export function cylinder(hex: string): Shape { - const shape: Shape = primitiveCylinder; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} - -/** - * Primitive Shape of a prism. - * - * @category Primitive - */ -const primitivePrism: Shape = shapeSetOrigin( - new Shape(extrudeLinear({ height: 1 }, primitives.triangle())), -); - -/** - * Returns a Shape of a prism of a set colour. - * - * @param {string} hex A hex colour code - */ -export function prism(hex: string): Shape { - const shape: Shape = primitivePrism; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} - -/** - * Primitive Shape of an extruded star. - * - * @category Primitive - */ -const primitiveStar: Shape = shapeSetOrigin( - new Shape(extrudeLinear({ height: 1 }, primitives.star({ outerRadius: 0.5 }))), -); - -/** - * Returns a Shape of an extruded star of a set colour. - * - * @param {string} hex A hex colour code - */ -export function star(hex: string): Shape { - const shape: Shape = primitiveStar; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} - -/** - * Primitive Shape of a square pyramid. - * - * @category Primitive - */ -const primitivePyramid: Shape = shapeSetOrigin( - new Shape( - primitives.cylinderElliptic({ - height: 1, - startRadius: [0.5, 0.5], - endRadius: [Number.MIN_VALUE, Number.MIN_VALUE], - segments: 4, - }), - ), -); - -/** - * Returns a Shape of a square pyramid of a set colour. - * - * @param {string} hex A hex colour code - */ -export function pyramid(hex: string): Shape { - const shape: Shape = primitivePyramid; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} - -/** - * Primitive Shape of a cone. - * - * @category Primitive - */ -const primitiveCone: Shape = shapeSetOrigin( - new Shape( - primitives.cylinderElliptic({ - height: 1, - startRadius: [0.5, 0.5], - endRadius: [Number.MIN_VALUE, Number.MIN_VALUE], - }), - ), -); - -/** - * Returns a Shape of a cone of a set colour. - * - * @param {string} hex A hex colour code - */ -export function cone(hex: string): Shape { - const shape: Shape = primitiveCone; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} - -/** - * Primitive Shape of a torus. - * - * @category Primitive - */ -const primitiveTorus: Shape = shapeSetOrigin( - new Shape( - primitives.torus({ - innerRadius: 0.125, - outerRadius: 0.375, - }), - ), -); - -/** - * Returns a Shape of a torus of a set colour. - * - * @param {string} hex A hex colour code - */ -export function torus(hex: string): Shape { - const shape: Shape = primitiveTorus; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} - -/** - * Primitive Shape of a rounded cube. - * - * @category Primitive - */ -const primitiveRoundedCube: Shape = shapeSetOrigin( - new Shape(primitives.roundedCuboid({ size: [1, 1, 1] })), -); - -/** - * Returns a Shape of a rounded cube of a set colour. - * - * @param {string} hex A hex colour code - */ -export function rounded_cube(hex: string): Shape { - const shape: Shape = primitiveRoundedCube; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} - -/** - * Primitive Shape of a rounded cylinder. - * - * @category Primitive - */ -const primitiveRoundedCylinder: Shape = shapeSetOrigin( - new Shape( - primitives.roundedCylinder({ - height: 1, - radius: 0.5, - }), - ), -); - -/** - * Returns a Shape of a rounded cylinder of a set colour. - * - * @param {string} hex A hex colour code - */ -export function rounded_cylinder(hex: string): Shape { - const shape: Shape = primitiveRoundedCylinder; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} - -/** - * Primitive Shape of a geodesic sphere. - * - * @category Primitive - */ -const primitiveGeodesicSphere: Shape = shapeSetOrigin( - new Shape(primitives.geodesicSphere({ radius: 0.5 })), -); - -/** - * Returns a Shape of a geodesic sphere of a set colour. - * - * @param {string} hex A hex colour code - */ -export function geodesic_sphere(hex: string): Shape { - const shape: Shape = primitiveGeodesicSphere; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} -// [Variables - Colours] - -/** - * A hex colour code for black (#000000). - * - * @category Colour - */ -export const black: string = '#000000'; - -/** - * A hex colour code for dark blue (#0000AA). - * - * @category Colour - */ -export const navy: string = '#0000AA'; - -/** - * A hex colour code for green (#00AA00). - * - * @category Colour - */ -export const green: string = '#00AA00'; - -/** - * A hex colour code for dark cyan (#00AAAA). - * - * @category Colour - */ -export const teal: string = '#00AAAA'; - -/** - * A hex colour code for dark red (#AA0000). - * - * @category Colour - */ -export const crimson: string = '#AA0000'; - -/** - * A hex colour code for purple (#AA00AA). - * - * @category Colour - */ -export const purple: string = '#AA00AA'; - -/** - * A hex colour code for orange (#FFAA00). - * - * @category Colour - */ -export const orange: string = '#FFAA00'; - -/** - * A hex colour code for light grey (#AAAAAA). This is the default colour used - * when storing a Shape. - * - * @category Colour - */ -export const silver: string = SILVER; - -/** - * A hex colour code for dark grey (#555555). - * - * @category Colour - */ -export const gray: string = '#555555'; - -/** - * A hex colour code for blue (#5555FF). - * - * @category Colour - */ -export const blue: string = '#5555FF'; - -/** - * A hex colour code for light green (#55FF55). - * - * @category Colour - */ -export const lime: string = '#55FF55'; - -/** - * A hex colour code for cyan (#55FFFF). - * - * @category Colour - */ -export const cyan: string = '#55FFFF'; - -/** - * A hex colour code for light red (#FF5555). - * - * @category Colour - */ -export const rose: string = '#FF5555'; - -/** - * A hex colour code for pink (#FF55FF). - * - * @category Colour - */ -export const pink: string = '#FF55FF'; - -/** - * A hex colour code for yellow (#FFFF55). - * - * @category Colour - */ -export const yellow: string = '#FFFF55'; - -/** - * A hex colour code for white (#FFFFFF). - * - * @category Colour - */ -export const white: string = '#FFFFFF'; - -// [Functions] - -/** - * Union of the two provided shapes to produce a new shape. - * - * @param {Shape} a - The first shape - * @param {Shape} b - The second shape - * @returns {Shape} The resulting unioned shape - */ -export function union(a: Shape, b: Shape): Shape { - let newSolid: Solid = _union(a.solid, b.solid); - return new Shape(newSolid); -} - -/** - * Subtraction of the second shape from the first shape to produce a new shape. - * - * @param {Shape} a - The shape to be subtracted from - * @param {Shape} b - The shape to remove from the first shape - * @returns {Shape} The resulting subtracted shape - */ -export function subtract(a: Shape, b: Shape): Shape { - let newSolid: Solid = _subtract(a.solid, b.solid); - return new Shape(newSolid); -} - -/** - * Intersection of the two shape to produce a new shape. - * - * @param {Shape} a - The first shape - * @param {Shape} b - The second shape - * @returns {Shape} The resulting intersection shape - */ -export function intersect(a: Shape, b: Shape): Shape { - let newSolid: Solid = _intersect(a.solid, b.solid); - return new Shape(newSolid); -} - -/** - * Scales the shape in the x, y and z direction with the specified factor. - * Factors must be non-zero. - * For example, scaling the shape by 1 in x, y and z directions results in - * the original shape. - * Scaling the shape by -1 in x direction and 1 in y and z directions results - * in the reflection - * - * @param {Entity} entity - The Group or Shape to be scaled - * @param {number} x - Scaling in the x direction - * @param {number} y - Scaling in the y direction - * @param {number} z - Scaling in the z direction - * @returns {Shape} Resulting Shape - */ -export function scale(entity: Entity, x: number, y: number, z: number): Entity { - if (x <= 0 || y <= 0 || z <= 0) { - throw new Error('factors must be non-zero'); - } - return entity.scale([x, y, z]); -} - -/** - * Translate / Move the shape by the provided x, y and z units from negative - * infinity to infinity. - * - * @param {Entity} entity - The Group or Shape to be translated - * @param {number} x - The number to shift the shape in the x direction - * @param {number} y - The number to shift the shape in the y direction - * @param {number} z - The number to shift the shape in the z direction - * @returns {Shape} The translated shape - */ -export function translate( - entity: Entity, - x: number, - y: number, - z: number, -): Entity { - return entity.translate([x, y, z]); -} - -/** - * Rotate the shape by the provided angles in the x, y and z direction. - * Angles provided are in the form of radians (i.e. 2π represent 360 - * degrees). Note that the order of rotation is from the x direction first, - * followed by the y and z directions. - * - * @param {Entity} entity - The Group or Shape to be rotated - * @param {number} x - Angle of rotation in the x direction - * @param {number} y - Angle of rotation in the y direction - * @param {number} z - Angle of rotation in the z direction - * @returns {Shape} The rotated shape - */ -export function rotate( - entity: Entity, - x: number, - y: number, - z: number, -): Entity { - return entity.rotate([x, y, z]); -} - -/** - * Returns a lambda function that contains the coordinates of the bounding box. - * Provided with the axis 'x', 'y' or 'z' and value 'min' for minimum and 'max' - * for maximum, it returns the coordinates of the bounding box. - * - * For example, - * ```` - * const a = bounding_box(sphere); - * a('x', 'min'); // Returns the minimum x coordinate of the bounding box - * ```` - * - * @param {Shape} shape - The scale to be measured - * @returns {(String, String) => number} A lambda function providing the - * shape's bounding box coordinates - */ - -export function bounding_box( - shape: Shape, -): (axis: String, min: String) => number { - let bounds: BoundingBox = measureBoundingBox(shape.solid); - return (axis: String, min: String): number => { - let i: number = axis === 'x' ? 0 : axis === 'y' ? 1 : axis === 'z' ? 2 : -1; - let j: number = min === 'min' ? 0 : min === 'max' ? 1 : -1; - if (i === -1 || j === -1) { - throw Error( - 'bounding_box returned function expects a proper axis and min String.', - ); - } else { - return bounds[j][i]; - } - }; -} - -/** - * Returns a hex colour code representing the colour specified by the given RGB values. - * @param {number} redComponent Red component of the colour - * @param {number} greenComponent Green component of the colour - * @param {number} blueComponent Blue component of the colour - * @returns {string} The hex colour code - */ -export function rgb( - redComponent: number, - greenComponent: number, - blueComponent: number, -): string { - if ( - redComponent < 0 - || redComponent > 255 - || greenComponent < 0 - || greenComponent > 255 - || blueComponent < 0 - || blueComponent > 255 - ) { - throw new Error('invalid argument value: expects [0, 255]'); - } - return `#${redComponent.toString(16)}${greenComponent.toString( - 16, - )}${blueComponent.toString(16)}`; -} - -/** - * Checks if the specified entity is a Shape. - * - * @param {unknown} entity - The entity to check - * @returns {boolean} Whether the entity is a Shape - */ -export function is_shape(entity: unknown): boolean { - return entity instanceof Shape; -} - -/** - * Checks if the specified entity is a Group. - * - * @param {unknown} entity - The entity to check - * @returns {boolean} Whether the entity is a Group - */ -export function is_group(entity: unknown): boolean { - return entity instanceof Group; -} - -/** - * Initializes a group of shapes, which is represented - * as a hierarchical tree structure, with groups as - * internal nodes and shapes as leaf nodes. - * @param {List} children - The Groups and/or Shapes - * to be placed inside this new Group - * @returns {Group} The newly created Group - */ -export function group(children: List): Group { - return new Group(children); -} - -/** - * Renders a Group of Shapes, along with a grid and axes. - * - * @param {Group} groupToRender The Group to be rendered - */ -export function render_grid_axes(groupToRender: Group): RenderGroup { - groupToRender.store(); - // Render group is returned for REPL text only; do not document - return Core.getRenderGroupManager() - .nextRenderGroup(true, true); -} - -/** - * Renders a Group of Shapes, along with a grid. - * - * @param {Group} groupToRender The Group to be rendered - */ -export function render_grid(groupToRender: Group): RenderGroup { - groupToRender.store(); - return Core.getRenderGroupManager() - .nextRenderGroup(true); -} - -/** - * Renders a Group of Shapes, along with X, Y and Z axes. - * - * @param {Group} groupToRender The Group to be rendered - */ -export function render_axes(groupToRender: Group): RenderGroup { - groupToRender.store(); - return Core.getRenderGroupManager() - .nextRenderGroup(undefined, true); -} - -/** - * Renders a Group of Shapes. - * - * @param {Group} groupToRender The Group to be rendered - */ -export function render(groupToRender: Group): RenderGroup { - groupToRender.store(); - return Core.getRenderGroupManager() - .nextRenderGroup(); -} - -/** - * Converts a shape to an downloadable STL file, which can be used for 3D printing. - */ -export async function shape_to_stl(shape: Shape): Promise { - await save( - new Blob(serialize({ binary: true }, shape.solid)), - 'Source Academy CSG' + '.stl', - ); -} +/** + * 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/csg/index.ts b/src/bundles/csg/index.ts index bcb051f3d..71c9ca359 100644 --- a/src/bundles/csg/index.ts +++ b/src/bundles/csg/index.ts @@ -3,6 +3,8 @@ import context from 'js-slang/context'; import { Core } from './core.js'; import { CsgModuleState } from './utilities.js'; + + /* [Main] */ let moduleState = new CsgModuleState(); @@ -10,49 +12,63 @@ context.moduleContexts.csg.state = moduleState; // We initialise Core for the first time over on the bundles' end here Core.initialize(moduleState); + + /* [Exports] */ export { + // Colors black, - blue, - bounding_box, - cone, + navy, + green, + teal, crimson, - cube, - cyan, - cylinder, - geodesic_sphere, + purple, + orange, + silver, gray, - green, - group, - intersect, - is_group, - is_shape, + blue, lime, - navy, - orange, - pink, - prism, - purple, - pyramid, - render, - render_axes, - render_grid, - render_grid_axes, - rgb, + cyan, rose, - rotate, + pink, + yellow, + white, + + // Primitives + cube, rounded_cube, + cylinder, rounded_cylinder, - scale, - shape_to_stl, - silver, sphere, + geodesic_sphere, + pyramid, + cone, + prism, star, - subtract, - teal, torus, - translate, + + // Operations union, - white, - yellow, + subtract, + intersect, + + // Transformations + translate, + rotate, + scale, + + // Utilities + group, + ungroup, + is_shape, + is_group, + bounding_box, + rgb, + download_shape_stl, + + // Rendering + render, + render_grid, + render_axes, + render_grid_axes, } from './functions'; diff --git a/src/bundles/csg/jscad/renderer.ts b/src/bundles/csg/jscad/renderer.ts index 2e7c3d011..8f9a785da 100644 --- a/src/bundles/csg/jscad/renderer.ts +++ b/src/bundles/csg/jscad/renderer.ts @@ -8,9 +8,6 @@ import { prepareRender, } from '@jscad/regl-renderer'; import { - ACE_GUTTER_BACKGROUND_COLOR, - ACE_GUTTER_TEXT_COLOR, - BP_TEXT_COLOR, DEFAULT_COLOR, GRID_PADDING, MAIN_TICKS, @@ -39,6 +36,9 @@ import type { WrappedRendererData, ZoomToFitStates, } from './types.js'; +import { ACE_GUTTER_BACKGROUND_COLOR, ACE_GUTTER_TEXT_COLOR, BP_TEXT_COLOR } from '../../../tabs/common/css_constants.js'; + + /* [Main] */ let { orbit } = controls; diff --git a/src/bundles/csg/samples.md b/src/bundles/csg/samples.md deleted file mode 100644 index 554d66c9a..000000000 --- a/src/bundles/csg/samples.md +++ /dev/null @@ -1,408 +0,0 @@ -```js -import { - cube, - sphere, - cylinder, - prism, - star, - pyramid, - cone, - torus, - rounded_cube, - rounded_cylinder, - geodesic_sphere, - - black, - navy, - green, - teal, - crimson, - purple, - orange, - silver, - gray, - blue, - lime, - cyan, - rose, - pink, - yellow, - white, - - union, - subtract, - intersect, - scale, - translate, - rotate, - group, - rgb, - bounding_box, - is_shape, - is_group, - render_grid_axes, - render_grid, - render_axes, - render, - shape_to_stl -} from 'csg'; -``` - -```js -// Showcase of the 16 default colours as a grid of spheres -// Source 4 - -let colours = [ - black, - navy, - green, - teal, - crimson, - purple, - orange, - silver, - gray, - blue, - lime, - cyan, - rose, - pink, - yellow, - white -]; - - -function translate_x(entity, factor) { - return translate(entity, factor, 0, 0); -} - -function translate_y(entity, factor) { - return translate(entity, 0, factor, 0); -} - -let lst = build_list(x => - translate_y( - translate_x(sphere(colours[x]), x % 4 * 2), - math_floor(x / 4) * 2), - 16); - -render_grid(group(lst)); -``` - -```js -// A spaghetti-code version of -// Me: Can I have Source Academy ship -// Mum: No, we have Source Academy ship at home -// Source Academy ship at home: -// Source 4 - -function scale_all(entity, factor) { - return scale(entity, factor, factor, factor); -} - -function scale_x(entity, factor) { - return scale(entity, factor, 1, 1); -} - -function scale_y(entity, factor) { - return scale(entity, 1, factor, 1); -} - -function scale_z(entity, factor) { - return scale(entity, 1, 1, factor); -} - -function translate_x(entity, factor) { - return translate(entity, factor, 0, 0); -} - -function translate_y(entity, factor) { - return translate(entity, 0, factor, 0); -} - -function translate_z(entity, factor) { - return translate(entity, 0, 0, factor); -} - -function rotate_x(entity, factor) { - return rotate(entity, factor, 0, 0); -} - -function rotate_y(entity, factor) { - return rotate(entity, 0, factor, 0); -} - -function rotate_z(entity, factor) { - return rotate(entity, 0, 0, factor); -} - -let off_white = "#CCCCCC"; -let darker_silver = "#777777"; -let shining_cyan = "#88FFFF"; - -// function store_scaled(shape, colour) { -// let factor = 50; -// shape = scale(shape, factor, factor, factor); -// store_as_color(shape, colour); -// } - -function centre(shape) { - const bounds = bounding_box(shape); - const offset_x = 0.5 -(bounds('x','min') + ((bounds('x','max') - bounds('x','min')) / 2)); - const offset_y = 0.5 -(bounds('y','min') + ((bounds('y','max') - bounds('y','min')) / 2)); - const offset_z = 0.5 -(bounds('z','min') + ((bounds('z','max') - bounds('z','min')) / 2)); - return translate(shape, offset_x, offset_y, offset_z); -} - -function clone(shape) { - return shape; -} - -function centre_using(target, source) { - const get = bounding_box(source); - const bounds = bounding_box(target); - const offset_x = get('x', 'min') + (get('x', 'max') - get('x', 'min')) / 2 - - (bounds('x','min') + ((bounds('x','max') - bounds('x','min')) / 2)); - const offset_y = get('y', 'min') + (get('y', 'max') - get('y', 'min')) / 2 - - (bounds('y','min') + ((bounds('y','max') - bounds('y','min')) / 2)); - const offset_z = get('z', 'min') + (get('z', 'max') - get('z', 'min')) / 2 - - (bounds('z','min') + ((bounds('z','max') - bounds('z','min')) / 2)); - return translate(target, offset_x, offset_y, offset_z); -} - - - - -// Main body -let main_body = scale(translate_z(rounded_cylinder(silver), 1), 1, 1, 1/3); - -// display(bounding_box(main_body)); - -//(Prong base, inner prong base) -let prong = sphere(orange); - -prong = subtract(prong, translate_z(cube(orange), 0.5)); -prong = subtract(prong, translate_x(centre(scale(sphere(orange), 1, 2, 1)), -0.75)); - -prong = translate_z(prong, -0.5); - -let inner_prong = clone(prong); -inner_prong = scale_all(inner_prong, 0.85); -inner_prong = centre_using(inner_prong, prong); - -prong = subtract( - prong, - translate( - centre(rotate_y( - centre(scale( - cube(orange), - 0.05, - 1, - 2 - )), - math_PI / 2 / 16 - )), - 0.35, - 0, - 0 - ) -); - - - -let foot = cylinder(orange); -foot = centre(rotate_y(foot, math_PI / 2)); -foot = centre(scale_x(foot, 0.25)); -foot = subtract(foot, translate_z(cube(purple), -0.5)); -foot = translate_z(foot, -1); -prong = union(prong, foot); - - - - - -prong = scale_y(prong, 1/8); -prong = scale_z(prong, 1/3); -inner_prong = scale_y(inner_prong, 1/8); -inner_prong = scale_z(inner_prong, 1/3); - -prong = translate(prong, 0.5, -1/16, 0.5); -inner_prong = translate(inner_prong, 0.55, -1/16, 0.5); - -// Left prong, inner left prong -let left_prong = prong; -let inner_left_prong = inner_prong; - -left_prong = translate_y(left_prong, -0.05); -inner_left_prong = translate_y(inner_left_prong, -0.05); - -// // Right prong, inner right prong -let right_prong = prong; -let inner_right_prong = inner_prong; - -right_prong = translate_y(right_prong, 1.05); -inner_right_prong = translate_y(inner_right_prong, 1.05); - -// Shield -let shield = torus(orange); - -shield = centre(shield); - -// shield = translate_z(shield, 0.45); - -shield = subtract(shield, centre(scale(sphere(orange), 0.9, 0.9, 1))); - -shield = centre(scale_all(shield, 1.2)); -shield = centre(scale_z(shield, 1.3)); - -let cubee = scale(cube(orange), 2/3, 2/3, 1); -shield = subtract(shield, translate_x(centre(scale(cube(orange), 2/3, 2/3, 1)), 0.5)); - -let ball = centre(scale_all(sphere(orange), 0.1)); -shield = union( - shield, - translate( - ball, - 1/12, - -0.55, - -1/12 - ) -); -shield = union( - shield, - translate( - ball, - 1/12, - 0.55, - -1/12 - ) -); - -shield = translate_z(shield, 0.05); - -// Small spot -let small_spot = rounded_cylinder(orange); - -small_spot = rotate_y(small_spot, math_PI / 2 / -16); -small_spot = centre(small_spot); - -small_spot = scale_all(small_spot, 1/6); -small_spot = centre(small_spot); - -small_spot = translate(small_spot, -0.2, 0, 1/6 - 0.05); - -// Big spot -let big_spot = rounded_cylinder(darker_silver); - -big_spot = rotate_y(big_spot, math_PI / 2 / -16); -big_spot = centre(big_spot); - -big_spot = scale_all(big_spot, 1/3); -big_spot = centre(big_spot); - -big_spot = translate(big_spot, -0.2, 0, 1/6 - 0.15); - -// Window -let window = rounded_cube(shining_cyan); - -window = scale(window, 1, 1/2, 1/10); -window = centre(window); -window = subtract(window, translate_x(cube(shining_cyan), -0.5)); -window = translate_z(window, -0.04); - -// (Ring base) -let ring = sphere(off_white); - -ring = scale(ring, 1.02, 1.02, 0.05); -ring = centre(ring); - -// Orange ring -let orange_ring = sphere(orange); - -orange_ring = scale(orange_ring, 1.02, 1.02, 0.05); -orange_ring = centre(orange_ring); - -orange_ring = translate_z(orange_ring, 0.08); - -// Ring 1 -let ring_1 = sphere(off_white); - -ring_1 = scale(ring_1, 1.02, 1.02, 0.05); -ring_1 = centre(ring_1); - -ring_1 = translate_z(ring_1, 0.08 - 0.015); - -// Ring 2 -let ring_2 = translate_z(ring_1, -0.015); - -// Ring 3 -let ring_3 = translate_z(ring_2, -0.015); - -// Done - -render_grid_axes(scale_all(group(list(main_body, left_prong, inner_left_prong, - right_prong, inner_right_prong, shield, small_spot, big_spot, - window, orange_ring, ring_1, ring_2, ring_3)), 50)); -``` - -```js -/* CLASSIC BOOLEAN OPERATIONS */ - -const Cube = centre(scale_all(cube(purple), 0.8)); -const Sphere = centre(sphere(navy)); - -const A = centre(scale(cylinder(teal), 0.4, 0.4, 1)); -const B = centre(translate(rotate_x(A, math_PI/2), 0, 0.75, 0.25)); -const C = centre(translate(rotate_y(A, math_PI/2), -0.25, 0, 0.75)); -let cylinder_union = union(A,B); -cylinder_union = union(cylinder_union,C); - -let fancy_shape = intersect(Cube,Sphere); -fancy_shape = subtract(fancy_shape, cylinder_union); - -render_grid(scale_all(fancy_shape,5)); -shape_to_stl(fancy_shape); -``` - -```js -/* CYLINDER INTERSECT */ - -const A_ = cylinder(blue); -const B_ = translate_y(rotate_x(cylinder(green), math_PI/2),1); -const C_ = translate_z(rotate_y(cylinder(yellow),math_PI/2),1); - -let steinmetz_solid = intersect(A_, B_); -steinmetz_solid = intersect(steinmetz_solid, C_); -render_grid(scale_all(steinmetz_solid,5)); -``` - -```js -/* CHRISTMAS TREE */ - -const branch = translate(scale(pyramid(green),1,1,0.25),-0.5,-0.5,0.3); -const trunk = scale( - translate(cylinder("#8B4513"),-0.5,-0.5,0.1), - 0.3,0.3,0.3); -const starAbove = translate( - rotate( - scale( - translate(star("#FFD700"),-0.5,-0.5,0), - 0.1,0.1,0.02), - 0,math_PI/2,0), - 0,0,1.5); - - -const num_layers = 15; -function build_xmas_tree(n, tree) { - return n === 0 - ? tree - : build_xmas_tree(n - 1, - group(list(tree, translate( - scale(branch, n/num_layers,n/num_layers,1), - 0,0, 1- n/num_layers) - ))); - -} - -render_grid(group(list(starAbove,build_xmas_tree(num_layers,branch),trunk))); -``` \ No newline at end of file diff --git a/src/bundles/csg/samples/_imports.js b/src/bundles/csg/samples/_imports.js new file mode 100644 index 000000000..e8eb3e549 --- /dev/null +++ b/src/bundles/csg/samples/_imports.js @@ -0,0 +1,136 @@ + +import { + // Color + black, + navy, + green, + teal, + crimson, + purple, + orange, + silver, + gray, + blue, + lime, + cyan, + rose, + pink, + yellow, + white, + + // Primitive + cube, + rounded_cube, + cylinder, + rounded_cylinder, + sphere, + geodesic_sphere, + pyramid, + cone, + prism, + star, + torus, + + // Operation + union, + subtract, + intersect, + + // Transformation + translate, + rotate, + scale, + + // Utility + group, + ungroup, + is_shape, + is_group, + bounding_box, + rgb, + download_shape_stl, + + // Render + render, + render_grid, + render_axes, + render_grid_axes +} from 'csg'; + +function translate_x(operable, factor) { + return translate(operable, factor, 0, 0); +} + +function translate_y(operable, factor) { + return translate(operable, 0, factor, 0); +} + +function translate_z(operable, factor) { + return translate(operable, 0, 0, factor); +} + +function rotate_x(operable, factor) { + return rotate(operable, factor, 0, 0); +} + +function rotate_y(operable, factor) { + return rotate(operable, 0, factor, 0); +} + +function rotate_z(operable, factor) { + return rotate(operable, 0, 0, factor); +} + +function scale_all(operable, factor) { + return scale(operable, factor, factor, factor); +} + +function scale_x(operable, factor) { + return scale(operable, factor, 1, 1); +} + +function scale_y(operable, factor) { + return scale(operable, 1, factor, 1); +} + +function scale_z(operable, factor) { + return scale(operable, 1, 1, factor); +} + +function _get_shape_middle(shape, axis) { + let get = bounding_box(shape); + let start = get(axis, 'min'); + let end = get(axis, 'max'); + let length = end - start; + return start + (length / 2); +} + +function _centre_at(shape, x, y, z) { + function calculate_offset(axis, centre_coord) { + return -_get_shape_middle(shape, axis) + centre_coord; + } + + return translate( + shape, + calculate_offset('x', x), + calculate_offset('y', y), + calculate_offset('z', z) + ); +} + +function centre(shape) { + return _centre_at(shape, 0.5, 0.5, 0.5); +} + +function centre_using(target, reference) { + return _centre_at( + target, + _get_shape_middle(reference, 'x'), + _get_shape_middle(reference, 'y'), + _get_shape_middle(reference, 'z') + ); +} + +function degrees_to_radians(degrees) { + return (degrees / 360) * (2 * math_PI); +} diff --git a/src/bundles/csg/samples/christmas.js b/src/bundles/csg/samples/christmas.js new file mode 100644 index 000000000..a923aaf89 --- /dev/null +++ b/src/bundles/csg/samples/christmas.js @@ -0,0 +1,29 @@ +// Source §2 +// Christmas tree + +const branch = translate(scale(pyramid(green),1,1,0.25),-0.5,-0.5,0.3); +const trunk = scale( + translate(cylinder("#8B4513"),-0.5,-0.5,0.1), + 0.3,0.3,0.3); +const starAbove = translate( + rotate( + scale( + translate(star("#FFD700"),-0.5,-0.5,0), + 0.1,0.1,0.02), + 0,math_PI/2,0), + 0,0,1.5); + + +const num_layers = 15; +function build_xmas_tree(n, tree) { + return n === 0 + ? tree + : build_xmas_tree(n - 1, + group(list(tree, translate( + scale(branch, n/num_layers,n/num_layers,1), + 0,0, 1- n/num_layers) + ))); + +} + +render_grid(group(list(starAbove,build_xmas_tree(num_layers,branch),trunk))); diff --git a/src/bundles/csg/samples/colours.js b/src/bundles/csg/samples/colours.js new file mode 100644 index 000000000..17027f12b --- /dev/null +++ b/src/bundles/csg/samples/colours.js @@ -0,0 +1,34 @@ +// Source §4 +// Showcase of the 16 default colours as a grid of spheres + +let colours = [ + black, + navy, + green, + teal, + crimson, + purple, + orange, + silver, + gray, + blue, + lime, + cyan, + rose, + pink, + yellow, + white +]; + +let l = build_list( + i => translate_y( + translate_x( + sphere(colours[i]), + ((i % 4) - 2) * 2 + ), + (math_floor(i / 4) - 2) * 2 + ), + 16 +); + +render_grid(group(l)); diff --git a/src/bundles/csg/samples/operations.js b/src/bundles/csg/samples/operations.js new file mode 100644 index 000000000..f14b46faf --- /dev/null +++ b/src/bundles/csg/samples/operations.js @@ -0,0 +1,17 @@ +// Source §3 +// Classic boolean operations demo + +const Cube = centre(scale_all(cube(purple), 0.8)); +const Sphere = centre(sphere(navy)); + +const A = centre(scale(cylinder(teal), 0.4, 0.4, 1)); +const B = centre(translate(rotate_x(A, math_PI/2), 0, 0.75, 0.25)); +const C = centre(translate(rotate_y(A, math_PI/2), -0.25, 0, 0.75)); +let cylinder_union = union(A,B); +cylinder_union = union(cylinder_union,C); + +let fancy_shape = intersect(Cube,Sphere); +fancy_shape = subtract(fancy_shape, cylinder_union); + +render_grid(scale_all(fancy_shape,5)); +// shape_to_stl(fancy_shape); diff --git a/src/bundles/csg/samples/primitives.js b/src/bundles/csg/samples/primitives.js new file mode 100644 index 000000000..07f813823 --- /dev/null +++ b/src/bundles/csg/samples/primitives.js @@ -0,0 +1,29 @@ +// Source §4 +// Showcase of all 11 primitive Shapes provided by default + +let primitives = [ + cube, + rounded_cube, + cylinder, + rounded_cylinder, + sphere, + geodesic_sphere, + pyramid, + cone, + prism, + star, + torus +]; + +let l = build_list( + i => translate_y( + translate_x( + primitives[i](silver), + ((i % 4) - 2) * 2 + ), + (math_floor(i / 4) - 2) * 2 + ), + 11 +); + +render_grid(group(l)); diff --git a/src/bundles/csg/samples/rotation.js b/src/bundles/csg/samples/rotation.js new file mode 100644 index 000000000..93764a233 --- /dev/null +++ b/src/bundles/csg/samples/rotation.js @@ -0,0 +1,17 @@ +// Source §3 +// Showcase of all 11 primitive Shapes provided by default + +let a = cube(green); +let b = cube(blue); + +a = rotate_x(a, degrees_to_radians(30)); +a = rotate_y(a, degrees_to_radians(30)); +a = rotate_z(a, degrees_to_radians(30)); + +b = rotate_z(b, degrees_to_radians(30)); +b = rotate_x(b, degrees_to_radians(30)); +b = rotate_y(b, degrees_to_radians(30)); + +// b = translate_x(b, 2); + +render_grid_axes(group(list(a, b))); diff --git a/src/bundles/csg/samples/ship.js b/src/bundles/csg/samples/ship.js new file mode 100644 index 000000000..fe7b38a4b --- /dev/null +++ b/src/bundles/csg/samples/ship.js @@ -0,0 +1,186 @@ +// Source §3 +// Me: Can I have Source Academy ship +// Mum: No, we have Source Academy ship at home +// Source Academy ship at home: + +/* [Utility Functions] */ +function debug(shape) { + let get = bounding_box(shape); + let xStart = get('x', 'min'); + let xEnd = get('x', 'max'); + let yStart = get('y', 'min'); + let yEnd = get('y', 'max'); + let zStart = get('z', 'min'); + let zEnd = get('z', 'max'); + + display("x: " + stringify(xStart) + " - " + stringify(xEnd)); + display("y: " + stringify(yStart) + " - " + stringify(yEnd)); + display("z: " + stringify(zStart) + " - " + stringify(zEnd)); +} + +/* [Extra Colours] */ +let off_white = "#CCCCCC"; +let darker_silver = "#777777"; +let shining_cyan = "#88FFFF"; + + + +/* [Main] */ + +// [Main Body] +let main_body = rounded_cylinder(silver); +// Flatten downwards +main_body = centre(scale_z(main_body, 1 / 3)); + +// [Template: Prong & Inner Prong] +function make_prong_draft(colour) { + let whole_prong = sphere(colour); + // Cut off top half + whole_prong = subtract(whole_prong, translate_z(cube(colour), 0.5)); + // Make a curved cut to remove the back portion + whole_prong = subtract(whole_prong, translate(centre(scale(sphere(colour), 1, 5, 1)), -0.75, 0, -0.1)); + + return whole_prong; +} +let prong = make_prong_draft(off_white); + +// Make a thin, slightly angled straight cut near the front +prong = subtract( + prong, + translate_x( + centre(rotate_y( + centre(scale_x( + cube(silver), + 0.04 + )), + degrees_to_radians(5) + )), + 0.35 + ) +); + +// Add a smaller, different colour copy of the prong draft inside +let inner_prong = make_prong_draft(silver); +inner_prong = scale_all(inner_prong, 0.85); +inner_prong = centre_using(inner_prong, prong); +inner_prong = translate_x(inner_prong, 0.03); +prong = union(prong, inner_prong); + +let foot = cylinder(off_white); +// Rotate forward +foot = centre(rotate_y(foot, degrees_to_radians(90))); +// Flatten forward +foot = centre(scale_x(foot, 0.25)); +// Cut off bottom half +foot = subtract(foot, translate_z(cube(silver), -0.5)); +// Move down forward and below prong +foot = translate(foot, 0.08, 0, -0.5); +prong = union(prong, foot); + +// Warp prong +prong = scale(prong, 1, 1 / 8, 1 / 3); +// Shift forward and up, sit balanced through x axis +prong = translate(prong, 0.5, -1 / 16, 0.35); + +let ball = sphere(gray); +// Add blue lights to ball +let light = cylinder(shining_cyan); +light = centre(rotate_x(light, degrees_to_radians(90))); +light = centre(scale(light, 0.2, 1, 0.2)); +ball = union(ball, light); +// Shrink down +ball = centre(scale_all(ball, 0.1)); +// Add ball behind prong +ball = translate(ball, 0.15, -0.5, -0.03); +prong = union(prong, ball); + +/* [Prongs] */ +let left_prong = translate_y(prong, -0.05); +let right_prong = translate_y(prong, 1.05); + +// render_grid_axes(group(list(main_body, left_prong, right_prong))); + +//TODO +// Shield +let shield = torus(darker_silver); + +shield = centre(shield); + +shield = subtract(shield, centre(scale(sphere(darker_silver), 0.9, 0.9, 1))); + +shield = centre(scale_all(shield, 1.2)); +shield = centre(scale_z(shield, 1.3)); +shield = subtract(shield, translate_x(centre(scale(cube(darker_silver), 2/3, 2/3, 1)), 0.5)); + +shield = translate_z(shield, 0.05); + +// Small spot +let small_spot = rounded_cylinder(orange); + +small_spot = rotate_y(small_spot, math_PI / 2 / -16); +small_spot = centre(small_spot); + +small_spot = scale_all(small_spot, 1/6); +small_spot = centre(small_spot); + +small_spot = translate(small_spot, -0.2, 0, 1/6 - 0.05); + +// Big spot +//TODO bigger +let big_spot = rounded_cylinder(darker_silver); + +big_spot = rotate_y(big_spot, math_PI / 2 / -16); +big_spot = centre(big_spot); + +big_spot = scale_all(big_spot, 1/3); +big_spot = centre(big_spot); + +big_spot = translate(big_spot, -0.2, 0, 1/6 - 0.15); + +// Window +let window = rounded_cube(shining_cyan); + +window = scale(window, 1, 1/2, 1/10); +window = centre(window); +window = subtract(window, translate_x(cube(shining_cyan), -0.5)); +window = translate_z(window, -0.04); + +// Orange ring +let orange_ring = sphere(orange); + +orange_ring = scale(orange_ring, 1.02, 1.02, 0.05); +orange_ring = centre(orange_ring); + +orange_ring = translate_z(orange_ring, 0.08); + +// Ring 1 +let ring_1 = sphere(white); + +ring_1 = scale(ring_1, 1.02, 1.02, 0.05); +ring_1 = centre(ring_1); + +ring_1 = translate_z(ring_1, 0.08 - 0.015); + +// Ring 2 +let ring_2 = translate_z(ring_1, -0.015); + +// Ring 3 +let ring_3 = translate_z(ring_2, -0.015); + +//TODO add ring 4 +//TODO tilt the whole main body slightly forward after union + +// Done +render_grid_axes(group(list( + main_body, + left_prong, + right_prong, + shield, + small_spot, + big_spot, + window, + orange_ring, + ring_1, + ring_2, + ring_3 +))); diff --git a/src/bundles/csg/samples/sierpinski.js b/src/bundles/csg/samples/sierpinski.js new file mode 100644 index 000000000..8da1d5d36 --- /dev/null +++ b/src/bundles/csg/samples/sierpinski.js @@ -0,0 +1,49 @@ +// Source §1 +// Prof Martin's Sierpinski fractals + +import { union, translate, scale, render, pyramid, sphere, cube } +from 'csg'; + +function repeat(n, trans, s) { + return n === 0 + ? s + : repeat(n - 1, trans, trans(s)); +} + +// sierpinski returns a shape transformer +// following Sierpinski's 3D fractal scheme +// v: vertical displacement of original shape +//. for lower level shapes +// h: horizontal displacement of original shape +//. for lower level shapes +function sierpinski(v, h) { + return o => { + const t1 = translate(o, h, h, -v); + const t2 = translate(o, -h, h, -v); + const t3 = translate(o, h, -h, -v); + const t4 = translate(o, -h, -h, -v); + const c = union(o, + union(union(t1, t2), + union(t3, t4))); + const c_scaled = scale(c, 0.5, 0.5, 0.5); + return c_scaled; + }; +} + +render(repeat(5, + sierpinski(1, 0.5), + pyramid('#edd4c8'))); + +// spheres are computationally expensive +// only try repeat 2 +/* +render(repeat(2, + sierpinski(0.75, 0.75), + sphere('#edd4c8'))); +*/ + +/* +render(repeat(6, + sierpinski(1, 1), + cube('#edd4c8'))); +*/ diff --git a/src/bundles/csg/samples/snowglobe.js b/src/bundles/csg/samples/snowglobe.js new file mode 100644 index 000000000..c6187a136 --- /dev/null +++ b/src/bundles/csg/samples/snowglobe.js @@ -0,0 +1,19 @@ +// Source §1 +// Snowglobe example used in the functions.ts summary at the top of the file + +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); diff --git a/src/bundles/csg/samples/steinmetz.js b/src/bundles/csg/samples/steinmetz.js new file mode 100644 index 000000000..4e10db8b7 --- /dev/null +++ b/src/bundles/csg/samples/steinmetz.js @@ -0,0 +1,10 @@ +// Source §3 +// Cylinder intersects - Steinmetz solid + +const A_ = cylinder(blue); +const B_ = translate_y(rotate_x(cylinder(green), math_PI/2),1); +const C_ = translate_z(rotate_y(cylinder(yellow),math_PI/2),1); + +let steinmetz_solid = intersect(A_, B_); +steinmetz_solid = intersect(steinmetz_solid, C_); +render_grid(scale_all(steinmetz_solid,5)); diff --git a/src/bundles/csg/stateful_renderer.ts b/src/bundles/csg/stateful_renderer.ts index 642709eea..8bf46625e 100644 --- a/src/bundles/csg/stateful_renderer.ts +++ b/src/bundles/csg/stateful_renderer.ts @@ -35,7 +35,6 @@ export default class StatefulRenderer { private loseCallback: Function, private restoreCallback: Function, ) { - //FIXME Issue #7 this.cameraState.position = [1000, 1000, 1500]; this.webGlListenerTracker = new ListenerTracker(canvas); diff --git a/src/bundles/csg/types.ts b/src/bundles/csg/types.ts index 7a3468ae0..e813ea89a 100644 --- a/src/bundles/csg/types.ts +++ b/src/bundles/csg/types.ts @@ -27,11 +27,6 @@ export type Color = RGB; export type Mat4 = Float32Array; -// Pair and List -export type Pair = [H, T]; -export type NonEmptyList = Pair; -export type List = null | NonEmptyList; - // @jscad\regl-renderer\src\cameras\perspectiveCamera.js // @jscad\regl-renderer\src\cameras\orthographicCamera.js export type PerspectiveCamera = typeof perspectiveCamera; diff --git a/src/bundles/csg/utilities.ts b/src/bundles/csg/utilities.ts index faf7a8d9d..22667c121 100644 --- a/src/bundles/csg/utilities.ts +++ b/src/bundles/csg/utilities.ts @@ -1,78 +1,83 @@ /* [Imports] */ import { - clone as _clone, transform as _transform, - type Geom3, } from '@jscad/modeling/src/geometries/geom3'; import mat4, { type Mat4 } from '@jscad/modeling/src/maths/mat4'; import { + center as _center, rotate as _rotate, scale as _scale, translate as _translate, + align, } from '@jscad/modeling/src/operations/transforms'; -import type { ModuleContext } from 'js-slang'; -import type { ModuleContexts, ReplResult } from '../../typings/type_helpers.js'; +import type { ReplResult } from '../../typings/type_helpers.js'; import { Core } from './core.js'; import type { AlphaColor, Color, Solid } from './jscad/types.js'; -import { type List } from './types'; -/* [Exports] */ -export interface Entity { - clone: () => Entity; + +/* [Exports] */ +export interface Operable { + applyTransforms: (newTransforms: Mat4) => Operable; store: (newTransforms?: Mat4) => void; - translate: (offset: [number, number, number]) => Entity; - rotate: (offset: [number, number, number]) => Entity; - scale: (offset: [number, number, number]) => Entity; + + translate: (offsets: [number, number, number]) => Operable; + rotate: (angles: [number, number, number]) => Operable; + scale: (factors: [number, number, number]) => Operable; } -export class Group implements ReplResult, Entity { - children: Entity[]; +export class Group implements Operable, ReplResult { + children: Operable[]; + constructor( - public childrenList: List, + _children: Operable[], public transforms: Mat4 = mat4.create(), ) { - this.children = listToArray(childrenList); - } - - toReplString(): string { - return ''; + // Duplicate the array to avoid modifying the original, maintaining + // stateless Operables for the user + this.children = [..._children]; } - clone(): Group { - return new Group(arrayToList(this.children.map((child) => child.clone()))); - } - - store(newTransforms?: Mat4): void { - this.transforms = mat4.multiply( + applyTransforms(newTransforms: Mat4): Operable { + let appliedTransforms: Mat4 = mat4.multiply( mat4.create(), - newTransforms || mat4.create(), + newTransforms, this.transforms, ); - this.children.forEach((child) => { - child.store(this.transforms); + // Return a new object for statelessness + return new Group( + this.children, + appliedTransforms, + ); + } + + store(newTransforms: Mat4 = mat4.create()): void { + let appliedGroup: Group = this.applyTransforms(newTransforms) as Group; + + this.children.forEach((child: Operable) => { + child.store(appliedGroup.transforms); }); } - translate(offset: [number, number, number]): Group { + translate(offsets: [number, number, number]): Group { return new Group( - this.childrenList, + this.children, mat4.multiply( mat4.create(), - mat4.fromTranslation(mat4.create(), offset), + mat4.fromTranslation(mat4.create(), offsets), this.transforms, ), ); } - rotate(offset: [number, number, number]): Group { - const yaw = offset[2]; - const pitch = offset[1]; - const roll = offset[0]; + rotate(angles: [number, number, number]): Group { + let yaw = angles[2]; + let pitch = angles[1]; + let roll = angles[0]; return new Group( - this.childrenList, + this.children, mat4.multiply( mat4.create(), mat4.fromTaitBryanRotation(mat4.create(), yaw, pitch, roll), @@ -81,58 +86,71 @@ export class Group implements ReplResult, Entity { ); } - scale(offset: [number, number, number]): Group { + scale(factors: [number, number, number]): Group { return new Group( - this.childrenList, + this.children, mat4.multiply( mat4.create(), - mat4.fromScaling(mat4.create(), offset), + mat4.fromScaling(mat4.create(), factors), this.transforms, ), ); } -} - -export class Shape implements ReplResult, Entity { - constructor(public solid: Solid) {} toReplString(): string { - return ''; + return ''; + } + + ungroup(): Operable[] { + // Return all children, but we need to account for this Group's unresolved + // transforms by applying them to each child + return this.children.map( + (child: Operable) => child.applyTransforms(this.transforms), + ); } +} + +export class Shape implements Operable, ReplResult { + constructor(public solid: Solid) {} - clone(): Shape { - return new Shape(_clone(this.solid as Geom3)); + applyTransforms(newTransforms: Mat4): Operable { + // Return a new object for statelessness + return new Shape(_transform(newTransforms, this.solid)); } - store(newTransforms?: Mat4): void { + store(newTransforms: Mat4 = mat4.create()): void { Core.getRenderGroupManager() .storeShape( - new Shape(_transform(newTransforms || mat4.create(), this.solid)), + this.applyTransforms(newTransforms) as Shape, ); } - translate(offset: [number, number, number]): Shape { - return new Shape(_translate(offset, this.solid)); + translate(offsets: [number, number, number]): Shape { + return new Shape(_translate(offsets, this.solid)); + } + + rotate(angles: [number, number, number]): Shape { + return new Shape(_rotate(angles, this.solid)); } - rotate(offset: [number, number, number]): Shape { - return new Shape(_rotate(offset, this.solid)); + scale(factors: [number, number, number]): Shape { + return new Shape(_scale(factors, this.solid)); } - scale(offset: [number, number, number]): Shape { - return new Shape(_scale(offset, this.solid)); + toReplString(): string { + return ''; } } export class RenderGroup implements ReplResult { - constructor(public canvasNumber: number) {} - render: boolean = false; hasGrid: boolean = true; hasAxis: boolean = true; shapes: Shape[] = []; + constructor(public canvasNumber: number) {} + toReplString(): string { return ``; } @@ -200,11 +218,15 @@ export class CsgModuleState { } } -export function getModuleContext( - moduleContexts: ModuleContexts, -): ModuleContext | null { - let potentialModuleContext: ModuleContext | undefined = moduleContexts.csg; - return potentialModuleContext ?? null; +export function centerPrimitive(shape: Shape) { + // Move centre of Shape to 0.5, 0.5, 0.5 + let solid: Solid = _center( + { + relativeTo: [0.5, 0.5, 0.5], + }, + shape.solid, + ); + return new Shape(solid); } export function hexToColor(hex: string): Color { @@ -232,36 +254,3 @@ export function colorToAlphaColor( export function hexToAlphaColor(hex: string): AlphaColor { return colorToAlphaColor(hexToColor(hex)); } - -export function clamp(value: number, lowest: number, highest: number): number { - value = Math.max(value, lowest); - value = Math.min(value, highest); - return value; -} - -function length(list: List): number { - let counter = 0; - while (!(list === null)) { - list = list[1]; - counter++; - } - return counter; -} - -function listToArray(list: List): Entity[] { - let retArr = new Array(length(list)); - let pointer = 0; - while (!(list === null)) { - retArr[pointer++] = list[0]; - list = list[1]; - } - return retArr; -} - -function arrayToList(arr: Entity[]): List { - let retList: List = null; - for (let i = arr.length - 1; i >= 0; --i) { - retList = [arr[i], retList]; - } - return retList; -} diff --git a/src/bundles/curve/functions.ts b/src/bundles/curve/functions.ts index 774d39fb3..06c520a1b 100644 --- a/src/bundles/curve/functions.ts +++ b/src/bundles/curve/functions.ts @@ -216,7 +216,7 @@ export const draw_points_full_view_proportional = createDrawFunction( * * @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 + * @return function of type 3D Curve → Drawing * @example * ``` * draw_3D_connected(100)(t => make_3D_point(t, t, t)); @@ -237,7 +237,7 @@ export const draw_3D_connected = createDrawFunction( * * @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 + * @return function of type 3D Curve → Drawing * @example * ``` * draw_3D_connected_full_view(100)(t => make_3D_point(t, t, t)); @@ -258,7 +258,7 @@ export const draw_3D_connected_full_view = createDrawFunction( * * @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 + * @return function of type 3D Curve → Drawing * @example * ``` * draw_3D_connected_full_view_proportional(100)(t => make_3D_point(t, t, t)); @@ -279,7 +279,7 @@ export const draw_3D_connected_full_view_proportional = createDrawFunction( * * @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 + * @return function of type 3D Curve → Drawing * @example * ``` * draw_3D_points(100)(t => make_3D_point(t, t, t)); @@ -295,7 +295,7 @@ export const draw_3D_points = createDrawFunction('none', 'points', '3D', false); * * @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 + * @return function of type 3D Curve → Drawing * @example * ``` * draw_3D_points_full_view(100)(t => make_3D_point(t, t, t)); @@ -316,7 +316,7 @@ export const draw_3D_points_full_view = createDrawFunction( * * @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 + * @return function of type 3D Curve → Drawing * @example * ``` * draw_3D_points_full_view_proportional(100)(t => make_3D_point(t, t, t)); @@ -798,12 +798,22 @@ export function arc(t: number): Point { } /** - * Create a animation of curves using a curve generating function. - * @param duration The duration of the animation in seconds - * @param fps Framerate of the animation in frames per second - * @param drawer Draw function to the generated curves with - * @param func Curve generating function. Takes in a timestamp value and returns a curve - * @return Curve Animation + * This function creates an animated Curve using the specified drawer function + * and Curve generator. + * + * The Curve generator should take a number and return a Curve. The number is + * the timestamp value of the animation, given in seconds. For example, when + * this function wants to request the Curve that should be used 7.5 seconds into + * the animation, it will pass in a timestamp of 7.5. + * + * The generated Curves are drawn using the specified drawer function of type + * Curve → Drawing. + * + * @param duration animation duration in seconds + * @param fps frame rate of the animation in frames per second + * @param drawer drawer function + * @param func Curve generator function + * @returns animated Curve */ export function animate_curve( duration: number, @@ -821,12 +831,22 @@ export function animate_curve( } /** - * Create a animation of curves using a curve generating function. - * @param duration The duration of the animation in seconds - * @param fps Framerate of the animation in frames per second - * @param drawer Draw function to the generated curves with - * @param func Curve generating function. Takes in a timestamp value and returns a curve - * @return 3D Curve Animation + * This function creates an animated 3D Curve using the specified 3D drawer + * function and 3D Curve generator. + * + * The 3D Curve generator should take a number and return a 3D Curve. The number + * is the timestamp value of the animation, given in seconds. For example, when + * this function wants to request the 3D Curve that should be used 7.5 seconds + * into the animation, it will pass in a timestamp of 7.5. + * + * The generated 3D Curves are drawn using the specified 3D drawer function of + * type 3D Curve → Drawing. + * + * @param duration animation duration in seconds + * @param fps frame rate of the animation in frames per second + * @param drawer 3D drawer function + * @param func 3D Curve generator function + * @returns animated 3D Curve */ export function animate_3D_curve( duration: number, diff --git a/src/bundles/curve/samples/canvases.js b/src/bundles/curve/samples/canvases.js new file mode 100644 index 000000000..cbac23d7c --- /dev/null +++ b/src/bundles/curve/samples/canvases.js @@ -0,0 +1,10 @@ +// Source §1 +// Simple curves that trigger the various types of canvases curve uses + +draw_connected_full_view(20)(unit_circle); + +draw_3D_connected(100)(t => make_3D_point(t, t * t, t)); + +animate_curve(3, 30, draw_connected_full_view_proportional(100), s => t => make_point(t - s, t * s)); + +animate_3D_curve(3, 40, draw_3D_connected_full_view_proportional(100), s => t => make_3D_point(t - s, t - t * s, t * s)); diff --git a/src/bundles/curve/samples/imports.js b/src/bundles/curve/samples/imports.js new file mode 100644 index 000000000..05d820448 --- /dev/null +++ b/src/bundles/curve/samples/imports.js @@ -0,0 +1,39 @@ + +import { + animate_3D_curve, + animate_curve, + arc, + b_of, + connect_ends, + connect_rigidly, + draw_3D_connected, + draw_3D_connected_full_view, + draw_3D_connected_full_view_proportional, + draw_3D_points, + draw_3D_points_full_view, + draw_3D_points_full_view_proportional, + draw_connected, + draw_connected_full_view, + draw_connected_full_view_proportional, + draw_points, + draw_points_full_view, + draw_points_full_view_proportional, + g_of, + invert, + make_3D_color_point, + make_3D_point, + make_color_point, + make_point, + put_in_standard_position, + r_of, + rotate_around_origin, + scale, + scale_proportional, + translate, + unit_circle, + unit_line, + unit_line_at, + x_of, + y_of, + z_of +} from 'curve'; diff --git a/src/bundles/rune/runes_ops.ts b/src/bundles/rune/runes_ops.ts index 995095f59..5aa3a9ec9 100644 --- a/src/bundles/rune/runes_ops.ts +++ b/src/bundles/rune/runes_ops.ts @@ -194,7 +194,7 @@ export const getHeart: () => Rune = () => { const root2 = Math.sqrt(2); const r = 4 / (2 + 3 * root2); const scaleX = 1 / (r * (1 + root2 / 2)); - const numPoints = 10; + const numPoints = 100; // right semi-circle const rightCenterX = r / root2; diff --git a/src/bundles/sound/functions.ts b/src/bundles/sound/functions.ts index 50693c23f..f1e0eb60a 100644 --- a/src/bundles/sound/functions.ts +++ b/src/bundles/sound/functions.ts @@ -277,6 +277,10 @@ export function record_for(duration: number, buffer: number): () => Sound { * @example const s = make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5); */ export function make_sound(wave: Wave, duration: number): Sound { + if (duration <= 0) { + throw new Error('Sound duration must be greater than 0'); + } + return pair((t: number) => (t >= duration ? 0 : wave(t)), duration); } diff --git a/src/common/utilities.ts b/src/common/utilities.ts new file mode 100644 index 000000000..15ee21d21 --- /dev/null +++ b/src/common/utilities.ts @@ -0,0 +1,4 @@ +/* [Exports] */ +export function degreesToRadians(degrees: number): number { + return (degrees / 360) * (2 * Math.PI); +} diff --git a/src/tabs/Csg/canvas_holder.tsx b/src/tabs/Csg/canvas_holder.tsx index 0c275a0fd..6ede5a2b4 100644 --- a/src/tabs/Csg/canvas_holder.tsx +++ b/src/tabs/Csg/canvas_holder.tsx @@ -1,173 +1,165 @@ -/* [Imports] */ -import { Spinner, SpinnerSize } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import React from 'react'; -import { - BP_BORDER_RADIUS, - BP_TAB_BUTTON_MARGIN, - BP_TAB_PANEL_MARGIN, - STANDARD_MARGIN, -} from '../../bundles/csg/constants.js'; -import { Core } from '../../bundles/csg/core.js'; -import StatefulRenderer from '../../bundles/csg/stateful_renderer.js'; -import type { RenderGroup } from '../../bundles/csg/utilities.js'; -import HoverControlHint from './hover_control_hint'; -import type { CanvasHolderProps, CanvasHolderState } from './types'; - - - -/* [Main] */ -export default class CanvasHolder extends React.Component< -CanvasHolderProps, -CanvasHolderState -> { - private readonly canvasReference: React.RefObject = React.createRef(); - - private statefulRenderer: StatefulRenderer | null = null; - - constructor(props: CanvasHolderProps) { - super(props); - - this.state = { - contextLost: false, - }; - } - - componentDidMount() { - console.debug(`>>> MOUNT #${this.props.componentNumber}`); - - let { current: canvas } = this.canvasReference; - if (canvas === null) return; - - let renderGroups: RenderGroup[] = Core - .getRenderGroupManager() - .getGroupsToRender(); - //TODO Issue #35 - let lastRenderGroup: RenderGroup = renderGroups.at(-1) as RenderGroup; - - this.statefulRenderer = new StatefulRenderer( - canvas, - lastRenderGroup, - this.props.componentNumber, - - () => this.setState({ contextLost: true }), - () => this.setState({ contextLost: false }), - ); - this.statefulRenderer.start(true); - } - - componentWillUnmount() { - console.debug(`>>> UNMOUNT #${this.props.componentNumber}`); - - this.statefulRenderer?.stop(true); - } - - // Only required method of a React Component. Returns a React Element created - // via JSX to instruct React to render a DOM node. Also attaches the - // canvasReference via the ref attribute, for imperatively modifying the - // canvas - render() { - return ( - <> -
-
- - - - - -
- -
- -
-
-
-

- WebGL Context Lost -

- -

- Your GPU is probably busy. Waiting for browser to re-establish connection... -

-
- - ); - } -} +/* [Imports] */ +import { Spinner, SpinnerSize } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import React from 'react'; +import { Core } from '../../bundles/csg/core.js'; +import StatefulRenderer from '../../bundles/csg/stateful_renderer.js'; +import type { RenderGroup } from '../../bundles/csg/utilities.js'; +import HoverControlHint from './hover_control_hint'; +import type { CanvasHolderProps, CanvasHolderState } from './types'; +import { BP_CARD_BORDER_RADIUS, BP_TAB_BUTTON_MARGIN, BP_TAB_PANEL_MARGIN, BP_TEXT_MARGIN, CANVAS_MAX_WIDTH } from '../common/css_constants.js'; + + + +/* [Main] */ +export default class CanvasHolder extends React.Component< +CanvasHolderProps, +CanvasHolderState +> { + private readonly canvasReference: React.RefObject = React.createRef(); + + private statefulRenderer: StatefulRenderer | null = null; + + constructor(props: CanvasHolderProps) { + super(props); + + this.state = { + isContextLost: false, + }; + } + + componentDidMount() { + console.debug(`>>> MOUNT #${this.props.componentNumber}`); + + let { current: canvas } = this.canvasReference; + if (canvas === null) return; + + let renderGroups: RenderGroup[] = Core + .getRenderGroupManager() + .getGroupsToRender(); + let lastRenderGroup: RenderGroup = renderGroups.at(-1) as RenderGroup; + + this.statefulRenderer = new StatefulRenderer( + canvas, + lastRenderGroup, + this.props.componentNumber, + + () => this.setState({ isContextLost: true }), + () => this.setState({ isContextLost: false }), + ); + this.statefulRenderer.start(true); + } + + componentWillUnmount() { + console.debug(`>>> UNMOUNT #${this.props.componentNumber}`); + + this.statefulRenderer?.stop(true); + } + + // Only required method of a React Component. Returns a React Element created + // via JSX to instruct React to render a DOM node. Also attaches the + // canvasReference via the ref attribute, for imperatively modifying the + // canvas + render() { + return <> +
+
+ + + + + +
+ +
+ +
+
+
+

+ WebGL Context Lost +

+ +

+ Your GPU is probably busy. Waiting for browser to re-establish connection... +

+
+ ; + } +} diff --git a/src/tabs/Csg/hover_control_hint.tsx b/src/tabs/Csg/hover_control_hint.tsx index d033a44cd..642e56e6a 100644 --- a/src/tabs/Csg/hover_control_hint.tsx +++ b/src/tabs/Csg/hover_control_hint.tsx @@ -1,68 +1,35 @@ /* [Imports] */ import { Icon } from '@blueprintjs/core'; +import { Tooltip2 } from '@blueprintjs/popover2'; import React from 'react'; -import { - BP_BORDER_RADIUS, - BP_ICON_COLOR, - BP_TOOLTIP_BACKGROUND_COLOR, - BP_TOOLTIP_PADDING, - BP_TOOLTIP_TEXT_COLOR, - SA_TAB_BUTTON_WIDTH, - SA_TAB_ICON_SIZE, -} from '../../bundles/csg/constants.js'; -import type { HintProps, HintState } from './types'; +import { BP_ICON_COLOR, SA_TAB_BUTTON_WIDTH, SA_TAB_ICON_SIZE } from '../common/css_constants'; +import type { HintProps } from './types'; -/* [Main] */ - -// [CSS Values] -export default class HoverControlHint extends React.Component< -HintProps, -HintState -> { - constructor(props: HintProps) { - super(props); - this.state = { - showTooltip: false, - }; - } +/* [Main] */ +export default class HoverControlHint extends React.Component { render() { - return ( -
this.setState({ showTooltip: true })} - onMouseLeave={() => this.setState({ showTooltip: false })} + height: SA_TAB_BUTTON_WIDTH, + }} + > + - - {this.props.tooltipText} - -
- ); + + ; } } diff --git a/src/tabs/Csg/types.ts b/src/tabs/Csg/types.ts index c05ac245f..a49ac1136 100644 --- a/src/tabs/Csg/types.ts +++ b/src/tabs/Csg/types.ts @@ -12,7 +12,7 @@ export type CanvasHolderProps = { // React Component State for the CSG canvas holder export type CanvasHolderState = { - contextLost: boolean; + isContextLost: boolean; }; // React Component Props for a control hint @@ -20,8 +20,3 @@ export type HintProps = { tooltipText: string; iconName: IconName; }; - -// React Component State for a control hint -export type HintState = { - showTooltip: boolean; -}; diff --git a/src/tabs/Curve/3Dcurve_anim_canvas.tsx b/src/tabs/Curve/animation_canvas_3d_curve.tsx similarity index 57% rename from src/tabs/Curve/3Dcurve_anim_canvas.tsx rename to src/tabs/Curve/animation_canvas_3d_curve.tsx index dee72228e..862187010 100644 --- a/src/tabs/Curve/3Dcurve_anim_canvas.tsx +++ b/src/tabs/Curve/animation_canvas_3d_curve.tsx @@ -1,354 +1,334 @@ -import { Button, Icon, Slider, Switch } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import { Tooltip2 } from '@blueprintjs/popover2'; -import React from 'react'; -import { type AnimatedCurve } from '../../bundles/curve/types'; -import WebGLCanvas from '../common/webgl_canvas'; - -type Props = { - animation: AnimatedCurve; -}; - -type State = { - /** Timestamp of the animation */ - animTimestamp: number; - - /** Boolean value indicating if the animation is playing */ - isPlaying: boolean; - - /** Previous value of `isPlaying` */ - wasPlaying: boolean; - - /** Boolean value indicating if auto play is selected */ - autoPlay: boolean; - - /** Curve Angle */ - curveAngle: number; -}; - -export default class Curve3DAnimationCanvas extends React.Component< -Props, -State -> { - private canvas: HTMLCanvasElement | null; - - /** - * The duration of one frame in milliseconds - */ - private readonly frameDuration: number; - - /** - * The duration of the entire animation - */ - private readonly animationDuration: number; - - /** - * Last timestamp since the previous `requestAnimationFrame` call - */ - private callbackTimestamp: number | null; - - constructor(props: Props | Readonly) { - super(props); - - this.state = { - animTimestamp: 0, - isPlaying: false, - wasPlaying: false, - autoPlay: true, - curveAngle: 0, - }; - - this.canvas = null; - this.frameDuration = 1000 / props.animation.fps; - this.animationDuration = Math.round(props.animation.duration * 1000); - this.callbackTimestamp = null; - } - - public componentDidMount() { - this.drawFrame(); - } - - /** - * Call this to actually draw a frame onto the canvas - */ - private drawFrame = () => { - if (this.canvas) { - const frame = this.props.animation.getFrame( - this.state.animTimestamp / 1000, - ); - frame.draw(this.canvas); - } - }; - - private reqFrame = () => requestAnimationFrame(this.animationCallback); - - /** - * Callback to use with `requestAnimationFrame` - */ - private animationCallback = (timeInMs: number) => { - if (!this.canvas || !this.state.isPlaying) return; - - if (!this.callbackTimestamp) { - this.callbackTimestamp = timeInMs; - this.drawFrame(); - this.reqFrame(); - return; - } - - const currentFrame = timeInMs - this.callbackTimestamp; - - if (currentFrame < this.frameDuration) { - // Not time to draw a new frame yet - this.reqFrame(); - return; - } - - this.callbackTimestamp = timeInMs; - if (this.state.animTimestamp >= this.animationDuration) { - // Animation has ended - if (this.state.autoPlay) { - // If autoplay is active, reset the animation - this.setState( - { - animTimestamp: 0, - }, - this.reqFrame, - ); - } else { - // Otherwise, stop the animation - this.setState( - { - isPlaying: false, - }, - () => { - this.callbackTimestamp = null; - }, - ); - } - } else { - // Animation hasn't ended, so just draw the next frame - this.drawFrame(); - this.setState( - (prev) => ({ - animTimestamp: prev.animTimestamp + currentFrame, - }), - this.reqFrame, - ); - } - }; - - /** - * Play button click handler - */ - private onPlayButtonClick = () => { - if (this.state.isPlaying) { - this.setState( - { - isPlaying: false, - }, - () => { - this.callbackTimestamp = null; - }, - ); - } else { - this.setState( - { - isPlaying: true, - }, - this.reqFrame, - ); - } - }; - - /** - * Reset button click handler - */ - private onResetButtonClick = () => { - this.setState( - { - animTimestamp: 0, - }, - () => { - if (this.state.isPlaying) this.reqFrame(); - else this.drawFrame(); - }, - ); - }; - - /** - * Slider value change handler - * @param newValue New value of the slider - */ - private onTimeSliderChange = (newValue: number) => { - this.callbackTimestamp = null; - this.setState( - (prev) => ({ - wasPlaying: prev.isPlaying, - isPlaying: false, - animTimestamp: newValue, - }), - this.drawFrame, - ); - }; - - /** - * Handler triggered when the slider is clicked off - */ - private onTimeSliderRelease = () => { - this.setState( - (prev) => ({ - isPlaying: prev.wasPlaying, - }), - () => { - if (!this.state.isPlaying) { - this.callbackTimestamp = null; - } else { - this.reqFrame(); - } - }, - ); - }; - - private onAngleSliderChange = (newAngle: number) => { - this.setState( - { - curveAngle: newAngle, - }, - () => { - this.props.animation.angle = newAngle; - if (this.state.isPlaying) this.reqFrame(); - else this.drawFrame(); - }, - ); - }; - - /** - * Auto play switch handler - */ - private autoPlaySwitchChanged = () => { - this.setState((prev) => ({ - autoPlay: !prev.autoPlay, - })); - }; - - public render() { - const buttons = ( -
-
- - - -
- - - -
- ); - - const sliders = ( -
- - -
- -
-
-
- ); - - return ( -
-
- {buttons} - {sliders} - -
-
- { - this.canvas = r; - }} - /> -
-
- ); - } -} +import { Button, Icon, Slider } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Tooltip2 } from '@blueprintjs/popover2'; +import React from 'react'; +import { type AnimatedCurve } from '../../bundles/curve/types'; +import AutoLoopSwitch from '../common/auto_loop_switch'; +import { BP_TAB_BUTTON_MARGIN, BP_TEXT_MARGIN, CANVAS_MAX_WIDTH } from '../common/css_constants'; +import PlayButton from '../common/play_button'; +import WebGLCanvas from '../common/web_gl_canvas'; + +type Props = { + animation: AnimatedCurve; +}; + +type State = { + /** Timestamp of the animation */ + animTimestamp: number; + + /** Boolean value indicating if the animation is playing */ + isPlaying: boolean; + + /** Previous value of `isPlaying` */ + wasPlaying: boolean; + + /** Whether auto loop is enabled */ + isAutoLooping: boolean; + + /** Curve angle in radians */ + displayAngle: number; +}; + +/** + * Canvas to animate 3D Curves. A combination of Canvas3dCurve and + * AnimationCanvas. + * + * Uses WebGLCanvas internally. + */ +export default class AnimationCanvas3dCurve extends React.Component< +Props, +State +> { + private canvas: HTMLCanvasElement | null; + + /** + * The duration of one frame in milliseconds + */ + private readonly frameDuration: number; + + /** + * The duration of the entire animation + */ + private readonly animationDuration: number; + + /** + * Last timestamp since the previous `requestAnimationFrame` call + */ + private callbackTimestamp: number | null; + + constructor(props: Props | Readonly) { + super(props); + + this.state = { + animTimestamp: 0, + isPlaying: false, + wasPlaying: false, + isAutoLooping: true, + displayAngle: 0, + }; + + this.canvas = null; + this.frameDuration = 1000 / props.animation.fps; + this.animationDuration = Math.round(props.animation.duration * 1000); + this.callbackTimestamp = null; + } + + /** + * Call this to actually draw a frame onto the canvas + */ + private drawFrame = () => { + if (this.canvas) { + const frame = this.props.animation.getFrame( + this.state.animTimestamp / 1000, + ); + frame.draw(this.canvas); + } + }; + + private reqFrame = () => requestAnimationFrame(this.animationCallback); + + /** + * Callback to use with `requestAnimationFrame` + */ + private animationCallback = (timeInMs: number) => { + if (!this.canvas || !this.state.isPlaying) return; + + if (!this.callbackTimestamp) { + this.callbackTimestamp = timeInMs; + this.drawFrame(); + this.reqFrame(); + return; + } + + const currentFrame = timeInMs - this.callbackTimestamp; + + if (currentFrame < this.frameDuration) { + // Not time to draw a new frame yet + this.reqFrame(); + return; + } + + this.callbackTimestamp = timeInMs; + if (this.state.animTimestamp >= this.animationDuration) { + // Animation has ended + if (this.state.isAutoLooping) { + // If auto loop is active, restart the animation + this.setState( + { + animTimestamp: 0, + }, + this.reqFrame, + ); + } else { + // Otherwise, stop the animation + this.setState( + { + isPlaying: false, + }, + () => { + this.callbackTimestamp = null; + }, + ); + } + } else { + // Animation hasn't ended, so just draw the next frame + this.drawFrame(); + this.setState( + (prev) => ({ + animTimestamp: prev.animTimestamp + currentFrame, + }), + this.reqFrame, + ); + } + }; + + /** + * Play button click handler + */ + private onPlayButtonClick = () => { + if (this.state.isPlaying) { + this.setState( + { isPlaying: false }, + () => { + this.callbackTimestamp = null; + }, + ); + } else { + this.setState( + { isPlaying: true }, + this.reqFrame, + ); + } + }; + + /** + * Reset button click handler + */ + private onResetButtonClick = () => { + this.setState( + { animTimestamp: 0 }, + () => { + if (this.state.isPlaying) { + // Force stop + this.onPlayButtonClick(); + } + + this.drawFrame(); + }, + ); + }; + + /** + * Slider value change handler + * @param newValue New value of the slider + */ + private onTimeSliderChange = (newValue: number) => { + this.callbackTimestamp = null; + this.setState( + (prev) => ({ + wasPlaying: prev.isPlaying, + isPlaying: false, + animTimestamp: newValue, + }), + this.drawFrame, + ); + }; + + /** + * Handler triggered when the slider is clicked off + */ + private onTimeSliderRelease = () => { + this.setState( + (prev) => ({ + isPlaying: prev.wasPlaying, + }), + () => { + if (!this.state.isPlaying) { + this.callbackTimestamp = null; + } else { + this.reqFrame(); + } + }, + ); + }; + + private onAngleSliderChange = (newAngle: number) => { + this.setState( + { + displayAngle: newAngle, + }, + () => { + this.props.animation.angle = newAngle; + if (this.state.isPlaying) this.reqFrame(); + else this.drawFrame(); + }, + ); + }; + + /** + * Auto loop switch onChange callback + */ + private onSwitchChange = () => { + this.setState((prev) => ({ + isAutoLooping: !prev.isAutoLooping, + })); + }; + + public componentDidMount() { + this.drawFrame(); + } + + public render() { + return
+
+
+ + + + +
+ + + + +
+ +
+
+
+ { + this.canvas = r; + }} + /> +
+
; + } +} diff --git a/src/tabs/Curve/curve_canvas3d.tsx b/src/tabs/Curve/canvas_3d_curve.tsx similarity index 50% rename from src/tabs/Curve/curve_canvas3d.tsx rename to src/tabs/Curve/canvas_3d_curve.tsx index cae10ae36..5ea542589 100644 --- a/src/tabs/Curve/curve_canvas3d.tsx +++ b/src/tabs/Curve/canvas_3d_curve.tsx @@ -1,183 +1,193 @@ -import { Slider, Button, Icon } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import React from 'react'; -import type { CurveDrawn } from '../../bundles/curve/curves_webgl'; -import WebGLCanvas from '../common/webgl_canvas'; - -type State = { - /** - * Slider component reflects this value. This value is also passed in as - * argument to render curves. - */ - rotationAngle: number; - - /** - * Set to true by default. Slider updates this value to false when interacted - * with. Recursive `autoRotate()` checks for this value to decide whether to - * stop recursion. Button checks for this value to decide whether clicking the - * button takes effect, for countering spam-clicking. - */ - isRotating: boolean; - - displayAngle: boolean; -}; - -type Props = { - curve: CurveDrawn; -}; - -/** - * 3D Version of the CurveCanvas to include the rotation angle slider - * and play button - */ -export default class CurveCanvas3D extends React.Component { - private $canvas: HTMLCanvasElement | null; - - constructor(props) { - super(props); - - this.$canvas = null; - this.state = { - rotationAngle: 0, - isRotating: false, - displayAngle: false, - }; - } - - public componentDidMount() { - if (this.$canvas) { - this.props.curve.init(this.$canvas); - this.props.curve.redraw((this.state.rotationAngle / 180) * Math.PI); - } - } - - /** - * Event handler for slider component. Updates the canvas for any change in - * rotation. - * - * @param newValue new rotation angle - */ - private onSliderChangeHandler = (newValue: number) => { - this.setState( - { - rotationAngle: newValue, - isRotating: false, - displayAngle: true, - }, - () => { - if (this.$canvas) { - this.props.curve.redraw((newValue / 180) * Math.PI); - } - }, - ); - }; - - /** - * Event handler for play button. Starts automated rotation by calling - * `autoRotate()`. - */ - private onClickHandler = () => { - if (!this.$canvas) return; - - this.setState( - (prevState) => ({ - isRotating: !prevState.isRotating, - }), - () => { - if (this.state.isRotating) { - this.autoRotate(); - } - }, - ); - }; - - /** - * Environment where `requestAnimationFrame` is called. - */ - private autoRotate = () => { - if (this.$canvas && this.state.isRotating) { - this.setState( - (prevState) => ({ - ...prevState, - rotationAngle: - prevState.rotationAngle >= 360 ? 0 : prevState.rotationAngle + 2, - }), - () => { - this.props.curve.redraw((this.state.rotationAngle / 180) * Math.PI); - window.requestAnimationFrame(this.autoRotate); - }, - ); - } - }; - - private onTextBoxChange = (event) => { - const angle = parseFloat(event.target.value); - this.setState( - () => ({ rotationAngle: angle }), - () => { - if (this.$canvas) { - this.props.curve.redraw((angle / 180) * Math.PI); - } - }, - ); - }; - - public render() { - return ( -
- { - this.$canvas = r; - }} - /> -
- - - -
-
- ); - } -} +import { Slider } from '@blueprintjs/core'; +import React from 'react'; +import type { CurveDrawn } from '../../bundles/curve/curves_webgl'; +import { BP_TAB_BUTTON_MARGIN, BP_TEXT_MARGIN, CANVAS_MAX_WIDTH } from '../common/css_constants'; +import PlayButton from '../common/play_button'; +import WebGLCanvas from '../common/web_gl_canvas'; +import { degreesToRadians } from '../../common/utilities'; + +type State = { + /** + * Slider component reflects this value. This value is also passed in as + * argument to render curves. + */ + displayAngle: number; + + /** + * Set to true by default. Slider updates this value to false when interacted + * with. Recursive `autoRotate()` checks for this value to decide whether to + * stop recursion. Button checks for this value to decide whether clicking the + * button takes effect, for countering spam-clicking. + */ + isRotating: boolean; +}; + +type Props = { + curve: CurveDrawn; +}; + +/** + * Canvas to display 3D Curves. + * + * Uses WebGLCanvas internally. + */ +export default class Canvas3dCurve extends React.Component { + private canvas: HTMLCanvasElement | null; + + constructor(props) { + super(props); + + this.canvas = null; + this.state = { + displayAngle: 0, + isRotating: false, + }; + } + + /** + * Event handler for slider component. Updates the canvas for any change in + * rotation. + * + * @param newValue new rotation angle + */ + private onSliderChangeHandler = (newValue: number) => { + this.setState( + { + displayAngle: newValue, + isRotating: false, + }, + () => { + if (this.canvas) { + this.props.curve.redraw(degreesToRadians(newValue)); + } + }, + ); + }; + + /** + * Event handler for play button. Starts automated rotation by calling + * `autoRotate()`. + */ + private onClickHandler = () => { + if (!this.canvas) return; + + this.setState( + (prevState) => ({ + isRotating: !prevState.isRotating, + }), + () => { + if (this.state.isRotating) { + this.autoRotate(); + } + }, + ); + }; + + /** + * Environment where `requestAnimationFrame` is called. + */ + private autoRotate = () => { + if (this.canvas && this.state.isRotating) { + this.setState( + (prevState) => ({ + ...prevState, + displayAngle: + prevState.displayAngle >= 360 ? 0 : prevState.displayAngle + 2, + }), + () => { + this.props.curve.redraw(degreesToRadians(this.state.displayAngle)); + window.requestAnimationFrame(this.autoRotate); + }, + ); + } + }; + + private onTextBoxChange = (event) => { + const angle = parseFloat(event.target.value); + this.setState( + () => ({ displayAngle: angle }), + () => { + if (this.canvas) { + this.props.curve.redraw(degreesToRadians(angle)); + } + }, + ); + }; + + public componentDidMount() { + if (this.canvas) { + this.props.curve.init(this.canvas); + this.props.curve.redraw(degreesToRadians(this.state.displayAngle)); + } + } + + public render() { + return
+
+
+ + + +
+
+
+ { + this.canvas = r; + }} + /> +
+
; + } +} diff --git a/src/tabs/Curve/index.tsx b/src/tabs/Curve/index.tsx index c5ff94d2f..37f413696 100644 --- a/src/tabs/Curve/index.tsx +++ b/src/tabs/Curve/index.tsx @@ -4,10 +4,10 @@ import type { AnimatedCurve } from '../../bundles/curve/types'; import { glAnimation } from '../../typings/anim_types'; import MultiItemDisplay from '../common/multi_item_display'; import type { DebuggerContext } from '../../typings/type_helpers'; -import Curve3DAnimationCanvas from './3Dcurve_anim_canvas'; -import CurveCanvas3D from './curve_canvas3d'; +import AnimationCanvas3dCurve from './animation_canvas_3d_curve'; +import Canvas3dCurve from './canvas_3d_curve'; import AnimationCanvas from '../common/animation_canvas'; -import WebGLCanvas from '../common/webgl_canvas'; +import WebGLCanvas from '../common/web_gl_canvas'; export default { toSpawn(context: DebuggerContext) { @@ -24,7 +24,7 @@ export default { const anim = curve as AnimatedCurve; return anim.is3D ? ( - + ) : ( @@ -33,7 +33,7 @@ export default { const curveDrawn = curve as CurveDrawn; return curveDrawn.is3D() ? ( - + ) : ( = this.animationDuration) { // Animation has ended - if (this.state.autoPlay) { - // If autoplay is active, reset the animation + if (this.state.isAutoLooping) { + // If auto loop is active, restart the animation this.setState( { animTimestamp: 0, @@ -154,11 +156,8 @@ AnimCanvasState * Play button click handler */ private onPlayButtonClick = () => { - if (this.state.isPlaying) { - this.stopAnimation(); - } else { - this.startAnimation(); - } + if (this.state.isPlaying) this.stopAnimation(); + else this.startAnimation(); }; /** @@ -166,11 +165,14 @@ AnimCanvasState */ private onResetButtonClick = () => { this.setState( - { - animTimestamp: 0, - }, + { animTimestamp: 0 }, () => { - if (!this.state.isPlaying) this.drawFrame(); + if (this.state.isPlaying) { + // Force stop + this.onPlayButtonClick(); + } + + this.drawFrame(); }, ); }; @@ -210,108 +212,80 @@ AnimCanvasState }; /** - * Auto play switch handler + * Auto loop switch onChange callback */ - private autoPlaySwitchChanged = () => { + private onSwitchChange = () => { this.setState((prev) => ({ - autoPlay: !prev.autoPlay, + isAutoLooping: !prev.isAutoLooping, })); }; public render() { - const buttons = ( + return
- - + +
- - -
- ); - - const animSlider = (
- { + this.canvas = r; + }} />
- ); - - return ( - <> -
- { - this.canvas = r; - }} - /> -
-
- {buttons} - {animSlider} - -
- - ); +
; } } diff --git a/src/tabs/common/auto_loop_switch.tsx b/src/tabs/common/auto_loop_switch.tsx new file mode 100644 index 000000000..0fc897271 --- /dev/null +++ b/src/tabs/common/auto_loop_switch.tsx @@ -0,0 +1,26 @@ +/* [Imports] */ +import { Switch } from '@blueprintjs/core'; +import React from 'react'; +import { type AutoLoopSwitchProps } from './types'; + + + +/* [Main] */ +export default class AutoLoopSwitch extends React.Component { + render() { + return ; + } +} diff --git a/src/tabs/common/css_constants.ts b/src/tabs/common/css_constants.ts new file mode 100644 index 000000000..f2b86bf6d --- /dev/null +++ b/src/tabs/common/css_constants.ts @@ -0,0 +1,25 @@ +/* [Imports] */ +import { IconSize } from '@blueprintjs/core'; + + + +/* [Exports] */ +// Values extracted from the styling of the Source Academy frontend +export const SA_TAB_BUTTON_WIDTH: string = '40px'; +export const SA_TAB_ICON_SIZE: number = IconSize.LARGE; + +// Values extracted from BlueprintJS V4 +export const BP_TAB_BUTTON_MARGIN: string = '20px'; +export const BP_TAB_PANEL_MARGIN: string = '20px'; +export const BP_CARD_BORDER_RADIUS: string = '2px'; +export const BP_TEXT_MARGIN: string = '10px'; + +export const BP_TEXT_COLOR: string = '#FFFFFF'; +export const BP_ICON_COLOR: string = '#A7B6C2'; + +// Values extracted from the Ace editor +export const ACE_GUTTER_TEXT_COLOR: string = '#8091A0'; +export const ACE_GUTTER_BACKGROUND_COLOR: string = '#34495E'; + +// Commonly reused values +export const CANVAS_MAX_WIDTH: string = 'max(70vh, 30vw)'; diff --git a/src/tabs/common/multi_item_display.tsx b/src/tabs/common/multi_item_display.tsx index d445313e3..f7bfdec78 100644 --- a/src/tabs/common/multi_item_display.tsx +++ b/src/tabs/common/multi_item_display.tsx @@ -24,7 +24,6 @@ const MultiItemDisplay = (props: { elements: JSX.Element[] }) => { alignItems: 'center', flexDirection: 'row', position: 'relative', - marginBottom: 10, }} > + ; + } +} diff --git a/src/tabs/common/types.ts b/src/tabs/common/types.ts new file mode 100644 index 000000000..2c1b2a7cd --- /dev/null +++ b/src/tabs/common/types.ts @@ -0,0 +1,10 @@ +/* [Exports] */ +export type PlayButtonProps = { + isPlaying: boolean, + onClickCallback: () => void, +}; + +export type AutoLoopSwitchProps = { + isAutoLooping: boolean, + onChangeCallback: () => void, +}; diff --git a/src/tabs/common/webgl_canvas.tsx b/src/tabs/common/web_gl_canvas.tsx similarity index 71% rename from src/tabs/common/webgl_canvas.tsx rename to src/tabs/common/web_gl_canvas.tsx index b2d4177c1..b271b5e3f 100644 --- a/src/tabs/common/webgl_canvas.tsx +++ b/src/tabs/common/web_gl_canvas.tsx @@ -1,30 +1,30 @@ -import React from 'react'; - -const defaultStyle = { - width: '100%', - maxWidth: 'max(70vh, 30vw)', - aspectRatio: '1', -} as React.CSSProperties; - -/** - * Canvas type used for the Curves and Runes modules. - * Standardizes the appearances of canvases - * for the tabs of both modules - */ -const WebGLCanvas = React.forwardRef( - (props: any, ref) => { - const style - = props.style !== undefined - ? { - ...defaultStyle, - ...props.style, - } - : defaultStyle; - - return ( - - ); - }, -); - -export default WebGLCanvas; +import React from 'react'; +import { CANVAS_MAX_WIDTH } from './css_constants'; + +const defaultStyle = { + width: '100%', + maxWidth: CANVAS_MAX_WIDTH, + aspectRatio: '1', +} as React.CSSProperties; + +/** + * Canvas component used by the curve and rune modules. Standardizes the + * appearances of canvases for both modules. + */ +const WebGLCanvas = React.forwardRef( + (props: any, ref) => { + const style + = props.style !== undefined + ? { + ...defaultStyle, + ...props.style, + } + : defaultStyle; + + return ( + + ); + }, +); + +export default WebGLCanvas; diff --git a/src/tabs/physics_2d/DebugDrawCanvas.tsx b/src/tabs/physics_2d/DebugDrawCanvas.tsx index bd05dc3a5..ea9ebb968 100644 --- a/src/tabs/physics_2d/DebugDrawCanvas.tsx +++ b/src/tabs/physics_2d/DebugDrawCanvas.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { DebugDraw } from '@box2d/debug-draw'; import { DrawShapes, type b2World } from '@box2d/core'; -import WebGLCanvas from '../common/webgl_canvas'; +import WebGLCanvas from '../common/web_gl_canvas'; import { type PhysicsWorld } from '../../bundles/physics_2d/PhysicsWorld'; type DebugDrawCanvasProps = { diff --git a/yarn.lock b/yarn.lock index f502cd8fb..f1cbe5183 100644 --- a/yarn.lock +++ b/yarn.lock @@ -786,10 +786,10 @@ resolved "https://registry.yarnpkg.com/@jscad/modeling/-/modeling-2.11.1.tgz#a700417b7b768690a5bd21163642c79f597910ca" integrity sha512-DPrbLcLHDJ1nbpB5FiMwy/DXCdJGkpDyDZQTYHwzK3Fq91x4iPOAABAKkX1QgJ7zlhitMNWo+j7+RmjMzqbYCw== -"@jscad/modeling@^2.9.5": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@jscad/modeling/-/modeling-2.11.0.tgz#890e8e3bda0fb89e8905e815eea8302225a12b89" - integrity sha512-B2GnufqIP6vLwQs9ZWBJRWir0dE9O5EV0Vtz2w9370S6i/6+IQA3Xqhghr8xGdEblKJoJXeE5GOOMUHEsqzoDA== +"@jscad/modeling@2.9.6": + version "2.9.6" + resolved "https://registry.yarnpkg.com/@jscad/modeling/-/modeling-2.9.6.tgz#a107e0de932dcdf7777c1dc639a68a9a6e78b9e9" + integrity sha512-w0BvB2UNYnEEzHvV1z09k/fs3c0Bkn9UJKJ40/5aaOl5nQLOVeB6WGUFpX5P8EYysuqEq1SIyGgXDaVpMp9p+A== "@jscad/regl-renderer@^2.6.1": version "2.6.5" @@ -1012,11 +1012,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@^4.14.191": - version "4.14.191" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" - integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== - "@types/node@*": version "18.14.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.0.tgz#94c47b9217bbac49d4a67a967fdcdeed89ebb7d0"