Skip to content

Commit

Permalink
feat: allow setting the camera position
Browse files Browse the repository at this point in the history
  • Loading branch information
gabotechs committed Jan 14, 2024
1 parent 3116492 commit 669fd08
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 182 deletions.
105 changes: 56 additions & 49 deletions .storybook/stories/FromFile.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,65 @@
import { CSSProperties, FC, useCallback, useState } from "react";
import { StlViewer, StlViewerProps } from "../../src";
import { ComponentMeta } from "@storybook/react";
import {CSSProperties, FC, useCallback, useState} from "react";
import {StlViewer, StlViewerProps} from "../../src";
import {ComponentMeta} from "@storybook/react";
import React from "react";

const style: CSSProperties = {
position: "absolute",
top: '0vh',
left: '0vw',
width: '100vw',
height: '100vh',
display: "flex",
alignItems: "center",
justifyContent: "center"
}
position: "absolute",
top: "0vh",
left: "0vw",
width: "100vw",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
};

function FromUrl(props: Omit<StlViewerProps, "url">) {
const [file, setFile] = useState<File>()

const preventDefault = useCallback((e: React.DragEvent<any>) => {
e.preventDefault()
}, [])

const onDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
console.log("file dropped")
setFile(e.dataTransfer.files[0])
}, [])

const extraProps = {onDragOver: preventDefault, onDragEnter: preventDefault, onDrop}

return (
<>
{file && <StlViewer
url={URL.createObjectURL(file)}
style={style}
shadows
orbitControls
modelProps={{
color: "#008675"
}}
{...extraProps}
{...props}
/>}
{!file && <div style={style} {...extraProps}>
<h4>drop here</h4>
</div>}
</>

);
const [file, setFile] = useState<File>();

const preventDefault = useCallback((e: React.DragEvent<any>) => {
e.preventDefault();
}, []);

const onDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
console.log("file dropped");
setFile(e.dataTransfer.files[0]);
}, []);

const extraProps = {
onDragOver: preventDefault,
onDragEnter: preventDefault,
onDrop,
};

return (
<>
{file && (
<StlViewer
url={URL.createObjectURL(file)}
style={style}
shadows
orbitControls
modelProps={{
color: "#008675",
}}
{...extraProps}
{...props}
/>
)}
{!file && (
<div style={style} {...extraProps}>
<h4>drop here</h4>
</div>
)}
</>
);
}

export const Primary = FromUrl.bind({})
export const Primary = FromUrl.bind({});

export default {
component: StlViewer,
title: "StlViewer from dropped file",
} as ComponentMeta<FC<StlViewerProps>>
component: StlViewer,
title: "StlViewer from dropped file",
} as ComponentMeta<FC<StlViewerProps>>;
206 changes: 113 additions & 93 deletions .storybook/stories/FromUrl.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,104 +1,124 @@
import React, { FC, useRef, useState } from "react";
import { StlViewer, StlViewerProps } from "../../src";
import { ComponentMeta } from "@storybook/react";
import { ModelRef } from "../../src/StlViewer/SceneSetup";
import React, {FC, useRef, useState} from "react";
import {ComponentMeta} from "@storybook/react";
import {StlViewer, StlViewerProps, ModelRef, CameraRef} from "../../src";

function download(filename: string, blob: Blob) {
const element = document.createElement('a');
element.setAttribute('download', filename);
element.style.display = 'none';
element.href = URL.createObjectURL(blob)
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
const element = document.createElement("a");
element.setAttribute("download", filename);
element.style.display = "none";
element.href = URL.createObjectURL(blob);
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}

const url = "https://storage.googleapis.com/ucloud-v3/61575ca49d8a1777fa431395.stl"
const url2 = "https://storage.googleapis.com/ucloud-v3/2272dfa00d58a59dae26a399.stl"
const url =
"https://storage.googleapis.com/ucloud-v3/61575ca49d8a1777fa431395.stl";
const url2 =
"https://storage.googleapis.com/ucloud-v3/2272dfa00d58a59dae26a399.stl";

function FromUrl(props: Omit<StlViewerProps, "url">) {
const ref = useRef<ModelRef>()
const [rotationX, setRotationX] = useState(0)
const [rotationY, setRotationY] = useState(0)
const [rotationZ, setRotationZ] = useState(0)
const ref = useRef<ModelRef>();
const cameraRef = useRef<CameraRef>();
const [rotationX, setRotationX] = useState(0);
const [rotationY, setRotationY] = useState(0);
const [rotationZ, setRotationZ] = useState(0);


return (
<>
<StlViewer
url={url2}
style={{
position: "absolute",
top: '0vh',
left: '0vw',
width: '100vw',
height: '100vh',
backgroundColor: "white"
}}
shadows
showAxes
orbitControls
cameraInitialPosition={{
latitude: Math.PI / 8,
longitude: -Math.PI / 8,
distance: 1
}}
modelProps={{
positionX: 150,
positionY: 150,
rotationX,
rotationY,
rotationZ,
scale: 1,
color: "#008675",
ref,
geometryProcessor: geometry => geometry
}}
floorProps={{
gridWidth: 300
}}
onFinishLoading={console.log}
{...props}
/>
<button
style={{
position: "absolute",
top: 20,
left: 100,
}}
onClick={() => {
if (ref.current) {
const file = ref.current.save()
download("exported.stl", file)
}
}}>
download
</button>
{[
["rotate x", setRotationX] as const,
["rotate y", setRotationY] as const,
["rotate z", setRotationZ] as const
].map(([text, setRotation], index) => (
<button
key={index}
style={{
position: "absolute",
top: 50+30*index,
left: 100,
}}
onClick={() => setRotation(prev => prev + 15/180*Math.PI)}
>
{text}
</button>
))}
</>

);
return (
<>
<StlViewer
url={url2}
style={{
position: "absolute",
top: "0vh",
left: "0vw",
width: "100vw",
height: "100vh",
backgroundColor: "white",
}}
shadows
showAxes
orbitControls
cameraProps={{
initialPosition: {
latitude: Math.PI / 8,
longitude: -Math.PI / 8,
distance: 1,
},
ref: cameraRef,
}}
modelProps={{
positionX: 150,
positionY: 150,
rotationX,
rotationY,
rotationZ,
scale: 1,
color: "#008675",
ref,
geometryProcessor: (geometry) => geometry,
}}
floorProps={{
gridWidth: 300,
}}
onFinishLoading={console.log}
{...props}
/>
<button
style={{
position: "absolute",
top: 20,
left: 100,
}}
onClick={() => {
cameraRef.current?.setCameraPosition({
latitude: Math.PI / 8,
longitude: -Math.PI / 8,
distance: 1,
});
}}
>
reset camera
</button>
<button
style={{
position: "absolute",
top: 50,
left: 100,
}}
onClick={() => {
if (ref.current) {
const file = ref.current.save();
download("exported.stl", file);
}
}}
>
download
</button>
{[
["rotate x", setRotationX] as const,
["rotate y", setRotationY] as const,
["rotate z", setRotationZ] as const,
].map(([text, setRotation], index) => (
<button
key={index}
style={{
position: "absolute",
top: 80 + 30 * index,
left: 100,
}}
onClick={() => setRotation((prev) => prev + (15 / 180) * Math.PI)}
>
{text}
</button>
))}
</>
);
}

export const Primary = FromUrl.bind({})
export const Primary = FromUrl.bind({});

export default {
component: StlViewer,
title: "StlViewer from url",
} as ComponentMeta<FC<StlViewerProps>>
component: StlViewer,
title: "StlViewer from url",
} as ComponentMeta<FC<StlViewerProps>>;
43 changes: 29 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,39 @@ You can see working the examples from `.storybook/stories` [here](https://gabote

## Props

| Prop | Type | Required | Notes |
|-------------------------|:------------------------------:|:--------:|:----------------------------------------------------------------------------------------------------------------:|
| `url` | `string` | `true` | url of the Stl file |
| `modelProps` | `ModelProps` | `false` | 3d model properties, see below |
| `floorProps` | `FloorProps` | `false` | floor properties, see below |
| `shadows` | `boolean` | `false` | render shadows projected by the model on the ground |
| `showAxes` | `boolean` | `false` | show x y z axis |
| `orbitControls` | `boolean` | `false` | enable camera orbit controls |
| `cameraInitialPosition` | `CameraInitialPosition` | `false` | set the initial position of the camera in geographic coordinates. The origin of coordinates is the object itself |
| `extraHeaders` | `Record<string, string>` | `false` | custom headers for making the http query |
| `onFinishLoading` | `(ev: ModelDimensions) => any` | `false` | callback triggered when Stl is fully loaded |
| `onError` | `(err: Error) => any` | `false` | callback triggered when an error occurred while loading Stl |
| `canvasId` | `string` | `false` | id of the canvas element used for rendering the model |
| Prop | Type | Required | Notes |
|-------------------|:------------------------------:|:--------:|:-----------------------------------------------------------:|
| `url` | `string` | `true` | url of the Stl file |
| `cameraProps` | `CameraProps` | `false` | camera properties, see below |
| `modelProps` | `ModelProps` | `false` | 3d model properties, see below |
| `floorProps` | `FloorProps` | `false` | floor properties, see below |
| `shadows` | `boolean` | `false` | render shadows projected by the model on the ground |
| `showAxes` | `boolean` | `false` | show x y z axis |
| `orbitControls` | `boolean` | `false` | enable camera orbit controls |
| `extraHeaders` | `Record<string, string>` | `false` | custom headers for making the http query |
| `onFinishLoading` | `(ev: ModelDimensions) => any` | `false` | callback triggered when Stl is fully loaded |
| `onError` | `(err: Error) => any` | `false` | callback triggered when an error occurred while loading Stl |
| `canvasId` | `string` | `false` | id of the canvas element used for rendering the model |

The component also accepts ```<div/>``` props

## Interfaces

### CameraProps

| Prop | Type | Required | Notes |
|-------------------|:----------------------:|:--------:|:--------------------------------------------------------------------------------------------------------:|
| `ref` | `{current: CameraRef}` | `false` | reference of the camera for accessing it's properties |
| `initialPosition` | `CameraPosition` | `false` | set the position of the camera in geographic coordinates. The origin of coordinates is the object itself |

### CameraRef

| Prop | Type | Required | Notes |
|---------------------|:-----------------------------------:|:--------:|:----------------------------------------------------------------------------------:|
| `setCameraPosition` | `(position: CameraPosition) => any` | `true` | imperatively sets the camera position based on the provided geographic coordinates |

setCameraPosition:

### ModelProps

| Prop | Type | Required | Notes |
Expand Down Expand Up @@ -102,7 +117,7 @@ The component also accepts ```<div/>``` props
| `height` | `number` | height of the 3d object |
| `length` | `number` | length of the 3d object |

### CameraInitialPosition
### CameraPosition

| Prop | Type | Required | Notes |
|-------------|:--------:|:--------:|:------------------------------------------------------------------------------------------------------------------------------------------------:|
Expand Down
Loading

0 comments on commit 669fd08

Please sign in to comment.