@threlte/flex 0.0.6 → 0.0.8

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,16 +1,28 @@
1
- <script>import { HierarchicalObject, T } from '@threlte/core';
1
+ <script>import { HierarchicalObject, T, createRawEventDispatcher } from '@threlte/core';
2
2
  import { onDestroy } from 'svelte';
3
3
  import { Group } from 'three';
4
4
  import { useFlex } from '../Flex/context';
5
+ import { createUseDimensionsContext } from '../hooks/useDimensions';
5
6
  import { createNodeContext } from '../nodes/context';
6
7
  export let order = undefined;
7
8
  let _class = '';
8
9
  export { _class as class };
9
- const { scaleFactor, onEvent, addNode, removeNode, updateNodeProps, mainAxis, crossAxis, depthAxis, classParser } = useFlex();
10
- export const group = new Group();
11
- export const contentGroup = new Group();
10
+ const dispatch = createRawEventDispatcher();
11
+ /**
12
+ * Create the context for `useDimensions`
13
+ */
14
+ const dimensionsContext = createUseDimensionsContext();
15
+ const { scaleFactor, onEvent, addNode, removeNode, updateNodeProps, mainAxis, crossAxis, depthAxis, classParser, reflow } = useFlex();
16
+ const group = new Group();
12
17
  group.userData.isNode = true;
13
- export const { node } = createNodeContext(order);
18
+ const contentGroup = new Group();
19
+ const { yoga } = useFlex();
20
+ const node = yoga.Node.create();
21
+ const parentNodeContext = createNodeContext(node);
22
+ parentNodeContext?.insertNode(node, order);
23
+ onDestroy(() => {
24
+ parentNodeContext?.removeNode(node);
25
+ });
14
26
  addNode(node, group, $$restProps);
15
27
  updateNodeProps(node, { ...classParser?.(_class, {}), ...$$restProps }, true);
16
28
  $: updateNodeProps(node, { ...classParser?.(_class, {}), ...$$restProps });
@@ -38,6 +50,12 @@ onEvent('reflow:after', () => {
38
50
  getContentGroup().position[$mainAxis] = computedWidth / 2;
39
51
  getContentGroup().position[$crossAxis] = -computedHeight / 2;
40
52
  getContentGroup().position[$depthAxis] = 0;
53
+ dimensionsContext.width.set(computedWidth);
54
+ dimensionsContext.height.set(computedHeight);
55
+ dispatch('reflow', {
56
+ width: computedWidth,
57
+ height: computedHeight
58
+ });
41
59
  });
42
60
  </script>
43
61
 
@@ -62,6 +80,7 @@ onEvent('reflow:after', () => {
62
80
  }}
63
81
  >
64
82
  <slot
83
+ {reflow}
65
84
  width={computedWidth}
66
85
  height={computedHeight}
67
86
  />
@@ -2,14 +2,20 @@ import { SvelteComponent } from 'svelte'
2
2
  import type { NodeProps } from '../lib/props'
3
3
 
4
4
  type BoxProps = NodeProps & {
5
- order?: number
5
+ order?: number | undefined
6
6
  class?: string
7
7
  }
8
8
 
9
- type BoxEvents = {}
9
+ type BoxEvents = {
10
+ reflow: {
11
+ width: number
12
+ height: number
13
+ }
14
+ }
10
15
 
11
16
  type BoxSlots = {
12
17
  default: {
18
+ reflow: () => void
13
19
  width: number
14
20
  height: number
15
21
  }
@@ -15,7 +15,13 @@ const component = forwardEventHandlers();
15
15
  {...$$restProps}
16
16
  bind:this={$component}
17
17
  let:reflow
18
+ let:width
19
+ let:height
18
20
  >
19
- <slot {reflow} />
21
+ <slot
22
+ {reflow}
23
+ {width}
24
+ {height}
25
+ />
20
26
  </InnerFlex>
21
27
  {/if}
@@ -10,6 +10,8 @@ type FlexEvents = ComponentEvents<InnerFlex>
10
10
  type FlexSlots = {
11
11
  default: {
12
12
  reflow: () => void
13
+ width: number
14
+ height: number
13
15
  }
14
16
  }
15
17
 
@@ -2,10 +2,12 @@
2
2
  import { onDestroy } from 'svelte';
3
3
  import { Box3, Group, Vector3 } from 'three';
4
4
  import { Direction } from 'yoga-layout';
5
+ import { createUseDimensionsContext } from '../hooks/useDimensions';
5
6
  import { getDepthAxis } from '../lib/getDepthAxis';
6
7
  import { getOrientedBoundingBoxSize } from '../lib/getOrientedBoundingBoxSize';
7
8
  import { getRootShift } from '../lib/getRootShift';
8
9
  import { applyNodeProps } from '../lib/props';
10
+ import { propsChanged } from '../lib/propsChanged';
9
11
  import { createNodeContext } from '../nodes/context';
10
12
  import { createFlexContext } from './context';
11
13
  export let yoga;
@@ -20,8 +22,14 @@ export { _class as class };
20
22
  const dispatch = createRawEventDispatcher();
21
23
  const rootGroup = new Group();
22
24
  rootGroup.userData.isNode = true;
25
+ const rootNode = yoga.Node.create();
26
+ createNodeContext(rootNode);
23
27
  const boundingBox = new Box3();
24
28
  const vec3 = new Vector3();
29
+ /**
30
+ * Create the context for `useDimensions`
31
+ */
32
+ const { width: computedWidth, height: computedHeight } = createUseDimensionsContext();
25
33
  /**
26
34
  * Reflowing inside useFrame automatically batches reflows to 1 per frame.
27
35
  */
@@ -68,9 +76,11 @@ const { start: reflow, stop } = useFrame(() => {
68
76
  maxY = Math.max(maxY, top + height);
69
77
  }
70
78
  flexContext.emit('reflow:after');
79
+ computedWidth.set((maxX - minX) / scaleFactor);
80
+ computedHeight.set((maxY - minY) / scaleFactor);
71
81
  dispatch('reflow', {
72
- width: (maxX - minX) / scaleFactor,
73
- height: (maxY - minY) / scaleFactor
82
+ width: computedWidth.current,
83
+ height: computedHeight.current
74
84
  });
75
85
  stop();
76
86
  }, { autostart: false });
@@ -83,25 +93,13 @@ const flexContext = createFlexContext({
83
93
  },
84
94
  updateNodeProps(node, props, force = false) {
85
95
  const nodeData = flexContext.nodes.get(node);
86
- if (!nodeData)
87
- return;
88
96
  // Updating the props can be forced and is done so on the initial call.
89
- if (!force) {
90
- // Because all NodeProps are primitive types, we can make a simple
91
- // comparison and only request a reflow when necessary. We do that by
92
- // checking the length of the props object and then checking if all keys
93
- // are the same and all values are the same.
94
- const previousKeys = Object.keys(nodeData.props);
95
- const currentKeys = Object.keys(props);
96
- if (previousKeys.length === currentKeys.length &&
97
- currentKeys.every((key) => previousKeys.includes(key)) &&
98
- previousKeys.every((key) => nodeData.props[key] === props[key])) {
99
- return;
100
- }
97
+ if (force || propsChanged(node, props)) {
98
+ applyNodeProps(node, props, scaleFactor);
99
+ reflow();
100
+ if (nodeData)
101
+ nodeData.props = props;
101
102
  }
102
- applyNodeProps(node, props, scaleFactor);
103
- nodeData.props = props;
104
- reflow();
105
103
  },
106
104
  removeNode(node) {
107
105
  flexContext.nodes.delete(node);
@@ -118,21 +116,26 @@ const flexContext = createFlexContext({
118
116
  classParser
119
117
  });
120
118
  const { mainAxis, crossAxis, depthAxis } = flexContext;
121
- const { node: rootNode } = createNodeContext();
122
119
  $: rootNode.setWidth(width * scaleFactor), rootNode.setHeight(height * scaleFactor);
123
- $: applyNodeProps(rootNode, { ...classParser?.(_class, {}), ...$$restProps }, scaleFactor),
124
- reflow();
125
- $: flexContext.rootWidth.set(width), flexContext.reflow('Updated root width');
126
- $: flexContext.rootHeight.set(height), flexContext.reflow('Updated root height');
127
- $: flexContext.mainAxis.set(plane[0]), flexContext.reflow('Updated main axis');
128
- $: flexContext.crossAxis.set(plane[1]), flexContext.reflow('Updated cross axis');
129
- $: flexContext.depthAxis.set(getDepthAxis(plane)), flexContext.reflow('Updated depth axis');
130
- $: flexContext.scaleFactor.set(scaleFactor), flexContext.reflow('Updated scale factor');
120
+ // prettier-ignore
121
+ flexContext.updateNodeProps(rootNode, { ...classParser?.(_class, {}), ...$$restProps }, true);
122
+ // prettier-ignore
123
+ $: flexContext.updateNodeProps(rootNode, { ...classParser?.(_class, {}), ...$$restProps });
124
+ $: flexContext.rootWidth.set(width), flexContext.reflow();
125
+ $: flexContext.rootHeight.set(height), flexContext.reflow();
126
+ $: flexContext.mainAxis.set(plane[0]), flexContext.reflow();
127
+ $: flexContext.crossAxis.set(plane[1]), flexContext.reflow();
128
+ $: flexContext.depthAxis.set(getDepthAxis(plane)), flexContext.reflow();
129
+ $: flexContext.scaleFactor.set(scaleFactor), flexContext.reflow();
131
130
  onDestroy(() => {
132
131
  rootNode.free();
133
132
  });
134
133
  </script>
135
134
 
136
135
  <T is={rootGroup}>
137
- <slot {reflow} />
136
+ <slot
137
+ {reflow}
138
+ width={$computedWidth}
139
+ height={$computedHeight}
140
+ />
138
141
  </T>
@@ -24,6 +24,8 @@ type InnerFlexEvents = {
24
24
  type InnerFlexSlots = {
25
25
  default: {
26
26
  reflow: () => void
27
+ width: number
28
+ height: number
27
29
  }
28
30
  }
29
31
 
@@ -25,7 +25,7 @@ export type FlexContextData = {
25
25
  rootGroup: Group;
26
26
  rootWidth: CurrentWritable<number>;
27
27
  rootHeight: CurrentWritable<number>;
28
- reflow: (msg?: string) => void;
28
+ reflow: () => void;
29
29
  classParser?: ClassParser;
30
30
  };
31
31
  export type FlexContext = FlexContextData & Emitter<FlexContextEvents> & {
@@ -0,0 +1,50 @@
1
+ import { type CurrentWritable } from '@threlte/core';
2
+ type UseDimensionsContext = {
3
+ width: CurrentWritable<number>;
4
+ height: CurrentWritable<number>;
5
+ };
6
+ export declare const flexContextName = "__threlte-flex-dimensions";
7
+ /**
8
+ * Creates a context for useDimensions.
9
+ */
10
+ export declare const createUseDimensionsContext: () => UseDimensionsContext;
11
+ /**
12
+ * The hook `useDimensions` can be used to retrieve the computed width and
13
+ * height of a `<Flex>` or `<Box>` component as
14
+ * [CurrentWritable](https://threlte.xyz/docs/reference/core/utilities#currentwritable)
15
+ * stores.
16
+ *
17
+ * ## Usage
18
+ *
19
+ * ### In a `<Flex>` component
20
+ *
21
+ * Because there's no viewport to measure, the width and height of a `<Flex>`
22
+ * component need to be set manually. Nevertheless, the dimensions of the
23
+ * contents of the `<Flex>` component will be measured after a layout reflow and
24
+ * can be retrieved using `useDimensions` in a direct child component to
25
+ * `<Flex>`.
26
+ *
27
+ * ### In a `<Box>` component
28
+ *
29
+ * By default `@threlte/flex` controls elements position only. In some cases you
30
+ * may want to control element sizing too. Since `@threlte/flex` has no
31
+ * information about how the inner content size works, you need to set your
32
+ * content size manually. You can do this by using `useDimensions` hook in a
33
+ * direct child component to `<Box>`.
34
+ *
35
+ * @example
36
+ * ```svelte
37
+ * <script>
38
+ * import { useDimensions } from '@threlte/flex'
39
+ *
40
+ * const { width, height } = useDimensions()
41
+ * </script>
42
+ *
43
+ * <T.Mesh scale.x={$width} scale.y={$height}>
44
+ * <T.BoxGeometry />
45
+ * <T.MeshBasicMaterial color="red" />
46
+ * </T.Mesh>
47
+ * ```
48
+ */
49
+ export declare const useDimensions: () => UseDimensionsContext;
50
+ export {};
@@ -0,0 +1,59 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { currentWritable } from '@threlte/core';
3
+ export const flexContextName = '__threlte-flex-dimensions';
4
+ /**
5
+ * Creates a context for useDimensions.
6
+ */
7
+ export const createUseDimensionsContext = () => {
8
+ const context = {
9
+ width: currentWritable(0),
10
+ height: currentWritable(0)
11
+ };
12
+ setContext(flexContextName, context);
13
+ return context;
14
+ };
15
+ /**
16
+ * The hook `useDimensions` can be used to retrieve the computed width and
17
+ * height of a `<Flex>` or `<Box>` component as
18
+ * [CurrentWritable](https://threlte.xyz/docs/reference/core/utilities#currentwritable)
19
+ * stores.
20
+ *
21
+ * ## Usage
22
+ *
23
+ * ### In a `<Flex>` component
24
+ *
25
+ * Because there's no viewport to measure, the width and height of a `<Flex>`
26
+ * component need to be set manually. Nevertheless, the dimensions of the
27
+ * contents of the `<Flex>` component will be measured after a layout reflow and
28
+ * can be retrieved using `useDimensions` in a direct child component to
29
+ * `<Flex>`.
30
+ *
31
+ * ### In a `<Box>` component
32
+ *
33
+ * By default `@threlte/flex` controls elements position only. In some cases you
34
+ * may want to control element sizing too. Since `@threlte/flex` has no
35
+ * information about how the inner content size works, you need to set your
36
+ * content size manually. You can do this by using `useDimensions` hook in a
37
+ * direct child component to `<Box>`.
38
+ *
39
+ * @example
40
+ * ```svelte
41
+ * <script>
42
+ * import { useDimensions } from '@threlte/flex'
43
+ *
44
+ * const { width, height } = useDimensions()
45
+ * </script>
46
+ *
47
+ * <T.Mesh scale.x={$width} scale.y={$height}>
48
+ * <T.BoxGeometry />
49
+ * <T.MeshBasicMaterial color="red" />
50
+ * </T.Mesh>
51
+ * ```
52
+ */
53
+ export const useDimensions = () => {
54
+ const context = getContext(flexContextName);
55
+ if (!context) {
56
+ throw new Error('useDimensions must be used within a <Flex> component');
57
+ }
58
+ return context;
59
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The hook useReflow allows you to manually request a [layout
3
+ * reflow](https://threlte.xyz/docs/reference/flex/flex#layout-reflow), for
4
+ * instance when a [`<Text>`](https://threlte.xyz/docs/reference/extras/text)
5
+ * component finished synchronizing or when a model has loaded into view and
6
+ * there’s no easy access to the reflow slot prop of the components `<Flex>` or
7
+ * `<Box>` because of component composition. Calls to`reflow` will be limited to
8
+ * once per frame, so it’s safe to call it from multiple components at a time.
9
+ */
10
+ export declare const useReflow: () => () => void;
@@ -0,0 +1,18 @@
1
+ import { getContext } from 'svelte';
2
+ import { flexContextName } from '../Flex/context';
3
+ /**
4
+ * The hook useReflow allows you to manually request a [layout
5
+ * reflow](https://threlte.xyz/docs/reference/flex/flex#layout-reflow), for
6
+ * instance when a [`<Text>`](https://threlte.xyz/docs/reference/extras/text)
7
+ * component finished synchronizing or when a model has loaded into view and
8
+ * there’s no easy access to the reflow slot prop of the components `<Flex>` or
9
+ * `<Box>` because of component composition. Calls to`reflow` will be limited to
10
+ * once per frame, so it’s safe to call it from multiple components at a time.
11
+ */
12
+ export const useReflow = () => {
13
+ const flexContext = getContext(flexContextName);
14
+ if (!flexContext) {
15
+ throw new Error('useReflow must be used within a <Flex> component');
16
+ }
17
+ return flexContext.reflow;
18
+ };
package/dist/index.d.ts CHANGED
@@ -3,3 +3,5 @@ export { default as Flex } from './Flex/Flex.svelte';
3
3
  export type { NodeProps } from './lib/props';
4
4
  export { createClassParser } from './parsers/createClassParser';
5
5
  export { tailwindParser } from './parsers/tailwindParser';
6
+ export { useReflow } from './hooks/useReflow';
7
+ export { useDimensions } from './hooks/useDimensions';
package/dist/index.js CHANGED
@@ -3,3 +3,6 @@ export { default as Flex } from './Flex/Flex.svelte';
3
3
  // parsers
4
4
  export { createClassParser } from './parsers/createClassParser';
5
5
  export { tailwindParser } from './parsers/tailwindParser';
6
+ // hooks
7
+ export { useReflow } from './hooks/useReflow';
8
+ export { useDimensions } from './hooks/useDimensions';
@@ -0,0 +1,8 @@
1
+ import type { NodeProps } from './props';
2
+ /**
3
+ * Because all NodeProps are primitive types, we can make a simple comparison
4
+ * and only request a reflow when necessary. We do that by checking the length
5
+ * of the props object and then checking if all keys are the same and all values
6
+ * are the same.
7
+ */
8
+ export declare const propsChanged: (node: Node, props: NodeProps) => boolean;
@@ -0,0 +1,26 @@
1
+ const nodePropsMap = new WeakMap();
2
+ /**
3
+ * Because all NodeProps are primitive types, we can make a simple comparison
4
+ * and only request a reflow when necessary. We do that by checking the length
5
+ * of the props object and then checking if all keys are the same and all values
6
+ * are the same.
7
+ */
8
+ export const propsChanged = (node, props) => {
9
+ // get a reference to the props data for this node
10
+ const propsData = nodePropsMap.get(node);
11
+ // assume that the props have changed
12
+ let changed = true;
13
+ if (propsData) {
14
+ // compare the keys and values of the previous and current props
15
+ const previousKeys = Object.keys(propsData);
16
+ const currentKeys = Object.keys(props);
17
+ if (previousKeys.length === currentKeys.length &&
18
+ currentKeys.every((key) => previousKeys.includes(key)) &&
19
+ previousKeys.every((key) => propsData[key] === props[key])) {
20
+ changed = false;
21
+ }
22
+ }
23
+ // update the props data for this node
24
+ nodePropsMap.set(node, props);
25
+ return changed;
26
+ };
@@ -1,9 +1,7 @@
1
1
  import type { Node } from 'yoga-layout';
2
2
  export type NodeContext = {
3
- node: Node;
4
- insertChild: (child: Node, order?: number) => void;
5
- removeChild: (child: Node) => void;
3
+ insertNode: (childNode: Node, order?: number) => void;
4
+ removeNode: (childNode: Node) => void;
6
5
  };
7
6
  export declare const nodeContextName = "__threlte-node";
8
- export declare const useNode: () => NodeContext;
9
- export declare const createNodeContext: (order?: number) => NodeContext;
7
+ export declare const createNodeContext: (node: Node) => NodeContext | undefined;
@@ -1,31 +1,47 @@
1
- import { getContext, onDestroy, setContext } from 'svelte';
2
- import { useFlex } from '../Flex/context';
1
+ import { getContext, setContext } from 'svelte';
3
2
  export const nodeContextName = '__threlte-node';
4
- export const useNode = () => {
5
- return getContext(nodeContextName);
6
- };
7
- export const createNodeContext = (order) => {
8
- const { yoga } = useFlex();
9
- const node = yoga.Node.create();
10
- const parentNodeContext = useNode();
11
- parentNodeContext?.insertChild(node, order);
12
- onDestroy(() => {
13
- parentNodeContext?.removeChild(node);
14
- });
15
- const data = {
16
- node,
17
- insertChild(child, order) {
3
+ export const createNodeContext = (node) => {
4
+ const childNodes = new Set();
5
+ const childNodesOrderMap = new Map();
6
+ const parentNodeContext = getContext(nodeContextName);
7
+ setContext(nodeContextName, {
8
+ insertNode(childNode, order) {
9
+ // we want to keep track of all child nodes
10
+ childNodes.add(childNode);
11
+ // Additionally, we need to keep track of child nodes that need to be inserted at a specific order
18
12
  if (order !== undefined) {
19
- data.node.insertChild(child, order);
13
+ childNodesOrderMap.set(childNode, {
14
+ requestedOrder: order
15
+ });
16
+ }
17
+ if (childNodesOrderMap.size) {
18
+ // we need to sort the child nodes by their requested order. We leave the nodes that don't have a requested order untouched.
19
+ const sorted = Array.from(childNodes)
20
+ .map((node, index) => {
21
+ return {
22
+ order: childNodesOrderMap.get(node)?.requestedOrder ?? index,
23
+ node
24
+ };
25
+ })
26
+ .sort((a, b) => a.order - b.order)
27
+ .map(({ node }) => node);
28
+ // Then we need to remove all child nodes from the node and insert them in the correct order.
29
+ sorted.forEach((childNode) => {
30
+ node.removeChild(childNode);
31
+ });
32
+ sorted.forEach((childNode, index) => {
33
+ node.insertChild(childNode, index);
34
+ });
20
35
  }
21
36
  else {
22
- data.node.insertChild(child, data.node.getChildCount());
37
+ node.insertChild(childNode, node.getChildCount());
23
38
  }
24
39
  },
25
- removeChild(child) {
26
- data.node.removeChild(child);
40
+ removeNode(childNode) {
41
+ node.removeChild(childNode);
42
+ childNodes.delete(childNode);
43
+ childNodesOrderMap.delete(childNode);
27
44
  }
28
- };
29
- setContext(nodeContextName, data);
30
- return data;
45
+ });
46
+ return parentNodeContext;
31
47
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threlte/flex",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "author": "Grischa Erbe <hello@legrisch.com> (https://legrisch.com)",
5
5
  "license": "MIT",
6
6
  "devDependencies": {
@@ -27,8 +27,8 @@
27
27
  "tslib": "^2.4.1",
28
28
  "typescript": "^5.0.0",
29
29
  "vite": "^4.3.6",
30
- "@threlte/core": "6.0.8",
31
- "@threlte/extras": "5.7.0"
30
+ "@threlte/core": "6.0.9",
31
+ "@threlte/extras": "6.0.0"
32
32
  },
33
33
  "dependencies": {
34
34
  "mitt": "^3.0.1",