@jscad/modeling 2.12.5 → 2.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [2.12.6](https://github.com/jscad/OpenJSCAD.org/compare/@jscad/modeling@2.12.5...@jscad/modeling@2.12.6) (2025-09-20)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **modeling:** corrected handling of pitch vs height in extrudeHelical ([074f05a](https://github.com/jscad/OpenJSCAD.org/commit/074f05aa5432fbdc8277088e9a45f003f2f978c7))
12
+
13
+
14
+
15
+
16
+
6
17
  ## [2.12.5](https://github.com/jscad/OpenJSCAD.org/compare/@jscad/modeling@2.12.4...@jscad/modeling@2.12.5) (2024-12-29)
7
18
 
8
19
  **Note:** Version bump only for package @jscad/modeling
@@ -923,7 +923,7 @@ const pointInTriangle=(n,a,e,r,i,o,t,x)=>(i-t)*(a-x)-(n-t)*(o-x)>=0&&(n-t)*(r-x)
923
923
  const mat4=require("../../maths/mat4"),geom2=require("../../geometries/geom2"),geom3=require("../../geometries/geom3"),poly3=require("../../geometries/poly3"),slice=require("./slice"),repairSlice=require("./slice/repair"),extrudeWalls=require("./extrudeWalls"),defaultCallback=(e,r,l)=>{let t=null;return geom2.isA(l)&&(t=slice.fromSides(geom2.toSides(l))),poly3.isA(l)&&(t=slice.fromPoints(poly3.toPoints(l))),0===e||1===e?slice.transform(mat4.fromTranslation(mat4.create(),[0,0,e]),t):null},extrudeFromSlices=(e,r)=>{const l={numberOfSlices:2,capStart:!0,capEnd:!0,close:!1,repair:!0,callback:defaultCallback},{numberOfSlices:t,capStart:o,capEnd:c,close:s,repair:i,callback:a}=Object.assign({},l,e);if(t<2)throw new Error("numberOfSlices must be 2 or more");i&&(r=repairSlice(r));const n=t-1;let u=null,m=null,f=null,g=[];for(let e=0;e<t;e++){const l=a(e/n,e,r);if(l){if(!slice.isA(l))throw new Error("the callback function must return slice objects");if(0===slice.toEdges(l).length)throw new Error("the callback function must return slices with one or more edges");f&&(g=g.concat(extrudeWalls(f,l))),0===e&&(u=l),e===t-1&&(m=l),f=l}}if(c){const e=slice.toPolygons(m);g=g.concat(e)}if(o){const e=slice.toPolygons(u).map(poly3.invert);g=g.concat(e)}return o||c||s&&!slice.equals(m,u)&&(g=g.concat(extrudeWalls(m,u))),geom3.create(g)};module.exports=extrudeFromSlices;
924
924
 
925
925
  },{"../../geometries/geom2":25,"../../geometries/geom3":41,"../../geometries/poly3":79,"../../maths/mat4":143,"./extrudeWalls":317,"./slice":326,"./slice/repair":328}],309:[function(require,module,exports){
926
- const{TAU:TAU}=require("../../maths/constants"),slice=require("./slice"),mat4=require("../../maths/mat4"),extrudeFromSlices=require("./extrudeFromSlices"),geom2=require("../../geometries/geom2"),extrudeHelical=(e,t)=>{const r={angle:TAU,startAngle:0,pitch:10,endOffset:0,segmentsPerRotation:32},{angle:a,endOffset:s,segmentsPerRotation:o,startAngle:i}=Object.assign({},r,e);let n;n=!e.pitch&&e.height?e.height/(a/TAU):e.pitch?e.pitch:r.pitch;if(o<3)throw new Error("The number of segments per rotation needs to be at least 3.");const m=geom2.toSides(t);if(0===m.length)throw new Error("the given geometry cannot be empty");const c=m.filter(e=>e[0][0]>=0);let l=slice.fromSides(m);0===c.length&&(l=slice.reverse(l));const h=Math.round(o/TAU*Math.abs(a)),g=h>=2?h:2,u=mat4.create();let f;return extrudeFromSlices({numberOfSlices:g+1,callback:(e,t,r)=>{const o=i+a/g*t,m=s/g*t,c=(o-i)/TAU*n;return mat4.multiply(u,mat4.fromTranslation(mat4.create(),[m,0,c*Math.sign(a)]),mat4.fromXRotation(mat4.create(),-TAU/4*Math.sign(a))),f=mat4.create(),mat4.multiply(f,mat4.fromZRotation(mat4.create(),o),u),slice.transform(f,r)}},l)};module.exports=extrudeHelical;
926
+ const{TAU:TAU}=require("../../maths/constants"),mat4=require("../../maths/mat4"),geom2=require("../../geometries/geom2"),extrudeFromSlices=require("./extrudeFromSlices"),slice=require("./slice"),extrudeHelical=(e,t)=>{const r={angle:TAU,startAngle:0,pitch:10,height:0,endOffset:0,segmentsPerRotation:32};let{angle:a,startAngle:s,pitch:o,height:n,endOffset:i,segmentsPerRotation:m}=Object.assign({},r,e);0!=n&&(o=n/(a/TAU));if(m<3)throw new Error("The number of segments per rotation needs to be at least 3.");const l=geom2.toSides(t);if(0===l.length)throw new Error("The given geometry cannot be empty");const c=l.filter(e=>e[0][0]>=0);let g=slice.fromSides(l);0===c.length&&(g=slice.reverse(g));const h=Math.round(m/TAU*Math.abs(a)),u=h>=2?h:2,f=mat4.create(),d=mat4.create();return extrudeFromSlices({numberOfSlices:u+1,callback:(e,t,r)=>{const n=s+a/u*t,m=i/u*t,l=(n-s)/TAU*o;return mat4.multiply(f,mat4.fromTranslation(mat4.create(),[m,0,l*Math.sign(a)]),mat4.fromXRotation(mat4.create(),-TAU/4*Math.sign(a))),mat4.multiply(d,mat4.fromZRotation(mat4.create(),n),f),slice.transform(d,r)}},g)};module.exports=extrudeHelical;
927
927
 
928
928
  },{"../../geometries/geom2":25,"../../maths/constants":94,"../../maths/mat4":143,"./extrudeFromSlices":308,"./slice":326}],310:[function(require,module,exports){
929
929
  const flatten=require("../../utils/flatten"),geom2=require("../../geometries/geom2"),path2=require("../../geometries/path2"),extrudeLinearGeom2=require("./extrudeLinearGeom2"),extrudeLinearPath2=require("./extrudeLinearPath2"),extrudeLinear=(e,...t)=>{const{height:r,twistAngle:i,twistSteps:n,repair:a}=Object.assign({},{height:1,twistAngle:0,twistSteps:1,repair:!0},e);if(0===(t=flatten(t)).length)throw new Error("wrong number of arguments");e={offset:[0,0,r],twistAngle:i,twistSteps:n,repair:a};const s=t.map(t=>path2.isA(t)?extrudeLinearPath2(e,t):geom2.isA(t)?extrudeLinearGeom2(e,t):t);return 1===s.length?s[0]:s};module.exports=extrudeLinear;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jscad/modeling",
3
- "version": "2.12.5",
3
+ "version": "2.12.6",
4
4
  "description": "Constructive Solid Geometry (CSG) Library for JSCAD",
5
5
  "homepage": "https://openjscad.xyz/",
6
6
  "repository": "https://github.com/jscad/OpenJSCAD.org",
@@ -61,5 +61,5 @@
61
61
  "nyc": "15.1.0",
62
62
  "uglifyify": "5.0.2"
63
63
  },
64
- "gitHead": "d8010c4f1d70685404a510d604f8b6d5c362f93a"
64
+ "gitHead": "11a9a2d9235d804360116f776076c9f3237a3eb0"
65
65
  }
@@ -1,16 +1,18 @@
1
1
  const { TAU } = require('../../maths/constants')
2
- const slice = require('./slice')
3
2
  const mat4 = require('../../maths/mat4')
4
- const extrudeFromSlices = require('./extrudeFromSlices')
3
+
5
4
  const geom2 = require('../../geometries/geom2')
6
5
 
6
+ const extrudeFromSlices = require('./extrudeFromSlices')
7
+ const slice = require('./slice')
8
+
7
9
  /**
8
10
  * Perform a helical extrude of the geometry, using the given options.
9
11
  *
10
12
  * @param {Object} options - options for extrusion
11
- * @param {Number} [options.angle=TAU] - angle of the extrusion (RADIANS) positive for right-hand rotation, negative for left-hand
13
+ * @param {Number} [options.angle=TAU] - angle of the extrusion (RADIANS); positive for right-hand rotation, negative for left-hand
12
14
  * @param {Number} [options.startAngle=0] - start angle of the extrusion (RADIANS)
13
- * @param {Number} [options.pitch=10] - elevation gain for each turn
15
+ * @param {Number} [options.pitch=10] - elevation gain for each full rotation
14
16
  * @param {Number} [options.height] - total height of the helix path. Ignored if pitch is set.
15
17
  * @param {Number} [options.endOffset=0] - offset the final radius of the extrusion, allowing for tapered helix, and or spiral
16
18
  * @param {Number} [options.segmentsPerRotation=32] - number of segments per full rotation of the extrusion
@@ -20,24 +22,23 @@ const geom2 = require('../../geometries/geom2')
20
22
  *
21
23
  * @example
22
24
  * const myshape = circle({size: 3, center: [10, 0]}) // position for extrusion about Z
23
- * const mycoil = extrudeHelical({angle: TAU*2, pitch: 10, segmentsPerRotation: 64}, myshape))
25
+ * const mycoil = extrudeHelical({angle: TAU * 2, pitch: 10, segmentsPerRotation: 64}, myshape))
24
26
  */
25
27
  const extrudeHelical = (options, geometry) => {
26
28
  const defaults = {
27
29
  angle: TAU,
28
30
  startAngle: 0,
29
31
  pitch: 10,
32
+ height: 0,
30
33
  endOffset: 0,
31
34
  segmentsPerRotation: 32
32
35
  }
33
- const { angle, endOffset, segmentsPerRotation, startAngle } = Object.assign({}, defaults, options)
34
-
35
- let pitch
36
- // ignore height if pitch is set
37
- if (!options.pitch && options.height) {
38
- pitch = options.height / (angle / TAU)
39
- } else {
40
- pitch = options.pitch ? options.pitch : defaults.pitch
36
+ let { angle, startAngle, pitch, height, endOffset, segmentsPerRotation } = Object.assign({}, defaults, options)
37
+
38
+ // calculate pitch from height if available
39
+ if (height != 0) {
40
+ // height / number of full rotations
41
+ pitch = height / (angle / TAU)
41
42
  }
42
43
 
43
44
  // needs at least 3 segments for each revolution
@@ -46,7 +47,7 @@ const extrudeHelical = (options, geometry) => {
46
47
  if (segmentsPerRotation < minNumberOfSegments) { throw new Error('The number of segments per rotation needs to be at least 3.') }
47
48
 
48
49
  const shapeSides = geom2.toSides(geometry)
49
- if (shapeSides.length === 0) throw new Error('the given geometry cannot be empty')
50
+ if (shapeSides.length === 0) throw new Error('The given geometry cannot be empty')
50
51
 
51
52
  // const pointsWithNegativeX = shapeSides.filter((s) => (s[0][0] < 0))
52
53
  const pointsWithPositiveX = shapeSides.filter((s) => (s[0][0] >= 0))
@@ -60,9 +61,11 @@ const extrudeHelical = (options, geometry) => {
60
61
 
61
62
  const calculatedSegments = Math.round(segmentsPerRotation / TAU * Math.abs(angle))
62
63
  const segments = calculatedSegments >= 2 ? calculatedSegments : 2
64
+
63
65
  // define transform matrix variables for performance increase
64
66
  const step1 = mat4.create()
65
- let matrix
67
+ const step2 = mat4.create()
68
+
66
69
  const sliceCallback = (progress, index, base) => {
67
70
  const zRotation = startAngle + angle / segments * index
68
71
  const xOffset = endOffset / segments * index
@@ -84,14 +87,13 @@ const extrudeHelical = (options, geometry) => {
84
87
  mat4.fromXRotation(mat4.create(), -TAU / 4 * Math.sign(angle)) // rotate the slice correctly to not create inside-out polygon
85
88
  )
86
89
 
87
- matrix = mat4.create()
88
90
  mat4.multiply(
89
- matrix,
91
+ step2,
90
92
  // finally rotate around Z axis
91
93
  mat4.fromZRotation(mat4.create(), zRotation),
92
94
  step1
93
95
  )
94
- return slice.transform(matrix, base)
96
+ return slice.transform(step2, base)
95
97
  }
96
98
 
97
99
  return extrudeFromSlices(
@@ -34,7 +34,17 @@ test('extrudeHelical: (pitch) extruding of a circle produces an expected geom3',
34
34
  const geometry2 = circle({ size: 3, center: [10, 0] })
35
35
  for (const index of [...Array(20).keys()]) {
36
36
  // also test negative pitches
37
- const geometry3 = extrudeHelical({ pitch: startPitch + index }, geometry2)
37
+ const geometry3 = extrudeHelical({ pitch: startPitch + index, startAngle: TAU * index }, geometry2)
38
+ t.notThrows(() => geom3.validate(geometry3))
39
+ }
40
+ })
41
+
42
+ test('extrudeHelical: (height) extruding of a circle produces an expected geom3', (t) => {
43
+ const startHeight = -5
44
+ const geometry2 = circle({ size: 3, center: [10, 0] })
45
+ for (const index of [...Array(10).keys()]) {
46
+ // also test negative heights
47
+ const geometry3 = extrudeHelical({ height: startHeight + index, angle: TAU * (index + 1) }, geometry2)
38
48
  t.notThrows(() => geom3.validate(geometry3))
39
49
  }
40
50
  })
@@ -49,12 +59,12 @@ test('extrudeHelical: (endRadiusOffset) extruding of a circle produces an expect
49
59
  }
50
60
  })
51
61
 
52
- test('extrudeHelical: (segments) extruding of a circle produces an expected geom3', (t) => {
62
+ test('extrudeHelical: (segmentsPerRotation) extruding of a circle produces an expected geom3', (t) => {
53
63
  const startSegments = 3
54
64
  const geometry2 = circle({ size: 3, center: [10, 0] })
55
65
  for (const index of [...Array(30).keys()]) {
56
66
  // also test negative pitches
57
- const geometry3 = extrudeHelical({ segments: startSegments + index }, geometry2)
67
+ const geometry3 = extrudeHelical({ segmentsPerRotation: startSegments + index }, geometry2)
58
68
  t.notThrows(() => geom3.validate(geometry3))
59
69
  }
60
70
  })