semi working angle

master
howard 2021-04-12 18:37:16 -07:00
parent 67d6bf8090
commit 8aeb5409c4
10 changed files with 500 additions and 42 deletions

View File

@ -111,8 +111,8 @@ export class Scene {
helpersGroup.add(this.axes); helpersGroup.add(this.axes);
const dist = 50 const dist = 15
const light1 = new THREE.PointLight(color.lighting, 0.7); const light1 = new THREE.PointLight(color.lighting, 0.6);
light1.position.set(dist, dist, dist); light1.position.set(dist, dist, dist);
helpersGroup.add(light1); helpersGroup.add(light1);
const light2 = new THREE.PointLight(color.lighting, 0.6); const light2 = new THREE.PointLight(color.lighting, 0.6);
@ -292,7 +292,7 @@ export class Scene {
} }
subtract(m1, m2) { subtract(m1, m2, op) {
let bspA = CSG.fromMesh(m1) let bspA = CSG.fromMesh(m1)
let bspB = CSG.fromMesh(m2) let bspB = CSG.fromMesh(m2)
m1.visible = false m1.visible = false
@ -302,13 +302,30 @@ export class Scene {
// // Subtract one bsp from the other via .subtract... other supported modes are .union and .intersect // // Subtract one bsp from the other via .subtract... other supported modes are .union and .intersect
let bspResult = bspA.subtract(bspB) let bspResult, opChar;
switch (op) {
case 's':
bspResult = bspA.subtract(bspB)
opChar = "-"
break;
case 'u':
bspResult = bspA.union(bspB)
opChar = "\u222a"
break;
case 'i':
bspResult = bspA.intersect(bspB)
opChar = "\u2229"
break;
default:
break;
}
// //Get the resulting mesh from the result bsp, and assign meshA.material to the resulting mesh // //Get the resulting mesh from the result bsp, and assign meshA.material to the resulting mesh
let mesh = CSG.toMesh(bspResult, m1.matrix, m1.material) let mesh = CSG.toMesh(bspResult, m1.matrix, m1.material)
mesh.userData.type = 'mesh' mesh.userData.type = 'mesh'
mesh.name = `${m1.name}-${m2.name}`
mesh.name = `(${m1.name}${opChar}${m2.name})`
mesh.layers.enable(1) mesh.layers.enable(1)

View File

@ -11,6 +11,7 @@ import { get3PtArc } from './drawArc'
import { replacer, reviver } from './utils' import { replacer, reviver } from './utils'
import { AxesHelper } from './sketchAxes' import { AxesHelper } from './sketchAxes'
import { drawDimension, _onMoveDimension, setDimLines, updateDim } from './drawDimension'; import { drawDimension, _onMoveDimension, setDimLines, updateDim } from './drawDimension';
import { drawAngle, _onMoveAngle, setAngLines, updateAng } from './drawAngle';
@ -128,9 +129,13 @@ class Sketch {
this.drawOnClick2 = drawOnClick2.bind(this); this.drawOnClick2 = drawOnClick2.bind(this);
this.drawDimension = drawDimension.bind(this) this.drawDimension = drawDimension.bind(this)
this.drawAngle = drawAngle.bind(this)
this._onMoveDimension = _onMoveDimension.bind(this) this._onMoveDimension = _onMoveDimension.bind(this)
this._onMoveAngle = _onMoveAngle.bind(this)
this.setDimLines = setDimLines.bind(this) this.setDimLines = setDimLines.bind(this)
this.setAngLines = setAngLines.bind(this)
this.updateDim = updateDim.bind(this) this.updateDim = updateDim.bind(this)
this.updateAng = updateAng.bind(this)
this.awaitSelection = awaitSelection.bind(this); this.awaitSelection = awaitSelection.bind(this);
@ -152,7 +157,7 @@ class Sketch {
this.setDimLines() this.setDimLines()
this.obj3d.traverse(e=>e.layers.enable(2)) this.obj3d.traverse(e => e.layers.enable(2))
this.obj3d.visible = true this.obj3d.visible = true
this.scene.axes.matrix = this.obj3d.matrix this.scene.axes.matrix = this.obj3d.matrix
this.scene.axes.visible = true this.scene.axes.visible = true
@ -168,7 +173,7 @@ class Sketch {
this.store.dispatch({ type: 'exit-sketch' }) this.store.dispatch({ type: 'exit-sketch' })
this.labelContainer.innerHTML = "" this.labelContainer.innerHTML = ""
this.obj3d.visible = false this.obj3d.visible = false
this.obj3d.traverse(e=>e.layers.disable(2)) this.obj3d.traverse(e => e.layers.disable(2))
this.scene.axes.visible = false this.scene.axes.visible = false
this.scene.activeSketch = null this.scene.activeSketch = null
} }
@ -221,6 +226,10 @@ class Sketch {
this.drawDimension() this.drawDimension()
this.mode = "" this.mode = ""
break; break;
case 'q':
this.drawAngle()
this.mode = ""
break;
case 'p': case 'p':
this.canvas.addEventListener('pointerdown', this.drawOnClick1) this.canvas.addEventListener('pointerdown', this.drawOnClick1)
this.mode = "point" this.mode = "point"
@ -427,9 +436,9 @@ class Sketch {
this.linkedObjs.size, links_buffer) this.linkedObjs.size, links_buffer)
/* /*
- loop to update all the children that are points - loop to update all the children that are points
- why +6? we skip first two triplets because it refers to a non-geometry children - why +6? we skip first two triplets because it refers to a non-geometry children
- we also sneak in updating lines children as well, by checking when ptsBuf[ptr] is NaN - we also sneak in updating lines children as well, by checking when ptsBuf[ptr] is NaN
*/ */
for (let i = 3, ptr = (pts_buffer >> 2) + 9; i < this.obj3d.children.length; i += 1, ptr += 3) { for (let i = 3, ptr = (pts_buffer >> 2) + 9; i < this.obj3d.children.length; i += 1, ptr += 3) {
@ -455,8 +464,9 @@ class Sketch {
/* /*
arcs were not updated in above loop, we go through all arcs linkedObjs arcs were not updated in above loop, we go through all arcs linkedObjs
and updated based on the control pts (which were updated in loop above) and updated based on the control pts (which were updated in loop above)
*/ */
for (let [k, obj] of this.linkedObjs) { for (let [k, obj] of this.linkedObjs) {
if (obj[0] != 'arc') continue; if (obj[0] != 'arc') continue;
const [p1, p2, c, arc] = obj[1].map(e => this.obj3d.children[this.objIdx.get(e)]) const [p1, p2, c, arc] = obj[1].map(e => this.obj3d.children[this.objIdx.get(e)])
@ -472,7 +482,8 @@ class Sketch {
} }
this.setDimLines() // this.setDimLines()
this.setAngLines()
this.obj3d.dispatchEvent({ type: 'change' }) this.obj3d.dispatchEvent({ type: 'change' })
} }

395
src/drawAngle.js Normal file
View File

@ -0,0 +1,395 @@
import * as THREE from '../node_modules/three/src/Three';
import { color } from './shared'
const lineMaterial = new THREE.LineBasicMaterial({
linewidth: 2,
color: color.dimension,
})
const pointMaterial = new THREE.PointsMaterial({
color: color.dimension,
size: 4,
})
const divisions = 12
export async function drawAngle() {
let selection = await this.awaitSelection({ line: 2 })
if (selection == null) return;
const line = new THREE.LineSegments(
new THREE.BufferGeometry().setAttribute('position',
new THREE.Float32BufferAttribute(Array((divisions + 2) * 2 * 3).fill(-0.001), 3)
),
lineMaterial.clone()
);
const point = new THREE.Points(
new THREE.BufferGeometry().setAttribute('position',
new THREE.Float32BufferAttribute(3, 3)
),
pointMaterial.clone()
)
line.userData.ids = selection.map(e => e.name)
line.layers.enable(2)
point.layers.enable(2)
let angle = getAngle(selection)
this.obj3d.children[1].add(line).add(point)
const onMove = this._onMoveAngle(point, line)
point.label = document.createElement('div');
point.label.textContent = angle.toFixed(3);
point.label.contentEditable = true;
this.labelContainer.append(point.label)
let onEnd, onKey;
let add = await new Promise((res) => {
onEnd = (e) => res(true)
onKey = (e) => e.key == 'Escape' && res(false)
this.canvas.addEventListener('pointermove', onMove)
this.canvas.addEventListener('pointerdown', onEnd)
window.addEventListener('keydown', onKey)
})
this.canvas.removeEventListener('pointermove', onMove)
this.canvas.removeEventListener('pointerdown', onEnd)
window.removeEventListener('keydown', onKey)
point.geometry.computeBoundingSphere()
line.geometry.computeBoundingSphere()
if (add) {
this.constraints.set(++this.c_id,
[
'angle', angle,
[-1, -1, selection[0].name, selection[1].name]
]
)
selection[0].userData.constraints.push(this.c_id)
selection[1].userData.constraints.push(this.c_id)
this.updateOtherBuffers()
line.name = this.c_id
line.userData.type = 'dimension'
point.name = this.c_id
point.userData.type = 'dimension'
point.label.addEventListener('focus', this.updateAng(this.c_id))
} else {
this.obj3d.children[1].children.splice(this.obj3d.children[1].length - 2, 2).forEach(
e => {
e.geometry.dispose()
e.material.dispose()
}
)
this.labelContainer.removeChild(this.labelContainer.lastChild);
sc.render()
}
return
}
export function updateAng(c_id) {
return (ev_focus) => {
let 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)
value = ent[1]
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()
}
})
}
}
let ids, _l1, _l2
export function _onMoveAngle(point, line) {
ids = line.userData.ids
_l1 = this.obj3d.children[this.objIdx.get(ids[0])].geometry.attributes.position.array
_l2 = this.obj3d.children[this.objIdx.get(ids[1])].geometry.attributes.position.array
let loc;
return (e) => {
loc = this.getLocation(e)
p3.set(loc.x, loc.y)
update(
line.geometry.attributes.position,
point.geometry.attributes.position,
_l1, _l2
)
// point.userData.offset = tagOffset.toArray() // save offset vector from center
point.userData.offset = tagOffset // save offset vector from center
tagOffset = undefined
sc.render()
}
}
export function setAngLines() {
const restoreLabels = this.labelContainer.childElementCount == 0;
const dims = this.obj3d.children[1].children
let point, dist;
for (let i = 0; i < dims.length; i += 2) {
if (restoreLabels) {
point = dims[i + 1] // point node is at i+1
dist = this.constraints.get(point.name)[1]
point.label = document.createElement('div');
point.label.textContent = dist.toFixed(3);
point.label.contentEditable = true;
this.labelContainer.append(point.label)
point.label.addEventListener('focus', this.updateAng(this.c_id))
}
ids = dims[i].userData.ids
_l1 = this.obj3d.children[this.objIdx.get(ids[0])].geometry.attributes.position.array
_l2 = this.obj3d.children[this.objIdx.get(ids[1])].geometry.attributes.position.array
tagOffset = dims[i + 1].userData.offset
update(
dims[i].geometry.attributes.position,
dims[i + 1].geometry.attributes.position,
_l1,
_l2
)
}
}
export function findIntersection(q, s, p, r) {
/*
Based on: https://stackoverflow.com/questions/563198/
q+s p+r
\/__________ q+u*s
/\
/ \
p q
u = (q p) × r / (r × s)
when r × s = 0, the lines are either colinear or parallel
function returns u
for "real" intersection to exist, 0<u<1
*/
const q_minus_p = q.clone().sub(p);
const r_cross_s = r.cross(s);
if (r_cross_s === 0) return null; //either colinear or parallel
return q_minus_p.cross(r) / r_cross_s;
}
/*
_l2:[x0,y0,z0,x1,y1,z1]
/
p3:tag-""/-.
tagOffset[1]-->| \
|__ . _|__ _l1:[x0,y0,z0,x1,y1,z1]
tagOffset[0]----^ ^--center
vecArr = [
0: _l1 origin
1: _l1 disp
2: _l2 origin
3: _l2 disp
4: center
5: tag disp from center
]
*/
const vecArr = Array(6)
for (var i = 0; i < vecArr.length; i++) vecArr[i] = new THREE.Vector2();
const a = Array(3)
const p3 = new THREE.Vector2()
let tagOffset
const getAngle = (Obj3dLines) => {
for (let i = 0; i < 2; i++) {
const arr = Obj3dLines[i].geometry.attributes.position.array
vecArr[2 * i].set(...arr.slice(0, 2))
vecArr[2 * i + 1].set(arr[3] - arr[0], arr[4] - arr[1])
}
const a1 = Math.atan2(vecArr[1].y, vecArr[1].x)
const a2 = Math.atan2(vecArr[3].y, vecArr[3].x)
let deltaAngle = Math.abs(a2 - a1)
if (deltaAngle > Math.PI) {
deltaAngle = Math.PI * 2 - deltaAngle
}
return deltaAngle / Math.PI * 180
}
function update(linegeom, pointgeom, _l1, _l2) {
let i = 0;
for (; i < 4;) {
const arr = i == 0 ? _l1 : _l2
vecArr[i++].set(arr[0], arr[1])
vecArr[i++].set(arr[3] - arr[0], arr[4] - arr[1])
}
const centerScalar = findIntersection(...vecArr.slice(0, 4))
const center = vecArr[i++].addVectors(vecArr[0], vecArr[1].clone().multiplyScalar(centerScalar))
// tagOffset = vecArr[i++].subVectors(p3, center)
if (tagOffset === undefined) {
tagOffset = vecArr[i++].subVectors(p3, center)
// } else if (Array.isArray(tagOffset)) {
// tagOffset = new THREE.Vector2(tagOffset[0],tagOffset[1])
} else {
p3.addVectors(center, tagOffset)
}
// console.log(p3, center, 'vr')
// console.log(vecArr, 'vecArr')
// console.log(tagOffset, xx)
// console.log(tagOffset.length())
const tagRadius = tagOffset.length()
/*
if tag is more than 90 deg away from midline, we shift everything by 180
a: array that describes absolute angular position of angle start, angle end, and tag
a[2]:
tag a[1]:angle end
\ | /
\ | /
___\|/___ a[0]+dA/2:midline
/ \
/ \
/ \
a[0]:angle start
*/
for (let j = 1, i = 0; j < vecArr.length; j += 2, i++) {
a[i] = Math.atan2(vecArr[j].y, vecArr[j].x)
}
let dA = unreflex(a[1] - a[0])
let tagtoMidline = unreflex(a[2] - (a[0] + dA / 2))
let shift = Math.abs(tagtoMidline) < Math.PI / 2 ? 0 : Math.PI;
let tA1 = unreflex(a[2] - (a[0] + shift))
let tA2 = unreflex(a[2] - (a[0] + dA + shift))
let a1, deltaAngle;
if (dA * tA1 < 0) {
a1 = a[0] + tA1 + shift
deltaAngle = dA - tA1
} else if (dA * tA2 > 0) {
a1 = a[0] + shift
deltaAngle = dA + tA2
} else {
a1 = a[0] + shift
deltaAngle = dA
}
let points = linegeom.array
let d = 0;
points[d++] = center.x + tagRadius * Math.cos(a1)
points[d++] = center.y + tagRadius * Math.sin(a1)
d++
const angle = a1 + (1 / divisions) * deltaAngle
points[d++] = center.x + tagRadius * Math.cos(angle)
points[d++] = center.y + tagRadius * Math.sin(angle)
d++
for (i = 2; i <= divisions; i++) {
points[d++] = points[d - 4]
points[d++] = points[d - 4]
d++
const angle = a1 + (i / divisions) * deltaAngle
points[d++] = center.x + tagRadius * Math.cos(angle)
points[d++] = center.y + tagRadius * Math.sin(angle)
d++
}
for (i = 0; i < 2; i++) {
points[d++] = vecArr[2 * i].x
points[d++] = vecArr[2 * i].y
d++
points[d++] = center.x + tagRadius * Math.cos(a[i] + shift)
points[d++] = center.y + tagRadius * Math.sin(a[i] + shift)
d++
}
linegeom.needsUpdate = true;
pointgeom.array.set(p3.toArray())
pointgeom.needsUpdate = true;
}
const twoPi = Math.PI * 2
const negTwoPi = - Math.PI * 2
const negPi = - Math.PI
function unreflex(angle) {
if (angle > Math.PI) {
angle = negTwoPi + angle
} else if (angle < negPi) {
angle = twoPi + angle
}
return angle
}

View File

@ -83,9 +83,38 @@ export function get3PtArc(p1, p2, c, divisions = n) {
const radius = Math.sqrt(v1[0] ** 2 + v1[1] ** 2) const radius = Math.sqrt(v1[0] ** 2 + v1[1] ** 2)
let deltaAngle = a2 - a1 let deltaAngle = a2 - a1
if (deltaAngle <=0) deltaAngle += Math.PI*2 if (deltaAngle <=0) deltaAngle += Math.PI*2
// console.log(deltaAngle)
// let deltaAngle = a2 - a1
// if (deltaAngle > Math.PI ){
// deltaAngle = - Math.PI*2 + deltaAngle
// } else if (deltaAngle < -Math.PI) {
// deltaAngle = Math.PI*2 + deltaAngle
// }
// let deltaAngle = Math.abs(a2 - a1)
// if (deltaAngle > Math.PI){
// deltaAngle = Math.PI*2 - deltaAngle
// }
let points = new Float32Array((divisions + 1) * 3)
for (let d = 0; d <= divisions; d++) {
const angle = a1 + (d / divisions) * deltaAngle;
points[3 * d] = c[0] + radius * Math.cos(angle);
points[3 * d + 1] = c[1] + radius * Math.sin(angle);
}
return points;
}
export function getAngleArc(a1, a2, c, radius, divisions = n) {
let deltaAngle = a2 - a1
let points = new Float32Array((divisions + 1) * 3) let points = new Float32Array((divisions + 1) * 3)

View File

@ -1,5 +1,5 @@
import * as THREE from '../node_modules/three/src/Three'; import * as THREE from '../node_modules/three/src/Three';
import { color, ptObj } from './shared' import { color} from './shared'
export function extrude(sketch) { export function extrude(sketch) {
let constraints = sketch.constraints; let constraints = sketch.constraints;
@ -36,7 +36,7 @@ export function extrude(sketch) {
) )
] ]
if (d == -1 || d == node) continue; if (d == -1 || d == node) continue;
if (d == children[1]) { if (d == children[4]) {
console.log('pair found') console.log('pair found')
}; };
findTouching(d) findTouching(d)
@ -52,7 +52,7 @@ export function extrude(sketch) {
if (c == -1) continue; if (c == -1) continue;
const d = children[objIdx.get(c)] const d = children[objIdx.get(c)]
if (d == node) continue; if (d == node) continue;
if (d == children[1]) { if (d == children[4]) {
console.log('loop found') console.log('loop found')
} else { } else {
if (!visited.has(d)) { if (!visited.has(d)) {

View File

@ -11,7 +11,7 @@ body {
font-family: sans-serif; font-family: sans-serif;
overflow: hidden; overflow: hidden;
--topNavH: 48px; --topNavH: 48px;
--sideNavW: 200px; --sideNavW: 240px;
} }
#c { #c {

View File

@ -15,6 +15,20 @@ export const NavBar = () => {
const treeEntries = useSelector(state => state.treeEntries) const treeEntries = useSelector(state => state.treeEntries)
const activeSketchId = useSelector(state => state.treeEntries.activeSketchId) const activeSketchId = useSelector(state => state.treeEntries.activeSketchId)
const boolOp = (code) => {
if (sc.selected.length != 2 || !sc.selected.every(e => e.userData.type == 'mesh')) return
const [m1, m2] = sc.selected
const mesh = sc.subtract(m1, m2, code)
dispatch({ type: 'rx-boolean', mesh, deps: [m1.name, m2.name] })
sc.render()
forceUpdate()
}
const extrude = () => {
console.log(treeEntries.tree[activeSketchId])
sc.extrude(treeEntries.byId[activeSketchId])
}
useEffect(() => { useEffect(() => {
if (!activeSketchId) { if (!activeSketchId) {
sc.canvas.addEventListener('pointermove', sc.onHover) sc.canvas.addEventListener('pointermove', sc.onHover)
@ -39,19 +53,10 @@ export const NavBar = () => {
}, 'Finish'] : }, 'Finish'] :
[FaEdit, sc.addSketch, 'Sketch [s]'] [FaEdit, sc.addSketch, 'Sketch [s]']
, ,
[FaCube, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Extrude [e]'], [FaCube, extrude , 'Extrude [e]'],
[Icon.Union, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Union'], [Icon.Union, ()=>boolOp('u'), 'Union'],
[Icon.Subtract, () => { [Icon.Subtract, ()=>boolOp('s'), 'Subtract'],
if (sc.selected.length != 2 || !sc.selected.every(e => e.userData.type == 'mesh')) return [Icon.Intersect, ()=>boolOp('i'), 'Intersect'],
// console.log('here')
const [m1, m2] = sc.selected
const mesh = sc.subtract(m1, m2)
dispatch({ type: 'rx-boolean', 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 [d]'], [Icon.Dimension, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Dimension [d]'],
[Icon.Line, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Line [l]'], [Icon.Line, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Line [l]'],
[Icon.Arc, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Arc [a]'], [Icon.Arc, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Arc [a]'],

View File

@ -92,7 +92,7 @@ export function reducer(state = {}, action) {
return update(state, { return update(state, {
treeEntries: { $set: obj } treeEntries: { $merge: obj }
}) })

View File

@ -48,12 +48,10 @@ const TreeEntry = ({ entId }) => {
const [_, forceUpdate] = useReducer(x => x + 1, 0); const [_, forceUpdate] = useReducer(x => x + 1, 0);
// const vis = obj3d.layers.mask & 1
return <div className='btn select-none flex justify-start w-full h-7 items-center text-sm' return <div className='btn select-none flex justify-start w-full h-7 items-center text-sm'
onDoubleClick={() => { onDoubleClick={() => {
if (entId[0] == 's') { if (obj3d.userData.type == 'sketch') {
activeSketchId && treeEntries[activeSketchId].deactivate() activeSketchId && treeEntries[activeSketchId].deactivate()
sketch.activate() sketch.activate()
sc.clearSelection() sc.clearSelection()
@ -67,14 +65,13 @@ const TreeEntry = ({ entId }) => {
sc.render() sc.render()
}} }}
onPointerLeave={() => { onPointerLeave={() => {
// console.log('activeid',activeSketchId,'visstate',visState) if (visible & obj3d.userData.type == 'sketch') return
if (visible & entId[0] == 's') return
if (sc.selected.includes(obj3d) || activeSketchId == obj3d.name) return if (sc.selected.includes(obj3d) || activeSketchId == obj3d.name) return
sc.setHover(obj3d, 0) sc.setHover(obj3d, 0)
sc.render() sc.render()
}} }}
onClick={() => { onClick={() => {
if (entId[0] == 'm') { if (obj3d.userData.type == 'mesh') {
sc.selected.push( sc.selected.push(
obj3d obj3d
) )

View File

@ -14,26 +14,30 @@ boolean flesh out refresh / replace mesh
- consume skeches after extrude // done - consume skeches after extrude // done
- selection hover disspates when rehovered //fixed - selection hover disspates when rehovered //fixed
- boolean unable to select click //fixed - boolean unable to select click //fixed
- hover sync between tree and work area // done, punt on stretch
vertical // done vertical // done
horizontal // done horizontal // done
- select sketch for extrusion, punt, leverage current sketch modality
- hover sync between tree and work area
- select sketch for extrusion
auto update extrude auto update extrude
extrude dialogue extrude dialogue
loopfind loopfind
button panel cleanup
file save, stl export file save, stl export
constriant buttons ,tangent, angle button panel cleanup
constraint angle
3 pt arc
constraint labels
reattach sketch reattach sketch
constraint labels, tangent
auto snap auto snap
tree ent renaming and better default names tree ent renaming and better default names