1 | const {EPS, angleEPS, areaEPS, defaultResolution3D} = require('./constants')
|
2 | const {Connector} = require('./connectors')
|
3 | const OrthoNormalBasis = require('./math/OrthoNormalBasis')
|
4 | const Vertex2D = require('./math/Vertex2')
|
5 | const Vertex3D = require('./math/Vertex3')
|
6 | const Vector2D = require('./math/Vector2')
|
7 | const Vector3D = require('./math/Vector3')
|
8 | const Polygon = require('./math/Polygon3')
|
9 | const Path2D = require('./math/Path2')
|
10 | const Side = require('./math/Side')
|
11 | const {linesIntersect} = require('./math/lineUtils')
|
12 | const {parseOptionAs3DVector, parseOptionAsBool, parseOptionAsFloat, parseOptionAsInt} = require('./optionParsers')
|
13 | const FuzzyCAGFactory = require('./FuzzyFactory2d')
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | let CAG = function () {
|
22 | this.sides = []
|
23 | this.isCanonicalized = false
|
24 | }
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | CAG.fromSides = function (sides) {
|
31 | let cag = new CAG()
|
32 | cag.sides = sides
|
33 | return cag
|
34 | }
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | CAG.fromFakeCSG = function (csg) {
|
40 | let sides = csg.polygons.map(function (p) {
|
41 | return Side._fromFakePolygon(p)
|
42 | })
|
43 | .filter(function (s) {
|
44 | return s !== null
|
45 | })
|
46 | return CAG.fromSides(sides)
|
47 | }
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | CAG.fromPoints = function (points) {
|
57 | let numpoints = points.length
|
58 | if (numpoints < 3) throw new Error('CAG shape needs at least 3 points')
|
59 | let sides = []
|
60 | let prevpoint = new Vector2D(points[numpoints - 1])
|
61 | let prevvertex = new Vertex2D(prevpoint)
|
62 | points.map(function (p) {
|
63 | let point = new Vector2D(p)
|
64 | let vertex = new Vertex2D(point)
|
65 | let side = new Side(prevvertex, vertex)
|
66 | sides.push(side)
|
67 | prevvertex = vertex
|
68 | })
|
69 | let result = CAG.fromSides(sides)
|
70 | if (result.isSelfIntersecting()) {
|
71 | throw new Error('Polygon is self intersecting!')
|
72 | }
|
73 | let area = result.area()
|
74 | if (Math.abs(area) < areaEPS) {
|
75 | throw new Error('Degenerate polygon!')
|
76 | }
|
77 | if (area < 0) {
|
78 | result = result.flipped()
|
79 | }
|
80 | result = result.canonicalized()
|
81 | return result
|
82 | }
|
83 |
|
84 | const CAGFromCAGFuzzyFactory = function (factory, sourcecag) {
|
85 | let _this = factory
|
86 | let newsides = sourcecag.sides.map(function (side) {
|
87 | return _this.getSide(side)
|
88 | })
|
89 |
|
90 | .filter(function (side) {
|
91 | return side.length() > EPS
|
92 | })
|
93 | return CAG.fromSides(newsides)
|
94 | }
|
95 |
|
96 | CAG.prototype = {
|
97 | toString: function () {
|
98 | let result = 'CAG (' + this.sides.length + ' sides):\n'
|
99 | this.sides.map(function (side) {
|
100 | result += ' ' + side.toString() + '\n'
|
101 | })
|
102 | return result
|
103 | },
|
104 |
|
105 | _toCSGWall: function (z0, z1) {
|
106 | const CSG = require('./CSG')
|
107 | let polygons = this.sides.map(function (side) {
|
108 | return side.toPolygon3D(z0, z1)
|
109 | })
|
110 | return CSG.fromPolygons(polygons)
|
111 | },
|
112 |
|
113 | _toVector3DPairs: function (m) {
|
114 |
|
115 | let pairs = this.sides.map(function (side) {
|
116 | let p0 = side.vertex0.pos
|
117 | let p1 = side.vertex1.pos
|
118 | return [Vector3D.Create(p0.x, p0.y, 0),
|
119 | Vector3D.Create(p1.x, p1.y, 0)]
|
120 | })
|
121 | if (typeof m !== 'undefined') {
|
122 | pairs = pairs.map(function (pair) {
|
123 | return pair.map(function (v) {
|
124 | return v.transform(m)
|
125 | })
|
126 | })
|
127 | }
|
128 | return pairs
|
129 | },
|
130 |
|
131 | |
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 | _toPlanePolygons: function (options) {
|
138 | const CSG = require('./CSG')
|
139 | let flipped = options.flipped || false
|
140 |
|
141 | let origin = [0, 0, 0]
|
142 | let defaultAxis = [0, 0, 1]
|
143 | let defaultNormal = [0, 1, 0]
|
144 | let thisConnector = new Connector(origin, defaultAxis, defaultNormal)
|
145 |
|
146 | let translation = options.translation || origin
|
147 | let axisVector = options.axisVector || defaultAxis
|
148 | let normalVector = options.normalVector || defaultNormal
|
149 |
|
150 | let toConnector = options.toConnector ||
|
151 | new Connector(translation, axisVector, normalVector)
|
152 |
|
153 | let m = thisConnector.getTransformationTo(toConnector, false, 0)
|
154 |
|
155 | let bounds = this.getBounds()
|
156 | bounds[0] = bounds[0].minus(new Vector2D(1, 1))
|
157 | bounds[1] = bounds[1].plus(new Vector2D(1, 1))
|
158 | let csgshell = this._toCSGWall(-1, 1)
|
159 | let csgplane = CSG.fromPolygons([new Polygon([
|
160 | new Vertex3D(new Vector3D(bounds[0].x, bounds[0].y, 0)),
|
161 | new Vertex3D(new Vector3D(bounds[1].x, bounds[0].y, 0)),
|
162 | new Vertex3D(new Vector3D(bounds[1].x, bounds[1].y, 0)),
|
163 | new Vertex3D(new Vector3D(bounds[0].x, bounds[1].y, 0))
|
164 | ])])
|
165 | if (flipped) {
|
166 | csgplane = csgplane.invert()
|
167 | }
|
168 |
|
169 | csgplane = csgplane.intersectSub(csgshell)
|
170 |
|
171 | let polys = csgplane.polygons.filter(function (polygon) {
|
172 | return Math.abs(polygon.plane.normal.z) > 0.99
|
173 | })
|
174 |
|
175 | return polys.map(function (poly) {
|
176 | return poly.transform(m)
|
177 | })
|
178 | },
|
179 |
|
180 | |
181 |
|
182 |
|
183 |
|
184 |
|
185 | _toWallPolygons: function (options) {
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 | let origin = [0, 0, 0]
|
192 | let defaultAxis = [0, 0, 1]
|
193 | let defaultNormal = [0, 1, 0]
|
194 | let thisConnector = new Connector(origin, defaultAxis, defaultNormal)
|
195 |
|
196 | let toConnector1 = options.toConnector1
|
197 |
|
198 | let toConnector2 = options.toConnector2
|
199 | if (!(toConnector1 instanceof Connector && toConnector2 instanceof Connector)) {
|
200 | throw new Error('could not parse Connector arguments toConnector1 or toConnector2')
|
201 | }
|
202 | if (options.cag) {
|
203 | if (options.cag.sides.length !== this.sides.length) {
|
204 | throw new Error('target cag needs same sides count as start cag')
|
205 | }
|
206 | }
|
207 |
|
208 | let toCag = options.cag || this
|
209 | let m1 = thisConnector.getTransformationTo(toConnector1, false, 0)
|
210 | let m2 = thisConnector.getTransformationTo(toConnector2, false, 0)
|
211 | let vps1 = this._toVector3DPairs(m1)
|
212 | let vps2 = toCag._toVector3DPairs(m2)
|
213 |
|
214 | let polygons = []
|
215 | vps1.forEach(function (vp1, i) {
|
216 | polygons.push(new Polygon([
|
217 | new Vertex3D(vps2[i][1]), new Vertex3D(vps2[i][0]), new Vertex3D(vp1[0])]))
|
218 | polygons.push(new Polygon([
|
219 | new Vertex3D(vps2[i][1]), new Vertex3D(vp1[0]), new Vertex3D(vp1[1])]))
|
220 | })
|
221 | return polygons
|
222 | },
|
223 |
|
224 | |
225 |
|
226 |
|
227 |
|
228 | toPoints: function () {
|
229 | let points = this.sides.map(function (side) {
|
230 | let v0 = side.vertex0
|
231 |
|
232 | return v0.pos
|
233 | })
|
234 |
|
235 |
|
236 | if (points.length > 0) {
|
237 | points.push(points.shift())
|
238 | }
|
239 | return points
|
240 | },
|
241 |
|
242 | union: function (cag) {
|
243 | let cags
|
244 | if (cag instanceof Array) {
|
245 | cags = cag
|
246 | } else {
|
247 | cags = [cag]
|
248 | }
|
249 | let r = this._toCSGWall(-1, 1)
|
250 | r = r.union(
|
251 | cags.map(function (cag) {
|
252 | return cag._toCSGWall(-1, 1).reTesselated()
|
253 | }), false, false)
|
254 | return CAG.fromFakeCSG(r).canonicalized()
|
255 | },
|
256 |
|
257 | subtract: function (cag) {
|
258 | let cags
|
259 | if (cag instanceof Array) {
|
260 | cags = cag
|
261 | } else {
|
262 | cags = [cag]
|
263 | }
|
264 | let r = this._toCSGWall(-1, 1)
|
265 | cags.map(function (cag) {
|
266 | r = r.subtractSub(cag._toCSGWall(-1, 1), false, false)
|
267 | })
|
268 | r = r.reTesselated()
|
269 | r = r.canonicalized()
|
270 | r = CAG.fromFakeCSG(r)
|
271 | r = r.canonicalized()
|
272 | return r
|
273 | },
|
274 |
|
275 | intersect: function (cag) {
|
276 | let cags
|
277 | if (cag instanceof Array) {
|
278 | cags = cag
|
279 | } else {
|
280 | cags = [cag]
|
281 | }
|
282 | let r = this._toCSGWall(-1, 1)
|
283 | cags.map(function (cag) {
|
284 | r = r.intersectSub(cag._toCSGWall(-1, 1), false, false)
|
285 | })
|
286 | r = r.reTesselated()
|
287 | r = r.canonicalized()
|
288 | r = CAG.fromFakeCSG(r)
|
289 | r = r.canonicalized()
|
290 | return r
|
291 | },
|
292 |
|
293 | transform: function (matrix4x4) {
|
294 | let ismirror = matrix4x4.isMirroring()
|
295 | let newsides = this.sides.map(function (side) {
|
296 | return side.transform(matrix4x4)
|
297 | })
|
298 | let result = CAG.fromSides(newsides)
|
299 | if (ismirror) {
|
300 | result = result.flipped()
|
301 | }
|
302 | return result
|
303 | },
|
304 |
|
305 |
|
306 |
|
307 |
|
308 | area: function () {
|
309 | let polygonArea = 0
|
310 | this.sides.map(function (side) {
|
311 | polygonArea += side.vertex0.pos.cross(side.vertex1.pos)
|
312 | })
|
313 | polygonArea *= 0.5
|
314 | return polygonArea
|
315 | },
|
316 |
|
317 | flipped: function () {
|
318 | let newsides = this.sides.map(function (side) {
|
319 | return side.flipped()
|
320 | })
|
321 | newsides.reverse()
|
322 | return CAG.fromSides(newsides)
|
323 | },
|
324 |
|
325 | getBounds: function () {
|
326 | let minpoint
|
327 | if (this.sides.length === 0) {
|
328 | minpoint = new Vector2D(0, 0)
|
329 | } else {
|
330 | minpoint = this.sides[0].vertex0.pos
|
331 | }
|
332 | let maxpoint = minpoint
|
333 | this.sides.map(function (side) {
|
334 | minpoint = minpoint.min(side.vertex0.pos)
|
335 | minpoint = minpoint.min(side.vertex1.pos)
|
336 | maxpoint = maxpoint.max(side.vertex0.pos)
|
337 | maxpoint = maxpoint.max(side.vertex1.pos)
|
338 | })
|
339 | return [minpoint, maxpoint]
|
340 | },
|
341 |
|
342 | isSelfIntersecting: function (debug) {
|
343 | let numsides = this.sides.length
|
344 | for (let i = 0; i < numsides; i++) {
|
345 | let side0 = this.sides[i]
|
346 | for (let ii = i + 1; ii < numsides; ii++) {
|
347 | let side1 = this.sides[ii]
|
348 | if (linesIntersect(side0.vertex0.pos, side0.vertex1.pos, side1.vertex0.pos, side1.vertex1.pos)) {
|
349 | if (debug) { console.log('side ' + i + ': ' + side0); console.log('side ' + ii + ': ' + side1) }
|
350 | return true
|
351 | }
|
352 | }
|
353 | }
|
354 | return false
|
355 | },
|
356 |
|
357 | expandedShell: function (radius, resolution) {
|
358 | resolution = resolution || 8
|
359 | if (resolution < 4) resolution = 4
|
360 | let cags = []
|
361 | let pointmap = {}
|
362 | let cag = this.canonicalized()
|
363 | cag.sides.map(function (side) {
|
364 | let d = side.vertex1.pos.minus(side.vertex0.pos)
|
365 | let dl = d.length()
|
366 | if (dl > EPS) {
|
367 | d = d.times(1.0 / dl)
|
368 | let normal = d.normal().times(radius)
|
369 | let shellpoints = [
|
370 | side.vertex1.pos.plus(normal),
|
371 | side.vertex1.pos.minus(normal),
|
372 | side.vertex0.pos.minus(normal),
|
373 | side.vertex0.pos.plus(normal)
|
374 | ]
|
375 |
|
376 | let newcag = CAG.fromPoints(shellpoints)
|
377 | cags.push(newcag)
|
378 | for (let step = 0; step < 2; step++) {
|
379 | let p1 = (step === 0) ? side.vertex0.pos : side.vertex1.pos
|
380 | let p2 = (step === 0) ? side.vertex1.pos : side.vertex0.pos
|
381 | let tag = p1.x + ' ' + p1.y
|
382 | if (!(tag in pointmap)) {
|
383 | pointmap[tag] = []
|
384 | }
|
385 | pointmap[tag].push({
|
386 | 'p1': p1,
|
387 | 'p2': p2
|
388 | })
|
389 | }
|
390 | }
|
391 | })
|
392 | for (let tag in pointmap) {
|
393 | let m = pointmap[tag]
|
394 | let angle1, angle2
|
395 | let pcenter = m[0].p1
|
396 | if (m.length === 2) {
|
397 | let end1 = m[0].p2
|
398 | let end2 = m[1].p2
|
399 | angle1 = end1.minus(pcenter).angleDegrees()
|
400 | angle2 = end2.minus(pcenter).angleDegrees()
|
401 | if (angle2 < angle1) angle2 += 360
|
402 | if (angle2 >= (angle1 + 360)) angle2 -= 360
|
403 | if (angle2 < angle1 + 180) {
|
404 | let t = angle2
|
405 | angle2 = angle1 + 360
|
406 | angle1 = t
|
407 | }
|
408 | angle1 += 90
|
409 | angle2 -= 90
|
410 | } else {
|
411 | angle1 = 0
|
412 | angle2 = 360
|
413 | }
|
414 | let fullcircle = (angle2 > angle1 + 359.999)
|
415 | if (fullcircle) {
|
416 | angle1 = 0
|
417 | angle2 = 360
|
418 | }
|
419 | if (angle2 > (angle1 + angleEPS)) {
|
420 | let points = []
|
421 | if (!fullcircle) {
|
422 | points.push(pcenter)
|
423 | }
|
424 | let numsteps = Math.round(resolution * (angle2 - angle1) / 360)
|
425 | if (numsteps < 1) numsteps = 1
|
426 | for (let step = 0; step <= numsteps; step++) {
|
427 | let angle = angle1 + step / numsteps * (angle2 - angle1)
|
428 | if (step === numsteps) angle = angle2
|
429 | let point = pcenter.plus(Vector2D.fromAngleDegrees(angle).times(radius))
|
430 | if ((!fullcircle) || (step > 0)) {
|
431 | points.push(point)
|
432 | }
|
433 | }
|
434 | let newcag = CAG.fromPointsNoCheck(points)
|
435 | cags.push(newcag)
|
436 | }
|
437 | }
|
438 | let result = new CAG()
|
439 | result = result.union(cags)
|
440 | return result
|
441 | },
|
442 |
|
443 | expand: function (radius, resolution) {
|
444 | let result = this.union(this.expandedShell(radius, resolution))
|
445 | return result
|
446 | },
|
447 |
|
448 | contract: function (radius, resolution) {
|
449 | let result = this.subtract(this.expandedShell(radius, resolution))
|
450 | return result
|
451 | },
|
452 |
|
453 |
|
454 |
|
455 |
|
456 |
|
457 |
|
458 |
|
459 |
|
460 |
|
461 |
|
462 |
|
463 | extrudeInOrthonormalBasis: function (orthonormalbasis, depth, options) {
|
464 |
|
465 | if (!(orthonormalbasis instanceof OrthoNormalBasis)) {
|
466 | throw new Error('extrudeInPlane: the first parameter should be a OrthoNormalBasis')
|
467 | }
|
468 | let extruded = this.extrude({
|
469 | offset: [0, 0, depth]
|
470 | })
|
471 | if (parseOptionAsBool(options, 'symmetrical', false)) {
|
472 | extruded = extruded.translate([0, 0, -depth / 2])
|
473 | }
|
474 | let matrix = orthonormalbasis.getInverseProjectionMatrix()
|
475 | extruded = extruded.transform(matrix)
|
476 | return extruded
|
477 | },
|
478 |
|
479 |
|
480 |
|
481 |
|
482 |
|
483 |
|
484 |
|
485 | extrudeInPlane: function (axis1, axis2, depth, options) {
|
486 | return this.extrudeInOrthonormalBasis(OrthoNormalBasis.GetCartesian(axis1, axis2), depth, options)
|
487 | },
|
488 |
|
489 |
|
490 |
|
491 |
|
492 |
|
493 |
|
494 |
|
495 | extrude: function (options) {
|
496 | const CSG = require('./CSG')
|
497 | if (this.sides.length === 0) {
|
498 |
|
499 | return new CSG()
|
500 | }
|
501 | let offsetVector = parseOptionAs3DVector(options, 'offset', [0, 0, 1])
|
502 | let twistangle = parseOptionAsFloat(options, 'twistangle', 0)
|
503 | let twiststeps = parseOptionAsInt(options, 'twiststeps', defaultResolution3D)
|
504 | if (offsetVector.z === 0) {
|
505 | throw new Error('offset cannot be orthogonal to Z axis')
|
506 | }
|
507 | if (twistangle === 0 || twiststeps < 1) {
|
508 | twiststeps = 1
|
509 | }
|
510 | let normalVector = Vector3D.Create(0, 1, 0)
|
511 |
|
512 | let polygons = []
|
513 |
|
514 | polygons = polygons.concat(this._toPlanePolygons({
|
515 | translation: [0, 0, 0],
|
516 | normalVector: normalVector,
|
517 | flipped: !(offsetVector.z < 0)}
|
518 | ))
|
519 | polygons = polygons.concat(this._toPlanePolygons({
|
520 | translation: offsetVector,
|
521 | normalVector: normalVector.rotateZ(twistangle),
|
522 | flipped: offsetVector.z < 0}))
|
523 |
|
524 | for (let i = 0; i < twiststeps; i++) {
|
525 | let c1 = new Connector(offsetVector.times(i / twiststeps), [0, 0, offsetVector.z],
|
526 | normalVector.rotateZ(i * twistangle / twiststeps))
|
527 | let c2 = new Connector(offsetVector.times((i + 1) / twiststeps), [0, 0, offsetVector.z],
|
528 | normalVector.rotateZ((i + 1) * twistangle / twiststeps))
|
529 | polygons = polygons.concat(this._toWallPolygons({toConnector1: c1, toConnector2: c2}))
|
530 | }
|
531 |
|
532 | return CSG.fromPolygons(polygons)
|
533 | },
|
534 |
|
535 | |
536 |
|
537 |
|
538 |
|
539 |
|
540 |
|
541 |
|
542 | rotateExtrude: function (options) {
|
543 | const CSG = require('./CSG')
|
544 | let alpha = parseOptionAsFloat(options, 'angle', 360)
|
545 | let resolution = parseOptionAsInt(options, 'resolution', defaultResolution3D)
|
546 |
|
547 | alpha = alpha > 360 ? alpha % 360 : alpha
|
548 | let origin = [0, 0, 0]
|
549 | let axisV = Vector3D.Create(0, 1, 0)
|
550 | let normalV = [0, 0, 1]
|
551 | let polygons = []
|
552 |
|
553 | let connS = new Connector(origin, axisV, normalV)
|
554 | if (alpha > 0 && alpha < 360) {
|
555 |
|
556 |
|
557 | let connE = new Connector(origin, axisV.rotateZ(-alpha), normalV)
|
558 | polygons = polygons.concat(
|
559 | this._toPlanePolygons({toConnector: connS, flipped: true}))
|
560 | polygons = polygons.concat(
|
561 | this._toPlanePolygons({toConnector: connE}))
|
562 | }
|
563 | let connT1 = connS
|
564 | let connT2
|
565 | let step = alpha / resolution
|
566 | for (let a = step; a <= alpha + EPS; a += step) {
|
567 | connT2 = new Connector(origin, axisV.rotateZ(-a), normalV)
|
568 | polygons = polygons.concat(this._toWallPolygons(
|
569 | {toConnector1: connT1, toConnector2: connT2}))
|
570 | connT1 = connT2
|
571 | }
|
572 | return CSG.fromPolygons(polygons).reTesselated()
|
573 | },
|
574 |
|
575 |
|
576 |
|
577 | check: function () {
|
578 | let errors = []
|
579 | if (this.isSelfIntersecting(true)) {
|
580 | errors.push('Self intersects')
|
581 | }
|
582 | let pointcount = {}
|
583 | this.sides.map(function (side) {
|
584 | function mappoint (p) {
|
585 | let tag = p.x + ' ' + p.y
|
586 | if (!(tag in pointcount)) pointcount[tag] = 0
|
587 | pointcount[tag] ++
|
588 | }
|
589 | mappoint(side.vertex0.pos)
|
590 | mappoint(side.vertex1.pos)
|
591 | })
|
592 | for (let tag in pointcount) {
|
593 | let count = pointcount[tag]
|
594 | if (count & 1) {
|
595 | errors.push('Uneven number of sides (' + count + ') for point ' + tag)
|
596 | }
|
597 | }
|
598 | let area = this.area()
|
599 | if (area < areaEPS) {
|
600 | errors.push('Area is ' + area)
|
601 | }
|
602 | if (errors.length > 0) {
|
603 | let ertxt = ''
|
604 | errors.map(function (err) {
|
605 | ertxt += err + '\n'
|
606 | })
|
607 | throw new Error(ertxt)
|
608 | }
|
609 | },
|
610 |
|
611 | canonicalized: function () {
|
612 | if (this.isCanonicalized) {
|
613 | return this
|
614 | } else {
|
615 | let factory = new FuzzyCAGFactory()
|
616 | let result = CAGFromCAGFuzzyFactory(factory, this)
|
617 | result.isCanonicalized = true
|
618 | return result
|
619 | }
|
620 | },
|
621 |
|
622 | |
623 |
|
624 |
|
625 |
|
626 | toCompactBinary: function () {
|
627 | let cag = this.canonicalized()
|
628 | let numsides = cag.sides.length
|
629 | let vertexmap = {}
|
630 | let vertices = []
|
631 | let numvertices = 0
|
632 | let sideVertexIndices = new Uint32Array(2 * numsides)
|
633 | let sidevertexindicesindex = 0
|
634 | cag.sides.map(function (side) {
|
635 | [side.vertex0, side.vertex1].map(function (v) {
|
636 | let vertextag = v.getTag()
|
637 | let vertexindex
|
638 | if (!(vertextag in vertexmap)) {
|
639 | vertexindex = numvertices++
|
640 | vertexmap[vertextag] = vertexindex
|
641 | vertices.push(v)
|
642 | } else {
|
643 | vertexindex = vertexmap[vertextag]
|
644 | }
|
645 | sideVertexIndices[sidevertexindicesindex++] = vertexindex
|
646 | })
|
647 | })
|
648 | let vertexData = new Float64Array(numvertices * 2)
|
649 | let verticesArrayIndex = 0
|
650 | vertices.map(function (v) {
|
651 | let pos = v.pos
|
652 | vertexData[verticesArrayIndex++] = pos._x
|
653 | vertexData[verticesArrayIndex++] = pos._y
|
654 | })
|
655 | let result = {
|
656 | 'class': 'CAG',
|
657 | sideVertexIndices: sideVertexIndices,
|
658 | vertexData: vertexData
|
659 | }
|
660 | return result
|
661 | },
|
662 |
|
663 | getOutlinePaths: function () {
|
664 | let cag = this.canonicalized()
|
665 | let sideTagToSideMap = {}
|
666 | let startVertexTagToSideTagMap = {}
|
667 | cag.sides.map(function (side) {
|
668 | let sidetag = side.getTag()
|
669 | sideTagToSideMap[sidetag] = side
|
670 | let startvertextag = side.vertex0.getTag()
|
671 | if (!(startvertextag in startVertexTagToSideTagMap)) {
|
672 | startVertexTagToSideTagMap[startvertextag] = []
|
673 | }
|
674 | startVertexTagToSideTagMap[startvertextag].push(sidetag)
|
675 | })
|
676 | let paths = []
|
677 | while (true) {
|
678 | let startsidetag = null
|
679 | for (let aVertexTag in startVertexTagToSideTagMap) {
|
680 | let sidesForThisVertex = startVertexTagToSideTagMap[aVertexTag]
|
681 | startsidetag = sidesForThisVertex[0]
|
682 | sidesForThisVertex.splice(0, 1)
|
683 | if (sidesForThisVertex.length === 0) {
|
684 | delete startVertexTagToSideTagMap[aVertexTag]
|
685 | }
|
686 | break
|
687 | }
|
688 | if (startsidetag === null) break
|
689 | let connectedVertexPoints = []
|
690 | let sidetag = startsidetag
|
691 | let thisside = sideTagToSideMap[sidetag]
|
692 | let startvertextag = thisside.vertex0.getTag()
|
693 | while (true) {
|
694 | connectedVertexPoints.push(thisside.vertex0.pos)
|
695 | let nextvertextag = thisside.vertex1.getTag()
|
696 | if (nextvertextag === startvertextag) break
|
697 | if (!(nextvertextag in startVertexTagToSideTagMap)) {
|
698 | throw new Error('Area is not closed!')
|
699 | }
|
700 | let nextpossiblesidetags = startVertexTagToSideTagMap[nextvertextag]
|
701 | let nextsideindex = -1
|
702 | if (nextpossiblesidetags.length === 1) {
|
703 | nextsideindex = 0
|
704 | } else {
|
705 |
|
706 |
|
707 | let bestangle = null
|
708 | let thisangle = thisside.direction().angleDegrees()
|
709 | for (let sideindex = 0; sideindex < nextpossiblesidetags.length; sideindex++) {
|
710 | let nextpossiblesidetag = nextpossiblesidetags[sideindex]
|
711 | let possibleside = sideTagToSideMap[nextpossiblesidetag]
|
712 | let angle = possibleside.direction().angleDegrees()
|
713 | let angledif = angle - thisangle
|
714 | if (angledif < -180) angledif += 360
|
715 | if (angledif >= 180) angledif -= 360
|
716 | if ((nextsideindex < 0) || (angledif > bestangle)) {
|
717 | nextsideindex = sideindex
|
718 | bestangle = angledif
|
719 | }
|
720 | }
|
721 | }
|
722 | let nextsidetag = nextpossiblesidetags[nextsideindex]
|
723 | nextpossiblesidetags.splice(nextsideindex, 1)
|
724 | if (nextpossiblesidetags.length === 0) {
|
725 | delete startVertexTagToSideTagMap[nextvertextag]
|
726 | }
|
727 | thisside = sideTagToSideMap[nextsidetag]
|
728 | }
|
729 |
|
730 |
|
731 | if (connectedVertexPoints.length > 0) {
|
732 | connectedVertexPoints.push(connectedVertexPoints.shift())
|
733 | }
|
734 | let path = new Path2D(connectedVertexPoints, true)
|
735 | paths.push(path)
|
736 | }
|
737 | return paths
|
738 | },
|
739 |
|
740 | |
741 |
|
742 |
|
743 |
|
744 |
|
745 |
|
746 |
|
747 |
|
748 | overCutInsideCorners: function (cutterradius) {
|
749 | let cag = this.canonicalized()
|
750 |
|
751 | let pointmap = {}
|
752 | cag.sides.map(function (side) {
|
753 | if (!(side.vertex0.getTag() in pointmap)) {
|
754 | pointmap[side.vertex0.getTag()] = {
|
755 | pos: side.vertex0.pos,
|
756 | from: [],
|
757 | to: []
|
758 | }
|
759 | }
|
760 | pointmap[side.vertex0.getTag()].to.push(side.vertex1.pos)
|
761 | if (!(side.vertex1.getTag() in pointmap)) {
|
762 | pointmap[side.vertex1.getTag()] = {
|
763 | pos: side.vertex1.pos,
|
764 | from: [],
|
765 | to: []
|
766 | }
|
767 | }
|
768 | pointmap[side.vertex1.getTag()].from.push(side.vertex0.pos)
|
769 | })
|
770 |
|
771 | let cutouts = []
|
772 | for (let pointtag in pointmap) {
|
773 | let pointobj = pointmap[pointtag]
|
774 | if ((pointobj.from.length === 1) && (pointobj.to.length === 1)) {
|
775 |
|
776 | let fromcoord = pointobj.from[0]
|
777 | let pointcoord = pointobj.pos
|
778 | let tocoord = pointobj.to[0]
|
779 | let v1 = pointcoord.minus(fromcoord).unit()
|
780 | let v2 = tocoord.minus(pointcoord).unit()
|
781 | let crossproduct = v1.cross(v2)
|
782 | let isInnerCorner = (crossproduct < 0.001)
|
783 | if (isInnerCorner) {
|
784 |
|
785 | let alpha = v2.angleRadians() - v1.angleRadians() + Math.PI
|
786 | if (alpha < 0) {
|
787 | alpha += 2 * Math.PI
|
788 | } else if (alpha >= 2 * Math.PI) {
|
789 | alpha -= 2 * Math.PI
|
790 | }
|
791 | let midvector = v2.minus(v1).unit()
|
792 | let circlesegmentangle = 30 / 180 * Math.PI
|
793 |
|
794 | let radiuscorrected = cutterradius / Math.cos(circlesegmentangle / 2)
|
795 | let circlecenter = pointcoord.plus(midvector.times(radiuscorrected))
|
796 |
|
797 | let startangle = alpha + midvector.angleRadians()
|
798 | let deltaangle = 2 * (Math.PI - alpha)
|
799 | let numsteps = 2 * Math.ceil(deltaangle / circlesegmentangle / 2)
|
800 |
|
801 | let points = [circlecenter]
|
802 | for (let i = 0; i <= numsteps; i++) {
|
803 | let angle = startangle + i / numsteps * deltaangle
|
804 | let p = Vector2D.fromAngleRadians(angle).times(radiuscorrected).plus(circlecenter)
|
805 | points.push(p)
|
806 | }
|
807 | cutouts.push(CAG.fromPoints(points))
|
808 | }
|
809 | }
|
810 | }
|
811 | let result = cag.subtract(cutouts)
|
812 | return result
|
813 | }
|
814 | }
|
815 |
|
816 | module.exports = CAG
|