@constructorfleet/sketchforge-engine 1.20.0 → 1.22.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.
@@ -8,7 +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 } from "./sketchCadProfile.js";
11
+ import { cadSketchRegions, cadSketchSweepPaths } from "./sketchCadProfile.js";
12
12
  export const SKF_SCHEMA_ID = "com.sketchforge.project";
13
13
  export const SKF_FORMAT_VERSION = 2;
14
14
  export const SKF_MINIMUM_READER_VERSION = 2;
@@ -441,6 +441,7 @@ async function serializeShapeNode(shape, nodeId, nodes, builder, sourceAssetsByA
441
441
  sketchSourceNodeId: undefined,
442
442
  faceDraft: undefined,
443
443
  faceDraftSourceFrame: undefined,
444
+ faceDraftSourceSegmentId: undefined,
444
445
  sketchSourceOfObjectId: shape.id,
445
446
  },
446
447
  });
@@ -579,33 +580,33 @@ function activeProjectIndexes(state) {
579
580
  });
580
581
  previous = featureId;
581
582
  }
582
- const treatments = Array.isArray(node.definition.edgeTreatments) ? node.definition.edgeTreatments : [];
583
- treatments.forEach((treatment, index) => {
584
- const type = treatment.kind === "fillet" ? "fillet" : "chamfer";
585
- const featureId = `feature/${safeNodeToken(node.nodeId)}/${type}/${index}`;
583
+ const faceDraft = node.definition.faceDraft;
584
+ if (faceDraft) {
585
+ const featureId = `feature/${safeNodeToken(node.nodeId)}/face-draft`;
586
586
  features.push({
587
587
  id: featureId,
588
- type,
588
+ type: "face-draft",
589
589
  outputObjectId: node.objectId,
590
590
  inputObjectIds: [node.objectId],
591
591
  dependsOnFeatureIds: previous ? [previous] : [],
592
- parameters: { ...treatment },
592
+ parameters: { ...faceDraft },
593
593
  });
594
594
  previous = featureId;
595
- });
596
- const faceDraft = node.definition.faceDraft;
597
- if (faceDraft) {
598
- const featureId = `feature/${safeNodeToken(node.nodeId)}/face-draft`;
595
+ }
596
+ const treatments = Array.isArray(node.definition.edgeTreatments) ? node.definition.edgeTreatments : [];
597
+ treatments.forEach((treatment, index) => {
598
+ const type = treatment.kind === "fillet" ? "fillet" : "chamfer";
599
+ const featureId = `feature/${safeNodeToken(node.nodeId)}/${type}/${index}`;
599
600
  features.push({
600
601
  id: featureId,
601
- type: "face-draft",
602
+ type,
602
603
  outputObjectId: node.objectId,
603
604
  inputObjectIds: [node.objectId],
604
605
  dependsOnFeatureIds: previous ? [previous] : [],
605
- parameters: { ...faceDraft },
606
+ parameters: { ...treatment },
606
607
  });
607
608
  previous = featureId;
608
- }
609
+ });
609
610
  if (node.definition.mirrorX || node.definition.mirrorY || node.definition.mirrorZ) {
610
611
  const featureId = `feature/${safeNodeToken(node.nodeId)}/mirror`;
611
612
  features.push({
@@ -993,14 +994,95 @@ function validateShapeDefinition(definition, label, options = {}) {
993
994
  && Math.abs(Math.abs(revolveSettings.sweepAngle) - 360) <= 1e-6
994
995
  && (revolveSettings.pitch === undefined || revolveSettings.pitch === 0)
995
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
+ }
996
1078
  if (kind !== "mesh"
997
1079
  || (definition.sketchOperation !== "extrude" && definition.sketchOperation !== "sweep" && !plainFullTurnRevolve)
998
1080
  || definition.sketchProfile === undefined
999
1081
  || plainFullTurnRevolve && cadSketchRegions(definition.sketchProfile).length !== 1
1000
1082
  || definition.sketchSourceId === undefined
1001
1083
  || definition.draftPrismAngle !== undefined
1002
- || (Array.isArray(definition.edgeTreatments) && definition.edgeTreatments.length > 0)
1003
- || options.hasEdgeTreatmentHistory) {
1084
+ || (treatments.length > 0 || options.hasEdgeTreatmentHistory)
1085
+ && !isPersistedRevolveDraftTreatment && !isPersistedExtrusionDraftTreatment && !isPersistedSweepDraftTreatment) {
1004
1086
  throw new LocalizedError("errors.skf.fieldInvalid", { label: `${label}.faceDraft` });
1005
1087
  }
1006
1088
  const sourceFaceId = stringValue(draft.sourceFaceId, `${label}.faceDraft.sourceFaceId`);
@@ -1033,6 +1115,47 @@ function validateShapeDefinition(definition, label, options = {}) {
1033
1115
  }
1034
1116
  });
1035
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
+ }
1036
1159
  if (kind === "gear") {
1037
1160
  const teeth = finiteNumber(definition.teeth, `${label}.teeth`);
1038
1161
  if (!Number.isInteger(teeth) || teeth < 6 || teeth > 64)
@@ -1203,6 +1326,7 @@ async function validateDocumentAndAssets(raw, files) {
1203
1326
  const definition = objectRecord(node.definition, `node '${nodeId}'.definition`);
1204
1327
  const objectId = validateShapeDefinition(definition, `node '${nodeId}'`, {
1205
1328
  hasEdgeTreatmentHistory: Boolean(node.edgeTreatmentHistory?.length),
1329
+ edgeTreatmentHistory: node.edgeTreatmentHistory,
1206
1330
  });
1207
1331
  if (node.objectId !== objectId)
1208
1332
  throw new LocalizedError("errors.skf.nodeObjectIdMismatch", { id: nodeId });
@@ -1538,6 +1662,7 @@ function validateLegacyRuntimeShape(shape, label) {
1538
1662
  delete definition.cadBrep;
1539
1663
  validateShapeDefinition(definition, label, {
1540
1664
  hasEdgeTreatmentHistory: Array.isArray(edgeHistory) && edgeHistory.length > 0,
1665
+ edgeTreatmentHistory: Array.isArray(edgeHistory) ? edgeHistory : undefined,
1541
1666
  });
1542
1667
  if (importedMesh) {
1543
1668
  const mesh = objectRecord(importedMesh, `${label}.importedMesh`);