fluidcad 0.0.35 → 0.0.37

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.
Files changed (186) hide show
  1. package/LICENSE.txt +21 -504
  2. package/README.md +1 -1
  3. package/bin/commands/login.js +33 -5
  4. package/bin/commands/mcp.js +3 -2
  5. package/bin/commands/publish.js +103 -8
  6. package/bin/lib/api-client.js +8 -0
  7. package/bin/lib/model-config.js +27 -4
  8. package/bin/lib/prompt.js +97 -0
  9. package/lib/dist/common/edge.d.ts +1 -1
  10. package/lib/dist/common/face.d.ts +1 -1
  11. package/lib/dist/common/scene-object.d.ts +6 -0
  12. package/lib/dist/common/scene-object.js +8 -0
  13. package/lib/dist/common/shape-factory.d.ts +1 -1
  14. package/lib/dist/common/shape-history-tracker.d.ts +1 -1
  15. package/lib/dist/common/shape.d.ts +1 -1
  16. package/lib/dist/common/solid.d.ts +1 -1
  17. package/lib/dist/common/transformable-primitive.d.ts +12 -1
  18. package/lib/dist/common/transformable-primitive.js +27 -0
  19. package/lib/dist/common/vertex.d.ts +1 -1
  20. package/lib/dist/common/wire.d.ts +1 -1
  21. package/lib/dist/core/2d/index.d.ts +1 -0
  22. package/lib/dist/core/2d/index.js +1 -0
  23. package/lib/dist/core/2d/text.d.ts +30 -0
  24. package/lib/dist/core/2d/text.js +37 -0
  25. package/lib/dist/core/helix.d.ts +20 -0
  26. package/lib/dist/core/helix.js +36 -0
  27. package/lib/dist/core/index.d.ts +3 -1
  28. package/lib/dist/core/index.js +2 -0
  29. package/lib/dist/core/interfaces.d.ts +180 -0
  30. package/lib/dist/core/wrap.d.ts +17 -0
  31. package/lib/dist/core/wrap.js +39 -0
  32. package/lib/dist/features/2d/text.d.ts +67 -0
  33. package/lib/dist/features/2d/text.js +320 -0
  34. package/lib/dist/features/cylinder.d.ts +3 -1
  35. package/lib/dist/features/cylinder.js +5 -2
  36. package/lib/dist/features/extrude-base.d.ts +1 -0
  37. package/lib/dist/features/extrude-to-face.d.ts +1 -0
  38. package/lib/dist/features/extrude-to-face.js +6 -0
  39. package/lib/dist/features/fillet.d.ts +1 -1
  40. package/lib/dist/features/helix.d.ts +41 -0
  41. package/lib/dist/features/helix.js +337 -0
  42. package/lib/dist/features/select.js +32 -8
  43. package/lib/dist/features/simple-extruder.d.ts +1 -1
  44. package/lib/dist/features/simple-extruder.js +7 -2
  45. package/lib/dist/features/sphere.d.ts +3 -1
  46. package/lib/dist/features/sphere.js +5 -2
  47. package/lib/dist/features/sweep.js +7 -2
  48. package/lib/dist/features/wrap.d.ts +39 -0
  49. package/lib/dist/features/wrap.js +116 -0
  50. package/lib/dist/filters/edge/belongs-to-face.d.ts +3 -1
  51. package/lib/dist/filters/edge/belongs-to-face.js +14 -10
  52. package/lib/dist/filters/filter.d.ts +1 -1
  53. package/lib/dist/filters/from-object.d.ts +1 -1
  54. package/lib/dist/filters/tangent-expander.d.ts +1 -1
  55. package/lib/dist/filters/tangent-expander.js +57 -40
  56. package/lib/dist/helpers/scene-helpers.d.ts +2 -0
  57. package/lib/dist/helpers/scene-helpers.js +1 -1
  58. package/lib/dist/index.d.ts +2 -0
  59. package/lib/dist/index.js +3 -1
  60. package/lib/dist/io/file-import.d.ts +7 -0
  61. package/lib/dist/io/file-import.js +28 -1
  62. package/lib/dist/io/font-registry.d.ts +45 -0
  63. package/lib/dist/io/font-registry.js +272 -0
  64. package/lib/dist/math/bspline-interpolation.d.ts +29 -0
  65. package/lib/dist/math/bspline-interpolation.js +194 -0
  66. package/lib/dist/oc/boolean-ops.d.ts +3 -1
  67. package/lib/dist/oc/boolean-ops.js +15 -1
  68. package/lib/dist/oc/color-transfer.d.ts +1 -1
  69. package/lib/dist/oc/constraints/constraint-helpers.d.ts +4 -4
  70. package/lib/dist/oc/constraints/curve/tangent-circle-solver.js +10 -9
  71. package/lib/dist/oc/constraints/curve/tangent-line-solver.js +5 -6
  72. package/lib/dist/oc/convert.d.ts +1 -1
  73. package/lib/dist/oc/draft-ops.d.ts +1 -1
  74. package/lib/dist/oc/edge-ops.d.ts +2 -2
  75. package/lib/dist/oc/edge-ops.js +13 -14
  76. package/lib/dist/oc/edge-props.d.ts +1 -1
  77. package/lib/dist/oc/edge-query.d.ts +1 -1
  78. package/lib/dist/oc/edge-query.js +3 -8
  79. package/lib/dist/oc/errors.d.ts +8 -0
  80. package/lib/dist/oc/errors.js +27 -0
  81. package/lib/dist/oc/explorer.d.ts +2 -2
  82. package/lib/dist/oc/extrude-ops.d.ts +28 -2
  83. package/lib/dist/oc/extrude-ops.js +56 -7
  84. package/lib/dist/oc/face-ops.d.ts +2 -1
  85. package/lib/dist/oc/face-ops.js +11 -0
  86. package/lib/dist/oc/face-props.d.ts +1 -1
  87. package/lib/dist/oc/face-query.d.ts +12 -1
  88. package/lib/dist/oc/face-query.js +39 -0
  89. package/lib/dist/oc/fillet-ops.d.ts +1 -1
  90. package/lib/dist/oc/fillet-ops.js +4 -4
  91. package/lib/dist/oc/geometry.d.ts +1 -1
  92. package/lib/dist/oc/geometry.js +12 -14
  93. package/lib/dist/oc/helix-ops.d.ts +37 -0
  94. package/lib/dist/oc/helix-ops.js +88 -0
  95. package/lib/dist/oc/hit-test.d.ts +1 -1
  96. package/lib/dist/oc/index.d.ts +4 -0
  97. package/lib/dist/oc/index.js +2 -0
  98. package/lib/dist/oc/init.d.ts +1 -1
  99. package/lib/dist/oc/init.js +1 -1
  100. package/lib/dist/oc/intersection.js +1 -1
  101. package/lib/dist/oc/io.d.ts +6 -6
  102. package/lib/dist/oc/io.js +31 -24
  103. package/lib/dist/oc/measure/classify.d.ts +34 -0
  104. package/lib/dist/oc/measure/classify.js +246 -0
  105. package/lib/dist/oc/measure/measure-ops.d.ts +9 -0
  106. package/lib/dist/oc/measure/measure-ops.js +210 -0
  107. package/lib/dist/oc/measure/measure-types.d.ts +39 -0
  108. package/lib/dist/oc/measure/measure-types.js +1 -0
  109. package/lib/dist/oc/measure/sampling.d.ts +9 -0
  110. package/lib/dist/oc/measure/sampling.js +77 -0
  111. package/lib/dist/oc/measure/vec.d.ts +13 -0
  112. package/lib/dist/oc/measure/vec.js +23 -0
  113. package/lib/dist/oc/mesh.d.ts +1 -1
  114. package/lib/dist/oc/mesh.js +40 -28
  115. package/lib/dist/oc/path-sampler.d.ts +29 -0
  116. package/lib/dist/oc/path-sampler.js +63 -0
  117. package/lib/dist/oc/props.d.ts +1 -1
  118. package/lib/dist/oc/props.js +4 -1
  119. package/lib/dist/oc/shape-hash.d.ts +26 -0
  120. package/lib/dist/oc/shape-hash.js +32 -0
  121. package/lib/dist/oc/shape-ops.d.ts +5 -3
  122. package/lib/dist/oc/shape-ops.js +6 -5
  123. package/lib/dist/oc/sweep-ops.d.ts +22 -1
  124. package/lib/dist/oc/sweep-ops.js +206 -18
  125. package/lib/dist/oc/text-outline.d.ts +62 -0
  126. package/lib/dist/oc/text-outline.js +212 -0
  127. package/lib/dist/oc/topology-index.d.ts +1 -1
  128. package/lib/dist/oc/vertex-ops.d.ts +1 -1
  129. package/lib/dist/oc/wire-ops.d.ts +1 -1
  130. package/lib/dist/oc/wire-ops.js +1 -1
  131. package/lib/dist/oc/wrap-development.d.ts +105 -0
  132. package/lib/dist/oc/wrap-development.js +179 -0
  133. package/lib/dist/oc/wrap-ops.d.ts +100 -0
  134. package/lib/dist/oc/wrap-ops.js +406 -0
  135. package/lib/dist/rendering/render-solid.js +10 -2
  136. package/lib/dist/scene-manager.d.ts +2 -0
  137. package/lib/dist/scene-manager.js +29 -0
  138. package/lib/dist/tests/features/cylinder-curve-filter.test.js +3 -3
  139. package/lib/dist/tests/features/extrude-to-face.test.js +38 -1
  140. package/lib/dist/tests/features/helix.test.d.ts +1 -0
  141. package/lib/dist/tests/features/helix.test.js +295 -0
  142. package/lib/dist/tests/features/repeat-primitive.test.d.ts +1 -0
  143. package/lib/dist/tests/features/repeat-primitive.test.js +60 -0
  144. package/lib/dist/tests/features/rib.test.js +6 -1
  145. package/lib/dist/tests/features/sweep.test.js +125 -1
  146. package/lib/dist/tests/features/text.test.d.ts +1 -0
  147. package/lib/dist/tests/features/text.test.js +347 -0
  148. package/lib/dist/tests/features/wrap-development.test.d.ts +1 -0
  149. package/lib/dist/tests/features/wrap-development.test.js +130 -0
  150. package/lib/dist/tests/features/wrap-extruded-target.test.d.ts +1 -0
  151. package/lib/dist/tests/features/wrap-extruded-target.test.js +106 -0
  152. package/lib/dist/tests/features/wrap-repeat.test.d.ts +1 -0
  153. package/lib/dist/tests/features/wrap-repeat.test.js +93 -0
  154. package/lib/dist/tests/features/wrap.test.d.ts +1 -0
  155. package/lib/dist/tests/features/wrap.test.js +331 -0
  156. package/lib/dist/tests/math/bspline-interpolation.test.d.ts +1 -0
  157. package/lib/dist/tests/math/bspline-interpolation.test.js +119 -0
  158. package/lib/dist/tests/measure.test.d.ts +1 -0
  159. package/lib/dist/tests/measure.test.js +288 -0
  160. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  161. package/llm-docs/api/helix.md +64 -0
  162. package/llm-docs/api/index.json +11 -2
  163. package/llm-docs/api/text.md +52 -0
  164. package/llm-docs/api/types/helix.md +105 -0
  165. package/llm-docs/api/types/text.md +138 -0
  166. package/llm-docs/api/types/wrap.md +131 -0
  167. package/llm-docs/api/wrap.md +62 -0
  168. package/llm-docs/index.json +121 -1
  169. package/mcp/dist/server.js +20 -1
  170. package/mcp/dist/tools/inspection.d.ts +17 -0
  171. package/mcp/dist/tools/inspection.js +14 -0
  172. package/package.json +7 -3
  173. package/server/dist/fluidcad-server.d.ts +29 -0
  174. package/server/dist/fluidcad-server.js +40 -0
  175. package/server/dist/index.js +4 -2
  176. package/server/dist/model-package/pack.js +7 -6
  177. package/server/dist/model-package/types.d.ts +4 -3
  178. package/server/dist/preferences.d.ts +4 -0
  179. package/server/dist/preferences.js +2 -0
  180. package/server/dist/routes/measure.d.ts +3 -0
  181. package/server/dist/routes/measure.js +32 -0
  182. package/server/dist/routes/preferences.js +6 -0
  183. package/server/dist/routes/sketch-edits.js +2 -1
  184. package/ui/dist/assets/{index-CDJmUpFI.css → index-dAFdg2Un.css} +1 -1
  185. package/ui/dist/assets/{index-MRqwG9Vh.js → index-no7mtr5s.js} +149 -102
  186. package/ui/dist/index.html +2 -2
@@ -0,0 +1,210 @@
1
+ import { getOC } from "../init.js";
2
+ import { classifyEdge, classifyFace } from "./classify.js";
3
+ import { maxDistanceBetween, sampleEntityPoints } from "./sampling.js";
4
+ import { acuteAngleDeg, add, areParallel, dist, dot, projectPointOnLine, scale, sub } from "./vec.js";
5
+ // sin(angle) tolerance for treating two directions as exactly parallel; loose
6
+ // enough to absorb numeric noise from booleans/imports, far below any
7
+ // deliberately modeled angle.
8
+ const PARALLEL_SIN_TOL = 1e-6;
9
+ const PERP_ANGLE_TOL_DEG = 1e-3;
10
+ const PRIMARY_LABELS = {
11
+ parallelDist: 'Parallel dist',
12
+ centerDist: 'Center dist',
13
+ axisDist: 'Axis dist',
14
+ minDist: 'Min dist',
15
+ totalArea: 'Area',
16
+ totalLength: 'Length',
17
+ };
18
+ function mkDist(from, to) {
19
+ return { value: dist(from, to), from, to };
20
+ }
21
+ function entityInfo(entity, ref) {
22
+ const info = { ref, geomType: entity.form };
23
+ if (entity.kind === 'face') {
24
+ info.area = entity.area;
25
+ }
26
+ else {
27
+ info.length = entity.length;
28
+ }
29
+ if (entity.radius !== undefined) {
30
+ info.radius = entity.radius;
31
+ }
32
+ return info;
33
+ }
34
+ /** Closest-pair distance between the two B-rep entities via BRepExtrema. */
35
+ function minDistanceBetween(a, b) {
36
+ const oc = getOC();
37
+ const progress = new oc.Message_ProgressRange();
38
+ const calc = new oc.BRepExtrema_DistShapeShape(a.shape, b.shape, oc.Extrema_ExtFlag.Extrema_ExtFlag_MIN, oc.Extrema_ExtAlgo.Extrema_ExtAlgo_Grad, progress);
39
+ let result;
40
+ if (calc.IsDone() && calc.NbSolution() > 0) {
41
+ const p1 = calc.PointOnShape1(1);
42
+ const p2 = calc.PointOnShape2(1);
43
+ result = {
44
+ value: calc.Value(),
45
+ from: { x: p1.X(), y: p1.Y(), z: p1.Z() },
46
+ to: { x: p2.X(), y: p2.Y(), z: p2.Z() },
47
+ };
48
+ p1.delete();
49
+ p2.delete();
50
+ }
51
+ calc.delete();
52
+ progress.delete();
53
+ return result;
54
+ }
55
+ /** A point on the entity's carrier line: the anchor itself for edges, the anchor projected onto the axis for cylinders/cones. */
56
+ function axisAnchor(entity) {
57
+ if (entity.form === 'line') {
58
+ return entity.anchor;
59
+ }
60
+ return projectPointOnLine(entity.anchor, entity.point, entity.dir);
61
+ }
62
+ /** Perpendicular distance from a parallel pair: plane↔plane, plane↔axis, axis↔axis. */
63
+ function parallelDistanceBetween(a, b) {
64
+ const aPlane = a.form === 'plane';
65
+ const bPlane = b.form === 'plane';
66
+ const aAxis = a.dirKind === 'axis';
67
+ const bAxis = b.dirKind === 'axis';
68
+ if (aPlane && bPlane) {
69
+ if (!areParallel(a.dir, b.dir, PARALLEL_SIN_TOL)) {
70
+ return undefined;
71
+ }
72
+ const signed = dot(sub(b.anchor, a.anchor), a.dir);
73
+ return { value: Math.abs(signed), from: a.anchor, to: add(a.anchor, scale(a.dir, signed)) };
74
+ }
75
+ if ((aPlane && bAxis) || (aAxis && bPlane)) {
76
+ const [plane, axis] = aPlane ? [a, b] : [b, a];
77
+ if (Math.abs(dot(plane.dir, axis.dir)) >= PARALLEL_SIN_TOL) {
78
+ return undefined;
79
+ }
80
+ const onAxis = axisAnchor(axis);
81
+ const signed = dot(sub(onAxis, plane.anchor), plane.dir);
82
+ const onPlane = sub(onAxis, scale(plane.dir, signed));
83
+ return aPlane ? mkDist(onPlane, onAxis) : mkDist(onAxis, onPlane);
84
+ }
85
+ if (a.form === 'line' && b.form === 'line') {
86
+ if (!areParallel(a.dir, b.dir, PARALLEL_SIN_TOL)) {
87
+ return undefined;
88
+ }
89
+ return mkDist(a.anchor, projectPointOnLine(a.anchor, b.point, b.dir));
90
+ }
91
+ return undefined;
92
+ }
93
+ /** Distance between the carrier axes of two parallel axis-bearing entities (cylinder/cone/line), at least one a surface. */
94
+ function axisDistanceBetween(a, b) {
95
+ if (a.dirKind !== 'axis' || b.dirKind !== 'axis') {
96
+ return undefined;
97
+ }
98
+ if (a.form === 'line' && b.form === 'line') {
99
+ return undefined;
100
+ }
101
+ if (!areParallel(a.dir, b.dir, PARALLEL_SIN_TOL)) {
102
+ return undefined;
103
+ }
104
+ const from = axisAnchor(a);
105
+ return mkDist(from, projectPointOnLine(from, b.point, b.dir));
106
+ }
107
+ /**
108
+ * Angle between two entities, normalized to [0, 90]°. Normal-vs-normal and
109
+ * axis-vs-axis compare directly; normal-vs-axis measures the axis against the
110
+ * plane (0° when the axis lies in the plane).
111
+ */
112
+ function angleBetween(a, b) {
113
+ if (!a.dir || !b.dir || !a.dirKind || !b.dirKind) {
114
+ return undefined;
115
+ }
116
+ if (a.dirKind === b.dirKind) {
117
+ const deg = acuteAngleDeg(a.dir, b.dir);
118
+ if (a.form === 'plane' && b.form === 'plane') {
119
+ const label = Math.abs(deg - 90) < PERP_ANGLE_TOL_DEG ? 'Perp planes angle' : 'Planes angle';
120
+ return { deg, label };
121
+ }
122
+ if (a.form === 'line' && b.form === 'line') {
123
+ return { deg, label: 'Lines angle' };
124
+ }
125
+ return { deg, label: a.dirKind === 'axis' ? 'Axes angle' : 'Angle' };
126
+ }
127
+ const [normal, axis] = a.dirKind === 'normal' ? [a, b] : [b, a];
128
+ const deg = 90 - acuteAngleDeg(normal.dir, axis.dir);
129
+ if (normal.form === 'plane') {
130
+ return { deg, label: axis.form === 'line' ? 'Line-plane angle' : 'Axis-plane angle' };
131
+ }
132
+ return { deg, label: 'Angle' };
133
+ }
134
+ function isParallelPair(a, b) {
135
+ if (!a.dir || !b.dir || !a.dirKind || !b.dirKind) {
136
+ return false;
137
+ }
138
+ if (a.dirKind === b.dirKind) {
139
+ return areParallel(a.dir, b.dir, PARALLEL_SIN_TOL);
140
+ }
141
+ return Math.abs(dot(a.dir, b.dir)) < PARALLEL_SIN_TOL;
142
+ }
143
+ function pickPrimary(a, b, result) {
144
+ const parallel = isParallelPair(a, b);
145
+ const planeLike = (e) => e.form === 'plane' || e.form === 'line';
146
+ if (planeLike(a) && planeLike(b)) {
147
+ return parallel && result.parallelDist ? 'parallelDist' : 'angle';
148
+ }
149
+ if (result.centerDist) {
150
+ return 'centerDist';
151
+ }
152
+ if (result.axisDist) {
153
+ return 'axisDist';
154
+ }
155
+ return 'minDist';
156
+ }
157
+ export class MeasureOps {
158
+ static measure(inputs) {
159
+ const classified = inputs.map((input) => input.ref.kind === 'face' ? classifyFace(input.shape) : classifyEdge(input.shape));
160
+ const result = {
161
+ entities: classified.map((entity, i) => entityInfo(entity, inputs[i].ref)),
162
+ primary: 'minDist',
163
+ primaryLabel: PRIMARY_LABELS.minDist,
164
+ };
165
+ const faces = classified.filter((e) => e.kind === 'face');
166
+ const edges = classified.filter((e) => e.kind === 'edge');
167
+ if (faces.length > 0) {
168
+ result.totalArea = faces.reduce((sum, e) => sum + (e.area ?? 0), 0);
169
+ }
170
+ if (edges.length > 0) {
171
+ result.totalLength = edges.reduce((sum, e) => sum + (e.length ?? 0), 0);
172
+ }
173
+ if (classified.length === 2) {
174
+ const [a, b] = classified;
175
+ result.minDist = minDistanceBetween(a, b);
176
+ result.maxDist = maxDistanceBetween(sampleEntityPoints(a), sampleEntityPoints(b));
177
+ result.parallelDist = parallelDistanceBetween(a, b);
178
+ result.axisDist = axisDistanceBetween(a, b);
179
+ if (a.center && b.center) {
180
+ result.centerDist = mkDist(a.center, b.center);
181
+ }
182
+ if (!isParallelPair(a, b)) {
183
+ const angle = angleBetween(a, b);
184
+ if (angle) {
185
+ result.angleDeg = angle.deg;
186
+ result.angleLabel = angle.label;
187
+ }
188
+ }
189
+ let primary = pickPrimary(a, b, result);
190
+ if (primary === 'angle' && result.angleDeg === undefined) {
191
+ primary = 'minDist';
192
+ }
193
+ if (primary !== 'angle' && primary !== 'minDist' && !result[primary]) {
194
+ primary = 'minDist';
195
+ }
196
+ result.primary = primary;
197
+ result.primaryLabel = primary === 'angle' ? result.angleLabel : PRIMARY_LABELS[primary];
198
+ return result;
199
+ }
200
+ // Single entity or 3+ entities: aggregate values only.
201
+ if (result.totalArea !== undefined) {
202
+ result.primary = 'totalArea';
203
+ }
204
+ else if (result.totalLength !== undefined) {
205
+ result.primary = 'totalLength';
206
+ }
207
+ result.primaryLabel = PRIMARY_LABELS[result.primary];
208
+ return result;
209
+ }
210
+ }
@@ -0,0 +1,39 @@
1
+ export interface MeasureVec {
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ }
6
+ /** A distance value together with the two world-space endpoints that realize it. */
7
+ export interface MeasureDistanceValue {
8
+ value: number;
9
+ from: MeasureVec;
10
+ to: MeasureVec;
11
+ }
12
+ export type MeasureEntityKind = 'face' | 'edge';
13
+ export interface MeasureEntityRef {
14
+ shapeId: string;
15
+ kind: MeasureEntityKind;
16
+ index: number;
17
+ }
18
+ export interface MeasureEntityInfo {
19
+ ref: MeasureEntityRef;
20
+ geomType: string;
21
+ area?: number;
22
+ length?: number;
23
+ radius?: number;
24
+ }
25
+ export type MeasurePrimaryKey = 'parallelDist' | 'centerDist' | 'axisDist' | 'minDist' | 'angle' | 'totalArea' | 'totalLength';
26
+ export interface MeasureResult {
27
+ entities: MeasureEntityInfo[];
28
+ primary: MeasurePrimaryKey;
29
+ primaryLabel: string;
30
+ minDist?: MeasureDistanceValue;
31
+ maxDist?: MeasureDistanceValue;
32
+ parallelDist?: MeasureDistanceValue;
33
+ centerDist?: MeasureDistanceValue;
34
+ axisDist?: MeasureDistanceValue;
35
+ angleDeg?: number;
36
+ angleLabel?: string;
37
+ totalArea?: number;
38
+ totalLength?: number;
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { MeasureDistanceValue, MeasureVec } from "./measure-types.js";
2
+ import type { ClassifiedEntity } from "./classify.js";
3
+ export declare function sampleEntityPoints(entity: ClassifiedEntity): MeasureVec[];
4
+ /**
5
+ * Approximate maximum distance between two entities as the farthest pair of
6
+ * sampled points (triangulation nodes / curve samples), so accuracy is bounded
7
+ * by the mesh deflection.
8
+ */
9
+ export declare function maxDistanceBetween(samplesA: MeasureVec[], samplesB: MeasureVec[]): MeasureDistanceValue;
@@ -0,0 +1,77 @@
1
+ import { getOC } from "../init.js";
2
+ import { Mesh } from "../mesh.js";
3
+ const MAX_FACE_SAMPLES = 400;
4
+ const EDGE_SAMPLES = 64;
5
+ function sampleFacePoints(shape) {
6
+ const oc = getOC();
7
+ const face = oc.TopoDS.Face(shape);
8
+ Mesh.ensureTriangulated(face);
9
+ const location = new oc.TopLoc_Location();
10
+ const triangulation = oc.BRep_Tool.Triangulation(face, location, 0);
11
+ if (triangulation.isNull()) {
12
+ location.delete();
13
+ return [];
14
+ }
15
+ const transform = location.Transformation();
16
+ const nbNodes = triangulation.NbNodes();
17
+ const stride = Math.max(1, Math.ceil(nbNodes / MAX_FACE_SAMPLES));
18
+ const points = [];
19
+ for (let i = 1; i <= nbNodes; i += stride) {
20
+ const node = triangulation.Node(i);
21
+ const p = node.Transformed(transform);
22
+ points.push({ x: p.X(), y: p.Y(), z: p.Z() });
23
+ node.delete();
24
+ p.delete();
25
+ }
26
+ transform.delete();
27
+ triangulation.delete();
28
+ location.delete();
29
+ return points;
30
+ }
31
+ function sampleEdgePoints(shape) {
32
+ const oc = getOC();
33
+ const edge = oc.TopoDS.Edge(shape);
34
+ const adaptor = new oc.BRepAdaptor_Curve(edge);
35
+ const first = adaptor.FirstParameter();
36
+ const last = adaptor.LastParameter();
37
+ const points = [];
38
+ for (let i = 0; i <= EDGE_SAMPLES; i++) {
39
+ const u = first + ((last - first) * i) / EDGE_SAMPLES;
40
+ const p = adaptor.Value(u);
41
+ points.push({ x: p.X(), y: p.Y(), z: p.Z() });
42
+ p.delete();
43
+ }
44
+ adaptor.delete();
45
+ return points;
46
+ }
47
+ export function sampleEntityPoints(entity) {
48
+ const points = entity.kind === 'face' ? sampleFacePoints(entity.shape) : sampleEdgePoints(entity.shape);
49
+ if (points.length === 0) {
50
+ points.push(entity.anchor);
51
+ }
52
+ return points;
53
+ }
54
+ /**
55
+ * Approximate maximum distance between two entities as the farthest pair of
56
+ * sampled points (triangulation nodes / curve samples), so accuracy is bounded
57
+ * by the mesh deflection.
58
+ */
59
+ export function maxDistanceBetween(samplesA, samplesB) {
60
+ let best = -1;
61
+ let from = samplesA[0];
62
+ let to = samplesB[0];
63
+ for (const a of samplesA) {
64
+ for (const b of samplesB) {
65
+ const dx = a.x - b.x;
66
+ const dy = a.y - b.y;
67
+ const dz = a.z - b.z;
68
+ const d2 = dx * dx + dy * dy + dz * dz;
69
+ if (d2 > best) {
70
+ best = d2;
71
+ from = a;
72
+ to = b;
73
+ }
74
+ }
75
+ }
76
+ return { value: Math.sqrt(best), from, to };
77
+ }
@@ -0,0 +1,13 @@
1
+ import type { MeasureVec } from "./measure-types.js";
2
+ export declare const sub: (a: MeasureVec, b: MeasureVec) => MeasureVec;
3
+ export declare const add: (a: MeasureVec, b: MeasureVec) => MeasureVec;
4
+ export declare const scale: (a: MeasureVec, s: number) => MeasureVec;
5
+ export declare const dot: (a: MeasureVec, b: MeasureVec) => number;
6
+ export declare const cross: (a: MeasureVec, b: MeasureVec) => MeasureVec;
7
+ export declare const len: (a: MeasureVec) => number;
8
+ export declare const dist: (a: MeasureVec, b: MeasureVec) => number;
9
+ export declare function projectPointOnLine(p: MeasureVec, linePoint: MeasureVec, lineDir: MeasureVec): MeasureVec;
10
+ /** Acute angle between two unit directions, in degrees ([0, 90], orientation-insensitive). */
11
+ export declare function acuteAngleDeg(a: MeasureVec, b: MeasureVec): number;
12
+ /** True when two unit directions are parallel (or anti-parallel) within `sinTol`. */
13
+ export declare function areParallel(a: MeasureVec, b: MeasureVec, sinTol: number): boolean;
@@ -0,0 +1,23 @@
1
+ export const sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y, z: a.z - b.z });
2
+ export const add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y, z: a.z + b.z });
3
+ export const scale = (a, s) => ({ x: a.x * s, y: a.y * s, z: a.z * s });
4
+ export const dot = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z;
5
+ export const cross = (a, b) => ({
6
+ x: a.y * b.z - a.z * b.y,
7
+ y: a.z * b.x - a.x * b.z,
8
+ z: a.x * b.y - a.y * b.x,
9
+ });
10
+ export const len = (a) => Math.sqrt(dot(a, a));
11
+ export const dist = (a, b) => len(sub(a, b));
12
+ export function projectPointOnLine(p, linePoint, lineDir) {
13
+ return add(linePoint, scale(lineDir, dot(sub(p, linePoint), lineDir)));
14
+ }
15
+ /** Acute angle between two unit directions, in degrees ([0, 90], orientation-insensitive). */
16
+ export function acuteAngleDeg(a, b) {
17
+ const c = Math.min(1, Math.abs(dot(a, b)));
18
+ return Math.acos(c) * (180 / Math.PI);
19
+ }
20
+ /** True when two unit directions are parallel (or anti-parallel) within `sinTol`. */
21
+ export function areParallel(a, b, sinTol) {
22
+ return len(cross(a, b)) < sinTol;
23
+ }
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Edge, TopoDS_Face, TopoDS_Shape } from "occjs-wrapper";
1
+ import type { TopoDS_Edge, TopoDS_Face, TopoDS_Shape } from "ocjs-fluidcad";
2
2
  import { Face } from "../common/face.js";
3
3
  import { Shape } from "../common/shape.js";
4
4
  export interface MeshData {
@@ -48,13 +48,12 @@ export class Mesh {
48
48
  const normals = [];
49
49
  const indices = [];
50
50
  const aLocation = new oc.TopLoc_Location();
51
- const faceTriangulation = oc.BRep_Tool.Triangulation(face, aLocation, 0);
52
- if (faceTriangulation.IsNull()) {
51
+ const triangulation = oc.BRep_Tool.Triangulation(face, aLocation, 0);
52
+ if (triangulation.isNull()) {
53
53
  aLocation.delete();
54
54
  return null;
55
55
  }
56
- const pc = new oc.Poly_Connect(faceTriangulation);
57
- const triangulation = faceTriangulation.get();
56
+ const pc = new oc.Poly_Connect(triangulation);
58
57
  const nbNodes = triangulation.NbNodes();
59
58
  for (let i = 1; i <= nbNodes; i++) {
60
59
  const t1 = aLocation.Transformation();
@@ -65,34 +64,50 @@ export class Mesh {
65
64
  p1.delete();
66
65
  t1.delete();
67
66
  }
68
- const faceNormals = new oc.TColgp_Array1OfDir(1, nbNodes);
69
- oc.StdPrs_ToolTriangulatedShape.Normal(face, pc, faceNormals);
67
+ // OCCT 8.0: StdPrs_ToolTriangulatedShape (TKV3d) is gone; the surface-normals
68
+ // utility moved to BRepLib_ToolTriangulatedShape (TKTopAlgo). It computes nodal
69
+ // normals into the triangulation itself (no-op if it already has them), which we
70
+ // then read back via Poly_Triangulation.Normal(i).
71
+ oc.BRepLib_ToolTriangulatedShape.ComputeNormals(face, triangulation, pc);
72
+ // A triangulation stores normals in the natural orientation of the
73
+ // underlying surface, NOT the face's topological orientation. For a
74
+ // TopAbs_REVERSED face the outward normal is the opposite of the surface
75
+ // normal, so we must flip the nodal normals here. (Pre-OCCT-8 this was done
76
+ // internally by StdPrs_ToolTriangulatedShape::Normal; BRepLib_ToolTriangulatedShape
77
+ // ::ComputeNormals does not, which left reversed faces shaded as if lit from
78
+ // inside the solid — they rendered dark.) The triangle winding below is
79
+ // swapped under the same condition so winding and shading normals agree.
80
+ const orient = face.Orientation();
81
+ const reversed = orient !== oc.TopAbs_Orientation.TopAbs_FORWARD;
70
82
  for (let i = 1; i <= nbNodes; i++) {
71
83
  const t1 = aLocation.Transformation();
72
- const d1 = faceNormals.Value(i);
84
+ const d1 = triangulation.Normal(i);
73
85
  const d = d1.Transformed(t1);
74
- normals.push(d.X(), d.Y(), d.Z());
86
+ if (reversed) {
87
+ normals.push(-d.X(), -d.Y(), -d.Z());
88
+ }
89
+ else {
90
+ normals.push(d.X(), d.Y(), d.Z());
91
+ }
75
92
  d1.delete();
76
93
  d.delete();
77
94
  t1.delete();
78
95
  }
79
- const orient = face.Orientation();
80
96
  const triangles = triangulation.Triangles();
81
97
  for (let nt = 1; nt <= triangulation.NbTriangles(); nt++) {
82
98
  const t = triangles.Value(nt);
83
99
  let n1 = t.Value(1) - 1;
84
100
  let n2 = t.Value(2) - 1;
85
101
  let n3 = t.Value(3) - 1;
86
- if (orient !== oc.TopAbs_Orientation.TopAbs_FORWARD) {
102
+ if (reversed) {
87
103
  [n1, n2] = [n2, n1];
88
104
  }
89
105
  indices.push(vertexOffset + n1, vertexOffset + n2, vertexOffset + n3);
90
106
  t.delete();
91
107
  }
92
108
  pc.delete();
93
- faceNormals.delete();
94
109
  triangles.delete();
95
- faceTriangulation.delete();
110
+ triangulation.delete();
96
111
  aLocation.delete();
97
112
  return { vertices, normals, indices, count: nbNodes };
98
113
  }
@@ -107,21 +122,19 @@ export class Mesh {
107
122
  return null;
108
123
  }
109
124
  const loc = new oc.TopLoc_Location();
110
- const triHandle = oc.BRep_Tool.Triangulation(face, loc, 0);
111
- if (triHandle.IsNull()) {
112
- triHandle.delete();
125
+ const tri = oc.BRep_Tool.Triangulation(face, loc, 0);
126
+ if (tri.isNull()) {
127
+ tri.delete();
113
128
  loc.delete();
114
129
  return null;
115
130
  }
116
- const polyHandle = oc.BRep_Tool.PolygonOnTriangulation(edge, triHandle, loc);
117
- if (polyHandle.IsNull()) {
118
- polyHandle.delete();
119
- triHandle.delete();
131
+ const poly = oc.BRep_Tool.PolygonOnTriangulation(edge, tri, loc);
132
+ if (poly.isNull()) {
133
+ poly.delete();
134
+ tri.delete();
120
135
  loc.delete();
121
136
  return null;
122
137
  }
123
- const tri = triHandle.get();
124
- const poly = polyHandle.get();
125
138
  const nbNodes = poly.NbNodes();
126
139
  const tx = loc.Transformation();
127
140
  const vertices = new Array(nbNodes * 3);
@@ -142,8 +155,8 @@ export class Mesh {
142
155
  indices[i * 2 + 1] = i + 1;
143
156
  }
144
157
  tx.delete();
145
- polyHandle.delete();
146
- triHandle.delete();
158
+ poly.delete();
159
+ tri.delete();
147
160
  loc.delete();
148
161
  return { vertices, normals: [], indices };
149
162
  }
@@ -161,15 +174,14 @@ export class Mesh {
161
174
  }
162
175
  Mesh.ensureTriangulated(edge, opts);
163
176
  const loc = new oc.TopLoc_Location();
164
- const polyHandle = oc.BRep_Tool.Polygon3D(ocEdge, loc);
165
- if (polyHandle.IsNull()) {
166
- polyHandle.delete();
177
+ const poly = oc.BRep_Tool.Polygon3D(ocEdge, loc);
178
+ if (poly.isNull()) {
179
+ poly.delete();
167
180
  loc.delete();
168
181
  ocEdge.delete();
169
182
  console.warn("Edge has no stored Polygon3D after meshing; returning empty polyline.");
170
183
  return { vertices: [], normals: [], indices: [] };
171
184
  }
172
- const poly = polyHandle.get();
173
185
  const nbNodes = poly.NbNodes();
174
186
  const nodes = poly.Nodes();
175
187
  const tx = loc.Transformation();
@@ -190,7 +202,7 @@ export class Mesh {
190
202
  indices[i * 2 + 1] = i + 1;
191
203
  }
192
204
  tx.delete();
193
- polyHandle.delete();
205
+ poly.delete();
194
206
  loc.delete();
195
207
  ocEdge.delete();
196
208
  return { vertices, normals: [], indices };
@@ -0,0 +1,29 @@
1
+ import { Wire } from "../common/wire.js";
2
+ import { Point } from "../math/point.js";
3
+ import { Vector3d } from "../math/vector3d.js";
4
+ export interface PathFrame {
5
+ point: Point;
6
+ /** Unit tangent in the direction of increasing arc length. */
7
+ tangent: Vector3d;
8
+ }
9
+ /**
10
+ * Arc-length parameterization of a wire. Wraps `BRepAdaptor_CompCurve` +
11
+ * `GCPnts_AbscissaPoint` so callers can evaluate a point and unit tangent at a
12
+ * distance measured along the path, independent of how the underlying curves
13
+ * are parameterized (exact on Béziers/B-splines, not just lines and arcs).
14
+ *
15
+ * Out-of-range distances wrap around on a closed wire and extrapolate
16
+ * linearly along the end tangents on an open one.
17
+ */
18
+ export declare class PathSampler {
19
+ private adaptor;
20
+ private firstParam;
21
+ readonly length: number;
22
+ readonly closed: boolean;
23
+ constructor(wire: Wire);
24
+ evalAt(s: number): PathFrame;
25
+ /** `count + 1` points evenly spaced by arc length, including both ends. */
26
+ sample(count: number): Point[];
27
+ private evalOn;
28
+ dispose(): void;
29
+ }
@@ -0,0 +1,63 @@
1
+ import { getOC } from "./init.js";
2
+ import { Convert } from "./convert.js";
3
+ /**
4
+ * Arc-length parameterization of a wire. Wraps `BRepAdaptor_CompCurve` +
5
+ * `GCPnts_AbscissaPoint` so callers can evaluate a point and unit tangent at a
6
+ * distance measured along the path, independent of how the underlying curves
7
+ * are parameterized (exact on Béziers/B-splines, not just lines and arcs).
8
+ *
9
+ * Out-of-range distances wrap around on a closed wire and extrapolate
10
+ * linearly along the end tangents on an open one.
11
+ */
12
+ export class PathSampler {
13
+ adaptor;
14
+ firstParam;
15
+ length;
16
+ closed;
17
+ constructor(wire) {
18
+ const oc = getOC();
19
+ this.adaptor = new oc.BRepAdaptor_CompCurve(wire.getShape(), false);
20
+ this.firstParam = this.adaptor.FirstParameter();
21
+ this.length = oc.GCPnts_AbscissaPoint.Length(this.adaptor);
22
+ this.closed = this.adaptor.IsClosed();
23
+ }
24
+ evalAt(s) {
25
+ if (this.closed) {
26
+ s = ((s % this.length) + this.length) % this.length;
27
+ }
28
+ else if (s < 0) {
29
+ const start = this.evalOn(0);
30
+ return { point: start.point.add(start.tangent.multiply(s)), tangent: start.tangent };
31
+ }
32
+ else if (s > this.length) {
33
+ const end = this.evalOn(this.length);
34
+ return { point: end.point.add(end.tangent.multiply(s - this.length)), tangent: end.tangent };
35
+ }
36
+ return this.evalOn(s);
37
+ }
38
+ /** `count + 1` points evenly spaced by arc length, including both ends. */
39
+ sample(count) {
40
+ const points = [];
41
+ for (let i = 0; i <= count; i++) {
42
+ points.push(this.evalOn((this.length * i) / count).point);
43
+ }
44
+ return points;
45
+ }
46
+ evalOn(s) {
47
+ const oc = getOC();
48
+ const abscissa = new oc.GCPnts_AbscissaPoint(this.adaptor, s, this.firstParam);
49
+ if (!abscissa.IsDone()) {
50
+ abscissa.delete();
51
+ throw new Error(`Failed to locate arc-length ${s} on path (length ${this.length}).`);
52
+ }
53
+ const u = abscissa.Parameter();
54
+ abscissa.delete();
55
+ const res = this.adaptor.EvalD1(u);
56
+ const point = Convert.toPoint(res.Point, true);
57
+ const tangent = Convert.toVector3d(res.D1, true).normalize();
58
+ return { point, tangent };
59
+ }
60
+ dispose() {
61
+ this.adaptor.delete();
62
+ }
63
+ }
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Shape } from "occjs-wrapper";
1
+ import type { TopoDS_Shape } from "ocjs-fluidcad";
2
2
  export interface ShapeProperties {
3
3
  volumeMm3: number;
4
4
  surfaceAreaMm2: number;
@@ -3,7 +3,10 @@ export class ShapeProps {
3
3
  static getProperties(shape) {
4
4
  const oc = getOC();
5
5
  const volumeProps = new oc.GProp_GProps();
6
- oc.BRepGProp.VolumeProperties(shape, volumeProps, false, false, false);
6
+ // Eps-driven adaptive integration: the default fixed-order quadrature
7
+ // under-integrates faces trimmed by fitted B-spline pcurves (seen with
8
+ // wrap() pads — ~7% volume error) while this overload stays exact.
9
+ oc.BRepGProp.VolumeProperties(shape, volumeProps, 1e-6, false, false);
7
10
  const volumeMm3 = volumeProps.Mass();
8
11
  const cog = volumeProps.CentreOfMass();
9
12
  const centroid = { x: cog.X(), y: cog.Y(), z: cog.Z() };
@@ -0,0 +1,26 @@
1
+ import type { TopoDS_Shape } from "ocjs-fluidcad";
2
+ /**
3
+ * Produces stable, `IsSame`-consistent integer keys for TopoDS shapes so they can
4
+ * be used as JS `Map`/`Set` keys.
5
+ *
6
+ * OCCT 8.0 removed `TopoDS_Shape::HashCode`. This interns shapes in a
7
+ * `TopTools_IndexedMapOfShape` — whose hasher compares keys with `IsSame` — and
8
+ * returns each shape's stable 1-based index. That gives the same bucketing the old
9
+ * `HashCode` + `IsSame` check provided, but collision-free (the same `IsSame`
10
+ * sub-shape always maps to the same key, distinct sub-shapes never collide).
11
+ *
12
+ * Wraps an OCCT map, so it owns native memory: scope one per operation and
13
+ * `delete()` it when done. Follows the same indexed-map convention as
14
+ * {@link TopologyIndex}.
15
+ */
16
+ export declare class ShapeHasher {
17
+ private readonly map;
18
+ constructor();
19
+ /**
20
+ * Stable, `IsSame`-consistent key for `shape`. Interns the shape on first use; an
21
+ * as-yet-unseen shape gets a fresh key, so a `Map` lookup against keys from other
22
+ * shapes correctly misses.
23
+ */
24
+ key(shape: TopoDS_Shape): number;
25
+ delete(): void;
26
+ }