2021-04-07 12:21:09 +08:00
|
|
|
|
|
|
|
|
2021-04-20 09:11:28 +08:00
|
|
|
import React, { useEffect, useReducer, useState } from 'react';
|
2021-04-07 12:21:09 +08:00
|
|
|
|
|
|
|
import { useDispatch, useSelector } from 'react-redux'
|
|
|
|
|
2021-04-19 15:30:29 +08:00
|
|
|
import { FaEdit, FaLinkedin, FaGithub } from 'react-icons/fa'
|
2021-04-19 11:53:02 +08:00
|
|
|
import { MdSave, MdFolder, MdInsertDriveFile } from 'react-icons/md'
|
2021-04-17 13:58:54 +08:00
|
|
|
|
2021-04-07 12:21:09 +08:00
|
|
|
import * as Icon from "./icons";
|
2021-04-17 11:54:01 +08:00
|
|
|
import { Dialog } from './dialog'
|
2021-04-20 09:11:28 +08:00
|
|
|
import { DropDown } from './dropDown'
|
2021-04-19 11:53:02 +08:00
|
|
|
import { STLExport, saveFile, openFile, verifyPermission } from './fileHelpers'
|
2021-04-17 21:32:14 +08:00
|
|
|
|
2021-04-17 11:54:01 +08:00
|
|
|
export const NavBar = () => {
|
2021-04-07 12:21:09 +08:00
|
|
|
const dispatch = useDispatch()
|
2021-04-19 11:53:02 +08:00
|
|
|
const sketchActive = useSelector(state => state.ui.sketchActive)
|
|
|
|
const treeEntries = useSelector(state => state.treeEntries)
|
2021-04-19 03:14:01 +08:00
|
|
|
const fileHandle = useSelector(state => state.ui.fileHandle)
|
2021-04-19 11:53:02 +08:00
|
|
|
const modified = useSelector(state => state.ui.modified)
|
2021-04-13 09:37:16 +08:00
|
|
|
|
|
|
|
const boolOp = (code) => {
|
2021-04-19 15:30:29 +08:00
|
|
|
if (sc.selected.length != 2 || !sc.selected.every(e => e.userData.type == 'mesh')) {
|
|
|
|
alert('please first select two bodies for boolean operation')
|
|
|
|
return
|
|
|
|
}
|
2021-04-13 09:37:16 +08:00
|
|
|
const [m1, m2] = sc.selected
|
2021-04-19 11:53:02 +08:00
|
|
|
|
|
|
|
const mesh = sc.boolOp(m1, m2, code)
|
|
|
|
|
|
|
|
sc.obj3d.add(mesh)
|
|
|
|
|
|
|
|
dispatch({
|
|
|
|
type: 'set-entry-visibility', obj: {
|
|
|
|
[m1.name]: false,
|
|
|
|
[m2.name]: false,
|
|
|
|
[mesh.name]: true,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
dispatch({
|
|
|
|
type: 'rx-boolean', mesh, deps: [m1.name, m2.name]
|
|
|
|
})
|
|
|
|
|
|
|
|
|
2021-04-13 09:37:16 +08:00
|
|
|
sc.render()
|
|
|
|
forceUpdate()
|
|
|
|
}
|
2021-04-15 06:46:30 +08:00
|
|
|
|
2021-04-21 18:54:57 +08:00
|
|
|
|
2021-04-19 15:30:29 +08:00
|
|
|
const addSketch = () => {
|
|
|
|
const sketch = sc.addSketch()
|
|
|
|
if (!sketch) {
|
|
|
|
alert('please select a plane or 3 points to define sketch plane')
|
|
|
|
return
|
|
|
|
}
|
2021-04-19 11:53:02 +08:00
|
|
|
|
|
|
|
dispatch({ type: 'rx-sketch', obj: sketch })
|
|
|
|
|
|
|
|
sketch.activate()
|
|
|
|
|
|
|
|
sc.render()
|
|
|
|
|
2021-04-17 11:54:01 +08:00
|
|
|
dispatch({ type: 'set-dialog', action: 'sketch' })
|
|
|
|
|
2021-04-15 06:46:30 +08:00
|
|
|
forceUpdate()
|
2021-04-13 09:37:16 +08:00
|
|
|
}
|
|
|
|
|
2021-04-19 11:53:02 +08:00
|
|
|
const confirmDiscard = () => !modified ? true : confirm('Discard changes? All changes will be lost.')
|
|
|
|
|
2021-04-19 03:14:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
2021-04-19 11:53:02 +08:00
|
|
|
useEffect(() => {
|
|
|
|
const onBeforeUnload = (e) => {
|
|
|
|
if (modified ||
|
|
|
|
(sc.activeSketch &&
|
|
|
|
(
|
|
|
|
sc.activeSketch.hasChanged
|
|
|
|
|| sc.activeSketch.idOnActivate != id
|
|
|
|
|| sc.activeSketch.c_idOnActivate != sc.activeSketch.c_id
|
|
|
|
)
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
e.preventDefault();
|
|
|
|
e.returnValue = `There are unsaved changes. Are you sure you want to leave?`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
window.addEventListener('beforeunload', onBeforeUnload)
|
|
|
|
return () => window.removeEventListener('beforeunload', onBeforeUnload)
|
|
|
|
}, [modified])
|
|
|
|
|
2021-04-17 18:25:43 +08:00
|
|
|
useEffect(() => { // hacky way to handle mounting and unmounting mouse listeners for feature mode
|
2021-04-19 11:53:02 +08:00
|
|
|
if (!sketchActive) {
|
2021-04-17 18:25:43 +08:00
|
|
|
sc.canvas.addEventListener('pointermove', sc.onHover)
|
|
|
|
sc.canvas.addEventListener('pointerdown', sc.onPick)
|
|
|
|
return () => {
|
|
|
|
sc.canvas.removeEventListener('pointermove', sc.onHover)
|
|
|
|
sc.canvas.removeEventListener('pointerdown', sc.onPick)
|
|
|
|
}
|
|
|
|
}
|
2021-04-19 11:53:02 +08:00
|
|
|
}, [sketchActive])
|
2021-04-07 12:21:09 +08:00
|
|
|
|
2021-04-17 11:54:01 +08:00
|
|
|
const sketchModeButtons = [
|
|
|
|
[Icon.Extrude, () => {
|
|
|
|
dispatch({ type: 'set-dialog', action: 'extrude', target: sc.activeSketch })
|
2021-04-17 21:32:14 +08:00
|
|
|
|
2021-04-20 09:11:28 +08:00
|
|
|
}, 'Extrude'],
|
|
|
|
[Icon.Dimension, () => sc.activeSketch.command('d'), 'Dimension (D)'],
|
|
|
|
[Icon.Line, () => sc.activeSketch.command('l'), 'Line (L)'],
|
|
|
|
[Icon.Arc, () => sc.activeSketch.command('a'), 'Arc (A)'],
|
|
|
|
[Icon.Coincident, () => sc.activeSketch.command('c'), 'Coincident (C)'],
|
|
|
|
[Icon.Vertical, () => sc.activeSketch.command('v'), 'Vertical (V)'],
|
|
|
|
[Icon.Horizontal, () => sc.activeSketch.command('h'), 'Horizontal (H)'],
|
|
|
|
[Icon.Tangent, () => sc.activeSketch.command('t'), 'Tangent (T)'],
|
2021-04-19 11:53:02 +08:00
|
|
|
[MdSave,
|
|
|
|
async () => {
|
2021-04-19 15:30:29 +08:00
|
|
|
if (await verifyPermission(fileHandle) === false) return
|
2021-04-19 11:53:02 +08:00
|
|
|
sc.refreshNode(sc.activeSketch.obj3d.name, treeEntries)
|
|
|
|
sc.activeSketch.clearSelection()
|
|
|
|
saveFile(fileHandle, JSON.stringify([id, sc.sid, sc.mid, treeEntries]), dispatch)
|
|
|
|
sc.render()
|
|
|
|
sc.activeSketch.setClean()
|
|
|
|
}
|
2021-04-22 04:21:26 +08:00
|
|
|
, 'Save'],
|
2021-04-14 04:39:33 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
2021-04-17 11:54:01 +08:00
|
|
|
const partModeButtons = [
|
2021-04-19 15:30:29 +08:00
|
|
|
[FaEdit, addSketch, 'Sketch'],
|
2021-04-17 11:54:01 +08:00
|
|
|
[Icon.Extrude, () => {
|
2021-04-19 15:30:29 +08:00
|
|
|
if (sc.selected[0] && treeEntries.byId[sc.selected[0].name].userData.type == 'sketch') {
|
|
|
|
dispatch({ type: 'set-dialog', action: 'extrude', target: treeEntries.byId[sc.selected[0].name] })
|
|
|
|
} else {
|
|
|
|
alert('please select a sketch from the left pane extrude')
|
|
|
|
}
|
|
|
|
|
2021-04-19 11:53:02 +08:00
|
|
|
}, 'Extrude'],
|
2021-04-17 11:54:01 +08:00
|
|
|
|
2021-04-14 04:39:33 +08:00
|
|
|
[Icon.Union, () => boolOp('u'), 'Union'],
|
|
|
|
[Icon.Subtract, () => boolOp('s'), 'Subtract'],
|
|
|
|
[Icon.Intersect, () => boolOp('i'), 'Intersect'],
|
2021-04-19 06:05:33 +08:00
|
|
|
[MdInsertDriveFile, () => {
|
2021-04-19 11:53:02 +08:00
|
|
|
if (!confirmDiscard()) return
|
2021-04-19 06:05:33 +08:00
|
|
|
sc.newPart()
|
|
|
|
dispatch({ type: 'new-part' })
|
|
|
|
sc.render()
|
2021-04-19 11:53:02 +08:00
|
|
|
}, 'New'],
|
2021-04-19 06:05:33 +08:00
|
|
|
[MdSave,
|
|
|
|
() => {
|
2021-04-19 11:53:02 +08:00
|
|
|
saveFile(fileHandle, JSON.stringify([id, sc.sid, sc.mid, treeEntries]), dispatch)
|
2021-04-19 06:05:33 +08:00
|
|
|
}
|
2021-04-19 11:53:02 +08:00
|
|
|
, 'Save'],
|
2021-04-19 06:05:33 +08:00
|
|
|
[MdFolder, () => {
|
2021-04-19 11:53:02 +08:00
|
|
|
if (!confirmDiscard()) return
|
2021-04-19 06:05:33 +08:00
|
|
|
openFile(dispatch).then(
|
2021-04-19 11:53:02 +08:00
|
|
|
sc.render
|
2021-04-19 06:05:33 +08:00
|
|
|
)
|
|
|
|
}, 'Open'],
|
|
|
|
[Icon.Stl, () => {
|
2021-04-19 15:30:29 +08:00
|
|
|
if (sc.selected[0] && sc.selected[0].userData.type == 'mesh') {
|
|
|
|
STLExport(fileHandle ? fileHandle.name.replace(/\.[^/.]+$/, "") : 'untitled')
|
|
|
|
} else {
|
|
|
|
alert('please first select one body to export')
|
|
|
|
}
|
|
|
|
}, 'Export to STL'],
|
2021-04-07 12:21:09 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
const [_, forceUpdate] = useReducer(x => x + 1, 0);
|
|
|
|
|
2021-04-20 09:11:28 +08:00
|
|
|
return <div className='topNav flex justify-center items-center bg-gray-800'>
|
|
|
|
|
|
|
|
{/* <div className='w-auto h-full flex-1 flex items-center justify-end'> */}
|
|
|
|
<div className='w-auto h-full flex-1 flex items-center justify-end md:justify-between'>
|
|
|
|
<div className='w-100 h-full items-center font-mono text-lg text-gray-200 select-none hidden md:flex mr-8'>
|
|
|
|
<Icon.Logo className='w-auto h-6 mx-1' />
|
|
|
|
three.cad
|
|
|
|
</div>
|
|
|
|
<div className='h-full w-48 flex items-center justify-end'>
|
|
|
|
<Dialog />
|
|
|
|
</div>
|
2021-04-17 11:54:01 +08:00
|
|
|
</div>
|
2021-04-20 09:11:28 +08:00
|
|
|
<div className='w-auto h-full flex'>
|
2021-04-22 04:21:26 +08:00
|
|
|
{(sketchActive ? sketchModeButtons : partModeButtons).map(
|
|
|
|
([Icon, fcn, txt], idx) => (
|
|
|
|
Icon !== undefined ?
|
|
|
|
<Icon className="btn text-gray-200 w-auto h-full p-3.5" tooltip={txt}
|
|
|
|
onClick={fcn} key={idx}
|
|
|
|
/> :
|
|
|
|
<div className="w-12 h-full"></div>
|
2021-04-20 09:11:28 +08:00
|
|
|
))
|
2021-04-17 13:58:54 +08:00
|
|
|
}
|
|
|
|
</div>
|
2021-04-20 09:11:28 +08:00
|
|
|
<div className='w-auto h-full flex-1 items-center justify-end flex-shrink-1 hidden lg:flex'>
|
|
|
|
<DropDown />
|
2021-04-19 15:30:29 +08:00
|
|
|
<a href='https://github.com/twpride/threeCAD' className='h-full w=auto'>
|
|
|
|
<FaGithub className="btn-green w-auto h-full p-3.5"></FaGithub>
|
|
|
|
</a>
|
|
|
|
<a href='https://www.linkedin.com/in/howard-hwang-b3000335' className='h-full w=auto'>
|
|
|
|
<FaLinkedin className="btn-green w-auto h-full p-3.5"></FaLinkedin>
|
|
|
|
</a>
|
2021-04-17 11:54:01 +08:00
|
|
|
</div>
|
|
|
|
|
2021-04-07 12:21:09 +08:00
|
|
|
</div>
|
2021-04-19 03:14:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-04-20 09:11:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
|