mathbox-react 0.0.9-dev-3 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import React from "react";
2
2
  import { Props, NodeType, MathboxSelection } from "mathbox";
3
3
  import { WithChildren } from "./types";
4
- declare type MathboxComponent<T extends NodeType> = React.ForwardRefExoticComponent<WithChildren<Props[T]> & React.RefAttributes<MathboxSelection<T>>>;
4
+ import { ParentNodeTypes } from "./util";
5
+ declare type MathboxComponent<T extends NodeType> = React.ForwardRefExoticComponent<(T extends ParentNodeTypes ? WithChildren<Props[T]> : Props[T]) & React.RefAttributes<MathboxSelection<T>>>;
5
6
  /**
6
7
  * Component wrapper for mathbox [`area`](https://gitgud.io/unconed/mathbox/-/blob/master/docs/primitives.md#dataarea).
7
8
  */
@@ -1,4 +1,4 @@
1
- import { MathboxSelection } from "mathbox";
1
+ import { MathboxSelection, NodeType } from "mathbox";
2
2
  export declare const isRootDestroyed: (selection: MathboxSelection) => boolean;
3
3
  /**
4
4
  * Check that parent is actually the selection's parent.
@@ -16,3 +16,23 @@ export declare const isRootDestroyed: (selection: MathboxSelection) => boolean;
16
16
  * smart about diffing out the actual changes.
17
17
  */
18
18
  export declare const isSelectionParent: (selection: MathboxSelection, parent: MathboxSelection) => boolean;
19
+ declare const CAN_HAVE_CHILDREN: readonly ["view", "cartesian", "cartesian4", "polar", "spherical", "stereographic", "stereographic4", "transform", "transform4", "vertex", "fragment", "layer", "mask", "group", "inherit", "root", "unit", "rtt", "clock", "now", "move", "present", "reveal", "slide"];
20
+ export declare type ParentNodeTypes = typeof CAN_HAVE_CHILDREN[number];
21
+ export declare const canNodeHaveChildren: (type: NodeType) => boolean;
22
+ export declare const capitalize: (s: string) => string;
23
+ /**
24
+ * Returns the same reference to `value` as long as the the given values are
25
+ * deep equal.
26
+ *
27
+ * This is, in general, not a great idea. See
28
+ * https://github.com/facebook/react/issues/14476#issuecomment-471199055
29
+ *
30
+ * This should work OK for mathbox options because the objects in question are
31
+ * either shallow, or are deep equal at a fairly shallow level.
32
+ * E.g., OrbitControls may not be a shall object, but if it's not, it will at
33
+ * least be deep equal between renders.
34
+ *
35
+ * But should work OK for mathbox options.
36
+ */
37
+ export declare const useDeepCompareMemo: <T>(value: T, initial: T) => T;
38
+ export {};
package/build/index.d.ts CHANGED
@@ -5,7 +5,10 @@ declare type WithChildren<T> = {
5
5
  children?: ReactNode | ReactNode[];
6
6
  } & T;
7
7
 
8
- declare type MathboxComponent<T extends NodeType> = React.ForwardRefExoticComponent<WithChildren<Props[T]> & React.RefAttributes<MathboxSelection<T>>>;
8
+ declare const CAN_HAVE_CHILDREN: readonly ["view", "cartesian", "cartesian4", "polar", "spherical", "stereographic", "stereographic4", "transform", "transform4", "vertex", "fragment", "layer", "mask", "group", "inherit", "root", "unit", "rtt", "clock", "now", "move", "present", "reveal", "slide"];
9
+ declare type ParentNodeTypes = typeof CAN_HAVE_CHILDREN[number];
10
+
11
+ declare type MathboxComponent<T extends NodeType> = React.ForwardRefExoticComponent<(T extends ParentNodeTypes ? WithChildren<Props[T]> : Props[T]) & React.RefAttributes<MathboxSelection<T>>>;
9
12
  /**
10
13
  * Component wrapper for mathbox [`area`](https://gitgud.io/unconed/mathbox/-/blob/master/docs/primitives.md#dataarea).
11
14
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mathbox-react",
3
- "version": "0.0.9-dev-3",
3
+ "version": "0.0.10",
4
4
  "description": "React wrapper for Mathbox",
5
5
  "license": "ISC",
6
6
  "author": "Chris Chudzicki",
@@ -31,6 +31,7 @@
31
31
  "@storybook/testing-library": "^0.0.9",
32
32
  "@testing-library/react": "^12.1.3",
33
33
  "@types/jest": "^27.4.1",
34
+ "@types/lodash": "^4.14.181",
34
35
  "@types/react": "^17.0.39",
35
36
  "@types/three": "^0.137.0",
36
37
  "babel-jest": "^27.5.1",
@@ -42,7 +43,7 @@
42
43
  "eslint-import-resolver-typescript": "^2.5.0",
43
44
  "eslint-plugin-prettier": "^4.0.0",
44
45
  "jest": "^27.5.1",
45
- "mathbox": "^2.1.3",
46
+ "mathbox": "^2.1.4",
46
47
  "prettier": "^2.5.1",
47
48
  "react": "^17.0.2",
48
49
  "react-dom": "^17.0.2",
@@ -55,8 +56,11 @@
55
56
  "typescript": "^4.5.5"
56
57
  },
57
58
  "peerDependencies": {
58
- "mathbox": "^2.1.3",
59
+ "mathbox": "^2.1.4",
59
60
  "react": "^17.0.2 || ^18.0.0",
60
61
  "three": ">=0.118.0"
62
+ },
63
+ "dependencies": {
64
+ "lodash": "^4.17.21"
61
65
  }
62
66
  }
@@ -3,12 +3,12 @@ import React, {
3
3
  useState,
4
4
  forwardRef,
5
5
  useImperativeHandle,
6
- useMemo,
7
6
  } from "react"
8
7
  import { Color } from "three"
9
8
  import { mathBox, RootProps, MathboxSelection, MathBoxOptions } from "mathbox"
10
9
  import MathboxAPIContext from "./MathboxNodeContext"
11
10
  import { WithChildren } from "./types"
11
+ import { useDeepCompareMemo } from "./util"
12
12
 
13
13
  type Props = WithChildren<
14
14
  {
@@ -22,7 +22,7 @@ const Mathbox = (
22
22
  ref: React.Ref<MathboxSelection<"root"> | null>
23
23
  ) => {
24
24
  const { container, children, options, ...rootProps } = props
25
- const mathboxOptions = useMemo(() => options ?? {}, [options])
25
+ const mathboxOptions = useDeepCompareMemo(options ?? {}, {})
26
26
  const [selection, setSelection] = useState<MathboxSelection<"root"> | null>(
27
27
  null
28
28
  )
@@ -39,6 +39,7 @@ const Mathbox = (
39
39
  * TODO: Should Mathbox component allow setting these more easily?
40
40
  */
41
41
  mathbox.three.renderer.setClearColor(new Color(0xffffff), 1.0)
42
+ mathbox.three.camera.position.set(1, 1, 2)
42
43
  return () => {
43
44
  mathbox.select("*").remove()
44
45
  mathbox.three.destroy()
@@ -3,7 +3,7 @@ import { render, act } from "@testing-library/react"
3
3
  import { MathboxSelection } from "mathbox"
4
4
  import ContainedMathbox from "./ContainedMathbox"
5
5
  import Mathbox from "./Mathbox"
6
- import { Cartesian, Grid } from "./components"
6
+ import { Cartesian, Grid, Voxel } from "./components"
7
7
  import { MathboxRef } from "./types"
8
8
 
9
9
  function assertNotNil<T>(value: T): asserts value is NonNullable<T> {
@@ -27,6 +27,24 @@ const assertSelectionsEqual = (s1: MathboxSelection, s2: MathboxSelection) => {
27
27
  })
28
28
  }
29
29
 
30
+ describe("ContainedMathbox", () => {
31
+ it("makes a new mathbox instance only if the options have changed", () => {
32
+ const mbRef: MathboxRef<"root"> = { current: null }
33
+ const { rerender } = render(<ContainedMathbox options={{}} ref={mbRef} />)
34
+
35
+ const mb0 = mbRef.current
36
+ assertNotNil(mb0)
37
+ rerender(<ContainedMathbox options={{}} ref={mbRef} />)
38
+ const mb1 = mbRef.current
39
+ expect(mb1).toBe(mb0)
40
+ assertNotNil(mb1)
41
+ rerender(<ContainedMathbox options={{ plugins: ["core"] }} ref={mbRef} />)
42
+ const mb2 = mbRef.current
43
+ assertNotNil(mb2)
44
+ expect(mb2).not.toBe(mb1)
45
+ })
46
+ })
47
+
30
48
  describe("Cartesian", () => {
31
49
  it("exposes Mathbox instance via ref", () => {
32
50
  const mbRef: MathboxRef<"root"> = { current: null }
@@ -145,7 +163,7 @@ describe("Cartesian", () => {
145
163
  * When re-rendered, this will create a new mathBox since the options
146
164
  * object prop will have changed.
147
165
  */
148
- const options2 = {}
166
+ const options2 = { plugins: ["core"] }
149
167
  expect(options1).not.toBe(options2)
150
168
 
151
169
  rerender(
@@ -163,7 +181,7 @@ describe("Cartesian", () => {
163
181
  assertNotNil(cartesian2)
164
182
  assertNotNil(grid2)
165
183
 
166
- // It creates a new instance b/c the options prop has changed by reference
184
+ // The options have changed (via deep equal) so a new instance is created
167
185
  expect(mb1).not.toBe(mb2)
168
186
 
169
187
  expect(cartesian2).toHaveLength(1)
@@ -238,3 +256,21 @@ describe("Cartesian", () => {
238
256
  expect(mbRef.current.select("grid").length).toBe(0)
239
257
  })
240
258
  })
259
+
260
+ describe("<Voxel />", () => {
261
+ it("throws a runtime error if it has children", () => {
262
+ const willThrow = () =>
263
+ render(
264
+ <ContainedMathbox>
265
+ <Cartesian>
266
+ {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
267
+ {/* @ts-ignore */}
268
+ <Voxel>
269
+ <Grid />
270
+ </Voxel>
271
+ </Cartesian>
272
+ </ContainedMathbox>
273
+ )
274
+ expect(willThrow).toThrow("Component <Voxel /> cannot have children.")
275
+ })
276
+ })
@@ -9,15 +9,24 @@ import React, {
9
9
  import { Props, NodeType, MathboxSelection } from "mathbox"
10
10
  import MathboxAPIContext from "./MathboxNodeContext"
11
11
  import { WithChildren } from "./types"
12
- import { isRootDestroyed, isSelectionParent } from "./util"
12
+ import {
13
+ isRootDestroyed,
14
+ isSelectionParent,
15
+ canNodeHaveChildren,
16
+ ParentNodeTypes,
17
+ capitalize,
18
+ } from "./util"
13
19
 
14
20
  type MathboxComponent<T extends NodeType> = React.ForwardRefExoticComponent<
15
- WithChildren<Props[T]> & React.RefAttributes<MathboxSelection<T>>
21
+ (T extends ParentNodeTypes ? WithChildren<Props[T]> : Props[T]) &
22
+ React.RefAttributes<MathboxSelection<T>>
16
23
  >
17
24
 
18
25
  const mathboxComponentFactory = <T extends NodeType>(
19
26
  type: T
20
27
  ): MathboxComponent<T> => {
28
+ const canHaveChildren = canNodeHaveChildren(type)
29
+ const componentName = capitalize(type)
21
30
  const Comp = (
22
31
  props: WithChildren<Props[T]>,
23
32
  ref: React.Ref<MathboxSelection<T> | null>
@@ -29,6 +38,7 @@ const mathboxComponentFactory = <T extends NodeType>(
29
38
  () => () => {
30
39
  if (selection.current) {
31
40
  selection.current.remove()
41
+ selection.current = null
32
42
  }
33
43
  },
34
44
  []
@@ -58,7 +68,12 @@ const mathboxComponentFactory = <T extends NodeType>(
58
68
  selection.current.set(others)
59
69
  }
60
70
  }, [parent, others])
61
-
71
+ if (!canHaveChildren) {
72
+ if (props.children) {
73
+ throw new Error(`Component <${componentName} /> cannot have children.`)
74
+ }
75
+ return null
76
+ }
62
77
  return (
63
78
  <MathboxAPIContext.Provider value={selection.current}>
64
79
  {props.children}
@@ -1,5 +1,7 @@
1
1
  /* eslint-disable no-underscore-dangle */
2
- import { MathboxSelection } from "mathbox"
2
+ import { isEqual } from "lodash"
3
+ import { useRef, useMemo } from "react"
4
+ import { MathboxSelection, NodeType } from "mathbox"
3
5
 
4
6
  type WithPrivateUp = MathboxSelection & { _up?: WithPrivateUp }
5
7
 
@@ -41,3 +43,64 @@ export const isSelectionParent = (
41
43
  const selectionParentNode = (selection as WithPrivateUp)._up?.[0]
42
44
  return selectionParentNode === parent[0]
43
45
  }
46
+
47
+ const CAN_HAVE_CHILDREN = [
48
+ "view",
49
+ "cartesian",
50
+ "cartesian4",
51
+ "polar",
52
+ "spherical",
53
+ "stereographic",
54
+ "stereographic4",
55
+ "transform",
56
+ "transform4",
57
+ "vertex",
58
+ "fragment",
59
+ "layer",
60
+ "mask",
61
+ "group",
62
+ "inherit",
63
+ "root",
64
+ "unit",
65
+ "rtt",
66
+ "clock",
67
+ "now",
68
+ "move",
69
+ "present",
70
+ "reveal",
71
+ "slide",
72
+ ] as const
73
+
74
+ export type ParentNodeTypes = typeof CAN_HAVE_CHILDREN[number]
75
+
76
+ export const canNodeHaveChildren = (type: NodeType) =>
77
+ (CAN_HAVE_CHILDREN as readonly string[]).includes(type)
78
+
79
+ export const capitalize = (s: string) => {
80
+ if (!s) return ""
81
+ return s[0].toLocaleUpperCase() + s.slice(1)
82
+ }
83
+
84
+ /**
85
+ * Returns the same reference to `value` as long as the the given values are
86
+ * deep equal.
87
+ *
88
+ * This is, in general, not a great idea. See
89
+ * https://github.com/facebook/react/issues/14476#issuecomment-471199055
90
+ *
91
+ * This should work OK for mathbox options because the objects in question are
92
+ * either shallow, or are deep equal at a fairly shallow level.
93
+ * E.g., OrbitControls may not be a shall object, but if it's not, it will at
94
+ * least be deep equal between renders.
95
+ *
96
+ * But should work OK for mathbox options.
97
+ */
98
+ export const useDeepCompareMemo = <T>(value: T, initial: T): T => {
99
+ const oldValue = useRef<T>(initial)
100
+ const memoOptions = useMemo(() => {
101
+ if (isEqual(value, oldValue.current)) return oldValue.current
102
+ oldValue.current = value
103
+ return value
104
+ }, [value])
105
+ return memoOptions
106
+ }