From 8aeb5409c4c91affb678a03c19c374246fc2f12b Mon Sep 17 00:00:00 2001 From: howard Date: Mon, 12 Apr 2021 18:37:16 -0700 Subject: [PATCH] semi working angle --- src/Scene.js | 27 ++- src/Sketch.js | 25 ++- src/drawAngle.js | 395 +++++++++++++++++++++++++++++++++++++++++++ src/drawArc.js | 31 +++- src/extrude.js | 6 +- src/react/app.css | 2 +- src/react/navBar.jsx | 31 ++-- src/react/reducer.js | 2 +- src/react/tree.jsx | 9 +- todo.txt | 14 +- 10 files changed, 500 insertions(+), 42 deletions(-) create mode 100644 src/drawAngle.js diff --git a/src/Scene.js b/src/Scene.js index 1c58282..7a20303 100644 --- a/src/Scene.js +++ b/src/Scene.js @@ -111,8 +111,8 @@ export class Scene { helpersGroup.add(this.axes); - const dist = 50 - const light1 = new THREE.PointLight(color.lighting, 0.7); + const dist = 15 + const light1 = new THREE.PointLight(color.lighting, 0.6); light1.position.set(dist, dist, dist); helpersGroup.add(light1); 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 bspB = CSG.fromMesh(m2) 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 - 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 let mesh = CSG.toMesh(bspResult, m1.matrix, m1.material) mesh.userData.type = 'mesh' - mesh.name = `${m1.name}-${m2.name}` + + mesh.name = `(${m1.name}${opChar}${m2.name})` mesh.layers.enable(1) diff --git a/src/Sketch.js b/src/Sketch.js index 47e1cba..ab058bb 100644 --- a/src/Sketch.js +++ b/src/Sketch.js @@ -11,6 +11,7 @@ import { get3PtArc } from './drawArc' import { replacer, reviver } from './utils' import { AxesHelper } from './sketchAxes' 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.drawDimension = drawDimension.bind(this) + this.drawAngle = drawAngle.bind(this) this._onMoveDimension = _onMoveDimension.bind(this) + this._onMoveAngle = _onMoveAngle.bind(this) this.setDimLines = setDimLines.bind(this) + this.setAngLines = setAngLines.bind(this) this.updateDim = updateDim.bind(this) + this.updateAng = updateAng.bind(this) this.awaitSelection = awaitSelection.bind(this); @@ -152,7 +157,7 @@ class Sketch { this.setDimLines() - this.obj3d.traverse(e=>e.layers.enable(2)) + this.obj3d.traverse(e => e.layers.enable(2)) this.obj3d.visible = true this.scene.axes.matrix = this.obj3d.matrix this.scene.axes.visible = true @@ -168,7 +173,7 @@ class Sketch { this.store.dispatch({ type: 'exit-sketch' }) this.labelContainer.innerHTML = "" 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.activeSketch = null } @@ -221,6 +226,10 @@ class Sketch { this.drawDimension() this.mode = "" break; + case 'q': + this.drawAngle() + this.mode = "" + break; case 'p': this.canvas.addEventListener('pointerdown', this.drawOnClick1) this.mode = "point" @@ -427,9 +436,9 @@ class Sketch { this.linkedObjs.size, links_buffer) /* - - loop to update all the children that are points - - 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 + - loop to update all the children that are points + - 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 */ 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 - 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) { if (obj[0] != 'arc') continue; 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' }) } diff --git a/src/drawAngle.js b/src/drawAngle.js new file mode 100644 index 0000000..26619b4 --- /dev/null +++ b/src/drawAngle.js @@ -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| \ + |__ . _|__ _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 +} \ No newline at end of file diff --git a/src/drawArc.js b/src/drawArc.js index 7154bb0..b68df97 100644 --- a/src/drawArc.js +++ b/src/drawArc.js @@ -83,9 +83,38 @@ export function get3PtArc(p1, p2, c, divisions = n) { const radius = Math.sqrt(v1[0] ** 2 + v1[1] ** 2) + let deltaAngle = a2 - a1 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) diff --git a/src/extrude.js b/src/extrude.js index 21bb1d0..00b5c19 100644 --- a/src/extrude.js +++ b/src/extrude.js @@ -1,5 +1,5 @@ import * as THREE from '../node_modules/three/src/Three'; -import { color, ptObj } from './shared' +import { color} from './shared' export function extrude(sketch) { let constraints = sketch.constraints; @@ -36,7 +36,7 @@ export function extrude(sketch) { ) ] if (d == -1 || d == node) continue; - if (d == children[1]) { + if (d == children[4]) { console.log('pair found') }; findTouching(d) @@ -52,7 +52,7 @@ export function extrude(sketch) { if (c == -1) continue; const d = children[objIdx.get(c)] if (d == node) continue; - if (d == children[1]) { + if (d == children[4]) { console.log('loop found') } else { if (!visited.has(d)) { diff --git a/src/react/app.css b/src/react/app.css index 2ced98c..2329f06 100644 --- a/src/react/app.css +++ b/src/react/app.css @@ -11,7 +11,7 @@ body { font-family: sans-serif; overflow: hidden; --topNavH: 48px; - --sideNavW: 200px; + --sideNavW: 240px; } #c { diff --git a/src/react/navBar.jsx b/src/react/navBar.jsx index c640b56..74fa6b2 100644 --- a/src/react/navBar.jsx +++ b/src/react/navBar.jsx @@ -15,6 +15,20 @@ export const NavBar = () => { const treeEntries = useSelector(state => state.treeEntries) 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(() => { if (!activeSketchId) { sc.canvas.addEventListener('pointermove', sc.onHover) @@ -39,19 +53,10 @@ export const NavBar = () => { }, 'Finish'] : [FaEdit, sc.addSketch, 'Sketch [s]'] , - [FaCube, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Extrude [e]'], - [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 = 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'], + [FaCube, extrude , 'Extrude [e]'], + [Icon.Union, ()=>boolOp('u'), 'Union'], + [Icon.Subtract, ()=>boolOp('s'), 'Subtract'], + [Icon.Intersect, ()=>boolOp('i'), 'Intersect'], [Icon.Dimension, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Dimension [d]'], [Icon.Line, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Line [l]'], [Icon.Arc, () => sc.extrude(treeEntries.byId[activeSketchId]), 'Arc [a]'], diff --git a/src/react/reducer.js b/src/react/reducer.js index 12e66d6..241f3ec 100644 --- a/src/react/reducer.js +++ b/src/react/reducer.js @@ -92,7 +92,7 @@ export function reducer(state = {}, action) { return update(state, { - treeEntries: { $set: obj } + treeEntries: { $merge: obj } }) diff --git a/src/react/tree.jsx b/src/react/tree.jsx index 3eb8d4f..9b31768 100644 --- a/src/react/tree.jsx +++ b/src/react/tree.jsx @@ -48,12 +48,10 @@ const TreeEntry = ({ entId }) => { const [_, forceUpdate] = useReducer(x => x + 1, 0); - // const vis = obj3d.layers.mask & 1 - return
{ - if (entId[0] == 's') { + if (obj3d.userData.type == 'sketch') { activeSketchId && treeEntries[activeSketchId].deactivate() sketch.activate() sc.clearSelection() @@ -67,14 +65,13 @@ const TreeEntry = ({ entId }) => { sc.render() }} onPointerLeave={() => { - // console.log('activeid',activeSketchId,'visstate',visState) - if (visible & entId[0] == 's') return + if (visible & obj3d.userData.type == 'sketch') return if (sc.selected.includes(obj3d) || activeSketchId == obj3d.name) return sc.setHover(obj3d, 0) sc.render() }} onClick={() => { - if (entId[0] == 'm') { + if (obj3d.userData.type == 'mesh') { sc.selected.push( obj3d ) diff --git a/todo.txt b/todo.txt index 5fb86b9..2a39e8d 100644 --- a/todo.txt +++ b/todo.txt @@ -14,26 +14,30 @@ boolean flesh out refresh / replace mesh - consume skeches after extrude // done - selection hover disspates when rehovered //fixed - boolean unable to select click //fixed +- hover sync between tree and work area // done, punt on stretch vertical // 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 extrude dialogue loopfind -button panel cleanup + file save, stl export -constriant buttons ,tangent, angle +button panel cleanup + +constraint angle +3 pt arc -constraint labels reattach sketch +constraint labels, tangent auto snap tree ent renaming and better default names \ No newline at end of file