circuit-json-to-step 0.0.3 → 0.0.5

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/dist/index.d.ts CHANGED
@@ -13,6 +13,12 @@ interface CircuitJsonToStepOptions {
13
13
  includeComponents?: boolean;
14
14
  /** Include external model meshes from model_*_url fields (default: false). Only applicable when includeComponents is true. */
15
15
  includeExternalMeshes?: boolean;
16
+ /**
17
+ * Pre-loaded STEP file contents, keyed by URL/path.
18
+ * If a URL is found here, the content is used directly instead of fetching.
19
+ * Useful for tests that need to load local files.
20
+ */
21
+ fsMap?: Record<string, string>;
16
22
  }
17
23
  /**
18
24
  * Converts circuit JSON to STEP format, creating holes in a PCB board
package/dist/index.js CHANGED
@@ -409,32 +409,26 @@ function rotateVector(vector, rotation) {
409
409
  }
410
410
 
411
411
  // lib/step-model-merger/read-step-file.ts
412
- import { promises as fs } from "fs";
413
- import path from "path";
414
- import { fileURLToPath } from "url";
415
412
  async function readStepFile(modelUrl) {
416
- if (/^https?:\/\//i.test(modelUrl)) {
417
- const globalFetch = globalThis.fetch;
418
- if (!globalFetch) {
419
- throw new Error("fetch is not available in this environment");
420
- }
421
- const res = await globalFetch(modelUrl);
422
- if (!res.ok) {
423
- throw new Error(`HTTP ${res.status} ${res.statusText}`);
424
- }
425
- return await res.text();
413
+ if (!/^https?:\/\//i.test(modelUrl)) {
414
+ throw new Error(
415
+ `Only HTTP(S) URLs are supported. For local files, read the file content and pass it directly. Received: ${modelUrl}`
416
+ );
417
+ }
418
+ const globalFetch = globalThis.fetch;
419
+ if (!globalFetch) {
420
+ throw new Error("fetch is not available in this environment");
426
421
  }
427
- if (modelUrl.startsWith("file://")) {
428
- const filePath = fileURLToPath(modelUrl);
429
- return await fs.readFile(filePath, "utf8");
422
+ const res = await globalFetch(modelUrl);
423
+ if (!res.ok) {
424
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
430
425
  }
431
- const resolvedPath = path.isAbsolute(modelUrl) ? modelUrl : path.resolve(process.cwd(), modelUrl);
432
- return await fs.readFile(resolvedPath, "utf8");
426
+ return await res.text();
433
427
  }
434
428
 
435
429
  // lib/step-model-merger.ts
436
430
  async function mergeExternalStepModels(options) {
437
- const { repo, circuitJson, boardThickness } = options;
431
+ const { repo, circuitJson, boardThickness, fsMap } = options;
438
432
  const cadComponents = circuitJson.filter(
439
433
  (item) => item?.type === "cad_component" && typeof item.model_step_url === "string"
440
434
  );
@@ -451,7 +445,7 @@ async function mergeExternalStepModels(options) {
451
445
  const componentId = component.cad_component_id ?? "";
452
446
  const stepUrl = component.model_step_url;
453
447
  try {
454
- const stepText = await readStepFile(stepUrl);
448
+ const stepText = fsMap?.[stepUrl] ?? await readStepFile(stepUrl);
455
449
  if (!stepText.trim()) {
456
450
  throw new Error("STEP file is empty");
457
451
  }
@@ -1085,7 +1079,8 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1085
1079
  const mergeResult = await mergeExternalStepModels({
1086
1080
  repo,
1087
1081
  circuitJson,
1088
- boardThickness
1082
+ boardThickness,
1083
+ fsMap: options.fsMap
1089
1084
  });
1090
1085
  handledComponentIds = mergeResult.handledComponentIds;
1091
1086
  handledPcbComponentIds = mergeResult.handledPcbComponentIds;
package/lib/index.ts CHANGED
@@ -57,6 +57,12 @@ export interface CircuitJsonToStepOptions {
57
57
  includeComponents?: boolean
58
58
  /** Include external model meshes from model_*_url fields (default: false). Only applicable when includeComponents is true. */
59
59
  includeExternalMeshes?: boolean
60
+ /**
61
+ * Pre-loaded STEP file contents, keyed by URL/path.
62
+ * If a URL is found here, the content is used directly instead of fetching.
63
+ * Useful for tests that need to load local files.
64
+ */
65
+ fsMap?: Record<string, string>
60
66
  }
61
67
 
62
68
  /**
@@ -510,6 +516,7 @@ export async function circuitJsonToStep(
510
516
  repo,
511
517
  circuitJson,
512
518
  boardThickness,
519
+ fsMap: options.fsMap,
513
520
  })
514
521
  handledComponentIds = mergeResult.handledComponentIds
515
522
  handledPcbComponentIds = mergeResult.handledPcbComponentIds
@@ -1,29 +1,23 @@
1
- import { promises as fs } from "node:fs"
2
- import path from "node:path"
3
- import { fileURLToPath } from "node:url"
4
-
1
+ /**
2
+ * Fetches STEP file content from an HTTP(S) URL.
3
+ * For local files, the caller must read the file and pass the content directly.
4
+ */
5
5
  export async function readStepFile(modelUrl: string): Promise<string> {
6
- if (/^https?:\/\//i.test(modelUrl)) {
7
- const globalFetch = (globalThis as any).fetch as
8
- | ((input: string, init?: unknown) => Promise<any>)
9
- | undefined
10
- if (!globalFetch) {
11
- throw new Error("fetch is not available in this environment")
12
- }
13
- const res = await globalFetch(modelUrl)
14
- if (!res.ok) {
15
- throw new Error(`HTTP ${res.status} ${res.statusText}`)
16
- }
17
- return await res.text()
6
+ if (!/^https?:\/\//i.test(modelUrl)) {
7
+ throw new Error(
8
+ `Only HTTP(S) URLs are supported. For local files, read the file content and pass it directly. Received: ${modelUrl}`,
9
+ )
18
10
  }
19
11
 
20
- if (modelUrl.startsWith("file://")) {
21
- const filePath = fileURLToPath(modelUrl)
22
- return await fs.readFile(filePath, "utf8")
12
+ const globalFetch = (globalThis as any).fetch as
13
+ | ((input: string, init?: unknown) => Promise<any>)
14
+ | undefined
15
+ if (!globalFetch) {
16
+ throw new Error("fetch is not available in this environment")
23
17
  }
24
-
25
- const resolvedPath = path.isAbsolute(modelUrl)
26
- ? modelUrl
27
- : path.resolve(process.cwd(), modelUrl)
28
- return await fs.readFile(resolvedPath, "utf8")
18
+ const res = await globalFetch(modelUrl)
19
+ if (!res.ok) {
20
+ throw new Error(`HTTP ${res.status} ${res.statusText}`)
21
+ }
22
+ return await res.text()
29
23
  }
@@ -37,4 +37,10 @@ export interface MergeStepModelOptions {
37
37
  repo: Repository
38
38
  circuitJson: CircuitJson
39
39
  boardThickness: number
40
+ /**
41
+ * Pre-loaded STEP file contents, keyed by file path.
42
+ * If a path is found here, the content is used directly instead of reading from disk.
43
+ * Useful for browser environments where fs is not available.
44
+ */
45
+ fsMap?: Record<string, string>
40
46
  }
@@ -33,7 +33,7 @@ export type {
33
33
  export async function mergeExternalStepModels(
34
34
  options: MergeStepModelOptions,
35
35
  ): Promise<MergeStepModelResult> {
36
- const { repo, circuitJson, boardThickness } = options
36
+ const { repo, circuitJson, boardThickness, fsMap } = options
37
37
  const cadComponents = (circuitJson as CadComponent[]).filter(
38
38
  (item) =>
39
39
  item?.type === "cad_component" && typeof item.model_step_url === "string",
@@ -55,7 +55,7 @@ export async function mergeExternalStepModels(
55
55
  const stepUrl = component.model_step_url!
56
56
 
57
57
  try {
58
- const stepText = await readStepFile(stepUrl)
58
+ const stepText = fsMap?.[stepUrl] ?? (await readStepFile(stepUrl))
59
59
  if (!stepText.trim()) {
60
60
  throw new Error("STEP file is empty")
61
61
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "circuit-json-to-step",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.3",
4
+ "version": "0.0.5",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "pull-reference": "git clone https://github.com/tscircuit/circuit-json.git && find circuit-json/tests -name '*.test.ts' -exec bash -c 'mv \"$0\" \"${0%.test.ts}.ts\"' {} \\; && git clone https://github.com/tscircuit/stepts.git && find stepts/tests -name '*.test.ts' -exec bash -c 'mv \"$0\" \"${0%.test.ts}.ts\"' {} \\;",
@@ -18,7 +18,7 @@
18
18
  "looks-same": "^10.0.1",
19
19
  "occt-import-js": "^0.0.23",
20
20
  "poppygl": "^0.0.13",
21
- "stepts": "^0.0.1",
21
+ "stepts": "^0.0.2",
22
22
  "tsup": "^8.5.0"
23
23
  },
24
24
  "peerDependencies": {
@@ -4,6 +4,7 @@ import { join } from "node:path"
4
4
  import { circuitJsonToStep } from "../../../lib/index"
5
5
  import { importStepWithOcct } from "../../utils/occt/importer"
6
6
  import type { OcctMesh } from "../../utils/occt/importer"
7
+ import { loadStepFilesFromCircuitJson } from "../../utils/load-step-files"
7
8
  import { parseRepository, ManifoldSolidBrep } from "stepts"
8
9
  import circuitJson from "./kicad-step.json"
9
10
 
@@ -90,10 +91,12 @@ test("kicad-step: switch fixture renders consistently", async () => {
90
91
  }, 30000)
91
92
 
92
93
  test("kicad-step: merges KiCad STEP models referenced via model_step_url", async () => {
94
+ const fsMap = await loadStepFilesFromCircuitJson(circuitJson)
93
95
  const stepText = await circuitJsonToStep(circuitJson as any, {
94
96
  includeComponents: true,
95
97
  includeExternalMeshes: true,
96
98
  productName: "KiCadStepMerge",
99
+ fsMap,
97
100
  })
98
101
 
99
102
  expect(stepText).toContain("KiCadStepMerge")
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Test utility for loading local STEP files.
3
+ * This file uses Node APIs and should only be imported in tests, not in lib.
4
+ */
5
+ import { readFile } from "node:fs/promises"
6
+
7
+ type CadComponent = {
8
+ type: string
9
+ model_step_url?: string
10
+ }
11
+
12
+ function isFilePath(value: string): boolean {
13
+ return !/^https?:\/\//i.test(value)
14
+ }
15
+
16
+ /**
17
+ * Loads all local STEP files referenced by cad_components in circuit JSON.
18
+ * Returns a map of file path to file content.
19
+ */
20
+ export async function loadStepFilesFromCircuitJson(
21
+ circuitJson: unknown[],
22
+ ): Promise<Record<string, string>> {
23
+ const stepContents: Record<string, string> = {}
24
+ const stepFilePaths = new Set<string>()
25
+
26
+ for (const item of circuitJson as CadComponent[]) {
27
+ if (
28
+ item?.type === "cad_component" &&
29
+ item.model_step_url &&
30
+ isFilePath(item.model_step_url)
31
+ ) {
32
+ stepFilePaths.add(item.model_step_url)
33
+ }
34
+ }
35
+
36
+ for (const filePath of stepFilePaths) {
37
+ stepContents[filePath] = await readFile(filePath, "utf8")
38
+ }
39
+
40
+ return stepContents
41
+ }