From 3b49000e2b807561dc387f0c6773b174290f7dbf Mon Sep 17 00:00:00 2001 From: howard Date: Mon, 5 Apr 2021 21:52:19 -0700 Subject: [PATCH] working dependency tree --- package-lock.json | 13 ++++ package.json | 1 + src/app.jsx | 70 ++++++++++++-------- src/depTree copy.js | 102 +++++++++++++++++++++++++++++ src/depTree.mjs | 120 ++++++++++++++++++++++++++++++++++ src/extrude.js | 2 +- src/index.js | 72 +++++++++++++------- src/sketcher/Sketch.js | 3 +- src/sketcher/drawDimension.js | 85 ++++++++++-------------- src/utils/mouseEvents.js | 60 ++++++++++------- todo.txt | 28 ++++++-- 11 files changed, 424 insertions(+), 132 deletions(-) create mode 100644 src/depTree copy.js create mode 100644 src/depTree.mjs diff --git a/package-lock.json b/package-lock.json index 8c5e888..4bb79f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "babel-loader": "^8.2.2", "css-loader": "^5.1.3", "gh-pages": "^3.1.0", + "immutability-helper": "^3.1.1", "mini-css-extract-plugin": "^1.4.0", "postcss": "^8.2.9", "postcss-loader": "^5.2.0", @@ -3633,6 +3634,12 @@ "postcss": "^8.1.0" } }, + "node_modules/immutability-helper": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz", + "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -11406,6 +11413,12 @@ "dev": true, "requires": {} }, + "immutability-helper": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz", + "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", diff --git a/package.json b/package.json index 242f612..e85352f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "babel-loader": "^8.2.2", "css-loader": "^5.1.3", "gh-pages": "^3.1.0", + "immutability-helper": "^3.1.1", "mini-css-extract-plugin": "^1.4.0", "postcss": "^8.2.9", "postcss-loader": "^5.2.0", diff --git a/src/app.jsx b/src/app.jsx index 24de728..b76b1f2 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -5,8 +5,7 @@ import './app.css' import { Provider, useDispatch, useSelector } from 'react-redux' import { FaCube, FaEdit } from 'react-icons/fa' -import { MdEdit, MdDone, MdVisibilityOff, MdVisibility } from 'react-icons/md' -import { RiShape2Fill } from 'react-icons/ri' +import { MdEdit, MdDone, MdVisibilityOff, MdVisibility, MdDelete } from 'react-icons/md' import * as Icon from "./icons"; import { color } from './utils/shared' @@ -22,16 +21,11 @@ export const Root = ({ store }) => ( const App = () => { const dispatch = useDispatch() const treeEntries = useSelector(state => state.treeEntries) - const activeSketchNid = useSelector(state => state.activeSketchNid) - - // const [state, setState] = useState('x') - // useEffect(()=>{ - // console.log('hereeee') - // },[state]) + const activeSketchId = useSelector(state => state.activeSketchId) useEffect(() => { - if (!activeSketchNid) { + if (!activeSketchId) { sc.canvas.addEventListener('pointermove', sc.onHover) sc.canvas.addEventListener('pointerdown', sc.onPick) return () => { @@ -39,27 +33,38 @@ const App = () => { sc.canvas.removeEventListener('pointerdown', sc.onPick) } } - }, [activeSketchNid]) + }, [activeSketchId]) const btnz = [ - activeSketchNid ? + activeSketchId ? [MdDone, () => { - treeEntries.byNid[activeSketchNid].deactivate() + treeEntries.byId[activeSketchId].deactivate() sc.activeSketch = null // sc.activeDim = this.activeSketch.obj3d.children[1].children }, 'Finish'] : [FaEdit, sc.addSketch, 'Sketch'] , - [FaCube, () => sc.extrude(treeEntries.byNid[activeSketchNid]), 'Extrude'], - [Icon.Union, () => sc.extrude(treeEntries.byNid[activeSketchNid]), 'Union'], - [Icon.Subtract, subtract, 'Subtract'], - [Icon.Intersect, () => sc.extrude(treeEntries.byNid[activeSketchNid]), 'Intersect'], - [Icon.Dimension, () => sc.extrude(treeEntries.byNid[activeSketchNid]), 'Dimension'], - [Icon.Line, () => sc.extrude(treeEntries.byNid[activeSketchNid]), 'Line'], - [Icon.Arc, () => sc.extrude(treeEntries.byNid[activeSketchNid]), 'Arc'], + [FaCube, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Extrude'], + [Icon.Union, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Union'], + [Icon.Subtract, ()=> { + if (sc.selected.length != 2 || !sc.selected.every(e => e.userData.type == 'mesh')) return + // console.log('here') + const [m1, m2] = sc.selected + const mesh = subtract(m1,m2) + + dispatch({ type: 'rx-extrusion', mesh, deps:[m1.name,m2.name] }) + sc.render() + forceUpdate() + }, 'Subtract'], + [Icon.Intersect, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Intersect'], + [Icon.Dimension, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Dimension'], + [Icon.Line, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Line'], + [Icon.Arc, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Arc'], ] + const [_, forceUpdate] = useReducer(x => x + 1, 0); + return
{ btnz.map(([Icon, fcn, txt], idx) => ( @@ -73,7 +78,7 @@ const App = () => { }
- {treeEntries.allNids.map((entId, idx) => ( + {treeEntries.allIds.map((entId, idx) => ( ))}
@@ -84,9 +89,10 @@ const App = () => { const TreeEntry = ({ entId }) => { - const treeEntries = useSelector(state => state.treeEntries.byNid) + const treeEntries = useSelector(state => state.treeEntries.byId) + const dispatch = useDispatch() - const activeSketchNid = useSelector(state => state.activeSketchNid) + const activeSketchId = useSelector(state => state.activeSketchId) let obj3d, entry; @@ -105,7 +111,7 @@ const TreeEntry = ({ entId }) => { return
{ - activeSketchNid && treeEntries[activeSketchNid].deactivate() + activeSketchId && treeEntries[activeSketchId].deactivate() entry.activate() sc.activeSketch = entry; }} @@ -113,6 +119,13 @@ const TreeEntry = ({ entId }) => {
+
{ + dispatch({type:'delete-node',id:entId}) + }} + > + +
{ vis ?
{ } -const subtract = () => { +const subtract = (m1,m2) => { // //Create a bsp tree from each of the meshes // console.log(sc.selected.length != 2 || !sc.selected.every(e => e.userData.type == 'mesh'), "wtf") - if (sc.selected.length != 2 || !sc.selected.every(e => e.userData.type == 'mesh')) return - // console.log('here') - const [m1, m2] = sc.selected + let bspA = BoolOp.fromMesh(m1) let bspB = BoolOp.fromMesh(m2) @@ -184,9 +195,12 @@ const subtract = () => { // //Get the resulting mesh from the result bsp, and assign meshA.material to the resulting mesh let meshResult = BoolOp.toMesh(bspResult, m1.matrix, m1.material) + meshResult.userData.type = 'mesh' + meshResult.userData.name = `${m1.name}-${m2.name}` sc.obj3d.add(meshResult) - sc.render() + + return meshResult } diff --git a/src/depTree copy.js b/src/depTree copy.js new file mode 100644 index 0000000..5c5700e --- /dev/null +++ b/src/depTree copy.js @@ -0,0 +1,102 @@ +class DepTree { + constructor() { + this.tree = {} + this.order = {} + this.arr = [] + } + + addParent(id) { + if (this.tree[id]) return + this.tree[id] = new Set() + this.order[id] = this.arr.length + this.arr.push(id) + } + + addChild(childId, ...parentIds) { + for (let parentId of parentIds) { + this.tree[parentId].add(childId) + } + + this.addParent(childId) + } + + + deleteNode(id) { + this.visited = new Set() + + this.nodesToDel = [] + this.dfs(id) + + const idx = [] + this.nodesToDel.sort((a, b) => this.order[b] - this.order[a]) + + for (let id of this.nodesToDel) { + idx.push(this.order[id]) + this.arr.splice(this.order[id], 1) + delete this.tree[id] + delete this.order[id] + } + + for (let i = idx[idx.length - 1]; i < this.arr.length; i++) { + this.order[this.arr[i]] = i + } + + const nodeToDelSet = new Set(this.nodesToDel) + + for (let k in this.tree) { + for (let ent of this.tree[k]) { + if (nodeToDelSet.has(ent)) { + this.tree[k].delete(ent) + } + } + } + + return idx + } + + + dfs(id) { + this.visited.add(id) + this.nodesToDel.push(id) + for (let x of this.tree[id]) { + if (!this.visited.has(x)) { + this.dfs(x) + } + } + } + +} + + +const dt = new DepTree() + + +dt.addParent('r1') +dt.addParent('r2') + + +dt.addChild('r3', 'r1', 'r2') + +dt.addParent('r4') + +dt.addChild('r5', 'r4', 'r3') + +dt.addChild('r6', 'r1', 'r5') + +dt.addChild('r7', 'r3', 'r5') + + +// const x = dt.deleteNode('r3') + +// console.log(x) +// console.log(dt.arr, dt.order) + +// expectd output +// [ 6, 5, 4, 2 ] +// [ 'r1', 'r2', 'r4' ] { r1: 0, r2: 1, r4: 2 } + + +const x = dt.deleteNode('r5') + +console.log(x) +console.log(dt.tree) \ No newline at end of file diff --git a/src/depTree.mjs b/src/depTree.mjs new file mode 100644 index 0000000..837a59a --- /dev/null +++ b/src/depTree.mjs @@ -0,0 +1,120 @@ +export class DepTree { + constructor(obj) { + if (obj) { + console.log('load', obj) + this.order = { ...obj.order } + this.byId = { ...obj.byId } + this.allIds = [...obj.allIds] + + this.tree = {} + for (let k in obj.tree) { + this.tree[k] = { ...obj.tree[k] } + } + + } else { + this.tree = {} + this.order = {} + this.allIds = [] + } + } + + addParent(id) { + if (this.tree[id]) return + this.tree[id] = {} + this.order[id] = this.allIds.length + this.allIds.push(id) + } + + addChild(childId, ...parentIds) { + for (let parentId of parentIds) { + this.tree[parentId][childId] = true; + } + + this.addParent(childId) + } + + + deleteNode(id) { + const dfs = (id) => { + visited.add(id) + nodesToDel.push(id) + for (let k in this.tree[id]) { + if (!visited.has(k)) { + dfs(k) + } + } + } + + const visited = new Set() + + const nodesToDel = [] + dfs(id) + + nodesToDel.sort((a, b) => this.order[b] - this.order[a]) + + + let spliceIdx; + for (let id of nodesToDel) { + spliceIdx = this.order[id] + + this.allIds.splice(spliceIdx, 1) + + const deletedObj = sc.obj3d.children.splice(spliceIdx + 4, 1)[0] + + deletedObj.traverse((obj)=>{ + if (obj.geometry) obj.geometry.dispose() + if (obj.material) obj.material.dispose() + }) + + delete this.tree[id] + delete this.byId[id] + delete this.order[id] + } + + for (let i = spliceIdx; i < this.allIds.length; i++) { + this.order[this.allIds[i]] = i + } + + const nodeToDelSet = new Set(nodesToDel) + for (let k in this.tree) { + for (let m in this.tree[k]) { + if (nodeToDelSet.has(m)) { + delete this.tree[k][m] + } + } + } + return this + } +} + + + + +// const dt = new DepTree() +// dt.addParent('r1') +// dt.addParent('r2') +// dt.addChild('r3', 'r1', 'r2') +// dt.addParent('r4') +// dt.addChild('r5', 'r4', 'r3') +// dt.addChild('r6', 'r1', 'r5') +// dt.addChild('r7', 'r3', 'r5') + +// dt.addParent('r8') + +// // console.log(dt) + +// // const x = dt.deleteNode('r3') +// // console.log(x) +// // console.log(dt.allIds, dt.order) +// // [ 6, 5, 4, 2 ] +// // [ 'r1', 'r2', 'r4' ] { r1: 0, r2: 1, r4: 2 } + + +// const x = dt.deleteNode('r5') +// console.log(dt) +// DepTree { +// tree: { r1: { r3: true }, r2: { r3: true }, r3: {}, r4: {}, r8: {} }, +// order: { r1: 0, r2: 1, r3: 2, r4: 3, r8: 4 }, +// allIds: [ 'r1', 'r2', 'r3', 'r4', 'r8' ] +// } + diff --git a/src/extrude.js b/src/extrude.js index 669fa60..81e1354 100644 --- a/src/extrude.js +++ b/src/extrude.js @@ -98,7 +98,7 @@ export function extrude(sketch) { this.render() // sketch.visible = false - this.store.dispatch({ type: 'rx-extrusion', mesh, sketch }) + this.store.dispatch({ type: 'rx-extrusion', mesh, sketchId: sketch.obj3d.name }) } diff --git a/src/index.js b/src/index.js index 1fd835d..9b0fafa 100644 --- a/src/index.js +++ b/src/index.js @@ -2,43 +2,64 @@ import ReactDOM from 'react-dom' import React from 'react' import { Root } from './app.jsx' - +import update from 'immutability-helper'; import { createStore, applyMiddleware } from 'redux' import logger from 'redux-logger' -let _entId = 0 +import { DepTree } from './depTree.mjs' + + + function reducer(state = {}, action) { switch (action.type) { case 'rx-sketch': - return { - ...state, + return update(state, { treeEntries: { - byNid: { ...state.treeEntries.byNid, [action.obj.obj3d.name]: action.obj }, - allNids: [...state.treeEntries.allNids, action.obj.obj3d.name] - } - } + byId: { [action.obj.obj3d.name]: { $set: action.obj } }, + allIds: { $push: [action.obj.obj3d.name] }, + tree: { [action.obj.obj3d.name]: { $set: {} } }, + order: { [action.obj.obj3d.name]: { $set: state.treeEntries.allIds.length } } + }, + }) + case 'set-active-sketch': - return { - ...state, activeSketchNid: action.sketch - } + return update(state, { + activeSketchId: { $set: action.sketch }, + }) case 'exit-sketch': return { - ...state, activeSketchNid: '' + ...state, activeSketchId: '' } case 'rx-extrusion': - return { - ...state, + + return update(state, { treeEntries: { - byNid: { ...state.treeEntries.byNid, [action.mesh.name]: action.mesh }, - allNids: [...state.treeEntries.allNids, action.mesh.name] - }, - mesh2sketch: { - ...state.mesh2sketch, - [action.sketch.obj3d.name]: action.mesh.name + byId: { + [action.mesh.name]: { $set: action.mesh } + }, + allIds: { $push: [action.mesh.name] }, + tree: { + [action.sketchId]: { [action.mesh.name]: { $set: true } }, + }, + order: { [action.mesh.name]: { $set: state.treeEntries.allIds.length } } } - } + }) + + case 'delete-node': + + const depTree = new DepTree(state.treeEntries) + + const obj = depTree.deleteNode(action.id) + + + return update(state, { + treeEntries: {$set: obj} + }) + + + case 'restore-state': return action.state default: @@ -48,12 +69,13 @@ function reducer(state = {}, action) { - const preloadedState = { treeEntries: { - byNid: {}, - allNids: [] - } + byId: {}, + allIds: [], + tree: {}, + order: {}, + }, } diff --git a/src/sketcher/Sketch.js b/src/sketcher/Sketch.js index 70dd07c..191cc41 100644 --- a/src/sketcher/Sketch.js +++ b/src/sketcher/Sketch.js @@ -9,7 +9,7 @@ import { get3PtArc } from './drawArc' import { _vec2, _vec3, raycaster, awaitPts } from '../utils/shared' import { replacer, reviver } from '../utils/mapJSONReplacer' import { AxesHelper } from '../utils/axes' -import { drawDimension, _onMoveDimension, setDimLines } from './drawDimension'; +import { drawDimension, _onMoveDimension, setDimLines, updateDim } from './drawDimension'; @@ -114,6 +114,7 @@ class Sketch { this.drawDimension = drawDimension.bind(this) this._onMoveDimension = _onMoveDimension.bind(this) this.setDimLines = setDimLines.bind(this) + this.updateDim = updateDim.bind(this) this.awaitPts = awaitPts.bind(this); diff --git a/src/sketcher/drawDimension.js b/src/sketcher/drawDimension.js index 599d586..8f561e3 100644 --- a/src/sketcher/drawDimension.js +++ b/src/sketcher/drawDimension.js @@ -87,29 +87,9 @@ export async function drawDimension() { point.name = this.c_id point.userData.type = 'dimension' - const updateDim = (c_id) => (ev_focus) => { - const value = ev_focus.target.textContent - console.log(value) - document.addEventListener('keydown', (e) => { - if (e.key == 'Enter') { - e.preventDefault() - const ent = this.constraints.get(c_id) - ent[1] = parseFloat(ev_focus.target.textContent) - this.constraints.set(c_id, ent) - this.updateOtherBuffers() - this.solve() - sc.render() - ev_focus.target.blur() - this.updateBoundingSpheres() - } else if (e.key == 'Escape') { - ev_focus.target.textContent = value - getSelection().empty() - ev_focus.target.blur() - } - }) - } - point.label.addEventListener('focus', updateDim(this.c_id)) + + point.label.addEventListener('focus', this.updateDim(this.c_id)) @@ -133,12 +113,39 @@ const p2 = new THREE.Vector2() const p3 = new THREE.Vector2() let dir, hyp, proj, perp, p1e, p2e, nids, _p1, _p2; + + +export function updateDim(c_id) { + return (ev_focus) => { + const value = ev_focus.target.textContent + document.addEventListener('keydown', (e) => { + if (e.key == 'Enter') { + e.preventDefault() + const ent = this.constraints.get(c_id) + ent[1] = parseFloat(ev_focus.target.textContent) + this.constraints.set(c_id, ent) + this.updateOtherBuffers() + this.solve() + sc.render() + ev_focus.target.blur() + this.updateBoundingSpheres() + } else if (e.key == 'Escape') { + ev_focus.target.textContent = value + getSelection().empty() + ev_focus.target.blur() + } + }) + } +} + + + export function _onMoveDimension(point, line) { nids = line.userData.nids - _p1 = this.obj3d.children[sketcher.objIdx.get(nids[0])].geometry.attributes.position.array - _p2 = this.obj3d.children[sketcher.objIdx.get(nids[1])].geometry.attributes.position.array + _p1 = this.obj3d.children[this.objIdx.get(nids[0])].geometry.attributes.position.array + _p2 = this.obj3d.children[this.objIdx.get(nids[1])].geometry.attributes.position.array p1.set(_p1[0], _p1[1]) p2.set(_p2[0], _p2[1]) @@ -163,27 +170,6 @@ export function _onMoveDimension(point, line) { export function setDimLines() { - const updateDim = (c_id) => (ev_focus) => { - const value = ev_focus.target.textContent - console.log(value) - document.addEventListener('keydown', (e) => { - if (e.key == 'Enter') { - e.preventDefault() - const ent = this.constraints.get(c_id) - ent[1] = parseFloat(ev_focus.target.textContent) - this.constraints.set(c_id, ent) - this.updateOtherBuffers() - this.solve() - sc.render() - ev_focus.target.blur() - this.updateBoundingSpheres() - } else if (e.key == 'Escape') { - ev_focus.target.textContent = value - getSelection().empty() - ev_focus.target.blur() - } - }) - } const restoreLabels = this.labelContainer.childElementCount == 0; @@ -200,18 +186,15 @@ export function setDimLines() { point.label.textContent = dist.toFixed(3); point.label.contentEditable = true; this.labelContainer.append(point.label) - - - point.label.addEventListener('focus', updateDim(this.c_id)) - + point.label.addEventListener('focus', this.updateDim(this.c_id)) } nids = dims[i].userData.nids - _p1 = this.obj3d.children[sketcher.objIdx.get(nids[0])].geometry.attributes.position.array - _p2 = this.obj3d.children[sketcher.objIdx.get(nids[1])].geometry.attributes.position.array + _p1 = this.obj3d.children[this.objIdx.get(nids[0])].geometry.attributes.position.array + _p2 = this.obj3d.children[this.objIdx.get(nids[1])].geometry.attributes.position.array const offset = dims[i + 1].userData.offset diff --git a/src/utils/mouseEvents.js b/src/utils/mouseEvents.js index 0f55b1b..d9c52c0 100644 --- a/src/utils/mouseEvents.js +++ b/src/utils/mouseEvents.js @@ -13,35 +13,51 @@ export function onHover(e) { ); let hoverPts; + let idx = [] + if (this.obj3d.userData.type == 'sketch') { hoverPts = raycaster.intersectObjects([...this.obj3d.children[1].children, ...this.obj3d.children]) - // if (!hoverPts.length) { - // hoverPts = raycaster.intersectObjects(this.obj3d.children) - // } - } else { - hoverPts = raycaster.intersectObjects(this.obj3d.children, true) - } - - // if (hoverDim.length) { - // } - - let idx = [] - if (hoverPts.length) { - let minDist = Infinity; - for (let i = 0; i < hoverPts.length; i++) { - if (!hoverPts[i].distanceToRay) continue; - if (hoverPts[i].distanceToRay < minDist - 0.0001) { - minDist = hoverPts[i].distanceToRay - idx = [i] - } else if (Math.abs(hoverPts[i].distanceToRay - minDist) < 0.0001) { - idx.push(i) + if (hoverPts.length) { + let minDist = Infinity; + for (let i = 0; i < hoverPts.length; i++) { + if (!hoverPts[i].distanceToRay) continue; + if (hoverPts[i].distanceToRay < minDist - 0.0001) { + minDist = hoverPts[i].distanceToRay + idx = [i] + } else if (Math.abs(hoverPts[i].distanceToRay - minDist) < 0.0001) { + idx.push(i) + } } + // console.log(hoverPts, idx) + if (!idx.length) idx.push(0) } - // console.log(hoverPts, idx) - if (!idx.length) idx.push(0) + + + } else { + // hoverPts = raycaster.intersectObjects(this.obj3d.children) + hoverPts = raycaster.intersectObjects(this.obj3d.children,true) + + + // for (let i = 0; i < hoverPts.length; i++) { + // const obj = hoverPts[i].object + // if (obj.userData.type == "mesh" && obj.visible || obj.userData.type == "plane") { + // idx.push(i) + // } + // } + if (hoverPts.length) { + // console.log(hoverPts) + if (!idx.length) idx.push(0) + } + + + + + } + + if (idx.length) { // after filtering, hovered objs still exists if (hoverPts[idx[0]].object != this.hovered[0]) { // if the previous hovered obj is not the same as current diff --git a/todo.txt b/todo.txt index 40e3db9..596d0c3 100644 --- a/todo.txt +++ b/todo.txt @@ -1,11 +1,31 @@ fix css on design tree (a lot of work) \ -clear dim on exit exit sketch / rehydrate when back or after loading -boolean flesh out refresh / replace mesh -dimension to origin +clear dim on exit exit sketch / rehydrate when back or after loading \\\ done + +boolean flesh out refresh / replace mesh / delete mesh + - need to auto hide ( consume) when new boolean created + - create derived part using relationship as name + - sensible default names, like extrude 1, sketch 1, leverage react for this + - hidden bodies messes up hover highlight + +extrude edit dialog. directio and magnitude + fix extrusion loop find + +dimension to origin + reattaching sketch file save stl export angle -other constraints / sprite \ No newline at end of file +other constraints / sprite + + + + + + + + + +finish mode messed up after restore \ No newline at end of file