cyclecad 2.1.0 → 3.1.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 (94) hide show
  1. package/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
  2. package/BILLING-INDEX.md +293 -0
  3. package/BILLING-INTEGRATION-GUIDE.md +414 -0
  4. package/COLLABORATION-INDEX.md +440 -0
  5. package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
  6. package/DELIVERABLES.txt +296 -445
  7. package/DOCKER-BUILD-MANIFEST.txt +483 -0
  8. package/DOCKER-FILES-REFERENCE.md +440 -0
  9. package/DOCKER-INFRASTRUCTURE.md +475 -0
  10. package/DOCKER-README.md +435 -0
  11. package/Dockerfile +33 -55
  12. package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
  13. package/ENHANCEMENT_SUMMARY.txt +308 -0
  14. package/FEATURE_INVENTORY.md +235 -0
  15. package/FUSION360_FEATURES_SUMMARY.md +452 -0
  16. package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
  17. package/FUSION360_PARITY_SUMMARY.md +520 -0
  18. package/FUSION360_QUICK_REFERENCE.md +351 -0
  19. package/MODULE_API_REFERENCE.md +712 -0
  20. package/MODULE_INVENTORY.txt +264 -0
  21. package/PWA-FILES-CREATED.txt +350 -0
  22. package/QUICK-START-TESTING.md +126 -0
  23. package/STEP-IMPORT-QUICKSTART.md +347 -0
  24. package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
  25. package/app/css/mobile.css +1074 -0
  26. package/app/icons/generate-icons.js +203 -0
  27. package/app/index.html +1342 -5031
  28. package/app/js/app.js +1312 -514
  29. package/app/js/billing-ui.js +990 -0
  30. package/app/js/brep-kernel.js +933 -981
  31. package/app/js/collab-client.js +750 -0
  32. package/app/js/mobile-nav.js +623 -0
  33. package/app/js/mobile-toolbar.js +476 -0
  34. package/app/js/modules/animation-module.js +497 -3
  35. package/app/js/modules/billing-module.js +724 -0
  36. package/app/js/modules/cam-module.js +507 -2
  37. package/app/js/modules/collaboration-module.js +513 -0
  38. package/app/js/modules/constraint-module.js +1266 -0
  39. package/app/js/modules/data-module.js +544 -1146
  40. package/app/js/modules/formats-module.js +438 -738
  41. package/app/js/modules/inspection-module.js +393 -0
  42. package/app/js/modules/mesh-module-enhanced.js +880 -0
  43. package/app/js/modules/plugin-module.js +597 -0
  44. package/app/js/modules/rendering-module.js +460 -0
  45. package/app/js/modules/scripting-module.js +593 -475
  46. package/app/js/modules/sketch-module.js +998 -2
  47. package/app/js/modules/step-module-enhanced.js +938 -0
  48. package/app/js/modules/surface-module.js +312 -0
  49. package/app/js/modules/version-module.js +420 -0
  50. package/app/js/offline-manager.js +705 -0
  51. package/app/js/responsive-init.js +360 -0
  52. package/app/js/touch-handler.js +429 -0
  53. package/app/manifest.json +211 -0
  54. package/app/offline.html +508 -0
  55. package/app/sw.js +571 -0
  56. package/app/tests/billing-tests.html +779 -0
  57. package/app/tests/brep-tests.html +980 -0
  58. package/app/tests/collab-tests.html +743 -0
  59. package/app/tests/mobile-tests.html +1299 -0
  60. package/app/tests/pwa-tests.html +1134 -0
  61. package/app/tests/step-tests.html +1042 -0
  62. package/app/tests/test-agent-v3.html +719 -0
  63. package/cycleCAD-Architecture-v2.pptx +0 -0
  64. package/docker-compose.yml +225 -0
  65. package/docs/BILLING-HELP.json +260 -0
  66. package/docs/BILLING-README.md +639 -0
  67. package/docs/BILLING-TUTORIAL.md +736 -0
  68. package/docs/BREP-HELP.json +326 -0
  69. package/docs/BREP-TUTORIAL.md +802 -0
  70. package/docs/COLLABORATION-HELP.json +228 -0
  71. package/docs/COLLABORATION-TUTORIAL.md +818 -0
  72. package/docs/DOCKER-HELP.json +224 -0
  73. package/docs/DOCKER-TUTORIAL.md +974 -0
  74. package/docs/MOBILE-HELP.json +243 -0
  75. package/docs/MOBILE-RESPONSIVE-README.md +378 -0
  76. package/docs/MOBILE-TUTORIAL.md +747 -0
  77. package/docs/PWA-HELP.json +228 -0
  78. package/docs/PWA-README.md +662 -0
  79. package/docs/PWA-TUTORIAL.md +757 -0
  80. package/docs/STEP-HELP.json +481 -0
  81. package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
  82. package/docs/TESTING-GUIDE.md +528 -0
  83. package/docs/TESTING-HELP.json +182 -0
  84. package/fusion-vs-cyclecad.html +1771 -0
  85. package/nginx.conf +237 -0
  86. package/package.json +1 -1
  87. package/server/Dockerfile.converter +51 -0
  88. package/server/Dockerfile.signaling +28 -0
  89. package/server/billing-server.js +487 -0
  90. package/server/converter-enhanced.py +528 -0
  91. package/server/requirements-converter.txt +29 -0
  92. package/server/signaling-server.js +801 -0
  93. package/tests/docker-tests.sh +389 -0
  94. package/~$cycleCAD-Architecture-v2.pptx +0 -0
@@ -74,27 +74,62 @@ const SketchModule = {
74
74
  getUI() {
75
75
  return `
76
76
  <div id="sketch-toolbar" style="display: none; background: #2a2a2a; padding: 8px; border-radius: 4px; flex-wrap: wrap; gap: 4px;">
77
+ <!-- BASIC TOOLS -->
77
78
  <button data-tool="line" class="sketch-tool-btn" title="Line (L)">—</button>
78
79
  <button data-tool="rectangle" class="sketch-tool-btn" title="Rectangle (R)">▭</button>
79
80
  <button data-tool="circle" class="sketch-tool-btn" title="Circle (C)">●</button>
80
81
  <button data-tool="arc" class="sketch-tool-btn" title="Arc (A)">⌒</button>
81
82
  <button data-tool="ellipse" class="sketch-tool-btn" title="Ellipse (E)">⬭</button>
82
83
  <button data-tool="spline" class="sketch-tool-btn" title="Spline (S)">✓</button>
84
+ <button data-tool="spline_fit" class="sketch-tool-btn" title="Fit Point Spline">↪</button>
83
85
  <button data-tool="polygon" class="sketch-tool-btn" title="Polygon (P)">⬡</button>
84
- <button data-tool="slot" class="sketch-tool-btn" title="Slot">⊟</button>
86
+
87
+ <!-- SLOT TOOLS -->
88
+ <button data-tool="slot" class="sketch-tool-btn" title="Slot (Center-Point)">⊟</button>
89
+ <button data-tool="slot_3point" class="sketch-tool-btn" title="Slot (3-Point)">⊟*</button>
90
+ <button data-tool="slot_ctc" class="sketch-tool-btn" title="Slot (Center-to-Center)">⊟**</button>
91
+
92
+ <!-- CONIC & TEXT -->
93
+ <button data-tool="conic" class="sketch-tool-btn" title="Conic (Parabola/Hyperbola)">∿</button>
85
94
  <button data-tool="text" class="sketch-tool-btn" title="Text (T)">T</button>
95
+ <button data-tool="text_path" class="sketch-tool-btn" title="Text Along Path">T↷</button>
96
+
97
+ <!-- REFERENCE -->
98
+ <button data-tool="point" class="sketch-tool-btn" title="Point (standalone)">•</button>
99
+ <button data-tool="midpoint" class="sketch-tool-btn" title="Midpoint">◈</button>
100
+
101
+ <!-- EDITING TOOLS -->
86
102
  <button data-tool="trim" class="sketch-tool-btn" title="Trim">✂</button>
103
+ <button data-tool="power_trim" class="sketch-tool-btn" title="Power Trim (drag)">✂✂</button>
104
+ <button data-tool="break" class="sketch-tool-btn" title="Break at Point">⊥</button>
87
105
  <button data-tool="extend" class="sketch-tool-btn" title="Extend">→</button>
88
106
  <button data-tool="offset" class="sketch-tool-btn" title="Offset">⟿</button>
89
107
  <button data-tool="mirror" class="sketch-tool-btn" title="Mirror">⇄</button>
90
108
  <button data-tool="fillet" class="sketch-tool-btn" title="Fillet">⌢</button>
91
109
  <button data-tool="chamfer" class="sketch-tool-btn" title="Chamfer">/</button>
110
+
111
+ <!-- PATTERN TOOLS -->
112
+ <button data-tool="rect_pattern" class="sketch-tool-btn" title="Rectangular Pattern">▦</button>
113
+ <button data-tool="circ_pattern" class="sketch-tool-btn" title="Circular Pattern">⊙</button>
114
+ <button data-tool="path_pattern" class="sketch-tool-btn" title="Pattern Along Path">▦→</button>
115
+
116
+ <!-- CONSTRUCTION & GEOMETRY -->
92
117
  <button data-tool="construction" class="sketch-tool-btn" title="Toggle Construction (G)">⋯</button>
118
+ <button data-tool="project" class="sketch-tool-btn" title="Project Edge">⌜</button>
119
+ <button data-tool="include" class="sketch-tool-btn" title="Include From Sketch">⊂</button>
120
+ <button data-tool="intersection" class="sketch-tool-btn" title="Intersection Curve">✕</button>
121
+
122
+ <!-- DIMENSIONS -->
93
123
  <button id="sketch-dimension-btn" class="sketch-tool-btn" title="Add Dimension (D)">📏</button>
124
+ <button data-tool="ordinate" class="sketch-tool-btn" title="Ordinate Dimension">📍</button>
125
+ <button data-tool="reference" class="sketch-tool-btn" title="Reference Dimension">⌀</button>
126
+ <button data-tool="auto_dim" class="sketch-tool-btn" title="Auto Dimension">✓📏</button>
127
+
128
+ <!-- FINISH -->
94
129
  <button id="sketch-finish-btn" style="margin-left: 16px; background: #00aa00; color: white;" title="Finish Sketch (Esc)">✓ Finish</button>
95
130
  </div>
96
131
  <div id="sketch-status-bar" style="display: none; color: #aaa; font-size: 12px; padding: 4px 8px; border-top: 1px solid #444; background: #1a1a1a;">
97
- Tool: <span id="sketch-tool-name">Line</span> | Grid: <span id="sketch-grid-size">5mm</span> | Entities: <span id="sketch-entity-count">0</span>
132
+ Tool: <span id="sketch-tool-name">Line</span> | Grid: <span id="sketch-grid-size">5mm</span> | Entities: <span id="sketch-entity-count">0</span> | DOF: <span id="sketch-dof">0</span>
98
133
  </div>
99
134
  <div id="sketch-dimension-input" style="display: none; position: fixed; background: #2a2a2a; border: 1px solid #444; border-radius: 4px; padding: 12px; z-index: 10000;">
100
135
  <label style="display: block; font-size: 12px; color: #aaa; margin-bottom: 4px;">Dimension Value (mm)</label>
@@ -519,6 +554,342 @@ const SketchModule = {
519
554
  });
520
555
  },
521
556
 
557
+ drawSlotCenterPoint(center, width, height, rotation = 0) {
558
+ /**
559
+ * CENTER-POINT SLOT: Slot defined by center, width, and height
560
+ *
561
+ * A slot is a rounded rectangle with semicircular ends.
562
+ * Specified by center point, width, and height of the slot.
563
+ */
564
+ return this.addEntity('slot_center', {
565
+ points: [center],
566
+ data: { width, height, rotation },
567
+ constraints: [{ type: 'fixed', point: center }]
568
+ });
569
+ },
570
+
571
+ drawSlot3Point(p1, p2, radius) {
572
+ /**
573
+ * 3-POINT SLOT: Define by two endpoints and radius (arc radius)
574
+ *
575
+ * Creates a slot with semicircular ends of given radius,
576
+ * connecting two specified endpoints.
577
+ */
578
+ const center = new THREE.Vector2(
579
+ (p1.x + p2.x) / 2,
580
+ (p1.y + p2.y) / 2
581
+ );
582
+ const length = p1.distanceTo(p2);
583
+ return this.addEntity('slot_3point', {
584
+ points: [p1, p2],
585
+ data: { radius, length, center }
586
+ });
587
+ },
588
+
589
+ drawSlotCenterToCenter(center1, center2, radius) {
590
+ /**
591
+ * CENTER-TO-CENTER SLOT: Define by arc centers and radius
592
+ *
593
+ * Slot with circular centers at two specified points,
594
+ * connected by tangent lines.
595
+ */
596
+ return this.addEntity('slot_ctc', {
597
+ points: [center1, center2],
598
+ data: { radius }
599
+ });
600
+ },
601
+
602
+ drawConic(type, params) {
603
+ /**
604
+ * CONIC SECTION DRAWING: Parabola, Hyperbola, or Ellipse
605
+ *
606
+ * @param {string} type - 'parabola' or 'hyperbola'
607
+ * @param {object} params - { focus, directrix, ... } or { foci, a, ... }
608
+ *
609
+ * Parabola: locus of points equidistant from focus and directrix
610
+ * Hyperbola: locus of points where |PF1| - |PF2| = 2a
611
+ */
612
+ if (type === 'parabola') {
613
+ const { focus, directrixLine } = params;
614
+ return this.addEntity('parabola', {
615
+ points: [focus],
616
+ data: { directrixLine, type: 'parabola' }
617
+ });
618
+ } else if (type === 'hyperbola') {
619
+ const { focus1, focus2, a } = params;
620
+ return this.addEntity('hyperbola', {
621
+ points: [focus1, focus2],
622
+ data: { a, type: 'hyperbola' }
623
+ });
624
+ }
625
+ return null;
626
+ },
627
+
628
+ drawRectangularPattern(entityIds, columns, rows, spacingX, spacingY) {
629
+ /**
630
+ * RECTANGULAR PATTERN: Array entity copies in grid
631
+ *
632
+ * Creates copies of selected entities in a column×row grid
633
+ * with specified spacing between copies.
634
+ */
635
+ if (entityIds.length === 0) return [];
636
+
637
+ const patterns = [];
638
+ const baseEntities = this.state.entities.filter(e => entityIds.includes(e.id));
639
+
640
+ for (let row = 0; row < rows; row++) {
641
+ for (let col = 0; col < columns; col++) {
642
+ if (row === 0 && col === 0) continue; // Skip original
643
+
644
+ const offset = new THREE.Vector2(col * spacingX, row * spacingY);
645
+
646
+ baseEntities.forEach(entity => {
647
+ const copiedPoints = entity.points.map(p => p.clone().add(offset));
648
+ const patternEntity = this.addEntity(entity.type, {
649
+ points: copiedPoints,
650
+ data: { ...entity.data, isPattern: true, baseId: entity.id },
651
+ isConstruction: entity.isConstruction
652
+ });
653
+ patterns.push(patternEntity);
654
+ });
655
+ }
656
+ }
657
+
658
+ return patterns;
659
+ },
660
+
661
+ drawCircularPattern(entityIds, center, count, angleSpan = Math.PI * 2) {
662
+ /**
663
+ * CIRCULAR PATTERN: Array entity copies around center
664
+ *
665
+ * Creates copies of selected entities arranged radially
666
+ * around a center point.
667
+ */
668
+ if (entityIds.length === 0) return [];
669
+
670
+ const patterns = [];
671
+ const baseEntities = this.state.entities.filter(e => entityIds.includes(e.id));
672
+ const angleStep = angleSpan / count;
673
+
674
+ for (let i = 1; i < count; i++) {
675
+ const angle = i * angleStep;
676
+ const cos = Math.cos(angle);
677
+ const sin = Math.sin(angle);
678
+
679
+ baseEntities.forEach(entity => {
680
+ const copiedPoints = entity.points.map(p => {
681
+ const relative = new THREE.Vector2(p.x - center.x, p.y - center.y);
682
+ const rotated = new THREE.Vector2(
683
+ relative.x * cos - relative.y * sin,
684
+ relative.x * sin + relative.y * cos
685
+ );
686
+ return rotated.add(center);
687
+ });
688
+
689
+ const patternEntity = this.addEntity(entity.type, {
690
+ points: copiedPoints,
691
+ data: { ...entity.data, isPattern: true, baseId: entity.id },
692
+ isConstruction: entity.isConstruction
693
+ });
694
+ patterns.push(patternEntity);
695
+ });
696
+ }
697
+
698
+ return patterns;
699
+ },
700
+
701
+ drawPatternAlongPath(entityIds, pathEntityId, count, spacing = null) {
702
+ /**
703
+ * PATTERN ALONG PATH: Array entity copies along a curve
704
+ *
705
+ * Creates copies of selected entities distributed along
706
+ * a line, arc, or spline path.
707
+ */
708
+ const pathEntity = this.state.entities.find(e => e.id === pathEntityId);
709
+ if (!pathEntity || !['line', 'arc', 'spline'].includes(pathEntity.type)) {
710
+ console.warn('Path must be line, arc, or spline');
711
+ return [];
712
+ }
713
+
714
+ const baseEntities = this.state.entities.filter(e => entityIds.includes(e.id));
715
+ const patterns = [];
716
+
717
+ for (let i = 1; i < count; i++) {
718
+ const t = i / count; // Parameter along path [0, 1]
719
+ const pathPoint = this.evaluateEntityAtParameter(pathEntity, t);
720
+ const pathTangent = this.evaluateEntityTangentAtParameter(pathEntity, t);
721
+ const angle = Math.atan2(pathTangent.y, pathTangent.x);
722
+
723
+ baseEntities.forEach(entity => {
724
+ const copiedPoints = entity.points.map(p => {
725
+ const relative = new THREE.Vector2(p.x - entity.points[0].x, p.y - entity.points[0].y);
726
+ const rotated = new THREE.Vector2(
727
+ relative.x * Math.cos(angle) - relative.y * Math.sin(angle),
728
+ relative.x * Math.sin(angle) + relative.y * Math.cos(angle)
729
+ );
730
+ return pathPoint.clone().add(rotated);
731
+ });
732
+
733
+ const patternEntity = this.addEntity(entity.type, {
734
+ points: copiedPoints,
735
+ data: { ...entity.data, isPattern: true, baseId: entity.id },
736
+ isConstruction: entity.isConstruction
737
+ });
738
+ patterns.push(patternEntity);
739
+ });
740
+ }
741
+
742
+ return patterns;
743
+ },
744
+
745
+ projectEdgeOntoSketch(edgeId) {
746
+ /**
747
+ * PROJECT 3D EDGE ONTO SKETCH PLANE
748
+ *
749
+ * Takes a 3D edge from the model and projects it orthogonally
750
+ * onto the current sketch plane. Useful for alignment.
751
+ */
752
+ // In production, would ray-cast edge with sketch plane
753
+ // For now, return a projected line entity
754
+ return {
755
+ message: 'Project edge ' + edgeId + ' onto sketch plane',
756
+ isConstruction: true
757
+ };
758
+ },
759
+
760
+ includeGeometryFromSketch(sourceSketchId) {
761
+ /**
762
+ * INCLUDE GEOMETRY FROM ANOTHER SKETCH
763
+ *
764
+ * References entities from another sketch in the current sketch.
765
+ * Changes to source sketch automatically update references.
766
+ */
767
+ return {
768
+ message: 'Include geometry from sketch ' + sourceSketchId,
769
+ linkedSketchId: sourceSketchId
770
+ };
771
+ },
772
+
773
+ drawIntersectionCurve(body1Id, body2Id, surface1Id, surface2Id) {
774
+ /**
775
+ * INTERSECTION CURVE: Sketch curve from intersecting surfaces
776
+ *
777
+ * Computes the intersection of two 3D surfaces/bodies and
778
+ * projects it onto the sketch plane as a construction curve.
779
+ */
780
+ return this.addEntity('intersection_curve', {
781
+ data: {
782
+ body1Id,
783
+ body2Id,
784
+ surface1Id,
785
+ surface2Id,
786
+ isConstruction: true
787
+ }
788
+ });
789
+ },
790
+
791
+ drawTextAlongPath(text, pathEntityId, fontSize = 10) {
792
+ /**
793
+ * TEXT ALONG PATH: Text that follows a curve
794
+ *
795
+ * Distributes text characters along a line, arc, or spline.
796
+ */
797
+ const pathEntity = this.state.entities.find(e => e.id === pathEntityId);
798
+ if (!pathEntity) return null;
799
+
800
+ return this.addEntity('text_along_path', {
801
+ data: { text, fontSize, pathEntityId, isConstruction: false }
802
+ });
803
+ },
804
+
805
+ drawFitPointSpline(points) {
806
+ /**
807
+ * FIT POINT SPLINE (through-point B-spline)
808
+ *
809
+ * Creates a spline that passes THROUGH all specified points
810
+ * (unlike control-point spline which passes near control points).
811
+ * Uses automatic knot vector generation for smooth interpolation.
812
+ */
813
+ if (points.length < 2) {
814
+ console.warn('Fit point spline requires at least 2 points');
815
+ return null;
816
+ }
817
+
818
+ return this.addEntity('spline_fit', {
819
+ points,
820
+ data: {
821
+ degree: Math.min(3, points.length - 1),
822
+ isFitPoint: true,
823
+ knotVector: this.generateFitPointKnots(points.length)
824
+ }
825
+ });
826
+ },
827
+
828
+ generateFitPointKnots(n) {
829
+ /**
830
+ * Generate knot vector for fit-point (interpolating) spline
831
+ * Uses Centripetal Catmull-Rom parameterization
832
+ */
833
+ const knots = [0, 0, 0, 0];
834
+ for (let i = 1; i <= n - 2; i++) {
835
+ knots.push(i);
836
+ }
837
+ knots.push(n - 1, n - 1, n - 1, n - 1);
838
+ return knots;
839
+ },
840
+
841
+ drawMidpoint(entityId) {
842
+ /**
843
+ * MIDPOINT: Create a point at midpoint of any edge
844
+ *
845
+ * Adds a construction point at the midpoint of a line, arc, or spline.
846
+ * Useful as reference for other constraints.
847
+ */
848
+ const entity = this.state.entities.find(e => e.id === entityId);
849
+ if (!entity || !['line', 'arc', 'spline'].includes(entity.type)) {
850
+ console.warn('Midpoint tool requires line, arc, or spline');
851
+ return null;
852
+ }
853
+
854
+ let midpoint;
855
+ if (entity.type === 'line') {
856
+ const [p1, p2] = entity.points;
857
+ midpoint = new THREE.Vector2(
858
+ (p1.x + p2.x) / 2,
859
+ (p1.y + p2.y) / 2
860
+ );
861
+ } else if (entity.type === 'arc') {
862
+ const [start, end] = entity.points;
863
+ midpoint = new THREE.Vector2(
864
+ (start.x + end.x) / 2,
865
+ (start.y + end.y) / 2
866
+ );
867
+ } else if (entity.type === 'spline') {
868
+ // Evaluate spline at t=0.5
869
+ midpoint = this.evaluateBSpline(entity.points, 0.5, entity.data.degree);
870
+ }
871
+
872
+ return this.addEntity('point', {
873
+ points: [midpoint],
874
+ data: { linkedEntityId: entityId, type: 'midpoint' },
875
+ isConstruction: true
876
+ });
877
+ },
878
+
879
+ drawPoint(point, isConstruction = true) {
880
+ /**
881
+ * POINT TOOL: Standalone point or center mark
882
+ *
883
+ * Creates a construction point (small circle) at specified location.
884
+ * Useful for reference geometry and constraint anchors.
885
+ */
886
+ return this.addEntity('point', {
887
+ points: [point],
888
+ data: { type: 'standalone' },
889
+ isConstruction
890
+ });
891
+ },
892
+
522
893
  // ===== EDITING TOOLS =====
523
894
 
524
895
  trim(entityId, clickPoint) {
@@ -571,6 +942,97 @@ const SketchModule = {
571
942
  window.dispatchEvent(new CustomEvent('sketch:entityModified', { detail: { entity } }));
572
943
  },
573
944
 
945
+ powerTrim(clickPoints) {
946
+ /**
947
+ * POWER TRIM: Drag to trim multiple entities at once
948
+ *
949
+ * Click and drag along entities to remove all segments
950
+ * that the drag line crosses. Works with line, arc, spline.
951
+ */
952
+ if (clickPoints.length < 2) return [];
953
+
954
+ const trimmedEntities = [];
955
+ const dragLine = { p1: clickPoints[0], p2: clickPoints[clickPoints.length - 1] };
956
+
957
+ this.state.entities.forEach(entity => {
958
+ if (!['line', 'arc', 'spline'].includes(entity.type)) return;
959
+
960
+ const crossings = this.findEntityDragCrossings(entity, dragLine);
961
+ if (crossings.length >= 2) {
962
+ // Remove segments between pairs of crossings
963
+ crossings.sort((a, b) => a.t - b.t);
964
+ for (let i = 0; i < crossings.length - 1; i++) {
965
+ this.trim(entity.id, new THREE.Vector2(
966
+ (crossings[i].point.x + crossings[i + 1].point.x) / 2,
967
+ (crossings[i].point.y + crossings[i + 1].point.y) / 2
968
+ ));
969
+ trimmedEntities.push(entity);
970
+ }
971
+ }
972
+ });
973
+
974
+ return trimmedEntities;
975
+ },
976
+
977
+ trimToIntersection(entityId, otherEntityId) {
978
+ /**
979
+ * TRIM TO NEAREST INTERSECTION
980
+ *
981
+ * Automatically trims entity to its nearest intersection
982
+ * with another specific entity.
983
+ */
984
+ const entity = this.state.entities.find(e => e.id === entityId);
985
+ const other = this.state.entities.find(e => e.id === otherEntityId);
986
+ if (!entity || !other) return;
987
+
988
+ const ints = this.findIntersection(entity, other);
989
+ if (ints.length === 0) return;
990
+
991
+ // Trim to nearest intersection
992
+ const nearest = ints.reduce((a, b) =>
993
+ a.t < b.t ? a : b
994
+ );
995
+
996
+ this.trim(entityId, nearest.point);
997
+ },
998
+
999
+ breakAtPoint(entityId, point) {
1000
+ /**
1001
+ * BREAK AT POINT: Split entity at specified point
1002
+ *
1003
+ * Breaks a line or arc into two segments at the given point
1004
+ * (useful for adding construction references).
1005
+ */
1006
+ const entity = this.state.entities.find(e => e.id === entityId);
1007
+ if (!entity || !['line', 'arc', 'spline'].includes(entity.type)) return;
1008
+
1009
+ const t = this.findParameterAlongEntity(entity, point);
1010
+ if (t === null) return;
1011
+
1012
+ if (entity.type === 'line') {
1013
+ const [p1, p2] = entity.points;
1014
+ this.addEntity('line', { points: [p1, point] });
1015
+ this.addEntity('line', { points: [point, p2] });
1016
+ const idx = this.state.entities.indexOf(entity);
1017
+ if (idx > -1) this.state.entities.splice(idx, 1);
1018
+ } else if (entity.type === 'arc') {
1019
+ const [start, end, center] = entity.points;
1020
+ const angle1 = Math.atan2(point.y - center.y, point.x - center.x);
1021
+ this.addEntity('arc', {
1022
+ points: [start, point, center],
1023
+ data: { ...entity.data, endAngle: angle1 }
1024
+ });
1025
+ this.addEntity('arc', {
1026
+ points: [point, end, center],
1027
+ data: { ...entity.data, startAngle: angle1 }
1028
+ });
1029
+ const idx = this.state.entities.indexOf(entity);
1030
+ if (idx > -1) this.state.entities.splice(idx, 1);
1031
+ }
1032
+
1033
+ window.dispatchEvent(new CustomEvent('sketch:entityModified', { detail: { entity } }));
1034
+ },
1035
+
574
1036
  splitLineAtTrim(entity, intStart, intEnd) {
575
1037
  // For line: keep segments before intStart and after intEnd, discard middle
576
1038
  const [p1, p2] = entity.points;
@@ -1480,6 +1942,173 @@ const SketchModule = {
1480
1942
  this.angleInRange(angle, arc.data.startAngle, arc.data.endAngle);
1481
1943
  },
1482
1944
 
1945
+ // ===== ADVANCED DIMENSIONS =====
1946
+
1947
+ addOrdinateDimension(entityId, baselineEntity, direction = 'X') {
1948
+ /**
1949
+ * ORDINATE DIMENSION: Baseline reference dimension system
1950
+ *
1951
+ * Creates a series of dimensions measured from a baseline
1952
+ * entity. All dimensions reference the same baseline (e.g., left edge).
1953
+ * More compact than individual linear dimensions.
1954
+ */
1955
+ const entity = this.state.entities.find(e => e.id === entityId);
1956
+ const baseline = this.state.entities.find(e => e.id === baselineEntity.id);
1957
+
1958
+ if (!entity || !baseline) return null;
1959
+
1960
+ const dim = {
1961
+ id: `dim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
1962
+ type: 'ordinate',
1963
+ entities: [entity.id, baseline.id],
1964
+ direction,
1965
+ driven: true,
1966
+ value: this.computeOrdinateMeasurement(entity, baseline, direction)
1967
+ };
1968
+
1969
+ this.state.dimensions.push(dim);
1970
+ window.dispatchEvent(new CustomEvent('sketch:dimensionAdded', { detail: { dimension: dim } }));
1971
+ return dim;
1972
+ },
1973
+
1974
+ autoDimension() {
1975
+ /**
1976
+ * AUTO-DIMENSION: Detect and apply minimal sufficient dimension set
1977
+ *
1978
+ * Analyzes sketch geometry and constraints to determine
1979
+ * the minimum number of dimensions needed to fully constrain the sketch.
1980
+ * Uses a greedy algorithm to select important dimensions.
1981
+ */
1982
+ const essentialDims = [];
1983
+ const profile = this.getProfile();
1984
+
1985
+ // Count degrees of freedom
1986
+ let dof = profile.entities.length * 3; // Each entity: 2 position + 1 orientation
1987
+ dof -= profile.entities.filter(e => e.constraints.some(c => c.type === 'fixed')).length * 3;
1988
+
1989
+ // Add dimensions for unconstrained entities
1990
+ profile.entities.forEach(entity => {
1991
+ if (dof <= 0) return;
1992
+
1993
+ const isConstrained = entity.constraints && entity.constraints.length > 0;
1994
+ if (!isConstrained) {
1995
+ let dimType = null;
1996
+ let value = null;
1997
+
1998
+ if (entity.type === 'line') {
1999
+ dimType = 'distance';
2000
+ value = entity.points[0].distanceTo(entity.points[1]);
2001
+ dof -= 1;
2002
+ } else if (entity.type === 'circle') {
2003
+ dimType = 'radius';
2004
+ value = entity.data.radius;
2005
+ dof -= 1;
2006
+ } else if (entity.type === 'arc') {
2007
+ dimType = 'radius';
2008
+ value = entity.data.radius;
2009
+ dof -= 1;
2010
+ }
2011
+
2012
+ if (dimType) {
2013
+ essentialDims.push({
2014
+ id: `auto_dim_${Date.now()}_${essentialDims.length}`,
2015
+ type: dimType,
2016
+ entities: [entity.id],
2017
+ value,
2018
+ driven: true
2019
+ });
2020
+ }
2021
+ }
2022
+ });
2023
+
2024
+ this.state.dimensions.push(...essentialDims);
2025
+ return essentialDims;
2026
+ },
2027
+
2028
+ addReferenceDimension(entityId, type = 'distance', value = null) {
2029
+ /**
2030
+ * REFERENCE DIMENSION: Non-driving dimension for display
2031
+ *
2032
+ * Creates a dimension that displays the measurement but
2033
+ * does not constrain the geometry. Useful for documenting
2034
+ * features and creating assembly notes.
2035
+ */
2036
+ const entity = this.state.entities.find(e => e.id === entityId);
2037
+ if (!entity) return null;
2038
+
2039
+ const computedValue = value !== null ? value : this.computeEntityMeasurement(entity, type);
2040
+
2041
+ const dim = {
2042
+ id: `ref_dim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
2043
+ type,
2044
+ entities: [entityId],
2045
+ value: computedValue,
2046
+ driven: false, // Key: reference dimensions are NOT driven
2047
+ isReference: true
2048
+ };
2049
+
2050
+ this.state.dimensions.push(dim);
2051
+ window.dispatchEvent(new CustomEvent('sketch:dimensionAdded', { detail: { dimension: dim } }));
2052
+ return dim;
2053
+ },
2054
+
2055
+ computeEntityMeasurement(entity, measurementType) {
2056
+ /**
2057
+ * Compute measurement value for an entity
2058
+ * @param {object} entity - sketch entity
2059
+ * @param {string} measurementType - 'distance', 'radius', 'diameter', 'angle', etc.
2060
+ * @returns {number} measurement value in current units
2061
+ */
2062
+ switch (measurementType) {
2063
+ case 'distance':
2064
+ if (entity.type === 'line') {
2065
+ return entity.points[0].distanceTo(entity.points[1]);
2066
+ }
2067
+ return 0;
2068
+
2069
+ case 'radius':
2070
+ if (entity.type === 'circle' || entity.type === 'arc') {
2071
+ return entity.data.radius || 0;
2072
+ }
2073
+ return 0;
2074
+
2075
+ case 'diameter':
2076
+ if (entity.type === 'circle' || entity.type === 'arc') {
2077
+ return (entity.data.radius || 0) * 2;
2078
+ }
2079
+ return 0;
2080
+
2081
+ case 'angle':
2082
+ if (entity.type === 'arc') {
2083
+ const { startAngle, endAngle } = entity.data;
2084
+ return (endAngle - startAngle) * (180 / Math.PI);
2085
+ }
2086
+ return 0;
2087
+
2088
+ default:
2089
+ return 0;
2090
+ }
2091
+ },
2092
+
2093
+ computeOrdinateMeasurement(entity, baseline, direction) {
2094
+ /**
2095
+ * Compute ordinate measurement (distance from baseline)
2096
+ * @param {object} entity - sketch entity to measure
2097
+ * @param {object} baseline - reference baseline entity
2098
+ * @param {string} direction - 'X' or 'Y'
2099
+ * @returns {number} measurement value
2100
+ */
2101
+ const baselinePos = baseline.points[0];
2102
+ const entityPos = entity.points[0];
2103
+
2104
+ if (direction === 'X') {
2105
+ return Math.abs(entityPos.x - baselinePos.x);
2106
+ } else if (direction === 'Y') {
2107
+ return Math.abs(entityPos.y - baselinePos.y);
2108
+ }
2109
+ return 0;
2110
+ },
2111
+
1483
2112
  getProfile() {
1484
2113
  // Extract closed wire from entities for extrude/revolve
1485
2114
  return {
@@ -1489,6 +2118,121 @@ const SketchModule = {
1489
2118
  };
1490
2119
  },
1491
2120
 
2121
+ // ===== HELPER METHODS FOR NEW TOOLS =====
2122
+
2123
+ evaluateEntityAtParameter(entity, t) {
2124
+ /**
2125
+ * Evaluate entity position at parameter t ∈ [0, 1]
2126
+ */
2127
+ if (entity.type === 'line') {
2128
+ const [p1, p2] = entity.points;
2129
+ return new THREE.Vector2(
2130
+ p1.x + t * (p2.x - p1.x),
2131
+ p1.y + t * (p2.y - p1.y)
2132
+ );
2133
+ } else if (entity.type === 'arc') {
2134
+ const [start, end] = entity.points;
2135
+ const [, , center] = entity.points;
2136
+ const angle = entity.data.startAngle + t * (entity.data.endAngle - entity.data.startAngle);
2137
+ return new THREE.Vector2(
2138
+ center.x + entity.data.radius * Math.cos(angle),
2139
+ center.y + entity.data.radius * Math.sin(angle)
2140
+ );
2141
+ } else if (entity.type === 'spline') {
2142
+ return this.evaluateBSpline(entity.points, t, entity.data.degree);
2143
+ }
2144
+ return entity.points[0];
2145
+ },
2146
+
2147
+ evaluateEntityTangentAtParameter(entity, t) {
2148
+ /**
2149
+ * Evaluate entity tangent vector at parameter t ∈ [0, 1]
2150
+ */
2151
+ const delta = 0.001;
2152
+ const p1 = this.evaluateEntityAtParameter(entity, Math.max(0, t - delta));
2153
+ const p2 = this.evaluateEntityAtParameter(entity, Math.min(1, t + delta));
2154
+ return new THREE.Vector2(p2.x - p1.x, p2.y - p1.y).normalize();
2155
+ },
2156
+
2157
+ findParameterAlongEntity(entity, point) {
2158
+ /**
2159
+ * Find parameter t along entity closest to given point
2160
+ * @returns {number|null} parameter t ∈ [0, 1], or null if not found
2161
+ */
2162
+ if (entity.type === 'line') {
2163
+ const [p1, p2] = entity.points;
2164
+ const dx = p2.x - p1.x;
2165
+ const dy = p2.y - p1.y;
2166
+ const len2 = dx * dx + dy * dy;
2167
+ if (len2 === 0) return 0;
2168
+
2169
+ const t = ((point.x - p1.x) * dx + (point.y - p1.y) * dy) / len2;
2170
+ return Math.max(0, Math.min(1, t));
2171
+ }
2172
+ return null;
2173
+ },
2174
+
2175
+ findEntityDragCrossings(entity, dragLine) {
2176
+ /**
2177
+ * Find all points where entity crosses a drag line
2178
+ */
2179
+ const crossings = [];
2180
+
2181
+ if (entity.type === 'line') {
2182
+ const int = this.lineLineIntersection(entity.points[0], entity.points[1], dragLine.p1, dragLine.p2);
2183
+ if (int) crossings.push(int);
2184
+ } else if (entity.type === 'arc') {
2185
+ const ints = this.lineArcIntersection(dragLine.p1, dragLine.p2, entity);
2186
+ crossings.push(...ints);
2187
+ }
2188
+
2189
+ return crossings;
2190
+ },
2191
+
2192
+ findAllIntersectionsOnEntity(entity) {
2193
+ /**
2194
+ * Find all intersections of an entity with other entities
2195
+ */
2196
+ const intersections = [];
2197
+
2198
+ this.state.entities.forEach(other => {
2199
+ if (other.id === entity.id) return;
2200
+ const ints = this.findIntersection(entity, other);
2201
+ intersections.push(...ints);
2202
+ });
2203
+
2204
+ return intersections;
2205
+ },
2206
+
2207
+ findIntersection(entity1, entity2) {
2208
+ /**
2209
+ * Find all intersections between two entities
2210
+ */
2211
+ const ints = [];
2212
+
2213
+ if (entity1.type === 'line' && entity2.type === 'line') {
2214
+ const int = this.lineLineIntersection(
2215
+ entity1.points[0], entity1.points[1],
2216
+ entity2.points[0], entity2.points[1]
2217
+ );
2218
+ if (int) ints.push(int);
2219
+ } else if (entity1.type === 'line' && entity2.type === 'circle') {
2220
+ const circleInts = this.lineCircleIntersection(
2221
+ entity1.points[0], entity1.points[1],
2222
+ entity2.points[0], entity2.data.radius
2223
+ );
2224
+ ints.push(...circleInts);
2225
+ } else if (entity1.type === 'circle' && entity2.type === 'circle') {
2226
+ const circleInts = this.circleCircleIntersection(
2227
+ entity1.points[0], entity1.data.radius,
2228
+ entity2.points[0], entity2.data.radius
2229
+ );
2230
+ ints.push(...circleInts);
2231
+ }
2232
+
2233
+ return ints;
2234
+ },
2235
+
1492
2236
  setupEventHandlers() {
1493
2237
  // Tool button clicks
1494
2238
  document.addEventListener('click', (e) => {
@@ -1659,4 +2403,256 @@ const SketchModule = {
1659
2403
  }
1660
2404
  };
1661
2405
 
2406
+ /**
2407
+ * HELP ENTRIES: Documentation for all sketch tools and features
2408
+ * Exported for Help System integration
2409
+ */
2410
+ SketchModule.HELP_ENTRIES = [
2411
+ // Basic Drawing Tools
2412
+ {
2413
+ id: 'sketch.line',
2414
+ title: 'Line Tool',
2415
+ description: 'Draw a straight line between two points. Click to set start, click again to set end.',
2416
+ category: 'Drawing',
2417
+ hotkey: 'L'
2418
+ },
2419
+ {
2420
+ id: 'sketch.rectangle',
2421
+ title: 'Rectangle Tool',
2422
+ description: 'Draw axis-aligned rectangle by two corner points.',
2423
+ category: 'Drawing',
2424
+ hotkey: 'R'
2425
+ },
2426
+ {
2427
+ id: 'sketch.circle',
2428
+ title: 'Circle Tool',
2429
+ description: 'Draw circle by center point and radius point.',
2430
+ category: 'Drawing',
2431
+ hotkey: 'C'
2432
+ },
2433
+ {
2434
+ id: 'sketch.arc',
2435
+ title: 'Arc Tool',
2436
+ description: 'Draw arc by start, end, and center points.',
2437
+ category: 'Drawing',
2438
+ hotkey: 'A'
2439
+ },
2440
+ {
2441
+ id: 'sketch.ellipse',
2442
+ title: 'Ellipse Tool',
2443
+ description: 'Draw ellipse by center and two axis endpoints.',
2444
+ category: 'Drawing',
2445
+ hotkey: 'E'
2446
+ },
2447
+ {
2448
+ id: 'sketch.spline',
2449
+ title: 'Control Point Spline',
2450
+ description: 'Draw cubic B-spline by control points. Double-click or Enter to finish.',
2451
+ category: 'Drawing',
2452
+ hotkey: 'S'
2453
+ },
2454
+ {
2455
+ id: 'sketch.spline_fit',
2456
+ title: 'Fit Point Spline',
2457
+ description: 'Draw interpolating spline through specified points (unlike control-point splines).',
2458
+ category: 'Drawing'
2459
+ },
2460
+ {
2461
+ id: 'sketch.polygon',
2462
+ title: 'Polygon Tool',
2463
+ description: 'Draw regular polygon by center and corner point.',
2464
+ category: 'Drawing',
2465
+ hotkey: 'P'
2466
+ },
2467
+
2468
+ // Slot Tools
2469
+ {
2470
+ id: 'sketch.slot',
2471
+ title: 'Slot (Center-Point)',
2472
+ description: 'Draw slot by center point, width, and height.',
2473
+ category: 'Drawing'
2474
+ },
2475
+ {
2476
+ id: 'sketch.slot_3point',
2477
+ title: 'Slot (3-Point)',
2478
+ description: 'Draw slot by two endpoints and arc radius.',
2479
+ category: 'Drawing'
2480
+ },
2481
+ {
2482
+ id: 'sketch.slot_ctc',
2483
+ title: 'Slot (Center-to-Center)',
2484
+ description: 'Draw slot by arc centers and radius.',
2485
+ category: 'Drawing'
2486
+ },
2487
+
2488
+ // Conic Sections
2489
+ {
2490
+ id: 'sketch.conic',
2491
+ title: 'Conic Sections',
2492
+ description: 'Draw parabola or hyperbola using focus/directrix (parabola) or foci (hyperbola).',
2493
+ category: 'Drawing'
2494
+ },
2495
+
2496
+ // Text Tools
2497
+ {
2498
+ id: 'sketch.text',
2499
+ title: 'Text Tool',
2500
+ description: 'Place text at a specific point in the sketch.',
2501
+ category: 'Drawing',
2502
+ hotkey: 'T'
2503
+ },
2504
+ {
2505
+ id: 'sketch.text_path',
2506
+ title: 'Text Along Path',
2507
+ description: 'Place text that follows a line, arc, or spline curve.',
2508
+ category: 'Drawing'
2509
+ },
2510
+
2511
+ // Reference Geometry
2512
+ {
2513
+ id: 'sketch.point',
2514
+ title: 'Point Tool',
2515
+ description: 'Create a standalone construction point for reference.',
2516
+ category: 'Reference'
2517
+ },
2518
+ {
2519
+ id: 'sketch.midpoint',
2520
+ title: 'Midpoint',
2521
+ description: 'Create a point at the midpoint of a line, arc, or spline.',
2522
+ category: 'Reference'
2523
+ },
2524
+
2525
+ // Editing Tools
2526
+ {
2527
+ id: 'sketch.trim',
2528
+ title: 'Trim Tool',
2529
+ description: 'Remove segments between intersections. Click on segment to trim.',
2530
+ category: 'Editing'
2531
+ },
2532
+ {
2533
+ id: 'sketch.power_trim',
2534
+ title: 'Power Trim',
2535
+ description: 'Click and drag to trim multiple entities at once.',
2536
+ category: 'Editing'
2537
+ },
2538
+ {
2539
+ id: 'sketch.break',
2540
+ title: 'Break at Point',
2541
+ description: 'Split a line or arc into two segments at a specified point.',
2542
+ category: 'Editing'
2543
+ },
2544
+ {
2545
+ id: 'sketch.extend',
2546
+ title: 'Extend Tool',
2547
+ description: 'Extend a line toward other geometry.',
2548
+ category: 'Editing'
2549
+ },
2550
+ {
2551
+ id: 'sketch.offset',
2552
+ title: 'Offset Tool',
2553
+ description: 'Create offset copies of lines and curves.',
2554
+ category: 'Editing'
2555
+ },
2556
+ {
2557
+ id: 'sketch.mirror',
2558
+ title: 'Mirror Tool',
2559
+ description: 'Mirror selected entities across a line.',
2560
+ category: 'Editing'
2561
+ },
2562
+ {
2563
+ id: 'sketch.fillet',
2564
+ title: 'Fillet Tool',
2565
+ description: 'Round corners between intersecting lines.',
2566
+ category: 'Editing'
2567
+ },
2568
+ {
2569
+ id: 'sketch.chamfer',
2570
+ title: 'Chamfer Tool',
2571
+ description: 'Create beveled edges between intersecting lines.',
2572
+ category: 'Editing'
2573
+ },
2574
+
2575
+ // Pattern Tools
2576
+ {
2577
+ id: 'sketch.rect_pattern',
2578
+ title: 'Rectangular Pattern',
2579
+ description: 'Array selected entities in a grid with specified spacing.',
2580
+ category: 'Pattern'
2581
+ },
2582
+ {
2583
+ id: 'sketch.circ_pattern',
2584
+ title: 'Circular Pattern',
2585
+ description: 'Array selected entities radially around a center point.',
2586
+ category: 'Pattern'
2587
+ },
2588
+ {
2589
+ id: 'sketch.path_pattern',
2590
+ title: 'Pattern Along Path',
2591
+ description: 'Array selected entities along a line, arc, or spline.',
2592
+ category: 'Pattern'
2593
+ },
2594
+
2595
+ // Geometry Operations
2596
+ {
2597
+ id: 'sketch.project',
2598
+ title: 'Project Edge',
2599
+ description: 'Project a 3D edge orthogonally onto the sketch plane.',
2600
+ category: 'Geometry'
2601
+ },
2602
+ {
2603
+ id: 'sketch.include',
2604
+ title: 'Include Geometry',
2605
+ description: 'Reference geometry from another sketch (linked, updates automatically).',
2606
+ category: 'Geometry'
2607
+ },
2608
+ {
2609
+ id: 'sketch.intersection',
2610
+ title: 'Intersection Curve',
2611
+ description: 'Create sketch curve from intersection of two 3D surfaces.',
2612
+ category: 'Geometry'
2613
+ },
2614
+ {
2615
+ id: 'sketch.construction',
2616
+ title: 'Construction Geometry',
2617
+ description: 'Toggle selected entities as construction (reference-only). (G)',
2618
+ category: 'Geometry',
2619
+ hotkey: 'G'
2620
+ },
2621
+
2622
+ // Dimension Tools
2623
+ {
2624
+ id: 'sketch.dimension',
2625
+ title: 'Add Dimension',
2626
+ description: 'Add linear, radial, angular, or diameter dimension to constrain geometry.',
2627
+ category: 'Dimensions',
2628
+ hotkey: 'D'
2629
+ },
2630
+ {
2631
+ id: 'sketch.ordinate',
2632
+ title: 'Ordinate Dimension',
2633
+ description: 'Create baseline-based ordinate dimensions for aligned measurements.',
2634
+ category: 'Dimensions'
2635
+ },
2636
+ {
2637
+ id: 'sketch.reference',
2638
+ title: 'Reference Dimension',
2639
+ description: 'Add non-driving dimension for documentation (does not constrain).',
2640
+ category: 'Dimensions'
2641
+ },
2642
+ {
2643
+ id: 'sketch.auto_dim',
2644
+ title: 'Auto Dimension',
2645
+ description: 'Automatically detect and apply minimal sufficient dimension set.',
2646
+ category: 'Dimensions'
2647
+ },
2648
+
2649
+ // Constraint System
2650
+ {
2651
+ id: 'sketch.constraints',
2652
+ title: 'Constraints Overview',
2653
+ description: 'Sketch constraints: coincident, horizontal, vertical, parallel, perpendicular, tangent, equal, fix, concentric, symmetric, collinear, midpoint, coradial.',
2654
+ category: 'Constraints'
2655
+ }
2656
+ ];
2657
+
1662
2658
  export default SketchModule;