Skip to content

Commit 3adc985

Browse files
zaloclaude
andcommitted
Fix Vercel deployment, address PR review comments, and add new bindings
- Add vercel.json to run build and serve from build/ directory - Add .nvmrc (Node 20) for Vercel compatibility - Fix TypeScript intellisense: match ofType() docs to actual enum values - Remove dead golden-layout code from postinstall.js - Add zero-vector validation in Section() and _normalize() - Add backward compat for loading old GoldenLayout project files - Rewrite selector helpers to use proper OCCT API (BRepAdaptor, GeomAbs enums) - Update tests for new binding-based implementations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fadc91d commit 3adc985

7 files changed

Lines changed: 104 additions & 237 deletions

File tree

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
20

js/CADWorker/CascadeStudioStandardLibrary.js

Lines changed: 44 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -952,74 +952,24 @@ function _edgeLength(edge) {
952952
}
953953

954954
function _edgeCurveType(edge) {
955-
// Sample 3 points along the edge to determine curve type geometrically
956-
// (GetType() is unavailable — GeomAbs_CurveType enum is unbound)
957955
let curve = new self.oc.BRepAdaptor_Curve_2(edge);
958-
let u0 = curve.FirstParameter(), u1 = curve.LastParameter();
959-
let p0 = new self.oc.gp_Pnt_1(), p1 = new self.oc.gp_Pnt_1(), p2 = new self.oc.gp_Pnt_1();
960-
curve.D0(u0, p0); curve.D0((u0 + u1) / 2, p1); curve.D0(u1, p2);
961-
let a = [p0.X(), p0.Y(), p0.Z()];
962-
let b = [p1.X(), p1.Y(), p1.Z()];
963-
let c = [p2.X(), p2.Y(), p2.Z()];
964-
965-
// Check collinearity: if cross product of (b-a) and (c-a) is ~zero → Line
966-
let ab = [b[0]-a[0], b[1]-a[1], b[2]-a[2]];
967-
let ac = [c[0]-a[0], c[1]-a[1], c[2]-a[2]];
968-
let cross = [
969-
ab[1]*ac[2] - ab[2]*ac[1],
970-
ab[2]*ac[0] - ab[0]*ac[2],
971-
ab[0]*ac[1] - ab[1]*ac[0]
972-
];
973-
let crossMag = Math.sqrt(cross[0]*cross[0] + cross[1]*cross[1] + cross[2]*cross[2]);
974-
let abMag = Math.sqrt(ab[0]*ab[0] + ab[1]*ab[1] + ab[2]*ab[2]);
975-
if (abMag < 1e-10 || crossMag / abMag < 1e-6) { return "Line"; }
976-
977-
// Check if it's a circle: sample more points and check equal distance from center
978-
let nSamples = 8;
979-
let pts = [];
980-
for (let i = 0; i <= nSamples; i++) {
981-
let u = u0 + (u1 - u0) * i / nSamples;
982-
let p = new self.oc.gp_Pnt_1();
983-
curve.D0(u, p);
984-
pts.push([p.X(), p.Y(), p.Z()]);
985-
}
986-
// Approximate center as average of all points
987-
let cx = 0, cy = 0, cz = 0;
988-
for (let p of pts) { cx += p[0]; cy += p[1]; cz += p[2]; }
989-
cx /= pts.length; cy /= pts.length; cz /= pts.length;
990-
// Check if all distances from center are approximately equal
991-
let dists = pts.map(p => Math.sqrt((p[0]-cx)**2 + (p[1]-cy)**2 + (p[2]-cz)**2));
992-
let avgDist = dists.reduce((s,d) => s+d, 0) / dists.length;
993-
let maxDeviation = Math.max(...dists.map(d => Math.abs(d - avgDist)));
994-
if (avgDist > 1e-10 && maxDeviation / avgDist < 0.01) { return "Circle"; }
995-
956+
let type = curve.GetType();
957+
let CT = self.oc.GeomAbs_CurveType;
958+
if (type === CT.GeomAbs_Line) return "Line";
959+
if (type === CT.GeomAbs_Circle) return "Circle";
960+
if (type === CT.GeomAbs_Ellipse) return "Ellipse";
961+
if (type === CT.GeomAbs_Hyperbola) return "Hyperbola";
962+
if (type === CT.GeomAbs_Parabola) return "Parabola";
963+
if (type === CT.GeomAbs_BezierCurve) return "BezierCurve";
964+
if (type === CT.GeomAbs_BSplineCurve) return "BSplineCurve";
996965
return "Other";
997966
}
998967

999968
function _edgeDirection(edge) {
1000-
// Compute direction from start and end points (no GetType() needed)
1001969
let curve = new self.oc.BRepAdaptor_Curve_2(edge);
1002-
let u0 = curve.FirstParameter(), u1 = curve.LastParameter();
1003-
let p0 = new self.oc.gp_Pnt_1(), p1 = new self.oc.gp_Pnt_1(), p2 = new self.oc.gp_Pnt_1();
1004-
curve.D0(u0, p0); curve.D0((u0 + u1) / 2, p1); curve.D0(u1, p2);
1005-
1006-
// Check if the edge is a line (collinear points)
1007-
let a = [p0.X(), p0.Y(), p0.Z()];
1008-
let b = [p1.X(), p1.Y(), p1.Z()];
1009-
let c = [p2.X(), p2.Y(), p2.Z()];
1010-
let ab = [b[0]-a[0], b[1]-a[1], b[2]-a[2]];
1011-
let ac = [c[0]-a[0], c[1]-a[1], c[2]-a[2]];
1012-
let cross = [
1013-
ab[1]*ac[2] - ab[2]*ac[1],
1014-
ab[2]*ac[0] - ab[0]*ac[2],
1015-
ab[0]*ac[1] - ab[1]*ac[0]
1016-
];
1017-
let crossMag = Math.sqrt(cross[0]*cross[0] + cross[1]*cross[1] + cross[2]*cross[2]);
1018-
let abMag = Math.sqrt(ab[0]*ab[0] + ab[1]*ab[1] + ab[2]*ab[2]);
1019-
if (abMag < 1e-10) { return null; }
1020-
if (crossMag / abMag > 1e-6) { return null; } // Not a line
1021-
1022-
return _normalize(ac);
970+
if (curve.GetType() !== self.oc.GeomAbs_CurveType.GeomAbs_Line) return null;
971+
let dir = curve.Line().Direction();
972+
return [dir.X(), dir.Y(), dir.Z()];
1023973
}
1024974

1025975
function _faceCentroid(face) {
@@ -1036,81 +986,17 @@ function _faceArea(face) {
1036986
}
1037987

1038988
function _faceNormal(face) {
1039-
// Compute face normal from edge geometry (BRepAdaptor_Surface not available)
1040-
// Sample 3 non-collinear points from the face's edges to determine normal
1041-
try {
1042-
let points = [];
1043-
let explorer = new self.oc.TopExp_Explorer_2(
1044-
face, self.oc.TopAbs_ShapeEnum.TopAbs_EDGE, self.oc.TopAbs_ShapeEnum.TopAbs_SHAPE
1045-
);
1046-
while (explorer.More() && points.length < 10) {
1047-
let edge = self.oc.TopoDS.Edge_1(explorer.Current());
1048-
let curve = new self.oc.BRepAdaptor_Curve_2(edge);
1049-
let u0 = curve.FirstParameter(), u1 = curve.LastParameter();
1050-
for (let t of [0, 0.5, 1]) {
1051-
let p = new self.oc.gp_Pnt_1();
1052-
curve.D0(u0 + (u1 - u0) * t, p);
1053-
points.push([p.X(), p.Y(), p.Z()]);
1054-
}
1055-
explorer.Next();
1056-
}
1057-
// Find 3 non-collinear points
1058-
if (points.length < 3) { return [0, 0, 1]; }
1059-
let a = points[0];
1060-
for (let i = 1; i < points.length; i++) {
1061-
let ab = [points[i][0]-a[0], points[i][1]-a[1], points[i][2]-a[2]];
1062-
let abLen = _vecLength(ab);
1063-
if (abLen < 1e-10) continue;
1064-
for (let j = i + 1; j < points.length; j++) {
1065-
let ac = [points[j][0]-a[0], points[j][1]-a[1], points[j][2]-a[2]];
1066-
let cross = [
1067-
ab[1]*ac[2] - ab[2]*ac[1],
1068-
ab[2]*ac[0] - ab[0]*ac[2],
1069-
ab[0]*ac[1] - ab[1]*ac[0]
1070-
];
1071-
let crossMag = _vecLength(cross);
1072-
if (crossMag > 1e-8) {
1073-
return _normalize(cross);
1074-
}
1075-
}
1076-
}
1077-
return [0, 0, 1]; // Fallback (degenerate face)
1078-
} catch (e) {
1079-
return [0, 0, 1]; // Fallback
1080-
}
1081-
}
1082-
1083-
function _isFacePlanar(face) {
1084-
// Check if all sampled points on the face lie on a plane
1085-
try {
1086-
let points = [];
1087-
let explorer = new self.oc.TopExp_Explorer_2(
1088-
face, self.oc.TopAbs_ShapeEnum.TopAbs_EDGE, self.oc.TopAbs_ShapeEnum.TopAbs_SHAPE
1089-
);
1090-
while (explorer.More()) {
1091-
let edge = self.oc.TopoDS.Edge_1(explorer.Current());
1092-
let curve = new self.oc.BRepAdaptor_Curve_2(edge);
1093-
let u0 = curve.FirstParameter(), u1 = curve.LastParameter();
1094-
for (let t = 0; t <= 1; t += 0.25) {
1095-
let p = new self.oc.gp_Pnt_1();
1096-
curve.D0(u0 + (u1 - u0) * t, p);
1097-
points.push([p.X(), p.Y(), p.Z()]);
1098-
}
1099-
explorer.Next();
1100-
}
1101-
if (points.length < 3) { return true; }
1102-
// Get the normal from first 3 non-collinear points
1103-
let normal = _faceNormal(face);
1104-
if (_vecLength(normal) < 1e-10) { return true; }
1105-
// Check all points lie on the plane defined by points[0] and normal
1106-
let d = _dot(points[0], normal);
1107-
for (let p of points) {
1108-
if (Math.abs(_dot(p, normal) - d) > 1e-4) { return false; }
1109-
}
1110-
return true;
1111-
} catch (e) {
1112-
return false;
1113-
}
989+
let surf = new self.oc.BRepAdaptor_Surface_2(face, true);
990+
let uMid = (surf.FirstUParameter() + surf.LastUParameter()) / 2;
991+
let vMid = (surf.FirstVParameter() + surf.LastVParameter()) / 2;
992+
let pnt = new self.oc.gp_Pnt_1();
993+
let du = new self.oc.gp_Vec_1();
994+
let dv = new self.oc.gp_Vec_1();
995+
surf.D1(uMid, vMid, pnt, du, dv);
996+
let normal = du.Crossed(dv);
997+
let mag = normal.Magnitude();
998+
if (mag < 1e-10) return [0, 0, 1];
999+
return [normal.X() / mag, normal.Y() / mag, normal.Z() / mag];
11141000
}
11151001

11161002
function _dot(a, b) {
@@ -1123,7 +1009,7 @@ function _vecLength(v) {
11231009

11241010
function _normalize(v) {
11251011
let len = _vecLength(v);
1126-
if (len < 1e-10) { return [0, 0, 0]; }
1012+
if (len < 1e-10) { throw new Error("Cannot normalize a zero-length vector; check your axis parameter"); }
11271013
return [v[0] / len, v[1] / len, v[2] / len];
11281014
}
11291015

@@ -1324,16 +1210,21 @@ class FaceSelector {
13241210
// --- Filtering ---
13251211

13261212
ofType(type) {
1327-
// BRepAdaptor_Surface not available in bindings — use geometry heuristics
1213+
let ST = self.oc.GeomAbs_SurfaceType;
1214+
let typeMap = {
1215+
"Plane": ST.GeomAbs_Plane,
1216+
"Cylinder": ST.GeomAbs_Cylinder,
1217+
"Cone": ST.GeomAbs_Cone,
1218+
"Sphere": ST.GeomAbs_Sphere,
1219+
"Torus": ST.GeomAbs_Torus,
1220+
"BSplineSurface": ST.GeomAbs_BSplineSurface,
1221+
"BezierSurface": ST.GeomAbs_BezierSurface,
1222+
};
1223+
let target = typeMap[type];
13281224
let sel = this._clone();
13291225
sel._entries = sel._entries.filter(e => {
1330-
let normal = _faceNormal(e.face);
1331-
if (type === "Plane") {
1332-
// A planar face has consistent normals at all edge points
1333-
return _isFacePlanar(e.face);
1334-
}
1335-
// Other types not reliably detectable without BRepAdaptor_Surface
1336-
return false;
1226+
let surf = new self.oc.BRepAdaptor_Surface_2(e.face, true);
1227+
return surf.GetType() === target;
13371228
});
13381229
return sel;
13391230
}
@@ -1505,59 +1396,14 @@ function Wedge(dx, dy, dz, ltx) {
15051396
function Section(shape, planeOrigin, planeNormal) {
15061397
if (!planeNormal) { planeNormal = [0, 0, 1]; }
15071398
if (!planeOrigin) { planeOrigin = [0, 0, 0]; }
1399+
if (_vecLength(planeNormal) < 1e-10) { throw new Error("Section: planeNormal must be a non-zero vector"); }
15081400
let curSection = self.CacheOp(arguments, "Section", () => {
1509-
// Create a very thin slab at the cutting plane and intersect with shape.
1510-
// (gp_Pln and BRepAlgoAPI_Section are not available in the bindings.)
1511-
let n = _normalize(planeNormal);
1512-
let thickness = 1e-3;
1513-
// Create a large box centered at origin
1514-
let slab = new self.oc.BRepPrimAPI_MakeBox_4(
1515-
new self.oc.gp_Pnt_3(-1e4, -1e4, -thickness / 2),
1516-
new self.oc.gp_Pnt_3( 1e4, 1e4, thickness / 2)
1517-
).Shape();
1518-
// Rotate slab to align Z-axis with planeNormal
1519-
if (Math.abs(n[2] - 1.0) > 1e-8) {
1520-
// Need to rotate: find rotation axis (cross product of Z and normal)
1521-
let zAxis = [0, 0, 1];
1522-
let rotAxis = [
1523-
zAxis[1]*n[2] - zAxis[2]*n[1],
1524-
zAxis[2]*n[0] - zAxis[0]*n[2],
1525-
zAxis[0]*n[1] - zAxis[1]*n[0]
1526-
];
1527-
let rotAxisLen = _vecLength(rotAxis);
1528-
if (rotAxisLen > 1e-10) {
1529-
let angle = Math.acos(Math.max(-1, Math.min(1, _dot(zAxis, n))));
1530-
rotAxis = _normalize(rotAxis);
1531-
let ax1 = new self.oc.gp_Ax1_2(
1532-
new self.oc.gp_Pnt_3(0, 0, 0),
1533-
new self.oc.gp_Dir_4(rotAxis[0], rotAxis[1], rotAxis[2])
1534-
);
1535-
let trsf = new self.oc.gp_Trsf_1();
1536-
trsf.SetRotation_1(ax1, angle);
1537-
let brep = new self.oc.BRepBuilderAPI_Transform_2(slab, trsf, true);
1538-
slab = brep.Shape();
1539-
} else if (n[2] < 0) {
1540-
// Normal is [0,0,-1], rotate 180 degrees around X
1541-
let ax1 = new self.oc.gp_Ax1_2(
1542-
new self.oc.gp_Pnt_3(0, 0, 0),
1543-
new self.oc.gp_Dir_4(1, 0, 0)
1544-
);
1545-
let trsf = new self.oc.gp_Trsf_1();
1546-
trsf.SetRotation_1(ax1, Math.PI);
1547-
let brep = new self.oc.BRepBuilderAPI_Transform_2(slab, trsf, true);
1548-
slab = brep.Shape();
1549-
}
1550-
}
1551-
// Translate slab to planeOrigin
1552-
if (planeOrigin[0] !== 0 || planeOrigin[1] !== 0 || planeOrigin[2] !== 0) {
1553-
let trsf = new self.oc.gp_Trsf_1();
1554-
trsf.SetTranslation_1(new self.oc.gp_Vec_4(planeOrigin[0], planeOrigin[1], planeOrigin[2]));
1555-
let brep = new self.oc.BRepBuilderAPI_Transform_2(slab, trsf, true);
1556-
slab = brep.Shape();
1557-
}
1558-
// Intersect
1559-
let common = new self.oc.BRepAlgoAPI_Common_3(shape, slab, new self.oc.Message_ProgressRange_1());
1560-
return common.Shape();
1401+
let origin = new self.oc.gp_Pnt_3(planeOrigin[0], planeOrigin[1], planeOrigin[2]);
1402+
let normal = new self.oc.gp_Dir_4(planeNormal[0], planeNormal[1], planeNormal[2]);
1403+
let plane = new self.oc.gp_Pln_3(origin, normal);
1404+
let section = new self.oc.BRepAlgoAPI_Section_5(shape, plane, false);
1405+
section.Build(new self.oc.Message_ProgressRange_1());
1406+
return section.Shape();
15611407
});
15621408
self.sceneShapes.push(curSection);
15631409
return curSection;

js/MainPage/CascadeMain.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,13 @@ class CascadeStudioApp {
127127
try {
128128
let parsed = JSON.parse(projectContent);
129129
if (parsed._cascadeState) {
130+
// New Dockview project format
130131
codeStr = parsed._cascadeState.code || codeStr;
131132
this.gui.state = parsed._cascadeState.guiState || {};
133+
} else if (parsed.content || parsed.root) {
134+
// Legacy GoldenLayout project format — extract code from componentState
135+
let code = this._extractLegacyCode(parsed);
136+
if (code) { codeStr = code; }
132137
}
133138
} catch (e) {
134139
console.error("Failed to parse project:", e);
@@ -326,6 +331,25 @@ class CascadeStudioApp {
326331
this.console.goldenContainer.setState({});
327332
}
328333

334+
/** Extract code from a legacy GoldenLayout project file. */
335+
_extractLegacyCode(parsed) {
336+
// Recursively search for componentState.code in GoldenLayout config
337+
function findCode(obj) {
338+
if (!obj || typeof obj !== 'object') return null;
339+
if (obj.componentState && obj.componentState.code) return obj.componentState.code;
340+
for (let key of ['content', 'root', 'children']) {
341+
if (Array.isArray(obj[key])) {
342+
for (let child of obj[key]) {
343+
let result = findCode(child);
344+
if (result) return result;
345+
}
346+
}
347+
}
348+
return null;
349+
}
350+
return findCode(parsed);
351+
}
352+
329353
// --- Static utility methods ---
330354

331355
/** Get a new file handle via the File System Access API. */

js/StandardLibraryIntellisense.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ function ChamferEdges(shape: oc.TopoDS_Shape, distance: number, edgeList: number
238238
* and call `.indices()` to get edge indices for FilletEdges/ChamferEdges.
239239
* @example```FilletEdges(box, 3, Edges(box).max([0,0,1]).indices());``` */
240240
class EdgeSelector {
241-
/** Filter to edges of a specific curve type: "Line", "Circle", "Ellipse", "BSpline", "Bezier" */
241+
/** Filter to edges of a specific curve type: "Line", "Circle", "Ellipse", "Hyperbola", "Parabola", "BezierCurve", "BSplineCurve" */
242242
ofType(type: string): EdgeSelector;
243243
/** Filter to edges whose direction is parallel to the given axis vector */
244244
parallel(axis: number[], tolerance?: number): EdgeSelector;
@@ -281,7 +281,7 @@ class EdgeSelector {
281281
* Use `Faces(shape)` to create a FaceSelector, then chain filtering methods.
282282
* @example```let topFace = Faces(box).max([0,0,1]).faces()[0];``` */
283283
class FaceSelector {
284-
/** Filter to faces of a specific surface type: "Plane", "Cylinder", "Cone", "Sphere", "Torus", "BSpline" */
284+
/** Filter to faces of a specific surface type: "Plane", "Cylinder", "Cone", "Sphere", "Torus", "BSplineSurface", "BezierSurface" */
285285
ofType(type: string): FaceSelector;
286286
/** Filter to faces whose normal is parallel to the given axis */
287287
parallel(axis: number[], tolerance?: number): FaceSelector;

scripts/postinstall.js

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,13 @@
11
/**
22
* Postinstall script for CascadeStudio.
3-
* Bundles golden-layout ESM (which has extensionless imports) into a single browser-ready file.
3+
* Bundles library ESM modules into single browser-ready files.
44
*/
55
const { execFileSync } = require('child_process');
66
const fs = require('fs');
77
const path = require('path');
88

99
const root = path.join(__dirname, '..');
1010
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
11-
const glInput = path.join(root, 'node_modules', 'golden-layout', 'dist', 'esm', 'index.js');
12-
const glOutput = path.join(root, 'lib', 'golden-layout', 'golden-layout.js');
13-
14-
// Bundle golden-layout ESM into a single file for buildless browser use
15-
if (fs.existsSync(glInput)) {
16-
const glDir = path.dirname(glOutput);
17-
if (!fs.existsSync(glDir)) { fs.mkdirSync(glDir, { recursive: true }); }
18-
19-
execFileSync(npx, [
20-
'esbuild', glInput,
21-
'--bundle', '--format=esm',
22-
'--outfile=' + glOutput, '--sourcemap'
23-
], { cwd: root, stdio: 'inherit' });
24-
console.log(' Bundled golden-layout ESM to', glOutput);
25-
26-
// Copy CSS files
27-
const cssBase = path.join(root, 'node_modules', 'golden-layout', 'dist', 'css', 'goldenlayout-base.css');
28-
const cssTheme = path.join(root, 'node_modules', 'golden-layout', 'dist', 'css', 'themes', 'goldenlayout-dark-theme.css');
29-
if (fs.existsSync(cssBase)) {
30-
fs.copyFileSync(cssBase, path.join(glDir, 'goldenlayout-base.css'));
31-
}
32-
if (fs.existsSync(cssTheme)) {
33-
fs.copyFileSync(cssTheme, path.join(glDir, 'goldenlayout-dark-theme.css'));
34-
}
35-
console.log(' Copied golden-layout CSS files');
36-
}
3711

3812
// Bundle dockview-core ESM into a single file for buildless browser use
3913
const dvInput = path.join(root, 'node_modules', 'dockview-core', 'dist', 'esm', 'index.js');

0 commit comments

Comments
 (0)