cad-mcp-server 0.1.0

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 (208) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/THIRD_PARTY_NOTICES.md +20 -0
  4. package/dist/cad/compare.d.ts +31 -0
  5. package/dist/cad/compare.d.ts.map +1 -0
  6. package/dist/cad/compare.js +51 -0
  7. package/dist/cad/compare.js.map +1 -0
  8. package/dist/cad/model-store.d.ts +52 -0
  9. package/dist/cad/model-store.d.ts.map +1 -0
  10. package/dist/cad/model-store.js +227 -0
  11. package/dist/cad/model-store.js.map +1 -0
  12. package/dist/cad/query/edges.d.ts +6 -0
  13. package/dist/cad/query/edges.d.ts.map +1 -0
  14. package/dist/cad/query/edges.js +257 -0
  15. package/dist/cad/query/edges.js.map +1 -0
  16. package/dist/cad/query/entities.d.ts +4 -0
  17. package/dist/cad/query/entities.d.ts.map +1 -0
  18. package/dist/cad/query/entities.js +185 -0
  19. package/dist/cad/query/entities.js.map +1 -0
  20. package/dist/cad/query/faces.d.ts +6 -0
  21. package/dist/cad/query/faces.d.ts.map +1 -0
  22. package/dist/cad/query/faces.js +305 -0
  23. package/dist/cad/query/faces.js.map +1 -0
  24. package/dist/cad/query/pmi.d.ts +3 -0
  25. package/dist/cad/query/pmi.d.ts.map +1 -0
  26. package/dist/cad/query/pmi.js +141 -0
  27. package/dist/cad/query/pmi.js.map +1 -0
  28. package/dist/cad/query/shared.d.ts +74 -0
  29. package/dist/cad/query/shared.d.ts.map +1 -0
  30. package/dist/cad/query/shared.js +181 -0
  31. package/dist/cad/query/shared.js.map +1 -0
  32. package/dist/cad/schema-version.d.ts +2 -0
  33. package/dist/cad/schema-version.d.ts.map +1 -0
  34. package/dist/cad/schema-version.js +2 -0
  35. package/dist/cad/schema-version.js.map +1 -0
  36. package/dist/compare.d.ts +31 -0
  37. package/dist/compare.d.ts.map +1 -0
  38. package/dist/compare.js +51 -0
  39. package/dist/compare.js.map +1 -0
  40. package/dist/index.d.ts +17 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +113 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/kernel/aag-utils.d.ts +7 -0
  45. package/dist/kernel/aag-utils.d.ts.map +1 -0
  46. package/dist/kernel/aag-utils.js +54 -0
  47. package/dist/kernel/aag-utils.js.map +1 -0
  48. package/dist/kernel/import.d.ts +4 -0
  49. package/dist/kernel/import.d.ts.map +1 -0
  50. package/dist/kernel/import.js +25 -0
  51. package/dist/kernel/import.js.map +1 -0
  52. package/dist/kernel/kernel.d.ts +3 -0
  53. package/dist/kernel/kernel.d.ts.map +1 -0
  54. package/dist/kernel/kernel.js +7 -0
  55. package/dist/kernel/kernel.js.map +1 -0
  56. package/dist/kernel/measure.d.ts +6 -0
  57. package/dist/kernel/measure.d.ts.map +1 -0
  58. package/dist/kernel/measure.js +23 -0
  59. package/dist/kernel/measure.js.map +1 -0
  60. package/dist/kernel/query-entities.d.ts +75 -0
  61. package/dist/kernel/query-entities.d.ts.map +1 -0
  62. package/dist/kernel/query-entities.js +190 -0
  63. package/dist/kernel/query-entities.js.map +1 -0
  64. package/dist/kernel/topology.d.ts +4 -0
  65. package/dist/kernel/topology.d.ts.map +1 -0
  66. package/dist/kernel/topology.js +48 -0
  67. package/dist/kernel/topology.js.map +1 -0
  68. package/dist/model-store.d.ts +52 -0
  69. package/dist/model-store.d.ts.map +1 -0
  70. package/dist/model-store.js +227 -0
  71. package/dist/model-store.js.map +1 -0
  72. package/dist/pmi/metadata.d.ts +15 -0
  73. package/dist/pmi/metadata.d.ts.map +1 -0
  74. package/dist/pmi/metadata.js +93 -0
  75. package/dist/pmi/metadata.js.map +1 -0
  76. package/dist/pmi/parser.d.ts +46 -0
  77. package/dist/pmi/parser.d.ts.map +1 -0
  78. package/dist/pmi/parser.js +400 -0
  79. package/dist/pmi/parser.js.map +1 -0
  80. package/dist/pmi/semantic-provider.d.ts +7 -0
  81. package/dist/pmi/semantic-provider.d.ts.map +1 -0
  82. package/dist/pmi/semantic-provider.js +96 -0
  83. package/dist/pmi/semantic-provider.js.map +1 -0
  84. package/dist/providers/brep.d.ts +39 -0
  85. package/dist/providers/brep.d.ts.map +1 -0
  86. package/dist/providers/brep.js +2 -0
  87. package/dist/providers/brep.js.map +1 -0
  88. package/dist/providers/lightweight-step/metadata.d.ts +15 -0
  89. package/dist/providers/lightweight-step/metadata.d.ts.map +1 -0
  90. package/dist/providers/lightweight-step/metadata.js +93 -0
  91. package/dist/providers/lightweight-step/metadata.js.map +1 -0
  92. package/dist/providers/lightweight-step/pmi-parser.d.ts +46 -0
  93. package/dist/providers/lightweight-step/pmi-parser.d.ts.map +1 -0
  94. package/dist/providers/lightweight-step/pmi-parser.js +400 -0
  95. package/dist/providers/lightweight-step/pmi-parser.js.map +1 -0
  96. package/dist/providers/lightweight-step/semantic-provider.d.ts +7 -0
  97. package/dist/providers/lightweight-step/semantic-provider.d.ts.map +1 -0
  98. package/dist/providers/lightweight-step/semantic-provider.js +96 -0
  99. package/dist/providers/lightweight-step/semantic-provider.js.map +1 -0
  100. package/dist/providers/occt-wasm/aag-utils.d.ts +7 -0
  101. package/dist/providers/occt-wasm/aag-utils.d.ts.map +1 -0
  102. package/dist/providers/occt-wasm/aag-utils.js +54 -0
  103. package/dist/providers/occt-wasm/aag-utils.js.map +1 -0
  104. package/dist/providers/occt-wasm/import.d.ts +4 -0
  105. package/dist/providers/occt-wasm/import.d.ts.map +1 -0
  106. package/dist/providers/occt-wasm/import.js +25 -0
  107. package/dist/providers/occt-wasm/import.js.map +1 -0
  108. package/dist/providers/occt-wasm/kernel.d.ts +3 -0
  109. package/dist/providers/occt-wasm/kernel.d.ts.map +1 -0
  110. package/dist/providers/occt-wasm/kernel.js +7 -0
  111. package/dist/providers/occt-wasm/kernel.js.map +1 -0
  112. package/dist/providers/occt-wasm/measure.d.ts +6 -0
  113. package/dist/providers/occt-wasm/measure.d.ts.map +1 -0
  114. package/dist/providers/occt-wasm/measure.js +23 -0
  115. package/dist/providers/occt-wasm/measure.js.map +1 -0
  116. package/dist/providers/occt-wasm/query-entities.d.ts +71 -0
  117. package/dist/providers/occt-wasm/query-entities.d.ts.map +1 -0
  118. package/dist/providers/occt-wasm/query-entities.js +177 -0
  119. package/dist/providers/occt-wasm/query-entities.js.map +1 -0
  120. package/dist/providers/occt-wasm/topology.d.ts +4 -0
  121. package/dist/providers/occt-wasm/topology.d.ts.map +1 -0
  122. package/dist/providers/occt-wasm/topology.js +48 -0
  123. package/dist/providers/occt-wasm/topology.js.map +1 -0
  124. package/dist/providers/schema.d.ts +50 -0
  125. package/dist/providers/schema.d.ts.map +1 -0
  126. package/dist/providers/schema.js +2 -0
  127. package/dist/providers/schema.js.map +1 -0
  128. package/dist/providers/semantic.d.ts +34 -0
  129. package/dist/providers/semantic.d.ts.map +1 -0
  130. package/dist/providers/semantic.js +2 -0
  131. package/dist/providers/semantic.js.map +1 -0
  132. package/dist/query/edges.d.ts +6 -0
  133. package/dist/query/edges.d.ts.map +1 -0
  134. package/dist/query/edges.js +257 -0
  135. package/dist/query/edges.js.map +1 -0
  136. package/dist/query/entities.d.ts +4 -0
  137. package/dist/query/entities.d.ts.map +1 -0
  138. package/dist/query/entities.js +203 -0
  139. package/dist/query/entities.js.map +1 -0
  140. package/dist/query/faces.d.ts +6 -0
  141. package/dist/query/faces.d.ts.map +1 -0
  142. package/dist/query/faces.js +309 -0
  143. package/dist/query/faces.js.map +1 -0
  144. package/dist/query/pmi.d.ts +3 -0
  145. package/dist/query/pmi.d.ts.map +1 -0
  146. package/dist/query/pmi.js +141 -0
  147. package/dist/query/pmi.js.map +1 -0
  148. package/dist/query/shared.d.ts +74 -0
  149. package/dist/query/shared.d.ts.map +1 -0
  150. package/dist/query/shared.js +181 -0
  151. package/dist/query/shared.js.map +1 -0
  152. package/dist/schema-version.d.ts +2 -0
  153. package/dist/schema-version.d.ts.map +1 -0
  154. package/dist/schema-version.js +2 -0
  155. package/dist/schema-version.js.map +1 -0
  156. package/dist/tools/shared.d.ts +11 -0
  157. package/dist/tools/shared.d.ts.map +1 -0
  158. package/dist/tools/shared.js +19 -0
  159. package/dist/tools/shared.js.map +1 -0
  160. package/dist/tools/step-tools.d.ts +758 -0
  161. package/dist/tools/step-tools.d.ts.map +1 -0
  162. package/dist/tools/step-tools.js +836 -0
  163. package/dist/tools/step-tools.js.map +1 -0
  164. package/dist/types/brep.d.ts +39 -0
  165. package/dist/types/brep.d.ts.map +1 -0
  166. package/dist/types/brep.js +2 -0
  167. package/dist/types/brep.js.map +1 -0
  168. package/dist/types/schema.d.ts +50 -0
  169. package/dist/types/schema.d.ts.map +1 -0
  170. package/dist/types/schema.js +2 -0
  171. package/dist/types/schema.js.map +1 -0
  172. package/dist/types/semantic.d.ts +34 -0
  173. package/dist/types/semantic.d.ts.map +1 -0
  174. package/dist/types/semantic.js +2 -0
  175. package/dist/types/semantic.js.map +1 -0
  176. package/dist/utils/errors.d.ts +7 -0
  177. package/dist/utils/errors.d.ts.map +1 -0
  178. package/dist/utils/errors.js +7 -0
  179. package/dist/utils/errors.js.map +1 -0
  180. package/dist/utils/ids.d.ts +2 -0
  181. package/dist/utils/ids.d.ts.map +1 -0
  182. package/dist/utils/ids.js +4 -0
  183. package/dist/utils/ids.js.map +1 -0
  184. package/dist/utils/numbers.d.ts +11 -0
  185. package/dist/utils/numbers.d.ts.map +1 -0
  186. package/dist/utils/numbers.js +15 -0
  187. package/dist/utils/numbers.js.map +1 -0
  188. package/dist/utils/vectors.d.ts +4 -0
  189. package/dist/utils/vectors.d.ts.map +1 -0
  190. package/dist/utils/vectors.js +14 -0
  191. package/dist/utils/vectors.js.map +1 -0
  192. package/docs/EXAMPLE_PROMPTS.md +61 -0
  193. package/node_modules/occt-wasm/dist/index.d.ts +303 -0
  194. package/node_modules/occt-wasm/dist/index.d.ts.map +1 -0
  195. package/node_modules/occt-wasm/dist/index.js +1125 -0
  196. package/node_modules/occt-wasm/dist/index.js.map +1 -0
  197. package/node_modules/occt-wasm/dist/occt-wasm.js +2 -0
  198. package/node_modules/occt-wasm/dist/occt-wasm.wasm +0 -0
  199. package/node_modules/occt-wasm/dist/raw-types.d.ts +229 -0
  200. package/node_modules/occt-wasm/dist/raw-types.d.ts.map +1 -0
  201. package/node_modules/occt-wasm/dist/raw-types.js +8 -0
  202. package/node_modules/occt-wasm/dist/raw-types.js.map +1 -0
  203. package/node_modules/occt-wasm/dist/types.d.ts +223 -0
  204. package/node_modules/occt-wasm/dist/types.d.ts.map +1 -0
  205. package/node_modules/occt-wasm/dist/types.js +129 -0
  206. package/node_modules/occt-wasm/dist/types.js.map +1 -0
  207. package/node_modules/occt-wasm/package.json +44 -0
  208. package/package.json +102 -0
@@ -0,0 +1,836 @@
1
+ import { z } from 'zod';
2
+ import { compareStepFiles } from '../compare.js';
3
+ import { withStepModel } from '../model-store.js';
4
+ import { CAD_RESPONSE_SCHEMA_VERSION } from '../schema-version.js';
5
+ import { queryStepEdges as queryEdgesService } from '../query/edges.js';
6
+ import { canDirectGetEntities, getStepEntitiesDirect } from '../query/entities.js';
7
+ import { queryStepFaces as queryFacesService } from '../query/faces.js';
8
+ import { queryStepPmi as queryPmiService } from '../query/pmi.js';
9
+ import { wrapTool } from './shared.js';
10
+ const stepFileInput = {
11
+ file_path: z.string().min(1).describe('Absolute or relative path to the STEP file.'),
12
+ };
13
+ const inspectStepFileSchema = {
14
+ ...stepFileInput,
15
+ };
16
+ const point3Schema = z.array(z.number().finite()).length(3);
17
+ const direction3Schema = point3Schema.refine(([x, y, z]) => x !== 0 || y !== 0 || z !== 0, {
18
+ message: 'Direction vector must be non-zero.',
19
+ });
20
+ const normalFilterSchema = z
21
+ .object({
22
+ parallel_to: direction3Schema.describe('Direction vector [x, y, z] to match face normals against. Only faces whose normal is parallel to this direction match.'),
23
+ tolerance_degrees: z
24
+ .number()
25
+ .nonnegative()
26
+ .max(180)
27
+ .describe('Angle tolerance in degrees (default: 10). Face normals within +/-tolerance of target direction match.')
28
+ .optional(),
29
+ })
30
+ .strict()
31
+ .describe('Filter by face normal direction. IMPORTANT: setting this restricts results to orientation-filtered faces. Only set when you need faces with a specific normal direction.')
32
+ .optional();
33
+ const edgeRadiusSchema = z
34
+ .object({
35
+ min: z
36
+ .number()
37
+ .nonnegative()
38
+ .describe('Minimum radius in mm. Only affects circular/curved edges.')
39
+ .optional(),
40
+ max: z
41
+ .number()
42
+ .nonnegative()
43
+ .describe('Maximum radius in mm. Only affects circular/curved edges.')
44
+ .optional(),
45
+ })
46
+ .strict()
47
+ .refine((r) => r.min === undefined || r.max === undefined || r.min <= r.max, {
48
+ message: 'radius.min must be <= radius.max.',
49
+ })
50
+ .describe('Filter circular/curved edges by radius. IMPORTANT: setting this restricts results to edges that carry a radius (circular/curved only). Only set when querying circular edges by radius. Do NOT set as a placeholder.')
51
+ .optional();
52
+ function uniqueArray(values) {
53
+ return new Set(values).size === values.length;
54
+ }
55
+ function boundedRange(input) {
56
+ return input.min === undefined || input.max === undefined || input.min <= input.max;
57
+ }
58
+ const resultModeSchema = z
59
+ .enum(['summary', 'entities', 'groups'])
60
+ .describe('Shape of the result object. "summary" = statistics and counts only, no entity list (fastest, fewest tokens). "entities" (default) = paginated entity list with projections. "groups" = aggregate the matched entities into groups (requires group_by; returns counts plus sample entity IDs per group). Use "summary" or "groups" first in a conversation, then drill into specific entities.')
61
+ .optional();
62
+ const returnTypeSchema = z
63
+ .enum(['summary', 'entities', 'groups'])
64
+ .describe('Result shape: "summary" returns statistics only (fastest). "entities" (default) returns paginated entities with projections. "groups" returns group counts with sample IDs (requires group_by).')
65
+ .optional();
66
+ const limitSchema = z
67
+ .number()
68
+ .int()
69
+ .positive()
70
+ .max(1000)
71
+ .describe('Maximum number of entities to return per page. Default: 100. Max: 1000. Use with offset for pagination.')
72
+ .optional();
73
+ const offsetSchema = z
74
+ .number()
75
+ .int()
76
+ .nonnegative()
77
+ .describe('Skip this many results before returning (for pagination). Default: 0. E.g., offset=100, limit=50 returns results 100-149.')
78
+ .optional();
79
+ const bodyIdSchema = z.string().regex(/^body:\d+$/, 'Body IDs must match body:N.');
80
+ const FACE_FIELDS = [
81
+ 'id',
82
+ 'surface_type',
83
+ 'area',
84
+ 'bbox',
85
+ 'bbox_center',
86
+ 'normal',
87
+ 'surface_parameters',
88
+ 'axis',
89
+ 'adjacent_faces',
90
+ 'closest_face_distance',
91
+ 'has_inner_wires',
92
+ 'body_id',
93
+ ];
94
+ const EDGE_FIELDS = [
95
+ 'id',
96
+ 'curve_type',
97
+ 'length',
98
+ 'bbox',
99
+ 'bbox_center',
100
+ 'radius',
101
+ 'start_point',
102
+ 'end_point',
103
+ 'adjacent_faces',
104
+ 'body_id',
105
+ ];
106
+ const FACE_GET_FIELDS = new Set(FACE_FIELDS);
107
+ const EDGE_GET_FIELDS = new Set(EDGE_FIELDS);
108
+ function mapBboxCenter(fields) {
109
+ return fields?.map((f) => (f === 'bbox_center' ? 'center' : f));
110
+ }
111
+ const faceIncludeSchema = z
112
+ .array(z
113
+ .enum(FACE_FIELDS.map((f) => (f === 'bbox_center' ? 'center' : f)))
114
+ .describe('Face projection fields: id=unique identifier, surface_type=geometry type, area=surface area mm^2, bbox=bounding box, center=centroid, normal=surface normal direction, surface_parameters=raw OCCT surface data (e.g. cylinder radius), axis=cylinder axis direction and location (cylindrical faces only), adjacent_faces=list of adjacent faces with dihedral angle, closest_face_distance=minimum distance to any other face in the model, has_inner_wires=whether the face boundary contains interior wire(s) (holes/openings), body_id=which body this face belongs to (body:0, body:1, ...). Default: id,surface_type,area,bbox,center.'))
115
+ .min(1)
116
+ .max(12)
117
+ .refine(uniqueArray, 'Include values must be unique.')
118
+ .describe('List of face properties to include in results. Omit to get default projection (id, surface_type, area, bbox, center).')
119
+ .optional();
120
+ const faceFieldsSchema = z
121
+ .array(z.enum(FACE_FIELDS))
122
+ .min(1)
123
+ .max(12)
124
+ .refine(uniqueArray, 'Field values must be unique.')
125
+ .describe('Face fields to include. Default: id,surface_type,area,bbox,bbox_center. New: axis returns {direction, location} for cylindrical faces.')
126
+ .optional();
127
+ const edgeIncludeSchema = z
128
+ .array(z
129
+ .enum(EDGE_FIELDS.map((f) => (f === 'bbox_center' ? 'center' : f)))
130
+ .describe('Edge projection fields: id=unique identifier, curve_type=line/circle/ellipse/bspline/other, length=edge length mm, bbox=bounding box, center=midpoint or arc center, radius=radius for circular curves (null for lines), start_point=endpoint [x,y,z], end_point=other endpoint [x,y,z], adjacent_faces=the faces that bound this edge with face_id and surface_type, body_id=which body this edge belongs to (body:0, body:1, ...). Default: id,curve_type,length,bbox,center.'))
131
+ .min(1)
132
+ .max(10)
133
+ .refine(uniqueArray, 'Include values must be unique.')
134
+ .describe('List of edge properties to include in results. Omit to get default projection (id, curve_type, length, bbox, center).')
135
+ .optional();
136
+ const edgeFieldsSchema = z
137
+ .array(z.enum(EDGE_FIELDS))
138
+ .min(1)
139
+ .max(10)
140
+ .refine(uniqueArray, 'Field values must be unique.')
141
+ .describe('Edge fields to include. Default: id,curve_type,length,bbox,bbox_center.')
142
+ .optional();
143
+ const faceGroupBySchema = z
144
+ .array(z
145
+ .enum(['surface_type', 'normal_direction', 'area_range', 'radius', 'body_id'])
146
+ .describe('Grouping dimension: surface_type=plane/cylinder/cone/etc; normal_direction=nearest principal axis (+X..-Z within 15 degrees, else off-axis); area_range=fixed log-scale size bucket in mm^2 (0-1, 1-10, 10-100, ...); radius=rounded to 0.5mm (cylindrical faces only); body_id=which body the face belongs to.'))
147
+ .min(1)
148
+ .max(5)
149
+ .refine(uniqueArray, 'Group-by values must be unique.')
150
+ .describe('List of dimensions to group faces by; required when result_mode is "groups". E.g., ["surface_type"] groups by geometry type. Combining dimensions produces one group per distinct key combination. Bucket widths are fixed by the server.')
151
+ .optional();
152
+ const edgeGroupBySchema = z
153
+ .array(z
154
+ .enum(['curve_type', 'length_range', 'body_id'])
155
+ .describe('Grouping dimension: curve_type=line/circle/ellipse/bspline/other; length_range=fixed log-scale length bucket in mm (0-1, 1-10, 10-100, ...); body_id=which body the edge belongs to.'))
156
+ .min(1)
157
+ .max(3)
158
+ .refine(uniqueArray, 'Group-by values must be unique.')
159
+ .describe('List of dimensions to group edges by; required when result_mode is "groups". E.g., ["curve_type","length_range"] groups by type and length bucket. Bucket widths are fixed by the server.')
160
+ .optional();
161
+ const faceSortSchema = z
162
+ .object({
163
+ by: z
164
+ .enum(['area', 'surface_type', 'center_x', 'center_y', 'center_z'])
165
+ .describe('Sort field: area=surface area, surface_type=plane/cylinder/etc (alphabetic), center_x/y/z=face centroid coordinate'),
166
+ direction: z
167
+ .enum(['asc', 'desc'])
168
+ .describe('"asc" (ascending, default) or "desc" (descending)')
169
+ .optional(),
170
+ })
171
+ .strict()
172
+ .describe('Sort results by one field and optional direction. E.g., {by:"area",direction:"desc"} sorts largest faces first.')
173
+ .optional();
174
+ const edgeSortSchema = z
175
+ .object({
176
+ by: z
177
+ .enum(['length', 'curve_type', 'radius', 'center_x', 'center_y', 'center_z'])
178
+ .describe('Sort field: length=edge length, curve_type=line/circle/etc (alphabetic), radius=circular radius, center_x/y/z=edge center coordinate'),
179
+ direction: z
180
+ .enum(['asc', 'desc'])
181
+ .describe('"asc" (ascending, default) or "desc" (descending)')
182
+ .optional(),
183
+ })
184
+ .strict()
185
+ .describe('Sort results by one field and optional direction. E.g., {by:"length",direction:"asc"} sorts shortest edges first (useful for finding tiny edges).')
186
+ .optional();
187
+ const faceFilterSchema = z
188
+ .object({
189
+ entity_ids: z
190
+ .array(z.string().min(1))
191
+ .min(1)
192
+ .max(200)
193
+ .refine(uniqueArray)
194
+ .describe('List of face IDs to retrieve (e.g., ["face:0", "face:5"]). Limits results to exactly these faces. Max 200 IDs.')
195
+ .optional(),
196
+ group_ids: z
197
+ .array(z.string().min(1))
198
+ .min(1)
199
+ .max(50)
200
+ .refine(uniqueArray)
201
+ .describe('Group IDs from a previous group_by result. Retrieves all entities within those groups. Requires the same group_by used to produce the groups. Use together to drill from grouped/counted populations into specific entities. Max 50 IDs.')
202
+ .optional(),
203
+ surface_type: z
204
+ .array(z.enum(['plane', 'cylinder', 'cone', 'sphere', 'torus', 'bspline', 'other']))
205
+ .min(1)
206
+ .max(7)
207
+ .describe('Surface geometry type(s). Returns only faces matching these types. "plane" = flat, "cylinder" = cylindrical, "cone" = conical, etc. Multiple types use AND logic (face must match one type).')
208
+ .refine(uniqueArray, 'Surface type values must be unique.')
209
+ .optional(),
210
+ area_min: z
211
+ .number()
212
+ .nonnegative()
213
+ .describe('Minimum face area in mm^2. Returns faces with area >= area_min. E.g., 100 returns faces >= 100 mm^2.')
214
+ .optional(),
215
+ area_max: z
216
+ .number()
217
+ .nonnegative()
218
+ .describe('Maximum face area in mm^2. Returns faces with area <= area_max. E.g., 1000 returns faces <= 1000 mm^2.')
219
+ .optional(),
220
+ normal_parallel_to: direction3Schema
221
+ .describe('Direction vector [x, y, z] to match face normals against. Returns faces whose surface normal is parallel to this direction. Normal tolerance determines how close "parallel" must be.')
222
+ .optional(),
223
+ normal_tolerance_degrees: z
224
+ .number()
225
+ .nonnegative()
226
+ .max(180)
227
+ .describe('Angle tolerance in degrees for normal_parallel_to matching. E.g., 10 degrees means normals within +/-10 degrees of the target direction pass.')
228
+ .optional(),
229
+ body_ids: z
230
+ .array(z.string().min(1))
231
+ .min(1)
232
+ .max(20)
233
+ .describe('Filter faces to specific body IDs (e.g., ["body:0", "body:1"]). Use after a summary/groups query to narrow to a subset of bodies in a multi-body model. Max 20 IDs.')
234
+ .optional(),
235
+ })
236
+ .strict()
237
+ .refine(({ area_min, area_max }) => boundedRange({ min: area_min, max: area_max }), {
238
+ message: 'area_min must be less than or equal to area_max.',
239
+ });
240
+ const edgeFilterSchema = z
241
+ .object({
242
+ entity_ids: z
243
+ .array(z.string().min(1))
244
+ .min(1)
245
+ .max(200)
246
+ .refine(uniqueArray)
247
+ .describe('List of edge IDs to retrieve (e.g., ["edge:0", "edge:42"]). Limits results to exactly these edges. Max 200 IDs.')
248
+ .optional(),
249
+ group_ids: z
250
+ .array(z.string().min(1))
251
+ .min(1)
252
+ .max(50)
253
+ .refine(uniqueArray)
254
+ .describe('Group IDs from a previous group_by result. Retrieves all entities within those groups. Requires the same group_by used to produce the groups. Use together to drill from grouped/counted populations into specific entities. Max 50 IDs.')
255
+ .optional(),
256
+ curve_type: z
257
+ .array(z.enum(['line', 'circle', 'ellipse', 'bspline', 'other']))
258
+ .min(1)
259
+ .max(5)
260
+ .describe('Edge curve type(s). Returns only edges matching these types. "line" = straight, "circle" = circular, "ellipse" = elliptical, "bspline" = spline curve. Multiple types use AND logic.')
261
+ .refine(uniqueArray, 'Curve type values must be unique.')
262
+ .optional(),
263
+ length_min: z
264
+ .number()
265
+ .nonnegative()
266
+ .describe('Minimum edge length in mm. Returns edges with length >= length_min. E.g., 10 returns edges >= 10 mm.')
267
+ .optional(),
268
+ length_max: z
269
+ .number()
270
+ .nonnegative()
271
+ .describe('Maximum edge length in mm. Returns edges with length <= length_max. E.g., 100 returns edges <= 100 mm.')
272
+ .optional(),
273
+ radius_min: z
274
+ .number()
275
+ .nonnegative()
276
+ .describe('Minimum radius in mm for circular/curved edges. Returns edges with radius >= radius_min. Only applies to circular curves.')
277
+ .optional(),
278
+ radius_max: z
279
+ .number()
280
+ .nonnegative()
281
+ .describe('Maximum radius in mm for circular/curved edges. Returns edges with radius <= radius_max. Only applies to circular curves.')
282
+ .optional(),
283
+ body_ids: z
284
+ .array(z.string().min(1))
285
+ .min(1)
286
+ .max(20)
287
+ .describe('Filter edges to specific body IDs (e.g., ["body:0", "body:1"]). Use after a summary/groups query to narrow to a subset of bodies in a multi-body model. Max 20 IDs.')
288
+ .optional(),
289
+ })
290
+ .strict()
291
+ .refine(({ length_min, length_max }) => boundedRange({ min: length_min, max: length_max }), {
292
+ message: 'length_min must be less than or equal to length_max.',
293
+ })
294
+ .refine(({ radius_min, radius_max }) => boundedRange({ min: radius_min, max: radius_max }), {
295
+ message: 'radius_min must be less than or equal to radius_max.',
296
+ });
297
+ /* ------------------------------------------------------------------ */
298
+ /* PMI query schema */
299
+ /* ------------------------------------------------------------------ */
300
+ const pmiTypeSchema = z
301
+ .enum(['geometric_tolerance', 'dimension', 'datum', 'annotation'])
302
+ .describe('PMI entity type category');
303
+ const toleranceSubtypeSchema = z
304
+ .enum([
305
+ 'position',
306
+ 'flatness',
307
+ 'straightness',
308
+ 'circularity',
309
+ 'cylindricity',
310
+ 'profile',
311
+ 'parallelism',
312
+ 'perpendicularity',
313
+ 'angularity',
314
+ 'concentricity',
315
+ 'runout',
316
+ 'symmetry',
317
+ 'coaxiality',
318
+ 'circular_runout',
319
+ 'total_runout',
320
+ 'surface_profile',
321
+ 'line_profile',
322
+ ])
323
+ .describe('Geometric tolerance subtype matching the STEP entity type name');
324
+ const pmiFilterSchema = z
325
+ .object({
326
+ pmi_types: z
327
+ .array(pmiTypeSchema)
328
+ .min(1)
329
+ .max(5)
330
+ .describe('Filter by PMI entity type category: geometric_tolerance (GD&T callouts), dimension (linear/angular/diametral sizes and locations), datum (datum references and systems), annotation (notes, surface finish, callouts). Multiple types use OR logic.')
331
+ .optional(),
332
+ tolerance_types: z
333
+ .array(toleranceSubtypeSchema)
334
+ .min(1)
335
+ .max(17)
336
+ .describe('Filter geometric tolerances by subtype: position, flatness, straightness, circularity, cylindricity, profile, parallelism, perpendicularity, angularity, concentricity, runout, symmetry, coaxiality. Only applies when pmi_types includes geometric_tolerance.')
337
+ .optional(),
338
+ value_min: z
339
+ .number()
340
+ .nonnegative()
341
+ .describe('Minimum tolerance/dimension value in mm. Returns PMI with value >= value_min.')
342
+ .optional(),
343
+ value_max: z
344
+ .number()
345
+ .nonnegative()
346
+ .describe('Maximum tolerance/dimension value in mm. Returns PMI with value <= value_max.')
347
+ .optional(),
348
+ })
349
+ .strict()
350
+ .refine(({ value_min, value_max }) => boundedRange({ min: value_min, max: value_max }), {
351
+ message: 'value_min must be less than or equal to value_max.',
352
+ });
353
+ const pmiGroupBySchema = z
354
+ .array(z
355
+ .enum(['type', 'tolerance_type', 'dimension_type', 'material_condition'])
356
+ .describe('Grouping dimension: type=geometric_tolerance/dimension/datum/annotation; tolerance_type=position/flatness/etc (geometric tolerances only); dimension_type=diameter/radius/length/location (dimensions only); material_condition=mmc/lmc/rfs (geometric tolerances only).'))
357
+ .min(1)
358
+ .max(3)
359
+ .refine(uniqueArray, 'Group-by values must be unique.')
360
+ .describe('List of dimensions to group PMI entities by. E.g., ["type"] groups by category; ["type","tolerance_type"] groups tolerances by subtype within the tolerance group.')
361
+ .optional();
362
+ const pmiSortSchema = z
363
+ .object({
364
+ by: z
365
+ .enum(['type', 'value', 'tolerance_type'])
366
+ .describe('Sort field: type=entity category (alphabetic), value= tolerance/dimension value, tolerance_type=geometric tolerance subtype'),
367
+ direction: z
368
+ .enum(['asc', 'desc'])
369
+ .describe('"asc" (ascending, default) or "desc" (descending)')
370
+ .optional(),
371
+ })
372
+ .strict()
373
+ .optional();
374
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
375
+ const internalFaceQuerySchema = {
376
+ filter: faceFilterSchema.optional(),
377
+ include: faceIncludeSchema,
378
+ group_by: faceGroupBySchema,
379
+ sort: faceSortSchema,
380
+ result_mode: resultModeSchema,
381
+ limit: limitSchema,
382
+ offset: offsetSchema,
383
+ };
384
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
385
+ const internalEdgeQuerySchema = {
386
+ filter: edgeFilterSchema.optional(),
387
+ include: edgeIncludeSchema,
388
+ group_by: edgeGroupBySchema,
389
+ sort: edgeSortSchema,
390
+ result_mode: resultModeSchema,
391
+ limit: limitSchema,
392
+ offset: offsetSchema,
393
+ };
394
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
395
+ const internalPmiQuerySchema = {
396
+ filter: pmiFilterSchema.optional(),
397
+ group_by: pmiGroupBySchema,
398
+ sort: pmiSortSchema,
399
+ result_mode: resultModeSchema,
400
+ limit: limitSchema,
401
+ offset: offsetSchema,
402
+ };
403
+ const findStepFacesSchema = {
404
+ ...stepFileInput,
405
+ surface_types: z
406
+ .array(z.enum(['plane', 'cylinder', 'cone', 'sphere', 'torus', 'bspline', 'other']))
407
+ .min(1)
408
+ .max(7)
409
+ .refine(uniqueArray, 'Surface type values must be unique.')
410
+ .describe('Surface geometry types to match. Multiple values use OR logic. Omit to include all types.')
411
+ .optional(),
412
+ area_min: z
413
+ .number()
414
+ .nonnegative()
415
+ .describe('Minimum face area in mm^2. Omit for no lower bound.')
416
+ .optional(),
417
+ area_max: z
418
+ .number()
419
+ .nonnegative()
420
+ .describe('Maximum face area in mm^2. Omit for no upper bound.')
421
+ .optional(),
422
+ normal: normalFilterSchema,
423
+ body_ids: z
424
+ .array(bodyIdSchema)
425
+ .min(1)
426
+ .refine(uniqueArray, 'Body IDs must be unique.')
427
+ .describe('Restrict to specific bodies in multi-body models. Omit to search all bodies.')
428
+ .optional(),
429
+ fields: faceFieldsSchema,
430
+ group_by: z
431
+ .array(z
432
+ .enum(['surface_type', 'area_range', 'radius', 'normal_direction', 'body_id'])
433
+ .describe('Grouping dimension: surface_type=plane/cylinder/etc; area_range=size bucket (0–1, 1–10, …); radius=rounded to 0.5mm (cylindrical faces only); normal_direction=nearest principal axis direction; body_id=which body the face belongs to.'))
434
+ .min(1)
435
+ .max(5)
436
+ .refine(uniqueArray, 'Group-by values must be unique.')
437
+ .describe('Group dimensions. Requires return_type:"groups". Ignored otherwise. Example: ["surface_type"] groups by geometry type.')
438
+ .optional(),
439
+ sort: faceSortSchema,
440
+ return_type: returnTypeSchema,
441
+ limit: limitSchema,
442
+ offset: offsetSchema,
443
+ };
444
+ const findStepEdgesSchema = {
445
+ ...stepFileInput,
446
+ curve_types: z
447
+ .array(z.enum(['line', 'circle', 'ellipse', 'bspline', 'other']))
448
+ .min(1)
449
+ .max(5)
450
+ .refine(uniqueArray, 'Curve type values must be unique.')
451
+ .describe('Edge curve types to match. Multiple values use OR logic. Omit to include all types.')
452
+ .optional(),
453
+ length_min: z
454
+ .number()
455
+ .nonnegative()
456
+ .describe('Minimum edge length in mm. Omit for no lower bound.')
457
+ .optional(),
458
+ length_max: z
459
+ .number()
460
+ .nonnegative()
461
+ .describe('Maximum edge length in mm. For tiny edges, set this alone and omit radius filters.')
462
+ .optional(),
463
+ radius: edgeRadiusSchema,
464
+ body_ids: z
465
+ .array(bodyIdSchema)
466
+ .min(1)
467
+ .refine(uniqueArray, 'Body IDs must be unique.')
468
+ .describe('Restrict to specific bodies in multi-body models. Omit to search all bodies.')
469
+ .optional(),
470
+ fields: edgeFieldsSchema,
471
+ group_by: edgeGroupBySchema,
472
+ sort: edgeSortSchema,
473
+ return_type: returnTypeSchema,
474
+ limit: limitSchema,
475
+ offset: offsetSchema,
476
+ };
477
+ const getStepEntitiesSchema = {
478
+ ...stepFileInput,
479
+ entity_type: z
480
+ .enum(['face', 'edge'])
481
+ .describe('Entity kind to retrieve. Determines valid ID prefix and field names.'),
482
+ entity_ids: z
483
+ .array(z.string().min(1))
484
+ .min(1)
485
+ .max(200)
486
+ .refine(uniqueArray, 'Entity IDs must be unique.')
487
+ .describe('Exact entity IDs. Must be face:N when entity_type is face, or edge:N when entity_type is edge.'),
488
+ fields: z
489
+ .array(z.enum([
490
+ ...FACE_FIELDS,
491
+ ...EDGE_FIELDS.filter((f) => !FACE_FIELDS.includes(f)),
492
+ ]))
493
+ .min(1)
494
+ .max(16)
495
+ .refine(uniqueArray, 'Field values must be unique.')
496
+ .describe('Entity fields to include. Face and edge fields are validated against entity_type.')
497
+ .optional(),
498
+ };
499
+ const pmiQuerySchema = {
500
+ ...stepFileInput,
501
+ pmi_types: z
502
+ .array(pmiTypeSchema)
503
+ .min(1)
504
+ .max(5)
505
+ .refine(uniqueArray, 'PMI type values must be unique.')
506
+ .describe('PMI categories to filter by: geometric_tolerance, dimension, datum, annotation. Omit to include all categories.')
507
+ .optional(),
508
+ tolerance_subtypes: z
509
+ .array(toleranceSubtypeSchema)
510
+ .min(1)
511
+ .max(17)
512
+ .refine(uniqueArray, 'Tolerance subtype values must be unique.')
513
+ .describe('Geometric tolerance subtypes to filter by (e.g., position, flatness). Only applies to geometric tolerance type.')
514
+ .optional(),
515
+ value_min: z
516
+ .number()
517
+ .nonnegative()
518
+ .describe('Minimum tolerance/dimension value in mm. Omit for no lower bound.')
519
+ .optional(),
520
+ value_max: z
521
+ .number()
522
+ .nonnegative()
523
+ .describe('Maximum tolerance/dimension value in mm. Omit for no upper bound.')
524
+ .optional(),
525
+ group_by: pmiGroupBySchema,
526
+ sort: pmiSortSchema,
527
+ return_type: returnTypeSchema,
528
+ limit: limitSchema,
529
+ offset: offsetSchema,
530
+ };
531
+ export const stepToolSchemas = {
532
+ inspectStepFile: inspectStepFileSchema,
533
+ findStepFaces: findStepFacesSchema,
534
+ findStepEdges: findStepEdgesSchema,
535
+ getStepEntities: getStepEntitiesSchema,
536
+ compareStepFiles: {
537
+ baseline_file_path: z
538
+ .string()
539
+ .min(1)
540
+ .describe('Absolute or relative path to the baseline/original STEP file.'),
541
+ comparison_file_path: z
542
+ .string()
543
+ .min(1)
544
+ .describe('Absolute or relative path to the comparison/changed STEP file.'),
545
+ },
546
+ queryStepPmi: pmiQuerySchema,
547
+ };
548
+ const queryOutputSchema = z
549
+ .object({
550
+ schema_version: z.literal('0.4'),
551
+ file_path: z.string(),
552
+ units: z.object({}).passthrough(),
553
+ coordinate_system: z.object({}).passthrough(),
554
+ query: z.object({}).passthrough(),
555
+ statistics: z.object({}).passthrough(),
556
+ pagination: z.object({
557
+ limit: z.number(),
558
+ offset: z.number(),
559
+ returned: z.number(),
560
+ total_matched: z.number(),
561
+ has_more: z.boolean(),
562
+ }),
563
+ entities: z.array(z.object({}).passthrough()),
564
+ groups: z.array(z.object({}).passthrough()),
565
+ warnings: z.array(z.unknown()),
566
+ limitations: z.array(z.unknown()),
567
+ })
568
+ .passthrough();
569
+ const compareOutputSchema = z
570
+ .object({
571
+ schema_version: z.literal('0.4'),
572
+ files: z.object({ a: z.string(), b: z.string() }),
573
+ deltas: z.object({}).passthrough(),
574
+ exchange: z.object({}).passthrough(),
575
+ warnings: z.array(z.object({}).passthrough()),
576
+ limitations: z.array(z.object({}).passthrough()),
577
+ providers: z.object({}).passthrough(),
578
+ })
579
+ .passthrough();
580
+ export const stepToolOutputSchemas = {
581
+ inspectStepFile: {
582
+ schema_version: z.literal('0.4'),
583
+ file_path: z.string(),
584
+ identity: z.object({
585
+ product_names: z.array(z.string()),
586
+ authoring_system: z.string().optional(),
587
+ organization_name: z.string().optional(),
588
+ }),
589
+ size: z.object({
590
+ bounding_box: z.object({}).passthrough(),
591
+ dimensions: z.object({}).passthrough(),
592
+ volume: z.number(),
593
+ surface_area: z.number(),
594
+ units: z.object({}).passthrough(),
595
+ }),
596
+ structure: z.object({
597
+ body_count: z.number(),
598
+ shape_type: z.string(),
599
+ is_assembly: z.boolean(),
600
+ product_count: z.number(),
601
+ schema: z.string().optional(),
602
+ application_protocol: z.string().optional(),
603
+ }),
604
+ health: z.object({
605
+ is_valid: z.boolean().optional(),
606
+ warning_count: z.number(),
607
+ high_warning_count: z.number(),
608
+ complexity: z.object({}).passthrough(),
609
+ }),
610
+ pmi: z.object({}).passthrough(),
611
+ topology_summary: z.object({}).passthrough().optional(),
612
+ geometry_extremes: z.object({}).passthrough().optional(),
613
+ warnings: z.array(z.object({}).passthrough()),
614
+ limitations: z.array(z.object({}).passthrough()),
615
+ },
616
+ findStepFaces: queryOutputSchema,
617
+ findStepEdges: queryOutputSchema,
618
+ getStepEntities: queryOutputSchema,
619
+ compareStepFiles: compareOutputSchema,
620
+ queryStepPmi: queryOutputSchema,
621
+ };
622
+ export async function handleInspectStepFile(filePath) {
623
+ return wrapTool(async () => {
624
+ return withStepModel(filePath, async (model) => {
625
+ const [brep, semantic] = await Promise.all([model.getBRepModel(), model.getSemanticModel()]);
626
+ return {
627
+ schema_version: CAD_RESPONSE_SCHEMA_VERSION,
628
+ file_path: filePath,
629
+ identity: {
630
+ product_names: semantic.productNames,
631
+ authoring_system: semantic.authoringSystem,
632
+ organization_name: semantic.organizationName,
633
+ },
634
+ size: {
635
+ bounding_box: brep.boundingBox,
636
+ dimensions: brep.dimensions,
637
+ volume: brep.volume,
638
+ surface_area: brep.surfaceArea,
639
+ units: brep.units,
640
+ },
641
+ structure: {
642
+ body_count: brep.bodyCount,
643
+ shape_type: brep.shapeType,
644
+ is_assembly: semantic.hasAssembly,
645
+ product_count: semantic.productCount,
646
+ schema: semantic.schema,
647
+ application_protocol: semantic.applicationProtocol,
648
+ },
649
+ health: {
650
+ is_valid: brep.health.isValid,
651
+ warning_count: brep.health.warnings.length,
652
+ high_warning_count: brep.health.warnings.filter((w) => w.severity === 'high').length,
653
+ complexity: {
654
+ body_count: brep.bodyCount,
655
+ face_count: brep.faceCount,
656
+ edge_count: brep.edgeStatistics?.count,
657
+ },
658
+ },
659
+ pmi: {
660
+ has_pmi: semantic.pmi?.hasGdt || semantic.pmi?.hasDimensions || false,
661
+ has_gdt: semantic.pmi?.hasGdt || false,
662
+ has_dimensions: semantic.pmi?.hasDimensions || false,
663
+ semantic_status: semantic.pmi?.semanticStatus || 'not_detected',
664
+ tolerance_entity_count: semantic.toleranceEntityCount,
665
+ },
666
+ topology_summary: {
667
+ faces: {
668
+ total: brep.faceCount,
669
+ },
670
+ edges: brep.edgeStatistics
671
+ ? {
672
+ total: brep.edgeStatistics.count,
673
+ by_curve_type: brep.edgeStatistics.byCurveType,
674
+ by_length_bucket: brep.edgeStatistics.byLengthRange,
675
+ length_range: {
676
+ min: brep.edgeStatistics.minLength,
677
+ max: brep.edgeStatistics.maxLength,
678
+ },
679
+ }
680
+ : undefined,
681
+ },
682
+ geometry_extremes: {
683
+ edges_length_lt_1_mm: brep.edgeStatistics ? brep.edgeStatistics.byLengthRange.tiny : 0,
684
+ min_edge_length: brep.edgeStatistics?.minLength || 0,
685
+ },
686
+ warnings: brep.health.warnings,
687
+ limitations: [
688
+ ...semantic.limitations,
689
+ {
690
+ source: 'inspect_step_file',
691
+ message: 'Face area extremes, surface-type counts, and adjacency graph are deferred. Use find_step_faces or find_step_edges with specific fields for those details.',
692
+ },
693
+ ],
694
+ };
695
+ });
696
+ });
697
+ }
698
+ export async function handleFindStepFaces(filePath, query) {
699
+ return wrapTool(async () => queryFacesService(filePath, adaptFindStepFaces(query)));
700
+ }
701
+ export async function handleFindStepEdges(filePath, query) {
702
+ return wrapTool(async () => queryEdgesService(filePath, adaptFindStepEdges(query)));
703
+ }
704
+ export async function handleGetStepEntities(filePath, query) {
705
+ return wrapTool(async () => {
706
+ const publicQuery = query;
707
+ if (!publicQuery?.entity_type)
708
+ throw invalidInput('entity_type is required.');
709
+ if (!publicQuery.entity_ids || publicQuery.entity_ids.length === 0) {
710
+ throw invalidInput('entity_ids is required and must contain at least one ID.');
711
+ }
712
+ if (publicQuery.entity_type === 'face') {
713
+ validateEntityIds(publicQuery.entity_ids, 'face');
714
+ validateEntityFields(publicQuery.fields, 'face');
715
+ if (canDirectGetEntities(publicQuery)) {
716
+ return getStepEntitiesDirect(filePath, publicQuery);
717
+ }
718
+ return queryFacesService(filePath, adaptGetStepFaces(publicQuery));
719
+ }
720
+ validateEntityIds(publicQuery.entity_ids, 'edge');
721
+ validateEntityFields(publicQuery.fields, 'edge');
722
+ if (canDirectGetEntities(publicQuery)) {
723
+ return getStepEntitiesDirect(filePath, publicQuery);
724
+ }
725
+ return queryEdgesService(filePath, adaptGetStepEdges(publicQuery));
726
+ });
727
+ }
728
+ export async function handleCompareStepFiles(fileA, fileB) {
729
+ return wrapTool(async () => compareStepFiles(fileA, fileB));
730
+ }
731
+ export async function handleQueryStepPmi(filePath, query) {
732
+ return wrapTool(async () => queryPmiService(filePath, adaptPmiQuery(query)));
733
+ }
734
+ export function adaptFindStepFaces(query) {
735
+ if (!boundedRange({ min: query?.area_min, max: query?.area_max })) {
736
+ throw invalidInput('area_min must be less than or equal to area_max.');
737
+ }
738
+ return {
739
+ filter: {
740
+ body_ids: query?.body_ids,
741
+ surface_type: query?.surface_types,
742
+ area_min: query?.area_min,
743
+ area_max: query?.area_max,
744
+ normal_parallel_to: query?.normal?.parallel_to,
745
+ normal_tolerance_degrees: query?.normal?.tolerance_degrees,
746
+ },
747
+ include: mapBboxCenter(query?.fields),
748
+ group_by: query?.group_by,
749
+ sort: query?.sort,
750
+ result_mode: query?.return_type,
751
+ limit: query?.limit,
752
+ offset: query?.offset,
753
+ };
754
+ }
755
+ export function adaptFindStepEdges(query) {
756
+ if (!boundedRange({ min: query?.length_min, max: query?.length_max })) {
757
+ throw invalidInput('length_min must be less than or equal to length_max.');
758
+ }
759
+ if (!boundedRange({ min: query?.radius?.min, max: query?.radius?.max })) {
760
+ throw invalidInput('radius.min must be less than or equal to radius.max.');
761
+ }
762
+ return {
763
+ filter: {
764
+ body_ids: query?.body_ids,
765
+ curve_type: query?.curve_types,
766
+ length_min: query?.length_min,
767
+ length_max: query?.length_max,
768
+ radius_min: query?.radius?.min,
769
+ radius_max: query?.radius?.max,
770
+ },
771
+ include: mapBboxCenter(query?.fields),
772
+ group_by: query?.group_by,
773
+ sort: query?.sort,
774
+ result_mode: query?.return_type,
775
+ limit: query?.limit,
776
+ offset: query?.offset,
777
+ };
778
+ }
779
+ function adaptGetStepFaces(query) {
780
+ return {
781
+ filter: { entity_ids: query.entity_ids },
782
+ include: mapBboxCenter(query.fields),
783
+ group_by: undefined,
784
+ sort: undefined,
785
+ result_mode: 'entities',
786
+ limit: query.entity_ids?.length,
787
+ offset: 0,
788
+ };
789
+ }
790
+ function adaptGetStepEdges(query) {
791
+ return {
792
+ filter: { entity_ids: query.entity_ids },
793
+ include: mapBboxCenter(query.fields),
794
+ group_by: undefined,
795
+ sort: undefined,
796
+ result_mode: 'entities',
797
+ limit: query.entity_ids?.length,
798
+ offset: 0,
799
+ };
800
+ }
801
+ export function adaptPmiQuery(query) {
802
+ if (!boundedRange({ min: query?.value_min, max: query?.value_max })) {
803
+ throw invalidInput('value_min must be less than or equal to value_max.');
804
+ }
805
+ return {
806
+ filter: {
807
+ pmi_types: query?.pmi_types,
808
+ tolerance_types: query?.tolerance_subtypes,
809
+ value_min: query?.value_min,
810
+ value_max: query?.value_max,
811
+ },
812
+ group_by: query?.group_by,
813
+ sort: query?.sort,
814
+ result_mode: query?.return_type,
815
+ limit: query?.limit,
816
+ offset: query?.offset,
817
+ };
818
+ }
819
+ function validateEntityIds(entityIds, entityType) {
820
+ const valid = entityIds.every((id) => entityType === 'face' ? /^face:\d+$/.test(id) : /^edge:\d+$/.test(id));
821
+ if (!valid)
822
+ throw invalidInput(`All entity_ids must match ${entityType}:N.`);
823
+ }
824
+ function validateEntityFields(fields, entityType) {
825
+ if (!fields)
826
+ return;
827
+ const allowed = entityType === 'face' ? FACE_GET_FIELDS : EDGE_GET_FIELDS;
828
+ const invalid = fields.filter((field) => !allowed.has(field));
829
+ if (invalid.length > 0) {
830
+ throw invalidInput(`Invalid ${entityType} fields: ${invalid.join(', ')}.`);
831
+ }
832
+ }
833
+ function invalidInput(message) {
834
+ return { type: 'invalid_input', message };
835
+ }
836
+ //# sourceMappingURL=step-tools.js.map