Add stl download for OpenSCAD and CadQuery IDEs

Resolves #330.
This commit is contained in:
Kurt Hutten
2021-05-29 21:06:33 +10:00
parent 32fa22efcd
commit bd58e6c7cb
12 changed files with 311 additions and 168 deletions

View File

@@ -1,4 +1,4 @@
const { runScad } = require('./runScad') const { runScad, stlExport } = require('./runScad')
const middy = require('middy') const middy = require('middy')
const { cors } = require('middy/middlewares') const { cors } = require('middy/middlewares')
const AWS = require('aws-sdk') const AWS = require('aws-sdk')
@@ -12,25 +12,52 @@ const {
const s3 = new AWS.S3() const s3 = new AWS.S3()
const openScadStlKey = (eventBody) => {
const { file } = JSON.parse(eventBody)
return `${makeHash(JSON.stringify(file))}.stl`
}
const preview = async (req, _context, callback) => { const preview = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false _context.callbackWaitsForEmptyEventLoop = false
const eventBody = req.body const eventBody = req.body
console.log('eventBody', eventBody) console.log('eventBody', eventBody)
const key = `${makeHash(eventBody)}.png` const key = `${makeHash(eventBody)}.png`
const stlKey = openScadStlKey(eventBody)
console.log('key', key) console.log('key', key)
const stlParams = {
Bucket: process.env.BUCKET,
Key: stlKey,
}
const params = { const params = {
Bucket: process.env.BUCKET, Bucket: process.env.BUCKET,
Key: key, Key: key,
} }
const previousAsset = await checkIfAlreadyExists(params, s3) const [previousAssetStl, previousAssetPng] = await Promise.all([
checkIfAlreadyExists(stlParams, s3),
checkIfAlreadyExists(params, s3),
])
const type = previousAssetStl.isAlreadyInBucket ? 'stl' : 'png'
const previousAsset = previousAssetStl.isAlreadyInBucket
? previousAssetStl
: previousAssetPng
if (previousAsset.isAlreadyInBucket) { if (previousAsset.isAlreadyInBucket) {
console.log('already in bucket') console.log('already in bucket')
const response = { const response = {
statusCode: 200, statusCode: 200,
body: JSON.stringify({ body: JSON.stringify({
url: getObjectUrl(params, s3), url: getObjectUrl(
consoleMessage: previousAsset.consoleMessage, {
Bucket: process.env.BUCKET,
Key: previousAssetStl.isAlreadyInBucket ? stlKey : key,
},
s3
),
consoleMessage:
previousAsset.consoleMessage || previousAssetPng.consoleMessage,
type,
}), }),
} }
callback(null, response) callback(null, response)
@@ -50,39 +77,46 @@ const preview = async (req, _context, callback) => {
}) })
} }
// const stl = async (req, _context, callback) => { const stl = async (req, _context, callback) => {
// _context.callbackWaitsForEmptyEventLoop = false _context.callbackWaitsForEmptyEventLoop = false
// const eventBody = Buffer.from(req.body, 'base64').toString('ascii') const eventBody = req.body
// console.log(eventBody, 'eventBody') console.log(eventBody, 'eventBody')
// const { file } = JSON.parse(eventBody) const stlKey = openScadStlKey(eventBody)
// const { error, result, tempFile } = await stlExport({ file })
// if (error) { console.log('key', stlKey)
// const response = {
// statusCode: 400, const params = {
// body: { error, tempFile }, Bucket: process.env.BUCKET,
// } Key: stlKey,
// callback(null, response) }
// } else { console.log('original params', params)
// console.log(`got result in route: ${result}, file is: ${tempFile}`) const previousAsset = await checkIfAlreadyExists(params, s3)
// const fs = require('fs') if (previousAsset.isAlreadyInBucket) {
// const stl = fs.readFileSync(`/tmp/${tempFile}/output.stl`, { console.log('already in bucket')
// encoding: 'base64', const response = {
// }) statusCode: 200,
// console.log('encoded stl', stl) body: JSON.stringify({
// const response = { url: getObjectUrl({ ...params }, s3),
// statusCode: 200, consoleMessage: previousAsset.consoleMessage,
// headers: { }),
// 'content-type': 'application/stl', }
// }, callback(null, response)
// body: stl, return
// isBase64Encoded: true, }
// } const { file } = JSON.parse(eventBody)
// console.log('callback fired') const { error, consoleMessage, fullPath } = await stlExport({ file })
// callback(null, response) await storeAssetAndReturnUrl({
// } error,
// } callback,
fullPath,
consoleMessage,
key: stlKey,
s3,
params,
})
}
module.exports = { module.exports = {
// stl: middy(stl).use(cors()), stl: middy(stl).use(cors()),
preview: middy(loggerWrap(preview)).use(cors()), preview: middy(loggerWrap(preview)).use(cors()),
} }

View File

@@ -35,14 +35,15 @@ module.exports.runScad = async ({
module.exports.stlExport = async ({ file } = {}) => { module.exports.stlExport = async ({ file } = {}) => {
const tempFile = await makeFile(file, '.scad', nanoid) const tempFile = await makeFile(file, '.scad', nanoid)
const fullPath = `/tmp/${tempFile}/output.stl`
try { try {
const result = await runCommand( const consoleMessage = await runCommand(
`openscad -o /tmp/${tempFile}/output.stl /tmp/${tempFile}/main.scad`, `xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad -o ${fullPath} /tmp/${tempFile}/main.scad`,
300000 // lambda will time out before this, we might need to look at background jobs if we do git integration stl generation 60000 // lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
) )
return { result, tempFile } return { consoleMessage, fullPath }
} catch (error) { } catch (error) {
return { error, tempFile } return { error, fullPath }
} }
} }

View File

@@ -63,18 +63,21 @@ functions:
timeout: 25 timeout: 25
environment: environment:
BUCKET: cad-preview-bucket-prod-001 BUCKET: cad-preview-bucket-prod-001
# openscadstl: openscadstl:
# image: image:
# name: openscadimage name: openscadimage
# command: command:
# - openscad.stl - openscad.stl
# entryPoint: entryPoint:
# - '/entrypoint.sh' - '/entrypoint.sh'
# events: events:
# - http: - http:
# path: openscad/stl path: openscad/stl
# method: post method: post
# timeout: 30 cors: true
timeout: 30
environment:
BUCKET: cad-preview-bucket-prod-001
cadquerystl: cadquerystl:
image: image:
name: cadqueryimage name: cadqueryimage

View File

@@ -20,6 +20,7 @@
"@redwoodjs/forms": "^0.31.0", "@redwoodjs/forms": "^0.31.0",
"@redwoodjs/router": "^0.31.0", "@redwoodjs/router": "^0.31.0",
"@redwoodjs/web": "^0.31.0", "@redwoodjs/web": "^0.31.0",
"browser-fs-access": "^0.17.2",
"cloudinary-react": "^1.6.7", "cloudinary-react": "^1.6.7",
"controlkit": "^0.1.9", "controlkit": "^0.1.9",
"get-active-classes": "^0.0.11", "get-active-classes": "^0.0.11",

View File

@@ -29,7 +29,7 @@ const IdeContainer = () => {
}) })
thunkDispatch((dispatch, getState) => { thunkDispatch((dispatch, getState) => {
const state = getState() const state = getState()
if (state.ideType === 'openScad') { if (['png', 'INIT'].includes(state.objectData?.type)) {
dispatch({ type: 'setLoading' }) dispatch({ type: 'setLoading' })
requestRender({ requestRender({
state, state,

View File

@@ -5,7 +5,10 @@ import { useIdeState, codeStorageKey } from 'src/helpers/hooks/useIdeState'
import { copyTextToClipboard } from 'src/helpers/clipboard' import { copyTextToClipboard } from 'src/helpers/clipboard'
import { requestRender } from 'src/helpers/hooks/useIdeState' import { requestRender } from 'src/helpers/hooks/useIdeState'
import { encode, decode } from 'src/helpers/compress' import { encode, decode } from 'src/helpers/compress'
import { flow } from 'lodash/fp' import { flow, identity } from 'lodash/fp'
import { fileSave } from 'browser-fs-access'
import { MeshBasicMaterial, Mesh, Scene } from 'three'
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
export const githubSafe = (url) => export const githubSafe = (url) =>
url.includes('github.com') url.includes('github.com')
@@ -80,6 +83,63 @@ const IdeToolbarNew = ({ cadPackage }) => {
copyTextToClipboard(window.location.href) copyTextToClipboard(window.location.href)
} }
} }
const PullTitleFromFirstLine = (code) => {
const firstLine = code.split('\n').filter(identity)[0]
if (!(firstLine.startsWith('//') || firstLine.startsWith('#'))) {
return 'object.stl'
}
return (
(firstLine.replace(/^(\/\/|#)\s*(.+)/, (_, __, titleWithSpaces) =>
titleWithSpaces.replaceAll(/\s/g, '-')
) || 'object') + '.stl'
)
}
const handleStlDownload = (({ geometry, fileName, type }) => () => {
const makeStlBlobFromGeo = flow(
(geo) => new Mesh(geo, new MeshBasicMaterial()),
(mesh) => new Scene().add(mesh),
(scene) => new STLExporter().parse(scene),
(stl) =>
new Blob([stl], {
type: 'text/plain',
})
)
const saveFile = (geometry) => {
const blob = makeStlBlobFromGeo(geometry)
fileSave(blob, {
fileName,
extensions: ['.stl'],
})
}
if (geometry) {
if (type === 'geometry') {
saveFile(geometry)
} else {
thunkDispatch((dispatch, getState) => {
const state = getState()
if (state.ideType === 'openScad') {
thunkDispatch((dispatch, getState) => {
const state = getState()
dispatch({ type: 'setLoading' })
requestRender({
state,
dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
specialCadProcess: 'stl',
}).then((result) => result && saveFile(result.data))
})
}
})
}
}
})({
type: state.objectData?.type,
geometry: state.objectData?.data,
fileName: PullTitleFromFirstLine(state.code),
})
return ( return (
<IdeContext.Provider value={{ state, thunkDispatch }}> <IdeContext.Provider value={{ state, thunkDispatch }}>
@@ -97,6 +157,12 @@ const IdeToolbarNew = ({ cadPackage }) => {
> >
Copy link Copy link
</button> </button>
<button
onClick={handleStlDownload}
className="border-2 text-gray-700 px-2 text-sm m-1 ml-2"
>
Download STL
</button>
</nav> </nav>
<IdeContainer /> <IdeContainer />
</div> </div>

View File

@@ -9,33 +9,16 @@ import {
} from 'react-three-fiber' } from 'react-three-fiber'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { Vector3 } from 'three' import { Vector3 } from 'three'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { requestRender } from 'src/helpers/hooks/useIdeState' import { requestRender } from 'src/helpers/hooks/useIdeState'
extend({ OrbitControls }) extend({ OrbitControls })
function Asset({ url, resetLoading, setLoading }) { function Asset({ geometry: incomingGeo }) {
const [loadedGeometry, setLoadedGeometry] = useState()
const mesh = useRef() const mesh = useRef()
const ref = useUpdate((geometry) => { const ref = useUpdate((geometry) => {
geometry.attributes = loadedGeometry.attributes geometry.attributes = incomingGeo.attributes
}) })
useEffect(() => { if (!incomingGeo) return null
if (url) {
const loader = new STLLoader()
setLoading()
loader.load(
url,
(geometry) => {
setLoadedGeometry(geometry)
resetLoading()
},
null,
resetLoading
)
}
}, [url])
if (!loadedGeometry) return null
return ( return (
<mesh ref={mesh} scale={[1, 1, 1]}> <mesh ref={mesh} scale={[1, 1, 1]}>
<bufferGeometry attach="geometry" ref={ref} /> <bufferGeometry attach="geometry" ref={ref} />
@@ -158,9 +141,6 @@ const IdeViewer = () => {
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
const [image, setImage] = useState() const [image, setImage] = useState()
const resetLoading = () => thunkDispatch({ type: 'resetLoading' })
const setLoading = () => thunkDispatch({ type: 'setLoading' })
useEffect(() => { useEffect(() => {
setImage(state.objectData?.type === 'png' && state.objectData?.data) setImage(state.objectData?.type === 'png' && state.objectData?.data)
setIsDragging(false) setIsDragging(false)
@@ -192,7 +172,7 @@ const IdeViewer = () => {
)} )}
<div // eslint-disable-line jsx-a11y/no-static-element-interactions <div // eslint-disable-line jsx-a11y/no-static-element-interactions
className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${ className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${
!(isDragging || state.ideType !== 'openScad') !(isDragging || state.objectData?.type !== 'png')
? 'hover:opacity-50' ? 'hover:opacity-50'
: 'opacity-100' : 'opacity-100'
}`} }`}
@@ -208,7 +188,7 @@ const IdeViewer = () => {
}) })
thunkDispatch((dispatch, getState) => { thunkDispatch((dispatch, getState) => {
const state = getState() const state = getState()
if (state.ideType === 'openScad') { if (['png', 'INIT'].includes(state.objectData?.type)) {
dispatch({ type: 'setLoading' }) dispatch({ type: 'setLoading' })
requestRender({ requestRender({
state, state,
@@ -223,7 +203,7 @@ const IdeViewer = () => {
/> />
<ambientLight /> <ambientLight />
<pointLight position={[15, 5, 10]} /> <pointLight position={[15, 5, 10]} />
{state.ideType === 'openScad' && ( {state.objectData?.type === 'png' && (
<> <>
<Sphere position={[0, 0, 0]} color={pink400} /> <Sphere position={[0, 0, 0]} color={pink400} />
<Box position={[0, 50, 0]} size={[1, 100, 1]} color={indigo900} /> <Box position={[0, 50, 0]} size={[1, 100, 1]} color={indigo900} />
@@ -235,13 +215,11 @@ const IdeViewer = () => {
<Box position={[50, 0, 0]} size={[100, 1, 1]} color={pink400} /> <Box position={[50, 0, 0]} size={[100, 1, 1]} color={pink400} />
</> </>
)} )}
{state.ideType === 'cadQuery' && (
<Asset <Asset
url={state.objectData?.type === 'stl' && state.objectData?.data} geometry={
resetLoading={resetLoading} state.objectData?.type === 'geometry' && state.objectData?.data
setLoading={setLoading} }
/> />
)}
</Canvas> </Canvas>
</div> </div>
{state.isLoading && ( {state.isLoading && (

View File

@@ -1,4 +1,9 @@
import { lambdaBaseURL } from './common' import {
lambdaBaseURL,
stlToGeometry,
createHealthyResponse,
createUnhealthyResponse,
} from './common'
export const render = async ({ code }) => { export const render = async ({ code }) => {
const body = JSON.stringify({ const body = JSON.stringify({
@@ -14,47 +19,26 @@ export const render = async ({ code }) => {
body, body,
}) })
if (response.status === 400) { if (response.status === 400) {
// TODO add proper error messages for CadQuery
const { error } = await response.json() const { error } = await response.json()
const cleanedErrorMessage = error.replace(
/["|']\/tmp\/.+\/main.scad["|']/g,
"'main.scad'"
)
return { return {
status: 'error', status: 'error',
message: { message: {
type: 'error', type: 'error',
message: cleanedErrorMessage, message: error,
time: new Date(), time: new Date(),
}, },
} }
} }
const data = await response.json() const data = await response.json()
return { const geometry = await stlToGeometry(data.url)
status: 'healthy', return createHealthyResponse({
objectData: { type: 'geometry',
type: 'stl', data: geometry,
data: data.url, consoleMessage: data.consoleMessage,
}, date: new Date(),
message: { })
type: 'message',
message: data.consoleMessage || 'Successful Render',
time: new Date(),
},
}
} catch (e) { } catch (e) {
// TODO handle errors better return createUnhealthyResponse(new Date())
// I think we should display something overlayed on the viewer window something like "network issue try again"
// and in future I think we need timeouts differently as they maybe from a user trying to render something too complex
// or something with minkowski in it :/ either way something like "render timed out, try again or here are tips to reduce part complexity" with a link talking about $fn and minkowski etc
return {
status: 'error',
message: {
type: 'error',
message: 'network issue',
time: new Date(),
},
}
} }
} }

View File

@@ -1,3 +1,40 @@
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
export const lambdaBaseURL = export const lambdaBaseURL =
process.env.CAD_LAMBDA_BASE_URL || process.env.CAD_LAMBDA_BASE_URL ||
'https://2inlbple1b.execute-api.us-east-1.amazonaws.com/prod2' 'https://2inlbple1b.execute-api.us-east-1.amazonaws.com/prod2'
export const stlToGeometry = (url) =>
new Promise((resolve, reject) => {
new STLLoader().load(url, resolve, null, reject)
})
export function createHealthyResponse({ date, data, consoleMessage, type }) {
return {
status: 'healthy',
objectData: {
type,
data: data,
},
message: {
type: 'message',
message: consoleMessage,
time: date,
},
}
}
export function createUnhealthyResponse(date, message = 'network issue') {
// TODO handle errors better
// I think we should display something overlayed on the viewer window something like "network issue try again"
// and in future I think we need timeouts differently as they maybe from a user trying to render something too complex
// or something with minkowski in it :/ either way something like "render timed out, try again or here are tips to reduce part complexity" with a link talking about $fn and minkowski etc
return {
status: 'error',
message: {
type: 'error',
message,
time: date,
},
}
}

View File

@@ -1,4 +1,9 @@
import { lambdaBaseURL } from './common' import {
lambdaBaseURL,
stlToGeometry,
createHealthyResponse,
createUnhealthyResponse,
} from './common'
export const render = async ({ code, settings }) => { export const render = async ({ code, settings }) => {
const pixelRatio = window.devicePixelRatio || 1 const pixelRatio = window.devicePixelRatio || 1
@@ -41,51 +46,61 @@ export const render = async ({ code, settings }) => {
}) })
if (response.status === 400) { if (response.status === 400) {
const { error } = await response.json() const { error } = await response.json()
const cleanedErrorMessage = error.replace( const cleanedErrorMessage = cleanError(error)
/["|']\/tmp\/.+\/main.scad["|']/g, return createUnhealthyResponse(new Date(), cleanedErrorMessage)
"'main.scad'"
)
return {
status: 'error',
message: {
type: 'error',
message: cleanedErrorMessage,
time: new Date(),
},
}
} }
const data = await response.json() const data = await response.json()
return { const type = data.type !== 'stl' ? 'png' : 'geometry'
status: 'healthy', const newData = data.type !== 'stl' ? data.url : stlToGeometry(data.url)
objectData: { return createHealthyResponse({
type: 'png', type,
data: data.url, data: await newData,
}, consoleMessage: data.consoleMessage,
message: { date: new Date(),
type: 'message', })
message: data.consoleMessage,
time: new Date(),
},
}
} catch (e) { } catch (e) {
// TODO handle errors better return createUnhealthyResponse(new Date())
// I think we should display something overlayed on the viewer window something like "network issue try again"
// and in future I think we need timeouts differently as they maybe from a user trying to render something too complex
// or something with minkowski in it :/ either way something like "render timed out, try again or here are tips to reduce part complexity" with a link talking about $fn and minkowski etc
return {
status: 'error',
message: {
type: 'error',
message: 'network issue',
time: new Date(),
},
} }
} }
export const stl = async ({ code, settings }) => {
const body = JSON.stringify({
settings: {},
file: code,
})
try {
const response = await fetch(lambdaBaseURL + '/openscad/stl', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
})
if (response.status === 400) {
const { error } = await response.json()
const cleanedErrorMessage = cleanError(error)
return createUnhealthyResponse(new Date(), cleanedErrorMessage)
}
const data = await response.json()
const geometry = await stlToGeometry(data.url)
return createHealthyResponse({
type: 'geometry',
data: geometry,
consoleMessage: data.consoleMessage,
date: new Date(),
})
} catch (e) {
return createUnhealthyResponse(new Date())
}
} }
const openScad = { const openScad = {
render, render,
// more functions to come stl,
} }
export default openScad export default openScad
function cleanError(error) {
return error.replace(/["|']\/tmp\/.+\/main.scad["|']/g, "'main.scad'")
}

View File

@@ -9,7 +9,10 @@ function withThunk(dispatch, getState) {
} }
const initCodeMap = { const initCodeMap = {
openScad: ` openScad: `// involute donut
// ^ first comment is used for download title (i.e "involute-donut.stl")
color(c="DarkGoldenrod")rotate_extrude()translate([20,0])circle(d=30); color(c="DarkGoldenrod")rotate_extrude()translate([20,0])circle(d=30);
donut(); donut();
module donut() { module donut() {
@@ -23,7 +26,11 @@ module stick(basewid, angl){
translate([0,0,10])sphere(9); translate([0,0,10])sphere(9);
} }
}`, }`,
cadQuery: `import cadquery as cq cadQuery: `# demo shaft coupler
# ^ first comment is used for download title (i.e. "demo-shaft-coupler.stl")
import cadquery as cq
from cadquery import exporters from cadquery import exporters
diam = 5.0 diam = 5.0
@@ -49,7 +56,7 @@ export const useIdeState = () => {
], ],
code, code,
objectData: { objectData: {
type: 'stl', type: 'INIT',
data: null, data: null,
}, },
layout: { layout: {
@@ -138,11 +145,16 @@ export const requestRender = ({
code, code,
camera, camera,
viewerSize, viewerSize,
specialCadProcess = null,
}) => { }) => {
if (
state.ideType !== 'INIT' && state.ideType !== 'INIT' &&
!state.isLoading && (!state.isLoading || state.objectData?.type === 'INIT')
cadPackages[state.ideType] ) {
.render({ const renderFn = specialCadProcess
? cadPackages[state.ideType][specialCadProcess]
: cadPackages[state.ideType].render
return renderFn({
code, code,
settings: { settings: {
camera, camera,
@@ -160,7 +172,9 @@ export const requestRender = ({
type: 'healthyRender', type: 'healthyRender',
payload: { objectData, message, lastRunCode: code }, payload: { objectData, message, lastRunCode: code },
}) })
return objectData
} }
}) })
.catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here .catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here
} }
}

View File

@@ -3097,6 +3097,11 @@
resolved "https://registry.yarnpkg.com/@types/line-column/-/line-column-1.0.0.tgz#fa5a59c21e885fef3739a273b43dacf55b63437f" resolved "https://registry.yarnpkg.com/@types/line-column/-/line-column-1.0.0.tgz#fa5a59c21e885fef3739a273b43dacf55b63437f"
integrity sha512-wbw+IDRw/xY/RGy+BL6f4Eey4jsUgHQrMuA4Qj0CSG3x/7C2Oc57pmRoM2z3M4DkylWRz+G1pfX06sCXQm0J+w== integrity sha512-wbw+IDRw/xY/RGy+BL6f4Eey4jsUgHQrMuA4Qj0CSG3x/7C2Oc57pmRoM2z3M4DkylWRz+G1pfX06sCXQm0J+w==
"@types/lodash@^4.14.170":
version "4.14.170"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
"@types/long@^4.0.0": "@types/long@^4.0.0":
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
@@ -4822,6 +4827,11 @@ brorand@^1.0.1, brorand@^1.1.0:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
browser-fs-access@^0.17.2:
version "0.17.2"
resolved "https://registry.yarnpkg.com/browser-fs-access/-/browser-fs-access-0.17.2.tgz#6debfe35ebce77a1eecca7822a449c740a8de9db"
integrity sha512-z3H37rU3fmKkGJ1r09v3hAwByUI0vYIh8a7LsUaQnnMxdVUJm1UzsffvNK6Qnvk9jGqvY7uiX2DPu4BZXavcWA==
browser-process-hrtime@^1.0.0: browser-process-hrtime@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"