@constructorfleet/sketchforge-engine 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +3 -3
  2. package/dist/domain/sketchforge.d.ts +43 -0
  3. package/dist/domain/sketchforge.d.ts.map +1 -1
  4. package/dist/engine.d.ts +1 -1
  5. package/dist/engine.d.ts.map +1 -1
  6. package/dist/engine.js +482 -29
  7. package/dist/engine.js.map +1 -1
  8. package/dist/faceDraftSegmentLineage.d.ts +45 -0
  9. package/dist/faceDraftSegmentLineage.d.ts.map +1 -0
  10. package/dist/faceDraftSegmentLineage.js +200 -0
  11. package/dist/faceDraftSegmentLineage.js.map +1 -0
  12. package/dist/faceLineage.d.ts +10 -0
  13. package/dist/faceLineage.d.ts.map +1 -0
  14. package/dist/faceLineage.js +84 -0
  15. package/dist/faceLineage.js.map +1 -0
  16. package/dist/parametricRecompute.d.ts +2 -0
  17. package/dist/parametricRecompute.d.ts.map +1 -1
  18. package/dist/parametricRecompute.js +152 -0
  19. package/dist/parametricRecompute.js.map +1 -1
  20. package/dist/revolveRequests.d.ts +12 -0
  21. package/dist/revolveRequests.d.ts.map +1 -1
  22. package/dist/serverCadOperations.d.ts +54 -3
  23. package/dist/serverCadOperations.d.ts.map +1 -1
  24. package/dist/serverCadOperations.js +224 -6
  25. package/dist/serverCadOperations.js.map +1 -1
  26. package/dist/serverCadShell.d.ts +3 -1
  27. package/dist/serverCadShell.d.ts.map +1 -1
  28. package/dist/serverCadShell.js +15 -5
  29. package/dist/serverCadShell.js.map +1 -1
  30. package/dist/sketchExtrusionRequest.d.ts.map +1 -1
  31. package/dist/sketchExtrusionRequest.js +1 -0
  32. package/dist/sketchExtrusionRequest.js.map +1 -1
  33. package/dist/skfProject.d.ts.map +1 -1
  34. package/dist/skfProject.js +210 -8
  35. package/dist/skfProject.js.map +1 -1
  36. package/dist/stepComponentMetadata.d.ts.map +1 -1
  37. package/dist/stepComponentMetadata.js +55 -2
  38. package/dist/stepComponentMetadata.js.map +1 -1
  39. package/dist/stepExport.d.ts.map +1 -1
  40. package/dist/stepExport.js +54 -8
  41. package/dist/stepExport.js.map +1 -1
  42. package/dist/stepGlbColors.d.ts.map +1 -1
  43. package/dist/stepGlbColors.js +10 -2
  44. package/dist/stepGlbColors.js.map +1 -1
  45. package/dist/support/lib/operator-configuration.json +10 -0
  46. package/package.json +1 -1
@@ -8,6 +8,7 @@ import { canonicalizeShape } from "./workplaneShapes.js";
8
8
  import { importedShapeFromStl } from "./stlImport.js";
9
9
  import { importedShapeFromSvg } from "./svgImport.js";
10
10
  import { normalizeSnapGrid, normalizeWorkspaceSettings } from "./workplaneSettings.js";
11
+ import { cadSketchRegions, cadSketchSweepPaths } from "./sketchCadProfile.js";
11
12
  export const SKF_SCHEMA_ID = "com.sketchforge.project";
12
13
  export const SKF_FORMAT_VERSION = 2;
13
14
  export const SKF_MINIMUM_READER_VERSION = 2;
@@ -30,7 +31,7 @@ const SHAPE_KINDS = new Set([
30
31
  ]);
31
32
  const LOFT_PROFILE_SHAPES = new Set(["Oval", "Rectangle", "Triangle", "Pentagon", "Hexagon"]);
32
33
  const FEATURE_TYPES = new Set([
33
- "group", "boolean-subtraction", "boolean-intersection", "mirror", "sketch-extrusion", "sketch-revolve", "sketch-sweep", "fillet", "chamfer",
34
+ "group", "boolean-subtraction", "boolean-intersection", "mirror", "sketch-extrusion", "sketch-revolve", "sketch-sweep", "fillet", "chamfer", "face-draft",
34
35
  ]);
35
36
  function exactArrayBuffer(bytes) {
36
37
  return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
@@ -438,6 +439,9 @@ async function serializeShapeNode(shape, nodeId, nodes, builder, sourceAssetsByA
438
439
  sketchRevolve: undefined,
439
440
  sketchSourceId: undefined,
440
441
  sketchSourceNodeId: undefined,
442
+ faceDraft: undefined,
443
+ faceDraftSourceFrame: undefined,
444
+ faceDraftSourceSegmentId: undefined,
441
445
  sketchSourceOfObjectId: shape.id,
442
446
  },
443
447
  });
@@ -576,6 +580,19 @@ function activeProjectIndexes(state) {
576
580
  });
577
581
  previous = featureId;
578
582
  }
583
+ const faceDraft = node.definition.faceDraft;
584
+ if (faceDraft) {
585
+ const featureId = `feature/${safeNodeToken(node.nodeId)}/face-draft`;
586
+ features.push({
587
+ id: featureId,
588
+ type: "face-draft",
589
+ outputObjectId: node.objectId,
590
+ inputObjectIds: [node.objectId],
591
+ dependsOnFeatureIds: previous ? [previous] : [],
592
+ parameters: { ...faceDraft },
593
+ });
594
+ previous = featureId;
595
+ }
579
596
  const treatments = Array.isArray(node.definition.edgeTreatments) ? node.definition.edgeTreatments : [];
580
597
  treatments.forEach((treatment, index) => {
581
598
  const type = treatment.kind === "fillet" ? "fillet" : "chamfer";
@@ -866,12 +883,19 @@ function validateSketchProfile(value, label) {
866
883
  throw new LocalizedError("errors.skf.fieldInvalid", { label: `${constraintLabel}.id` });
867
884
  constraintIds.add(id);
868
885
  const kind = stringValue(constraint.kind, `${constraintLabel}.kind`);
869
- if (kind === "horizontal" || kind === "vertical") {
886
+ if (kind === "fixed") {
887
+ const pointId = stringValue(constraint.pointId, `${constraintLabel}.pointId`);
888
+ if (!pointIds.has(pointId))
889
+ throw new LocalizedError("errors.skf.fieldInvalid", { label: `${constraintLabel}.pointId` });
890
+ finiteNumber(constraint.xMm, `${constraintLabel}.xMm`);
891
+ finiteNumber(constraint.zMm, `${constraintLabel}.zMm`);
892
+ }
893
+ else if (kind === "horizontal" || kind === "vertical") {
870
894
  if (!segmentIds.has(stringValue(constraint.segmentId, `${constraintLabel}.segmentId`))) {
871
895
  throw new LocalizedError("errors.skf.fieldInvalid", { label: `${constraintLabel}.segmentId` });
872
896
  }
873
897
  }
874
- else if (kind === "perpendicular") {
898
+ else if (kind === "perpendicular" || kind === "parallel") {
875
899
  if (!Array.isArray(constraint.segmentIds) || constraint.segmentIds.length !== 2 || constraint.segmentIds[0] === constraint.segmentIds[1] || !constraint.segmentIds.every((segmentId) => typeof segmentId === "string" && segmentIds.has(segmentId))) {
876
900
  throw new LocalizedError("errors.skf.fieldInvalid", { label: `${constraintLabel}.segmentIds` });
877
901
  }
@@ -890,7 +914,7 @@ function validateSketchProfile(value, label) {
890
914
  });
891
915
  }
892
916
  }
893
- function validateShapeDefinition(definition, label) {
917
+ function validateShapeDefinition(definition, label, options = {}) {
894
918
  const id = stringValue(definition.id, `${label}.id`);
895
919
  stringValue(definition.name, `${label}.name`);
896
920
  const kind = stringValue(definition.kind, `${label}.kind`);
@@ -919,13 +943,13 @@ function validateShapeDefinition(definition, label) {
919
943
  if (definition.faceColors !== undefined) {
920
944
  const colors = objectRecord(definition.faceColors, `${label}.faceColors`);
921
945
  for (const [faceId, color] of Object.entries(colors)) {
922
- if (!(/^(?:\d+|face_.+)$/.test(faceId)) || typeof color !== "string" || !/^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/.test(color)) {
946
+ if (!faceId.trim() || typeof color !== "string" || !/^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/.test(color)) {
923
947
  throw new LocalizedError("errors.skf.fieldInvalid", { label: `${label}.faceColors.${faceId}` });
924
948
  }
925
949
  }
926
950
  }
927
951
  if (definition.faceIds !== undefined && (!Array.isArray(definition.faceIds)
928
- || definition.faceIds.some((faceId) => typeof faceId !== "string" || !faceId.startsWith("face_"))
952
+ || definition.faceIds.some((faceId) => typeof faceId !== "string" || !faceId.trim())
929
953
  || new Set(definition.faceIds).size !== definition.faceIds.length)) {
930
954
  throw new LocalizedError("errors.skf.fieldInvalid", { label: `${label}.faceIds` });
931
955
  }
@@ -960,6 +984,178 @@ function validateShapeDefinition(definition, label) {
960
984
  finiteNumber(settings[field], `${label}.sketchRevolve.${field}`);
961
985
  });
962
986
  }
987
+ if (definition.faceDraft !== undefined) {
988
+ const draft = objectRecord(definition.faceDraft, `${label}.faceDraft`);
989
+ const revolveSettings = definition.sketchRevolve && typeof definition.sketchRevolve === "object" && !Array.isArray(definition.sketchRevolve)
990
+ ? definition.sketchRevolve
991
+ : undefined;
992
+ const plainFullTurnRevolve = definition.sketchOperation === "revolve" && revolveSettings
993
+ && typeof revolveSettings.sweepAngle === "number"
994
+ && Math.abs(Math.abs(revolveSettings.sweepAngle) - 360) <= 1e-6
995
+ && (revolveSettings.pitch === undefined || revolveSettings.pitch === 0)
996
+ && (revolveSettings.coreRadius === undefined || revolveSettings.coreRadius === 0);
997
+ const treatments = Array.isArray(definition.edgeTreatments) ? definition.edgeTreatments : [];
998
+ const treatment = treatments[0] && typeof treatments[0] === "object" && !Array.isArray(treatments[0])
999
+ ? treatments[0]
1000
+ : undefined;
1001
+ const history = options.edgeTreatmentHistory ?? [];
1002
+ const historyFeature = history[0]?.feature;
1003
+ const isPersistedRevolveDraftTreatment = Boolean(plainFullTurnRevolve
1004
+ && definition.sketchProfile !== undefined
1005
+ && cadSketchRegions(definition.sketchProfile).length === 1
1006
+ && definition.draftPrismAngle === undefined
1007
+ && treatments.length === 1
1008
+ && (treatment?.kind === "chamfer" || treatment?.kind === "fillet")
1009
+ && treatment.allEdges === true
1010
+ && treatment.edges === undefined
1011
+ && (treatment.kind === "fillet"
1012
+ || typeof treatment.chamferAngle === "number" && Math.abs(treatment.chamferAngle - 45) < 0.001)
1013
+ && typeof treatment.amount === "number"
1014
+ && Number.isFinite(treatment.amount)
1015
+ && treatment.amount > 0
1016
+ && Number.isInteger(treatment.edgeCount)
1017
+ && treatment.edgeCount > 0
1018
+ && history.length === 1
1019
+ && historyFeature?.kind === treatment.kind
1020
+ && historyFeature.allEdges === treatment.allEdges
1021
+ && (treatment.kind === "fillet" || historyFeature.chamferAngle === treatment.chamferAngle)
1022
+ && historyFeature.amount === treatment.amount
1023
+ && historyFeature.edgeCount === treatment.edgeCount);
1024
+ const extrusionRegions = definition.sketchOperation === "extrude" && definition.sketchProfile !== undefined
1025
+ ? cadSketchRegions(definition.sketchProfile)
1026
+ : [];
1027
+ const isPersistedExtrusionDraftTreatment = Boolean(definition.sketchOperation === "extrude"
1028
+ && extrusionRegions.length === 1
1029
+ && extrusionRegions[0]?.holes.length === 0
1030
+ && definition.draftPrismAngle === undefined
1031
+ && treatments.length === 1
1032
+ && (treatment?.kind === "fillet" || treatment?.kind === "chamfer")
1033
+ && treatment.allEdges === true
1034
+ && treatment.edges === undefined
1035
+ && (treatment.kind === "fillet"
1036
+ || typeof treatment.chamferAngle === "number" && Math.abs(treatment.chamferAngle - 45) < 0.001)
1037
+ && typeof treatment.amount === "number"
1038
+ && Number.isFinite(treatment.amount)
1039
+ && treatment.amount > 0
1040
+ && Number.isInteger(treatment.edgeCount)
1041
+ && treatment.edgeCount > 0
1042
+ && history.length === 1
1043
+ && historyFeature?.kind === treatment.kind
1044
+ && historyFeature.allEdges === true
1045
+ && (treatment.kind === "fillet" || historyFeature.chamferAngle === treatment.chamferAngle)
1046
+ && historyFeature.amount === treatment.amount
1047
+ && historyFeature.edgeCount === treatment.edgeCount);
1048
+ let isPersistedSweepDraftTreatment = false;
1049
+ if (definition.sketchOperation === "sweep" && definition.sketchProfile !== undefined) {
1050
+ try {
1051
+ const { sections, spine } = cadSketchSweepPaths(definition.sketchProfile);
1052
+ isPersistedSweepDraftTreatment = Boolean(sections.length === 1
1053
+ && sections[0]?.holes.length === 0
1054
+ && spine.steps.length > 0
1055
+ && definition.draftPrismAngle === undefined
1056
+ && treatments.length === 1
1057
+ && (treatment?.kind === "fillet" || treatment?.kind === "chamfer")
1058
+ && treatment.allEdges === true
1059
+ && treatment.edges === undefined
1060
+ && (treatment.kind === "fillet"
1061
+ || typeof treatment.chamferAngle === "number" && Math.abs(treatment.chamferAngle - 45) < 0.001)
1062
+ && typeof treatment.amount === "number"
1063
+ && Number.isFinite(treatment.amount)
1064
+ && treatment.amount > 0
1065
+ && Number.isInteger(treatment.edgeCount)
1066
+ && treatment.edgeCount > 0
1067
+ && history.length === 1
1068
+ && historyFeature?.kind === treatment.kind
1069
+ && historyFeature.allEdges === true
1070
+ && (treatment.kind === "fillet" || historyFeature.chamferAngle === treatment.chamferAngle)
1071
+ && historyFeature.amount === treatment.amount
1072
+ && historyFeature.edgeCount === treatment.edgeCount);
1073
+ }
1074
+ catch {
1075
+ isPersistedSweepDraftTreatment = false;
1076
+ }
1077
+ }
1078
+ if (kind !== "mesh"
1079
+ || (definition.sketchOperation !== "extrude" && definition.sketchOperation !== "sweep" && !plainFullTurnRevolve)
1080
+ || definition.sketchProfile === undefined
1081
+ || plainFullTurnRevolve && cadSketchRegions(definition.sketchProfile).length !== 1
1082
+ || definition.sketchSourceId === undefined
1083
+ || definition.draftPrismAngle !== undefined
1084
+ || (treatments.length > 0 || options.hasEdgeTreatmentHistory)
1085
+ && !isPersistedRevolveDraftTreatment && !isPersistedExtrusionDraftTreatment && !isPersistedSweepDraftTreatment) {
1086
+ throw new LocalizedError("errors.skf.fieldInvalid", { label: `${label}.faceDraft` });
1087
+ }
1088
+ const sourceFaceId = stringValue(draft.sourceFaceId, `${label}.faceDraft.sourceFaceId`);
1089
+ if (!sourceFaceId.trim())
1090
+ throw new LocalizedError("errors.skf.fieldInvalid", { label: `${label}.faceDraft.sourceFaceId` });
1091
+ const angle = finiteNumber(draft.angle, `${label}.faceDraft.angle`);
1092
+ if (angle === 0 || Math.abs(angle) >= 45)
1093
+ throw new LocalizedError("errors.skf.outOfRange", { label: `${label}.faceDraft.angle` });
1094
+ if (!["x", "y", "z", "-x", "-y", "-z"].includes(draft.pullDirection)) {
1095
+ throw new LocalizedError("errors.skf.fieldInvalid", { label: `${label}.faceDraft.pullDirection` });
1096
+ }
1097
+ const normal = objectRecord(draft.sourceFaceNormal, `${label}.faceDraft.sourceFaceNormal`);
1098
+ const coordinates = ["x", "y", "z"].map((axis) => finiteNumber(normal[axis], `${label}.faceDraft.sourceFaceNormal.${axis}`));
1099
+ const magnitude = Math.hypot(...coordinates);
1100
+ if (Math.abs(magnitude - 1) > 1e-4)
1101
+ throw new LocalizedError("errors.skf.fieldInvalid", { label: `${label}.faceDraft.sourceFaceNormal` });
1102
+ }
1103
+ if (definition.faceDraftSourceFrame !== undefined) {
1104
+ const frame = objectRecord(definition.faceDraftSourceFrame, `${label}.faceDraftSourceFrame`);
1105
+ ["x", "z", "elevation", "width", "depth", "height"].forEach((field) => finiteNumber(frame[field], `${label}.faceDraftSourceFrame.${field}`));
1106
+ if (["width", "depth", "height"].some((field) => frame[field] <= 0)) {
1107
+ throw new LocalizedError("errors.skf.nonPositiveDimensions", { label: `${label}.faceDraftSourceFrame` });
1108
+ }
1109
+ }
1110
+ if (definition.faceDraftSourceScale !== undefined) {
1111
+ const scale = objectRecord(definition.faceDraftSourceScale, `${label}.faceDraftSourceScale`);
1112
+ ["width", "depth", "height"].forEach((field) => {
1113
+ if (finiteNumber(scale[field], `${label}.faceDraftSourceScale.${field}`) <= 0) {
1114
+ throw new LocalizedError("errors.skf.fieldInvalid", { label: `${label}.faceDraftSourceScale.${field}` });
1115
+ }
1116
+ });
1117
+ }
1118
+ if (definition.faceDraftSourceFaceIds !== undefined) {
1119
+ if (!Array.isArray(definition.faceDraftSourceFaceIds) || definition.faceDraftSourceFaceIds.length === 0) {
1120
+ throw new LocalizedError("errors.skf.fieldInvalid", { label: `${label}.faceDraftSourceFaceIds` });
1121
+ }
1122
+ const sourceFaceIds = definition.faceDraftSourceFaceIds.map((id, index) => stringValue(id, `${label}.faceDraftSourceFaceIds[${index}]`));
1123
+ if (sourceFaceIds.some((id) => !id.trim()) || new Set(sourceFaceIds).size !== sourceFaceIds.length
1124
+ || typeof definition.faceDraft === "object" && definition.faceDraft !== null
1125
+ && !sourceFaceIds.includes(definition.faceDraft.sourceFaceId)) {
1126
+ throw new LocalizedError("errors.skf.fieldInvalid", { label: `${label}.faceDraftSourceFaceIds` });
1127
+ }
1128
+ }
1129
+ if (definition.faceDraftSourceSegmentId !== undefined) {
1130
+ const sourceSegmentId = stringValue(definition.faceDraftSourceSegmentId, `${label}.faceDraftSourceSegmentId`);
1131
+ const profile = definition.sketchProfile;
1132
+ let regions = [];
1133
+ try {
1134
+ if (profile)
1135
+ regions = cadSketchRegions(profile);
1136
+ }
1137
+ catch {
1138
+ regions = [];
1139
+ }
1140
+ const segments = profile?.segments ?? [];
1141
+ const pointsById = new Map(profile?.points.map((point) => [point.id, point]) ?? []);
1142
+ const sourceSegmentMatches = segments.filter((segment) => segment.id === sourceSegmentId);
1143
+ const containsBezier = segments.some((segment) => {
1144
+ if (segment.kind !== undefined && segment.kind !== "line")
1145
+ return true;
1146
+ if (segment.kind !== undefined)
1147
+ return false;
1148
+ const start = pointsById.get(segment.startId);
1149
+ const end = pointsById.get(segment.endId);
1150
+ return Boolean(start && end && ((start.handleOut && end.handleIn) || (start.handleIn && end.handleOut)));
1151
+ });
1152
+ if (!sourceSegmentId.trim() || kind !== "mesh" || definition.sketchOperation !== "extrude"
1153
+ || definition.faceDraft === undefined || !profile || regions.length !== 1 || regions[0]?.holes.length !== 0
1154
+ || regions[0]?.outer.steps.length !== segments.length || containsBezier
1155
+ || sourceSegmentMatches.length !== 1) {
1156
+ throw new LocalizedError("errors.skf.fieldInvalid", { label: `${label}.faceDraftSourceSegmentId` });
1157
+ }
1158
+ }
963
1159
  if (kind === "gear") {
964
1160
  const teeth = finiteNumber(definition.teeth, `${label}.teeth`);
965
1161
  if (!Number.isInteger(teeth) || teeth < 6 || teeth > 64)
@@ -1128,7 +1324,10 @@ async function validateDocumentAndAssets(raw, files) {
1128
1324
  if (nodeById.has(nodeId))
1129
1325
  throw new LocalizedError("errors.skf.stateDuplicateNodeId", { state: stateId, id: nodeId });
1130
1326
  const definition = objectRecord(node.definition, `node '${nodeId}'.definition`);
1131
- const objectId = validateShapeDefinition(definition, `node '${nodeId}'`);
1327
+ const objectId = validateShapeDefinition(definition, `node '${nodeId}'`, {
1328
+ hasEdgeTreatmentHistory: Boolean(node.edgeTreatmentHistory?.length),
1329
+ edgeTreatmentHistory: node.edgeTreatmentHistory,
1330
+ });
1132
1331
  if (node.objectId !== objectId)
1133
1332
  throw new LocalizedError("errors.skf.nodeObjectIdMismatch", { id: nodeId });
1134
1333
  if (node.importedMesh) {
@@ -1461,7 +1660,10 @@ function validateLegacyRuntimeShape(shape, label) {
1461
1660
  delete definition.groupedShapes;
1462
1661
  delete definition.edgeTreatmentHistory;
1463
1662
  delete definition.cadBrep;
1464
- validateShapeDefinition(definition, label);
1663
+ validateShapeDefinition(definition, label, {
1664
+ hasEdgeTreatmentHistory: Array.isArray(edgeHistory) && edgeHistory.length > 0,
1665
+ edgeTreatmentHistory: Array.isArray(edgeHistory) ? edgeHistory : undefined,
1666
+ });
1465
1667
  if (importedMesh) {
1466
1668
  const mesh = objectRecord(importedMesh, `${label}.importedMesh`);
1467
1669
  if (!Array.isArray(mesh.positions) || mesh.positions.length > SKF_LIMITS.meshNumbers || mesh.positions.some((value) => typeof value !== "number" || !Number.isFinite(value))) {