fluidcad 0.0.34 → 0.0.36

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 (176) hide show
  1. package/README.md +69 -0
  2. package/bin/commands/login.js +148 -0
  3. package/bin/commands/mcp.js +3 -2
  4. package/bin/commands/pack.js +49 -0
  5. package/bin/commands/publish.js +231 -0
  6. package/bin/fluidcad.js +6 -0
  7. package/bin/lib/api-client.js +48 -0
  8. package/bin/lib/browser.js +16 -0
  9. package/bin/lib/config.js +39 -0
  10. package/bin/lib/model-config.js +61 -0
  11. package/bin/lib/prompt.js +97 -0
  12. package/bin/lib/workspace.js +57 -0
  13. package/lib/dist/common/shape-factory.d.ts +2 -1
  14. package/lib/dist/common/shape-factory.js +4 -0
  15. package/lib/dist/common/transformable-primitive.d.ts +6 -5
  16. package/lib/dist/common/transformable-primitive.js +8 -7
  17. package/lib/dist/common/vertex.js +0 -1
  18. package/lib/dist/core/2d/aline.d.ts +4 -3
  19. package/lib/dist/core/2d/aline.js +3 -2
  20. package/lib/dist/core/2d/arc.d.ts +3 -2
  21. package/lib/dist/core/2d/arc.js +4 -3
  22. package/lib/dist/core/2d/bezier.d.ts +8 -6
  23. package/lib/dist/core/2d/circle.d.ts +4 -3
  24. package/lib/dist/core/2d/circle.js +3 -2
  25. package/lib/dist/core/2d/ellipse.d.ts +5 -4
  26. package/lib/dist/core/2d/ellipse.js +5 -4
  27. package/lib/dist/core/2d/hline.d.ts +4 -3
  28. package/lib/dist/core/2d/hline.js +5 -3
  29. package/lib/dist/core/2d/line.js +1 -0
  30. package/lib/dist/core/2d/offset.d.ts +3 -2
  31. package/lib/dist/core/2d/offset.js +6 -5
  32. package/lib/dist/core/2d/polygon.d.ts +5 -4
  33. package/lib/dist/core/2d/polygon.js +10 -9
  34. package/lib/dist/core/2d/rect.d.ts +4 -3
  35. package/lib/dist/core/2d/rect.js +10 -9
  36. package/lib/dist/core/2d/slot.d.ts +14 -6
  37. package/lib/dist/core/2d/slot.js +19 -8
  38. package/lib/dist/core/2d/vline.d.ts +4 -3
  39. package/lib/dist/core/2d/vline.js +5 -3
  40. package/lib/dist/core/chamfer.d.ts +5 -4
  41. package/lib/dist/core/chamfer.js +7 -6
  42. package/lib/dist/core/color.d.ts +3 -2
  43. package/lib/dist/core/color.js +2 -1
  44. package/lib/dist/core/cut.d.ts +4 -3
  45. package/lib/dist/core/cut.js +5 -4
  46. package/lib/dist/core/cylinder.d.ts +2 -1
  47. package/lib/dist/core/cylinder.js +2 -1
  48. package/lib/dist/core/draft.d.ts +3 -2
  49. package/lib/dist/core/draft.js +3 -2
  50. package/lib/dist/core/extrude.d.ts +4 -3
  51. package/lib/dist/core/extrude.js +5 -4
  52. package/lib/dist/core/fillet.d.ts +5 -4
  53. package/lib/dist/core/fillet.js +6 -5
  54. package/lib/dist/core/index.d.ts +1 -0
  55. package/lib/dist/core/index.js +1 -0
  56. package/lib/dist/core/interfaces.d.ts +25 -24
  57. package/lib/dist/core/param.d.ts +74 -0
  58. package/lib/dist/core/param.js +147 -0
  59. package/lib/dist/core/repeat.d.ts +2 -1
  60. package/lib/dist/core/repeat.js +10 -8
  61. package/lib/dist/core/revolve.d.ts +2 -1
  62. package/lib/dist/core/revolve.js +3 -2
  63. package/lib/dist/core/rib.d.ts +3 -2
  64. package/lib/dist/core/rib.js +6 -2
  65. package/lib/dist/core/rotate.d.ts +5 -4
  66. package/lib/dist/core/rotate.js +4 -3
  67. package/lib/dist/core/shell.d.ts +3 -2
  68. package/lib/dist/core/shell.js +3 -2
  69. package/lib/dist/core/sphere.d.ts +3 -2
  70. package/lib/dist/core/sphere.js +2 -1
  71. package/lib/dist/core/translate.d.ts +7 -6
  72. package/lib/dist/core/translate.js +6 -5
  73. package/lib/dist/features/2d/arc.js +5 -5
  74. package/lib/dist/features/2d/bezier.js +16 -16
  75. package/lib/dist/features/2d/circle.js +4 -0
  76. package/lib/dist/features/2d/ellipse.js +4 -0
  77. package/lib/dist/features/2d/hline.d.ts +3 -0
  78. package/lib/dist/features/2d/hline.js +9 -2
  79. package/lib/dist/features/2d/line.d.ts +3 -0
  80. package/lib/dist/features/2d/line.js +11 -3
  81. package/lib/dist/features/2d/sketch.js +5 -1
  82. package/lib/dist/features/2d/slot.d.ts +5 -0
  83. package/lib/dist/features/2d/slot.js +52 -7
  84. package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
  85. package/lib/dist/features/2d/tarc-to-point.js +3 -0
  86. package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
  87. package/lib/dist/features/2d/tarc.js +3 -0
  88. package/lib/dist/features/2d/vline.d.ts +3 -0
  89. package/lib/dist/features/2d/vline.js +9 -2
  90. package/lib/dist/features/copy-circular.d.ts +4 -3
  91. package/lib/dist/features/copy-circular.js +16 -9
  92. package/lib/dist/features/copy-circular2d.js +16 -9
  93. package/lib/dist/features/copy-linear.d.ts +4 -3
  94. package/lib/dist/features/copy-linear.js +18 -12
  95. package/lib/dist/features/copy-linear2d.js +18 -12
  96. package/lib/dist/features/extrude-base.d.ts +4 -3
  97. package/lib/dist/features/extrude-base.js +10 -3
  98. package/lib/dist/features/mirror-shape2d.js +2 -2
  99. package/lib/dist/features/repeat-base.d.ts +13 -0
  100. package/lib/dist/features/repeat-base.js +21 -0
  101. package/lib/dist/features/repeat-circular.d.ts +6 -5
  102. package/lib/dist/features/repeat-circular.js +3 -6
  103. package/lib/dist/features/repeat-linear.d.ts +7 -7
  104. package/lib/dist/features/repeat-linear.js +3 -6
  105. package/lib/dist/index.d.ts +5 -0
  106. package/lib/dist/index.js +8 -1
  107. package/lib/dist/io/file-import.d.ts +7 -0
  108. package/lib/dist/io/file-import.js +30 -10
  109. package/lib/dist/math/lazy-matrix.d.ts +5 -0
  110. package/lib/dist/math/lazy-matrix.js +78 -10
  111. package/lib/dist/oc/boolean-ops.d.ts +2 -2
  112. package/lib/dist/param-registry.d.ts +34 -0
  113. package/lib/dist/param-registry.js +60 -0
  114. package/lib/dist/rendering/mesh-builder.js +2 -1
  115. package/lib/dist/tests/features/copy-circular.test.js +1 -1
  116. package/lib/dist/tests/features/copy-linear.test.js +10 -10
  117. package/lib/dist/tests/features/repeat-user-repro-cache.test.d.ts +1 -0
  118. package/lib/dist/tests/features/repeat-user-repro-cache.test.js +97 -0
  119. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  120. package/llm-docs/api/bezier.md +10 -11
  121. package/llm-docs/api/index.json +1 -1
  122. package/llm-docs/api/types/arc-points.md +2 -2
  123. package/llm-docs/api/types/cut.md +10 -10
  124. package/llm-docs/api/types/extrude.md +10 -10
  125. package/llm-docs/api/types/loft.md +6 -6
  126. package/llm-docs/api/types/revolve.md +6 -6
  127. package/llm-docs/api/types/rib.md +2 -2
  128. package/llm-docs/api/types/slot.md +2 -2
  129. package/llm-docs/api/types/sweep.md +10 -10
  130. package/llm-docs/api/types/transformable.md +14 -14
  131. package/llm-docs/index.json +12 -12
  132. package/mcp/dist/client.d.ts +1 -0
  133. package/mcp/dist/client.js +8 -1
  134. package/mcp/dist/server.js +14 -1
  135. package/mcp/dist/tools/engine.d.ts +16 -0
  136. package/mcp/dist/tools/engine.js +45 -0
  137. package/package.json +9 -3
  138. package/server/dist/api.d.ts +37 -0
  139. package/server/dist/api.js +44 -0
  140. package/server/dist/code-editor.d.ts +64 -0
  141. package/server/dist/code-editor.js +520 -2
  142. package/server/dist/fluidcad-server.d.ts +87 -1
  143. package/server/dist/fluidcad-server.js +254 -88
  144. package/server/dist/host/blocked-imports.d.ts +8 -0
  145. package/server/dist/host/blocked-imports.js +30 -0
  146. package/server/dist/{vite-manager.d.ts → host/local-scene-host.d.ts} +3 -1
  147. package/server/dist/{vite-manager.js → host/local-scene-host.js} +6 -26
  148. package/server/dist/host/scene-host.d.ts +19 -0
  149. package/server/dist/host/scene-host.js +1 -0
  150. package/server/dist/index.js +24 -117
  151. package/server/dist/model-package/capture-params.d.ts +19 -0
  152. package/server/dist/model-package/capture-params.js +42 -0
  153. package/server/dist/model-package/pack.d.ts +23 -0
  154. package/server/dist/model-package/pack.js +230 -0
  155. package/server/dist/model-package/types.d.ts +79 -0
  156. package/server/dist/model-package/types.js +17 -0
  157. package/server/dist/routes/hit-test.d.ts +3 -0
  158. package/server/dist/routes/hit-test.js +17 -0
  159. package/server/dist/routes/pack.d.ts +10 -0
  160. package/server/dist/routes/pack.js +47 -0
  161. package/server/dist/routes/params.d.ts +3 -0
  162. package/server/dist/routes/params.js +75 -0
  163. package/server/dist/routes/sketch-edits.d.ts +3 -0
  164. package/server/dist/routes/sketch-edits.js +542 -0
  165. package/server/dist/routes/timeline.d.ts +3 -0
  166. package/server/dist/routes/timeline.js +49 -0
  167. package/server/dist/server-core.d.ts +53 -0
  168. package/server/dist/server-core.js +147 -0
  169. package/server/dist/ws-protocol.d.ts +101 -2
  170. package/ui/dist/assets/index-CDJmUpFI.css +2 -0
  171. package/ui/dist/assets/index-MRqwG9Vh.js +5417 -0
  172. package/ui/dist/index.html +2 -2
  173. package/server/dist/routes/actions.d.ts +0 -3
  174. package/server/dist/routes/actions.js +0 -309
  175. package/ui/dist/assets/index-BdqrMDRu.js +0 -4946
  176. package/ui/dist/assets/index-DR7c2Qk9.css +0 -2
@@ -1,19 +1,19 @@
1
1
  import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
2
- import { Axis } from "../math/axis.js";
3
- import { AxisObjectBase } from "./axis-renderable-base.js";
4
- export type RepeatAxisSource = Axis | AxisObjectBase;
2
+ import { RepeatBase, RepeatAxisSource } from "./repeat-base.js";
3
+ export { RepeatAxisSource } from "./repeat-base.js";
4
+ import { type NumberParam } from "../core/param.js";
5
5
  export type LinearRepeatOptions = {
6
- count: number | number[];
6
+ count: NumberParam | number[];
7
7
  centered?: boolean;
8
8
  skip?: number[][];
9
9
  } & ({
10
- offset: number | number[];
10
+ offset: NumberParam | number[];
11
11
  length?: never;
12
12
  } | {
13
- length: number | number[];
13
+ length: NumberParam | number[];
14
14
  offset?: never;
15
15
  });
16
- export declare class RepeatLinear extends SceneObject {
16
+ export declare class RepeatLinear extends RepeatBase {
17
17
  axes: RepeatAxisSource[];
18
18
  options: LinearRepeatOptions;
19
19
  targetObjects: SceneObject[] | null;
@@ -1,9 +1,6 @@
1
- import { SceneObject } from "../common/scene-object.js";
2
1
  import { AxisObjectBase } from "./axis-renderable-base.js";
3
- function axisOf(source) {
4
- return source instanceof AxisObjectBase ? source.getAxis() : source;
5
- }
6
- export class RepeatLinear extends SceneObject {
2
+ import { RepeatBase } from "./repeat-base.js";
3
+ export class RepeatLinear extends RepeatBase {
7
4
  axes;
8
5
  options;
9
6
  targetObjects;
@@ -36,7 +33,7 @@ export class RepeatLinear extends SceneObject {
36
33
  return false;
37
34
  }
38
35
  for (let i = 0; i < this.axes.length; i++) {
39
- if (!axisOf(this.axes[i]).equals(axisOf(other.axes[i]))) {
36
+ if (!RepeatLinear.axisSourceEquals(this.axes[i], other.axes[i])) {
40
37
  return false;
41
38
  }
42
39
  }
@@ -17,6 +17,11 @@ export type SceneParserContext = {
17
17
  getActiveSketch(): Sketch | null;
18
18
  };
19
19
  export declare function registerBuilder<T extends Function>(builder: (context: SceneParserContext) => T): T;
20
+ export { createParamRegistry, getParamRegistry } from './param-registry.js';
21
+ export type { ParamDefinition, MultiControlType, SelectOption, ParamVal, ParamScalar } from './param-registry.js';
22
+ export { setAssetProvider } from './io/file-import.js';
23
+ export type { AssetProvider } from './io/file-import.js';
24
+ export { getSceneManager } from './scene-manager.js';
20
25
  export interface FluidCADOptions {
21
26
  mesh?: {
22
27
  lineDeflection?: number;
package/lib/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { loadOC } from "./load.js";
2
- import { createManager, getCurrentScene } from "./scene-manager.js";
2
+ import { createManager, getCurrentScene, getSceneManager } from "./scene-manager.js";
3
3
  import { parse as parseStackTrace } from "stacktrace-parser";
4
4
  export function captureSourceLocation() {
5
5
  const stack = new Error().stack;
@@ -88,8 +88,15 @@ export function registerBuilder(builder) {
88
88
  return fn;
89
89
  ;
90
90
  }
91
+ export { createParamRegistry, getParamRegistry } from './param-registry.js';
92
+ export { setAssetProvider } from './io/file-import.js';
93
+ export { getSceneManager } from './scene-manager.js';
91
94
  export async function init(options) {
92
95
  await loadOC();
96
+ const existing = getSceneManager();
97
+ if (existing) {
98
+ return existing;
99
+ }
93
100
  const resolvedPath = process.env.FLUIDCAD_WORKSPACE_PATH || '';
94
101
  return createManager(resolvedPath, options);
95
102
  }
@@ -1,5 +1,12 @@
1
1
  import { Shape } from "../common/shape.js";
2
2
  import { Solid } from "../common/solid.js";
3
+ /**
4
+ * Override hook for hub mode (and tests). When set, asset reads consult
5
+ * the provider instead of the filesystem — paths are workspace-relative
6
+ * (e.g. `imports/foo.brep`). Returning null falls back to disk.
7
+ */
8
+ export type AssetProvider = (workspaceRelPath: string) => Uint8Array | null;
9
+ export declare function setAssetProvider(provider: AssetProvider | null): void;
3
10
  export declare class FileImport {
4
11
  static deserializeShapes(fileName: string): Solid[];
5
12
  static serializeShape(shape: Shape, workspacePath: string, fileName: string): void;
@@ -2,17 +2,37 @@ import * as fs from "fs";
2
2
  import { join } from "path";
3
3
  import { OcIO } from "../oc/io.js";
4
4
  import { getSceneManager } from "../scene-manager.js";
5
+ let assetProvider = null;
6
+ export function setAssetProvider(provider) {
7
+ assetProvider = provider;
8
+ }
9
+ function readWorkspaceAsset(relPath) {
10
+ if (assetProvider) {
11
+ const bytes = assetProvider(relPath);
12
+ if (bytes) {
13
+ return { text: Buffer.from(bytes).toString('utf8'), exists: true };
14
+ }
15
+ }
16
+ const sceneManager = getSceneManager();
17
+ const filePath = join(sceneManager.rootPath, relPath);
18
+ if (!fs.existsSync(filePath)) {
19
+ return { exists: false };
20
+ }
21
+ return { text: fs.readFileSync(filePath, 'utf8'), exists: true };
22
+ }
5
23
  export class FileImport {
6
24
  static deserializeShapes(fileName) {
7
25
  if (!fileName.endsWith(('.brep'))) {
8
26
  fileName += '.brep';
9
27
  }
10
- const sceneManager = getSceneManager();
11
- const filePath = join(sceneManager.rootPath, 'imports', fileName);
28
+ const relPath = join('imports', fileName);
12
29
  console.log(`Reading file ${fileName}`);
13
- const file = fs.readFileSync(filePath, 'utf8');
14
- console.log(`File ${filePath} read successfully, size: ${file.length} bytes`);
15
- return OcIO.readBRepSolids(fileName, file);
30
+ const result = readWorkspaceAsset(relPath);
31
+ if (!result.exists) {
32
+ throw new Error(`Imported asset not found: ${relPath}`);
33
+ }
34
+ console.log(`File ${relPath} read successfully, size: ${result.text.length} bytes`);
35
+ return OcIO.readBRepSolids(fileName, result.text);
16
36
  }
17
37
  static serializeShape(shape, workspacePath, fileName) {
18
38
  const file = OcIO.writeBRep(shape, fileName);
@@ -56,11 +76,11 @@ export class FileImport {
56
76
  // Read color metadata from JSON sidecar (skipped when noColors is set)
57
77
  let colorData = [];
58
78
  if (!options?.noColors) {
59
- const sceneManager = getSceneManager();
60
- const jsonPath = join(sceneManager.rootPath, 'imports', brepFileName + '.colors.json');
61
- if (fs.existsSync(jsonPath)) {
62
- colorData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
63
- console.log(`Loaded color metadata from ${jsonPath}`);
79
+ const relPath = join('imports', brepFileName + '.colors.json');
80
+ const result = readWorkspaceAsset(relPath);
81
+ if (result.exists) {
82
+ colorData = JSON.parse(result.text);
83
+ console.log(`Loaded color metadata from ${relPath}`);
64
84
  }
65
85
  }
66
86
  const include = options?.include;
@@ -4,10 +4,12 @@ import { Plane } from "./plane.js";
4
4
  /** Anything that can be resolved to an `Axis` once built — e.g. `AxisObjectBase`. */
5
5
  export interface AxisLazySource {
6
6
  getAxis(): Axis;
7
+ compareTo?(other: unknown): boolean;
7
8
  }
8
9
  /** Anything that can be resolved to a `Plane` once built — e.g. `PlaneObjectBase`. */
9
10
  export interface PlaneLazySource {
10
11
  getPlane(): Plane;
12
+ compareTo?(other: unknown): boolean;
11
13
  }
12
14
  /**
13
15
  * Memoized thunk over a Matrix4. Lets parse-time code thread a transform
@@ -20,9 +22,12 @@ export interface PlaneLazySource {
20
22
  export declare class LazyMatrix {
21
23
  private _resolver;
22
24
  private _cached;
25
+ private _identity;
23
26
  private constructor();
24
27
  resolve(): Matrix4;
25
28
  equals(other: LazyMatrix, tolerance?: number): boolean;
29
+ private static axisSourceEquals;
30
+ private static planeSourceEquals;
26
31
  static of(matrix: Matrix4): LazyMatrix;
27
32
  static from(resolver: () => Matrix4): LazyMatrix;
28
33
  static mirror(plane: Plane | PlaneLazySource): LazyMatrix;
@@ -16,9 +16,11 @@ function isPlaneLazySource(p) {
16
16
  export class LazyMatrix {
17
17
  _resolver;
18
18
  _cached;
19
- constructor(resolver, cached = null) {
19
+ _identity;
20
+ constructor(resolver, identity, cached = null) {
20
21
  this._resolver = resolver;
21
22
  this._cached = cached;
23
+ this._identity = identity;
22
24
  }
23
25
  resolve() {
24
26
  if (this._cached === null) {
@@ -27,40 +29,106 @@ export class LazyMatrix {
27
29
  return this._cached;
28
30
  }
29
31
  equals(other, tolerance = 0) {
30
- return this.resolve().equals(other.resolve(), tolerance);
32
+ const a = this._identity;
33
+ const b = other._identity;
34
+ if (a.kind !== b.kind) {
35
+ return false;
36
+ }
37
+ switch (a.kind) {
38
+ case "eager":
39
+ return a.matrix.equals(b.matrix, tolerance);
40
+ case "rotation": {
41
+ const bo = b;
42
+ return Math.abs(a.angle - bo.angle) <= tolerance
43
+ && LazyMatrix.axisSourceEquals(a.axis, bo.axis, tolerance);
44
+ }
45
+ case "translation": {
46
+ const bo = b;
47
+ return Math.abs(a.distance - bo.distance) <= tolerance
48
+ && LazyMatrix.axisSourceEquals(a.axis, bo.axis, tolerance);
49
+ }
50
+ case "mirror": {
51
+ const bo = b;
52
+ return LazyMatrix.planeSourceEquals(a.plane, bo.plane, tolerance);
53
+ }
54
+ case "opaque":
55
+ // No structural information — can't compare safely without resolving,
56
+ // and resolving may dereference unbuilt sources. Treat as unequal so
57
+ // the caller takes the rebuild path instead of crashing.
58
+ return false;
59
+ }
60
+ }
61
+ static axisSourceEquals(a, b, tolerance) {
62
+ if (isAxisLazySource(a)) {
63
+ if (!isAxisLazySource(b)) {
64
+ return false;
65
+ }
66
+ if (a === b) {
67
+ return true;
68
+ }
69
+ if (typeof a.compareTo === "function" && typeof b.compareTo === "function") {
70
+ return a.compareTo(b);
71
+ }
72
+ return false;
73
+ }
74
+ if (isAxisLazySource(b)) {
75
+ return false;
76
+ }
77
+ return a.equals(b, tolerance);
78
+ }
79
+ static planeSourceEquals(a, b, tolerance) {
80
+ if (isPlaneLazySource(a)) {
81
+ if (!isPlaneLazySource(b)) {
82
+ return false;
83
+ }
84
+ if (a === b) {
85
+ return true;
86
+ }
87
+ if (typeof a.compareTo === "function" && typeof b.compareTo === "function") {
88
+ return a.compareTo(b);
89
+ }
90
+ return false;
91
+ }
92
+ if (isPlaneLazySource(b)) {
93
+ return false;
94
+ }
95
+ return a.compareTo(b, tolerance);
31
96
  }
32
97
  static of(matrix) {
33
- return new LazyMatrix(() => matrix, matrix);
98
+ return new LazyMatrix(() => matrix, { kind: "eager", matrix }, matrix);
34
99
  }
35
100
  static from(resolver) {
36
- return new LazyMatrix(resolver);
101
+ return new LazyMatrix(resolver, { kind: "opaque" });
37
102
  }
38
103
  static mirror(plane) {
39
104
  if (isPlaneLazySource(plane)) {
40
105
  return new LazyMatrix(() => {
41
106
  const p = plane.getPlane();
42
107
  return Matrix4.mirrorPlane(p.normal, p.origin);
43
- });
108
+ }, { kind: "mirror", plane });
44
109
  }
45
- return LazyMatrix.of(Matrix4.mirrorPlane(plane.normal, plane.origin));
110
+ const matrix = Matrix4.mirrorPlane(plane.normal, plane.origin);
111
+ return new LazyMatrix(() => matrix, { kind: "mirror", plane }, matrix);
46
112
  }
47
113
  static rotation(axis, angle) {
48
114
  if (isAxisLazySource(axis)) {
49
115
  return new LazyMatrix(() => {
50
116
  const a = axis.getAxis();
51
117
  return Matrix4.fromRotationAroundAxis(a.origin, a.direction, angle);
52
- });
118
+ }, { kind: "rotation", axis, angle });
53
119
  }
54
- return LazyMatrix.of(Matrix4.fromRotationAroundAxis(axis.origin, axis.direction, angle));
120
+ const matrix = Matrix4.fromRotationAroundAxis(axis.origin, axis.direction, angle);
121
+ return new LazyMatrix(() => matrix, { kind: "rotation", axis, angle }, matrix);
55
122
  }
56
123
  static translation(axis, distance) {
57
124
  if (isAxisLazySource(axis)) {
58
125
  return new LazyMatrix(() => {
59
126
  const dir = axis.getAxis().direction;
60
127
  return Matrix4.fromTranslation(dir.x * distance, dir.y * distance, dir.z * distance);
61
- });
128
+ }, { kind: "translation", axis, distance });
62
129
  }
63
130
  const dir = axis.direction;
64
- return LazyMatrix.of(Matrix4.fromTranslation(dir.x * distance, dir.y * distance, dir.z * distance));
131
+ const matrix = Matrix4.fromTranslation(dir.x * distance, dir.y * distance, dir.z * distance);
132
+ return new LazyMatrix(() => matrix, { kind: "translation", axis, distance }, matrix);
65
133
  }
66
134
  }
@@ -8,8 +8,8 @@ export declare class BooleanOps {
8
8
  static cutShapes(shape: Shape, tool: Shape): Shape;
9
9
  static cutShapesRaw(shape: TopoDS_Shape, tool: TopoDS_Shape): TopoDS_Shape;
10
10
  static cutMultiShape(stocks: Shape[], tools: Shape[], plane?: Plane, cutDistance?: number): {
11
- result: Edge | import("../common/wire.js").Wire | Solid | Face;
12
- modified: (shape: Shape) => (Edge | import("../common/wire.js").Wire | Solid | Face)[];
11
+ result: import("../common/vertex.js").Vertex | Edge | import("../common/wire.js").Wire | Solid | Face;
12
+ modified: (shape: Shape) => (import("../common/vertex.js").Vertex | Edge | import("../common/wire.js").Wire | Solid | Face)[];
13
13
  sectionEdges: Edge[];
14
14
  startEdges: Edge[];
15
15
  endEdges: Edge[];
@@ -0,0 +1,34 @@
1
+ export type ControlType = 'auto' | 'text' | 'number' | 'slider' | 'select' | 'checkbox' | 'color';
2
+ export type MultiControlType = 'select' | 'checkboxes' | 'chips';
3
+ export type SelectOption = {
4
+ label: string;
5
+ value: string | number;
6
+ };
7
+ export type ParamScalar = string | number | boolean;
8
+ export type ParamVal = ParamScalar | (string | number)[];
9
+ export type ParamDefinition = {
10
+ label: string;
11
+ defaultValue: ParamVal;
12
+ currentValue: ParamVal;
13
+ controlType: ControlType;
14
+ description?: string;
15
+ group?: string;
16
+ min?: number;
17
+ max?: number;
18
+ step?: number;
19
+ options?: SelectOption[];
20
+ multi?: boolean;
21
+ multiControlType?: MultiControlType;
22
+ };
23
+ export declare class ParamRegistry {
24
+ private definitions;
25
+ private overrides;
26
+ register(def: ParamDefinition): void;
27
+ resolve(label: string, defaultValue: (string | number)[]): (string | number)[];
28
+ resolve<T extends string | number | boolean>(label: string, defaultValue: T): T;
29
+ setOverrides(overrides: Map<string, any>): void;
30
+ getDefinitions(): ParamDefinition[];
31
+ clear(): void;
32
+ }
33
+ export declare function createParamRegistry(): ParamRegistry;
34
+ export declare function getParamRegistry(): ParamRegistry;
@@ -0,0 +1,60 @@
1
+ export class ParamRegistry {
2
+ definitions = new Map();
3
+ overrides = new Map();
4
+ register(def) {
5
+ this.definitions.set(def.label, def);
6
+ }
7
+ resolve(label, defaultValue) {
8
+ if (!this.overrides.has(label)) {
9
+ return defaultValue;
10
+ }
11
+ const override = this.overrides.get(label);
12
+ if (Array.isArray(defaultValue)) {
13
+ if (Array.isArray(override)) {
14
+ return override;
15
+ }
16
+ if (override != null) {
17
+ return [override];
18
+ }
19
+ return defaultValue;
20
+ }
21
+ if (typeof defaultValue === 'boolean') {
22
+ if (override === true || override === 'true' || override === 1) {
23
+ return true;
24
+ }
25
+ if (override === false || override === 'false' || override === 0) {
26
+ return false;
27
+ }
28
+ return defaultValue;
29
+ }
30
+ if (typeof defaultValue === 'number') {
31
+ const num = Number(override);
32
+ if (Number.isFinite(num)) {
33
+ return num;
34
+ }
35
+ return defaultValue;
36
+ }
37
+ return String(override);
38
+ }
39
+ setOverrides(overrides) {
40
+ this.overrides = overrides;
41
+ }
42
+ getDefinitions() {
43
+ return Array.from(this.definitions.values());
44
+ }
45
+ clear() {
46
+ this.definitions.clear();
47
+ this.overrides.clear();
48
+ }
49
+ }
50
+ let currentRegistry = null;
51
+ export function createParamRegistry() {
52
+ currentRegistry = new ParamRegistry();
53
+ return currentRegistry;
54
+ }
55
+ export function getParamRegistry() {
56
+ if (!currentRegistry) {
57
+ currentRegistry = new ParamRegistry();
58
+ }
59
+ return currentRegistry;
60
+ }
@@ -33,7 +33,8 @@ export class MeshBuilder {
33
33
  console.warn("Shell shapes are not supported yet.");
34
34
  }
35
35
  else if (Explorer.isVertex(shape)) {
36
- console.warn("Vertex shapes are not supported yet.");
36
+ const pt = shapeObj.toPoint();
37
+ result = { vertices: [pt.x, pt.y, pt.z], normals: [], indices: [] };
37
38
  }
38
39
  else {
39
40
  console.warn("Shape is not a valid TopoDS_Shape.");
@@ -28,7 +28,7 @@ describe("copy circular", () => {
28
28
  const c = copy("circular", "z", { count: 3, angle: 180 }, e);
29
29
  render();
30
30
  const shapes = c.getShapes();
31
- expect(shapes).toHaveLength(2);
31
+ expect(shapes).toHaveLength(3);
32
32
  });
33
33
  it("should skip specified indices", () => {
34
34
  sketch("xy", () => {
@@ -25,11 +25,11 @@ describe("copy linear", () => {
25
25
  const e = extrude(10).new();
26
26
  const c = copy("linear", "x", { count: 3, offset: 50 }, e);
27
27
  render();
28
- // 2 copies (index 1 and 2), each offset by 50 along X
28
+ // Original + 2 copies (index 1 and 2), each offset by 50 along X
29
29
  const shapes = c.getShapes();
30
- expect(shapes).toHaveLength(2);
31
- const bbox1 = ShapeOps.getBoundingBox(shapes[0]);
32
- const bbox2 = ShapeOps.getBoundingBox(shapes[1]);
30
+ expect(shapes).toHaveLength(3);
31
+ const bbox1 = ShapeOps.getBoundingBox(shapes[1]);
32
+ const bbox2 = ShapeOps.getBoundingBox(shapes[2]);
33
33
  expect(bbox1.minX).toBeCloseTo(50, 0);
34
34
  expect(bbox2.minX).toBeCloseTo(100, 0);
35
35
  });
@@ -41,8 +41,8 @@ describe("copy linear", () => {
41
41
  const c = copy("linear", "y", { count: 2, offset: 60 }, e);
42
42
  render();
43
43
  const shapes = c.getShapes();
44
- expect(shapes).toHaveLength(1);
45
- const bbox = ShapeOps.getBoundingBox(shapes[0]);
44
+ expect(shapes).toHaveLength(2);
45
+ const bbox = ShapeOps.getBoundingBox(shapes[1]);
46
46
  expect(bbox.minY).toBeCloseTo(60, 0);
47
47
  });
48
48
  it("should copy along Z axis", () => {
@@ -53,8 +53,8 @@ describe("copy linear", () => {
53
53
  const c = copy("linear", "z", { count: 2, offset: 30 }, e);
54
54
  render();
55
55
  const shapes = c.getShapes();
56
- expect(shapes).toHaveLength(1);
57
- const bbox = ShapeOps.getBoundingBox(shapes[0]);
56
+ expect(shapes).toHaveLength(2);
57
+ const bbox = ShapeOps.getBoundingBox(shapes[1]);
58
58
  expect(bbox.minZ).toBeCloseTo(30, 0);
59
59
  });
60
60
  it("should distribute copies by total length", () => {
@@ -66,8 +66,8 @@ describe("copy linear", () => {
66
66
  const c = copy("linear", "x", { count: 4, length: 120 }, e);
67
67
  render();
68
68
  const shapes = c.getShapes();
69
- expect(shapes).toHaveLength(3);
70
- const bbox = ShapeOps.getBoundingBox(shapes[0]);
69
+ expect(shapes).toHaveLength(4);
70
+ const bbox = ShapeOps.getBoundingBox(shapes[1]);
71
71
  expect(bbox.minX).toBeCloseTo(40, 0);
72
72
  });
73
73
  it("should create a 2D grid with multiple axes", () => {
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { setupOC, render } from "../setup.js";
3
+ import { getSceneManager } from "../../scene-manager.js";
4
+ import { SceneCompare } from "../../rendering/scene-compare.js";
5
+ import sketch from "../../core/sketch.js";
6
+ import extrude from "../../core/extrude.js";
7
+ import axis from "../../core/axis.js";
8
+ import repeat from "../../core/repeat.js";
9
+ import sweep from "../../core/sweep.js";
10
+ import cut from "../../core/cut.js";
11
+ import remove from "../../core/remove.js";
12
+ import { rect, circle, vLine, tArc, tLine, slot, move, hMove } from "../../core/2d/index.js";
13
+ import copy from "../../core/copy.js";
14
+ // Regression: re-rendering a scene whose `repeat("circular", ...)` axis is an
15
+ // AxisFromEdge derived from a sketch line used to crash inside
16
+ // SceneCompare.compare with "Cannot read properties of undefined (reading
17
+ // 'origin')". RepeatCircular.compareTo eagerly called getAxis() on the
18
+ // new-scene axis source, which hadn't been built yet (compare runs pre-render).
19
+ describe("repeat circular cache-compare on unbuilt axis source", () => {
20
+ setupOC();
21
+ function buildScene(includeRemove) {
22
+ const spine = sketch("front", () => {
23
+ vLine(1.5);
24
+ tArc(-4, 45);
25
+ const topSegment = tLine(1.5);
26
+ return { topSegment };
27
+ }).reusable();
28
+ const profile = sketch("top", () => {
29
+ const innerPipe = circle(1.5);
30
+ const outerPipe = circle(2);
31
+ return { innerPipe, outerPipe };
32
+ });
33
+ const pipe = sweep(spine, profile.regions.outerPipe);
34
+ sketch("top", () => {
35
+ rect(3.5).centered().radius(0.5);
36
+ move([-2.5 / 2, -2.5 / 2]);
37
+ const c = circle(0.5);
38
+ copy("circular", [0, 0], { count: 4, angle: 360 }, c);
39
+ });
40
+ extrude(0.375);
41
+ sketch(pipe.endFaces(), () => {
42
+ circle(4);
43
+ });
44
+ const upperFlange = extrude(-0.625);
45
+ sweep(spine, profile.regions.innerPipe).remove();
46
+ const slots = sketch(upperFlange.endFaces(), () => {
47
+ hMove(3.25 / 2);
48
+ const outerSlot = slot(1, 0.75 / 2);
49
+ const innerSlot = slot(1, 0.45 / 2);
50
+ return { outerSlot, innerSlot };
51
+ });
52
+ const s1 = cut(slots.regions.innerSlot);
53
+ const s2 = cut(0.25, slots.regions.outerSlot);
54
+ const a = axis(spine.regions.topSegment);
55
+ repeat("circular", a, { count: 4, angle: 360 }, s1, s2);
56
+ if (includeRemove) {
57
+ remove(spine);
58
+ }
59
+ }
60
+ function currentScene() {
61
+ const mgr = getSceneManager();
62
+ if (!mgr) {
63
+ throw new Error("Scene manager not initialized");
64
+ }
65
+ return mgr.currentScene;
66
+ }
67
+ function startNewScene() {
68
+ const mgr = getSceneManager();
69
+ if (!mgr) {
70
+ throw new Error("Scene manager not initialized");
71
+ }
72
+ return mgr.startScene();
73
+ }
74
+ it("re-parsing the same script with cache compare does not throw", () => {
75
+ buildScene(false);
76
+ render();
77
+ const previousScene = currentScene();
78
+ expect(previousScene.getAllSceneObjects().some(o => o.getError())).toBe(false);
79
+ const newScene = startNewScene();
80
+ buildScene(false);
81
+ SceneCompare.compare(previousScene, newScene);
82
+ const rendered = render();
83
+ const errored = rendered.getAllSceneObjects().filter(o => o.getError());
84
+ expect(errored.map(o => `${o.getUniqueType()}: ${o.getError()}`)).toEqual([]);
85
+ });
86
+ it("appending remove(spine) after a cached render does not throw", () => {
87
+ buildScene(false);
88
+ render();
89
+ const previousScene = currentScene();
90
+ const newScene = startNewScene();
91
+ buildScene(true);
92
+ SceneCompare.compare(previousScene, newScene);
93
+ const rendered = render();
94
+ const errored = rendered.getAllSceneObjects().filter(o => o.getError());
95
+ expect(errored.map(o => `${o.getUniqueType()}: ${o.getError()}`)).toEqual([]);
96
+ });
97
+ });