import Vector from '../math/vector' import * as cad_utils from './cad-utils' import * as math from '../math/math' import {Matrix3, AXIS, ORIGIN} from '../math/l3space' import {HashTable} from '../utils/hashmap' import Counters from './counters' function SketchConnection(a, b, sketchObject) { this.a = a; this.b = b; this.sketchObject = sketchObject; } var _SKETCH_OBJ_COUNTER = 0; export function readSketchGeom(sketch) { function genId() { return _SKETCH_OBJ_COUNTER++; } var out = {connections : [], loops : []}; if (sketch.layers !== undefined) { for (var l = 0; l < sketch.layers.length; ++l) { var layer = sketch.layers[l]; if (layer.name == "_construction_") continue; for (var i = 0; i < layer.data.length; ++i) { var obj = layer.data[i]; if (obj.edge !== undefined) continue; if (!!obj.aux) continue; if (obj._class === 'TCAD.TWO.Segment') { var segA = new Vector(obj.points[0][1][1], obj.points[0][2][1], 0); var segB = new Vector(obj.points[1][1][1], obj.points[1][2][1], 0); out.connections.push(new SketchConnection(segA, segB, {_class : obj._class, id : genId()})); } else if (obj._class === 'TCAD.TWO.Arc') { var arcA = new Vector(obj.points[0][1][1], obj.points[0][2][1], 0); var arcB = new Vector(obj.points[1][1][1], obj.points[1][2][1], 0); var arcCenter = new Vector(obj.points[2][1][1], obj.points[2][2][1], 0); var approxedArc = approxArc(arcA, arcB, arcCenter, 20); var arcData = {_class : obj._class, id : genId()}; for (var j = 0; j < approxedArc.length - 1; j++) { out.connections.push(new SketchConnection(approxedArc[j], approxedArc[j+1], arcData)); } } else if (obj._class === 'TCAD.TWO.Circle') { var circleCenter = new Vector(obj.c[1][1], obj.c[2][1], 0); var approxedCircle = approxCircle(circleCenter, obj.r, 20); var circleData = {_class : obj._class, id : genId()}; var loop = []; var p, q, n = approxedCircle.length; for (p = n - 1, q = 0; q < n; p = q++) { loop.push(new SketchConnection(approxedCircle[p], approxedCircle[q], circleData)); } out.loops.push(loop); } } } } return out; } export function approxArc(ao, bo, c, resolution) { var a = ao.minus(c); var b = bo.minus(c); var points = [ao]; var abAngle = Math.atan2(b.y, b.x) - Math.atan2(a.y, a.x); if (abAngle > Math.PI * 2) abAngle = Math.PI / 2 - abAngle; if (abAngle < 0) abAngle = Math.PI * 2 + abAngle; var r = a.length(); resolution = 1; //var step = Math.acos(1 - ((resolution * resolution) / (2 * r * r))); var step = resolution / (2 * Math.PI); var k = Math.round(abAngle / step); var angle = Math.atan2(a.y, a.x) + step; for (var i = 0; i < k - 1; ++i) { points.push(new Vector(c.x + r*Math.cos(angle), c.y + r*Math.sin(angle))); angle += step; } points.push(bo); return points; } export function approxCircle(c, r, resolution) { var points = []; resolution = 1; //var step = Math.acos(1 - ((resolution * resolution) / (2 * r * r))); var step = resolution / (2 * Math.PI); var k = Math.round((2 * Math.PI) / step); for (var i = 0, angle = 0; i < k; ++i, angle += step) { points.push(new Vector(c.x + r*Math.cos(angle), c.y + r*Math.sin(angle))); } return points; } export function getSketchedPolygons3D(app, face) { var savedFace = localStorage.getItem(app.faceStorageKey(face.id)); if (savedFace == null) return null; var geom = readSketchGeom(JSON.parse(savedFace)); var polygons2D = cad_utils.sketchToPolygons(geom); var normal = face.csgGroup.normal; var depth = null; var sketchedPolygons = []; for (var i = 0; i < polygons2D.length; i++) { var poly2D = polygons2D[i]; if (poly2D.length < 3) continue; if (depth == null) { var _3dTransformation = new Matrix3().setBasis(face.basis()); //we lost depth or z off in 2d sketch, calculate it again depth = face.csgGroup.plane.w; } var polygon = []; for (var m = 0; m < poly2D.length; ++m) { var vec = poly2D[m]; vec.z = depth; // var a = _3dTransformation.apply(new Vector(poly2D[m][0], poly2D[m][1], depth)); var a = _3dTransformation.apply(vec); a.sketchConnectionObject = vec.sketchConnectionObject; polygon.push(a); } sketchedPolygons.push(polygon); } return sketchedPolygons; } export function extrude(app, request) { var face = request.face; var sketchedPolygons = getSketchedPolygons3D(app, face); if (sketchedPolygons == null) return null; var normal = cad_utils.vec(face.csgGroup.plane.normal); var toMeldWith = []; for (var i = 0; i < sketchedPolygons.length; i++) { var extruded = cad_utils.extrude(sketchedPolygons[i], normal, request.params.target, request.params.expansionFactor ); toMeldWith = toMeldWith.concat(extruded); } var solid = request.solids[0]; var meld = CSG.fromPolygons(_triangulateCSG(toMeldWith)); if (solid.mergeable) { meld = solid.csg.union(meld); } face.csgGroup.shared.__tcad.faceId += '$'; return [cad_utils.createSolid(meld)]; } function _pointOnLine(p, a, b) { var ab = a.minus(b); var ap = a.minus(p); var dp = ab.dot(ap); var abLength = ab.length(); var apLength = ap.length(); return apLength > 0 && apLength < abLength && math.areEqual(abLength * apLength, dp, 1E-6); } export function polygonsToSegments(polygons) { function selfIntersecting(a, b, c) { var f = _pointOnLine; return f(c, a, b) || f(a, b, c) || f(b, c, a); } //polygons.filter(function(p) { // //}); //magnitude of cross product is the area of parallelogram //var area = points[b].pos.minus(points[a].pos).cross(points[c].pos.minus(points[a].pos)).length() / 2.0; //if (selfIntersecting(points[a].pos, points[b].pos, points[c].pos)) { //continue; //} var segmentsByPolygon = []; for (var pi = 0; pi < polygons.length; pi++) { var segments = []; var poly = polygons[pi]; var p, q, n = poly.vertices.length; for(p = n - 1, q = 0; q < n; p = q ++) { var a = poly.vertices[p]; var b = poly.vertices[q]; segments.push([a.pos, b.pos]); } segmentsByPolygon.push(segments); } return segmentsByPolygon; } export function reconstructSketchBounds(csg, face, strict) { strict = strict || false; var polygons = csg.toPolygons(); var plane = face.csgGroup.plane; var outerEdges = []; var planePolygons = []; for (var pi = 0; pi < polygons.length; pi++) { var poly = polygons[pi]; if (math.equal(poly.plane.normal.dot(plane.normal), 1)) { if (math.equal(plane.w, poly.plane.w) && (!strict || !!poly.shared.__tcad && poly.shared.__tcad.faceId === face.id)) { planePolygons.push(poly); } continue; } var p, q, n = poly.vertices.length; for(p = n - 1, q = 0; q < n; p = q ++) { var a = poly.vertices[p]; var b = poly.vertices[q]; var pointAOnPlane = math.equal(plane.signedDistanceToPoint(a.pos), 0); if (!pointAOnPlane) continue; var pointBOnPlane = math.equal(plane.signedDistanceToPoint(b.pos), 0); if (pointBOnPlane) { outerEdges.push([a.pos, b.pos, poly]); } } } var outline = findOutline(planePolygons); pickUpCraftInfo(outline, outerEdges); return segmentsToPaths(outline); } function pickUpCraftInfo(outline, outerEdges) { var eq = math.strictEqual; for (var psi1 = 0; psi1 < outline.length; psi1++) { var s1 = outline[psi1]; for (var psi2 = 0; psi2 < outerEdges.length; psi2++) { var s2 = outerEdges[psi2]; if (math.equal(Math.abs(s1[0].minus(s1[1]).unit().dot(s2[0].minus(s2[1]).unit())), 1) && (eq(s1[0], s2[0]) || eq(s1[1], s2[1]) || eq(s1[0], s2[1]) || eq(s1[1], s2[0]) || _pointOnLine(s1[0], s2[0], s2[1]) || _pointOnLine(s1[1], s2[0], s2[1]))) { s1[2] = s2[2]; } } } } function getOutlineByCollision(segments, outerEdges) { var eq = math.strictEqual; var outline = []; for (var psi1 = 0; psi1 < segments.length; psi1++) { var s1 = segments[psi1]; for (var psi2 = 0; psi2 < outerEdges.length; psi2++) { var s2 = outerEdges[psi2]; if (math.equal(Math.abs(s1[0].minus(s1[1]).unit().dot(s2[0].minus(s2[1]).unit())), 1) && (eq(s1[0], s2[0]) || eq(s1[1], s2[1]) || eq(s1[0], s2[1]) || eq(s1[1], s2[0]) || _pointOnLine(s1[0], s2[0], s2[1]) || _pointOnLine(s1[1], s2[0], s2[1]))) { outline.push(s1); } } } return outline; } function findOutline (planePolygons, outer) { var segmentsByPolygon = polygonsToSegments(planePolygons); //simplifySegments(segmentsByPolygon); var planeSegments = cad_utils.arrFlatten1L(segmentsByPolygon); //planeSegments = removeSharedEdges(planeSegments); removeTJoints(planeSegments); planeSegments = removeSharedEdges(planeSegments); return planeSegments; } function removeSharedEdges(segments) { segments = segments.slice(); var eq = math.strictEqual; for (var psi1 = 0; psi1 < segments.length; psi1++) { var s1 = segments[psi1]; if (s1 == null) continue; for (var psi2 = 0; psi2 < segments.length; psi2++) { if (psi1 === psi2) continue; var s2 = segments[psi2]; if (s2 == null) continue; if ((eq(s1[0], s2[0]) && eq(s1[1], s2[1]) || (eq(s1[0], s2[1]) && eq(s1[1], s2[0])))) { segments[psi1] = null; segments[psi2] = null; } } } return segments.filter(function(e) {return e !== null}); } function simplifySegments(polygonToSegments) { for (var pi1 = 0; pi1 < polygonToSegments.length; ++pi1) { for (var pi2 = 0; pi2 < polygonToSegments.length; ++pi2) { if (pi1 === pi2) continue; var polygon1 = polygonToSegments[pi1]; var polygon2 = polygonToSegments[pi2]; for (var si1 = 0; si1 < polygon1.length; ++si1) { var seg1 = polygon1[si1]; for (var si2 = 0; si2 < polygon2.length; ++si2) { var point = polygon2[si2][0]; if (_pointOnLine(point, seg1[0], seg1[1])) { polygon1.push([point, seg1[1]]); seg1[1] = point; } } } } } } function _closeFactorToLine(p, seg1, seg2) { var a = p.minus(seg1); var b = seg2.minus(seg1); var bn = b.unit(); var projLength = bn.dot(a); var bx = bn.times(projLength); if (!(projLength > 0 && projLength < b.length())) { return -1; } var c = a.minus(bx); return c.length(); } function removeTJoints(segments) { var pointIndex = HashTable.forVector3d(); for (var i = 0; i < segments.length; ++i) { pointIndex.put(segments[i][0], 1); pointIndex.put(segments[i][1], 1); } var points = pointIndex.getKeys(); var eq = math.strictEqual; for (var pi1 = 0; pi1 < points.length; ++pi1) { var point = points[pi1]; var best = null, bestFactor; for (var pi2 = 0; pi2 < segments.length; ++pi2) { var seg = segments[pi2]; if (eq(seg[0], point) || eq(seg[1], point)) continue; var factor = _closeFactorToLine(point, seg[0], seg[1]); if (factor != -1 && factor < 1E-6 && (best == null || factor < bestFactor)) { best = seg; bestFactor = factor; } } if (best != null) { segments.push([point, best[1]]); best[1] = point; } } } function deleteRedundantPoints(path) { var cleanedPath = []; //Delete redundant point var pathLength = path.length; var skipMode = false; for (var pi = 0; pi < pathLength; pi++) { var bIdx = ((pi + 1) % pathLength); var a = path[pi]; var b = path[bIdx]; var c = path[(pi + 2) % pathLength]; var eq = math.areEqual; if (!skipMode) cleanedPath.push(a); skipMode = eq(a.minus(b).unit().dot(b.minus(c).unit()), 1, 1E-9); } return cleanedPath; } function segmentsToPaths(segments) { var veq = math.strictEqual; var paths = []; var index = HashTable.forVector3d(); var csgIndex = HashTable.forEdge(); function indexPoint(p, edge) { var edges = index.get(p); if (edges === null) { edges = []; index.put(p, edges); } edges.push(edge); } for (var si = 0; si < segments.length; si++) { var k = segments[si]; indexPoint(k[0], k); indexPoint(k[1], k); var csgInfo = k[2]; if (csgInfo !== undefined && csgInfo !== null) { csgIndex.put([k[0], k[1]], csgInfo); } k[3] = false; } function nextPoint(p) { var edges = index.get(p); if (edges === null) return null; for (var i = 0; i < edges.length; i++) { var edge = edges[i]; if (edge[3]) continue; var res = null; if (veq(p, edge[0])) res = edge[1]; if (veq(p, edge[1])) res = edge[0]; if (res != null) { edge[3] = true; return res; } } return null; } var path; for (var ei = 0; ei < segments.length; ei++) { var edge = segments[ei]; if (edge[3]) { continue; } edge[3] = true; path = [edge[0], edge[1]]; paths.push(path); var next = nextPoint(edge[1]); while (next !== null) { if (!veq(next, path[0])) { path.push(next); next = nextPoint(next); } else { next = null; } } } var filteredPaths = []; for (var i = 0; i < paths.length; i++) { path = paths[i]; //Set derived from object to be able to recunstruct cad_utils.iteratePath(path, 0, function (a, b) { var fromPolygon = csgIndex.get([a, b]); if (fromPolygon !== null) { if (fromPolygon.shared.__tcad.csgInfo) { a.sketchConnectionObject = fromPolygon.shared.__tcad.csgInfo.derivedFrom; } } return true; }); path = deleteRedundantPoints(path); if (path.length > 2) { filteredPaths.push({ vertices: path }); } } return filteredPaths; } function _triangulateCSG(polygons) { function csgVec(v) { return new CSG.Vector3D(v.x, v.y, v.z); } var triangled = []; for (var ei = 0; ei < polygons.length; ++ei) { var poly = polygons[ei]; var points = poly.vertices; var refs = cad_utils.triangulate(points, poly.plane.normal); for ( var i = 0; i < refs.length; ++ i ) { var a = refs[i][0]; var b = refs[i][1]; var c = refs[i][2]; var csgPoly = new CSG.Polygon([points[a], points[b], points[c]], poly.shared, poly.plane); triangled.push(csgPoly); } } return triangled; } function splitTwoSegments(a, b) { var da = a[1].minus(a[0]); var db = b[1].minus(b[0]); var dc = b[0].minus(a[0]); var daXdb = da.cross(db); if (Math.abs(dc.dot(daXdb)) > 1e-6) { // lines are not coplanar return null; } var veq = math.strictEqual; if (veq(a[0], b[0]) || veq(a[0], b[1]) || veq(a[1], b[0]) || veq(a[1], b[1])) { return null; } var dcXdb = dc.cross(db); function _split(s, ip) { if (s[0].equals(ip) || s[1].equals(ip)) { return [s]; } return [[s[0], ip, s[2]], [ip, s[1], s[2]]] } var s = dcXdb.dot(daXdb) / daXdb.lengthSquared(); if (s > 0.0 && s < 1.0) { var ip = a[0].plus(da.times(s)); return { splitterParts : _split(a, ip), residual : _split(b, ip) } } return null; } function attract(vectors, precision) { var eq = math.areEqual(); var dist = math.distanceAB3; vectors = vectors.slice(); for (var i = 0; i < vectors.length; i++) { var v1 = vectors[i]; if (v1 == null) continue; for (var j = i + 1; j < vectors.length; j++) { var v2 = vectors[j]; if (v2 == null) continue; if (dist(v1, v2) <= precision) { Vector.prototype.setV.call(v2, v1); vectors[j] = null; } } } } function recoverySketchInfo(polygons) { var nonStructuralGons = []; var sketchEdges = HashTable.forDoubleArray(); function key(a, b) {return [a.x, a.y, b.x, b.y]} for (var pi = 0; pi < polygons.length; pi++) { var poly = polygons[pi]; var paths = []; poly.collectPaths(paths); var i, path, n, p, q; for (i = 0; i < paths.length; i++) { path = paths[i]; if (poly.csgInfo !== undefined && poly.csgInfo.derivedFrom !== undefined) { n = path.length; for (p = n - 1, q = 0; q < n ; p = q++ ) { sketchEdges.put(key(path[p], path[q]), poly.csgInfo); } } else { nonStructuralGons.push(path); } } } for (i = 0; i < nonStructuralGons.length; i++) { path = nonStructuralGons[i]; n = path.length; for (p = n - 1, q = 0; q < n ; p = q++ ) { var csgInfo = sketchEdges.get(key(path[p], path[q])); if (csgInfo === null) { csgInfo = sketchEdges.get(key(path[q], path[p])); } if (csgInfo) { path[p].sketchConnectionObject = csgInfo.derivedFrom; } } } } export function cut(app, request) { var face = request.face; var sketchedPolygons = getSketchedPolygons3D(app, face); if (sketchedPolygons == null) return null; var normal = cad_utils.vec(face.csgGroup.plane.normal); var cutter = []; for (var i = 0; i < sketchedPolygons.length; i++) { var extruded = cad_utils.extrude(sketchedPolygons[i], normal, request.params.target, request.params.expansionFactor ); cutter = cutter.concat(extruded); } var cutterCSG = CSG.fromPolygons(_triangulateCSG(cutter)); face.csgGroup.shared.__tcad.faceId += '$'; var outSolids = []; for (var si = 0; si < request.solids.length; si++) { var work = request.solids[si].csg; var cut = work.subtract(cutterCSG); var solidMesh = cad_utils.createSolid(cut); outSolids.push(solidMesh); } return outSolids; } export function Craft(app) { this.app = app; this.history = []; this._historyPointer = 0; Object.defineProperty(this, "historyPointer", { get: function() {return this._historyPointer}, set: function(value) { if (this._historyPointer === value) return; this._historyPointer = value; this.reset(this.history.slice(0, this._historyPointer)); this.app.bus.notify('craft'); this.app.bus.notify('historyPointer'); this.app.viewer.render(); } }); } Craft.prototype.loadHistory = function(history) { this.history = history; this._historyPointer = history.length; this.reset(history); this.app.bus.notify('craft'); this.app.bus.notify('historyPointer'); this.app.viewer.render(); }; Craft.prototype.reset = function(modifications) { Counters.solid = 0; Counters.shared = 0; this.app.findAllSolids().forEach(function(s) {s.vanish()}) for (var i = 0; i < modifications.length; i++) { var request = materialize(this.app.indexEntities(), modifications[i]); this.modifyInternal(request); } }; Craft.prototype.finishHistoryEditing = function() { this.loadHistory(this.history); }; Craft.prototype.current = function() { return this.history[this.history.length - 1]; }; function detach(request) { var detachedConfig = {}; for (var prop in request) { if (request.hasOwnProperty(prop)) { var value = request[prop]; if (prop == 'solids') { detachedConfig[prop] = value.map(function(s){return s.tCadId}); } else if (prop == 'face') { detachedConfig[prop] = value.id; } else if (prop == 'target') { detachedConfig[prop] = [value.x, value.y, value.z]; } else if (prop == 'basis') { detachedConfig[prop] = value.map(function(v){return [v.x, v.y, v.z]}); } else if (prop == 'params') { detachedConfig[prop] = detach(value); } else { detachedConfig[prop] = value; } } } return detachedConfig } function materialize(index, detachedConfig) { var request = {}; function required(value) { if (value == null || value == undefined) throw "value is required"; return value; } for (var prop in detachedConfig) { if (detachedConfig.hasOwnProperty(prop)) { var value = detachedConfig[prop]; if (prop == 'solids') { request[prop] = value.map(function(id){return required(index.solids[id])}); } else if (prop == 'target') { request[prop] = new Vector().set3(value); } else if (prop == 'face') { request[prop] = required(index.faces[value]); } else if (prop == 'basis') { request[prop] = value.map(function(v) {return new Vector().set3(v)}); } else if (prop == 'params') { request[prop] = materialize(index, value); } else { request[prop] = value; } } } return request; } Craft.prototype.modifyInternal = function(request) { var op = OPERATIONS[request.type]; if (!op) return; var newSolids = op(this.app, request); if (newSolids == null) return; var i; for (i = 0; i < request.solids.length; i++) { request.solids[i].vanish(); } for (i = 0; i < newSolids.length; i++) { this.app.viewer.workGroup.add(newSolids[i].cadGroup); } }; Craft.prototype.modify = function(request, overriding) { this.modifyInternal(request); var detachedRequest = detach(request); if (!overriding && this._historyPointer != this.history.length) { this.history.splice(this._historyPointer + 1, 0, null); } this.history[this._historyPointer] = detachedRequest; this._historyPointer ++; this.app.bus.notify('craft'); this.app.bus.notify('historyPointer'); this.app.viewer.render(); }; export const OPERATIONS = { CUT : cut, PAD : extrude, PLANE : function(app, request) { return [cad_utils.createPlane(request.params.basis, request.params.depth)]; }, BOX : function(app, request) { var p = request.params; return [cad_utils.createCSGBox(p.w, p.h, p.d)]; }, SPHERE : function(app, request) { return [cad_utils.createSphere(request.params.radius)]; } };