forgecad 0.9.4 → 0.9.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.
Files changed (82) hide show
  1. package/dist/assets/{AdminPage-jwoEgwE_.js → AdminPage-uTtcSXtn.js} +1 -1
  2. package/dist/assets/{BlogPage-Ck7g3ue2.js → BlogPage-DYJMjWx3.js} +1 -1
  3. package/dist/assets/{DocsPage-9WaRC14b.js → DocsPage-C58f0K5v.js} +1 -6
  4. package/dist/assets/{EditorApp-Dja2jMmW.js → EditorApp-DNH1TEz1.js} +282 -62
  5. package/dist/assets/{EmbedViewer-37_PfMwv.js → EmbedViewer-CMXWA2LX.js} +2 -2
  6. package/dist/assets/{LandingPageProofDriven-CO8WL0CY.js → LandingPageProofDriven-CAu2OZFn.js} +1 -1
  7. package/dist/assets/{PricingPage-DADKGuOa.js → PricingPage-BIgW7m3X.js} +1 -1
  8. package/dist/assets/{SettingsPage-DKKI4W49.js → SettingsPage-N1l1tMXO.js} +1 -1
  9. package/dist/assets/{app-CwI02pTA.js → app-CFy7g5WP.js} +74 -12
  10. package/dist/assets/cli/{render-Kw5hLEcL.js → render-BrVVdj_T.js} +453 -41
  11. package/dist/assets/{evalWorker-D6ub3kfS.js → evalWorker-c_SB9gg3.js} +2057 -446
  12. package/dist/assets/{manifold-lru0jwVw.js → manifold-CRoBhJKH.js} +2 -2
  13. package/dist/assets/{manifold-CwDdMKyc.js → manifold-Cjk7WhRs.js} +1 -1
  14. package/dist/assets/{manifold-DTvmxSDf.js → manifold-Dp6pvFr6.js} +1 -1
  15. package/dist/assets/{renderSceneState-tvtNKNRi.js → renderSceneState-3DfsSASX.js} +1 -1
  16. package/dist/assets/{reportWorker-DeqktDGt.js → reportWorker-BLkuIoS8.js} +2052 -443
  17. package/dist/assets/{sectionPlaneMath-C8N0w8o3.js → sectionPlaneMath-CykEnkvQ.js} +2258 -518
  18. package/dist/cli/render.html +1 -1
  19. package/dist/docs/index.html +2 -2
  20. package/dist/docs-raw/AI/usage.md +0 -1
  21. package/dist/docs-raw/API/core/concepts.md +11 -1
  22. package/dist/docs-raw/CLI.md +64 -13
  23. package/dist/docs-raw/generated/assembly.md +8 -3
  24. package/dist/docs-raw/generated/concepts.md +44 -41
  25. package/dist/docs-raw/generated/core.md +97 -47
  26. package/dist/docs-raw/generated/curves.md +6 -580
  27. package/dist/docs-raw/generated/lib.md +40 -3
  28. package/dist/docs-raw/generated/output.md +6 -1
  29. package/dist/docs-raw/generated/sdf.md +50 -4
  30. package/dist/docs-raw/generated/viewport.md +1 -9
  31. package/dist/docs-raw/guides/inspection-bundles.md +31 -6
  32. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -0
  33. package/dist/docs-raw/skills/forgecad-image-replicator.md +3 -1
  34. package/dist/docs-raw/skills/forgecad-make-a-model.md +48 -4
  35. package/dist/docs-raw/skills/forgecad-render-inspect.md +3 -1
  36. package/dist/docs-raw/skills/forgecad-visual-spec.md +2 -0
  37. package/dist/docs-raw/skills/forgecad.md +2 -1
  38. package/dist/docs-raw/skills/index.md +0 -1
  39. package/dist/index.html +1 -1
  40. package/dist/sitemap.xml +6 -6
  41. package/dist-cli/blender/render.py +43 -8
  42. package/dist-cli/forgecad.js +4941 -1758
  43. package/dist-cli/forgecad.js.map +1 -1
  44. package/dist-skill/CONTEXT.md +255 -656
  45. package/dist-skill/SKILL-dev.md +2 -1
  46. package/dist-skill/SKILL.md +2 -1
  47. package/dist-skill/docs/API/core/concepts.md +11 -1
  48. package/dist-skill/docs/CLI.md +64 -13
  49. package/dist-skill/docs/generated/assembly.md +8 -3
  50. package/dist-skill/docs/generated/core.md +97 -47
  51. package/dist-skill/docs/generated/curves.md +6 -580
  52. package/dist-skill/docs/generated/lib.md +40 -3
  53. package/dist-skill/docs/generated/output.md +6 -1
  54. package/dist-skill/docs/generated/sdf.md +50 -4
  55. package/dist-skill/docs/generated/viewport.md +1 -9
  56. package/dist-skill/docs/guides/inspection-bundles.md +31 -6
  57. package/dist-skill/docs-dev/API/core/concepts.md +11 -1
  58. package/dist-skill/docs-dev/CLI.md +64 -13
  59. package/dist-skill/docs-dev/generated/assembly.md +8 -3
  60. package/dist-skill/docs-dev/generated/core.md +97 -47
  61. package/dist-skill/docs-dev/generated/curves.md +6 -580
  62. package/dist-skill/docs-dev/generated/lib.md +40 -3
  63. package/dist-skill/docs-dev/generated/output.md +6 -1
  64. package/dist-skill/docs-dev/generated/sdf.md +50 -4
  65. package/dist-skill/docs-dev/generated/viewport.md +1 -9
  66. package/dist-skill/docs-dev/guides/inspection-bundles.md +31 -6
  67. package/dist-skill/library/README.md +0 -1
  68. package/dist-skill/library/forgecad-blockout-model/SKILL.md +1 -0
  69. package/dist-skill/library/forgecad-image-replicator/SKILL.md +3 -1
  70. package/dist-skill/library/forgecad-make-a-model/SKILL.md +48 -4
  71. package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
  72. package/dist-skill/library/forgecad-visual-spec/SKILL.md +2 -0
  73. package/examples/api/drive-wheel-regions.forge.js +43 -0
  74. package/examples/api/sdf-circular-array-knurling.forge.js +19 -0
  75. package/examples/api/sdf-pattern2d-ceramic-ripple-set.forge.js +83 -0
  76. package/examples/api/sdf-pattern2d-grip-tread.forge.js +72 -0
  77. package/examples/api/sdf-pattern2d-orbital-jewelry.forge.js +62 -0
  78. package/examples/api/sdf-surface-basket-weave.forge.js +67 -0
  79. package/examples/api/sector-gear-body.forge.js +34 -0
  80. package/package.json +1 -1
  81. package/dist/docs-raw/skills/forgecad-api-dogfood.md +0 -130
  82. package/dist-skill/library/forgecad-api-dogfood/SKILL.md +0 -125
@@ -1,8 +1,8 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { D as DoubleSide, bH as initSolverWasm, bG as initKernel, S as Scene, bI as BoxGeometry, bd as MeshStandardMaterial, a4 as BackSide, b0 as PointLight, M as Mesh, aa as MeshBasicMaterial, bJ as localAabbPlaneRelation, h as Vector2, bK as ShapeUtils, g as Vector3, e as Color, aC as resolveForgeRenderStyle, b9 as getRenderStylePreset, ax as setParamOverrides, b6 as runScript, a0 as MathUtils, G as Box3, bL as Group, b3 as shapeToGeometry, b7 as MeshPhysicalMaterial, ba as AdditiveBlending, aH as LineBasicMaterial, b8 as LineSegments, aG as BufferGeometry, P as PerspectiveCamera, k as ShaderMaterial, bM as intersectWithPlane, W as WebGLRenderer, A as ACESFilmicToneMapping, c as SRGBColorSpace, bN as parseCameraCliSpec, bO as PMREMGenerator, aV as CanvasTexture, aW as Object3D, aX as FogExp2, aY as Fog, aZ as AmbientLight, b1 as DirectionalLight, a_ as HemisphereLight, bz as findJointAnimationClip, p as Plane, Y as Vector4, $ as Matrix4, bg as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bf as buildSdfRaymarchFragmentShader, O as OrthographicCamera, bA as resolveJointAnimation, bB as resolveJointViewValues, R as Raycaster, aU as BufferAttribute, bP as worldAuthorPlaneToLocal, a$ as SpotLight } from "../sectionPlaneMath-C8N0w8o3.js";
5
- import { m as mergeViewportRenderSceneStates, p as parseRenderSceneCliSpec } from "../renderSceneState-tvtNKNRi.js";
4
+ import { D as DoubleSide, bH as initSolverWasm, bG as initKernel, S as Scene, bI as BoxGeometry, bd as MeshStandardMaterial, a4 as BackSide, b0 as PointLight, M as Mesh, aa as MeshBasicMaterial, bJ as localAabbPlaneRelation, h as Vector2, bK as ShapeUtils, a0 as MathUtils, g as Vector3, G as Box3, aU as BufferAttribute, e as Color, aC as resolveForgeRenderStyle, b9 as getRenderStylePreset, ax as setParamOverrides, b6 as runScript, bL as Group, b3 as shapeToGeometry, b7 as MeshPhysicalMaterial, ba as AdditiveBlending, aH as LineBasicMaterial, b8 as LineSegments, aG as BufferGeometry, P as PerspectiveCamera, k as ShaderMaterial, bM as intersectWithPlane, W as WebGLRenderer, A as ACESFilmicToneMapping, c as SRGBColorSpace, bN as parseCameraCliSpec, bO as PMREMGenerator, aV as CanvasTexture, aW as Object3D, aX as FogExp2, aY as Fog, aZ as AmbientLight, b1 as DirectionalLight, a_ as HemisphereLight, bz as findJointAnimationClip, p as Plane, Y as Vector4, $ as Matrix4, bg as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bf as buildSdfRaymarchFragmentShader, O as OrthographicCamera, bA as resolveJointAnimation, bB as resolveJointViewValues, R as Raycaster, bP as worldAuthorPlaneToLocal, a$ as SpotLight } from "../sectionPlaneMath-CykEnkvQ.js";
5
+ import { m as mergeViewportRenderSceneStates, p as parseRenderSceneCliSpec } from "../renderSceneState-3DfsSASX.js";
6
6
  const CAD_MATERIAL_PROPS = {
7
7
  color: 6003669,
8
8
  metalness: 0.05,
@@ -269,17 +269,17 @@ function stitchLoops(points, edges) {
269
269
  const warnings = [];
270
270
  const adjacency = /* @__PURE__ */ new Map();
271
271
  const unusedEdges = /* @__PURE__ */ new Set();
272
- const edgeKey = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
272
+ const edgeKey2 = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
273
273
  for (const [a, b] of edges) {
274
274
  if (!adjacency.has(a)) adjacency.set(a, []);
275
275
  if (!adjacency.has(b)) adjacency.set(b, []);
276
276
  (_a = adjacency.get(a)) == null ? void 0 : _a.push(b);
277
277
  (_b = adjacency.get(b)) == null ? void 0 : _b.push(a);
278
- unusedEdges.add(edgeKey(a, b));
278
+ unusedEdges.add(edgeKey2(a, b));
279
279
  }
280
280
  const loops = [];
281
281
  for (const [edgeA, edgeB] of edges) {
282
- const firstKey = edgeKey(edgeA, edgeB);
282
+ const firstKey = edgeKey2(edgeA, edgeB);
283
283
  if (!unusedEdges.has(firstKey)) continue;
284
284
  const loop = [edgeA, edgeB];
285
285
  unusedEdges.delete(firstKey);
@@ -288,12 +288,12 @@ function stitchLoops(points, edges) {
288
288
  let closed = false;
289
289
  for (let guard = 0; guard < points.length + edges.length + 8; guard += 1) {
290
290
  const neighbors = adjacency.get(current) ?? [];
291
- const next = neighbors.find((candidate) => candidate !== previous && unusedEdges.has(edgeKey(current, candidate)));
291
+ const next = neighbors.find((candidate) => candidate !== previous && unusedEdges.has(edgeKey2(current, candidate)));
292
292
  if (next === void 0) {
293
293
  if (current === edgeA) closed = true;
294
294
  break;
295
295
  }
296
- unusedEdges.delete(edgeKey(current, next));
296
+ unusedEdges.delete(edgeKey2(current, next));
297
297
  if (next === edgeA) {
298
298
  closed = true;
299
299
  break;
@@ -1069,6 +1069,38 @@ function analyzeDistanceInspection(entries, rawOptions = {}) {
1069
1069
  warnings: [...connectivity.warnings]
1070
1070
  };
1071
1071
  }
1072
+ const CAMERA_TOKEN_DIRECTIONS = {
1073
+ front: [0, -1, 0.2],
1074
+ back: [0, 1, 0.2],
1075
+ side: [1, 0, 0.2],
1076
+ right: [1, 0, 0.2],
1077
+ top: [0, -0.01, 1],
1078
+ iso: [0.6, -0.6, 0.4]
1079
+ };
1080
+ function normalizeCameraDirection(dir) {
1081
+ const len = Math.sqrt(dir[0] ** 2 + dir[1] ** 2 + dir[2] ** 2) || 1;
1082
+ return [dir[0] / len, dir[1] / len, dir[2] / len];
1083
+ }
1084
+ function sphericalToCameraDirection(azimuthDeg, elevationDeg) {
1085
+ const az = azimuthDeg * Math.PI / 180;
1086
+ const el = elevationDeg * Math.PI / 180;
1087
+ const cosEl = Math.cos(el);
1088
+ return [cosEl * Math.sin(az), -cosEl * Math.cos(az), Math.sin(el)];
1089
+ }
1090
+ function parseCameraToken(token) {
1091
+ const normalized = token.trim();
1092
+ const preset = CAMERA_TOKEN_DIRECTIONS[normalized];
1093
+ if (preset) return { label: normalized, dir: preset };
1094
+ const parts = normalized.split(":").map((s) => Number.parseFloat(s));
1095
+ if (parts.length >= 2 && parts.length <= 3 && parts.every((n) => Number.isFinite(n))) {
1096
+ const dir = sphericalToCameraDirection(parts[0], parts[1]);
1097
+ const label = `az${parts[0]}_el${parts[1]}`;
1098
+ return { label, dir, distance: parts[2] };
1099
+ }
1100
+ throw new Error(
1101
+ `Unknown camera "${token}". Use a preset (front, back, side, right, top, iso) or azimuth:elevation in degrees (e.g. 45:30).`
1102
+ );
1103
+ }
1072
1104
  function formatAvailableViews(views) {
1073
1105
  const names = Object.keys(views ?? {}).sort();
1074
1106
  if (names.length === 0) {
@@ -1302,6 +1334,244 @@ function summarizeThicknessSamples(samples, options) {
1302
1334
  unresolvedAreaPercent: percent(unresolvedArea, totalArea)
1303
1335
  };
1304
1336
  }
1337
+ const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
1338
+ smoothAngleDeg: 5,
1339
+ sharpAngleDeg: 30,
1340
+ harshAngleDeg: 90
1341
+ };
1342
+ const ROUGHNESS_COLORS = {
1343
+ smooth: [62, 72, 84],
1344
+ moderate: [255, 214, 0],
1345
+ sharp: [255, 124, 34],
1346
+ harsh: [255, 42, 96]
1347
+ };
1348
+ function resolveRoughnessInspectionOptions(raw = {}) {
1349
+ const options = {
1350
+ smoothAngleDeg: raw.smoothAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.smoothAngleDeg,
1351
+ sharpAngleDeg: raw.sharpAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.sharpAngleDeg,
1352
+ harshAngleDeg: raw.harshAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.harshAngleDeg
1353
+ };
1354
+ if (!Number.isFinite(options.smoothAngleDeg) || options.smoothAngleDeg < 0) {
1355
+ throw new Error(`smoothAngleDeg must be a finite non-negative angle (got ${options.smoothAngleDeg}).`);
1356
+ }
1357
+ if (!Number.isFinite(options.sharpAngleDeg) || options.sharpAngleDeg <= options.smoothAngleDeg) {
1358
+ throw new Error(`sharpAngleDeg must be greater than smoothAngleDeg (got ${options.sharpAngleDeg}).`);
1359
+ }
1360
+ if (!Number.isFinite(options.harshAngleDeg) || options.harshAngleDeg <= options.sharpAngleDeg || options.harshAngleDeg > 180) {
1361
+ throw new Error(`harshAngleDeg must be greater than sharpAngleDeg and <= 180 (got ${options.harshAngleDeg}).`);
1362
+ }
1363
+ return options;
1364
+ }
1365
+ function roughnessClassForAngle(angleDeg, options) {
1366
+ if (angleDeg >= options.harshAngleDeg) return "harsh";
1367
+ if (angleDeg >= options.sharpAngleDeg) return "sharp";
1368
+ if (angleDeg >= options.smoothAngleDeg) return "moderate";
1369
+ return "smooth";
1370
+ }
1371
+ function roughnessScoreForAngle(angleDeg, options) {
1372
+ if (angleDeg < options.sharpAngleDeg) return 0;
1373
+ if (angleDeg < options.harshAngleDeg) {
1374
+ return MathUtils.lerp(0.48, 0.82, (angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg));
1375
+ }
1376
+ return 1;
1377
+ }
1378
+ function roughnessColorForAngle(angleDeg, options) {
1379
+ const cls = roughnessClassForAngle(angleDeg, options);
1380
+ if (cls === "smooth" || cls === "harsh") return ROUGHNESS_COLORS[cls];
1381
+ if (cls === "moderate") {
1382
+ return lerpRgb(
1383
+ ROUGHNESS_COLORS.moderate,
1384
+ ROUGHNESS_COLORS.sharp,
1385
+ (angleDeg - options.smoothAngleDeg) / (options.sharpAngleDeg - options.smoothAngleDeg)
1386
+ );
1387
+ }
1388
+ return lerpRgb(
1389
+ ROUGHNESS_COLORS.sharp,
1390
+ ROUGHNESS_COLORS.harsh,
1391
+ (angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg)
1392
+ );
1393
+ }
1394
+ function lerpRgb(a, b, t) {
1395
+ const clamped = MathUtils.clamp(t, 0, 1);
1396
+ return [
1397
+ Math.round(MathUtils.lerp(a[0], b[0], clamped)),
1398
+ Math.round(MathUtils.lerp(a[1], b[1], clamped)),
1399
+ Math.round(MathUtils.lerp(a[2], b[2], clamped))
1400
+ ];
1401
+ }
1402
+ function emptyRoughnessSummary() {
1403
+ return {
1404
+ triangleCount: 0,
1405
+ edgeCount: 0,
1406
+ boundaryEdgeCount: 0,
1407
+ nonManifoldEdgeCount: 0,
1408
+ smoothAreaPercent: 0,
1409
+ moderateAreaPercent: 0,
1410
+ sharpAreaPercent: 0,
1411
+ harshAreaPercent: 0,
1412
+ roughAreaPercent: 0,
1413
+ meanAngleDeg: null,
1414
+ p50AngleDeg: null,
1415
+ p90AngleDeg: null,
1416
+ p95AngleDeg: null,
1417
+ p99AngleDeg: null,
1418
+ maxAngleDeg: null,
1419
+ qualityScore: 0
1420
+ };
1421
+ }
1422
+ function summarizeRoughnessTriangles(triangles, edgeAngles, edgeCount, boundaryEdgeCount, nonManifoldEdgeCount, options) {
1423
+ const areaByClass = {
1424
+ smooth: 0,
1425
+ moderate: 0,
1426
+ sharp: 0,
1427
+ harsh: 0
1428
+ };
1429
+ let totalArea = 0;
1430
+ for (const tri of triangles) {
1431
+ const area = Number.isFinite(tri.area) ? tri.area : 0;
1432
+ totalArea += area;
1433
+ areaByClass[roughnessClassForAngle(tri.maxAngleDeg, options)] += area;
1434
+ }
1435
+ const sortedAngles = [...edgeAngles].sort((lhs, rhs) => lhs - rhs);
1436
+ const meanAngleDeg = sortedAngles.length > 0 ? Number((sortedAngles.reduce((sum, angle) => sum + angle, 0) / sortedAngles.length).toFixed(2)) : null;
1437
+ const qualityScore = totalArea > 0 ? Math.round(
1438
+ MathUtils.clamp(
1439
+ 100 * (areaByClass.smooth + areaByClass.moderate * 0.9) / totalArea - 50 * areaByClass.harsh / totalArea,
1440
+ 0,
1441
+ 100
1442
+ )
1443
+ ) : 0;
1444
+ const safePercent = (value) => totalArea > 0 ? Number((value / totalArea * 100).toFixed(2)) : 0;
1445
+ return {
1446
+ triangleCount: triangles.length,
1447
+ edgeCount,
1448
+ boundaryEdgeCount,
1449
+ nonManifoldEdgeCount,
1450
+ smoothAreaPercent: safePercent(areaByClass.smooth),
1451
+ moderateAreaPercent: safePercent(areaByClass.moderate),
1452
+ sharpAreaPercent: safePercent(areaByClass.sharp),
1453
+ harshAreaPercent: safePercent(areaByClass.harsh),
1454
+ roughAreaPercent: safePercent(areaByClass.sharp + areaByClass.harsh),
1455
+ meanAngleDeg,
1456
+ p50AngleDeg: percentile(sortedAngles, 0.5),
1457
+ p90AngleDeg: percentile(sortedAngles, 0.9),
1458
+ p95AngleDeg: percentile(sortedAngles, 0.95),
1459
+ p99AngleDeg: percentile(sortedAngles, 0.99),
1460
+ maxAngleDeg: sortedAngles.length > 0 ? Number(sortedAngles[sortedAngles.length - 1].toFixed(2)) : null,
1461
+ qualityScore
1462
+ };
1463
+ }
1464
+ function percentile(sorted, q) {
1465
+ if (sorted.length === 0) return null;
1466
+ const index = MathUtils.clamp(Math.floor(sorted.length * q), 0, sorted.length - 1);
1467
+ return Number(sorted[index].toFixed(2));
1468
+ }
1469
+ const DEG_PER_RAD = 180 / Math.PI;
1470
+ function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
1471
+ const options = resolveRoughnessInspectionOptions(rawOptions);
1472
+ const geometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry.clone();
1473
+ const position = geometry.getAttribute("position");
1474
+ const warnings = [];
1475
+ if (!position || position.count < 3) {
1476
+ return { geometry, summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
1477
+ }
1478
+ const triangleCount = Math.floor(position.count / 3);
1479
+ const normals = new Array(triangleCount);
1480
+ const triangles = new Array(triangleCount);
1481
+ const edges = /* @__PURE__ */ new Map();
1482
+ const colors = new Float32Array(position.count * 3);
1483
+ const scores = new Float32Array(position.count);
1484
+ const a = new Vector3();
1485
+ const b = new Vector3();
1486
+ const c = new Vector3();
1487
+ const ac = new Vector3();
1488
+ const normal = new Vector3();
1489
+ const bbox = new Box3().setFromBufferAttribute(position);
1490
+ const snap = Math.max(1e-6, bbox.getSize(new Vector3()).length() * 1e-8);
1491
+ for (let tri = 0; tri < triangleCount; tri += 1) {
1492
+ const offset = tri * 3;
1493
+ readVertex(position, offset, a);
1494
+ readVertex(position, offset + 1, b);
1495
+ readVertex(position, offset + 2, c);
1496
+ normal.subVectors(b, a).cross(ac.subVectors(c, a));
1497
+ const areaTwice = normal.length();
1498
+ triangles[tri] = { area: areaTwice * 0.5, maxAngleDeg: 0 };
1499
+ normals[tri] = areaTwice > 1e-12 ? normal.multiplyScalar(1 / areaTwice).clone() : new Vector3(0, 0, 1);
1500
+ const keys = [vertexKey(a, snap), vertexKey(b, snap), vertexKey(c, snap)];
1501
+ for (let edge = 0; edge < 3; edge += 1) {
1502
+ const key = edgeKey(keys[edge], keys[(edge + 1) % 3]);
1503
+ let record = edges.get(key);
1504
+ if (!record) {
1505
+ record = { triangles: [] };
1506
+ edges.set(key, record);
1507
+ }
1508
+ record.triangles.push(tri);
1509
+ }
1510
+ }
1511
+ const { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles } = markTriangleRoughness(edges, triangles, normals);
1512
+ if (boundaryEdgeCount > 0) warnings.push(`${boundaryEdgeCount} boundary edge(s) were treated as harsh roughness.`);
1513
+ if (nonManifoldEdgeCount > 0) warnings.push(`${nonManifoldEdgeCount} non-manifold edge(s) were treated as harsh roughness.`);
1514
+ for (let tri = 0; tri < triangleCount; tri += 1) {
1515
+ const { maxAngleDeg } = triangles[tri];
1516
+ const color = roughnessColorForAngle(maxAngleDeg, options);
1517
+ const score = roughnessScoreForAngle(maxAngleDeg, options);
1518
+ const offset = tri * 3;
1519
+ for (let vertex = 0; vertex < 3; vertex += 1) {
1520
+ const colorOffset = (offset + vertex) * 3;
1521
+ colors[colorOffset] = color[0] / 255;
1522
+ colors[colorOffset + 1] = color[1] / 255;
1523
+ colors[colorOffset + 2] = color[2] / 255;
1524
+ scores[offset + vertex] = score;
1525
+ }
1526
+ }
1527
+ geometry.setAttribute("color", new BufferAttribute(colors, 3));
1528
+ geometry.setAttribute("roughnessScore", new BufferAttribute(scores, 1));
1529
+ geometry.computeBoundingBox();
1530
+ return {
1531
+ geometry,
1532
+ summary: summarizeRoughnessTriangles(triangles, edgeAngles, edges.size, boundaryEdgeCount, nonManifoldEdgeCount, options),
1533
+ warnings
1534
+ };
1535
+ }
1536
+ function markTriangleRoughness(edges, triangles, normals) {
1537
+ const edgeAngles = [];
1538
+ let boundaryEdgeCount = 0;
1539
+ let nonManifoldEdgeCount = 0;
1540
+ for (const edge of edges.values()) {
1541
+ if (edge.triangles.length === 1) {
1542
+ boundaryEdgeCount += 1;
1543
+ markTriangles(edge.triangles, triangles, 180);
1544
+ edgeAngles.push(180);
1545
+ continue;
1546
+ }
1547
+ if (edge.triangles.length > 2) {
1548
+ nonManifoldEdgeCount += 1;
1549
+ markTriangles(edge.triangles, triangles, 180);
1550
+ edgeAngles.push(180);
1551
+ continue;
1552
+ }
1553
+ const [first, second] = edge.triangles;
1554
+ const dot = MathUtils.clamp(normals[first].dot(normals[second]), -1, 1);
1555
+ const angleDeg = Math.acos(dot) * DEG_PER_RAD;
1556
+ markTriangles(edge.triangles, triangles, angleDeg);
1557
+ edgeAngles.push(angleDeg);
1558
+ }
1559
+ return { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles };
1560
+ }
1561
+ function readVertex(position, index, target) {
1562
+ target.set(position.getX(index), position.getY(index), position.getZ(index));
1563
+ }
1564
+ function vertexKey(point, snap) {
1565
+ return `${Math.round(point.x / snap)},${Math.round(point.y / snap)},${Math.round(point.z / snap)}`;
1566
+ }
1567
+ function edgeKey(a, b) {
1568
+ return a < b ? `${a}|${b}` : `${b}|${a}`;
1569
+ }
1570
+ function markTriangles(indices, triangles, angleDeg) {
1571
+ for (const index of indices) {
1572
+ triangles[index].maxAngleDeg = Math.max(triangles[index].maxAngleDeg, angleDeg);
1573
+ }
1574
+ }
1305
1575
  const canvas = document.getElementById("canvas");
1306
1576
  const exportCanvas = document.createElement("canvas");
1307
1577
  const exportCtx = exportCanvas.getContext("2d");
@@ -1569,39 +1839,8 @@ function updateSdfRaymarchUniforms(session) {
1569
1839
  sdf.material.uniforms.uIsOrthographic.value = isOrthographic;
1570
1840
  }
1571
1841
  }
1572
- const ANGLE_DIRS = {
1573
- front: [0, -1, 0.2],
1574
- back: [0, 1, 0.2],
1575
- side: [1, 0, 0.2],
1576
- right: [1, 0, 0.2],
1577
- top: [0, -0.01, 1],
1578
- iso: [0.6, -0.6, 0.4]
1579
- };
1580
- function normalizeVector(dir) {
1581
- const len = Math.sqrt(dir[0] ** 2 + dir[1] ** 2 + dir[2] ** 2) || 1;
1582
- return [dir[0] / len, dir[1] / len, dir[2] / len];
1583
- }
1584
- function sphericalToDir(azimuthDeg, elevationDeg) {
1585
- const az = azimuthDeg * Math.PI / 180;
1586
- const el = elevationDeg * Math.PI / 180;
1587
- const cosEl = Math.cos(el);
1588
- return [cosEl * Math.sin(az), -cosEl * Math.cos(az), Math.sin(el)];
1589
- }
1590
- function parseCameraToken(token) {
1591
- const preset = ANGLE_DIRS[token];
1592
- if (preset) return { label: token, dir: preset };
1593
- const parts = token.split(":").map((s) => Number.parseFloat(s));
1594
- if (parts.length >= 2 && parts.length <= 3 && parts.every((n) => Number.isFinite(n))) {
1595
- const dir = sphericalToDir(parts[0], parts[1]);
1596
- const label = `az${parts[0]}_el${parts[1]}`;
1597
- return { label, dir, distance: parts[2] };
1598
- }
1599
- throw new Error(
1600
- `Unknown camera "${token}". Use a preset (front, back, side, right, top, iso) or azimuth:elevation in degrees (e.g. 45:30).`
1601
- );
1602
- }
1603
1842
  function applyDirectionCamera(session, dir, distanceOverride) {
1604
- const d = normalizeVector(dir);
1843
+ const d = normalizeCameraDirection(dir);
1605
1844
  const dist = distanceOverride ?? session.distance;
1606
1845
  session.camera.position.set(session.center.x + d[0] * dist, session.center.y + d[1] * dist, session.center.z + d[2] * dist);
1607
1846
  session.camera.up.set(0, 0, 1);
@@ -2274,6 +2513,130 @@ function renderCurrentThickness(session, rawOptions) {
2274
2513
  material.dispose();
2275
2514
  }
2276
2515
  }
2516
+ const ROUGHNESS_SMOOTH_OPACITY = 0.16;
2517
+ const ROUGHNESS_HARSH_OPACITY = 1;
2518
+ function createRoughnessMaterial(clippingPlanes) {
2519
+ const material = new ShaderMaterial({
2520
+ transparent: true,
2521
+ depthTest: true,
2522
+ depthWrite: true,
2523
+ clippingPlanes: clippingPlanes ?? void 0,
2524
+ uniforms: {
2525
+ smoothOpacity: { value: ROUGHNESS_SMOOTH_OPACITY },
2526
+ harshOpacity: { value: ROUGHNESS_HARSH_OPACITY }
2527
+ },
2528
+ vertexShader: `
2529
+ attribute vec3 color;
2530
+ attribute float roughnessScore;
2531
+ varying vec3 vColor;
2532
+ varying float vRoughnessScore;
2533
+ varying vec3 vViewNormal;
2534
+
2535
+ void main() {
2536
+ vColor = color;
2537
+ vRoughnessScore = roughnessScore;
2538
+ vViewNormal = normalize(normalMatrix * normal);
2539
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
2540
+ }
2541
+ `,
2542
+ fragmentShader: `
2543
+ precision highp float;
2544
+ uniform float smoothOpacity;
2545
+ uniform float harshOpacity;
2546
+ varying vec3 vColor;
2547
+ varying float vRoughnessScore;
2548
+ varying vec3 vViewNormal;
2549
+
2550
+ void main() {
2551
+ vec3 shadowLight = normalize(vec3(0.32, 0.42, 0.84));
2552
+ float shade = 0.34 + 0.48 * max(dot(normalize(vViewNormal), shadowLight), 0.0);
2553
+ vec3 smoothShadow = vec3(0.26, 0.30, 0.35) * shade;
2554
+ float colorMix = smoothstep(0.04, 0.35, vRoughnessScore);
2555
+ vec3 finalColor = mix(smoothShadow, vColor, colorMix);
2556
+ float alpha = mix(smoothOpacity, harshOpacity, smoothstep(0.02, 0.55, vRoughnessScore));
2557
+ gl_FragColor = vec4(finalColor, alpha);
2558
+ }
2559
+ `
2560
+ });
2561
+ material.toneMapped = false;
2562
+ return material;
2563
+ }
2564
+ function renderCurrentRoughness(session, rawOptions) {
2565
+ const r = getRenderer(session.size, session.pixelRatio);
2566
+ const options = resolveRoughnessInspectionOptions(rawOptions);
2567
+ const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
2568
+ const warnings = [];
2569
+ const objects = [];
2570
+ const replacements = session.renderables.map((renderable, index) => {
2571
+ const previousMaterial = renderable.solid.material;
2572
+ const previousGeometry = renderable.solid.geometry;
2573
+ if (renderable.sdfRaymarch) {
2574
+ const material2 = new MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false });
2575
+ material2.toneMapped = false;
2576
+ renderable.solid.material = material2;
2577
+ warnings.push(`${renderable.name}: SDF raymarch objects are not included in mesh roughness inspection.`);
2578
+ return { renderable, previousMaterial, previousGeometry, material: material2, geometry: null };
2579
+ }
2580
+ const analysis = analyzeRoughnessGeometry(previousGeometry, options);
2581
+ const bbox = analysis.geometry.boundingBox;
2582
+ const sourceObject = byId.get(renderable.id);
2583
+ if (analysis.warnings.length > 0) {
2584
+ analysis.warnings.forEach((warning) => warnings.push(`${renderable.name}: ${warning}`));
2585
+ }
2586
+ objects.push({
2587
+ index: index + 1,
2588
+ id: renderable.id,
2589
+ name: renderable.name,
2590
+ groupName: renderable.groupName,
2591
+ treePath: sourceObject == null ? void 0 : sourceObject.treePath,
2592
+ mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
2593
+ bbox: {
2594
+ min: bbox ? [bbox.min.x, bbox.min.y, bbox.min.z] : [0, 0, 0],
2595
+ max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
2596
+ },
2597
+ ...analysis.summary
2598
+ });
2599
+ const material = createRoughnessMaterial(renderable.solidMaterial.clippingPlanes);
2600
+ renderable.solid.geometry = analysis.geometry;
2601
+ renderable.solid.material = material;
2602
+ return { renderable, previousMaterial, previousGeometry, material, geometry: analysis.geometry };
2603
+ });
2604
+ try {
2605
+ const png = withSolidOnlyVisibility(
2606
+ session,
2607
+ () => withTemporarySceneBackground(session, new Color(0), () => {
2608
+ updateSdfRaymarchUniforms(session);
2609
+ r.render(session.scene, session.camera);
2610
+ return captureRenderedPng(session.size);
2611
+ })
2612
+ );
2613
+ return {
2614
+ png,
2615
+ report: {
2616
+ method: "mesh-dihedral-angle",
2617
+ options,
2618
+ objectCount: objects.length,
2619
+ objects,
2620
+ warnings,
2621
+ style: {
2622
+ smoothColor: ROUGHNESS_COLORS.smooth,
2623
+ moderateColor: ROUGHNESS_COLORS.moderate,
2624
+ sharpColor: ROUGHNESS_COLORS.sharp,
2625
+ harshColor: ROUGHNESS_COLORS.harsh,
2626
+ smoothOpacity: ROUGHNESS_SMOOTH_OPACITY,
2627
+ harshOpacity: ROUGHNESS_HARSH_OPACITY
2628
+ }
2629
+ }
2630
+ };
2631
+ } finally {
2632
+ replacements.forEach(({ renderable, previousMaterial, previousGeometry, material, geometry }) => {
2633
+ renderable.solid.geometry = previousGeometry;
2634
+ renderable.solid.material = previousMaterial;
2635
+ geometry == null ? void 0 : geometry.dispose();
2636
+ material.dispose();
2637
+ });
2638
+ }
2639
+ }
2277
2640
  function emptySectionSvg() {
2278
2641
  return {
2279
2642
  svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 2 2" width="400" height="400"><rect x="-1" y="-1" width="2" height="2" fill="#2a2a2a"/></svg>',
@@ -2756,7 +3119,7 @@ function applyOrbitPose(session, turn, pitchDeg) {
2756
3119
  const pitch = MathUtils.degToRad(clampedPitch);
2757
3120
  const cosPitch = Math.cos(pitch);
2758
3121
  const dir = [Math.sin(yaw) * cosPitch, -Math.cos(yaw) * cosPitch, Math.sin(pitch)];
2759
- const d = normalizeVector(dir);
3122
+ const d = normalizeCameraDirection(dir);
2760
3123
  session.camera.position.set(
2761
3124
  session.orbitTarget.x + d[0] * session.orbitRadius,
2762
3125
  session.orbitTarget.y + d[1] * session.orbitRadius,
@@ -3190,6 +3553,12 @@ function createSession(code, opts) {
3190
3553
  const scene = new Scene();
3191
3554
  const sceneConfig = result.sceneConfig ?? null;
3192
3555
  try {
3556
+ if ((opts == null ? void 0 : opts.viewName) && (opts == null ? void 0 : opts.cameraToken)) {
3557
+ return {
3558
+ ok: false,
3559
+ error: "Cannot use --view with --camera. Choose either a model-declared view or an explicit camera."
3560
+ };
3561
+ }
3193
3562
  if (opts == null ? void 0 : opts.viewName) {
3194
3563
  if (requestedSceneState == null ? void 0 : requestedSceneState.camera) {
3195
3564
  return {
@@ -3227,6 +3596,34 @@ function createSession(code, opts) {
3227
3596
  const fov = 45;
3228
3597
  const distance = maxDim / (2 * Math.tan(fov * Math.PI / 360)) * 1.6;
3229
3598
  const cameraFov = ((_c = requestedSceneState == null ? void 0 : requestedSceneState.camera) == null ? void 0 : _c.fov) ?? ((_d = sceneConfig == null ? void 0 : sceneConfig.camera) == null ? void 0 : _d.fov) ?? fov;
3599
+ try {
3600
+ if (opts == null ? void 0 : opts.cameraToken) {
3601
+ if (requestedSceneState == null ? void 0 : requestedSceneState.camera) {
3602
+ return {
3603
+ ok: false,
3604
+ error: "Cannot use camera presets/angles with an explicit render camera. Remove --camera-json or the camera field from --scene."
3605
+ };
3606
+ }
3607
+ const parsed = parseCameraToken(opts.cameraToken);
3608
+ const dir = normalizeCameraDirection(parsed.dir);
3609
+ const tokenDistance = parsed.distance ?? distance;
3610
+ requestedSceneState = mergeViewportRenderSceneStates(requestedSceneState, {
3611
+ camera: {
3612
+ projectionMode: "perspective",
3613
+ position: [
3614
+ center.x + dir[0] * tokenDistance,
3615
+ center.y + dir[1] * tokenDistance,
3616
+ center.z + dir[2] * tokenDistance
3617
+ ],
3618
+ target: [center.x, center.y, center.z],
3619
+ up: [0, 0, 1],
3620
+ fov: cameraFov
3621
+ }
3622
+ });
3623
+ }
3624
+ } catch (err) {
3625
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
3626
+ }
3230
3627
  const joints = ((_e = result.jointsView) == null ? void 0 : _e.enabled) === false ? [] : ((_f = result.jointsView) == null ? void 0 : _f.joints) ?? [];
3231
3628
  const jointCouplings = ((_g = result.jointsView) == null ? void 0 : _g.enabled) === false ? [] : ((_h = result.jointsView) == null ? void 0 : _h.couplings) ?? [];
3232
3629
  const animationClips = ((_i = result.jointsView) == null ? void 0 : _i.enabled) === false ? [] : ((_j = result.jointsView) == null ? void 0 : _j.animations) ?? [];
@@ -3525,7 +3922,7 @@ window.__forgeRender = async (code, opts) => {
3525
3922
  const session = built.session;
3526
3923
  await emitInspectProgress(opts, { type: "session-done", objectCount: session.objects.length });
3527
3924
  const renderMode = (opts == null ? void 0 : opts.renderMode) === "wireframe" ? "wireframe" : "solid";
3528
- const edgePreset = (opts == null ? void 0 : opts.edges) ?? "thin";
3925
+ const edgePreset = (opts == null ? void 0 : opts.edges) ?? "off";
3529
3926
  setSessionMode(session, renderMode);
3530
3927
  if (renderMode === "solid") {
3531
3928
  session.renderables.forEach((r) => {
@@ -3547,11 +3944,13 @@ window.__forgeRender = async (code, opts) => {
3547
3944
  const distanceRenders = {};
3548
3945
  const collisionRenders = {};
3549
3946
  const thicknessRenders = {};
3947
+ const roughnessRenders = {};
3550
3948
  let maskObjects = [];
3551
3949
  let connectivityReport = null;
3552
3950
  let distanceReport = null;
3553
3951
  let collisionReport = null;
3554
3952
  let thicknessReport = null;
3953
+ let roughnessReport = null;
3555
3954
  let sectionAtlas = null;
3556
3955
  const channelViewCounts = /* @__PURE__ */ new Map();
3557
3956
  const markChannelViewStart = async (channel, view) => {
@@ -3592,6 +3991,13 @@ window.__forgeRender = async (code, opts) => {
3592
3991
  normalRenders[label] = renderCurrentNormals(session);
3593
3992
  await markChannelViewDone("normals", label);
3594
3993
  }
3994
+ if (requestedChannels.has("roughness")) {
3995
+ await markChannelViewStart("roughness", label);
3996
+ const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness);
3997
+ roughnessRenders[label] = roughness.png;
3998
+ roughnessReport = roughness.report;
3999
+ await markChannelViewDone("roughness", label);
4000
+ }
3595
4001
  if (requestedChannels.has("mask")) {
3596
4002
  await markChannelViewStart("mask", label);
3597
4003
  const mask = renderCurrentMask(session);
@@ -3652,6 +4058,12 @@ window.__forgeRender = async (code, opts) => {
3652
4058
  renders,
3653
4059
  depth: depthRenders,
3654
4060
  normals: normalRenders,
4061
+ roughness: roughnessReport ? {
4062
+ ...roughnessReport,
4063
+ views: roughnessRenders
4064
+ } : {
4065
+ views: roughnessRenders
4066
+ },
3655
4067
  mask: {
3656
4068
  views: maskRenders,
3657
4069
  objects: maskObjects