diff --git a/README.md b/README.md index f2903c0b..6d116bfc 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,17 @@ Build performant 3D user interfaces for Three.js using @react-three/fiber and yo TODO Release -- drag/click threshold -- add shadcn components -- cli for kits -- add apfel components -- Content "measureContent" flag => allow disabling content measuring and scaling -- support for visibility="hidden" -- input -- decrease clipping rect when scrollbar present +- feat: nesting inside non root/container components (e.g. image) +- feat: support more characters for different languages +- fix: always loading normal font +- fix: scrollbar border radius to high (happens with very long panels) +- feat: drag/click threshold +- feat: cli for kits +- feat: add apfel components +- feat: Content "measureContent" flag => allow disabling content measuring and scaling +- feat: support for visibility="hidden" +- feat: input +- fix: decrease clipping rect when scrollbar present TODO Later diff --git a/docs/getting-started/basic-elements.md b/docs/getting-started/components-and-properties.md similarity index 100% rename from docs/getting-started/basic-elements.md rename to docs/getting-started/components-and-properties.md diff --git a/docs/getting-started/first-layout.md b/docs/getting-started/first-layout.md index e69de29b..f17662a5 100644 --- a/docs/getting-started/first-layout.md +++ b/docs/getting-started/first-layout.md @@ -0,0 +1,28 @@ +import Image from '@theme/IdealImage'; +import { CodesandboxEmbed } from '../CodesandboxEmbed.tsx' + +# First Layout + +At first, we will create 3 containers. One container is the root node with a size of 2 by 1 three.js untits, expressed by `Root`. The `Root` has a horizontal (row) flex-direction, while the children expressed by `Container` equally fill its width with a margin between them. + + + + + +```tsx +import { Canvas } from "@react-three/fiber"; +import { OrbitControls } from "@react-three/drei"; +import { Root, Container } from "@react-three/uikit"; + +export default function App() { + return ( + + + + + + + + ); +} +``` diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md index ec8f5ffa..bfd44ae2 100644 --- a/docs/getting-started/introduction.md +++ b/docs/getting-started/introduction.md @@ -46,14 +46,21 @@ createRoot(document.getElementById('root')).render( The tutorials expect some level of familarity with react, threejs, and @react-three/fiber. -1. Build your [First Layout]() -2. Learn about the [Available Components and Their Properties]() -3. Get inspired by our [Examples]() +1. Build your [First Layout](./first-layout.md) +2. Learn about the [Available Components and Their Properties](./components-and-properties.md) +3. Get inspired by our [Examples](./examples.md) 4. Learn more about -- Using [Custom Materials]() -- Using [Custom Fonts]() -- Creating [Responsivene User Interfaces]() -- [Scrolling]() -- [Sizing]() +- Using [Custom Materials](../tutorials/custom-materials.mdx) +- Using [Custom Fonts](../tutorials/fonts.mdx) +- Creating [Responsivene User Interfaces](../tutorials/responsive.mdx) +- [Scrolling](../tutorials/scroll.mdx) +- [Sizing](../tutorials/sizing.mdx +) 5. Learn about [Common Pitfalls]() and how to [Optimize Performance]() +## Migration guides + +- from [Koestlich](../migration/from-koestlich.mdx) +- from HTML/CSS +- from Tailwind + diff --git a/examples/uikit/src/App.tsx b/examples/uikit/src/App.tsx index cb62ee28..80313eb5 100644 --- a/examples/uikit/src/App.tsx +++ b/examples/uikit/src/App.tsx @@ -56,6 +56,7 @@ export default function App() { cursor="pointer" > {t} + more (x.value = hover ? 'yellow' : undefined)} @@ -70,7 +71,16 @@ export default function App() { - console.log(w, h)}> + console.log(w, h)} + keepAspectRatio={false} + borderRight={100} + > diff --git a/examples/uikit/vite.config.ts b/examples/uikit/vite.config.ts index 5eb10be6..9a823cca 100644 --- a/examples/uikit/vite.config.ts +++ b/examples/uikit/vite.config.ts @@ -5,9 +5,6 @@ import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], - optimizeDeps: { - include: ['@react-three/uikit-lucide', '@react-three/uikit'], - }, resolve: { alias: [ { find: '@', replacement: path.resolve(__dirname, '../../packages/kits/default') }, diff --git a/packages/uikit/src/components/content.tsx b/packages/uikit/src/components/content.tsx index 98572b53..0f669ea0 100644 --- a/packages/uikit/src/components/content.tsx +++ b/packages/uikit/src/components/content.tsx @@ -13,7 +13,7 @@ import { } from '../properties/utils.js' import { alignmentZMap, fitNormalizedContentInside, useRootGroupRef, useSignalEffect } from '../utils.js' import { Box3, Group, Mesh, Vector3 } from 'three' -import { effect, Signal, signal } from '@preact/signals-core' +import { computed, effect, Signal, signal } from '@preact/signals-core' import { useApplyHoverProperties } from '../hover.js' import { ComponentInternals, @@ -55,6 +55,7 @@ export const Content = forwardRef< children?: ReactNode zIndexOffset?: ZIndexOffset backgroundMaterialClass?: MaterialClass + keepAspectRatio?: boolean } & ContentProperties & EventHandlers & LayoutListeners & @@ -86,7 +87,7 @@ export const Content = forwardRef< const innerGroupRef = useRef(null) const rootGroupRef = useRootGroupRef() const orderInfo = useOrderInfo(ElementType.Object, undefined, backgroundOrderInfo) - const aspectRatio = useNormalizedContent( + const size = useNormalizedContent( collection, innerGroupRef, rootGroupRef, @@ -99,26 +100,47 @@ export const Content = forwardRef< useApplyProperties(collection, properties) useApplyResponsiveProperties(collection, properties) const hoverHandlers = useApplyHoverProperties(collection, properties) - writeCollection(collection, 'aspectRatio', aspectRatio) + const aspectRatio = useMemo( + () => + computed(() => { + const [x, y] = size.value + return x / y + }), + [size], + ) + if ((properties.keepAspectRatio ?? true) === true) { + writeCollection(collection, 'aspectRatio', aspectRatio) + } finalizeCollection(collection) const outerGroupRef = useRef(null) useEffect( () => effect(() => { - const [offsetX, offsetY, scale] = fitNormalizedContentInside( - node.size, - node.paddingInset, - node.borderInset, - node.pixelSize, - aspectRatio.value ?? 1, - ) + const [width, height] = node.size.value + const [pTop, pRight, pBottom, pLeft] = node.paddingInset.value + const [bTop, bRight, bBottom, bLeft] = node.borderInset.value + const topInset = pTop + bTop + const rightInset = pRight + bRight + const bottomInset = pBottom + bBottom + const leftInset = pLeft + bLeft + + const innerWidth = width - leftInset - rightInset + const innerHeight = height - topInset - bottomInset + + const { pixelSize } = node + const { current } = outerGroupRef - current?.position.set(offsetX, offsetY, 0) - current?.scale.setScalar(scale) + current?.position.set((leftInset - rightInset) * 0.5 * pixelSize, (bottomInset - topInset) * 0.5 * pixelSize, 0) + const [, y, z] = size.value + current?.scale.set( + innerWidth * pixelSize, + innerHeight * pixelSize, + properties.keepAspectRatio ? (innerHeight * pixelSize * z) / y : z, + ) current?.updateMatrix() }), - [node, aspectRatio], + [node, properties.keepAspectRatio, size], ) const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) @@ -151,8 +173,8 @@ function useNormalizedContent( rootCameraDistance: CameraDistanceRef, parentClippingRect: Signal | undefined, orderInfo: OrderInfo, -): Signal { - const aspectRatio = useMemo(() => signal(undefined), []) +): Signal { + const sizeSignal = useMemo(() => signal(new Vector3(1, 1, 1)), []) const clippingPlanes = useGlobalClippingPlanes(parentClippingRect, rootGroupRef) const getPropertySignal = useGetBatchedProperties(collection, propertyKeys) useEffect(() => { @@ -171,24 +193,24 @@ function useNormalizedContent( const parent = group.parent parent?.remove(group) box3Helper.setFromObject(group) - const vector = new Vector3() - box3Helper.getSize(vector) - const scale = 1 / vector.y - const depth = vector.z - aspectRatio.value = vector.x / vector.y - group.scale.set(1, 1, 1).multiplyScalar(scale) + const size = new Vector3() + const center = new Vector3() + box3Helper.getSize(size) + const depth = size.z + sizeSignal.value = size + group.scale.set(1, 1, 1).divide(size) if (parent != null) { parent.add(group) } - box3Helper.getCenter(vector) + box3Helper.getCenter(center) return effect(() => { - group.position.copy(vector).negate() + group.position.copy(center).negate() group.position.z -= alignmentZMap[getPropertySignal.value('depthAlign') ?? 'back'] * depth - group.position.multiplyScalar(scale) + group.position.divide(size) group.updateMatrix() }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [getPropertySignal, rootCameraDistance, clippingPlanes, rootGroupRef]) - return aspectRatio + return sizeSignal } diff --git a/packages/uikit/src/components/text.tsx b/packages/uikit/src/components/text.tsx index f4065520..d07fa922 100644 --- a/packages/uikit/src/components/text.tsx +++ b/packages/uikit/src/components/text.tsx @@ -42,7 +42,7 @@ export type TextProperties = WithConditionals< export const Text = forwardRef< ComponentInternals, { - children: string | Signal + children: string | Signal | Array> backgroundMaterialClass?: MaterialClass zIndexOffset?: ZIndexOffset } & TextProperties & diff --git a/packages/uikit/src/panel/panel-material.ts b/packages/uikit/src/panel/panel-material.ts index fbbbdb38..fccb7f1d 100644 --- a/packages/uikit/src/panel/panel-material.ts +++ b/packages/uikit/src/panel/panel-material.ts @@ -361,12 +361,12 @@ export function compilePanelMaterial(parameters: WebGLProgramParametersWithUnifo if(backgroundColor.r < 0.0 && backgroundOpacity >= 0.0) { backgroundColor = vec3(1.0); } - if(backgroundOpacity < 0.0 && backgroundColor.r >= 0.0) { - backgroundOpacity = 1.0; + if(backgroundOpacity < 0.0) { + backgroundOpacity = backgroundColor.r >= 0.0 ? 1.0 : 0.0; } - if(backgroundOpacity <= 0.0) { - discard; + if(backgroundOpacity < 0.0) { + backgroundOpacity = 0.0; } diffuseColor.rgb = mix(borderColor, diffuseColor.rgb * backgroundColor, transition); diff --git a/packages/uikit/src/text/react.tsx b/packages/uikit/src/text/react.tsx index 2e553a4e..21eb57ee 100644 --- a/packages/uikit/src/text/react.tsx +++ b/packages/uikit/src/text/react.tsx @@ -114,7 +114,7 @@ export type InstancedTextProperties = TextAlignProperties & export function useInstancedText( collection: ManagerCollection, - text: string | Signal, + text: string | Signal | Array>, matrix: Signal, node: FlexNode, isHidden: Signal | undefined, @@ -123,7 +123,8 @@ export function useInstancedText( ) { const getGroup = useContext(InstancedGlyphContext) const fontSignal = useFont(collection) - const textSignal = useMemo(() => signal | undefined>(undefined), []) + // eslint-disable-next-line react-hooks/exhaustive-deps + const textSignal = useMemo(() => signal | Array>>(text), []) textSignal.value = text const propertiesRef = useRef(undefined) @@ -229,7 +230,7 @@ function getWeightNumber(value: string): number { export function useMeasureFunc( collection: ManagerCollection, fontSignal: Signal, - textSignal: Signal | undefined>, + textSignal: Signal | Array | string>>, propertiesRef: MutableRefObject, ) { const getGlyphProperties = useGetBatchedProperties(collection, glyphPropertyKeys) @@ -240,10 +241,10 @@ export function useMeasureFunc( if (font == null) { return undefined } - const text = readReactive(textSignal.value) - if (text == null) { - return undefined - } + const textSignalValue = textSignal.value + const text = Array.isArray(textSignalValue) + ? textSignalValue.map((t) => readReactive(t)).join('') + : readReactive(textSignalValue) const letterSpacing = getGlyphProperties.value('letterSpacing') ?? 0 const lineHeight = getGlyphProperties.value('lineHeight') ?? 1.2 const fontSize = getGlyphProperties.value('fontSize') ?? 16 diff --git a/packages/uikit/src/text/render/instanced-glyph.ts b/packages/uikit/src/text/render/instanced-glyph.ts index bd1cb24a..3566316c 100644 --- a/packages/uikit/src/text/render/instanced-glyph.ts +++ b/packages/uikit/src/text/render/instanced-glyph.ts @@ -95,10 +95,14 @@ export class InstancedGlyph { instanceRGBA.needsUpdate = true } - updateTransformation(x: number, y: number, fontSize: number): void { - if (this.x === x && this.y === y && this.fontSize === fontSize) { + updateGlyphAndTransformation(glyphInfo: GlyphInfo, x: number, y: number, fontSize: number): void { + if (this.glyphInfo === this.glyphInfo && this.x === x && this.y === y && this.fontSize === fontSize) { return } + if (this.glyphInfo != glyphInfo) { + this.glyphInfo = glyphInfo + this.writeUV() + } this.x = x this.y = y this.fontSize = fontSize @@ -113,14 +117,6 @@ export class InstancedGlyph { this.writeUpdatedMatrix() } - updateGlyphInfo(glyphInfo: GlyphInfo): void { - if (this.glyphInfo === glyphInfo) { - return - } - this.glyphInfo = glyphInfo - this.writeUV() - } - private writeUV(): void { if (this.index == null || this.glyphInfo == null) { return diff --git a/packages/uikit/src/text/render/instanced-text.ts b/packages/uikit/src/text/render/instanced-text.ts index e79ae36a..41acb083 100644 --- a/packages/uikit/src/text/render/instanced-text.ts +++ b/packages/uikit/src/text/render/instanced-text.ts @@ -154,12 +154,12 @@ export class InstancedText { this.parentClippingRect?.peek(), ) } - glyph.updateTransformation( + glyph.updateGlyphAndTransformation( + glyphInfo, pixelSize * (x + getGlyphOffsetX(font, fontSize, glyphInfo, prevGlyphId)), -pixelSize * (y + getGlyphOffsetY(fontSize, lineHeight, glyphInfo)), pixelSize * fontSize, ) - glyph.updateGlyphInfo(glyphInfo) glyph.show() ++glyphIndex diff --git a/packages/uikit/src/text/wrapper/word-wrapper.ts b/packages/uikit/src/text/wrapper/word-wrapper.ts index 255d585b..1d428989 100644 --- a/packages/uikit/src/text/wrapper/word-wrapper.ts +++ b/packages/uikit/src/text/wrapper/word-wrapper.ts @@ -34,7 +34,10 @@ export const WordWrapper: GlyphWrapper = ({ text, fontSize, font, letterSpacing continue } - if (textIndex < text.length && text[textIndex] != ' ') { + const newChar = text[textIndex] + + if (newChar != ' ' && newChar != '\n' && textIndex < text.length) { + //no reason to advance the save point continue }