fix dirty file save logic

master
howard 2021-04-18 20:53:02 -07:00
parent 132be08553
commit 662ced6edd
11 changed files with 205 additions and 118 deletions

File diff suppressed because one or more lines are too long

1
example_parts/test3.json Normal file

File diff suppressed because one or more lines are too long

1
example_parts/test4.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -36,12 +36,12 @@ export class Scene {
this.sid = 1
this.mid = 1
this.store = store;
this.canvas = document.querySelector('#c');
this.rect = this.canvas.getBoundingClientRect().toJSON()
this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas });
this.store = store
const size = 1;
const near = 0;
@ -178,11 +178,6 @@ export class Scene {
}
saveScene() {
return JSON.stringify([id, this.sid, this.mid, this.store.getState().treeEntries])
}
clearScene() {
const deleted = this.obj3d.children.splice(1)
@ -240,7 +235,6 @@ export class Scene {
}
}
this.store.dispatch({ type: 'restore-state', state })
return state
}
@ -262,17 +256,10 @@ export class Scene {
this.obj3d.add(mesh)
this.store.dispatch({ type: 'rx-extrusion', mesh, sketchId: sketch.obj3d.name })
if (this.activeSketch == sketch) {
this.store.dispatch({ type: 'finish-sketch' })
sketch.deactivate()
}
this.render()
return mesh
}
boolOp(m1, m2, op, refresh = false) {
boolOp(m1, m2, op) {
let bspA = CSG.fromMesh(m1)
let bspB = CSG.fromMesh(m2)
m1.visible = false
@ -312,33 +299,16 @@ export class Scene {
mesh.add(vertices)
if (!refresh) {
sc.obj3d.add(mesh)
this.store.dispatch({
type: 'set-entry-visibility', obj: {
[m1.name]: false,
[m2.name]: false,
[mesh.name]: true,
}
})
this.store.dispatch({
type: 'rx-boolean', mesh, deps: [m1.name, m2.name]
})
} else {
return mesh
}
return mesh
}
refreshNode(id) {
refreshNode(id, { byId, tree }) {
let curId
let que = [id]
let idx = 0
// let newNodes = {}
const { byId, tree } = this.store.getState().treeEntries
while (idx < que.length) {
curId = que[idx++]
@ -348,7 +318,7 @@ export class Scene {
if (info.length == 2) {
newNode = extrude(byId[info[0]], info[1])
} else if (info.length == 3) {
newNode = this.boolOp(byId[info[0]], byId[info[1]], info[2], true)
newNode = this.boolOp(byId[info[0]], byId[info[1]], info[2])
}
byId[curId].geometry.copy(newNode.geometry)
byId[curId].geometry.parameters = newNode.geometry.parameters // took 2 hours to figure out
@ -433,14 +403,9 @@ async function addSketch() {
this.clearSelection()
sketch.obj3d.addEventListener('change', this.render);
this.store.dispatch({ type: 'rx-sketch', obj: sketch })
sketch.activate()
this.render()
return sketch
}

View File

@ -9,7 +9,6 @@ import { onHover, onDrag, onPick, onRelease, clearSelection} from './mouseEvents
import { setCoincident, setOrdinate, setTangent } from './constraintEvents'
import { get3PtArc } from './drawArc'
import { replacer, reviver } from './utils'
import { AxesHelper } from './sketchAxes'
import { drawDimension, _onMoveDimension, setDimLines, updateDim } from './drawDimension';
@ -19,7 +18,6 @@ class Sketch {
constructor(scene, preload) {
// [0]:x, [1]:y, [2]:z
this.ptsBuf = new Float32Array(this.max_pts * 3).fill(NaN)
@ -96,7 +94,6 @@ class Sketch {
this.camera = scene.camera
this.canvas = scene.canvas
this.rect = scene.rect
this.store = scene.store;
@ -153,15 +150,28 @@ class Sketch {
}
setClean() {
this.hasChanged = false
this.idOnActivate = id
this.c_idOnActivate = this.c_id
const changeDetector = (e) => {
if (this.selected.length && e.buttons) {
this.canvas.removeEventListener('pointermove', changeDetector)
this.hasChanged = true
}
}
this.canvas.addEventListener('pointermove', changeDetector)
}
activate() {
console.log('activate sketch')
window.addEventListener('keydown', this.onKeyPress)
this.canvas.addEventListener('pointerdown', this.onPick)
this.canvas.addEventListener('pointermove', this.onHover)
this.store.dispatch({ type: 'set-active-sketch', activeSketchId: this.obj3d.name })
this.setDimLines()
this.obj3d.traverse(e => e.layers.enable(2))
@ -172,21 +182,12 @@ class Sketch {
window.sketcher = this
// overkill but good solution if this check was more costly
this.hasChanged = false
this.idOnActivate = id
this.c_idOnActivate = this.c_id
// console.log(this,this.selected)
const changeDetector = (e) => {
if (this.selected.length && e.buttons) {
this.canvas.removeEventListener('pointermove', changeDetector)
this.hasChanged = true
}
}
this.canvas.addEventListener('pointermove', changeDetector)
this.setClean()
}
deactivate() {
console.log('deactivate')
window.removeEventListener('keydown', this.onKeyPress)
this.canvas.removeEventListener('pointerdown', this.onPick)
this.canvas.removeEventListener('pointermove', this.onHover)

View File

@ -10,34 +10,42 @@ import * as Icon from "./icons";
export const Dialog = () => {
const dialog = useSelector(state => state.ui.dialog)
const treeEntries = useSelector(state => state.treeEntries)
const dispatch = useDispatch()
const ref = useRef()
useEffect(() => {
console.log(dialog)
if (!ref.current) return
ref.current.focus()
}, [dialog])
const extrude = () => {
sc.extrude(dialog.target, ref.current.value)
sc.render()
const mesh = sc.extrude(dialog.target, ref.current.value)
dispatch({ type: 'rx-extrusion', mesh, sketchId: dialog.target.obj3d.name })
if (sc.activeSketch == dialog.target) {
dispatch({ type: 'finish-sketch' })
dialog.target.deactivate()
}
dispatch({ type: "clear-dialog" })
sc.render()
}
const extrudeEdit = () => {
dialog.target.userData.featureInfo[1] = ref.current.value
sc.refreshNode(dialog.target.name)
sc.refreshNode(dialog.target.name, treeEntries)
dispatch({ type: 'set-modified', status: true })
sc.render()
dispatch({ type: "clear-dialog" })
sc.render()
}
@ -53,7 +61,13 @@ export const Dialog = () => {
onClick={extrude}
/>
<MdClose className="btn w-auto h-full p-3.5 mr-6"
onClick={() => dispatch({ type: "clear-dialog" })}
onClick={() => {
if (sc.activeSketch == dialog.target) { // if extrude dialog launched from sketch mode we set dialog back to the sketch dialog
dispatch({ type: 'set-dialog', action: 'sketch' })
} else {
dispatch({ type: "clear-dialog" })
}
}}
/>
</>
case 'extrude-edit':
@ -79,7 +93,9 @@ export const Dialog = () => {
|| sc.activeSketch.idOnActivate != id
|| sc.activeSketch.c_idOnActivate != sc.activeSketch.c_id
) {
sc.refreshNode(sc.activeSketch.obj3d.name)
sc.refreshNode(sc.activeSketch.obj3d.name, treeEntries)
dispatch({ type: 'set-modified', status: true })
}
dispatch({ type: 'finish-sketch' })
@ -96,9 +112,10 @@ export const Dialog = () => {
|| sc.activeSketch.c_idOnActivate != sc.activeSketch.c_id
) {
dispatch({ type: "restore-sketch" })
} else {
dispatch({ type: 'finish-sketch' })
// dispatch({ type: 'set-modified', status: false })
}
dispatch({ type: 'finish-sketch' })
sc.activeSketch.deactivate()
sc.render()

View File

@ -16,12 +16,14 @@ var tzoffset = (new Date()).getTimezoneOffset() * 60000;
export function STLExport(filename) {
if (sc.selected[0] && sc.selected[0].userData.type == 'mesh') {
const result = STLexp.parse(sc.selected[0], { binary: true });
const time = (new Date(Date.now() - tzoffset)).toISOString().slice(0, -5).replace(/:/g, '-');
saveLegacy(new Blob([result], { type: 'model/stl' }), `${filename}_${time}.stl`);
} else {
alert('please select one body to export')
}
}
@ -48,7 +50,7 @@ export async function saveFileAs(file, dispatch) {
const opts = {
types: [{
// description: 'Text file',
description: 'Text file',
accept: { 'application/json': ['.json'] },
}],
};
@ -114,11 +116,12 @@ export async function openFile(dispatch) {
try {
const file = await fileHandle.getFile();
const text = await file.text();;
sc.loadState(text)
dispatch({ type: 'restore-state', state: sc.loadState(text) })
dispatch({ type: 'set-file-handle', fileHandle })
// app.setModified(false);
// app.setFocus(true);
} catch (ex) {
const msg = `An error occured reading ${fileHandle}`;
console.error(msg, ex);
@ -127,3 +130,30 @@ export async function openFile(dispatch) {
};
export function confirmDiscard(modified) {
if (!modified) {
return true;
}
const confirmMsg = 'Discard changes? All changes will be lost.';
return confirm(confirmMsg);
};
export async function verifyPermission(fileHandle) {
const opts = {
mode:'readwrite'
};
// Check if we already have permission, if so, return true.
if (await fileHandle.queryPermission(opts) === 'granted') {
return true;
}
// Request permission to the file, if the user grants permission, return true.
if (await fileHandle.requestPermission(opts) === 'granted') {
return true;
}
// The user did nt grant permission, return false.
return false;
}

View File

@ -4,43 +4,88 @@ import React, { useEffect, useReducer } from 'react';
import { useDispatch, useSelector } from 'react-redux'
import { FaEdit, FaFileDownload } from 'react-icons/fa'
import { MdSave, MdFolder, MdFileUpload, MdInsertDriveFile } from 'react-icons/md'
import { FaRegFolderOpen, FaFile } from 'react-icons/fa'
import { FaEdit } from 'react-icons/fa'
import { MdSave, MdFolder, MdInsertDriveFile } from 'react-icons/md'
import * as Icon from "./icons";
import { Dialog } from './dialog'
import { STLExport, saveFile, openFile } from './fileHelpers'
import { STLExport, saveFile, openFile, verifyPermission } from './fileHelpers'
export const NavBar = () => {
const dispatch = useDispatch()
const activeSketchId = useSelector(state => state.treeEntries.activeSketchId)
const treeEntriesById = useSelector(state => state.treeEntries.byId)
const sketchActive = useSelector(state => state.ui.sketchActive)
const treeEntries = useSelector(state => state.treeEntries)
const fileHandle = useSelector(state => state.ui.fileHandle)
const modified = useSelector(state => state.ui.modified)
const boolOp = (code) => {
if (sc.selected.length != 2 || !sc.selected.every(e => e.userData.type == 'mesh')) return
const [m1, m2] = sc.selected
sc.boolOp(m1, m2, code)
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]
})
sc.render()
forceUpdate()
}
const addSketch = () => {
sc.addSketch()
const addSketch = async () => {
const sketch = await sc.addSketch()
dispatch({ type: 'rx-sketch', obj: sketch })
sketch.activate()
sc.render()
dispatch({ type: 'set-dialog', action: 'sketch' })
forceUpdate()
}
const confirmDiscard = () => !modified ? true : confirm('Discard changes? All changes will be lost.')
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])
useEffect(() => { // hacky way to handle mounting and unmounting mouse listeners for feature mode
if (!activeSketchId) {
if (!sketchActive) {
sc.canvas.addEventListener('pointermove', sc.onHover)
sc.canvas.addEventListener('pointerdown', sc.onPick)
return () => {
@ -48,11 +93,10 @@ export const NavBar = () => {
sc.canvas.removeEventListener('pointerdown', sc.onPick)
}
}
}, [activeSketchId])
}, [sketchActive])
const sketchModeButtons = [
[Icon.Extrude, () => {
dispatch({ type: 'finish-sketch' })
dispatch({ type: 'set-dialog', action: 'extrude', target: sc.activeSketch })
}, 'Extrude [e]'],
@ -63,32 +107,43 @@ export const NavBar = () => {
[Icon.Vertical, () => sc.activeSketch.command('v'), 'Vertical [v]'],
[Icon.Horizontal, () => sc.activeSketch.command('h'), 'Horizontal [h]'],
[Icon.Tangent, () => sc.activeSketch.command('t'), 'Tangent [t]'],
[Icon.Tangent, () => sc.activeSketch.command('t'), 'Tangent [t]'],
[MdSave,
async () => {
if(await verifyPermission(fileHandle) === false) return
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()
}
, 'Save']
]
const partModeButtons = [
[FaEdit, addSketch, 'Sketch [s]'],
[Icon.Extrude, () => {
dispatch({ type: 'set-dialog', action: 'extrude', target: treeEntriesById[sc.selected[0].name] })
}, 'Extrude [e]'],
dispatch({ type: 'set-dialog', action: 'extrude', target: treeEntries.byId[sc.selected[0].name] })
}, 'Extrude'],
[Icon.Union, () => boolOp('u'), 'Union'],
[Icon.Subtract, () => boolOp('s'), 'Subtract'],
[Icon.Intersect, () => boolOp('i'), 'Intersect'],
[MdInsertDriveFile, () => {
if (!confirmDiscard()) return
sc.newPart()
dispatch({ type: 'new-part' })
sc.render()
}, 'New [ctrl+n]'],
}, 'New'],
[MdSave,
() => {
saveFile(fileHandle, sc.saveScene(), dispatch)
saveFile(fileHandle, JSON.stringify([id, sc.sid, sc.mid, treeEntries]), dispatch)
}
, 'Save [ctrl+s]'],
, 'Save'],
[MdFolder, () => {
if (!confirmDiscard()) return
openFile(dispatch).then(
()=>sc.render()
sc.render
)
}, 'Open'],
[Icon.Stl, () => {
@ -106,7 +161,7 @@ export const NavBar = () => {
</div>
<div className='w-auto h-full flex-none'>
{
activeSketchId ?
sketchActive ?
sketchModeButtons.map(([Icon, fcn, txt, shortcut], idx) => (
<Icon className="btn w-auto h-full p-3.5" tooltip={txt}
onClick={fcn} key={idx}

View File

@ -10,7 +10,6 @@ const defaultState = {
tree: {},
order: {},
visible: {},
activeSketchId: ""
}
let cache
@ -33,21 +32,19 @@ export function treeEntries(state = defaultState, action) {
}
case 'set-active-sketch':
cache = JSON.stringify(state.byId[action.activeSketchId])
cache = JSON.stringify(action.sketch)
return update(state, {
visible: { [action.activeSketchId]: { $set: true } },
activeSketchId: { $set: action.activeSketchId },
visible: { [action.sketch.obj3d.name]: { $set: true } },
})
case 'finish-sketch':
return update(state, {
activeSketchId: { $set: "" },
visible: { [state.activeSketchId]: { $set: false } },
visible: { [sc.activeSketch.obj3d.name]: { $set: false } },
})
case 'restore-sketch':
const sketch = sc.loadSketch(cache)
const deletedObj = sc.obj3d.children.splice(state.order[state.activeSketchId] + 1, 1,
const deletedObj = sc.obj3d.children.splice(state.order[sc.activeSketch.obj3d.name] + 1, 1,
sketch.obj3d
)[0]
@ -59,9 +56,7 @@ export function treeEntries(state = defaultState, action) {
sc.activeSketch = sketch
return update(state, {
activeSketchId: { $set: "" },
byId: { [state.activeSketchId]: { $set: sketch } },
visible: { [state.activeSketchId]: { $set: false } },
byId: { [sc.activeSketch.obj3d.name]: { $set: sketch } },
})
case 'rx-extrusion':
@ -108,7 +103,18 @@ export function treeEntries(state = defaultState, action) {
export function ui(state = { dialog: {}, filePane: false }, action) {
switch (action.type) {
case 'set-active-sketch':
return update(state, {
sketchActive: { $set: true },
})
case 'rx-sketch':
return update(state, {
sketchActive: { $set: true },
})
case 'finish-sketch':
return update(state, {
sketchActive: { $set: false },
})
case 'set-dialog':
return update(state, {
dialog: { $set: { target: action.target, action: action.action } },
@ -127,6 +133,18 @@ export function ui(state = { dialog: {}, filePane: false }, action) {
fileHandle: { $set: null },
modified: { $set: false },
})
case 'set-modified':
return update(state, {
modified: { $set: action.status },
})
case 'delete-node':
return update(state, {
modified: { $set: true },
})
case 'rx-extrusion':
return update(state, {
modified: { $set: true },
})
default:
return state
}

View File

@ -8,10 +8,12 @@ import { FaCube, FaEdit } from 'react-icons/fa'
export const Tree = () => {
const treeEntries = useSelector(state => state.treeEntries)
const ref = useRef()
const fileHandle = useSelector(state => state.ui.fileHandle)
return <div className='sideNav flex flex-col bg-gray-800'>
<input className='w-16 text-gray-50 h-7 mx-1 border-0 focus:outline-none bg-transparent' type="text" defaultValue="untitled" step="0.1" ref={ref} />
<div className='w-16 text-gray-50 h-7 mx-1 border-0 focus:outline-none bg-transparent'>
{fileHandle ? fileHandle.name.replace(/\.[^/.]+$/, "") : 'untitled'}
</div>
{treeEntries.allIds.map((entId, idx) => (
<TreeEntry key={idx} entId={entId} />
))}
@ -55,7 +57,11 @@ const TreeEntry = ({ entId }) => {
dispatch({ type: 'finish-sketch' })
sc.activeSketch.deactivate()
}
sketch.activate()
dispatch({ type: 'set-active-sketch', sketch })
sc.clearSelection()
sc.activeSketch = sketch;
dispatch({ type: 'set-dialog', action: 'sketch' })
@ -120,14 +126,6 @@ const TreeEntry = ({ entId }) => {
e.stopPropagation()
}}
/>
{/* <MdRefresh className='btn-green h-full w-auto p-1.5'
onClick={(e) => {
e.stopPropagation()
sc.refreshNode(entId)
sc.render()
}}
/> */}
{
visible ?

View File

@ -36,6 +36,7 @@ auto update extrude // done
extrude edit dialog // done
file save, stl export// done
-unable cancel out of new sketches //fixed seemingly
-sometimes unable to hit return and change dimensionk
-unable to delete arc