@threlte/flex 0.0.2
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.
- package/LICENSE.md +21 -0
- package/README.md +67 -0
- package/dist/Box/Box.svelte +62 -0
- package/dist/Box/Box.svelte.d.ts +62 -0
- package/dist/Flex/Flex.svelte +21 -0
- package/dist/Flex/Flex.svelte.d.ts +61 -0
- package/dist/Flex/InnerFlex.svelte +137 -0
- package/dist/Flex/InnerFlex.svelte.d.ts +63 -0
- package/dist/Flex/context.d.ts +39 -0
- package/dist/Flex/context.js +18 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lib/alignFlexProps.d.ts +9 -0
- package/dist/lib/alignFlexProps.js +10 -0
- package/dist/lib/getDepthAxis.d.ts +2 -0
- package/dist/lib/getDepthAxis.js +10 -0
- package/dist/lib/getFlex2DSize.d.ts +2 -0
- package/dist/lib/getFlex2DSize.js +10 -0
- package/dist/lib/getOrientedBoundingBoxSize.d.ts +11 -0
- package/dist/lib/getOrientedBoundingBoxSize.js +25 -0
- package/dist/lib/getRootShift.d.ts +2 -0
- package/dist/lib/getRootShift.js +10 -0
- package/dist/lib/isTopLevelChildNode.d.ts +1 -0
- package/dist/lib/isTopLevelChildNode.js +1 -0
- package/dist/lib/props.d.ts +52 -0
- package/dist/lib/props.js +65 -0
- package/dist/nodes/context.d.ts +9 -0
- package/dist/nodes/context.js +31 -0
- package/dist/parsers/createClassParser.d.ts +8 -0
- package/dist/parsers/createClassParser.js +9 -0
- package/dist/parsers/tailwindParser.d.ts +2 -0
- package/dist/parsers/tailwindParser.js +266 -0
- package/dist/stores/yoga.d.ts +1 -0
- package/dist/stores/yoga.js +12 -0
- package/package.json +62 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Grischa Erbe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<div align="right">
|
|
2
|
+
<a href="https://www.npmjs.com/package/@threlte/flex">
|
|
3
|
+
<img alt="npm" src="https://img.shields.io/npm/v/@threlte/flex?color=fe4100&labelColor=171d27&logo=npm&logoColor=white"/>
|
|
4
|
+
</a>
|
|
5
|
+
<a href="https://github.com/threlte/threlte/blob/main/LICENSE.md">
|
|
6
|
+
<img alt="license" src="https://img.shields.io/npm/l/@threlte/core?color=fe4100&labelColor=171d27&logo=git&logoColor=white"/>
|
|
7
|
+
</a>
|
|
8
|
+
<a href="https://discord.com/channels/985983540804091964">
|
|
9
|
+
<img alt="discord" src="https://img.shields.io/discord/985983540804091964?label=discord&color=fe4100&labelColor=171d27&logo=discord&logoColor=white"/>
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://threlte.xyz">
|
|
12
|
+
<img alt="docs" src="https://img.shields.io/website?down_color=red&down_message=offline&label=docs&color=fe4100&labelColor=171d27&up_message=online&url=https%3A%2F%2Fthrelte.xyz&logo=svelte&logoColor=white"/>
|
|
13
|
+
</a>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<a href="https://threlte.xyz">
|
|
17
|
+
<img src="https://threlte.xyz/logo/threlte-banner.jpg"/>
|
|
18
|
+
</a>
|
|
19
|
+
|
|
20
|
+
## Rapidly Build Interactive 3D Apps for the Web
|
|
21
|
+
|
|
22
|
+
Threlte is a [Svelte](https://svelte.dev/) library that simplifies creating 3D apps for the web. It provides a **declarative**, **type-safe**, **reactive** and **interactive** API out-of-the-box.
|
|
23
|
+
|
|
24
|
+
Threlte's **3D rendering** is powered by [Three.js](https://threejs.org/), and it also provides a **physics engine** through [Rapier](https://rapier.rs/) and an **animation studio** via [Theatre.js](https://www.theatrejs.com/); see [packages](#packages) for details.
|
|
25
|
+
|
|
26
|
+
Check out our **[documentation](https://threlte.xyz)** and our **[Discord community](https://discord.gg/EqUBCfCaGm)**.
|
|
27
|
+
|
|
28
|
+
## @threlte/flex
|
|
29
|
+
|
|
30
|
+
[@threlte/flex](https://threlte.xyz/docs/reference/flex/getting-started) makes placing 3D content based on flexbox principles a breeze.
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
### Installation
|
|
35
|
+
|
|
36
|
+
For a quick interactive setup of a fresh Threlte project, run:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm create threlte my-project
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Alternatively you can check out the full [installation instructions](https://threlte.xyz/docs/learn/getting-started/installation).
|
|
43
|
+
|
|
44
|
+
### Tutorial
|
|
45
|
+
|
|
46
|
+
To get a hang of the basics, we recommend following our [introductory tutorial](https://threlte.xyz/docs/learn/getting-started/your-first-scene).
|
|
47
|
+
|
|
48
|
+
### Support
|
|
49
|
+
|
|
50
|
+
Have questions? Feel free to ask in our [Discord support forum](https://discord.com/channels/985983540804091964/1031843197963477002).
|
|
51
|
+
|
|
52
|
+
## Contributing
|
|
53
|
+
|
|
54
|
+
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
|
55
|
+
|
|
56
|
+
- **Filing Issues** - if you have feature requestions or you think you spotted a bug, [submit an issue](https://github.com/threlte/threlte/issues/new).
|
|
57
|
+
- **Contributing Code** - if you would like to drop us a PR, read the [contribution guide](https://github.com/threlte/threlte/blob/main/CONTRIBUTING.md) first.
|
|
58
|
+
|
|
59
|
+
## Sponsors
|
|
60
|
+
|
|
61
|
+
[](https://vercel.com/?utm_source=threlte&utm_campaign=oss)
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### License
|
|
66
|
+
|
|
67
|
+
The MIT License (MIT). Please see the [License File](LICENSE.md) for more information.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script>import { HierarchicalObject, T } from '@threlte/core';
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import { Group } from 'three';
|
|
4
|
+
import { useFlex } from '../Flex/context';
|
|
5
|
+
import { createNodeContext } from '../nodes/context';
|
|
6
|
+
export let order = undefined;
|
|
7
|
+
let _class = '';
|
|
8
|
+
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();
|
|
12
|
+
group.userData.isNode = true;
|
|
13
|
+
export const { node } = createNodeContext(order);
|
|
14
|
+
addNode(node, group, $$restProps);
|
|
15
|
+
updateNodeProps(node, { ...classParser?.(_class), ...$$restProps }, true);
|
|
16
|
+
$: updateNodeProps(node, { ...classParser?.(_class), ...$$restProps });
|
|
17
|
+
onDestroy(() => {
|
|
18
|
+
removeNode(node);
|
|
19
|
+
});
|
|
20
|
+
let computedWidth = 1;
|
|
21
|
+
let computedHeight = 1;
|
|
22
|
+
// after the parent has been reflowed, we can use the calculated layout to set the properties of the box
|
|
23
|
+
onEvent('reflow:after', () => {
|
|
24
|
+
computedWidth =
|
|
25
|
+
typeof $$restProps.width === 'number'
|
|
26
|
+
? $$restProps.width
|
|
27
|
+
: node.getComputedWidth() / $scaleFactor;
|
|
28
|
+
computedHeight =
|
|
29
|
+
typeof $$restProps.height === 'number'
|
|
30
|
+
? $$restProps.height
|
|
31
|
+
: node.getComputedHeight() / $scaleFactor;
|
|
32
|
+
contentGroup.position[$mainAxis] = computedWidth / 2;
|
|
33
|
+
contentGroup.position[$crossAxis] = -computedHeight / 2;
|
|
34
|
+
contentGroup.position[$depthAxis] = 0;
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<T is={group}>
|
|
39
|
+
<T is={contentGroup} />
|
|
40
|
+
</T>
|
|
41
|
+
|
|
42
|
+
<HierarchicalObject
|
|
43
|
+
onChildMount={(child) => {
|
|
44
|
+
if (child.userData.isNode) {
|
|
45
|
+
group.add(child)
|
|
46
|
+
} else {
|
|
47
|
+
contentGroup.add(child)
|
|
48
|
+
}
|
|
49
|
+
}}
|
|
50
|
+
onChildDestroy={(child) => {
|
|
51
|
+
if (child.userData.isNode) {
|
|
52
|
+
group.remove(child)
|
|
53
|
+
} else {
|
|
54
|
+
contentGroup.remove(child)
|
|
55
|
+
}
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<slot
|
|
59
|
+
width={computedWidth}
|
|
60
|
+
height={computedHeight}
|
|
61
|
+
/>
|
|
62
|
+
</HierarchicalObject>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
import { Group } from 'three';
|
|
3
|
+
declare const __propDef: {
|
|
4
|
+
props: {
|
|
5
|
+
group?: Group | undefined;
|
|
6
|
+
contentGroup?: Group | undefined;
|
|
7
|
+
node?: any;
|
|
8
|
+
alignItems?: string | number | symbol | undefined;
|
|
9
|
+
alignSelf?: string | number | symbol | undefined;
|
|
10
|
+
alignContent?: string | number | symbol | undefined;
|
|
11
|
+
justifyContent?: string | number | symbol | undefined;
|
|
12
|
+
flexDirection?: string | number | symbol | undefined;
|
|
13
|
+
flexWrap?: string | number | symbol | undefined;
|
|
14
|
+
flex?: unknown;
|
|
15
|
+
flexBasis?: unknown;
|
|
16
|
+
flexGrow?: unknown;
|
|
17
|
+
flexShrink?: unknown;
|
|
18
|
+
height?: unknown;
|
|
19
|
+
width?: unknown;
|
|
20
|
+
maxHeight?: unknown;
|
|
21
|
+
maxWidth?: unknown;
|
|
22
|
+
minHeight?: unknown;
|
|
23
|
+
minWidth?: unknown;
|
|
24
|
+
top?: unknown;
|
|
25
|
+
right?: unknown;
|
|
26
|
+
bottom?: unknown;
|
|
27
|
+
left?: unknown;
|
|
28
|
+
padding?: unknown;
|
|
29
|
+
paddingTop?: unknown;
|
|
30
|
+
paddingRight?: unknown;
|
|
31
|
+
paddingBottom?: unknown;
|
|
32
|
+
paddingLeft?: unknown;
|
|
33
|
+
margin?: unknown;
|
|
34
|
+
marginTop?: unknown;
|
|
35
|
+
marginRight?: unknown;
|
|
36
|
+
marginBottom?: unknown;
|
|
37
|
+
marginLeft?: unknown;
|
|
38
|
+
gap?: unknown;
|
|
39
|
+
gapColumn?: unknown;
|
|
40
|
+
gapRow?: unknown;
|
|
41
|
+
aspectRatio?: unknown;
|
|
42
|
+
order?: number | undefined;
|
|
43
|
+
class?: string | undefined;
|
|
44
|
+
};
|
|
45
|
+
events: {
|
|
46
|
+
[evt: string]: CustomEvent<any>;
|
|
47
|
+
};
|
|
48
|
+
slots: {
|
|
49
|
+
default: {
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export type BoxProps = typeof __propDef.props;
|
|
56
|
+
export type BoxEvents = typeof __propDef.events;
|
|
57
|
+
export type BoxSlots = typeof __propDef.slots;
|
|
58
|
+
export default class Box extends SvelteComponentTyped<BoxProps, BoxEvents, BoxSlots> {
|
|
59
|
+
get group(): Group;
|
|
60
|
+
get contentGroup(): Group;
|
|
61
|
+
}
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script>import { forwardEventHandlers } from '@threlte/core';
|
|
2
|
+
import { loadYoga } from 'yoga-layout';
|
|
3
|
+
import InnerFlex from './InnerFlex.svelte';
|
|
4
|
+
let yoga;
|
|
5
|
+
const initialize = async () => {
|
|
6
|
+
yoga = await loadYoga();
|
|
7
|
+
};
|
|
8
|
+
initialize();
|
|
9
|
+
const component = forwardEventHandlers();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
{#if yoga}
|
|
13
|
+
<InnerFlex
|
|
14
|
+
{yoga}
|
|
15
|
+
{...$$restProps}
|
|
16
|
+
bind:this={$component}
|
|
17
|
+
let:reflow
|
|
18
|
+
>
|
|
19
|
+
<slot {reflow} />
|
|
20
|
+
</InnerFlex>
|
|
21
|
+
{/if}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: {
|
|
4
|
+
flex?: unknown;
|
|
5
|
+
height?: number | undefined;
|
|
6
|
+
width?: number | undefined;
|
|
7
|
+
left?: unknown;
|
|
8
|
+
top?: unknown;
|
|
9
|
+
alignContent?: string | number | symbol | undefined;
|
|
10
|
+
alignItems?: string | number | symbol | undefined;
|
|
11
|
+
alignSelf?: string | number | symbol | undefined;
|
|
12
|
+
aspectRatio?: unknown;
|
|
13
|
+
bottom?: unknown;
|
|
14
|
+
direction?: string | number | symbol | undefined;
|
|
15
|
+
flexBasis?: unknown;
|
|
16
|
+
flexDirection?: string | number | symbol | undefined;
|
|
17
|
+
flexGrow?: unknown;
|
|
18
|
+
flexShrink?: unknown;
|
|
19
|
+
flexWrap?: string | number | symbol | undefined;
|
|
20
|
+
gap?: unknown;
|
|
21
|
+
justifyContent?: string | number | symbol | undefined;
|
|
22
|
+
margin?: unknown;
|
|
23
|
+
marginBottom?: unknown;
|
|
24
|
+
marginLeft?: unknown;
|
|
25
|
+
marginRight?: unknown;
|
|
26
|
+
marginTop?: unknown;
|
|
27
|
+
maxHeight?: unknown;
|
|
28
|
+
maxWidth?: unknown;
|
|
29
|
+
minHeight?: unknown;
|
|
30
|
+
minWidth?: unknown;
|
|
31
|
+
padding?: unknown;
|
|
32
|
+
paddingBottom?: unknown;
|
|
33
|
+
paddingLeft?: unknown;
|
|
34
|
+
paddingRight?: unknown;
|
|
35
|
+
paddingTop?: unknown;
|
|
36
|
+
right?: unknown;
|
|
37
|
+
class?: string | undefined;
|
|
38
|
+
plane?: import("../lib/props").FlexPlane | undefined;
|
|
39
|
+
gapColumn?: unknown;
|
|
40
|
+
gapRow?: unknown;
|
|
41
|
+
scaleFactor?: number | undefined;
|
|
42
|
+
classParser?: import("../lib/props").ClassParser | undefined;
|
|
43
|
+
};
|
|
44
|
+
slots: {
|
|
45
|
+
default: {
|
|
46
|
+
reflow: () => void;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
events: {
|
|
50
|
+
reflow: {
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export type FlexProps = typeof __propDef.props;
|
|
57
|
+
export type FlexEvents = typeof __propDef.events;
|
|
58
|
+
export type FlexSlots = typeof __propDef.slots;
|
|
59
|
+
export default class Flex extends SvelteComponentTyped<FlexProps, FlexEvents, FlexSlots> {
|
|
60
|
+
}
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<script>import { T, createRawEventDispatcher, currentWritable, useFrame } from '@threlte/core';
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import { Box3, Group, Vector3 } from 'three';
|
|
4
|
+
import { Direction } from 'yoga-layout';
|
|
5
|
+
import { getDepthAxis } from '../lib/getDepthAxis';
|
|
6
|
+
import { getOrientedBoundingBoxSize } from '../lib/getOrientedBoundingBoxSize';
|
|
7
|
+
import { getRootShift } from '../lib/getRootShift';
|
|
8
|
+
import { applyNodeProps } from '../lib/props';
|
|
9
|
+
import { createNodeContext } from '../nodes/context';
|
|
10
|
+
import { createFlexContext } from './context';
|
|
11
|
+
export let yoga;
|
|
12
|
+
export let width = 1;
|
|
13
|
+
export let height = 1;
|
|
14
|
+
export let plane = 'xy';
|
|
15
|
+
export let direction = 'LTR';
|
|
16
|
+
export let scaleFactor = 1000;
|
|
17
|
+
export let classParser = undefined;
|
|
18
|
+
let _class = '';
|
|
19
|
+
export { _class as class };
|
|
20
|
+
const dispatch = createRawEventDispatcher();
|
|
21
|
+
const rootGroup = new Group();
|
|
22
|
+
rootGroup.userData.isNode = true;
|
|
23
|
+
const boundingBox = new Box3();
|
|
24
|
+
const vec3 = new Vector3();
|
|
25
|
+
/**
|
|
26
|
+
* Reflowing inside useFrame automatically batches reflows to 1 per frame.
|
|
27
|
+
*/
|
|
28
|
+
const { start: reflow, stop } = useFrame(() => {
|
|
29
|
+
flexContext.emit('reflow:before');
|
|
30
|
+
for (const { node, group, props } of flexContext.nodes.values()) {
|
|
31
|
+
const scaledWidth = typeof props.width === 'number' ? props.width * scaleFactor : props.width;
|
|
32
|
+
const scaledHeight = typeof props.height === 'number' ? props.height * scaleFactor : props.height;
|
|
33
|
+
if (scaledWidth !== undefined && scaledHeight !== undefined) {
|
|
34
|
+
// Forced size, no need to calculate bounding box
|
|
35
|
+
node.setWidth(scaledWidth);
|
|
36
|
+
node.setHeight(scaledHeight);
|
|
37
|
+
}
|
|
38
|
+
else if (node.getChildCount() === 0) {
|
|
39
|
+
// No size specified, calculate size
|
|
40
|
+
if (rootGroup) {
|
|
41
|
+
getOrientedBoundingBoxSize(group, rootGroup, boundingBox, vec3);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// rootGroup ref is missing for some reason, let's just use usual bounding box
|
|
45
|
+
boundingBox.setFromObject(group).getSize(vec3);
|
|
46
|
+
}
|
|
47
|
+
node.setWidth(scaledWidth || vec3[$mainAxis] * scaleFactor);
|
|
48
|
+
node.setHeight(scaledHeight || vec3[$crossAxis] * scaleFactor);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
rootNode.calculateLayout(width * scaleFactor, height * scaleFactor, Direction[direction]);
|
|
52
|
+
const rootWidth = rootNode.getComputedWidth();
|
|
53
|
+
const rootHeight = rootNode.getComputedHeight();
|
|
54
|
+
let minX = 0;
|
|
55
|
+
let maxX = 0;
|
|
56
|
+
let minY = 0;
|
|
57
|
+
let maxY = 0;
|
|
58
|
+
// Reposition after recalculation
|
|
59
|
+
for (const { node, group } of flexContext.nodes.values()) {
|
|
60
|
+
const { left, top, width, height } = node.getComputedLayout();
|
|
61
|
+
const [mainAxisShift, crossAxisShift] = getRootShift(rootWidth, rootHeight, node);
|
|
62
|
+
group.position[$mainAxis] = (mainAxisShift + left) / scaleFactor;
|
|
63
|
+
group.position[$crossAxis] = -(crossAxisShift + top) / scaleFactor;
|
|
64
|
+
group.position[$depthAxis] = 0;
|
|
65
|
+
minX = Math.min(minX, left);
|
|
66
|
+
minY = Math.min(minY, top);
|
|
67
|
+
maxX = Math.max(maxX, left + width);
|
|
68
|
+
maxY = Math.max(maxY, top + height);
|
|
69
|
+
}
|
|
70
|
+
flexContext.emit('reflow:after');
|
|
71
|
+
dispatch('reflow', {
|
|
72
|
+
width: (maxX - minX) / scaleFactor,
|
|
73
|
+
height: (maxY - minY) / scaleFactor
|
|
74
|
+
});
|
|
75
|
+
stop();
|
|
76
|
+
}, { autostart: false });
|
|
77
|
+
const flexContext = createFlexContext({
|
|
78
|
+
yoga,
|
|
79
|
+
nodes: new Map(),
|
|
80
|
+
addNode(node, group, props) {
|
|
81
|
+
flexContext.nodes.set(node, { node, group, props });
|
|
82
|
+
reflow();
|
|
83
|
+
},
|
|
84
|
+
updateNodeProps(node, props, force = false) {
|
|
85
|
+
const nodeData = flexContext.nodes.get(node);
|
|
86
|
+
if (!nodeData)
|
|
87
|
+
return;
|
|
88
|
+
// 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
|
+
}
|
|
101
|
+
}
|
|
102
|
+
applyNodeProps(node, props, scaleFactor);
|
|
103
|
+
nodeData.props = props;
|
|
104
|
+
reflow();
|
|
105
|
+
},
|
|
106
|
+
removeNode(node) {
|
|
107
|
+
flexContext.nodes.delete(node);
|
|
108
|
+
reflow();
|
|
109
|
+
},
|
|
110
|
+
rootWidth: currentWritable(width),
|
|
111
|
+
rootHeight: currentWritable(height),
|
|
112
|
+
scaleFactor: currentWritable(scaleFactor ?? 1000),
|
|
113
|
+
mainAxis: currentWritable(plane[0]),
|
|
114
|
+
crossAxis: currentWritable(plane[1]),
|
|
115
|
+
depthAxis: currentWritable(getDepthAxis(plane)),
|
|
116
|
+
rootGroup: rootGroup,
|
|
117
|
+
reflow,
|
|
118
|
+
classParser
|
|
119
|
+
});
|
|
120
|
+
const { mainAxis, crossAxis, depthAxis } = flexContext;
|
|
121
|
+
const { node: rootNode } = createNodeContext();
|
|
122
|
+
$: rootNode.setWidth(width * scaleFactor), rootNode.setHeight(height * scaleFactor);
|
|
123
|
+
$: applyNodeProps(rootNode, { ...classParser?.(_class), ...$$restProps }, scaleFactor), reflow();
|
|
124
|
+
$: flexContext.rootWidth.set(width), flexContext.reflow('Updated root width');
|
|
125
|
+
$: flexContext.rootHeight.set(height), flexContext.reflow('Updated root height');
|
|
126
|
+
$: flexContext.mainAxis.set(plane[0]), flexContext.reflow('Updated main axis');
|
|
127
|
+
$: flexContext.crossAxis.set(plane[1]), flexContext.reflow('Updated cross axis');
|
|
128
|
+
$: flexContext.depthAxis.set(getDepthAxis(plane)), flexContext.reflow('Updated depth axis');
|
|
129
|
+
$: flexContext.scaleFactor.set(scaleFactor), flexContext.reflow('Updated scale factor');
|
|
130
|
+
onDestroy(() => {
|
|
131
|
+
rootNode.free();
|
|
132
|
+
});
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<T is={rootGroup}>
|
|
136
|
+
<slot {reflow} />
|
|
137
|
+
</T>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
import { type ClassParser, type FlexPlane } from '../lib/props';
|
|
3
|
+
declare const __propDef: {
|
|
4
|
+
props: {
|
|
5
|
+
alignItems?: string | number | symbol | undefined;
|
|
6
|
+
alignSelf?: string | number | symbol | undefined;
|
|
7
|
+
alignContent?: string | number | symbol | undefined;
|
|
8
|
+
justifyContent?: string | number | symbol | undefined;
|
|
9
|
+
flexDirection?: string | number | symbol | undefined;
|
|
10
|
+
flexWrap?: string | number | symbol | undefined;
|
|
11
|
+
flex?: unknown;
|
|
12
|
+
flexBasis?: unknown;
|
|
13
|
+
flexGrow?: unknown;
|
|
14
|
+
flexShrink?: unknown;
|
|
15
|
+
height?: number | undefined;
|
|
16
|
+
width?: number | undefined;
|
|
17
|
+
maxHeight?: unknown;
|
|
18
|
+
maxWidth?: unknown;
|
|
19
|
+
minHeight?: unknown;
|
|
20
|
+
minWidth?: unknown;
|
|
21
|
+
top?: unknown;
|
|
22
|
+
right?: unknown;
|
|
23
|
+
bottom?: unknown;
|
|
24
|
+
left?: unknown;
|
|
25
|
+
padding?: unknown;
|
|
26
|
+
paddingTop?: unknown;
|
|
27
|
+
paddingRight?: unknown;
|
|
28
|
+
paddingBottom?: unknown;
|
|
29
|
+
paddingLeft?: unknown;
|
|
30
|
+
margin?: unknown;
|
|
31
|
+
marginTop?: unknown;
|
|
32
|
+
marginRight?: unknown;
|
|
33
|
+
marginBottom?: unknown;
|
|
34
|
+
marginLeft?: unknown;
|
|
35
|
+
gap?: unknown;
|
|
36
|
+
gapColumn?: unknown;
|
|
37
|
+
gapRow?: unknown;
|
|
38
|
+
aspectRatio?: unknown;
|
|
39
|
+
yoga: Yoga;
|
|
40
|
+
plane?: FlexPlane | undefined;
|
|
41
|
+
direction?: string | number | symbol | undefined;
|
|
42
|
+
scaleFactor?: number | undefined;
|
|
43
|
+
class?: string | undefined;
|
|
44
|
+
classParser?: ClassParser | undefined;
|
|
45
|
+
};
|
|
46
|
+
slots: {
|
|
47
|
+
default: {
|
|
48
|
+
reflow: () => void;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
events: {
|
|
52
|
+
reflow: {
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export type InnerFlexProps = typeof __propDef.props;
|
|
59
|
+
export type InnerFlexEvents = typeof __propDef.events;
|
|
60
|
+
export type InnerFlexSlots = typeof __propDef.slots;
|
|
61
|
+
export default class InnerFlex extends SvelteComponentTyped<InnerFlexProps, InnerFlexEvents, InnerFlexSlots> {
|
|
62
|
+
}
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CurrentWritable } from '@threlte/core';
|
|
2
|
+
import { type Emitter } from 'mitt';
|
|
3
|
+
import type { Group } from 'three';
|
|
4
|
+
import type { Node, Yoga } from 'yoga-layout';
|
|
5
|
+
import type { Axis, ClassParser, NodeProps } from '../lib/props';
|
|
6
|
+
type FlexContextEvents = {
|
|
7
|
+
'reflow:before': void;
|
|
8
|
+
'reflow:after': void;
|
|
9
|
+
};
|
|
10
|
+
type FlexContextNode = {
|
|
11
|
+
node: Node;
|
|
12
|
+
group: THREE.Group;
|
|
13
|
+
props: NodeProps;
|
|
14
|
+
};
|
|
15
|
+
export type FlexContextData = {
|
|
16
|
+
yoga: Yoga;
|
|
17
|
+
nodes: Map<Node, FlexContextNode>;
|
|
18
|
+
addNode: (node: Node, group: Group, props: NodeProps) => void;
|
|
19
|
+
updateNodeProps: (node: Node, props: NodeProps, force?: boolean) => void;
|
|
20
|
+
removeNode: (node: Node) => void;
|
|
21
|
+
scaleFactor: CurrentWritable<number>;
|
|
22
|
+
mainAxis: CurrentWritable<Axis>;
|
|
23
|
+
crossAxis: CurrentWritable<Axis>;
|
|
24
|
+
depthAxis: CurrentWritable<Axis>;
|
|
25
|
+
rootGroup: Group;
|
|
26
|
+
rootWidth: CurrentWritable<number>;
|
|
27
|
+
rootHeight: CurrentWritable<number>;
|
|
28
|
+
reflow: (msg?: string) => void;
|
|
29
|
+
classParser?: ClassParser;
|
|
30
|
+
};
|
|
31
|
+
export type FlexContext = FlexContextData & Emitter<FlexContextEvents> & {
|
|
32
|
+
onEvent: <Type extends keyof FlexContextEvents>(type: Type, callback: (payload: FlexContextEvents[Type]) => void) => void;
|
|
33
|
+
};
|
|
34
|
+
export declare const flexContextName = "__threlte-flex";
|
|
35
|
+
export declare const createFlexContext: (data: FlexContextData) => FlexContextData & Emitter<FlexContextEvents> & {
|
|
36
|
+
onEvent: <Type extends keyof FlexContextEvents>(type: Type, callback: (payload: FlexContextEvents[Type]) => void) => void;
|
|
37
|
+
};
|
|
38
|
+
export declare const useFlex: () => FlexContext;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import mitt, {} from 'mitt';
|
|
2
|
+
import { getContext, onDestroy, setContext } from 'svelte';
|
|
3
|
+
export const flexContextName = '__threlte-flex';
|
|
4
|
+
export const createFlexContext = (data) => {
|
|
5
|
+
const emitter = mitt();
|
|
6
|
+
const eventCallback = {
|
|
7
|
+
onEvent: (type, callback) => {
|
|
8
|
+
emitter.on(type, callback);
|
|
9
|
+
onDestroy(() => emitter.off(type, callback));
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const context = Object.assign(data, emitter, eventCallback);
|
|
13
|
+
setContext(flexContextName, context);
|
|
14
|
+
return context;
|
|
15
|
+
};
|
|
16
|
+
export const useFlex = () => {
|
|
17
|
+
return getContext(flexContextName);
|
|
18
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as Box } from './Box/Box.svelte';
|
|
2
|
+
export { default as Flex } from './Flex/Flex.svelte';
|
|
3
|
+
export type { NodeProps } from './lib/props';
|
|
4
|
+
export { createClassParser } from './parsers/createClassParser';
|
|
5
|
+
export { tailwindParser } from './parsers/tailwindParser';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NodeProps } from './props';
|
|
2
|
+
/**
|
|
3
|
+
* This function will align the flex props to the web-standard.
|
|
4
|
+
*
|
|
5
|
+
* yoga-layout uses `Column` as the default flex direction, but we
|
|
6
|
+
* want to use `Row` as the default (web-standard). This function will return
|
|
7
|
+
* the props with the default flex direction applied if it is not already set.
|
|
8
|
+
*/
|
|
9
|
+
export declare const alignFlexProps: (props: NodeProps) => NodeProps;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This function will align the flex props to the web-standard.
|
|
3
|
+
*
|
|
4
|
+
* yoga-layout uses `Column` as the default flex direction, but we
|
|
5
|
+
* want to use `Row` as the default (web-standard). This function will return
|
|
6
|
+
* the props with the default flex direction applied if it is not already set.
|
|
7
|
+
*/
|
|
8
|
+
export const alignFlexProps = (props) => {
|
|
9
|
+
return { flexDirection: 'Row', ...props };
|
|
10
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Box3, type Object3D, type Vector3 } from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* Adapted code from https://github.com/mrdoob/three.js/issues/11967
|
|
4
|
+
* Calculates oriented bounding box size
|
|
5
|
+
* Essentially it negates flex root rotation to provide proper number
|
|
6
|
+
* E.g. if root flex group rotatet 45 degress, a cube box of size 1 will report sizes of sqrt(2)
|
|
7
|
+
* but it should still be 1
|
|
8
|
+
*
|
|
9
|
+
* NB: This doesn't work when object itself is rotated (well, for now)
|
|
10
|
+
*/
|
|
11
|
+
export declare const getOrientedBoundingBoxSize: (object: Object3D, root: Object3D, bb: Box3, size: Vector3) => void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Matrix4 } from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* Adapted code from https://github.com/mrdoob/three.js/issues/11967
|
|
4
|
+
* Calculates oriented bounding box size
|
|
5
|
+
* Essentially it negates flex root rotation to provide proper number
|
|
6
|
+
* E.g. if root flex group rotatet 45 degress, a cube box of size 1 will report sizes of sqrt(2)
|
|
7
|
+
* but it should still be 1
|
|
8
|
+
*
|
|
9
|
+
* NB: This doesn't work when object itself is rotated (well, for now)
|
|
10
|
+
*/
|
|
11
|
+
export const getOrientedBoundingBoxSize = (object, root, bb, size) => {
|
|
12
|
+
object.updateMatrix();
|
|
13
|
+
const oldMatrix = object.matrix;
|
|
14
|
+
const oldMatrixAutoUpdate = object.matrixAutoUpdate;
|
|
15
|
+
root.updateMatrixWorld();
|
|
16
|
+
const m = new Matrix4().copy(root.matrixWorld).invert();
|
|
17
|
+
object.matrix = m;
|
|
18
|
+
// to prevent matrix being reassigned
|
|
19
|
+
object.matrixAutoUpdate = false;
|
|
20
|
+
root.updateMatrixWorld();
|
|
21
|
+
bb.setFromObject(object).getSize(size);
|
|
22
|
+
object.matrix = oldMatrix;
|
|
23
|
+
object.matrixAutoUpdate = oldMatrixAutoUpdate;
|
|
24
|
+
root.updateMatrixWorld();
|
|
25
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { isTopLevelChildNode } from './isTopLevelChildNode';
|
|
2
|
+
/** @returns [mainAxisShift, crossAxisShift] */
|
|
3
|
+
export const getRootShift = (rootWidth, rootHeight, node) => {
|
|
4
|
+
if (!isTopLevelChildNode(node)) {
|
|
5
|
+
return [0, 0];
|
|
6
|
+
}
|
|
7
|
+
const mainAxisShift = -rootWidth / 2;
|
|
8
|
+
const crossAxisShift = -rootHeight / 2;
|
|
9
|
+
return [mainAxisShift, crossAxisShift];
|
|
10
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isTopLevelChildNode: (node: Node) => boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isTopLevelChildNode = (node) => !node.getParent()?.getParent();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Align, FlexDirection, Justify, Node, Wrap } from 'yoga-layout';
|
|
2
|
+
export type FlexPlane = 'xy' | 'yz' | 'xz';
|
|
3
|
+
export type ClassParser = (className: string) => NodeProps;
|
|
4
|
+
export type Axis = 'x' | 'y' | 'z';
|
|
5
|
+
/**
|
|
6
|
+
* This map provides the prop setters as well as the types for the props. The
|
|
7
|
+
* first input of a setter is used as the prop value. The second input is the
|
|
8
|
+
* node to apply the prop to. The value of a prop needs to be a primitive type,
|
|
9
|
+
* so that it can be trivially compared to the previous value. This is used to
|
|
10
|
+
* prevent unnecessary reflows.
|
|
11
|
+
*/
|
|
12
|
+
export declare const propSetter: {
|
|
13
|
+
alignItems: (align: keyof typeof Align, node: Node) => any;
|
|
14
|
+
alignSelf: (align: keyof typeof Align, node: Node) => any;
|
|
15
|
+
alignContent: (align: keyof typeof Align, node: Node) => any;
|
|
16
|
+
justifyContent: (justify: keyof typeof Justify, node: Node) => any;
|
|
17
|
+
flexDirection: (dir: keyof typeof FlexDirection, node: Node) => any;
|
|
18
|
+
flexWrap: (wrap: keyof typeof Wrap, node: Node) => any;
|
|
19
|
+
flex: (flex: Parameters<Node['setFlex']>[0], node: Node) => any;
|
|
20
|
+
flexBasis: (basis: Parameters<Node['setFlexBasis']>[0], node: Node) => any;
|
|
21
|
+
flexGrow: (grow: Parameters<Node['setFlexGrow']>[0], node: Node) => any;
|
|
22
|
+
flexShrink: (shrink: Parameters<Node['setFlexShrink']>[0], node: Node) => any;
|
|
23
|
+
height: (height: Parameters<Node['setHeight']>[0], node: Node) => any;
|
|
24
|
+
width: (width: Parameters<Node['setWidth']>[0], node: Node) => any;
|
|
25
|
+
maxHeight: (maxHeight: Parameters<Node['setMaxHeight']>[0], node: Node) => any;
|
|
26
|
+
maxWidth: (maxWidth: Parameters<Node['setMaxWidth']>[0], node: Node) => any;
|
|
27
|
+
minHeight: (minHeight: Parameters<Node['setMinHeight']>[0], node: Node) => any;
|
|
28
|
+
minWidth: (minWidth: Parameters<Node['setMinWidth']>[0], node: Node) => any;
|
|
29
|
+
/** As of now, this won't work since the bounding box is still computed by nodes marked as absolutely positioned */
|
|
30
|
+
top: (top: Parameters<Node['setPosition']>[1], node: Node) => any;
|
|
31
|
+
right: (right: Parameters<Node['setPosition']>[1], node: Node) => any;
|
|
32
|
+
bottom: (bottom: Parameters<Node['setPosition']>[1], node: Node) => any;
|
|
33
|
+
left: (left: Parameters<Node['setPosition']>[1], node: Node) => any;
|
|
34
|
+
padding: (padding: Parameters<Node['setPadding']>[1], node: Node) => any;
|
|
35
|
+
paddingTop: (paddingTop: Parameters<Node['setPadding']>[1], node: Node) => any;
|
|
36
|
+
paddingRight: (paddingRight: Parameters<Node['setPadding']>[1], node: Node) => any;
|
|
37
|
+
paddingBottom: (paddingBottom: Parameters<Node['setPadding']>[1], node: Node) => any;
|
|
38
|
+
paddingLeft: (paddingLeft: Parameters<Node['setPadding']>[1], node: Node) => any;
|
|
39
|
+
margin: (margin: Parameters<Node['setMargin']>[1], node: Node) => any;
|
|
40
|
+
marginTop: (marginTop: Parameters<Node['setMargin']>[1], node: Node) => any;
|
|
41
|
+
marginRight: (marginRight: Parameters<Node['setMargin']>[1], node: Node) => any;
|
|
42
|
+
marginBottom: (marginBottom: Parameters<Node['setMargin']>[1], node: Node) => any;
|
|
43
|
+
marginLeft: (marginLeft: Parameters<Node['setMargin']>[1], node: Node) => any;
|
|
44
|
+
gap: (gap: Parameters<Node['setGap']>[1], node: Node) => any;
|
|
45
|
+
gapColumn: (gapColumn: Parameters<Node['setGap']>[1], node: Node) => any;
|
|
46
|
+
gapRow: (gapRow: Parameters<Node['setGap']>[1], node: Node) => any;
|
|
47
|
+
aspectRatio: (aspectRatio: Parameters<Node['setAspectRatio']>[0], node: Node) => any;
|
|
48
|
+
};
|
|
49
|
+
export type NodeProps = {
|
|
50
|
+
[Key in keyof typeof propSetter]?: Parameters<(typeof propSetter)[Key]>[0];
|
|
51
|
+
};
|
|
52
|
+
export declare const applyNodeProps: (node: Node, props: NodeProps, scaleFactor: number) => void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as Yoga from 'yoga-layout';
|
|
2
|
+
import { alignFlexProps } from './alignFlexProps';
|
|
3
|
+
// prettier-ignore
|
|
4
|
+
/**
|
|
5
|
+
* This map provides the prop setters as well as the types for the props. The
|
|
6
|
+
* first input of a setter is used as the prop value. The second input is the
|
|
7
|
+
* node to apply the prop to. The value of a prop needs to be a primitive type,
|
|
8
|
+
* so that it can be trivially compared to the previous value. This is used to
|
|
9
|
+
* prevent unnecessary reflows.
|
|
10
|
+
*/
|
|
11
|
+
export const propSetter = {
|
|
12
|
+
alignItems: (align, node) => node.setAlignItems(Yoga.Align[align]),
|
|
13
|
+
alignSelf: (align, node) => node.setAlignSelf(Yoga.Align[align]),
|
|
14
|
+
alignContent: (align, node) => node.setAlignContent(Yoga.Align[align]),
|
|
15
|
+
justifyContent: (justify, node) => node.setJustifyContent(Yoga.Justify[justify]),
|
|
16
|
+
flexDirection: (dir, node) => node.setFlexDirection(Yoga.FlexDirection[dir]),
|
|
17
|
+
flexWrap: (wrap, node) => node.setFlexWrap(Yoga.Wrap[wrap]),
|
|
18
|
+
flex: (flex, node) => node.setFlex(flex),
|
|
19
|
+
flexBasis: (basis, node) => node.setFlexBasis(basis),
|
|
20
|
+
flexGrow: (grow, node) => node.setFlexGrow(grow),
|
|
21
|
+
flexShrink: (shrink, node) => node.setFlexShrink(shrink),
|
|
22
|
+
height: (height, node) => node.setHeight(height),
|
|
23
|
+
width: (width, node) => node.setWidth(width),
|
|
24
|
+
maxHeight: (maxHeight, node) => node.setMaxHeight(maxHeight),
|
|
25
|
+
maxWidth: (maxWidth, node) => node.setMaxWidth(maxWidth),
|
|
26
|
+
minHeight: (minHeight, node) => node.setMinHeight(minHeight),
|
|
27
|
+
minWidth: (minWidth, node) => node.setMinWidth(minWidth),
|
|
28
|
+
/** As of now, this won't work since the bounding box is still computed by nodes marked as absolutely positioned */
|
|
29
|
+
// position: (positionType: keyof typeof PositionType, node: Node) => node.setPositionType(Yoga.PositionType[positionType]),
|
|
30
|
+
top: (top, node) => node.setPosition(Yoga.Edge.Top, top),
|
|
31
|
+
right: (right, node) => node.setPosition(Yoga.Edge.Right, right),
|
|
32
|
+
bottom: (bottom, node) => node.setPosition(Yoga.Edge.Bottom, bottom),
|
|
33
|
+
left: (left, node) => node.setPosition(Yoga.Edge.Left, left),
|
|
34
|
+
padding: (padding, node) => node.setPadding(Yoga.Edge.All, padding),
|
|
35
|
+
paddingTop: (paddingTop, node) => node.setPadding(Yoga.Edge.Top, paddingTop),
|
|
36
|
+
paddingRight: (paddingRight, node) => node.setPadding(Yoga.Edge.Right, paddingRight),
|
|
37
|
+
paddingBottom: (paddingBottom, node) => node.setPadding(Yoga.Edge.Bottom, paddingBottom),
|
|
38
|
+
paddingLeft: (paddingLeft, node) => node.setPadding(Yoga.Edge.Left, paddingLeft),
|
|
39
|
+
margin: (margin, node) => node.setMargin(Yoga.Edge.All, margin),
|
|
40
|
+
marginTop: (marginTop, node) => node.setMargin(Yoga.Edge.Top, marginTop),
|
|
41
|
+
marginRight: (marginRight, node) => node.setMargin(Yoga.Edge.Right, marginRight),
|
|
42
|
+
marginBottom: (marginBottom, node) => node.setMargin(Yoga.Edge.Bottom, marginBottom),
|
|
43
|
+
marginLeft: (marginLeft, node) => node.setMargin(Yoga.Edge.Left, marginLeft),
|
|
44
|
+
gap: (gap, node) => node.setGap(Yoga.Gutter.All, gap),
|
|
45
|
+
gapColumn: (gapColumn, node) => node.setGap(Yoga.Gutter.Column, gapColumn),
|
|
46
|
+
gapRow: (gapRow, node) => node.setGap(Yoga.Gutter.Row, gapRow),
|
|
47
|
+
aspectRatio: (aspectRatio, node) => node.setAspectRatio(aspectRatio),
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Applies scale factor to props that are numbers. This is used to scale the
|
|
51
|
+
* props to the current scale factor of the root node because yoga-layout
|
|
52
|
+
* is made to work with integer values.
|
|
53
|
+
*/
|
|
54
|
+
const applyScaleFactor = (prop, scaleFactor) => {
|
|
55
|
+
if (typeof prop === 'number') {
|
|
56
|
+
return prop * scaleFactor;
|
|
57
|
+
}
|
|
58
|
+
return prop;
|
|
59
|
+
};
|
|
60
|
+
export const applyNodeProps = (node, props, scaleFactor) => {
|
|
61
|
+
return Object.entries(alignFlexProps(props)).forEach(([key, value]) => {
|
|
62
|
+
const scaledValue = applyScaleFactor(value, scaleFactor);
|
|
63
|
+
propSetter[key](scaledValue, node);
|
|
64
|
+
});
|
|
65
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Node } from 'yoga-layout';
|
|
2
|
+
export type NodeContext = {
|
|
3
|
+
node: Node;
|
|
4
|
+
insertChild: (child: Node, order?: number) => void;
|
|
5
|
+
removeChild: (child: Node) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare const nodeContextName = "__threlte-node";
|
|
8
|
+
export declare const useNode: () => NodeContext;
|
|
9
|
+
export declare const createNodeContext: (order?: number) => NodeContext;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getContext, onDestroy, setContext } from 'svelte';
|
|
2
|
+
import { useFlex } from '../Flex/context';
|
|
3
|
+
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) {
|
|
18
|
+
if (order !== undefined) {
|
|
19
|
+
data.node.insertChild(child, order);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
data.node.insertChild(child, data.node.getChildCount());
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
removeChild(child) {
|
|
26
|
+
data.node.removeChild(child);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
setContext(nodeContextName, data);
|
|
30
|
+
return data;
|
|
31
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NodeProps } from '../lib/props';
|
|
2
|
+
/**
|
|
3
|
+
* This function is a type helper for creating a class parser. A class parser is
|
|
4
|
+
* a function that takes a class name and returns a NodeProps object. This can
|
|
5
|
+
* be used to create Tailwind-like class names that resolve to yoga-layout
|
|
6
|
+
* props.
|
|
7
|
+
*/
|
|
8
|
+
export declare const createClassParser: (callback: (className: string) => NodeProps) => (className: string) => NodeProps;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This function is a type helper for creating a class parser. A class parser is
|
|
3
|
+
* a function that takes a class name and returns a NodeProps object. This can
|
|
4
|
+
* be used to create Tailwind-like class names that resolve to yoga-layout
|
|
5
|
+
* props.
|
|
6
|
+
*/
|
|
7
|
+
export const createClassParser = (callback) => {
|
|
8
|
+
return callback;
|
|
9
|
+
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { createClassParser } from './createClassParser';
|
|
2
|
+
export const tailwindParser = createClassParser((string) => {
|
|
3
|
+
const styles = {};
|
|
4
|
+
const classes = string.split(' ').map((className) => className.trim());
|
|
5
|
+
const parseNumericOrAutoOrPercentageValue = (value) => {
|
|
6
|
+
if (value === 'auto') {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
else if (value.endsWith('%')) {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
else if (value === 'full') {
|
|
13
|
+
return '100%';
|
|
14
|
+
}
|
|
15
|
+
else if (value.match(/^\d+\/\d+$/)) {
|
|
16
|
+
const [width, height] = value.split('/');
|
|
17
|
+
const percentage = Math.round((Number(width) / Number(height)) * 100);
|
|
18
|
+
return `${percentage}%`;
|
|
19
|
+
}
|
|
20
|
+
return Number(value);
|
|
21
|
+
};
|
|
22
|
+
const parseNumericOrPercentageValue = (value) => {
|
|
23
|
+
if (value.endsWith('%')) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
else if (value === 'full') {
|
|
27
|
+
return '100%';
|
|
28
|
+
}
|
|
29
|
+
else if (value.match(/^\d+\/\d+$/)) {
|
|
30
|
+
const [width, height] = value.split('/');
|
|
31
|
+
const percentage = Math.round((Number(width) / Number(height)) * 100);
|
|
32
|
+
return `${percentage}%`;
|
|
33
|
+
}
|
|
34
|
+
return Number(value);
|
|
35
|
+
};
|
|
36
|
+
classes.forEach((className) => {
|
|
37
|
+
// padding
|
|
38
|
+
if (className.startsWith('p-')) {
|
|
39
|
+
const [, value] = className.split('-');
|
|
40
|
+
styles.padding = parseNumericOrPercentageValue(value);
|
|
41
|
+
}
|
|
42
|
+
if (className.startsWith('px-')) {
|
|
43
|
+
const [, value] = className.split('-');
|
|
44
|
+
styles.paddingLeft = parseNumericOrPercentageValue(value);
|
|
45
|
+
styles.paddingRight = parseNumericOrPercentageValue(value);
|
|
46
|
+
}
|
|
47
|
+
if (className.startsWith('py-')) {
|
|
48
|
+
const [, value] = className.split('-');
|
|
49
|
+
styles.paddingTop = parseNumericOrPercentageValue(value);
|
|
50
|
+
styles.paddingBottom = parseNumericOrPercentageValue(value);
|
|
51
|
+
}
|
|
52
|
+
if (className.startsWith('pt-')) {
|
|
53
|
+
const [, value] = className.split('-');
|
|
54
|
+
styles.paddingTop = parseNumericOrPercentageValue(value);
|
|
55
|
+
}
|
|
56
|
+
if (className.startsWith('pr-')) {
|
|
57
|
+
const [, value] = className.split('-');
|
|
58
|
+
styles.paddingRight = parseNumericOrPercentageValue(value);
|
|
59
|
+
}
|
|
60
|
+
if (className.startsWith('pb-')) {
|
|
61
|
+
const [, value] = className.split('-');
|
|
62
|
+
styles.paddingBottom = parseNumericOrPercentageValue(value);
|
|
63
|
+
}
|
|
64
|
+
if (className.startsWith('pl-')) {
|
|
65
|
+
const [, value] = className.split('-');
|
|
66
|
+
styles.paddingLeft = parseNumericOrPercentageValue(value);
|
|
67
|
+
}
|
|
68
|
+
// margin
|
|
69
|
+
if (className.startsWith('m-')) {
|
|
70
|
+
const [, value] = className.split('-');
|
|
71
|
+
styles.margin = parseNumericOrAutoOrPercentageValue(value);
|
|
72
|
+
}
|
|
73
|
+
if (className.startsWith('mx-')) {
|
|
74
|
+
const [, value] = className.split('-');
|
|
75
|
+
styles.marginLeft = parseNumericOrAutoOrPercentageValue(value);
|
|
76
|
+
styles.marginRight = parseNumericOrAutoOrPercentageValue(value);
|
|
77
|
+
}
|
|
78
|
+
if (className.startsWith('my-')) {
|
|
79
|
+
const [, value] = className.split('-');
|
|
80
|
+
styles.marginTop = parseNumericOrAutoOrPercentageValue(value);
|
|
81
|
+
styles.marginBottom = parseNumericOrAutoOrPercentageValue(value);
|
|
82
|
+
}
|
|
83
|
+
if (className.startsWith('mt-')) {
|
|
84
|
+
const [, value] = className.split('-');
|
|
85
|
+
styles.marginTop = parseNumericOrAutoOrPercentageValue(value);
|
|
86
|
+
}
|
|
87
|
+
if (className.startsWith('mr-')) {
|
|
88
|
+
const [, value] = className.split('-');
|
|
89
|
+
styles.marginRight = parseNumericOrAutoOrPercentageValue(value);
|
|
90
|
+
}
|
|
91
|
+
if (className.startsWith('mb-')) {
|
|
92
|
+
const [, value] = className.split('-');
|
|
93
|
+
styles.marginBottom = parseNumericOrAutoOrPercentageValue(value);
|
|
94
|
+
}
|
|
95
|
+
if (className.startsWith('ml-')) {
|
|
96
|
+
const [, value] = className.split('-');
|
|
97
|
+
styles.marginLeft = parseNumericOrAutoOrPercentageValue(value);
|
|
98
|
+
}
|
|
99
|
+
// width
|
|
100
|
+
if (className.startsWith('w-')) {
|
|
101
|
+
const [, value] = className.split('-');
|
|
102
|
+
styles.width = parseNumericOrAutoOrPercentageValue(value);
|
|
103
|
+
}
|
|
104
|
+
// height
|
|
105
|
+
if (className.startsWith('h-')) {
|
|
106
|
+
const [, value] = className.split('-');
|
|
107
|
+
styles.height = parseNumericOrAutoOrPercentageValue(value);
|
|
108
|
+
}
|
|
109
|
+
// flex-basis
|
|
110
|
+
if (className.startsWith('basis-')) {
|
|
111
|
+
const [, value] = className.split('-');
|
|
112
|
+
styles.flexBasis = parseNumericOrAutoOrPercentageValue(value);
|
|
113
|
+
}
|
|
114
|
+
// flex-grow
|
|
115
|
+
if (className.startsWith('grow-')) {
|
|
116
|
+
const [, value] = className.split('-');
|
|
117
|
+
styles.flexGrow = Number(value);
|
|
118
|
+
}
|
|
119
|
+
// flex-shrink
|
|
120
|
+
if (className.startsWith('shrink-')) {
|
|
121
|
+
const [, value] = className.split('-');
|
|
122
|
+
styles.flexShrink = Number(value);
|
|
123
|
+
}
|
|
124
|
+
// flex-direction
|
|
125
|
+
if (className.startsWith('flex-')) {
|
|
126
|
+
// first the flex direction
|
|
127
|
+
switch (className) {
|
|
128
|
+
case 'flex-row':
|
|
129
|
+
styles.flexDirection = 'Row';
|
|
130
|
+
case 'flex-row-reverse':
|
|
131
|
+
styles.flexDirection = 'RowReverse';
|
|
132
|
+
case 'flex-col':
|
|
133
|
+
styles.flexDirection = 'Column';
|
|
134
|
+
case 'flex-col-reverse':
|
|
135
|
+
styles.flexDirection = 'ColumnReverse';
|
|
136
|
+
// flex-wrap
|
|
137
|
+
case 'flex-wrap':
|
|
138
|
+
styles.flexWrap = 'Wrap';
|
|
139
|
+
case 'flex-wrap-reverse':
|
|
140
|
+
styles.flexWrap = 'WrapReverse';
|
|
141
|
+
case 'flex-nowrap':
|
|
142
|
+
styles.flexWrap = 'NoWrap';
|
|
143
|
+
default:
|
|
144
|
+
// flex shorthand
|
|
145
|
+
const [, value] = className.split('-');
|
|
146
|
+
styles.flex = Number(value);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// justify-content
|
|
150
|
+
if (className.startsWith('justify-')) {
|
|
151
|
+
switch (className) {
|
|
152
|
+
case 'justify-start':
|
|
153
|
+
styles.justifyContent = 'FlexStart';
|
|
154
|
+
case 'justify-end':
|
|
155
|
+
styles.justifyContent = 'FlexEnd';
|
|
156
|
+
case 'justify-center':
|
|
157
|
+
styles.justifyContent = 'Center';
|
|
158
|
+
case 'justify-between':
|
|
159
|
+
styles.justifyContent = 'SpaceBetween';
|
|
160
|
+
case 'justify-around':
|
|
161
|
+
styles.justifyContent = 'SpaceAround';
|
|
162
|
+
case 'justify-evenly':
|
|
163
|
+
styles.justifyContent = 'SpaceEvenly';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// align-items
|
|
167
|
+
if (className.startsWith('items-')) {
|
|
168
|
+
switch (className) {
|
|
169
|
+
case 'items-start':
|
|
170
|
+
styles.alignItems = 'FlexStart';
|
|
171
|
+
case 'items-end':
|
|
172
|
+
styles.alignItems = 'FlexEnd';
|
|
173
|
+
case 'items-center':
|
|
174
|
+
styles.alignItems = 'Center';
|
|
175
|
+
case 'items-baseline':
|
|
176
|
+
styles.alignItems = 'Baseline';
|
|
177
|
+
case 'items-stretch':
|
|
178
|
+
styles.alignItems = 'Stretch';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// align-content
|
|
182
|
+
if (className.startsWith('content-')) {
|
|
183
|
+
switch (className) {
|
|
184
|
+
case 'content-normal':
|
|
185
|
+
styles.alignContent = 'Auto';
|
|
186
|
+
case 'content-start':
|
|
187
|
+
styles.alignContent = 'FlexStart';
|
|
188
|
+
case 'content-end':
|
|
189
|
+
styles.alignContent = 'FlexEnd';
|
|
190
|
+
case 'content-center':
|
|
191
|
+
styles.alignContent = 'Center';
|
|
192
|
+
case 'content-between':
|
|
193
|
+
styles.alignContent = 'SpaceBetween';
|
|
194
|
+
case 'content-around':
|
|
195
|
+
styles.alignContent = 'SpaceAround';
|
|
196
|
+
case 'content-stretch':
|
|
197
|
+
styles.alignContent = 'Stretch';
|
|
198
|
+
case 'content-baseline':
|
|
199
|
+
styles.alignContent = 'Baseline';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// align-self
|
|
203
|
+
if (className.startsWith('self-')) {
|
|
204
|
+
switch (className) {
|
|
205
|
+
case 'self-auto':
|
|
206
|
+
styles.alignSelf = 'Auto';
|
|
207
|
+
case 'self-start':
|
|
208
|
+
styles.alignSelf = 'FlexStart';
|
|
209
|
+
case 'self-end':
|
|
210
|
+
styles.alignSelf = 'FlexEnd';
|
|
211
|
+
case 'self-center':
|
|
212
|
+
styles.alignSelf = 'Center';
|
|
213
|
+
case 'self-stretch':
|
|
214
|
+
styles.alignSelf = 'Stretch';
|
|
215
|
+
case 'self-baseline':
|
|
216
|
+
styles.alignSelf = 'Baseline';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Gaps
|
|
220
|
+
if (className.startsWith('gap-x-')) {
|
|
221
|
+
const [, value] = className.split('-');
|
|
222
|
+
styles.gapColumn = Number(value);
|
|
223
|
+
}
|
|
224
|
+
else if (className.startsWith('gap-y-')) {
|
|
225
|
+
const [, value] = className.split('-');
|
|
226
|
+
styles.gapRow = Number(value);
|
|
227
|
+
}
|
|
228
|
+
else if (className.startsWith('gap-')) {
|
|
229
|
+
const [, value] = className.split('-');
|
|
230
|
+
styles.gap = Number(value);
|
|
231
|
+
}
|
|
232
|
+
// Position
|
|
233
|
+
if (className.startsWith('top-')) {
|
|
234
|
+
const [, value] = className.split('-');
|
|
235
|
+
styles.top = parseNumericOrPercentageValue(value);
|
|
236
|
+
}
|
|
237
|
+
if (className.startsWith('right-')) {
|
|
238
|
+
const [, value] = className.split('-');
|
|
239
|
+
styles.right = parseNumericOrPercentageValue(value);
|
|
240
|
+
}
|
|
241
|
+
if (className.startsWith('bottom-')) {
|
|
242
|
+
const [, value] = className.split('-');
|
|
243
|
+
styles.bottom = parseNumericOrPercentageValue(value);
|
|
244
|
+
}
|
|
245
|
+
if (className.startsWith('left-')) {
|
|
246
|
+
const [, value] = className.split('-');
|
|
247
|
+
styles.left = parseNumericOrPercentageValue(value);
|
|
248
|
+
}
|
|
249
|
+
// aspect ratio
|
|
250
|
+
if (className.startsWith('aspect-')) {
|
|
251
|
+
switch (className) {
|
|
252
|
+
case 'aspect-square':
|
|
253
|
+
styles.aspectRatio = 1;
|
|
254
|
+
case 'aspect-landscape':
|
|
255
|
+
styles.aspectRatio = 16 / 9;
|
|
256
|
+
case 'aspect-portrait':
|
|
257
|
+
styles.aspectRatio = 9 / 16;
|
|
258
|
+
default:
|
|
259
|
+
const [, value] = className.split('-');
|
|
260
|
+
const [width, height] = value.split('/');
|
|
261
|
+
styles.aspectRatio = Number(width) / Number(height);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return styles;
|
|
266
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const yoga: import("@threlte/core").CurrentWritable<any>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { currentWritable } from '@threlte/core';
|
|
2
|
+
import { loadYoga } from 'yoga-layout';
|
|
3
|
+
let loading = false;
|
|
4
|
+
let loaded = false;
|
|
5
|
+
export const yoga = currentWritable(undefined);
|
|
6
|
+
const load = async () => {
|
|
7
|
+
if (loading || loaded)
|
|
8
|
+
return;
|
|
9
|
+
const yogaInstance = await loadYoga();
|
|
10
|
+
yoga.set(yogaInstance);
|
|
11
|
+
};
|
|
12
|
+
load();
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@threlte/flex",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"author": "Grischa Erbe <hello@legrisch.com> (https://legrisch.com)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@sveltejs/adapter-auto": "^2.0.0",
|
|
8
|
+
"@sveltejs/kit": "^1.20.4",
|
|
9
|
+
"@sveltejs/package": "^2.1.0",
|
|
10
|
+
"@types/node": "^18.0.3",
|
|
11
|
+
"@types/three": "^0.155.1",
|
|
12
|
+
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
|
13
|
+
"@typescript-eslint/parser": "^5.45.0",
|
|
14
|
+
"@yushijinhun/three-minifier-rollup": "^0.3.1",
|
|
15
|
+
"eslint": "^8.28.0",
|
|
16
|
+
"eslint-config-prettier": "^8.5.0",
|
|
17
|
+
"eslint-plugin-svelte": "^2.30.0",
|
|
18
|
+
"prettier": "^2.8.8",
|
|
19
|
+
"prettier-plugin-svelte": "^2.10.1",
|
|
20
|
+
"publint": "^0.1.12",
|
|
21
|
+
"rimraf": "^5.0.1",
|
|
22
|
+
"svelte": "^4.1.1",
|
|
23
|
+
"svelte-check": "^3.4.3",
|
|
24
|
+
"svelte-preprocess": "^5.0.4",
|
|
25
|
+
"svelte2tsx": "^0.6.19",
|
|
26
|
+
"three": "^0.155.0",
|
|
27
|
+
"tslib": "^2.4.1",
|
|
28
|
+
"typescript": "^5.0.0",
|
|
29
|
+
"vite": "^4.3.6",
|
|
30
|
+
"@threlte/core": "6.0.8",
|
|
31
|
+
"@threlte/extras": "5.6.4"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"mitt": "^3.0.1",
|
|
35
|
+
"yoga-layout": "^2.0.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"svelte": ">=4",
|
|
39
|
+
"three": ">=0.133"
|
|
40
|
+
},
|
|
41
|
+
"type": "module",
|
|
42
|
+
"exports": {
|
|
43
|
+
".": {
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"svelte": "./dist/index.js"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"types": "./dist/index.d.ts",
|
|
49
|
+
"svelte": "./dist/index.js",
|
|
50
|
+
"files": [
|
|
51
|
+
"dist"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"dev": "vite dev",
|
|
55
|
+
"package": "svelte-kit sync && svelte-package && node ./scripts/cleanupPackage.js && publint",
|
|
56
|
+
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
57
|
+
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
|
58
|
+
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
|
59
|
+
"format": "prettier --write --plugin-search-dir=. .",
|
|
60
|
+
"cleanup": "rimraf node_modules .svelte-kit dist"
|
|
61
|
+
}
|
|
62
|
+
}
|