From b55aef835e14a8fddf02329097a1297711ea2252 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 00:09:29 +0800 Subject: [PATCH 01/43] feat(src): Extract & update CSG's CSS constants --- src/bundles/csg/constants.ts | 20 -------------------- src/tabs/common/css_constants.ts | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 20 deletions(-) create mode 100644 src/tabs/common/css_constants.ts diff --git a/src/bundles/csg/constants.ts b/src/bundles/csg/constants.ts index 1d66072be..804aa9116 100644 --- a/src/bundles/csg/constants.ts +++ b/src/bundles/csg/constants.ts @@ -1,6 +1,3 @@ -/* [Imports] */ -import { IconSize } from '@blueprintjs/core'; - /* [Exports] */ //NOTE Silver is in here to avoid circular dependencies, instead of in @@ -8,23 +5,6 @@ import { IconSize } from '@blueprintjs/core'; 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 grid constants export const MAIN_TICKS: number = 1; export const SUB_TICKS: number = MAIN_TICKS / 4; diff --git a/src/tabs/common/css_constants.ts b/src/tabs/common/css_constants.ts new file mode 100644 index 000000000..9b447a248 --- /dev/null +++ b/src/tabs/common/css_constants.ts @@ -0,0 +1,22 @@ +/* [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'; From 3b5767189ba87f526eebb557b5fb0703da0d76fc Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 00:10:39 +0800 Subject: [PATCH 02/43] chore(csg): Remove V1 comments referencing fork issues --- src/bundles/csg/stateful_renderer.ts | 1 - src/tabs/Csg/canvas_holder.tsx | 345 +++++++++++++-------------- 2 files changed, 172 insertions(+), 174 deletions(-) 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/tabs/Csg/canvas_holder.tsx b/src/tabs/Csg/canvas_holder.tsx index 0c275a0fd..b98394a3d 100644 --- a/src/tabs/Csg/canvas_holder.tsx +++ b/src/tabs/Csg/canvas_holder.tsx @@ -1,173 +1,172 @@ -/* [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 { + 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(); + 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... +

+
+ + ); + } +} From ac9c122256a444d13756f743c334a926df43c696 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 00:38:05 +0800 Subject: [PATCH 03/43] feat(csg): Migrate to BlueprintJS Tooltips --- src/tabs/Csg/canvas_holder.tsx | 23 +++++------ src/tabs/Csg/hover_control_hint.tsx | 59 +++++++---------------------- src/tabs/Csg/types.ts | 7 +--- 3 files changed, 24 insertions(+), 65 deletions(-) diff --git a/src/tabs/Csg/canvas_holder.tsx b/src/tabs/Csg/canvas_holder.tsx index b98394a3d..405c307f8 100644 --- a/src/tabs/Csg/canvas_holder.tsx +++ b/src/tabs/Csg/canvas_holder.tsx @@ -2,17 +2,12 @@ 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'; +import { BP_CARD_BORDER_RADIUS, BP_TAB_BUTTON_MARGIN, BP_TAB_PANEL_MARGIN, BP_TEXT_MARGIN } from '../common/css_constants.js'; @@ -29,7 +24,7 @@ CanvasHolderState super(props); this.state = { - contextLost: false, + isContextLost: false, }; } @@ -49,8 +44,8 @@ CanvasHolderState lastRenderGroup, this.props.componentNumber, - () => this.setState({ contextLost: true }), - () => this.setState({ contextLost: false }), + () => this.setState({ isContextLost: true }), + () => this.setState({ isContextLost: false }), ); this.statefulRenderer.start(true); } @@ -70,7 +65,7 @@ CanvasHolderState <>

WebGL Context Lost @@ -160,7 +155,7 @@ CanvasHolderState

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..89ed63e46 100644 --- a/src/tabs/Csg/hover_control_hint.tsx +++ b/src/tabs/Csg/hover_control_hint.tsx @@ -1,32 +1,14 @@ /* [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 })} > - - - {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; -}; From a597fc7a5830b0d188fce4868580b257ee1a6c94 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 05:22:36 +0800 Subject: [PATCH 04/43] devfeat(csg): Update samples --- src/bundles/csg/samples.md | 408 -------------------------- src/bundles/csg/samples/christmas.js | 29 ++ src/bundles/csg/samples/colours.js | 42 +++ src/bundles/csg/samples/imports.js | 48 +++ src/bundles/csg/samples/operations.js | 54 ++++ src/bundles/csg/samples/ship.js | 265 +++++++++++++++++ src/bundles/csg/samples/steinmetz.js | 30 ++ 7 files changed, 468 insertions(+), 408 deletions(-) delete mode 100644 src/bundles/csg/samples.md create mode 100644 src/bundles/csg/samples/christmas.js create mode 100644 src/bundles/csg/samples/colours.js create mode 100644 src/bundles/csg/samples/imports.js create mode 100644 src/bundles/csg/samples/operations.js create mode 100644 src/bundles/csg/samples/ship.js create mode 100644 src/bundles/csg/samples/steinmetz.js 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/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..7e6f49a0d --- /dev/null +++ b/src/bundles/csg/samples/colours.js @@ -0,0 +1,42 @@ +// 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 +]; + +function translate_x(entity, factor) { + return translate(entity, factor, 0, 0); +} + +function translate_y(entity, factor) { + return translate(entity, 0, factor, 0); +} + +let l = build_list( + x => translate_y( + translate_x( + sphere(colours[x]), + x % 4 * 2 + ), + math_floor(x / 4) * 2 + ), + 16 +); + +render_grid(group(l)); diff --git a/src/bundles/csg/samples/imports.js b/src/bundles/csg/samples/imports.js new file mode 100644 index 000000000..4c01d28ab --- /dev/null +++ b/src/bundles/csg/samples/imports.js @@ -0,0 +1,48 @@ + +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'; diff --git a/src/bundles/csg/samples/operations.js b/src/bundles/csg/samples/operations.js new file mode 100644 index 000000000..275d4c91e --- /dev/null +++ b/src/bundles/csg/samples/operations.js @@ -0,0 +1,54 @@ +// Source §3 +// Classic boolean operations demo + +function scale_all(entity, factor) { + return scale(entity, factor, factor, factor); +} + +function rotate_x(entity, factor) { + return rotate(entity, factor, 0, 0); +} + +function rotate_y(entity, factor) { + return rotate(entity, 0, factor, 0); +} + +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); +} + +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/ship.js b/src/bundles/csg/samples/ship.js new file mode 100644 index 000000000..af7897343 --- /dev/null +++ b/src/bundles/csg/samples/ship.js @@ -0,0 +1,265 @@ +// Source §3 +// Me: Can I have Source Academy ship +// Mum: No, we have Source Academy ship at home +// Source Academy ship at home: + +/* [Convenience Functions] */ +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); +} + +/* [Utility Functions] */ +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); +} + +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/steinmetz.js b/src/bundles/csg/samples/steinmetz.js new file mode 100644 index 000000000..0319defb7 --- /dev/null +++ b/src/bundles/csg/samples/steinmetz.js @@ -0,0 +1,30 @@ +// Source §3 +// Cylinder intersects - Steinmetz solid + +function scale_all(entity, factor) { + return scale(entity, factor, factor, factor); +} + +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); +} + +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)); From 8559080b8819cd2ba8451942f8090eb0a0a49ed4 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 05:22:49 +0800 Subject: [PATCH 05/43] devfeat(curve): Add samples --- src/bundles/curve/samples/canvases.js | 9 +++++++ src/bundles/curve/samples/imports.js | 39 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/bundles/curve/samples/canvases.js create mode 100644 src/bundles/curve/samples/imports.js diff --git a/src/bundles/curve/samples/canvases.js b/src/bundles/curve/samples/canvases.js new file mode 100644 index 000000000..4e99fd357 --- /dev/null +++ b/src/bundles/curve/samples/canvases.js @@ -0,0 +1,9 @@ +// 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'; From e8f73d9c1745b84924250b229564d49ce56194c7 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 06:10:46 +0800 Subject: [PATCH 06/43] docs(curve): Update animation function docs --- src/bundles/curve/functions.ts | 56 +++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 18 deletions(-) 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, From 89da19ed1e87d5f0e89ac90999f1d86a110c0f23 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 06:43:47 +0800 Subject: [PATCH 07/43] ref(csg): Remove default colour The addition of operation colour preservation and removal of the store/render pattern means that all Shapes now require an explicit colour. Changing the default colour of the wrapped renderer no longer has a visible effect. --- src/bundles/csg/constants.ts | 5 ----- src/bundles/csg/jscad/renderer.ts | 9 ++------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/bundles/csg/constants.ts b/src/bundles/csg/constants.ts index 804aa9116..11163ca2c 100644 --- a/src/bundles/csg/constants.ts +++ b/src/bundles/csg/constants.ts @@ -1,10 +1,5 @@ /* [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; - // Renderer grid constants export const MAIN_TICKS: number = 1; export const SUB_TICKS: number = MAIN_TICKS / 4; diff --git a/src/bundles/csg/jscad/renderer.ts b/src/bundles/csg/jscad/renderer.ts index 2e7c3d011..5e32577fd 100644 --- a/src/bundles/csg/jscad/renderer.ts +++ b/src/bundles/csg/jscad/renderer.ts @@ -8,10 +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, ROTATION_SPEED, @@ -39,14 +35,13 @@ 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; function solidsToGeometryEntities(solids: Solid[]): GeometryEntity[] { - let options: EntitiesFromSolidsOptions = { - color: hexToAlphaColor(DEFAULT_COLOR), - }; + let options: EntitiesFromSolidsOptions = {}; return (entitiesFromSolids( options, ...solids, From 1c8f2b44f72928ecbec0facfb75af16ae74436c8 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 09:15:59 +0800 Subject: [PATCH 08/43] fix(build): Fix `build modules` building no tabs Revise/clarify the logic of retrieveBundlesAndTabs() and update tests/comment. --- scripts/bin/build/buildUtils.js | 84 ++++++++++-------- scripts/bin/build/modules/index.js | 2 +- .../src/build/__tests__/buildUtils.test.ts | 4 +- scripts/src/build/buildUtils.ts | 88 +++++++++++-------- scripts/src/build/modules/index.ts | 2 +- 5 files changed, 100 insertions(+), 80 deletions(-) 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/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/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), ]); From 41b4ec8dd72648eb155da12a310e50e983aa78fc Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 12:35:40 +0800 Subject: [PATCH 09/43] feat(): Enhance & make curve canvases consistent --- src/common/utilities.ts | 4 + src/tabs/Csg/canvas_holder.tsx | 180 +++-- src/tabs/Csg/hover_control_hint.tsx | 40 +- ...nvas.tsx => animation_canvas_3d_curve.tsx} | 688 +++++++++--------- ...curve_canvas3d.tsx => canvas_3d_curve.tsx} | 376 +++++----- src/tabs/Curve/index.tsx | 10 +- src/tabs/Rune/hollusion_canvas.tsx | 2 +- src/tabs/Rune/index.tsx | 2 +- src/tabs/common/animation_canvas.tsx | 170 ++--- src/tabs/common/auto_loop_switch.tsx | 26 + src/tabs/common/css_constants.ts | 3 + src/tabs/common/play_button.tsx | 24 + src/tabs/common/types.ts | 10 + .../{webgl_canvas.tsx => web_gl_canvas.tsx} | 60 +- src/tabs/physics_2d/DebugDrawCanvas.tsx | 2 +- 15 files changed, 812 insertions(+), 785 deletions(-) create mode 100644 src/common/utilities.ts rename src/tabs/Curve/{3Dcurve_anim_canvas.tsx => animation_canvas_3d_curve.tsx} (57%) rename src/tabs/Curve/{curve_canvas3d.tsx => canvas_3d_curve.tsx} (50%) create mode 100644 src/tabs/common/auto_loop_switch.tsx create mode 100644 src/tabs/common/play_button.tsx create mode 100644 src/tabs/common/types.ts rename src/tabs/common/{webgl_canvas.tsx => web_gl_canvas.tsx} (71%) 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 405c307f8..f0d8f9256 100644 --- a/src/tabs/Csg/canvas_holder.tsx +++ b/src/tabs/Csg/canvas_holder.tsx @@ -7,7 +7,7 @@ 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 } from '../common/css_constants.js'; +import { BP_CARD_BORDER_RADIUS, BP_TAB_BUTTON_MARGIN, BP_TAB_PANEL_MARGIN, BP_TEXT_MARGIN, CANVAS_MAX_WIDTH } from '../common/css_constants.js'; @@ -61,107 +61,105 @@ CanvasHolderState // canvasReference via the ref attribute, for imperatively modifying the // canvas render() { - return ( - <> + return <> +
-
- - - - - -
- -
- -
+ + + + +
+
-

- WebGL Context Lost -

- -

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

+ // These get set on the fly by the dynamic resizer in + // StatefulRenderer's InputTracker + width="0" + height="0" + />
- - ); +
+
+

+ 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 89ed63e46..642e56e6a 100644 --- a/src/tabs/Csg/hover_control_hint.tsx +++ b/src/tabs/Csg/hover_control_hint.tsx @@ -10,28 +10,26 @@ import type { HintProps } from './types'; /* [Main] */ export default class HoverControlHint extends React.Component { render() { - return ( -
+ - - - -
- ); + + +

; } } 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 index 9b447a248..f2b86bf6d 100644 --- a/src/tabs/common/css_constants.ts +++ b/src/tabs/common/css_constants.ts @@ -20,3 +20,6 @@ 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/play_button.tsx b/src/tabs/common/play_button.tsx new file mode 100644 index 000000000..73bd130f2 --- /dev/null +++ b/src/tabs/common/play_button.tsx @@ -0,0 +1,24 @@ +/* [Imports] */ +import { Button, Icon } from '@blueprintjs/core'; +import { Tooltip2 } from '@blueprintjs/popover2'; +import React from 'react'; +import { type PlayButtonProps } from './types'; +import { IconNames } from '@blueprintjs/icons'; + + + +/* [Main] */ +export default class PlayButton extends React.Component { + render() { + return + + ; + } +} 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 = { From 96cce0073642f2d2b8ab6f622dcc114eb08fbdf1 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 12:42:55 +0800 Subject: [PATCH 10/43] feat(csg): Replace hint dots with colons --- src/tabs/Csg/canvas_holder.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tabs/Csg/canvas_holder.tsx b/src/tabs/Csg/canvas_holder.tsx index f0d8f9256..6ede5a2b4 100644 --- a/src/tabs/Csg/canvas_holder.tsx +++ b/src/tabs/Csg/canvas_holder.tsx @@ -79,23 +79,23 @@ CanvasHolderState }} > From d951c8a34d95124e377f91b997966153f1b335c5 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 12:43:17 +0800 Subject: [PATCH 11/43] ref(csg): Use real js-slang lists --- src/bundles/csg/types.ts | 5 ----- src/bundles/csg/utilities.ts | 7 +++++-- 2 files changed, 5 insertions(+), 7 deletions(-) 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..771228ad4 100644 --- a/src/bundles/csg/utilities.ts +++ b/src/bundles/csg/utilities.ts @@ -14,10 +14,13 @@ import type { ModuleContext } from 'js-slang'; import type { ModuleContexts, 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'; +import { + type List, +} from 'js-slang/dist/stdlib/list'; -/* [Exports] */ + +/* [Exports] */ export interface Entity { clone: () => Entity; store: (newTransforms?: Mat4) => void; From 7509b9d5b8e74140cf0e6d4c026427f8da22acc2 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:39:35 +0800 Subject: [PATCH 12/43] devfeat(csg): Update colours sample --- src/bundles/csg/samples/colours.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bundles/csg/samples/colours.js b/src/bundles/csg/samples/colours.js index 7e6f49a0d..6d87e96a6 100644 --- a/src/bundles/csg/samples/colours.js +++ b/src/bundles/csg/samples/colours.js @@ -29,12 +29,12 @@ function translate_y(entity, factor) { } let l = build_list( - x => translate_y( + i => translate_y( translate_x( - sphere(colours[x]), - x % 4 * 2 + sphere(colours[i]), + ((i % 4) - 2) * 2 ), - math_floor(x / 4) * 2 + (math_floor(i / 4) - 2) * 2 ), 16 ); From 3ec40f5d593a87840d5c51c085e825317dd3fa1a Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:41:15 +0800 Subject: [PATCH 13/43] devfeat(csg): Add primitives sample --- src/bundles/csg/samples/primitives.js | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/bundles/csg/samples/primitives.js diff --git a/src/bundles/csg/samples/primitives.js b/src/bundles/csg/samples/primitives.js new file mode 100644 index 000000000..55f875c8d --- /dev/null +++ b/src/bundles/csg/samples/primitives.js @@ -0,0 +1,37 @@ +// 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, +]; + +function translate_x(entity, factor) { + return translate(entity, factor, 0, 0); +} + +function translate_y(entity, factor) { + return translate(entity, 0, factor, 0); +} + +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)); From 7453ce48d1b918fbcba47d53c632c716055a16af Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 02:30:38 +0800 Subject: [PATCH 14/43] chore(pkg): Lock modeling version for patch file --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 7801c19a4..50a695a36 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,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", @@ -115,4 +115,4 @@ "scripts/src/jest.config.js" ] } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index f502cd8fb..a7f54b412 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" From 5ecb319c66827e987c0dda53d1a8b78778c9fdde Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 02:33:23 +0800 Subject: [PATCH 15/43] ref(build): Reorder config to match Left over from tests with explicit tsconfig path when troubleshooting build errors --- scripts/bin/build/dev.js | 5 +++-- scripts/src/build/dev.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/bin/build/dev.js b/scripts/bin/build/dev.js index 490b317da..72f82ba6c 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,10 @@ const getBundleContext = ({ srcDir, outDir }, bundles, app) => esbuild({ }); const getTabContext = ({ srcDir, outDir }, tabs) => esbuild({ ...tabOptions, + tsconfig: `${srcDir}/tsconfig.json`, + 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/dev.ts b/scripts/src/build/dev.ts index 8824a33c1..840d4bb03 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,10 @@ const getBundleContext = ({ srcDir, outDir }: ContextOptions, bundles: string[], const getTabContext = ({ srcDir, outDir }: ContextOptions, tabs: string[]) => esbuild({ ...tabOptions, + tsconfig: `${srcDir}/tsconfig.json`, + entryPoints: tabs.map(tabNameExpander(srcDir)), outbase: outDir, outdir: outDir, - entryPoints: tabs.map(tabNameExpander(srcDir)), external: ['react*', 'react-dom'], plugins: [{ name: 'Tab Compiler', From beb901e0fa375598ffcfdc51522b715140dd1ad0 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 02:35:35 +0800 Subject: [PATCH 16/43] ref(csg): Rename to Operable, no List, move/del funcs --- src/bundles/csg/utilities.ts | 75 ++++++++++++------------------------ 1 file changed, 24 insertions(+), 51 deletions(-) diff --git a/src/bundles/csg/utilities.ts b/src/bundles/csg/utilities.ts index 771228ad4..6cc637b87 100644 --- a/src/bundles/csg/utilities.ts +++ b/src/bundles/csg/utilities.ts @@ -1,4 +1,5 @@ /* [Imports] */ +import * as _ from 'lodash'; import { clone as _clone, transform as _transform, @@ -9,33 +10,33 @@ import { 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 'js-slang/dist/stdlib/list'; /* [Exports] */ -export interface Entity { - clone: () => Entity; +export interface Operable { + clone: () => Operable; store: (newTransforms?: Mat4) => void; - translate: (offset: [number, number, number]) => Entity; - rotate: (offset: [number, number, number]) => Entity; - scale: (offset: [number, number, number]) => Entity; + translate: (offset: [number, number, number]) => Operable; + rotate: (offset: [number, number, number]) => Operable; + scale: (offset: [number, number, number]) => Operable; } -export class Group implements ReplResult, Entity { - children: Entity[]; +export class Group implements Operable, ReplResult { + private children: Operable[]; + constructor( - public childrenList: List, + _children: Operable[], public transforms: Mat4 = mat4.create(), ) { - this.children = listToArray(childrenList); + // Duplicate the array to avoid modifying the original, maintaining + // stateless Operables for the user + this.children = _.cloneDeep(_children); } toReplString(): string { @@ -43,7 +44,7 @@ export class Group implements ReplResult, Entity { } clone(): Group { - return new Group(arrayToList(this.children.map((child) => child.clone()))); + return new Group(this.children); } store(newTransforms?: Mat4): void { @@ -60,7 +61,7 @@ export class Group implements ReplResult, Entity { translate(offset: [number, number, number]): Group { return new Group( - this.childrenList, + this.children, mat4.multiply( mat4.create(), mat4.fromTranslation(mat4.create(), offset), @@ -75,7 +76,7 @@ export class Group implements ReplResult, Entity { const roll = offset[0]; return new Group( - this.childrenList, + this.children, mat4.multiply( mat4.create(), mat4.fromTaitBryanRotation(mat4.create(), yaw, pitch, roll), @@ -86,7 +87,7 @@ export class Group implements ReplResult, Entity { scale(offset: [number, number, number]): Group { return new Group( - this.childrenList, + this.children, mat4.multiply( mat4.create(), mat4.fromScaling(mat4.create(), offset), @@ -96,7 +97,7 @@ export class Group implements ReplResult, Entity { } } -export class Shape implements ReplResult, Entity { +export class Shape implements Operable, ReplResult { constructor(public solid: Solid) {} toReplString(): string { @@ -203,11 +204,10 @@ export class CsgModuleState { } } -export function getModuleContext( - moduleContexts: ModuleContexts, -): ModuleContext | null { - let potentialModuleContext: ModuleContext | undefined = moduleContexts.csg; - return potentialModuleContext ?? null; +export function alignOrigin(shape: Shape) { + // Align minimum bounds of Shape to 0 0 0 + let newSolid: Solid = align({ modes: ['min', 'min', 'min'] }, shape.solid); + return new Shape(newSolid); } export function hexToColor(hex: string): Color { @@ -241,30 +241,3 @@ export function clamp(value: number, lowest: number, highest: number): number { 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; -} From 9a9644d21e6388e50dd153ed1ad312dbd2d7e236 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 02:35:50 +0800 Subject: [PATCH 17/43] fix(csg): Fix primitive sample trailing comma --- src/bundles/csg/samples/primitives.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundles/csg/samples/primitives.js b/src/bundles/csg/samples/primitives.js index 55f875c8d..2064b80fe 100644 --- a/src/bundles/csg/samples/primitives.js +++ b/src/bundles/csg/samples/primitives.js @@ -12,7 +12,7 @@ let primitives = [ cone, prism, star, - torus, + torus ]; function translate_x(entity, factor) { From 63a5f353cb34e1fec8c0a026838747742718a1cb Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 02:38:50 +0800 Subject: [PATCH 18/43] devfeat(csg): Add Prof's Sierpinski fractal --- src/bundles/csg/samples/sierpinski.js | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/bundles/csg/samples/sierpinski.js diff --git a/src/bundles/csg/samples/sierpinski.js b/src/bundles/csg/samples/sierpinski.js new file mode 100644 index 000000000..518043378 --- /dev/null +++ b/src/bundles/csg/samples/sierpinski.js @@ -0,0 +1,29 @@ +// Source §1 +// Prof Martin's Sierpinski triangle fractal + +import { union, translate, scale, +group, render, pyramid, sphere, cube } +from 'csg'; + +const r = 0.75; // vertical stretch factor +const shape = scale(pyramid('#edd4c8'), 1, 1, r); + +function repeat(n, trans, s) { + return n === 0 + ? s + : repeat(n - 1, trans, trans(s)); +} + +function sierpinski(o) { + const t1 = translate(o, 0. , 0.5, -r); + const t2 = translate(o, 0.5, 0 , -r); + const t3 = translate(o, -0.5, 0 , -r); + const t4 = translate(o, 0. ,-0.5, -r); + const s = union(o, + union(union(t1, t2), + union(t3, t4))); + const s_scaled = scale(s, 0.5, 0.5, 0.5); + return s_scaled; +} + +render(repeat(5, sierpinski, shape)); From ac51bf88a89766127ce518d09a47f1cba0c2a4f4 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 05:44:44 +0800 Subject: [PATCH 19/43] FEAT(csg): Overhaul functions, documentation --- src/bundles/csg/functions.ts | 959 +++++++++++++++++++++-------------- src/bundles/csg/index.ts | 80 +-- src/bundles/csg/utilities.ts | 53 +- 3 files changed, 653 insertions(+), 439 deletions(-) diff --git a/src/bundles/csg/functions.ts b/src/bundles/csg/functions.ts index d8bb3e1b4..32cc2a582 100644 --- a/src/bundles/csg/functions.ts +++ b/src/bundles/csg/functions.ts @@ -1,16 +1,63 @@ /** - * The module `csg` provides functions for drawing Constructive Solid Geometry (CSG) called `Shape`. - * - * A *Shape* is defined by its polygons and vertices. + * 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 + * // Example + * 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); + * ``` * * @module csg - * @author Liu Muchen * @author Joel Leow + * @author Liu Muchen + * @author Ng Yin Joe + * @author Yu Chenbo */ + + /* [Imports] */ import { primitives } from '@jscad/modeling'; -import { colorize as _colorize } from '@jscad/modeling/src/colors'; +import { colorize as colorSolid } from '@jscad/modeling/src/colors'; import { measureBoundingBox, type BoundingBox, @@ -21,525 +68,686 @@ import { 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 { + head, + list, + tail, + type List, +} from 'js-slang/dist/stdlib/list'; 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 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; +} -/** - * 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); +export function arrayToList(array: Operable[]): List { + return list(...array); } + + /* [Exports] */ -// [Variables - Primitive shapes] +// [Variables - Colors] + /** - * Primitive Shape of a cube. + * A hex color code for black (#000000). * - * @category Primitive + * @category Color */ -const primitiveCube: Shape = shapeSetOrigin( - new Shape(primitives.cube({ size: 1 })), -); +export const black: string = '#000000'; /** - * Returns a Shape of a cube of a set colour or the default colour when - * colour information is omitted. + * A hex color code for dark blue (#0000AA). * - * @param {string} hex A hex colour code + * @category Color */ -export function cube(hex: string): Shape { - const shape: Shape = primitiveCube; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} +export const navy: string = '#0000AA'; /** - * Primitive Shape of a sphere. + * A hex color code for green (#00AA00). * - * @category Primitive + * @category Color */ -const primitiveSphere: Shape = shapeSetOrigin( - new Shape(primitives.sphere({ radius: 0.5 })), -); +export const green: string = '#00AA00'; /** - * Returns a Shape of a sphere of a set colour. + * A hex color code for dark cyan (#00AAAA). * - * @param {string} hex A hex colour code + * @category Color */ -export function sphere(hex: string): Shape { - const shape: Shape = primitiveSphere; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} +export const teal: string = '#00AAAA'; /** - * Primitive Shape of a cylinder. + * A hex color code for dark red (#AA0000). * - * @category Primitive + * @category Color */ -const primitiveCylinder: Shape = shapeSetOrigin( - new Shape( - primitives.cylinder({ - radius: 0.5, - height: 1, - }), - ), -); +export const crimson: string = '#AA0000'; /** - * Returns a Shape of a cylinder of a set colour. + * A hex color code for purple (#AA00AA). * - * @param {string} hex A hex colour code + * @category Color */ -export function cylinder(hex: string): Shape { - const shape: Shape = primitiveCylinder; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} +export const purple: string = '#AA00AA'; /** - * Primitive Shape of a prism. + * A hex color code for orange (#FFAA00). * - * @category Primitive + * @category Color */ -const primitivePrism: Shape = shapeSetOrigin( - new Shape(extrudeLinear({ height: 1 }, primitives.triangle())), -); +export const orange: string = '#FFAA00'; /** - * Returns a Shape of a prism of a set colour. + * A hex color code for light gray (#AAAAAA). * - * @param {string} hex A hex colour code + * @category Color */ -export function prism(hex: string): Shape { - const shape: Shape = primitivePrism; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} +export const silver: string = '#AAAAAA'; /** - * Primitive Shape of an extruded star. + * A hex color code for dark gray (#555555). * - * @category Primitive + * @category Color */ -const primitiveStar: Shape = shapeSetOrigin( - new Shape(extrudeLinear({ height: 1 }, primitives.star({ outerRadius: 0.5 }))), -); +export const gray: string = '#555555'; /** - * Returns a Shape of an extruded star of a set colour. + * A hex color code for blue (#5555FF). * - * @param {string} hex A hex colour code + * @category Color */ -export function star(hex: string): Shape { - const shape: Shape = primitiveStar; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} +export const blue: string = '#5555FF'; /** - * Primitive Shape of a square pyramid. + * A hex color code for light green (#55FF55). * - * @category Primitive + * @category Color */ -const primitivePyramid: Shape = shapeSetOrigin( - new Shape( - primitives.cylinderElliptic({ - height: 1, - startRadius: [0.5, 0.5], - endRadius: [Number.MIN_VALUE, Number.MIN_VALUE], - segments: 4, - }), - ), -); +export const lime: string = '#55FF55'; /** - * Returns a Shape of a square pyramid of a set colour. + * A hex color code for cyan (#55FFFF). * - * @param {string} hex A hex colour code + * @category Color */ -export function pyramid(hex: string): Shape { - const shape: Shape = primitivePyramid; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} +export const cyan: string = '#55FFFF'; /** - * Primitive Shape of a cone. + * A hex color code for light red (#FF5555). * - * @category Primitive + * @category Color */ -const primitiveCone: Shape = shapeSetOrigin( - new Shape( - primitives.cylinderElliptic({ - height: 1, - startRadius: [0.5, 0.5], - endRadius: [Number.MIN_VALUE, Number.MIN_VALUE], - }), - ), -); +export const rose: string = '#FF5555'; /** - * Returns a Shape of a cone of a set colour. + * A hex color code for pink (#FF55FF). * - * @param {string} hex A hex colour code + * @category Color */ -export function cone(hex: string): Shape { - const shape: Shape = primitiveCone; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} +export const pink: string = '#FF55FF'; /** - * Primitive Shape of a torus. + * A hex color code for yellow (#FFFF55). * - * @category Primitive + * @category Color */ -const primitiveTorus: Shape = shapeSetOrigin( - new Shape( - primitives.torus({ - innerRadius: 0.125, - outerRadius: 0.375, - }), - ), -); +export const yellow: string = '#FFFF55'; /** - * Returns a Shape of a torus of a set colour. + * A hex color code for white (#FFFFFF). * - * @param {string} hex A hex colour code + * @category Color */ -export function torus(hex: string): Shape { - const shape: Shape = primitiveTorus; - return new Shape(_colorize(hexToColor(hex), shape.solid)); -} +export const white: string = '#FFFFFF'; + +// [Functions - Primitives] /** - * Primitive Shape of a rounded cube. + * Returns a cube Shape in the specified color. + * + * Side length: 1 + * Center: (0.5, 0.5, 0.5) + * + * @param hex hex color code * * @category Primitive */ -const primitiveRoundedCube: Shape = shapeSetOrigin( - new Shape(primitives.roundedCuboid({ size: [1, 1, 1] })), -); +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 Shape of a rounded cube of a set colour. + * Returns a rounded cube Shape in the specified color. + * + * Side length: 1 + * Center: (0.5, 0.5, 0.5) * - * @param {string} hex A hex colour code + * @param hex hex color code + * + * @category Primitive */ export function rounded_cube(hex: string): Shape { - const shape: Shape = primitiveRoundedCube; - return new Shape(_colorize(hexToColor(hex), shape.solid)); + let solid: Solid = primitives.roundedCuboid({ size: [1, 1, 1] }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); } /** - * Primitive Shape of a rounded cylinder. + * 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 Primitive */ -const primitiveRoundedCylinder: Shape = shapeSetOrigin( - new Shape( - primitives.roundedCylinder({ - height: 1, - radius: 0.5, - }), - ), -); +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 Shape of a rounded cylinder of a set colour. + * Returns a rounded, upright cylinder Shape in the specified color. + * + * Height: 1 + * Radius: 0.5 + * Center: (0.5, 0.5, 0.5) * - * @param {string} hex A hex colour code + * @param hex hex color code + * + * @category Primitive */ export function rounded_cylinder(hex: string): Shape { - const shape: Shape = primitiveRoundedCylinder; - return new Shape(_colorize(hexToColor(hex), shape.solid)); + let solid: Solid = primitives.roundedCylinder({ + height: 1, + radius: 0.5, + }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); } /** - * Primitive Shape of a geodesic sphere. + * Returns a sphere Shape in the specified color. + * + * Radius: 0.5 + * Center: (0.5, 0.5, 0.5) + * + * @param hex hex color code * * @category Primitive */ -const primitiveGeodesicSphere: Shape = shapeSetOrigin( - new Shape(primitives.geodesicSphere({ radius: 0.5 })), -); +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 Shape of a geodesic sphere of a set colour. + * Returns a geodesic sphere Shape in the specified color. * - * @param {string} hex A hex colour code + * Radius: 0.5 + * Center: Floating at (0.5, 0.5, 0.5) + * + * @param hex hex color code + * + * @category Primitive */ export function geodesic_sphere(hex: string): Shape { - const shape: Shape = primitiveGeodesicSphere; - return new Shape(_colorize(hexToColor(hex), shape.solid)); + let solid: Solid = primitives.geodesicSphere({ radius: 0.5 }); + let shape: Shape = new Shape( + colorSolid( + hexToColor(hex), + solid, + ), + ); + return centerPrimitive(shape); } -// [Variables - Colours] /** - * A hex colour code for black (#000000). + * Returns a square pyramid Shape in the specified color. * - * @category Colour - */ -export const black: string = '#000000'; - -/** - * A hex colour code for dark blue (#0000AA). + * Height: 1 + * Side length: 1 + * Center: (0.5, 0.5, 0.5) * - * @category Colour - */ -export const navy: string = '#0000AA'; - -/** - * A hex colour code for green (#00AA00). + * @param hex hex color code * - * @category Colour + * @category Primitive */ -export const green: string = '#00AA00'; +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); +} /** - * A hex colour code for dark cyan (#00AAAA). + * Returns a cone Shape in the specified color. * - * @category Colour - */ -export const teal: string = '#00AAAA'; - -/** - * A hex colour code for dark red (#AA0000). + * Height: 1 + * Radius: 0.5 + * Center: (0.5, 0.5, 0.5) * - * @category Colour - */ -export const crimson: string = '#AA0000'; - -/** - * A hex colour code for purple (#AA00AA). + * @param hex hex color code * - * @category Colour + * @category Primitive */ -export const purple: string = '#AA00AA'; +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); +} /** - * A hex colour code for orange (#FFAA00). + * Returns an upright triangular prism Shape in the specified color. * - * @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. + * Height: 1 + * Side length: 1 + * Center: (0.5, 0.5, 0.5) * - * @category Colour - */ -export const silver: string = SILVER; - -/** - * A hex colour code for dark grey (#555555). + * @param hex hex color code * - * @category Colour + * @category Primitive */ -export const gray: string = '#555555'; +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); +} /** - * A hex colour code for blue (#5555FF). + * Returns an upright extruded star Shape in the specified color. * - * @category Colour - */ -export const blue: string = '#5555FF'; - -/** - * A hex colour code for light green (#55FF55). + * Height: 1 + * Center: (0.5, 0.5, 0.5) * - * @category Colour + * @param hex hex color code + * + * @category Primitive */ -export const lime: string = '#55FF55'; +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); +} /** - * A hex colour code for cyan (#55FFFF). + * Returns a torus (donut) Shape in the specified color. + * + * Inner radius: 0.15 (ring is 0.3 thick) + * Total radius: 0.5 (from the hole to "outside") + * Center: Floating at (0.5, 0.5, 0.5) + * + * @param hex hex color code * - * @category Colour + * @category Primitive */ -export const cyan: string = '#55FFFF'; +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] /** - * A hex colour code for light red (#FF5555). + * Returns the union of the two specified Shapes. + * + * @param first first Shape + * @param second second Shape + * @returns unioned Shape * - * @category Colour + * @category Operation */ -export const rose: string = '#FF5555'; +export function union(first: Shape, second: Shape): Shape { + let solid: Solid = _union(first.solid, second.solid); + return new Shape(solid); +} /** - * A hex colour code for pink (#FF55FF). + * 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 Colour + * @category Operation */ -export const pink: string = '#FF55FF'; +export function subtract(target: Shape, subtractedShape: Shape): Shape { + let solid: Solid = _subtract(target.solid, subtractedShape.solid); + return new Shape(solid); +} /** - * A hex colour code for yellow (#FFFF55). + * Returns the intersection of the two specified Shapes. + * + * @param first first Shape + * @param second second Shape + * @returns intersected Shape * - * @category Colour + * @category Operation */ -export const yellow: string = '#FFFF55'; +export function intersect(first: Shape, second: Shape): Shape { + let solid: Solid = _intersect(first.solid, second.solid); + return new Shape(solid); +} + +// [Functions - Transformations] /** - * A hex colour code for white (#FFFFFF). + * 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 Colour + * @category Transformation */ -export const white: string = '#FFFFFF'; - -// [Functions] +export function translate( + operable: Operable, + xOffset: number, + yOffset: number, + zOffset: number, +): Operable { + return operable.translate([xOffset, yOffset, zOffset]); +} /** - * Union of the two provided shapes to produce a new shape. + * 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 * - * @param {Shape} a - The first shape - * @param {Shape} b - The second shape - * @returns {Shape} The resulting unioned shape + * @category Transformation */ -export function union(a: Shape, b: Shape): Shape { - let newSolid: Solid = _union(a.solid, b.solid); - return new Shape(newSolid); +export function rotate( + operable: Operable, + xAngle: number, + yAngle: number, + zAngle: number, +): Operable { + return operable.rotate([xAngle, yAngle, zAngle]); } /** - * Subtraction of the second shape from the first shape to produce a new shape. + * 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 1 results in the original Shape, while a factor of + * -1 results in a reflection of the Shape. A factor of 0.5 results in a smaller + * Shape, while a factor of 2 results in a larger Shape. Factors cannot be 0. + * + * @param operable Shape or Group + * @param xFactor x scaling factor + * @param yFactor y scaling factor + * @param zFactor z scaling factor + * @returns scaled 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 + * @category Transformation */ -export function subtract(a: Shape, b: Shape): Shape { - let newSolid: Solid = _subtract(a.solid, b.solid); - return new Shape(newSolid); +export function scale( + operable: Operable, + xFactor: number, + yFactor: number, + zFactor: number, +): Operable { + if (xFactor === 0 || yFactor === 0 || zFactor === 0) { + throw new Error('Scaling factor cannot be 0'); + } + + return operable.scale([xFactor, yFactor, zFactor]); } +// [Functions - Utilities] + /** - * Intersection of the two shape to produce a new shape. + * 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 {Shape} a - The first shape - * @param {Shape} b - The second shape - * @returns {Shape} The resulting intersection shape + * @param operables list of Shapes and/or Groups + * @returns new Group + * + * @category Utility */ -export function intersect(a: Shape, b: Shape): Shape { - let newSolid: Solid = _intersect(a.solid, b.solid); - return new Shape(newSolid); +export function group(operables: List): Group { + return new Group(listToArray(operables)); } /** - * 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 + * 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 * - * @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 + * @category Utility */ -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'); +export function ungroup(g: Group): List { + if (!is_group(g)) { + throw new Error('Only Groups can be ungrouped'); } - return entity.scale([x, y, z]); + + return arrayToList(g.children); } /** - * Translate / Move the shape by the provided x, y and z units from negative - * infinity to infinity. + * Checks if the given parameter is a Shape. * - * @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 + * @param parameter parameter to check + * @returns whether parameter is a Shape + * + * @category Utility */ -export function translate( - entity: Entity, - x: number, - y: number, - z: number, -): Entity { - return entity.translate([x, y, z]); +export function is_shape(parameter: unknown): boolean { + return parameter instanceof Shape; } /** - * 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. + * Checks if the given parameter is a Group. * - * @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 + * @param parameter parameter to check + * @returns whether parameter is a Group + * + * @category Utility */ -export function rotate( - entity: Entity, - x: number, - y: number, - z: number, -): Entity { - return entity.rotate([x, y, z]); +export function is_group(parameter: unknown): boolean { + return parameter instanceof Group; } /** - * 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. + * 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). * - * For example, - * ```` - * const a = bounding_box(sphere); - * a('x', 'min'); // Returns the minimum x coordinate of the bounding box - * ```` + * ```js + * // Example + * const getter_function = bounding_box(sphere(silver)); + * display(getter_function("y", "max")); // Displays 1, the maximum y coordinate + * ``` * - * @param {Shape} shape - The scale to be measured - * @returns {(String, String) => number} A lambda function providing the - * shape's bounding box coordinates + * @param shape Shape to measure + * @returns bounding box getter function + * + * @category Utility */ - export function bounding_box( shape: Shape, -): (axis: String, min: String) => number { +): (axis: string, minMax: 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.', + + 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}`, ); - } else { - return bounds[j][i]; } + + 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 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 + * Returns a hex color code representing the specified RGB values. + * + * @param redComponent red component of the color + * @param greenComponent green component of the color + * @param blueComponent blue component of the color + * @returns hex color code + * + * @category Utility */ export function rgb( redComponent: number, @@ -554,96 +762,83 @@ export function rgb( || blueComponent < 0 || blueComponent > 255 ) { - throw new Error('invalid argument value: expects [0, 255]'); + throw new Error('RGB components must be between 0 and 255 (inclusive)'); } - 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; + return `#${redComponent.toString(16)}${greenComponent.toString(16)} + ${blueComponent.toString(16)}`; } /** - * Checks if the specified entity is a Group. + * Exports the specified Shape as an STL file, downloaded to your device. * - * @param {unknown} entity - The entity to check - * @returns {boolean} Whether the entity is a Group + * The file can be used for purposes such as 3D printing. + * + * @param shape Shape to export */ -export function is_group(entity: unknown): boolean { - return entity instanceof Group; +export async function download_shape_stl(shape: Shape): Promise { + await save( + new Blob(serialize({ binary: true }, shape.solid)), + 'Source Academy CSG Shape.stl', + ); } -/** - * 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); -} +// [Functions - Rendering] /** - * Renders a Group of Shapes, along with a grid and axes. + * Renders the specified Operable. * - * @param {Group} groupToRender The Group to be rendered + * @param operable Shape or Group to render + * + * @category Render */ -export function render_grid_axes(groupToRender: Group): RenderGroup { - groupToRender.store(); +export function render(operable: Operable): RenderGroup { + 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(true, true); + .nextRenderGroup(); } /** - * Renders a Group of Shapes, along with a grid. + * Renders the specified Operable, along with a grid. + * + * @param operable Shape or Group to render * - * @param {Group} groupToRender The Group to be rendered + * @category Render */ -export function render_grid(groupToRender: Group): RenderGroup { - groupToRender.store(); +export function render_grid(operable: Operable): RenderGroup { + operable.store(); + return Core.getRenderGroupManager() .nextRenderGroup(true); } /** - * Renders a Group of Shapes, along with X, Y and Z axes. + * Renders the specified Operable, along with z, y, and z axes. * - * @param {Group} groupToRender The Group to be rendered + * @param operable Shape or Group to render + * + * @category Render */ -export function render_axes(groupToRender: Group): RenderGroup { - groupToRender.store(); +export function render_axes(operable: Operable): RenderGroup { + operable.store(); + return Core.getRenderGroupManager() .nextRenderGroup(undefined, true); } /** - * Renders a Group of Shapes. + * Renders the specified Operable, along with both a grid and axes. * - * @param {Group} groupToRender The Group to be rendered + * @param operable Shape or Group to render + * + * @category Render */ -export function render(groupToRender: Group): RenderGroup { - groupToRender.store(); - return Core.getRenderGroupManager() - .nextRenderGroup(); -} +export function render_grid_axes(operable: Operable): RenderGroup { + operable.store(); -/** - * 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', - ); + return Core.getRenderGroupManager() + .nextRenderGroup(true, true); } diff --git a/src/bundles/csg/index.ts b/src/bundles/csg/index.ts index bcb051f3d..d1eaf9e80 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 { + // Color 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, + + // Primitive + cube, rounded_cube, + cylinder, rounded_cylinder, - scale, - shape_to_stl, - silver, sphere, + geodesic_sphere, + pyramid, + cone, + prism, star, - subtract, - teal, torus, - translate, + + // Operation union, - white, - yellow, + 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 './functions'; diff --git a/src/bundles/csg/utilities.ts b/src/bundles/csg/utilities.ts index 6cc637b87..9034327fa 100644 --- a/src/bundles/csg/utilities.ts +++ b/src/bundles/csg/utilities.ts @@ -1,17 +1,16 @@ /* [Imports] */ -import * as _ from 'lodash'; 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 _ from 'lodash'; import type { ReplResult } from '../../typings/type_helpers.js'; import { Core } from './core.js'; import type { AlphaColor, Color, Solid } from './jscad/types.js'; @@ -19,16 +18,17 @@ import type { AlphaColor, Color, Solid } from './jscad/types.js'; /* [Exports] */ +//TODO rename params etc export interface Operable { - clone: () => Operable; store: (newTransforms?: Mat4) => void; + translate: (offset: [number, number, number]) => Operable; rotate: (offset: [number, number, number]) => Operable; scale: (offset: [number, number, number]) => Operable; } export class Group implements Operable, ReplResult { - private children: Operable[]; + children: Operable[]; constructor( _children: Operable[], @@ -39,14 +39,6 @@ export class Group implements Operable, ReplResult { this.children = _.cloneDeep(_children); } - toReplString(): string { - return ''; - } - - clone(): Group { - return new Group(this.children); - } - store(newTransforms?: Mat4): void { this.transforms = mat4.multiply( mat4.create(), @@ -95,18 +87,14 @@ export class Group implements Operable, ReplResult { ), ); } -} - -export class Shape implements Operable, ReplResult { - constructor(public solid: Solid) {} toReplString(): string { - return ''; + return ''; } +} - clone(): Shape { - return new Shape(_clone(this.solid as Geom3)); - } +export class Shape implements Operable, ReplResult { + constructor(public solid: Solid) {} store(newTransforms?: Mat4): void { Core.getRenderGroupManager() @@ -126,17 +114,21 @@ export class Shape implements Operable, ReplResult { 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 ``; } @@ -204,10 +196,21 @@ export class CsgModuleState { } } +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 alignOrigin(shape: Shape) { // Align minimum bounds of Shape to 0 0 0 - let newSolid: Solid = align({ modes: ['min', 'min', 'min'] }, shape.solid); - return new Shape(newSolid); + let solid: Solid = align({ modes: ['min', 'min', 'min'] }, shape.solid); + return new Shape(solid); } export function hexToColor(hex: string): Color { From 191f3ae7f08b3f2e7a8b95e32acd9e685b897459 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 05:46:18 +0800 Subject: [PATCH 20/43] devfeat(csg): Update sample import names --- src/bundles/csg/samples/imports.js | 46 ++++++++++++++++++------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/bundles/csg/samples/imports.js b/src/bundles/csg/samples/imports.js index 4c01d28ab..80b0d39d2 100644 --- a/src/bundles/csg/samples/imports.js +++ b/src/bundles/csg/samples/imports.js @@ -1,17 +1,6 @@ import { - cube, - sphere, - cylinder, - prism, - star, - pyramid, - cone, - torus, - rounded_cube, - rounded_cylinder, - geodesic_sphere, - + // Color black, navy, green, @@ -29,20 +18,41 @@ import { yellow, white, + // Primitive + cube, + rounded_cube, + cylinder, + rounded_cylinder, + sphere, + geodesic_sphere, + pyramid, + cone, + prism, + star, + torus, + + // Operation union, subtract, intersect, - scale, + + // Transformation translate, rotate, + scale, + + // Utility group, - rgb, - bounding_box, + ungroup, is_shape, is_group, - render_grid_axes, + bounding_box, + rgb, + download_shape_stl, + + // Render + render, render_grid, render_axes, - render, - shape_to_stl + render_grid_axes } from 'csg'; From 7dfcd362417e65f9953b27ad257fb5fec2f6ec1e Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 05:46:30 +0800 Subject: [PATCH 21/43] ref(csg): Reorder sample functions --- src/bundles/csg/samples/operations.js | 8 +++---- src/bundles/csg/samples/ship.js | 32 +++++++++++++-------------- src/bundles/csg/samples/steinmetz.js | 16 +++++++------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/bundles/csg/samples/operations.js b/src/bundles/csg/samples/operations.js index 275d4c91e..e5e49372e 100644 --- a/src/bundles/csg/samples/operations.js +++ b/src/bundles/csg/samples/operations.js @@ -1,10 +1,6 @@ // Source §3 // Classic boolean operations demo -function scale_all(entity, factor) { - return scale(entity, factor, factor, factor); -} - function rotate_x(entity, factor) { return rotate(entity, factor, 0, 0); } @@ -13,6 +9,10 @@ function rotate_y(entity, factor) { return rotate(entity, 0, factor, 0); } +function scale_all(entity, factor) { + return scale(entity, factor, factor, factor); +} + function _get_shape_middle(shape, axis) { let get = bounding_box(shape); let start = get(axis, 'min'); diff --git a/src/bundles/csg/samples/ship.js b/src/bundles/csg/samples/ship.js index af7897343..b0eeb8046 100644 --- a/src/bundles/csg/samples/ship.js +++ b/src/bundles/csg/samples/ship.js @@ -4,22 +4,6 @@ // Source Academy ship at home: /* [Convenience Functions] */ -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); } @@ -44,6 +28,22 @@ function rotate_z(entity, factor) { return rotate(entity, 0, 0, factor); } +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); +} + /* [Utility Functions] */ function _get_shape_middle(shape, axis) { let get = bounding_box(shape); diff --git a/src/bundles/csg/samples/steinmetz.js b/src/bundles/csg/samples/steinmetz.js index 0319defb7..df98c6f2e 100644 --- a/src/bundles/csg/samples/steinmetz.js +++ b/src/bundles/csg/samples/steinmetz.js @@ -1,8 +1,12 @@ // Source §3 // Cylinder intersects - Steinmetz solid -function scale_all(entity, factor) { - return scale(entity, factor, factor, factor); +function rotate_x(entity, factor) { + return rotate(entity, factor, 0, 0); +} + +function rotate_y(entity, factor) { + return rotate(entity, 0, factor, 0); } function translate_y(entity, factor) { @@ -13,12 +17,8 @@ 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 scale_all(entity, factor) { + return scale(entity, factor, factor, factor); } const A_ = cylinder(blue); From dcb28d560179a87f256b2f1bac296075fd2b652c Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 05:46:44 +0800 Subject: [PATCH 22/43] devfeat(csg): Add rotation sample --- src/bundles/csg/samples/rotation.js | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/bundles/csg/samples/rotation.js diff --git a/src/bundles/csg/samples/rotation.js b/src/bundles/csg/samples/rotation.js new file mode 100644 index 000000000..3f0e38f40 --- /dev/null +++ b/src/bundles/csg/samples/rotation.js @@ -0,0 +1,37 @@ +// Source §3 +// Showcase of all 11 primitive Shapes provided by default + +function translate_x(entity, factor) { + return translate(entity, factor, 0, 0); +} + +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); +} + +function degrees_to_radians(degrees) { + return (degrees / 360) * (2 * math_PI); +} + +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))); From 43323322acdda2c5ce2186807d4eff33701b4e3f Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 05:46:58 +0800 Subject: [PATCH 23/43] devfeat(csg): Add summary snowglobe example as sample --- src/bundles/csg/samples/snowglobe.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/bundles/csg/samples/snowglobe.js 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); From 217756ba11eb38d1ce4453f82b30aed42434a265 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 05:56:39 +0800 Subject: [PATCH 24/43] docs(csg): Plural categories, bulleted list, tweaks --- src/bundles/csg/functions.ts | 148 ++++++++++++++++++----------------- src/bundles/csg/index.ts | 12 +-- 2 files changed, 81 insertions(+), 79 deletions(-) diff --git a/src/bundles/csg/functions.ts b/src/bundles/csg/functions.ts index 32cc2a582..31f4b1e07 100644 --- a/src/bundles/csg/functions.ts +++ b/src/bundles/csg/functions.ts @@ -27,7 +27,7 @@ * (right-handed coordinate system). * * ```js - * // Example + * // Sample usage * import { * silver, crimson, cyan, * cube, cone, sphere, @@ -126,112 +126,112 @@ export function arrayToList(array: Operable[]): List { /** * A hex color code for black (#000000). * - * @category Color + * @category Colors */ export const black: string = '#000000'; /** * A hex color code for dark blue (#0000AA). * - * @category Color + * @category Colors */ export const navy: string = '#0000AA'; /** * A hex color code for green (#00AA00). * - * @category Color + * @category Colors */ export const green: string = '#00AA00'; /** * A hex color code for dark cyan (#00AAAA). * - * @category Color + * @category Colors */ export const teal: string = '#00AAAA'; /** * A hex color code for dark red (#AA0000). * - * @category Color + * @category Colors */ export const crimson: string = '#AA0000'; /** * A hex color code for purple (#AA00AA). * - * @category Color + * @category Colors */ export const purple: string = '#AA00AA'; /** * A hex color code for orange (#FFAA00). * - * @category Color + * @category Colors */ export const orange: string = '#FFAA00'; /** * A hex color code for light gray (#AAAAAA). * - * @category Color + * @category Colors */ export const silver: string = '#AAAAAA'; /** * A hex color code for dark gray (#555555). * - * @category Color + * @category Colors */ export const gray: string = '#555555'; /** * A hex color code for blue (#5555FF). * - * @category Color + * @category Colors */ export const blue: string = '#5555FF'; /** * A hex color code for light green (#55FF55). * - * @category Color + * @category Colors */ export const lime: string = '#55FF55'; /** * A hex color code for cyan (#55FFFF). * - * @category Color + * @category Colors */ export const cyan: string = '#55FFFF'; /** * A hex color code for light red (#FF5555). * - * @category Color + * @category Colors */ export const rose: string = '#FF5555'; /** * A hex color code for pink (#FF55FF). * - * @category Color + * @category Colors */ export const pink: string = '#FF55FF'; /** * A hex color code for yellow (#FFFF55). * - * @category Color + * @category Colors */ export const yellow: string = '#FFFF55'; /** * A hex color code for white (#FFFFFF). * - * @category Color + * @category Colors */ export const white: string = '#FFFFFF'; @@ -240,12 +240,12 @@ export const white: string = '#FFFFFF'; /** * Returns a cube Shape in the specified color. * - * Side length: 1 - * Center: (0.5, 0.5, 0.5) + * - Side length: 1 + * - Center: (0.5, 0.5, 0.5) * * @param hex hex color code * - * @category Primitive + * @category Primitives */ export function cube(hex: string): Shape { let solid: Solid = primitives.cube({ size: 1 }); @@ -261,12 +261,12 @@ export function cube(hex: string): Shape { /** * Returns a rounded cube Shape in the specified color. * - * Side length: 1 - * Center: (0.5, 0.5, 0.5) + * - Side length: 1 + * - Center: (0.5, 0.5, 0.5) * * @param hex hex color code * - * @category Primitive + * @category Primitives */ export function rounded_cube(hex: string): Shape { let solid: Solid = primitives.roundedCuboid({ size: [1, 1, 1] }); @@ -282,13 +282,13 @@ export function rounded_cube(hex: string): Shape { /** * Returns an upright cylinder Shape in the specified color. * - * Height: 1 - * Radius: 0.5 - * Center: (0.5, 0.5, 0.5) + * - Height: 1 + * - Radius: 0.5 + * - Center: (0.5, 0.5, 0.5) * * @param hex hex color code * - * @category Primitive + * @category Primitives */ export function cylinder(hex: string): Shape { let solid: Solid = primitives.cylinder({ @@ -307,13 +307,13 @@ export function cylinder(hex: string): Shape { /** * Returns a rounded, upright cylinder Shape in the specified color. * - * Height: 1 - * Radius: 0.5 - * Center: (0.5, 0.5, 0.5) + * - Height: 1 + * - Radius: 0.5 + * - Center: (0.5, 0.5, 0.5) * * @param hex hex color code * - * @category Primitive + * @category Primitives */ export function rounded_cylinder(hex: string): Shape { let solid: Solid = primitives.roundedCylinder({ @@ -332,12 +332,12 @@ export function rounded_cylinder(hex: string): Shape { /** * Returns a sphere Shape in the specified color. * - * Radius: 0.5 - * Center: (0.5, 0.5, 0.5) + * - Radius: 0.5 + * - Center: (0.5, 0.5, 0.5) * * @param hex hex color code * - * @category Primitive + * @category Primitives */ export function sphere(hex: string): Shape { let solid: Solid = primitives.sphere({ radius: 0.5 }); @@ -353,12 +353,12 @@ export function sphere(hex: string): Shape { /** * Returns a geodesic sphere Shape in the specified color. * - * Radius: 0.5 - * Center: Floating at (0.5, 0.5, 0.5) + * - Radius: 0.5 + * - Center: Floating at (0.5, 0.5, 0.5) * * @param hex hex color code * - * @category Primitive + * @category Primitives */ export function geodesic_sphere(hex: string): Shape { let solid: Solid = primitives.geodesicSphere({ radius: 0.5 }); @@ -374,13 +374,13 @@ export function geodesic_sphere(hex: string): Shape { /** * Returns a square pyramid Shape in the specified color. * - * Height: 1 - * Side length: 1 - * Center: (0.5, 0.5, 0.5) + * - Height: 1 + * - Side length: 1 + * - Center: (0.5, 0.5, 0.5) * * @param hex hex color code * - * @category Primitive + * @category Primitives */ export function pyramid(hex: string): Shape { let pythagorasSide: number = Math.sqrt(2); // sqrt(1^2 + 1^2) @@ -406,13 +406,13 @@ export function pyramid(hex: string): Shape { /** * Returns a cone Shape in the specified color. * - * Height: 1 - * Radius: 0.5 - * Center: (0.5, 0.5, 0.5) + * - Height: 1 + * - Radius: 0.5 + * - Center: (0.5, 0.5, 0.5) * * @param hex hex color code * - * @category Primitive + * @category Primitives */ export function cone(hex: string): Shape { let solid: Solid = primitives.cylinderElliptic({ @@ -432,13 +432,13 @@ export function cone(hex: string): Shape { /** * Returns an upright triangular prism Shape in the specified color. * - * Height: 1 - * Side length: 1 - * Center: (0.5, 0.5, 0.5) + * - Height: 1 + * - Side length: 1 + * - Center: (0.5, 0.5, 0.5) * * @param hex hex color code * - * @category Primitive + * @category Primitives */ export function prism(hex: string): Shape { let solid: Solid = extrudeLinear( @@ -458,12 +458,12 @@ export function prism(hex: string): Shape { /** * Returns an upright extruded star Shape in the specified color. * - * Height: 1 - * Center: (0.5, 0.5, 0.5) + * - Height: 1 + * - Center: (0.5, 0.5, 0.5) * * @param hex hex color code * - * @category Primitive + * @category Primitives */ export function star(hex: string): Shape { let solid: Solid = extrudeLinear( @@ -482,13 +482,13 @@ export function star(hex: string): 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 hole to "outside") - * Center: Floating at (0.5, 0.5, 0.5) + * - 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 Primitive + * @category Primitives */ export function torus(hex: string): Shape { let solid: Solid = primitives.torus({ @@ -513,7 +513,7 @@ export function torus(hex: string): Shape { * @param second second Shape * @returns unioned Shape * - * @category Operation + * @category Operations */ export function union(first: Shape, second: Shape): Shape { let solid: Solid = _union(first.solid, second.solid); @@ -528,7 +528,7 @@ export function union(first: Shape, second: Shape): Shape { * @param subtractedShape Shape to remove from the first Shape * @returns subtracted Shape * - * @category Operation + * @category Operations */ export function subtract(target: Shape, subtractedShape: Shape): Shape { let solid: Solid = _subtract(target.solid, subtractedShape.solid); @@ -542,7 +542,7 @@ export function subtract(target: Shape, subtractedShape: Shape): Shape { * @param second second Shape * @returns intersected Shape * - * @category Operation + * @category Operations */ export function intersect(first: Shape, second: Shape): Shape { let solid: Solid = _intersect(first.solid, second.solid); @@ -561,7 +561,7 @@ export function intersect(first: Shape, second: Shape): Shape { * @param zOffset z offset * @returns translated Shape * - * @category Transformation + * @category Transformations */ export function translate( operable: Operable, @@ -586,7 +586,7 @@ export function translate( * @param zAngle z angle in radians * @returns rotated Shape * - * @category Transformation + * @category Transformations */ export function rotate( operable: Operable, @@ -611,7 +611,7 @@ export function rotate( * @param zFactor z scaling factor * @returns scaled Shape * - * @category Transformation + * @category Transformations */ export function scale( operable: Operable, @@ -639,7 +639,7 @@ export function scale( * @param operables list of Shapes and/or Groups * @returns new Group * - * @category Utility + * @category Utilities */ export function group(operables: List): Group { return new Group(listToArray(operables)); @@ -652,7 +652,7 @@ export function group(operables: List): Group { * @param g Group to ungroup * @returns ungrouped list of Shapes and/or Groups * - * @category Utility + * @category Utilities */ export function ungroup(g: Group): List { if (!is_group(g)) { @@ -668,7 +668,7 @@ export function ungroup(g: Group): List { * @param parameter parameter to check * @returns whether parameter is a Shape * - * @category Utility + * @category Utilities */ export function is_shape(parameter: unknown): boolean { return parameter instanceof Shape; @@ -680,7 +680,7 @@ export function is_shape(parameter: unknown): boolean { * @param parameter parameter to check * @returns whether parameter is a Group * - * @category Utility + * @category Utilities */ export function is_group(parameter: unknown): boolean { return parameter instanceof Group; @@ -700,7 +700,7 @@ export function is_group(parameter: unknown): boolean { * coordinates will be (1, 1, 1). * * ```js - * // Example + * // Sample usage * const getter_function = bounding_box(sphere(silver)); * display(getter_function("y", "max")); // Displays 1, the maximum y coordinate * ``` @@ -708,7 +708,7 @@ export function is_group(parameter: unknown): boolean { * @param shape Shape to measure * @returns bounding box getter function * - * @category Utility + * @category Utilities */ export function bounding_box( shape: Shape, @@ -747,7 +747,7 @@ export function bounding_box( * @param blueComponent blue component of the color * @returns hex color code * - * @category Utility + * @category Utilities */ export function rgb( redComponent: number, @@ -775,6 +775,8 @@ export function rgb( * 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 { await save( @@ -790,7 +792,7 @@ export async function download_shape_stl(shape: Shape): Promise { * * @param operable Shape or Group to render * - * @category Render + * @category Rendering */ export function render(operable: Operable): RenderGroup { operable.store(); @@ -806,7 +808,7 @@ export function render(operable: Operable): RenderGroup { * * @param operable Shape or Group to render * - * @category Render + * @category Rendering */ export function render_grid(operable: Operable): RenderGroup { operable.store(); @@ -820,7 +822,7 @@ export function render_grid(operable: Operable): RenderGroup { * * @param operable Shape or Group to render * - * @category Render + * @category Rendering */ export function render_axes(operable: Operable): RenderGroup { operable.store(); @@ -834,7 +836,7 @@ export function render_axes(operable: Operable): RenderGroup { * * @param operable Shape or Group to render * - * @category Render + * @category Rendering */ export function render_grid_axes(operable: Operable): RenderGroup { operable.store(); diff --git a/src/bundles/csg/index.ts b/src/bundles/csg/index.ts index d1eaf9e80..71c9ca359 100644 --- a/src/bundles/csg/index.ts +++ b/src/bundles/csg/index.ts @@ -16,7 +16,7 @@ Core.initialize(moduleState); /* [Exports] */ export { - // Color + // Colors black, navy, green, @@ -34,7 +34,7 @@ export { yellow, white, - // Primitive + // Primitives cube, rounded_cube, cylinder, @@ -47,17 +47,17 @@ export { star, torus, - // Operation + // Operations union, subtract, intersect, - // Transformation + // Transformations translate, rotate, scale, - // Utility + // Utilities group, ungroup, is_shape, @@ -66,7 +66,7 @@ export { rgb, download_shape_stl, - // Render + // Rendering render, render_grid, render_axes, From 23a6eb45ff83259e6bc3303d1f8667aa9af5913b Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 06:40:45 +0800 Subject: [PATCH 25/43] ref(csg): Refactor operables Rename params, non-deep children copy for new Group --- src/bundles/csg/utilities.ts | 50 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/bundles/csg/utilities.ts b/src/bundles/csg/utilities.ts index 9034327fa..4438dc454 100644 --- a/src/bundles/csg/utilities.ts +++ b/src/bundles/csg/utilities.ts @@ -10,7 +10,6 @@ import { translate as _translate, align, } from '@jscad/modeling/src/operations/transforms'; -import _ from 'lodash'; import type { ReplResult } from '../../typings/type_helpers.js'; import { Core } from './core.js'; import type { AlphaColor, Color, Solid } from './jscad/types.js'; @@ -18,13 +17,12 @@ import type { AlphaColor, Color, Solid } from './jscad/types.js'; /* [Exports] */ -//TODO rename params etc export interface Operable { store: (newTransforms?: Mat4) => void; - translate: (offset: [number, number, number]) => Operable; - rotate: (offset: [number, number, number]) => Operable; - scale: (offset: [number, number, number]) => Operable; + translate: (offsets: [number, number, number]) => Operable; + rotate: (angles: [number, number, number]) => Operable; + scale: (factors: [number, number, number]) => Operable; } export class Group implements Operable, ReplResult { @@ -36,36 +34,38 @@ export class Group implements Operable, ReplResult { ) { // Duplicate the array to avoid modifying the original, maintaining // stateless Operables for the user - this.children = _.cloneDeep(_children); + this.children = [..._children]; } - store(newTransforms?: Mat4): void { + store(newTransforms: Mat4 = mat4.create()): void { + // Update own transforms this.transforms = mat4.multiply( mat4.create(), - newTransforms || mat4.create(), + newTransforms, this.transforms, ); - this.children.forEach((child) => { + // Apply own transforms to all children + this.children.forEach((child: Operable) => { child.store(this.transforms); }); } - translate(offset: [number, number, number]): Group { + translate(offsets: [number, number, number]): Group { return new Group( 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.children, @@ -77,12 +77,12 @@ export class Group implements Operable, ReplResult { ); } - scale(offset: [number, number, number]): Group { + scale(factors: [number, number, number]): Group { return new Group( this.children, mat4.multiply( mat4.create(), - mat4.fromScaling(mat4.create(), offset), + mat4.fromScaling(mat4.create(), factors), this.transforms, ), ); @@ -96,23 +96,23 @@ export class Group implements Operable, ReplResult { export class Shape implements Operable, ReplResult { constructor(public solid: Solid) {} - store(newTransforms?: Mat4): void { + store(newTransforms: Mat4 = mat4.create()): void { Core.getRenderGroupManager() .storeShape( - new Shape(_transform(newTransforms || mat4.create(), this.solid)), + new Shape(_transform(newTransforms, this.solid)), ); } - 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(offset: [number, number, number]): Shape { - return new Shape(_rotate(offset, this.solid)); + rotate(angles: [number, number, number]): Shape { + return new Shape(_rotate(angles, this.solid)); } - scale(offset: [number, number, number]): Shape { - return new Shape(_scale(offset, this.solid)); + scale(factors: [number, number, number]): Shape { + return new Shape(_scale(factors, this.solid)); } toReplString(): string { From be185d35eb9065507a8a7701ae83b5771c1664a0 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 06:41:26 +0800 Subject: [PATCH 26/43] feat(csg): Improve error feedback for pitfalls --- src/bundles/csg/functions.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/bundles/csg/functions.ts b/src/bundles/csg/functions.ts index 31f4b1e07..b584e9caa 100644 --- a/src/bundles/csg/functions.ts +++ b/src/bundles/csg/functions.ts @@ -516,6 +516,10 @@ export function torus(hex: string): 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); } @@ -531,6 +535,10 @@ export function union(first: Shape, second: Shape): 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); } @@ -545,6 +553,10 @@ export function subtract(target: Shape, subtractedShape: Shape): 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); } @@ -655,6 +667,7 @@ export function group(operables: List): Group { * @category Utilities */ export function ungroup(g: Group): List { + //FIXME if (!is_group(g)) { throw new Error('Only Groups can be ungrouped'); } @@ -779,6 +792,10 @@ export function rgb( * @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', From c37f8b07625e59500c7f89a0d4a9b19bdb86a439 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 07:29:40 +0800 Subject: [PATCH 27/43] fix(csg): Implement proper ungroup --- src/bundles/csg/functions.ts | 1725 +++++++++++++++++----------------- src/bundles/csg/utilities.ts | 34 +- 2 files changed, 890 insertions(+), 869 deletions(-) diff --git a/src/bundles/csg/functions.ts b/src/bundles/csg/functions.ts index b584e9caa..61f0b8707 100644 --- a/src/bundles/csg/functions.ts +++ b/src/bundles/csg/functions.ts @@ -1,863 +1,862 @@ -/** - * 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); - * ``` - * - * @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, -} 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 - * - Side 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 1 results in the original Shape, while a factor of - * -1 results in a reflection of the Shape. A factor of 0.5 results in a smaller - * Shape, while a factor of 2 results in a larger Shape. Factors cannot be 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) { - throw new Error('Scaling factor cannot be 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 { - 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 { - //FIXME - if (!is_group(g)) { - throw new Error('Only Groups can be ungrouped'); - } - - return arrayToList(g.children); -} - -/** - * 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 redComponent red component of the color - * @param greenComponent green component of the color - * @param blueComponent blue component of the color - * @returns hex color code - * - * @category Utilities - */ -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('RGB components must be between 0 and 255 (inclusive)'); - } - - return `#${redComponent.toString(16)}${greenComponent.toString(16)} - ${blueComponent.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 { - 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 { - 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 { - 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 { - operable.store(); - - return Core.getRenderGroupManager() - .nextRenderGroup(true, true); -} +/** + * The CSG module enables working with Constructive Solid Geometry in the Source + * Academy. Users are able to program colored 3D models and interact with them + * in a tab. + * + * The main objects in use are called Shapes. Users can create, operate on, + * transform, and finally render these Shapes. + * + * There are also Groups, which contain Shapes, but can also contain other + * nested Groups. Groups allow many Shapes to be transformed in tandem, as + * opposed to having to call transform functions on each Shape individually. + * + * An object that is either a Shape or a Group is called an Operable. Operables + * as a whole are stateless, which means that passing them into functions does + * not modify the original Operable; instead, the newly created Operable is + * returned. Therefore, it is safe to reuse existing Operables after passing + * them into functions, as they remain immutable. + * + * When you are done modeling your Operables, pass them to one of the CSG + * rendering functions to have them displayed in a tab. + * + * When rendering, you may optionally render with a grid and/or axes displayed, + * depending on the rendering function used. The grid appears on the XY-plane + * with white lines every 1 unit of distance, and slightly fainter lines every + * 0.25 units of distance. The axes for x, y, and z are coloured red, green, and + * blue respectively. The positive z direction is upwards from the flat plane + * (right-handed coordinate system). + * + * ```js + * // Sample usage + * import { + * silver, crimson, cyan, + * cube, cone, sphere, + * intersect, union, scale, translate, + * render_grid_axes + * } from "csg"; + * + * const base = intersect( + * scale(cube(silver), 1, 1, 0.3), + * scale(cone(crimson), 1, 1, 3) + * ); + * const snowglobe = union( + * translate(sphere(cyan), 0, 0, 0.22), + * base + * ); + * render_grid_axes(snowglobe); + * ``` + * + * @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, +} 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 + * - Side 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 1 results in the original Shape, while a factor of + * -1 results in a reflection of the Shape. A factor of 0.5 results in a smaller + * Shape, while a factor of 2 results in a larger Shape. Factors cannot be 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) { + throw new Error('Scaling factor cannot be 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 { + 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 redComponent red component of the color + * @param greenComponent green component of the color + * @param blueComponent blue component of the color + * @returns hex color code + * + * @category Utilities + */ +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('RGB components must be between 0 and 255 (inclusive)'); + } + + return `#${redComponent.toString(16)}${greenComponent.toString(16)} + ${blueComponent.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 { + 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 { + 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 { + 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 { + operable.store(); + + return Core.getRenderGroupManager() + .nextRenderGroup(true, true); +} diff --git a/src/bundles/csg/utilities.ts b/src/bundles/csg/utilities.ts index 4438dc454..ce8a196c1 100644 --- a/src/bundles/csg/utilities.ts +++ b/src/bundles/csg/utilities.ts @@ -18,6 +18,7 @@ import type { AlphaColor, Color, Solid } from './jscad/types.js'; /* [Exports] */ export interface Operable { + applyTransforms: (newTransforms: Mat4) => Operable; store: (newTransforms?: Mat4) => void; translate: (offsets: [number, number, number]) => Operable; @@ -37,17 +38,25 @@ export class Group implements Operable, ReplResult { this.children = [..._children]; } - store(newTransforms: Mat4 = mat4.create()): void { - // Update own transforms - this.transforms = mat4.multiply( + applyTransforms(newTransforms: Mat4): Operable { + let appliedTransforms: Mat4 = mat4.multiply( mat4.create(), newTransforms, this.transforms, ); - // Apply own transforms to all children + // 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(this.transforms); + child.store(appliedGroup.transforms); }); } @@ -91,15 +100,28 @@ export class Group implements Operable, ReplResult { toReplString(): string { 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) {} + applyTransforms(newTransforms: Mat4): Operable { + // Return a new object for statelessness + return new Shape(_transform(newTransforms, this.solid)); + } + store(newTransforms: Mat4 = mat4.create()): void { Core.getRenderGroupManager() .storeShape( - new Shape(_transform(newTransforms, this.solid)), + this.applyTransforms(newTransforms) as Shape, ); } From e0be46760ac85c7756864390229463b366390d9f Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 07:30:03 +0800 Subject: [PATCH 28/43] feat(csg): Improve error feedback for pitfalls --- src/bundles/csg/functions.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/bundles/csg/functions.ts b/src/bundles/csg/functions.ts index 61f0b8707..e3a537e66 100644 --- a/src/bundles/csg/functions.ts +++ b/src/bundles/csg/functions.ts @@ -74,6 +74,7 @@ import { list, tail, type List, + is_list, } from 'js-slang/dist/stdlib/list'; import save from 'save-file'; import { Core } from './core.js'; @@ -654,6 +655,10 @@ export function scale( * @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)); } @@ -811,6 +816,10 @@ export async function download_shape_stl(shape: Shape): Promise { * @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. @@ -827,6 +836,10 @@ export function render(operable: Operable): RenderGroup { * @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() @@ -841,6 +854,10 @@ export function render_grid(operable: Operable): RenderGroup { * @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() @@ -855,6 +872,10 @@ export function render_axes(operable: Operable): RenderGroup { * @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() From dbcdbd5f267521c109d2e08a239ded59a6817631 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 08:16:54 +0800 Subject: [PATCH 29/43] feat(csg): Update sample utility funcs --- src/bundles/csg/samples/_imports.js | 136 ++++++++++++++++++++++++++ src/bundles/csg/samples/colours.js | 8 -- src/bundles/csg/samples/imports.js | 58 ----------- src/bundles/csg/samples/operations.js | 37 ------- src/bundles/csg/samples/primitives.js | 8 -- src/bundles/csg/samples/rotation.js | 20 ---- src/bundles/csg/samples/ship.js | 79 --------------- src/bundles/csg/samples/steinmetz.js | 20 ---- 8 files changed, 136 insertions(+), 230 deletions(-) create mode 100644 src/bundles/csg/samples/_imports.js delete mode 100644 src/bundles/csg/samples/imports.js 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/colours.js b/src/bundles/csg/samples/colours.js index 6d87e96a6..17027f12b 100644 --- a/src/bundles/csg/samples/colours.js +++ b/src/bundles/csg/samples/colours.js @@ -20,14 +20,6 @@ let colours = [ white ]; -function translate_x(entity, factor) { - return translate(entity, factor, 0, 0); -} - -function translate_y(entity, factor) { - return translate(entity, 0, factor, 0); -} - let l = build_list( i => translate_y( translate_x( diff --git a/src/bundles/csg/samples/imports.js b/src/bundles/csg/samples/imports.js deleted file mode 100644 index 80b0d39d2..000000000 --- a/src/bundles/csg/samples/imports.js +++ /dev/null @@ -1,58 +0,0 @@ - -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'; diff --git a/src/bundles/csg/samples/operations.js b/src/bundles/csg/samples/operations.js index e5e49372e..f14b46faf 100644 --- a/src/bundles/csg/samples/operations.js +++ b/src/bundles/csg/samples/operations.js @@ -1,43 +1,6 @@ // Source §3 // Classic boolean operations demo -function rotate_x(entity, factor) { - return rotate(entity, factor, 0, 0); -} - -function rotate_y(entity, factor) { - return rotate(entity, 0, factor, 0); -} - -function scale_all(entity, factor) { - return scale(entity, factor, factor, 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); -} - const Cube = centre(scale_all(cube(purple), 0.8)); const Sphere = centre(sphere(navy)); diff --git a/src/bundles/csg/samples/primitives.js b/src/bundles/csg/samples/primitives.js index 2064b80fe..07f813823 100644 --- a/src/bundles/csg/samples/primitives.js +++ b/src/bundles/csg/samples/primitives.js @@ -15,14 +15,6 @@ let primitives = [ torus ]; -function translate_x(entity, factor) { - return translate(entity, factor, 0, 0); -} - -function translate_y(entity, factor) { - return translate(entity, 0, factor, 0); -} - let l = build_list( i => translate_y( translate_x( diff --git a/src/bundles/csg/samples/rotation.js b/src/bundles/csg/samples/rotation.js index 3f0e38f40..93764a233 100644 --- a/src/bundles/csg/samples/rotation.js +++ b/src/bundles/csg/samples/rotation.js @@ -1,26 +1,6 @@ // Source §3 // Showcase of all 11 primitive Shapes provided by default -function translate_x(entity, factor) { - return translate(entity, factor, 0, 0); -} - -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); -} - -function degrees_to_radians(degrees) { - return (degrees / 360) * (2 * math_PI); -} - let a = cube(green); let b = cube(blue); diff --git a/src/bundles/csg/samples/ship.js b/src/bundles/csg/samples/ship.js index b0eeb8046..fe7b38a4b 100644 --- a/src/bundles/csg/samples/ship.js +++ b/src/bundles/csg/samples/ship.js @@ -3,86 +3,7 @@ // Mum: No, we have Source Academy ship at home // Source Academy ship at home: -/* [Convenience Functions] */ -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); -} - -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); -} - /* [Utility Functions] */ -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); -} - function debug(shape) { let get = bounding_box(shape); let xStart = get('x', 'min'); diff --git a/src/bundles/csg/samples/steinmetz.js b/src/bundles/csg/samples/steinmetz.js index df98c6f2e..4e10db8b7 100644 --- a/src/bundles/csg/samples/steinmetz.js +++ b/src/bundles/csg/samples/steinmetz.js @@ -1,26 +1,6 @@ // Source §3 // Cylinder intersects - Steinmetz solid -function rotate_x(entity, factor) { - return rotate(entity, factor, 0, 0); -} - -function rotate_y(entity, factor) { - return rotate(entity, 0, factor, 0); -} - -function translate_y(entity, factor) { - return translate(entity, 0, factor, 0); -} - -function translate_z(entity, factor) { - return translate(entity, 0, 0, factor); -} - -function scale_all(entity, factor) { - return scale(entity, factor, factor, factor); -} - 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); From 9624a2619e5055e6a33490204e319fbb0efd2f86 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 08:17:16 +0800 Subject: [PATCH 30/43] feat(csg): Update sierpinski sample for new primitive --- src/bundles/csg/samples/sierpinski.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/bundles/csg/samples/sierpinski.js b/src/bundles/csg/samples/sierpinski.js index 518043378..358f9d7f0 100644 --- a/src/bundles/csg/samples/sierpinski.js +++ b/src/bundles/csg/samples/sierpinski.js @@ -1,11 +1,10 @@ // Source §1 // Prof Martin's Sierpinski triangle fractal -import { union, translate, scale, -group, render, pyramid, sphere, cube } +import { union, translate, scale, render, pyramid } from 'csg'; -const r = 0.75; // vertical stretch factor +const r = 1; // vertical stretch factor const shape = scale(pyramid('#edd4c8'), 1, 1, r); function repeat(n, trans, s) { @@ -15,10 +14,10 @@ function repeat(n, trans, s) { } function sierpinski(o) { - const t1 = translate(o, 0. , 0.5, -r); - const t2 = translate(o, 0.5, 0 , -r); - const t3 = translate(o, -0.5, 0 , -r); - const t4 = translate(o, 0. ,-0.5, -r); + const t1 = translate(o, 0.5, 0.5, -r); + const t2 = translate(o, -0.5, 0.5, -r); + const t3 = translate(o, 0.5, -0.5, -r); + const t4 = translate(o, -0.5, -0.5, -r); const s = union(o, union(union(t1, t2), union(t3, t4))); From ad2e6946aa0729392e7d123f4b6fa631cf992f31 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:03:48 +0800 Subject: [PATCH 31/43] fix(rune): Fix low resolution heart --- src/bundles/rune/runes_ops.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From f4e7f3bb54a161f61859f8f398c76998711437ef Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:07:19 +0800 Subject: [PATCH 32/43] docs(csg): Link to GitHub samples in summary --- src/bundles/csg/functions.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bundles/csg/functions.ts b/src/bundles/csg/functions.ts index e3a537e66..4b4cc3a12 100644 --- a/src/bundles/csg/functions.ts +++ b/src/bundles/csg/functions.ts @@ -46,6 +46,8 @@ * 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 From c0b7844d2435fee20e66f467bc961c6d0a57296d Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:24:52 +0800 Subject: [PATCH 33/43] feat(csg): Update sierpinski sample --- src/bundles/csg/samples/sierpinski.js | 57 ++++++++++++++++++--------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/src/bundles/csg/samples/sierpinski.js b/src/bundles/csg/samples/sierpinski.js index 358f9d7f0..8da1d5d36 100644 --- a/src/bundles/csg/samples/sierpinski.js +++ b/src/bundles/csg/samples/sierpinski.js @@ -1,28 +1,49 @@ // Source §1 -// Prof Martin's Sierpinski triangle fractal +// Prof Martin's Sierpinski fractals -import { union, translate, scale, render, pyramid } +import { union, translate, scale, render, pyramid, sphere, cube } from 'csg'; -const r = 1; // vertical stretch factor -const shape = scale(pyramid('#edd4c8'), 1, 1, r); - function repeat(n, trans, s) { return n === 0 - ? s - : repeat(n - 1, trans, trans(s)); + ? s + : repeat(n - 1, trans, trans(s)); } -function sierpinski(o) { - const t1 = translate(o, 0.5, 0.5, -r); - const t2 = translate(o, -0.5, 0.5, -r); - const t3 = translate(o, 0.5, -0.5, -r); - const t4 = translate(o, -0.5, -0.5, -r); - const s = union(o, - union(union(t1, t2), - union(t3, t4))); - const s_scaled = scale(s, 0.5, 0.5, 0.5); - return s_scaled; +// 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, shape)); +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'))); +*/ From 3a2a0164108a17c039384e9fa04bddcbd337ecc1 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:25:48 +0800 Subject: [PATCH 34/43] feat(csg): Readd custom default colour due to bug #227 --- src/bundles/csg/jscad/renderer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bundles/csg/jscad/renderer.ts b/src/bundles/csg/jscad/renderer.ts index 5e32577fd..f7270cdb4 100644 --- a/src/bundles/csg/jscad/renderer.ts +++ b/src/bundles/csg/jscad/renderer.ts @@ -41,7 +41,9 @@ import { ACE_GUTTER_BACKGROUND_COLOR, ACE_GUTTER_TEXT_COLOR, BP_TEXT_COLOR } fro let { orbit } = controls; function solidsToGeometryEntities(solids: Solid[]): GeometryEntity[] { - let options: EntitiesFromSolidsOptions = {}; + let options: EntitiesFromSolidsOptions = { + color: hexToAlphaColor('#55ffaa'), + }; return (entitiesFromSolids( options, ...solids, From 9bc7c806de9492aeb7f396255e5ca65ca0ef3a77 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:54:16 +0800 Subject: [PATCH 35/43] ref(build): Remove leftover from troubleshooting --- scripts/bin/build/dev.js | 1 - scripts/src/build/dev.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/scripts/bin/build/dev.js b/scripts/bin/build/dev.js index 72f82ba6c..6dd38dbde 100644 --- a/scripts/bin/build/dev.js +++ b/scripts/bin/build/dev.js @@ -55,7 +55,6 @@ const getBundleContext = ({ srcDir, outDir }, bundles, app) => esbuild({ }); const getTabContext = ({ srcDir, outDir }, tabs) => esbuild({ ...tabOptions, - tsconfig: `${srcDir}/tsconfig.json`, entryPoints: tabs.map(tabNameExpander(srcDir)), outbase: outDir, outdir: outDir, diff --git a/scripts/src/build/dev.ts b/scripts/src/build/dev.ts index 840d4bb03..e3c812ec8 100644 --- a/scripts/src/build/dev.ts +++ b/scripts/src/build/dev.ts @@ -74,7 +74,6 @@ const getBundleContext = ({ srcDir, outDir }: ContextOptions, bundles: string[], const getTabContext = ({ srcDir, outDir }: ContextOptions, tabs: string[]) => esbuild({ ...tabOptions, - tsconfig: `${srcDir}/tsconfig.json`, entryPoints: tabs.map(tabNameExpander(srcDir)), outbase: outDir, outdir: outDir, From fd66e351a89332ebb1008b513e9233106bf8881f Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:27:54 +0800 Subject: [PATCH 36/43] docs(csg): Tweak pyramid dimensions description --- src/bundles/csg/functions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundles/csg/functions.ts b/src/bundles/csg/functions.ts index 4b4cc3a12..c66498451 100644 --- a/src/bundles/csg/functions.ts +++ b/src/bundles/csg/functions.ts @@ -378,7 +378,7 @@ export function geodesic_sphere(hex: string): Shape { * Returns a square pyramid Shape in the specified color. * * - Height: 1 - * - Side length: 1 + * - Base length: 1 * - Center: (0.5, 0.5, 0.5) * * @param hex hex color code From 48a1a3bcc3bed26249515a157e58278ce4c5d160 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:03:52 +0800 Subject: [PATCH 37/43] feat(sound): Throw error on invalid sound durations Closes #110 --- src/bundles/sound/functions.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bundles/sound/functions.ts b/src/bundles/sound/functions.ts index 52c66f5ae..571e6df6b 100644 --- a/src/bundles/sound/functions.ts +++ b/src/bundles/sound/functions.ts @@ -276,6 +276,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); } From d1d8ac6c4311cffb0c582a3e2f8cde7eb83491b0 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Wed, 6 Sep 2023 15:25:40 +0800 Subject: [PATCH 38/43] chore(pkg): Remove unused lodash --- package.json | 2 -- yarn.lock | 5 ----- 2 files changed, 7 deletions(-) diff --git a/package.json b/package.json index 50a695a36..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", @@ -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", diff --git a/yarn.lock b/yarn.lock index a7f54b412..f1cbe5183 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From 636ff3e3ebce23317df510d4b3215c9de6ed9f22 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Thu, 7 Sep 2023 01:22:24 +0800 Subject: [PATCH 39/43] feat(curve): Add Source version to canvases sample --- src/bundles/curve/samples/canvases.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bundles/curve/samples/canvases.js b/src/bundles/curve/samples/canvases.js index 4e99fd357..cbac23d7c 100644 --- a/src/bundles/curve/samples/canvases.js +++ b/src/bundles/curve/samples/canvases.js @@ -1,3 +1,4 @@ +// Source §1 // Simple curves that trigger the various types of canvases curve uses draw_connected_full_view(20)(unit_circle); From bb2137ba947abb19d756567c206e056d08e00091 Mon Sep 17 00:00:00 2001 From: Cloud <44526554+Cloud7050@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:17:15 +0800 Subject: [PATCH 40/43] feat(tabs): Remove enforced spacing of MultiItemDisplay --- src/tabs/common/multi_item_display.tsx | 3 --- 1 file changed, 3 deletions(-) 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, }} >