M0: xtask layers, parity, oracle, assets, ci; Fusion command catalog
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0983f80f01
commit
3714342737
6 files changed
+2285
-1
No files matched your search
+398
@@ -0,0 +1,398 @@
|
||||
# Fusion command parity
|
||||
|
||||
Generated by `cargo xtask parity` from `xtask/data/fusion-catalog.tsv` (the toolbar commands of Fusion's Design workspace, ids and names only) and the live command registry (`solvecraft-cli commands`). A command counts as live when SolveCraft registers the same command id. This measures breadth, not depth; see ROADMAP.md and docs/oracle.md for depth.
|
||||
|
||||
**SOLID + SKETCH: 46 / 291 commands live (16%).** All Design tabs: 95 / 1175 (8%). Registered commands: 67.
|
||||
|
||||
| Tab | Panel | Live | Total | % |
|
||||
|---|---|---:|---:|---:|
|
||||
| SOLID | CREATE | 7 | 35 | 20% |
|
||||
| SOLID | MODIFY | 7 | 32 | 22% |
|
||||
| SOLID | ASSEMBLE | 0 | 21 | 0% |
|
||||
| SOLID | CONFIGURE | 0 | 3 | 0% |
|
||||
| SOLID | CONSTRUCT | 0 | 20 | 0% |
|
||||
| SOLID | INSPECT | 1 | 25 | 4% |
|
||||
| SOLID | INSERT | 0 | 11 | 0% |
|
||||
| SOLID | SELECT | 0 | 15 | 0% |
|
||||
| SOLID | POSITION | 0 | 2 | 0% |
|
||||
| SKETCH | CREATE | 16 | 41 | 39% |
|
||||
| SKETCH | MODIFY | 2 | 12 | 17% |
|
||||
| SKETCH | CONSTRAINTS | 11 | 15 | 73% |
|
||||
| SKETCH | CONFIGURE | 0 | 3 | 0% |
|
||||
| SKETCH | INSPECT | 1 | 25 | 4% |
|
||||
| SKETCH | INSERT | 0 | 13 | 0% |
|
||||
| SKETCH | ASSEMBLE | 0 | 1 | 0% |
|
||||
| SKETCH | SELECT | 0 | 15 | 0% |
|
||||
| SKETCH | FINISH SKETCH | 1 | 2 | 50% |
|
||||
| SURFACE | CREATE | 1 | 25 | 4% |
|
||||
| SURFACE | MODIFY | 6 | 26 | 23% |
|
||||
| SURFACE | ASSEMBLE | 0 | 20 | 0% |
|
||||
| SURFACE | CONFIGURE | 0 | 3 | 0% |
|
||||
| SURFACE | CONSTRUCT | 0 | 20 | 0% |
|
||||
| SURFACE | INSPECT | 1 | 25 | 4% |
|
||||
| SURFACE | INSERT | 0 | 11 | 0% |
|
||||
| SURFACE | SELECT | 0 | 15 | 0% |
|
||||
| SURFACE | POSITION | 0 | 2 | 0% |
|
||||
| MESH | CREATE | 0 | 5 | 0% |
|
||||
| MESH | PREPARE | 0 | 4 | 0% |
|
||||
| MESH | MODIFY | 4 | 30 | 13% |
|
||||
| MESH | ASSEMBLE | 0 | 21 | 0% |
|
||||
| MESH | CONFIGURE | 0 | 3 | 0% |
|
||||
| MESH | CONSTRUCT | 0 | 20 | 0% |
|
||||
| MESH | INSPECT | 1 | 25 | 4% |
|
||||
| MESH | INSERT | 0 | 10 | 0% |
|
||||
| MESH | SELECT | 0 | 10 | 0% |
|
||||
| MESH | EXPORT | 0 | 1 | 0% |
|
||||
| MESH | POSITION | 0 | 2 | 0% |
|
||||
| FORM | CREATE | 1 | 14 | 7% |
|
||||
| FORM | MODIFY | 2 | 31 | 6% |
|
||||
| FORM | SYMMETRY | 0 | 6 | 0% |
|
||||
| FORM | UTILITIES | 0 | 5 | 0% |
|
||||
| FORM | CONSTRUCT | 0 | 20 | 0% |
|
||||
| FORM | INSPECT | 1 | 25 | 4% |
|
||||
| FORM | INSERT | 0 | 13 | 0% |
|
||||
| FORM | ASSEMBLE | 0 | 1 | 0% |
|
||||
| FORM | SELECT | 0 | 15 | 0% |
|
||||
| FORM | FINISH FORM | 0 | 1 | 0% |
|
||||
| SHEET METAL | CREATE | 2 | 19 | 11% |
|
||||
| SHEET METAL | MODIFY | 6 | 21 | 29% |
|
||||
| SHEET METAL | ASSEMBLE | 0 | 20 | 0% |
|
||||
| SHEET METAL | CONFIGURE | 0 | 3 | 0% |
|
||||
| SHEET METAL | CONSTRUCT | 0 | 20 | 0% |
|
||||
| SHEET METAL | INSPECT | 1 | 25 | 4% |
|
||||
| SHEET METAL | INSERT | 0 | 11 | 0% |
|
||||
| SHEET METAL | SELECT | 0 | 15 | 0% |
|
||||
| SHEET METAL | REFOLD | 0 | 1 | 0% |
|
||||
| SHEET METAL | POSITION | 0 | 2 | 0% |
|
||||
| PLASTIC | SETUP | 0 | 2 | 0% |
|
||||
| PLASTIC | CREATE | 7 | 35 | 20% |
|
||||
| PLASTIC | MODIFY | 7 | 32 | 22% |
|
||||
| PLASTIC | ASSEMBLE | 0 | 20 | 0% |
|
||||
| PLASTIC | CONFIGURE | 0 | 3 | 0% |
|
||||
| PLASTIC | CONSTRUCT | 0 | 20 | 0% |
|
||||
| PLASTIC | INSPECT | 1 | 26 | 4% |
|
||||
| PLASTIC | INSERT | 0 | 11 | 0% |
|
||||
| PLASTIC | SELECT | 0 | 15 | 0% |
|
||||
| PLASTIC | POSITION | 0 | 2 | 0% |
|
||||
| ASSEMBLY | INSERT | 0 | 8 | 0% |
|
||||
| ASSEMBLY | CREATE | 1 | 8 | 12% |
|
||||
| ASSEMBLY | RELATIONSHIPS | 0 | 11 | 0% |
|
||||
| ASSEMBLY | MOTION | 0 | 3 | 0% |
|
||||
| ASSEMBLY | MODIFY | 4 | 11 | 36% |
|
||||
| ASSEMBLY | CONTACT | 0 | 2 | 0% |
|
||||
| ASSEMBLY | CONFIGURE | 0 | 3 | 0% |
|
||||
| ASSEMBLY | CONSTRUCT | 0 | 20 | 0% |
|
||||
| ASSEMBLY | INSPECT | 1 | 25 | 4% |
|
||||
| ASSEMBLY | SELECT | 0 | 15 | 0% |
|
||||
| ASSEMBLY | POSITION | 0 | 2 | 0% |
|
||||
| MANAGE | ASSIGN | 0 | 1 | 0% |
|
||||
| MANAGE | RELEASE | 0 | 2 | 0% |
|
||||
| MANAGE | BOM | 0 | 2 | 0% |
|
||||
| MANAGE | PROCESS MANAGEMENT | 0 | 3 | 0% |
|
||||
| MANAGE | TASK MANAGEMENT | 0 | 3 | 0% |
|
||||
| UTILITIES | MODELING TOOLS | 0 | 1 | 0% |
|
||||
| UTILITIES | MAKE | 0 | 1 | 0% |
|
||||
| UTILITIES | NEST | 0 | 1 | 0% |
|
||||
| UTILITIES | ADD-INS | 0 | 2 | 0% |
|
||||
| UTILITIES | UTILITY | 1 | 3 | 33% |
|
||||
| UTILITIES | INSPECT | 1 | 24 | 4% |
|
||||
| UTILITIES | CREATE PMI | 0 | 4 | 0% |
|
||||
| UTILITIES | SELECT | 0 | 15 | 0% |
|
||||
| UTILITIES | POSITION | 0 | 2 | 0% |
|
||||
|
||||
## Live (SOLID, SKETCH)
|
||||
|
||||
- SOLID › CREATE › Create Sketch (`SketchCreate`)
|
||||
- SOLID › CREATE › Extrude (`Extrude`)
|
||||
- SOLID › CREATE › Revolve (`Revolve`)
|
||||
- SOLID › CREATE › Box (`PrimitiveBox`)
|
||||
- SOLID › CREATE › Cylinder (`PrimitiveCylinder`)
|
||||
- SOLID › CREATE › Sphere (`PrimitiveSphere`)
|
||||
- SOLID › CREATE › Torus (`PrimitiveTorus`)
|
||||
- SOLID › MODIFY › Fillet (`FusionFilletEdgesCommand`)
|
||||
- SOLID › MODIFY › Chamfer (`FusionChamferCommand`)
|
||||
- SOLID › MODIFY › Combine (`FusionCombineCommand`)
|
||||
- SOLID › MODIFY › Move/Copy (`FusionMoveCommand`)
|
||||
- SOLID › MODIFY › Delete (`FusionDeleteCommand`)
|
||||
- SOLID › MODIFY › Change Parameters (`ChangeParameterCommand`)
|
||||
- SOLID › MODIFY › Compute All (`FusionComputeAllCommand`)
|
||||
- SOLID › INSPECT › Measure (`MeasureCommand`)
|
||||
- SKETCH › CREATE › Line (`DrawPolyline`)
|
||||
- SKETCH › CREATE › 2-Point Rectangle (`ShapeRectangleTwoPoint`)
|
||||
- SKETCH › CREATE › 3-Point Rectangle (`ShapeRectangleThreePoint`)
|
||||
- SKETCH › CREATE › Center Rectangle (`ShapeRectangleCenter`)
|
||||
- SKETCH › CREATE › Center Diameter Circle (`CircleCenterRadius`)
|
||||
- SKETCH › CREATE › 2-Point Circle (`CircleTwoPoint`)
|
||||
- SKETCH › CREATE › 3-Point Circle (`CircleThreePoint`)
|
||||
- SKETCH › CREATE › 3-Point Arc (`ArcThreePoint`)
|
||||
- SKETCH › CREATE › Center Point Arc (`ArcCenterTwoPoint`)
|
||||
- SKETCH › CREATE › Circumscribed Polygon (`ShapePolygonCircumscribed`)
|
||||
- SKETCH › CREATE › Inscribed Polygon (`ShapePolygonInscribed`)
|
||||
- SKETCH › CREATE › Edge Polygon (`ShapePolygonEdge`)
|
||||
- SKETCH › CREATE › Center to Center Slot (`ShapeSlotCenterToCenter`)
|
||||
- SKETCH › CREATE › Overall Slot (`ShapeSlotOverall`)
|
||||
- SKETCH › CREATE › Point (`DrawPoint`)
|
||||
- SKETCH › CREATE › Sketch Dimension (`SketchDimension`)
|
||||
- SKETCH › MODIFY › Move/Copy (`FusionMoveCommand`)
|
||||
- SKETCH › MODIFY › Change Parameters (`ChangeParameterCommand`)
|
||||
- SKETCH › CONSTRAINTS › Horizontal/Vertical (`ConstraintHorizontalVertical`)
|
||||
- SKETCH › CONSTRAINTS › Coincident (`ConstraintCoincident`)
|
||||
- SKETCH › CONSTRAINTS › Tangent (`ConstraintTangent`)
|
||||
- SKETCH › CONSTRAINTS › Equal (`ConstraintEqual`)
|
||||
- SKETCH › CONSTRAINTS › Parallel (`ConstraintParallel`)
|
||||
- SKETCH › CONSTRAINTS › Perpendicular (`ConstraintPerpendicular`)
|
||||
- SKETCH › CONSTRAINTS › Fix/UnFix (`ConstraintFix`)
|
||||
- SKETCH › CONSTRAINTS › MidPoint (`ConstraintMidPoint`)
|
||||
- SKETCH › CONSTRAINTS › Concentric (`ConstraintConcentric`)
|
||||
- SKETCH › CONSTRAINTS › Collinear (`ConstraintCollinear`)
|
||||
- SKETCH › CONSTRAINTS › Symmetry (`ConstraintSymmetry`)
|
||||
- SKETCH › INSPECT › Measure (`MeasureCommand`)
|
||||
- SKETCH › FINISH SKETCH › Finish Sketch (`SketchStop`)
|
||||
|
||||
## Not yet (SOLID, SKETCH)
|
||||
|
||||
- SOLID › CREATE › Create Form (`TSplineBaseFeatureCreationCommand`)
|
||||
- SOLID › CREATE › Derive (`PushDeriveCommand`)
|
||||
- SOLID › CREATE › Automated Modeling (`AutomatedModelingCommand`)
|
||||
- SOLID › CREATE › Sweep (`Sweep`)
|
||||
- SOLID › CREATE › Loft (`SolidLoft`)
|
||||
- SOLID › CREATE › Rib (`FusionRibCommand`)
|
||||
- SOLID › CREATE › Web (`FusionWebCommand`)
|
||||
- SOLID › CREATE › Emboss (`EmbossCmd`)
|
||||
- SOLID › CREATE › Hole (`FusionHoleCommand`)
|
||||
- SOLID › CREATE › Thread (`FusionThreadCommand`)
|
||||
- SOLID › CREATE › Coil (`PrimitiveCoil`)
|
||||
- SOLID › CREATE › Pipe (`PrimitivePipe`)
|
||||
- SOLID › CREATE › Bounding Solid (`StockModelCommand`)
|
||||
- SOLID › CREATE › Rectangular Pattern (`PatternRectangular`)
|
||||
- SOLID › CREATE › Circular Pattern (`PatternCircular`)
|
||||
- SOLID › CREATE › Pattern on Path (`PatternOnPath`)
|
||||
- SOLID › CREATE › Geometric Pattern (`FusionPatternGeometricCommand`)
|
||||
- SOLID › CREATE › Mirror (`MirrorCommand`)
|
||||
- SOLID › CREATE › Thicken (`FusionSurfaceThickenCommand`)
|
||||
- SOLID › CREATE › Boundary Fill (`SurfaceSculpt`)
|
||||
- SOLID › CREATE › Find Features (`FusionFindFeaturesCommand`)
|
||||
- SOLID › CREATE › Fluid Volume (`EnclosureCommand`)
|
||||
- SOLID › CREATE › Create Base Feature (`BaseFeatureCreationCommand`)
|
||||
- SOLID › CREATE › Create Linked PCB (`PCBExternalComponentCreationCmd`)
|
||||
- SOLID › CREATE › Derive PCB (`PCBDeriveCmd`)
|
||||
- SOLID › CREATE › Create Unlinked PCB (`PCBCreateCmd`)
|
||||
- SOLID › CREATE › Joint Origin (`JointOrigin`)
|
||||
- SOLID › CREATE › Generate Timeline (`TimelineBuilderCommand`)
|
||||
- SOLID › MODIFY › Press Pull (`FusionPressPullCommand`)
|
||||
- SOLID › MODIFY › Edit Face (`EditFaceCommand`)
|
||||
- SOLID › MODIFY › Shell (`FusionShellBodyCommand`)
|
||||
- SOLID › MODIFY › Draft (`FusionDraftCommand`)
|
||||
- SOLID › MODIFY › Scale (`ModifyScale`)
|
||||
- SOLID › MODIFY › Offset Face (`FusionOffsetFacesCommand`)
|
||||
- SOLID › MODIFY › Replace Face (`FusionReplaceFaceCommand`)
|
||||
- SOLID › MODIFY › Split Face (`FusionSplitFaceCommand`)
|
||||
- SOLID › MODIFY › Split Body (`FusionSplitBodyCommand`)
|
||||
- SOLID › MODIFY › Silhouette Split (`FusionPartingLineSplitCmd`)
|
||||
- SOLID › MODIFY › Align (`AlignCmd`)
|
||||
- SOLID › MODIFY › Remove (`SoftDeleteCommand`)
|
||||
- SOLID › MODIFY › Arrange (`ArrangeCommand`)
|
||||
- SOLID › MODIFY › Remove Features (`FusionRemoveFeaturesCommand`)
|
||||
- SOLID › MODIFY › Remove Faces (`FusionRemoveFacesCommand`)
|
||||
- SOLID › MODIFY › Replace with Primitives (`ReplaceWithPrimitivesCommand`)
|
||||
- SOLID › MODIFY › Volumetric Lattice (`VolFieldCreateLatticeCommand`)
|
||||
- SOLID › MODIFY › Volumetric Texture (`VolFieldTextureCommand`)
|
||||
- SOLID › MODIFY › VolFieldModifyModelCommand (`VolFieldModifyModelCommand`)
|
||||
- SOLID › MODIFY › Physical Material (`PhysicalMaterialCommand`)
|
||||
- SOLID › MODIFY › Appearance (`AppearanceCommand`)
|
||||
- SOLID › MODIFY › Manage Materials (`MaterialCommand`)
|
||||
- SOLID › MODIFY › Compute Unresolved (`FusionComputeUnresolvedCommand`)
|
||||
- SOLID › MODIFY › Convert (`TSpline2BRepCommand`)
|
||||
- SOLID › MODIFY › Bill of Materials (`MaterializedBomHybridCmd`)
|
||||
- SOLID › ASSEMBLE › Insert Component (`FusionImportCommandFromToolbar`)
|
||||
- SOLID › ASSEMBLE › New Component (`FusionCreateNewComponentCommand`)
|
||||
- SOLID › ASSEMBLE › Duplicate With Joints (`FusionDuplicateWithJointsCommand`)
|
||||
- SOLID › ASSEMBLE › Constrain Components (`AssemblyMateCommand`)
|
||||
- SOLID › ASSEMBLE › Align Constraint (`AlignConstraintCmd`)
|
||||
- SOLID › ASSEMBLE › Angle Constraint (`AngleConstraintCmd`)
|
||||
- SOLID › ASSEMBLE › Center Constraint (`MidplaneConstraintCmd`)
|
||||
- SOLID › ASSEMBLE › Tangent Constraint (`TangentConstraintCmd`)
|
||||
- SOLID › ASSEMBLE › Joint Constraint (`JointConstraintCmd`)
|
||||
- SOLID › ASSEMBLE › Joint (`JointAssembleCmdNew`)
|
||||
- SOLID › ASSEMBLE › As-Built Joint (`JointAsBuiltCmd`)
|
||||
- SOLID › ASSEMBLE › As-Built Constraint (`AsBuiltConstraintCmd`)
|
||||
- SOLID › ASSEMBLE › Rigid Group (`RigidGroupCmd`)
|
||||
- SOLID › ASSEMBLE › Drive Joints (`FusionMoveJointsCommand`)
|
||||
- SOLID › ASSEMBLE › Motion Link (`FusionMotionRelationshipCommand`)
|
||||
- SOLID › ASSEMBLE › Motion Study (`FusionMotionStudyCommand`)
|
||||
- SOLID › ASSEMBLE › Enable Contact Sets (`EnableContactSetsCmd`)
|
||||
- SOLID › ASSEMBLE › Enable All Contact (`EnableAllContactCmd`)
|
||||
- SOLID › ASSEMBLE › Disable Contact (`DisableAllContactCmd`)
|
||||
- SOLID › ASSEMBLE › New Contact Set (`ContactSetCmd`)
|
||||
- SOLID › CONFIGURE › Configure (`FusionStartDesignConfigModeCmd`)
|
||||
- SOLID › CONFIGURE › Display Configuration Table (`FusionShowDesignConfigPanelCmd`)
|
||||
- SOLID › CONFIGURE › Create Configuration Rules (`FusionConfigurationRulesCmd`)
|
||||
- SOLID › CONSTRUCT › User Coordinate System (UCS) (`CoordinateSystemCommand`)
|
||||
- SOLID › CONSTRUCT › Offset Plane (`ConstructionPlaneOffsetFromPlaneCommand`)
|
||||
- SOLID › CONSTRUCT › Plane at Angle (`ConstructionPlaneAtAngleCommand`)
|
||||
- SOLID › CONSTRUCT › Tangent Plane (`ConstructionTangentPlaneCommand`)
|
||||
- SOLID › CONSTRUCT › Midplane (`ConstructionMidPlaneCommand`)
|
||||
- SOLID › CONSTRUCT › Perpendicular Plane (`ConstructionPlanePerpendicularToPlaneCommand`)
|
||||
- SOLID › CONSTRUCT › Plane Through Two Edges (`ConstructionPlaneFromTwoEdgesCommand`)
|
||||
- SOLID › CONSTRUCT › Plane Through Three Points (`ConstructionPlaneFromThreePointsCommand`)
|
||||
- SOLID › CONSTRUCT › Plane Along Path (`ConstructionPlaneAlongPathCommand`)
|
||||
- SOLID › CONSTRUCT › Axis Through Cylinder/Cone/Torus (`ConstructionAxisThroughCylinderCommand`)
|
||||
- SOLID › CONSTRUCT › Axis Perpendicular To Face (`ConstructionAxisNormalToFaceCommand`)
|
||||
- SOLID › CONSTRUCT › Axis Through Two Planes (`ConstructionAxisFromTwoPlanesCommand`)
|
||||
- SOLID › CONSTRUCT › Axis Through Two Points (`ConstructionAxisThroughTwoPointsCommand`)
|
||||
- SOLID › CONSTRUCT › Axis Through Edge (`ConstructionAxisThroughEdgeCommand`)
|
||||
- SOLID › CONSTRUCT › Point At Vertex (`ConstructionPointFromPointCommand`)
|
||||
- SOLID › CONSTRUCT › Point Through Two Edges (`ConstructionPointThroughTwoEdgesCommand`)
|
||||
- SOLID › CONSTRUCT › Point Through Three Planes (`ConstructionPointFromThreePlanesCommand`)
|
||||
- SOLID › CONSTRUCT › Point At Center Of Circle/Sphere/Torus (`ConstructionPointFromCircleOrSphereCommand`)
|
||||
- SOLID › CONSTRUCT › Point At Edge And Plane (`ConstructionPointAtEdgeAndPlaneCommand`)
|
||||
- SOLID › CONSTRUCT › Point Along Path (`ConstructionPointAlongPathCommand`)
|
||||
- SOLID › INSPECT › Interference (`InterferenceCheckCommand`)
|
||||
- SOLID › INSPECT › Section Analysis (`FusionHalfSectionViewCommand`)
|
||||
- SOLID › INSPECT › Curvature Comb Analysis (`FusionCurvatureCombAnalysisCommand`)
|
||||
- SOLID › INSPECT › Curvature Map Analysis (`FusionCurvatureMapAnalysisCommand`)
|
||||
- SOLID › INSPECT › Draft Analysis (`FusionDraftAnalysisCommand`)
|
||||
- SOLID › INSPECT › Environment Map Analysis (`FusionEnvironmentMapAnalysisCommand`)
|
||||
- SOLID › INSPECT › Isocurve Analysis (`FusionIsoCurveAnalysisCommand`)
|
||||
- SOLID › INSPECT › Validate (`FusionSurfaceValidateCommand`)
|
||||
- SOLID › INSPECT › Zebra Analysis (`FusionZebraAnalysisCommand`)
|
||||
- SOLID › INSPECT › Accessibility Analysis (`FusionAccessibilityAnalysisCommand`)
|
||||
- SOLID › INSPECT › Center of Mass (`FusionCenterOfMassCommand`)
|
||||
- SOLID › INSPECT › Minimum Radius Analysis (`FusionMinimumRadiusAnalysisCommand`)
|
||||
- SOLID › INSPECT › Component Connection Analysis (`FusionComponentConnectionAnalysisCommand`)
|
||||
- SOLID › INSPECT › DOF Analysis (`FusionDOFAnalysisCommand`)
|
||||
- SOLID › INSPECT › Edit Component Connection Analysis (`FusionEditComponentConnectionAnalysisCommand`)
|
||||
- SOLID › INSPECT › Fastener Stack Analysis (`FusionFastenerStackAnalysisCommand`)
|
||||
- SOLID › INSPECT › Find Similar Components (`SimilarPartsCommand`)
|
||||
- SOLID › INSPECT › Navigator (`GraphViewCmd`)
|
||||
- SOLID › INSPECT › Display Component Colors (`FusionViewColorCyclingToggleCmd`)
|
||||
- SOLID › INSPECT › Display Mesh Face Groups (`FacegroupsToggleCmd`)
|
||||
- SOLID › INSPECT › Assembly Statistics (`FusionAssemblyStatisticsCommand`)
|
||||
- SOLID › INSPECT › Change Summary (`ShowChangeAwarenessToolbarCmd`)
|
||||
- SOLID › INSPECT › History (`ShowHistoryToolbarCmd`)
|
||||
- SOLID › INSPECT › Sustainability Insights (`GravityClimate_ManufacturingSustainabilityInsights_MainDialog`)
|
||||
- SOLID › INSERT › Decal (`FusionAddEditDecalCommand`)
|
||||
- SOLID › INSERT › Canvas (`FusionAddCanvasCommand`)
|
||||
- SOLID › INSERT › Insert T-Spline (`TSplineImportCommand`)
|
||||
- SOLID › INSERT › Insert SVG (`SketchImportSVG`)
|
||||
- SOLID › INSERT › Insert DXF (`ImportDxfFileCommand`)
|
||||
- SOLID › INSERT › Insert Mesh (`ParaMeshInsertAlignCommand`)
|
||||
- SOLID › INSERT › Insert Fastener (`FusionFastenersCommand`)
|
||||
- SOLID › INSERT › Insert Derive (`PullDeriveCommand`)
|
||||
- SOLID › INSERT › Insert McMaster-Carr Component (`InsertMcMasterCarrComponentCommand`)
|
||||
- SOLID › INSERT › Insert a manufacturer part (`cadenasparts4cad`)
|
||||
- SOLID › INSERT › Insert TraceParts Supplier Components (`traceparts_insert`)
|
||||
- SOLID › ASSEMBLE › Add To Assembly (`NewAssemblyFromPartCommand`)
|
||||
- SOLID › SELECT › Select (`SelectCommand`)
|
||||
- SOLID › SELECT › Window Selection (`selectWindow`)
|
||||
- SOLID › SELECT › Freeform Selection (`selectFreeForm`)
|
||||
- SOLID › SELECT › Paint Selection (`selectPaint`)
|
||||
- SOLID › SELECT › Select By Name (`SelectByNameCommand`)
|
||||
- SOLID › SELECT › Select By Boundary (`SelectByBoundaryCommand`)
|
||||
- SOLID › SELECT › Select By Size (`FusionSelectBodiesBySizeCommand`)
|
||||
- SOLID › SELECT › Invert Selection (`SelectByInvertCommand`)
|
||||
- SOLID › SELECT › Seed And Boundary (`FusionSelectSeedAndBoundaryFacesCommand`)
|
||||
- SOLID › SELECT › Component Drag (`FusionDragCompControlsCmd`)
|
||||
- SOLID › SELECT › Select Body Priority (`SelectBodyPriorityCommand`)
|
||||
- SOLID › SELECT › Select Face Priority (`SelectFacePriorityCommand`)
|
||||
- SOLID › SELECT › Select Edge Priority (`SelectEdgePriorityCommand`)
|
||||
- SOLID › SELECT › Select Component Priority (`SelectComponentPriorityCommand`)
|
||||
- SOLID › SELECT › Selection Filters (`SelectionFilterCommand`)
|
||||
- SOLID › POSITION › Capture Position (`SnapshotCmd`)
|
||||
- SOLID › POSITION › Revert Position (`AsBuiltPositionsCmd`)
|
||||
- SKETCH › CREATE › Midpoint Line (`SketchMidpointLine`)
|
||||
- SKETCH › CREATE › SketchConstrainer (`SketchConstrainer`)
|
||||
- SKETCH › CREATE › 2-Tangent Circle (`CircleTanTanRadius`)
|
||||
- SKETCH › CREATE › 3-Tangent Circle (`CircleThreeTangent`)
|
||||
- SKETCH › CREATE › Tangent Arc (`ArcTangent`)
|
||||
- SKETCH › CREATE › Ellipse (`CircleElipse`)
|
||||
- SKETCH › CREATE › Center Point Slot (`ShapeSlotCenterPoint`)
|
||||
- SKETCH › CREATE › Three Point Arc Slot (`ShapeArcSlotThreePoint`)
|
||||
- SKETCH › CREATE › Center Point Arc Slot (`ShapeArcSlotCenterTwoPoint`)
|
||||
- SKETCH › CREATE › Fit Point Spline (`DrawSpline`)
|
||||
- SKETCH › CREATE › Control Point Spline (`DrawCVMSpline3D`)
|
||||
- SKETCH › CREATE › Control Point Spline (`DrawCVMSpline5D`)
|
||||
- SKETCH › CREATE › Conic Curve (`ConicCurveCmd`)
|
||||
- SKETCH › CREATE › Text (`MTextCmd`)
|
||||
- SKETCH › CREATE › Fit Curves to Mesh Section (`FitCurvesToSectionCommand`)
|
||||
- SKETCH › CREATE › Mirror (`MirrorSketchCommand`)
|
||||
- SKETCH › CREATE › Circular Pattern (`CircularSketchPatternCommand`)
|
||||
- SKETCH › CREATE › Rectangular Pattern (`RectangularSketchPatternCommand`)
|
||||
- SKETCH › CREATE › Project (`ProjectNewCmd`)
|
||||
- SKETCH › CREATE › Intersect (`IntersectCmd`)
|
||||
- SKETCH › CREATE › Spun Profile (`SpunProfileCmd`)
|
||||
- SKETCH › CREATE › Include 3D Geometry (`Include3DGeometry`)
|
||||
- SKETCH › CREATE › Project To Surface (`ProjectToSurface`)
|
||||
- SKETCH › CREATE › Intersection Curve (`IntersectionCurve`)
|
||||
- SKETCH › CREATE › Isoparametric Curve (`SketchIsoparametricCurve`)
|
||||
- SKETCH › MODIFY › Fillet (`FilletSketchCmd`)
|
||||
- SKETCH › MODIFY › Equal Distance Chamfer (`ChamferSketchEqualDistance`)
|
||||
- SKETCH › MODIFY › Distance and Angle Chamfer (`ChamferSketchDistanceAngle`)
|
||||
- SKETCH › MODIFY › Two Distance Chamfer (`ChamferSketchDistanceDistance`)
|
||||
- SKETCH › MODIFY › Blend Curve (`BlendG1CurveSketchCmd`)
|
||||
- SKETCH › MODIFY › Offset (`Offset`)
|
||||
- SKETCH › MODIFY › Trim (`TrimSketchCmd`)
|
||||
- SKETCH › MODIFY › Extend (`ExtendSketchCmd`)
|
||||
- SKETCH › MODIFY › Break (`BreakSketchCmd`)
|
||||
- SKETCH › MODIFY › Sketch Scale (`SketchScaleCmd`)
|
||||
- SKETCH › CONSTRAINTS › AutoConstrain (`SketchAutoConstraintAndDimCmd`)
|
||||
- SKETCH › CONSTRAINTS › AutoConstrain from datum (`SketchAutoConstraintAndDimFromDatumCmd`)
|
||||
- SKETCH › CONSTRAINTS › Curvature (`ConstraintSmooth`)
|
||||
- SKETCH › CONSTRAINTS › Polygon (`SketchPolygonConstraintCmd`)
|
||||
- SKETCH › CONFIGURE › Configure (`FusionStartDesignConfigModeCmd`)
|
||||
- SKETCH › CONFIGURE › Display Configuration Table (`FusionShowDesignConfigPanelCmd`)
|
||||
- SKETCH › CONFIGURE › Create Configuration Rules (`FusionConfigurationRulesCmd`)
|
||||
- SKETCH › INSPECT › Interference (`InterferenceCheckCommand`)
|
||||
- SKETCH › INSPECT › Section Analysis (`FusionHalfSectionViewCommand`)
|
||||
- SKETCH › INSPECT › Curvature Comb Analysis (`FusionCurvatureCombAnalysisCommand`)
|
||||
- SKETCH › INSPECT › Curvature Map Analysis (`FusionCurvatureMapAnalysisCommand`)
|
||||
- SKETCH › INSPECT › Draft Analysis (`FusionDraftAnalysisCommand`)
|
||||
- SKETCH › INSPECT › Environment Map Analysis (`FusionEnvironmentMapAnalysisCommand`)
|
||||
- SKETCH › INSPECT › Isocurve Analysis (`FusionIsoCurveAnalysisCommand`)
|
||||
- SKETCH › INSPECT › Validate (`FusionSurfaceValidateCommand`)
|
||||
- SKETCH › INSPECT › Zebra Analysis (`FusionZebraAnalysisCommand`)
|
||||
- SKETCH › INSPECT › Accessibility Analysis (`FusionAccessibilityAnalysisCommand`)
|
||||
- SKETCH › INSPECT › Center of Mass (`FusionCenterOfMassCommand`)
|
||||
- SKETCH › INSPECT › Minimum Radius Analysis (`FusionMinimumRadiusAnalysisCommand`)
|
||||
- SKETCH › INSPECT › Component Connection Analysis (`FusionComponentConnectionAnalysisCommand`)
|
||||
- SKETCH › INSPECT › DOF Analysis (`FusionDOFAnalysisCommand`)
|
||||
- SKETCH › INSPECT › Edit Component Connection Analysis (`FusionEditComponentConnectionAnalysisCommand`)
|
||||
- SKETCH › INSPECT › Fastener Stack Analysis (`FusionFastenerStackAnalysisCommand`)
|
||||
- SKETCH › INSPECT › Find Similar Components (`SimilarPartsCommand`)
|
||||
- SKETCH › INSPECT › Navigator (`GraphViewCmd`)
|
||||
- SKETCH › INSPECT › Display Component Colors (`FusionViewColorCyclingToggleCmd`)
|
||||
- SKETCH › INSPECT › Display Mesh Face Groups (`FacegroupsToggleCmd`)
|
||||
- SKETCH › INSPECT › Assembly Statistics (`FusionAssemblyStatisticsCommand`)
|
||||
- SKETCH › INSPECT › Change Summary (`ShowChangeAwarenessToolbarCmd`)
|
||||
- SKETCH › INSPECT › History (`ShowHistoryToolbarCmd`)
|
||||
- SKETCH › INSPECT › Sustainability Insights (`GravityClimate_ManufacturingSustainabilityInsights_MainDialog`)
|
||||
- SKETCH › INSERT › Decal (`FusionAddEditDecalCommand`)
|
||||
- SKETCH › INSERT › Canvas (`FusionAddCanvasCommand`)
|
||||
- SKETCH › INSERT › Insert T-Spline (`TSplineImportCommand`)
|
||||
- SKETCH › INSERT › Insert SVG (`SketchImportSVG`)
|
||||
- SKETCH › INSERT › Insert DXF (`ImportDxfFileCommand`)
|
||||
- SKETCH › INSERT › Insert Mesh (`ParaMeshInsertAlignCommand`)
|
||||
- SKETCH › INSERT › Insert Component (`FusionImportCommandFromToolbar`)
|
||||
- SKETCH › INSERT › Insert Fastener (`FusionFastenersCommand`)
|
||||
- SKETCH › INSERT › Duplicate With Joints (`FusionDuplicateWithJointsCommand`)
|
||||
- SKETCH › INSERT › Insert Derive (`PullDeriveCommand`)
|
||||
- SKETCH › INSERT › Insert McMaster-Carr Component (`InsertMcMasterCarrComponentCommand`)
|
||||
- SKETCH › INSERT › Insert a manufacturer part (`cadenasparts4cad`)
|
||||
- SKETCH › INSERT › Insert TraceParts Supplier Components (`traceparts_insert`)
|
||||
- SKETCH › ASSEMBLE › Add To Assembly (`NewAssemblyFromPartCommand`)
|
||||
- SKETCH › SELECT › Select (`SelectCommand`)
|
||||
- SKETCH › SELECT › Window Selection (`selectWindow`)
|
||||
- SKETCH › SELECT › Freeform Selection (`selectFreeForm`)
|
||||
- SKETCH › SELECT › Paint Selection (`selectPaint`)
|
||||
- SKETCH › SELECT › Select By Name (`SelectByNameCommand`)
|
||||
- SKETCH › SELECT › Select By Boundary (`SelectByBoundaryCommand`)
|
||||
- SKETCH › SELECT › Select By Size (`FusionSelectBodiesBySizeCommand`)
|
||||
- SKETCH › SELECT › Invert Selection (`SelectByInvertCommand`)
|
||||
- SKETCH › SELECT › Seed And Boundary (`FusionSelectSeedAndBoundaryFacesCommand`)
|
||||
- SKETCH › SELECT › Component Drag (`FusionDragCompControlsCmd`)
|
||||
- SKETCH › SELECT › Select Body Priority (`SelectBodyPriorityCommand`)
|
||||
- SKETCH › SELECT › Select Face Priority (`SelectFacePriorityCommand`)
|
||||
- SKETCH › SELECT › Select Edge Priority (`SelectEdgePriorityCommand`)
|
||||
- SKETCH › SELECT › Select Component Priority (`SelectComponentPriorityCommand`)
|
||||
- SKETCH › SELECT › Selection Filters (`SelectionFilterCommand`)
|
||||
- SKETCH › FINISH SKETCH › Finish with AutoConstrain (`SketchAutoConstrainAndFinish`)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,39 @@
|
||||
//! `cargo xtask assets`: every non-code asset in the repository must have a row in
|
||||
//! ATTRIBUTION.md (author, source, licence). Icons are drawn in code and need no file.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
const ASSET_EXT: &[&str] = &["png", "jpg", "jpeg", "svg", "ico", "icns", "ttf", "otf", "woff", "woff2", "gif", "bmp", "webp", "wav", "mp3", "stl", "step", "stp", "obj"];
|
||||
const SKIP_DIRS: &[&str] = &["target", ".git", "plan", "dist"];
|
||||
|
||||
fn walk(dir: &Path, root: &Path, out: &mut Vec<String>) {
|
||||
let Ok(rd) = std::fs::read_dir(dir) else { return };
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
if p.is_dir() {
|
||||
if !SKIP_DIRS.contains(&name.as_str()) && !name.starts_with('.') {
|
||||
walk(&p, root, out);
|
||||
}
|
||||
} else if let Some(ext) = p.extension().map(|x| x.to_string_lossy().to_ascii_lowercase())
|
||||
&& ASSET_EXT.contains(&ext.as_str())
|
||||
&& let Ok(rel) = p.strip_prefix(root)
|
||||
{
|
||||
out.push(rel.to_string_lossy().replace('\\', "/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(root: &Path) -> Result<(), String> {
|
||||
let attribution = std::fs::read_to_string(root.join("ATTRIBUTION.md")).map_err(|e| format!("ATTRIBUTION.md: {e}"))?;
|
||||
let mut files = Vec::new();
|
||||
walk(root, root, &mut files);
|
||||
files.sort();
|
||||
let missing: Vec<&String> = files.iter().filter(|f| !attribution.contains(f.as_str())).collect();
|
||||
if missing.is_empty() {
|
||||
println!("assets: {} asset file(s), all attributed", files.len());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("assets missing from ATTRIBUTION.md:\n{}", missing.iter().map(|m| format!(" {m}")).collect::<Vec<_>>().join("\n")))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
//! Dependency layering rules (plan/architecture.md §3).
|
||||
//!
|
||||
//! Adapted from CADCraft's xtask (MIT OR Apache-2.0), see ATTRIBUTION.md.
|
||||
//!
|
||||
//! The rule engine works on a small, metadata-independent model so it can be
|
||||
//! unit-tested; `from_metadata` builds that model from `cargo metadata`.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// Where a workspace crate sits.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Class {
|
||||
/// Regular layered crate.
|
||||
Layer(u8),
|
||||
/// Layer 0, and additionally may depend on no workspace crate at all.
|
||||
Standalone,
|
||||
/// Test tooling: may depend on anything up to L5 (it sits at L6 for rule
|
||||
/// purposes); other crates may use it only as a dev-dependency.
|
||||
Testkit,
|
||||
/// Binaries and build tooling: exempt from the rules.
|
||||
Exempt,
|
||||
}
|
||||
|
||||
impl Class {
|
||||
fn layer(self) -> Option<u8> {
|
||||
match self {
|
||||
Class::Layer(l) => Some(l),
|
||||
Class::Standalone => Some(0),
|
||||
Class::Testkit => Some(6),
|
||||
Class::Exempt => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The layering table. Names are package names without the `solvecraft-`
|
||||
/// prefix.
|
||||
pub const TABLE: &[(&str, Class)] = &[
|
||||
("geom", Class::Layer(0)),
|
||||
("sketch", Class::Layer(1)),
|
||||
("kernel", Class::Layer(1)),
|
||||
("render", Class::Layer(1)),
|
||||
("doc", Class::Layer(2)),
|
||||
("io", Class::Layer(3)),
|
||||
("engine", Class::Layer(4)),
|
||||
("ui-egui", Class::Layer(5)),
|
||||
("testkit", Class::Testkit),
|
||||
// apps and tooling
|
||||
("solvecraft", Class::Exempt),
|
||||
("cli", Class::Exempt),
|
||||
("xtask", Class::Exempt),
|
||||
];
|
||||
|
||||
/// Explicit orderings *within* a layer (earlier may be used by later).
|
||||
pub const INTRA_LAYER_ORDER: &[&[&str]] = &[];
|
||||
|
||||
fn intra_layer_allowed(from: &str, to: &str) -> bool {
|
||||
let (from, to) = (short_name(from), short_name(to));
|
||||
INTRA_LAYER_ORDER.iter().any(|chain| match (chain.iter().position(|n| *n == from), chain.iter().position(|n| *n == to)) {
|
||||
(Some(f), Some(t)) => t < f,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// External crates that constitute a UI toolkit / windowing dependency.
|
||||
/// Entries ending in `*` are prefixes.
|
||||
pub const UI_CRATES: &[&str] = &["egui", "eframe", "egui-wgpu", "wgpu", "winit", "egui_kittest", "rfd", "bevy*"];
|
||||
|
||||
/// First layer allowed to use UI crates.
|
||||
pub const UI_MIN_LAYER: u8 = 5;
|
||||
|
||||
pub fn short_name(pkg: &str) -> &str {
|
||||
pkg.strip_prefix("solvecraft-").unwrap_or(pkg)
|
||||
}
|
||||
|
||||
pub fn classify(pkg: &str) -> Option<Class> {
|
||||
let s = short_name(pkg);
|
||||
TABLE.iter().find(|(n, _)| *n == s).map(|(_, c)| *c)
|
||||
}
|
||||
|
||||
fn is_ui_crate(name: &str) -> bool {
|
||||
UI_CRATES.iter().any(|p| match p.strip_suffix('*') {
|
||||
Some(prefix) => name.starts_with(prefix),
|
||||
None => name == *p,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DepKind {
|
||||
Normal,
|
||||
Dev,
|
||||
Build,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Dep {
|
||||
pub name: String,
|
||||
pub kind: DepKind,
|
||||
/// `true` if the dependency is a workspace member.
|
||||
pub workspace: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Crate {
|
||||
pub name: String,
|
||||
pub deps: Vec<Dep>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Violation {
|
||||
Unregistered { krate: String },
|
||||
Upward { krate: String, dep: String, from: u8, to: u8, kind: DepKind },
|
||||
StandaloneHasWorkspaceDep { krate: String, dep: String },
|
||||
TestkitAsNormalDep { krate: String },
|
||||
UiBelowL5 { krate: String, dep: String, layer: u8 },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Violation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Violation::Unregistered { krate } => {
|
||||
write!(f, "{krate}: unknown workspace crate; register it in xtask/src/layers.rs TABLE (see plan/architecture.md §3)")
|
||||
}
|
||||
Violation::Upward { krate, dep, from, to, kind } => {
|
||||
write!(f, "{krate} (L{from}) -> {dep} (L{to}) [{kind:?}]: may only depend on strictly lower layers")
|
||||
}
|
||||
Violation::StandaloneHasWorkspaceDep { krate, dep } => {
|
||||
write!(f, "{krate}: standalone crate must not depend on workspace crate {dep}")
|
||||
}
|
||||
Violation::TestkitAsNormalDep { krate } => {
|
||||
write!(f, "{krate}: solvecraft-testkit may only be a dev-dependency")
|
||||
}
|
||||
Violation::UiBelowL5 { krate, dep, layer } => {
|
||||
write!(f, "{krate} (L{layer}) depends on UI crate `{dep}`; UI toolkits are only allowed in L5+")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check all rules. Returns violations sorted for stable output.
|
||||
pub fn check(crates: &[Crate]) -> Vec<Violation> {
|
||||
let mut out = Vec::new();
|
||||
for c in crates {
|
||||
let Some(class) = classify(&c.name) else {
|
||||
out.push(Violation::Unregistered { krate: c.name.clone() });
|
||||
continue;
|
||||
};
|
||||
if class == Class::Exempt {
|
||||
continue;
|
||||
}
|
||||
let layer = class.layer().unwrap_or(0);
|
||||
for d in &c.deps {
|
||||
// Self dev-dependencies (e.g. to enable features in tests) are fine.
|
||||
if d.name == c.name {
|
||||
continue;
|
||||
}
|
||||
if d.workspace {
|
||||
if class == Class::Standalone {
|
||||
out.push(Violation::StandaloneHasWorkspaceDep { krate: c.name.clone(), dep: d.name.clone() });
|
||||
continue;
|
||||
}
|
||||
match classify(&d.name) {
|
||||
// Unregistered deps are reported on their own entry.
|
||||
None => {}
|
||||
Some(Class::Testkit) => {
|
||||
if d.kind != DepKind::Dev {
|
||||
out.push(Violation::TestkitAsNormalDep { krate: c.name.clone() });
|
||||
}
|
||||
}
|
||||
Some(dc) => {
|
||||
let to = dc.layer().unwrap_or(u8::MAX);
|
||||
if to >= layer && !(to == layer && intra_layer_allowed(&c.name, &d.name)) {
|
||||
out.push(Violation::Upward {
|
||||
krate: c.name.clone(),
|
||||
dep: d.name.clone(),
|
||||
from: layer,
|
||||
to: if to == u8::MAX { 7 } else { to },
|
||||
kind: d.kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if layer < UI_MIN_LAYER && is_ui_crate(&d.name) {
|
||||
out.push(Violation::UiBelowL5 { krate: c.name.clone(), dep: d.name.clone(), layer });
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort_by_key(|v| v.to_string());
|
||||
out.dedup();
|
||||
out
|
||||
}
|
||||
|
||||
/// Build the model from `cargo metadata --format-version 1 --no-deps`.
|
||||
pub fn from_metadata(meta: &Value) -> Result<Vec<Crate>, String> {
|
||||
let pkgs = meta["packages"].as_array().ok_or("metadata: no packages array")?;
|
||||
let members: Vec<&str> = pkgs.iter().filter_map(|p| p["name"].as_str()).collect();
|
||||
let mut out = Vec::new();
|
||||
for p in pkgs {
|
||||
let name = p["name"].as_str().ok_or("package without name")?.to_owned();
|
||||
let mut deps = Vec::new();
|
||||
for d in p["dependencies"].as_array().into_iter().flatten() {
|
||||
let dname = d["name"].as_str().unwrap_or_default().to_owned();
|
||||
let kind = match d["kind"].as_str() {
|
||||
Some("dev") => DepKind::Dev,
|
||||
Some("build") => DepKind::Build,
|
||||
_ => DepKind::Normal,
|
||||
};
|
||||
let workspace = members.contains(&dname.as_str()) || d["path"].is_string();
|
||||
deps.push(Dep { name: dname, kind, workspace });
|
||||
}
|
||||
out.push(Crate { name, deps });
|
||||
}
|
||||
out.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn describe(class: Option<Class>) -> String {
|
||||
match class {
|
||||
Some(Class::Layer(l)) => format!("L{l}"),
|
||||
Some(Class::Standalone) => "L0 standalone".into(),
|
||||
Some(Class::Testkit) => "testkit".into(),
|
||||
Some(Class::Exempt) => "exempt".into(),
|
||||
None => "UNREGISTERED".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn c(name: &str, deps: &[(&str, DepKind, bool)]) -> Crate {
|
||||
Crate { name: name.into(), deps: deps.iter().map(|(n, k, w)| Dep { name: (*n).into(), kind: *k, workspace: *w }).collect() }
|
||||
}
|
||||
use DepKind::*;
|
||||
|
||||
#[test]
|
||||
fn clean_downward_graph_passes() {
|
||||
let g = [
|
||||
c("solvecraft-geom", &[("kurbo", Normal, false)]),
|
||||
c("solvecraft-doc", &[("solvecraft-geom", Normal, true)]),
|
||||
c("solvecraft-engine", &[("solvecraft-doc", Normal, true), ("solvecraft-testkit", Dev, true)]),
|
||||
c("solvecraft-ui-egui", &[("solvecraft-engine", Normal, true), ("egui", Normal, false)]),
|
||||
c("solvecraft-cli", &[("solvecraft-ui-egui", Normal, true)]),
|
||||
];
|
||||
assert!(check(&g).is_empty(), "{:?}", check(&g));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upward_dependency_flagged() {
|
||||
let v = check(&[c("solvecraft-doc", &[("solvecraft-engine", Normal, true)])]);
|
||||
assert!(matches!(v[..], [Violation::Upward { from: 2, to: 4, .. }]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sideways_dependency_flagged() {
|
||||
let v = check(&[c("solvecraft-kernel", &[("solvecraft-sketch", Normal, true)])]);
|
||||
assert!(matches!(v[..], [Violation::Upward { from: 1, to: 1, .. }]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_dev_dependency_ignored() {
|
||||
assert!(check(&[c("solvecraft-doc", &[("solvecraft-doc", Dev, true)])]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upward_dev_dependency_flagged() {
|
||||
let v = check(&[c("solvecraft-geom", &[("solvecraft-doc", Dev, true)])]);
|
||||
assert!(matches!(v[..], [Violation::Upward { kind: Dev, .. }]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_crates_below_l6_flagged() {
|
||||
for dep in ["egui", "eframe", "winit", "egui_kittest", "rfd", "bevy_ecs", "bevy"] {
|
||||
let v = check(&[c("solvecraft-engine", &[(dep, Normal, false)])]);
|
||||
assert!(matches!(v[..], [Violation::UiBelowL5 { layer: 4, .. }]), "{dep}");
|
||||
}
|
||||
assert!(check(&[c("solvecraft-engine", &[("egui_extras_not", Normal, false)])]).is_empty());
|
||||
assert!(check(&[c("solvecraft-ui-egui", &[("winit", Normal, false)])]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unregistered_crate_is_error() {
|
||||
let v = check(&[c("solvecraft-mystery", &[])]);
|
||||
assert!(matches!(&v[..], [Violation::Unregistered { krate }] if krate == "solvecraft-mystery"));
|
||||
assert!(v[0].to_string().contains("register"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn testkit_only_as_dev_dependency() {
|
||||
let v = check(&[c("solvecraft-doc", &[("solvecraft-testkit", Normal, true)])]);
|
||||
assert!(matches!(v[..], [Violation::TestkitAsNormalDep { .. }]));
|
||||
assert!(check(&[c("solvecraft-doc", &[("solvecraft-testkit", Dev, true)])]).is_empty());
|
||||
assert!(check(&[c("solvecraft-testkit", &[("solvecraft-engine", Normal, true)])]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apps_and_xtask_exempt() {
|
||||
for app in ["solvecraft", "solvecraft-cli", "xtask"] {
|
||||
assert!(check(&[c(app, &[("egui", Normal, false), ("solvecraft-ui-egui", Normal, true)])]).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_parsing() {
|
||||
let meta: Value = serde_json::from_str(
|
||||
r#"{"packages":[
|
||||
{"name":"solvecraft-doc","dependencies":[
|
||||
{"name":"solvecraft-geom","kind":null,"path":"/x/crates/geom"},
|
||||
{"name":"serde","kind":null},
|
||||
{"name":"proptest","kind":"dev"}]},
|
||||
{"name":"solvecraft-geom","dependencies":[]}
|
||||
]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let g = from_metadata(&meta).unwrap();
|
||||
assert_eq!(g.len(), 2);
|
||||
let doc = g.iter().find(|c| c.name == "solvecraft-doc").unwrap();
|
||||
assert!(doc.deps[0].workspace && !doc.deps[1].workspace);
|
||||
assert_eq!(doc.deps[2].kind, Dev);
|
||||
assert!(check(&g).is_empty());
|
||||
}
|
||||
}
|
||||
+177
-1
@@ -1 +1,177 @@
|
||||
fn main() {}
|
||||
//! Workspace tooling: `cargo xtask <command>`.
|
||||
//!
|
||||
//! Pure Rust (std + serde_json). External tools (`cargo`) are invoked through
|
||||
//! `std::process::Command`.
|
||||
|
||||
mod assets;
|
||||
mod layers;
|
||||
mod parity;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, ExitCode};
|
||||
|
||||
const USAGE: &str = "\
|
||||
usage: cargo xtask <command>
|
||||
|
||||
commands:
|
||||
layers enforce the crate dependency layering (plan/architecture.md §3)
|
||||
assets check that every non-code asset file is attributed in ATTRIBUTION.md
|
||||
parity [--refresh]
|
||||
recompute Fusion command parity in docs/parity.md (--refresh rebuilds
|
||||
xtask/data/fusion-catalog.tsv from plan/fusion/menu-tree.json)
|
||||
oracle replay plan/fusion/oracle/*/recipe.json and compare with measure.json;
|
||||
writes docs/oracle.md
|
||||
ci fmt --check, clippy -D warnings, test, assets, layers (stops at first failure)
|
||||
";
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let rest: Vec<&str> = args.iter().skip(1).map(String::as_str).collect();
|
||||
let result = match args.first().map(String::as_str) {
|
||||
Some("layers") => cmd_layers(),
|
||||
Some("assets") => assets::run(&root()),
|
||||
Some("parity") => parity::run(&root(), rest.contains(&"--refresh")),
|
||||
Some("oracle") => cmd_oracle(),
|
||||
Some("ci") => cmd_ci(),
|
||||
Some("-h" | "--help" | "help") | None => {
|
||||
print!("{USAGE}");
|
||||
Ok(())
|
||||
}
|
||||
Some(other) => Err(format!("unknown command `{other}`\n\n{USAGE}")),
|
||||
};
|
||||
match result {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Workspace root (parent of the xtask crate).
|
||||
pub fn root() -> PathBuf {
|
||||
let here = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
here.parent().unwrap_or(here).to_path_buf()
|
||||
}
|
||||
|
||||
pub fn cargo() -> Command {
|
||||
let mut c = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()));
|
||||
c.current_dir(root());
|
||||
c
|
||||
}
|
||||
|
||||
fn run(mut cmd: Command, what: &str) -> Result<(), String> {
|
||||
eprintln!("$ {what}");
|
||||
let status = cmd.status().map_err(|e| format!("{what}: failed to spawn: {e}"))?;
|
||||
if status.success() { Ok(()) } else { Err(format!("{what}: exited with {status}")) }
|
||||
}
|
||||
|
||||
pub fn metadata() -> Result<serde_json::Value, String> {
|
||||
let out = cargo().args(["metadata", "--format-version", "1", "--no-deps"]).output().map_err(|e| format!("cargo metadata: {e}"))?;
|
||||
if !out.status.success() {
|
||||
return Err(format!("cargo metadata failed:\n{}", String::from_utf8_lossy(&out.stderr)));
|
||||
}
|
||||
serde_json::from_slice(&out.stdout).map_err(|e| format!("cargo metadata: bad JSON: {e}"))
|
||||
}
|
||||
|
||||
fn cmd_layers() -> Result<(), String> {
|
||||
let crates = layers::from_metadata(&metadata()?)?;
|
||||
println!("Dependency layering (plan/architecture.md §3)\n");
|
||||
println!("{:<28} {:<14} workspace deps", "crate", "layer");
|
||||
for c in &crates {
|
||||
let ws: Vec<String> = c
|
||||
.deps
|
||||
.iter()
|
||||
.filter(|d| d.workspace)
|
||||
.map(|d| {
|
||||
let k = match d.kind {
|
||||
layers::DepKind::Normal => "",
|
||||
layers::DepKind::Dev => " (dev)",
|
||||
layers::DepKind::Build => " (build)",
|
||||
};
|
||||
format!("{}{k}", layers::short_name(&d.name))
|
||||
})
|
||||
.collect();
|
||||
println!("{:<28} {:<14} {}", c.name, layers::describe(layers::classify(&c.name)), ws.join(", "));
|
||||
}
|
||||
let violations = layers::check(&crates);
|
||||
println!();
|
||||
if violations.is_empty() {
|
||||
println!("OK: {} crates, no layering violations.", crates.len());
|
||||
Ok(())
|
||||
} else {
|
||||
println!("{} violation(s):", violations.len());
|
||||
for v in &violations {
|
||||
println!(" - {v}");
|
||||
}
|
||||
Err(format!("{} layering violation(s)", violations.len()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the CLI's oracle replay over every case directory and write docs/oracle.md.
|
||||
fn cmd_oracle() -> Result<(), String> {
|
||||
let dir = root().join("plan/fusion/oracle");
|
||||
let mut cases: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
.map_err(|e| format!("{}: {e} (the oracle data is local, in plan/fusion/oracle)", dir.display()))?
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.join("recipe.json").is_file() && p.join("measure.json").is_file())
|
||||
.collect();
|
||||
cases.sort();
|
||||
if cases.is_empty() {
|
||||
return Err("no oracle cases with recipe.json and measure.json".into());
|
||||
}
|
||||
let mut cmd = cargo();
|
||||
cmd.args(["run", "-q", "--release", "-p", "solvecraft-cli", "--", "oracle", "--json"]);
|
||||
for c in &cases {
|
||||
cmd.arg(c);
|
||||
}
|
||||
let out = cmd.output().map_err(|e| format!("solvecraft-cli: {e}"))?;
|
||||
let results: serde_json::Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("oracle JSON: {e}\n{}", String::from_utf8_lossy(&out.stderr)))?;
|
||||
let list = results.as_array().cloned().unwrap_or_default();
|
||||
let pass = list.iter().filter(|r| r["pass"].as_bool().unwrap_or(false)).count();
|
||||
let skipped = list.iter().filter(|r| r["skipped"].as_bool().unwrap_or(false)).count();
|
||||
let mut md = format!(
|
||||
"# Fusion oracle\n\nGenerated by `cargo xtask oracle`. Each case is a part built in Fusion (recipe and measurements kept locally in `plan/fusion/oracle/`, not committed). SolveCraft replays the recipe through its command engine and compares body count, volume and area (relative tolerance 1e-3) and face/edge/vertex counts (exact).\n\n**{pass} / {} cases pass** ({skipped} need features not implemented yet).\n\n| Case | Result | Notes |\n|---|---|---|\n",
|
||||
list.len()
|
||||
);
|
||||
for r in &list {
|
||||
let status = if r["pass"].as_bool().unwrap_or(false) {
|
||||
"pass"
|
||||
} else if r["skipped"].as_bool().unwrap_or(false) {
|
||||
"not yet"
|
||||
} else {
|
||||
"FAIL"
|
||||
};
|
||||
let mut notes: Vec<String> = r["checks"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|c| !c["ok"].as_bool().unwrap_or(false))
|
||||
.map(|c| format!("{}: got {} want {}", c["check"].as_str().unwrap_or(""), c["got"], c["want"]))
|
||||
.collect();
|
||||
if let Some(e) = r["error"].as_str() {
|
||||
notes.push(e.replace('|', "/"));
|
||||
}
|
||||
md += &format!("| {} | {status} | {} |\n", r["case"].as_str().unwrap_or(""), notes.join("; "));
|
||||
}
|
||||
std::fs::write(root().join("docs/oracle.md"), md).map_err(|e| format!("docs/oracle.md: {e}"))?;
|
||||
println!("oracle: {pass}/{} cases pass ({skipped} not yet supported) → docs/oracle.md", list.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_ci() -> Result<(), String> {
|
||||
let mut c = cargo();
|
||||
c.args(["fmt", "--all", "--", "--check"]);
|
||||
run(c, "cargo fmt --check")?;
|
||||
let mut c = cargo();
|
||||
c.args(["clippy", "--workspace", "--all-targets", "--release", "--", "-D", "warnings"]);
|
||||
run(c, "cargo clippy -D warnings")?;
|
||||
let mut c = cargo();
|
||||
c.args(["test", "--workspace", "--release"]);
|
||||
run(c, "cargo test")?;
|
||||
assets::run(&root())?;
|
||||
cmd_layers()?;
|
||||
eprintln!("ci: all gates passed");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
//! `cargo xtask parity`: Fusion Design-workspace command parity.
|
||||
//!
|
||||
//! `xtask/data/fusion-catalog.tsv` lists the toolbar commands of Fusion's Design workspace as
|
||||
//! `tab \t panel \t command id \t command name` (ids and names only, nothing else). It is
|
||||
//! derived from the local, uncommitted `plan/fusion/menu-tree.json` with `--refresh`. A catalog
|
||||
//! entry is live when SolveCraft registers a command with the same id.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// Design workspace tabs in the catalog (menu-tree tab id, display name).
|
||||
const TABS: &[(&str, &str)] = &[
|
||||
("SolidTab", "SOLID"),
|
||||
("SketchTab", "SKETCH"),
|
||||
("SurfaceTab", "SURFACE"),
|
||||
("ParaMeshOuterTab", "MESH"),
|
||||
("FormTab", "FORM"),
|
||||
("SheetMetalTab", "SHEET METAL"),
|
||||
("PlasticTab", "PLASTIC"),
|
||||
("AssemblyTab", "ASSEMBLY"),
|
||||
("ManageTab", "MANAGE"),
|
||||
("ToolsTab", "UTILITIES"),
|
||||
];
|
||||
|
||||
/// The tabs the headline number counts (M0 scope).
|
||||
const HEADLINE: &[&str] = &["SOLID", "SKETCH"];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Entry {
|
||||
pub tab: String,
|
||||
pub panel: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
fn clean(s: &str) -> String {
|
||||
s.replace(['\t', '\n', '\r'], " ").trim().to_string()
|
||||
}
|
||||
|
||||
fn walk(controls: &[Value], defs: &[Value], tab: &str, panel: &str, out: &mut Vec<Entry>) {
|
||||
for c in controls {
|
||||
match c["type"].as_str() {
|
||||
Some("CommandControl") => {
|
||||
let id = c["command"]["id"].as_str().or(c["id"].as_str()).unwrap_or("");
|
||||
if id.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let name = c["command"]["name"]
|
||||
.as_str()
|
||||
.filter(|n| !n.is_empty())
|
||||
.or_else(|| defs.iter().find(|d| d["id"].as_str() == Some(id)).and_then(|d| d["name"].as_str()))
|
||||
.unwrap_or(id);
|
||||
if !out.iter().any(|e| e.tab == tab && e.id == id) {
|
||||
out.push(Entry { tab: tab.into(), panel: panel.into(), id: clean(id), name: clean(name) });
|
||||
}
|
||||
}
|
||||
Some("DropDownControl") => walk(c["controls"].as_array().map(Vec::as_slice).unwrap_or(&[]), defs, tab, panel, out),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Catalog entries from Fusion's menu-tree JSON.
|
||||
pub fn from_menu_tree(v: &Value) -> Vec<Entry> {
|
||||
let defs = v["commandDefinitions"].as_array().map(Vec::as_slice).unwrap_or(&[]);
|
||||
let mut out = Vec::new();
|
||||
for (tid, tname) in TABS {
|
||||
let tab = &v["tabs"][format!("DesignProductType/{tid}")];
|
||||
for p in tab["panels"].as_array().into_iter().flatten() {
|
||||
let panel = clean(p["name"].as_str().unwrap_or("")).to_uppercase();
|
||||
walk(p["controls"].as_array().map(Vec::as_slice).unwrap_or(&[]), defs, tname, &panel, &mut out);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn parse_catalog(s: &str) -> Vec<Entry> {
|
||||
s.lines()
|
||||
.filter(|l| !l.starts_with('#') && !l.trim().is_empty())
|
||||
.filter_map(|l| {
|
||||
let mut it = l.split('\t');
|
||||
Some(Entry { tab: it.next()?.into(), panel: it.next()?.into(), id: it.next()?.into(), name: it.next().unwrap_or("").into() })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn run(root: &Path, refresh: bool) -> Result<(), String> {
|
||||
let cat_path = root.join("xtask/data/fusion-catalog.tsv");
|
||||
if refresh {
|
||||
let mt = root.join("plan/fusion/menu-tree.json");
|
||||
let s = std::fs::read_to_string(&mt).map_err(|e| format!("{}: {e}", mt.display()))?;
|
||||
let v: Value = serde_json::from_str(&s).map_err(|e| format!("menu-tree.json: {e}"))?;
|
||||
let entries = from_menu_tree(&v);
|
||||
let mut out = String::from("# Fusion Design workspace toolbar: tab, panel, command id, command name (ids and names only).\n# Regenerate with `cargo xtask parity --refresh` (needs the local plan/fusion/menu-tree.json).\n");
|
||||
for e in &entries {
|
||||
out += &format!("{}\t{}\t{}\t{}\n", e.tab, e.panel, e.id, e.name);
|
||||
}
|
||||
std::fs::create_dir_all(root.join("xtask/data")).map_err(|e| e.to_string())?;
|
||||
std::fs::write(&cat_path, out).map_err(|e| format!("{}: {e}", cat_path.display()))?;
|
||||
println!("parity: catalog refreshed with {} entries", entries.len());
|
||||
}
|
||||
let catalog = parse_catalog(&std::fs::read_to_string(&cat_path).map_err(|e| format!("{}: {e}", cat_path.display()))?);
|
||||
let out = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
|
||||
.current_dir(root)
|
||||
.args(["run", "-q", "--release", "-p", "solvecraft-cli", "--", "commands"])
|
||||
.output()
|
||||
.map_err(|e| format!("solvecraft-cli: {e}"))?;
|
||||
let cmds: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("commands JSON: {e}"))?;
|
||||
let ids: Vec<String> = cmds.as_array().into_iter().flatten().filter_map(|c| c["id"].as_str().map(str::to_string)).collect();
|
||||
let live = |e: &Entry| ids.iter().any(|i| *i == e.id);
|
||||
let pct = |a: usize, b: usize| if b == 0 { 0.0 } else { 100.0 * a as f64 / b as f64 };
|
||||
|
||||
let head: Vec<&Entry> = catalog.iter().filter(|e| HEADLINE.contains(&e.tab.as_str())).collect();
|
||||
let head_live = head.iter().filter(|e| live(e)).count();
|
||||
let all_live = catalog.iter().filter(|e| live(e)).count();
|
||||
let mut md = format!(
|
||||
"# Fusion command parity\n\nGenerated by `cargo xtask parity` from `xtask/data/fusion-catalog.tsv` (the toolbar commands of Fusion's Design workspace, ids and names only) and the live command registry (`solvecraft-cli commands`). A command counts as live when SolveCraft registers the same command id. This measures breadth, not depth; see ROADMAP.md and docs/oracle.md for depth.\n\n**SOLID + SKETCH: {head_live} / {} commands live ({:.0}%).** All Design tabs: {all_live} / {} ({:.0}%). Registered commands: {}.\n\n| Tab | Panel | Live | Total | % |\n|---|---|---:|---:|---:|\n",
|
||||
head.len(),
|
||||
pct(head_live, head.len()),
|
||||
catalog.len(),
|
||||
pct(all_live, catalog.len()),
|
||||
ids.len()
|
||||
);
|
||||
let mut groups: Vec<(String, String, usize, usize)> = Vec::new();
|
||||
for e in &catalog {
|
||||
let l = usize::from(live(e));
|
||||
match groups.iter_mut().find(|g| g.0 == e.tab && g.1 == e.panel) {
|
||||
Some(g) => {
|
||||
g.2 += l;
|
||||
g.3 += 1;
|
||||
}
|
||||
None => groups.push((e.tab.clone(), e.panel.clone(), l, 1)),
|
||||
}
|
||||
}
|
||||
for (t, p, l, n) in &groups {
|
||||
md += &format!("| {t} | {p} | {l} | {n} | {:.0}% |\n", pct(*l, *n));
|
||||
}
|
||||
md += "\n## Live (SOLID, SKETCH)\n\n";
|
||||
for e in head.iter().filter(|e| live(e)) {
|
||||
md += &format!("- {} › {} › {} (`{}`)\n", e.tab, e.panel, e.name, e.id);
|
||||
}
|
||||
md += "\n## Not yet (SOLID, SKETCH)\n\n";
|
||||
for e in head.iter().filter(|e| !live(e)) {
|
||||
md += &format!("- {} › {} › {} (`{}`)\n", e.tab, e.panel, e.name, e.id);
|
||||
}
|
||||
std::fs::write(root.join("docs/parity.md"), md).map_err(|e| format!("docs/parity.md: {e}"))?;
|
||||
println!("parity: SOLID+SKETCH {head_live}/{} ({:.0}%), all tabs {all_live}/{} → docs/parity.md", head.len(), pct(head_live, head.len()), catalog.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn catalog_from_menu_tree() {
|
||||
let v: Value = serde_json::from_str(
|
||||
r#"{"commandDefinitions":[{"id":"B","name":"Bee"}],"tabs":{"DesignProductType/SolidTab":{"panels":[{"name":"Create","controls":[
|
||||
{"type":"CommandControl","id":"A","command":{"id":"A","name":"Aye"}},
|
||||
{"type":"SeparatorControl","id":"s"},
|
||||
{"type":"DropDownControl","id":"d","controls":[{"type":"CommandControl","id":"B","command":{"id":"B"}}]}]}]}}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let e = from_menu_tree(&v);
|
||||
assert_eq!(e.len(), 2);
|
||||
assert_eq!((e[0].tab.as_str(), e[0].panel.as_str(), e[0].id.as_str(), e[0].name.as_str()), ("SOLID", "CREATE", "A", "Aye"));
|
||||
assert_eq!(e[1].name, "Bee");
|
||||
let tsv = "# c\nSOLID\tCREATE\tA\tAye\n";
|
||||
assert_eq!(parse_catalog(tsv).len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user