From d3cb790bd9133cbbb15ee69d25d9bbb851028787 Mon Sep 17 00:00:00 2001 From: meelstorm <8780022+TesAnti@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:03:59 +0200 Subject: [PATCH] check point --- .gitignore | 484 ++++ ...ye.VisionBuilder.UI.RecipeConverter.csproj | 24 + .../Program.cs | 11 + .../gold_b97.hrcp | Bin 0 -> 32204 bytes .../gold_b97.xhrcp | 150 ++ .../EmulationCameraImageSource.cs | 100 + .../EmulationSettings.cs | 27 + ....VisionBuilder.UI.Sources.Emulation.csproj | 13 + Hawkeye.VisionBuilder.Workflow/Annotations.cs | 1704 ++++++++++++ .../BaseOperation.cs | 284 ++ .../BaseOperationDecorator.cs | 24 + Hawkeye.VisionBuilder.Workflow/CLAUDE.md | 157 ++ .../ColorStudioClasses/CategoryItem.cs | 44 + .../ColorStudioClasses/CategoryTree.cs | 33 + .../ColorStudioClasses/ColorConfig.cs | 47 + .../Filters/AddFilterItem.cs | 52 + .../Filters/BlobsFilterItem.cs | 66 + .../Filters/BlurFilterItem.cs | 30 + .../ColorStudioClasses/Filters/IFilter.cs | 13 + .../Filters/IFilterParent.cs | 6 + .../Filters/IntersectionFilterItem.cs | 52 + .../Filters/MorphologyFilterItem.cs | 78 + .../Filters/RangeFilterItem.cs | 101 + .../Filters/SobelFilterItem.cs | 25 + .../Filters/SpatialFilterItem.cs | 176 ++ .../Filters/SpatialFilterItemGPU.cs | 177 ++ .../Filters/SubtractFilterItem.cs | 52 + .../ColorStudioClasses/HSIValue.cs | 125 + .../ColorStudioClasses/Kernels.cs | 157 ++ .../ColorStudioClasses/Range.cs | 64 + .../Configuration/BadTweakLearning.cs | 20 + .../Configuration/ECameraType.cs | 8 + .../Configuration/EOutputs.cs | 8 + .../Configuration/RecipeSelectionConfig.cs | 19 + .../Configuration/WorkflowConfiguration.cs | 58 + Hawkeye.VisionBuilder.Workflow/Context.cs | 13 + .../DataTransfer/CSharpDataTransferMQ.cs | 55 + .../DataTransfer/CSharpDataTransferMQRPC.cs | 130 + .../DataTransfer/ChildProcessTracker.cs | 135 + .../DataTransfer/PythonModelProxy.cs | 31 + .../DataTransfer/PythonModelProxyRPC.cs | 38 + .../DataTransfer/PythonScriptRunner.cs | 53 + .../DataTransfer/WslScriptRunner.cs | 66 + .../Datatypes/Elements/Align/AlignElement.cs | 35 + .../ArrayHorizontalAnchorNode.cs | 18 + .../ArrayHorizontal/ArrayHorizontalElement.cs | 86 + .../Datatypes/Elements/Edge/EdgeElement.cs | 40 + .../Datatypes/Elements/Edge/EdgeOrigin.cs | 54 + .../FixedRectangleAnchorNode.cs | 21 + .../FixedRectangle/FixedRectangleElement.cs | 58 + .../Datatypes/Elements/ICanvas.cs | 13 + .../Datatypes/Elements/IGraphicsElement.cs | 23 + .../Datatypes/Elements/INode.cs | 9 + .../Datatypes/Elements/Line/LineElement.cs | 38 + .../Datatypes/Elements/LineType.cs | 8 + .../Elements/Origin/OriginElement.cs | 27 + .../Elements/Parent/IHaveParentCoordinates.cs | 9 + .../Elements/Parent/NoParentCoordinates.cs | 14 + .../Datatypes/Elements/Poly/PolyElement.cs | 42 + .../Elements/Rectangle/RectangleAnchorNode.cs | 20 + .../Elements/Rectangle/RectangleElement.cs | 177 ++ .../Elements/Rectangle/RectangleSizeNode.cs | 18 + .../RotatedRectangleAnchorNode.cs | 18 + .../RotatedRectangleElement.cs | 123 + .../RotatedRectangleHalfWidthNode.cs | 18 + .../RotatedRectangleHeightNode.cs | 16 + .../Datatypes/FilePath.cs | 7 + .../Datatypes/HawkeyeImage.cs | 10 + .../Datatypes/LutData.cs | 68 + .../Datatypes/Pattern.cs | 6 + .../Datatypes/SelectFromList.cs | 7 + .../Hawkeye.VisionBuilder.Workflow.csproj | 46 + .../Links/IHaveEdge.cs | 8 + .../Links/IHaveImage.cs | 8 + .../Links/IHaveOrigin.cs | 12 + .../Links/IHaveSearchArea.cs | 8 + .../Links/NoOrigin.cs | 37 + Hawkeye.VisionBuilder.Workflow/MathHelper.cs | 314 +++ Hawkeye.VisionBuilder.Workflow/Matrix.cs | 2352 +++++++++++++++++ .../OperationDescription.cs | 8 + .../OperationDiscoveryService.cs | 58 + .../Operations/AI/AnomalyAI.cs | 125 + .../AI/ArrayModelMatchingOperation.cs | 184 ++ .../AI/ArrayModelSamplerOperation.cs | 87 + .../AI/BackgroundSeparationModelOperation.cs | 124 + .../Operations/AI/Color128BinaryOperation.cs | 33 + .../Operations/AI/Color128HalfOperation.cs | 113 + .../Operations/AI/Color128SimpleOperation.cs | 36 + .../Operations/AI/ColorAIOperation.cs | 114 + .../Operations/AI/ColorModelOperation.cs | 42 + .../Operations/AI/ModelAIOperation.cs | 159 ++ .../Operations/AI/MultichannelAI.cs | 148 ++ .../Operations/AI/RawModelAIOperation.cs | 150 ++ .../Operations/AI/YoloDetectionOperation.cs | 142 + .../Operations/AI/YoloPickDetected.cs | 115 + .../Operations/ActivateImageOperation.cs | 13 + .../Operations/Attributes/Category.cs | 12 + .../Attributes/IgnoreOperationAttribute.cs | 7 + .../Attributes/NotForToolAttribute.cs | 7 + .../Attributes/RestrictReferenceAttribute.cs | 12 + .../Operations/Basic/CannyOperation.cs | 38 + .../Basic/DetectionPaddingOperation.cs | 55 + .../Operations/Basic/GaussianBlurOperation.cs | 50 + .../Basic/PorabollisticHoughOperation.cs | 54 + .../Operations/Basic/SkeletonOperation.cs | 40 + .../Basic/ThresholdMinMaxOperation.cs | 47 + .../Operations/Basic/ThresholdOperation.cs | 44 + .../Operations/Edge/SobelOperation.cs | 28 + .../Filters/ColorProfileOperation.cs | 146 + .../Filters/GaborFilterOperation.cs | 97 + .../Operations/Filters/LUTFilterOperation.cs | 60 + .../Filters/NoiseFilterOperation.cs | 41 + .../Filters/RangeFilterOperation.cs | 55 + .../Operations/GetImageOperation.cs | 71 + .../Operations/Image/ConvertOperation.cs | 44 + .../Operations/Image/GetChannelOperation.cs | 38 + .../Operations/Image/ImageSizeOperation.cs | 29 + .../Image/ImageSizeProportionOperation.cs | 44 + .../Operations/Image/InvertOperation.cs | 31 + .../Operations/Image/ToGrayscaleOperation.cs | 25 + .../Memory/ImageFromMemoryOperation.cs | 42 + .../Memory/ImageToMemoryOperation.cs | 36 + .../Operations/Morphology/ClosingOperation.cs | 43 + .../Operations/Morphology/CutOperation.cs | 83 + .../Morphology/DilationOperation.cs | 44 + .../Operations/Morphology/HatsOperation.cs | 43 + .../Operations/Morphology/OpeningOperation.cs | 43 + .../Morphology/SubtractOperation.cs | 48 + .../Morphology/TakeBiggestOperation.cs | 54 + .../Operations/Morphology/UnionOperation.cs | 51 + .../Simple/Blobs/FindBlobOperation.cs | 158 ++ .../Simple/Blobs/FindManyBlobsExtOperation.cs | 233 ++ .../Simple/Blobs/FindManyBlobsOperation.cs | 175 ++ .../Simple/Blobs/FindRectangleOperation.cs | 112 + .../Operations/Simple/Blobs/TwoBlobsAlign.cs | 106 + .../Simple/Edges/EdgeIntersectionOperation.cs | 104 + .../Simple/Edges/FindEdgeOperation.cs | 130 + .../DistanceTransformOperation.cs | 32 + .../Overrides/OperationOverride.cs | 17 + Hawkeye.VisionBuilder.Workflow/Quaterion.cs | 1165 ++++++++ Hawkeye.VisionBuilder.Workflow/ScriptValue.cs | 6 + Hawkeye.VisionBuilder.Workflow/Vector2.cs | 1251 +++++++++ Hawkeye.VisionBuilder.Workflow/Vector3.cs | 1464 ++++++++++ Hawkeye.VisionBuilder.Workflow/Vector4.cs | 1395 ++++++++++ .../WorkflowList.cs | 280 ++ .../ActionSelector.Designer.cs | 125 + Hawkeye.VisionBuilder/ActionSelector.cs | 20 + Hawkeye.VisionBuilder/ActionSelector.resx | 60 + .../ColorProfileSelection.Designer.cs | 86 + .../ColorProfileSelection.cs | 78 + .../ColorProfileSelection.resx | 120 + .../Features/ImagePreview/EControlMode.cs | 7 + .../ImagePreviewControl.Designer.cs | 37 + .../ImagePreview/ImagePreviewControl.cs | 329 +++ .../ImagePreview/ImagePreviewControl.resx | 120 + .../ImagePreviewPanel.Designer.cs | 77 + .../ImagePreview/ImagePreviewPanel.cs | 54 + .../ImagePreview/ImagePreviewPanel.resx | 133 + .../ImageStatisticsDialog.Designer.cs | 96 + .../ImageStatistics/ImageStatisticsDialog.cs | 194 ++ .../ImageStatisticsDialog.resx | 120 + .../ImagesStrip/ImageStripPanel.Designer.cs | 221 ++ .../Features/ImagesStrip/ImageStripPanel.cs | 319 +++ .../Features/ImagesStrip/ImageStripPanel.resx | 151 ++ .../ImagesStrip/Resource1.Designer.cs | 133 + .../Features/ImagesStrip/Resource1.resx | 142 + .../ImagesStrip/ToolstripNumberControl.cs | 41 + .../ImagesStrip/images/backward-fast.png | Bin 0 -> 5920 bytes .../Features/ImagesStrip/images/backward.png | Bin 0 -> 5300 bytes .../ImagesStrip/images/forward-fast.png | Bin 0 -> 5158 bytes .../Features/ImagesStrip/images/forward.png | Bin 0 -> 5412 bytes .../Features/ImagesStrip/images/pause.png | Bin 0 -> 2796 bytes .../Features/ImagesStrip/images/play.png | Bin 0 -> 3493 bytes .../Features/ImagesStrip/images/repeat.png | Bin 0 -> 6104 bytes .../ToolEditor/CodeCompletitionControl.cs | 145 + .../ToolConfigurationPanel.Designer.cs | 130 + .../ToolEditor/ToolConfigurationPanel.cs | 311 +++ .../ToolEditor/ToolConfigurationPanel.resx | 60 + .../ToolConfigurationRow.Designer.cs | 73 + .../ToolEditor/ToolConfigurationRow.cs | 32 + .../ToolEditor/ToolConfigurationRow.resx | 60 + .../VisionSteps/VisionStepsPanel.Designer.cs | 191 ++ .../Features/VisionSteps/VisionStepsPanel.cs | 355 +++ .../VisionSteps/VisionStepsPanel.resx | 78 + .../Features/Yolo/AttributeSetter.Designer.cs | 193 ++ .../Features/Yolo/AttributeSetter.cs | 50 + .../Features/Yolo/AttributeSetter.resx | 120 + .../Hawkeye.VisionBuilder.csproj | 56 + .../Localization/ILocalizer.cs | 6 + .../Localization/NoLocalization.cs | 9 + Hawkeye.VisionBuilder/MainWindow.Designer.cs | 248 ++ Hawkeye.VisionBuilder/MainWindow.cs | 418 +++ Hawkeye.VisionBuilder/MainWindow.resx | 126 + .../Panels/BasePannel.Designer.cs | 49 + Hawkeye.VisionBuilder/Panels/BasePannel.cs | 20 + Hawkeye.VisionBuilder/Panels/BasePannel.resx | 60 + .../Panels/IInitializable.cs | 9 + Hawkeye.VisionBuilder/Program.cs | 43 + Hawkeye.VisionBuilder/Resources.Designer.cs | 83 + Hawkeye.VisionBuilder/Resources.resx | 127 + Hawkeye.VisionBuilder/UIEventHandler.cs | 6 + Hawkeye.VisionBuilder/WinConsole.cs | 118 + Hawkeye.VisionBuilder/icons/diaphragm.png | Bin 0 -> 1332 bytes Hawkeye.VisionBuilder/icons/grid.png | Bin 0 -> 515 bytes VisionBuilder.UI.Common/CameraSettings.cs | 31 + .../Commands/ImageProcessedEvent.cs | 17 + .../Commands/Interfaces/ICommand.cs | 6 + .../Commands/Interfaces/IEvent.cs | 6 + .../Commands/Interfaces/IEventHandler.cs | 6 + .../Commands/SessionEndedEvent.cs | 8 + .../Commands/SessionStartedEvent.cs | 10 + .../ExampleMainViewModel.cs | 21 + VisionBuilder.UI.Common/Extensions.cs | 19 + VisionBuilder.UI.Common/ISettings.cs | 8 + VisionBuilder.UI.Common/NoLearning.cs | 17 + VisionBuilder.UI.Common/PathSettingsUtils.cs | 47 + .../Processing/IImageSource.cs | 9 + .../Processing/ILoadingService.cs | 7 + .../Processing/IRecognitionControl.cs | 17 + VisionBuilder.UI.Common/UIConfiguration.cs | 21 + .../ViewModel/Classes/ErrorData.cs | 16 + .../ViewModel/Classes/RecipeData.cs | 9 + .../ViewModel/ErrorPreviewVM.cs | 102 + VisionBuilder.UI.Common/ViewModel/ErrorsVM.cs | 60 + .../Interfaces/UI/IImagePreviewService.cs | 9 + .../ViewModel/Interfaces/UI/ILearningTool.cs | 9 + .../Interfaces/UI/IMainWindowService.cs | 6 + .../UI/IRecipeSelectionDialogService.cs | 6 + .../Interfaces/UI/ISettingsService.cs | 6 + .../UI/ISingleCameraCreationService.cs | 6 + .../ViewModel/MainWindowVM.cs | 30 + .../ViewModel/PreviewVM.cs | 18 + .../ViewModel/RecipeSelectionVM.cs | 10 + .../ViewModel/SingleCameraVM.cs | 112 + .../ViewModel/StatisticsVM.cs | 58 + .../VisionBuilder.UI.Common.csproj | 19 + .../HawkeyeRecognitionControl.cs | 179 ++ .../HawkeyeRecognitionSettings.cs | 20 + .../OpenCVCanvas.cs | 63 + ...ionBuilder.UI.Recipes.HawkeyeRecipe.csproj | 14 + .../WorkflowRecipeHelper.cs | 19 + .../BasicSyntax.cs | 32 + ...onBuilder.UI.Recipes.Scripted.Tests.csproj | 24 + .../.claude/settings.local.json | 10 + .../ETokenType.cs | 2 + .../Tokenizer.cs | 143 + .../VisionBuilder.UI.Recipes.Scripted.csproj | 16 + .../VisionBuilder.UI.Ringbuffer.csproj | 14 + .../VisionBuilderRingbuffer.cs | 127 + .../VisionBuilderRingbufferSettings.cs | 85 + VisionBuilder.UI.Statistics/README.md | 121 + .../VisionBuilder.UI.Statistics.csproj | 18 + .../VisionBuilderStatistics.cs | 145 + .../VisionBuilderStatisticsSettings.cs | 64 + VisionBuilder.UI.Tests/MSTestSettings.cs | 1 + VisionBuilder.UI.Tests/VMTests.cs | 19 + .../VisionBuilder.UI.Tests.csproj | 23 + VisionBuilder.UI.Windows.Test/AppSettings.cs | 8 + .../Form1.Designer.cs | 135 + VisionBuilder.UI.Windows.Test/Form1.cs | 33 + VisionBuilder.UI.Windows.Test/Form1.resx | 120 + VisionBuilder.UI.Windows.Test/Program.cs | 93 + .../VisionBuilder.UI.Windows.Test.csproj | 32 + .../Components/ErrorPreview.Designer.cs | 57 + .../Components/ErrorPreview.cs | 135 + .../Components/ErrorPreview.resx | 120 + .../Components/PreviewWindow.cs | 44 + .../SingleCameraControl.Designer.cs | 215 ++ .../Components/SingleCameraControl.cs | 45 + .../Components/SingleCameraControl.resx | 120 + .../Components/Stats.Designer.cs | 95 + VisionBuilder.UI.Windows/Components/Stats.cs | 115 + .../Components/Stats.resx | 120 + .../Dialogs/ImagePreview.Designer.cs | 168 ++ .../Dialogs/ImagePreview.cs | 74 + .../Dialogs/ImagePreview.resx | 120 + .../Dialogs/RecipeSelectionDialog.Designer.cs | 85 + .../Dialogs/RecipeSelectionDialog.cs | 77 + .../Dialogs/RecipeSelectionDialog.resx | 120 + .../Services/ImagePreviewService.cs | 22 + .../Services/LoadingService.cs | 17 + .../Services/RecipeSelectionDialogService.cs | 18 + .../Services/SettingsService.cs | 34 + .../VisionBuilder.UI.Windows.csproj | 21 + VisionBuilder.UI.sln | 162 ++ .../Inspectron.Camera/CameraExtensions.cs | 14 + .../Inspectron.Camera/CameraParameter.cs | 61 + .../Emulation/EmulationCamera.cs | 167 ++ .../Emulation/EmulationCameraCapabilities.cs | 10 + .../Emulation/EmulationCameraProvider.cs | 34 + .../Enums/EStandardRotation.cs | 10 + framework/Inspectron.Camera/ICamera.cs | 91 + .../Inspectron.Camera/ICameraCapabilities.cs | 25 + .../Inspectron.Camera/ICameraParameter.cs | 27 + .../Inspectron.Camera/ICameraProvider.cs | 22 + .../Inspectron.Camera/Image/BitmapImage.cs | 35 + .../Inspectron.Camera/Image/ByteImage.cs | 131 + .../Inspectron.Camera/Image/Extensions.cs | 18 + framework/Inspectron.Camera/Image/IImage.cs | 14 + .../Inspectron.Camera/Image/IImageLock.cs | 10 + .../Inspectron.Camera.csproj | 17 + .../NoImage/NoImageCamera.cs | 74 + .../NoImage/NoImageCameraCapabilities.cs | 10 + .../NoImage/NoImageCameraProvider.cs | 18 + .../Filesystems/AsyncFilesystem.cs | 82 + .../Filesystems/DirectFilesystem.cs | 31 + framework/Inspectron.Fastbuffer/IndexFile.cs | 150 ++ .../Inspectron.Fastbuffer.csproj | 17 + .../Inspectron.Fastbuffer.sln | 31 + .../Interfaces/IFilesystem.cs | 8 + .../IsolatedRingbuffer.cs | 38 + .../RingbufferRepository.cs | 52 + .../Inspectron.Ringbuffer/Index/GroupIndex.cs | 92 + .../Index/ProductIndex.cs | 70 + .../Inspectron.Ringbuffer/Index/RootIndex.cs | 56 + .../Inspectron.Ringbuffer.csproj | 14 + .../Interfaces/IFileIdentity.cs | 9 + .../Interfaces/IFileIdentityFactory.cs | 8 + .../Interfaces/IFileVerifier.cs | 10 + .../Interfaces/IFolderWritter.cs | 8 + .../Interfaces/IIndex.cs | 11 + .../RingbufferConfiguration.cs | 25 + .../Inspectron.Ringbuffer/RingbufferFolder.cs | 150 ++ .../Inspectron.Ringbuffer/RingbufferPath.cs | 182 ++ .../RingbufferPathConfiguration.cs | 35 + .../Inspectron.Ringbuffer/SaveCandidate.cs | 16 + .../Configuration/DefaultControlFactory.cs | 187 ++ .../Configuration/IControlFactory.cs | 11 + .../Configuration/OptionsSetting.cs | 12 + .../Configuration/OptionsWindow.Designer.cs | 69 + .../Configuration/OptionsWindow.cs | 177 ++ .../Configuration/OptionsWindow.resx | 120 + .../Inspectron.Settings.Windows.csproj | 16 + .../Inspectron.Settings.Windows/PropHelper.cs | 18 + .../Controls/CheckEditor.Designer.cs | 61 + .../Controls/CheckEditor.cs | 28 + .../Controls/CheckEditor.resx | 120 + .../Controls/EnumEditor.Designer.cs | 46 + .../Controls/EnumEditor.cs | 50 + .../Controls/EnumEditor.resx | 120 + .../Controls/NumberEditor.Designer.cs | 73 + .../Controls/NumberEditor.cs | 56 + .../Controls/NumberEditor.resx | 120 + .../Controls/PathEditor.Designer.cs | 90 + .../Controls/PathEditor.cs | 57 + .../Controls/PathEditor.resx | 120 + .../Controls/StringEditor.Designer.cs | 72 + .../Controls/StringEditor.cs | 41 + .../Controls/StringEditor.resx | 120 + .../Enums/ESetupResult.cs | 9 + .../Inspectron.Settings.WindowsWizard.csproj | 30 + .../Interfaces/ISetupItem.cs | 7 + .../PropHelper.cs | 17 + .../Resource1.Designer.cs | 73 + .../Resource1.resx | 124 + .../Resources/setup.png | Bin 0 -> 134004 bytes .../SetupPage.Designer.cs | 139 + .../SetupPage.cs | 156 ++ .../SetupPage.resx | 60 + .../Inspectron.Settings/AdaptablePath.cs | 164 ++ .../AdaptationException.cs | 25 + framework/Inspectron.Settings/Adapters.cs | 336 +++ .../Attributes/SettingDescriptionAttribute.cs | 14 + .../Attributes/SettingPreviewAttribute.cs | 17 + .../BoundPropertyDescriptor.cs | 494 ++++ framework/Inspectron.Settings/Event.cs | 43 + framework/Inspectron.Settings/IAdaptable.cs | 13 + framework/Inspectron.Settings/IDecoratable.cs | 14 + framework/Inspectron.Settings/ITreeView.cs | 20 + .../Inspectron.Settings.csproj | 8 + .../Inspectron.Settings/InspectronSettings.cs | 845 ++++++ framework/Inspectron.Settings/ItemInfo.cs | 148 ++ framework/Inspectron.Settings/Path.cs | 405 +++ framework/Inspectron.Settings/Tree.cs | 336 +++ .../Inspectron.Statistics/CsvStatistics.cs | 48 + .../Inspectron.Statistics/IStatistics.cs | 6 + .../Inspectron.Statistics.csproj | 13 + .../LockedCsvStatistics.cs | 92 + .../Inspectron.Statistics/StatisticsRecord.cs | 6 + .../StatisticsWritter.cs | 64 + .../Animations/AnimationDirection.cs | 12 + .../Animations/AnimationManager.cs | 363 +++ .../Animations/Animations.cs | 53 + framework/MaterialSkin.Core/ColorScheme.cs | 371 +++ .../Controls/MaterialCheckBox.cs | 250 ++ .../Controls/MaterialComboBox.cs | 44 + .../Controls/MaterialContextMenuStrip.cs | 195 ++ .../Controls/MaterialDivider.cs | 27 + .../Controls/MaterialFlatButton.cs | 200 ++ .../Controls/MaterialForm.cs | 619 +++++ .../Controls/MaterialForm.resx | 120 + .../Controls/MaterialIcon.cs | 171 ++ .../Controls/MaterialIndicator.Designer.cs | 44 + .../Controls/MaterialIndicator.cs | 51 + .../Controls/MaterialIndicator.resx | 120 + .../Controls/MaterialInputBox.Designer.cs | 115 + .../Controls/MaterialInputBox.cs | 48 + .../Controls/MaterialInputBox.resx | 60 + .../Controls/MaterialLabel.cs | 24 + .../Controls/MaterialListView.cs | 163 ++ .../Controls/MaterialLoader.Designer.cs | 47 + .../Controls/MaterialLoader.cs | 139 + .../Controls/MaterialLoader.resx | 120 + .../Controls/MaterialMessageBox.Designer.cs | 90 + .../Controls/MaterialMessageBox.cs | 86 + .../Controls/MaterialMessageBox.resx | 120 + .../Controls/MaterialPanel.cs | 16 + .../Controls/MaterialProgressBar.cs | 134 + .../Controls/MaterialRadioButton.cs | 211 ++ .../Controls/MaterialRaisedButton.cs | 216 ++ .../Controls/MaterialSingleLineTextField.cs | 1169 ++++++++ .../Controls/MaterialTabControl.cs | 22 + .../Controls/MaterialTabSelector.cs | 175 ++ .../Controls/MaterialUpDown.Designer.cs | 103 + .../Controls/MaterialUpDown.cs | 256 ++ .../Controls/MaterialUpDown.resx | 120 + framework/MaterialSkin.Core/DrawHelper.cs | 43 + .../Gesture/GestureEventArgs.cs | 279 ++ .../Gesture/GestureListener.cs | 446 ++++ .../Gesture/NativeMethods.cs | 341 +++ .../Gesture/UnmanagedLibrary.cs | 142 + .../MaterialSkin.Core/IMaterialControl.cs | 17 + .../MaterialSkin.Core.csproj | 117 + .../MaterialSkin.Core/MaterialSkinManager.cs | 349 +++ .../Resources/Resource1.Designer.cs | 113 + .../Resources/Resource1.resx | 136 + .../Resources/Roboto-Medium.ttf | Bin 0 -> 127488 bytes .../Resources/Roboto-Regular.ttf | Bin 0 -> 126072 bytes .../Resources/drop-down-arrow.png | Bin 0 -> 478 bytes .../Resources/round-delete-button.png | Bin 0 -> 11422 bytes .../Resources/rounded-add-button.png | Bin 0 -> 11862 bytes 431 files changed, 44078 insertions(+) create mode 100644 .gitignore create mode 100644 Hawkeye.VisionBuilder.UI.RecipeConverter/Hawkeye.VisionBuilder.UI.RecipeConverter.csproj create mode 100644 Hawkeye.VisionBuilder.UI.RecipeConverter/Program.cs create mode 100644 Hawkeye.VisionBuilder.UI.RecipeConverter/gold_b97.hrcp create mode 100644 Hawkeye.VisionBuilder.UI.RecipeConverter/gold_b97.xhrcp create mode 100644 Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs create mode 100644 Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationSettings.cs create mode 100644 Hawkeye.VisionBuilder.UI.Sources.Emulation/Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj create mode 100644 Hawkeye.VisionBuilder.Workflow/Annotations.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/BaseOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/CLAUDE.md create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/CategoryItem.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/CategoryTree.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/ColorConfig.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/AddFilterItem.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/BlobsFilterItem.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/BlurFilterItem.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IFilter.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IFilterParent.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IntersectionFilterItem.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/MorphologyFilterItem.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/RangeFilterItem.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SobelFilterItem.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SpatialFilterItem.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SpatialFilterItemGPU.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SubtractFilterItem.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/HSIValue.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Kernels.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Range.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Configuration/BadTweakLearning.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Configuration/ECameraType.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Configuration/EOutputs.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Configuration/RecipeSelectionConfig.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Configuration/WorkflowConfiguration.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Context.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/DataTransfer/CSharpDataTransferMQ.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/DataTransfer/CSharpDataTransferMQRPC.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/DataTransfer/ChildProcessTracker.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonModelProxy.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonModelProxyRPC.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonScriptRunner.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/DataTransfer/WslScriptRunner.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Align/AlignElement.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ArrayHorizontal/ArrayHorizontalAnchorNode.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ArrayHorizontal/ArrayHorizontalElement.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Edge/EdgeElement.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Edge/EdgeOrigin.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/FixedRectangle/FixedRectangleAnchorNode.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/FixedRectangle/FixedRectangleElement.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ICanvas.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/IGraphicsElement.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/INode.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Line/LineElement.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/LineType.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Origin/OriginElement.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Parent/IHaveParentCoordinates.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Parent/NoParentCoordinates.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Poly/PolyElement.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleAnchorNode.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleElement.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleSizeNode.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleAnchorNode.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleElement.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleHalfWidthNode.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleHeightNode.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/FilePath.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/HawkeyeImage.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/LutData.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/Pattern.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Datatypes/SelectFromList.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Hawkeye.VisionBuilder.Workflow.csproj create mode 100644 Hawkeye.VisionBuilder.Workflow/Links/IHaveEdge.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Links/IHaveImage.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Links/IHaveOrigin.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Links/IHaveSearchArea.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Links/NoOrigin.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/MathHelper.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Matrix.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/OperationDescription.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/OperationDiscoveryService.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelSamplerOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorAIOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/ActivateImageOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Attributes/Category.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Attributes/IgnoreOperationAttribute.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Attributes/NotForToolAttribute.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Attributes/RestrictReferenceAttribute.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Basic/SkeletonOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Edge/SobelOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Image/ConvertOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Image/InvertOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Image/ToGrayscaleOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Operations/Transformations/DistanceTransformOperation.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Overrides/OperationOverride.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Quaterion.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/ScriptValue.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Vector2.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Vector3.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/Vector4.cs create mode 100644 Hawkeye.VisionBuilder.Workflow/WorkflowList.cs create mode 100644 Hawkeye.VisionBuilder/ActionSelector.Designer.cs create mode 100644 Hawkeye.VisionBuilder/ActionSelector.cs create mode 100644 Hawkeye.VisionBuilder/ActionSelector.resx create mode 100644 Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.cs create mode 100644 Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.resx create mode 100644 Hawkeye.VisionBuilder/Features/ImagePreview/EControlMode.cs create mode 100644 Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.cs create mode 100644 Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.resx create mode 100644 Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.cs create mode 100644 Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.resx create mode 100644 Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs create mode 100644 Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.resx create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.cs create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.resx create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/Resource1.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/Resource1.resx create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/ToolstripNumberControl.cs create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/images/backward-fast.png create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/images/backward.png create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/images/forward-fast.png create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/images/forward.png create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/images/pause.png create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/images/play.png create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/images/repeat.png create mode 100644 Hawkeye.VisionBuilder/Features/ToolEditor/CodeCompletitionControl.cs create mode 100644 Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.cs create mode 100644 Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.resx create mode 100644 Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.cs create mode 100644 Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.resx create mode 100644 Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs create mode 100644 Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx create mode 100644 Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.cs create mode 100644 Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.resx create mode 100644 Hawkeye.VisionBuilder/Hawkeye.VisionBuilder.csproj create mode 100644 Hawkeye.VisionBuilder/Localization/ILocalizer.cs create mode 100644 Hawkeye.VisionBuilder/Localization/NoLocalization.cs create mode 100644 Hawkeye.VisionBuilder/MainWindow.Designer.cs create mode 100644 Hawkeye.VisionBuilder/MainWindow.cs create mode 100644 Hawkeye.VisionBuilder/MainWindow.resx create mode 100644 Hawkeye.VisionBuilder/Panels/BasePannel.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Panels/BasePannel.cs create mode 100644 Hawkeye.VisionBuilder/Panels/BasePannel.resx create mode 100644 Hawkeye.VisionBuilder/Panels/IInitializable.cs create mode 100644 Hawkeye.VisionBuilder/Program.cs create mode 100644 Hawkeye.VisionBuilder/Resources.Designer.cs create mode 100644 Hawkeye.VisionBuilder/Resources.resx create mode 100644 Hawkeye.VisionBuilder/UIEventHandler.cs create mode 100644 Hawkeye.VisionBuilder/WinConsole.cs create mode 100644 Hawkeye.VisionBuilder/icons/diaphragm.png create mode 100644 Hawkeye.VisionBuilder/icons/grid.png create mode 100644 VisionBuilder.UI.Common/CameraSettings.cs create mode 100644 VisionBuilder.UI.Common/Commands/ImageProcessedEvent.cs create mode 100644 VisionBuilder.UI.Common/Commands/Interfaces/ICommand.cs create mode 100644 VisionBuilder.UI.Common/Commands/Interfaces/IEvent.cs create mode 100644 VisionBuilder.UI.Common/Commands/Interfaces/IEventHandler.cs create mode 100644 VisionBuilder.UI.Common/Commands/SessionEndedEvent.cs create mode 100644 VisionBuilder.UI.Common/Commands/SessionStartedEvent.cs create mode 100644 VisionBuilder.UI.Common/ExampleMainViewModel.cs create mode 100644 VisionBuilder.UI.Common/Extensions.cs create mode 100644 VisionBuilder.UI.Common/ISettings.cs create mode 100644 VisionBuilder.UI.Common/NoLearning.cs create mode 100644 VisionBuilder.UI.Common/PathSettingsUtils.cs create mode 100644 VisionBuilder.UI.Common/Processing/IImageSource.cs create mode 100644 VisionBuilder.UI.Common/Processing/ILoadingService.cs create mode 100644 VisionBuilder.UI.Common/Processing/IRecognitionControl.cs create mode 100644 VisionBuilder.UI.Common/UIConfiguration.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/Classes/ErrorData.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/Classes/RecipeData.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/ErrorPreviewVM.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/ErrorsVM.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IImagePreviewService.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ILearningTool.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IMainWindowService.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IRecipeSelectionDialogService.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ISettingsService.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ISingleCameraCreationService.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/MainWindowVM.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/PreviewVM.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/RecipeSelectionVM.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs create mode 100644 VisionBuilder.UI.Common/ViewModel/StatisticsVM.cs create mode 100644 VisionBuilder.UI.Common/VisionBuilder.UI.Common.csproj create mode 100644 VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs create mode 100644 VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionSettings.cs create mode 100644 VisionBuilder.UI.Recipes.HawkeyeRecipe/OpenCVCanvas.cs create mode 100644 VisionBuilder.UI.Recipes.HawkeyeRecipe/VisionBuilder.UI.Recipes.HawkeyeRecipe.csproj create mode 100644 VisionBuilder.UI.Recipes.HawkeyeRecipe/WorkflowRecipeHelper.cs create mode 100644 VisionBuilder.UI.Recipes.Scripted.Tests/BasicSyntax.cs create mode 100644 VisionBuilder.UI.Recipes.Scripted.Tests/VisionBuilder.UI.Recipes.Scripted.Tests.csproj create mode 100644 VisionBuilder.UI.Recipes.Scripted/.claude/settings.local.json create mode 100644 VisionBuilder.UI.Recipes.Scripted/ETokenType.cs create mode 100644 VisionBuilder.UI.Recipes.Scripted/Tokenizer.cs create mode 100644 VisionBuilder.UI.Recipes.Scripted/VisionBuilder.UI.Recipes.Scripted.csproj create mode 100644 VisionBuilder.UI.Ringbuffer/VisionBuilder.UI.Ringbuffer.csproj create mode 100644 VisionBuilder.UI.Ringbuffer/VisionBuilderRingbuffer.cs create mode 100644 VisionBuilder.UI.Ringbuffer/VisionBuilderRingbufferSettings.cs create mode 100644 VisionBuilder.UI.Statistics/README.md create mode 100644 VisionBuilder.UI.Statistics/VisionBuilder.UI.Statistics.csproj create mode 100644 VisionBuilder.UI.Statistics/VisionBuilderStatistics.cs create mode 100644 VisionBuilder.UI.Statistics/VisionBuilderStatisticsSettings.cs create mode 100644 VisionBuilder.UI.Tests/MSTestSettings.cs create mode 100644 VisionBuilder.UI.Tests/VMTests.cs create mode 100644 VisionBuilder.UI.Tests/VisionBuilder.UI.Tests.csproj create mode 100644 VisionBuilder.UI.Windows.Test/AppSettings.cs create mode 100644 VisionBuilder.UI.Windows.Test/Form1.Designer.cs create mode 100644 VisionBuilder.UI.Windows.Test/Form1.cs create mode 100644 VisionBuilder.UI.Windows.Test/Form1.resx create mode 100644 VisionBuilder.UI.Windows.Test/Program.cs create mode 100644 VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj create mode 100644 VisionBuilder.UI.Windows/Components/ErrorPreview.Designer.cs create mode 100644 VisionBuilder.UI.Windows/Components/ErrorPreview.cs create mode 100644 VisionBuilder.UI.Windows/Components/ErrorPreview.resx create mode 100644 VisionBuilder.UI.Windows/Components/PreviewWindow.cs create mode 100644 VisionBuilder.UI.Windows/Components/SingleCameraControl.Designer.cs create mode 100644 VisionBuilder.UI.Windows/Components/SingleCameraControl.cs create mode 100644 VisionBuilder.UI.Windows/Components/SingleCameraControl.resx create mode 100644 VisionBuilder.UI.Windows/Components/Stats.Designer.cs create mode 100644 VisionBuilder.UI.Windows/Components/Stats.cs create mode 100644 VisionBuilder.UI.Windows/Components/Stats.resx create mode 100644 VisionBuilder.UI.Windows/Dialogs/ImagePreview.Designer.cs create mode 100644 VisionBuilder.UI.Windows/Dialogs/ImagePreview.cs create mode 100644 VisionBuilder.UI.Windows/Dialogs/ImagePreview.resx create mode 100644 VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.Designer.cs create mode 100644 VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.cs create mode 100644 VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.resx create mode 100644 VisionBuilder.UI.Windows/Services/ImagePreviewService.cs create mode 100644 VisionBuilder.UI.Windows/Services/LoadingService.cs create mode 100644 VisionBuilder.UI.Windows/Services/RecipeSelectionDialogService.cs create mode 100644 VisionBuilder.UI.Windows/Services/SettingsService.cs create mode 100644 VisionBuilder.UI.Windows/VisionBuilder.UI.Windows.csproj create mode 100644 VisionBuilder.UI.sln create mode 100644 framework/Inspectron.Camera/CameraExtensions.cs create mode 100644 framework/Inspectron.Camera/CameraParameter.cs create mode 100644 framework/Inspectron.Camera/Emulation/EmulationCamera.cs create mode 100644 framework/Inspectron.Camera/Emulation/EmulationCameraCapabilities.cs create mode 100644 framework/Inspectron.Camera/Emulation/EmulationCameraProvider.cs create mode 100644 framework/Inspectron.Camera/Enums/EStandardRotation.cs create mode 100644 framework/Inspectron.Camera/ICamera.cs create mode 100644 framework/Inspectron.Camera/ICameraCapabilities.cs create mode 100644 framework/Inspectron.Camera/ICameraParameter.cs create mode 100644 framework/Inspectron.Camera/ICameraProvider.cs create mode 100644 framework/Inspectron.Camera/Image/BitmapImage.cs create mode 100644 framework/Inspectron.Camera/Image/ByteImage.cs create mode 100644 framework/Inspectron.Camera/Image/Extensions.cs create mode 100644 framework/Inspectron.Camera/Image/IImage.cs create mode 100644 framework/Inspectron.Camera/Image/IImageLock.cs create mode 100644 framework/Inspectron.Camera/Inspectron.Camera.csproj create mode 100644 framework/Inspectron.Camera/NoImage/NoImageCamera.cs create mode 100644 framework/Inspectron.Camera/NoImage/NoImageCameraCapabilities.cs create mode 100644 framework/Inspectron.Camera/NoImage/NoImageCameraProvider.cs create mode 100644 framework/Inspectron.Fastbuffer/Filesystems/AsyncFilesystem.cs create mode 100644 framework/Inspectron.Fastbuffer/Filesystems/DirectFilesystem.cs create mode 100644 framework/Inspectron.Fastbuffer/IndexFile.cs create mode 100644 framework/Inspectron.Fastbuffer/Inspectron.Fastbuffer.csproj create mode 100644 framework/Inspectron.Fastbuffer/Inspectron.Fastbuffer.sln create mode 100644 framework/Inspectron.Fastbuffer/Interfaces/IFilesystem.cs create mode 100644 framework/Inspectron.Fastbuffer/IsolatedRingbuffer.cs create mode 100644 framework/Inspectron.Fastbuffer/RingbufferRepository.cs create mode 100644 framework/Inspectron.Ringbuffer/Index/GroupIndex.cs create mode 100644 framework/Inspectron.Ringbuffer/Index/ProductIndex.cs create mode 100644 framework/Inspectron.Ringbuffer/Index/RootIndex.cs create mode 100644 framework/Inspectron.Ringbuffer/Inspectron.Ringbuffer.csproj create mode 100644 framework/Inspectron.Ringbuffer/Interfaces/IFileIdentity.cs create mode 100644 framework/Inspectron.Ringbuffer/Interfaces/IFileIdentityFactory.cs create mode 100644 framework/Inspectron.Ringbuffer/Interfaces/IFileVerifier.cs create mode 100644 framework/Inspectron.Ringbuffer/Interfaces/IFolderWritter.cs create mode 100644 framework/Inspectron.Ringbuffer/Interfaces/IIndex.cs create mode 100644 framework/Inspectron.Ringbuffer/RingbufferConfiguration.cs create mode 100644 framework/Inspectron.Ringbuffer/RingbufferFolder.cs create mode 100644 framework/Inspectron.Ringbuffer/RingbufferPath.cs create mode 100644 framework/Inspectron.Ringbuffer/RingbufferPathConfiguration.cs create mode 100644 framework/Inspectron.Ringbuffer/SaveCandidate.cs create mode 100644 framework/Inspectron.Settings.Windows/Configuration/DefaultControlFactory.cs create mode 100644 framework/Inspectron.Settings.Windows/Configuration/IControlFactory.cs create mode 100644 framework/Inspectron.Settings.Windows/Configuration/OptionsSetting.cs create mode 100644 framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs create mode 100644 framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.cs create mode 100644 framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.resx create mode 100644 framework/Inspectron.Settings.Windows/Inspectron.Settings.Windows.csproj create mode 100644 framework/Inspectron.Settings.Windows/PropHelper.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.Designer.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.resx create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.Designer.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.resx create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/NumberEditor.Designer.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/NumberEditor.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/NumberEditor.resx create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.Designer.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.resx create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.Designer.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.resx create mode 100644 framework/Inspectron.Settings.WindowsWizard/Enums/ESetupResult.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Inspectron.Settings.WindowsWizard.csproj create mode 100644 framework/Inspectron.Settings.WindowsWizard/Interfaces/ISetupItem.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/PropHelper.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Resource1.Designer.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/Resource1.resx create mode 100644 framework/Inspectron.Settings.WindowsWizard/Resources/setup.png create mode 100644 framework/Inspectron.Settings.WindowsWizard/SetupPage.Designer.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/SetupPage.cs create mode 100644 framework/Inspectron.Settings.WindowsWizard/SetupPage.resx create mode 100644 framework/Inspectron.Settings/AdaptablePath.cs create mode 100644 framework/Inspectron.Settings/AdaptationException.cs create mode 100644 framework/Inspectron.Settings/Adapters.cs create mode 100644 framework/Inspectron.Settings/Attributes/SettingDescriptionAttribute.cs create mode 100644 framework/Inspectron.Settings/Attributes/SettingPreviewAttribute.cs create mode 100644 framework/Inspectron.Settings/BoundPropertyDescriptor.cs create mode 100644 framework/Inspectron.Settings/Event.cs create mode 100644 framework/Inspectron.Settings/IAdaptable.cs create mode 100644 framework/Inspectron.Settings/IDecoratable.cs create mode 100644 framework/Inspectron.Settings/ITreeView.cs create mode 100644 framework/Inspectron.Settings/Inspectron.Settings.csproj create mode 100644 framework/Inspectron.Settings/InspectronSettings.cs create mode 100644 framework/Inspectron.Settings/ItemInfo.cs create mode 100644 framework/Inspectron.Settings/Path.cs create mode 100644 framework/Inspectron.Settings/Tree.cs create mode 100644 framework/Inspectron.Statistics/CsvStatistics.cs create mode 100644 framework/Inspectron.Statistics/IStatistics.cs create mode 100644 framework/Inspectron.Statistics/Inspectron.Statistics.csproj create mode 100644 framework/Inspectron.Statistics/LockedCsvStatistics.cs create mode 100644 framework/Inspectron.Statistics/StatisticsRecord.cs create mode 100644 framework/Inspectron.Statistics/StatisticsWritter.cs create mode 100644 framework/MaterialSkin.Core/Animations/AnimationDirection.cs create mode 100644 framework/MaterialSkin.Core/Animations/AnimationManager.cs create mode 100644 framework/MaterialSkin.Core/Animations/Animations.cs create mode 100644 framework/MaterialSkin.Core/ColorScheme.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialCheckBox.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialComboBox.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialContextMenuStrip.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialDivider.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialFlatButton.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialForm.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialForm.resx create mode 100644 framework/MaterialSkin.Core/Controls/MaterialIcon.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialIndicator.Designer.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialIndicator.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialIndicator.resx create mode 100644 framework/MaterialSkin.Core/Controls/MaterialInputBox.Designer.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialInputBox.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialInputBox.resx create mode 100644 framework/MaterialSkin.Core/Controls/MaterialLabel.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialListView.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialLoader.Designer.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialLoader.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialLoader.resx create mode 100644 framework/MaterialSkin.Core/Controls/MaterialMessageBox.Designer.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialMessageBox.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialMessageBox.resx create mode 100644 framework/MaterialSkin.Core/Controls/MaterialPanel.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialProgressBar.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialRadioButton.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialRaisedButton.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialSingleLineTextField.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialTabControl.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialTabSelector.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialUpDown.Designer.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialUpDown.cs create mode 100644 framework/MaterialSkin.Core/Controls/MaterialUpDown.resx create mode 100644 framework/MaterialSkin.Core/DrawHelper.cs create mode 100644 framework/MaterialSkin.Core/Gesture/GestureEventArgs.cs create mode 100644 framework/MaterialSkin.Core/Gesture/GestureListener.cs create mode 100644 framework/MaterialSkin.Core/Gesture/NativeMethods.cs create mode 100644 framework/MaterialSkin.Core/Gesture/UnmanagedLibrary.cs create mode 100644 framework/MaterialSkin.Core/IMaterialControl.cs create mode 100644 framework/MaterialSkin.Core/MaterialSkin.Core.csproj create mode 100644 framework/MaterialSkin.Core/MaterialSkinManager.cs create mode 100644 framework/MaterialSkin.Core/Resources/Resource1.Designer.cs create mode 100644 framework/MaterialSkin.Core/Resources/Resource1.resx create mode 100644 framework/MaterialSkin.Core/Resources/Roboto-Medium.ttf create mode 100644 framework/MaterialSkin.Core/Resources/Roboto-Regular.ttf create mode 100644 framework/MaterialSkin.Core/Resources/drop-down-arrow.png create mode 100644 framework/MaterialSkin.Core/Resources/round-delete-button.png create mode 100644 framework/MaterialSkin.Core/Resources/rounded-add-button.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc78471 --- /dev/null +++ b/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/Hawkeye.VisionBuilder.UI.RecipeConverter/Hawkeye.VisionBuilder.UI.RecipeConverter.csproj b/Hawkeye.VisionBuilder.UI.RecipeConverter/Hawkeye.VisionBuilder.UI.RecipeConverter.csproj new file mode 100644 index 0000000..be426c3 --- /dev/null +++ b/Hawkeye.VisionBuilder.UI.RecipeConverter/Hawkeye.VisionBuilder.UI.RecipeConverter.csproj @@ -0,0 +1,24 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/Hawkeye.VisionBuilder.UI.RecipeConverter/Program.cs b/Hawkeye.VisionBuilder.UI.RecipeConverter/Program.cs new file mode 100644 index 0000000..2e709b7 --- /dev/null +++ b/Hawkeye.VisionBuilder.UI.RecipeConverter/Program.cs @@ -0,0 +1,11 @@ +using System.Xml; +using Hawkeye.VisionBuilder.Workflow; + +WorkflowList workflowList = new WorkflowList(); +var file = File.OpenRead("gold_b97.hrcp"); +using (var br = new BinaryReader(file)) +{ + workflowList.Load(br,new OperationDiscoveryService(workflowList)); +} + + diff --git a/Hawkeye.VisionBuilder.UI.RecipeConverter/gold_b97.hrcp b/Hawkeye.VisionBuilder.UI.RecipeConverter/gold_b97.hrcp new file mode 100644 index 0000000000000000000000000000000000000000..2849317d2b7ea0b8c7fe618d4a1b17a838ff4d17 GIT binary patch literal 32204 zcmdSAWmFqozwnK_LveQsfj}SxcPUcbp%6%dOK}LrwK&CFT#HL71&S9bE(MAgC~l=# z@rUca&V9~vo)7O@=fio|eNWcR-ZROp%y0d5@BJSXl>gee6vWL9JmBsg<{I`cFi&$u zxQC;stDCuss}qb_+uh!kS&&)9)fFcA|NPF`-o@U=)ddCxnZsSYJ`@#7Q?LF*WUF5y&onUZxej`_RM}(8B z55JZh+#Txq_>Bj@3f%LVGt?ITpO^TU|G#hMV>WzbdAwIjkRSNhhmTpo%gNKr9WLbp z_wsayI`J`Uds#c#+o;3+^j#g{E>bRDPEPC)Fc2gL6$Ao6Lc&4-VW=<+0I?Ag16T{$ zKoCL*s2~^)!};gaFgToLA08|)k0}!?mh611< zxG+Fa6b=ImKXO_NLWus`BMIVYx!c>?yFi_?{|9h#&-hJTom>^+o^YGL1opoUCIl81 zgdjwO06_2~jxf{)0k9Sm5d+vjKyX1ZgdhYC6eRj@k0eOV&u^{>^@N&hy29X29_H3? z4^Mtq7Z=}uc`4dJMG-J-8`vW(2nrAeLxcekkO&L_gTaBKa2qHH3P${QDGFi!L#V0$ za}jFW+c^F$MmX$$%f`guf>5ZCwJ-oCX#HqoFpwBP3}_7nK)_-MgbhLn0=5ywwt>1s zo#9S@gP=aT3!%qdMxOXTTY8Sa&;Las5kg?F*rQhjAjE_oO%D!703f0waDa#iNDKmj ziU@&#FbrpVC&#}D(f|DjK@cGX5cudSVPFspAS@^h0f^ba;Q$+$o}QhnyQj4~918oJljuJ< z^CD6I5l8-N|Kj|s{TGcRqSinVMEKFS+JNDYoMI4B00d|)3IHQ)fT9q=N1FvBFpcc& zJ^w>t(f<7q#egCrFc1s~5PTHsUs2r-NcfkF z{Yy#z*?z+Rmre#?V3_D*!U_b~h(aEdfydkcAoh5Cj3hSJU>hMDu(dUaP!a0xs9@)6 zos{3q(;rGKFRi|Q-FS`-cz5d%LOqP57QFyRouW1fHj2wOvea2r98 zFx(nU^bdXge^?3r{a6J>MPUd*1o#nC=#fxZL=XV6ejF?Ufi{mrD^Y6^8$m%_T{w){ z$KJ)(<8P4?{w2T1?Jw>9XPf`n{p&pce~_>L-{VPTcUR~CpGK5oa2p8Z(H{YXMTH*4 zD<}+nO!{mD0U%);QJ5f5L=+6MA^FGP%^AAPSCjFqK;gN$G+7PQIkvBYRj^-4h9(eXmyj#3VfAP%)zdklA zuZgh!j0@>&)0ZUqb_esO@QM%Qg7YIz&h-rz1g*fdNZUD9*~XA?p2ShAO}UvJQk1mt z5>IsVO?zh@8B=GFm#ZZn0EWNU5L|_0gWmKO;teRC6K7amYHAhJfG$SseJfO{n|_xPwm`uIr*jeckX&M1c(F8s_H37e_jf9aR7a4jEX;Fljya)Nh)lr zEm84;^28+@ERbX$x(SvsI&L>O>N6CA-t?VKr7k)%>RN*k;}>Vr(?1xfBbg|J z%)O@aIGFQY%t;eKT$k3|7R#9#BDT1aYGJb#Z!K^lIuL<*AcH|CEvED(P=jLgeqW65scV&(9TvksrI`2C_ zMGeT8HYF@p>b>1zTo>zqS$hI>03WN2%Ce(3%2HKpMn{7$io;k5hC<}KlD--DupRP$ zTPTQrQc)l;yRE`;6gw%x>Op?yP1z4ar}d5;`hFIE)YTmjx2kQ%-8gLrmd)=%W8v;F zLKoPEk3q%ug~;90`6YukD(jv5Qs2ij*&m zf9`3T=J`u}baYXXs!*e`GsvF?ACYpu9@~6?b>e=seJs5cg1#|2E?`3+f_D|vd4GTS zD$}jw;a1=t4H6u1cP=A+q+anP@MODFl~VUI<8J zHh&s*UTHBH3>&ZM*vu=A>p~CK6*RpLTLh$zexs8(R=$^Sj1FP=wl|wJB0nO8^!;QA zaq_-dXbHageoX+)O;@WrjN{BgBc)sxp8L z4!qe~7BTtC%pa3;Nd8$COZhmb=(TEDsBQk|y{q2oeSsBLE|XWQN66Wi!^E|nf8I^P z*V|TemYcu5H<}Y*WPFi3ev@@m_SP$}V`Tihk7CJbR_E=^Dha%N$t=6NJY8~kSWu5$ z!-6D5b|)OQA_R>eM|W+35uw&KfFyc<>GwIi=u&WC^gL?yzOmvd?a^G&Z%kv90DL^D zqy5Oa0Y@v5tJ}K)qr6wos>@sE2|zyfR&@Mxr%Wp{x3^wwCzE@XyUYW7dcqM)V~L50 zwWc1q_kY}xUhRA#J*ksulY&?cTml)3mrRVcRKrz7{O*Hyzkdh3`qcI2WWcNA)p^~r zWc=&-gN-QHAm5ue%>%lr6}q+wEihB>9l-<$jngfGQNrKa`F7+C4n{l@czZ3tCm{1kBr}sUPT)- zKe2tF-@(#Az^R*wBcW)vI<>&+5@XQI{HPeg7rji&?b$XIZCZ#|=5yn*Z>#){w-25+ zG&8->bRD`|_F-aNwU~6Vla!`^$(44pyZOeQm~&-SAn4xE0%+M0L|Z$U`hI@{Zma%l zy+aR8IXI-AW@zb1{_uC;Zj=$);=2PzulenjADOmJuStc@K``-0=Pnm0L`gzVqB-yORq<~;~^}aV9 zftRPIN?eOoIG4Me)k}m+)we=}>S%TpSLITTZ-k#Q^c#XF+f|12>)@dTbW+Y9PW7_G zDh3UTeU)y-?rj?vp8Wzia-4Q5)~68<`vMPI&hGCv)^g<4w*7OZ3irqE&TU=A(dOj7 zuzfqc@N9oxyrjN?Citr?kx3viiY2dg?AeLRHj_-eXwi$rj_SBD29gO3`gjx*i=yP> z-E1We#OEoz*Rqk7Ei-JOb`qA|7+pb$;!7Mh2#(!_tm~6$;jrvku{k^Oh=ZgxZMvC3 z9KkM^{B#A>xRI})ylPpq%fD4|AtP;5_tf1(p>^1HDZygXi(gVRVau09>t$dK1{mdI{3KG|u_8Xps)&%F zGTplo%RfAGQfgCim|5?L|BP-_ewOF*u_C4Rk>N1FTJ#r7fvD%8g20B8O$C zz$!5-ST|v;fXUT(XpO@nIMT{do4HBBHlYOQ2|W4&y9u|Fs?;1tChoCCED}Q6n$NJD6&R5 z{A1CVCry&62lI>cbQN0@(mION{=#hz#m%Y%-0p(t^2#=H?sr zABDbahU*7+AAEUwR{B(N%uWA-zng_d0j2392e)Jlz8j}t9KfGHz*GmoRu2w~M@i`G zyd4dyk@9!>P#)&)M?Wy=rdu4Q-oxd!Wehz^X2x%mko*Mb=Hkv2$$BQ@zyWU?YV?9eiT;&+_XaY@?P)czn zPyK2y`+{sKzGB+=gw~e?&aTGTA;0vEM;rY^!7S5HCYKtFKXiv@eQT)BE+r+SLn<>H zK_om*Tgoe0(rL}{&!kPhs8&6Q+2{1E8WFVcmDJyO%}zi@b4AuxexFgBoPrT^SX0326Gav{iLN%i>a8a~S9;tcV>MewYWEm-~ zEeB*240UAin>jjNL~8&E=%5<<(cD8$P2o-`l>wP%np$&rJv~!mgQp`pHkYM?UlQbv z6va5L@KF7ydtdXq)j3xa&Q`~~WQ`kjXhRErSggHAd>bLk>`^Uon0pyVV57K9Al`>7 zo}i1sZ%~`Jw;qZd;Ww0>itC}qV4b;_ednWOetkIUY?bSHhYxy;a;$x!n^D ziXEY*QD#TW&IrYZC1V~~1p|w|@EEdLfx;oZn;oxxP#7nl?vQzY%9N}Aa>D0L)o8pF zQ%r*fBXFnt=IxkKBP*un1rYeCi&ib~Tr4h?VcdMS$@86jqhBx3V|jzGviGX@@>1wv zGvh|8CZi6oT zTHPPcdS7|z^4`yH*!onXo>s2j<_M)YPo0GnrELgJ8}z0(;e1v4NKLj~s3wnRpMt~O z7L{U=MARDnoHci2@^U+NfRXx_YF<}=;!_{`<)oKwm$tPyqCZw2*6eHYWot!)F7%qJ zas6N2$Qh}g?=D^*-=9(8`24wZeK^j$6?jnNhOHb?+k9R4_Jf({dYY-DBjDnc-@Li= zVdM0zCA~Sea*qEjW4-2Twuun-LPAWUEqehm~nN7oT z3T(`oF%Un=7zpYxDFgUJo&$+rn5q?i<~t=B*PpR_<^L+q>l80Rv^A(|JLtaOz@vD4 zaWU_qs_$#m_TnY+0BO7j+(Fk7ohj(%>|)$ipxzWzoAkc)9dtygQI@1ApeJ#9mr7^G;J^B#u8H@w$ch+ zV>&YNozXB382@Pf(htQaZWZDy4Nc@FGw8W?pMrcIM`Ib>nENyq;0| zrbqk3+;Hny_frzYWUi}65HX8>%gM?9rR&2_gJRmbCMpVqsBvb~EYDI$Tl@9beLKOX zF$2{oSWV*r?$3_O-%hQScWaw}4)9Ax0KPnBoSw%Vd#6cg?)0)x!H{Nti}EE3}adHj{8%VkU*0aW5= zcQ@ONkfpoA;S5(4g$}7#=0ukVL+8|EoZ&Yugk#-7cgGLv4W(QHZ@xkjE?0xJ>Wpip zgY@*CFH1|s)(CF1dvLhp3CINa-W@TJsLNty_22nfFi{v-9b{+B4i1+II_BY35kH(Q z+sfPs%`oZ_*h8?6_J7VlKbBYtIEl2UobzFt+pH%^8}M~9k^AuVS-O+T;L{Ch-RwbX zJ8!Y=ic!uM6XJqQ7@hb$ykB7t-6Ny2eI~|R)?q5n{%CE69;?*KTu;=8&}Txf1gG-H zXbr}4a22a8yIsMd{CaX#t8M(zCOioXpTQvP;rii^z?-^(cWOfX8(ZgH3LXpUcp#VN zMBBRnB;8FG`o$B7t%;Gqsu9)^U=exe1LdOb%VOHZW|GdY*Epp z5inR}vy?tETSoWW%V_1maNv)8@4+0VjV@k|NbLxKjVAnsZ? zu3CpESHqlm@1|6F>5TT^Fxg|C`k*$uk{f_C-WoR+sO`5GaP3K-$X~TxHoRfW*ETR5 zbs0uAThqEDRE#n7X$Y;`Wdl==WS5qd<9wH#<}EZp^+j^9RgAla$!ugfvo;PyGhK61 zXJ*S9g-cpZ_ohm-X%-{Wi?zz(CjaOnJk0?sqIe<}qt(qRv~4qv_ELRoWyv_@yPF)# z@N&KqoENLc5Z&scU;BB)VE&j@4+CbFY?L<1m(7>8A{J)awQ0;v*ZPa-5SDTqm;0N} z>l;9O!1dt9HIN8}&rXuSS5xx|?#7DQ6FoeEVyj+Wf{J!x)K|U-!dz2la!Wcmc=bE5 zI;6oCBc+tNWA^MzI7&tCO@+)b<9VCAUdR(ke%hS;bV!l_pQ(ICAmvj^LySX*y?B zA6Xbk9No}%%lFKh2>ixe3HjrC-@7_NrKXx0JNzYA1jkqbmQsPdr1;dghb9M5A$ToV zZyeE8K;c?vRZKvJXM{`CZ58`nuPMem7;TKVvPiRNCSsC-Vg}6l;VJsr*F{X?G6TE* zE$&aKc5I0=;Eyv-t>q*OXE*@n9PTxgD*7#+UJ?v=W+`d$x~z`bC@>!HqHB=k!aKy! zPI(zA3qTEhi;5rKm*Q+?9P6{aewUnZ^YjRld+E2+uZ+&?wzZfBz&O8hDg=Aj zU=*O$B3Cel6%;NzL1QQ~A+-B)jC)pgEWa3(#oaJJvp}F)l2)HMT2r^*a{JxgFASPF z$Sf78x#7{H^c3QPKxg}n-ma4C9CeP}td)sZJ z-LMs6+)X!r`Tf>58gHCsw&PiFQ2dC$mp?>U$5&Jkj(34WYbOMAw(&G~Dz3VC#S!f8 z&FL=wL51w(Uk%eeSQ`F09T=?KEWJHHg) zjw~eGP#ro%B*$$+(`Sgeg-2Yut((}DUfazG$)Kn+t>Glsa>pc}XL%wDiML>AWJT8~ z8vIUwgk$zHBc*F*--%k#e2qufQ7NqRcK-n{@wW@EJV$;%yoWkj$*}fY_lx(?*8G(< z3`*Y+y0{P_Dg2c^(;{Ntx|l65tDK*379lq58iEgO!I1;f{)O(A)Ia#SGrtnC(4jHs zS9;|ksnxPzT7}im`M4uvtqi~o3semy(A?mm-X-OTcmgZ*e1#FpWJb5jkfJGJACpf5 z8L)IGr|+}jGzYKIl;l1Jpr^Sc^?N6ri79B^PI7k;3JE77;h#HWsXqz7Fsn|2qL+ z@sP=YbT(aFFPgbvzLMdgy)$`e3MZ=pcBGXDq^1Hrg-%_+-72HRUa3n z?{)P6kB&WO{>qHt`Y` zaL{`xSxAaLZ+u|N&9|MNDzE4ET*cr zgVNY@^HRc}Ru+YOo+De%NVfw1nv>nTi!yrMMSgT}$S#YnR<^erL5)&#$p{AxM#QuXz z<1lVzcsoziUTKc@{I6 zgH=Ks#fBUk6w$~QH~J!Y5E7=}i^;S2zD`a)g1_nA;VWg zx(rTHqHP#|G8CaoEt^Qkkm`)Xni+4OT^twq6*rsZb$<_zM;6Qmo^r`F>K-Jn23WfC zmq+9bzZFhOoQj&2GjoE!kXN21x2hnK6A_@q635 zhn75p&h&7yyDacRv`^ntrVvZ|)!og2%#|39Q)_GJ%-z}OzIppSRb%mC*?s<<$&`(@V@WDca? zyk9wxQKqG(WoNWNe0}z7USdOWyv(&@(T3+#PwL^IOlI3KNo!mykyz=uoR)xAEu%EH ze9?9aMSgQ6s^J0h+O)CQIpDU}$2I8p7fYQZF~GPF|fzY#p1?rp1~G< z#*Kfed9~F`b3>Odo$cjQe=Fy!-eF&#XO0=M2WK2-1FkEbH*cCPMxNJCEPlB<=?yro z`IRCSKyYQb6zH2R)3P^~^QN_*$wRv8w1hv5dD=F+;_`P)%&Pum z>KpKOa5M1XSCr=Rr|#BQ7jFdK()QmZHb)KDpueG-6ZU;2m79Cr0~gADoK;+I%ey~I zjpmRPI(k>;#jsN?MJb(gdX2()HaxJ{yP39&+W6@!p5_tKadC)~UnR{hF@u--_+fNLACUTpnp&p7dwGb3HqgtD~Dm z+q}_tDqDKHy1X>L_jkj8K0BecG7?K%TDLDrk4_R8{qZ%o79F@cfzQ-%?0+_2eJA00 zG5jWhdad(nRL0ZPU~{5%_kfs&=f(BOat4ZG!0p!b$tL|%CTjk0?v;l>zkhA^@nw}R zwuRo5dHH!Phi1Q1$nNmUJQ7EMH`Te5h9mBpgu|Hh<(9oj)r-;~a;kO&?@n&>Bw%(nV zdasTjzTDkXw2qMs2j}kvY6aNB9hNPP*-Vg|zxPmEx(AvqgI;}p6-(&e8F+a$#Lz;v zfBUDH@iYHSFZJV)-D|0-@XWMbv}yLWsuLz+h`H zK2sQ&D?{I3Y-n4>5XFU<4J{_Jp*%Sl)lP17Uek@C@n9MiA$c1b3S(YG zMDji(_JGItgD!W37HA|WEgmzG2Xs07nJ?cz&3&4sCa0>&(4+}=&91d)#5ocWaLc~= zasC@fE7heXv-C{6%5i2BsE^7!$ZL-6DsY(b30LV2t86H8`KRiO%r|xhJ7@26!)_>c z6f`Da$I*5E$_za;PX?$>@zLEyXcd@~r3}x3n!!4DZ{YqPE@VX(`Z)%s3>$zL$Unvn zLZKGvyxNtdJv1qe=x_|a@o)01LZmFd&KCOd&*e16S}J%;?eIObwo#qpCyQo+X}Lz2)>PtCso7xi zhHTJYR#A$qXcSUppXybIsT^H+%9gd(ecS5oWkPXxFq?#S$xmygur}te%Xp*+!P*`y z#RG#HpfWEUx7TW)U#Ao_JBYp>5jU!DMV7<(3guObLfM76&1if)dU)NB*km`O!PI?N zYW{SeFwmpQL8LeTjV=pV6Q4XGR*{?(t&fpy%b2uAVJ?{^`82R3K?({CkMuPBJe9jl-o5Dzna#=OZawU1pr-8CT zzwp^*%T=QOzAPG~N zl;vFq%YxqOBHxo%l2U)liwnu3hWEDO()2m(kJ{CR9%%HCNI9Xjg_c~2&(_&}?HQC! zKjOQcP?0v9YAil4;WnWi(q>M^gJPz^@25kszB{xYDz_8|DI>K z7_cO(Cr&v2umssMe0x>(@ruiDis522QiPN&MU!pUvglQBbgBEallp{uOMs`a22>)> zFiuXGRcgPRu`a=2uLKei)4?C3_@Syl>*-o|S?-m)kP;bIskWloH#{XuU(O$LneuMM zS$bR1VmXYYZrG?pF%$qHd+x9E;ai5O3}q!2<9ovo{?jabk!l#t|w&rA+Af0$|dudlkyZ}Rcd z>iCA9Kb5L(OXB`YPm-moO<|UWWV7Z}D<&8c4$+dl>?=k(HiVMl*3Bkk`Ik=n7pCj$ z_G^qK)17T`51suo#Kp`?pN)`v2f){JV~JnUdtN++HL#RbLKpJ5WF%ZL?FhRh3SV;i z$c+g3RleNn!%0pAf>B;#IXY{XUYEITA`4wCwtwVBm1?W|`UmMzGeodqkq2(JFxn@q1VSDt7;U=2WauKu=i59}JJ-)!?D{ZPmpK-KxDUu`>+`1FaXd6RyH zC$c?*(Pq)YbFvAQ`h|6{luKz;<#%vBID3^MxT(KsD2Rs?>zC)R*vJpbiBaSe$ex}J zx*$}GOYn0FGclClB5yHZhMaow?kp111~~F4Sl@?)8kwi7yrIk^pvFN35?CM1-hTHh zR+u99d$mZ31{ztDunO~$RGRm?q{;IY6Wu9lQ8dgWYP1y-DO*Fp)}$R@V(6L&ZbmAs zdA3XujU!E-+ukAIHcKGOK##cO^OQuUSek0~C*%zy@C;K&Zd{NQy z#lhia12`fR;ptBkh(vof`~1h{S)spQG~l?iq1o@+cY6v2%fSt&Bnmx;Xlm9uke}F6 z80}VK3R9HSY_fpE!AH;ZOQ{gH#={>d!~?|g&or`Za>*vI2|&kYYoJ0OjjVO@@xX2% z23J2$5laz_2Sc_ft1;`H?i2xTbOTC?xw7|wWwMnjP1bAcx)0A0&%)9McZzLaGh;kf zvv!4$*x6TeT61<$hXb_}NJ1jr-jsx=5R~G~>a(%Vt63_diePc-;p`$B>Zl_oKXZkU zyuwmVEq^Yk^P00$$eJd zq7HmOIulFt6Aajujpg=IJQX|~HJOu&Umu%&Zk9GQRWmgwJ=i& zJ##C!DGq_hJg8Fw*U$eXa#7JBZy45!!$wNPNfMJysbH(0iD`^_&hevGLVBgvGX!%csiD6E9LYAHvEz$lGzxNJ_KG} zpwoH>RCK{Cza)s8=pHcbXcziKQURcw8Mh`c&JUk~SHY!-ULvk8&nph{H^+ zuECmNqIc<9e5g>jFLNWbQi1m)-m13pd)YW0qXR5(DHFrR=ros*^MvQ&R`*5BQ&;w= zr4R+xb32dMyx{Ro_b>y-X9pG%b?Xl9{af?o;a?;>>SOmfDwN}fLx3!cA3{I2)y9ov zQnQow;&3qC4AxRROaWMcNK<&hOTHD&aES}on)EW!c5X#=5{yr+O$!i{H&_bVxmf*^ z)g68fXcQ!j8UQT%>L}85PS|3n_b0!=n*@oBOR2N2D-Q`O#+$CWzPjT` zfJdIYjRzZ_+V%a6S*!%uJQ?w$(8re;M1P51rKa6=Vl5e^`efKW3Hj7S1je5pD>UJ| zezv}XJ$}1mmcdm*>@mZd#o3rbV}Ksxe%pT@k9@I_MTY|gY>*|I4SKOzg}!fzEj9;(5EvcB*w}MIP$Pc#wv&7 zJ*(Vm%5Oav(^(QZtR2XatB^Y#`G)ZgVz>FhEOW4iX11Def4QhHi@{dsBCOY1+KcZ_d))5u)iw5mHNh-{RClaFhEAhgpJgNz~q08PGgKrwvgI!+KUN zaxTpCj2kTtNBZi3YC488xHWhsole4JDJM$QHI}10e$vV28BvVAG(?C{IlW*34U3)6 z;II}0EqNdk6$d))fUfD`{Mp2YX3woVBQd}Vd9yUHH{9T}@u#{nV|>-)VoQry*j=sd z;)1_DOCt$qQp)!Ig?W^iHD?v>YvnGAd{0YMaY6eje3`0K za(I-CjFV6i9D!$(=+CUIEu(XOk^KA}H|AnuCC6=*ciY5O+u785Io4Qq-1*?U5Oj`| z+0_g6x?E_AbzM?T8cA4xmX#UK+F(XxzyIEA4h5s|VgUf!%;n)Uj{Aj{C3Sruh0m`~ z=4k%~ZBs?yg@aUUZ!(HGNE7iST4MsmcIva$ z?T&!7|EC46{hVzdmlu{1+iNQ;X0LtHw9-9{a1<@os{IfB25=CD_;XQ!G3Q?Sx-5N^%=|6m&QExdI=u% z$igQTNN9Ibs&1V%nX2-!!52H#JgFfJ3Ksj{E#YR??lxMW9`w-&mlv71-(l9#vb<-j zx2_$KIG@hzWh!ql_{Du=M(yD$1bM$C=yq8BHa4fa($(_NWQqL@=xeLzkJF%-7q;?* zF*3VRKYyrx?Ne10vyRor!`|^%_q(yRFFN*X+QxGi#V^jg&%>%giy4-k>l1V`3l`_k z?13S(mo?wWeWEmTi|h#rn8n|!M6_uHQwuv19OsUNj(F+_zn^wcp3;>gw8})&p0d(3 z;hsQS7ana#3c{+;y|ghT@_di*gIRRBg_AiWGYG}-47*5xDs1?fXw1hkdv;&tle5q! z-xs{WBIU7-M4?qAfBTiT&!rn!aJvYfE{(q+|6f^X&&B0z3`{*}|?;osM*L)om*SpQV?Nlh-R$wU!b~2mW{G zMyn4aQI@Z+4CvujEA8HHcR9mtNWs`JCyVditII2517{!l=_8&u#P*R|pHNi}?XH@A$ms){zxSv2i9w(*hq z(-pPwwl;7hvGbJ}xRE9vKaN-?Xw~xJw(KL76H+C^g`V$Yl%q_0LvxU0eT#YX)ztp6 zd8emU!*f~57wv5=Uangj=s(d2$jCbCq)Vn7e3)?km4N_eiiTKElk((LRqbaBrnIte zYA2~uyGpC%^ZdE3NK)0*Es+$`kJT5b8KZ-&6Q9Fo)LC{>c6dNzRv~h!=W=^h1*zIX zn~WMG2nC#qPJpFAcW9jh^u&r2 zf5y@GjU7rh!NZuVq}SSet-}|7=x;aX9(Etg7jC0o+}~^MeTo3fNo!8`&DB&R$u)H4 zlxj?Ce|cG|m)>YG9v(9AMWuHuRUP~PnIyh{o5y&(AQpJ~Sf6#{(=UHcg<%BOk5`)X=tF6d8x|7;gPrnUZcY31?fSn8h9P(W?!36XkmiU?R8q`-xZD&zx$ zQQGXg5C5|6wlW282|*-4Hq3Ht)_C@HQ(5*!$z zGk)CG`+Dh1k!|=R}Y7oCneJv7WvSGz7q@G$EC7qQYPXR=arT zq|to8)ci02JoQKA0@bwY{?T5~w{A(p@tqEO-8yNm0tM;&kqoO)A)N-Q^syceNj!M_ z7KvIT*#WCsdhi#kY!GV(5YRMeo+WewDTxfjYu1vr(YbwY7Db!xgyQxRI8GHORLb2Ix(L9&*uL7;x-+2!*oi`_)SIw5!Tj~ye0}50x z9Bd zkddGdWmn3xV2(-N@?Z%eTsuvH+4jd6h*q9TFrPEao23!a`)ccegShio-tFS*waUXC zdg8l|-$UnI_bWkH%MY7DKP)Z-e!JcwsVqA^cl$DAZg(wj$MQbsPrV3Sn>u-^6|R7W zqpg9Vn{^f2>2@)@$y?IAdg~+OF1g!giS#I1#<=;=;^%wn>xFzMy}Hm#TyFhPFF`EW z6mlLW^I%MK@b=-i02A^S-L=!-PucTYg32eEfrnPF(O3T9-Qu@I@POw1LxRgF3;-X^ z9R8~JlfZdoK&866l?rR_r_h^bRHw%HR)Z9mAvJ*42Ga6G`^Im}Zu)fS1F zm~d5)7yzY_F7OGiAv>xtr?`Z5!E!>eKbsQ!;d)}cH&x5?(3zZ`Vr3Z(tLoLEwO1Qo zdB2koVqN+5J!6+FEN#NJ z)(YCDsnZMd8RW`?mDz5LmgN0H-ou}h@mHP?@je}=qZw*+2QaaYj%*tfH3;v7f3{8G zx!GKT&rz#(=?>gwh>)+%N2iHy>MdKMzlE=V?G#=tsYkFb$iK^~Ui`xwx_Rv+97ld5 zcY#HojV03yA^`^siHzvSDzEKM*e+RwNUsHDZ(+hSLRp*0kucRpR*WhuYl`4Tk++hQ zwC*j`kQE*}=wTY4LjUcl-}Mr^G>9vE#>#Mm4Uy=j%-EpT1|B*|m#H`&=x&d~4~u`y*E% zX!r8|vUUb4>2vh0nzuRk`lp`2-L7N9)gSJt%_-sD$eNhC^(KyZ6S91UWy$g~mFJ|4 zbva7*WQAY+eSTMw>cLhek&#Z6z1=GX#3Ev!&ii(9vq!C#AtXd1FkA0>_Vz7bNl~{5 zN*cS~pY(i#Ww|#gKc<(I>9Un9o13!ddD10$E4gI?GCp|w7uR&nN9_+I?Uwfvy}O{Ow&}IWYoGhJ zAjt8&mkda2Q-MkP)i$XpLD zKg_z`E}g#}SZbMYwFo%r%lol)2(>!?bn|o8O!Pv)ViiJthn=BL^I9n@fw=Xx%}?O` zCqA)?NV%A96MN1lyA!P?9G~R6Vigr+&RlYGz~KfHOG1U}@vJ|$6njQ&F+rNRI6yW% z?zk+qQW1OwxiQCQQu7n2=!z^?Kc_8)-K3=_VobND(ha9n1d3}D5(3z@TTExF$rZmY zlKjUTK2%PAsHu%z7uszT6*5v%D5P^1D2oW0Yx=L_(2Al^b`9 zTpvGbE#3Ut;*vg~^8rM*-IqQ5a<%-P2~2mjkbambx;#GMQ-9ACu=VvWY|$|4_;$_5 z@Rjs;sljmn!1WW~{hVQ$%WjE>g|ggs5lNWQ3rUEJ%hRmm2{uU#QG=%`SYq>W4jQrY>OZXxKnB2i zK!c|;Jd4I4vn@jZo#B^AH^0K}R>cdn4qNfuX>oiC{Y&uJa0~ru{kHV@ua%3Kcqp$3 zz@I19T9|>C9^aUDD&VP3a!I`M6Cr~4Jfrek2o7jE&M>7%Jb#7N$*$J&O}}NpscXPd zL2c0a&@M=2c1xrMm9KdvX!rN<<#NzmUcl*L@0~e+Kk3zQ4NC4K9GI=WGS!Inzb;wPb&Xmb#B}x>cbrd4N34P3%Gua6wOwr-)Yd-$; zXVc9=s0xsYz))#$(Fp-2WQw3AR8?e@&rnCY7|L)PQgTD|B6I_V`OZcD!pG0PtL-m; z?EK4D-@N|SA1^lg;Y!=pmggTWd#_%+ef-YN)d2?Iy5}!mxbfiQ_ivsZ2QJa#bHQ zn(I9jQ7>t@mDfore8A#PQ0X zeCEE7ee$3D!~f$LN?IjTCbt@tlW58UFGTmG+`C0#XO_Uo?6PoKSXy}0@GM4!Xi$n# zdXZ&HGC`&^X3IiYRmyv)T6QNoV5W>>K1G&P7hQdd?#wRL#2T_N@zJM0Z$^j!WMQae z6km{lC31jel!h!t08@laYy}0;++AWa1(7mjPZJ1B$@2ZL*`)XHWFI?XA9L#@o<+Wks z!s63^b0gxn{>dLB-Ex45sI^R83z-yvo28uIEo){&H!Q?7qeY-FR082-W_T&q(K#0s zslYOWQ5(7maurvTLrC<&OhRTt)`?2>BmuYV9p={B%${M&OaU-MV&)Ui{EBRZN-_*d znyHo%U1bntNvOe#5*|H=G1yB$A`~bj1QFe%s`k$Ay(GJ*AiVF)8(X&c<-hjO=3S>= z{^5SP{>tfJ`So_~`fT;B|NEa`IhyZya0eG2e(3~}GXMY}07*naRQ08cSKn<9{_KY^ zv3QAS(b1x9pV)fn!;f71(NEv^=!3hL-@f*ZKX~Kn;Ugb9TUOIu58wOX`*tq9wSV>X zx6beE;Q3E|^&kKCxrZKl?T0TP;J$7?(~CD|fdi4nos2H0YB4mMt=B9pX~AA-J;P|> z7+RT1cOh_y!zu)PkfGd;O5tUczpZy#RI2be`6PCFMR6{uWX#z-a2{a z!o%m$T)j<^M>h|i`o*WVmpg~MyXWpZdFjV*z5F}ByI#~YInnSl&)@m|?_Iw0o>RNm z5AXfVXJ7cOubqGD{_p?!w^zIubz!|VGbZH4Ai=GM(v7WSAn6F96f$LEBv9l6Xb~}z zYrrT8v|^E7jMUV-(-Z|3&E?@@Q}?liXj7=lM5F-9Ng*ex03k2x>e0Ph=G;9@7D7?- z@n=4tFbU}~7*?`}WzB zT8w$HXI>up#G^YqCw}iA{oc*v1G&ze!t&ZX<-tczuXoEvJNe|(2d}(YH&5>E$LlZs zbS~#Gcou1L8Kt0&g&A&wXhZ1{1kIT;&lJtl8hcg9TqY3g1}8h{nJ7?NRW>t92@&RM zpru5Iy4<~W7y$?f0%pDjhF~CrC{h~xJWI;(#587EjlDp@N1y#XUDlBpj7b{AVJKmZ zM#`BUJ{3|iBr}YJ96dP@O-YyV9vYMY5lBx$FEQ4E?mC0``Q~f_HMRF6J!o%AyuM>^(qSu=?xi$B?WfT*hnLwge)yBDG-(v#dN5ldmr7WaEpXxrxY_9 z0#V?J=JK^N`Q&q-Go!kN3t4ChjYI7!RcIb28d!q8nzhJ6gPGJpV=|J(dMd+9=_XsJ zXhKpJgUafH-}vDBmcBliT7T?wzqEU0-nemztD83#i~Aor{g3~*R}R)&S=^S~T5J{! zoA+$reb3h3yYqcdKC(0Pp#APY{c5I^9a&(FY3?La&`4d_IJz-y9rcn*bC15TGD!?G z7f6br4Cc(FTOu9aT@p@1b(NT(e@t9yPC2Tq&`|y*i@BZcW2TtgrPk-U+SN`>#&wk|XZ++!> zF+TA4z5n8m-`?Llxv|aWHl*GA^n=GYSEtWxZEx2*9)9rq|LPkY{LMFRLFxu?%h?WG zV{tKinj@0ApeH23v(8F7D}+o33?(ZRfEhE*NE#pp!kOZTOa~H9HxcNK$*d{SNcWN$ z7(Jq?f-UF+qFC)2p)d-v=q5D+Z5>qCiA^!8gv0$CJ+tO;#5{d^C$eEGvmKR;^hF?D z!6%;koKVGX$)v;RvVjFvDh+KZYlHxYA;1DDLAgqYEG0_=8&Q-UCD8|%67uX4NW(9 z+0ff+*xn{1RMxi+T$LQc`_|SbI0Zcsy-h~B%4+t_4GWS9W(tvRM_1aZ^J6#3vIG^D zj=k&Ci8D3ZYGxUO>&zlfr5VI0pZz?zAvLl|BMfdZx)l$uJr`mZoPPyE&1?>Z%R&a1 zRVfppj0y?{GZ>WNXbKAJFgstTd+*zM^9L_K{OludefI}#>UZ6}`JK0ywsVd$lw`9k z{lv!d#Hq7cvlOhRa_jB=P=KpoAuJ3R^?mn} zW$p*__PMiO2Frf?=H6mwbQUXxA*)6TSh%zIr266B{`TopXjQJ&;qmgMD~d6b#Suz{ zb%!Gq8V**g?F|Es7MU!{fFE8zIC1vG3{_KPkQwr$>$k_9O*M5p8f_C9LACWvGd}V3 zmq?oC@~PjH829$ZC<`PNmHzX4&v0pL+g_-pf4C92bV> zHqs)DMZ>zSw(7E%1Pcum(t56gKFF0s6$K-E-#@VJ4P=LkLDE2vTYGzVpV+wfq0?vY zd!H0%b~e8GfBs)@-rm|gRodV~l%tk8g)e0owMyT1W1ieRxi~rA{{Je1lUQ4`^E}M& z`+jTvd+&4ZIk%}>Evpk7iKNIXl0{M^)flo7MNkv9u#o^UGV(kbCWB1kVMb{No*1wV zB!Q9$RvINSV5qS`*|I26q$r9kQf!K>Tld!O?>T$#f35F(Q+l57UJS0o+=!Ej;#yV^ zGf~Srp6t#~Rix&agwaf}TSuj~#FFJAue3%kwk_y6U;{`R+jplZopbzHOU zmQHyFp4@)W+8J}SO~3Zs)x)FJ;i<^uF=SBlV)X4v$wVA)-Pt~SPLbS?+$<60vQPI< z9&Fn@Vyi>;YgYqnvS%La(F&qeY}?&=FvIeb^<>kUG~^yw;bTRRfVPASU>(WHYBtCP zVmXPL>CnhxU{e5PYUb)tRp~uOMXF3_*i;py=~=5MXO+OB>_I?$?X$ne^qg~4(F|nU zokZKT=11?|!Lmgfp|i6y2!ikb@ZRP$P>tZ`#wk*y@7}t1(l-;U)L;0KXV0(hQsdfF z=jS)BFI88cIWP2UPwoDv|Ms_&m-p}N!Dfh#YtJ&#tiJ!jp*C&lS&h5XZ5Wdx$K0Qk zjznk8WdTa5TGLS{gi52u3NgwB+M8@7swWFG9Hc7(7AShA%Vcv-QbD4j2pJkV*;CaZ z1`@N`I9B#8M!5qb!ziFKs$4yD$gCb#5h$9oRaLZNl2wI|L5)52|L~sg^8Y?Fq<&gm>~u=SWX(%NHbWpP&tAzQDG>6 z5`kVEtvCp|2?uI9GB#<)swAz^l9r;W#-!*;hmb%N8dx$*aFkY!W+lh zi%Gx*0|H8k5+!T(K?qZAG8+TOax0WdNi5Da0=0@<55q8^HZxf$78`&3GrttGRhYR$ z8{tkAI)qO3WC9r`V3S3QG^uRR7|bPGVajmPp_g8{%1z(&%_slP$L_p!!SVRojT^`H z$?m*;<;&mx>c9Dm=!NRU{PB)u+$yz5OUqL#An%1iWd5 z(rU^EN>E9vq}a?MAl=1j$_3J{LPJ<-Su;!`Dma;%2Hn^T6W)LWNT{jW#M&#W5jj~Z zng}?HfMGf21RDX!6HaQA6VO!I71bv*NH>;kzdHU3IH_y(m+_-t2nCj1QPhToRA5Yl%E&;XBAn^q(u%nlDLB~HaNKv9f~!D(xyIRSp`Grt-p>+qfUOHS{bj}Zd^26W$%Gb_rzW9|t_~PwH z7m2nQ6Sm4#G;>W34JWc;>qQ&3bnk7rnj-zoD`(4gx7%#r`o`Oj582p_f+oC4xFKm4 zI#s5tfEca_LXOE)23y(z6IdZcEi=SAl2JBfRjV*FfFi}rqM=TdW5$S56+==EsDWt| zQ;dR{6;L_@G3ek_wz4hK#AfiJ=GSNlVNXSX5Zu_&tEz`# zvUqo4oapZ)HiWbfbq)~(ED48@FM z7LM!Tn25a{HjA@6%odxrr$gw)@x_ENm_a7e(1fn=)g1zS~G|+Q7a@|R* z*+AHxFjx~H);^W7$biXKYZ9s^2Z)SxAxt#jI2p~$Y=@Q|Sx^a)Y|LQ=Z3vjtGK$$i zKq-p0Wvp-js&GsQE_xaPBxH;{3{2*tnv!;uO=6B}4?%Y2bYLE%hljy~S735T6f@+Rp4Y)T{ zk%+2-3^xikqTndj9MP6yIYc`G36`rRZA-R#dEz~WOxuvjsGREJG3TP@HnO^5z(@;R zqc%yiEC^gHH^d-KaKEl{l~5_*NGw@{pZMf&6bIDJ46A~4iwG&f1`oKC1MW~25Kyw{ z2Bkn3tSPI?ronk~dUExtlZegn-u=rlE*})zin2xIS`Ou`5*=9GGfrE*{hc(Q|rj`V5tCE=OCdhIZ5^76fhSa6ZiWUHp=6jN) zF$$eXG18XFay2kX8AU5GVWoh{$*F2C{!pQrQgt;6}%-RU+S-RU>p zc>3A<`onMDTc@-vyUp^!kH7HD%P-%3=gx~CdH=iLc=O`o(GPDO*Gs_N?E>5pm3DJX zlo~SKQd-huQ*F8!mYELm3$Zjz(vg>tYoRx`>uB zodz0k1j*{eA#oxmt4ax1vMu&twqyoTRIL{^Ba7&EOAYx*4u0%YzbQjuqlk_`OxMzb zGLkePHh2mIw7M2@200z%%G`jn9Av`n?CJL8?j?)Ggt06<9uuCRh>=+zetA8)_CntK z&O7&Rx4d@aJh{Dc_q(^3D_iY1H$U-r|JA?#qar{2$yYx0bAR{i|K^J)yK6u4s&3z^ zwEybs-+S~R9j(_S8eHIH^=d_m9_bbt2v8r;N%XS8(XID??AhD* z4-bCuXlZfw^a)*04*N&%T*7I3;*ymAAaAT{r)#D9zL-OwSm(- zaoZ!+NTbS-1*cVJ2#ublRxvm!(}ZNW+hG)tMiK-&Ap>D2z*teigcXV?nufinyBR{Q zrKp@oqgQW9FB!-E{R?1AjZ5;zl^sYHv-Y$w^?2sl4mpsOp{xwcX89QW*ynyTW`p+5 zpol7Lny@fHPNq<%1OW^cDrOb~FiMosXNW?SQna1C~tp3do$Eq=7^M%ZAn9MjTmn zY@??@qY3T>$DBq&VD;nifR!0F_xrP(Pn*Nwxc#uMZ^CT}x_3-UE+oomwy__1+P3g4 z$+V}#c<}h*+W8fr5+wvNRD5tTb|*Wpk4K@a7iL0^eOxAA|H5zhxM8I0U`gzBL@W*z zGBCxkOR=+wC~(#&3Pe-J3^scf#fQ<_Q#a43);1g9_~^lYxAehz>pKtWx*Ct4e`fsT zfAaVCfB0`7;>FMZWE~rJ=w3j zwwBwx*q_|EB8+lhAFevzwSwAwxW|>PsGv?T*TLCX$HRWNyPB9V*GI?Ac{31Mj_u^2 zp~(`30dJlh_g8kCp#yC?}iZ%Tdl2fzL8 zr=PlO=PUo&KmN-<`j?OX@|$t(nUDSaF9Ntc)_1<~?Os3rm{#RfA%6Gr@4vCA$ zzH+`BRC8WD+3z+z5e$~uJ6V8~>ogb)tJK067V~kn(+z4=wEg4Dv(qcCa2Ioed)Ic( z%igy{RxZbRu{|ZS2{ARfR?>&Nhm)J9uuJ{%-CHd;nJOL6p1IajRvwQ| z<^ItRzVhU=KmYzw^UGiS&KVxosHgwafAlB+!@r1edmksSe&OeDyzhM(+jZpGQ_p_u z%U^%?N1yq(|L324^UH5l(_wvdvOAg86uG}YPR|yDV(bqWyWPerOqEnZWKC5sfhfi0 z-6vO`+Sl-s+I>X_UAl90J{REXwA)%KC zI6ad>Dar$GGE9I|gGo1Jna6rhsz8@9Dacdz`+r+?+M z_uqVc<+;B{-y>Lo| zIIVc}*p#5wxSs5anc0+FT6L)44quYxG6dDmbR`gjl_o%J{`^6_`cuF7)3^WX z_Vb^ACHL#x&+mWt-f{on+uyilO0{!KLU4iQEQo}YOH^0P!h}gz8YCFKG&C2xz@g;q zR0c>ZIRk_%84yqv#DHX!DV5R^X9_Ud=owQkD3MWuI7`sFc)3h3D#)Htw2^MkDU`vg zp5v0KqEWK;1u28CedbqG%qV3?G|uKWI$6!C<_3#MHLjJYu9nRuGyyJGo6Z;}8<%!c zS5NxYXRp8d+6UkM=G)Kw$mz4&XZ9ESeYQXRfBrr1{rjK(%nLVOc;yQ}`{J!P-?_N8 z>iLtCjxYW8_s(zJap4=^eDuz7=H9$H zu^P?{HN)@-_R%Ubsts$waf(#XHBmuEmbE~xEQT;)O}K!?ELt`wt3owWK$Fw14ddv_ z3ia$$s~DWjU?2;m=X7fYQ;?xl3a;d&8-S>qg2I?cRwR?m<7yU-w3xGE$mS#HC9d%yUKesGz5`^}4X{!oW&58wLkwN1Ts>j&Svt$P!v zSA4HNGVN<8Jbm_NxQ!esG;DwH*1I_ZWH`}m5Ct|D$O^mcCJlH)O{c(+XGNj}(@}CD zi*U6ncX5za5Q}L@ot3byKqVtsnYE~2I2W-JjUHl8PudB{VI(QA=xJ`@L4=*M44W{g ztEuK;LOuY>3B&;iW$?96|AuT}O)YXY$5d}oQduID5Tle)2m)lfh?uNKad7n16hHjN zi-)_HZK&TkKgltdJpaH;o5xR{d;fE1yR+lv`sP3VC%4C=vlq@k^1@ky#y9`qYxf>J{@xES-#yk^CT~98KCSTi+2-W>d6q<#?K<4M_1GEd&PbqH$rMl~ z0^!PJv(RWJ0adK1hAOwoYz}MowU5oTxm4T7=oySTEtbHP=u`oPl-V1ZWncnL%!1*i z2x0}q*`U=3C{c*YQYUM;m{3fzW2Q-9&T3vqm9rOS1_@78@wHF?YE?s|%n}180;U>~ z;!-ToVamNQEy^V|BvVPq&L*udyz*RZT3sCaiJ}v+{KU`xOc&L4@6KE6yZgK2*2CZX z-6J>GZY)3d(;wZd;COuJhld}|FaGu0x1U_X?Z)}lTMx$Ey;z~sZNK{LwRG#MzUz0s zbMN93jG|er08&Om>hLsZM7e?_svLNaB~3W8O+ri?KxDX28&VXJ(=AXg8nyr{P!lO2 z5C#c>v$BsE-7Gi=xdwuCPLZiV7L&BA86XIiD+RfbOt5BVPMb8W!En;x$KUu>IoygR z({!Wu#DeTxLYR&Uu-uJeOU?x?;fS(}B?vC^g%8|NF*bN~>+sC;oe}SQ`o)j_>}OiU ztTFodF%05-~XZKo;-Z`*^fT;;)kFAZP^^}-oN+I)ebdesg*6b0MaaABV+2-6-kr>fNEMDFpY}Dq-DXzV4fJ$FrUeqVdvV>0FMQ-x z4RMn^ltW+mkA7;sd;f>ue(Q%{d;8mOzg;dGKi1yNxN_;slPilR?>~HyYnWF*@CzT~ zTaW(yD-Xx!G~?>73;*MEHbRPX)d|NZCx`@?(tIZ>E!U;6&@+dFvX?9Rgn z@4V|C*Uzq9-#qKvfARGPe08c<6RX6Yy~6FrjUA4UA6_sar;=igmXVFZdz?%$QqYux zk<3Uyn#guwb!KDp1XZLAPXew5eX`ObGmJ{Shk(c@X=kxZ0k6o-SYI6-hQbDfwVFvajhX1VzKr+*V3Xy%9#!UZYCHo6)ZBC;gV zU}Qzt(QF`Sy3A@7MGO`;%f>V@>i_@@`bk7VRLS|x&9d3;&bOcc`JcYn-~QMCG%^Lx^e5lMcv@V?dmT)dwA!;JMUbs5SyH25riPL)>v0hkM}Ni1r$qZ zTZqAIMvx<`q<|M|sHzyOv^rHpV%<0fLpEU*C$fR00VHUF#R;~mL2p0^M;q*olSWw! zYIOv;az-iaq6=jZQ*MAMaB6O=9X%J-m@Sti*v)1fGwsS+M_C?OiHax7_9XMb*FOKt z9?`heK=lbeDRH17Qvjiq6 zy1@pg*$kSFDu#FI#IFEK; z%4^3mTQb2V2nO7OWkdjlXfa2X*b3E9!=0!G7*uFhfpT*jLQl<@EY)CXk`}bYsKUgi z6($c&$_y~IaxAE*7#%U?O-HWbKr2_NG?igb_GV9(k`4wk7h)P5|0~Qz^J* zyJR)6XbF)Hsj@888bh;*L5VCQU1@{G3QOT*0fh}Y?Ff%`Oi0wS!aKV~&1xx=t-Qus z*_I%Rc}SlIr;Qwn?)7*W6Ft%x}T=|-g{kx_l>*y?23bXt4WCSxEMie3;E zOcVY5814?!t#&7;u153n(Ua;yMAf*mJ4Mul>+xC}QB)z*eOVO6T#j{MC`xG4wG|Vk zu|Dj}SxI5;*TZ&MrW|I(W@ZX=93RK_1Qs#Y{bt$PR-h^hf6GtqjNMZ~H!u&6FKbnD zb?(n^-h|nl_wL_kZrK^FpI$||P>qN*$EY^Oz(8SYZMg-_PrdOUPfw^kG8*5brTeIA zV~J(X5xo=r80(3ZL1r%;$&HLP*S1k}PSJy85W`%k43muGer`4``aGsN%@B%@_xG-D zu2!JMZmbW`uDA}sLNFug8kbtOMuBRF{p0P)iA6zK!cZ>E zs>1QG2e$A&s<-Q>9VKlqE^f_vwiLb1-IXi7kmAMt$8BL~o3XZK%W&j&|L}2-ZMM-i zY+cU9^5Nafv!_mDqx})CqgXT#mvy>RijA8&Qq5IHQ>_VNVvZHN-A0?6=JLk_`aWRKucgjAvcC~!+;GPo0to^wD{8QBv z-aPo>!`KLI3F`E$PeTSoXj8H(F{jc{S_N)^hAJXmqz<#T;Go0`a1;>}6$uMYhUftW zs!&J?v*x;H1#rYv_BE>2oP7O_UqUU_3Szi}=)h3zq!Fqz*~57RH$vF~jWI`c@#v|| z*-n)uU*0=FO+F6j>echmfcv9|_hNPjW*x6xyOyw{=BnfUduH$Aa^oZd0PX(h*4wNd z0wdS+=b!2@6c68dK(<6v)Y;7wB@1%oqkGvyGiFm=+4TffaY(6iYt5kM5f-?LEw!|2 zCQ-+U)iEO%P&6_c%hp61EC{Tyn>5lDSs5WFb7Nazahd`KSKV;9k_kxC?962su8Qe#Ng|%{VKyG zW}sC|mDEGI*=|UpT1Bx?nfA0g5Y3n%XsS4a7!wRKqah<=t!jb;PNqn+J7FJ)E-N`5 zYj_K|I9f4l16D<1Hj=6_J2tbgk>N<0DYB`??A2C|W}s7pGb})*B66Y;P`R0_z}8JD`c#f@69?q#%}OD9UCPC=-Th5RI4?;V2Yg{4FVw1Zj?BBNMd%1%tF*RB@VU z6=9ZVhN56FfQpRr5!$5o^oCxG0OfK6kxnpT)C~8Gs$yG~=q<)^KTBlouJn7ihV!DL zVJdlSj>*Z_KKaXF5mdEeN#PbuoLpI&I+n~yl&TI1uw-;$O##_J66I>a0xB60Py|R8 zSVrp{6O3YIrGpW2K{mpeq?rwxuI`vlAX0@2wb{(F66gR5?9PFR0t9fF!sw8TXc$QW zL(pVbnwnJ`s%V!Tpas(?fkvw2u9~)JxmOKGHn+;mXiA%Qh>7M*xzJz>9GFo7l`$im z3DZ?gam0HJI?!|JLWXlp*r-}Dj%#L^L-2qJS04F^Py8ITEfye_q$nDUt)gEEF)Ro% z$8iu$B8W*AK{}2^*s_sC$QZ0ff&tQ_)BrN4yh%AQC#|ef2UE677G=XxPQ^@h7h5qS z14NqWMT4_ou|;Bv!8xI@O+)F~0~kc(EXrmS42p$nLk<^21HvRQO<7bqqZB|=M^<1i z)dpeZwqin4#Y7MV%P3nist*lix7J=Um4-~CkZ#m0t6kal!6(jb)tZTQOz(jz#9AfW zg$NWs_WFOwW!t8c!2+5ZED7TFWZ@=IVOH&jtRo97WmF4hf2XKWeGM(NA0!^qpkDUz{BydCYV(KvSm9&0S2)0^^g7nc0vXkF@#q9ThOL2T3Rn7 z`l7?(;0&lR13(c?o>=JQgnKy|4OK3>nMNZ`CcT-MaurpOZ0hLTfo_~;WMz2~GB5{4 zHo8g`#X>?@#kaCk+M3PI}sEJ*?^u+A+C^lfgpadw(jggi-0fef+ z=|!BXRx!aT2OL?GL@@$XO`^F~Wmpr{0FuKFL*Y@1LWvBc7-qq26WIVx)FQ4`3nQ5< z0fsGT29yAcK~E^)G^Mo=W^0~l2{1<~B55L#M5qu|)0Eo26siKuevxFW(i{r)dFbXJ4f|(=L<^(X3v7?TB?Z^Lv*zSy(6sV{oHbM}E zO~q`>>DC^jNKa*}-r%W1nP_l!$5HOKSrb8^6L5)wm3U7gTecC!9?IfGMhQ$KDr%E( zFf)=C-BP2mW=12j;0QWOvlWsFccG$~AXG9+762oW#)3XLO*EBK$!aQ899;`&(ovdy zbxE^9AWTNOOhlS73pQ*w379>~Q^yD%*_#XRv5FHpIHMxxWh6(`n6B7v7CBnmKfKU+ qYm>*!?pX5z)nQU78pyPr{Qm*7q>-EcNX`cU0000 + + + + + + Original + + + Hawkeye.VisionBuilder.Workflow.Datatypes.FilePath + + + Image + + + Image + 1 + 9500 + 999999 + 50 + 999999 + 50 + 999999 + + + Image + 3 + 9500 + 999999 + 50 + 999999 + 50 + 999999 + + + Image + 4 + 9500 + 999999 + 50 + 999999 + 50 + 999999 + + + Image + 5 + 9500 + 999999 + 50 + 999999 + 50 + 999999 + + + Image + 6 + 9500 + 999999 + 50 + 999999 + 50 + 999999 + + + Image + 7 + 9500 + 999999 + 50 + 999999 + 50 + 999999 + + + Image + 8 + 9500 + 999999 + 50 + 999999 + 70 + 999999 + + + Image + 14 + 9500 + 99999 + 50 + 99999 + 50 + 99999 + + + Image + 2 + 9500 + 99999 + 50 + 99999 + 50 + 99999 + + + Image + 10 + 3000 + 999999 + 5 + 999999 + 5 + 999999 + + + Image + 11 + 0 + 999999 + 5 + 999999 + 5 + 999999 + + + Image + 12 + 0 + 999999 + 5 + 999999 + 5 + 999999 + + + Image + 13 + 4000 + 999999 + 0 + 999999 + 0 + 999999 + + + Original + + + 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 + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs b/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs new file mode 100644 index 0000000..8a2cd72 --- /dev/null +++ b/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs @@ -0,0 +1,100 @@ +using OpenCvSharp; +using System.Drawing; +using Ninject; +using VisionBuilder.UI.Common.RecipeProcessing; + +namespace Hawkeye.VisionBuilder.UI.Sources.Emulation +{ + public class EmulationCameraImageSource:IImageSource, IInitializable + { + private readonly EmulationSettings _settings; + private readonly string _name; + private string _currentFolder; + private string[] _selectedFiles; + + public EmulationCameraImageSource(EmulationSettings settings) + { + _settings = settings; + } + + public string GetName() + { + return _name; + } + + public async Task GetImage(CancellationToken token) + { + if (_selectedFiles.Length == 0) return null; + var id = (_index) % _selectedFiles.Length; + if (id == 0) + { + if (_index > 0) + OnLoopOver(); + if (_settings.SingleRun && _index > 0) + { + // infinite sleep + await Task.Delay(Timeout.Infinite, token); + } + Console.WriteLine("starting new emulation cycle"); + await Task.Delay(_settings.CycleDelay, token); + } + else + { + await Task.Delay(_settings.PerImageDelay, token); + } + + var path = _selectedFiles[id]; + _index++; + + + + Console.WriteLine($"Getting image {path}"); + + return Cv2.ImRead(path); + } + public void SetFolder(string imagesFolder) + { + try + { + _currentFolder = imagesFolder; + var path = Path.Combine(@"..\Data\Emulation", imagesFolder); + _selectedFiles = Directory.GetFiles(path, "*.bmp") + .Concat(Directory.GetFiles(path, "*.png")) + .Concat(Directory.GetFiles(path, "*.jpg")) + .Where(x => !Path.GetFileNameWithoutExtension(x).Contains("_analysis")) + .ToArray(); + } + catch + { + } + } + + public void SetFolderAbsolute(string imagesFolder) + { + try + { + _currentFolder = imagesFolder; + var path = imagesFolder; + _selectedFiles = Directory.GetFiles(path, "*.bmp") + .Concat(Directory.GetFiles(path, "*.png")) + .Concat(Directory.GetFiles(path, "*.jpg")) + .Where(x => !Path.GetFileNameWithoutExtension(x).Contains("_analysis")) + .ToArray(); + } + catch + { + } + } + + + public event Action OnLoopOver = delegate { }; + private int _index = 0; + + + public void Initialize() + { + if(string.IsNullOrEmpty(_settings.EmulationPath)) return; + SetFolderAbsolute(_settings.EmulationPath); + } + } +} diff --git a/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationSettings.cs b/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationSettings.cs new file mode 100644 index 0000000..17622e3 --- /dev/null +++ b/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationSettings.cs @@ -0,0 +1,27 @@ +using Inspectron.Settings; +using VisionBuilder.UI.Common; + +namespace Hawkeye.VisionBuilder.UI.Sources.Emulation; + +public class EmulationSettings : ISettings +{ + public string CameraName { get; } + + public EmulationSettings(string cameraName) + { + CameraName = cameraName; + } + + public int CycleDelay { get; set; } = 1; + public int PerImageDelay { get; set; } = 5000; + public bool SingleRun { get; set; } = false; + public string EmulationPath { get; set; } + + public void RegisterSettings(InspectronSettings settings) + { + settings.RegisterSimple(this, () => CycleDelay, CameraName+"/Emulation", nameof(CycleDelay)); + settings.RegisterSimple(this, () => PerImageDelay, CameraName + "/Emulation", nameof(PerImageDelay)); + settings.RegisterSimple(this, () => SingleRun, CameraName + "/Emulation", nameof(SingleRun)); + settings.RegisterSimple(this, () => EmulationPath, CameraName + "/Emulation", nameof(EmulationPath)); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.UI.Sources.Emulation/Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj b/Hawkeye.VisionBuilder.UI.Sources.Emulation/Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj new file mode 100644 index 0000000..d475293 --- /dev/null +++ b/Hawkeye.VisionBuilder.UI.Sources.Emulation/Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Hawkeye.VisionBuilder.Workflow/Annotations.cs b/Hawkeye.VisionBuilder.Workflow/Annotations.cs new file mode 100644 index 0000000..04d24ad --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Annotations.cs @@ -0,0 +1,1704 @@ +/* MIT License + +Copyright (c) 2016 JetBrains http://www.jetbrains.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#nullable disable + + + +// ReSharper disable UnusedType.Global + +#pragma warning disable 1591 +// ReSharper disable UnusedMember.Global +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable IntroduceOptionalParameters.Global +// ReSharper disable MemberCanBeProtected.Global +// ReSharper disable InconsistentNaming + +namespace Hawkeye.VisionBuilder.Workflow +{ + /// + /// Indicates that the value of the marked element could be null sometimes, + /// so checking for null is required before its usage. + /// + /// + /// [CanBeNull] object Test() => null; + /// + /// void UseTest() { + /// var p = Test(); + /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class CanBeNullAttribute : Attribute { } + + /// + /// Indicates that the value of the marked element can never be null. + /// + /// + /// [NotNull] object Foo() { + /// return null; // Warning: Possible 'null' assignment + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class NotNullAttribute : Attribute { } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can never be null. + /// + /// + /// public void Foo([ItemNotNull]List<string> books) + /// { + /// foreach (var book in books) { + /// if (book != null) // Warning: Expression is always true + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemNotNullAttribute : Attribute { } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can be null. + /// + /// + /// public void Foo([ItemCanBeNull]List<string> books) + /// { + /// foreach (var book in books) + /// { + /// // Warning: Possible 'System.NullReferenceException' + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemCanBeNullAttribute : Attribute { } + + /// + /// Indicates that the marked method builds string by the format pattern and (optional) arguments. + /// The parameter, which contains the format string, should be given in the constructor. The format string + /// should be in -like form. + /// + /// + /// [StringFormatMethod("message")] + /// void ShowError(string message, params object[] args) { /* do something */ } + /// + /// void Foo() { + /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string + /// } + /// + [AttributeUsage( + AttributeTargets.Constructor | AttributeTargets.Method | + AttributeTargets.Property | AttributeTargets.Delegate)] + public sealed class StringFormatMethodAttribute : Attribute + { + /// + /// Specifies which parameter of an annotated method should be treated as the format string + /// + public StringFormatMethodAttribute([NotNull] string formatParameterName) + { + FormatParameterName = formatParameterName; + } + + [NotNull] public string FormatParameterName { get; } + } + + /// + /// Indicates that the marked parameter is a message template where placeholders are to be replaced by the following arguments + /// in the order in which they appear + /// + /// + /// void LogInfo([StructuredMessageTemplate]string message, params object[] args) { /* do something */ } + /// + /// void Foo() { + /// LogInfo("User created: {username}"); // Warning: Non-existing argument in format string + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class StructuredMessageTemplateAttribute : Attribute {} + + /// + /// Use this annotation to specify a type that contains static or const fields + /// with values for the annotated property/field/parameter. + /// The specified type will be used to improve completion suggestions. + /// + /// + /// namespace TestNamespace + /// { + /// public class Constants + /// { + /// public static int INT_CONST = 1; + /// public const string STRING_CONST = "1"; + /// } + /// + /// public class Class1 + /// { + /// [ValueProvider("TestNamespace.Constants")] public int myField; + /// public void Foo([ValueProvider("TestNamespace.Constants")] string str) { } + /// + /// public void Test() + /// { + /// Foo(/*try completion here*/);// + /// myField = /*try completion here*/ + /// } + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field, + AllowMultiple = true)] + public sealed class ValueProviderAttribute : Attribute + { + public ValueProviderAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + /// + /// Indicates that the integral value falls into the specified interval. + /// It's allowed to specify multiple non-intersecting intervals. + /// Values of interval boundaries are inclusive. + /// + /// + /// void Foo([ValueRange(0, 100)] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate, + AllowMultiple = true)] + public sealed class ValueRangeAttribute : Attribute + { + public object From { get; } + public object To { get; } + + public ValueRangeAttribute(long from, long to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(ulong from, ulong to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(long value) + { + From = To = value; + } + + public ValueRangeAttribute(ulong value) + { + From = To = value; + } + } + + /// + /// Indicates that the integral value never falls below zero. + /// + /// + /// void Foo([NonNegativeValue] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate)] + public sealed class NonNegativeValueAttribute : Attribute { } + + /// + /// Indicates that the function argument should be a string literal and match + /// one of the parameters of the caller function. This annotation is used for paramerers + /// like 'string paramName' parameter of the constuctor. + /// + /// + /// void Foo(string param) { + /// if (param == null) + /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InvokerParameterNameAttribute : Attribute { } + + /// + /// Indicates that the method is contained in a type that implements + /// System.ComponentModel.INotifyPropertyChanged interface and this method + /// is used to notify that some property value changed. + /// + /// + /// The method should be non-static and conform to one of the supported signatures: + /// + /// NotifyChanged(string) + /// NotifyChanged(params string[]) + /// NotifyChanged{T}(Expression{Func{T}}) + /// NotifyChanged{T,U}(Expression{Func{T,U}}) + /// SetProperty{T}(ref T, T, string) + /// + /// + /// + /// public class Foo : INotifyPropertyChanged { + /// public event PropertyChangedEventHandler PropertyChanged; + /// + /// [NotifyPropertyChangedInvocator] + /// protected virtual void NotifyChanged(string propertyName) { ... } + /// + /// string _name; + /// + /// public string Name { + /// get { return _name; } + /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } + /// } + /// } + /// + /// Examples of generated notifications: + /// + /// NotifyChanged("Property") + /// NotifyChanged(() => Property) + /// NotifyChanged((VM x) => x.Property) + /// SetProperty(ref myField, value, "Property") + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute + { + public NotifyPropertyChangedInvocatorAttribute() { } + public NotifyPropertyChangedInvocatorAttribute([NotNull] string parameterName) + { + ParameterName = parameterName; + } + + [CanBeNull] public string ParameterName { get; } + } + + /// + /// Describes dependency between method input and output. + /// + /// + ///

Function Definition Table syntax:

+ /// + /// FDT ::= FDTRow [;FDTRow]* + /// FDTRow ::= Input => Output | Output <= Input + /// Input ::= ParameterName: Value [, Input]* + /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} + /// Value ::= true | false | null | notnull | canbenull + /// + /// If the method has a single input parameter, its name could be omitted.
+ /// Using halt (or void/nothing, which is the same) for the method output + /// means that the method doesn't return normally (throws or terminates the process).
+ /// Value canbenull is only applicable for output parameters.
+ /// You can use multiple [ContractAnnotation] for each FDT row, or use single attribute + /// with rows separated by the semicolon. There is no notion of order rows, all rows are checked + /// for applicability and applied per each program state tracked by the analysis engine.
+ ///
+ /// + /// + /// [ContractAnnotation("=> halt")] + /// public void TerminationMethod() + /// + /// + /// [ContractAnnotation("null <= param:null")] // reverse condition syntax + /// public string GetName(string surname) + /// + /// + /// [ContractAnnotation("s:null => true")] + /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() + /// + /// + /// // A method that returns null if the parameter is null, + /// // and not null if the parameter is not null + /// [ContractAnnotation("null => null; notnull => notnull")] + /// public object Transform(object data) + /// + /// + /// [ContractAnnotation("=> true, result: notnull; => false, result: null")] + /// public bool TryParse(string s, out Person result) + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public sealed class ContractAnnotationAttribute : Attribute + { + public ContractAnnotationAttribute([NotNull] string contract) + : this(contract, false) { } + + public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) + { + Contract = contract; + ForceFullStates = forceFullStates; + } + + [NotNull] public string Contract { get; } + + public bool ForceFullStates { get; } + } + + /// + /// Indicates whether the marked element should be localized. + /// + /// + /// [LocalizationRequiredAttribute(true)] + /// class Foo { + /// string str = "my string"; // Warning: Localizable string + /// } + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class LocalizationRequiredAttribute : Attribute + { + public LocalizationRequiredAttribute() : this(true) { } + + public LocalizationRequiredAttribute(bool required) + { + Required = required; + } + + public bool Required { get; } + } + + /// + /// Indicates that the value of the marked type (or its derivatives) + /// cannot be compared using '==' or '!=' operators and Equals() + /// should be used instead. However, using '==' or '!=' for comparison + /// with null is always permitted. + /// + /// + /// [CannotApplyEqualityOperator] + /// class NoEquality { } + /// + /// class UsesNoEquality { + /// void Test() { + /// var ca1 = new NoEquality(); + /// var ca2 = new NoEquality(); + /// if (ca1 != null) { // OK + /// bool condition = ca1 == ca2; // Warning + /// } + /// } + /// } + /// + [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] + public sealed class CannotApplyEqualityOperatorAttribute : Attribute { } + + /// + /// When applied to a target attribute, specifies a requirement for any type marked + /// with the target attribute to implement or inherit specific type or types. + /// + /// + /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement + /// class ComponentAttribute : Attribute { } + /// + /// [Component] // ComponentAttribute requires implementing IComponent interface + /// class MyComponent : IComponent { } + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [BaseTypeRequired(typeof(Attribute))] + public sealed class BaseTypeRequiredAttribute : Attribute + { + public BaseTypeRequiredAttribute([NotNull] Type baseType) + { + BaseType = baseType; + } + + [NotNull] public Type BaseType { get; } + } + + /// + /// Indicates that the marked symbol is used implicitly (e.g. via reflection, in external library), + /// so this symbol will be ignored by usage-checking inspections.
+ /// You can use and + /// to configure how this attribute is applied. + ///
+ /// + /// [UsedImplicitly] + /// public class TypeConverter {} + /// + /// public class SummaryData + /// { + /// [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] + /// public SummaryData() {} + /// } + /// + /// [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors | ImplicitUseTargetFlags.Default)] + /// public interface IService {} + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class UsedImplicitlyAttribute : Attribute + { + public UsedImplicitlyAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + public ImplicitUseKindFlags UseKindFlags { get; } + + public ImplicitUseTargetFlags TargetFlags { get; } + } + + /// + /// Can be applied to attributes, type parameters, and parameters of a type assignable from . + /// When applied to an attribute, the decorated attribute behaves the same as . + /// When applied to a type parameter or to a parameter of type , + /// indicates that the corresponding type is used implicitly. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter | AttributeTargets.Parameter)] + public sealed class MeansImplicitUseAttribute : Attribute + { + public MeansImplicitUseAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; } + + [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; } + } + + /// + /// Specifies the details of implicitly used symbol when it is marked + /// with or . + /// + [Flags] + public enum ImplicitUseKindFlags + { + Default = Access | Assign | InstantiatedWithFixedConstructorSignature, + /// Only entity marked with attribute considered used. + Access = 1, + /// Indicates implicit assignment to a member. + Assign = 2, + /// + /// Indicates implicit instantiation of a type with fixed constructor signature. + /// That means any unused constructor parameters won't be reported as such. + /// + InstantiatedWithFixedConstructorSignature = 4, + /// Indicates implicit instantiation of a type. + InstantiatedNoFixedConstructorSignature = 8, + } + + /// + /// Specifies what is considered to be used implicitly when marked + /// with or . + /// + [Flags] + public enum ImplicitUseTargetFlags + { + Default = Itself, + Itself = 1, + /// Members of the type marked with the attribute are considered used. + Members = 2, + /// Inherited entities are considered used. + WithInheritors = 4, + /// Entity marked with the attribute and all its members considered used. + WithMembers = Itself | Members + } + + /// + /// This attribute is intended to mark publicly available API, + /// which should not be removed and so is treated as used. + /// + [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class PublicAPIAttribute : Attribute + { + public PublicAPIAttribute() { } + + public PublicAPIAttribute([NotNull] string comment) + { + Comment = comment; + } + + [CanBeNull] public string Comment { get; } + } + + /// + /// Tells the code analysis engine if the parameter is completely handled when the invoked method is on stack. + /// If the parameter is a delegate, indicates that delegate can only be invoked during method execution + /// (the delegate can be invoked zero or multiple times, but not stored to some field and invoked later, + /// when the containing method is no longer on the execution stack). + /// If the parameter is an enumerable, indicates that it is enumerated while the method is executed. + /// If is true, the attribute will only takes effect if the method invocation is located under the 'await' expression. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InstantHandleAttribute : Attribute + { + /// + /// Require the method invocation to be used under the 'await' expression for this attribute to take effect on code analysis engine. + /// Can be used for delegate/enumerable parameters of 'async' methods. + /// + public bool RequireAwait { get; set; } + } + + /// + /// Indicates that a method does not make any observable state changes. + /// The same as System.Diagnostics.Contracts.PureAttribute. + /// + /// + /// [Pure] int Multiply(int x, int y) => x * y; + /// + /// void M() { + /// Multiply(123, 42); // Warning: Return value of pure method is not used + /// } + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class PureAttribute : Attribute { } + + /// + /// Indicates that the return value of the method invocation must be used. + /// + /// + /// Methods decorated with this attribute (in contrast to pure methods) might change state, + /// but make no sense without using their return value.
+ /// Similarly to , this attribute + /// will help to detect usages of the method when the return value is not used. + /// Optionally, you can specify a message to use when showing warnings, e.g. + /// [MustUseReturnValue("Use the return value to...")]. + ///
+ [AttributeUsage(AttributeTargets.Method)] + public sealed class MustUseReturnValueAttribute : Attribute + { + public MustUseReturnValueAttribute() { } + + public MustUseReturnValueAttribute([NotNull] string justification) + { + Justification = justification; + } + + [CanBeNull] public string Justification { get; } + } + + /// + /// This annotation allows to enforce allocation-less usage patterns of delegates for performance-critical APIs. + /// When this annotation is applied to the parameter of delegate type, IDE checks the input argument of this parameter: + /// * When lambda expression or anonymous method is passed as an argument, IDE verifies that the passed closure + /// has no captures of the containing local variables and the compiler is able to cache the delegate instance + /// to avoid heap allocations. Otherwise the warning is produced. + /// * IDE warns when method name or local function name is passed as an argument as this always results + /// in heap allocation of the delegate instance. + /// + /// + /// In C# 9.0 code IDE would also suggest to annotate the anonymous function with 'static' modifier + /// to make use of the similar analysis provided by the language/compiler. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class RequireStaticDelegateAttribute : Attribute + { + public bool IsError { get; set; } + } + + /// + /// Indicates the type member or parameter of some type, that should be used instead of all other ways + /// to get the value of that type. This annotation is useful when you have some "context" value evaluated + /// and stored somewhere, meaning that all other ways to get this value must be consolidated with existing one. + /// + /// + /// class Foo { + /// [ProvidesContext] IBarService _barService = ...; + /// + /// void ProcessNode(INode node) { + /// DoSomething(node, node.GetGlobalServices().Bar); + /// // ^ Warning: use value of '_barService' field + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Method | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.GenericParameter)] + public sealed class ProvidesContextAttribute : Attribute { } + + /// + /// Indicates that a parameter is a path to a file or a folder within a web project. + /// Path can be relative or absolute, starting from web root (~). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class PathReferenceAttribute : Attribute + { + public PathReferenceAttribute() { } + + public PathReferenceAttribute([NotNull, PathReference] string basePath) + { + BasePath = basePath; + } + + [CanBeNull] public string BasePath { get; } + } + + /// + /// An extension method marked with this attribute is processed by code completion + /// as a 'Source Template'. When the extension method is completed over some expression, its source code + /// is automatically expanded like a template at call site. + /// + /// + /// Template method body can contain valid source code and/or special comments starting with '$'. + /// Text inside these comments is added as source code when the template is applied. Template parameters + /// can be used either as additional method parameters or as identifiers wrapped in two '$' signs. + /// Use the attribute to specify macros for parameters. + /// + /// + /// In this example, the 'forEach' method is a source template available over all values + /// of enumerable types, producing ordinary C# 'foreach' statement and placing caret inside block: + /// + /// [SourceTemplate] + /// public static void forEach<T>(this IEnumerable<T> xs) { + /// foreach (var x in xs) { + /// //$ $END$ + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class SourceTemplateAttribute : Attribute { } + + /// + /// Allows specifying a macro for a parameter of a source template. + /// + /// + /// You can apply the attribute on the whole method or on any of its additional parameters. The macro expression + /// is defined in the property. When applied on a method, the target + /// template parameter is defined in the property. To apply the macro silently + /// for the parameter, set the property value = -1. + /// + /// + /// Applying the attribute on a source template method: + /// + /// [SourceTemplate, Macro(Target = "item", Expression = "suggestVariableName()")] + /// public static void forEach<T>(this IEnumerable<T> collection) { + /// foreach (var item in collection) { + /// //$ $END$ + /// } + /// } + /// + /// Applying the attribute on a template method parameter: + /// + /// [SourceTemplate] + /// public static void something(this Entity x, [Macro(Expression = "guid()", Editable = -1)] string newguid) { + /// /*$ var $x$Id = "$newguid$" + x.ToString(); + /// x.DoSomething($x$Id); */ + /// } + /// + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = true)] + public sealed class MacroAttribute : Attribute + { + /// + /// Allows specifying a macro that will be executed for a source template + /// parameter when the template is expanded. + /// + [CanBeNull] public string Expression { get; set; } + + /// + /// Allows specifying which occurrence of the target parameter becomes editable when the template is deployed. + /// + /// + /// If the target parameter is used several times in the template, only one occurrence becomes editable; + /// other occurrences are changed synchronously. To specify the zero-based index of the editable occurrence, + /// use values >= 0. To make the parameter non-editable when the template is expanded, use -1. + /// + public int Editable { get; set; } + + /// + /// Identifies the target parameter of a source template if the + /// is applied on a template method. + /// + [CanBeNull] public string Target { get; set; } + } + + /// + /// Indicates how method, constructor invocation, or property access + /// over collection type affects the contents of the collection. + /// When applied to a return value of a method indicates if the returned collection + /// is created exclusively for the caller (CollectionAccessType.UpdatedContent) or + /// can be read/updated from outside (CollectionAccessType.Read | CollectionAccessType.UpdatedContent) + /// Use to specify the access type. + /// + /// + /// Using this attribute only makes sense if all collection methods are marked with this attribute. + /// + /// + /// public class MyStringCollection : List<string> + /// { + /// [CollectionAccess(CollectionAccessType.Read)] + /// public string GetFirstString() + /// { + /// return this.ElementAt(0); + /// } + /// } + /// class Test + /// { + /// public void Foo() + /// { + /// // Warning: Contents of the collection is never updated + /// var col = new MyStringCollection(); + /// string x = col.GetFirstString(); + /// } + /// } + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property | AttributeTargets.ReturnValue)] + public sealed class CollectionAccessAttribute : Attribute + { + public CollectionAccessAttribute(CollectionAccessType collectionAccessType) + { + CollectionAccessType = collectionAccessType; + } + + public CollectionAccessType CollectionAccessType { get; } + } + + /// + /// Provides a value for the to define + /// how the collection method invocation affects the contents of the collection. + /// + [Flags] + public enum CollectionAccessType + { + /// Method does not use or modify content of the collection. + None = 0, + /// Method only reads content of the collection but does not modify it. + Read = 1, + /// Method can change content of the collection but does not add new elements. + ModifyExistingContent = 2, + /// Method can add new elements to the collection. + UpdatedContent = ModifyExistingContent | 4 + } + + /// + /// Indicates that the marked method is assertion method, i.e. it halts the control flow if + /// one of the conditions is satisfied. To set the condition, mark one of the parameters with + /// attribute. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class AssertionMethodAttribute : Attribute { } + + /// + /// Indicates the condition parameter of the assertion method. The method itself should be + /// marked by attribute. The mandatory argument of + /// the attribute is the assertion type. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AssertionConditionAttribute : Attribute + { + public AssertionConditionAttribute(AssertionConditionType conditionType) + { + ConditionType = conditionType; + } + + public AssertionConditionType ConditionType { get; } + } + + /// + /// Specifies assertion type. If the assertion method argument satisfies the condition, + /// then the execution continues. Otherwise, execution is assumed to be halted. + /// + public enum AssertionConditionType + { + /// Marked parameter should be evaluated to true. + IS_TRUE = 0, + /// Marked parameter should be evaluated to false. + IS_FALSE = 1, + /// Marked parameter should be evaluated to null value. + IS_NULL = 2, + /// Marked parameter should be evaluated to not null value. + IS_NOT_NULL = 3, + } + + /// + /// Indicates that the marked method unconditionally terminates control flow execution. + /// For example, it could unconditionally throw exception. + /// + [Obsolete("Use [ContractAnnotation('=> halt')] instead")] + [AttributeUsage(AttributeTargets.Method)] + public sealed class TerminatesProgramAttribute : Attribute { } + + /// + /// Indicates that the method is a pure LINQ method, with postponed enumeration (like Enumerable.Select, + /// .Where). This annotation allows inference of [InstantHandle] annotation for parameters + /// of delegate type by analyzing LINQ method chains. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class LinqTunnelAttribute : Attribute { } + + /// + /// Indicates that IEnumerable passed as a parameter is not enumerated. + /// Use this annotation to suppress the 'Possible multiple enumeration of IEnumerable' inspection. + /// + /// + /// static void ThrowIfNull<T>([NoEnumeration] T v, string n) where T : class + /// { + /// // custom check for null but no enumeration + /// } + /// + /// void Foo(IEnumerable<string> values) + /// { + /// ThrowIfNull(values, nameof(values)); + /// var x = values.ToList(); // No warnings about multiple enumeration + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class NoEnumerationAttribute : Attribute { } + + /// + /// Indicates that the marked parameter, field, or property is a regular expression pattern. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class RegexPatternAttribute : Attribute { } + + /// + /// Language of injected code fragment inside marked by string literal. + /// + public enum InjectedLanguage + { + CSS, + HTML, + JAVASCRIPT, + JSON, + XML + } + + /// + /// Indicates that the marked parameter, field, or property is accepting a string literal + /// containing code fragment in a language specified by the . + /// + /// + /// void Foo([LanguageInjection(InjectedLanguage.CSS, Prefix = "body{", Suffix = "}")] string cssProps) + /// { + /// // cssProps should only contains a list of CSS properties + /// } + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class LanguageInjectionAttribute : Attribute + { + public LanguageInjectionAttribute(InjectedLanguage injectedLanguage) + { + InjectedLanguage = injectedLanguage; + } + + /// Specify a language of injected code fragment. + public InjectedLanguage InjectedLanguage { get; } + + /// Specify a string that "precedes" injected string literal. + [CanBeNull] public string Prefix { get; set; } + + /// Specify a string that "follows" injected string literal. + [CanBeNull] public string Suffix { get; set; } + } + + /// + /// Prevents the Member Reordering feature from tossing members of the marked class. + /// + /// + /// The attribute must be mentioned in your member reordering patterns. + /// + [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.Enum)] + public sealed class NoReorderAttribute : Attribute { } + + /// + /// + /// Defines the code search template using the Structural Search and Replace syntax. + /// It allows you to find and, if necessary, replace blocks of code that match a specific pattern. + /// Search and replace patterns consist of a textual part and placeholders. + /// Textural part must contain only identifiers allowed in the target language and will be matched exactly (white spaces, tabulation characters, and line breaks are ignored). + /// Placeholders allow matching variable parts of the target code blocks. + /// A placeholder has the following format: $placeholder_name$- where placeholder_name is an arbitrary identifier. + /// + /// + /// Available placeholders: + /// + /// $this$ - expression of containing type + /// $thisType$ - containing type + /// $member$ - current member placeholder + /// $qualifier$ - this placeholder is available in the replace pattern and can be used to insert qualifier expression matched by the $member$ placeholder. + /// (Note that if $qualifier$ placeholder is used, then $member$ placeholder will match only qualified references) + /// $expression$ - expression of any type + /// $identifier$ - identifier placeholder + /// $args$ - any number of arguments + /// $arg$ - single argument + /// $arg1$ ... $arg10$ - single argument + /// $stmts$ - any number of statements + /// $stmt$ - single statement + /// $stmt1$ ... $stmt10$ - single statement + /// $name{Expression, 'Namespace.FooType'}$ - expression with 'Namespace.FooType' type + /// $expression{'Namespace.FooType'}$ - expression with 'Namespace.FooType' type + /// $name{Type, 'Namespace.FooType'}$ - 'Namespace.FooType' type + /// $type{'Namespace.FooType'}$ - 'Namespace.FooType' type + /// $statement{1,2}$ - 1 or 2 statements + /// + /// + /// + /// Note that you can also define your own placeholders of the supported types and specify arguments for each placeholder type. + /// This can be done using the following format: $name{type, arguments}$. Where 'name' - is the name of your placeholder, + /// 'type' - is the type of your placeholder (one of the following: Expression, Type, Identifier, Statement, Argument, Member), + /// 'arguments' - arguments list for your placeholder. Each placeholder type supports it's own arguments, check examples below for mode details. + /// Placeholder type may be omitted and determined from the placeholder name, if name has one of the following prefixes: + /// + /// expr, expression - expression placeholder, e.g. $exprPlaceholder{}$, $expressionFoo{}$ + /// arg, argument - argument placeholder, e.g. $argPlaceholder{}$, $argumentFoo{}$ + /// ident, identifier - identifier placeholder, e.g. $identPlaceholder{}$, $identifierFoo{}$ + /// stmt, statement - statement placeholder, e.g. $stmtPlaceholder{}$, $statementFoo{}$ + /// type - type placeholder, e.g. $typePlaceholder{}$, $typeFoo{}$ + /// member - member placeholder, e.g. $memberPlaceholder{}$, $memberFoo{}$ + /// + /// + /// + /// Expression placeholder arguments: + /// + /// expressionType - string value in single quotes, specifies full type name to match (empty string by default) + /// exactType - boolean value, specifies if expression should have exact type match (false by default) + /// + /// Examples: + /// + /// $myExpr{Expression, 'Namespace.FooType', true}$ - defines expression placeholder, matching expressions of the 'Namespace.FooType' type with exact matching. + /// $myExpr{Expression, 'Namespace.FooType'}$ - defines expression placeholder, matching expressions of the 'Namespace.FooType' type or expressions which can be implicitly converted to 'Namespace.FooType'. + /// $myExpr{Expression}$ - defines expression placeholder, matching expressions of any type. + /// $exprFoo{'Namespace.FooType', true}$ - defines expression placeholder, matching expressions of the 'Namespace.FooType' type with exact matching. + /// + /// + /// + /// Type placeholder arguments: + /// + /// type - string value in single quotes, specifies full type name to match (empty string by default) + /// exactType - boolean value, specifies if expression should have exact type match (false by default) + /// + /// Examples: + /// + /// $myType{Type, 'Namespace.FooType', true}$ - defines type placeholder, matching 'Namespace.FooType' types with exact matching. + /// $myType{Type, 'Namespace.FooType'}$ - defines type placeholder, matching 'Namespace.FooType' types or types, which can be implicitly converted to 'Namespace.FooType'. + /// $myType{Type}$ - defines type placeholder, matching any type. + /// $typeFoo{'Namespace.FooType', true}$ - defines types placeholder, matching 'Namespace.FooType' types with exact matching. + /// + /// + /// + /// Identifier placeholder arguments: + /// + /// nameRegex - string value in single quotes, specifies regex to use for matching (empty string by default) + /// nameRegexCaseSensitive - boolean value, specifies if name regex is case sensitive (true by default) + /// type - string value in single quotes, specifies full type name to match (empty string by default) + /// exactType - boolean value, specifies if expression should have exact type match (false by default) + /// + /// Examples: + /// + /// $myIdentifier{Identifier, 'my.*', false, 'Namespace.FooType', true}$ - defines identifier placeholder, matching identifiers (ignoring case) starting with 'my' prefix with 'Namespace.FooType' type. + /// $myIdentifier{Identifier, 'my.*', true, 'Namespace.FooType', true}$ - defines identifier placeholder, matching identifiers (case sensitively) starting with 'my' prefix with 'Namespace.FooType' type. + /// $identFoo{'my.*'}$ - defines identifier placeholder, matching identifiers (case sensitively) starting with 'my' prefix. + /// + /// + /// + /// Statement placeholder arguments: + /// + /// minimalOccurrences - minimal number of statements to match (-1 by default) + /// maximalOccurrences - maximal number of statements to match (-1 by default) + /// + /// Examples: + /// + /// $myStmt{Statement, 1, 2}$ - defines statement placeholder, matching 1 or 2 statements. + /// $myStmt{Statement}$ - defines statement placeholder, matching any number of statements. + /// $stmtFoo{1, 2}$ - defines statement placeholder, matching 1 or 2 statements. + /// + /// + /// + /// Argument placeholder arguments: + /// + /// minimalOccurrences - minimal number of arguments to match (-1 by default) + /// maximalOccurrences - maximal number of arguments to match (-1 by default) + /// + /// Examples: + /// + /// $myArg{Argument, 1, 2}$ - defines argument placeholder, matching 1 or 2 arguments. + /// $myArg{Argument}$ - defines argument placeholder, matching any number of arguments. + /// $argFoo{1, 2}$ - defines argument placeholder, matching 1 or 2 arguments. + /// + /// + /// + /// Member placeholder arguments: + /// + /// docId - string value in single quotes, specifies XML documentation id of the member to match (empty by default) + /// + /// Examples: + /// + /// $myMember{Member, 'M:System.String.IsNullOrEmpty(System.String)'}$ - defines member placeholder, matching 'IsNullOrEmpty' member of the 'System.String' type. + /// $memberFoo{'M:System.String.IsNullOrEmpty(System.String)'}$ - defines member placeholder, matching 'IsNullOrEmpty' member of the 'System.String' type. + /// + /// + /// + /// For more information please refer to the Structural Search and Replace article. + /// + /// + [AttributeUsage( + AttributeTargets.Method + | AttributeTargets.Constructor + | AttributeTargets.Property + | AttributeTargets.Field + | AttributeTargets.Event + | AttributeTargets.Interface + | AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Enum, + AllowMultiple = true, + Inherited = false)] + public sealed class CodeTemplateAttribute : Attribute + { + public CodeTemplateAttribute(string searchTemplate) + { + SearchTemplate = searchTemplate; + } + + /// + /// Structural search pattern to use in the code template. + /// Pattern includes textual part, which must contain only identifiers allowed in the target language, + /// and placeholders, which allow matching variable parts of the target code blocks. + /// + public string SearchTemplate { get; } + + /// + /// Message to show when the search pattern was found. + /// You can also prepend the message text with "Error:", "Warning:", "Suggestion:" or "Hint:" prefix to specify the pattern severity. + /// Code patterns with replace template produce suggestions by default. + /// However, if replace template is not provided, then warning severity will be used. + /// + public string Message { get; set; } + + /// + /// Structural search replace pattern to use in code template replacement. + /// + public string ReplaceTemplate { get; set; } + + /// + /// Replace message to show in the light bulb. + /// + public string ReplaceMessage { get; set; } + + /// + /// Apply code formatting after code replacement. + /// + public bool FormatAfterReplace { get; set; } = true; + + /// + /// Whether similar code blocks should be matched. + /// + public bool MatchSimilarConstructs { get; set; } + + /// + /// Automatically insert namespace import directives or remove qualifiers that become redundant after the template is applied. + /// + public bool ShortenReferences { get; set; } + + /// + /// String to use as a suppression key. + /// By default the following suppression key is used 'CodeTemplate_SomeType_SomeMember', + /// where 'SomeType' and 'SomeMember' are names of the associated containing type and member to which this attribute is applied. + /// + public string SuppressionKey { get; set; } + } + + #region ASP.NET + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspChildControlTypeAttribute : Attribute + { + public AspChildControlTypeAttribute([NotNull] string tagName, [NotNull] Type controlType) + { + TagName = tagName; + ControlType = controlType; + } + + [NotNull] public string TagName { get; } + + [NotNull] public Type ControlType { get; } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldsAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspMethodPropertyAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspRequiredAttributeAttribute : Attribute + { + public AspRequiredAttributeAttribute([NotNull] string attribute) + { + Attribute = attribute; + } + + [NotNull] public string Attribute { get; } + } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspTypePropertyAttribute : Attribute + { + public bool CreateConstructorReferences { get; } + + public AspTypePropertyAttribute(bool createConstructorReferences) + { + CreateConstructorReferences = createConstructorReferences; + } + } + + #endregion + + #region ASP.NET MVC + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute + { + public AspMvcAreaMasterLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute + { + public AspMvcAreaPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaViewLocationFormatAttribute : Attribute + { + public AspMvcAreaViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcMasterLocationFormatAttribute : Attribute + { + public AspMvcMasterLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcPartialViewLocationFormatAttribute : Attribute + { + public AspMvcPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcViewLocationFormatAttribute : Attribute + { + public AspMvcViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC action. If applied to a method, the MVC action name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcActionAttribute : Attribute + { + public AspMvcActionAttribute() { } + + public AspMvcActionAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC area. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcAreaAttribute : Attribute + { + public AspMvcAreaAttribute() { } + + public AspMvcAreaAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is + /// an MVC controller. If applied to a method, the MVC controller name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcControllerAttribute : Attribute + { + public AspMvcControllerAttribute() { } + + public AspMvcControllerAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC Master. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcMasterAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC model type. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, Object). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcModelTypeAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is an MVC + /// partial view. If applied to a method, the MVC partial view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcPartialViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Allows disabling inspections for MVC views within a class or a method. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class AspMvcSuppressViewErrorAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcDisplayTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC editor template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcEditorTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC template. + /// Use this attribute for custom wrappers similar to + /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component. If applied to a method, the MVC view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Controller.View(Object). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component name. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewComponentAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component view. If applied to a method, the MVC view component view name is default. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewComponentViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. When applied to a parameter of an attribute, + /// indicates that this parameter is an MVC action name. + /// + /// + /// [ActionName("Foo")] + /// public ActionResult Login(string returnUrl) { + /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK + /// return RedirectToAction("Bar"); // Error: Cannot resolve action + /// } + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] + public sealed class AspMvcActionSelectorAttribute : Attribute { } + + #endregion + + #region ASP.NET Routing + + /// + /// Indicates that the marked parameter, field, or property is a route template. + /// + /// + /// This attribute allows IDE to recognize the use of web frameworks' route templates + /// to enable syntax highlighting, code completion, navigation, rename and other features in string literals. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class RouteTemplateAttribute : Attribute { } + + /// + /// Indicates that the marked type is custom route parameter constraint, + /// which is registered in application's Startup with name ConstraintName + /// + /// + /// You can specify ProposedType if target constraint matches only route parameters of specific type, + /// it will allow IDE to create method's parameter from usage in route template + /// with specified type instead of default System.String + /// and check if constraint's proposed type conflicts with matched parameter's type + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class RouteParameterConstraintAttribute : Attribute + { + [NotNull] public string ConstraintName { get; } + [CanBeNull] public Type ProposedType { get; set; } + + public RouteParameterConstraintAttribute([NotNull] string constraintName) + { + ConstraintName = constraintName; + } + } + + /// + /// Indicates that the marked parameter, field, or property is an URI string. + /// + /// + /// This attribute enables code completion, navigation, rename and other features + /// in URI string literals assigned to annotated parameter, field or property. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class UriStringAttribute : Attribute + { + public UriStringAttribute() { } + + public UriStringAttribute(string httpVerb) + { + HttpVerb = httpVerb; + } + + [CanBeNull] public string HttpVerb { get; } + } + + /// + /// Indicates that the marked method declares routing convention for ASP.NET + /// + /// + /// IDE will analyze all usages of methods marked with this attribute, + /// and will add all routes to completion, navigation and other features over URI strings + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class AspRouteConventionAttribute : Attribute + { + public AspRouteConventionAttribute() { } + + public AspRouteConventionAttribute(string predefinedPattern) + { + PredefinedPattern = predefinedPattern; + } + + [CanBeNull] public string PredefinedPattern { get; } + } + + /// + /// Indicates that the marked method parameter contains default route values of routing convention for ASP.NET + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspDefaultRouteValuesAttribute : Attribute { } + + /// + /// Indicates that the marked method parameter contains constraints on route values of routing convention for ASP.NET + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspRouteValuesConstraintsAttribute : Attribute { } + + /// + /// Indicates that the marked parameter or property contains routing order provided by ASP.NET routing attribute + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] + public sealed class AspRouteOrderAttribute : Attribute { } + + /// + /// Indicates that the marked parameter or property contains HTTP verbs provided by ASP.NET routing attribute + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] + public sealed class AspRouteVerbsAttribute : Attribute { } + + /// + /// Indicates that the marked attribute is used for attribute routing in ASP.NET + /// + /// + /// IDE will analyze all usages of attributes marked with this attribute, + /// and will add all routes to completion, navigation and other features over URI strings + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class AspAttributeRoutingAttribute : Attribute + { + public string HttpVerb { get; set; } + } + + /// + /// Indicates that the marked method declares ASP.NET Minimal API endpoint + /// + /// + /// IDE will analyze all usages of methods marked with this attribute, + /// and will add all routes to completion, navigation and other features over URI strings + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class AspMinimalApiDeclarationAttribute : Attribute + { + public string HttpVerb { get; set; } + } + + /// + /// Indicates that the marked parameter contains ASP.NET Minimal API endpoint handler + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMinimalApiHandlerAttribute : Attribute { } + + #endregion + + #region Razor + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] + public sealed class HtmlElementAttributesAttribute : Attribute + { + public HtmlElementAttributesAttribute() { } + + public HtmlElementAttributesAttribute([NotNull] string name) + { + Name = name; + } + + [CanBeNull] public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class HtmlAttributeValueAttribute : Attribute + { + public HtmlAttributeValueAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + /// + /// Razor attribute. Indicates that the marked parameter or method is a Razor section. + /// Use this attribute for custom wrappers similar to + /// System.Web.WebPages.WebPageBase.RenderSection(String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + public sealed class RazorSectionAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorImportNamespaceAttribute : Attribute + { + public RazorImportNamespaceAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorInjectionAttribute : Attribute + { + public RazorInjectionAttribute([NotNull] string type, [NotNull] string fieldName) + { + Type = type; + FieldName = fieldName; + } + + [NotNull] public string Type { get; } + + [NotNull] public string FieldName { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorDirectiveAttribute : Attribute + { + public RazorDirectiveAttribute([NotNull] string directive) + { + Directive = directive; + } + + [NotNull] public string Directive { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorPageBaseTypeAttribute : Attribute + { + public RazorPageBaseTypeAttribute([NotNull] string baseType) + { + BaseType = baseType; + } + public RazorPageBaseTypeAttribute([NotNull] string baseType, string pageName) + { + BaseType = baseType; + PageName = pageName; + } + + [NotNull] public string BaseType { get; } + [CanBeNull] public string PageName { get; } + } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorHelperCommonAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class RazorLayoutAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteLiteralMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class RazorWriteMethodParameterAttribute : Attribute { } + + #endregion + + #region XAML + + /// + /// XAML attribute. Indicates the type that has ItemsSource property and should be treated + /// as ItemsControl-derived type, to enable inner items DataContext type resolve. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class XamlItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the property of some BindingBase-derived type, that + /// is used to bind some item of ItemsControl-derived type. This annotation will + /// enable the DataContext type resolve for XAML bindings for such properties. + /// + /// + /// Property should have the tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class XamlItemBindingOfItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the property of some Style-derived type, that + /// is used to style items of ItemsControl-derived type. This annotation will + /// enable the DataContext type resolve for XAML bindings for such properties. + /// + /// + /// Property should have the tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class XamlItemStyleOfItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates that DependencyProperty has OneWay binding mode by default. + /// + /// + /// This attribute must be applied to DependencyProperty's CLR accessor property if it is present, to DependencyProperty descriptor field otherwise. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class XamlOneWayBindingModeByDefaultAttribute : Attribute { } + + /// + /// XAML attribute. Indicates that DependencyProperty has TwoWay binding mode by default. + /// + /// + /// This attribute must be applied to DependencyProperty's CLR accessor property if it is present, to DependencyProperty descriptor field otherwise. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class XamlTwoWayBindingModeByDefaultAttribute : Attribute { } + + #endregion +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/BaseOperation.cs b/Hawkeye.VisionBuilder.Workflow/BaseOperation.cs new file mode 100644 index 0000000..f539355 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/BaseOperation.cs @@ -0,0 +1,284 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Links; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using Microsoft.Scripting.Hosting; +using Ninject.Infrastructure.Language; +using OpenCvSharp; +using System.Diagnostics; +using System.Reflection; +using System.Runtime; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; +using RectangleElement = Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle.RectangleElement; + +namespace Hawkeye.VisionBuilder.Workflow +{ + public abstract class BaseOperation + { + private static readonly ScriptScope _scope; + private static readonly ScriptEngine _engine; + + static BaseOperation() + { + _engine = IronPython.Hosting.Python.CreateEngine(); + _scope = _engine.CreateScope(); + } + protected BaseOperation() + { + TypeName = MakeOperationName(this.GetType().Name); + Label = MakeOperationName(this.GetType().Name,false); + + + } + + public static void SetScriptValue(string name, T value) + { + _scope.SetVariable(name,value); + } + + public static T GetScriptValue(ScriptValue scriptValue) + { + return _engine.Execute(scriptValue.Script,_scope); + } + [NotForTool] + public bool CanHaveProcessingError { get; set; } = true; + [NotForTool] + public string Status { get; set; } + + protected bool CheckImageExists(Context context) + { + if (context.ActiveImage == null) + { + Status = "No image available"; + Result = false; + return false; + } + + return true; + } + + protected bool CheckColorful(Context context) + { + if (context.ActiveImage.ImageData.Type() != MatType.CV_8UC3) + { + Status="Image is not colorful"; + Result = false; + return false; + } + + return true; + } + + protected bool CheckGrayscale(Context context) + { + if (context.ActiveImage.ImageData.Type() != MatType.CV_8UC1) + { + Status="Image is not grayscale"; + Result = false; + return false; + } + + return true; + } + [NotForTool] + public bool NeedsUpdate { get; set; } = true; + [NotForTool] + public Guid Id { get; protected set; }= Guid.NewGuid(); + public static string MakeOperationName(string typeName,bool useSpaces=true) + { + return Regex.Replace(typeName.Replace("Operation", ""), "(\\B[A-Z]+?(?=[A-Z][^A-Z])|\\B[A-Z]+?(?=[^A-Z]))", (useSpaces?" ":"")+"$1"); + } + + public bool Result { get; set; } = true; + public string ResultString + { + get + { + return this.ToString(); + } + } + [NotForTool] + public string TypeName { get; private set; } + [NotForTool] + public string Label { get; set; } + + public TimeSpan ExecutionTime { get; set; } + public string ExecutionTimeString + { + get + { + return ((int)ExecutionTime.TotalMilliseconds) + " ms"; + } + } + + public void Interpret(Context context) + { + + + Result = true; + Status = ""; + Stopwatch sw = Stopwatch.StartNew(); + InterpretInternal(context); + sw.Stop(); + ExecutionTime = sw.Elapsed; + _scope.SetVariable(Label,this); + + } + + protected abstract void InterpretInternal(Context context); + + public Dictionary GetParameters() + { + + var props = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Where(x=>!x.HasAttribute(typeof(NotForToolAttribute))); + return props.ToDictionary(x => x.Name, x => x.GetValue(this)!); + } + + public virtual void SetParameters(Dictionary parameters) + { + foreach (KeyValuePair pair in parameters) + { + this.GetType().GetProperty(pair.Key, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + ?.SetValue(this,pair.Value); + } + } + + + public virtual void SaveXML(XmlWriter writer) + { + writer.WriteAttributeString("Id", Id.ToString()); + writer.WriteAttributeString("Label", Label); + + // Save all public properties that are not marked with NotForTool + var properties = GetParameters(); + foreach (var property in properties) + { + if (property.Value != null) + { + writer.WriteStartElement("Property"); + writer.WriteAttributeString("Name", property.Key); + writer.WriteAttributeString("Type", property.Value.GetType().FullName); + + // Handle different types appropriately + if (property.Value is string || property.Value.GetType().IsPrimitive || property.Value.GetType().IsEnum) + { + writer.WriteString(property.Value.ToString()); + } + else if (property.Value is Guid guid) + { + writer.WriteString(guid.ToString()); + } + else + { + // For complex types, convert to string representation + writer.WriteString(property.Value.ToString()); + } + + writer.WriteEndElement(); + } + } + } + + public virtual void LoadXML(XmlReader reader) + { + if (reader.GetAttribute("Id") != null) + Id = new Guid(reader.GetAttribute("Id")); + if (reader.GetAttribute("Label") != null) + Label = reader.GetAttribute("Label"); + + var properties = new Dictionary(); + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element && reader.Name == "Property") + { + string propertyName = reader.GetAttribute("Name"); + string propertyType = reader.GetAttribute("Type"); + string propertyValue = reader.ReadElementContentAsString(); + + if (!string.IsNullOrEmpty(propertyName) && !string.IsNullOrEmpty(propertyValue)) + { + // Convert string value back to appropriate type + object convertedValue = ConvertFromString(propertyValue, propertyType); + if (convertedValue != null) + { + properties[propertyName] = convertedValue; + } + } + } + else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == GetOperationElementName()) + { + break; + } + } + + if (properties.Count > 0) + { + SetParameters(properties); + } + } + + private string GetOperationElementName() + { + return this.GetType().Name; + } + + private object ConvertFromString(string value, string typeName) + { + try + { + var type = Type.GetType(typeName); + if (type == null) return value; + + if (type == typeof(string)) + return value; + else if (type == typeof(int)) + return int.Parse(value); + else if (type == typeof(double)) + return double.Parse(value); + else if (type == typeof(float)) + return float.Parse(value); + else if (type == typeof(bool)) + return bool.Parse(value); + else if (type == typeof(Guid)) + return new Guid(value); + else if (type.IsEnum) + return Enum.Parse(type, value); + else + return value; // Fallback to string + } + catch + { + return value; // Fallback to string if conversion fails + } + } + + + public override string ToString() + { + return Status; + } + + public virtual void Save(BinaryWriter bw) + { + bw.Write(Id.ToString()); + bw.Write(Label); + } + + public virtual void Load(BinaryReader br) + { + Id = new Guid(br.ReadString()); + Label = br.ReadString(); + } + + public void SetError(string eMessage) + { + Status=eMessage; + Result = false; + } + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs b/Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs new file mode 100644 index 0000000..bfbe1fa --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/BaseOperationDecorator.cs @@ -0,0 +1,24 @@ +namespace Hawkeye.VisionBuilder.Workflow; + +public class BaseOperationDecorator +{ + private readonly BaseOperation _baseOperation; + + public BaseOperationDecorator(BaseOperation baseOperation) + { + _baseOperation = baseOperation; + } + + public bool Result => Operation.Result; + public string ResultString => Operation.ToString(); + public string TypeName => Operation.TypeName; + public string Label + { + get => Operation.Label; + set => Operation.Label = value; + } + + public string ExecutionTimeString => Operation.ExecutionTimeString; + + public BaseOperation Operation => _baseOperation; +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/CLAUDE.md b/Hawkeye.VisionBuilder.Workflow/CLAUDE.md new file mode 100644 index 0000000..f6934f3 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/CLAUDE.md @@ -0,0 +1,157 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Hawkeye.VisionBuilder.Workflow is a computer vision workflow system built on .NET 8.0 that processes images through configurable operation pipelines. The system supports both CPU and GPU configurations with ONNX runtime for AI models. + +## Build Configuration + +The project uses conditional package references based on configuration: +- **CPU Configuration**: Uses `YoloV8` (4.1.5) and `Microsoft.ML.OnnxRuntime` (1.18.0) +- **GPU Configuration**: Uses `YoloV8.Gpu` (4.1.7) and `Microsoft.ML.OnnxRuntime.Gpu` (1.18.0) + +Build the project with: +```bash +dotnet build -c Debug # For GPU configuration +dotnet build -c CPU # For CPU-only configuration +``` + +## Core Architecture + +### Operation System +- **BaseOperation**: Abstract base class for all image processing operations. All operations inherit from this and implement `InterpretInternal(Context context)` +- **Context**: Holds the current processing state including `ActiveImage`, `LastCameraImage`, `GraphicsElements`, and image `Memory` +- **OperationDiscoveryService**: Dynamically discovers and instantiates operations using reflection and Ninject DI + +### Operation Categories +Operations are organized in folders by category: +- `AI/`: AI model operations (YOLO, ONNX models, color classification) +- `Simple/`: Basic operations (blob detection, edge finding) +- `Filters/`: Image filtering operations +- `Morphology/`: Morphological operations (dilation, erosion, etc.) +- `Image/`: Image manipulation (convert, channels, etc.) +- `Basic/`: Fundamental operations (threshold, blur, etc.) + +### Key Components +- **HawkeyeImage**: Wrapper around OpenCV Mat with filename metadata +- **WorkflowConfiguration**: Manages camera types, Python paths, outputs, and recipe selection +- **IImageSource**: Interface for image input sources with event-driven architecture +- **Graphics Elements**: Drawable elements (rectangles, lines, polygons) for ROI definition + +### Python Integration +- Uses IronPython for scripting capabilities +- Python models executed via `PythonModelProxy` and `PythonModelProxyRPC` +- Default Python path: `~/miniconda3/envs/ai3_cpu/python.exe` + +### Operation Attributes +- `[Category("name")]`: Groups operations in UI +- `[NotForTool]`: Excludes properties from parameter serialization +- `[IgnoreOperation]`: Excludes operations from discovery + +## Development Patterns + +### Creating New Operations +1. Inherit from `BaseOperation` +2. Add `[Category("CategoryName")]` attribute +3. Implement `InterpretInternal(Context context)` method +4. Use helper methods: `CheckImageExists()`, `CheckColorful()`, `CheckGrayscale()` +5. Set `Result` and `Status` properties for operation feedback + +### Image Processing Flow +1. Operations receive `Context` with current `ActiveImage` +2. Process image using OpenCV operations +3. Update context with results (new images, graphics elements) +4. Set operation status and result state + +### Memory Management +Operations can store/retrieve images using `context.Memory` dictionary for intermediate results. + +## Recipe Serialization System + +The workflow system supports two serialization formats for saving and loading recipes: + +### Binary Serialization (.hrcp files) +The original format using `BinaryWriter` and `BinaryReader`: +- **WorkflowList.Save(BinaryWriter)**: Saves configuration, operations, and recipe image +- **WorkflowList.Load(BinaryReader, OperationDiscoveryService)**: Loads complete workflow +- **BaseOperation.Save(BinaryWriter)**: Saves operation ID and label +- **BaseOperation.Load(BinaryReader)**: Loads operation ID and label + +### XML Serialization (.xhrcp files) +The new human-readable format using `XmlWriter` and `XmlReader`: +- **WorkflowList.SaveXML(XmlWriter)**: Saves complete workflow as structured XML +- **WorkflowList.LoadXML(XmlReader, OperationDiscoveryService)**: Loads workflow from XML +- **BaseOperation.SaveXML(XmlWriter)**: Saves operation properties and metadata +- **BaseOperation.LoadXML(XmlReader)**: Loads operation properties with type conversion + +### XML Structure +```xml + + + + + Value + ... + + + Base64EncodedImageData + +``` + +### Implementing Custom Operation Serialization + +For operations that need custom XML serialization beyond the default property serialization: + +#### Override SaveXML for Custom Save Logic: +```csharp +public override void SaveXML(XmlWriter writer) +{ + base.SaveXML(writer); // Save standard properties + + // Custom serialization logic + writer.WriteStartElement("CustomData"); + writer.WriteAttributeString("SpecialProperty", SpecialValue.ToString()); + writer.WriteEndElement(); +} +``` + +#### Override LoadXML for Custom Load Logic: +```csharp +public override void LoadXML(XmlReader reader) +{ + base.LoadXML(reader); // Load standard properties + + // Custom deserialization logic + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element && reader.Name == "CustomData") + { + SpecialValue = reader.GetAttribute("SpecialProperty"); + } + else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == GetOperationElementName()) + { + break; + } + } +} +``` + +#### Override GetOperationElementName for Custom Element Names: +```csharp +protected override string GetOperationElementName() +{ + return "MyCustomOperation"; // Used for XML end element detection +} +``` + +### Supported Property Types in XML Serialization +The default XML serialization handles these types automatically: +- Primitive types (int, double, float, bool) +- String +- Guid +- Enums +- Complex types (converted to string representation) + +For complex types requiring special serialization, override the SaveXML/LoadXML methods. \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/CategoryItem.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/CategoryItem.cs new file mode 100644 index 0000000..b09ecd2 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/CategoryItem.cs @@ -0,0 +1,44 @@ +using Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses; + +public class CategoryItem:IFilterParent +{ + public string Name { get; set; } + public List Filters { get; set; } = new List(); + + public void Save(BinaryWriter writer) + { + writer.Write(Name); + writer.Write(Filters.Count); + foreach (var filter in Filters) + { + var type = filter.GetType(); + writer.Write(type.FullName); + filter.Save(writer); + } + } + + public static CategoryItem Load(BinaryReader reader) + { + var category = new CategoryItem(); + category.Name = reader.ReadString(); + var count = reader.ReadInt32(); + for (int i = 0; i < count; i++) + { + var typeName = reader.ReadString(); + var type = Type.GetType(typeName); + var filter = (IFilter)Activator.CreateInstance(type); + filter.Load(reader); + filter.Parent = category; + category.Filters.Add(filter); + } + return category; + } + + public override string ToString() + { + return Name; + } +} + diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/CategoryTree.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/CategoryTree.cs new file mode 100644 index 0000000..60ec456 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/CategoryTree.cs @@ -0,0 +1,33 @@ +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses; + +public class CategoryTree +{ + public List CategoryItems { get; set; }=new List(); + + public void Save(BinaryWriter writer) + { + writer.Write(CategoryItems.Count); + foreach (var category in CategoryItems) + { + category.Save(writer); + } + } + + public static CategoryTree Load(BinaryReader reader) + { + var tree = new CategoryTree(); + var count = reader.ReadInt32(); + for (int i = 0; i < count; i++) + { + tree.CategoryItems.Add(CategoryItem.Load(reader)); + } + return tree; + } + + public static CategoryTree LoadFromFile(string path) + { + using var file = File.OpenRead(path); + using var reader = new BinaryReader(file); + return Load(reader); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/ColorConfig.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/ColorConfig.cs new file mode 100644 index 0000000..b901d5f --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/ColorConfig.cs @@ -0,0 +1,47 @@ +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses +{ + public class ColorConfig + { + private int _intensityMaxGrey; + + public ColorConfig() + { + MaxBlobSize = 2000; + HueHighSaturationMinGrey = 20; + HueHighSaturationMaxGrey = 50; + HighSaturationMinGrey = 100; + HighSaturationMaxGrey = 255; + ClosingCircleRadius = 8.0; + IntensityMaxGrey = 255; + IntensityMinGrey = 0; + MedianRadius = 10; + MedianMargin = 1; + UseMedianFilter = true; + } + + public int MaxBlobSize { get; set; } + public int HueHighSaturationMinGrey { get; set; } + public int HueHighSaturationMaxGrey { get; set; } + public int HighSaturationMinGrey { get; set; } + public int HighSaturationMaxGrey { get; set; } + + public int IntensityMaxGrey + { + get { return _intensityMaxGrey; } + set { _intensityMaxGrey = value; } + } + + public int IntensityMinGrey { get; set; } + + public double ClosingCircleRadius { get; set; } + + public int MedianRadius { get; set; } + public int MedianMargin { get; set; } + + public bool UseMedianFilter { get; set; } + + public bool GroupColorEnabled { get; set; } + public List GroupColorValues { get; set; }=new List(); + public HSIValue HSI { get; set; } + } +} diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/AddFilterItem.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/AddFilterItem.cs new file mode 100644 index 0000000..ebd3e9d --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/AddFilterItem.cs @@ -0,0 +1,52 @@ +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +public class AddFilterItem : IFilter, IFilterParent +{ + public IFilterParent Parent { get; set; } + + public List Filters { get; set; } = new List(); + + public void Transform(ref Mat image, ref Mat res) + { + using var tracker = new ResourcesTracker(); + var temp = tracker.NewMat(res.Size(), MatType.CV_8UC1, Scalar.Black); + foreach (var filter in Filters) + { + filter.Transform(ref image, ref temp); + } + res = res + temp; + temp.Dispose(); + } + + public void Save(BinaryWriter bw) + { + bw.Write(Filters.Count); + foreach (var filter in Filters) + { + bw.Write(filter.GetType().FullName); + filter.Save(bw); + } + } + + public void Load(BinaryReader reader) + { + int count = reader.ReadInt32(); + for (int i = 0; i < count; i++) + { + var type = reader.ReadString(); + var filter = (IFilter)Activator.CreateInstance(Type.GetType(type)); + filter.Load(reader); + filter.Parent = this; + Filters.Add(filter); + } + } + + public bool IsSelected { get; set; } + + public override string ToString() + { + return "Add"; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/BlobsFilterItem.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/BlobsFilterItem.cs new file mode 100644 index 0000000..ebb2a4e --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/BlobsFilterItem.cs @@ -0,0 +1,66 @@ +using System.Text; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +public class BlobsFilterItem:IFilter +{ + public int MinArea { get; set; } = 0; + public int MaxArea { get; set; } = 999999; + public int MinRadius { get; set; } = 0; + public int MaxRadius { get; set; } = 999999; + + public IFilterParent Parent { get; set; } + public void Transform(ref Mat image, ref Mat res) + { + var contours = res + .FindContoursAsArray(RetrievalModes.List, ContourApproximationModes.ApproxSimple); + + Mat result = new Mat(res.Rows, res.Cols, MatType.CV_8U, new Scalar(0)); + + foreach (Point[] contour in contours) + { + var area = Cv2.ContourArea(contour); + if (area < MinArea || area > MaxArea) + continue; + var radius = GetContourRadius(contour); + if (radius < MinRadius || radius > MaxRadius) + continue; + + Cv2.DrawContours(result, new[] { contour }, -1, new Scalar(255), -1); + } + + res = result; + } + int GetContourRadius(Point[] contour) + { + Cv2.MinEnclosingCircle(contour, out var center, out var radius); + return (int)radius; + } + public void Save(BinaryWriter writer) + { + writer.Write(MinArea); + writer.Write(MaxArea); + writer.Write(MinRadius); + writer.Write(MaxRadius); + } + + public void Load(BinaryReader reader) + { + MinArea = reader.ReadInt32(); + MaxArea = reader.ReadInt32(); + MinRadius = reader.ReadInt32(); + MaxRadius = reader.ReadInt32(); + } + + public bool IsSelected { get; set; } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("Blobs: "); + sb.Append($"Area:{MinArea}-{MaxArea}, "); + sb.Append($"Radius:{MinRadius}-{MaxRadius}"); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/BlurFilterItem.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/BlurFilterItem.cs new file mode 100644 index 0000000..1b9f942 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/BlurFilterItem.cs @@ -0,0 +1,30 @@ +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +public class BlurFilterItem:IFilter +{ + public IFilterParent Parent { get; set; } + public int Radius { get; set; } = 15; + public void Transform(ref Mat image, ref Mat res) + { + image = image.Blur(new Size(Radius, Radius)); + } + + public void Save(BinaryWriter writer) + { + writer.Write(Radius); + } + + public void Load(BinaryReader reader) + { + Radius = reader.ReadInt32(); + } + + public bool IsSelected { get; set; } + + public override string ToString() + { + return "Blur "+Radius; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IFilter.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IFilter.cs new file mode 100644 index 0000000..2a6678e --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IFilter.cs @@ -0,0 +1,13 @@ +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +public interface IFilter +{ + public IFilterParent Parent { get; set; } + void Transform(ref Mat image, ref Mat res); + void Save(BinaryWriter writer); + void Load(BinaryReader reader); + public bool IsSelected { get; set; } + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IFilterParent.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IFilterParent.cs new file mode 100644 index 0000000..e696fea --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IFilterParent.cs @@ -0,0 +1,6 @@ +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +public interface IFilterParent +{ + public List Filters { get; set; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IntersectionFilterItem.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IntersectionFilterItem.cs new file mode 100644 index 0000000..633c856 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/IntersectionFilterItem.cs @@ -0,0 +1,52 @@ +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +public class IntersectionFilterItem : IFilter, IFilterParent +{ + public IFilterParent Parent { get; set; } + + public List Filters { get; set; } = new List(); + + public void Transform(ref Mat image, ref Mat res) + { + using var tracker = new ResourcesTracker(); + var temp = tracker.NewMat(res.Size(), MatType.CV_8UC1, Scalar.Black); + foreach (var filter in Filters) + { + filter.Transform(ref image, ref temp); + } + res = res & temp; + temp.Dispose(); + } + + public void Save(BinaryWriter bw) + { + bw.Write(Filters.Count); + foreach (var filter in Filters) + { + bw.Write(filter.GetType().FullName); + filter.Save(bw); + } + } + + public void Load(BinaryReader reader) + { + int count = reader.ReadInt32(); + for (int i = 0; i < count; i++) + { + var type = reader.ReadString(); + var filter = (IFilter)Activator.CreateInstance(Type.GetType(type)); + filter.Load(reader); + filter.Parent = this; + Filters.Add(filter); + } + } + + public bool IsSelected { get; set; } + + public override string ToString() + { + return "Intersection"; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/MorphologyFilterItem.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/MorphologyFilterItem.cs new file mode 100644 index 0000000..c492152 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/MorphologyFilterItem.cs @@ -0,0 +1,78 @@ +using System.Diagnostics; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +public class MorphologyFilterItem:IFilter +{ + public enum EMorphologyType + { + Erode, + Dilate, + Close, + Open + } + + public int Size { get; set; } = 1; + public EMorphologyType MorphologyType { get; set; }=EMorphologyType.Erode; + public void Load(BinaryReader reader) + { + Size = reader.ReadInt32(); + MorphologyType = (EMorphologyType)reader.ReadInt32(); + } + + public bool IsSelected { get; set; } + + public IFilterParent Parent { get; set; } + + public MorphologyFilterItem() + { + } + + public MorphologyFilterItem(CategoryItem parent) + { + Parent = parent; + } + + public void Transform(ref Mat image, ref Mat res) + { + if(Size==0) + return; + + var sw=Stopwatch.StartNew(); + + var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse,new Size((int)(Size), (int)(Size))); + + switch (MorphologyType) + { + case EMorphologyType.Dilate: + //Cv2.Dilate(res, res,kernel); + Cv2.MorphologyEx(res, res, MorphTypes.Dilate, kernel); + break; + case EMorphologyType.Erode: + Cv2.Erode(res, res,kernel); + break; + case EMorphologyType.Close: + Cv2.MorphologyEx(res, res, MorphTypes.Close, kernel); + break; + case EMorphologyType.Open: + Cv2.MorphologyEx(res, res, MorphTypes.Open, kernel); + break; + } + + sw.Stop(); + + + } + + public void Save(BinaryWriter writer) + { + writer.Write(Size); + writer.Write((int)MorphologyType); + } + + public override string ToString() + { + return $"{MorphologyType} {Size}"; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/RangeFilterItem.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/RangeFilterItem.cs new file mode 100644 index 0000000..77abdb9 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/RangeFilterItem.cs @@ -0,0 +1,101 @@ +using System.Windows.Markup; +using OpenCvSharp; +using Size = OpenCvSharp.Size; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +public class RangeFilterItem : IFilter +{ + public IFilterParent Parent { get; set; } + + public RangeFilterItem(CategoryItem parent) + { + Parent = parent; + } + + public RangeFilterItem() + { + } + + public int HMin { get; set; } = 0; + public int HMax { get; set; } = 100; + public int SMin { get; set; } = 0; + public int SMax { get; set; } = 255; + public int VMin { get; set; } = 0; + public int VMax { get; set; } = 255; + public int Dilation { get; set; } = 0; + public int Closing { get; set; } = 0; + + + public override string ToString() + { + return $"H: {HMin}-{HMax}, S: {SMin}-{SMax}, V: {VMin}-{VMax}"; + } + + public void Transform(ref Mat hsv, ref Mat res) + { + var lower = new Scalar(HMin, SMin, VMin); + var upper = new Scalar(HMax, SMax, VMax); + var mask = hsv.InRange(lower, upper); + + if (Dilation > 0) + { + // generate circular kernel + var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(Dilation, Dilation)); + mask = mask.Dilate(kernel); + kernel.Dispose(); + } + + if (Closing > 0) + { + // generate circular kernel + var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(Closing, Closing)); + mask = mask.MorphologyEx(MorphTypes.Close, kernel); + kernel.Dispose(); + } + + res+=mask; + + mask.Dispose(); + } + + public void Save(BinaryWriter writer) + { + writer.Write(HMin); + writer.Write(HMax); + writer.Write(SMin); + writer.Write(SMax); + writer.Write(VMin); + writer.Write(VMax); + writer.Write(Dilation); + writer.Write(Closing); + } + + public void Load(BinaryReader reader) + { + HMin = reader.ReadInt32(); + HMax = reader.ReadInt32(); + SMin = reader.ReadInt32(); + SMax = reader.ReadInt32(); + VMin = reader.ReadInt32(); + VMax = reader.ReadInt32(); + Dilation = reader.ReadInt32(); + Closing = reader.ReadInt32(); + } + + public bool IsSelected { get; set; } + + public static RangeFilterItem Load(BinaryReader reader, CategoryItem parent) + { + var filter = new RangeFilterItem(parent); + filter.HMin = reader.ReadInt32(); + filter.HMax = reader.ReadInt32(); + filter.SMin = reader.ReadInt32(); + filter.SMax = reader.ReadInt32(); + filter.VMin = reader.ReadInt32(); + filter.VMax = reader.ReadInt32(); + filter.Dilation = reader.ReadInt32(); + filter.Closing = reader.ReadInt32(); + return filter; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SobelFilterItem.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SobelFilterItem.cs new file mode 100644 index 0000000..88995d8 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SobelFilterItem.cs @@ -0,0 +1,25 @@ +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +public class SobelFilterItem:IFilter +{ + public IFilterParent Parent { get; set; } + public void Transform(ref Mat image, ref Mat res) + { + var filtered=image.Sobel(MatType.CV_32F, 1, 1, 3); + filtered.ConvertTo(image, MatType.CV_8UC3); + } + + public void Save(BinaryWriter writer) + { + + } + + public void Load(BinaryReader reader) + { + + } + + public bool IsSelected { get; set; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SpatialFilterItem.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SpatialFilterItem.cs new file mode 100644 index 0000000..36ff667 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SpatialFilterItem.cs @@ -0,0 +1,176 @@ +using System.Diagnostics; +using OpenCvSharp; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Hawkeye.VisionBuilder.Workflow.Datatypes; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +public class SpatialFilterItem : IFilter +{ + private Mat? _mask=null; + + public IFilterParent Parent { get; set; } + + public bool Subtract { get; set; } + + public string? FilePath { get; set; } + + public void ReloadMask() + { + if (!File.Exists(FilePath)) + { + CreateMask(); + }else + if (!string.IsNullOrEmpty(FilePath)) + { + _mask = new Mat(FilePath,ImreadModes.Grayscale); + } + } + + public SpatialFilterItem() + { + + } + + void CreateMask() + { + _mask = new Mat(13005 + 256, 256, MatType.CV_8UC1, (Scalar)0); + } + + public SpatialFilterItem(CategoryItem parent) : this() + { + Parent = parent; + } + + public Mat Mask + { + get + { + if (_mask == null) + { + if (FilePath != null) + { + ReloadMask(); + } + else + { + CreateMask(); + } + } + + return _mask; + } + set + { + _mask = value; + } + } + + struct PixelStruct + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 65536*3)] + public byte[] Pixels; + } + struct PixelStructMask + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3394816)] + public byte[] Pixels; + } + public static Mat lastInput; + public unsafe void Transform(ref Mat image, ref Mat res) + { + + //var pixelStruct = Marshal.PtrToStructure(new IntPtr(image.DataPointer)); + var candyBytes = GetPixels(image); + var candidates = new Mat(image.Size(), MatType.CV_8UC1, (Scalar)0); + Mat tmp=new Mat(); + image.CopyTo(tmp); + lastInput = tmp; + //var pixelStructMask = Marshal.PtrToStructure(new IntPtr(Mask.DataPointer)); + + var maskBytes = GetPixels(Mask); + + var len = candyBytes.Length; + var w = image.Width; + var mw = Mask.Width; + + var lenDiv3 = len / 3; + + for (var q = 0; q < lenDiv3; q++) + { + var i = q * 3; + + var p = i / 3; + var y = p / w; + var x = p % w; + + var yp = (int)candyBytes[i + 0]; + var xp = candyBytes[i + 1]; + var vp = ((byte) (candyBytes[i + 2] / 5f)) * 255; + + var hp = yp + vp; + + var maskColor = maskBytes[hp * mw + xp]; + if (maskColor > 0) + candidates.Set(y, x, 255); + } + + + + + if (Subtract && !IsSelected) + { + res -= candidates; + } + else + { + res += candidates; + + } + + + + } + + string Hash(Span bytes) { + using var sha = SHA256.Create(); + var hash = sha.ComputeHash(bytes.ToArray()); + return Convert.ToBase64String(hash); + } + unsafe Span GetPixels(Mat image) where T : unmanaged + { + + return new Span(image.DataPointer, image.Width * image.Height * image.Channels()); + + } + public void Save(BinaryWriter writer) + { + writer.Write(FilePath??""); + writer.Write(Subtract); + } + + public void Load(BinaryReader reader) + { + FilePath = reader.ReadString(); + Console.WriteLine("Loaded path:"+FilePath); + if(FilePath.StartsWith("..\\../")) + FilePath = FilePath.Replace("..\\../", "..\\"); + Subtract = reader.ReadBoolean(); + } + + public bool IsSelected { get; set; } + + + public override string ToString() + { + var totalColors = 0; + if (Mask != null) + { + totalColors = Mask.CountNonZero(); + } + return $"{(Subtract ? "-" : "+")}Spatial Filter({totalColors} colors)"; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SpatialFilterItemGPU.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SpatialFilterItemGPU.cs new file mode 100644 index 0000000..e2e229e --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SpatialFilterItemGPU.cs @@ -0,0 +1,177 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using ILGPU; +using ILGPU.Runtime; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +[Obsolete] +public class SpatialFilterItemGPU:IFilter +{ + private Mat? _mask = null; + + private MemoryBuffer1D _maskGpu; + + + + public IFilterParent Parent { get; set; } + + public bool Subtract { get; set; } + + public string? FilePath { get; set; } + + public void ReloadMask() + { + if (!File.Exists(FilePath)) + { + CreateMask(); + } + else + if (!string.IsNullOrEmpty(FilePath)) + { + _mask = new Mat(FilePath).CvtColor(ColorConversionCodes.BGR2GRAY); + } + LoadMaskToGpu(); + } + + private unsafe void LoadMaskToGpu() + { + var maskBytes = new Span(Mask.DataPointer, Mask.Width * Mask.Height).ToArray(); + if (_maskGpu != null) + { + _maskGpu.Dispose(); + } + _maskGpu = Kernels.Accelerator.Allocate1D(maskBytes); + + + } + + + private static MemoryBuffer1D? _imageMemory; + private static MemoryBuffer1D? _resMemory; + + + public SpatialFilterItemGPU() + { + + + } + + void CreateMask() + { + _mask = new Mat(13005 + 256, 256, MatType.CV_8UC1, (Scalar)0); + } + + public SpatialFilterItemGPU(CategoryItem parent) : this() + { + Parent = parent; + } + + public Mat Mask + { + get + { + if (_mask == null) + { + if (FilePath != null) + { + ReloadMask(); + } + else + { + CreateMask(); + } + } + + return _mask; + } + set + { + _mask = value; + } + } + + private static Mat? _lastLoadedImage; + + + public unsafe void Transform(ref Mat image, ref Mat res) + { + + + + var w = image.Width; + var mw = Mask.Width; + + + + if (_lastLoadedImage != image) + { + var imageBytes = new ReadOnlySpan(image.DataPointer, image.Width * image.Height * 3); + _imageMemory ??= Kernels.Accelerator.Allocate1D(imageBytes.Length); + _imageMemory.View.BaseView.CopyFromCPU(imageBytes); + _lastLoadedImage = image; + } + + var len = (int)_imageMemory.Length; + + _resMemory ??= Kernels.Accelerator.Allocate1D(len / 3); + + + + Kernels.SpatialKernel(len/3, w, mw, _imageMemory.View, _maskGpu.View, _resMemory.View); + var arr = _resMemory.GetAsArray1D(); + + + var candidates=new Mat(new[]{image.Height,image.Width}, MatType.CV_8UC1); + candidates.SetArray(arr); + + + if (Subtract && !IsSelected) + { + res -= candidates; + } + else + { + res |= candidates; + + } + + + candidates.Dispose(); + + + + } + + public void Save(BinaryWriter writer) + { + writer.Write(FilePath ?? ""); + writer.Write(Subtract); + } + + public void Load(BinaryReader reader) + { + FilePath = reader.ReadString(); + Console.WriteLine("Loaded path:" + FilePath); + if (FilePath.StartsWith("..\\../")) + FilePath = FilePath.Replace("..\\../", "..\\"); + Subtract = reader.ReadBoolean(); + } + + public bool IsSelected { get; set; } + + + public override string ToString() + { + var totalColors = 0; + if (Mask != null) + { + totalColors = Mask.CountNonZero(); + } + return $"{(Subtract ? "-" : "+")}Spatial Filter({totalColors} colors)"; + } + + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SubtractFilterItem.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SubtractFilterItem.cs new file mode 100644 index 0000000..04c1944 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Filters/SubtractFilterItem.cs @@ -0,0 +1,52 @@ +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses.Filters; + +public class SubtractFilterItem : IFilter,IFilterParent +{ + public IFilterParent Parent { get; set; } + + public List Filters { get; set; } = new List(); + + public void Transform(ref Mat image, ref Mat res) + { + using var tracker = new ResourcesTracker(); + var temp = tracker.NewMat(res.Size(),MatType.CV_8UC1,Scalar.Black); + foreach (var filter in Filters) + { + filter.Transform(ref image, ref temp); + } + res = res - temp; + temp.Dispose(); + } + + public void Save(BinaryWriter bw) + { + bw.Write(Filters.Count); + foreach (var filter in Filters) + { + bw.Write(filter.GetType().FullName); + filter.Save(bw); + } + } + + public void Load(BinaryReader reader) + { + int count = reader.ReadInt32(); + for (int i = 0; i < count; i++) + { + var type = reader.ReadString(); + var filter = (IFilter)Activator.CreateInstance(Type.GetType(type)); + filter.Load(reader); + filter.Parent = this; + Filters.Add(filter); + } + } + + public bool IsSelected { get; set; } + + public override string ToString() + { + return "Subtract"; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/HSIValue.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/HSIValue.cs new file mode 100644 index 0000000..f700d79 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/HSIValue.cs @@ -0,0 +1,125 @@ +using System.ComponentModel; +using System.Drawing; +using System.Runtime.CompilerServices; +using System.Xml.Serialization; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses +{ + public class HSIValue + { + + public HSIValue() + { + + } + public int MinSize { get; set; } = 1; + public int MinComponentSize { get; set; } = 1; + public int PreFilterId { get; set; } = 0; + public int PreFilterSize { get; set; } = 10; + + public int Closing { get; set; } + + public int Circle { get; set; } = 1; + public string Tag { get; set; } + private Range _hue = new Range() {Minimum = 0,Maximum = 10}; + private Range _saturation = new Range() { Minimum = 0, Maximum = 10 }; + private Range _intensity = new Range() { Minimum = 0, Maximum = 10 }; + + public string Name { get; set; } + + public Range Hue + { + get { return _hue; } + set + { + if (Equals(value, _hue)) return; + _hue = value; + OnPropertyChanged(); + } + } + + public Range Saturation + { + get { return _saturation; } + set + { + if (Equals(value, _saturation)) return; + _saturation = value; + OnPropertyChanged(); + } + } + + public Range Intensity + { + get { return _intensity; } + set + { + if (Equals(value, _intensity)) return; + _intensity = value; + OnPropertyChanged(); + } + } + + public bool Active { get; set; } + public List SubColors { get; set; }=new List(); + public Rectangle GetRectangle() + { + Rectangle th = new Rectangle((int) (Hue.Minimum), (int) (Saturation.Minimum), + (int) (Hue.Maximum) - (int) (Hue.Minimum), (int) (Saturation.Maximum) - (int) (Saturation.Minimum)); + return th; + } + [XmlIgnore] + public bool ShowStar { get; set; } + + public int Dilation { get; set; } + + + public override string ToString() + { + return $"{Name} {Hue}:{Saturation}:{Intensity}{(ShowStar?"*":"")}"; + } + + + + public static explicit operator ColorConfig(HSIValue value) + { + var config = new ColorConfig() + { + ClosingCircleRadius = value.Circle, + HueHighSaturationMaxGrey = value.Hue.Maximum, + HueHighSaturationMinGrey = value.Hue.Minimum, + HighSaturationMaxGrey = value.Saturation.Maximum, + HighSaturationMinGrey = value.Saturation.Minimum, + IntensityMaxGrey = value.Intensity.Maximum, + IntensityMinGrey = value.Intensity.Minimum, + MaxBlobSize = value.MinSize, + MedianMargin = 1, + MedianRadius = 10, + UseMedianFilter = true, + + }; + return config; + } + + public static implicit operator HSIValue(ColorConfig config) + { + return new HSIValue() + { + Hue = new Range(config.HueHighSaturationMinGrey, config.HueHighSaturationMaxGrey), + Saturation = new Range(config.HighSaturationMinGrey, config.HighSaturationMaxGrey), + Intensity = new Range(config.IntensityMinGrey, config.IntensityMaxGrey), + Circle = (int)config.ClosingCircleRadius, + MinSize = config.MaxBlobSize + }; + } + + public event PropertyChangedEventHandler PropertyChanged; + + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Kernels.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Kernels.cs new file mode 100644 index 0000000..8d64af9 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Kernels.cs @@ -0,0 +1,157 @@ +using ILGPU; +using ILGPU.Runtime; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses; + +public static class Kernels +{ + static Kernels() + { + Init(); + AppDomain.CurrentDomain.ProcessExit += + Destruct; + } + + private static void Destruct(object? sender, EventArgs e) + { + _accelerator.Dispose(); + _gpuContext.Dispose(); + } + + + private static object _lock = new object(); + private static ILGPU.Context _gpuContext; + private static Accelerator _accelerator; + private static Action, ArrayView, ArrayView> _spatialKernel; + private static Action, ArrayView> _dilationKernel; + + public static Action, ArrayView, ArrayView> SpatialKernel => _spatialKernel; + + public static Action, ArrayView> Dilation => _dilationKernel; + + public static Accelerator Accelerator + { + get => _accelerator; + set => _accelerator = value; + } + + static Dictionary> _imageCache = new (); + + public static unsafe MemoryBuffer1D TransformImageCached(Mat image) + { + if (_imageCache.ContainsKey(image)) + return _imageCache[image]; + var imageBytes = new Span(image.DataPointer, image.Width * image.Height * 3); + + var res=Kernels.Accelerator.Allocate1D(imageBytes.ToArray()); + _imageCache[image] = res; + return res; + } + + + static void Init() + { + lock (_lock) + { + + if (_gpuContext != null) return; + + _gpuContext = ILGPU.Context.Create(builder => builder.AllAccelerators()); + _accelerator = _gpuContext.Devices.First(x => x.AcceleratorType == AcceleratorType.Cuda) + .CreateAccelerator(_gpuContext); + Console.WriteLine("GPU acceleration initialized"); + + + _spatialKernel = _accelerator + .LoadAutoGroupedStreamKernel, ArrayView, ArrayView>( + Kernels.TransformSpatialKernel); + + _dilationKernel = _accelerator + .LoadAutoGroupedStreamKernel,ArrayView>( + Kernels.DilationKernel); + + } + } + + private static void TransformSpatialKernel(Index1D index, int w, int mw, ArrayView candyBytes, ArrayView maskBytes, ArrayView res) + { + var i = index; + var p = i * 3; + var y = p / w; + var x = p % w; + + + + var yp = (int)candyBytes[p + 0]; + var xp = candyBytes[p + 1]; + var vp = (byte)(candyBytes[p + 2] / 5f) * 255; + + var hp = yp + vp; + var maskColor = maskBytes[hp * mw + xp]; + if (maskColor > 0) + res[i] = 255; + else + res[i] = 0; + } + + private static void DilationKernel(Index1D index, int w, int size, ArrayView maskBytes, + ArrayView res) + { + var i = index; + var p = i ; + var y = p / w; + var x = p % w; + + + var halfSize = size-1 / 2; + + + if (maskBytes[p] > 0) + { + for (int j = -halfSize; j <= halfSize; j++) + { + for (int k = -halfSize; k <= halfSize; k++) + { + var yp = y + j; + var xp = x + k; + if (yp < 0 || yp >= w || xp < 0 || xp >= w) + continue; + res[yp * w + xp] = 255; + } + } + } + + } + + private static void ErosionKernel(Index1D index, int w, int size, ArrayView maskBytes, + ArrayView res) + { + var i = index; + var p = i ; + var y = p / w; + var x = p % w; + + + var halfSize = size-1 / 2; + + + if (maskBytes[p] <255) + { + for (int j = -halfSize; j <= halfSize; j++) + { + for (int k = -halfSize; k <= halfSize; k++) + { + var yp = y + j; + var xp = x + k; + if (yp < 0 || yp >= w || xp < 0 || xp >= w) + continue; + res[yp * w + xp] = 0; + } + } + } + + } + + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Range.cs b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Range.cs new file mode 100644 index 0000000..9c5361d --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ColorStudioClasses/Range.cs @@ -0,0 +1,64 @@ +namespace Hawkeye.VisionBuilder.Workflow.ColorStudioClasses +{ + /// The Range class. + /// Generic parameter. + public class Range where T : IComparable + { + + public Range() + { + + } + public Range(T min,T max) + { + this.Maximum = max; + this.Minimum = min; + } + + /// Minimum value of the range. + public T Minimum { get; set; } + + /// Maximum value of the range. + public T Maximum { get; set; } + + /// Presents the Range in readable format. + /// String representation of the Range + public override string ToString() + { + return string.Format("[{0} - {1}]", this.Minimum, this.Maximum); + } + + /// Determines if the range is valid. + /// True if range is valid, else false + public bool IsValid() + { + return this.Minimum.CompareTo(this.Maximum) <= 0; + } + + /// Determines if the provided value is inside the range. + /// The value to test + /// True if the value is inside Range, else false + public bool ContainsValue(T value) + { + return (this.Minimum.CompareTo(value) <= 0) && (value.CompareTo(this.Maximum) <= 0); + } + + /// Determines if this Range is inside the bounds of another range. + /// The parent range to test on + /// True if range is inclusive, else false + public bool IsInsideRange(Range range) + { + return this.IsValid() && range.IsValid() && range.ContainsValue(this.Minimum) && range.ContainsValue(this.Maximum); + } + + /// Determines if another range is inside the bounds of this range. + /// The child range to test + /// True if range is inside, else false + public bool ContainsRange(Range range) + { + return this.IsValid() && range.IsValid() && this.ContainsValue(range.Minimum) && this.ContainsValue(range.Maximum); + } + + + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Configuration/BadTweakLearning.cs b/Hawkeye.VisionBuilder.Workflow/Configuration/BadTweakLearning.cs new file mode 100644 index 0000000..e966408 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Configuration/BadTweakLearning.cs @@ -0,0 +1,20 @@ +namespace Hawkeye.VisionBuilder.Workflow.Configuration; + +public class BadTweakLearning +{ + public bool Enabled { get; set; } = false; + public int ThresholdLevel { get; set; } = 80; + + public void Save(BinaryWriter bw) + { + bw.Write(Enabled); + bw.Write(ThresholdLevel); + } + + public void Load(BinaryReader br) + { + Enabled = br.ReadBoolean(); + ThresholdLevel = br.ReadInt32(); + } + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Configuration/ECameraType.cs b/Hawkeye.VisionBuilder.Workflow/Configuration/ECameraType.cs new file mode 100644 index 0000000..5fc0a2c --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Configuration/ECameraType.cs @@ -0,0 +1,8 @@ +namespace Hawkeye.VisionBuilder.Workflow.Configuration; + +public enum ECameraType +{ + Emulation=0, + Hawkeye=1 + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Configuration/EOutputs.cs b/Hawkeye.VisionBuilder.Workflow/Configuration/EOutputs.cs new file mode 100644 index 0000000..3a833e6 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Configuration/EOutputs.cs @@ -0,0 +1,8 @@ +namespace Hawkeye.VisionBuilder.Workflow.Configuration; + +public enum EOutputs +{ + None, + Firmata, + NetServer +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Configuration/RecipeSelectionConfig.cs b/Hawkeye.VisionBuilder.Workflow/Configuration/RecipeSelectionConfig.cs new file mode 100644 index 0000000..589c78a --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Configuration/RecipeSelectionConfig.cs @@ -0,0 +1,19 @@ +namespace Hawkeye.VisionBuilder.Workflow.Configuration; + +public class RecipeSelectionConfig +{ + public bool SerialEnabled { get; set; } = false; + public string ComPort { get; set; } = "COM3"; + + public void Save(BinaryWriter bw) + { + bw.Write(SerialEnabled); + bw.Write(ComPort); + } + + public void Load(BinaryReader br) + { + SerialEnabled = br.ReadBoolean(); + ComPort = br.ReadString(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Configuration/WorkflowConfiguration.cs b/Hawkeye.VisionBuilder.Workflow/Configuration/WorkflowConfiguration.cs new file mode 100644 index 0000000..8ffdaa0 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Configuration/WorkflowConfiguration.cs @@ -0,0 +1,58 @@ +using System.Reflection.Metadata.Ecma335; +using System.Security.AccessControl; +using Hawkeye.VisionBuilder.Workflow.Operations; + + +namespace Hawkeye.VisionBuilder.Workflow.Configuration; + +public class WorkflowConfiguration +{ + public WorkflowConfiguration() + { + var userFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + PythonPath = Path.Combine(userFolder, @"miniconda3\envs\ai3_cpu\python.exe"); + } + + public ECameraType RuntimeCameraType { get; set; } = ECameraType.Emulation; + public ECameraType DevelopmentCameraType { get; set; } = ECameraType.Emulation; + + public string EmulationPath { get; set; } = "..\\Data\\Emulation"; + public string PythonPath { get; set; } + + public EOutputs Outputs { get; set; } + public int ResultPin { get; set; } + public string SerialPort { get; set; } = ""; + public int Delay { get; set; } = 0; + + + public BadTweakLearning BadTweakLearning { get; set; } = new BadTweakLearning(); + public RecipeSelectionConfig RecipeSelectionConfig { get; set; }=new RecipeSelectionConfig(); + + public void Save(BinaryWriter bw) + { + bw.Write((int)RuntimeCameraType); + bw.Write((int)DevelopmentCameraType); + bw.Write((int)Outputs); + bw.Write(EmulationPath); + bw.Write(PythonPath); + bw.Write(ResultPin); + bw.Write(SerialPort); + bw.Write(Delay); + BadTweakLearning.Save(bw); + RecipeSelectionConfig.Save(bw); + } + + public void Load(BinaryReader br) + { + RuntimeCameraType = (ECameraType) br.ReadInt32(); + DevelopmentCameraType = (ECameraType) br.ReadInt32(); + Outputs = (EOutputs) br.ReadInt32(); + EmulationPath = br.ReadString(); + PythonPath = br.ReadString(); + ResultPin = br.ReadInt32(); + SerialPort = br.ReadString(); + Delay = br.ReadInt32(); + BadTweakLearning.Load(br); + RecipeSelectionConfig.Load(br); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Context.cs b/Hawkeye.VisionBuilder.Workflow/Context.cs new file mode 100644 index 0000000..de08597 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Context.cs @@ -0,0 +1,13 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements; + +namespace Hawkeye.VisionBuilder.Workflow; + +public class Context +{ + public HawkeyeImage? ActiveImage { get; set; } + public HawkeyeImage? LastCameraImage { get; set; } + public List GraphicsElements { get; set; }=new List(); + public Dictionary Memory { get; set; } = new Dictionary(); + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/DataTransfer/CSharpDataTransferMQ.cs b/Hawkeye.VisionBuilder.Workflow/DataTransfer/CSharpDataTransferMQ.cs new file mode 100644 index 0000000..d776dd8 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/DataTransfer/CSharpDataTransferMQ.cs @@ -0,0 +1,55 @@ +using NetMQ; +using NetMQ.Sockets; + +namespace Hawkeye.VisionBuilder.Workflow.DataTransfer; + +public class CSharpDataTransferMQ +{ + + private object _syncObject = new object(); + private readonly ResponseSocket _server; + + public CSharpDataTransferMQ() + { + this._server = new ResponseSocket(); + _server.Bind("tcp://*:5555"); + + } + + public byte[] TransferData(string command, byte mode, byte[] data) + { + lock (_syncObject) + { + return TransferDataInternal(command,mode,data); + } + + } + + private byte[] TransferDataInternal(string command,byte mode,byte[] data) + { + Console.WriteLine("Waiting for request"); + _server.ReceiveFrameBytes(); + + MemoryStream ms = new MemoryStream(); + BinaryWriter bw = new BinaryWriter(ms); + var functionBytes=System.Text.Encoding.ASCII.GetBytes(command); + bw.Write(functionBytes.Length); + bw.Write(functionBytes); + bw.Write(mode); + bw.Write(data); + + + + + + Console.WriteLine("Sending response"); + _server.SendFrame(ms.ToArray()); + + Console.WriteLine("Waiting for request"); + var result= _server.ReceiveFrameBytes(); + + _server.SendFrame("OK"); + + return result; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/DataTransfer/CSharpDataTransferMQRPC.cs b/Hawkeye.VisionBuilder.Workflow/DataTransfer/CSharpDataTransferMQRPC.cs new file mode 100644 index 0000000..7b3fd2e --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/DataTransfer/CSharpDataTransferMQRPC.cs @@ -0,0 +1,130 @@ +using MessagePack; +using NetMQ; +using NetMQ.Sockets; + +namespace Hawkeye.VisionBuilder.Workflow.DataTransfer; +[MessagePackObject] +public class MethodCall +{ + [Key(0)] + public string Name { get; set; } + [Key(1)] + public List Parameters { get; set; } + + public MethodCall() + { + } + + public MethodCall(string name, params object[] parameters) + { + Name=name; + Parameters=new List(parameters); + } +} +[MessagePackObject] +public class TransferData +{ + [Key(0)] + public ETransferType DataType { get; set; } + [Key(1)] + public int Int { get; set; } + [Key(2)] + public string String { get; set; } + [Key(3)] + public byte[] Bytes { get; set; } + [Key(4)] + public float[] Floats { get; set; } + + public static implicit operator TransferData(int value) + { + return new TransferData() + { + DataType = ETransferType.Int, + Int = value + }; + } + + public static implicit operator TransferData(string value) + { + return new TransferData() + { + DataType = ETransferType.String, + String = value + }; + } + + public static implicit operator TransferData(byte[] value) + { + return new TransferData() + { + DataType = ETransferType.Bytes, + Bytes = value + }; + } + public static implicit operator TransferData(float[] value) + { + return new TransferData() + { + DataType = ETransferType.Floats, + Floats = value + }; + } +} + +public enum ETransferType +{ + Void=0, + Int=1, + String=2, + Bytes=3, + Floats=4 +} +public class CSharpDataTransferMQRPC +{ + private object _syncObject = new object(); + private readonly ResponseSocket _server; + + public CSharpDataTransferMQRPC() + { + this._server = new ResponseSocket(); + _server.Bind("tcp://*:5555"); + + + } + + public T TransferData(MethodCall call) + { + lock (_syncObject) + { + return TransferDataInternal(call); + } + + } + + private T TransferDataInternal(MethodCall call) + { + Console.WriteLine("Waiting for request"); + _server.ReceiveFrameBytes(); + + + Console.WriteLine("Sending response"); + + var serialized = MessagePack.MessagePackSerializer.Serialize(call); + + _server.SendFrame(serialized); + + + + Console.WriteLine("Waiting for request"); + var result = _server.ReceiveFrameBytes(); + + var decoded=MessagePackSerializer.Deserialize(result); + + + + _server.SendFrame("OK"); + + + return (T)Convert.ChangeType(decoded, typeof(T)); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/DataTransfer/ChildProcessTracker.cs b/Hawkeye.VisionBuilder.Workflow/DataTransfer/ChildProcessTracker.cs new file mode 100644 index 0000000..4e211a1 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/DataTransfer/ChildProcessTracker.cs @@ -0,0 +1,135 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Hawkeye.VisionBuilder.Workflow.DataTransfer; + +public static class ChildProcessTracker +{ + /// + /// Add the process to be tracked. If our current process is killed, the child processes + /// that we are tracking will be automatically killed, too. If the child process terminates + /// first, that's fine, too. + /// + public static void AddProcess(Process process) + { + if (s_jobHandle != IntPtr.Zero) + { + bool success = AssignProcessToJobObject(s_jobHandle, process.Handle); + if (!success && !process.HasExited) + throw new Win32Exception(); + } + } + + static ChildProcessTracker() + { + // This feature requires Windows 8 or later. To support Windows 7 requires + // registry settings to be added if you are using Visual Studio plus an + // app.manifest change. + // https://stackoverflow.com/a/4232259/386091 + // https://stackoverflow.com/a/9507862/386091 + if (Environment.OSVersion.Version < new Version(6, 2)) + return; + + // The job name is optional (and can be null) but it helps with diagnostics. + // If it's not null, it has to be unique. Use SysInternals' Handle command-line + // utility: handle -a ChildProcessTracker + string jobName = "ChildProcessTracker" + Process.GetCurrentProcess().Id; + s_jobHandle = CreateJobObject(IntPtr.Zero, jobName); + + var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION(); + + // This is the key flag. When our process is killed, Windows will automatically + // close the job handle, and when that happens, we want the child processes to + // be killed, too. + info.LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION(); + extendedInfo.BasicLimitInformation = info; + + int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length); + try + { + Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); + + if (!SetInformationJobObject(s_jobHandle, JobObjectInfoType.ExtendedLimitInformation, + extendedInfoPtr, (uint)length)) + { + throw new Win32Exception(); + } + } + finally + { + Marshal.FreeHGlobal(extendedInfoPtr); + } + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string name); + + [DllImport("kernel32.dll")] + static extern bool SetInformationJobObject(IntPtr job, JobObjectInfoType infoType, + IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); + + // Windows will automatically close any open job handles when our process terminates. + // This can be verified by using SysInternals' Handle utility. When the job handle + // is closed, the child processes will be killed. + private static readonly IntPtr s_jobHandle; +} + +public enum JobObjectInfoType +{ + AssociateCompletionPortInformation = 7, + BasicLimitInformation = 2, + BasicUIRestrictions = 4, + EndOfJobTimeInformation = 6, + ExtendedLimitInformation = 9, + SecurityLimitInformation = 5, + GroupInformation = 11 +} + +[StructLayout(LayoutKind.Sequential)] +public struct JOBOBJECT_BASIC_LIMIT_INFORMATION +{ + public Int64 PerProcessUserTimeLimit; + public Int64 PerJobUserTimeLimit; + public JOBOBJECTLIMIT LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public UInt32 ActiveProcessLimit; + public Int64 Affinity; + public UInt32 PriorityClass; + public UInt32 SchedulingClass; +} + +[Flags] +public enum JOBOBJECTLIMIT : uint +{ + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000 +} + +[StructLayout(LayoutKind.Sequential)] +public struct IO_COUNTERS +{ + public UInt64 ReadOperationCount; + public UInt64 WriteOperationCount; + public UInt64 OtherOperationCount; + public UInt64 ReadTransferCount; + public UInt64 WriteTransferCount; + public UInt64 OtherTransferCount; +} + +[StructLayout(LayoutKind.Sequential)] +public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION +{ + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonModelProxy.cs b/Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonModelProxy.cs new file mode 100644 index 0000000..3b3cba1 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonModelProxy.cs @@ -0,0 +1,31 @@ +namespace Hawkeye.VisionBuilder.Workflow.DataTransfer; + +public static class PythonModelProxy +{ + + private static bool _isInitialized = false; + private static PythonScriptRunner _pythonScriptRunner; + private static CSharpDataTransferMQ _cSharpDataTransfer; + + public static void Initialize(string pythonPath, string scriptFile= "Hawkeye.ModelLoader.py") + { + if (_isInitialized) return; + if (!File.Exists(pythonPath)) return; + _isInitialized = true; + + var relativePath = @"..\Data\AI"; + var absolutePath = Path.GetFullPath(relativePath); + _cSharpDataTransfer = new CSharpDataTransferMQ(); + _pythonScriptRunner = new PythonScriptRunner(pythonPath, scriptFile, absolutePath); + _pythonScriptRunner.Start(); + + + + } + + public static CSharpDataTransferMQ GetInterface() + { + if (!_isInitialized) return null; + return _cSharpDataTransfer; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonModelProxyRPC.cs b/Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonModelProxyRPC.cs new file mode 100644 index 0000000..c236661 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonModelProxyRPC.cs @@ -0,0 +1,38 @@ +namespace Hawkeye.VisionBuilder.Workflow.DataTransfer; + +public static class PythonModelProxyRPC +{ + private static bool _isInitialized = false; + private static PythonScriptRunner _pythonScriptRunner; + private static CSharpDataTransferMQRPC _cSharpDataTransferMQRPC; + + public static void Initialize(string scriptFile = @"..\Data\models\Segment128Half.py") + { + if (_isInitialized) return; + + var pythonPath = Environment.ExpandEnvironmentVariables(@"%userprofile%\miniconda3\envs\ai4\python.exe"); + //var pythonPath = Environment.ExpandEnvironmentVariables(@"/root/ai3/bin/python3"); + var scriptRelPath = scriptFile; + var scriptAbsPath = Path.GetFullPath(scriptRelPath); + var workDirectory = Path.GetDirectoryName(scriptAbsPath); + Directory.CreateDirectory(workDirectory); + + + if (!File.Exists(pythonPath)) return; + _isInitialized = true; + + _cSharpDataTransferMQRPC = new CSharpDataTransferMQRPC(); + _pythonScriptRunner = new PythonScriptRunner(pythonPath, scriptAbsPath, workDirectory); + + _pythonScriptRunner.Start(); + + + + } + + public static CSharpDataTransferMQRPC GetInterface() + { + if (!_isInitialized) Initialize(); + return _cSharpDataTransferMQRPC; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonScriptRunner.cs b/Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonScriptRunner.cs new file mode 100644 index 0000000..9c1749d --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/DataTransfer/PythonScriptRunner.cs @@ -0,0 +1,53 @@ +using System.Diagnostics; + +namespace Hawkeye.VisionBuilder.Workflow.DataTransfer; + +public class PythonScriptRunner +{ + private Process process; + private readonly string _pythonPath; + private string scriptPath; + private readonly string _workingDirectory; + + public PythonScriptRunner(string pythonPath, string scriptPath,string workingDirectory) + { + _pythonPath = pythonPath; + this.scriptPath = scriptPath; + _workingDirectory = workingDirectory; + } + + public void Start() + { + ProcessStartInfo startInfo = new ProcessStartInfo(); + + + startInfo.FileName = _pythonPath; // set the path to the Python executable if not in PATH + startInfo.Arguments = "\""+scriptPath+ "\""; + startInfo.WorkingDirectory = _workingDirectory; + + // enable running in the background + startInfo.UseShellExecute = false; + //startInfo.CreateNoWindow = true; + // create and start the process + process = new Process(); + + + process.StartInfo = startInfo; + process.Start(); + // on windows only + if(Environment.OSVersion.Platform == PlatformID.Win32NT) + ChildProcessTracker.AddProcess(process); + + + } + + public void Stop() + { + if (process != null && !process.HasExited) + { + process.Kill(); + process.Dispose(); + process = null; + } + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/DataTransfer/WslScriptRunner.cs b/Hawkeye.VisionBuilder.Workflow/DataTransfer/WslScriptRunner.cs new file mode 100644 index 0000000..899dc51 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/DataTransfer/WslScriptRunner.cs @@ -0,0 +1,66 @@ +using System.Diagnostics; + +namespace Hawkeye.VisionBuilder.Workflow.DataTransfer; + +public class WslScriptRunner +{ + private Process process; + private readonly string _pythonPath; + private string scriptPath; + private readonly string _workingDirectory; + + public WslScriptRunner(string pythonPath, string scriptPath,string workingDirectory) + { + _pythonPath = pythonPath; + this.scriptPath = scriptPath; + _workingDirectory = workingDirectory; + } + public static string ConvertToWslPath(string windowsPath) + { + if (string.IsNullOrWhiteSpace(windowsPath)) + throw new ArgumentException("Path cannot be null or empty.", nameof(windowsPath)); + + // Replace backslashes with forward slashes + string wslPath = windowsPath.Replace('\\', '/'); + + // Extract the drive letter and convert it to WSL format + if (wslPath.Length > 1 && wslPath[1] == ':') + { + char driveLetter = char.ToLower(wslPath[0]); + wslPath = $"/mnt/{driveLetter}{wslPath.Substring(2)}"; + } + + return wslPath; + } + public void Start() + { + + + + //startInfo.CreateNoWindow = true; + // create and start the process + process = new Process(); + var arg = + $@"--distribution ubuntu --user root --cd ""{ConvertToWslPath(_workingDirectory)}"" -- /root/ai3/bin/python3 ""{ConvertToWslPath(scriptPath)}"""; + process.StartInfo = new ProcessStartInfo("wsl",arg) + { + UseShellExecute = false + }; + process.Start(); + // on windows only + if(Environment.OSVersion.Platform == PlatformID.Win32NT) + ChildProcessTracker.AddProcess(process); + + + } + + public void Stop() + { + if (process != null && !process.HasExited) + { + process.Kill(); + process.Dispose(); + process = null; + } + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Align/AlignElement.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Align/AlignElement.cs new file mode 100644 index 0000000..96b66ac --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Align/AlignElement.cs @@ -0,0 +1,35 @@ + +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Align; + +public class AlignElement:IGraphicsElement +{ + public bool Editable { get; set; } + public bool IsGood { get; set; } + public Vector2 Location { get; set; } + public void Draw(ICanvas canvas) + { + canvas.DrawCross(Location); + } + + public void FillMat(Mat mat) + { + throw new NotSupportedException(); + } + + public INode? GetNode(Vector2 position, float thDistance) + { + return null; + } + + public void SetPivot(Vector2 pivot) + { + throw new NotSupportedException(); + } + + public void MovePivot(Vector2 pivot) + { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ArrayHorizontal/ArrayHorizontalAnchorNode.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ArrayHorizontal/ArrayHorizontalAnchorNode.cs new file mode 100644 index 0000000..95f4948 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ArrayHorizontal/ArrayHorizontalAnchorNode.cs @@ -0,0 +1,18 @@ + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.ArrayHorizontal; + +public class ArrayHorizontalAnchorNode:INode +{ + private readonly ArrayHorizontalElement _parent; + + public ArrayHorizontalAnchorNode(ArrayHorizontalElement parent) + { + _parent = parent; + } + + public void SetPosition(Vector2 position) + { + _parent.Location = position - _parent.Offset; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ArrayHorizontal/ArrayHorizontalElement.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ArrayHorizontal/ArrayHorizontalElement.cs new file mode 100644 index 0000000..750cf85 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ArrayHorizontal/ArrayHorizontalElement.cs @@ -0,0 +1,86 @@ + +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.ArrayHorizontal; + +public class ArrayHorizontalElement:IGraphicsElement +{ + public bool Editable { get; set; } + + public Dictionary IsGood=new Dictionary(); + + public Vector2 Location { get; set; } + public Vector2 Offset { get; set; } + + + public Vector2 BlockSize { get; set; } + public int Margin { get; set; } = 5; + public int BlockCount { get; set; } = 5; + + public void Draw(ICanvas canvas) + { + GenerateLocations().Select((l, i) => (l, i)).ToList().ForEach(((Vector2 l, int i) x) => canvas.DrawRectangle(x.l, BlockSize, IsGood.GetValueOrDefault(x.i,true))); + canvas.DrawNode(Offset + Location); + + } + + public IEnumerable GenerateLocations() + { + for (int i = 0; i < BlockCount; i++) + yield return Offset + Location + (new Vector2(1, 0) * i * (BlockSize.X + Margin)); + } + + public void FillMat(Mat mat) + { + + } + + + public INode? GetNode(Vector2 position, float thDistance) + { + if ((Location + Offset - position).Length() < thDistance) + { + return new ArrayHorizontalAnchorNode(this); + } + + + return null; + } + + public void SetPivot(Vector2 pivot) + { + Location += Offset; + Offset = pivot; + Location -= Offset; + } + + public void MovePivot(Vector2 pivot) + { + Offset = pivot; + } + + + + + public void Save(BinaryWriter bw) + { + bw.Write(Editable); + bw.Write(Location.X); + bw.Write(Location.Y); + bw.Write(Offset.X); + bw.Write(Offset.Y); + bw.Write(BlockSize.X); + bw.Write(BlockSize.Y); + bw.Write(BlockCount); + + } + + public void Load(BinaryReader br) + { + Editable = br.ReadBoolean(); + Location = new Vector2(br.ReadSingle(), br.ReadSingle()); + Offset = new Vector2(br.ReadSingle(), br.ReadSingle()); + BlockSize = new Vector2(br.ReadSingle(), br.ReadSingle()); + BlockCount = br.ReadInt32(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Edge/EdgeElement.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Edge/EdgeElement.cs new file mode 100644 index 0000000..61a772f --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Edge/EdgeElement.cs @@ -0,0 +1,40 @@ + +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Edge; + +public class EdgeElement:IGraphicsElement +{ + public bool Editable { get; set; } + public bool IsGood { get; set; } + public Vector2 Location { get; set; } + public float Rotation { get; set; } + public void Draw(ICanvas canvas) + { + float endX = Location.X + MathF.Cos(Rotation)*1000; + float endY = Location.Y + MathF.Sin(Rotation) * 1000; + float startX = Location.X - MathF.Cos(Rotation) * 1000; + float startY = Location.Y - MathF.Sin(Rotation) * 1000; + canvas.DrawLine(new Vector2(startX,startY), new Vector2(endX, endY), LineType.Construction); + } + + public void FillMat(Mat mat) + { + throw new NotImplementedException(); + } + + public INode? GetNode(Vector2 position, float thDistance) + { + throw new NotImplementedException(); + } + + public void SetPivot(Vector2 pivot) + { + throw new NotImplementedException(); + } + + public void MovePivot(Vector2 pivot) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Edge/EdgeOrigin.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Edge/EdgeOrigin.cs new file mode 100644 index 0000000..e27d219 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Edge/EdgeOrigin.cs @@ -0,0 +1,54 @@ + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Edge; + +public struct EdgeOrigin +{ + public Vector2 Location { get; set; } + public float Rotation { get; set; } + public bool Found { get; set; } + public float endX => Location.X + MathF.Cos(Rotation) * 1000; + public float endY => Location.Y + MathF.Sin(Rotation) * 1000; + public float startX => Location.X - MathF.Cos(Rotation) * 1000; + public float startY => Location.Y - MathF.Sin(Rotation) * 1000; + public float Slope=>(endY-startY)/(endX-startX); + public float YIntercept => startY - (Slope * startX); + public bool Intersection(EdgeOrigin other, out float x, out float y) + { + x = 0; + y = 0; + // Line 1 + Vector2 line1Start = new Vector2(startX, startY); + + + // Line 2 + Vector2 line2Start = new Vector2(other.startX, other.startY); + + + // Calculate the slopes of the lines + double line1Slope = Slope; + double line2Slope = other.Slope; + + if (line1Slope == line2Slope) return false; + + // Calculate the y-intercepts of the lines + double line1Intercept = line1Start.Y - line1Slope * line1Start.X; + double line2Intercept = line2Start.Y - line2Slope * line2Start.X; + + // Calculate the intersection point + double intersectionX = (line2Intercept - line1Intercept) / (line1Slope - line2Slope); + double intersectionY = line1Slope * intersectionX + line1Intercept; + + // Create the intersection point object + x = (float)intersectionX; + y = (float)intersectionY; + return true; + } + + public override string ToString() + { + return $"{Location.ToString()} {Rotation} rad"; + } + + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/FixedRectangle/FixedRectangleAnchorNode.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/FixedRectangle/FixedRectangleAnchorNode.cs new file mode 100644 index 0000000..f46b098 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/FixedRectangle/FixedRectangleAnchorNode.cs @@ -0,0 +1,21 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.FixedRectangle; + +public class FixedRectangleAnchorNode:INode +{ + private readonly FixedRectangleElement _owner; + + public FixedRectangleAnchorNode(FixedRectangleElement owner) + { + _owner = owner; + } + + public void SetPosition(Vector2 position) + { + + _owner.Location=position-_owner.Offset; + + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/FixedRectangle/FixedRectangleElement.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/FixedRectangle/FixedRectangleElement.cs new file mode 100644 index 0000000..25a8f12 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/FixedRectangle/FixedRectangleElement.cs @@ -0,0 +1,58 @@ + +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.FixedRectangle; + +public class FixedRectangleElement : IGraphicsElement +{ + public bool Editable { get; set; } + public bool IsGood { get; set; } + public Vector2 Location { get; set; } + public Vector2 Offset { get; set; } + public Vector2 Size { get; set; } + + public void Draw(ICanvas canvas) + { + canvas.DrawRectangle(Offset + Location, Size, IsGood); + canvas.DrawNode(Offset + Location); + + } + + public void FillMat(Mat mat) + { + var location = Offset + Location; + Cv2.Rectangle(mat, + new Rect((int)location.X, (int)location.Y, (int)Size.X, + (int)Size.Y), + Scalar.White, -1); + } + + public INode? GetNode(Vector2 position, float thDistance) + { + if ((Location + Offset - position).Length() < thDistance) + { + return new FixedRectangleAnchorNode(this); + } + + return null; + } + + public void SetPivot(Vector2 pivot) + { + Location += Offset; + Offset = pivot; + Location -= Offset; + } + + public void MovePivot(Vector2 pivot) + { + Offset = pivot; + } + + public FixedRectangleElement() + { + Location = Vector2.Zero; + Size = Vector2.Zero; + Offset = Vector2.Zero; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ICanvas.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ICanvas.cs new file mode 100644 index 0000000..b373258 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/ICanvas.cs @@ -0,0 +1,13 @@ + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements; + +public interface ICanvas +{ + void DrawRectangle(Vector2 start, Vector2 size, bool isGood); + void DrawLine(Vector2 start, Vector2 end, LineType type); + void DrawPoly(Vector2[] points, bool isGood); + void DrawCross(Vector2 location); + void DrawNode(Vector2 location); + void DrawArrow(Vector2 location, Vector2 direction); +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/IGraphicsElement.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/IGraphicsElement.cs new file mode 100644 index 0000000..8359697 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/IGraphicsElement.cs @@ -0,0 +1,23 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Parent; +using Hawkeye.VisionBuilder.Workflow.Links; + +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements; + +public interface IGraphicsElement +{ + + + + public bool Editable { get; set; } + + public void Draw(ICanvas canvas); + public void FillMat(Mat mat); + public INode? GetNode(Vector2 position, float thDistance); + public void SetPivot(Vector2 pivot); + public void MovePivot(Vector2 pivot); + + + +} diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/INode.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/INode.cs new file mode 100644 index 0000000..a57db14 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/INode.cs @@ -0,0 +1,9 @@ + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements; + +public interface INode +{ + public void SetPosition(Vector2 position); + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Line/LineElement.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Line/LineElement.cs new file mode 100644 index 0000000..0e0dea8 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Line/LineElement.cs @@ -0,0 +1,38 @@ + +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Line; + +public class LineElement:IGraphicsElement +{ + public bool Editable { get; set; } + public bool IsGood { get; set; } + + public Vector2 Start { get; set; } + public Vector2 End { get; set; } + + public void Draw(ICanvas canvas) + { + canvas.DrawLine(Start, End,LineType.Construction); + } + + public void FillMat(Mat mat) + { + throw new NotImplementedException(); + } + + public INode? GetNode(Vector2 position, float thDistance) + { + return null; + } + + public void SetPivot(Vector2 pivot) + { + + } + + public void MovePivot(Vector2 pivot) + { + + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/LineType.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/LineType.cs new file mode 100644 index 0000000..1be34c3 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/LineType.cs @@ -0,0 +1,8 @@ +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements; + +public enum LineType +{ + Good, + Bad, + Construction +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Origin/OriginElement.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Origin/OriginElement.cs new file mode 100644 index 0000000..a93b732 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Origin/OriginElement.cs @@ -0,0 +1,27 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Parent; +using Hawkeye.VisionBuilder.Workflow.Links; + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; + +public class OriginElement +{ + private static readonly OriginElement _default = new OriginElement(); + private Vector2 _location; + + public static OriginElement Default => _default; + + public Vector2 Location + { + get => _location; + set => _location = value; + } + + + public OriginElement() + { + + } + + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Parent/IHaveParentCoordinates.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Parent/IHaveParentCoordinates.cs new file mode 100644 index 0000000..fc6b74a --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Parent/IHaveParentCoordinates.cs @@ -0,0 +1,9 @@ + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Parent; + +public interface IHaveParentCoordinates +{ + public Vector2 Location { get; } + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Parent/NoParentCoordinates.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Parent/NoParentCoordinates.cs new file mode 100644 index 0000000..80fdfa3 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Parent/NoParentCoordinates.cs @@ -0,0 +1,14 @@ + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Parent; + +public class NoParentCoordinates:IHaveParentCoordinates +{ + private NoParentCoordinates() + { + } + + public static NoParentCoordinates Instance { get; }=new NoParentCoordinates(); + public Vector2 Location { get; set; }=Vector2.Zero; + public float Rotation { get; set; } = 0; +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Poly/PolyElement.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Poly/PolyElement.cs new file mode 100644 index 0000000..992df58 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Poly/PolyElement.cs @@ -0,0 +1,42 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Parent; +using Hawkeye.VisionBuilder.Workflow.Links; + +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Poly; + +public class PolyElement : IGraphicsElement +{ + + public IGraphicsElement Parent { get; set; } + public Vector2 Location { get; set; } + public bool Editable { get; set; } + public bool IsGood { get; set; } + + public void Draw(ICanvas canvas) + { + canvas.DrawPoly(Points, IsGood); + } + + public void FillMat(Mat mat) + { + throw new NotImplementedException(); + } + + public INode? GetNode(Vector2 position, float thDistance) + { + return null; + } + + public void SetPivot(Vector2 pivot) + { + throw new NotImplementedException(); + } + + public void MovePivot(Vector2 pivot) + { + throw new NotImplementedException(); + } + + public Vector2[] Points { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleAnchorNode.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleAnchorNode.cs new file mode 100644 index 0000000..24e6a4f --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleAnchorNode.cs @@ -0,0 +1,20 @@ + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; + +public class RectangleAnchorNode:INode +{ + private readonly RectangleElement _owner; + + public RectangleAnchorNode(RectangleElement owner) + { + _owner = owner; + } + + public void SetPosition(Vector2 position) + { + + _owner.Location=position-_owner.Offset; + + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleElement.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleElement.cs new file mode 100644 index 0000000..ec434e3 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleElement.cs @@ -0,0 +1,177 @@ +using System.Runtime.CompilerServices; +using System.Xml; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Parent; +using Hawkeye.VisionBuilder.Workflow.Links; + +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; + +public class RectangleElement : IGraphicsElement +{ + + public bool Editable { get; set; } + public bool IsGood { get; set; } + + public Vector2 Location { get; set; } + public Vector2 Offset { get; set; } + + public Vector2 Size { get; set; } + + public void Draw(ICanvas canvas) + { + + canvas.DrawRectangle(Offset+Location, Size,IsGood); + canvas.DrawNode(Offset+Location); + canvas.DrawNode(Offset+Location + Size); + + } + + public void FillMat(Mat mat) + { + var location = Offset + Location; + Cv2.Rectangle(mat, + new Rect((int)location.X, (int)location.Y, (int)Size.X, + (int)Size.Y), + Scalar.White, -1); + } + + + public INode? GetNode(Vector2 position,float thDistance) + { + if ((Location + Offset - position).Length()< thDistance) + { + return new RectangleAnchorNode(this); + } + if((Size + Location+Offset - position).Length() < thDistance) + { + return new RectangleSizeNode(this); + } + + return null; + } + + public void SetPivot(Vector2 pivot) + { + Location += Offset; + Offset =pivot; + Location-=Offset; + } + + public void MovePivot(Vector2 pivot) + { + Offset = pivot; + } + + public RectangleElement() + { + + } + + + public void Save(BinaryWriter bw) + { + bw.Write(Editable); + bw.Write(IsGood); + bw.Write(Location.X); + bw.Write(Location.Y); + bw.Write(Offset.X); + bw.Write(Offset.Y); + bw.Write(Size.X); + bw.Write(Size.Y); + } + + public void Load(BinaryReader br) + { + Editable = br.ReadBoolean(); + IsGood = br.ReadBoolean(); + Location = new Vector2(br.ReadSingle(), br.ReadSingle()); + Offset = new Vector2(br.ReadSingle(), br.ReadSingle()); + Size = new Vector2(br.ReadSingle(), br.ReadSingle()); + } + + public void SaveXML(XmlWriter writer) + { + writer.WriteStartElement("Editable"); + writer.WriteString(Editable.ToString()); + writer.WriteEndElement(); + + writer.WriteStartElement("IsGood"); + writer.WriteString(IsGood.ToString()); + writer.WriteEndElement(); + + writer.WriteStartElement("LocationX"); + writer.WriteString(Location.X.ToString()); + writer.WriteEndElement(); + + writer.WriteStartElement("LocationY"); + writer.WriteString(Location.Y.ToString()); + writer.WriteEndElement(); + + writer.WriteStartElement("OffsetX"); + writer.WriteString(Offset.X.ToString()); + writer.WriteEndElement(); + + writer.WriteStartElement("OffsetY"); + writer.WriteString(Offset.Y.ToString()); + writer.WriteEndElement(); + + writer.WriteStartElement("SizeX"); + writer.WriteString(Size.X.ToString()); + writer.WriteEndElement(); + + writer.WriteStartElement("SizeY"); + writer.WriteString(Size.Y.ToString()); + writer.WriteEndElement(); + } + + public void LoadXML(XmlReader reader) + { + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element && reader.Name == "Editable") + { + Editable = bool.Parse(reader.ReadElementContentAsString()); + } + else if (reader.NodeType == XmlNodeType.Element && reader.Name == "IsGood") + { + IsGood = bool.Parse(reader.ReadElementContentAsString()); + } + else if (reader.NodeType == XmlNodeType.Element && reader.Name == "LocationX") + { + var x = float.Parse(reader.ReadElementContentAsString()); + Location = new Vector2(x, Location.Y); + } + else if (reader.NodeType == XmlNodeType.Element && reader.Name == "LocationY") + { + var y = float.Parse(reader.ReadElementContentAsString()); + Location = new Vector2(Location.X, y); + } + else if (reader.NodeType == XmlNodeType.Element && reader.Name == "OffsetX") + { + var x = float.Parse(reader.ReadElementContentAsString()); + Offset = new Vector2(x, Offset.Y); + } + else if (reader.NodeType == XmlNodeType.Element && reader.Name == "OffsetY") + { + var y = float.Parse(reader.ReadElementContentAsString()); + Offset = new Vector2(Offset.X, y); + } + else if (reader.NodeType == XmlNodeType.Element && reader.Name == "SizeX") + { + var x = float.Parse(reader.ReadElementContentAsString()); + Size = new Vector2(x, Size.Y); + } + else if (reader.NodeType == XmlNodeType.Element && reader.Name == "SizeY") + { + var y = float.Parse(reader.ReadElementContentAsString()); + Size = new Vector2(Size.X, y); + } + else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "RectangleElement") + { + break; + } + } + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleSizeNode.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleSizeNode.cs new file mode 100644 index 0000000..6cb8c3b --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/Rectangle/RectangleSizeNode.cs @@ -0,0 +1,18 @@ + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; + +public class RectangleSizeNode:INode +{ + private readonly RectangleElement _owner; + + public RectangleSizeNode(RectangleElement owner) + { + _owner = owner; + } + + public void SetPosition(Vector2 position) + { + _owner.Size=position-_owner.Location - _owner.Offset; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleAnchorNode.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleAnchorNode.cs new file mode 100644 index 0000000..c6ccb1b --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleAnchorNode.cs @@ -0,0 +1,18 @@ +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.RotatedRectangle; + +public class RotatedRectangleAnchorNode:INode +{ + private readonly RotatedRectangleElement _owner; + + public RotatedRectangleAnchorNode(RotatedRectangleElement owner) + { + _owner = owner; + } + + public void SetPosition(Vector2 position) + { + + _owner.Location=position-_owner.Offset; + + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleElement.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleElement.cs new file mode 100644 index 0000000..4da71d2 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleElement.cs @@ -0,0 +1,123 @@ +using System.Runtime.InteropServices; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; + +using OpenCvSharp; +using Point = OpenCvSharp.Point; + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.RotatedRectangle; + +public class RotatedRectangleElement:IGraphicsElement +{ + public bool Editable { get; set; } + public bool IsGood { get; set; } + + public Vector2 Location { get; set; } + public Vector2 Offset { get; set; } + + public Vector2 Height { get; set; } + public float HalfWidth { get; set; } + + + public void Draw(ICanvas canvas) + { + var type = IsGood ? LineType.Good : LineType.Bad; + var perpendicular = new Vector2(Height.Y, -Height.X); + perpendicular.Normalize(); + + canvas.DrawLine(Offset + Location - perpendicular * HalfWidth, Offset + Location+perpendicular*HalfWidth, type); + canvas.DrawLine(Offset + Location + Height - perpendicular * HalfWidth, Offset + Location + Height + perpendicular * HalfWidth, type); + canvas.DrawLine( + Offset + Location + perpendicular * HalfWidth + , Offset + Location + Height + perpendicular * HalfWidth + , type); + canvas.DrawLine( + Offset + Location - perpendicular * HalfWidth + , Offset + Location + Height - perpendicular * HalfWidth + , type); + canvas.DrawNode(Offset + Location); + canvas.DrawNode(Offset + Location + Height); + canvas.DrawNode(Offset + Location + Height + perpendicular * HalfWidth); + + canvas.DrawArrow(Offset + Location, Height); + + } + + public void FillMat(Mat mat) + { + var perpendicular = new Vector2(Height.Y, -Height.X); + perpendicular.Normalize(); + var p1 = Offset + Location - perpendicular * HalfWidth; + var p2 = Offset + Location + perpendicular * HalfWidth; + var p3 = Offset + Location + Height + perpendicular * HalfWidth; + var p4 = Offset + Location + Height - perpendicular * HalfWidth; + mat.FillPoly(new IEnumerable[] + { + new List() + { + new Point(p1.X,p1.Y), + new Point(p2.X,p2.Y), + new Point(p3.X,p3.Y), + new Point(p4.X,p4.Y), + } + },Scalar.White); + } + + + public INode? GetNode(Vector2 position, float thDistance) + { + if ((Location + Offset - position).Length() < thDistance) + { + return new RotatedRectangleAnchorNode(this); + } + if ((Height + Location + Offset - position).Length() < thDistance) + { + return new RotatedRectangleHeightNode(this); + } + + var perpendicular = new Vector2(Height.Y, -Height.X); + perpendicular.Normalize(); + if (((Offset + Location + Height + perpendicular * HalfWidth) - position).Length() < thDistance) + { + return new RotatedRectangleHalfWidthNode(this); + } + return null; + } + + public void SetPivot(Vector2 pivot) + { + Location += Offset; + Offset = pivot; + Location -= Offset; + } + + public void MovePivot(Vector2 pivot) + { + Offset = pivot; + } + + + + + public void Save(BinaryWriter bw) + { + bw.Write(Editable); + bw.Write(IsGood); + bw.Write(Location.X); + bw.Write(Location.Y); + bw.Write(Offset.X); + bw.Write(Offset.Y); + bw.Write(Height.X); + bw.Write(Height.Y); + bw.Write(HalfWidth); + } + + public void Load(BinaryReader br) + { + Editable = br.ReadBoolean(); + IsGood = br.ReadBoolean(); + Location = new Vector2(br.ReadSingle(), br.ReadSingle()); + Offset = new Vector2(br.ReadSingle(), br.ReadSingle()); + Height = new Vector2(br.ReadSingle(), br.ReadSingle()); + HalfWidth = br.ReadSingle(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleHalfWidthNode.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleHalfWidthNode.cs new file mode 100644 index 0000000..89c1c48 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleHalfWidthNode.cs @@ -0,0 +1,18 @@ + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.RotatedRectangle; + +public class RotatedRectangleHalfWidthNode:INode +{ + private readonly RotatedRectangleElement _owner; + + public RotatedRectangleHalfWidthNode(RotatedRectangleElement owner) + { + _owner = owner; + } + + public void SetPosition(Vector2 position) + { + _owner.HalfWidth = (position - (_owner.Height + _owner.Location + _owner.Offset)).Length(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleHeightNode.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleHeightNode.cs new file mode 100644 index 0000000..eff8e2b --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Elements/RotatedRectangle/RotatedRectangleHeightNode.cs @@ -0,0 +1,16 @@ +namespace Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.RotatedRectangle; + +public class RotatedRectangleHeightNode:INode +{ + private readonly RotatedRectangleElement _owner; + + public RotatedRectangleHeightNode(RotatedRectangleElement owner) + { + _owner = owner; + } + + public void SetPosition(Vector2 position) + { + _owner.Height=position-_owner.Location; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/FilePath.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/FilePath.cs new file mode 100644 index 0000000..979c4c1 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/FilePath.cs @@ -0,0 +1,7 @@ +namespace Hawkeye.VisionBuilder.Workflow.Datatypes; + +public class FilePath +{ + public string Path { get; set; } + public string Format { get; set; } = "Model file(*.h5)|*.h5"; +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/HawkeyeImage.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/HawkeyeImage.cs new file mode 100644 index 0000000..446c05f --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/HawkeyeImage.cs @@ -0,0 +1,10 @@ +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes; + +public class HawkeyeImage +{ + + public Mat ImageData { get; init; } + public string Filename { get; init; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/LutData.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/LutData.cs new file mode 100644 index 0000000..8a6ab60 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/LutData.cs @@ -0,0 +1,68 @@ + + +namespace Hawkeye.VisionBuilder.Workflow.Datatypes; + +public class LutData +{ + public LutData() + { + LUT = new byte[256 * 4]; + + Points = new Vector2[4][]; + for (int i = 0; i < 4; i++) + { + Points[i] = new Vector2[2]; + + Points[i][0] = new Vector2(0, 0); + Points[i][1] = new Vector2(255, 255); + } + + + + for (int i = 0; i < 256; i++) + { + LUT[i] = (byte)i; + LUT[i+256] = (byte)i; + LUT[i+256*2] = (byte)i; + LUT[i+256*3] = (byte)i; + } + } + + public byte[] LUT { get; set; } + public Vector2[][] Points { get; set; } + + public void Serialize(BinaryWriter bw) + { + bw.Write(LUT.Length); + bw.Write(LUT); + bw.Write(Points.Length); + for (int i = 0; i < Points.Length; i++) + { + bw.Write(Points[i].Length); + for (int j = 0; j < Points[i].Length; j++) + { + bw.Write(Points[i][j].X); + bw.Write(Points[i][j].Y); + } + } + } + + public void Deserialize(BinaryReader br) + { + var length = br.ReadInt32(); + LUT = br.ReadBytes(length); + length = br.ReadInt32(); + Points = new Vector2[length][]; + for (int i = 0; i < length; i++) + { + var length2 = br.ReadInt32(); + Points[i] = new Vector2[length2]; + for (int j = 0; j < length2; j++) + { + Points[i][j] = new Vector2(br.ReadSingle(), br.ReadSingle()); + } + } + + } + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/Pattern.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/Pattern.cs new file mode 100644 index 0000000..d75f4b1 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/Pattern.cs @@ -0,0 +1,6 @@ +namespace Hawkeye.VisionBuilder.Workflow.Datatypes; + +public class Pattern +{ + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Datatypes/SelectFromList.cs b/Hawkeye.VisionBuilder.Workflow/Datatypes/SelectFromList.cs new file mode 100644 index 0000000..9e81f10 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Datatypes/SelectFromList.cs @@ -0,0 +1,7 @@ +namespace Hawkeye.VisionBuilder.Workflow.Datatypes; + +public class SelectFromList +{ + public string Value { get; set; } + public Func> GetList { get; set; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Hawkeye.VisionBuilder.Workflow.csproj b/Hawkeye.VisionBuilder.Workflow/Hawkeye.VisionBuilder.Workflow.csproj new file mode 100644 index 0000000..f0fb47d --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Hawkeye.VisionBuilder.Workflow.csproj @@ -0,0 +1,46 @@ + + + + net8.0 + enable + enable + True + x64 + Debug;Release;CPU + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hawkeye.VisionBuilder.Workflow/Links/IHaveEdge.cs b/Hawkeye.VisionBuilder.Workflow/Links/IHaveEdge.cs new file mode 100644 index 0000000..a029074 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Links/IHaveEdge.cs @@ -0,0 +1,8 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Edge; + +namespace Hawkeye.VisionBuilder.Workflow.Links; + +public interface IHaveEdge +{ + public EdgeOrigin EdgeOrigin { get; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Links/IHaveImage.cs b/Hawkeye.VisionBuilder.Workflow/Links/IHaveImage.cs new file mode 100644 index 0000000..be6ca9a --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Links/IHaveImage.cs @@ -0,0 +1,8 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; + +namespace Hawkeye.VisionBuilder.Workflow.Links; + +public interface IHaveImage +{ + public HawkeyeImage Image { get; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Links/IHaveOrigin.cs b/Hawkeye.VisionBuilder.Workflow/Links/IHaveOrigin.cs new file mode 100644 index 0000000..d6a7791 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Links/IHaveOrigin.cs @@ -0,0 +1,12 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; + + +namespace Hawkeye.VisionBuilder.Workflow.Links; + +public interface IHaveOrigin +{ + public Guid Id { get; } + public OriginElement Origin { get; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Links/IHaveSearchArea.cs b/Hawkeye.VisionBuilder.Workflow/Links/IHaveSearchArea.cs new file mode 100644 index 0000000..ccc2e02 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Links/IHaveSearchArea.cs @@ -0,0 +1,8 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; + +namespace Hawkeye.VisionBuilder.Workflow.Links; + +public interface IHaveSearchArea +{ + public RectangleElement SearchArea { get; set; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Links/NoOrigin.cs b/Hawkeye.VisionBuilder.Workflow/Links/NoOrigin.cs new file mode 100644 index 0000000..24b67cb --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Links/NoOrigin.cs @@ -0,0 +1,37 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Edge; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; + + +namespace Hawkeye.VisionBuilder.Workflow.Links; + +public class NoOrigin:BaseOperation,IHaveOrigin,IHaveEdge +{ + private NoOrigin() + { + } + + public static NoOrigin Instance { get; } = new NoOrigin(); + + public Guid Id { get; } = new Guid(); + public OriginElement Origin { get; } = new OriginElement(); + public string Label { get; set; } = "No origin"; + + protected override void InterpretInternal(Context context) + { + throw new NotImplementedException(); + } + + public override void Save(BinaryWriter bw) + { + throw new NotImplementedException(); + } + + public override void Load(BinaryReader br) + { + throw new NotImplementedException(); + } + + public IHaveOrigin Parent { get; set; } + + public EdgeOrigin EdgeOrigin { get; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/MathHelper.cs b/Hawkeye.VisionBuilder.Workflow/MathHelper.cs new file mode 100644 index 0000000..2d4ec9e --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/MathHelper.cs @@ -0,0 +1,314 @@ +namespace Hawkeye.VisionBuilder.Workflow +{ + /// + /// Contains commonly used precalculated values and mathematical operations. + /// + public static class MathHelper + { + /// + /// Represents the mathematical constant e(2.71828175). + /// + public const float E = (float)Math.E; + + /// + /// Represents the log base ten of e(0.4342945). + /// + public const float Log10E = 0.4342945f; + + /// + /// Represents the log base two of e(1.442695). + /// + public const float Log2E = 1.442695f; + + /// + /// Represents the value of pi(3.14159274). + /// + public const float Pi = (float)Math.PI; + + /// + /// Represents the value of pi divided by two(1.57079637). + /// + public const float PiOver2 = (float)(Math.PI / 2.0); + + /// + /// Represents the value of pi divided by four(0.7853982). + /// + public const float PiOver4 = (float)(Math.PI / 4.0); + + /// + /// Represents the value of pi times two(6.28318548). + /// + public const float TwoPi = (float)(Math.PI * 2.0); + + /// + /// Returns the Cartesian coordinate for one axis of a point that is defined by a given triangle and two normalized barycentric (areal) coordinates. + /// + /// The coordinate on one axis of vertex 1 of the defining triangle. + /// The coordinate on the same axis of vertex 2 of the defining triangle. + /// The coordinate on the same axis of vertex 3 of the defining triangle. + /// The normalized barycentric (areal) coordinate b2, equal to the weighting factor for vertex 2, the coordinate of which is specified in value2. + /// The normalized barycentric (areal) coordinate b3, equal to the weighting factor for vertex 3, the coordinate of which is specified in value3. + /// Cartesian coordinate of the specified point with respect to the axis being used. + public static float Barycentric(float value1, float value2, float value3, float amount1, float amount2) + { + return value1 + (value2 - value1) * amount1 + (value3 - value1) * amount2; + } + + /// + /// Performs a Catmull-Rom interpolation using the specified positions. + /// + /// The first position in the interpolation. + /// The second position in the interpolation. + /// The third position in the interpolation. + /// The fourth position in the interpolation. + /// Weighting factor. + /// A position that is the result of the Catmull-Rom interpolation. + public static float CatmullRom(float value1, float value2, float value3, float value4, float amount) + { + // Using formula from http://www.mvps.org/directx/articles/catmull/ + // Internally using doubles not to lose precission + double amountSquared = amount * amount; + double amountCubed = amountSquared * amount; + return (float)(0.5 * (2.0 * value2 + + (value3 - value1) * amount + + (2.0 * value1 - 5.0 * value2 + 4.0 * value3 - value4) * amountSquared + + (3.0 * value2 - value1 - 3.0 * value3 + value4) * amountCubed)); + } + + /// + /// Restricts a value to be within a specified range. + /// + /// The value to clamp. + /// The minimum value. If value is less than min, min will be returned. + /// The maximum value. If value is greater than max, max will be returned. + /// The clamped value. + public static float Clamp(float value, float min, float max) + { + // First we check to see if we're greater than the max + value = (value > max) ? max : value; + + // Then we check to see if we're less than the min. + value = (value < min) ? min : value; + + // There's no check to see if min > max. + return value; + } + + /// + /// Restricts a value to be within a specified range. + /// + /// The value to clamp. + /// The minimum value. If value is less than min, min will be returned. + /// The maximum value. If value is greater than max, max will be returned. + /// The clamped value. + public static int Clamp(int value, int min, int max) + { + value = (value > max) ? max : value; + value = (value < min) ? min : value; + return value; + } + + /// + /// Calculates the absolute value of the difference of two values. + /// + /// Source value. + /// Source value. + /// Distance between the two values. + public static float Distance(float value1, float value2) + { + return Math.Abs(value1 - value2); + } + + /// + /// Performs a Hermite spline interpolation. + /// + /// Source position. + /// Source tangent. + /// Source position. + /// Source tangent. + /// Weighting factor. + /// The result of the Hermite spline interpolation. + public static float Hermite(float value1, float tangent1, float value2, float tangent2, float amount) + { + // All transformed to double not to lose precission + // Otherwise, for high numbers of param:amount the result is NaN instead of Infinity + double v1 = value1, v2 = value2, t1 = tangent1, t2 = tangent2, s = amount, result; + double sCubed = s * s * s; + double sSquared = s * s; + + if (amount == 0f) + result = value1; + else if (amount == 1f) + result = value2; + else + result = (2 * v1 - 2 * v2 + t2 + t1) * sCubed + + (3 * v2 - 3 * v1 - 2 * t1 - t2) * sSquared + + t1 * s + + v1; + return (float)result; + } + + + /// + /// Linearly interpolates between two values. + /// + /// Source value. + /// Destination value. + /// Value between 0 and 1 indicating the weight of value2. + /// Interpolated value. + /// This method performs the linear interpolation based on the following formula: + /// value1 + (value2 - value1) * amount. + /// Passing amount a value of 0 will cause value1 to be returned, a value of 1 will cause value2 to be returned. + /// See for a less efficient version with more precision around edge cases. + /// + public static float Lerp(float value1, float value2, float amount) + { + return value1 + (value2 - value1) * amount; + } + + + /// + /// Linearly interpolates between two values. + /// This method is a less efficient, more precise version of . + /// See remarks for more info. + /// + /// Source value. + /// Destination value. + /// Value between 0 and 1 indicating the weight of value2. + /// Interpolated value. + /// This method performs the linear interpolation based on the following formula: + /// ((1 - amount) * value1) + (value2 * amount). + /// Passing amount a value of 0 will cause value1 to be returned, a value of 1 will cause value2 to be returned. + /// This method does not have the floating point precision issue that has. + /// i.e. If there is a big gap between value1 and value2 in magnitude (e.g. value1=10000000000000000, value2=1), + /// right at the edge of the interpolation range (amount=1), will return 0 (whereas it should return 1). + /// This also holds for value1=10^17, value2=10; value1=10^18,value2=10^2... so on. + /// For an in depth explanation of the issue, see below references: + /// Relevant Wikipedia Article: https://en.wikipedia.org/wiki/Linear_interpolation#Programming_language_support + /// Relevant StackOverflow Answer: http://stackoverflow.com/questions/4353525/floating-point-linear-interpolation#answer-23716956 + /// + public static float LerpPrecise(float value1, float value2, float amount) + { + return ((1 - amount) * value1) + (value2 * amount); + } + + /// + /// Returns the greater of two values. + /// + /// Source value. + /// Source value. + /// The greater value. + public static float Max(float value1, float value2) + { + return value1 > value2 ? value1 : value2; + } + + /// + /// Returns the greater of two values. + /// + /// Source value. + /// Source value. + /// The greater value. + public static int Max(int value1, int value2) + { + return value1 > value2 ? value1 : value2; + } + + /// + /// Returns the lesser of two values. + /// + /// Source value. + /// Source value. + /// The lesser value. + public static float Min(float value1, float value2) + { + return value1 < value2 ? value1 : value2; + } + + /// + /// Returns the lesser of two values. + /// + /// Source value. + /// Source value. + /// The lesser value. + public static int Min(int value1, int value2) + { + return value1 < value2 ? value1 : value2; + } + + /// + /// Interpolates between two values using a cubic equation. + /// + /// Source value. + /// Source value. + /// Weighting value. + /// Interpolated value. + public static float SmoothStep(float value1, float value2, float amount) + { + // It is expected that 0 < amount < 1 + // If amount < 0, return value1 + // If amount > 1, return value2 + float result = MathHelper.Clamp(amount, 0f, 1f); + result = MathHelper.Hermite(value1, 0f, value2, 0f, result); + + return result; + } + + /// + /// Converts radians to degrees. + /// + /// The angle in radians. + /// The angle in degrees. + /// + /// This method uses double precission internally, + /// though it returns single float + /// Factor = 180 / pi + /// + public static float ToDegrees(float radians) + { + return (float)(radians * 57.295779513082320876798154814105); + } + + /// + /// Converts degrees to radians. + /// + /// The angle in degrees. + /// The angle in radians. + /// + /// This method uses double precission internally, + /// though it returns single float + /// Factor = pi / 180 + /// + public static float ToRadians(float degrees) + { + return (float)(degrees * 0.017453292519943295769236907684886); + } + + /// + /// Reduces a given angle to a value between Ï€ and -Ï€. + /// + /// The angle to reduce, in radians. + /// The new angle, in radians. + public static float WrapAngle(float angle) + { + if ((angle > -Pi) && (angle <= Pi)) + return angle; + angle %= TwoPi; + if (angle <= -Pi) + return angle + TwoPi; + if (angle > Pi) + return angle - TwoPi; + return angle; + } + + /// + /// Determines if value is powered by two. + /// + /// A value. + /// true if value is powered by two; otherwise false. + public static bool IsPowerOfTwo(int value) + { + return (value > 0) && ((value & (value - 1)) == 0); + } + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Matrix.cs b/Hawkeye.VisionBuilder.Workflow/Matrix.cs new file mode 100644 index 0000000..c5a18ab --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Matrix.cs @@ -0,0 +1,2352 @@ +namespace Hawkeye.VisionBuilder.Workflow +{ + public struct Matrix : IEquatable + { + #region Public Constructors + + /// + /// Constructs a matrix. + /// + /// A first row and first column value. + /// A first row and second column value. + /// A first row and third column value. + /// A first row and fourth column value. + /// A second row and first column value. + /// A second row and second column value. + /// A second row and third column value. + /// A second row and fourth column value. + /// A third row and first column value. + /// A third row and second column value. + /// A third row and third column value. + /// A third row and fourth column value. + /// A fourth row and first column value. + /// A fourth row and second column value. + /// A fourth row and third column value. + /// A fourth row and fourth column value. + public Matrix(float m11, float m12, float m13, float m14, float m21, float m22, float m23, float m24, float m31, + float m32, float m33, float m34, float m41, float m42, float m43, float m44) + { + this.M11 = m11; + this.M12 = m12; + this.M13 = m13; + this.M14 = m14; + this.M21 = m21; + this.M22 = m22; + this.M23 = m23; + this.M24 = m24; + this.M31 = m31; + this.M32 = m32; + this.M33 = m33; + this.M34 = m34; + this.M41 = m41; + this.M42 = m42; + this.M43 = m43; + this.M44 = m44; + } + + /// + /// Constructs a matrix. + /// + /// A first row of the created matrix. + /// A second row of the created matrix. + /// A third row of the created matrix. + /// A fourth row of the created matrix. + public Matrix(Vector4 row1, Vector4 row2, Vector4 row3, Vector4 row4) + { + this.M11 = row1.X; + this.M12 = row1.Y; + this.M13 = row1.Z; + this.M14 = row1.W; + this.M21 = row2.X; + this.M22 = row2.Y; + this.M23 = row2.Z; + this.M24 = row2.W; + this.M31 = row3.X; + this.M32 = row3.Y; + this.M33 = row3.Z; + this.M34 = row3.W; + this.M41 = row4.X; + this.M42 = row4.Y; + this.M43 = row4.Z; + this.M44 = row4.W; + } + + #endregion + + #region Public Fields + + /// + /// A first row and first column value. + /// + + public float M11; + + /// + /// A first row and second column value. + /// + + public float M12; + + /// + /// A first row and third column value. + /// + + public float M13; + + /// + /// A first row and fourth column value. + /// + + public float M14; + + /// + /// A second row and first column value. + /// + + public float M21; + + /// + /// A second row and second column value. + /// + + public float M22; + + /// + /// A second row and third column value. + /// + + public float M23; + + /// + /// A second row and fourth column value. + /// + + public float M24; + + /// + /// A third row and first column value. + /// + + public float M31; + + /// + /// A third row and second column value. + /// + + public float M32; + + /// + /// A third row and third column value. + /// + + public float M33; + + /// + /// A third row and fourth column value. + /// + + public float M34; + + /// + /// A fourth row and first column value. + /// + + public float M41; + + /// + /// A fourth row and second column value. + /// + + public float M42; + + /// + /// A fourth row and third column value. + /// + + public float M43; + + /// + /// A fourth row and fourth column value. + /// + + public float M44; + + #endregion + + #region Indexers + + public float this[int index] + { + get + { + switch (index) + { + case 0: return M11; + case 1: return M12; + case 2: return M13; + case 3: return M14; + case 4: return M21; + case 5: return M22; + case 6: return M23; + case 7: return M24; + case 8: return M31; + case 9: return M32; + case 10: return M33; + case 11: return M34; + case 12: return M41; + case 13: return M42; + case 14: return M43; + case 15: return M44; + } + throw new ArgumentOutOfRangeException(); + } + + set + { + switch (index) + { + case 0: M11 = value; break; + case 1: M12 = value; break; + case 2: M13 = value; break; + case 3: M14 = value; break; + case 4: M21 = value; break; + case 5: M22 = value; break; + case 6: M23 = value; break; + case 7: M24 = value; break; + case 8: M31 = value; break; + case 9: M32 = value; break; + case 10: M33 = value; break; + case 11: M34 = value; break; + case 12: M41 = value; break; + case 13: M42 = value; break; + case 14: M43 = value; break; + case 15: M44 = value; break; + default: throw new ArgumentOutOfRangeException(); + } + } + } + + public float this[int row, int column] + { + get + { + return this[(row * 4) + column]; + } + + set + { + this[(row * 4) + column] = value; + } + } + + #endregion + + #region Private Members + private static Matrix identity = new Matrix(1f, 0f, 0f, 0f, + 0f, 1f, 0f, 0f, + 0f, 0f, 1f, 0f, + 0f, 0f, 0f, 1f); + #endregion + + #region Public Properties + + /// + /// The backward vector formed from the third row M31, M32, M33 elements. + /// + public Vector3 Backward + { + get + { + return new Vector3(this.M31, this.M32, this.M33); + } + set + { + this.M31 = value.X; + this.M32 = value.Y; + this.M33 = value.Z; + } + } + + /// + /// The down vector formed from the second row -M21, -M22, -M23 elements. + /// + public Vector3 Down + { + get + { + return new Vector3(-this.M21, -this.M22, -this.M23); + } + set + { + this.M21 = -value.X; + this.M22 = -value.Y; + this.M23 = -value.Z; + } + } + + /// + /// The forward vector formed from the third row -M31, -M32, -M33 elements. + /// + public Vector3 Forward + { + get + { + return new Vector3(-this.M31, -this.M32, -this.M33); + } + set + { + this.M31 = -value.X; + this.M32 = -value.Y; + this.M33 = -value.Z; + } + } + + /// + /// Returns the identity matrix. + /// + public static Matrix Identity + { + get { return identity; } + } + + /// + /// The left vector formed from the first row -M11, -M12, -M13 elements. + /// + public Vector3 Left + { + get + { + return new Vector3(-this.M11, -this.M12, -this.M13); + } + set + { + this.M11 = -value.X; + this.M12 = -value.Y; + this.M13 = -value.Z; + } + } + + /// + /// The right vector formed from the first row M11, M12, M13 elements. + /// + public Vector3 Right + { + get + { + return new Vector3(this.M11, this.M12, this.M13); + } + set + { + this.M11 = value.X; + this.M12 = value.Y; + this.M13 = value.Z; + } + } + + /// + /// Position stored in this matrix. + /// + public Vector3 Translation + { + get + { + return new Vector3(this.M41, this.M42, this.M43); + } + set + { + this.M41 = value.X; + this.M42 = value.Y; + this.M43 = value.Z; + } + } + + /// + /// The upper vector formed from the second row M21, M22, M23 elements. + /// + public Vector3 Up + { + get + { + return new Vector3(this.M21, this.M22, this.M23); + } + set + { + this.M21 = value.X; + this.M22 = value.Y; + this.M23 = value.Z; + } + } + #endregion + + #region Public Methods + + /// + /// Creates a new which contains sum of two matrixes. + /// + /// The first matrix to add. + /// The second matrix to add. + /// The result of the matrix addition. + public static Matrix Add(Matrix matrix1, Matrix matrix2) + { + matrix1.M11 += matrix2.M11; + matrix1.M12 += matrix2.M12; + matrix1.M13 += matrix2.M13; + matrix1.M14 += matrix2.M14; + matrix1.M21 += matrix2.M21; + matrix1.M22 += matrix2.M22; + matrix1.M23 += matrix2.M23; + matrix1.M24 += matrix2.M24; + matrix1.M31 += matrix2.M31; + matrix1.M32 += matrix2.M32; + matrix1.M33 += matrix2.M33; + matrix1.M34 += matrix2.M34; + matrix1.M41 += matrix2.M41; + matrix1.M42 += matrix2.M42; + matrix1.M43 += matrix2.M43; + matrix1.M44 += matrix2.M44; + return matrix1; + } + + /// + /// Creates a new which contains sum of two matrixes. + /// + /// The first matrix to add. + /// The second matrix to add. + /// The result of the matrix addition as an output parameter. + public static void Add(ref Matrix matrix1, ref Matrix matrix2, out Matrix result) + { + result.M11 = matrix1.M11 + matrix2.M11; + result.M12 = matrix1.M12 + matrix2.M12; + result.M13 = matrix1.M13 + matrix2.M13; + result.M14 = matrix1.M14 + matrix2.M14; + result.M21 = matrix1.M21 + matrix2.M21; + result.M22 = matrix1.M22 + matrix2.M22; + result.M23 = matrix1.M23 + matrix2.M23; + result.M24 = matrix1.M24 + matrix2.M24; + result.M31 = matrix1.M31 + matrix2.M31; + result.M32 = matrix1.M32 + matrix2.M32; + result.M33 = matrix1.M33 + matrix2.M33; + result.M34 = matrix1.M34 + matrix2.M34; + result.M41 = matrix1.M41 + matrix2.M41; + result.M42 = matrix1.M42 + matrix2.M42; + result.M43 = matrix1.M43 + matrix2.M43; + result.M44 = matrix1.M44 + matrix2.M44; + + } + + /// + /// Creates a new for spherical billboarding that rotates around specified object position. + /// + /// Position of billboard object. It will rotate around that vector. + /// The camera position. + /// The camera up vector. + /// Optional camera forward vector. + /// The for spherical billboarding. + public static Matrix CreateBillboard(Vector3 objectPosition, Vector3 cameraPosition, + Vector3 cameraUpVector, Nullable cameraForwardVector) + { + Matrix result; + + // Delegate to the other overload of the function to do the work + CreateBillboard(ref objectPosition, ref cameraPosition, ref cameraUpVector, cameraForwardVector, out result); + + return result; + } + + /// + /// Creates a new for spherical billboarding that rotates around specified object position. + /// + /// Position of billboard object. It will rotate around that vector. + /// The camera position. + /// The camera up vector. + /// Optional camera forward vector. + /// The for spherical billboarding as an output parameter. + public static void CreateBillboard(ref Vector3 objectPosition, ref Vector3 cameraPosition, + ref Vector3 cameraUpVector, Vector3? cameraForwardVector, out Matrix result) + { + Vector3 vector; + Vector3 vector2; + Vector3 vector3; + vector.X = objectPosition.X - cameraPosition.X; + vector.Y = objectPosition.Y - cameraPosition.Y; + vector.Z = objectPosition.Z - cameraPosition.Z; + float num = vector.LengthSquared(); + if (num < 0.0001f) + { + vector = cameraForwardVector.HasValue ? -cameraForwardVector.Value : Vector3.Forward; + } + else + { + Vector3.Multiply(ref vector, (float)(1f / ((float)Math.Sqrt((double)num))), out vector); + } + Vector3.Cross(ref cameraUpVector, ref vector, out vector3); + vector3.Normalize(); + Vector3.Cross(ref vector, ref vector3, out vector2); + result.M11 = vector3.X; + result.M12 = vector3.Y; + result.M13 = vector3.Z; + result.M14 = 0; + result.M21 = vector2.X; + result.M22 = vector2.Y; + result.M23 = vector2.Z; + result.M24 = 0; + result.M31 = vector.X; + result.M32 = vector.Y; + result.M33 = vector.Z; + result.M34 = 0; + result.M41 = objectPosition.X; + result.M42 = objectPosition.Y; + result.M43 = objectPosition.Z; + result.M44 = 1; + } + + /// + /// Creates a new for cylindrical billboarding that rotates around specified axis. + /// + /// Object position the billboard will rotate around. + /// Camera position. + /// Axis of billboard for rotation. + /// Optional camera forward vector. + /// Optional object forward vector. + /// The for cylindrical billboarding. + public static Matrix CreateConstrainedBillboard(Vector3 objectPosition, Vector3 cameraPosition, + Vector3 rotateAxis, Nullable cameraForwardVector, Nullable objectForwardVector) + { + Matrix result; + CreateConstrainedBillboard(ref objectPosition, ref cameraPosition, ref rotateAxis, + cameraForwardVector, objectForwardVector, out result); + return result; + } + + /// + /// Creates a new for cylindrical billboarding that rotates around specified axis. + /// + /// Object position the billboard will rotate around. + /// Camera position. + /// Axis of billboard for rotation. + /// Optional camera forward vector. + /// Optional object forward vector. + /// The for cylindrical billboarding as an output parameter. + public static void CreateConstrainedBillboard(ref Vector3 objectPosition, ref Vector3 cameraPosition, + ref Vector3 rotateAxis, Vector3? cameraForwardVector, Vector3? objectForwardVector, out Matrix result) + { + float num; + Vector3 vector; + Vector3 vector2; + Vector3 vector3; + vector2.X = objectPosition.X - cameraPosition.X; + vector2.Y = objectPosition.Y - cameraPosition.Y; + vector2.Z = objectPosition.Z - cameraPosition.Z; + float num2 = vector2.LengthSquared(); + if (num2 < 0.0001f) + { + vector2 = cameraForwardVector.HasValue ? -cameraForwardVector.Value : Vector3.Forward; + } + else + { + Vector3.Multiply(ref vector2, (float)(1f / ((float)Math.Sqrt((double)num2))), out vector2); + } + Vector3 vector4 = rotateAxis; + Vector3.Dot(ref rotateAxis, ref vector2, out num); + if (Math.Abs(num) > 0.9982547f) + { + if (objectForwardVector.HasValue) + { + vector = objectForwardVector.Value; + Vector3.Dot(ref rotateAxis, ref vector, out num); + if (Math.Abs(num) > 0.9982547f) + { + num = ((rotateAxis.X * Vector3.Forward.X) + (rotateAxis.Y * Vector3.Forward.Y)) + (rotateAxis.Z * Vector3.Forward.Z); + vector = (Math.Abs(num) > 0.9982547f) ? Vector3.Right : Vector3.Forward; + } + } + else + { + num = ((rotateAxis.X * Vector3.Forward.X) + (rotateAxis.Y * Vector3.Forward.Y)) + (rotateAxis.Z * Vector3.Forward.Z); + vector = (Math.Abs(num) > 0.9982547f) ? Vector3.Right : Vector3.Forward; + } + Vector3.Cross(ref rotateAxis, ref vector, out vector3); + vector3.Normalize(); + Vector3.Cross(ref vector3, ref rotateAxis, out vector); + vector.Normalize(); + } + else + { + Vector3.Cross(ref rotateAxis, ref vector2, out vector3); + vector3.Normalize(); + Vector3.Cross(ref vector3, ref vector4, out vector); + vector.Normalize(); + } + result.M11 = vector3.X; + result.M12 = vector3.Y; + result.M13 = vector3.Z; + result.M14 = 0; + result.M21 = vector4.X; + result.M22 = vector4.Y; + result.M23 = vector4.Z; + result.M24 = 0; + result.M31 = vector.X; + result.M32 = vector.Y; + result.M33 = vector.Z; + result.M34 = 0; + result.M41 = objectPosition.X; + result.M42 = objectPosition.Y; + result.M43 = objectPosition.Z; + result.M44 = 1; + + } + + /// + /// Creates a new which contains the rotation moment around specified axis. + /// + /// The axis of rotation. + /// The angle of rotation in radians. + /// The rotation . + public static Matrix CreateFromAxisAngle(Vector3 axis, float angle) + { + Matrix result; + CreateFromAxisAngle(ref axis, angle, out result); + return result; + } + + /// + /// Creates a new which contains the rotation moment around specified axis. + /// + /// The axis of rotation. + /// The angle of rotation in radians. + /// The rotation as an output parameter. + public static void CreateFromAxisAngle(ref Vector3 axis, float angle, out Matrix result) + { + float x = axis.X; + float y = axis.Y; + float z = axis.Z; + float num2 = (float)Math.Sin((double)angle); + float num = (float)Math.Cos((double)angle); + float num11 = x * x; + float num10 = y * y; + float num9 = z * z; + float num8 = x * y; + float num7 = x * z; + float num6 = y * z; + result.M11 = num11 + (num * (1f - num11)); + result.M12 = (num8 - (num * num8)) + (num2 * z); + result.M13 = (num7 - (num * num7)) - (num2 * y); + result.M14 = 0; + result.M21 = (num8 - (num * num8)) - (num2 * z); + result.M22 = num10 + (num * (1f - num10)); + result.M23 = (num6 - (num * num6)) + (num2 * x); + result.M24 = 0; + result.M31 = (num7 - (num * num7)) + (num2 * y); + result.M32 = (num6 - (num * num6)) - (num2 * x); + result.M33 = num9 + (num * (1f - num9)); + result.M34 = 0; + result.M41 = 0; + result.M42 = 0; + result.M43 = 0; + result.M44 = 1; + } + + /// + /// Creates a new rotation from a . + /// + /// of rotation moment. + /// The rotation . + public static Matrix CreateFromQuaternion(Quaternion quaternion) + { + Matrix result; + CreateFromQuaternion(ref quaternion, out result); + return result; + } + + /// + /// Creates a new rotation from a . + /// + /// of rotation moment. + /// The rotation as an output parameter. + public static void CreateFromQuaternion(ref Quaternion quaternion, out Matrix result) + { + float num9 = quaternion.X * quaternion.X; + float num8 = quaternion.Y * quaternion.Y; + float num7 = quaternion.Z * quaternion.Z; + float num6 = quaternion.X * quaternion.Y; + float num5 = quaternion.Z * quaternion.W; + float num4 = quaternion.Z * quaternion.X; + float num3 = quaternion.Y * quaternion.W; + float num2 = quaternion.Y * quaternion.Z; + float num = quaternion.X * quaternion.W; + result.M11 = 1f - (2f * (num8 + num7)); + result.M12 = 2f * (num6 + num5); + result.M13 = 2f * (num4 - num3); + result.M14 = 0f; + result.M21 = 2f * (num6 - num5); + result.M22 = 1f - (2f * (num7 + num9)); + result.M23 = 2f * (num2 + num); + result.M24 = 0f; + result.M31 = 2f * (num4 + num3); + result.M32 = 2f * (num2 - num); + result.M33 = 1f - (2f * (num8 + num9)); + result.M34 = 0f; + result.M41 = 0f; + result.M42 = 0f; + result.M43 = 0f; + result.M44 = 1f; + } + + /// + /// Creates a new rotation from the specified yaw, pitch and roll values. + /// + /// The yaw rotation value in radians. + /// The pitch rotation value in radians. + /// The roll rotation value in radians. + /// The rotation . + /// For more information about yaw, pitch and roll visit http://en.wikipedia.org/wiki/Euler_angles. + /// + public static Matrix CreateFromYawPitchRoll(float yaw, float pitch, float roll) + { + Matrix matrix; + CreateFromYawPitchRoll(yaw, pitch, roll, out matrix); + return matrix; + } + + /// + /// Creates a new rotation from the specified yaw, pitch and roll values. + /// + /// The yaw rotation value in radians. + /// The pitch rotation value in radians. + /// The roll rotation value in radians. + /// The rotation as an output parameter. + /// For more information about yaw, pitch and roll visit http://en.wikipedia.org/wiki/Euler_angles. + /// + public static void CreateFromYawPitchRoll(float yaw, float pitch, float roll, out Matrix result) + { + Quaternion quaternion; + Quaternion.CreateFromYawPitchRoll(yaw, pitch, roll, out quaternion); + CreateFromQuaternion(ref quaternion, out result); + } + + /// + /// Creates a new viewing . + /// + /// Position of the camera. + /// Lookup vector of the camera. + /// The direction of the upper edge of the camera. + /// The viewing . + public static Matrix CreateLookAt(Vector3 cameraPosition, Vector3 cameraTarget, Vector3 cameraUpVector) + { + Matrix matrix; + CreateLookAt(ref cameraPosition, ref cameraTarget, ref cameraUpVector, out matrix); + return matrix; + } + + /// + /// Creates a new viewing . + /// + /// Position of the camera. + /// Lookup vector of the camera. + /// The direction of the upper edge of the camera. + /// The viewing as an output parameter. + public static void CreateLookAt(ref Vector3 cameraPosition, ref Vector3 cameraTarget, ref Vector3 cameraUpVector, out Matrix result) + { + var vector = Vector3.Normalize(cameraPosition - cameraTarget); + var vector2 = Vector3.Normalize(Vector3.Cross(cameraUpVector, vector)); + var vector3 = Vector3.Cross(vector, vector2); + result.M11 = vector2.X; + result.M12 = vector3.X; + result.M13 = vector.X; + result.M14 = 0f; + result.M21 = vector2.Y; + result.M22 = vector3.Y; + result.M23 = vector.Y; + result.M24 = 0f; + result.M31 = vector2.Z; + result.M32 = vector3.Z; + result.M33 = vector.Z; + result.M34 = 0f; + result.M41 = -Vector3.Dot(vector2, cameraPosition); + result.M42 = -Vector3.Dot(vector3, cameraPosition); + result.M43 = -Vector3.Dot(vector, cameraPosition); + result.M44 = 1f; + } + + /// + /// Creates a new projection for orthographic view. + /// + /// Width of the viewing volume. + /// Height of the viewing volume. + /// Depth of the near plane. + /// Depth of the far plane. + /// The new projection for orthographic view. + public static Matrix CreateOrthographic(float width, float height, float zNearPlane, float zFarPlane) + { + Matrix matrix; + CreateOrthographic(width, height, zNearPlane, zFarPlane, out matrix); + return matrix; + } + + /// + /// Creates a new projection for orthographic view. + /// + /// Width of the viewing volume. + /// Height of the viewing volume. + /// Depth of the near plane. + /// Depth of the far plane. + /// The new projection for orthographic view as an output parameter. + public static void CreateOrthographic(float width, float height, float zNearPlane, float zFarPlane, out Matrix result) + { + result.M11 = 2f / width; + result.M12 = result.M13 = result.M14 = 0f; + result.M22 = 2f / height; + result.M21 = result.M23 = result.M24 = 0f; + result.M33 = 1f / (zNearPlane - zFarPlane); + result.M31 = result.M32 = result.M34 = 0f; + result.M41 = result.M42 = 0f; + result.M43 = zNearPlane / (zNearPlane - zFarPlane); + result.M44 = 1f; + } + + /// + /// Creates a new projection for customized orthographic view. + /// + /// Lower x-value at the near plane. + /// Upper x-value at the near plane. + /// Lower y-coordinate at the near plane. + /// Upper y-value at the near plane. + /// Depth of the near plane. + /// Depth of the far plane. + /// The new projection for customized orthographic view. + public static Matrix CreateOrthographicOffCenter(float left, float right, float bottom, float top, float zNearPlane, float zFarPlane) + { + Matrix matrix; + CreateOrthographicOffCenter(left, right, bottom, top, zNearPlane, zFarPlane, out matrix); + return matrix; + } + + + + /// + /// Creates a new projection for customized orthographic view. + /// + /// Lower x-value at the near plane. + /// Upper x-value at the near plane. + /// Lower y-coordinate at the near plane. + /// Upper y-value at the near plane. + /// Depth of the near plane. + /// Depth of the far plane. + /// The new projection for customized orthographic view as an output parameter. + public static void CreateOrthographicOffCenter(float left, float right, float bottom, float top, float zNearPlane, float zFarPlane, out Matrix result) + { + result.M11 = (float)(2.0 / ((double)right - (double)left)); + result.M12 = 0.0f; + result.M13 = 0.0f; + result.M14 = 0.0f; + result.M21 = 0.0f; + result.M22 = (float)(2.0 / ((double)top - (double)bottom)); + result.M23 = 0.0f; + result.M24 = 0.0f; + result.M31 = 0.0f; + result.M32 = 0.0f; + result.M33 = (float)(1.0 / ((double)zNearPlane - (double)zFarPlane)); + result.M34 = 0.0f; + result.M41 = (float)(((double)left + (double)right) / ((double)left - (double)right)); + result.M42 = (float)(((double)top + (double)bottom) / ((double)bottom - (double)top)); + result.M43 = (float)((double)zNearPlane / ((double)zNearPlane - (double)zFarPlane)); + result.M44 = 1.0f; + } + + /// + /// Creates a new projection for perspective view. + /// + /// Width of the viewing volume. + /// Height of the viewing volume. + /// Distance to the near plane. + /// Distance to the far plane. + /// The new projection for perspective view. + public static Matrix CreatePerspective(float width, float height, float nearPlaneDistance, float farPlaneDistance) + { + Matrix matrix; + CreatePerspective(width, height, nearPlaneDistance, farPlaneDistance, out matrix); + return matrix; + } + + /// + /// Creates a new projection for perspective view. + /// + /// Width of the viewing volume. + /// Height of the viewing volume. + /// Distance to the near plane. + /// Distance to the far plane. + /// The new projection for perspective view as an output parameter. + public static void CreatePerspective(float width, float height, float nearPlaneDistance, float farPlaneDistance, out Matrix result) + { + if (nearPlaneDistance <= 0f) + { + throw new ArgumentException("nearPlaneDistance <= 0"); + } + if (farPlaneDistance <= 0f) + { + throw new ArgumentException("farPlaneDistance <= 0"); + } + if (nearPlaneDistance >= farPlaneDistance) + { + throw new ArgumentException("nearPlaneDistance >= farPlaneDistance"); + } + result.M11 = (2f * nearPlaneDistance) / width; + result.M12 = result.M13 = result.M14 = 0f; + result.M22 = (2f * nearPlaneDistance) / height; + result.M21 = result.M23 = result.M24 = 0f; + result.M33 = farPlaneDistance / (nearPlaneDistance - farPlaneDistance); + result.M31 = result.M32 = 0f; + result.M34 = -1f; + result.M41 = result.M42 = result.M44 = 0f; + result.M43 = (nearPlaneDistance * farPlaneDistance) / (nearPlaneDistance - farPlaneDistance); + } + + /// + /// Creates a new projection for perspective view with field of view. + /// + /// Field of view in the y direction in radians. + /// Width divided by height of the viewing volume. + /// Distance to the near plane. + /// Distance to the far plane. + /// The new projection for perspective view with FOV. + public static Matrix CreatePerspectiveFieldOfView(float fieldOfView, float aspectRatio, float nearPlaneDistance, float farPlaneDistance) + { + Matrix result; + CreatePerspectiveFieldOfView(fieldOfView, aspectRatio, nearPlaneDistance, farPlaneDistance, out result); + return result; + } + + /// + /// Creates a new projection for perspective view with field of view. + /// + /// Field of view in the y direction in radians. + /// Width divided by height of the viewing volume. + /// Distance of the near plane. + /// Distance of the far plane. + /// The new projection for perspective view with FOV as an output parameter. + public static void CreatePerspectiveFieldOfView(float fieldOfView, float aspectRatio, float nearPlaneDistance, float farPlaneDistance, out Matrix result) + { + if ((fieldOfView <= 0f) || (fieldOfView >= 3.141593f)) + { + throw new ArgumentException("fieldOfView <= 0 or >= PI"); + } + if (nearPlaneDistance <= 0f) + { + throw new ArgumentException("nearPlaneDistance <= 0"); + } + if (farPlaneDistance <= 0f) + { + throw new ArgumentException("farPlaneDistance <= 0"); + } + if (nearPlaneDistance >= farPlaneDistance) + { + throw new ArgumentException("nearPlaneDistance >= farPlaneDistance"); + } + float num = 1f / ((float)Math.Tan((double)(fieldOfView * 0.5f))); + float num9 = num / aspectRatio; + result.M11 = num9; + result.M12 = result.M13 = result.M14 = 0; + result.M22 = num; + result.M21 = result.M23 = result.M24 = 0; + result.M31 = result.M32 = 0f; + result.M33 = farPlaneDistance / (nearPlaneDistance - farPlaneDistance); + result.M34 = -1; + result.M41 = result.M42 = result.M44 = 0; + result.M43 = (nearPlaneDistance * farPlaneDistance) / (nearPlaneDistance - farPlaneDistance); + } + + /// + /// Creates a new projection for customized perspective view. + /// + /// Lower x-value at the near plane. + /// Upper x-value at the near plane. + /// Lower y-coordinate at the near plane. + /// Upper y-value at the near plane. + /// Distance to the near plane. + /// Distance to the far plane. + /// The new for customized perspective view. + public static Matrix CreatePerspectiveOffCenter(float left, float right, float bottom, float top, float nearPlaneDistance, float farPlaneDistance) + { + Matrix result; + CreatePerspectiveOffCenter(left, right, bottom, top, nearPlaneDistance, farPlaneDistance, out result); + return result; + } + + + /// + /// Creates a new projection for customized perspective view. + /// + /// Lower x-value at the near plane. + /// Upper x-value at the near plane. + /// Lower y-coordinate at the near plane. + /// Upper y-value at the near plane. + /// Distance to the near plane. + /// Distance to the far plane. + /// The new for customized perspective view as an output parameter. + public static void CreatePerspectiveOffCenter(float left, float right, float bottom, float top, float nearPlaneDistance, float farPlaneDistance, out Matrix result) + { + if (nearPlaneDistance <= 0f) + { + throw new ArgumentException("nearPlaneDistance <= 0"); + } + if (farPlaneDistance <= 0f) + { + throw new ArgumentException("farPlaneDistance <= 0"); + } + if (nearPlaneDistance >= farPlaneDistance) + { + throw new ArgumentException("nearPlaneDistance >= farPlaneDistance"); + } + result.M11 = (2f * nearPlaneDistance) / (right - left); + result.M12 = result.M13 = result.M14 = 0; + result.M22 = (2f * nearPlaneDistance) / (top - bottom); + result.M21 = result.M23 = result.M24 = 0; + result.M31 = (left + right) / (right - left); + result.M32 = (top + bottom) / (top - bottom); + result.M33 = farPlaneDistance / (nearPlaneDistance - farPlaneDistance); + result.M34 = -1; + result.M43 = (nearPlaneDistance * farPlaneDistance) / (nearPlaneDistance - farPlaneDistance); + result.M41 = result.M42 = result.M44 = 0; + } + + /// + /// Creates a new rotation around X axis. + /// + /// Angle in radians. + /// The rotation around X axis. + public static Matrix CreateRotationX(float radians) + { + Matrix result; + CreateRotationX(radians, out result); + return result; + } + + /// + /// Creates a new rotation around X axis. + /// + /// Angle in radians. + /// The rotation around X axis as an output parameter. + public static void CreateRotationX(float radians, out Matrix result) + { + result = Matrix.Identity; + + var val1 = (float)Math.Cos(radians); + var val2 = (float)Math.Sin(radians); + + result.M22 = val1; + result.M23 = val2; + result.M32 = -val2; + result.M33 = val1; + } + + /// + /// Creates a new rotation around Y axis. + /// + /// Angle in radians. + /// The rotation around Y axis. + public static Matrix CreateRotationY(float radians) + { + Matrix result; + CreateRotationY(radians, out result); + return result; + } + + /// + /// Creates a new rotation around Y axis. + /// + /// Angle in radians. + /// The rotation around Y axis as an output parameter. + public static void CreateRotationY(float radians, out Matrix result) + { + result = Matrix.Identity; + + var val1 = (float)Math.Cos(radians); + var val2 = (float)Math.Sin(radians); + + result.M11 = val1; + result.M13 = -val2; + result.M31 = val2; + result.M33 = val1; + } + + /// + /// Creates a new rotation around Z axis. + /// + /// Angle in radians. + /// The rotation around Z axis. + public static Matrix CreateRotationZ(float radians) + { + Matrix result; + CreateRotationZ(radians, out result); + return result; + } + + /// + /// Creates a new rotation around Z axis. + /// + /// Angle in radians. + /// The rotation around Z axis as an output parameter. + public static void CreateRotationZ(float radians, out Matrix result) + { + result = Matrix.Identity; + + var val1 = (float)Math.Cos(radians); + var val2 = (float)Math.Sin(radians); + + result.M11 = val1; + result.M12 = val2; + result.M21 = -val2; + result.M22 = val1; + } + + /// + /// Creates a new scaling . + /// + /// Scale value for all three axises. + /// The scaling . + public static Matrix CreateScale(float scale) + { + Matrix result; + CreateScale(scale, scale, scale, out result); + return result; + } + + /// + /// Creates a new scaling . + /// + /// Scale value for all three axises. + /// The scaling as an output parameter. + public static void CreateScale(float scale, out Matrix result) + { + CreateScale(scale, scale, scale, out result); + } + + /// + /// Creates a new scaling . + /// + /// Scale value for X axis. + /// Scale value for Y axis. + /// Scale value for Z axis. + /// The scaling . + public static Matrix CreateScale(float xScale, float yScale, float zScale) + { + Matrix result; + CreateScale(xScale, yScale, zScale, out result); + return result; + } + + /// + /// Creates a new scaling . + /// + /// Scale value for X axis. + /// Scale value for Y axis. + /// Scale value for Z axis. + /// The scaling as an output parameter. + public static void CreateScale(float xScale, float yScale, float zScale, out Matrix result) + { + result.M11 = xScale; + result.M12 = 0; + result.M13 = 0; + result.M14 = 0; + result.M21 = 0; + result.M22 = yScale; + result.M23 = 0; + result.M24 = 0; + result.M31 = 0; + result.M32 = 0; + result.M33 = zScale; + result.M34 = 0; + result.M41 = 0; + result.M42 = 0; + result.M43 = 0; + result.M44 = 1; + } + + /// + /// Creates a new scaling . + /// + /// representing x,y and z scale values. + /// The scaling . + public static Matrix CreateScale(Vector3 scales) + { + Matrix result; + CreateScale(ref scales, out result); + return result; + } + + /// + /// Creates a new scaling . + /// + /// representing x,y and z scale values. + /// The scaling as an output parameter. + public static void CreateScale(ref Vector3 scales, out Matrix result) + { + result.M11 = scales.X; + result.M12 = 0; + result.M13 = 0; + result.M14 = 0; + result.M21 = 0; + result.M22 = scales.Y; + result.M23 = 0; + result.M24 = 0; + result.M31 = 0; + result.M32 = 0; + result.M33 = scales.Z; + result.M34 = 0; + result.M41 = 0; + result.M42 = 0; + result.M43 = 0; + result.M44 = 1; + } + + + + + + + /// + /// Creates a new translation . + /// + /// X coordinate of translation. + /// Y coordinate of translation. + /// Z coordinate of translation. + /// The translation . + public static Matrix CreateTranslation(float xPosition, float yPosition, float zPosition) + { + Matrix result; + CreateTranslation(xPosition, yPosition, zPosition, out result); + return result; + } + + /// + /// Creates a new translation . + /// + /// X,Y and Z coordinates of translation. + /// The translation as an output parameter. + public static void CreateTranslation(ref Vector3 position, out Matrix result) + { + result.M11 = 1; + result.M12 = 0; + result.M13 = 0; + result.M14 = 0; + result.M21 = 0; + result.M22 = 1; + result.M23 = 0; + result.M24 = 0; + result.M31 = 0; + result.M32 = 0; + result.M33 = 1; + result.M34 = 0; + result.M41 = position.X; + result.M42 = position.Y; + result.M43 = position.Z; + result.M44 = 1; + } + + /// + /// Creates a new translation . + /// + /// X,Y and Z coordinates of translation. + /// The translation . + public static Matrix CreateTranslation(Vector3 position) + { + Matrix result; + CreateTranslation(ref position, out result); + return result; + } + + /// + /// Creates a new translation . + /// + /// X coordinate of translation. + /// Y coordinate of translation. + /// Z coordinate of translation. + /// The translation as an output parameter. + public static void CreateTranslation(float xPosition, float yPosition, float zPosition, out Matrix result) + { + result.M11 = 1; + result.M12 = 0; + result.M13 = 0; + result.M14 = 0; + result.M21 = 0; + result.M22 = 1; + result.M23 = 0; + result.M24 = 0; + result.M31 = 0; + result.M32 = 0; + result.M33 = 1; + result.M34 = 0; + result.M41 = xPosition; + result.M42 = yPosition; + result.M43 = zPosition; + result.M44 = 1; + } + + + + /// + /// Creates a new world . + /// + /// The position vector. + /// The forward direction vector. + /// The upward direction vector. Usually . + /// The world . + public static Matrix CreateWorld(Vector3 position, Vector3 forward, Vector3 up) + { + Matrix ret; + CreateWorld(ref position, ref forward, ref up, out ret); + return ret; + } + + /// + /// Creates a new world . + /// + /// The position vector. + /// The forward direction vector. + /// The upward direction vector. Usually . + /// The world as an output parameter. + public static void CreateWorld(ref Vector3 position, ref Vector3 forward, ref Vector3 up, out Matrix result) + { + Vector3 x, y, z; + Vector3.Normalize(ref forward, out z); + Vector3.Cross(ref forward, ref up, out x); + Vector3.Cross(ref x, ref forward, out y); + x.Normalize(); + y.Normalize(); + + result = new Matrix(); + result.Right = x; + result.Up = y; + result.Forward = z; + result.Translation = position; + result.M44 = 1f; + } + + /// + /// Decomposes this matrix to translation, rotation and scale elements. Returns true if matrix can be decomposed; false otherwise. + /// + /// Scale vector as an output parameter. + /// Rotation quaternion as an output parameter. + /// Translation vector as an output parameter. + /// true if matrix can be decomposed; false otherwise. + public bool Decompose(out Vector3 scale, out Quaternion rotation, out Vector3 translation) + { + translation.X = this.M41; + translation.Y = this.M42; + translation.Z = this.M43; + + float xs = (Math.Sign(M11 * M12 * M13 * M14) < 0) ? -1 : 1; + float ys = (Math.Sign(M21 * M22 * M23 * M24) < 0) ? -1 : 1; + float zs = (Math.Sign(M31 * M32 * M33 * M34) < 0) ? -1 : 1; + + scale.X = xs * (float)Math.Sqrt(this.M11 * this.M11 + this.M12 * this.M12 + this.M13 * this.M13); + scale.Y = ys * (float)Math.Sqrt(this.M21 * this.M21 + this.M22 * this.M22 + this.M23 * this.M23); + scale.Z = zs * (float)Math.Sqrt(this.M31 * this.M31 + this.M32 * this.M32 + this.M33 * this.M33); + + if (scale.X == 0.0 || scale.Y == 0.0 || scale.Z == 0.0) + { + rotation = Quaternion.Identity; + return false; + } + + Matrix m1 = new Matrix(this.M11 / scale.X, M12 / scale.X, M13 / scale.X, 0, + this.M21 / scale.Y, M22 / scale.Y, M23 / scale.Y, 0, + this.M31 / scale.Z, M32 / scale.Z, M33 / scale.Z, 0, + 0, 0, 0, 1); + + rotation = Quaternion.CreateFromRotationMatrix(m1); + return true; + } + + /// + /// Returns a determinant of this . + /// + /// Determinant of this + /// See more about determinant here - http://en.wikipedia.org/wiki/Determinant. + /// + public float Determinant() + { + float num22 = this.M11; + float num21 = this.M12; + float num20 = this.M13; + float num19 = this.M14; + float num12 = this.M21; + float num11 = this.M22; + float num10 = this.M23; + float num9 = this.M24; + float num8 = this.M31; + float num7 = this.M32; + float num6 = this.M33; + float num5 = this.M34; + float num4 = this.M41; + float num3 = this.M42; + float num2 = this.M43; + float num = this.M44; + float num18 = (num6 * num) - (num5 * num2); + float num17 = (num7 * num) - (num5 * num3); + float num16 = (num7 * num2) - (num6 * num3); + float num15 = (num8 * num) - (num5 * num4); + float num14 = (num8 * num2) - (num6 * num4); + float num13 = (num8 * num3) - (num7 * num4); + return ((((num22 * (((num11 * num18) - (num10 * num17)) + (num9 * num16))) - (num21 * (((num12 * num18) - (num10 * num15)) + (num9 * num14)))) + (num20 * (((num12 * num17) - (num11 * num15)) + (num9 * num13)))) - (num19 * (((num12 * num16) - (num11 * num14)) + (num10 * num13)))); + } + + /// + /// Divides the elements of a by the elements of another matrix. + /// + /// Source . + /// Divisor . + /// The result of dividing the matrix. + public static Matrix Divide(Matrix matrix1, Matrix matrix2) + { + matrix1.M11 = matrix1.M11 / matrix2.M11; + matrix1.M12 = matrix1.M12 / matrix2.M12; + matrix1.M13 = matrix1.M13 / matrix2.M13; + matrix1.M14 = matrix1.M14 / matrix2.M14; + matrix1.M21 = matrix1.M21 / matrix2.M21; + matrix1.M22 = matrix1.M22 / matrix2.M22; + matrix1.M23 = matrix1.M23 / matrix2.M23; + matrix1.M24 = matrix1.M24 / matrix2.M24; + matrix1.M31 = matrix1.M31 / matrix2.M31; + matrix1.M32 = matrix1.M32 / matrix2.M32; + matrix1.M33 = matrix1.M33 / matrix2.M33; + matrix1.M34 = matrix1.M34 / matrix2.M34; + matrix1.M41 = matrix1.M41 / matrix2.M41; + matrix1.M42 = matrix1.M42 / matrix2.M42; + matrix1.M43 = matrix1.M43 / matrix2.M43; + matrix1.M44 = matrix1.M44 / matrix2.M44; + return matrix1; + } + + /// + /// Divides the elements of a by the elements of another matrix. + /// + /// Source . + /// Divisor . + /// The result of dividing the matrix as an output parameter. + public static void Divide(ref Matrix matrix1, ref Matrix matrix2, out Matrix result) + { + result.M11 = matrix1.M11 / matrix2.M11; + result.M12 = matrix1.M12 / matrix2.M12; + result.M13 = matrix1.M13 / matrix2.M13; + result.M14 = matrix1.M14 / matrix2.M14; + result.M21 = matrix1.M21 / matrix2.M21; + result.M22 = matrix1.M22 / matrix2.M22; + result.M23 = matrix1.M23 / matrix2.M23; + result.M24 = matrix1.M24 / matrix2.M24; + result.M31 = matrix1.M31 / matrix2.M31; + result.M32 = matrix1.M32 / matrix2.M32; + result.M33 = matrix1.M33 / matrix2.M33; + result.M34 = matrix1.M34 / matrix2.M34; + result.M41 = matrix1.M41 / matrix2.M41; + result.M42 = matrix1.M42 / matrix2.M42; + result.M43 = matrix1.M43 / matrix2.M43; + result.M44 = matrix1.M44 / matrix2.M44; + } + + /// + /// Divides the elements of a by a scalar. + /// + /// Source . + /// Divisor scalar. + /// The result of dividing a matrix by a scalar. + public static Matrix Divide(Matrix matrix1, float divider) + { + float num = 1f / divider; + matrix1.M11 = matrix1.M11 * num; + matrix1.M12 = matrix1.M12 * num; + matrix1.M13 = matrix1.M13 * num; + matrix1.M14 = matrix1.M14 * num; + matrix1.M21 = matrix1.M21 * num; + matrix1.M22 = matrix1.M22 * num; + matrix1.M23 = matrix1.M23 * num; + matrix1.M24 = matrix1.M24 * num; + matrix1.M31 = matrix1.M31 * num; + matrix1.M32 = matrix1.M32 * num; + matrix1.M33 = matrix1.M33 * num; + matrix1.M34 = matrix1.M34 * num; + matrix1.M41 = matrix1.M41 * num; + matrix1.M42 = matrix1.M42 * num; + matrix1.M43 = matrix1.M43 * num; + matrix1.M44 = matrix1.M44 * num; + return matrix1; + } + + /// + /// Divides the elements of a by a scalar. + /// + /// Source . + /// Divisor scalar. + /// The result of dividing a matrix by a scalar as an output parameter. + public static void Divide(ref Matrix matrix1, float divider, out Matrix result) + { + float num = 1f / divider; + result.M11 = matrix1.M11 * num; + result.M12 = matrix1.M12 * num; + result.M13 = matrix1.M13 * num; + result.M14 = matrix1.M14 * num; + result.M21 = matrix1.M21 * num; + result.M22 = matrix1.M22 * num; + result.M23 = matrix1.M23 * num; + result.M24 = matrix1.M24 * num; + result.M31 = matrix1.M31 * num; + result.M32 = matrix1.M32 * num; + result.M33 = matrix1.M33 * num; + result.M34 = matrix1.M34 * num; + result.M41 = matrix1.M41 * num; + result.M42 = matrix1.M42 * num; + result.M43 = matrix1.M43 * num; + result.M44 = matrix1.M44 * num; + } + + /// + /// Compares whether current instance is equal to specified without any tolerance. + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public bool Equals(Matrix other) + { + return ((((((this.M11 == other.M11) && (this.M22 == other.M22)) && ((this.M33 == other.M33) && (this.M44 == other.M44))) && (((this.M12 == other.M12) && (this.M13 == other.M13)) && ((this.M14 == other.M14) && (this.M21 == other.M21)))) && ((((this.M23 == other.M23) && (this.M24 == other.M24)) && ((this.M31 == other.M31) && (this.M32 == other.M32))) && (((this.M34 == other.M34) && (this.M41 == other.M41)) && (this.M42 == other.M42)))) && (this.M43 == other.M43)); + } + + /// + /// Compares whether current instance is equal to specified without any tolerance. + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public override bool Equals(object obj) + { + bool flag = false; + if (obj is Matrix) + { + flag = this.Equals((Matrix)obj); + } + return flag; + } + + /// + /// Gets the hash code of this . + /// + /// Hash code of this . + public override int GetHashCode() + { + return (((((((((((((((this.M11.GetHashCode() + this.M12.GetHashCode()) + this.M13.GetHashCode()) + this.M14.GetHashCode()) + this.M21.GetHashCode()) + this.M22.GetHashCode()) + this.M23.GetHashCode()) + this.M24.GetHashCode()) + this.M31.GetHashCode()) + this.M32.GetHashCode()) + this.M33.GetHashCode()) + this.M34.GetHashCode()) + this.M41.GetHashCode()) + this.M42.GetHashCode()) + this.M43.GetHashCode()) + this.M44.GetHashCode()); + } + + /// + /// Creates a new which contains inversion of the specified matrix. + /// + /// Source . + /// The inverted matrix. + public static Matrix Invert(Matrix matrix) + { + Matrix result; + Invert(ref matrix, out result); + return result; + } + + /// + /// Creates a new which contains inversion of the specified matrix. + /// + /// Source . + /// The inverted matrix as output parameter. + public static void Invert(ref Matrix matrix, out Matrix result) + { + float num1 = matrix.M11; + float num2 = matrix.M12; + float num3 = matrix.M13; + float num4 = matrix.M14; + float num5 = matrix.M21; + float num6 = matrix.M22; + float num7 = matrix.M23; + float num8 = matrix.M24; + float num9 = matrix.M31; + float num10 = matrix.M32; + float num11 = matrix.M33; + float num12 = matrix.M34; + float num13 = matrix.M41; + float num14 = matrix.M42; + float num15 = matrix.M43; + float num16 = matrix.M44; + float num17 = (float)((double)num11 * (double)num16 - (double)num12 * (double)num15); + float num18 = (float)((double)num10 * (double)num16 - (double)num12 * (double)num14); + float num19 = (float)((double)num10 * (double)num15 - (double)num11 * (double)num14); + float num20 = (float)((double)num9 * (double)num16 - (double)num12 * (double)num13); + float num21 = (float)((double)num9 * (double)num15 - (double)num11 * (double)num13); + float num22 = (float)((double)num9 * (double)num14 - (double)num10 * (double)num13); + float num23 = (float)((double)num6 * (double)num17 - (double)num7 * (double)num18 + (double)num8 * (double)num19); + float num24 = (float)-((double)num5 * (double)num17 - (double)num7 * (double)num20 + (double)num8 * (double)num21); + float num25 = (float)((double)num5 * (double)num18 - (double)num6 * (double)num20 + (double)num8 * (double)num22); + float num26 = (float)-((double)num5 * (double)num19 - (double)num6 * (double)num21 + (double)num7 * (double)num22); + float num27 = (float)(1.0 / ((double)num1 * (double)num23 + (double)num2 * (double)num24 + (double)num3 * (double)num25 + (double)num4 * (double)num26)); + + result.M11 = num23 * num27; + result.M21 = num24 * num27; + result.M31 = num25 * num27; + result.M41 = num26 * num27; + result.M12 = (float)-((double)num2 * (double)num17 - (double)num3 * (double)num18 + (double)num4 * (double)num19) * num27; + result.M22 = (float)((double)num1 * (double)num17 - (double)num3 * (double)num20 + (double)num4 * (double)num21) * num27; + result.M32 = (float)-((double)num1 * (double)num18 - (double)num2 * (double)num20 + (double)num4 * (double)num22) * num27; + result.M42 = (float)((double)num1 * (double)num19 - (double)num2 * (double)num21 + (double)num3 * (double)num22) * num27; + float num28 = (float)((double)num7 * (double)num16 - (double)num8 * (double)num15); + float num29 = (float)((double)num6 * (double)num16 - (double)num8 * (double)num14); + float num30 = (float)((double)num6 * (double)num15 - (double)num7 * (double)num14); + float num31 = (float)((double)num5 * (double)num16 - (double)num8 * (double)num13); + float num32 = (float)((double)num5 * (double)num15 - (double)num7 * (double)num13); + float num33 = (float)((double)num5 * (double)num14 - (double)num6 * (double)num13); + result.M13 = (float)((double)num2 * (double)num28 - (double)num3 * (double)num29 + (double)num4 * (double)num30) * num27; + result.M23 = (float)-((double)num1 * (double)num28 - (double)num3 * (double)num31 + (double)num4 * (double)num32) * num27; + result.M33 = (float)((double)num1 * (double)num29 - (double)num2 * (double)num31 + (double)num4 * (double)num33) * num27; + result.M43 = (float)-((double)num1 * (double)num30 - (double)num2 * (double)num32 + (double)num3 * (double)num33) * num27; + float num34 = (float)((double)num7 * (double)num12 - (double)num8 * (double)num11); + float num35 = (float)((double)num6 * (double)num12 - (double)num8 * (double)num10); + float num36 = (float)((double)num6 * (double)num11 - (double)num7 * (double)num10); + float num37 = (float)((double)num5 * (double)num12 - (double)num8 * (double)num9); + float num38 = (float)((double)num5 * (double)num11 - (double)num7 * (double)num9); + float num39 = (float)((double)num5 * (double)num10 - (double)num6 * (double)num9); + result.M14 = (float)-((double)num2 * (double)num34 - (double)num3 * (double)num35 + (double)num4 * (double)num36) * num27; + result.M24 = (float)((double)num1 * (double)num34 - (double)num3 * (double)num37 + (double)num4 * (double)num38) * num27; + result.M34 = (float)-((double)num1 * (double)num35 - (double)num2 * (double)num37 + (double)num4 * (double)num39) * num27; + result.M44 = (float)((double)num1 * (double)num36 - (double)num2 * (double)num38 + (double)num3 * (double)num39) * num27; + + + /* + + + /// + // Use Laplace expansion theorem to calculate the inverse of a 4x4 matrix + // + // 1. Calculate the 2x2 determinants needed the 4x4 determinant based on the 2x2 determinants + // 3. Create the adjugate matrix, which satisfies: A * adj(A) = det(A) * I + // 4. Divide adjugate matrix with the determinant to find the inverse + + float det1, det2, det3, det4, det5, det6, det7, det8, det9, det10, det11, det12; + float detMatrix; + FindDeterminants(ref matrix, out detMatrix, out det1, out det2, out det3, out det4, out det5, out det6, + out det7, out det8, out det9, out det10, out det11, out det12); + + float invDetMatrix = 1f / detMatrix; + + Matrix ret; // Allow for matrix and result to point to the same structure + + ret.M11 = (matrix.M22*det12 - matrix.M23*det11 + matrix.M24*det10) * invDetMatrix; + ret.M12 = (-matrix.M12*det12 + matrix.M13*det11 - matrix.M14*det10) * invDetMatrix; + ret.M13 = (matrix.M42*det6 - matrix.M43*det5 + matrix.M44*det4) * invDetMatrix; + ret.M14 = (-matrix.M32*det6 + matrix.M33*det5 - matrix.M34*det4) * invDetMatrix; + ret.M21 = (-matrix.M21*det12 + matrix.M23*det9 - matrix.M24*det8) * invDetMatrix; + ret.M22 = (matrix.M11*det12 - matrix.M13*det9 + matrix.M14*det8) * invDetMatrix; + ret.M23 = (-matrix.M41*det6 + matrix.M43*det3 - matrix.M44*det2) * invDetMatrix; + ret.M24 = (matrix.M31*det6 - matrix.M33*det3 + matrix.M34*det2) * invDetMatrix; + ret.M31 = (matrix.M21*det11 - matrix.M22*det9 + matrix.M24*det7) * invDetMatrix; + ret.M32 = (-matrix.M11*det11 + matrix.M12*det9 - matrix.M14*det7) * invDetMatrix; + ret.M33 = (matrix.M41*det5 - matrix.M42*det3 + matrix.M44*det1) * invDetMatrix; + ret.M34 = (-matrix.M31*det5 + matrix.M32*det3 - matrix.M34*det1) * invDetMatrix; + ret.M41 = (-matrix.M21*det10 + matrix.M22*det8 - matrix.M23*det7) * invDetMatrix; + ret.M42 = (matrix.M11*det10 - matrix.M12*det8 + matrix.M13*det7) * invDetMatrix; + ret.M43 = (-matrix.M41*det4 + matrix.M42*det2 - matrix.M43*det1) * invDetMatrix; + ret.M44 = (matrix.M31*det4 - matrix.M32*det2 + matrix.M33*det1) * invDetMatrix; + + result = ret; + */ + } + + /// + /// Creates a new that contains linear interpolation of the values in specified matrixes. + /// + /// The first . + /// The second . + /// Weighting value(between 0.0 and 1.0). + /// >The result of linear interpolation of the specified matrixes. + public static Matrix Lerp(Matrix matrix1, Matrix matrix2, float amount) + { + matrix1.M11 = matrix1.M11 + ((matrix2.M11 - matrix1.M11) * amount); + matrix1.M12 = matrix1.M12 + ((matrix2.M12 - matrix1.M12) * amount); + matrix1.M13 = matrix1.M13 + ((matrix2.M13 - matrix1.M13) * amount); + matrix1.M14 = matrix1.M14 + ((matrix2.M14 - matrix1.M14) * amount); + matrix1.M21 = matrix1.M21 + ((matrix2.M21 - matrix1.M21) * amount); + matrix1.M22 = matrix1.M22 + ((matrix2.M22 - matrix1.M22) * amount); + matrix1.M23 = matrix1.M23 + ((matrix2.M23 - matrix1.M23) * amount); + matrix1.M24 = matrix1.M24 + ((matrix2.M24 - matrix1.M24) * amount); + matrix1.M31 = matrix1.M31 + ((matrix2.M31 - matrix1.M31) * amount); + matrix1.M32 = matrix1.M32 + ((matrix2.M32 - matrix1.M32) * amount); + matrix1.M33 = matrix1.M33 + ((matrix2.M33 - matrix1.M33) * amount); + matrix1.M34 = matrix1.M34 + ((matrix2.M34 - matrix1.M34) * amount); + matrix1.M41 = matrix1.M41 + ((matrix2.M41 - matrix1.M41) * amount); + matrix1.M42 = matrix1.M42 + ((matrix2.M42 - matrix1.M42) * amount); + matrix1.M43 = matrix1.M43 + ((matrix2.M43 - matrix1.M43) * amount); + matrix1.M44 = matrix1.M44 + ((matrix2.M44 - matrix1.M44) * amount); + return matrix1; + } + + /// + /// Creates a new that contains linear interpolation of the values in specified matrixes. + /// + /// The first . + /// The second . + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified matrixes as an output parameter. + public static void Lerp(ref Matrix matrix1, ref Matrix matrix2, float amount, out Matrix result) + { + result.M11 = matrix1.M11 + ((matrix2.M11 - matrix1.M11) * amount); + result.M12 = matrix1.M12 + ((matrix2.M12 - matrix1.M12) * amount); + result.M13 = matrix1.M13 + ((matrix2.M13 - matrix1.M13) * amount); + result.M14 = matrix1.M14 + ((matrix2.M14 - matrix1.M14) * amount); + result.M21 = matrix1.M21 + ((matrix2.M21 - matrix1.M21) * amount); + result.M22 = matrix1.M22 + ((matrix2.M22 - matrix1.M22) * amount); + result.M23 = matrix1.M23 + ((matrix2.M23 - matrix1.M23) * amount); + result.M24 = matrix1.M24 + ((matrix2.M24 - matrix1.M24) * amount); + result.M31 = matrix1.M31 + ((matrix2.M31 - matrix1.M31) * amount); + result.M32 = matrix1.M32 + ((matrix2.M32 - matrix1.M32) * amount); + result.M33 = matrix1.M33 + ((matrix2.M33 - matrix1.M33) * amount); + result.M34 = matrix1.M34 + ((matrix2.M34 - matrix1.M34) * amount); + result.M41 = matrix1.M41 + ((matrix2.M41 - matrix1.M41) * amount); + result.M42 = matrix1.M42 + ((matrix2.M42 - matrix1.M42) * amount); + result.M43 = matrix1.M43 + ((matrix2.M43 - matrix1.M43) * amount); + result.M44 = matrix1.M44 + ((matrix2.M44 - matrix1.M44) * amount); + } + + /// + /// Creates a new that contains a multiplication of two matrix. + /// + /// Source . + /// Source . + /// Result of the matrix multiplication. + public static Matrix Multiply(Matrix matrix1, Matrix matrix2) + { + var m11 = (((matrix1.M11 * matrix2.M11) + (matrix1.M12 * matrix2.M21)) + (matrix1.M13 * matrix2.M31)) + (matrix1.M14 * matrix2.M41); + var m12 = (((matrix1.M11 * matrix2.M12) + (matrix1.M12 * matrix2.M22)) + (matrix1.M13 * matrix2.M32)) + (matrix1.M14 * matrix2.M42); + var m13 = (((matrix1.M11 * matrix2.M13) + (matrix1.M12 * matrix2.M23)) + (matrix1.M13 * matrix2.M33)) + (matrix1.M14 * matrix2.M43); + var m14 = (((matrix1.M11 * matrix2.M14) + (matrix1.M12 * matrix2.M24)) + (matrix1.M13 * matrix2.M34)) + (matrix1.M14 * matrix2.M44); + var m21 = (((matrix1.M21 * matrix2.M11) + (matrix1.M22 * matrix2.M21)) + (matrix1.M23 * matrix2.M31)) + (matrix1.M24 * matrix2.M41); + var m22 = (((matrix1.M21 * matrix2.M12) + (matrix1.M22 * matrix2.M22)) + (matrix1.M23 * matrix2.M32)) + (matrix1.M24 * matrix2.M42); + var m23 = (((matrix1.M21 * matrix2.M13) + (matrix1.M22 * matrix2.M23)) + (matrix1.M23 * matrix2.M33)) + (matrix1.M24 * matrix2.M43); + var m24 = (((matrix1.M21 * matrix2.M14) + (matrix1.M22 * matrix2.M24)) + (matrix1.M23 * matrix2.M34)) + (matrix1.M24 * matrix2.M44); + var m31 = (((matrix1.M31 * matrix2.M11) + (matrix1.M32 * matrix2.M21)) + (matrix1.M33 * matrix2.M31)) + (matrix1.M34 * matrix2.M41); + var m32 = (((matrix1.M31 * matrix2.M12) + (matrix1.M32 * matrix2.M22)) + (matrix1.M33 * matrix2.M32)) + (matrix1.M34 * matrix2.M42); + var m33 = (((matrix1.M31 * matrix2.M13) + (matrix1.M32 * matrix2.M23)) + (matrix1.M33 * matrix2.M33)) + (matrix1.M34 * matrix2.M43); + var m34 = (((matrix1.M31 * matrix2.M14) + (matrix1.M32 * matrix2.M24)) + (matrix1.M33 * matrix2.M34)) + (matrix1.M34 * matrix2.M44); + var m41 = (((matrix1.M41 * matrix2.M11) + (matrix1.M42 * matrix2.M21)) + (matrix1.M43 * matrix2.M31)) + (matrix1.M44 * matrix2.M41); + var m42 = (((matrix1.M41 * matrix2.M12) + (matrix1.M42 * matrix2.M22)) + (matrix1.M43 * matrix2.M32)) + (matrix1.M44 * matrix2.M42); + var m43 = (((matrix1.M41 * matrix2.M13) + (matrix1.M42 * matrix2.M23)) + (matrix1.M43 * matrix2.M33)) + (matrix1.M44 * matrix2.M43); + var m44 = (((matrix1.M41 * matrix2.M14) + (matrix1.M42 * matrix2.M24)) + (matrix1.M43 * matrix2.M34)) + (matrix1.M44 * matrix2.M44); + matrix1.M11 = m11; + matrix1.M12 = m12; + matrix1.M13 = m13; + matrix1.M14 = m14; + matrix1.M21 = m21; + matrix1.M22 = m22; + matrix1.M23 = m23; + matrix1.M24 = m24; + matrix1.M31 = m31; + matrix1.M32 = m32; + matrix1.M33 = m33; + matrix1.M34 = m34; + matrix1.M41 = m41; + matrix1.M42 = m42; + matrix1.M43 = m43; + matrix1.M44 = m44; + return matrix1; + } + + /// + /// Creates a new that contains a multiplication of two matrix. + /// + /// Source . + /// Source . + /// Result of the matrix multiplication as an output parameter. + public static void Multiply(ref Matrix matrix1, ref Matrix matrix2, out Matrix result) + { + var m11 = (((matrix1.M11 * matrix2.M11) + (matrix1.M12 * matrix2.M21)) + (matrix1.M13 * matrix2.M31)) + (matrix1.M14 * matrix2.M41); + var m12 = (((matrix1.M11 * matrix2.M12) + (matrix1.M12 * matrix2.M22)) + (matrix1.M13 * matrix2.M32)) + (matrix1.M14 * matrix2.M42); + var m13 = (((matrix1.M11 * matrix2.M13) + (matrix1.M12 * matrix2.M23)) + (matrix1.M13 * matrix2.M33)) + (matrix1.M14 * matrix2.M43); + var m14 = (((matrix1.M11 * matrix2.M14) + (matrix1.M12 * matrix2.M24)) + (matrix1.M13 * matrix2.M34)) + (matrix1.M14 * matrix2.M44); + var m21 = (((matrix1.M21 * matrix2.M11) + (matrix1.M22 * matrix2.M21)) + (matrix1.M23 * matrix2.M31)) + (matrix1.M24 * matrix2.M41); + var m22 = (((matrix1.M21 * matrix2.M12) + (matrix1.M22 * matrix2.M22)) + (matrix1.M23 * matrix2.M32)) + (matrix1.M24 * matrix2.M42); + var m23 = (((matrix1.M21 * matrix2.M13) + (matrix1.M22 * matrix2.M23)) + (matrix1.M23 * matrix2.M33)) + (matrix1.M24 * matrix2.M43); + var m24 = (((matrix1.M21 * matrix2.M14) + (matrix1.M22 * matrix2.M24)) + (matrix1.M23 * matrix2.M34)) + (matrix1.M24 * matrix2.M44); + var m31 = (((matrix1.M31 * matrix2.M11) + (matrix1.M32 * matrix2.M21)) + (matrix1.M33 * matrix2.M31)) + (matrix1.M34 * matrix2.M41); + var m32 = (((matrix1.M31 * matrix2.M12) + (matrix1.M32 * matrix2.M22)) + (matrix1.M33 * matrix2.M32)) + (matrix1.M34 * matrix2.M42); + var m33 = (((matrix1.M31 * matrix2.M13) + (matrix1.M32 * matrix2.M23)) + (matrix1.M33 * matrix2.M33)) + (matrix1.M34 * matrix2.M43); + var m34 = (((matrix1.M31 * matrix2.M14) + (matrix1.M32 * matrix2.M24)) + (matrix1.M33 * matrix2.M34)) + (matrix1.M34 * matrix2.M44); + var m41 = (((matrix1.M41 * matrix2.M11) + (matrix1.M42 * matrix2.M21)) + (matrix1.M43 * matrix2.M31)) + (matrix1.M44 * matrix2.M41); + var m42 = (((matrix1.M41 * matrix2.M12) + (matrix1.M42 * matrix2.M22)) + (matrix1.M43 * matrix2.M32)) + (matrix1.M44 * matrix2.M42); + var m43 = (((matrix1.M41 * matrix2.M13) + (matrix1.M42 * matrix2.M23)) + (matrix1.M43 * matrix2.M33)) + (matrix1.M44 * matrix2.M43); + var m44 = (((matrix1.M41 * matrix2.M14) + (matrix1.M42 * matrix2.M24)) + (matrix1.M43 * matrix2.M34)) + (matrix1.M44 * matrix2.M44); + result.M11 = m11; + result.M12 = m12; + result.M13 = m13; + result.M14 = m14; + result.M21 = m21; + result.M22 = m22; + result.M23 = m23; + result.M24 = m24; + result.M31 = m31; + result.M32 = m32; + result.M33 = m33; + result.M34 = m34; + result.M41 = m41; + result.M42 = m42; + result.M43 = m43; + result.M44 = m44; + } + + /// + /// Creates a new that contains a multiplication of and a scalar. + /// + /// Source . + /// Scalar value. + /// Result of the matrix multiplication with a scalar. + public static Matrix Multiply(Matrix matrix1, float scaleFactor) + { + matrix1.M11 *= scaleFactor; + matrix1.M12 *= scaleFactor; + matrix1.M13 *= scaleFactor; + matrix1.M14 *= scaleFactor; + matrix1.M21 *= scaleFactor; + matrix1.M22 *= scaleFactor; + matrix1.M23 *= scaleFactor; + matrix1.M24 *= scaleFactor; + matrix1.M31 *= scaleFactor; + matrix1.M32 *= scaleFactor; + matrix1.M33 *= scaleFactor; + matrix1.M34 *= scaleFactor; + matrix1.M41 *= scaleFactor; + matrix1.M42 *= scaleFactor; + matrix1.M43 *= scaleFactor; + matrix1.M44 *= scaleFactor; + return matrix1; + } + + /// + /// Creates a new that contains a multiplication of and a scalar. + /// + /// Source . + /// Scalar value. + /// Result of the matrix multiplication with a scalar as an output parameter. + public static void Multiply(ref Matrix matrix1, float scaleFactor, out Matrix result) + { + result.M11 = matrix1.M11 * scaleFactor; + result.M12 = matrix1.M12 * scaleFactor; + result.M13 = matrix1.M13 * scaleFactor; + result.M14 = matrix1.M14 * scaleFactor; + result.M21 = matrix1.M21 * scaleFactor; + result.M22 = matrix1.M22 * scaleFactor; + result.M23 = matrix1.M23 * scaleFactor; + result.M24 = matrix1.M24 * scaleFactor; + result.M31 = matrix1.M31 * scaleFactor; + result.M32 = matrix1.M32 * scaleFactor; + result.M33 = matrix1.M33 * scaleFactor; + result.M34 = matrix1.M34 * scaleFactor; + result.M41 = matrix1.M41 * scaleFactor; + result.M42 = matrix1.M42 * scaleFactor; + result.M43 = matrix1.M43 * scaleFactor; + result.M44 = matrix1.M44 * scaleFactor; + + } + + /// + /// Copy the values of specified to the float array. + /// + /// The source . + /// The array which matrix values will be stored. + /// + /// Required for OpenGL 2.0 projection matrix stuff. + /// + public static float[] ToFloatArray(Matrix matrix) + { + float[] matarray = { + matrix.M11, matrix.M12, matrix.M13, matrix.M14, + matrix.M21, matrix.M22, matrix.M23, matrix.M24, + matrix.M31, matrix.M32, matrix.M33, matrix.M34, + matrix.M41, matrix.M42, matrix.M43, matrix.M44 + }; + return matarray; + } + + /// + /// Returns a matrix with the all values negated. + /// + /// Source . + /// Result of the matrix negation. + public static Matrix Negate(Matrix matrix) + { + matrix.M11 = -matrix.M11; + matrix.M12 = -matrix.M12; + matrix.M13 = -matrix.M13; + matrix.M14 = -matrix.M14; + matrix.M21 = -matrix.M21; + matrix.M22 = -matrix.M22; + matrix.M23 = -matrix.M23; + matrix.M24 = -matrix.M24; + matrix.M31 = -matrix.M31; + matrix.M32 = -matrix.M32; + matrix.M33 = -matrix.M33; + matrix.M34 = -matrix.M34; + matrix.M41 = -matrix.M41; + matrix.M42 = -matrix.M42; + matrix.M43 = -matrix.M43; + matrix.M44 = -matrix.M44; + return matrix; + } + + /// + /// Returns a matrix with the all values negated. + /// + /// Source . + /// Result of the matrix negation as an output parameter. + public static void Negate(ref Matrix matrix, out Matrix result) + { + result.M11 = -matrix.M11; + result.M12 = -matrix.M12; + result.M13 = -matrix.M13; + result.M14 = -matrix.M14; + result.M21 = -matrix.M21; + result.M22 = -matrix.M22; + result.M23 = -matrix.M23; + result.M24 = -matrix.M24; + result.M31 = -matrix.M31; + result.M32 = -matrix.M32; + result.M33 = -matrix.M33; + result.M34 = -matrix.M34; + result.M41 = -matrix.M41; + result.M42 = -matrix.M42; + result.M43 = -matrix.M43; + result.M44 = -matrix.M44; + } + + /// + /// Adds two matrixes. + /// + /// Source on the left of the add sign. + /// Source on the right of the add sign. + /// Sum of the matrixes. + public static Matrix operator +(Matrix matrix1, Matrix matrix2) + { + matrix1.M11 = matrix1.M11 + matrix2.M11; + matrix1.M12 = matrix1.M12 + matrix2.M12; + matrix1.M13 = matrix1.M13 + matrix2.M13; + matrix1.M14 = matrix1.M14 + matrix2.M14; + matrix1.M21 = matrix1.M21 + matrix2.M21; + matrix1.M22 = matrix1.M22 + matrix2.M22; + matrix1.M23 = matrix1.M23 + matrix2.M23; + matrix1.M24 = matrix1.M24 + matrix2.M24; + matrix1.M31 = matrix1.M31 + matrix2.M31; + matrix1.M32 = matrix1.M32 + matrix2.M32; + matrix1.M33 = matrix1.M33 + matrix2.M33; + matrix1.M34 = matrix1.M34 + matrix2.M34; + matrix1.M41 = matrix1.M41 + matrix2.M41; + matrix1.M42 = matrix1.M42 + matrix2.M42; + matrix1.M43 = matrix1.M43 + matrix2.M43; + matrix1.M44 = matrix1.M44 + matrix2.M44; + return matrix1; + } + + /// + /// Divides the elements of a by the elements of another . + /// + /// Source on the left of the div sign. + /// Divisor on the right of the div sign. + /// The result of dividing the matrixes. + public static Matrix operator /(Matrix matrix1, Matrix matrix2) + { + matrix1.M11 = matrix1.M11 / matrix2.M11; + matrix1.M12 = matrix1.M12 / matrix2.M12; + matrix1.M13 = matrix1.M13 / matrix2.M13; + matrix1.M14 = matrix1.M14 / matrix2.M14; + matrix1.M21 = matrix1.M21 / matrix2.M21; + matrix1.M22 = matrix1.M22 / matrix2.M22; + matrix1.M23 = matrix1.M23 / matrix2.M23; + matrix1.M24 = matrix1.M24 / matrix2.M24; + matrix1.M31 = matrix1.M31 / matrix2.M31; + matrix1.M32 = matrix1.M32 / matrix2.M32; + matrix1.M33 = matrix1.M33 / matrix2.M33; + matrix1.M34 = matrix1.M34 / matrix2.M34; + matrix1.M41 = matrix1.M41 / matrix2.M41; + matrix1.M42 = matrix1.M42 / matrix2.M42; + matrix1.M43 = matrix1.M43 / matrix2.M43; + matrix1.M44 = matrix1.M44 / matrix2.M44; + return matrix1; + } + + /// + /// Divides the elements of a by a scalar. + /// + /// Source on the left of the div sign. + /// Divisor scalar on the right of the div sign. + /// The result of dividing a matrix by a scalar. + public static Matrix operator /(Matrix matrix, float divider) + { + float num = 1f / divider; + matrix.M11 = matrix.M11 * num; + matrix.M12 = matrix.M12 * num; + matrix.M13 = matrix.M13 * num; + matrix.M14 = matrix.M14 * num; + matrix.M21 = matrix.M21 * num; + matrix.M22 = matrix.M22 * num; + matrix.M23 = matrix.M23 * num; + matrix.M24 = matrix.M24 * num; + matrix.M31 = matrix.M31 * num; + matrix.M32 = matrix.M32 * num; + matrix.M33 = matrix.M33 * num; + matrix.M34 = matrix.M34 * num; + matrix.M41 = matrix.M41 * num; + matrix.M42 = matrix.M42 * num; + matrix.M43 = matrix.M43 * num; + matrix.M44 = matrix.M44 * num; + return matrix; + } + + /// + /// Compares whether two instances are equal without any tolerance. + /// + /// Source on the left of the equal sign. + /// Source on the right of the equal sign. + /// true if the instances are equal; false otherwise. + public static bool operator ==(Matrix matrix1, Matrix matrix2) + { + return ( + matrix1.M11 == matrix2.M11 && + matrix1.M12 == matrix2.M12 && + matrix1.M13 == matrix2.M13 && + matrix1.M14 == matrix2.M14 && + matrix1.M21 == matrix2.M21 && + matrix1.M22 == matrix2.M22 && + matrix1.M23 == matrix2.M23 && + matrix1.M24 == matrix2.M24 && + matrix1.M31 == matrix2.M31 && + matrix1.M32 == matrix2.M32 && + matrix1.M33 == matrix2.M33 && + matrix1.M34 == matrix2.M34 && + matrix1.M41 == matrix2.M41 && + matrix1.M42 == matrix2.M42 && + matrix1.M43 == matrix2.M43 && + matrix1.M44 == matrix2.M44 + ); + } + + /// + /// Compares whether two instances are not equal without any tolerance. + /// + /// Source on the left of the not equal sign. + /// Source on the right of the not equal sign. + /// true if the instances are not equal; false otherwise. + public static bool operator !=(Matrix matrix1, Matrix matrix2) + { + return ( + matrix1.M11 != matrix2.M11 || + matrix1.M12 != matrix2.M12 || + matrix1.M13 != matrix2.M13 || + matrix1.M14 != matrix2.M14 || + matrix1.M21 != matrix2.M21 || + matrix1.M22 != matrix2.M22 || + matrix1.M23 != matrix2.M23 || + matrix1.M24 != matrix2.M24 || + matrix1.M31 != matrix2.M31 || + matrix1.M32 != matrix2.M32 || + matrix1.M33 != matrix2.M33 || + matrix1.M34 != matrix2.M34 || + matrix1.M41 != matrix2.M41 || + matrix1.M42 != matrix2.M42 || + matrix1.M43 != matrix2.M43 || + matrix1.M44 != matrix2.M44 + ); + } + + /// + /// Multiplies two matrixes. + /// + /// Source on the left of the mul sign. + /// Source on the right of the mul sign. + /// Result of the matrix multiplication. + /// + /// Using matrix multiplication algorithm - see http://en.wikipedia.org/wiki/Matrix_multiplication. + /// + public static Matrix operator *(Matrix matrix1, Matrix matrix2) + { + var m11 = (((matrix1.M11 * matrix2.M11) + (matrix1.M12 * matrix2.M21)) + (matrix1.M13 * matrix2.M31)) + (matrix1.M14 * matrix2.M41); + var m12 = (((matrix1.M11 * matrix2.M12) + (matrix1.M12 * matrix2.M22)) + (matrix1.M13 * matrix2.M32)) + (matrix1.M14 * matrix2.M42); + var m13 = (((matrix1.M11 * matrix2.M13) + (matrix1.M12 * matrix2.M23)) + (matrix1.M13 * matrix2.M33)) + (matrix1.M14 * matrix2.M43); + var m14 = (((matrix1.M11 * matrix2.M14) + (matrix1.M12 * matrix2.M24)) + (matrix1.M13 * matrix2.M34)) + (matrix1.M14 * matrix2.M44); + var m21 = (((matrix1.M21 * matrix2.M11) + (matrix1.M22 * matrix2.M21)) + (matrix1.M23 * matrix2.M31)) + (matrix1.M24 * matrix2.M41); + var m22 = (((matrix1.M21 * matrix2.M12) + (matrix1.M22 * matrix2.M22)) + (matrix1.M23 * matrix2.M32)) + (matrix1.M24 * matrix2.M42); + var m23 = (((matrix1.M21 * matrix2.M13) + (matrix1.M22 * matrix2.M23)) + (matrix1.M23 * matrix2.M33)) + (matrix1.M24 * matrix2.M43); + var m24 = (((matrix1.M21 * matrix2.M14) + (matrix1.M22 * matrix2.M24)) + (matrix1.M23 * matrix2.M34)) + (matrix1.M24 * matrix2.M44); + var m31 = (((matrix1.M31 * matrix2.M11) + (matrix1.M32 * matrix2.M21)) + (matrix1.M33 * matrix2.M31)) + (matrix1.M34 * matrix2.M41); + var m32 = (((matrix1.M31 * matrix2.M12) + (matrix1.M32 * matrix2.M22)) + (matrix1.M33 * matrix2.M32)) + (matrix1.M34 * matrix2.M42); + var m33 = (((matrix1.M31 * matrix2.M13) + (matrix1.M32 * matrix2.M23)) + (matrix1.M33 * matrix2.M33)) + (matrix1.M34 * matrix2.M43); + var m34 = (((matrix1.M31 * matrix2.M14) + (matrix1.M32 * matrix2.M24)) + (matrix1.M33 * matrix2.M34)) + (matrix1.M34 * matrix2.M44); + var m41 = (((matrix1.M41 * matrix2.M11) + (matrix1.M42 * matrix2.M21)) + (matrix1.M43 * matrix2.M31)) + (matrix1.M44 * matrix2.M41); + var m42 = (((matrix1.M41 * matrix2.M12) + (matrix1.M42 * matrix2.M22)) + (matrix1.M43 * matrix2.M32)) + (matrix1.M44 * matrix2.M42); + var m43 = (((matrix1.M41 * matrix2.M13) + (matrix1.M42 * matrix2.M23)) + (matrix1.M43 * matrix2.M33)) + (matrix1.M44 * matrix2.M43); + var m44 = (((matrix1.M41 * matrix2.M14) + (matrix1.M42 * matrix2.M24)) + (matrix1.M43 * matrix2.M34)) + (matrix1.M44 * matrix2.M44); + matrix1.M11 = m11; + matrix1.M12 = m12; + matrix1.M13 = m13; + matrix1.M14 = m14; + matrix1.M21 = m21; + matrix1.M22 = m22; + matrix1.M23 = m23; + matrix1.M24 = m24; + matrix1.M31 = m31; + matrix1.M32 = m32; + matrix1.M33 = m33; + matrix1.M34 = m34; + matrix1.M41 = m41; + matrix1.M42 = m42; + matrix1.M43 = m43; + matrix1.M44 = m44; + return matrix1; + } + + /// + /// Multiplies the elements of matrix by a scalar. + /// + /// Source on the left of the mul sign. + /// Scalar value on the right of the mul sign. + /// Result of the matrix multiplication with a scalar. + public static Matrix operator *(Matrix matrix, float scaleFactor) + { + matrix.M11 = matrix.M11 * scaleFactor; + matrix.M12 = matrix.M12 * scaleFactor; + matrix.M13 = matrix.M13 * scaleFactor; + matrix.M14 = matrix.M14 * scaleFactor; + matrix.M21 = matrix.M21 * scaleFactor; + matrix.M22 = matrix.M22 * scaleFactor; + matrix.M23 = matrix.M23 * scaleFactor; + matrix.M24 = matrix.M24 * scaleFactor; + matrix.M31 = matrix.M31 * scaleFactor; + matrix.M32 = matrix.M32 * scaleFactor; + matrix.M33 = matrix.M33 * scaleFactor; + matrix.M34 = matrix.M34 * scaleFactor; + matrix.M41 = matrix.M41 * scaleFactor; + matrix.M42 = matrix.M42 * scaleFactor; + matrix.M43 = matrix.M43 * scaleFactor; + matrix.M44 = matrix.M44 * scaleFactor; + return matrix; + } + + /// + /// Subtracts the values of one from another . + /// + /// Source on the left of the sub sign. + /// Source on the right of the sub sign. + /// Result of the matrix subtraction. + public static Matrix operator -(Matrix matrix1, Matrix matrix2) + { + matrix1.M11 = matrix1.M11 - matrix2.M11; + matrix1.M12 = matrix1.M12 - matrix2.M12; + matrix1.M13 = matrix1.M13 - matrix2.M13; + matrix1.M14 = matrix1.M14 - matrix2.M14; + matrix1.M21 = matrix1.M21 - matrix2.M21; + matrix1.M22 = matrix1.M22 - matrix2.M22; + matrix1.M23 = matrix1.M23 - matrix2.M23; + matrix1.M24 = matrix1.M24 - matrix2.M24; + matrix1.M31 = matrix1.M31 - matrix2.M31; + matrix1.M32 = matrix1.M32 - matrix2.M32; + matrix1.M33 = matrix1.M33 - matrix2.M33; + matrix1.M34 = matrix1.M34 - matrix2.M34; + matrix1.M41 = matrix1.M41 - matrix2.M41; + matrix1.M42 = matrix1.M42 - matrix2.M42; + matrix1.M43 = matrix1.M43 - matrix2.M43; + matrix1.M44 = matrix1.M44 - matrix2.M44; + return matrix1; + } + + /// + /// Inverts values in the specified . + /// + /// Source on the right of the sub sign. + /// Result of the inversion. + public static Matrix operator -(Matrix matrix) + { + matrix.M11 = -matrix.M11; + matrix.M12 = -matrix.M12; + matrix.M13 = -matrix.M13; + matrix.M14 = -matrix.M14; + matrix.M21 = -matrix.M21; + matrix.M22 = -matrix.M22; + matrix.M23 = -matrix.M23; + matrix.M24 = -matrix.M24; + matrix.M31 = -matrix.M31; + matrix.M32 = -matrix.M32; + matrix.M33 = -matrix.M33; + matrix.M34 = -matrix.M34; + matrix.M41 = -matrix.M41; + matrix.M42 = -matrix.M42; + matrix.M43 = -matrix.M43; + matrix.M44 = -matrix.M44; + return matrix; + } + + /// + /// Creates a new that contains subtraction of one matrix from another. + /// + /// The first . + /// The second . + /// The result of the matrix subtraction. + public static Matrix Subtract(Matrix matrix1, Matrix matrix2) + { + matrix1.M11 = matrix1.M11 - matrix2.M11; + matrix1.M12 = matrix1.M12 - matrix2.M12; + matrix1.M13 = matrix1.M13 - matrix2.M13; + matrix1.M14 = matrix1.M14 - matrix2.M14; + matrix1.M21 = matrix1.M21 - matrix2.M21; + matrix1.M22 = matrix1.M22 - matrix2.M22; + matrix1.M23 = matrix1.M23 - matrix2.M23; + matrix1.M24 = matrix1.M24 - matrix2.M24; + matrix1.M31 = matrix1.M31 - matrix2.M31; + matrix1.M32 = matrix1.M32 - matrix2.M32; + matrix1.M33 = matrix1.M33 - matrix2.M33; + matrix1.M34 = matrix1.M34 - matrix2.M34; + matrix1.M41 = matrix1.M41 - matrix2.M41; + matrix1.M42 = matrix1.M42 - matrix2.M42; + matrix1.M43 = matrix1.M43 - matrix2.M43; + matrix1.M44 = matrix1.M44 - matrix2.M44; + return matrix1; + } + + /// + /// Creates a new that contains subtraction of one matrix from another. + /// + /// The first . + /// The second . + /// The result of the matrix subtraction as an output parameter. + public static void Subtract(ref Matrix matrix1, ref Matrix matrix2, out Matrix result) + { + result.M11 = matrix1.M11 - matrix2.M11; + result.M12 = matrix1.M12 - matrix2.M12; + result.M13 = matrix1.M13 - matrix2.M13; + result.M14 = matrix1.M14 - matrix2.M14; + result.M21 = matrix1.M21 - matrix2.M21; + result.M22 = matrix1.M22 - matrix2.M22; + result.M23 = matrix1.M23 - matrix2.M23; + result.M24 = matrix1.M24 - matrix2.M24; + result.M31 = matrix1.M31 - matrix2.M31; + result.M32 = matrix1.M32 - matrix2.M32; + result.M33 = matrix1.M33 - matrix2.M33; + result.M34 = matrix1.M34 - matrix2.M34; + result.M41 = matrix1.M41 - matrix2.M41; + result.M42 = matrix1.M42 - matrix2.M42; + result.M43 = matrix1.M43 - matrix2.M43; + result.M44 = matrix1.M44 - matrix2.M44; + } + + internal string DebugDisplayString + { + get + { + if (this == Identity) + { + return "Identity"; + } + + return string.Concat( + "( ", this.M11.ToString(), " ", this.M12.ToString(), " ", this.M13.ToString(), " ", this.M14.ToString(), " ) \r\n", + "( ", this.M21.ToString(), " ", this.M22.ToString(), " ", this.M23.ToString(), " ", this.M24.ToString(), " ) \r\n", + "( ", this.M31.ToString(), " ", this.M32.ToString(), " ", this.M33.ToString(), " ", this.M34.ToString(), " ) \r\n", + "( ", this.M41.ToString(), " ", this.M42.ToString(), " ", this.M43.ToString(), " ", this.M44.ToString(), " )"); + } + } + + /// + /// Returns a representation of this in the format: + /// {M11:[] M12:[] M13:[] M14:[]} + /// {M21:[] M12:[] M13:[] M14:[]} + /// {M31:[] M32:[] M33:[] M34:[]} + /// {M41:[] M42:[] M43:[] M44:[]} + /// + /// A representation of this . + public override string ToString() + { + return "{M11:" + M11 + " M12:" + M12 + " M13:" + M13 + " M14:" + M14 + "}" + + " {M21:" + M21 + " M22:" + M22 + " M23:" + M23 + " M24:" + M24 + "}" + + " {M31:" + M31 + " M32:" + M32 + " M33:" + M33 + " M34:" + M34 + "}" + + " {M41:" + M41 + " M42:" + M42 + " M43:" + M43 + " M44:" + M44 + "}"; + } + + /// + /// Swap the matrix rows and columns. + /// + /// The matrix for transposing operation. + /// The new which contains the transposing result. + public static Matrix Transpose(Matrix matrix) + { + Matrix ret; + Transpose(ref matrix, out ret); + return ret; + } + + /// + /// Swap the matrix rows and columns. + /// + /// The matrix for transposing operation. + /// The new which contains the transposing result as an output parameter. + public static void Transpose(ref Matrix matrix, out Matrix result) + { + Matrix ret; + + ret.M11 = matrix.M11; + ret.M12 = matrix.M21; + ret.M13 = matrix.M31; + ret.M14 = matrix.M41; + + ret.M21 = matrix.M12; + ret.M22 = matrix.M22; + ret.M23 = matrix.M32; + ret.M24 = matrix.M42; + + ret.M31 = matrix.M13; + ret.M32 = matrix.M23; + ret.M33 = matrix.M33; + ret.M34 = matrix.M43; + + ret.M41 = matrix.M14; + ret.M42 = matrix.M24; + ret.M43 = matrix.M34; + ret.M44 = matrix.M44; + + result = ret; + } + #endregion + + #region Private Static Methods + + /// + /// Helper method for using the Laplace expansion theorem using two rows expansions to calculate major and + /// minor determinants of a 4x4 matrix. This method is used for inverting a matrix. + /// + private static void FindDeterminants(ref Matrix matrix, out float major, + out float minor1, out float minor2, out float minor3, out float minor4, out float minor5, out float minor6, + out float minor7, out float minor8, out float minor9, out float minor10, out float minor11, out float minor12) + { + double det1 = (double)matrix.M11 * (double)matrix.M22 - (double)matrix.M12 * (double)matrix.M21; + double det2 = (double)matrix.M11 * (double)matrix.M23 - (double)matrix.M13 * (double)matrix.M21; + double det3 = (double)matrix.M11 * (double)matrix.M24 - (double)matrix.M14 * (double)matrix.M21; + double det4 = (double)matrix.M12 * (double)matrix.M23 - (double)matrix.M13 * (double)matrix.M22; + double det5 = (double)matrix.M12 * (double)matrix.M24 - (double)matrix.M14 * (double)matrix.M22; + double det6 = (double)matrix.M13 * (double)matrix.M24 - (double)matrix.M14 * (double)matrix.M23; + double det7 = (double)matrix.M31 * (double)matrix.M42 - (double)matrix.M32 * (double)matrix.M41; + double det8 = (double)matrix.M31 * (double)matrix.M43 - (double)matrix.M33 * (double)matrix.M41; + double det9 = (double)matrix.M31 * (double)matrix.M44 - (double)matrix.M34 * (double)matrix.M41; + double det10 = (double)matrix.M32 * (double)matrix.M43 - (double)matrix.M33 * (double)matrix.M42; + double det11 = (double)matrix.M32 * (double)matrix.M44 - (double)matrix.M34 * (double)matrix.M42; + double det12 = (double)matrix.M33 * (double)matrix.M44 - (double)matrix.M34 * (double)matrix.M43; + + major = (float)(det1 * det12 - det2 * det11 + det3 * det10 + det4 * det9 - det5 * det8 + det6 * det7); + minor1 = (float)det1; + minor2 = (float)det2; + minor3 = (float)det3; + minor4 = (float)det4; + minor5 = (float)det5; + minor6 = (float)det6; + minor7 = (float)det7; + minor8 = (float)det8; + minor9 = (float)det9; + minor10 = (float)det10; + minor11 = (float)det11; + minor12 = (float)det12; + } + + #endregion + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/OperationDescription.cs b/Hawkeye.VisionBuilder.Workflow/OperationDescription.cs new file mode 100644 index 0000000..12caaed --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/OperationDescription.cs @@ -0,0 +1,8 @@ +namespace Hawkeye.VisionBuilder.Workflow; + +public class OperationDescription +{ + public string Name { get; set; } + public string Category { get; set; } + public Type Type { get; set; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/OperationDiscoveryService.cs b/Hawkeye.VisionBuilder.Workflow/OperationDiscoveryService.cs new file mode 100644 index 0000000..183e777 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/OperationDiscoveryService.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using System.Text.RegularExpressions; +using Hawkeye.VisionBuilder.Workflow.Configuration; +using Hawkeye.VisionBuilder.Workflow.Links; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using Ninject; +using Ninject.Infrastructure.Language; + +namespace Hawkeye.VisionBuilder.Workflow; + +public class OperationDiscoveryService +{ + + private readonly WorkflowList _workflowList; + private readonly StandardKernel _kernel; + + public OperationDiscoveryService( WorkflowList workflowList) + { + + _workflowList = workflowList; + _kernel = new StandardKernel(); + + _kernel.Bind().ToConstant(workflowList); + + } + + + + public List DiscoverOperations() + { + return this.GetType().Assembly.GetTypes() + .Where(x => x.IsSubclassOf(typeof(BaseOperation))&&!x.HasAttribute(typeof(IgnoreOperationAttribute))) + .Where(x=>x.Name!=nameof(NoOrigin)) + .Select(x=>new OperationDescription() + { + Name = BaseOperation.MakeOperationName(x.Name), + Category = GetCategory(x), + Type = x + }).ToList(); + } + + private string GetCategory(Type t) + { + var category=t.GetCustomAttribute(typeof(Category)); + if (category!=null) + { + return (category as Category).Name; + } + + return "General"; + } + + public BaseOperation CreateInstance(Type type) + { + return (BaseOperation)_kernel.Get(type); + } + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs new file mode 100644 index 0000000..c84b60a --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/AnomalyAI.cs @@ -0,0 +1,125 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; +using System.Runtime.InteropServices; +using Hawkeye.VisionBuilder.Workflow.DataTransfer; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +public class AnomalyAI: BaseOperation +{ + + private readonly string _modelName; + + public AnomalyAI() + { + + _cSharpDataTransferMQRPC = PythonModelProxyRPC.GetInterface(); + CanHaveProcessingError = false; + } + + private readonly CSharpDataTransferMQRPC _cSharpDataTransferMQRPC; + private int _imageWidth; + private int _imageHeight; + + private bool _initialized = false; + private FilePath _modelFilePath = new FilePath(); + + + public string MemorySlotName { get; set; } = "Anomaly"; + + public FilePath ModelFilePath + { + get => _modelFilePath; + set + { + _modelFilePath = value; + _initialized = false; + } + } + + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckColorful(context)) return; + + if (!_initialized) + { + if (!File.Exists(ModelFilePath?.Path)) + { + this.SetError("Model file not found"); + return; + } + _cSharpDataTransferMQRPC.TransferData(new MethodCall("load_model", ModelFilePath.Path, _modelName)); + _initialized = true; + } + + + + _cSharpDataTransferMQRPC.TransferData(new MethodCall("activate_model", _modelName)); + + var sizeEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall("get_accept_size")); + var size = sizeEncoded.Select(Convert.ToInt32).ToArray(); + _imageWidth = size[2]; + _imageHeight = size[1]; + + var outputSize = _cSharpDataTransferMQRPC.TransferData(new MethodCall("get_output_size")); + var outputSizeArray = outputSize.Select(Convert.ToInt32).ToArray(); + + + var currentImage = context.ActiveImage; + + var rightColor = currentImage.ImageData.CvtColor(ColorConversionCodes.BGR2RGB); + var resized = rightColor.Resize(new Size(_imageWidth,_imageHeight)); + int dataSize = resized.Rows * resized.Cols * resized.ElemSize(); + var byteArray = new byte[dataSize]; + Marshal.Copy(resized.Data, byteArray, 0, dataSize); + + + var resultEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall( "predict_anomaly", byteArray)); + + var result = resultEncoded.Select(Convert.ToByte).ToArray(); + + var defect = result.Take(1).ToArray()[0]; + + if (defect > 50) + { + + + byte[] resultColor = result.Skip(1).ToArray(); + + + + // black and white mask + var mask = new Mat(30, 48, MatType.CV_8UC1, resultColor); + + var maskResized = mask.Resize(new Size(currentImage.ImageData.Width, currentImage.ImageData.Height)); + + context.ActiveImage = new HawkeyeImage() {ImageData = maskResized}; + } + else + { + context.ActiveImage = new HawkeyeImage() { ImageData = new Mat(currentImage.ImageData.Size(), MatType.CV_8UC1, new Scalar(0)) }; + } + + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(_modelFilePath.Path); + bw.Write(_modelFilePath.Format); + + } + + public override void Load(BinaryReader br) + { + base.Load(br); + _modelFilePath.Path = br.ReadString(); + _modelFilePath.Format = br.ReadString(); + + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs new file mode 100644 index 0000000..8d51895 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelMatchingOperation.cs @@ -0,0 +1,184 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.FixedRectangle; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +using OpenCvSharp; +using System.Runtime.InteropServices; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.ArrayHorizontal; +using Hawkeye.VisionBuilder.Workflow.DataTransfer; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +[IgnoreOperation] +public class ArrayModelMatchingOperation:BaseOperation +{ + private readonly WorkflowList _workflowList; + private readonly CSharpDataTransferMQ _transfer; + public Guid ReferenceId { get; set; } = new Guid(); + public ArrayHorizontalElement SearchArea { get; set; } + public string ModelName { get; set; } = "cls_candy5.h5"; + public Action ExportImages { get; set; } + public int Margin { get; set; } = 5; + public int Amount { get; set; } = 4; + + private int _channels = 3; + private List _lastCutImages; + + public ArrayModelMatchingOperation(WorkflowList workflowList) + { + _workflowList = workflowList; + _transfer = PythonModelProxy.GetInterface(); + + + SearchArea = new ArrayHorizontalElement() + { + Editable = true, + Location = Vector2.One * 100, + BlockSize = new Vector2(100, 100), + + }; + ExportImages = () => + { + if (!Directory.Exists(Path.Combine(@"..\Data\Export", ModelName))) + { + Directory.CreateDirectory(Path.Combine(@"..\Data\Export", ModelName)); + } + + // save last cut images + var i = 0; + foreach (var cutImage in _lastCutImages) + { + cutImage.SaveImage(Path.Combine(@"..\Data\Export", ModelName, $"{Guid.NewGuid().ToString()}.png")); + } + + }; + ReloadModelInfo(); + } + + private void ReloadModelInfo() + { + var responseBytes = _transfer.TransferData(ModelName, 2, Array.Empty()); + int[] sizes = new int[3]; + Buffer.BlockCopy(responseBytes, 0, sizes, 0, 12); + SearchArea.BlockSize = new Vector2(sizes[1], sizes[0]); + _channels = sizes[2]; + } + + byte[] MatToBytes(Mat mat) + { + if (mat.Channels() == 3) + { + mat = mat.CvtColor(_channels == 3 ? ColorConversionCodes.BGR2RGB : ColorConversionCodes.BGR2GRAY); + } + + + IntPtr dataPtr = mat.Data; + + // Calculate the size of the image data + int dataSize = mat.Rows * mat.Cols * mat.ElemSize(); + + + + + // Copy the image data into a byte array + byte[] byteArray = new byte[dataSize]; + + Marshal.Copy(dataPtr, byteArray, 0, dataSize); + + return byteArray; + } + + protected override void InterpretInternal(Context context) + { + Result = true; + if (!File.Exists(Path.Combine(@"..\Data\AI", ModelName))) + { + Status = $"Model `{ModelName}` not found"; + Result = false; + return; + } + if (!CheckImageExists(context)) return; + switch (_channels) + { + case 3 when !CheckColorful(context): + case 1 when !CheckGrayscale(context): + return; + } + + var refLocation = _workflowList.GetOriginById(ReferenceId).Origin.Location; + SearchArea.MovePivot(refLocation); + + SearchArea.Margin=Margin; + SearchArea.BlockCount=Amount; + var img = context.ActiveImage.ImageData; + + _lastCutImages = SearchArea + .GenerateLocations() + .Select(x => img[(int) x.Y, (int) x.Y + (int) SearchArea.BlockSize.Y, (int) x.X, + (int) x.X + (int) SearchArea.BlockSize.X]).ToList(); + var images= _lastCutImages.Select(MatToBytes).ToList(); + + + var bytes= images.SelectMany(x => x).ToArray(); + + var len = images.Count; + var bytes2 = new byte[bytes.Length + 4]; + bytes2[3] = (byte)(len >> 24); + bytes2[2] = (byte)(len >> 16); + bytes2[1] = (byte)(len >> 8); + bytes2[0] = (byte)(len >> 0); + + Array.Copy(bytes, 0, bytes2, 4, bytes.Length); + + var answer = _transfer.TransferData(ModelName, 0,bytes2); + float[] floatArray = new float[answer.Length / 4]; + Buffer.BlockCopy(answer, 0, floatArray, 0, answer.Length); + + // Print answer + for (int i = 0; i < Amount; i++) + { + Console.WriteLine(floatArray[i]); + } + + for (int i = 0; i < Amount; i++) + { + SearchArea.IsGood[i] = floatArray[i] > 0.7; + } + + + + context.GraphicsElements.Add(SearchArea); + Result = SearchArea.IsGood.All(x=>x.Value==true); + } + + public override void SetParameters(Dictionary parameters) + { + base.SetParameters(parameters); + ReloadModelInfo(); + SearchArea.SetPivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); + + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(ReferenceId.ToString()); + bw.Write(Margin); + bw.Write(Amount); + bw.Write(ModelName); + SearchArea.Save(bw); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + ReferenceId = new Guid(br.ReadString()); + Margin = br.ReadInt32(); + Amount = br.ReadInt32(); + ModelName = br.ReadString(); + SearchArea = new ArrayHorizontalElement(); + SearchArea.Load(br); + ReloadModelInfo(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelSamplerOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelSamplerOperation.cs new file mode 100644 index 0000000..c81fe00 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ArrayModelSamplerOperation.cs @@ -0,0 +1,87 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.ArrayHorizontal; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +using OpenCvSharp; +using System.Runtime.InteropServices; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +[IgnoreOperation] +public class ArrayModelSamplerOperation:BaseOperation +{ + private Mat _lastImage; + + public RectangleElement SearchArea { get; set; } + public string SampleStorage { get; set; } = "samples.storage"; + public Action Sample { get; set; } + + public ArrayModelSamplerOperation() + { + SearchArea = new RectangleElement() + { + Editable = true, + Location = Vector2.One * 100, + Size = new Vector2(100, 100), + + }; + Sample = SaveSample; + } + + byte[] MatToBytes(Mat mat) + { + if (mat.Channels() == 3) + { + mat = mat.CvtColor(ColorConversionCodes.BGR2RGB); + } + + + IntPtr dataPtr = mat.Data; + int dataSize = mat.Rows * mat.Cols * mat.ElemSize(); + byte[] byteArray = new byte[dataSize]; + + Marshal.Copy(dataPtr, byteArray, 0, dataSize); + + return byteArray; + } + + private void SaveSample() + { + + + + Mat mask = new Mat(_lastImage.Size(), MatType.CV_8UC1, Scalar.Black); + SearchArea.FillMat(mask); + var imageBytes = MatToBytes(_lastImage); + var maskBytes = MatToBytes(mask); + // Append to the end of storage file + + var storagePath=Path.Combine("..\\Data\\AI", SampleStorage); + + + + using (var file = new System.IO.FileStream(storagePath, System.IO.FileMode.Append)) + { + // Write size + file.Write(BitConverter.GetBytes(_lastImage.Width), 0, 4); + file.Write(BitConverter.GetBytes(_lastImage.Height), 0, 4); + file.Write(imageBytes, 0, imageBytes.Length); + // Write mask + file.Write(maskBytes, 0, maskBytes.Length); + } + } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if(!CheckColorful(context)) return; + + context.GraphicsElements.Add(SearchArea); + + _lastImage=context.ActiveImage.ImageData; + Result = true; + } + + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs new file mode 100644 index 0000000..420be4a --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/BackgroundSeparationModelOperation.cs @@ -0,0 +1,124 @@ +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; +using System.Drawing; +using System.Runtime.InteropServices; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; +using Hawkeye.VisionBuilder.Workflow.DataTransfer; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Poly; +using Hawkeye.VisionBuilder.Workflow.Links; + + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +[IgnoreOperation] +public class BackgroundSeparationModelOperation:BaseOperation,IHaveOrigin +{ + private readonly CSharpDataTransferMQ _transfer; + private int _channels; + public string ModelName { get; set; } = "background.h5"; + + + public BackgroundSeparationModelOperation() + { + _transfer = PythonModelProxy.GetInterface(); + + } + + byte[] MatToBytes(Mat mat) + { + if (mat.Channels() == 3) + { + mat = mat.CvtColor(ColorConversionCodes.BGR2RGB); + } + + + IntPtr dataPtr = mat.Data; + int dataSize = mat.Rows * mat.Cols * mat.ElemSize(); + byte[] byteArray = new byte[dataSize]; + + Marshal.Copy(dataPtr, byteArray, 0, dataSize); + + return byteArray; + } + + protected override void InterpretInternal(Context context) + { + Result = true; + if (!CheckImageExists(context)) return; + if (!CheckColorful(context)) return; + if (!File.Exists(Path.Combine(@"..\Data\AI", ModelName))) + { + Status= $"Model `{ModelName}` not found"; + Result = false; + return; + } + + + var img = context.ActiveImage.ImageData; + + var bytes= MatToBytes(img); + var responseBytes = _transfer.TransferData(ModelName, 3, bytes); + var response = new Mat(img.Rows, img.Cols, MatType.CV_8UC1, responseBytes); + + var contours = response.Threshold(128, 255, ThresholdTypes.Binary).FindContoursAsArray(RetrievalModes.List, ContourApproximationModes.ApproxSimple); + + if (contours.Length == 0) + { + Status = "No object found"; + Result = false; + return; + } + + // find biggest contour + var max = 0; + var maxIndex = 0; + for (var i = 0; i < contours.Length; i++) + { + var area = Cv2.ContourArea(contours[i]); + if (area > max) + { + max = (int)area; + maxIndex = i; + } + } + + PolyElement poly = new PolyElement() { IsGood = true,Editable = false}; + + poly.Points = contours[maxIndex].Select(x => new Vector2(x.X, x.Y)).ToArray(); + + context.GraphicsElements.Add(poly); + + + var rect = Cv2.MinAreaRect(contours[maxIndex]); + var bound = rect.BoundingRect(); + Origin=new OriginElement() + { + Location = new Vector2(bound.Left, bound.Top), + }; + + Status= $"Object found at {Origin.Location}"; + Result = true; + + } + + public override void SetParameters(Dictionary parameters) + { + base.SetParameters(parameters); + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(ModelName); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + ModelName = br.ReadString(); + } + + public OriginElement Origin { get; set; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs new file mode 100644 index 0000000..1d7ce47 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128BinaryOperation.cs @@ -0,0 +1,33 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +[IgnoreOperation] +public class Color128BinaryOperation:ColorAIOperation +{ + public Color128BinaryOperation() : base("prob128binary") + { + IsRaw = true; + } + + + public override string FilterClasses { get; set; } = "1"; + public override FilePath ModelFilePath { get; set; } = new FilePath() { Format = "Model file(*.h5)|*.h5" }; + + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(FilterClasses); + bw.Write(ModelFilePath.Path); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + FilterClasses = br.ReadString(); + ModelFilePath.Path = br.ReadString(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs new file mode 100644 index 0000000..787056d --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128HalfOperation.cs @@ -0,0 +1,113 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using Hawkeye.VisionBuilder.Workflow.DataTransfer; + + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +[IgnoreOperation] +public class Color128HalfOperation:BaseOperation +{ + public Color128HalfOperation() + { + _cSharpDataTransferMQRPC = PythonModelProxyRPC.GetInterface(); + _cSharpDataTransferMQRPC.TransferData(new MethodCall("activate_model", "segment128half")); + var sizeEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall("get_accept_size")); + var size = sizeEncoded.Select(Convert.ToInt32).ToArray(); + _imageWidth = size[0]; + _imageHeight = size[1]; + CanHaveProcessingError = false; + } + + public string FilterClasses { get; set; } = "0"; + public FilePath ModelFilePath { get; set; } = new FilePath() { Format = "Model file(*.h5)|*.h5" }; + + + private readonly CSharpDataTransferMQRPC _cSharpDataTransferMQRPC; + private readonly int _imageWidth; + private readonly int _imageHeight; + + private bool _initialized = false; + + protected override void InterpretInternal(Context context) + { + if(!CheckImageExists(context))return; + if(!CheckColorful(context))return; + + _cSharpDataTransferMQRPC.TransferData(new MethodCall("activate_model", "segment128half")); + + if (!_initialized) + { + if (!File.Exists(ModelFilePath.Path)) + { + this.SetError("Model file not found"); + return; + } + _cSharpDataTransferMQRPC.TransferData(new MethodCall("load_model", ModelFilePath.Path)); + _initialized = true; + } + + var currentImage = context.ActiveImage; + + var rightColor = currentImage.ImageData.CvtColor(ColorConversionCodes.BGR2RGB); + var resized = rightColor.Resize(new Size(_imageHeight, _imageWidth)); + int dataSize = resized.Rows * resized.Cols * resized.ElemSize(); + var byteArray = new byte[dataSize]; + Marshal.Copy(resized.Data, byteArray, 0, dataSize); + + var allowed = FilterClasses.Split(',',StringSplitOptions.RemoveEmptyEntries).Select(x => Convert.ToInt32(x)).ToArray(); + + var allowedFlags = new byte[128]; + foreach (var i in allowed) + { + allowedFlags[i] = 1; + } + + var resultEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall("predict", byteArray)); + + var result = resultEncoded.Select(Convert.ToByte).ToArray(); + + var resultColor = result.Select(x => + allowedFlags[x]==1 + ? (byte)255 + : (byte)0) + .ToArray(); + + // black and white mask + var mask = new Mat(_imageHeight, _imageWidth, MatType.CV_8UC1, resultColor); + + var maskResized = mask.Resize(new Size(currentImage.ImageData.Width, currentImage.ImageData.Height)); + + context.ActiveImage = new HawkeyeImage() {ImageData = maskResized}; + + + Result = true; + + + } + + public override void SetParameters(Dictionary parameters) + { + base.SetParameters(parameters); + _initialized = false; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(FilterClasses); + bw.Write(ModelFilePath.Path); + + } + + public override void Load(BinaryReader br) + { + base.Load(br); + FilterClasses = br.ReadString(); + ModelFilePath.Path = br.ReadString(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs new file mode 100644 index 0000000..21f108b --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/Color128SimpleOperation.cs @@ -0,0 +1,36 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; +using System.Runtime.InteropServices; +using Hawkeye.VisionBuilder.Workflow.DataTransfer; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +[IgnoreOperation] +public class Color128SimpleOperation: ColorAIOperation +{ + public Color128SimpleOperation():base("bg128simple") + { + + } + + + public override string FilterClasses { get; set; } = "1"; + public override FilePath ModelFilePath { get; set; } = new FilePath(){Format = "Model file(*.h5)|*.h5"}; + + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(FilterClasses); + bw.Write(ModelFilePath.Path); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + FilterClasses = br.ReadString(); + ModelFilePath.Path = br.ReadString(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorAIOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorAIOperation.cs new file mode 100644 index 0000000..5a17478 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorAIOperation.cs @@ -0,0 +1,114 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; +using System.Runtime.InteropServices; +using Hawkeye.VisionBuilder.Workflow.DataTransfer; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[IgnoreOperation] +public abstract class ColorAIOperation:BaseOperation +{ + private readonly string _modelName; + + public ColorAIOperation(string model_name) + { + _modelName = model_name; + _cSharpDataTransferMQRPC = PythonModelProxyRPC.GetInterface(); + CanHaveProcessingError = false; + } + + private readonly CSharpDataTransferMQRPC _cSharpDataTransferMQRPC; + private int _imageWidth; + private int _imageHeight; + + private bool _initialized = false; + private FilePath _modelFilePath; + + + public virtual string FilterClasses { get; set; } + + public virtual FilePath ModelFilePath + { + get => _modelFilePath; + set + { + _modelFilePath = value; + _initialized = false; + } + } + + public bool IsRaw { get; set; } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckColorful(context)) return; + + if (!_initialized) + { + if (!File.Exists(ModelFilePath.Path)) + { + this.SetError("Model file not found"); + return; + } + _cSharpDataTransferMQRPC.TransferData(new MethodCall("load_model", ModelFilePath.Path, _modelName)); + _initialized = true; + } + + + + _cSharpDataTransferMQRPC.TransferData(new MethodCall("activate_model", _modelName)); + + var sizeEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall("get_accept_size")); + var size = sizeEncoded.Select(Convert.ToInt32).ToArray(); + _imageWidth = size[1]; + _imageHeight = size[2]; + + + var currentImage = context.ActiveImage; + + var rightColor = currentImage.ImageData.CvtColor(ColorConversionCodes.BGR2RGB); + var resized = rightColor.Resize(new Size(_imageHeight, _imageWidth)); + int dataSize = resized.Rows * resized.Cols * resized.ElemSize(); + var byteArray = new byte[dataSize]; + Marshal.Copy(resized.Data, byteArray, 0, dataSize); + + var allowed = FilterClasses.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(x => Convert.ToInt32(x)).ToArray(); + + var allowedFlags = new byte[128]; + foreach (var i in allowed) + { + allowedFlags[i] = 1; + } + + var resultEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall(IsRaw?"predict_raw": "predict", byteArray)); + + var result = resultEncoded.Select(Convert.ToByte).ToArray(); + + byte[] resultColor; + if (IsRaw) + { + resultColor = result.Chunk(25).Select(x => + { + return x.Select((x, i) => allowedFlags[i] == 1 ? (byte) (x) : (byte) 0).Max(); + }).ToArray(); + //resultColor = result.Select(x => (byte)(x)).ToArray(); + } + else + { + resultColor = result.Select(x => allowedFlags[x]==1? (byte)(x * 255) : (byte)0).ToArray(); + } + + + // black and white mask + var mask = new Mat(_imageHeight, _imageWidth, MatType.CV_8UC1, resultColor); + + var maskResized = mask.Resize(new Size(currentImage.ImageData.Width, currentImage.ImageData.Height)); + + context.ActiveImage = new HawkeyeImage() { ImageData = maskResized }; + + + Result = true; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs new file mode 100644 index 0000000..f662f56 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ColorModelOperation.cs @@ -0,0 +1,42 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +public class ColorModelOperation : ColorAIOperation +{ + public ColorModelOperation() : base("office2") + { + IsRaw = true; + } + + + public override string FilterClasses { get; set; } = "1"; + public override FilePath ModelFilePath { get; set; } = new FilePath() { Format = "Model file(*.h5)|*.h5" }; + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(FilterClasses); + // get current directory + var dir = Directory.GetCurrentDirectory(); + // get relative path + var relPath = Path.GetRelativePath(dir, ModelFilePath.Path); + + + bw.Write(relPath); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + FilterClasses = br.ReadString(); + ModelFilePath.Path = br.ReadString(); + // get current directory + var dir = Directory.GetCurrentDirectory(); + // get absolute path + var absPath = Path.GetFullPath(Path.Combine(dir, ModelFilePath.Path)); + ModelFilePath.Path = absPath; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs new file mode 100644 index 0000000..57b7902 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/ModelAIOperation.cs @@ -0,0 +1,159 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using OpenCvSharp; +using System.Runtime.InteropServices; +using Hawkeye.VisionBuilder.Workflow.DataTransfer; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +public class ModelAIOperation: BaseOperation +{ + + + private readonly string _modelName; + + public ModelAIOperation() + { + + _cSharpDataTransferMQRPC = PythonModelProxyRPC.GetInterface(); + CanHaveProcessingError = false; + } + + private readonly CSharpDataTransferMQRPC _cSharpDataTransferMQRPC; + private int _imageWidth; + private int _imageHeight; + + + + public string FilterClasses { get; set; } = "1"; + + private bool _initialized = false; + private FilePath _modelFilePath=new FilePath(); + + + public FilePath ModelFilePath + { + get => _modelFilePath; + set + { + _modelFilePath = value; + _initialized = false; + } + } + + public bool IsCategorical { get; set; } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckColorful(context)) return; + + if (!_initialized) + { + if (!File.Exists(ModelFilePath?.Path)) + { + this.SetError("Model file not found"); + return; + } + + // is relative path + if (ModelFilePath.Path.StartsWith(".")) + { + var dir = Directory.GetCurrentDirectory(); + var absPath = Path.GetFullPath(Path.Combine(dir, ModelFilePath.Path)); + ModelFilePath.Path = absPath; + } + + _cSharpDataTransferMQRPC.TransferData(new MethodCall("load_model", ModelFilePath.Path, _modelName)); + _initialized = true; + } + + + + _cSharpDataTransferMQRPC.TransferData(new MethodCall("activate_model", _modelName)); + + var sizeEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall("get_accept_size")); + var size = sizeEncoded.Select(Convert.ToInt32).ToArray(); + _imageWidth = size[1]; + _imageHeight = size[2]; + + var outputSize = _cSharpDataTransferMQRPC.TransferData(new MethodCall("get_output_size")); + var outputSizeArray = outputSize.Select(Convert.ToInt32).ToArray(); + + int classes; + if (outputSizeArray.Length<3) + { + classes=1; + } + else + { + classes = outputSizeArray[2]; + } + + + + var currentImage = context.ActiveImage; + + var rightColor = currentImage.ImageData.CvtColor(ColorConversionCodes.BGR2RGB); + var resized = rightColor.Resize(new Size(_imageHeight, _imageWidth)); + int dataSize = resized.Rows * resized.Cols * resized.ElemSize(); + var byteArray = new byte[dataSize]; + Marshal.Copy(resized.Data, byteArray, 0, dataSize); + + var allowed = FilterClasses.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(x => Convert.ToInt32(x)).ToArray(); + + var allowedFlags = new byte[128]; + foreach (var i in allowed) + { + allowedFlags[i] = 1; + } + + var resultEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall(IsCategorical ? "predict_raw" : "predict", byteArray)); + + var result = resultEncoded.Select(Convert.ToByte).ToArray(); + // todo: copy and name RawModelAIOperation + byte[] resultColor; + if (IsCategorical) + { + resultColor = result.Chunk(classes).Select(x => + { + return x.Select((x, i) => allowedFlags[i] == 1 ? (byte)(x) : (byte)0).Max(); + }).ToArray(); + + } + else + { + resultColor = result.Select(x => allowedFlags[x] == 1 ? (byte)(x * 255) : (byte)0).ToArray(); + } + + + // black and white mask + var mask = new Mat(outputSizeArray[0], outputSizeArray[1], MatType.CV_8UC1, resultColor); + + var maskResized = mask.Resize(new Size(currentImage.ImageData.Width, currentImage.ImageData.Height)); + + context.ActiveImage = new HawkeyeImage() { ImageData = maskResized }; + + + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(_modelFilePath.Path); + bw.Write(_modelFilePath.Format); + bw.Write(FilterClasses); + bw.Write(IsCategorical); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + _modelFilePath.Path = br.ReadString(); + _modelFilePath.Format = br.ReadString(); + FilterClasses = br.ReadString(); + IsCategorical = br.ReadBoolean(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs new file mode 100644 index 0000000..babfa78 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/MultichannelAI.cs @@ -0,0 +1,148 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Hawkeye.VisionBuilder.Workflow.DataTransfer; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +public class MultichannelAI: BaseOperation +{ + + private readonly string _modelName; + + public MultichannelAI() + { + + _cSharpDataTransferMQRPC = PythonModelProxyRPC.GetInterface(); + CanHaveProcessingError = false; + } + + private readonly CSharpDataTransferMQRPC _cSharpDataTransferMQRPC; + private int _imageWidth; + private int _imageHeight; + + + + + + private bool _initialized = false; + private FilePath _modelFilePath = new FilePath(); + + + public FilePath ModelFilePath + { + get => _modelFilePath; + set + { + _modelFilePath = value; + _initialized = false; + } + } + + + + protected unsafe override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckColorful(context)) return; + + if (!_initialized) + { + if (!File.Exists(ModelFilePath?.Path)) + { + this.SetError("Model file not found"); + return; + } + _cSharpDataTransferMQRPC.TransferData(new MethodCall("load_model", ModelFilePath.Path, _modelName)); + _initialized = true; + } + + + + _cSharpDataTransferMQRPC.TransferData(new MethodCall("activate_model", _modelName)); + + var sizeEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall("get_accept_size")); + var size = sizeEncoded.Select(Convert.ToInt32).ToArray(); + _imageWidth = size[1]; + _imageHeight = size[2]; + + var outputSize = _cSharpDataTransferMQRPC.TransferData(new MethodCall("get_output_size")); + var outputSizeArray = outputSize.Select(Convert.ToInt32).ToArray(); + + int classes; + if (outputSizeArray.Length < 3) + { + classes = 1; + } + else + { + classes = outputSizeArray[2]; + } + + + + var currentImage = context.ActiveImage; + + var rightColor = currentImage.ImageData.CvtColor(ColorConversionCodes.BGR2RGB); + var resized = rightColor.Resize(new Size(_imageHeight, _imageWidth)); + int dataSize = resized.Rows * resized.Cols * resized.ElemSize(); + var byteArray = new byte[dataSize]; + Marshal.Copy(resized.Data, byteArray, 0, dataSize); + + Mat[] channels = new Mat[classes]; + for (int i = 0; i < classes; i++) + { + channels[i] = new Mat(_imageHeight, _imageWidth, MatType.CV_8UC1); + } + + var resultEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall("predict_raw", byteArray)); + + var result = resultEncoded.Select(Convert.ToByte).ToArray(); + + var chunked = result.Chunk(classes).ToArray(); + + int pixel=0; + foreach (var chunk in chunked) + { + for (int i = 0; i < classes; i++) + { + var data = (byte*)channels[i].Data.ToPointer(); + var coordX = (int)(pixel / _imageWidth); + var coordY = pixel % _imageWidth; + data[coordX * _imageWidth + coordY] = chunk[i]; + } + pixel++; + } + + //Cv2.ImShow("0", channels[0]); + //Cv2.ImShow("1", channels[1]); + + using var res = new Mat(); + Cv2.Merge(channels,res); + + var resResized = res.Resize(new Size(currentImage.ImageData.Width, currentImage.ImageData.Height)); + context.ActiveImage = new HawkeyeImage() {ImageData = resResized }; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(_modelFilePath.Path); + bw.Write(_modelFilePath.Format); + + + } + + public override void Load(BinaryReader br) + { + base.Load(br); + _modelFilePath.Path = br.ReadString(); + _modelFilePath.Format = br.ReadString(); + + + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs new file mode 100644 index 0000000..be8ad84 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/RawModelAIOperation.cs @@ -0,0 +1,150 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using OpenCvSharp; +using System.Runtime.InteropServices; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using Hawkeye.VisionBuilder.Workflow.DataTransfer; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +public class RawModelAIOperation: BaseOperation +{ + + + private string _modelName; + + public RawModelAIOperation() + { + + _cSharpDataTransferMQRPC = PythonModelProxyRPC.GetInterface(); + CanHaveProcessingError = false; + } + + private CSharpDataTransferMQRPC _cSharpDataTransferMQRPC; + private int _imageWidth; + private int _imageHeight; + + + + public string FilterClasses { get; set; } = "1"; + + private bool _initialized = false; + private FilePath _modelFilePath=new FilePath(); + + + public FilePath ModelFilePath + { + get => _modelFilePath; + set + { + _modelFilePath = value; + _initialized = false; + } + } + + public bool IsCategorical { get; set; } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckColorful(context)) return; + + if (!_initialized) + { + if (!File.Exists(ModelFilePath?.Path)) + { + this.SetError("Model file not found"); + return; + } + + _modelName = Path.GetFileNameWithoutExtension(ModelFilePath.Path); + + // is relative path + if (ModelFilePath.Path.StartsWith(".")) + { + var dir = Directory.GetCurrentDirectory(); + var absPath = Path.GetFullPath(Path.Combine(dir, ModelFilePath.Path)); + ModelFilePath.Path = absPath; + } + + _cSharpDataTransferMQRPC.TransferData(new MethodCall("load_model", ModelFilePath.Path, _modelName)); + _initialized = true; + } + + + + _cSharpDataTransferMQRPC.TransferData(new MethodCall("activate_model", _modelName)); + + var sizeEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall("get_accept_size")); + var size = sizeEncoded.Select(Convert.ToInt32).ToArray(); + _imageWidth = size[1]; + _imageHeight = size[2]; + + var outputSize = _cSharpDataTransferMQRPC.TransferData(new MethodCall("get_output_size")); + var outputSizeArray = outputSize.Select(Convert.ToInt32).ToArray(); + + int classes; + if (outputSizeArray.Length<3) + { + classes=1; + } + else + { + classes = outputSizeArray[2]; + } + + + + var currentImage = context.ActiveImage; + + var rightColor = currentImage.ImageData.CvtColor(ColorConversionCodes.BGR2RGB); + var resized = rightColor.Resize(new Size(_imageHeight, _imageWidth)); + int dataSize = resized.Rows * resized.Cols * resized.ElemSize(); + var byteArray = new byte[dataSize]; + Marshal.Copy(resized.Data, byteArray, 0, dataSize); + + var allowed = FilterClasses.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(x => Convert.ToInt32(x)).ToArray(); + + var allowedFlags = new byte[128]; + foreach (var i in allowed) + { + allowedFlags[i] = 1; + } + + var resultEncoded = _cSharpDataTransferMQRPC.TransferData(new MethodCall("predict_raw", byteArray)); + + var result = resultEncoded.Select(Convert.ToByte).ToArray(); + + byte[] resultColor; + resultColor = result.Select(x => (byte)(x) ).ToArray(); + + + // black and white mask + var mask = new Mat(outputSizeArray[0], outputSizeArray[1], MatType.CV_8UC1, resultColor); + + var maskResized = mask.Resize(new Size(currentImage.ImageData.Width, currentImage.ImageData.Height)); + + context.ActiveImage = new HawkeyeImage() { ImageData = maskResized }; + + + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(_modelFilePath.Path); + bw.Write(_modelFilePath.Format); + bw.Write(FilterClasses); + bw.Write(IsCategorical); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + _modelFilePath.Path = br.ReadString(); + _modelFilePath.Format = br.ReadString(); + FilterClasses = br.ReadString(); + IsCategorical = br.ReadBoolean(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs new file mode 100644 index 0000000..d382089 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloDetectionOperation.cs @@ -0,0 +1,142 @@ +using System.Diagnostics; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using static IronPython.Runtime.Profiler; +using System.Runtime.InteropServices; +using System.Text; +using Compunet.YoloV8; +using Compunet.YoloV8.Data; +using OpenCvSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; +[Category("AI")] +public class YoloDetectionOperation:BaseOperation +{ + + private bool _initialized = false; + private FilePath _modelFilePath = new FilePath(){ Format = "Onnx format(*.onnx)|*.onnx" }; + + + public FilePath ModelFilePath + { + get => _modelFilePath; + set + { + _modelFilePath = value; + _initialized = false; + } + } + + bool EnsureInitialized() + { + if (!_initialized) + { + if (!File.Exists(ModelFilePath?.Path)) + { + this.SetError("Model file not found"); + return false; + } + _predictor = YoloV8Predictor.Create(ModelFilePath?.Path); + + _predictor.Configuration.SuppressParallelInference=true; + _initialized = true; + } + return true; + } + byte[] _buffer=new byte[0]; + private YoloV8Predictor _predictor; + + + void EnsureBuffer(int size) + { + if (_buffer.Length != size) + { + _buffer = new byte[size]; + } + } + protected override void InterpretInternal(Context context) + { + CheckColorful(context); + try + { + if (!EnsureInitialized()) return; + var swImagePreparation = Stopwatch.StartNew(); + + var image = context.ActiveImage.ImageData.CvtColor(ColorConversionCodes.BGR2RGB); + EnsureBuffer(image.Cols * image.Rows * image.Channels()); + Marshal.Copy(image.Data, _buffer, 0, _buffer.Length); + Image img = SixLabors.ImageSharp.Image.LoadPixelData(_buffer, image.Width, image.Height); + // convert to rgb24 + + + swImagePreparation.Stop(); + + var swPrediction = Stopwatch.StartNew(); + + + + var result = _predictor.Detect(img); + + + + swPrediction.Stop(); + + + var swPostProcessing = Stopwatch.StartNew(); + Mat res = new Mat(image.Size(), MatType.CV_8UC1, new Scalar(0)); + + StringBuilder sb = new StringBuilder(); + Dictionary classes = new Dictionary(); + + + + foreach (BoundingBox box in result.Boxes.OrderBy(x=>x.Class.Id).ToList()) + { + if (box.Confidence<0.7) + { + continue; + } + + if (box.Class.Id + 1 == 10) + { + + } + classes[box.Class.Id + 1] = box.Class.Name; + var rect = box.Bounds; + res.Rectangle(new OpenCvSharp.Rect(rect.Left, rect.Top, rect.Width, rect.Height), + new Scalar(box.Class.Id + 1), -1); + } + swPostProcessing.Stop(); + + + foreach (var c in classes) + { + sb.AppendLine($"{c.Key}={c.Value};"); + } + + Status = sb.ToString(); + + context.ActiveImage = new HawkeyeImage() {ImageData = res}; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(ModelFilePath.Path); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + ModelFilePath.Path = br.ReadString(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs new file mode 100644 index 0000000..05824da --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/AI/YoloPickDetected.cs @@ -0,0 +1,115 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Poly; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +using OpenCvSharp; +using static Hawkeye.VisionBuilder.Workflow.Operations.FindManyBlobsOperation; +using Point = OpenCvSharp.Point; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.AI; + +[Category("AI")] +public class YoloPickDetected:BaseOperation +{ + public string SlotName { get; set; } = "Image"; + public int TypeId { get; set; } + + public int MinArea { get; set; } = 0; + public int MaxArea { get; set; } = 99999; + public int MinWidth { get; set; } = 0; + public int MaxWidth { get; set; } = 99999; + public int MinHeight { get; set; } = 0; + public int MaxHeight { get; set; } = 99999; + + + protected override void InterpretInternal(Context context) + { + Status = "No blobs found"; + var image=context.Memory[SlotName]; + var threshold= image.ImageData.InRange(TypeId, TypeId); + var contours = threshold + .FindContoursAsArray(RetrievalModes.List, ContourApproximationModes.ApproxSimple); + + //case: no blobs found + if (contours.Length == 0) + { + + return; + } + + var validContours = new List(); + + foreach (var contour in contours) + { + var boundingRect = Cv2.BoundingRect(contour); + var area = Cv2.ContourArea(contour); + if (boundingRect.Width >= MinWidth && boundingRect.Width <= MaxWidth && + boundingRect.Height >= MinHeight && boundingRect.Height <= MaxHeight && + area >= MinArea && area <= MaxArea + ) + { + validContours.Add(contour); + } + } + + int badCount = 0; + foreach (var contourMatch in validContours) + { + PolyElement poly = new PolyElement() { IsGood = false }; + + poly.Points = contourMatch.Select(x => new Vector2(x.X, x.Y)).ToArray(); + + context.GraphicsElements.Add(poly); + badCount++; + } + + + + if (badCount > 0) + { + // find smallest contour size + var smallest = validContours.OrderBy(x => Cv2.ContourArea(x)).First(); + var smallestBoundingRect = Cv2.BoundingRect(smallest); + var biggest = validContours.OrderBy(x => Cv2.ContourArea(x)).Last(); + var biggestBoundingRect = Cv2.BoundingRect(biggest); + var smallestArea = Cv2.ContourArea(smallest); + var biggestArea = Cv2.ContourArea(biggest); + Status = $"Found: {badCount} Min area:{smallestArea} Max area:{biggestArea}"; + + + + Result = false; + return; + } + + Result = true; + + + + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(SlotName); + bw.Write(TypeId); + bw.Write(MinWidth); + bw.Write(MaxWidth); + bw.Write(MinHeight); + bw.Write(MaxHeight); + bw.Write(MinArea); + bw.Write(MaxArea); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + SlotName = br.ReadString(); + TypeId = br.ReadInt32(); + MinWidth = br.ReadInt32(); + MaxWidth = br.ReadInt32(); + MinHeight = br.ReadInt32(); + MaxHeight = br.ReadInt32(); + MinArea = br.ReadInt32(); + MaxArea = br.ReadInt32(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/ActivateImageOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/ActivateImageOperation.cs new file mode 100644 index 0000000..b07d69f --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/ActivateImageOperation.cs @@ -0,0 +1,13 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; + +namespace Hawkeye.VisionBuilder.Workflow.Operations; + +public class ActivateImageOperation:BaseOperation +{ + public ScriptValue ImageSource { get; set; } = new ScriptValue(); + + protected override void InterpretInternal(Context context) + { + context.ActiveImage = GetScriptValue(ImageSource); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/Category.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/Category.cs new file mode 100644 index 0000000..904281f --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/Category.cs @@ -0,0 +1,12 @@ +namespace Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public class Category:Attribute +{ + public string Name { get; } + + public Category(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/IgnoreOperationAttribute.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/IgnoreOperationAttribute.cs new file mode 100644 index 0000000..bf9b613 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/IgnoreOperationAttribute.cs @@ -0,0 +1,7 @@ +namespace Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +[AttributeUsage(AttributeTargets.Class,Inherited = false)] +public class IgnoreOperationAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/NotForToolAttribute.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/NotForToolAttribute.cs new file mode 100644 index 0000000..d330186 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/NotForToolAttribute.cs @@ -0,0 +1,7 @@ +namespace Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +[AttributeUsage(AttributeTargets.Property)] +public class NotForToolAttribute:Attribute +{ + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/RestrictReferenceAttribute.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/RestrictReferenceAttribute.cs new file mode 100644 index 0000000..ff610e4 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Attributes/RestrictReferenceAttribute.cs @@ -0,0 +1,12 @@ +namespace Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +[AttributeUsage(AttributeTargets.Property)] +public class RestrictReferenceAttribute:Attribute +{ + public RestrictReferenceAttribute(Type type) + { + Type = type; + } + + public Type Type { get; set; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs new file mode 100644 index 0000000..110081f --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/CannyOperation.cs @@ -0,0 +1,38 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Basic; + +[Category("Basic")] +public class CannyOperation:BaseOperation +{ + public int Threshold1 { get; set; } = 100; + public int Threshold2 { get; set; } = 200; + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + + context.ActiveImage = new HawkeyeImage() + { + ImageData = context.ActiveImage.ImageData.Canny(Threshold1, Threshold2) + }; + + Status = $"Canny with Threshold1={Threshold1} and Threshold2={Threshold2}"; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(Threshold1); + bw.Write(Threshold2); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + Threshold1 = br.ReadInt32(); + Threshold2 = br.ReadInt32(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs new file mode 100644 index 0000000..c8a9378 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/DetectionPaddingOperation.cs @@ -0,0 +1,55 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Basic; + +[Category("Basic")] +public class DetectionPaddingOperation:BaseOperation +{ + public int HorizontalPadding { get; set; } + public int VerticalPadding { get; set; } + + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckGrayscale(context)) return; + + // fill edges with black on top and bottom without changing the image size + var image = context.ActiveImage; + var newImage = new Mat(image.ImageData.Rows, image.ImageData.Cols, MatType.CV_8UC1, Scalar.Black); + + + + var mask = new Mat(image.ImageData.Rows, image.ImageData.Cols, MatType.CV_8UC1, Scalar.Black); + var maskRoi = mask[VerticalPadding, image.ImageData.Rows - VerticalPadding, HorizontalPadding, image.ImageData.Cols - HorizontalPadding]; + //fill the mask with white + maskRoi.SetTo(Scalar.White); + + image.ImageData.CopyTo(newImage, mask); + + mask.Dispose(); + maskRoi.Dispose(); + context.ActiveImage = new HawkeyeImage() + { + ImageData = newImage + }; + + + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(HorizontalPadding); + bw.Write(VerticalPadding); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + HorizontalPadding = br.ReadInt32(); + VerticalPadding = br.ReadInt32(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs new file mode 100644 index 0000000..fc19b14 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/GaussianBlurOperation.cs @@ -0,0 +1,50 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Basic; + +[Category("Basic")] +public class GaussianBlurOperation : BaseOperation +{ + public ScriptValue KernelSize { get; set; } = new ScriptValue() { Script = "3" }; + + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + var kernelSize = GetScriptValue(KernelSize); + if (kernelSize < 1||IsEven(kernelSize)) + { + Status = $"{nameof(KernelSize)} should be more than 0 and be odd number"; + Result = false; + return; + } + + context.ActiveImage = new HawkeyeImage() + { + ImageData = context.ActiveImage.ImageData.GaussianBlur(new OpenCvSharp.Size(kernelSize, kernelSize), 0, 0) + }; + Status = $"Gaussian blur with kernel size {KernelSize}"; + Result = true; + } + + private bool IsEven(int kernelSize) + { + return kernelSize % 2 == 0; + } + + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(KernelSize.Script); + + } + + public override void Load(BinaryReader br) + { + base.Load(br); + KernelSize.Script = br.ReadString(); + + } +} diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs new file mode 100644 index 0000000..f018414 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/PorabollisticHoughOperation.cs @@ -0,0 +1,54 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Line; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp.Extensions; +using Point = OpenCvSharp.Point; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Basic; + +[Category("Basic")] +public class PorabollisticHoughOperation:BaseOperation +{ + public double DistanceResolution { get; set; } = 1; + public double AngleResolution { get; set; } = Math.PI / 180.0; + public int Threshold { get; set; } = 50; + public int MinLineLength { get; set; } = 50; + public int MaxLineGap { get; set; } = 10; + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + + var segments = context.ActiveImage.ImageData.HoughLinesP(DistanceResolution, AngleResolution, Threshold, MinLineLength, MaxLineGap); + + foreach (var segment in segments) + { + context.GraphicsElements.Add(new LineElement() + { + Start = new Vector2(segment.P1.X, segment.P1.Y), + End = new Vector2(segment.P2.X, segment.P2.Y) + }); + } + + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(DistanceResolution); + bw.Write(AngleResolution); + bw.Write(Threshold); + bw.Write(MinLineLength); + bw.Write(MaxLineGap); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + DistanceResolution = br.ReadDouble(); + AngleResolution = br.ReadDouble(); + Threshold = br.ReadInt32(); + MinLineLength = br.ReadInt32(); + MaxLineGap = br.ReadInt32(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/SkeletonOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/SkeletonOperation.cs new file mode 100644 index 0000000..4627a3f --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/SkeletonOperation.cs @@ -0,0 +1,40 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Basic; + +[Category("Basic")] +public class SkeletonOperation:BaseOperation +{ + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if(!CheckGrayscale(context)) return; + + + var skeleton = new Mat(context.ActiveImage.ImageData.Size(), MatType.CV_8UC1, 0); + var temp = context.ActiveImage.ImageData.Clone(); + var element = Cv2.GetStructuringElement(MorphShapes.Cross, new OpenCvSharp.Size(3, 3)); + var image = context.ActiveImage.ImageData.Clone(); + + do + { + var eroded = image.Erode(element); + + temp =eroded.Dilate(element); + + Cv2.Subtract(image,temp, temp); + Cv2.BitwiseOr(skeleton, temp, skeleton); + + eroded.CopyTo(image); + } while (Cv2.CountNonZero(image) != 0); + + context.ActiveImage = new HawkeyeImage() + { + ImageData = skeleton + }; + + Result = true; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs new file mode 100644 index 0000000..055f1cc --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdMinMaxOperation.cs @@ -0,0 +1,47 @@ +namespace Hawkeye.VisionBuilder.Workflow.Operations.Basic; + +using global::Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + + + +[Category("Basic")] +public class ThresholdMinMaxOperation : BaseOperation +{ + public ScriptValue ThresholdMin { get; set; } = new ScriptValue() { Script = "128" }; + public ScriptValue ThresholdMax { get; set; } = new ScriptValue() { Script = "255" }; + + public ThresholdMinMaxOperation() + { + CanHaveProcessingError = false; + } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + + if (!CheckGrayscale(context)) return; + + context.ActiveImage = new HawkeyeImage() + { + ImageData = context.ActiveImage.ImageData.InRange(GetScriptValue(ThresholdMin), GetScriptValue(ThresholdMax)) + }; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(ThresholdMin.Script); + bw.Write(ThresholdMax.Script); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + ThresholdMin.Script = br.ReadString(); + ThresholdMax.Script = br.ReadString(); + } +} diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs new file mode 100644 index 0000000..02cb9f8 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Basic/ThresholdOperation.cs @@ -0,0 +1,44 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Basic; + +[Category("Basic")] +public class ThresholdOperation:BaseOperation +{ + + public ScriptValue ThresholdMin { get; set; } = new ScriptValue(){Script = "128"}; + + public ThresholdOperation() + { + CanHaveProcessingError = false; + } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + + if(!CheckGrayscale(context))return; + + context.ActiveImage = new HawkeyeImage() + { + ImageData = context.ActiveImage.ImageData.Threshold(GetScriptValue(ThresholdMin), 255,ThresholdTypes.Binary) + }; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(ThresholdMin.Script); + + } + + public override void Load(BinaryReader br) + { + base.Load(br); + ThresholdMin.Script = br.ReadString(); + + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Edge/SobelOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Edge/SobelOperation.cs new file mode 100644 index 0000000..aad4270 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Edge/SobelOperation.cs @@ -0,0 +1,28 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Edge; + +[Category("Edge")] +public class SobelOperation:BaseOperation +{ + protected override void InterpretInternal(Context context) + { + if(!CheckImageExists(context)) return; + var sobelY = context.ActiveImage!.ImageData.Sobel(MatType.CV_16S, 0, 1); + var sobelX = context.ActiveImage!.ImageData.Sobel(MatType.CV_16S, 1, 0); + + sobelX = sobelX.ConvertScaleAbs() * 0.5; + sobelY = sobelY.ConvertScaleAbs() * 0.5; + + sobelX += sobelY; + + context.ActiveImage = new HawkeyeImage() + { + ImageData = sobelX + }; + Status = "Sobel"; + Result = true; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs new file mode 100644 index 0000000..fb524b1 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/ColorProfileOperation.cs @@ -0,0 +1,146 @@ +using System.Diagnostics; +using System.Reflection; +using Hawkeye.VisionBuilder.Workflow.ColorStudioClasses; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Filters; + +[Category("Filters")] +public class ColorProfileOperation:BaseOperation +{ + private FilePath _colorProfilePath = new FilePath(){Format = "*.colorprofile|*.colorprofile" }; + + + bool _initialized = false; + private CategoryTree _categoryTree; + private CategoryItem _categoryItem; + private SelectFromList _category; + + + public Action RunColorStudio => RunColorStudioInternal; + + void RunColorStudioInternal() + { + var colorStudioPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"..\ColorStudio\Hawkeye.ColorStudio.exe"); + var process = Process.Start(colorStudioPath, new[]{ _colorProfilePath.Path,_lastImagePath,Category.Value}); + process?.WaitForExit(); + _initialized=false; + } + + + public ColorProfileOperation() + { + _category = new SelectFromList() + { + Value = "none", + GetList = ()=> GetCategories() + }; + } + + private List GetCategories() + { + var list = new List(); + if (_categoryTree != null) + { + list = _categoryTree.CategoryItems.Select(c => c.Name).ToList(); + } + return list; + } + + public SelectFromList Category + { + get => _category; + set + { + _category = value; + _initialized = false; + } + } + + public FilePath ColorProfilePath + { + get => _colorProfilePath; + set + { + _initialized=false; + _colorProfilePath = value; + } + } + + string _lastImagePath = ""; + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckColorful(context)) return; + _lastImagePath = context.ActiveImage?.Filename; + if (!_initialized) + { + if (!File.Exists(ColorProfilePath?.Path)) + { + this.SetError("Color profile file not found"); + + Result=false; + return; + } + _categoryTree = CategoryTree.LoadFromFile(ColorProfilePath.Path); + _categoryItem = _categoryTree.CategoryItems.FirstOrDefault(c => c.Name == Category.Value); + if (_categoryItem == null) + { + this.SetError("Category not found"); + Result=false; + return; + } + + _initialized = true; + } + var hsv = context.ActiveImage.ImageData.CvtColor(ColorConversionCodes.BGR2HSV_FULL); + var filters = _categoryItem.Filters; + + + int index = 0; + var resized = hsv.Resize(new Size(256,256),interpolation:InterpolationFlags.Nearest); + Mat res = new Mat(resized.Size(), MatType.CV_8UC1, (Scalar)0); + foreach (var filter in filters) + { + + filter.Transform(ref resized, ref res); + + index++; + } + var upsampled = res.Resize(hsv.Size(), interpolation:InterpolationFlags.Nearest); + + context.ActiveImage = new HawkeyeImage() {ImageData = upsampled }; + + + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(_colorProfilePath.Path); + bw.Write(_category.Value); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + _colorProfilePath.Path = EnsureRelativePath(br.ReadString()); + _category.Value = br.ReadString(); + _initialized = false; + } + + string EnsureRelativePath(string path) + { + if (path.StartsWith("..")) + { + return path; + } + path = Path.GetFullPath(path); + var dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + path = Path.GetRelativePath(dir, path); + return path; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs new file mode 100644 index 0000000..1881572 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/GaborFilterOperation.cs @@ -0,0 +1,97 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Filters; + +[Category("Filters")] +public class GaborFilterOperation : BaseOperation +{ + public GaborFilterOperation() + { + CanHaveProcessingError = false; + } + + public int KSize { get; set; } = 35; + public double Sigma { get; set; } = 3; + public int NumFilters { get; set; } = 1; + public double Lambda { get; set; } = 10; + public double Gamma { get; set; } = 0.5; + public double ThetaFromAngle { get; set; } = 0; + public double ThetaToAngle { get; set; } = 0; + + protected override void InterpretInternal(Context context) + { + Result = true; + if (!CheckImageExists(context)) return; + + var thetaFrom = ThetaFromAngle / 180 * Math.PI; + var thetaTo = ThetaToAngle / 180 * Math.PI; + + + double[] steps; + if (NumFilters == 1) + { + steps=new double[] {thetaFrom}; + } + else + { + // check theta not equal + if (thetaFrom == thetaTo) + { + Status = "ThetaFrom and ThetaTo must be different"; + Result = false; + return; + } + var step = (thetaTo - thetaFrom) / NumFilters; + + steps = Enumerable.Range(0, NumFilters).Select(i => thetaFrom + i * step).ToArray(); + } + + var image = context.ActiveImage.ImageData; + var newimage = new Mat(image.Size(), image.Type(), 0); + + foreach (double theta in steps) + { + var kernel = Cv2.GetGaborKernel(new Size(KSize, KSize), Sigma, theta, Lambda, Gamma, 0, MatType.CV_64F); + kernel /= 1 * kernel.Sum().Val0; + var filtered = new Mat(image.Size(), image.Type(), 0); + Cv2.Filter2D(image, filtered, -1, kernel); + Cv2.Max(newimage, filtered, newimage); + } + + context.ActiveImage = new HawkeyeImage() + { + ImageData = newimage + }; + + Result = true; + + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(KSize); + bw.Write(Sigma); + bw.Write(NumFilters); + bw.Write(Lambda); + bw.Write(Gamma); + bw.Write(ThetaFromAngle); + bw.Write(ThetaToAngle); + + } + + public override void Load(BinaryReader br) + { + base.Load(br); + KSize = br.ReadInt32(); + Sigma = br.ReadDouble(); + NumFilters = br.ReadInt32(); + Lambda = br.ReadDouble(); + Gamma = br.ReadDouble(); + ThetaFromAngle = br.ReadDouble(); + ThetaToAngle = br.ReadDouble(); + + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs new file mode 100644 index 0000000..84b4a47 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/LUTFilterOperation.cs @@ -0,0 +1,60 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Filters; +[Category("Filters")] +public class LUTFilterOperation : BaseOperation +{ + + + + public LutData LutData { get; set; } = new LutData(); + + [NotForTool] + public HawkeyeImage LastImage { get; set; } = null; + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + + LastImage = context.ActiveImage; + var mat = context.ActiveImage.ImageData; + if (mat.Channels() == 3) + { + Mat[] tmp = new Mat[3]; + Span span = new Span(LutData.LUT); + for (int i = 0; i < 3; i++) + { + var channel = mat.ExtractChannel(i); + + tmp[i] = channel.LUT(span[(i * 256)..((i + 1) * 256)].ToArray()); + } + + mat = new Mat(mat.Size(), mat.Type()); + Cv2.Merge(tmp, mat); + + } + + if (mat.Channels() == 1) + { + Span span = new Span(LutData.LUT); + mat = mat.LUT(span[0..256].ToArray()); + } + context.ActiveImage = new HawkeyeImage() { ImageData = mat }; + Status = ""; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + LutData.Serialize(bw); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + LutData.Deserialize(br); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs new file mode 100644 index 0000000..891173e --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/NoiseFilterOperation.cs @@ -0,0 +1,41 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations; + +[Category("Filters")] +public class NoiseFilterOperation:BaseOperation +{ + public int NoiseSize { get; set; } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if(!CheckGrayscale(context))return; + if (NoiseSize < 1) + { + Status = $"{nameof(NoiseSize)} should be more than 0"; + Result = false; + return; + } + context.ActiveImage = new HawkeyeImage() + { + ImageData = context.ActiveImage.ImageData.MorphologyEx(MorphTypes.Open, Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(NoiseSize, NoiseSize))) + }; + Status = $"Remove noise with size less than {NoiseSize}"; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(NoiseSize); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + NoiseSize = br.ReadInt32(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs new file mode 100644 index 0000000..3eaa9a8 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Filters/RangeFilterOperation.cs @@ -0,0 +1,55 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Filters; + +[Category("Filters")] +public class RangeFilterOperation: BaseOperation +{ + public RangeFilterOperation() + { + CanHaveProcessingError = false; + } + + public int C1Min { get; set; } + public int C1Max { get; set; } + public int C2Min { get; set; } + public int C2Max { get; set; } + public int C3Min { get; set; } + public int C3Max { get; set; } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + var mat = context.ActiveImage.ImageData; + var res = mat.InRange(new Scalar(C1Min, C2Min, C3Min), new Scalar(C1Max, C2Max, C3Max)); + context.ActiveImage = new HawkeyeImage() { ImageData = res }; + Status = ""; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(C1Min); + bw.Write(C1Max); + bw.Write(C2Min); + bw.Write(C2Max); + bw.Write(C3Min); + bw.Write(C3Max); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + C1Min = br.ReadInt32(); + C1Max = br.ReadInt32(); + C2Min = br.ReadInt32(); + C2Max = br.ReadInt32(); + C3Min = br.ReadInt32(); + C3Max = br.ReadInt32(); + } + + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs new file mode 100644 index 0000000..7e1e760 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs @@ -0,0 +1,71 @@ +using Hawkeye.VisionBuilder.Workflow.Configuration; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Links; + + +using OpenCvSharp; +using OpenCvSharp.Extensions; +using Rectangle = Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle.RectangleElement; + +namespace Hawkeye.VisionBuilder.Workflow.Operations; + +public class GetImageOperation:BaseOperation,IHaveImage +{ + private readonly WorkflowList _workflowList; + + + + public GetImageOperation(WorkflowList workflowList) + { + _workflowList = workflowList; + } + + public HawkeyeImage? Image { get; set; } + + + protected override void InterpretInternal(Context context) + { + + var image = _workflowList.ImageSource.GetImage(CancellationToken.None).Result; + + + + + context.ActiveImage = new HawkeyeImage(){ImageData = image}; + context.LastCameraImage = context.ActiveImage; + Image = context.ActiveImage; + + if (CheckImageExists(context)) + { + Status= $"Image size: {context.ActiveImage.ImageData.Width}x{context.ActiveImage.ImageData.Height}"; + Result = true; + } + else + { + Status = "No image"; + Result = false; + } + + } + + + /// + /// Save the operation to the binary writer + /// + /// + public override void Save(BinaryWriter bw) + { + bw.Write(Id.ToString()); + bw.Write(Label); + } + + /// + /// Load the operation from the binary reader + /// + /// + public override void Load(BinaryReader br) + { + Id = new Guid(br.ReadString()); + Label = br.ReadString(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Image/ConvertOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ConvertOperation.cs new file mode 100644 index 0000000..ee5c7e6 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ConvertOperation.cs @@ -0,0 +1,44 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Image; + +[Category("Image")] +public class ConvertOperation: BaseOperation +{ + public enum EConversionType + { + RGBToHSV, + HSVToRGB, + } + public ConvertOperation() + { + CanHaveProcessingError = false; + } + + public EConversionType ConversionType { get; set; } + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + var mat = context.ActiveImage.ImageData; + Mat res; + switch (ConversionType) + { + case EConversionType.RGBToHSV: + res = mat.CvtColor(OpenCvSharp.ColorConversionCodes.BGR2HSV); + break; + case EConversionType.HSVToRGB: + res = mat.CvtColor(OpenCvSharp.ColorConversionCodes.HSV2BGR); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + context.ActiveImage = new HawkeyeImage() { ImageData = res}; + Status = ""; + Result = true; + } + + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs new file mode 100644 index 0000000..61f611d --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Image/GetChannelOperation.cs @@ -0,0 +1,38 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Image; + +[Category("Image")] +public class GetChannelOperation: BaseOperation +{ + public int Channel { get; set; } + + public GetChannelOperation() + { + CanHaveProcessingError = false; + } + + protected override void InterpretInternal(Context context) + { + var channel = context.ActiveImage.ImageData.ExtractChannel(Channel); + context.ActiveImage = new HawkeyeImage() {ImageData = channel}; + } + + public override string ToString() + { + return $"Get channel {Channel}"; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(Channel); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + Channel = br.ReadInt32(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeOperation.cs new file mode 100644 index 0000000..40b6a44 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeOperation.cs @@ -0,0 +1,29 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Image; + +[Category("Image")] +public class ImageSizeOperation : BaseOperation +{ + + public ImageSizeOperation() + { + CanHaveProcessingError = false; + } + public int Width { get; set; } = 450; + public int Height { get; set; } = 352; + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + + context.ActiveImage = new HawkeyeImage() + { + ImageData = context.ActiveImage.ImageData.Resize(new OpenCvSharp.Size(Width, Height)) + }; + + Status = $"Image resized to {Width}x{Height}"; + Result = true; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs new file mode 100644 index 0000000..aa60361 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ImageSizeProportionOperation.cs @@ -0,0 +1,44 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Image; + +[Category("Image")] +public class ImageSizeProportionOperation:BaseOperation +{ + public ImageSizeProportionOperation() + { + CanHaveProcessingError = false; + } + + public double Width { get; set; } = 0.25; + public double Height { get; set; } = 0.25; + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + + context.ActiveImage = new HawkeyeImage() + { + ImageData = context.ActiveImage!.ImageData.Resize(Size.Zero, Width, Height) + }; + + Status = $"Image resized to {context.ActiveImage.ImageData.Width}x{context.ActiveImage.ImageData.Height}"; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(Width); + bw.Write(Height); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + Width = br.ReadDouble(); + Height = br.ReadDouble(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Image/InvertOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Image/InvertOperation.cs new file mode 100644 index 0000000..5d0affc --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Image/InvertOperation.cs @@ -0,0 +1,31 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Image; + +[Category("Image")] +public class InvertOperation:BaseOperation +{ + + public InvertOperation() + { + CanHaveProcessingError = false; + } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + + + var res = new Mat(context.ActiveImage.ImageData.Size(), context.ActiveImage.ImageData.Type(), 0); + Cv2.BitwiseNot(context.ActiveImage.ImageData, res); + context.ActiveImage = new HawkeyeImage() + { + ImageData = res + }; + + Status = $"Image inverted"; + Result = true; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Image/ToGrayscaleOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ToGrayscaleOperation.cs new file mode 100644 index 0000000..f3ffc83 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Image/ToGrayscaleOperation.cs @@ -0,0 +1,25 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Image; + +[Category("Image")] +public class ToGrayscaleOperation : BaseOperation +{ + public ToGrayscaleOperation() + { + CanHaveProcessingError = false; + } + public HawkeyeImage? Image { get; set; } + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckColorful(context)) return; + + context.ActiveImage = new HawkeyeImage() { ImageData = context.ActiveImage.ImageData.CvtColor(ColorConversionCodes.BGR2GRAY) }; + Status = "Convert image to grayscale"; + Result = true; + Image = context.ActiveImage; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs new file mode 100644 index 0000000..07afbd7 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageFromMemoryOperation.cs @@ -0,0 +1,42 @@ +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Memory; + +[Category("Memory")] +public class ImageFromMemoryOperation:BaseOperation +{ + public ImageFromMemoryOperation() + { + CanHaveProcessingError = false; + } + + public string SlotName { get; set; } = "Image"; + + + protected override void InterpretInternal(Context context) + { + if (context.Memory.ContainsKey(SlotName)) + { + context.ActiveImage = context.Memory[SlotName]; + Status = "Image from slot "+SlotName; + Result = true; + } + else + { + Status = "Slot not found"; + Result = false; + } + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(SlotName); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + SlotName = br.ReadString(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs new file mode 100644 index 0000000..97f2b1f --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Memory/ImageToMemoryOperation.cs @@ -0,0 +1,36 @@ +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Memory; + +[Category("Memory")] +public class ImageToMemoryOperation:BaseOperation +{ + public ImageToMemoryOperation() + { + CanHaveProcessingError = false; + } + + public string SlotName { get; set; } = "Image"; + + protected override void InterpretInternal(Context context) + { + if(!CheckImageExists(context)) return; + + context.Memory[SlotName] = context.ActiveImage!; + Status = "Image to slot "+SlotName; + + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(SlotName); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + SlotName = br.ReadString(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs new file mode 100644 index 0000000..2dada3f --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/ClosingOperation.cs @@ -0,0 +1,43 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Morphology; + +[Category("Morphology")] +public class ClosingOperation:BaseOperation +{ + public ClosingOperation() + { + CanHaveProcessingError = false; + } + + public int Closing { get; set; } = 1; + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckGrayscale(context)) return; + + var element = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(Closing, Closing)); + context.ActiveImage = new HawkeyeImage() + { + ImageData = context.ActiveImage!.ImageData.MorphologyEx(MorphTypes.Close, element) + }; + + Status = $"Closing {Closing}"; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(Closing); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + Closing = br.ReadInt32(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs new file mode 100644 index 0000000..ad1d261 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/CutOperation.cs @@ -0,0 +1,83 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Morphology; + +[Category("Morphology")] +public class CutOperation:BaseOperation +{ + + private readonly WorkflowList _workflowList; + public Guid ReferencePoint { get; set; } = new Guid(); + + public RectangleElement? SearchArea { get; set; } = null; + + public CutOperation(WorkflowList workflowList) + { + _workflowList = workflowList; + SearchArea = new RectangleElement() + { + Editable = true, + Location = Vector2.One * 5, + Size = new Vector2(100, 100), + IsGood = true + }; + + CanHaveProcessingError = false; + + } + + public override void SetParameters(Dictionary parameters) + { + base.SetParameters(parameters); + + SearchArea.SetPivot(_workflowList.GetOriginById(ReferencePoint).Origin.Location); + } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context))return; + + + + + + SearchArea.MovePivot(_workflowList.GetOriginById(ReferencePoint).Origin.Location); + context.GraphicsElements.Add(SearchArea); + + var img = context.ActiveImage.ImageData; + var mat = new Mat(img.Size(), img.Type(), Scalar.Black); + SearchArea.FillMat(mat); + + var cut = img.BitwiseAnd(mat).ToMat(); + + context.ActiveImage=new HawkeyeImage() + { + ImageData = cut + }; + + Status="Cut"; + Result = true; + + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(ReferencePoint.ToString()); + + SearchArea.Save(bw); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + ReferencePoint = new Guid(br.ReadString()); + SearchArea = new RectangleElement(); + + SearchArea.Load(br); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs new file mode 100644 index 0000000..438b7cd --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/DilationOperation.cs @@ -0,0 +1,44 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Morphology; + +[Category("Morphology")] +public class DilationOperation:BaseOperation +{ + + public DilationOperation() + { + CanHaveProcessingError = false; + } + + public int Dilation { get; set; } = 1; + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckGrayscale(context)) return; + + var element=Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(Dilation, Dilation)); + context.ActiveImage = new HawkeyeImage() + { + ImageData = context.ActiveImage!.ImageData.Dilate(element) + }; + + Status="Dilation "+Dilation; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(Dilation); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + Dilation = br.ReadInt32(); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs new file mode 100644 index 0000000..207a72a --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/HatsOperation.cs @@ -0,0 +1,43 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Morphology; + +[Category("Morphology")] +public class HatsOperation : BaseOperation +{ + public HatsOperation() + { + CanHaveProcessingError = false; + } + + public int HatSize { get; set; } = 1; + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckGrayscale(context)) return; + + var element = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(HatSize, HatSize)); + context.ActiveImage = new HawkeyeImage() + { + ImageData = context.ActiveImage!.ImageData.MorphologyEx(MorphTypes.TopHat, element) + }; + + Status = $"Black Hat {HatSize}"; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(HatSize); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + HatSize = br.ReadInt32(); + } +} diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs new file mode 100644 index 0000000..f5b0c54 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/OpeningOperation.cs @@ -0,0 +1,43 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Morphology; + +[Category("Morphology")] +public class OpeningOperation : BaseOperation +{ + public OpeningOperation() + { + CanHaveProcessingError = false; + } + + public int Opening { get; set; } = 1; + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckGrayscale(context)) return; + + var element = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(Opening, Opening)); + context.ActiveImage = new HawkeyeImage() + { + ImageData = context.ActiveImage!.ImageData.MorphologyEx(MorphTypes.Open, element) + }; + + Status = $"Opening {Opening}"; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(Opening); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + Opening = br.ReadInt32(); + } +} diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs new file mode 100644 index 0000000..42b213e --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/SubtractOperation.cs @@ -0,0 +1,48 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Morphology; + +[Category("Morphology")] +public class SubtractOperation : BaseOperation +{ + public string SlotName { get; set; } = "Image"; + + public SubtractOperation() + { + CanHaveProcessingError = false; + } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckGrayscale(context)) return; + + if (!context.Memory.ContainsKey(SlotName)) + { + Status = "Slot not found"; + Result = false; + return; + } + + var first = context.ActiveImage; + var second = context.Memory[SlotName]; + + context.ActiveImage = new HawkeyeImage() + { + ImageData = first.ImageData - second.ImageData + }; + + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + } +} diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs new file mode 100644 index 0000000..bb6727c --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/TakeBiggestOperation.cs @@ -0,0 +1,54 @@ +using System.Collections; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Morphology; + +[Category("Morphology")] +public class TakeBiggestOperation : BaseOperation +{ + public TakeBiggestOperation() + { + CanHaveProcessingError = false; + } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckGrayscale(context)) return; + + var image = context.ActiveImage!.ImageData; + + Cv2.FindContours(image, out var contours, out var hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); + + if (contours.Length == 0) + { + SetError("No blobs found."); + return; + } + + var largestContour = contours.OrderByDescending(c => Cv2.ContourArea(c)).First(); + var mask = Mat.Zeros(image.Size(), MatType.CV_8UC1).ToMat(); + + Cv2.DrawContours(mask, new[] {largestContour}, -1, 255, -1); + + context.ActiveImage = new HawkeyeImage() + { + ImageData = mask + }; + + Status = "Biggest blob extracted"; + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + } +} diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs new file mode 100644 index 0000000..c96db08 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Morphology/UnionOperation.cs @@ -0,0 +1,51 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Morphology; + +[Category("Morphology")] +public class UnionOperation : BaseOperation +{ + public string SlotName { get; set; } = "Image"; + + public UnionOperation() + { + CanHaveProcessingError = false; + } + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckGrayscale(context)) return; + + if (!context.Memory.ContainsKey(SlotName)) + { + Status = "Slot not found"; + Result = false; + return; + } + + var first = context.ActiveImage; + var second = context.Memory[SlotName]; + + context.ActiveImage = new HawkeyeImage() + { + ImageData = first.ImageData.BitwiseOr(second.ImageData) + }; + + + + Result = true; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + } +} diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs new file mode 100644 index 0000000..0ed4360 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindBlobOperation.cs @@ -0,0 +1,158 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Poly; +using Hawkeye.VisionBuilder.Workflow.Links; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +using OpenCvSharp; +using Point = OpenCvSharp.Point; +using RectangleElement = Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle.RectangleElement; + +namespace Hawkeye.VisionBuilder.Workflow.Operations; + +[Category("Simple/Blobs")] +public class FindBlobOperation : BaseOperation, IHaveOrigin, IHaveSearchArea +{ + private readonly WorkflowList _workflowList; + + public int MinArea { get; set; } = 100; + + public int MaxArea { get; set; } = 99999; + + public int MinRadius { get; set; } = 0; + + public int MaxRadius { get; set; } = 99999; + + public bool BadIfFound { get; set; } + + + + public Guid ReferenceId { get; set; } = new Guid(); + + public RectangleElement SearchArea { get; set; } + + //processing result of current element + [NotForTool] public OriginElement Origin { get; private set; } = OriginElement.Default; + + public FindBlobOperation(WorkflowList workflowList) + { + _workflowList = workflowList; + SearchArea = new RectangleElement() + { + Editable = true, + Location = Vector2.One * 100, + Size = Vector2.One * 100, + IsGood = true + }; + } + + + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if (!CheckGrayscale(context)) return; + + SearchArea.MovePivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); + + var img = context.ActiveImage.ImageData; + var mat = new Mat(img.Size(), img.Type(), Scalar.Black); + + SearchArea.FillMat(mat); + + var contoursMat = img.BitwiseAnd(mat).ToMat(); + + + var contours = contoursMat.Threshold(128, 255, ThresholdTypes.Binary) + .FindContoursAsArray(RetrievalModes.List, ContourApproximationModes.ApproxSimple); + + //case: no blobs found + if (contours.Length == 0) + { + Status = "No blobs found"; + SearchArea.IsGood = false; + context.GraphicsElements.Add(SearchArea); + Result = BadIfFound; + return; + } + + int GetContourRadius(Point[] contour) + { + Cv2.MinEnclosingCircle(contour, out var center, out var radius); + return (int)radius; + } + //case: blobs found but not matching criteria + var contour = contours.Select(x => new { c = x, area = (int)Cv2.ContourArea(x),radius= GetContourRadius(x) }) + .OrderBy(x => Math.Min(Math.Abs(x.area - MinArea), Math.Abs(x.area - MaxArea))); + var match = contour.Where(x=>x.c.Length>1) + .Where(x => x.area >= MinArea && x.area <= MaxArea) + .FirstOrDefault(x => x.radius >= MinRadius && x.radius <= MaxRadius); + + if (match == null) + { + var best = contour.First(); + + Status = $"Area: {best.area} Radius: {best.radius}"; + PolyElement poly = new PolyElement() { IsGood = BadIfFound }; + + poly.Points = best.c.Select(x => new Vector2(x.X, x.Y)).ToArray(); + + context.GraphicsElements.Add(poly); + + + context.GraphicsElements.Add(SearchArea); + Result = BadIfFound; + return; + } + + { + PolyElement poly = new PolyElement() { IsGood = !BadIfFound }; + + poly.Points = match.c.Select(x => new Vector2(x.X, x.Y)).ToArray(); + + context.GraphicsElements.Add(poly); + SearchArea.IsGood = true; + context.GraphicsElements.Add(SearchArea); + var center = poly.Points.Aggregate((v1, v2) => v1 + v2) / poly.Points.Length; + Origin = new OriginElement() { Location = center }; + Status = $"Area: {match.area} Radius: {match.radius}"; + Result = !BadIfFound; + } + } + + public override void SetParameters(Dictionary parameters) + { + base.SetParameters(parameters); + + SearchArea.SetPivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); + } + + + public override void Save(BinaryWriter bw) + { + bw.Write(Id.ToString()); + bw.Write(Label); + bw.Write(MinArea); + bw.Write(MaxArea); + bw.Write(MinRadius); + bw.Write(MaxRadius); + bw.Write(BadIfFound); + bw.Write(ReferenceId.ToString()); + + SearchArea.Save(bw); + + + } + + public override void Load(BinaryReader br) + { + Id = new Guid(br.ReadString()); + Label = br.ReadString(); + MinArea = br.ReadInt32(); + MaxArea = br.ReadInt32(); + MinRadius = br.ReadInt32(); + MaxRadius = br.ReadInt32(); + BadIfFound = br.ReadBoolean(); + ReferenceId = new Guid(br.ReadString()); + + SearchArea.Load(br); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs new file mode 100644 index 0000000..92ba5e5 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsExtOperation.cs @@ -0,0 +1,233 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Poly; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +using OpenCvSharp; +using Point = OpenCvSharp.Point; + +namespace Hawkeye.VisionBuilder.Workflow.Operations; + +[Category("Simple/Blobs")] +public class FindManyBlobsExtOperation : BaseOperation +{ + private readonly WorkflowList _workflowList; + + public int MinArea { get; set; } = 100; + public int MaxArea { get; set; } = 99999; + + public int MinRadius { get; set; } = 0; + public int MaxRadius { get; set; } = 99999; + + public bool CheckInternalRadius { get; set; } = false; + public int MinInternalRadius { get; set; } = 0; + public int MaxInternalRadius { get; set; } = 99999; + + public bool CheckWidth { get; set; } = false; + public int MinWidth { get; set; } = 0; + public int MaxWidth { get; set; } = 99999; + + public bool CheckHeight { get; set; } = false; + public int MinHeight { get; set; } = 0; + public int MaxHeight { get; set; } = 99999; + + public Guid ReferenceId { get; set; } = new Guid(); + + public RectangleElement SearchArea { get; set; } + + public FindManyBlobsExtOperation(WorkflowList workflowList) + { + _workflowList = workflowList; + SearchArea = new RectangleElement() + { + Editable = true, + Location = Vector2.One * 100, + Size = Vector2.One * 100, + IsGood = true + }; + } + + public record ContourMatch(Point[] c, int area, int radius, int internalRadius, int width, int height) + { + public override string ToString() + { + return $"{{area={area}, radius={radius}, internalRadius={internalRadius}, width={width}, height={height}}}"; + } + } + + protected override void InterpretInternal(Context context) + { + Result = true; + if (!CheckImageExists(context)) return; + if (!CheckGrayscale(context)) return; + + if (SearchArea == null) + { + SearchArea = new RectangleElement() + { + Editable = true, + Location = Vector2.Zero, + Size = new Vector2(context.ActiveImage.ImageData.Width, context.ActiveImage.ImageData.Height), + IsGood = true + }; + } + + SearchArea.MovePivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); + + var img = context.ActiveImage.ImageData; + var mat = new Mat(img.Size(), img.Type(), Scalar.Black); + + SearchArea.FillMat(mat); + + var contoursMat = img.BitwiseAnd(mat).ToMat(); + + var contours = contoursMat.Threshold(128, 255, ThresholdTypes.Binary) + .FindContoursAsArray(RetrievalModes.List, ContourApproximationModes.ApproxSimple); + context.GraphicsElements.Add(SearchArea); + + // Case: no blobs found + if (contours.Length == 0) + { + Status = "No blobs found"; + SearchArea.IsGood = true; + return; + } + + int GetContourRadius(Point[] contour) + { + Cv2.MinEnclosingCircle(contour, out _, out var radius); + return (int)radius; + } + + int GetContourInternalRadius(Point[] contour) + { + + // Create a mask for the contour with the size of the bounding rectangle. + Mat mask = new Mat(contoursMat.Height, contoursMat.Width, MatType.CV_8UC1, Scalar.All(0)); + + + + // Fill the mask with the polygon defined by the contour. + Cv2.FillPoly(mask, new Point[][] { contour }, Scalar.White); + + // Compute the distance transform. + using Mat dist = new Mat(); + Cv2.DistanceTransform(mask, dist, DistanceTypes.C, DistanceTransformMasks.Mask3); + Cv2.MinMaxLoc(dist, out double minVal, out var maxVal); + + return (int)maxVal; + } + + (int width, int height) GetContourDimensions(Point[] contour) + { + var rect = Cv2.BoundingRect(contour); + return (rect.Width, rect.Height); + } + + var contourMatches = contours.Select(c => + { + int area = (int)Cv2.ContourArea(c); + int radius =-1; + radius = GetContourRadius(c); + int innerCircle=-1; + if (CheckInternalRadius) + innerCircle = GetContourInternalRadius(c); + var (width, height) = GetContourDimensions(c); + return new ContourMatch(c, area, radius, innerCircle, width, height); + }) + .OrderBy(cm => Math.Min(Math.Abs(cm.area - MinArea), Math.Abs(cm.area - MaxArea))) + .ToList(); + + var matches = contourMatches.Where(cm => cm.c.Length > 1) + .Where(cm => cm.area >= MinArea && cm.area <= MaxArea) + .Where(cm => cm.radius >= MinRadius && cm.radius <= MaxRadius); + + + + if (CheckInternalRadius) + { + matches = matches.Where(cm => cm.internalRadius >= MinInternalRadius && cm.internalRadius <= MaxInternalRadius); + } + + if (CheckWidth) + { + matches = matches.Where(cm => cm.width >= MinWidth && cm.width <= MaxWidth); + } + + if (CheckHeight) + { + matches = matches.Where(cm => cm.height >= MinHeight && cm.height <= MaxHeight); + } + + var finalMatches = matches.ToList(); + + int badCount = 0; + foreach (var contourMatch in finalMatches) + { + PolyElement poly = new PolyElement() { IsGood = false }; + poly.Points = contourMatch.c.Select(p => new Vector2(p.X, p.Y)).ToArray(); + context.GraphicsElements.Add(poly); + badCount++; + } + + if (badCount > 0) + { + Status = $"Found: {badCount} First: {finalMatches.First()}"; + Result = false; + SearchArea.IsGood = false; + return; + } + + SearchArea.IsGood = true; + Status = $"Best: {contourMatches.First()}"; + Result = true; + } + + public override void SetParameters(Dictionary parameters) + { + base.SetParameters(parameters); + SearchArea.SetPivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); + } + + public override void Save(BinaryWriter bw) + { + bw.Write(Id.ToString()); + bw.Write(Label); + bw.Write(MinArea); + bw.Write(MaxArea); + bw.Write(MinRadius); + bw.Write(MaxRadius); + bw.Write(CheckInternalRadius); + bw.Write(MinInternalRadius); + bw.Write(MaxInternalRadius); + bw.Write(CheckWidth); + bw.Write(MinWidth); + bw.Write(MaxWidth); + bw.Write(CheckHeight); + bw.Write(MinHeight); + bw.Write(MaxHeight); + bw.Write(ReferenceId.ToString()); + SearchArea.Save(bw); + } + + public override void Load(BinaryReader br) + { + Id = new Guid(br.ReadString()); + Label = br.ReadString(); + MinArea = br.ReadInt32(); + MaxArea = br.ReadInt32(); + MinRadius = br.ReadInt32(); + MaxRadius = br.ReadInt32(); + CheckInternalRadius = br.ReadBoolean(); + MinInternalRadius = br.ReadInt32(); + MaxInternalRadius = br.ReadInt32(); + CheckWidth = br.ReadBoolean(); + MinWidth = br.ReadInt32(); + MaxWidth = br.ReadInt32(); + CheckHeight = br.ReadBoolean(); + MinHeight = br.ReadInt32(); + MaxHeight = br.ReadInt32(); + ReferenceId = new Guid(br.ReadString()); + SearchArea.Load(br); + } +} diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs new file mode 100644 index 0000000..a680547 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindManyBlobsOperation.cs @@ -0,0 +1,175 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Poly; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +using OpenCvSharp; +using Point = OpenCvSharp.Point; + +namespace Hawkeye.VisionBuilder.Workflow.Operations; + +[Category("Simple/Blobs")] +public class FindManyBlobsOperation:BaseOperation +{ + private readonly WorkflowList _workflowList; + + public int MinArea { get; set; } = 100; + + public int MaxArea { get; set; } = 99999; + + public int MinRadius { get; set; } = 0; + + public int MaxRadius { get; set; } = 99999; + + + + + + public Guid ReferenceId { get; set; } = new Guid(); + + public RectangleElement SearchArea { get; set; } + + //processing result of current element + + + public FindManyBlobsOperation(WorkflowList workflowList) + { + _workflowList = workflowList; + SearchArea = new RectangleElement() + { + Editable = true, + Location = Vector2.One * 100, + Size = Vector2.One * 100, + IsGood = true + }; + } + + + public record ContourMatch(Point[] c, int area, int radius) + { + + + public override string ToString() + { + return $"{{area={area}, radius={radius}}}"; + } + } + + protected override void InterpretInternal(Context context) + { + Result = true; + if (!CheckImageExists(context)) return; + if (!CheckGrayscale(context)) return; + + if (SearchArea==null) + { + SearchArea = new RectangleElement() + { + Editable = true, + Location = Vector2.Zero, + Size = new Vector2(context.ActiveImage.ImageData.Width, context.ActiveImage.ImageData.Height), + IsGood = true + }; + } + + SearchArea.MovePivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); + + var img = context.ActiveImage.ImageData; + var mat = new Mat(img.Size(), img.Type(), Scalar.Black); + + SearchArea.FillMat(mat); + + var contoursMat = img.BitwiseAnd(mat).ToMat(); + + + var contours = contoursMat.Threshold(128, 255, ThresholdTypes.Binary) + .FindContoursAsArray(RetrievalModes.List, ContourApproximationModes.ApproxSimple); + context.GraphicsElements.Add(SearchArea); + //case: no blobs found + if (contours.Length == 0) + { + Status = "No blobs found"; + SearchArea.IsGood = true; + + return; + } + + int GetContourRadius(Point[] contour) + { + Cv2.MinEnclosingCircle(contour, out var center, out var radius); + return (int)radius; + } + //case: blobs found but not matching criteria + var contour = contours.Select(x => new ContourMatch(x, (int)Cv2.ContourArea(x), GetContourRadius(x))) + .OrderBy(x => Math.Min(Math.Abs(x.area - MinArea), Math.Abs(x.area - MaxArea))).ToList(); + var match = contour.Where(x => x.c.Length > 1) + .Where(x => x.area >= MinArea && x.area <= MaxArea) + .Where(x => x.radius >= MinRadius && x.radius <= MaxRadius).ToList(); + + + + int badCount = 0; + foreach (ContourMatch contourMatch in match) + { + PolyElement poly = new PolyElement() { IsGood = false }; + + poly.Points = contourMatch.c.Select(x => new Vector2(x.X, x.Y)).ToArray(); + + context.GraphicsElements.Add(poly); + badCount++; + } + + + + if (badCount > 0) + { + Status = $"Found: {badCount} First: {match.First().ToString()}"; + Result = false; + SearchArea.IsGood = false; + return; + } + + SearchArea.IsGood = true; + Status = $"Best: {contour.First().ToString()}"; + Result = true; + + + + } + + public override void SetParameters(Dictionary parameters) + { + base.SetParameters(parameters); + + SearchArea.SetPivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); + } + + + public override void Save(BinaryWriter bw) + { + bw.Write(Id.ToString()); + bw.Write(Label); + bw.Write(MinArea); + bw.Write(MaxArea); + bw.Write(MinRadius); + bw.Write(MaxRadius); + bw.Write(ReferenceId.ToString()); + + SearchArea.Save(bw); + + + } + + public override void Load(BinaryReader br) + { + Id = new Guid(br.ReadString()); + Label = br.ReadString(); + MinArea = br.ReadInt32(); + MaxArea = br.ReadInt32(); + MinRadius = br.ReadInt32(); + MaxRadius = br.ReadInt32(); + ReferenceId = new Guid(br.ReadString()); + + SearchArea.Load(br); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs new file mode 100644 index 0000000..7800b7d --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/FindRectangleOperation.cs @@ -0,0 +1,112 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +using OpenCvSharp; +using System; +using System.Security.Cryptography.Xml; +using Point = OpenCvSharp.Point; + +namespace Hawkeye.VisionBuilder.Workflow.Operations; +[Category("Simple/Blobs")] +public class FindRectangleOperation:BaseOperation +{ + private readonly WorkflowList _workflowList; + + public int MinWidth { get; set; } + public int MaxWidth { get; set; } = 999999; + public int MinHeight { get; set; } + public int MaxHeight { get; set; } = 999999; + + public Guid ReferenceId { get; set; } = new Guid(); + + public bool BadIfFound { get; set; }=false; + + public FindRectangleOperation(WorkflowList workflowList) + { + _workflowList = workflowList; + SearchArea = new RectangleElement() + { + Editable = true, + Location = Vector2.One * 100, + Size = Vector2.One * 100, + IsGood = true + }; + } + + public RectangleElement SearchArea { get; set; } + + //processing result of current element + [NotForTool] public OriginElement Origin { get; private set; } = OriginElement.Default; + + protected override void InterpretInternal(Context context) + { + if(!CheckImageExists(context)) return; + SearchArea.MovePivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); + var image = context.ActiveImage.ImageData; + var mat = new Mat(image.Size(), image.Type(), Scalar.Black); + + SearchArea.FillMat(mat); + + var contoursMat = image.BitwiseAnd(mat).ToMat(); + context.GraphicsElements.Add(SearchArea); + // find rectangle on image + Cv2.FindContours(contoursMat, out var contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple); + foreach (Point[] contour in contours) + { + var rect = Cv2.BoundingRect(contour); + if (rect.Width >= MinWidth && rect.Width <= MaxWidth && rect.Height >= MinHeight && + rect.Height <= MaxHeight) + { + Status = "Rectangle: " + rect; + Result = !BadIfFound; + RectangleElement re = new RectangleElement() + { + Editable = false, + IsGood = !BadIfFound, + Location = new Vector2(rect.X, rect.Y), + Size = new Vector2(rect.Width, rect.Height) + }; + context.GraphicsElements.Add(re); + return; + } + } + + Status = "No rectangle found"; + Result = BadIfFound; + + + + } + + public override void SetParameters(Dictionary parameters) + { + base.SetParameters(parameters); + + SearchArea.SetPivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(ReferenceId.ToString()); + bw.Write(MinWidth); + bw.Write(MaxWidth); + bw.Write(MinHeight); + bw.Write(MaxHeight); + bw.Write(BadIfFound); + SearchArea.Save(bw); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + ReferenceId = new Guid(br.ReadString()); + MinWidth = br.ReadInt32(); + MaxWidth = br.ReadInt32(); + MinHeight = br.ReadInt32(); + MaxHeight = br.ReadInt32(); + BadIfFound = br.ReadBoolean(); + SearchArea.Load(br); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs new file mode 100644 index 0000000..017d7ef --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Blobs/TwoBlobsAlign.cs @@ -0,0 +1,106 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Align; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; +using Hawkeye.VisionBuilder.Workflow.Links; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations; + +[Category("Simple/Blobs")] +public class TwoBlobsAlign:BaseOperation,IHaveOrigin +{ + private readonly WorkflowList _workflowList; + + public TwoBlobsAlign(WorkflowList workflowList) + { + _workflowList = workflowList; + } + + public Guid Reference1 { get; set; } = NoOrigin.Instance.Id; + public Guid Reference2 { get; set; } = NoOrigin.Instance.Id; + public int DesiredAngle { get; set; } + + [NotForTool] public OriginElement Origin { get; private set; } = OriginElement.Default; + private string _status; + + protected override void InterpretInternal(Context context) + { + var reference1 = _workflowList.GetOriginById(Reference1); + var reference2 = _workflowList.GetOriginById(Reference2); + if (reference1 == NoOrigin.Instance) + { + _status = $"{nameof(Reference1)} must be set"; + Result = false; + return; + } + if (reference2 == NoOrigin.Instance) + { + _status = $"{nameof(Reference2)} must be set"; + Result = false; + return; + } + if (!(reference1 as BaseOperation).Result) + { + _status = $"{nameof(Reference1)} detection failed"; + Result = false; + return; + } + if (!(reference2 as BaseOperation).Result) + { + _status = $"{nameof(Reference2)} detection failed"; + Result = false; + return; + } + + if (context.ActiveImage == null) + { + _status = $"Image required"; + Result = false; + return; + } + + var center = (reference1.Origin.Location + reference2.Origin.Location) / 2; + + var deltaX = reference2.Origin.Location.X-reference1.Origin.Location.X; + var deltaY = reference2.Origin.Location.Y-reference1.Origin.Location.Y; + + var angle = ((float)Math.Atan2(deltaY, deltaX))/MathF.PI*180f; + var deltaAngle = DesiredAngle - angle; + var matrix=Cv2.GetRotationMatrix2D(new Point2f(center.X, center.Y), -deltaAngle, 1); + + var destImage = new Mat(context.ActiveImage.ImageData.Size(), context.ActiveImage.ImageData.Type()); + + + + Cv2.WarpAffine(context.ActiveImage.ImageData, destImage,matrix,destImage.Size()); + + context.ActiveImage=new HawkeyeImage(){Filename = context.ActiveImage.Filename,ImageData = destImage }; + + Origin = new OriginElement() {Location = center }; + _status = center.ToString(); + Result = true; + context.GraphicsElements.Add(new AlignElement(){Location = center}); + } + public override string ToString() + { + return _status; + } + + public override void Save(BinaryWriter bw) + { + base.Save(bw); + bw.Write(Reference1.ToString()); + bw.Write(Reference2.ToString()); + bw.Write(DesiredAngle); + } + + public override void Load(BinaryReader br) + { + base.Load(br); + Reference1 = new Guid(br.ReadString()); + Reference2 = new Guid(br.ReadString()); + DesiredAngle = br.ReadInt32(); + + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs new file mode 100644 index 0000000..6b77a7b --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/EdgeIntersectionOperation.cs @@ -0,0 +1,104 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Align; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Edge; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; +using Hawkeye.VisionBuilder.Workflow.Links; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + + +namespace Hawkeye.VisionBuilder.Workflow.Operations.PositionDetection.Edges; +[Category("Simple/Edges")] +public class EdgeIntersectionOperation : BaseOperation, IHaveOrigin +{ + private readonly WorkflowList _workflowList; + private string _status; + public override string ToString() + { + return _status; + } + + public EdgeIntersectionOperation(WorkflowList workflowList) + { + _workflowList = workflowList; + } + + public Guid ReferenceEdge1 { get; set; } + + public Guid ReferenceEdge2 { get; set; } + + protected override void InterpretInternal(Context context) + { + var reference1 = _workflowList.GetById(ReferenceEdge1); + var reference2 = _workflowList.GetById(ReferenceEdge2); + if (reference1 == NoOrigin.Instance) + { + _status = $"{nameof(ReferenceEdge1)} must be set"; + Result = false; + return; + } + if (reference2 == NoOrigin.Instance) + { + _status = $"{nameof(ReferenceEdge2)} must be set"; + Result = false; + return; + } + if (!(reference1 as BaseOperation).Result) + { + _status = $"{nameof(ReferenceEdge1)} detection failed"; + Result = false; + return; + } + if (!(reference2 as BaseOperation).Result) + { + _status = $"{nameof(ReferenceEdge2)} detection failed"; + Result = false; + return; + } + + var edge1 = reference1 as FindEdgeOperation; + var edge2 = reference2 as FindEdgeOperation; + + + //context.GraphicsElements.Add(new EdgeElement() + //{ + // Location = edge1.EdgeOrigin.Location, + // Rotation = edge1.EdgeOrigin.Rotation + //}); + //context.GraphicsElements.Add(new EdgeElement() + //{ + // Location = edge2.EdgeOrigin.Location, + // Rotation = edge2.EdgeOrigin.Rotation + //}); + + if (edge1.EdgeOrigin.Intersection(edge2.EdgeOrigin, out var x, out var y)) + { + Result = true; + Origin = new OriginElement() + { + Location = new Vector2(x, y) + }; + _status = $"{Origin.Location}"; + context.GraphicsElements.Add(new AlignElement() { Location = Origin.Location }); + return; + } + _status = $"Edges are parallel"; + Result = false; + + } + + public override void Save(BinaryWriter bw) + { + bw.Write(Id.ToString()); + bw.Write(ReferenceEdge1.ToString()); + bw.Write(ReferenceEdge2.ToString()); + } + + public override void Load(BinaryReader br) + { + Id = new Guid(br.ReadString()); + ReferenceEdge1 = new Guid(br.ReadString()); + ReferenceEdge2 = new Guid(br.ReadString()); + + } + + public OriginElement Origin { get; set; } = OriginElement.Default; +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs new file mode 100644 index 0000000..ab97113 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Simple/Edges/FindEdgeOperation.cs @@ -0,0 +1,130 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Align; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Edge; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.RotatedRectangle; +using Hawkeye.VisionBuilder.Workflow.Links; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; + +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.PositionDetection.Edges; +[Category("Simple/Edges")] +public class FindEdgeOperation : BaseOperation, IHaveEdge, IHaveOrigin +{ + private readonly WorkflowList _workflowList; + + public Guid ReferenceId { get; set; } = new Guid(); + public EdgeOrigin EdgeOrigin { get; set; } + public FindEdgeOperation(WorkflowList workflowList) + { + _workflowList = workflowList; + SearchArea = new RotatedRectangleElement() + { + Location = new Vector2(100, 100), + Editable = true, + HalfWidth = 50, + Height = new Vector2(0, 100), + IsGood = true + }; + } + private string _status; + public override string ToString() + { + return _status; + } + + protected override void InterpretInternal(Context context) + { + if (context.ActiveImage == null) + { + Result = false; + _status = "No image provided"; + return; + } + + SearchArea.MovePivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); + context.GraphicsElements.Add(SearchArea); + + + var img = context.ActiveImage.ImageData; + var mask = new Mat(img.Size(), img.Type(), Scalar.Black); + + SearchArea.FillMat(mask); + + var edges = img.Canny(100, 200); + var mat = edges.BitwiseAnd(mask).ToMat(); + + + mat.MinMaxLoc(out _, out var maxVal, out _, out var maxLoc); + if (maxVal < 0.01) + { + _status = "No edges found"; + SearchArea.IsGood = false; + context.GraphicsElements.Add(SearchArea); + Result = false; + return; + } + + + Cv2.FindContours(mat, out var contours, out var hierarchy, RetrievalModes.CComp, ContourApproximationModes.ApproxSimple); + //var closest = contours.OrderBy(c => + //{ + + // return (c.Select(x => new Vector2(x.X, x.Y)).Aggregate((v1, v2) => v1 + v2) / c.Length - SearchArea.Location).Length(); + + //}).First(); + + var closest = contours.SelectMany(x => x); + var line = Cv2.FitLine(closest, DistanceTypes.L2, 0, 0.01, 0.01); + + + + context.GraphicsElements.Add(new EdgeElement() + { + Location = new Vector2((float)line.X1, (float)line.Y1), + Rotation = (float)line.GetVectorRadian() + }); + + EdgeOrigin = new EdgeOrigin() + { + Found = true, + Location = new Vector2((float)line.X1, (float)line.Y1), + Rotation = (float)line.GetVectorRadian() + }; + Origin = new OriginElement() + { + Location = new Vector2((float)line.X1, (float)line.Y1) + }; + context.GraphicsElements.Add(new AlignElement() { Location = new Vector2((float)line.X1, (float)line.Y1) }); + _status = EdgeOrigin.ToString(); + SearchArea.IsGood = true; + Result = true; + + } + public override void SetParameters(Dictionary parameters) + { + base.SetParameters(parameters); + + SearchArea.SetPivot(_workflowList.GetOriginById(ReferenceId).Origin.Location); + } + public override void Save(BinaryWriter bw) + { + bw.Write(Id.ToString()); + bw.Write(Label); + bw.Write(ReferenceId.ToString()); + SearchArea.Save(bw); + } + + public override void Load(BinaryReader br) + { + Id = new Guid(br.ReadString()); + Label = br.ReadString(); + ReferenceId = new Guid(br.ReadString()); + SearchArea = new RotatedRectangleElement(); + SearchArea.Load(br); + } + + + public RotatedRectangleElement SearchArea { get; set; } + public OriginElement Origin { get; set; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/Transformations/DistanceTransformOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/Transformations/DistanceTransformOperation.cs new file mode 100644 index 0000000..96f1841 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Operations/Transformations/DistanceTransformOperation.cs @@ -0,0 +1,32 @@ +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using OpenCvSharp; + +namespace Hawkeye.VisionBuilder.Workflow.Operations.Transformations; + +[Category("Transformations")] +public class DistanceTransformOperation:BaseOperation +{ + protected override void InterpretInternal(Context context) + { + if (!CheckImageExists(context)) return; + if(!CheckGrayscale(context))return; + + Mat res=new Mat(context.ActiveImage.ImageData.Size(),MatType.CV_8UC1); + var distance = context.ActiveImage.ImageData.DistanceTransform(DistanceTypes.C, DistanceTransformMasks.Mask3); + + Cv2.MinMaxLoc(distance, out double minVal, out var maxVal); + // clamp the distance transform to 0-255 + Cv2.Normalize(distance, res, 0, maxVal, NormTypes.MinMax, MatType.CV_8UC1); + + + + context.ActiveImage = new HawkeyeImage() + { + ImageData = res + }; + + Status = $"Distance transform"; + Result = true; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Overrides/OperationOverride.cs b/Hawkeye.VisionBuilder.Workflow/Overrides/OperationOverride.cs new file mode 100644 index 0000000..499d215 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Overrides/OperationOverride.cs @@ -0,0 +1,17 @@ +namespace Hawkeye.VisionBuilder.Workflow.Overrides; + +public class OperationOverride +{ + public string Name { get; set; } + public string Property { get; set; } + public object Value { get; set; } + + public void Apply(WorkflowList workflowList) + { + var operation = workflowList.Operations.FirstOrDefault(o => o.Label == Name); + if (operation == null) return; + var property = operation.GetType().GetProperty(Property); + if (property == null) return; + property.SetValue(operation, Value); + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Quaterion.cs b/Hawkeye.VisionBuilder.Workflow/Quaterion.cs new file mode 100644 index 0000000..d6c33e0 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Quaterion.cs @@ -0,0 +1,1165 @@ +namespace Hawkeye.VisionBuilder.Workflow +{ + public struct Quaternion : IEquatable + { + #region Private Fields + + private static readonly Quaternion _identity = new Quaternion(0, 0, 0, 1); + + #endregion + + #region Public Fields + + /// + /// The x coordinate of this . + /// + + public float X; + + /// + /// The y coordinate of this . + /// + + public float Y; + + /// + /// The z coordinate of this . + /// + + public float Z; + + /// + /// The rotation component of this . + /// + + public float W; + + #endregion + + #region Constructors + + /// + /// Constructs a quaternion with X, Y, Z and W from four values. + /// + /// The x coordinate in 3d-space. + /// The y coordinate in 3d-space. + /// The z coordinate in 3d-space. + /// The rotation component. + public Quaternion(float x, float y, float z, float w) + { + this.X = x; + this.Y = y; + this.Z = z; + this.W = w; + } + + /// + /// Constructs a quaternion with X, Y, Z from and rotation component from a scalar. + /// + /// The x, y, z coordinates in 3d-space. + /// The rotation component. + public Quaternion(Vector3 value, float w) + { + this.X = value.X; + this.Y = value.Y; + this.Z = value.Z; + this.W = w; + } + + /// + /// Constructs a quaternion from . + /// + /// The x, y, z coordinates in 3d-space and the rotation component. + public Quaternion(Vector4 value) + { + this.X = value.X; + this.Y = value.Y; + this.Z = value.Z; + this.W = value.W; + } + + #endregion + + #region Public Properties + + /// + /// Returns a quaternion representing no rotation. + /// + public static Quaternion Identity + { + get { return _identity; } + } + + #endregion + + #region Internal Properties + + internal string DebugDisplayString + { + get + { + if (this == Quaternion._identity) + { + return "Identity"; + } + + return string.Concat( + this.X.ToString(), " ", + this.Y.ToString(), " ", + this.Z.ToString(), " ", + this.W.ToString() + ); + } + } + + #endregion + + #region Public Methods + + #region Add + + /// + /// Creates a new that contains the sum of two quaternions. + /// + /// Source . + /// Source . + /// The result of the quaternion addition. + public static Quaternion Add(Quaternion quaternion1, Quaternion quaternion2) + { + Quaternion quaternion; + quaternion.X = quaternion1.X + quaternion2.X; + quaternion.Y = quaternion1.Y + quaternion2.Y; + quaternion.Z = quaternion1.Z + quaternion2.Z; + quaternion.W = quaternion1.W + quaternion2.W; + return quaternion; + } + + /// + /// Creates a new that contains the sum of two quaternions. + /// + /// Source . + /// Source . + /// The result of the quaternion addition as an output parameter. + public static void Add(ref Quaternion quaternion1, ref Quaternion quaternion2, out Quaternion result) + { + result.X = quaternion1.X + quaternion2.X; + result.Y = quaternion1.Y + quaternion2.Y; + result.Z = quaternion1.Z + quaternion2.Z; + result.W = quaternion1.W + quaternion2.W; + } + + #endregion + + #region Concatenate + + /// + /// Creates a new that contains concatenation between two quaternion. + /// + /// The first to concatenate. + /// The second to concatenate. + /// The result of rotation of followed by rotation. + public static Quaternion Concatenate(Quaternion value1, Quaternion value2) + { + Quaternion quaternion; + + float x1 = value1.X; + float y1 = value1.Y; + float z1 = value1.Z; + float w1 = value1.W; + + float x2 = value2.X; + float y2 = value2.Y; + float z2 = value2.Z; + float w2 = value2.W; + + quaternion.X = ((x2 * w1) + (x1 * w2)) + ((y2 * z1) - (z2 * y1)); + quaternion.Y = ((y2 * w1) + (y1 * w2)) + ((z2 * x1) - (x2 * z1)); + quaternion.Z = ((z2 * w1) + (z1 * w2)) + ((x2 * y1) - (y2 * x1)); + quaternion.W = (w2 * w1) - (((x2 * x1) + (y2 * y1)) + (z2 * z1)); + + return quaternion; + } + + /// + /// Creates a new that contains concatenation between two quaternion. + /// + /// The first to concatenate. + /// The second to concatenate. + /// The result of rotation of followed by rotation as an output parameter. + public static void Concatenate(ref Quaternion value1, ref Quaternion value2, out Quaternion result) + { + float x1 = value1.X; + float y1 = value1.Y; + float z1 = value1.Z; + float w1 = value1.W; + + float x2 = value2.X; + float y2 = value2.Y; + float z2 = value2.Z; + float w2 = value2.W; + + result.X = ((x2 * w1) + (x1 * w2)) + ((y2 * z1) - (z2 * y1)); + result.Y = ((y2 * w1) + (y1 * w2)) + ((z2 * x1) - (x2 * z1)); + result.Z = ((z2 * w1) + (z1 * w2)) + ((x2 * y1) - (y2 * x1)); + result.W = (w2 * w1) - (((x2 * x1) + (y2 * y1)) + (z2 * z1)); + } + + #endregion + + #region Conjugate + + /// + /// Transforms this quaternion into its conjugated version. + /// + public void Conjugate() + { + X = -X; + Y = -Y; + Z = -Z; + } + + /// + /// Creates a new that contains conjugated version of the specified quaternion. + /// + /// The quaternion which values will be used to create the conjugated version. + /// The conjugate version of the specified quaternion. + public static Quaternion Conjugate(Quaternion value) + { + return new Quaternion(-value.X, -value.Y, -value.Z, value.W); + } + + /// + /// Creates a new that contains conjugated version of the specified quaternion. + /// + /// The quaternion which values will be used to create the conjugated version. + /// The conjugated version of the specified quaternion as an output parameter. + public static void Conjugate(ref Quaternion value, out Quaternion result) + { + result.X = -value.X; + result.Y = -value.Y; + result.Z = -value.Z; + result.W = value.W; + } + + #endregion + + #region CreateFromAxisAngle + + /// + /// Creates a new from the specified axis and angle. + /// + /// The axis of rotation. + /// The angle in radians. + /// The new quaternion builded from axis and angle. + public static Quaternion CreateFromAxisAngle(Vector3 axis, float angle) + { + float half = angle * 0.5f; + float sin = (float)Math.Sin(half); + float cos = (float)Math.Cos(half); + return new Quaternion(axis.X * sin, axis.Y * sin, axis.Z * sin, cos); + } + + /// + /// Creates a new from the specified axis and angle. + /// + /// The axis of rotation. + /// The angle in radians. + /// The new quaternion builded from axis and angle as an output parameter. + public static void CreateFromAxisAngle(ref Vector3 axis, float angle, out Quaternion result) + { + float half = angle * 0.5f; + float sin = (float)Math.Sin(half); + float cos = (float)Math.Cos(half); + result.X = axis.X * sin; + result.Y = axis.Y * sin; + result.Z = axis.Z * sin; + result.W = cos; + } + + #endregion + + #region CreateFromRotationMatrix + + /// + /// Creates a new from the specified . + /// + /// The rotation matrix. + /// A quaternion composed from the rotation part of the matrix. + public static Quaternion CreateFromRotationMatrix(Matrix matrix) + { + Quaternion quaternion; + float sqrt; + float half; + float scale = matrix.M11 + matrix.M22 + matrix.M33; + + if (scale > 0.0f) + { + sqrt = (float)Math.Sqrt(scale + 1.0f); + quaternion.W = sqrt * 0.5f; + sqrt = 0.5f / sqrt; + + quaternion.X = (matrix.M23 - matrix.M32) * sqrt; + quaternion.Y = (matrix.M31 - matrix.M13) * sqrt; + quaternion.Z = (matrix.M12 - matrix.M21) * sqrt; + + return quaternion; + } + if ((matrix.M11 >= matrix.M22) && (matrix.M11 >= matrix.M33)) + { + sqrt = (float)Math.Sqrt(1.0f + matrix.M11 - matrix.M22 - matrix.M33); + half = 0.5f / sqrt; + + quaternion.X = 0.5f * sqrt; + quaternion.Y = (matrix.M12 + matrix.M21) * half; + quaternion.Z = (matrix.M13 + matrix.M31) * half; + quaternion.W = (matrix.M23 - matrix.M32) * half; + + return quaternion; + } + if (matrix.M22 > matrix.M33) + { + sqrt = (float)Math.Sqrt(1.0f + matrix.M22 - matrix.M11 - matrix.M33); + half = 0.5f / sqrt; + + quaternion.X = (matrix.M21 + matrix.M12) * half; + quaternion.Y = 0.5f * sqrt; + quaternion.Z = (matrix.M32 + matrix.M23) * half; + quaternion.W = (matrix.M31 - matrix.M13) * half; + + return quaternion; + } + sqrt = (float)Math.Sqrt(1.0f + matrix.M33 - matrix.M11 - matrix.M22); + half = 0.5f / sqrt; + + quaternion.X = (matrix.M31 + matrix.M13) * half; + quaternion.Y = (matrix.M32 + matrix.M23) * half; + quaternion.Z = 0.5f * sqrt; + quaternion.W = (matrix.M12 - matrix.M21) * half; + + return quaternion; + } + + /// + /// Creates a new from the specified . + /// + /// The rotation matrix. + /// A quaternion composed from the rotation part of the matrix as an output parameter. + public static void CreateFromRotationMatrix(ref Matrix matrix, out Quaternion result) + { + float sqrt; + float half; + float scale = matrix.M11 + matrix.M22 + matrix.M33; + + if (scale > 0.0f) + { + sqrt = (float)Math.Sqrt(scale + 1.0f); + result.W = sqrt * 0.5f; + sqrt = 0.5f / sqrt; + + result.X = (matrix.M23 - matrix.M32) * sqrt; + result.Y = (matrix.M31 - matrix.M13) * sqrt; + result.Z = (matrix.M12 - matrix.M21) * sqrt; + } + else + if ((matrix.M11 >= matrix.M22) && (matrix.M11 >= matrix.M33)) + { + sqrt = (float)Math.Sqrt(1.0f + matrix.M11 - matrix.M22 - matrix.M33); + half = 0.5f / sqrt; + + result.X = 0.5f * sqrt; + result.Y = (matrix.M12 + matrix.M21) * half; + result.Z = (matrix.M13 + matrix.M31) * half; + result.W = (matrix.M23 - matrix.M32) * half; + } + else if (matrix.M22 > matrix.M33) + { + sqrt = (float)Math.Sqrt(1.0f + matrix.M22 - matrix.M11 - matrix.M33); + half = 0.5f / sqrt; + + result.X = (matrix.M21 + matrix.M12) * half; + result.Y = 0.5f * sqrt; + result.Z = (matrix.M32 + matrix.M23) * half; + result.W = (matrix.M31 - matrix.M13) * half; + } + else + { + sqrt = (float)Math.Sqrt(1.0f + matrix.M33 - matrix.M11 - matrix.M22); + half = 0.5f / sqrt; + + result.X = (matrix.M31 + matrix.M13) * half; + result.Y = (matrix.M32 + matrix.M23) * half; + result.Z = 0.5f * sqrt; + result.W = (matrix.M12 - matrix.M21) * half; + } + } + + #endregion + + #region CreateFromYawPitchRoll + + /// + /// Creates a new from the specified yaw, pitch and roll angles. + /// + /// Yaw around the y axis in radians. + /// Pitch around the x axis in radians. + /// Roll around the z axis in radians. + /// A new quaternion from the concatenated yaw, pitch, and roll angles. + public static Quaternion CreateFromYawPitchRoll(float yaw, float pitch, float roll) + { + float halfRoll = roll * 0.5f; + float halfPitch = pitch * 0.5f; + float halfYaw = yaw * 0.5f; + + float sinRoll = (float)Math.Sin(halfRoll); + float cosRoll = (float)Math.Cos(halfRoll); + float sinPitch = (float)Math.Sin(halfPitch); + float cosPitch = (float)Math.Cos(halfPitch); + float sinYaw = (float)Math.Sin(halfYaw); + float cosYaw = (float)Math.Cos(halfYaw); + + return new Quaternion((cosYaw * sinPitch * cosRoll) + (sinYaw * cosPitch * sinRoll), + (sinYaw * cosPitch * cosRoll) - (cosYaw * sinPitch * sinRoll), + (cosYaw * cosPitch * sinRoll) - (sinYaw * sinPitch * cosRoll), + (cosYaw * cosPitch * cosRoll) + (sinYaw * sinPitch * sinRoll)); + } + + /// + /// Creates a new from the specified yaw, pitch and roll angles. + /// + /// Yaw around the y axis in radians. + /// Pitch around the x axis in radians. + /// Roll around the z axis in radians. + /// A new quaternion from the concatenated yaw, pitch, and roll angles as an output parameter. + public static void CreateFromYawPitchRoll(float yaw, float pitch, float roll, out Quaternion result) + { + float halfRoll = roll * 0.5f; + float halfPitch = pitch * 0.5f; + float halfYaw = yaw * 0.5f; + + float sinRoll = (float)Math.Sin(halfRoll); + float cosRoll = (float)Math.Cos(halfRoll); + float sinPitch = (float)Math.Sin(halfPitch); + float cosPitch = (float)Math.Cos(halfPitch); + float sinYaw = (float)Math.Sin(halfYaw); + float cosYaw = (float)Math.Cos(halfYaw); + + result.X = (cosYaw * sinPitch * cosRoll) + (sinYaw * cosPitch * sinRoll); + result.Y = (sinYaw * cosPitch * cosRoll) - (cosYaw * sinPitch * sinRoll); + result.Z = (cosYaw * cosPitch * sinRoll) - (sinYaw * sinPitch * cosRoll); + result.W = (cosYaw * cosPitch * cosRoll) + (sinYaw * sinPitch * sinRoll); + } + + #endregion + + #region Divide + + /// + /// Divides a by the other . + /// + /// Source . + /// Divisor . + /// The result of dividing the quaternions. + public static Quaternion Divide(Quaternion quaternion1, Quaternion quaternion2) + { + Quaternion quaternion; + float x = quaternion1.X; + float y = quaternion1.Y; + float z = quaternion1.Z; + float w = quaternion1.W; + float num14 = (((quaternion2.X * quaternion2.X) + (quaternion2.Y * quaternion2.Y)) + (quaternion2.Z * quaternion2.Z)) + (quaternion2.W * quaternion2.W); + float num5 = 1f / num14; + float num4 = -quaternion2.X * num5; + float num3 = -quaternion2.Y * num5; + float num2 = -quaternion2.Z * num5; + float num = quaternion2.W * num5; + float num13 = (y * num2) - (z * num3); + float num12 = (z * num4) - (x * num2); + float num11 = (x * num3) - (y * num4); + float num10 = ((x * num4) + (y * num3)) + (z * num2); + quaternion.X = ((x * num) + (num4 * w)) + num13; + quaternion.Y = ((y * num) + (num3 * w)) + num12; + quaternion.Z = ((z * num) + (num2 * w)) + num11; + quaternion.W = (w * num) - num10; + return quaternion; + } + + /// + /// Divides a by the other . + /// + /// Source . + /// Divisor . + /// The result of dividing the quaternions as an output parameter. + public static void Divide(ref Quaternion quaternion1, ref Quaternion quaternion2, out Quaternion result) + { + float x = quaternion1.X; + float y = quaternion1.Y; + float z = quaternion1.Z; + float w = quaternion1.W; + float num14 = (((quaternion2.X * quaternion2.X) + (quaternion2.Y * quaternion2.Y)) + (quaternion2.Z * quaternion2.Z)) + (quaternion2.W * quaternion2.W); + float num5 = 1f / num14; + float num4 = -quaternion2.X * num5; + float num3 = -quaternion2.Y * num5; + float num2 = -quaternion2.Z * num5; + float num = quaternion2.W * num5; + float num13 = (y * num2) - (z * num3); + float num12 = (z * num4) - (x * num2); + float num11 = (x * num3) - (y * num4); + float num10 = ((x * num4) + (y * num3)) + (z * num2); + result.X = ((x * num) + (num4 * w)) + num13; + result.Y = ((y * num) + (num3 * w)) + num12; + result.Z = ((z * num) + (num2 * w)) + num11; + result.W = (w * num) - num10; + } + + #endregion + + #region Dot + + /// + /// Returns a dot product of two quaternions. + /// + /// The first quaternion. + /// The second quaternion. + /// The dot product of two quaternions. + public static float Dot(Quaternion quaternion1, Quaternion quaternion2) + { + return ((((quaternion1.X * quaternion2.X) + (quaternion1.Y * quaternion2.Y)) + (quaternion1.Z * quaternion2.Z)) + (quaternion1.W * quaternion2.W)); + } + + /// + /// Returns a dot product of two quaternions. + /// + /// The first quaternion. + /// The second quaternion. + /// The dot product of two quaternions as an output parameter. + public static void Dot(ref Quaternion quaternion1, ref Quaternion quaternion2, out float result) + { + result = (((quaternion1.X * quaternion2.X) + (quaternion1.Y * quaternion2.Y)) + (quaternion1.Z * quaternion2.Z)) + (quaternion1.W * quaternion2.W); + } + + #endregion + + #region Equals + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public override bool Equals(object obj) + { + if (obj is Quaternion) + return Equals((Quaternion)obj); + return false; + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public bool Equals(Quaternion other) + { + return X == other.X && + Y == other.Y && + Z == other.Z && + W == other.W; + } + + #endregion + + /// + /// Gets the hash code of this . + /// + /// Hash code of this . + public override int GetHashCode() + { + return X.GetHashCode() + Y.GetHashCode() + Z.GetHashCode() + W.GetHashCode(); + } + + #region Inverse + + /// + /// Returns the inverse quaternion which represents the opposite rotation. + /// + /// Source . + /// The inverse quaternion. + public static Quaternion Inverse(Quaternion quaternion) + { + Quaternion quaternion2; + float num2 = (((quaternion.X * quaternion.X) + (quaternion.Y * quaternion.Y)) + (quaternion.Z * quaternion.Z)) + (quaternion.W * quaternion.W); + float num = 1f / num2; + quaternion2.X = -quaternion.X * num; + quaternion2.Y = -quaternion.Y * num; + quaternion2.Z = -quaternion.Z * num; + quaternion2.W = quaternion.W * num; + return quaternion2; + } + + /// + /// Returns the inverse quaternion which represents the opposite rotation. + /// + /// Source . + /// The inverse quaternion as an output parameter. + public static void Inverse(ref Quaternion quaternion, out Quaternion result) + { + float num2 = (((quaternion.X * quaternion.X) + (quaternion.Y * quaternion.Y)) + (quaternion.Z * quaternion.Z)) + (quaternion.W * quaternion.W); + float num = 1f / num2; + result.X = -quaternion.X * num; + result.Y = -quaternion.Y * num; + result.Z = -quaternion.Z * num; + result.W = quaternion.W * num; + } + + #endregion + + /// + /// Returns the magnitude of the quaternion components. + /// + /// The magnitude of the quaternion components. + public float Length() + { + return (float)Math.Sqrt((X * X) + (Y * Y) + (Z * Z) + (W * W)); + } + + /// + /// Returns the squared magnitude of the quaternion components. + /// + /// The squared magnitude of the quaternion components. + public float LengthSquared() + { + return (X * X) + (Y * Y) + (Z * Z) + (W * W); + } + + #region Lerp + + /// + /// Performs a linear blend between two quaternions. + /// + /// Source . + /// Source . + /// The blend amount where 0 returns and 1 . + /// The result of linear blending between two quaternions. + public static Quaternion Lerp(Quaternion quaternion1, Quaternion quaternion2, float amount) + { + float num = amount; + float num2 = 1f - num; + Quaternion quaternion = new Quaternion(); + float num5 = (((quaternion1.X * quaternion2.X) + (quaternion1.Y * quaternion2.Y)) + (quaternion1.Z * quaternion2.Z)) + (quaternion1.W * quaternion2.W); + if (num5 >= 0f) + { + quaternion.X = (num2 * quaternion1.X) + (num * quaternion2.X); + quaternion.Y = (num2 * quaternion1.Y) + (num * quaternion2.Y); + quaternion.Z = (num2 * quaternion1.Z) + (num * quaternion2.Z); + quaternion.W = (num2 * quaternion1.W) + (num * quaternion2.W); + } + else + { + quaternion.X = (num2 * quaternion1.X) - (num * quaternion2.X); + quaternion.Y = (num2 * quaternion1.Y) - (num * quaternion2.Y); + quaternion.Z = (num2 * quaternion1.Z) - (num * quaternion2.Z); + quaternion.W = (num2 * quaternion1.W) - (num * quaternion2.W); + } + float num4 = (((quaternion.X * quaternion.X) + (quaternion.Y * quaternion.Y)) + (quaternion.Z * quaternion.Z)) + (quaternion.W * quaternion.W); + float num3 = 1f / ((float)Math.Sqrt((double)num4)); + quaternion.X *= num3; + quaternion.Y *= num3; + quaternion.Z *= num3; + quaternion.W *= num3; + return quaternion; + } + + /// + /// Performs a linear blend between two quaternions. + /// + /// Source . + /// Source . + /// The blend amount where 0 returns and 1 . + /// The result of linear blending between two quaternions as an output parameter. + public static void Lerp(ref Quaternion quaternion1, ref Quaternion quaternion2, float amount, out Quaternion result) + { + float num = amount; + float num2 = 1f - num; + float num5 = (((quaternion1.X * quaternion2.X) + (quaternion1.Y * quaternion2.Y)) + (quaternion1.Z * quaternion2.Z)) + (quaternion1.W * quaternion2.W); + if (num5 >= 0f) + { + result.X = (num2 * quaternion1.X) + (num * quaternion2.X); + result.Y = (num2 * quaternion1.Y) + (num * quaternion2.Y); + result.Z = (num2 * quaternion1.Z) + (num * quaternion2.Z); + result.W = (num2 * quaternion1.W) + (num * quaternion2.W); + } + else + { + result.X = (num2 * quaternion1.X) - (num * quaternion2.X); + result.Y = (num2 * quaternion1.Y) - (num * quaternion2.Y); + result.Z = (num2 * quaternion1.Z) - (num * quaternion2.Z); + result.W = (num2 * quaternion1.W) - (num * quaternion2.W); + } + float num4 = (((result.X * result.X) + (result.Y * result.Y)) + (result.Z * result.Z)) + (result.W * result.W); + float num3 = 1f / ((float)Math.Sqrt((double)num4)); + result.X *= num3; + result.Y *= num3; + result.Z *= num3; + result.W *= num3; + + } + + #endregion + + #region Slerp + + /// + /// Performs a spherical linear blend between two quaternions. + /// + /// Source . + /// Source . + /// The blend amount where 0 returns and 1 . + /// The result of spherical linear blending between two quaternions. + public static Quaternion Slerp(Quaternion quaternion1, Quaternion quaternion2, float amount) + { + float num2; + float num3; + Quaternion quaternion; + float num = amount; + float num4 = (((quaternion1.X * quaternion2.X) + (quaternion1.Y * quaternion2.Y)) + (quaternion1.Z * quaternion2.Z)) + (quaternion1.W * quaternion2.W); + bool flag = false; + if (num4 < 0f) + { + flag = true; + num4 = -num4; + } + if (num4 > 0.999999f) + { + num3 = 1f - num; + num2 = flag ? -num : num; + } + else + { + float num5 = (float)Math.Acos((double)num4); + float num6 = (float)(1.0 / Math.Sin((double)num5)); + num3 = ((float)Math.Sin((double)((1f - num) * num5))) * num6; + num2 = flag ? (((float)-Math.Sin((double)(num * num5))) * num6) : (((float)Math.Sin((double)(num * num5))) * num6); + } + quaternion.X = (num3 * quaternion1.X) + (num2 * quaternion2.X); + quaternion.Y = (num3 * quaternion1.Y) + (num2 * quaternion2.Y); + quaternion.Z = (num3 * quaternion1.Z) + (num2 * quaternion2.Z); + quaternion.W = (num3 * quaternion1.W) + (num2 * quaternion2.W); + return quaternion; + } + + /// + /// Performs a spherical linear blend between two quaternions. + /// + /// Source . + /// Source . + /// The blend amount where 0 returns and 1 . + /// The result of spherical linear blending between two quaternions as an output parameter. + public static void Slerp(ref Quaternion quaternion1, ref Quaternion quaternion2, float amount, out Quaternion result) + { + float num2; + float num3; + float num = amount; + float num4 = (((quaternion1.X * quaternion2.X) + (quaternion1.Y * quaternion2.Y)) + (quaternion1.Z * quaternion2.Z)) + (quaternion1.W * quaternion2.W); + bool flag = false; + if (num4 < 0f) + { + flag = true; + num4 = -num4; + } + if (num4 > 0.999999f) + { + num3 = 1f - num; + num2 = flag ? -num : num; + } + else + { + float num5 = (float)Math.Acos((double)num4); + float num6 = (float)(1.0 / Math.Sin((double)num5)); + num3 = ((float)Math.Sin((double)((1f - num) * num5))) * num6; + num2 = flag ? (((float)-Math.Sin((double)(num * num5))) * num6) : (((float)Math.Sin((double)(num * num5))) * num6); + } + result.X = (num3 * quaternion1.X) + (num2 * quaternion2.X); + result.Y = (num3 * quaternion1.Y) + (num2 * quaternion2.Y); + result.Z = (num3 * quaternion1.Z) + (num2 * quaternion2.Z); + result.W = (num3 * quaternion1.W) + (num2 * quaternion2.W); + } + + #endregion + + #region Subtract + + /// + /// Creates a new that contains subtraction of one from another. + /// + /// Source . + /// Source . + /// The result of the quaternion subtraction. + public static Quaternion Subtract(Quaternion quaternion1, Quaternion quaternion2) + { + Quaternion quaternion; + quaternion.X = quaternion1.X - quaternion2.X; + quaternion.Y = quaternion1.Y - quaternion2.Y; + quaternion.Z = quaternion1.Z - quaternion2.Z; + quaternion.W = quaternion1.W - quaternion2.W; + return quaternion; + } + + /// + /// Creates a new that contains subtraction of one from another. + /// + /// Source . + /// Source . + /// The result of the quaternion subtraction as an output parameter. + public static void Subtract(ref Quaternion quaternion1, ref Quaternion quaternion2, out Quaternion result) + { + result.X = quaternion1.X - quaternion2.X; + result.Y = quaternion1.Y - quaternion2.Y; + result.Z = quaternion1.Z - quaternion2.Z; + result.W = quaternion1.W - quaternion2.W; + } + + #endregion + + #region Multiply + + /// + /// Creates a new that contains a multiplication of two quaternions. + /// + /// Source . + /// Source . + /// The result of the quaternion multiplication. + public static Quaternion Multiply(Quaternion quaternion1, Quaternion quaternion2) + { + Quaternion quaternion; + float x = quaternion1.X; + float y = quaternion1.Y; + float z = quaternion1.Z; + float w = quaternion1.W; + float num4 = quaternion2.X; + float num3 = quaternion2.Y; + float num2 = quaternion2.Z; + float num = quaternion2.W; + float num12 = (y * num2) - (z * num3); + float num11 = (z * num4) - (x * num2); + float num10 = (x * num3) - (y * num4); + float num9 = ((x * num4) + (y * num3)) + (z * num2); + quaternion.X = ((x * num) + (num4 * w)) + num12; + quaternion.Y = ((y * num) + (num3 * w)) + num11; + quaternion.Z = ((z * num) + (num2 * w)) + num10; + quaternion.W = (w * num) - num9; + return quaternion; + } + + /// + /// Creates a new that contains a multiplication of and a scalar. + /// + /// Source . + /// Scalar value. + /// The result of the quaternion multiplication with a scalar. + public static Quaternion Multiply(Quaternion quaternion1, float scaleFactor) + { + Quaternion quaternion; + quaternion.X = quaternion1.X * scaleFactor; + quaternion.Y = quaternion1.Y * scaleFactor; + quaternion.Z = quaternion1.Z * scaleFactor; + quaternion.W = quaternion1.W * scaleFactor; + return quaternion; + } + + /// + /// Creates a new that contains a multiplication of and a scalar. + /// + /// Source . + /// Scalar value. + /// The result of the quaternion multiplication with a scalar as an output parameter. + public static void Multiply(ref Quaternion quaternion1, float scaleFactor, out Quaternion result) + { + result.X = quaternion1.X * scaleFactor; + result.Y = quaternion1.Y * scaleFactor; + result.Z = quaternion1.Z * scaleFactor; + result.W = quaternion1.W * scaleFactor; + } + + /// + /// Creates a new that contains a multiplication of two quaternions. + /// + /// Source . + /// Source . + /// The result of the quaternion multiplication as an output parameter. + public static void Multiply(ref Quaternion quaternion1, ref Quaternion quaternion2, out Quaternion result) + { + float x = quaternion1.X; + float y = quaternion1.Y; + float z = quaternion1.Z; + float w = quaternion1.W; + float num4 = quaternion2.X; + float num3 = quaternion2.Y; + float num2 = quaternion2.Z; + float num = quaternion2.W; + float num12 = (y * num2) - (z * num3); + float num11 = (z * num4) - (x * num2); + float num10 = (x * num3) - (y * num4); + float num9 = ((x * num4) + (y * num3)) + (z * num2); + result.X = ((x * num) + (num4 * w)) + num12; + result.Y = ((y * num) + (num3 * w)) + num11; + result.Z = ((z * num) + (num2 * w)) + num10; + result.W = (w * num) - num9; + } + + #endregion + + #region Negate + + /// + /// Flips the sign of the all the quaternion components. + /// + /// Source . + /// The result of the quaternion negation. + public static Quaternion Negate(Quaternion quaternion) + { + return new Quaternion(-quaternion.X, -quaternion.Y, -quaternion.Z, -quaternion.W); + } + + /// + /// Flips the sign of the all the quaternion components. + /// + /// Source . + /// The result of the quaternion negation as an output parameter. + public static void Negate(ref Quaternion quaternion, out Quaternion result) + { + result.X = -quaternion.X; + result.Y = -quaternion.Y; + result.Z = -quaternion.Z; + result.W = -quaternion.W; + } + + #endregion + + #region Normalize + + /// + /// Scales the quaternion magnitude to unit length. + /// + public void Normalize() + { + float num = 1f / ((float)Math.Sqrt((X * X) + (Y * Y) + (Z * Z) + (W * W))); + X *= num; + Y *= num; + Z *= num; + W *= num; + } + + /// + /// Scales the quaternion magnitude to unit length. + /// + /// Source . + /// The unit length quaternion. + public static Quaternion Normalize(Quaternion quaternion) + { + Quaternion result; + float num = 1f / ((float)Math.Sqrt((quaternion.X * quaternion.X) + (quaternion.Y * quaternion.Y) + (quaternion.Z * quaternion.Z) + (quaternion.W * quaternion.W))); + result.X = quaternion.X * num; + result.Y = quaternion.Y * num; + result.Z = quaternion.Z * num; + result.W = quaternion.W * num; + return result; + } + + /// + /// Scales the quaternion magnitude to unit length. + /// + /// Source . + /// The unit length quaternion an output parameter. + public static void Normalize(ref Quaternion quaternion, out Quaternion result) + { + float num = 1f / ((float)Math.Sqrt((quaternion.X * quaternion.X) + (quaternion.Y * quaternion.Y) + (quaternion.Z * quaternion.Z) + (quaternion.W * quaternion.W))); + result.X = quaternion.X * num; + result.Y = quaternion.Y * num; + result.Z = quaternion.Z * num; + result.W = quaternion.W * num; + } + + #endregion + + /// + /// Returns a representation of this in the format: + /// {X:[] Y:[] Z:[] W:[]} + /// + /// A representation of this . + public override string ToString() + { + return "{X:" + X + " Y:" + Y + " Z:" + Z + " W:" + W + "}"; + } + + /// + /// Gets a representation for this object. + /// + /// A representation for this object. + public Vector4 ToVector4() + { + return new Vector4(X, Y, Z, W); + } + + public void Deconstruct(out float x, out float y, out float z, out float w) + { + x = X; + y = Y; + z = Z; + w = W; + } + + #endregion + + #region Operators + + /// + /// Adds two quaternions. + /// + /// Source on the left of the add sign. + /// Source on the right of the add sign. + /// Sum of the vectors. + public static Quaternion operator +(Quaternion quaternion1, Quaternion quaternion2) + { + Quaternion quaternion; + quaternion.X = quaternion1.X + quaternion2.X; + quaternion.Y = quaternion1.Y + quaternion2.Y; + quaternion.Z = quaternion1.Z + quaternion2.Z; + quaternion.W = quaternion1.W + quaternion2.W; + return quaternion; + } + + /// + /// Divides a by the other . + /// + /// Source on the left of the div sign. + /// Divisor on the right of the div sign. + /// The result of dividing the quaternions. + public static Quaternion operator /(Quaternion quaternion1, Quaternion quaternion2) + { + Quaternion quaternion; + float x = quaternion1.X; + float y = quaternion1.Y; + float z = quaternion1.Z; + float w = quaternion1.W; + float num14 = (((quaternion2.X * quaternion2.X) + (quaternion2.Y * quaternion2.Y)) + (quaternion2.Z * quaternion2.Z)) + (quaternion2.W * quaternion2.W); + float num5 = 1f / num14; + float num4 = -quaternion2.X * num5; + float num3 = -quaternion2.Y * num5; + float num2 = -quaternion2.Z * num5; + float num = quaternion2.W * num5; + float num13 = (y * num2) - (z * num3); + float num12 = (z * num4) - (x * num2); + float num11 = (x * num3) - (y * num4); + float num10 = ((x * num4) + (y * num3)) + (z * num2); + quaternion.X = ((x * num) + (num4 * w)) + num13; + quaternion.Y = ((y * num) + (num3 * w)) + num12; + quaternion.Z = ((z * num) + (num2 * w)) + num11; + quaternion.W = (w * num) - num10; + return quaternion; + } + + /// + /// Compares whether two instances are equal. + /// + /// instance on the left of the equal sign. + /// instance on the right of the equal sign. + /// true if the instances are equal; false otherwise. + public static bool operator ==(Quaternion quaternion1, Quaternion quaternion2) + { + return ((((quaternion1.X == quaternion2.X) && (quaternion1.Y == quaternion2.Y)) && (quaternion1.Z == quaternion2.Z)) && (quaternion1.W == quaternion2.W)); + } + + /// + /// Compares whether two instances are not equal. + /// + /// instance on the left of the not equal sign. + /// instance on the right of the not equal sign. + /// true if the instances are not equal; false otherwise. + public static bool operator !=(Quaternion quaternion1, Quaternion quaternion2) + { + if (((quaternion1.X == quaternion2.X) && (quaternion1.Y == quaternion2.Y)) && (quaternion1.Z == quaternion2.Z)) + { + return (quaternion1.W != quaternion2.W); + } + return true; + } + + /// + /// Multiplies two quaternions. + /// + /// Source on the left of the mul sign. + /// Source on the right of the mul sign. + /// Result of the quaternions multiplication. + public static Quaternion operator *(Quaternion quaternion1, Quaternion quaternion2) + { + Quaternion quaternion; + float x = quaternion1.X; + float y = quaternion1.Y; + float z = quaternion1.Z; + float w = quaternion1.W; + float num4 = quaternion2.X; + float num3 = quaternion2.Y; + float num2 = quaternion2.Z; + float num = quaternion2.W; + float num12 = (y * num2) - (z * num3); + float num11 = (z * num4) - (x * num2); + float num10 = (x * num3) - (y * num4); + float num9 = ((x * num4) + (y * num3)) + (z * num2); + quaternion.X = ((x * num) + (num4 * w)) + num12; + quaternion.Y = ((y * num) + (num3 * w)) + num11; + quaternion.Z = ((z * num) + (num2 * w)) + num10; + quaternion.W = (w * num) - num9; + return quaternion; + } + + /// + /// Multiplies the components of quaternion by a scalar. + /// + /// Source on the left of the mul sign. + /// Scalar value on the right of the mul sign. + /// Result of the quaternion multiplication with a scalar. + public static Quaternion operator *(Quaternion quaternion1, float scaleFactor) + { + Quaternion quaternion; + quaternion.X = quaternion1.X * scaleFactor; + quaternion.Y = quaternion1.Y * scaleFactor; + quaternion.Z = quaternion1.Z * scaleFactor; + quaternion.W = quaternion1.W * scaleFactor; + return quaternion; + } + + /// + /// Subtracts a from a . + /// + /// Source on the left of the sub sign. + /// Source on the right of the sub sign. + /// Result of the quaternion subtraction. + public static Quaternion operator -(Quaternion quaternion1, Quaternion quaternion2) + { + Quaternion quaternion; + quaternion.X = quaternion1.X - quaternion2.X; + quaternion.Y = quaternion1.Y - quaternion2.Y; + quaternion.Z = quaternion1.Z - quaternion2.Z; + quaternion.W = quaternion1.W - quaternion2.W; + return quaternion; + + } + + /// + /// Flips the sign of the all the quaternion components. + /// + /// Source on the right of the sub sign. + /// The result of the quaternion negation. + public static Quaternion operator -(Quaternion quaternion) + { + Quaternion quaternion2; + quaternion2.X = -quaternion.X; + quaternion2.Y = -quaternion.Y; + quaternion2.Z = -quaternion.Z; + quaternion2.W = -quaternion.W; + return quaternion2; + } + + #endregion + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/ScriptValue.cs b/Hawkeye.VisionBuilder.Workflow/ScriptValue.cs new file mode 100644 index 0000000..dca8663 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/ScriptValue.cs @@ -0,0 +1,6 @@ +namespace Hawkeye.VisionBuilder.Workflow; + +public class ScriptValue +{ + public string Script { get; set; } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Vector2.cs b/Hawkeye.VisionBuilder.Workflow/Vector2.cs new file mode 100644 index 0000000..cafd259 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Vector2.cs @@ -0,0 +1,1251 @@ +using System.Runtime.CompilerServices; + +namespace Hawkeye.VisionBuilder.Workflow +{ + public struct Vector2 : IEquatable + { + #region Private Fields + + private static readonly Vector2 zeroVector = new Vector2(0f, 0f); + private static readonly Vector2 unitVector = new Vector2(1f, 1f); + private static readonly Vector2 unitXVector = new Vector2(1f, 0f); + private static readonly Vector2 unitYVector = new Vector2(0f, 1f); + + #endregion + + #region Public Fields + + /// + /// The x coordinate of this . + /// + + public float X; + + /// + /// The y coordinate of this . + /// + + public float Y; + + #endregion + + #region Properties + + /// + /// Returns a with components 0, 0. + /// + public static Vector2 Zero + { + get { return zeroVector; } + } + + /// + /// Returns a with components 1, 1. + /// + public static Vector2 One + { + get { return unitVector; } + } + + /// + /// Returns a with components 1, 0. + /// + public static Vector2 UnitX + { + get { return unitXVector; } + } + + /// + /// Returns a with components 0, 1. + /// + public static Vector2 UnitY + { + get { return unitYVector; } + } + + #endregion + + #region Internal Properties + + internal string DebugDisplayString + { + get + { + return string.Concat( + this.X.ToString(), " ", + this.Y.ToString() + ); + } + } + + #endregion + + #region Constructors + + /// + /// Constructs a 2d vector with X and Y from two values. + /// + /// The x coordinate in 2d-space. + /// The y coordinate in 2d-space. + public Vector2(float x, float y) + { + this.X = x; + this.Y = y; + } + + /// + /// Constructs a 2d vector with X and Y set to the same value. + /// + /// The x and y coordinates in 2d-space. + public Vector2(float value) + { + this.X = value; + this.Y = value; + } + + #endregion + + #region Operators + + /// + /// Inverts values in the specified . + /// + /// Source on the right of the sub sign. + /// Result of the inversion. + public static Vector2 operator -(Vector2 value) + { + value.X = -value.X; + value.Y = -value.Y; + return value; + } + + /// + /// Adds two vectors. + /// + /// Source on the left of the add sign. + /// Source on the right of the add sign. + /// Sum of the vectors. + public static Vector2 operator +(Vector2 value1, Vector2 value2) + { + value1.X += value2.X; + value1.Y += value2.Y; + return value1; + } + + /// + /// Subtracts a from a . + /// + /// Source on the left of the sub sign. + /// Source on the right of the sub sign. + /// Result of the vector subtraction. + public static Vector2 operator -(Vector2 value1, Vector2 value2) + { + value1.X -= value2.X; + value1.Y -= value2.Y; + return value1; + } + + /// + /// Multiplies the components of two vectors by each other. + /// + /// Source on the left of the mul sign. + /// Source on the right of the mul sign. + /// Result of the vector multiplication. + public static Vector2 operator *(Vector2 value1, Vector2 value2) + { + value1.X *= value2.X; + value1.Y *= value2.Y; + return value1; + } + + /// + /// Multiplies the components of vector by a scalar. + /// + /// Source on the left of the mul sign. + /// Scalar value on the right of the mul sign. + /// Result of the vector multiplication with a scalar. + public static Vector2 operator *(Vector2 value, float scaleFactor) + { + value.X *= scaleFactor; + value.Y *= scaleFactor; + return value; + } + + /// + /// Multiplies the components of vector by a scalar. + /// + /// Scalar value on the left of the mul sign. + /// Source on the right of the mul sign. + /// Result of the vector multiplication with a scalar. + public static Vector2 operator *(float scaleFactor, Vector2 value) + { + value.X *= scaleFactor; + value.Y *= scaleFactor; + return value; + } + + /// + /// Divides the components of a by the components of another . + /// + /// Source on the left of the div sign. + /// Divisor on the right of the div sign. + /// The result of dividing the vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator /(Vector2 value1, Vector2 value2) + { + value1.X /= value2.X; + value1.Y /= value2.Y; + return value1; + } + + /// + /// Divides the components of a by a scalar. + /// + /// Source on the left of the div sign. + /// Divisor scalar on the right of the div sign. + /// The result of dividing a vector by a scalar. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator /(Vector2 value1, float divider) + { + float factor = 1 / divider; + value1.X *= factor; + value1.Y *= factor; + return value1; + } + + /// + /// Compares whether two instances are equal. + /// + /// instance on the left of the equal sign. + /// instance on the right of the equal sign. + /// true if the instances are equal; false otherwise. + public static bool operator ==(Vector2 value1, Vector2 value2) + { + return value1.X == value2.X && value1.Y == value2.Y; + } + + /// + /// Compares whether two instances are not equal. + /// + /// instance on the left of the not equal sign. + /// instance on the right of the not equal sign. + /// true if the instances are not equal; false otherwise. + public static bool operator !=(Vector2 value1, Vector2 value2) + { + return value1.X != value2.X || value1.Y != value2.Y; + } + + #endregion + + #region Public Methods + + /// + /// Performs vector addition on and . + /// + /// The first vector to add. + /// The second vector to add. + /// The result of the vector addition. + public static Vector2 Add(Vector2 value1, Vector2 value2) + { + value1.X += value2.X; + value1.Y += value2.Y; + return value1; + } + + /// + /// Performs vector addition on and + /// , storing the result of the + /// addition in . + /// + /// The first vector to add. + /// The second vector to add. + /// The result of the vector addition. + public static void Add(ref Vector2 value1, ref Vector2 value2, out Vector2 result) + { + result.X = value1.X + value2.X; + result.Y = value1.Y + value2.Y; + } + + /// + /// Creates a new that contains the cartesian coordinates of a vector specified in barycentric coordinates and relative to 2d-triangle. + /// + /// The first vector of 2d-triangle. + /// The second vector of 2d-triangle. + /// The third vector of 2d-triangle. + /// Barycentric scalar b2 which represents a weighting factor towards second vector of 2d-triangle. + /// Barycentric scalar b3 which represents a weighting factor towards third vector of 2d-triangle. + /// The cartesian translation of barycentric coordinates. + public static Vector2 Barycentric(Vector2 value1, Vector2 value2, Vector2 value3, float amount1, float amount2) + { + return new Vector2( + MathHelper.Barycentric(value1.X, value2.X, value3.X, amount1, amount2), + MathHelper.Barycentric(value1.Y, value2.Y, value3.Y, amount1, amount2)); + } + + /// + /// Creates a new that contains the cartesian coordinates of a vector specified in barycentric coordinates and relative to 2d-triangle. + /// + /// The first vector of 2d-triangle. + /// The second vector of 2d-triangle. + /// The third vector of 2d-triangle. + /// Barycentric scalar b2 which represents a weighting factor towards second vector of 2d-triangle. + /// Barycentric scalar b3 which represents a weighting factor towards third vector of 2d-triangle. + /// The cartesian translation of barycentric coordinates as an output parameter. + public static void Barycentric(ref Vector2 value1, ref Vector2 value2, ref Vector2 value3, float amount1, float amount2, out Vector2 result) + { + result.X = MathHelper.Barycentric(value1.X, value2.X, value3.X, amount1, amount2); + result.Y = MathHelper.Barycentric(value1.Y, value2.Y, value3.Y, amount1, amount2); + } + + /// + /// Creates a new that contains CatmullRom interpolation of the specified vectors. + /// + /// The first vector in interpolation. + /// The second vector in interpolation. + /// The third vector in interpolation. + /// The fourth vector in interpolation. + /// Weighting factor. + /// The result of CatmullRom interpolation. + public static Vector2 CatmullRom(Vector2 value1, Vector2 value2, Vector2 value3, Vector2 value4, float amount) + { + return new Vector2( + MathHelper.CatmullRom(value1.X, value2.X, value3.X, value4.X, amount), + MathHelper.CatmullRom(value1.Y, value2.Y, value3.Y, value4.Y, amount)); + } + + /// + /// Creates a new that contains CatmullRom interpolation of the specified vectors. + /// + /// The first vector in interpolation. + /// The second vector in interpolation. + /// The third vector in interpolation. + /// The fourth vector in interpolation. + /// Weighting factor. + /// The result of CatmullRom interpolation as an output parameter. + public static void CatmullRom(ref Vector2 value1, ref Vector2 value2, ref Vector2 value3, ref Vector2 value4, float amount, out Vector2 result) + { + result.X = MathHelper.CatmullRom(value1.X, value2.X, value3.X, value4.X, amount); + result.Y = MathHelper.CatmullRom(value1.Y, value2.Y, value3.Y, value4.Y, amount); + } + + /// + /// Round the members of this towards positive infinity. + /// + public void Ceiling() + { + X = (float)Math.Ceiling(X); + Y = (float)Math.Ceiling(Y); + } + + /// + /// Creates a new that contains members from another vector rounded towards positive infinity. + /// + /// Source . + /// The rounded . + public static Vector2 Ceiling(Vector2 value) + { + value.X = (float)Math.Ceiling(value.X); + value.Y = (float)Math.Ceiling(value.Y); + return value; + } + + /// + /// Creates a new that contains members from another vector rounded towards positive infinity. + /// + /// Source . + /// The rounded . + public static void Ceiling(ref Vector2 value, out Vector2 result) + { + result.X = (float)Math.Ceiling(value.X); + result.Y = (float)Math.Ceiling(value.Y); + } + + /// + /// Clamps the specified value within a range. + /// + /// The value to clamp. + /// The min value. + /// The max value. + /// The clamped value. + public static Vector2 Clamp(Vector2 value1, Vector2 min, Vector2 max) + { + return new Vector2( + MathHelper.Clamp(value1.X, min.X, max.X), + MathHelper.Clamp(value1.Y, min.Y, max.Y)); + } + + /// + /// Clamps the specified value within a range. + /// + /// The value to clamp. + /// The min value. + /// The max value. + /// The clamped value as an output parameter. + public static void Clamp(ref Vector2 value1, ref Vector2 min, ref Vector2 max, out Vector2 result) + { + result.X = MathHelper.Clamp(value1.X, min.X, max.X); + result.Y = MathHelper.Clamp(value1.Y, min.Y, max.Y); + } + + /// + /// Returns the distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The distance between two vectors. + public static float Distance(Vector2 value1, Vector2 value2) + { + float v1 = value1.X - value2.X, v2 = value1.Y - value2.Y; + return (float)Math.Sqrt((v1 * v1) + (v2 * v2)); + } + + /// + /// Returns the distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The distance between two vectors as an output parameter. + public static void Distance(ref Vector2 value1, ref Vector2 value2, out float result) + { + float v1 = value1.X - value2.X, v2 = value1.Y - value2.Y; + result = (float)Math.Sqrt((v1 * v1) + (v2 * v2)); + } + + /// + /// Returns the squared distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The squared distance between two vectors. + public static float DistanceSquared(Vector2 value1, Vector2 value2) + { + float v1 = value1.X - value2.X, v2 = value1.Y - value2.Y; + return (v1 * v1) + (v2 * v2); + } + + /// + /// Returns the squared distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The squared distance between two vectors as an output parameter. + public static void DistanceSquared(ref Vector2 value1, ref Vector2 value2, out float result) + { + float v1 = value1.X - value2.X, v2 = value1.Y - value2.Y; + result = (v1 * v1) + (v2 * v2); + } + + /// + /// Divides the components of a by the components of another . + /// + /// Source . + /// Divisor . + /// The result of dividing the vectors. + public static Vector2 Divide(Vector2 value1, Vector2 value2) + { + value1.X /= value2.X; + value1.Y /= value2.Y; + return value1; + } + + /// + /// Divides the components of a by the components of another . + /// + /// Source . + /// Divisor . + /// The result of dividing the vectors as an output parameter. + public static void Divide(ref Vector2 value1, ref Vector2 value2, out Vector2 result) + { + result.X = value1.X / value2.X; + result.Y = value1.Y / value2.Y; + } + + /// + /// Divides the components of a by a scalar. + /// + /// Source . + /// Divisor scalar. + /// The result of dividing a vector by a scalar. + public static Vector2 Divide(Vector2 value1, float divider) + { + float factor = 1 / divider; + value1.X *= factor; + value1.Y *= factor; + return value1; + } + + /// + /// Divides the components of a by a scalar. + /// + /// Source . + /// Divisor scalar. + /// The result of dividing a vector by a scalar as an output parameter. + public static void Divide(ref Vector2 value1, float divider, out Vector2 result) + { + float factor = 1 / divider; + result.X = value1.X * factor; + result.Y = value1.Y * factor; + } + + /// + /// Returns a dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product of two vectors. + public static float Dot(Vector2 value1, Vector2 value2) + { + return (value1.X * value2.X) + (value1.Y * value2.Y); + } + + /// + /// Returns a dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product of two vectors as an output parameter. + public static void Dot(ref Vector2 value1, ref Vector2 value2, out float result) + { + result = (value1.X * value2.X) + (value1.Y * value2.Y); + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public override bool Equals(object obj) + { + if (obj is Vector2) + { + return Equals((Vector2)obj); + } + + return false; + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public bool Equals(Vector2 other) + { + return (X == other.X) && (Y == other.Y); + } + + /// + /// Round the members of this towards negative infinity. + /// + public void Floor() + { + X = (float)Math.Floor(X); + Y = (float)Math.Floor(Y); + } + + /// + /// Creates a new that contains members from another vector rounded towards negative infinity. + /// + /// Source . + /// The rounded . + public static Vector2 Floor(Vector2 value) + { + value.X = (float)Math.Floor(value.X); + value.Y = (float)Math.Floor(value.Y); + return value; + } + + /// + /// Creates a new that contains members from another vector rounded towards negative infinity. + /// + /// Source . + /// The rounded . + public static void Floor(ref Vector2 value, out Vector2 result) + { + result.X = (float)Math.Floor(value.X); + result.Y = (float)Math.Floor(value.Y); + } + + /// + /// Gets the hash code of this . + /// + /// Hash code of this . + public override int GetHashCode() + { + unchecked + { + return (X.GetHashCode() * 397) ^ Y.GetHashCode(); + } + } + + /// + /// Creates a new that contains hermite spline interpolation. + /// + /// The first position vector. + /// The first tangent vector. + /// The second position vector. + /// The second tangent vector. + /// Weighting factor. + /// The hermite spline interpolation vector. + public static Vector2 Hermite(Vector2 value1, Vector2 tangent1, Vector2 value2, Vector2 tangent2, float amount) + { + return new Vector2(MathHelper.Hermite(value1.X, tangent1.X, value2.X, tangent2.X, amount), MathHelper.Hermite(value1.Y, tangent1.Y, value2.Y, tangent2.Y, amount)); + } + + /// + /// Creates a new that contains hermite spline interpolation. + /// + /// The first position vector. + /// The first tangent vector. + /// The second position vector. + /// The second tangent vector. + /// Weighting factor. + /// The hermite spline interpolation vector as an output parameter. + public static void Hermite(ref Vector2 value1, ref Vector2 tangent1, ref Vector2 value2, ref Vector2 tangent2, float amount, out Vector2 result) + { + result.X = MathHelper.Hermite(value1.X, tangent1.X, value2.X, tangent2.X, amount); + result.Y = MathHelper.Hermite(value1.Y, tangent1.Y, value2.Y, tangent2.Y, amount); + } + + /// + /// Returns the length of this . + /// + /// The length of this . + public float Length() + { + return (float)Math.Sqrt((X * X) + (Y * Y)); + } + + /// + /// Returns the squared length of this . + /// + /// The squared length of this . + public float LengthSquared() + { + return (X * X) + (Y * Y); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors. + public static Vector2 Lerp(Vector2 value1, Vector2 value2, float amount) + { + return new Vector2( + MathHelper.Lerp(value1.X, value2.X, amount), + MathHelper.Lerp(value1.Y, value2.Y, amount)); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors as an output parameter. + public static void Lerp(ref Vector2 value1, ref Vector2 value2, float amount, out Vector2 result) + { + result.X = MathHelper.Lerp(value1.X, value2.X, amount); + result.Y = MathHelper.Lerp(value1.Y, value2.Y, amount); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// Uses on MathHelper for the interpolation. + /// Less efficient but more precise compared to . + /// See remarks section of on MathHelper for more info. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors. + public static Vector2 LerpPrecise(Vector2 value1, Vector2 value2, float amount) + { + return new Vector2( + MathHelper.LerpPrecise(value1.X, value2.X, amount), + MathHelper.LerpPrecise(value1.Y, value2.Y, amount)); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// Uses on MathHelper for the interpolation. + /// Less efficient but more precise compared to . + /// See remarks section of on MathHelper for more info. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors as an output parameter. + public static void LerpPrecise(ref Vector2 value1, ref Vector2 value2, float amount, out Vector2 result) + { + result.X = MathHelper.LerpPrecise(value1.X, value2.X, amount); + result.Y = MathHelper.LerpPrecise(value1.Y, value2.Y, amount); + } + + /// + /// Creates a new that contains a maximal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with maximal values from the two vectors. + public static Vector2 Max(Vector2 value1, Vector2 value2) + { + return new Vector2(value1.X > value2.X ? value1.X : value2.X, + value1.Y > value2.Y ? value1.Y : value2.Y); + } + + /// + /// Creates a new that contains a maximal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with maximal values from the two vectors as an output parameter. + public static void Max(ref Vector2 value1, ref Vector2 value2, out Vector2 result) + { + result.X = value1.X > value2.X ? value1.X : value2.X; + result.Y = value1.Y > value2.Y ? value1.Y : value2.Y; + } + + /// + /// Creates a new that contains a minimal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with minimal values from the two vectors. + public static Vector2 Min(Vector2 value1, Vector2 value2) + { + return new Vector2(value1.X < value2.X ? value1.X : value2.X, + value1.Y < value2.Y ? value1.Y : value2.Y); + } + + /// + /// Creates a new that contains a minimal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with minimal values from the two vectors as an output parameter. + public static void Min(ref Vector2 value1, ref Vector2 value2, out Vector2 result) + { + result.X = value1.X < value2.X ? value1.X : value2.X; + result.Y = value1.Y < value2.Y ? value1.Y : value2.Y; + } + + /// + /// Creates a new that contains a multiplication of two vectors. + /// + /// Source . + /// Source . + /// The result of the vector multiplication. + public static Vector2 Multiply(Vector2 value1, Vector2 value2) + { + value1.X *= value2.X; + value1.Y *= value2.Y; + return value1; + } + + /// + /// Creates a new that contains a multiplication of two vectors. + /// + /// Source . + /// Source . + /// The result of the vector multiplication as an output parameter. + public static void Multiply(ref Vector2 value1, ref Vector2 value2, out Vector2 result) + { + result.X = value1.X * value2.X; + result.Y = value1.Y * value2.Y; + } + + /// + /// Creates a new that contains a multiplication of and a scalar. + /// + /// Source . + /// Scalar value. + /// The result of the vector multiplication with a scalar. + public static Vector2 Multiply(Vector2 value1, float scaleFactor) + { + value1.X *= scaleFactor; + value1.Y *= scaleFactor; + return value1; + } + + /// + /// Creates a new that contains a multiplication of and a scalar. + /// + /// Source . + /// Scalar value. + /// The result of the multiplication with a scalar as an output parameter. + public static void Multiply(ref Vector2 value1, float scaleFactor, out Vector2 result) + { + result.X = value1.X * scaleFactor; + result.Y = value1.Y * scaleFactor; + } + + /// + /// Creates a new that contains the specified vector inversion. + /// + /// Source . + /// The result of the vector inversion. + public static Vector2 Negate(Vector2 value) + { + value.X = -value.X; + value.Y = -value.Y; + return value; + } + + /// + /// Creates a new that contains the specified vector inversion. + /// + /// Source . + /// The result of the vector inversion as an output parameter. + public static void Negate(ref Vector2 value, out Vector2 result) + { + result.X = -value.X; + result.Y = -value.Y; + } + + /// + /// Turns this to a unit vector with the same direction. + /// + public void Normalize() + { + float val = 1.0f / (float)Math.Sqrt((X * X) + (Y * Y)); + X *= val; + Y *= val; + } + + /// + /// Creates a new that contains a normalized values from another vector. + /// + /// Source . + /// Unit vector. + public static Vector2 Normalize(Vector2 value) + { + float val = 1.0f / (float)Math.Sqrt((value.X * value.X) + (value.Y * value.Y)); + value.X *= val; + value.Y *= val; + return value; + } + + /// + /// Creates a new that contains a normalized values from another vector. + /// + /// Source . + /// Unit vector as an output parameter. + public static void Normalize(ref Vector2 value, out Vector2 result) + { + float val = 1.0f / (float)Math.Sqrt((value.X * value.X) + (value.Y * value.Y)); + result.X = value.X * val; + result.Y = value.Y * val; + } + + /// + /// Creates a new that contains reflect vector of the given vector and normal. + /// + /// Source . + /// Reflection normal. + /// Reflected vector. + public static Vector2 Reflect(Vector2 vector, Vector2 normal) + { + Vector2 result; + float val = 2.0f * ((vector.X * normal.X) + (vector.Y * normal.Y)); + result.X = vector.X - (normal.X * val); + result.Y = vector.Y - (normal.Y * val); + return result; + } + + /// + /// Creates a new that contains reflect vector of the given vector and normal. + /// + /// Source . + /// Reflection normal. + /// Reflected vector as an output parameter. + public static void Reflect(ref Vector2 vector, ref Vector2 normal, out Vector2 result) + { + float val = 2.0f * ((vector.X * normal.X) + (vector.Y * normal.Y)); + result.X = vector.X - (normal.X * val); + result.Y = vector.Y - (normal.Y * val); + } + + /// + /// Round the members of this to the nearest integer value. + /// + public void Round() + { + X = (float)Math.Round(X); + Y = (float)Math.Round(Y); + } + + /// + /// Creates a new that contains members from another vector rounded to the nearest integer value. + /// + /// Source . + /// The rounded . + public static Vector2 Round(Vector2 value) + { + value.X = (float)Math.Round(value.X); + value.Y = (float)Math.Round(value.Y); + return value; + } + + /// + /// Creates a new that contains members from another vector rounded to the nearest integer value. + /// + /// Source . + /// The rounded . + public static void Round(ref Vector2 value, out Vector2 result) + { + result.X = (float)Math.Round(value.X); + result.Y = (float)Math.Round(value.Y); + } + + /// + /// Creates a new that contains cubic interpolation of the specified vectors. + /// + /// Source . + /// Source . + /// Weighting value. + /// Cubic interpolation of the specified vectors. + public static Vector2 SmoothStep(Vector2 value1, Vector2 value2, float amount) + { + return new Vector2( + MathHelper.SmoothStep(value1.X, value2.X, amount), + MathHelper.SmoothStep(value1.Y, value2.Y, amount)); + } + + /// + /// Creates a new that contains cubic interpolation of the specified vectors. + /// + /// Source . + /// Source . + /// Weighting value. + /// Cubic interpolation of the specified vectors as an output parameter. + public static void SmoothStep(ref Vector2 value1, ref Vector2 value2, float amount, out Vector2 result) + { + result.X = MathHelper.SmoothStep(value1.X, value2.X, amount); + result.Y = MathHelper.SmoothStep(value1.Y, value2.Y, amount); + } + + /// + /// Creates a new that contains subtraction of on from a another. + /// + /// Source . + /// Source . + /// The result of the vector subtraction. + public static Vector2 Subtract(Vector2 value1, Vector2 value2) + { + value1.X -= value2.X; + value1.Y -= value2.Y; + return value1; + } + + /// + /// Creates a new that contains subtraction of on from a another. + /// + /// Source . + /// Source . + /// The result of the vector subtraction as an output parameter. + public static void Subtract(ref Vector2 value1, ref Vector2 value2, out Vector2 result) + { + result.X = value1.X - value2.X; + result.Y = value1.Y - value2.Y; + } + + /// + /// Returns a representation of this in the format: + /// {X:[] Y:[]} + /// + /// A representation of this . + public override string ToString() + { + return "{X:" + X + " Y:" + Y + "}"; + } + + + /// + /// Creates a new that contains a transformation of 2d-vector by the specified . + /// + /// Source . + /// The transformation . + /// Transformed . + public static Vector2 Transform(Vector2 position, Matrix matrix) + { + return new Vector2((position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M41, (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M42); + } + + /// + /// Creates a new that contains a transformation of 2d-vector by the specified . + /// + /// Source . + /// The transformation . + /// Transformed as an output parameter. + public static void Transform(ref Vector2 position, ref Matrix matrix, out Vector2 result) + { + var x = (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M41; + var y = (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M42; + result.X = x; + result.Y = y; + } + + /// + /// Creates a new that contains a transformation of 2d-vector by the specified , representing the rotation. + /// + /// Source . + /// The which contains rotation transformation. + /// Transformed . + public static Vector2 Transform(Vector2 value, Quaternion rotation) + { + Transform(ref value, ref rotation, out value); + return value; + } + + /// + /// Creates a new that contains a transformation of 2d-vector by the specified , representing the rotation. + /// + /// Source . + /// The which contains rotation transformation. + /// Transformed as an output parameter. + public static void Transform(ref Vector2 value, ref Quaternion rotation, out Vector2 result) + { + var rot1 = new Vector3(rotation.X + rotation.X, rotation.Y + rotation.Y, rotation.Z + rotation.Z); + var rot2 = new Vector3(rotation.X, rotation.X, rotation.W); + var rot3 = new Vector3(1, rotation.Y, rotation.Z); + var rot4 = rot1 * rot2; + var rot5 = rot1 * rot3; + + var v = new Vector2(); + v.X = (float)((double)value.X * (1.0 - (double)rot5.Y - (double)rot5.Z) + (double)value.Y * ((double)rot4.Y - (double)rot4.Z)); + v.Y = (float)((double)value.X * ((double)rot4.Y + (double)rot4.Z) + (double)value.Y * (1.0 - (double)rot4.X - (double)rot5.Z)); + result.X = v.X; + result.Y = v.Y; + } + + /// + /// Apply transformation on vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The starting index of transformation in the source array. + /// The transformation . + /// Destination array. + /// The starting index in the destination array, where the first should be written. + /// The number of vectors to be transformed. + public static void Transform( + Vector2[] sourceArray, + int sourceIndex, + ref Matrix matrix, + Vector2[] destinationArray, + int destinationIndex, + int length) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (sourceArray.Length < sourceIndex + length) + throw new ArgumentException("Source array length is lesser than sourceIndex + length"); + if (destinationArray.Length < destinationIndex + length) + throw new ArgumentException("Destination array length is lesser than destinationIndex + length"); + + for (int x = 0; x < length; x++) + { + var position = sourceArray[sourceIndex + x]; + var destination = destinationArray[destinationIndex + x]; + destination.X = (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M41; + destination.Y = (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M42; + destinationArray[destinationIndex + x] = destination; + } + } + + /// + /// Apply transformation on vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The starting index of transformation in the source array. + /// The which contains rotation transformation. + /// Destination array. + /// The starting index in the destination array, where the first should be written. + /// The number of vectors to be transformed. + public static void Transform + ( + Vector2[] sourceArray, + int sourceIndex, + ref Quaternion rotation, + Vector2[] destinationArray, + int destinationIndex, + int length + ) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (sourceArray.Length < sourceIndex + length) + throw new ArgumentException("Source array length is lesser than sourceIndex + length"); + if (destinationArray.Length < destinationIndex + length) + throw new ArgumentException("Destination array length is lesser than destinationIndex + length"); + + for (int x = 0; x < length; x++) + { + var position = sourceArray[sourceIndex + x]; + var destination = destinationArray[destinationIndex + x]; + + Vector2 v; + Transform(ref position, ref rotation, out v); + + destination.X = v.X; + destination.Y = v.Y; + + destinationArray[destinationIndex + x] = destination; + } + } + + /// + /// Apply transformation on all vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The transformation . + /// Destination array. + public static void Transform( + Vector2[] sourceArray, + ref Matrix matrix, + Vector2[] destinationArray) + { + Transform(sourceArray, 0, ref matrix, destinationArray, 0, sourceArray.Length); + } + + /// + /// Apply transformation on all vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The which contains rotation transformation. + /// Destination array. + public static void Transform + ( + Vector2[] sourceArray, + ref Quaternion rotation, + Vector2[] destinationArray + ) + { + Transform(sourceArray, 0, ref rotation, destinationArray, 0, sourceArray.Length); + } + + /// + /// Creates a new that contains a transformation of the specified normal by the specified . + /// + /// Source which represents a normal vector. + /// The transformation . + /// Transformed normal. + public static Vector2 TransformNormal(Vector2 normal, Matrix matrix) + { + return new Vector2((normal.X * matrix.M11) + (normal.Y * matrix.M21), (normal.X * matrix.M12) + (normal.Y * matrix.M22)); + } + + /// + /// Creates a new that contains a transformation of the specified normal by the specified . + /// + /// Source which represents a normal vector. + /// The transformation . + /// Transformed normal as an output parameter. + public static void TransformNormal(ref Vector2 normal, ref Matrix matrix, out Vector2 result) + { + var x = (normal.X * matrix.M11) + (normal.Y * matrix.M21); + var y = (normal.X * matrix.M12) + (normal.Y * matrix.M22); + result.X = x; + result.Y = y; + } + + /// + /// Apply transformation on normals within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The starting index of transformation in the source array. + /// The transformation . + /// Destination array. + /// The starting index in the destination array, where the first should be written. + /// The number of normals to be transformed. + public static void TransformNormal + ( + Vector2[] sourceArray, + int sourceIndex, + ref Matrix matrix, + Vector2[] destinationArray, + int destinationIndex, + int length + ) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (sourceArray.Length < sourceIndex + length) + throw new ArgumentException("Source array length is lesser than sourceIndex + length"); + if (destinationArray.Length < destinationIndex + length) + throw new ArgumentException("Destination array length is lesser than destinationIndex + length"); + + for (int i = 0; i < length; i++) + { + var normal = sourceArray[sourceIndex + i]; + + destinationArray[destinationIndex + i] = new Vector2((normal.X * matrix.M11) + (normal.Y * matrix.M21), + (normal.X * matrix.M12) + (normal.Y * matrix.M22)); + } + } + + /// + /// Apply transformation on all normals within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The transformation . + /// Destination array. + public static void TransformNormal + ( + Vector2[] sourceArray, + ref Matrix matrix, + Vector2[] destinationArray + ) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (destinationArray.Length < sourceArray.Length) + throw new ArgumentException("Destination array length is lesser than source array length"); + + for (int i = 0; i < sourceArray.Length; i++) + { + var normal = sourceArray[i]; + + destinationArray[i] = new Vector2((normal.X * matrix.M11) + (normal.Y * matrix.M21), + (normal.X * matrix.M12) + (normal.Y * matrix.M22)); + } + } + + /// + /// Deconstruction method for . + /// + /// + /// + public void Deconstruct(out float x, out float y) + { + x = X; + y = Y; + } + + #endregion + + /// + /// Returns the angle in radians between the specified vector and the right vector. + /// + /// + /// + public static float AngleToRightRadians(Vector2 downVector) + { + var angle = (float)Math.Atan2(downVector.Y, downVector.X); + if (angle < 0) + angle += MathHelper.TwoPi; + return angle; + } + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Vector3.cs b/Hawkeye.VisionBuilder.Workflow/Vector3.cs new file mode 100644 index 0000000..c0b1c00 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Vector3.cs @@ -0,0 +1,1464 @@ +using System.Text; + +namespace Hawkeye.VisionBuilder.Workflow +{ + public struct Vector3 : IEquatable + { + #region Private Fields + + private static readonly Vector3 zero = new Vector3(0f, 0f, 0f); + private static readonly Vector3 one = new Vector3(1f, 1f, 1f); + private static readonly Vector3 unitX = new Vector3(1f, 0f, 0f); + private static readonly Vector3 unitY = new Vector3(0f, 1f, 0f); + private static readonly Vector3 unitZ = new Vector3(0f, 0f, 1f); + private static readonly Vector3 up = new Vector3(0f, 1f, 0f); + private static readonly Vector3 down = new Vector3(0f, -1f, 0f); + private static readonly Vector3 right = new Vector3(1f, 0f, 0f); + private static readonly Vector3 left = new Vector3(-1f, 0f, 0f); + private static readonly Vector3 forward = new Vector3(0f, 0f, -1f); + private static readonly Vector3 backward = new Vector3(0f, 0f, 1f); + + #endregion + + #region Public Fields + + /// + /// The x coordinate of this . + /// + + public float X; + + /// + /// The y coordinate of this . + /// + + public float Y; + + /// + /// The z coordinate of this . + /// + + public float Z; + + #endregion + + #region Public Properties + + /// + /// Returns a with components 0, 0, 0. + /// + public static Vector3 Zero + { + get { return zero; } + } + + /// + /// Returns a with components 1, 1, 1. + /// + public static Vector3 One + { + get { return one; } + } + + /// + /// Returns a with components 1, 0, 0. + /// + public static Vector3 UnitX + { + get { return unitX; } + } + + /// + /// Returns a with components 0, 1, 0. + /// + public static Vector3 UnitY + { + get { return unitY; } + } + + /// + /// Returns a with components 0, 0, 1. + /// + public static Vector3 UnitZ + { + get { return unitZ; } + } + + /// + /// Returns a with components 0, 1, 0. + /// + public static Vector3 Up + { + get { return up; } + } + + /// + /// Returns a with components 0, -1, 0. + /// + public static Vector3 Down + { + get { return down; } + } + + /// + /// Returns a with components 1, 0, 0. + /// + public static Vector3 Right + { + get { return right; } + } + + /// + /// Returns a with components -1, 0, 0. + /// + public static Vector3 Left + { + get { return left; } + } + + /// + /// Returns a with components 0, 0, -1. + /// + public static Vector3 Forward + { + get { return forward; } + } + + /// + /// Returns a with components 0, 0, 1. + /// + public static Vector3 Backward + { + get { return backward; } + } + + #endregion + + #region Internal Properties + + internal string DebugDisplayString + { + get + { + return string.Concat( + this.X.ToString(), " ", + this.Y.ToString(), " ", + this.Z.ToString() + ); + } + } + + #endregion + + #region Constructors + + /// + /// Constructs a 3d vector with X, Y and Z from three values. + /// + /// The x coordinate in 3d-space. + /// The y coordinate in 3d-space. + /// The z coordinate in 3d-space. + public Vector3(float x, float y, float z) + { + this.X = x; + this.Y = y; + this.Z = z; + } + + /// + /// Constructs a 3d vector with X, Y and Z set to the same value. + /// + /// The x, y and z coordinates in 3d-space. + public Vector3(float value) + { + this.X = value; + this.Y = value; + this.Z = value; + } + + /// + /// Constructs a 3d vector with X, Y from and Z from a scalar. + /// + /// The x and y coordinates in 3d-space. + /// The z coordinate in 3d-space. + public Vector3(Vector2 value, float z) + { + this.X = value.X; + this.Y = value.Y; + this.Z = z; + } + + #endregion + + #region Public Methods + + /// + /// Performs vector addition on and . + /// + /// The first vector to add. + /// The second vector to add. + /// The result of the vector addition. + public static Vector3 Add(Vector3 value1, Vector3 value2) + { + value1.X += value2.X; + value1.Y += value2.Y; + value1.Z += value2.Z; + return value1; + } + + /// + /// Performs vector addition on and + /// , storing the result of the + /// addition in . + /// + /// The first vector to add. + /// The second vector to add. + /// The result of the vector addition. + public static void Add(ref Vector3 value1, ref Vector3 value2, out Vector3 result) + { + result.X = value1.X + value2.X; + result.Y = value1.Y + value2.Y; + result.Z = value1.Z + value2.Z; + } + + /// + /// Creates a new that contains the cartesian coordinates of a vector specified in barycentric coordinates and relative to 3d-triangle. + /// + /// The first vector of 3d-triangle. + /// The second vector of 3d-triangle. + /// The third vector of 3d-triangle. + /// Barycentric scalar b2 which represents a weighting factor towards second vector of 3d-triangle. + /// Barycentric scalar b3 which represents a weighting factor towards third vector of 3d-triangle. + /// The cartesian translation of barycentric coordinates. + public static Vector3 Barycentric(Vector3 value1, Vector3 value2, Vector3 value3, float amount1, float amount2) + { + return new Vector3( + MathHelper.Barycentric(value1.X, value2.X, value3.X, amount1, amount2), + MathHelper.Barycentric(value1.Y, value2.Y, value3.Y, amount1, amount2), + MathHelper.Barycentric(value1.Z, value2.Z, value3.Z, amount1, amount2)); + } + + /// + /// Creates a new that contains the cartesian coordinates of a vector specified in barycentric coordinates and relative to 3d-triangle. + /// + /// The first vector of 3d-triangle. + /// The second vector of 3d-triangle. + /// The third vector of 3d-triangle. + /// Barycentric scalar b2 which represents a weighting factor towards second vector of 3d-triangle. + /// Barycentric scalar b3 which represents a weighting factor towards third vector of 3d-triangle. + /// The cartesian translation of barycentric coordinates as an output parameter. + public static void Barycentric(ref Vector3 value1, ref Vector3 value2, ref Vector3 value3, float amount1, float amount2, out Vector3 result) + { + result.X = MathHelper.Barycentric(value1.X, value2.X, value3.X, amount1, amount2); + result.Y = MathHelper.Barycentric(value1.Y, value2.Y, value3.Y, amount1, amount2); + result.Z = MathHelper.Barycentric(value1.Z, value2.Z, value3.Z, amount1, amount2); + } + + /// + /// Creates a new that contains CatmullRom interpolation of the specified vectors. + /// + /// The first vector in interpolation. + /// The second vector in interpolation. + /// The third vector in interpolation. + /// The fourth vector in interpolation. + /// Weighting factor. + /// The result of CatmullRom interpolation. + public static Vector3 CatmullRom(Vector3 value1, Vector3 value2, Vector3 value3, Vector3 value4, float amount) + { + return new Vector3( + MathHelper.CatmullRom(value1.X, value2.X, value3.X, value4.X, amount), + MathHelper.CatmullRom(value1.Y, value2.Y, value3.Y, value4.Y, amount), + MathHelper.CatmullRom(value1.Z, value2.Z, value3.Z, value4.Z, amount)); + } + + /// + /// Creates a new that contains CatmullRom interpolation of the specified vectors. + /// + /// The first vector in interpolation. + /// The second vector in interpolation. + /// The third vector in interpolation. + /// The fourth vector in interpolation. + /// Weighting factor. + /// The result of CatmullRom interpolation as an output parameter. + public static void CatmullRom(ref Vector3 value1, ref Vector3 value2, ref Vector3 value3, ref Vector3 value4, float amount, out Vector3 result) + { + result.X = MathHelper.CatmullRom(value1.X, value2.X, value3.X, value4.X, amount); + result.Y = MathHelper.CatmullRom(value1.Y, value2.Y, value3.Y, value4.Y, amount); + result.Z = MathHelper.CatmullRom(value1.Z, value2.Z, value3.Z, value4.Z, amount); + } + + /// + /// Round the members of this towards positive infinity. + /// + public void Ceiling() + { + X = (float)Math.Ceiling(X); + Y = (float)Math.Ceiling(Y); + Z = (float)Math.Ceiling(Z); + } + + /// + /// Creates a new that contains members from another vector rounded towards positive infinity. + /// + /// Source . + /// The rounded . + public static Vector3 Ceiling(Vector3 value) + { + value.X = (float)Math.Ceiling(value.X); + value.Y = (float)Math.Ceiling(value.Y); + value.Z = (float)Math.Ceiling(value.Z); + return value; + } + + /// + /// Creates a new that contains members from another vector rounded towards positive infinity. + /// + /// Source . + /// The rounded . + public static void Ceiling(ref Vector3 value, out Vector3 result) + { + result.X = (float)Math.Ceiling(value.X); + result.Y = (float)Math.Ceiling(value.Y); + result.Z = (float)Math.Ceiling(value.Z); + } + + /// + /// Clamps the specified value within a range. + /// + /// The value to clamp. + /// The min value. + /// The max value. + /// The clamped value. + public static Vector3 Clamp(Vector3 value1, Vector3 min, Vector3 max) + { + return new Vector3( + MathHelper.Clamp(value1.X, min.X, max.X), + MathHelper.Clamp(value1.Y, min.Y, max.Y), + MathHelper.Clamp(value1.Z, min.Z, max.Z)); + } + + /// + /// Clamps the specified value within a range. + /// + /// The value to clamp. + /// The min value. + /// The max value. + /// The clamped value as an output parameter. + public static void Clamp(ref Vector3 value1, ref Vector3 min, ref Vector3 max, out Vector3 result) + { + result.X = MathHelper.Clamp(value1.X, min.X, max.X); + result.Y = MathHelper.Clamp(value1.Y, min.Y, max.Y); + result.Z = MathHelper.Clamp(value1.Z, min.Z, max.Z); + } + + /// + /// Computes the cross product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The cross product of two vectors. + public static Vector3 Cross(Vector3 vector1, Vector3 vector2) + { + Cross(ref vector1, ref vector2, out vector1); + return vector1; + } + + /// + /// Computes the cross product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The cross product of two vectors as an output parameter. + public static void Cross(ref Vector3 vector1, ref Vector3 vector2, out Vector3 result) + { + var x = vector1.Y * vector2.Z - vector2.Y * vector1.Z; + var y = -(vector1.X * vector2.Z - vector2.X * vector1.Z); + var z = vector1.X * vector2.Y - vector2.X * vector1.Y; + result.X = x; + result.Y = y; + result.Z = z; + } + + /// + /// Returns the distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The distance between two vectors. + public static float Distance(Vector3 value1, Vector3 value2) + { + float result; + DistanceSquared(ref value1, ref value2, out result); + return (float)Math.Sqrt(result); + } + + /// + /// Returns the distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The distance between two vectors as an output parameter. + public static void Distance(ref Vector3 value1, ref Vector3 value2, out float result) + { + DistanceSquared(ref value1, ref value2, out result); + result = (float)Math.Sqrt(result); + } + + /// + /// Returns the squared distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The squared distance between two vectors. + public static float DistanceSquared(Vector3 value1, Vector3 value2) + { + return (value1.X - value2.X) * (value1.X - value2.X) + + (value1.Y - value2.Y) * (value1.Y - value2.Y) + + (value1.Z - value2.Z) * (value1.Z - value2.Z); + } + + /// + /// Returns the squared distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The squared distance between two vectors as an output parameter. + public static void DistanceSquared(ref Vector3 value1, ref Vector3 value2, out float result) + { + result = (value1.X - value2.X) * (value1.X - value2.X) + + (value1.Y - value2.Y) * (value1.Y - value2.Y) + + (value1.Z - value2.Z) * (value1.Z - value2.Z); + } + + /// + /// Divides the components of a by the components of another . + /// + /// Source . + /// Divisor . + /// The result of dividing the vectors. + public static Vector3 Divide(Vector3 value1, Vector3 value2) + { + value1.X /= value2.X; + value1.Y /= value2.Y; + value1.Z /= value2.Z; + return value1; + } + + /// + /// Divides the components of a by a scalar. + /// + /// Source . + /// Divisor scalar. + /// The result of dividing a vector by a scalar. + public static Vector3 Divide(Vector3 value1, float divider) + { + float factor = 1 / divider; + value1.X *= factor; + value1.Y *= factor; + value1.Z *= factor; + return value1; + } + + /// + /// Divides the components of a by a scalar. + /// + /// Source . + /// Divisor scalar. + /// The result of dividing a vector by a scalar as an output parameter. + public static void Divide(ref Vector3 value1, float divider, out Vector3 result) + { + float factor = 1 / divider; + result.X = value1.X * factor; + result.Y = value1.Y * factor; + result.Z = value1.Z * factor; + } + + /// + /// Divides the components of a by the components of another . + /// + /// Source . + /// Divisor . + /// The result of dividing the vectors as an output parameter. + public static void Divide(ref Vector3 value1, ref Vector3 value2, out Vector3 result) + { + result.X = value1.X / value2.X; + result.Y = value1.Y / value2.Y; + result.Z = value1.Z / value2.Z; + } + + /// + /// Returns a dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product of two vectors. + public static float Dot(Vector3 value1, Vector3 value2) + { + return value1.X * value2.X + value1.Y * value2.Y + value1.Z * value2.Z; + } + + /// + /// Returns a dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product of two vectors as an output parameter. + public static void Dot(ref Vector3 value1, ref Vector3 value2, out float result) + { + result = value1.X * value2.X + value1.Y * value2.Y + value1.Z * value2.Z; + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public override bool Equals(object obj) + { + if (!(obj is Vector3)) + return false; + + var other = (Vector3)obj; + return X == other.X && + Y == other.Y && + Z == other.Z; + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public bool Equals(Vector3 other) + { + return X == other.X && + Y == other.Y && + Z == other.Z; + } + + /// + /// Round the members of this towards negative infinity. + /// + public void Floor() + { + X = (float)Math.Floor(X); + Y = (float)Math.Floor(Y); + Z = (float)Math.Floor(Z); + } + + /// + /// Creates a new that contains members from another vector rounded towards negative infinity. + /// + /// Source . + /// The rounded . + public static Vector3 Floor(Vector3 value) + { + value.X = (float)Math.Floor(value.X); + value.Y = (float)Math.Floor(value.Y); + value.Z = (float)Math.Floor(value.Z); + return value; + } + + /// + /// Creates a new that contains members from another vector rounded towards negative infinity. + /// + /// Source . + /// The rounded . + public static void Floor(ref Vector3 value, out Vector3 result) + { + result.X = (float)Math.Floor(value.X); + result.Y = (float)Math.Floor(value.Y); + result.Z = (float)Math.Floor(value.Z); + } + + /// + /// Gets the hash code of this . + /// + /// Hash code of this . + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } + + /// + /// Creates a new that contains hermite spline interpolation. + /// + /// The first position vector. + /// The first tangent vector. + /// The second position vector. + /// The second tangent vector. + /// Weighting factor. + /// The hermite spline interpolation vector. + public static Vector3 Hermite(Vector3 value1, Vector3 tangent1, Vector3 value2, Vector3 tangent2, float amount) + { + return new Vector3(MathHelper.Hermite(value1.X, tangent1.X, value2.X, tangent2.X, amount), + MathHelper.Hermite(value1.Y, tangent1.Y, value2.Y, tangent2.Y, amount), + MathHelper.Hermite(value1.Z, tangent1.Z, value2.Z, tangent2.Z, amount)); + } + + /// + /// Creates a new that contains hermite spline interpolation. + /// + /// The first position vector. + /// The first tangent vector. + /// The second position vector. + /// The second tangent vector. + /// Weighting factor. + /// The hermite spline interpolation vector as an output parameter. + public static void Hermite(ref Vector3 value1, ref Vector3 tangent1, ref Vector3 value2, ref Vector3 tangent2, float amount, out Vector3 result) + { + result.X = MathHelper.Hermite(value1.X, tangent1.X, value2.X, tangent2.X, amount); + result.Y = MathHelper.Hermite(value1.Y, tangent1.Y, value2.Y, tangent2.Y, amount); + result.Z = MathHelper.Hermite(value1.Z, tangent1.Z, value2.Z, tangent2.Z, amount); + } + + /// + /// Returns the length of this . + /// + /// The length of this . + public float Length() + { + return (float)Math.Sqrt((X * X) + (Y * Y) + (Z * Z)); + } + + /// + /// Returns the squared length of this . + /// + /// The squared length of this . + public float LengthSquared() + { + return (X * X) + (Y * Y) + (Z * Z); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors. + public static Vector3 Lerp(Vector3 value1, Vector3 value2, float amount) + { + return new Vector3( + MathHelper.Lerp(value1.X, value2.X, amount), + MathHelper.Lerp(value1.Y, value2.Y, amount), + MathHelper.Lerp(value1.Z, value2.Z, amount)); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors as an output parameter. + public static void Lerp(ref Vector3 value1, ref Vector3 value2, float amount, out Vector3 result) + { + result.X = MathHelper.Lerp(value1.X, value2.X, amount); + result.Y = MathHelper.Lerp(value1.Y, value2.Y, amount); + result.Z = MathHelper.Lerp(value1.Z, value2.Z, amount); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// Uses on MathHelper for the interpolation. + /// Less efficient but more precise compared to . + /// See remarks section of on MathHelper for more info. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors. + public static Vector3 LerpPrecise(Vector3 value1, Vector3 value2, float amount) + { + return new Vector3( + MathHelper.LerpPrecise(value1.X, value2.X, amount), + MathHelper.LerpPrecise(value1.Y, value2.Y, amount), + MathHelper.LerpPrecise(value1.Z, value2.Z, amount)); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// Uses on MathHelper for the interpolation. + /// Less efficient but more precise compared to . + /// See remarks section of on MathHelper for more info. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors as an output parameter. + public static void LerpPrecise(ref Vector3 value1, ref Vector3 value2, float amount, out Vector3 result) + { + result.X = MathHelper.LerpPrecise(value1.X, value2.X, amount); + result.Y = MathHelper.LerpPrecise(value1.Y, value2.Y, amount); + result.Z = MathHelper.LerpPrecise(value1.Z, value2.Z, amount); + } + + /// + /// Creates a new that contains a maximal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with maximal values from the two vectors. + public static Vector3 Max(Vector3 value1, Vector3 value2) + { + return new Vector3( + MathHelper.Max(value1.X, value2.X), + MathHelper.Max(value1.Y, value2.Y), + MathHelper.Max(value1.Z, value2.Z)); + } + + /// + /// Creates a new that contains a maximal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with maximal values from the two vectors as an output parameter. + public static void Max(ref Vector3 value1, ref Vector3 value2, out Vector3 result) + { + result.X = MathHelper.Max(value1.X, value2.X); + result.Y = MathHelper.Max(value1.Y, value2.Y); + result.Z = MathHelper.Max(value1.Z, value2.Z); + } + + /// + /// Creates a new that contains a minimal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with minimal values from the two vectors. + public static Vector3 Min(Vector3 value1, Vector3 value2) + { + return new Vector3( + MathHelper.Min(value1.X, value2.X), + MathHelper.Min(value1.Y, value2.Y), + MathHelper.Min(value1.Z, value2.Z)); + } + + /// + /// Creates a new that contains a minimal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with minimal values from the two vectors as an output parameter. + public static void Min(ref Vector3 value1, ref Vector3 value2, out Vector3 result) + { + result.X = MathHelper.Min(value1.X, value2.X); + result.Y = MathHelper.Min(value1.Y, value2.Y); + result.Z = MathHelper.Min(value1.Z, value2.Z); + } + + /// + /// Creates a new that contains a multiplication of two vectors. + /// + /// Source . + /// Source . + /// The result of the vector multiplication. + public static Vector3 Multiply(Vector3 value1, Vector3 value2) + { + value1.X *= value2.X; + value1.Y *= value2.Y; + value1.Z *= value2.Z; + return value1; + } + + /// + /// Creates a new that contains a multiplication of and a scalar. + /// + /// Source . + /// Scalar value. + /// The result of the vector multiplication with a scalar. + public static Vector3 Multiply(Vector3 value1, float scaleFactor) + { + value1.X *= scaleFactor; + value1.Y *= scaleFactor; + value1.Z *= scaleFactor; + return value1; + } + + /// + /// Creates a new that contains a multiplication of and a scalar. + /// + /// Source . + /// Scalar value. + /// The result of the multiplication with a scalar as an output parameter. + public static void Multiply(ref Vector3 value1, float scaleFactor, out Vector3 result) + { + result.X = value1.X * scaleFactor; + result.Y = value1.Y * scaleFactor; + result.Z = value1.Z * scaleFactor; + } + + /// + /// Creates a new that contains a multiplication of two vectors. + /// + /// Source . + /// Source . + /// The result of the vector multiplication as an output parameter. + public static void Multiply(ref Vector3 value1, ref Vector3 value2, out Vector3 result) + { + result.X = value1.X * value2.X; + result.Y = value1.Y * value2.Y; + result.Z = value1.Z * value2.Z; + } + + /// + /// Creates a new that contains the specified vector inversion. + /// + /// Source . + /// The result of the vector inversion. + public static Vector3 Negate(Vector3 value) + { + value = new Vector3(-value.X, -value.Y, -value.Z); + return value; + } + + /// + /// Creates a new that contains the specified vector inversion. + /// + /// Source . + /// The result of the vector inversion as an output parameter. + public static void Negate(ref Vector3 value, out Vector3 result) + { + result.X = -value.X; + result.Y = -value.Y; + result.Z = -value.Z; + } + + /// + /// Turns this to a unit vector with the same direction. + /// + public void Normalize() + { + float factor = (float)Math.Sqrt((X * X) + (Y * Y) + (Z * Z)); + factor = 1f / factor; + X *= factor; + Y *= factor; + Z *= factor; + } + + /// + /// Creates a new that contains a normalized values from another vector. + /// + /// Source . + /// Unit vector. + public static Vector3 Normalize(Vector3 value) + { + float factor = (float)Math.Sqrt((value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z)); + factor = 1f / factor; + return new Vector3(value.X * factor, value.Y * factor, value.Z * factor); + } + + /// + /// Creates a new that contains a normalized values from another vector. + /// + /// Source . + /// Unit vector as an output parameter. + public static void Normalize(ref Vector3 value, out Vector3 result) + { + float factor = (float)Math.Sqrt((value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z)); + factor = 1f / factor; + result.X = value.X * factor; + result.Y = value.Y * factor; + result.Z = value.Z * factor; + } + + /// + /// Creates a new that contains reflect vector of the given vector and normal. + /// + /// Source . + /// Reflection normal. + /// Reflected vector. + public static Vector3 Reflect(Vector3 vector, Vector3 normal) + { + // I is the original array + // N is the normal of the incident plane + // R = I - (2 * N * ( DotProduct[ I,N] )) + Vector3 reflectedVector; + // inline the dotProduct here instead of calling method + float dotProduct = ((vector.X * normal.X) + (vector.Y * normal.Y)) + (vector.Z * normal.Z); + reflectedVector.X = vector.X - (2.0f * normal.X) * dotProduct; + reflectedVector.Y = vector.Y - (2.0f * normal.Y) * dotProduct; + reflectedVector.Z = vector.Z - (2.0f * normal.Z) * dotProduct; + + return reflectedVector; + } + + /// + /// Creates a new that contains reflect vector of the given vector and normal. + /// + /// Source . + /// Reflection normal. + /// Reflected vector as an output parameter. + public static void Reflect(ref Vector3 vector, ref Vector3 normal, out Vector3 result) + { + // I is the original array + // N is the normal of the incident plane + // R = I - (2 * N * ( DotProduct[ I,N] )) + + // inline the dotProduct here instead of calling method + float dotProduct = ((vector.X * normal.X) + (vector.Y * normal.Y)) + (vector.Z * normal.Z); + result.X = vector.X - (2.0f * normal.X) * dotProduct; + result.Y = vector.Y - (2.0f * normal.Y) * dotProduct; + result.Z = vector.Z - (2.0f * normal.Z) * dotProduct; + } + + /// + /// Round the members of this towards the nearest integer value. + /// + public void Round() + { + X = (float)Math.Round(X); + Y = (float)Math.Round(Y); + Z = (float)Math.Round(Z); + } + + /// + /// Creates a new that contains members from another vector rounded to the nearest integer value. + /// + /// Source . + /// The rounded . + public static Vector3 Round(Vector3 value) + { + value.X = (float)Math.Round(value.X); + value.Y = (float)Math.Round(value.Y); + value.Z = (float)Math.Round(value.Z); + return value; + } + + /// + /// Creates a new that contains members from another vector rounded to the nearest integer value. + /// + /// Source . + /// The rounded . + public static void Round(ref Vector3 value, out Vector3 result) + { + result.X = (float)Math.Round(value.X); + result.Y = (float)Math.Round(value.Y); + result.Z = (float)Math.Round(value.Z); + } + + /// + /// Creates a new that contains cubic interpolation of the specified vectors. + /// + /// Source . + /// Source . + /// Weighting value. + /// Cubic interpolation of the specified vectors. + public static Vector3 SmoothStep(Vector3 value1, Vector3 value2, float amount) + { + return new Vector3( + MathHelper.SmoothStep(value1.X, value2.X, amount), + MathHelper.SmoothStep(value1.Y, value2.Y, amount), + MathHelper.SmoothStep(value1.Z, value2.Z, amount)); + } + + /// + /// Creates a new that contains cubic interpolation of the specified vectors. + /// + /// Source . + /// Source . + /// Weighting value. + /// Cubic interpolation of the specified vectors as an output parameter. + public static void SmoothStep(ref Vector3 value1, ref Vector3 value2, float amount, out Vector3 result) + { + result.X = MathHelper.SmoothStep(value1.X, value2.X, amount); + result.Y = MathHelper.SmoothStep(value1.Y, value2.Y, amount); + result.Z = MathHelper.SmoothStep(value1.Z, value2.Z, amount); + } + + /// + /// Creates a new that contains subtraction of on from a another. + /// + /// Source . + /// Source . + /// The result of the vector subtraction. + public static Vector3 Subtract(Vector3 value1, Vector3 value2) + { + value1.X -= value2.X; + value1.Y -= value2.Y; + value1.Z -= value2.Z; + return value1; + } + + /// + /// Creates a new that contains subtraction of on from a another. + /// + /// Source . + /// Source . + /// The result of the vector subtraction as an output parameter. + public static void Subtract(ref Vector3 value1, ref Vector3 value2, out Vector3 result) + { + result.X = value1.X - value2.X; + result.Y = value1.Y - value2.Y; + result.Z = value1.Z - value2.Z; + } + + /// + /// Returns a representation of this in the format: + /// {X:[] Y:[] Z:[]} + /// + /// A representation of this . + public override string ToString() + { + StringBuilder sb = new StringBuilder(32); + sb.Append("{X:"); + sb.Append(this.X); + sb.Append(" Y:"); + sb.Append(this.Y); + sb.Append(" Z:"); + sb.Append(this.Z); + sb.Append("}"); + return sb.ToString(); + } + + #region Transform + + /// + /// Creates a new that contains a transformation of 3d-vector by the specified . + /// + /// Source . + /// The transformation . + /// Transformed . + public static Vector3 Transform(Vector3 position, Matrix matrix) + { + Transform(ref position, ref matrix, out position); + return position; + } + + /// + /// Creates a new that contains a transformation of 3d-vector by the specified . + /// + /// Source . + /// The transformation . + /// Transformed as an output parameter. + public static void Transform(ref Vector3 position, ref Matrix matrix, out Vector3 result) + { + var x = (position.X * matrix.M11) + (position.Y * matrix.M21) + (position.Z * matrix.M31) + matrix.M41; + var y = (position.X * matrix.M12) + (position.Y * matrix.M22) + (position.Z * matrix.M32) + matrix.M42; + var z = (position.X * matrix.M13) + (position.Y * matrix.M23) + (position.Z * matrix.M33) + matrix.M43; + result.X = x; + result.Y = y; + result.Z = z; + } + + /// + /// Creates a new that contains a transformation of 3d-vector by the specified , representing the rotation. + /// + /// Source . + /// The which contains rotation transformation. + /// Transformed . + public static Vector3 Transform(Vector3 value, Quaternion rotation) + { + Vector3 result; + Transform(ref value, ref rotation, out result); + return result; + } + + /// + /// Creates a new that contains a transformation of 3d-vector by the specified , representing the rotation. + /// + /// Source . + /// The which contains rotation transformation. + /// Transformed as an output parameter. + public static void Transform(ref Vector3 value, ref Quaternion rotation, out Vector3 result) + { + float x = 2 * (rotation.Y * value.Z - rotation.Z * value.Y); + float y = 2 * (rotation.Z * value.X - rotation.X * value.Z); + float z = 2 * (rotation.X * value.Y - rotation.Y * value.X); + + result.X = value.X + x * rotation.W + (rotation.Y * z - rotation.Z * y); + result.Y = value.Y + y * rotation.W + (rotation.Z * x - rotation.X * z); + result.Z = value.Z + z * rotation.W + (rotation.X * y - rotation.Y * x); + } + + /// + /// Apply transformation on vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The starting index of transformation in the source array. + /// The transformation . + /// Destination array. + /// The starting index in the destination array, where the first should be written. + /// The number of vectors to be transformed. + public static void Transform(Vector3[] sourceArray, int sourceIndex, ref Matrix matrix, Vector3[] destinationArray, int destinationIndex, int length) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (sourceArray.Length < sourceIndex + length) + throw new ArgumentException("Source array length is lesser than sourceIndex + length"); + if (destinationArray.Length < destinationIndex + length) + throw new ArgumentException("Destination array length is lesser than destinationIndex + length"); + + // TODO: Are there options on some platforms to implement a vectorized version of this? + + for (var i = 0; i < length; i++) + { + var position = sourceArray[sourceIndex + i]; + destinationArray[destinationIndex + i] = + new Vector3( + (position.X * matrix.M11) + (position.Y * matrix.M21) + (position.Z * matrix.M31) + matrix.M41, + (position.X * matrix.M12) + (position.Y * matrix.M22) + (position.Z * matrix.M32) + matrix.M42, + (position.X * matrix.M13) + (position.Y * matrix.M23) + (position.Z * matrix.M33) + matrix.M43); + } + } + + /// + /// Apply transformation on vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The starting index of transformation in the source array. + /// The which contains rotation transformation. + /// Destination array. + /// The starting index in the destination array, where the first should be written. + /// The number of vectors to be transformed. + public static void Transform(Vector3[] sourceArray, int sourceIndex, ref Quaternion rotation, Vector3[] destinationArray, int destinationIndex, int length) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (sourceArray.Length < sourceIndex + length) + throw new ArgumentException("Source array length is lesser than sourceIndex + length"); + if (destinationArray.Length < destinationIndex + length) + throw new ArgumentException("Destination array length is lesser than destinationIndex + length"); + + // TODO: Are there options on some platforms to implement a vectorized version of this? + + for (var i = 0; i < length; i++) + { + var position = sourceArray[sourceIndex + i]; + + float x = 2 * (rotation.Y * position.Z - rotation.Z * position.Y); + float y = 2 * (rotation.Z * position.X - rotation.X * position.Z); + float z = 2 * (rotation.X * position.Y - rotation.Y * position.X); + + destinationArray[destinationIndex + i] = + new Vector3( + position.X + x * rotation.W + (rotation.Y * z - rotation.Z * y), + position.Y + y * rotation.W + (rotation.Z * x - rotation.X * z), + position.Z + z * rotation.W + (rotation.X * y - rotation.Y * x)); + } + } + + /// + /// Apply transformation on all vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The transformation . + /// Destination array. + public static void Transform(Vector3[] sourceArray, ref Matrix matrix, Vector3[] destinationArray) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (destinationArray.Length < sourceArray.Length) + throw new ArgumentException("Destination array length is lesser than source array length"); + + // TODO: Are there options on some platforms to implement a vectorized version of this? + + for (var i = 0; i < sourceArray.Length; i++) + { + var position = sourceArray[i]; + destinationArray[i] = + new Vector3( + (position.X * matrix.M11) + (position.Y * matrix.M21) + (position.Z * matrix.M31) + matrix.M41, + (position.X * matrix.M12) + (position.Y * matrix.M22) + (position.Z * matrix.M32) + matrix.M42, + (position.X * matrix.M13) + (position.Y * matrix.M23) + (position.Z * matrix.M33) + matrix.M43); + } + } + + /// + /// Apply transformation on all vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The which contains rotation transformation. + /// Destination array. + public static void Transform(Vector3[] sourceArray, ref Quaternion rotation, Vector3[] destinationArray) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (destinationArray.Length < sourceArray.Length) + throw new ArgumentException("Destination array length is lesser than source array length"); + + // TODO: Are there options on some platforms to implement a vectorized version of this? + + for (var i = 0; i < sourceArray.Length; i++) + { + var position = sourceArray[i]; + + float x = 2 * (rotation.Y * position.Z - rotation.Z * position.Y); + float y = 2 * (rotation.Z * position.X - rotation.X * position.Z); + float z = 2 * (rotation.X * position.Y - rotation.Y * position.X); + + destinationArray[i] = + new Vector3( + position.X + x * rotation.W + (rotation.Y * z - rotation.Z * y), + position.Y + y * rotation.W + (rotation.Z * x - rotation.X * z), + position.Z + z * rotation.W + (rotation.X * y - rotation.Y * x)); + } + } + + #endregion + + #region TransformNormal + + /// + /// Creates a new that contains a transformation of the specified normal by the specified . + /// + /// Source which represents a normal vector. + /// The transformation . + /// Transformed normal. + public static Vector3 TransformNormal(Vector3 normal, Matrix matrix) + { + TransformNormal(ref normal, ref matrix, out normal); + return normal; + } + + /// + /// Creates a new that contains a transformation of the specified normal by the specified . + /// + /// Source which represents a normal vector. + /// The transformation . + /// Transformed normal as an output parameter. + public static void TransformNormal(ref Vector3 normal, ref Matrix matrix, out Vector3 result) + { + var x = (normal.X * matrix.M11) + (normal.Y * matrix.M21) + (normal.Z * matrix.M31); + var y = (normal.X * matrix.M12) + (normal.Y * matrix.M22) + (normal.Z * matrix.M32); + var z = (normal.X * matrix.M13) + (normal.Y * matrix.M23) + (normal.Z * matrix.M33); + result.X = x; + result.Y = y; + result.Z = z; + } + + /// + /// Apply transformation on normals within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The starting index of transformation in the source array. + /// The transformation . + /// Destination array. + /// The starting index in the destination array, where the first should be written. + /// The number of normals to be transformed. + public static void TransformNormal(Vector3[] sourceArray, + int sourceIndex, + ref Matrix matrix, + Vector3[] destinationArray, + int destinationIndex, + int length) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (sourceArray.Length < sourceIndex + length) + throw new ArgumentException("Source array length is lesser than sourceIndex + length"); + if (destinationArray.Length < destinationIndex + length) + throw new ArgumentException("Destination array length is lesser than destinationIndex + length"); + + for (int x = 0; x < length; x++) + { + var normal = sourceArray[sourceIndex + x]; + + destinationArray[destinationIndex + x] = + new Vector3( + (normal.X * matrix.M11) + (normal.Y * matrix.M21) + (normal.Z * matrix.M31), + (normal.X * matrix.M12) + (normal.Y * matrix.M22) + (normal.Z * matrix.M32), + (normal.X * matrix.M13) + (normal.Y * matrix.M23) + (normal.Z * matrix.M33)); + } + } + + /// + /// Apply transformation on all normals within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The transformation . + /// Destination array. + public static void TransformNormal(Vector3[] sourceArray, ref Matrix matrix, Vector3[] destinationArray) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (destinationArray.Length < sourceArray.Length) + throw new ArgumentException("Destination array length is lesser than source array length"); + + for (var i = 0; i < sourceArray.Length; i++) + { + var normal = sourceArray[i]; + + destinationArray[i] = + new Vector3( + (normal.X * matrix.M11) + (normal.Y * matrix.M21) + (normal.Z * matrix.M31), + (normal.X * matrix.M12) + (normal.Y * matrix.M22) + (normal.Z * matrix.M32), + (normal.X * matrix.M13) + (normal.Y * matrix.M23) + (normal.Z * matrix.M33)); + } + } + + #endregion + + /// + /// Deconstruction method for . + /// + /// + /// + /// + public void Deconstruct(out float x, out float y, out float z) + { + x = X; + y = Y; + z = Z; + } + + #endregion + + #region Operators + + /// + /// Compares whether two instances are equal. + /// + /// instance on the left of the equal sign. + /// instance on the right of the equal sign. + /// true if the instances are equal; false otherwise. + public static bool operator ==(Vector3 value1, Vector3 value2) + { + return value1.X == value2.X + && value1.Y == value2.Y + && value1.Z == value2.Z; + } + + /// + /// Compares whether two instances are not equal. + /// + /// instance on the left of the not equal sign. + /// instance on the right of the not equal sign. + /// true if the instances are not equal; false otherwise. + public static bool operator !=(Vector3 value1, Vector3 value2) + { + return !(value1 == value2); + } + + /// + /// Adds two vectors. + /// + /// Source on the left of the add sign. + /// Source on the right of the add sign. + /// Sum of the vectors. + public static Vector3 operator +(Vector3 value1, Vector3 value2) + { + value1.X += value2.X; + value1.Y += value2.Y; + value1.Z += value2.Z; + return value1; + } + + /// + /// Inverts values in the specified . + /// + /// Source on the right of the sub sign. + /// Result of the inversion. + public static Vector3 operator -(Vector3 value) + { + value = new Vector3(-value.X, -value.Y, -value.Z); + return value; + } + + /// + /// Subtracts a from a . + /// + /// Source on the left of the sub sign. + /// Source on the right of the sub sign. + /// Result of the vector subtraction. + public static Vector3 operator -(Vector3 value1, Vector3 value2) + { + value1.X -= value2.X; + value1.Y -= value2.Y; + value1.Z -= value2.Z; + return value1; + } + + /// + /// Multiplies the components of two vectors by each other. + /// + /// Source on the left of the mul sign. + /// Source on the right of the mul sign. + /// Result of the vector multiplication. + public static Vector3 operator *(Vector3 value1, Vector3 value2) + { + value1.X *= value2.X; + value1.Y *= value2.Y; + value1.Z *= value2.Z; + return value1; + } + + /// + /// Multiplies the components of vector by a scalar. + /// + /// Source on the left of the mul sign. + /// Scalar value on the right of the mul sign. + /// Result of the vector multiplication with a scalar. + public static Vector3 operator *(Vector3 value, float scaleFactor) + { + value.X *= scaleFactor; + value.Y *= scaleFactor; + value.Z *= scaleFactor; + return value; + } + + /// + /// Multiplies the components of vector by a scalar. + /// + /// Scalar value on the left of the mul sign. + /// Source on the right of the mul sign. + /// Result of the vector multiplication with a scalar. + public static Vector3 operator *(float scaleFactor, Vector3 value) + { + value.X *= scaleFactor; + value.Y *= scaleFactor; + value.Z *= scaleFactor; + return value; + } + + /// + /// Divides the components of a by the components of another . + /// + /// Source on the left of the div sign. + /// Divisor on the right of the div sign. + /// The result of dividing the vectors. + public static Vector3 operator /(Vector3 value1, Vector3 value2) + { + value1.X /= value2.X; + value1.Y /= value2.Y; + value1.Z /= value2.Z; + return value1; + } + + /// + /// Divides the components of a by a scalar. + /// + /// Source on the left of the div sign. + /// Divisor scalar on the right of the div sign. + /// The result of dividing a vector by a scalar. + public static Vector3 operator /(Vector3 value1, float divider) + { + float factor = 1 / divider; + value1.X *= factor; + value1.Y *= factor; + value1.Z *= factor; + return value1; + } + + #endregion + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Vector4.cs b/Hawkeye.VisionBuilder.Workflow/Vector4.cs new file mode 100644 index 0000000..cd25881 --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/Vector4.cs @@ -0,0 +1,1395 @@ +namespace Hawkeye.VisionBuilder.Workflow +{ + public struct Vector4 : IEquatable + { + #region Private Fields + + private static readonly Vector4 zero = new Vector4(); + private static readonly Vector4 one = new Vector4(1f, 1f, 1f, 1f); + private static readonly Vector4 unitX = new Vector4(1f, 0f, 0f, 0f); + private static readonly Vector4 unitY = new Vector4(0f, 1f, 0f, 0f); + private static readonly Vector4 unitZ = new Vector4(0f, 0f, 1f, 0f); + private static readonly Vector4 unitW = new Vector4(0f, 0f, 0f, 1f); + + #endregion + + #region Public Fields + + /// + /// The x coordinate of this . + /// + + public float X; + + /// + /// The y coordinate of this . + /// + + public float Y; + + /// + /// The z coordinate of this . + /// + + public float Z; + + /// + /// The w coordinate of this . + /// + + public float W; + + #endregion + + #region Public Properties + + /// + /// Returns a with components 0, 0, 0, 0. + /// + public static Vector4 Zero + { + get { return zero; } + } + + /// + /// Returns a with components 1, 1, 1, 1. + /// + public static Vector4 One + { + get { return one; } + } + + /// + /// Returns a with components 1, 0, 0, 0. + /// + public static Vector4 UnitX + { + get { return unitX; } + } + + /// + /// Returns a with components 0, 1, 0, 0. + /// + public static Vector4 UnitY + { + get { return unitY; } + } + + /// + /// Returns a with components 0, 0, 1, 0. + /// + public static Vector4 UnitZ + { + get { return unitZ; } + } + + /// + /// Returns a with components 0, 0, 0, 1. + /// + public static Vector4 UnitW + { + get { return unitW; } + } + + #endregion + + #region Internal Properties + + internal string DebugDisplayString + { + get + { + return string.Concat( + this.X.ToString(), " ", + this.Y.ToString(), " ", + this.Z.ToString(), " ", + this.W.ToString() + ); + } + } + + #endregion + + #region Constructors + + /// + /// Constructs a 3d vector with X, Y, Z and W from four values. + /// + /// The x coordinate in 4d-space. + /// The y coordinate in 4d-space. + /// The z coordinate in 4d-space. + /// The w coordinate in 4d-space. + public Vector4(float x, float y, float z, float w) + { + this.X = x; + this.Y = y; + this.Z = z; + this.W = w; + } + + /// + /// Constructs a 3d vector with X and Z from and Z and W from the scalars. + /// + /// The x and y coordinates in 4d-space. + /// The z coordinate in 4d-space. + /// The w coordinate in 4d-space. + public Vector4(Vector2 value, float z, float w) + { + this.X = value.X; + this.Y = value.Y; + this.Z = z; + this.W = w; + } + + /// + /// Constructs a 3d vector with X, Y, Z from and W from a scalar. + /// + /// The x, y and z coordinates in 4d-space. + /// The w coordinate in 4d-space. + public Vector4(Vector3 value, float w) + { + this.X = value.X; + this.Y = value.Y; + this.Z = value.Z; + this.W = w; + } + + /// + /// Constructs a 4d vector with X, Y, Z and W set to the same value. + /// + /// The x, y, z and w coordinates in 4d-space. + public Vector4(float value) + { + this.X = value; + this.Y = value; + this.Z = value; + this.W = value; + } + + #endregion + + #region Public Methods + + /// + /// Performs vector addition on and . + /// + /// The first vector to add. + /// The second vector to add. + /// The result of the vector addition. + public static Vector4 Add(Vector4 value1, Vector4 value2) + { + value1.X += value2.X; + value1.Y += value2.Y; + value1.Z += value2.Z; + value1.W += value2.W; + return value1; + } + + /// + /// Performs vector addition on and + /// , storing the result of the + /// addition in . + /// + /// The first vector to add. + /// The second vector to add. + /// The result of the vector addition. + public static void Add(ref Vector4 value1, ref Vector4 value2, out Vector4 result) + { + result.X = value1.X + value2.X; + result.Y = value1.Y + value2.Y; + result.Z = value1.Z + value2.Z; + result.W = value1.W + value2.W; + } + + /// + /// Creates a new that contains the cartesian coordinates of a vector specified in barycentric coordinates and relative to 4d-triangle. + /// + /// The first vector of 4d-triangle. + /// The second vector of 4d-triangle. + /// The third vector of 4d-triangle. + /// Barycentric scalar b2 which represents a weighting factor towards second vector of 4d-triangle. + /// Barycentric scalar b3 which represents a weighting factor towards third vector of 4d-triangle. + /// The cartesian translation of barycentric coordinates. + public static Vector4 Barycentric(Vector4 value1, Vector4 value2, Vector4 value3, float amount1, float amount2) + { + return new Vector4( + MathHelper.Barycentric(value1.X, value2.X, value3.X, amount1, amount2), + MathHelper.Barycentric(value1.Y, value2.Y, value3.Y, amount1, amount2), + MathHelper.Barycentric(value1.Z, value2.Z, value3.Z, amount1, amount2), + MathHelper.Barycentric(value1.W, value2.W, value3.W, amount1, amount2)); + } + + /// + /// Creates a new that contains the cartesian coordinates of a vector specified in barycentric coordinates and relative to 4d-triangle. + /// + /// The first vector of 4d-triangle. + /// The second vector of 4d-triangle. + /// The third vector of 4d-triangle. + /// Barycentric scalar b2 which represents a weighting factor towards second vector of 4d-triangle. + /// Barycentric scalar b3 which represents a weighting factor towards third vector of 4d-triangle. + /// The cartesian translation of barycentric coordinates as an output parameter. + public static void Barycentric(ref Vector4 value1, ref Vector4 value2, ref Vector4 value3, float amount1, float amount2, out Vector4 result) + { + result.X = MathHelper.Barycentric(value1.X, value2.X, value3.X, amount1, amount2); + result.Y = MathHelper.Barycentric(value1.Y, value2.Y, value3.Y, amount1, amount2); + result.Z = MathHelper.Barycentric(value1.Z, value2.Z, value3.Z, amount1, amount2); + result.W = MathHelper.Barycentric(value1.W, value2.W, value3.W, amount1, amount2); + } + + /// + /// Creates a new that contains CatmullRom interpolation of the specified vectors. + /// + /// The first vector in interpolation. + /// The second vector in interpolation. + /// The third vector in interpolation. + /// The fourth vector in interpolation. + /// Weighting factor. + /// The result of CatmullRom interpolation. + public static Vector4 CatmullRom(Vector4 value1, Vector4 value2, Vector4 value3, Vector4 value4, float amount) + { + return new Vector4( + MathHelper.CatmullRom(value1.X, value2.X, value3.X, value4.X, amount), + MathHelper.CatmullRom(value1.Y, value2.Y, value3.Y, value4.Y, amount), + MathHelper.CatmullRom(value1.Z, value2.Z, value3.Z, value4.Z, amount), + MathHelper.CatmullRom(value1.W, value2.W, value3.W, value4.W, amount)); + } + + /// + /// Creates a new that contains CatmullRom interpolation of the specified vectors. + /// + /// The first vector in interpolation. + /// The second vector in interpolation. + /// The third vector in interpolation. + /// The fourth vector in interpolation. + /// Weighting factor. + /// The result of CatmullRom interpolation as an output parameter. + public static void CatmullRom(ref Vector4 value1, ref Vector4 value2, ref Vector4 value3, ref Vector4 value4, float amount, out Vector4 result) + { + result.X = MathHelper.CatmullRom(value1.X, value2.X, value3.X, value4.X, amount); + result.Y = MathHelper.CatmullRom(value1.Y, value2.Y, value3.Y, value4.Y, amount); + result.Z = MathHelper.CatmullRom(value1.Z, value2.Z, value3.Z, value4.Z, amount); + result.W = MathHelper.CatmullRom(value1.W, value2.W, value3.W, value4.W, amount); + } + + /// + /// Round the members of this towards positive infinity. + /// + public void Ceiling() + { + X = (float)Math.Ceiling(X); + Y = (float)Math.Ceiling(Y); + Z = (float)Math.Ceiling(Z); + W = (float)Math.Ceiling(W); + } + + /// + /// Creates a new that contains members from another vector rounded towards positive infinity. + /// + /// Source . + /// The rounded . + public static Vector4 Ceiling(Vector4 value) + { + value.X = (float)Math.Ceiling(value.X); + value.Y = (float)Math.Ceiling(value.Y); + value.Z = (float)Math.Ceiling(value.Z); + value.W = (float)Math.Ceiling(value.W); + return value; + } + + /// + /// Creates a new that contains members from another vector rounded towards positive infinity. + /// + /// Source . + /// The rounded . + public static void Ceiling(ref Vector4 value, out Vector4 result) + { + result.X = (float)Math.Ceiling(value.X); + result.Y = (float)Math.Ceiling(value.Y); + result.Z = (float)Math.Ceiling(value.Z); + result.W = (float)Math.Ceiling(value.W); + } + + /// + /// Clamps the specified value within a range. + /// + /// The value to clamp. + /// The min value. + /// The max value. + /// The clamped value. + public static Vector4 Clamp(Vector4 value1, Vector4 min, Vector4 max) + { + return new Vector4( + MathHelper.Clamp(value1.X, min.X, max.X), + MathHelper.Clamp(value1.Y, min.Y, max.Y), + MathHelper.Clamp(value1.Z, min.Z, max.Z), + MathHelper.Clamp(value1.W, min.W, max.W)); + } + + /// + /// Clamps the specified value within a range. + /// + /// The value to clamp. + /// The min value. + /// The max value. + /// The clamped value as an output parameter. + public static void Clamp(ref Vector4 value1, ref Vector4 min, ref Vector4 max, out Vector4 result) + { + result.X = MathHelper.Clamp(value1.X, min.X, max.X); + result.Y = MathHelper.Clamp(value1.Y, min.Y, max.Y); + result.Z = MathHelper.Clamp(value1.Z, min.Z, max.Z); + result.W = MathHelper.Clamp(value1.W, min.W, max.W); + } + + /// + /// Returns the distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The distance between two vectors. + public static float Distance(Vector4 value1, Vector4 value2) + { + return (float)Math.Sqrt(DistanceSquared(value1, value2)); + } + + /// + /// Returns the distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The distance between two vectors as an output parameter. + public static void Distance(ref Vector4 value1, ref Vector4 value2, out float result) + { + result = (float)Math.Sqrt(DistanceSquared(value1, value2)); + } + + /// + /// Returns the squared distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The squared distance between two vectors. + public static float DistanceSquared(Vector4 value1, Vector4 value2) + { + return (value1.W - value2.W) * (value1.W - value2.W) + + (value1.X - value2.X) * (value1.X - value2.X) + + (value1.Y - value2.Y) * (value1.Y - value2.Y) + + (value1.Z - value2.Z) * (value1.Z - value2.Z); + } + + /// + /// Returns the squared distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The squared distance between two vectors as an output parameter. + public static void DistanceSquared(ref Vector4 value1, ref Vector4 value2, out float result) + { + result = (value1.W - value2.W) * (value1.W - value2.W) + + (value1.X - value2.X) * (value1.X - value2.X) + + (value1.Y - value2.Y) * (value1.Y - value2.Y) + + (value1.Z - value2.Z) * (value1.Z - value2.Z); + } + + /// + /// Divides the components of a by the components of another . + /// + /// Source . + /// Divisor . + /// The result of dividing the vectors. + public static Vector4 Divide(Vector4 value1, Vector4 value2) + { + value1.W /= value2.W; + value1.X /= value2.X; + value1.Y /= value2.Y; + value1.Z /= value2.Z; + return value1; + } + + /// + /// Divides the components of a by a scalar. + /// + /// Source . + /// Divisor scalar. + /// The result of dividing a vector by a scalar. + public static Vector4 Divide(Vector4 value1, float divider) + { + float factor = 1f / divider; + value1.W *= factor; + value1.X *= factor; + value1.Y *= factor; + value1.Z *= factor; + return value1; + } + + /// + /// Divides the components of a by a scalar. + /// + /// Source . + /// Divisor scalar. + /// The result of dividing a vector by a scalar as an output parameter. + public static void Divide(ref Vector4 value1, float divider, out Vector4 result) + { + float factor = 1f / divider; + result.W = value1.W * factor; + result.X = value1.X * factor; + result.Y = value1.Y * factor; + result.Z = value1.Z * factor; + } + + /// + /// Divides the components of a by the components of another . + /// + /// Source . + /// Divisor . + /// The result of dividing the vectors as an output parameter. + public static void Divide(ref Vector4 value1, ref Vector4 value2, out Vector4 result) + { + result.W = value1.W / value2.W; + result.X = value1.X / value2.X; + result.Y = value1.Y / value2.Y; + result.Z = value1.Z / value2.Z; + } + + /// + /// Returns a dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product of two vectors. + public static float Dot(Vector4 value1, Vector4 value2) + { + return value1.X * value2.X + value1.Y * value2.Y + value1.Z * value2.Z + value1.W * value2.W; + } + + /// + /// Returns a dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product of two vectors as an output parameter. + public static void Dot(ref Vector4 value1, ref Vector4 value2, out float result) + { + result = value1.X * value2.X + value1.Y * value2.Y + value1.Z * value2.Z + value1.W * value2.W; + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public override bool Equals(object obj) + { + return (obj is Vector4) ? this == (Vector4)obj : false; + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public bool Equals(Vector4 other) + { + return this.W == other.W + && this.X == other.X + && this.Y == other.Y + && this.Z == other.Z; + } + + /// + /// Round the members of this towards negative infinity. + /// + public void Floor() + { + X = (float)Math.Floor(X); + Y = (float)Math.Floor(Y); + Z = (float)Math.Floor(Z); + W = (float)Math.Floor(W); + } + + /// + /// Creates a new that contains members from another vector rounded towards negative infinity. + /// + /// Source . + /// The rounded . + public static Vector4 Floor(Vector4 value) + { + value.X = (float)Math.Floor(value.X); + value.Y = (float)Math.Floor(value.Y); + value.Z = (float)Math.Floor(value.Z); + value.W = (float)Math.Floor(value.W); + return value; + } + + /// + /// Creates a new that contains members from another vector rounded towards negative infinity. + /// + /// Source . + /// The rounded . + public static void Floor(ref Vector4 value, out Vector4 result) + { + result.X = (float)Math.Floor(value.X); + result.Y = (float)Math.Floor(value.Y); + result.Z = (float)Math.Floor(value.Z); + result.W = (float)Math.Floor(value.W); + } + + /// + /// Gets the hash code of this . + /// + /// Hash code of this . + public override int GetHashCode() + { + unchecked + { + var hashCode = W.GetHashCode(); + hashCode = (hashCode * 397) ^ X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } + + /// + /// Creates a new that contains hermite spline interpolation. + /// + /// The first position vector. + /// The first tangent vector. + /// The second position vector. + /// The second tangent vector. + /// Weighting factor. + /// The hermite spline interpolation vector. + public static Vector4 Hermite(Vector4 value1, Vector4 tangent1, Vector4 value2, Vector4 tangent2, float amount) + { + return new Vector4(MathHelper.Hermite(value1.X, tangent1.X, value2.X, tangent2.X, amount), + MathHelper.Hermite(value1.Y, tangent1.Y, value2.Y, tangent2.Y, amount), + MathHelper.Hermite(value1.Z, tangent1.Z, value2.Z, tangent2.Z, amount), + MathHelper.Hermite(value1.W, tangent1.W, value2.W, tangent2.W, amount)); + } + + /// + /// Creates a new that contains hermite spline interpolation. + /// + /// The first position vector. + /// The first tangent vector. + /// The second position vector. + /// The second tangent vector. + /// Weighting factor. + /// The hermite spline interpolation vector as an output parameter. + public static void Hermite(ref Vector4 value1, ref Vector4 tangent1, ref Vector4 value2, ref Vector4 tangent2, float amount, out Vector4 result) + { + result.W = MathHelper.Hermite(value1.W, tangent1.W, value2.W, tangent2.W, amount); + result.X = MathHelper.Hermite(value1.X, tangent1.X, value2.X, tangent2.X, amount); + result.Y = MathHelper.Hermite(value1.Y, tangent1.Y, value2.Y, tangent2.Y, amount); + result.Z = MathHelper.Hermite(value1.Z, tangent1.Z, value2.Z, tangent2.Z, amount); + } + + /// + /// Returns the length of this . + /// + /// The length of this . + public float Length() + { + return (float)Math.Sqrt((X * X) + (Y * Y) + (Z * Z) + (W * W)); + } + + /// + /// Returns the squared length of this . + /// + /// The squared length of this . + public float LengthSquared() + { + return (X * X) + (Y * Y) + (Z * Z) + (W * W); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors. + public static Vector4 Lerp(Vector4 value1, Vector4 value2, float amount) + { + return new Vector4( + MathHelper.Lerp(value1.X, value2.X, amount), + MathHelper.Lerp(value1.Y, value2.Y, amount), + MathHelper.Lerp(value1.Z, value2.Z, amount), + MathHelper.Lerp(value1.W, value2.W, amount)); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors as an output parameter. + public static void Lerp(ref Vector4 value1, ref Vector4 value2, float amount, out Vector4 result) + { + result.X = MathHelper.Lerp(value1.X, value2.X, amount); + result.Y = MathHelper.Lerp(value1.Y, value2.Y, amount); + result.Z = MathHelper.Lerp(value1.Z, value2.Z, amount); + result.W = MathHelper.Lerp(value1.W, value2.W, amount); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// Uses on MathHelper for the interpolation. + /// Less efficient but more precise compared to . + /// See remarks section of on MathHelper for more info. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors. + public static Vector4 LerpPrecise(Vector4 value1, Vector4 value2, float amount) + { + return new Vector4( + MathHelper.LerpPrecise(value1.X, value2.X, amount), + MathHelper.LerpPrecise(value1.Y, value2.Y, amount), + MathHelper.LerpPrecise(value1.Z, value2.Z, amount), + MathHelper.LerpPrecise(value1.W, value2.W, amount)); + } + + /// + /// Creates a new that contains linear interpolation of the specified vectors. + /// Uses on MathHelper for the interpolation. + /// Less efficient but more precise compared to . + /// See remarks section of on MathHelper for more info. + /// + /// The first vector. + /// The second vector. + /// Weighting value(between 0.0 and 1.0). + /// The result of linear interpolation of the specified vectors as an output parameter. + public static void LerpPrecise(ref Vector4 value1, ref Vector4 value2, float amount, out Vector4 result) + { + result.X = MathHelper.LerpPrecise(value1.X, value2.X, amount); + result.Y = MathHelper.LerpPrecise(value1.Y, value2.Y, amount); + result.Z = MathHelper.LerpPrecise(value1.Z, value2.Z, amount); + result.W = MathHelper.LerpPrecise(value1.W, value2.W, amount); + } + + /// + /// Creates a new that contains a maximal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with maximal values from the two vectors. + public static Vector4 Max(Vector4 value1, Vector4 value2) + { + return new Vector4( + MathHelper.Max(value1.X, value2.X), + MathHelper.Max(value1.Y, value2.Y), + MathHelper.Max(value1.Z, value2.Z), + MathHelper.Max(value1.W, value2.W)); + } + + /// + /// Creates a new that contains a maximal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with maximal values from the two vectors as an output parameter. + public static void Max(ref Vector4 value1, ref Vector4 value2, out Vector4 result) + { + result.X = MathHelper.Max(value1.X, value2.X); + result.Y = MathHelper.Max(value1.Y, value2.Y); + result.Z = MathHelper.Max(value1.Z, value2.Z); + result.W = MathHelper.Max(value1.W, value2.W); + } + + /// + /// Creates a new that contains a minimal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with minimal values from the two vectors. + public static Vector4 Min(Vector4 value1, Vector4 value2) + { + return new Vector4( + MathHelper.Min(value1.X, value2.X), + MathHelper.Min(value1.Y, value2.Y), + MathHelper.Min(value1.Z, value2.Z), + MathHelper.Min(value1.W, value2.W)); + } + + /// + /// Creates a new that contains a minimal values from the two vectors. + /// + /// The first vector. + /// The second vector. + /// The with minimal values from the two vectors as an output parameter. + public static void Min(ref Vector4 value1, ref Vector4 value2, out Vector4 result) + { + result.X = MathHelper.Min(value1.X, value2.X); + result.Y = MathHelper.Min(value1.Y, value2.Y); + result.Z = MathHelper.Min(value1.Z, value2.Z); + result.W = MathHelper.Min(value1.W, value2.W); + } + + /// + /// Creates a new that contains a multiplication of two vectors. + /// + /// Source . + /// Source . + /// The result of the vector multiplication. + public static Vector4 Multiply(Vector4 value1, Vector4 value2) + { + value1.W *= value2.W; + value1.X *= value2.X; + value1.Y *= value2.Y; + value1.Z *= value2.Z; + return value1; + } + + /// + /// Creates a new that contains a multiplication of and a scalar. + /// + /// Source . + /// Scalar value. + /// The result of the vector multiplication with a scalar. + public static Vector4 Multiply(Vector4 value1, float scaleFactor) + { + value1.W *= scaleFactor; + value1.X *= scaleFactor; + value1.Y *= scaleFactor; + value1.Z *= scaleFactor; + return value1; + } + + /// + /// Creates a new that contains a multiplication of and a scalar. + /// + /// Source . + /// Scalar value. + /// The result of the multiplication with a scalar as an output parameter. + public static void Multiply(ref Vector4 value1, float scaleFactor, out Vector4 result) + { + result.W = value1.W * scaleFactor; + result.X = value1.X * scaleFactor; + result.Y = value1.Y * scaleFactor; + result.Z = value1.Z * scaleFactor; + } + + /// + /// Creates a new that contains a multiplication of two vectors. + /// + /// Source . + /// Source . + /// The result of the vector multiplication as an output parameter. + public static void Multiply(ref Vector4 value1, ref Vector4 value2, out Vector4 result) + { + result.W = value1.W * value2.W; + result.X = value1.X * value2.X; + result.Y = value1.Y * value2.Y; + result.Z = value1.Z * value2.Z; + } + + /// + /// Creates a new that contains the specified vector inversion. + /// + /// Source . + /// The result of the vector inversion. + public static Vector4 Negate(Vector4 value) + { + value = new Vector4(-value.X, -value.Y, -value.Z, -value.W); + return value; + } + + /// + /// Creates a new that contains the specified vector inversion. + /// + /// Source . + /// The result of the vector inversion as an output parameter. + public static void Negate(ref Vector4 value, out Vector4 result) + { + result.X = -value.X; + result.Y = -value.Y; + result.Z = -value.Z; + result.W = -value.W; + } + + /// + /// Turns this to a unit vector with the same direction. + /// + public void Normalize() + { + float factor = (float)Math.Sqrt((X * X) + (Y * Y) + (Z * Z) + (W * W)); + factor = 1f / factor; + X *= factor; + Y *= factor; + Z *= factor; + W *= factor; + } + + /// + /// Creates a new that contains a normalized values from another vector. + /// + /// Source . + /// Unit vector. + public static Vector4 Normalize(Vector4 value) + { + float factor = (float)Math.Sqrt((value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z) + (value.W * value.W)); + factor = 1f / factor; + return new Vector4(value.X * factor, value.Y * factor, value.Z * factor, value.W * factor); + } + + /// + /// Creates a new that contains a normalized values from another vector. + /// + /// Source . + /// Unit vector as an output parameter. + public static void Normalize(ref Vector4 value, out Vector4 result) + { + float factor = (float)Math.Sqrt((value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z) + (value.W * value.W)); + factor = 1f / factor; + result.W = value.W * factor; + result.X = value.X * factor; + result.Y = value.Y * factor; + result.Z = value.Z * factor; + } + + /// + /// Round the members of this to the nearest integer value. + /// + public void Round() + { + X = (float)Math.Round(X); + Y = (float)Math.Round(Y); + Z = (float)Math.Round(Z); + W = (float)Math.Round(W); + } + + /// + /// Creates a new that contains members from another vector rounded to the nearest integer value. + /// + /// Source . + /// The rounded . + public static Vector4 Round(Vector4 value) + { + value.X = (float)Math.Round(value.X); + value.Y = (float)Math.Round(value.Y); + value.Z = (float)Math.Round(value.Z); + value.W = (float)Math.Round(value.W); + return value; + } + + /// + /// Creates a new that contains members from another vector rounded to the nearest integer value. + /// + /// Source . + /// The rounded . + public static void Round(ref Vector4 value, out Vector4 result) + { + result.X = (float)Math.Round(value.X); + result.Y = (float)Math.Round(value.Y); + result.Z = (float)Math.Round(value.Z); + result.W = (float)Math.Round(value.W); + } + + /// + /// Creates a new that contains cubic interpolation of the specified vectors. + /// + /// Source . + /// Source . + /// Weighting value. + /// Cubic interpolation of the specified vectors. + public static Vector4 SmoothStep(Vector4 value1, Vector4 value2, float amount) + { + return new Vector4( + MathHelper.SmoothStep(value1.X, value2.X, amount), + MathHelper.SmoothStep(value1.Y, value2.Y, amount), + MathHelper.SmoothStep(value1.Z, value2.Z, amount), + MathHelper.SmoothStep(value1.W, value2.W, amount)); + } + + /// + /// Creates a new that contains cubic interpolation of the specified vectors. + /// + /// Source . + /// Source . + /// Weighting value. + /// Cubic interpolation of the specified vectors as an output parameter. + public static void SmoothStep(ref Vector4 value1, ref Vector4 value2, float amount, out Vector4 result) + { + result.X = MathHelper.SmoothStep(value1.X, value2.X, amount); + result.Y = MathHelper.SmoothStep(value1.Y, value2.Y, amount); + result.Z = MathHelper.SmoothStep(value1.Z, value2.Z, amount); + result.W = MathHelper.SmoothStep(value1.W, value2.W, amount); + } + + /// + /// Creates a new that contains subtraction of on from a another. + /// + /// Source . + /// Source . + /// The result of the vector subtraction. + public static Vector4 Subtract(Vector4 value1, Vector4 value2) + { + value1.W -= value2.W; + value1.X -= value2.X; + value1.Y -= value2.Y; + value1.Z -= value2.Z; + return value1; + } + + /// + /// Creates a new that contains subtraction of on from a another. + /// + /// Source . + /// Source . + /// The result of the vector subtraction as an output parameter. + public static void Subtract(ref Vector4 value1, ref Vector4 value2, out Vector4 result) + { + result.W = value1.W - value2.W; + result.X = value1.X - value2.X; + result.Y = value1.Y - value2.Y; + result.Z = value1.Z - value2.Z; + } + + #region Transform + + /// + /// Creates a new that contains a transformation of 2d-vector by the specified . + /// + /// Source . + /// The transformation . + /// Transformed . + public static Vector4 Transform(Vector2 value, Matrix matrix) + { + Vector4 result; + Transform(ref value, ref matrix, out result); + return result; + } + + /// + /// Creates a new that contains a transformation of 2d-vector by the specified . + /// + /// Source . + /// The which contains rotation transformation. + /// Transformed . + public static Vector4 Transform(Vector2 value, Quaternion rotation) + { + Vector4 result; + Transform(ref value, ref rotation, out result); + return result; + } + + /// + /// Creates a new that contains a transformation of 3d-vector by the specified . + /// + /// Source . + /// The transformation . + /// Transformed . + public static Vector4 Transform(Vector3 value, Matrix matrix) + { + Vector4 result; + Transform(ref value, ref matrix, out result); + return result; + } + + /// + /// Creates a new that contains a transformation of 3d-vector by the specified . + /// + /// Source . + /// The which contains rotation transformation. + /// Transformed . + public static Vector4 Transform(Vector3 value, Quaternion rotation) + { + Vector4 result; + Transform(ref value, ref rotation, out result); + return result; + } + + /// + /// Creates a new that contains a transformation of 4d-vector by the specified . + /// + /// Source . + /// The transformation . + /// Transformed . + public static Vector4 Transform(Vector4 value, Matrix matrix) + { + Transform(ref value, ref matrix, out value); + return value; + } + + /// + /// Creates a new that contains a transformation of 4d-vector by the specified . + /// + /// Source . + /// The which contains rotation transformation. + /// Transformed . + public static Vector4 Transform(Vector4 value, Quaternion rotation) + { + Vector4 result; + Transform(ref value, ref rotation, out result); + return result; + } + + /// + /// Creates a new that contains a transformation of 2d-vector by the specified . + /// + /// Source . + /// The transformation . + /// Transformed as an output parameter. + public static void Transform(ref Vector2 value, ref Matrix matrix, out Vector4 result) + { + result.X = (value.X * matrix.M11) + (value.Y * matrix.M21) + matrix.M41; + result.Y = (value.X * matrix.M12) + (value.Y * matrix.M22) + matrix.M42; + result.Z = (value.X * matrix.M13) + (value.Y * matrix.M23) + matrix.M43; + result.W = (value.X * matrix.M14) + (value.Y * matrix.M24) + matrix.M44; + } + + /// + /// Creates a new that contains a transformation of 2d-vector by the specified . + /// + /// Source . + /// The which contains rotation transformation. + /// Transformed as an output parameter. + public static void Transform(ref Vector2 value, ref Quaternion rotation, out Vector4 result) + { + throw new NotImplementedException(); + } + + /// + /// Creates a new that contains a transformation of 3d-vector by the specified . + /// + /// Source . + /// The transformation . + /// Transformed as an output parameter. + public static void Transform(ref Vector3 value, ref Matrix matrix, out Vector4 result) + { + result.X = (value.X * matrix.M11) + (value.Y * matrix.M21) + (value.Z * matrix.M31) + matrix.M41; + result.Y = (value.X * matrix.M12) + (value.Y * matrix.M22) + (value.Z * matrix.M32) + matrix.M42; + result.Z = (value.X * matrix.M13) + (value.Y * matrix.M23) + (value.Z * matrix.M33) + matrix.M43; + result.W = (value.X * matrix.M14) + (value.Y * matrix.M24) + (value.Z * matrix.M34) + matrix.M44; + } + + /// + /// Creates a new that contains a transformation of 3d-vector by the specified . + /// + /// Source . + /// The which contains rotation transformation. + /// Transformed as an output parameter. + public static void Transform(ref Vector3 value, ref Quaternion rotation, out Vector4 result) + { + throw new NotImplementedException(); + } + + /// + /// Creates a new that contains a transformation of 4d-vector by the specified . + /// + /// Source . + /// The transformation . + /// Transformed as an output parameter. + public static void Transform(ref Vector4 value, ref Matrix matrix, out Vector4 result) + { + var x = (value.X * matrix.M11) + (value.Y * matrix.M21) + (value.Z * matrix.M31) + (value.W * matrix.M41); + var y = (value.X * matrix.M12) + (value.Y * matrix.M22) + (value.Z * matrix.M32) + (value.W * matrix.M42); + var z = (value.X * matrix.M13) + (value.Y * matrix.M23) + (value.Z * matrix.M33) + (value.W * matrix.M43); + var w = (value.X * matrix.M14) + (value.Y * matrix.M24) + (value.Z * matrix.M34) + (value.W * matrix.M44); + result.X = x; + result.Y = y; + result.Z = z; + result.W = w; + } + + /// + /// Creates a new that contains a transformation of 4d-vector by the specified . + /// + /// Source . + /// The which contains rotation transformation. + /// Transformed as an output parameter. + public static void Transform(ref Vector4 value, ref Quaternion rotation, out Vector4 result) + { + throw new NotImplementedException(); + } + + /// + /// Apply transformation on vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The starting index of transformation in the source array. + /// The transformation . + /// Destination array. + /// The starting index in the destination array, where the first should be written. + /// The number of vectors to be transformed. + public static void Transform + ( + Vector4[] sourceArray, + int sourceIndex, + ref Matrix matrix, + Vector4[] destinationArray, + int destinationIndex, + int length + ) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (sourceArray.Length < sourceIndex + length) + throw new ArgumentException("Source array length is lesser than sourceIndex + length"); + if (destinationArray.Length < destinationIndex + length) + throw new ArgumentException("Destination array length is lesser than destinationIndex + length"); + + for (var i = 0; i < length; i++) + { + var value = sourceArray[sourceIndex + i]; + destinationArray[destinationIndex + i] = Transform(value, matrix); + } + } + + /// + /// Apply transformation on vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The starting index of transformation in the source array. + /// The which contains rotation transformation. + /// Destination array. + /// The starting index in the destination array, where the first should be written. + /// The number of vectors to be transformed. + public static void Transform( + Vector4[] sourceArray, + int sourceIndex, + ref Quaternion rotation, + Vector4[] destinationArray, + int destinationIndex, + int length + ) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (sourceArray.Length < sourceIndex + length) + throw new ArgumentException("Source array length is lesser than sourceIndex + length"); + if (destinationArray.Length < destinationIndex + length) + throw new ArgumentException("Destination array length is lesser than destinationIndex + length"); + + for (var i = 0; i < length; i++) + { + var value = sourceArray[sourceIndex + i]; + destinationArray[destinationIndex + i] = Transform(value, rotation); + } + } + + /// + /// Apply transformation on all vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The transformation . + /// Destination array. + public static void Transform(Vector4[] sourceArray, ref Matrix matrix, Vector4[] destinationArray) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (destinationArray.Length < sourceArray.Length) + throw new ArgumentException("Destination array length is lesser than source array length"); + + for (var i = 0; i < sourceArray.Length; i++) + { + var value = sourceArray[i]; + destinationArray[i] = Transform(value, matrix); + } + } + + /// + /// Apply transformation on all vectors within array of by the specified and places the results in an another array. + /// + /// Source array. + /// The which contains rotation transformation. + /// Destination array. + public static void Transform(Vector4[] sourceArray, ref Quaternion rotation, Vector4[] destinationArray) + { + if (sourceArray == null) + throw new ArgumentNullException("sourceArray"); + if (destinationArray == null) + throw new ArgumentNullException("destinationArray"); + if (destinationArray.Length < sourceArray.Length) + throw new ArgumentException("Destination array length is lesser than source array length"); + + for (var i = 0; i < sourceArray.Length; i++) + { + var value = sourceArray[i]; + destinationArray[i] = Transform(value, rotation); + } + } + + #endregion + + /// + /// Returns a representation of this in the format: + /// {X:[] Y:[] Z:[] W:[]} + /// + /// A representation of this . + public override string ToString() + { + return "{X:" + X + " Y:" + Y + " Z:" + Z + " W:" + W + "}"; + } + + /// + /// Deconstruction method for . + /// + /// + /// + /// + /// + public void Deconstruct(out float x, out float y, out float z, out float w) + { + x = X; + y = Y; + z = Z; + w = W; + } + + #endregion + + #region Operators + + /// + /// Inverts values in the specified . + /// + /// Source on the right of the sub sign. + /// Result of the inversion. + public static Vector4 operator -(Vector4 value) + { + return new Vector4(-value.X, -value.Y, -value.Z, -value.W); + } + + /// + /// Compares whether two instances are equal. + /// + /// instance on the left of the equal sign. + /// instance on the right of the equal sign. + /// true if the instances are equal; false otherwise. + public static bool operator ==(Vector4 value1, Vector4 value2) + { + return value1.W == value2.W + && value1.X == value2.X + && value1.Y == value2.Y + && value1.Z == value2.Z; + } + + /// + /// Compares whether two instances are not equal. + /// + /// instance on the left of the not equal sign. + /// instance on the right of the not equal sign. + /// true if the instances are not equal; false otherwise. + public static bool operator !=(Vector4 value1, Vector4 value2) + { + return !(value1 == value2); + } + + /// + /// Adds two vectors. + /// + /// Source on the left of the add sign. + /// Source on the right of the add sign. + /// Sum of the vectors. + public static Vector4 operator +(Vector4 value1, Vector4 value2) + { + value1.W += value2.W; + value1.X += value2.X; + value1.Y += value2.Y; + value1.Z += value2.Z; + return value1; + } + + /// + /// Subtracts a from a . + /// + /// Source on the left of the sub sign. + /// Source on the right of the sub sign. + /// Result of the vector subtraction. + public static Vector4 operator -(Vector4 value1, Vector4 value2) + { + value1.W -= value2.W; + value1.X -= value2.X; + value1.Y -= value2.Y; + value1.Z -= value2.Z; + return value1; + } + + /// + /// Multiplies the components of two vectors by each other. + /// + /// Source on the left of the mul sign. + /// Source on the right of the mul sign. + /// Result of the vector multiplication. + public static Vector4 operator *(Vector4 value1, Vector4 value2) + { + value1.W *= value2.W; + value1.X *= value2.X; + value1.Y *= value2.Y; + value1.Z *= value2.Z; + return value1; + } + + /// + /// Multiplies the components of vector by a scalar. + /// + /// Source on the left of the mul sign. + /// Scalar value on the right of the mul sign. + /// Result of the vector multiplication with a scalar. + public static Vector4 operator *(Vector4 value, float scaleFactor) + { + value.W *= scaleFactor; + value.X *= scaleFactor; + value.Y *= scaleFactor; + value.Z *= scaleFactor; + return value; + } + + /// + /// Multiplies the components of vector by a scalar. + /// + /// Scalar value on the left of the mul sign. + /// Source on the right of the mul sign. + /// Result of the vector multiplication with a scalar. + public static Vector4 operator *(float scaleFactor, Vector4 value) + { + value.W *= scaleFactor; + value.X *= scaleFactor; + value.Y *= scaleFactor; + value.Z *= scaleFactor; + return value; + } + + /// + /// Divides the components of a by the components of another . + /// + /// Source on the left of the div sign. + /// Divisor on the right of the div sign. + /// The result of dividing the vectors. + public static Vector4 operator /(Vector4 value1, Vector4 value2) + { + value1.W /= value2.W; + value1.X /= value2.X; + value1.Y /= value2.Y; + value1.Z /= value2.Z; + return value1; + } + + /// + /// Divides the components of a by a scalar. + /// + /// Source on the left of the div sign. + /// Divisor scalar on the right of the div sign. + /// The result of dividing a vector by a scalar. + public static Vector4 operator /(Vector4 value1, float divider) + { + float factor = 1f / divider; + value1.W *= factor; + value1.X *= factor; + value1.Y *= factor; + value1.Z *= factor; + return value1; + } + + #endregion + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs b/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs new file mode 100644 index 0000000..678796a --- /dev/null +++ b/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs @@ -0,0 +1,280 @@ +using Hawkeye.VisionBuilder.Workflow.Configuration; +using Hawkeye.VisionBuilder.Workflow.Links; +using OpenCvSharp; +using System.ComponentModel.Design.Serialization; +using System.Diagnostics; +using System.Drawing; +using System.Reflection.PortableExecutable; +using System.Xml; +using VisionBuilder.UI.Common.RecipeProcessing; +using Size = OpenCvSharp.Size; + +namespace Hawkeye.VisionBuilder.Workflow; + +public class WorkflowList +{ + + + public IImageSource ImageSource { get; set; } + public Mat? RecipeImage { get; set; } + + public WorkflowConfiguration Configuration { get; set; } = new WorkflowConfiguration(); + public List Operations { get; set; } = new List(); + public Context Context { get; set; } = new Context(); + + private int _executionCounter = 0; + + public void Execute() + { + + Context.GraphicsElements.Clear(); + UpdateConfigVariables(); + long total = 0; + foreach (BaseOperation operation in Operations) + { + operation.Interpret(Context); + } + _executionCounter++; + if (_executionCounter % 10 == 0) + GC.Collect(); + + + } + + public BaseOperation GetById(Guid originId) + { + var operation = Operations.FirstOrDefault(x => + x.Id == originId); + if(operation==null)return NoOrigin.Instance; + return operation as BaseOperation; + } + + public IHaveOrigin GetOriginById(Guid originId) + { + return GetById(originId) as IHaveOrigin ?? NoOrigin.Instance; + } + + public void ExecuteTill(BaseOperation op) + { + + Context.GraphicsElements.Clear(); + UpdateConfigVariables(); + foreach (BaseOperation operation in Operations) + { + if(operation == op) break; + operation.Interpret(Context); + } + } + public void ExecuteOne(BaseOperation operation) + { + Context.GraphicsElements.Clear(); + UpdateConfigVariables(); + try + { + + operation.Interpret(Context); + } + catch (Exception e) + { + operation.SetError(e.Message); + } + + } + + void UpdateConfigVariables() + { + BaseOperation.SetScriptValue("config", Configuration); + } + public void Save(BinaryWriter bw) + { + Configuration.Save(bw); + bw.Write(Operations.Count); + foreach (BaseOperation operation in Operations) + { + bw.Write(operation.GetType().AssemblyQualifiedName); + operation.Save(bw); + } + if (RecipeImage == null) + { + var mat = new Mat(128, 128, MatType.CV_8UC3, Scalar.Gray); + var image = mat.ToBytes(); + bw.Write(image.Length); + bw.Write(image); + } + else + { + var mat = RecipeImage; + var image = mat.Resize(new Size(128, 128)).ToBytes(); + bw.Write(image.Length); + bw.Write(image); + } + + } + public event Action PythonMissing=delegate { }; + public void Load(BinaryReader br, OperationDiscoveryService operationDiscoveryService) + { + Configuration.Load(br); + if (!File.Exists(Configuration.PythonPath)) + PythonMissing(); + int count = br.ReadInt32(); + Operations.Clear(); + for (int i = 0; i < count; i++) + { + string operationType = br.ReadString(); + var type=Type.GetType(operationType); + BaseOperation operation = operationDiscoveryService.CreateInstance(type); + operation.Load(br); + Operations.Add(operation); + } + int length = br.ReadInt32(); + var image = br.ReadBytes(length); + RecipeImage = Mat.FromImageData(image); + + } + + public static WorkflowList LoadFromFile(string fileName) + { + using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read)) + using (var br = new BinaryReader(fs)) + { + var workflowList = new WorkflowList(); + workflowList.Load(br, new OperationDiscoveryService(workflowList)); + return workflowList; + } + } + + public void SaveXML(XmlWriter writer) + { + writer.WriteStartElement("Workflow"); + + // Save Configuration + writer.WriteStartElement("Configuration"); + writer.WriteAttributeString("RuntimeCameraType", Configuration.RuntimeCameraType.ToString()); + writer.WriteAttributeString("DevelopmentCameraType", Configuration.DevelopmentCameraType.ToString()); + writer.WriteAttributeString("Outputs", Configuration.Outputs.ToString()); + writer.WriteAttributeString("EmulationPath", Configuration.EmulationPath); + writer.WriteAttributeString("PythonPath", Configuration.PythonPath); + writer.WriteAttributeString("ResultPin", Configuration.ResultPin.ToString()); + writer.WriteAttributeString("SerialPort", Configuration.SerialPort); + writer.WriteAttributeString("Delay", Configuration.Delay.ToString()); + writer.WriteEndElement(); // Configuration + + // Save Operations + writer.WriteStartElement("Operations"); + writer.WriteAttributeString("Count", Operations.Count.ToString()); + + foreach (BaseOperation operation in Operations) + { + writer.WriteStartElement("Operation"); + writer.WriteAttributeString("Type", operation.GetType().AssemblyQualifiedName); + operation.SaveXML(writer); + writer.WriteEndElement(); // Operation + } + writer.WriteEndElement(); // Operations + + // Save Recipe Image + writer.WriteStartElement("RecipeImage"); + if (RecipeImage == null) + { + var mat = new Mat(128, 128, MatType.CV_8UC3, Scalar.Gray); + var image = mat.ToBytes(); + writer.WriteAttributeString("Length", image.Length.ToString()); + writer.WriteBase64(image, 0, image.Length); + } + else + { + var mat = RecipeImage; + var image = mat.Resize(new Size(128, 128)).ToBytes(); + writer.WriteAttributeString("Length", image.Length.ToString()); + writer.WriteBase64(image, 0, image.Length); + } + writer.WriteEndElement(); // RecipeImage + + writer.WriteEndElement(); // Workflow + } + + public void LoadXML(XmlReader reader, OperationDiscoveryService operationDiscoveryService) + { + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Configuration": + LoadConfigurationXML(reader); + break; + case "Operations": + LoadOperationsXML(reader, operationDiscoveryService); + break; + case "RecipeImage": + LoadRecipeImageXML(reader); + break; + } + } + } + } + + private void LoadConfigurationXML(XmlReader reader) + { + if (reader.GetAttribute("RuntimeCameraType") != null) + Configuration.RuntimeCameraType = Enum.Parse(reader.GetAttribute("RuntimeCameraType")); + if (reader.GetAttribute("DevelopmentCameraType") != null) + Configuration.DevelopmentCameraType = Enum.Parse(reader.GetAttribute("DevelopmentCameraType")); + if (reader.GetAttribute("Outputs") != null) + Configuration.Outputs = Enum.Parse(reader.GetAttribute("Outputs")); + if (reader.GetAttribute("EmulationPath") != null) + Configuration.EmulationPath = reader.GetAttribute("EmulationPath"); + if (reader.GetAttribute("PythonPath") != null) + Configuration.PythonPath = reader.GetAttribute("PythonPath"); + if (reader.GetAttribute("ResultPin") != null) + Configuration.ResultPin = int.Parse(reader.GetAttribute("ResultPin")); + if (reader.GetAttribute("SerialPort") != null) + Configuration.SerialPort = reader.GetAttribute("SerialPort"); + if (reader.GetAttribute("Delay") != null) + Configuration.Delay = int.Parse(reader.GetAttribute("Delay")); + } + + private void LoadOperationsXML(XmlReader reader, OperationDiscoveryService operationDiscoveryService) + { + Operations.Clear(); + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element && reader.Name == "Operation") + { + string operationType = reader.GetAttribute("Type"); + var type = Type.GetType(operationType); + if (type != null) + { + BaseOperation operation = operationDiscoveryService.CreateInstance(type); + // go to child elements of Operation + operation.LoadXML(reader); + Operations.Add(operation); + } + } + else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "Operations") + { + break; + } + } + } + + private void LoadRecipeImageXML(XmlReader reader) + { + if (reader.GetAttribute("Length") != null) + { + int length = int.Parse(reader.GetAttribute("Length")); + byte[] imageData = new byte[length]; + int bytesRead = reader.ReadElementContentAsBase64(imageData, 0, length); + if (bytesRead > 0) + { + RecipeImage = Mat.FromImageData(imageData); + } + } + } + + + + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/ActionSelector.Designer.cs b/Hawkeye.VisionBuilder/ActionSelector.Designer.cs new file mode 100644 index 0000000..35a51a1 --- /dev/null +++ b/Hawkeye.VisionBuilder/ActionSelector.Designer.cs @@ -0,0 +1,125 @@ +namespace Hawkeye.VisionBuilder +{ + partial class ActionSelector + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.pictureBox1 = new System.Windows.Forms.PictureBox(); + this.button1 = new System.Windows.Forms.Button(); + this.pictureBox2 = new System.Windows.Forms.PictureBox(); + this.button2 = new System.Windows.Forms.Button(); + this.pictureBox3 = new System.Windows.Forms.PictureBox(); + this.button3 = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox2)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox3)).BeginInit(); + this.SuspendLayout(); + // + // pictureBox1 + // + this.pictureBox1.Location = new System.Drawing.Point(24, 24); + this.pictureBox1.Name = "pictureBox1"; + this.pictureBox1.Size = new System.Drawing.Size(176, 176); + this.pictureBox1.TabIndex = 3; + this.pictureBox1.TabStop = false; + // + // button1 + // + this.button1.Location = new System.Drawing.Point(56, 208); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(112, 23); + this.button1.TabIndex = 0; + this.button1.Text = "Select image set"; + this.button1.UseVisualStyleBackColor = true; + // + // pictureBox2 + // + this.pictureBox2.Location = new System.Drawing.Point(224, 24); + this.pictureBox2.Name = "pictureBox2"; + this.pictureBox2.Size = new System.Drawing.Size(176, 176); + this.pictureBox2.TabIndex = 5; + this.pictureBox2.TabStop = false; + // + // button2 + // + this.button2.Location = new System.Drawing.Point(256, 208); + this.button2.Name = "button2"; + this.button2.Size = new System.Drawing.Size(112, 23); + this.button2.TabIndex = 4; + this.button2.Text = "Build recipe"; + this.button2.UseVisualStyleBackColor = true; + // + // pictureBox3 + // + this.pictureBox3.Location = new System.Drawing.Point(424, 24); + this.pictureBox3.Name = "pictureBox3"; + this.pictureBox3.Size = new System.Drawing.Size(176, 176); + this.pictureBox3.TabIndex = 7; + this.pictureBox3.TabStop = false; + // + // button3 + // + this.button3.Location = new System.Drawing.Point(456, 208); + this.button3.Name = "button3"; + this.button3.Size = new System.Drawing.Size(112, 23); + this.button3.TabIndex = 6; + this.button3.Text = "Specify outputs"; + this.button3.UseVisualStyleBackColor = true; + // + // Form2 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(626, 258); + this.Controls.Add(this.pictureBox3); + this.Controls.Add(this.button3); + this.Controls.Add(this.pictureBox2); + this.Controls.Add(this.button2); + this.Controls.Add(this.pictureBox1); + this.Controls.Add(this.button1); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "Form2"; + this.Text = "Form2"; + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox2)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox3)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private PictureBox pictureBox1; + private Button button1; + private PictureBox pictureBox2; + private Button button2; + private PictureBox pictureBox3; + private Button button3; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/ActionSelector.cs b/Hawkeye.VisionBuilder/ActionSelector.cs new file mode 100644 index 0000000..336a1d8 --- /dev/null +++ b/Hawkeye.VisionBuilder/ActionSelector.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Hawkeye.VisionBuilder +{ + public partial class ActionSelector : Form + { + public ActionSelector() + { + InitializeComponent(); + } + } +} diff --git a/Hawkeye.VisionBuilder/ActionSelector.resx b/Hawkeye.VisionBuilder/ActionSelector.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/Hawkeye.VisionBuilder/ActionSelector.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.Designer.cs b/Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.Designer.cs new file mode 100644 index 0000000..a3242f3 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.Designer.cs @@ -0,0 +1,86 @@ +namespace Hawkeye.VisionBuilder.Features.ColorProgileGenerator +{ + partial class ColorProfileSelection + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + listView1 = new ListView(); + btnProfilePath = new Button(); + button1 = new Button(); + SuspendLayout(); + // + // listView1 + // + listView1.CheckBoxes = true; + listView1.Location = new Point(16, 48); + listView1.Name = "listView1"; + listView1.Size = new Size(312, 368); + listView1.TabIndex = 0; + listView1.UseCompatibleStateImageBehavior = false; + listView1.View = View.List; + // + // btnProfilePath + // + btnProfilePath.Location = new Point(16, 8); + btnProfilePath.Name = "btnProfilePath"; + btnProfilePath.Size = new Size(312, 23); + btnProfilePath.TabIndex = 1; + btnProfilePath.Text = "Select profile file"; + btnProfilePath.UseVisualStyleBackColor = true; + btnProfilePath.Click += btnProfilePath_Click; + // + // button1 + // + button1.Location = new Point(248, 424); + button1.Name = "button1"; + button1.Size = new Size(75, 23); + button1.TabIndex = 2; + button1.Text = "Generate"; + button1.UseVisualStyleBackColor = true; + button1.Click += button1_Click; + // + // ColorProfileSelection + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(348, 463); + Controls.Add(button1); + Controls.Add(btnProfilePath); + Controls.Add(listView1); + FormBorderStyle = FormBorderStyle.FixedToolWindow; + Name = "ColorProfileSelection"; + Text = "ColorProfileSelection"; + ResumeLayout(false); + } + + #endregion + + private ListView listView1; + private Button btnProfilePath; + private Button button1; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.cs b/Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.cs new file mode 100644 index 0000000..1aa00f2 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.cs @@ -0,0 +1,78 @@ +using Hawkeye.VisionBuilder.Workflow.ColorStudioClasses; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using Hawkeye.VisionBuilder.Workflow; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle; +using Hawkeye.VisionBuilder.Workflow.Operations; +using Hawkeye.VisionBuilder.Workflow.Operations.Filters; +using Hawkeye.VisionBuilder.Workflow.Operations.Memory; + + +namespace Hawkeye.VisionBuilder.Features.ColorProgileGenerator +{ + public partial class ColorProfileSelection : Form + { + private readonly WorkflowList _workflowList; + + public ColorProfileSelection(WorkflowList workflowList) + { + _workflowList = workflowList; + InitializeComponent(); + } + + private void btnProfilePath_Click(object sender, EventArgs e) + { + var dialog = new OpenFileDialog(); + dialog.Filter = "Color Profile File (*.colorprofile)|*.colorprofile"; + if (dialog.ShowDialog() == DialogResult.OK) + { + btnProfilePath.Text = dialog.FileName; + LoadProfile(dialog.FileName); + } + } + + private void LoadProfile(string dialogFileName) + { + var categoryTree = CategoryTree.LoadFromFile(dialogFileName); + var categories = categoryTree.CategoryItems.Select(c => c.Name).ToList(); + listView1.Items.Clear(); + foreach (var category in categories) + { + listView1.Items.Add(category); + } + + } + + private void button1_Click(object sender, EventArgs e) + { + var checkedItems= listView1.CheckedItems; + + foreach (ListViewItem item in checkedItems) + { + var name = item.Text; + var operation = new ColorProfileOperation(); + operation.Category.Value = name; + operation.ColorProfilePath.Path = btnProfilePath.Text; + _workflowList.Operations.Add(operation); + + var blobFilter = new FindManyBlobsOperation(_workflowList); + blobFilter.SearchArea = null; + blobFilter.Label = name; + _workflowList.Operations.Add(blobFilter); + + var imageFromMemory = new ImageFromMemoryOperation(); + _workflowList.Operations.Add(imageFromMemory); + + } + Close(); + + } + } +} diff --git a/Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.resx b/Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.resx new file mode 100644 index 0000000..af32865 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ColorProgileGenerator/ColorProfileSelection.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImagePreview/EControlMode.cs b/Hawkeye.VisionBuilder/Features/ImagePreview/EControlMode.cs new file mode 100644 index 0000000..512b37e --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagePreview/EControlMode.cs @@ -0,0 +1,7 @@ +namespace Hawkeye.VisionBuilder.Features.ImagePreview; + +public enum EControlMode +{ + Preview, + Creation +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.Designer.cs b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.Designer.cs new file mode 100644 index 0000000..7472aa6 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.Designer.cs @@ -0,0 +1,37 @@ +namespace Hawkeye.VisionBuilder.Features.ImagePreview +{ + partial class ImagePreviewControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + } + + #endregion + } +} diff --git a/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.cs b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.cs new file mode 100644 index 0000000..294d69c --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.cs @@ -0,0 +1,329 @@ + +using System.Drawing; +using System.Drawing.Drawing2D; +using Hawkeye.VisionBuilder.Workflow; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Origin; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Poly; +using OpenCvSharp; +using OpenCvSharp.Extensions; +using Point = System.Drawing.Point; +using PointF = System.Drawing.PointF; +using Matrix = Hawkeye.VisionBuilder.Workflow.Matrix; +using Vector2 = Hawkeye.VisionBuilder.Workflow.Vector2; + +namespace Hawkeye.VisionBuilder.Features.ImagePreview +{ + public partial class ImagePreviewControl : UserControl,ICanvas + { + private Bitmap _backBuffer; + public List GraphicsElements { get; set; }=new List(); + public EControlMode Mode { get; set; } = EControlMode.Preview; + + public Hawkeye.VisionBuilder.Workflow.Datatypes.HawkeyeImage Image + { + get => _image; + set + { + var needsCentering = _image == null; + _image = value; + if(needsCentering) CenterImage(); + } + } + + public void CenterImage() + { + TransformationMatrix= Workflow.Matrix.Identity; + var hRatio = Width/ (float)_image.ImageData.Width; + var vRatio = Height / (float) _image.ImageData.Height; + TransformationMatrix*=Matrix.CreateScale(Math.Min(hRatio,vRatio)); + var shift = new Vector2(Width,Height)- Vector2.Transform(new Vector2(_image.ImageData.Width, _image.ImageData.Height), + TransformationMatrix); + TransformationMatrix *= Matrix.CreateTranslation(shift.X / 2f, shift.Y / 2f, 0); + + } + + public ImagePreviewControl() + { + InitializeComponent(); + + _backBuffer = new Bitmap(Width, Height); + + MouseWheel += ZoomPictureBoxWheel_MouseWheel; + DoubleClick += ZoomPictureBoxWheel_DoubleClick; + DoubleBuffered = true; + SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + } + + private void ZoomPictureBoxWheel_DoubleClick(object sender, EventArgs e) + { + + + Invalidate(); + } + + private float _zoom = 1; + private void ZoomPictureBoxWheel_MouseWheel(object sender, MouseEventArgs e) + { + _zoom *= (e.Delta > 0) ? 0.9f : (1f / 0.9f); + TransformationMatrix = TransformationMatrix + * Matrix.CreateTranslation(-_lastMousePosition.X, -_lastMousePosition.Y, 0) + * Matrix.CreateScale((e.Delta > 0) ? 0.9f : (1f / 0.9f)) + * Matrix.CreateTranslation(_lastMousePosition.X, _lastMousePosition.Y, 0); + + Invalidate(); + } + + public void FitToScreen() + { + if (Image == null) return; + var width = Image.ImageData.Width; + var height = Image.ImageData.Height; + var aspectRatio = (float)width / height; + var screenAspectRatio = (float)Width / Height; + + if (aspectRatio > screenAspectRatio) + { + _zoom = Width / (float)width; + TransformationMatrix = Matrix.Identity; + TransformationMatrix *= Matrix.CreateScale(_zoom); + var shift = new Vector2(Width, Height) - Vector2.Transform(new Vector2(width, height), + TransformationMatrix); + TransformationMatrix *= Matrix.CreateTranslation(shift.X / 2f, shift.Y / 2f, 0); + } + else + { + _zoom = Height / (float)height; + TransformationMatrix = Matrix.Identity; + TransformationMatrix *= Matrix.CreateScale(_zoom); + var shift = new Vector2(Width, Height) - Vector2.Transform(new Vector2(width, height), + TransformationMatrix); + TransformationMatrix *= Matrix.CreateTranslation(shift.X / 2f, shift.Y / 2f, 0); + } + Invalidate(); + } + + private MouseButtons _mouseButton = MouseButtons.None; + + public Matrix TransformationMatrix { get; set; }=Matrix.Identity; + + + private Vector2 _lastMousePosition; + private const float NODE_SIZE = 10; + protected override void OnMouseDown(MouseEventArgs e) + { + _mouseButton = e.Button; + _lastMousePosition = new Vector2(e.X, e.Y); + var threshold = NODE_SIZE / _zoom ; + + var inverted = Matrix.Invert(TransformationMatrix); + + var imageCoords = Vector2.Transform(new Vector2(e.X, e.Y), inverted); + _selectedNode=GraphicsElements.Where(x=>x.Editable).Select(x => x.GetNode(imageCoords, threshold)).FirstOrDefault(x => x != null); + } + + protected override void OnMouseUp(MouseEventArgs e) + { + _mouseButton = MouseButtons.None; + } + ToolTip _toolTip = new ToolTip(); + + static bool RectangleContainsPoint(int x, int y, int width, int height, int px, int py) + { + return px >= x && px <= x + width && py >= y && py <= y + height; + } + + protected override void OnMouseLeave(EventArgs e) + { + _toolTip.Hide(this); + } + + string GetContourInfo(Mat image, int x, int y) + { + var contours = image.FindContoursAsArray(RetrievalModes.List, ContourApproximationModes.ApproxSimple); + var contour = contours.FirstOrDefault(c => Cv2.PointPolygonTest(c, new Point2f(x, y), false) >= 0); + if (contour == null) return "No contour"; + var area = Cv2.ContourArea(contour); + var rect = Cv2.BoundingRect(contour); + var width = rect.Width; + var height = rect.Height; + return $"Area: {area}, Width: {width}, Height: {height}"; + + } + + protected override void OnMouseMove(MouseEventArgs e) + { + // if Ctrl is pressed + + + + if ((ModifierKeys & Keys.Control) != 0&&_mouseButton == MouseButtons.None) + { + if(Image==null||Image.ImageData==null) return; + var inverted = Matrix.Invert(TransformationMatrix); + var imageCoords = Vector2.Transform(new Vector2(e.X, e.Y), inverted); + if(!RectangleContainsPoint(0,0,Image.ImageData.Width,Image.ImageData.Height,(int)imageCoords.X,(int)imageCoords.Y)) return; + if (Image.ImageData.Type() == MatType.CV_8UC3) + { + var pixel = Image.ImageData.Get((int)imageCoords.Y,(int)imageCoords.X); + _toolTip.Show($"R:{pixel.Item0} G:{pixel.Item1} B:{pixel.Item2}", this, e.X + 15, e.Y + 15, 5000); + return; + } + + if (Image.ImageData.Type() == MatType.CV_8UC1) + { + var pixel = Image.ImageData.Get((int)imageCoords.Y,(int)imageCoords.X); + var info= GetContourInfo(Image.ImageData, (int)imageCoords.X, (int)imageCoords.Y); + _toolTip.Show($"Gray:{pixel}\n"+info, this, e.X+15, e.Y+15,5000); + return; + } + + return; + } + + if (Mode == EControlMode.Preview) + { + if (_mouseButton == MouseButtons.Middle) + { + TransformationMatrix *= + Matrix.CreateTranslation(e.X - _lastMousePosition.X, e.Y - _lastMousePosition.Y, 0); + + } + } + + _mouseShift = new Vector2(e.X, e.Y) - _lastMousePosition; + if (_mouseButton == MouseButtons.Left&& _selectedNode != null) + { + var inverted = Matrix.Invert(TransformationMatrix); + var transformed = Vector2.Transform(new Vector2(e.X, e.Y), inverted); + + + _selectedNode.SetPosition(transformed); + + + } + + _lastMousePosition = new Vector2(e.X, e.Y); + Invalidate(); + } + + private Graphics _currentGraphics; + private Workflow.Datatypes.HawkeyeImage? _image; + private Vector2 _mouseShift; + private INode _selectedNode; + + protected override void OnPaint(PaintEventArgs pe) + { + if (Image == null) + { + pe.Graphics.Clear(Color.Black); + return; + } + try + { + using (Graphics g = Graphics.FromImage(_backBuffer)) + { + _currentGraphics = g; + + g.Clear(Color.Black); + var img = Image.ImageData.ToBitmap(); + var pos = Vector2.Transform(new Vector2(0, 0), TransformationMatrix); + var size = Vector2.Transform(new Vector2(img.Width, img.Height), TransformationMatrix); + g.DrawImage(img, pos.X, pos.Y, size.X - pos.X, size.Y - pos.Y); + + + foreach (IGraphicsElement element in GraphicsElements) + { + element.Draw(this); + } + + + } + + pe.Graphics.DrawImage(_backBuffer, 0, 0); + } + catch + { + + } + + } + + + + protected override void OnPaintBackground(PaintEventArgs pevent) + { + //base.OnPaintBackground(pevent); + } + + protected override void OnResize(EventArgs e) + { + if (Width == 0) return; + _backBuffer = new Bitmap(Width, Height); + } + + + + public void DrawNode(Vector2 location) + { + + var imageCoordSpace = Vector2.Transform(location, TransformationMatrix); + var start = imageCoordSpace - new Vector2(NODE_SIZE/2, NODE_SIZE / 2); + var end = (imageCoordSpace + new Vector2(NODE_SIZE / 2, NODE_SIZE / 2))-start; + + + _currentGraphics.FillRectangle(Brushes.White,start.X,start.Y,end.X,end.Y); + _currentGraphics.DrawRectangle(Pens.Black,start.X,start.Y,end.X,end.Y); + + } + + public void DrawArrow(Vector2 location, Vector2 direction) + { + var imageCoordSpaceLocation = Vector2.Transform(location, TransformationMatrix); + var imageCoordSpaceSize = Vector2.Transform(location + direction, TransformationMatrix); + var arrowPen = new Pen(Color.Aqua, 2); + arrowPen.EndCap = LineCap.Custom; + arrowPen.CustomEndCap = new AdjustableArrowCap(5,5,true); + + _currentGraphics.DrawLine(arrowPen, imageCoordSpaceLocation.X, imageCoordSpaceLocation.Y, imageCoordSpaceSize.X, imageCoordSpaceSize.Y); + } + + + public void DrawRectangle(Vector2 start, Vector2 size, bool isGood) + { + var imageCoordSpaceLocation = Vector2.Transform(start, TransformationMatrix); + var imageCoordSpaceSize = Vector2.Transform(start+size, TransformationMatrix)-imageCoordSpaceLocation; + + var pen = new Pen(isGood ? Color.Green : Color.Red, 2); + _currentGraphics.DrawRectangle(pen, imageCoordSpaceLocation.X, imageCoordSpaceLocation.Y, imageCoordSpaceSize.X, imageCoordSpaceSize.Y); + } + + public void DrawLine(Vector2 start, Vector2 end,LineType type) + { + var coordStart = Vector2.Transform(start, TransformationMatrix); + var coordEnd = Vector2.Transform(end, TransformationMatrix); + var pen = new Pen(type ==LineType.Good? Color.Green:(type==LineType.Bad? Color.Red:Color.Aqua),2); + _currentGraphics.DrawLine(pen,coordStart.X,coordStart.Y,coordEnd.X,coordEnd.Y); + } + + public void DrawPoly(Vector2[] points, bool isGood) + { + var pen = new Pen(isGood ? Color.Green : Color.Red, 3); + var coords = points.Select(x=> Vector2.Transform(x, TransformationMatrix)).Select(x => new PointF(x.X, x.Y)).ToArray(); + _currentGraphics.DrawPolygon(pen, coords); + } + + public void DrawCross(Vector2 location) + { + + var startH=Vector2.Transform(location-new Vector2(10,0),TransformationMatrix); + var endH = Vector2.Transform(location+new Vector2(10,0), TransformationMatrix); + var startV =Vector2.Transform(location-new Vector2(0,20), TransformationMatrix); + var endV =Vector2.Transform(location+new Vector2(0,20), TransformationMatrix); + var pen = new Pen(Color.Aqua, 1); + _currentGraphics.DrawLine(pen, startH.X, startH.Y, endH.X, endH.Y); + _currentGraphics.DrawLine(pen, startV.X, startV.Y, endV.X, endV.Y); + } + } +} diff --git a/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.resx b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.Designer.cs b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.Designer.cs new file mode 100644 index 0000000..671cc3c --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.Designer.cs @@ -0,0 +1,77 @@ +namespace Hawkeye.VisionBuilder.Features.ImagePreview +{ + partial class ImagePreviewPanel + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ImagePreviewPanel)); + toolStrip1 = new ToolStrip(); + btnFit = new ToolStripButton(); + toolStrip1.SuspendLayout(); + SuspendLayout(); + // + // toolStrip1 + // + toolStrip1.Items.AddRange(new ToolStripItem[] { btnFit }); + toolStrip1.Location = new Point(0, 0); + toolStrip1.Name = "toolStrip1"; + toolStrip1.Size = new Size(800, 25); + toolStrip1.TabIndex = 0; + toolStrip1.Text = "toolStrip1"; + // + // btnFit + // + btnFit.DisplayStyle = ToolStripItemDisplayStyle.Text; + btnFit.Image = (Image)resources.GetObject("btnFit.Image"); + btnFit.ImageTransparentColor = Color.Magenta; + btnFit.Name = "btnFit"; + btnFit.Size = new Size(24, 22); + btnFit.Text = "Fit"; + btnFit.Click += btnFit_Click; + // + // ImagePreviewPanel + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(800, 450); + Controls.Add(toolStrip1); + DoubleBuffered = true; + Name = "ImagePreviewPanel"; + Text = "Image Preview"; + toolStrip1.ResumeLayout(false); + toolStrip1.PerformLayout(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private ImagePreviewControl imagePreviewControl; + private ToolStrip toolStrip1; + private ToolStripButton btnFit; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.cs b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.cs new file mode 100644 index 0000000..47e65f1 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; + +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using Hawkeye.VisionBuilder.Panels; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements; + +namespace Hawkeye.VisionBuilder.Features.ImagePreview +{ + public partial class ImagePreviewPanel : BasePannel + { + public ImagePreviewPanel() + { + InitializeComponent(); + imagePreviewControl = new ImagePreviewControl() + { + Dock = DockStyle.Fill + }; + this.Controls.Add(imagePreviewControl); + imagePreviewControl.BringToFront(); + } + + public Workflow.Datatypes.HawkeyeImage CurrentImage { get; set; } + public void SetImage(Workflow.Datatypes.HawkeyeImage image) + { + if (image == null) return; + imagePreviewControl.Image = image; + CurrentImage = image; + imagePreviewControl.Invalidate(); + } + + public void ClearGraphics() + { + imagePreviewControl.GraphicsElements.Clear(); + } + + + public void AddGraphics(IGraphicsElement element) + { + imagePreviewControl.GraphicsElements.Add(element); + } + + private void btnFit_Click(object sender, EventArgs e) + { + imagePreviewControl.FitToScreen(); + } + } +} diff --git a/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.resx b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.resx new file mode 100644 index 0000000..d57b143 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagePreview/ImagePreviewPanel.resx @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACRSURBVDhPY/j27dt/SjDYACcnJ7IwigEf3n8kCZNswPNb + J/+f6DYF0yA+yQac6Db5f6hWCmwIiE+mC0wIu2DS2Vf/F1x6DefjwlgNyNr34r/0wkdgTMgQDAOQNRNj + CIoBOg0rMTTDMLIhIHbriZeYBmDTiIxBGkEYxge5liQDsGGQqykyAISpZwAlmIEywMAAAAc1/Jwvt6sN + AAAAAElFTkSuQmCC + + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.Designer.cs b/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.Designer.cs new file mode 100644 index 0000000..7dfba81 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.Designer.cs @@ -0,0 +1,96 @@ +namespace Hawkeye.VisionBuilder.Features.ImageStatistics +{ + partial class ImageStatisticsDialog + { + private System.ComponentModel.IContainer components = null; + private System.Windows.Forms.TreeView treeView; + private System.Windows.Forms.Button btnReset; + private System.Windows.Forms.Button btnClose; + private System.Windows.Forms.Button btnBackward; + private System.Windows.Forms.Button btnForward; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + private void InitializeComponent() + { + this.treeView = new System.Windows.Forms.TreeView(); + this.btnReset = new System.Windows.Forms.Button(); + this.btnClose = new System.Windows.Forms.Button(); + this.btnBackward = new System.Windows.Forms.Button(); + this.btnForward = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // treeView + // + this.treeView.Location = new System.Drawing.Point(12, 12); + this.treeView.Name = "treeView"; + this.treeView.Size = new System.Drawing.Size(360, 300); + this.treeView.TabIndex = 0; + this.treeView.NodeMouseClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.treeView_NodeMouseClick); + // + // btnReset + // + this.btnReset.Location = new System.Drawing.Point(12, 320); + this.btnReset.Name = "btnReset"; + this.btnReset.Size = new System.Drawing.Size(75, 30); + this.btnReset.TabIndex = 1; + this.btnReset.Text = "Reset"; + this.btnReset.UseVisualStyleBackColor = true; + this.btnReset.Click += new System.EventHandler(this.btnReset_Click); + // + // btnClose + // + this.btnClose.Location = new System.Drawing.Point(297, 320); + this.btnClose.Name = "btnClose"; + this.btnClose.Size = new System.Drawing.Size(75, 30); + this.btnClose.TabIndex = 2; + this.btnClose.Text = "Close"; + this.btnClose.UseVisualStyleBackColor = true; + this.btnClose.Click += new System.EventHandler(this.btnClose_Click); + // + // btnBackward + // + this.btnBackward.Location = new System.Drawing.Point(100, 320); + this.btnBackward.Name = "btnBackward"; + this.btnBackward.Size = new System.Drawing.Size(75, 30); + this.btnBackward.TabIndex = 3; + this.btnBackward.Text = "<"; + this.btnBackward.UseVisualStyleBackColor = true; + this.btnBackward.Click += new System.EventHandler(this.btnBackward_Click); + // + // btnForward + // + this.btnForward.Location = new System.Drawing.Point(190, 320); + this.btnForward.Name = "btnForward"; + this.btnForward.Size = new System.Drawing.Size(75, 30); + this.btnForward.TabIndex = 4; + this.btnForward.Text = ">"; + this.btnForward.UseVisualStyleBackColor = true; + this.btnForward.Click += new System.EventHandler(this.btnForward_Click); + // + // ImageStatisticsDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 20F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(384, 361); + this.Controls.Add(this.btnForward); + this.Controls.Add(this.btnBackward); + this.Controls.Add(this.btnClose); + this.Controls.Add(this.btnReset); + this.Controls.Add(this.treeView); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "ImageStatisticsDialog"; + this.Text = "Image Statistics"; + this.ResumeLayout(false); + } + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs b/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs new file mode 100644 index 0000000..701fe04 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.cs @@ -0,0 +1,194 @@ +using System; +using System.Windows.Forms; +using System.Collections.Generic; +using System.Linq; + +namespace Hawkeye.VisionBuilder.Features.ImageStatistics +{ + public partial class ImageStatisticsDialog : Form + { + private TreeNode _goodNode; + private TreeNode _badNode; + private HashSet _allFiles = new HashSet(StringComparer.OrdinalIgnoreCase); + private TreeNode? _selectedLeafNode = null; + private Dictionary _badDefectCounts = new(StringComparer.OrdinalIgnoreCase); + + public event Action? FileSelected; + + public ImageStatisticsDialog() + { + InitializeComponent(); + InitializeTree(); + treeView.AfterSelect += treeView_AfterSelect; + UpdateNavigationButtons(); + } + + private void InitializeTree() + { + treeView.Nodes.Clear(); + _goodNode = new TreeNode(GetBranchText("Good", 0)); + _badNode = new TreeNode(GetBranchText("Bad", 0)); + treeView.Nodes.Add(_goodNode); + treeView.Nodes.Add(_badNode); + treeView.ExpandAll(); + _allFiles.Clear(); + _badDefectCounts.Clear(); + } + + public void AddGoodImage(string filePath) + { + if (_allFiles.Contains(filePath)) + return; + string nodeText = System.IO.Path.GetFileName(filePath); + _goodNode.Nodes.Add(new TreeNode(nodeText) { Tag = filePath }); + _allFiles.Add(filePath); + UpdateBranchText(_goodNode, "Good"); + treeView.ExpandAll(); + } + + public void AddBadImage(string filePath, string[]? defects = null) + { + if (_allFiles.Contains(filePath)) + return; + string nodeText = System.IO.Path.GetFileName(filePath); + if (defects != null && defects.Length > 0) + { + nodeText += " (" + string.Join(", ", defects) + ")"; + foreach (var defect in defects) + { + if (string.IsNullOrWhiteSpace(defect)) continue; + if (_badDefectCounts.ContainsKey(defect)) + _badDefectCounts[defect]++; + else + _badDefectCounts[defect] = 1; + } + } + _badNode.Nodes.Add(new TreeNode(nodeText) { Tag = filePath }); + _allFiles.Add(filePath); + UpdateBranchText(_badNode, "Bad"); + treeView.ExpandAll(); + } + + private void UpdateBranchText(TreeNode node, string name) + { + if (node == _badNode) + { + node.Text = GetBranchText(name, node.Nodes.Count); + } + else + { + node.Text = GetBranchText(name, node.Nodes.Count); + } + } + + private string GetBranchText(string name, int count) + { + if (name == "Bad") + { + if (_badDefectCounts.Count > 0) + { + var defectSummary = string.Join(", ", + _badDefectCounts + .OrderByDescending(kv => kv.Value) + .ThenBy(kv => kv.Key) + .Select(kv => $"{kv.Key} {kv.Value}") + ); + return $"{name} ({defectSummary})"; + } + } + return $"{name} ({count})"; + } + + private void btnReset_Click(object sender, EventArgs e) + { + InitializeTree(); + _badDefectCounts.Clear(); + } + + private void btnClose_Click(object sender, EventArgs e) + { + this.Close(); + } + + private void treeView_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e) + { + // Only fire event for leaf nodes (filenames) + if (e.Node.Parent != null && e.Node.Tag is string filePath) + { + _selectedLeafNode = e.Node; + FileSelected?.Invoke(filePath); + } + else + { + _selectedLeafNode = null; + } + UpdateNavigationButtons(); + } + + private void treeView_AfterSelect(object? sender, TreeViewEventArgs e) + { + // Only consider leaf nodes (filenames) + if (e.Node.Parent != null && e.Node.Tag is string) + { + _selectedLeafNode = e.Node; + } + else + { + _selectedLeafNode = null; + } + UpdateNavigationButtons(); + } + + private void UpdateNavigationButtons() + { + if (_selectedLeafNode == null) + { + btnBackward.Enabled = false; + btnForward.Enabled = false; + return; + } + var parent = _selectedLeafNode.Parent; + if (parent == null) + { + btnBackward.Enabled = false; + btnForward.Enabled = false; + return; + } + int idx = parent.Nodes.IndexOf(_selectedLeafNode); + btnBackward.Enabled = idx > 0; + btnForward.Enabled = idx < parent.Nodes.Count - 1; + } + + private void btnBackward_Click(object sender, EventArgs e) + { + if (_selectedLeafNode == null) return; + var parent = _selectedLeafNode.Parent; + if (parent == null) return; + int idx = parent.Nodes.IndexOf(_selectedLeafNode); + if (idx > 0) + { + var prev = parent.Nodes[idx - 1]; + treeView.SelectedNode = prev; + treeView.Focus(); + if (prev.Tag is string filePath) + FileSelected?.Invoke(filePath); + } + } + + private void btnForward_Click(object sender, EventArgs e) + { + if (_selectedLeafNode == null) return; + var parent = _selectedLeafNode.Parent; + if (parent == null) return; + int idx = parent.Nodes.IndexOf(_selectedLeafNode); + if (idx < parent.Nodes.Count - 1) + { + var next = parent.Nodes[idx + 1]; + treeView.SelectedNode = next; + treeView.Focus(); + if (next.Tag is string filePath) + FileSelected?.Invoke(filePath); + } + } + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.resx b/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImageStatistics/ImageStatisticsDialog.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.Designer.cs b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.Designer.cs new file mode 100644 index 0000000..826389f --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.Designer.cs @@ -0,0 +1,221 @@ +namespace Hawkeye.VisionBuilder.Features.ImagesStrip +{ + partial class ImageStripPanel + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ImageStripPanel)); + toolStrip1 = new ToolStrip(); + lblSelectedImage = new ToolStripLabel(); + btnToStart = new ToolStripButton(); + btnPrev = new ToolStripButton(); + btnPlay = new ToolStripButton(); + btnNext = new ToolStripButton(); + btnToEnd = new ToolStripButton(); + toolStripSeparator1 = new ToolStripSeparator(); + cbRepeat = new ToolStripButton(); + toolStripLabel1 = new ToolStripLabel(); + ncInterval = new ToolStripNumberControl(); + ckbExecuteWorkflow = new ToolStripButton(); + ckbStopOnError = new ToolStripButton(); + ckbStopOnNoError = new ToolStripButton(); + listView1 = new ListView(); + toolStrip1.SuspendLayout(); + SuspendLayout(); + // + // toolStrip1 + // + toolStrip1.Items.AddRange(new ToolStripItem[] { lblSelectedImage, btnToStart, btnPrev, btnPlay, btnNext, btnToEnd, toolStripSeparator1, cbRepeat, toolStripLabel1, ncInterval, ckbExecuteWorkflow, ckbStopOnError, ckbStopOnNoError }); + toolStrip1.Location = new Point(0, 0); + toolStrip1.Name = "toolStrip1"; + toolStrip1.Size = new Size(876, 26); + toolStrip1.TabIndex = 0; + toolStrip1.Text = "toolStrip1"; + // + // lblSelectedImage + // + lblSelectedImage.Name = "lblSelectedImage"; + lblSelectedImage.Size = new Size(32, 23); + lblSelectedImage.Text = "--/--"; + // + // btnToStart + // + btnToStart.DisplayStyle = ToolStripItemDisplayStyle.Image; + btnToStart.Image = Resource1.backward_fast; + btnToStart.ImageTransparentColor = Color.Magenta; + btnToStart.Name = "btnToStart"; + btnToStart.Size = new Size(23, 23); + btnToStart.Text = "First"; + btnToStart.Click += btnToStart_Click; + // + // btnPrev + // + btnPrev.DisplayStyle = ToolStripItemDisplayStyle.Image; + btnPrev.Image = Resource1.backward; + btnPrev.ImageTransparentColor = Color.Magenta; + btnPrev.Name = "btnPrev"; + btnPrev.Size = new Size(23, 23); + btnPrev.Text = "Previous"; + btnPrev.Click += btnPrev_Click; + // + // btnPlay + // + btnPlay.CheckOnClick = true; + btnPlay.DisplayStyle = ToolStripItemDisplayStyle.Image; + btnPlay.Image = Resource1.play; + btnPlay.ImageTransparentColor = Color.Magenta; + btnPlay.Name = "btnPlay"; + btnPlay.Size = new Size(23, 23); + btnPlay.Text = "Play"; + btnPlay.Click += btnPlay_Click; + // + // btnNext + // + btnNext.DisplayStyle = ToolStripItemDisplayStyle.Image; + btnNext.Image = Resource1.forward; + btnNext.ImageTransparentColor = Color.Magenta; + btnNext.Name = "btnNext"; + btnNext.Size = new Size(23, 23); + btnNext.Text = "Next"; + btnNext.Click += btnNext_Click; + // + // btnToEnd + // + btnToEnd.DisplayStyle = ToolStripItemDisplayStyle.Image; + btnToEnd.Image = Resource1.forward_fast; + btnToEnd.ImageTransparentColor = Color.Magenta; + btnToEnd.Name = "btnToEnd"; + btnToEnd.Size = new Size(23, 23); + btnToEnd.Text = "Last"; + btnToEnd.Click += btnToEnd_Click; + // + // toolStripSeparator1 + // + toolStripSeparator1.Name = "toolStripSeparator1"; + toolStripSeparator1.Size = new Size(6, 26); + // + // cbRepeat + // + cbRepeat.CheckOnClick = true; + cbRepeat.DisplayStyle = ToolStripItemDisplayStyle.Image; + cbRepeat.Image = Resource1.repeat; + cbRepeat.ImageTransparentColor = Color.Magenta; + cbRepeat.Name = "cbRepeat"; + cbRepeat.Size = new Size(23, 23); + cbRepeat.Text = "Repeat"; + // + // toolStripLabel1 + // + toolStripLabel1.Name = "toolStripLabel1"; + toolStripLabel1.Size = new Size(49, 23); + toolStripLabel1.Text = "Interval:"; + // + // ncInterval + // + ncInterval.AutoSize = false; + ncInterval.Name = "ncInterval"; + ncInterval.Size = new Size(64, 23); + ncInterval.Text = "100"; + // + // ckbExecuteWorkflow + // + ckbExecuteWorkflow.CheckOnClick = true; + ckbExecuteWorkflow.DisplayStyle = ToolStripItemDisplayStyle.Text; + ckbExecuteWorkflow.Image = (Image)resources.GetObject("ckbExecuteWorkflow.Image"); + ckbExecuteWorkflow.ImageTransparentColor = Color.Magenta; + ckbExecuteWorkflow.Name = "ckbExecuteWorkflow"; + ckbExecuteWorkflow.Size = new Size(104, 23); + ckbExecuteWorkflow.Text = "Execute workflow"; + // + // ckbStopOnError + // + ckbStopOnError.CheckOnClick = true; + ckbStopOnError.DisplayStyle = ToolStripItemDisplayStyle.Text; + ckbStopOnError.Image = (Image)resources.GetObject("ckbStopOnError.Image"); + ckbStopOnError.ImageTransparentColor = Color.Magenta; + ckbStopOnError.Name = "ckbStopOnError"; + ckbStopOnError.Size = new Size(80, 23); + ckbStopOnError.Text = "Stop on error"; + // + // ckbStopOnNoError + // + ckbStopOnNoError.CheckOnClick = true; + ckbStopOnNoError.DisplayStyle = ToolStripItemDisplayStyle.Text; + ckbStopOnNoError.Image = (Image)resources.GetObject("ckbStopOnNoError.Image"); + ckbStopOnNoError.ImageTransparentColor = Color.Magenta; + ckbStopOnNoError.Name = "ckbStopOnNoError"; + ckbStopOnNoError.Size = new Size(97, 23); + ckbStopOnNoError.Text = "Stop on no error"; + // + // listView1 + // + listView1.Alignment = ListViewAlignment.Left; + listView1.Dock = DockStyle.Fill; + listView1.HeaderStyle = ColumnHeaderStyle.None; + listView1.LabelWrap = false; + listView1.Location = new Point(0, 26); + listView1.MultiSelect = false; + listView1.Name = "listView1"; + listView1.Size = new Size(876, 171); + listView1.TabIndex = 1; + listView1.UseCompatibleStateImageBehavior = false; + // + // ImageStripPanel + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(876, 197); + Controls.Add(listView1); + Controls.Add(toolStrip1); + MaximizeBox = false; + MinimizeBox = false; + Name = "ImageStripPanel"; + toolStrip1.ResumeLayout(false); + toolStrip1.PerformLayout(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private ToolStrip toolStrip1; + private ToolStripButton btnToStart; + private ToolStripButton btnPrev; + private ToolStripButton btnPlay; + private ToolStripButton btnNext; + private ToolStripButton btnToEnd; + private ToolStripSeparator toolStripSeparator1; + private ToolStripButton cbRepeat; + private ListView listView1; + private ToolStripLabel toolStripLabel1; + private ToolStripNumberControl ncInterval; + private ToolStripButton ckbExecuteWorkflow; + private ToolStripLabel lblSelectedImage; + private ToolStripButton ckbStopOnError; + private ToolStripButton ckbStopOnNoError; + } +} diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.cs b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.cs new file mode 100644 index 0000000..93694e7 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.cs @@ -0,0 +1,319 @@ +using System.Drawing.Drawing2D; +using OpenCvSharp; +using OpenCvSharp.Extensions; +using VisionBuilder.UI.Common.RecipeProcessing; +using Size = System.Drawing.Size; + +namespace Hawkeye.VisionBuilder.Features.ImagesStrip +{ + public partial class ImageStripPanel : Form, IImageSource + { + + + public ImageStripPanel() + { + + InitializeComponent(); + + listView1.SelectedIndexChanged += ListView1_SelectedIndexChanged; + } + + private void ListView1_SelectedIndexChanged(object? sender, EventArgs e) + { + if (listView1.SelectedItems.Count > 0) + { + lblSelectedImage.Text = $"{listView1.SelectedIndices[0] + 1}/{listView1.Items.Count}"; + var path = (string)listView1.SelectedItems[0].Tag; + listView1.SelectedItems[0].ImageKey = path; + var img = new Mat(path); + var image = new Workflow.Datatypes.HawkeyeImage() + { + Filename = path, + ImageData = img + }; + listView1.LargeImageList.Images.RemoveByKey(path); + listView1.LargeImageList.Images.Add(path, img.Resize(new OpenCvSharp.Size(108, 108)).ToBitmap()); + ImageSelected(image); + } + } + + public bool IsExecutingWorkflow + { + get => ckbExecuteWorkflow.Checked; + set => ckbExecuteWorkflow.Checked = value; + } + + public event Action ImageSelected = delegate { }; + public void LoadImage(Mat image) + { + ImageList imageList = new ImageList(); + imageList.ImageSize = new Size(108, 108); + listView1.LargeImageList = imageList; + listView1.Items.Clear(); + imageList.Images.Add("image", image.ToBitmap()); + var item = listView1.Items.Add("image"); + item.Tag = new Workflow.Datatypes.HawkeyeImage() + { + Filename = "image", + ImageData = image + }; + item.Text = ""; + item.ImageKey = "image"; + + listView1.SelectedIndices.Clear(); + listView1.SelectedIndices.Add(0); + } + public Bitmap FitImage(Image image, int targetWidth, int targetHeight) + { + var aspectRatio = (float)image.Width / image.Height; + int width; + int height; + if ((float)targetWidth / targetHeight > aspectRatio) + { + width = (int)(targetHeight * aspectRatio); + height = targetHeight; + } + else + { + width = targetWidth; + height = (int)(targetWidth / aspectRatio); + } + + Bitmap bitmap = new Bitmap(targetWidth, targetHeight); + using (Graphics graphics = Graphics.FromImage(bitmap)) + { + graphics.DrawImage(image, new Rectangle(0, 0, width, height)); + return bitmap; + } + } + + public void LoadImages(string directory) + { + listView1.BeginUpdate(); + ImageList imageList = new ImageList(); + imageList.ImageSize = new Size(108, 108); + listView1.LargeImageList = imageList; + listView1.Items.Clear(); + var mock = new Bitmap(108, 108); + using (Graphics g = Graphics.FromImage(mock)) + { + var brush = new HatchBrush(HatchStyle.Percent50, Color.LightGray, Color.Transparent); + g.FillRectangle(brush, 0, 0, 108, 108); + + g.DrawString("Not loaded", new Font("Arial", 12), Brushes.Gray, new PointF(0, 0)); + } + var allImages = Directory.GetFiles(directory, "*.bmp") + .Concat(Directory.GetFiles(directory, "*.png")) + .Concat(Directory.GetFiles(directory, "*.jpg")) + .Where(x => !Path.GetFileNameWithoutExtension(x).Contains("_analysis")) + //.Select(x=>new {path=x,bmp = new Bitmap(x)}) + .Select(x => new { path = x, bmp = mock }) + .ToArray(); + + + imageList.Images.Add("none", mock); + + + foreach (var image in allImages) + { + + + var item = listView1.Items.Add(image.path); + item.Text = ""; + item.Tag = image.path; + item.ImageKey = "none"; + } + + if (listView1.Items.Count > 0) + { + listView1.SelectedIndices.Clear(); + listView1.SelectedIndices.Add(0); + toolStrip1.Enabled = true; + } + else + { + toolStrip1.Enabled = false; + } + listView1.EndUpdate(); + } + + private void btnToStart_Click(object sender, EventArgs e) + { + listView1.SelectedIndices.Clear(); + listView1.SelectedIndices.Add(0); + listView1.EnsureVisible(listView1.SelectedIndices[0]); + } + + public void Prev() + { + btnPrev_Click(null, null); + } + private void btnPrev_Click(object sender, EventArgs e) + { + if (listView1.SelectedIndices.Count == 0) return; + var index = listView1.SelectedIndices[0]; + index--; + if (index < 0) index++; + listView1.SelectedIndices.Clear(); + listView1.SelectedIndices.Add(index); + listView1.EnsureVisible(listView1.SelectedIndices[0]); + } + + public void Next() + { + btnNext_Click(null, null); + } + + private void btnNext_Click(object sender, EventArgs e) + { + + if (listView1.SelectedIndices.Count == 0) return; + var index = listView1.SelectedIndices[0]; + index++; + if (index > listView1.Items.Count - 1) index--; + listView1.SelectedIndices.Clear(); + listView1.SelectedIndices.Add(index); + listView1.EnsureVisible(listView1.SelectedIndices[0]); + } + + private void btnToEnd_Click(object sender, EventArgs e) + { + listView1.SelectedIndices.Clear(); + listView1.SelectedIndices.Add(listView1.Items.Count - 1); + listView1.EnsureVisible(listView1.SelectedIndices[0]); + } + + private void btnPlay_Click(object sender, EventArgs e) + { + Task.Run(Playback); + } + + private async Task Playback() + { + while (btnPlay.Checked) + { + Action a = () => + { + if (listView1.SelectedIndices.Count == 0) return; + var index = listView1.SelectedIndices[0]; + index++; + if (index > listView1.Items.Count - 1) + { + if (cbRepeat.Checked) + { + index = 0; + } + else + { + btnPlay.Checked = false; + return; + } + + } + listView1.SelectedIndices.Clear(); + listView1.SelectedIndices.Add(index); + listView1.EnsureVisible(listView1.SelectedIndices[0]); + }; + listView1.Invoke(a); + await Task.Delay(int.Parse((string)ncInterval.Text)); + } + } + + public string GetName() + { + return "ImageStripPanel"; + } + + public Task GetImage(CancellationToken token) + { + return Task.FromResult(GetImage()); + } + + public Mat GetImage() + { + if (listView1.SelectedIndices.Count == 0) return null; + + var path = listView1.SelectedItems[0].ImageKey; + + return new Mat(path); + } + + public string GetImagePath() + { + if (listView1.SelectedIndices.Count == 0) return null; + var path = listView1.SelectedItems[0].Tag as string; + if (path == null) return null; + return path; + } + + public bool StopOnErrors + { + get => ckbStopOnError.Checked; + set => ckbStopOnError.Checked = value; + } + + public bool StopOnNoErrors + { + get => ckbStopOnNoError.Checked; + set => ckbStopOnNoError.Checked = value; + } + + + public void InformImageProcessed(bool result) + { + if (!result) + { + if (ckbStopOnError.Checked) + { + btnPlay.Checked = false; + } + } + else + { + if (ckbStopOnNoError.Checked) + { + btnPlay.Checked = false; + } + } + } + + public void RemoveImage(Workflow.Datatypes.HawkeyeImage currentImage) + { + if (listView1.SelectedIndices.Count == 0) return; + var index = listView1.SelectedIndices[0]; + listView1.Items.RemoveAt(index); + if (listView1.Items.Count > 0) + { + if (index > listView1.Items.Count - 1) index--; + listView1.SelectedIndices.Clear(); + listView1.SelectedIndices.Add(index); + listView1.EnsureVisible(listView1.SelectedIndices[0]); + } + + } + + public List GetFileList() + { + return listView1.Items.Cast().Select(x => (string)x.Tag).ToList(); + } + + public void ReSelect() + { + if(listView1.SelectedIndices.Count == 0) return; + var index = listView1.SelectedIndices[0]; + listView1.SelectedIndices.Clear(); + listView1.SelectedIndices.Add(index); + listView1.EnsureVisible(listView1.SelectedIndices[0]); + } + + public void SelectImage(string nextFile) + { + var index = listView1.Items.Cast().Where(x => (string)x.Tag == nextFile).Select(x => x.Index).FirstOrDefault(); + if (index == -1) return; + listView1.SelectedIndices.Clear(); + listView1.SelectedIndices.Add(index); + listView1.EnsureVisible(listView1.SelectedIndices[0]); + + } + } +} diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.resx b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.resx new file mode 100644 index 0000000..89db3fd --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.resx @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACRSURBVDhPY/j27dt/SjDYACcnJ7IwigEf3n8kCZNswPNb + J/+f6DYF0yA+yQac6Db5f6hWCmwIiE+mC0wIu2DS2Vf/F1x6DefjwlgNyNr34r/0wkdgTMgQDAOQNRNj + CIoBOg0rMTTDMLIhIHbriZeYBmDTiIxBGkEYxge5liQDsGGQqykyAISpZwAlmIEywMAAAAc1/Jwvt6sN + AAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACRSURBVDhPY/j27dt/SjDYACcnJ7IwigEf3n8kCZNswPNb + J/+f6DYF0yA+yQac6Db5f6hWCmwIiE+mC0wIu2DS2Vf/F1x6DefjwlgNyNr34r/0wkdgTMgQDAOQNRNj + CIoBOg0rMTTDMLIhIHbriZeYBmDTiIxBGkEYxge5liQDsGGQqykyAISpZwAlmIEywMAAAAc1/Jwvt6sN + AAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACRSURBVDhPY/j27dt/SjDYACcnJ7IwigEf3n8kCZNswPNb + J/+f6DYF0yA+yQac6Db5f6hWCmwIiE+mC0wIu2DS2Vf/F1x6DefjwlgNyNr34r/0wkdgTMgQDAOQNRNj + CIoBOg0rMTTDMLIhIHbriZeYBmDTiIxBGkEYxge5liQDsGGQqykyAISpZwAlmIEywMAAAAc1/Jwvt6sN + AAAAAElFTkSuQmCC + + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/Resource1.Designer.cs b/Hawkeye.VisionBuilder/Features/ImagesStrip/Resource1.Designer.cs new file mode 100644 index 0000000..99badb0 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/Resource1.Designer.cs @@ -0,0 +1,133 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Hawkeye.VisionBuilder.Features.ImagesStrip { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource1 { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource1() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Hawkeye.VisionBuilder.Features.ImagesStrip.Resource1", typeof(Resource1).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap backward { + get { + object obj = ResourceManager.GetObject("backward", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap backward_fast { + get { + object obj = ResourceManager.GetObject("backward-fast", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap forward { + get { + object obj = ResourceManager.GetObject("forward", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap forward_fast { + get { + object obj = ResourceManager.GetObject("forward-fast", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap pause { + get { + object obj = ResourceManager.GetObject("pause", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap play { + get { + object obj = ResourceManager.GetObject("play", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap repeat { + get { + object obj = ResourceManager.GetObject("repeat", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/Resource1.resx b/Hawkeye.VisionBuilder/Features/ImagesStrip/Resource1.resx new file mode 100644 index 0000000..65a053b --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/Resource1.resx @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + images\backward.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + images\backward-fast.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + images\forward.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + images\forward-fast.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + images\pause.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + images\play.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + images\repeat.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/ToolstripNumberControl.cs b/Hawkeye.VisionBuilder/Features/ImagesStrip/ToolstripNumberControl.cs new file mode 100644 index 0000000..805c673 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/ToolstripNumberControl.cs @@ -0,0 +1,41 @@ +using System.Windows.Forms.Design; + +namespace Hawkeye.VisionBuilder.Features.ImagesStrip; + +[ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.ToolStrip)] +public class ToolStripNumberControl : ToolStripControlHost +{ + public ToolStripNumberControl() + : base(new NumericUpDown()) + { + (Control as NumericUpDown).Maximum = 10000; + (Control as NumericUpDown).Minimum = 0; + } + + protected override void OnSubscribeControlEvents(Control control) + { + base.OnSubscribeControlEvents(control); + ((NumericUpDown)control).ValueChanged += new EventHandler(OnValueChanged); + } + + protected override void OnUnsubscribeControlEvents(Control control) + { + base.OnUnsubscribeControlEvents(control); + ((NumericUpDown)control).ValueChanged -= new EventHandler(OnValueChanged); + } + + public event EventHandler ValueChanged; + + public Control NumericUpDownControl + { + get { return Control as NumericUpDown; } + } + + public void OnValueChanged(object sender, EventArgs e) + { + if (ValueChanged != null) + { + ValueChanged(this, e); + } + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/images/backward-fast.png b/Hawkeye.VisionBuilder/Features/ImagesStrip/images/backward-fast.png new file mode 100644 index 0000000000000000000000000000000000000000..1c5a5521feb2009b468e7c06ec5d8314288dc7e2 GIT binary patch literal 5920 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 E1&FX9n*aa+ literal 0 HcmV?d00001 diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/images/backward.png b/Hawkeye.VisionBuilder/Features/ImagesStrip/images/backward.png new file mode 100644 index 0000000000000000000000000000000000000000..c15d5bb6e14477da276c720ac1663add598ea963 GIT binary patch literal 5300 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 QR`tBzX_I5#FTRog0o-mQD*ylh literal 0 HcmV?d00001 diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/images/forward-fast.png b/Hawkeye.VisionBuilder/Features/ImagesStrip/images/forward-fast.png new file mode 100644 index 0000000000000000000000000000000000000000..760839c4f80dbaa2077debf8be2f41e38941ff2c GIT binary patch literal 5158 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 literal 0 HcmV?d00001 diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/images/forward.png b/Hawkeye.VisionBuilder/Features/ImagesStrip/images/forward.png new file mode 100644 index 0000000000000000000000000000000000000000..65eb772a562e00e3fc0d160c0071be0eb4390338 GIT binary patch literal 5412 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 Xrk;Q7F604AxywIC^HKy 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 0) + { + _form.Show(); + textBox.Focus(); + } + else + { + _form.Hide(); + } + this.Visible = this.Items.Count > 0; + + } + + + + private void TextBox_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Down && this.Visible) + { + this.Focus(); + this.SelectedIndex = 0; + } + else if (e.KeyCode == Keys.Escape) + { + Cancel(); + } + else if (e.KeyCode == Keys.Enter && this.Visible && this.SelectedIndex != -1) + { + Accept(); + } + } + + public delegate void NeedSugestionsDelegate(string input, List suggestions); + public event NeedSugestionsDelegate NeedSugestions =delegate{ }; + + private List GetSuggestions(string input) + { + var source = new List(); + + NeedSugestions(input, source); + if (source.Count == 0) + { + Visible = false; + _form.Hide(); + } + return source.ToList(); + } + } +} diff --git a/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.Designer.cs b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.Designer.cs new file mode 100644 index 0000000..5e108ea --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.Designer.cs @@ -0,0 +1,130 @@ +namespace Hawkeye.VisionBuilder.Features.ToolEditor +{ + partial class ToolConfigurationPanel + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel(); + this.panel1 = new System.Windows.Forms.Panel(); + this.panel2 = new System.Windows.Forms.Panel(); + this.btnOk = new System.Windows.Forms.Button(); + this.btnCancel = new System.Windows.Forms.Button(); + this.cbAutoApply = new System.Windows.Forms.CheckBox(); + this.panel1.SuspendLayout(); + this.panel2.SuspendLayout(); + this.SuspendLayout(); + // + // flowLayoutPanel1 + // + this.flowLayoutPanel1.AutoScroll = true; + this.flowLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; + this.flowLayoutPanel1.FlowDirection = System.Windows.Forms.FlowDirection.TopDown; + this.flowLayoutPanel1.Location = new System.Drawing.Point(0, 0); + this.flowLayoutPanel1.Name = "flowLayoutPanel1"; + this.flowLayoutPanel1.Size = new System.Drawing.Size(800, 392); + this.flowLayoutPanel1.TabIndex = 0; + this.flowLayoutPanel1.WrapContents = false; + // + // panel1 + // + this.panel1.Controls.Add(this.panel2); + this.panel1.Controls.Add(this.cbAutoApply); + this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom; + this.panel1.Location = new System.Drawing.Point(0, 392); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(800, 58); + this.panel1.TabIndex = 1; + // + // panel2 + // + this.panel2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.panel2.Controls.Add(this.btnOk); + this.panel2.Controls.Add(this.btnCancel); + this.panel2.Enabled = false; + this.panel2.Location = new System.Drawing.Point(624, 8); + this.panel2.Name = "panel2"; + this.panel2.Size = new System.Drawing.Size(168, 40); + this.panel2.TabIndex = 3; + // + // btnOk + // + this.btnOk.Location = new System.Drawing.Point(8, 8); + this.btnOk.Name = "btnOk"; + this.btnOk.Size = new System.Drawing.Size(75, 23); + this.btnOk.TabIndex = 0; + this.btnOk.Text = "Apply"; + this.btnOk.UseVisualStyleBackColor = true; + this.btnOk.Click += new System.EventHandler(this.btnOk_Click); + // + // btnCancel + // + this.btnCancel.Location = new System.Drawing.Point(88, 8); + this.btnCancel.Name = "btnCancel"; + this.btnCancel.Size = new System.Drawing.Size(75, 23); + this.btnCancel.TabIndex = 1; + this.btnCancel.Text = "Revert"; + this.btnCancel.UseVisualStyleBackColor = true; + this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click); + // + // cbAutoApply + // + this.cbAutoApply.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.cbAutoApply.AutoSize = true; + this.cbAutoApply.Location = new System.Drawing.Point(528, 16); + this.cbAutoApply.Name = "cbAutoApply"; + this.cbAutoApply.Size = new System.Drawing.Size(86, 19); + this.cbAutoApply.TabIndex = 2; + this.cbAutoApply.Text = "Auto-apply"; + this.cbAutoApply.UseVisualStyleBackColor = true; + this.cbAutoApply.Visible = false; + // + // ToolConfigurationPanel + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.flowLayoutPanel1); + this.Controls.Add(this.panel1); + this.Name = "ToolConfigurationPanel"; + this.Text = "Tool Configuration"; + this.panel1.ResumeLayout(false); + this.panel1.PerformLayout(); + this.panel2.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private FlowLayoutPanel flowLayoutPanel1; + private Panel panel1; + private Button btnCancel; + private Button btnOk; + private CheckBox cbAutoApply; + private Panel panel2; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.cs b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.cs new file mode 100644 index 0000000..a4ded19 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.cs @@ -0,0 +1,311 @@ +using System.Reflection; +using Hawkeye.VisionBuilder.Features.ImagePreview; +using Hawkeye.VisionBuilder.Features.VisionSteps; +using Hawkeye.VisionBuilder.Panels; +using Hawkeye.VisionBuilder.Workflow; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Links; +using Hawkeye.VisionBuilder.Workflow.Operations.Attributes; +using Hawkeye.VisionBuilder.Workflow.Operations.Filters; +using Rectangle = Hawkeye.VisionBuilder.Workflow.Datatypes.Elements.Rectangle.RectangleElement; + +namespace Hawkeye.VisionBuilder.Features.ToolEditor +{ + public partial class ToolConfigurationPanel : BasePannel + { + private readonly WorkflowList _workflowList; + private readonly VisionStepsPanel _visionStepsPanel; + private BaseOperation _selectedOperation; + private Dictionary _parameters; + + public ToolConfigurationPanel(WorkflowList workflowList,VisionStepsPanel visionStepsPanel) + { + _workflowList = workflowList; + _visionStepsPanel = visionStepsPanel; + InitializeComponent(); + } + + + public void SetOperation(BaseOperation operation) + { + _selectedOperation=operation; + panel2.Enabled=false; + + //todo: generate header + + var parameters = operation.GetParameters(); + _parameters=parameters; + this.flowLayoutPanel1.Controls.Clear(); + foreach (var p in parameters) + { + GenerateParameterField(parameters,p.Key, operation); + } + + } + + private void GenerateParameterField(Dictionary toolParameter, string pKey, + BaseOperation operation) + { + + + var value = toolParameter[pKey]; + if (value is int i) + { + var control = new NumericUpDown() { Minimum = -99999999, Maximum = 99999999, Value = i, }; + control.TextChanged += (sender, args) => + { + + panel2.Enabled = true; + TriggerChanged(); + }; + control.ValueChanged += (sender, args) => + { + + toolParameter[pKey] = ((int) control.Value); + panel2.Enabled = true; + TriggerChanged(); + }; + var row = new ToolConfigurationRow(BaseOperation.MakeOperationName(pKey), control); + row.Width = flowLayoutPanel1.Width - 50; + this.flowLayoutPanel1.Controls.Add(row); + }else if (value is double d) + { + var control = new NumericUpDown() { Minimum = -99999999, Maximum = 99999999, Value = (decimal)d, Increment = (decimal)0.1, DecimalPlaces = 2}; + control.TextChanged += (sender, args) => + { + + panel2.Enabled = true; + TriggerChanged(); + }; + control.ValueChanged += (sender, args) => + { + + toolParameter[pKey] = ((double) control.Value); + panel2.Enabled = true; + TriggerChanged(); + }; + var row = new ToolConfigurationRow(BaseOperation.MakeOperationName(pKey), control); + row.Width = flowLayoutPanel1.Width - 50; + this.flowLayoutPanel1.Controls.Add(row); + }else if (value is string s) + { + var control = new TextBox() { Text = s }; + control.TextChanged += (sender, args) => + { + toolParameter[pKey] = (control.Text); + panel2.Enabled = true; + TriggerChanged(); + }; + var row = new ToolConfigurationRow(BaseOperation.MakeOperationName(pKey), control); + row.Width = flowLayoutPanel1.Width - 50; + this.flowLayoutPanel1.Controls.Add(row); + } + else if (value is FilePath filePath) + { + + var control = new Button() + { + Text = filePath.Path, + + }; + control.Click += (sender, args) => + { + OpenFileDialog dialog = new OpenFileDialog(); + dialog.Filter = filePath.Format; + dialog.FileName = filePath.Path; + if (dialog.ShowDialog() == DialogResult.OK) + { + control.Text = dialog.FileName; + filePath.Path = MakeRelative(dialog.FileName,Environment.CurrentDirectory); + control.Text = filePath.Path; + panel2.Enabled = true; + TriggerChanged(); + } + }; + var row = new ToolConfigurationRow(BaseOperation.MakeOperationName(pKey), control); + row.Width = flowLayoutPanel1.Width - 50; + this.flowLayoutPanel1.Controls.Add(row); + + } + else if (value is SelectFromList selectFromList) + { + var control = new ComboBox() + { + DropDownStyle = ComboBoxStyle.DropDownList + }; + control.Items.AddRange(selectFromList.GetList().ToArray()); + control.SelectedItem = selectFromList.Value; + control.SelectedIndexChanged += (sender, args) => + { + selectFromList.Value = (control.SelectedItem as string); + panel2.Enabled = true; + TriggerChanged(); + }; + var row = new ToolConfigurationRow(BaseOperation.MakeOperationName(pKey), control); + row.Width = flowLayoutPanel1.Width - 50; + this.flowLayoutPanel1.Controls.Add(row); + } + else if (value is ScriptValue scriptValue) + { + var control = new TextBox() { Text = scriptValue.Script }; + control.TextChanged += (sender, args) => + { + scriptValue.Script = (control.Text); + panel2.Enabled = true; + TriggerChanged(); + }; + + var row = new ToolConfigurationRow(BaseOperation.MakeOperationName(pKey), control); + row.Width = flowLayoutPanel1.Width - 50; + this.flowLayoutPanel1.Controls.Add(row); + var cc = new CodeCompletionControl(control); + cc.NeedSugestions += Cc_NeedSugestions; + } + else if (value is bool b) + { + var control = new CheckBox() { Checked = b }; + control.CheckedChanged += (sender, args) => + { + toolParameter[pKey] = (control.Checked); + panel2.Enabled = true; + TriggerChanged(); + }; + var row = new ToolConfigurationRow(BaseOperation.MakeOperationName(pKey), control); + row.Width = flowLayoutPanel1.Width - 50; + this.flowLayoutPanel1.Controls.Add(row); + } + if (value is Guid refId && pKey.StartsWith("Reference")) + { + var control = new ComboBox() + { + DropDownStyle = ComboBoxStyle.DropDownList + }; + control.SelectedIndexChanged += (sender, args) => + { + toolParameter[pKey] = ((control.SelectedItem)as BaseOperation)?.Id??NoOrigin.Instance.Id; + panel2.Enabled = true; + TriggerChanged(); + }; + var candidates = _workflowList.Operations.TakeWhile(x=> x.Id != operation.Id); + control.Items.Add(NoOrigin.Instance); + if (pKey.Contains("Edge")) + { + control.Items.AddRange(candidates.Where(x => x is IHaveEdge).Select(x => x as IHaveEdge).ToArray()); + } + else + { + control.Items.AddRange(candidates.Where(x => x is IHaveOrigin ).Select(x => x as IHaveOrigin).ToArray()); + } + + + control.DisplayMember = nameof(BaseOperation.Label); + control.SelectedItem = candidates.FirstOrDefault(x=>x.Id==refId)?? NoOrigin.Instance; + var row = new ToolConfigurationRow(BaseOperation.MakeOperationName(pKey), control); + row.Width = flowLayoutPanel1.Width - 50; + this.flowLayoutPanel1.Controls.Add(row); + } + else if(value is Action a) + { + var control = new Button() + { + Text = "Execute" + }; + + control.Click += (sender, args) => a(); + var row = new ToolConfigurationRow(BaseOperation.MakeOperationName(pKey), control); + row.Width = flowLayoutPanel1.Width - 50; + this.flowLayoutPanel1.Controls.Add(row); + + } + + + + } + + private static string MakeRelative(string path, string basePath) + { + var uri = new Uri(path); + var baseUri = new Uri(basePath); + + // if path can't be made relative + if (baseUri.Scheme != uri.Scheme) + { + return path; + } + + var res= baseUri.MakeRelativeUri(uri).ToString(); + // ensure windows path separator + return "..\\"+res.Replace("/", "\\"); + + + } + + private void Cc_NeedSugestions(string input, List suggestions) + { + + var start = input.IndexOf("."); + if (start == -1) + { + + suggestions.AddRange(_workflowList.Operations.Select(x => x.Label).Where(x=>x.StartsWith(input))); + suggestions.Add("config"); + return; + } + var word = input.Substring(0, start); + if (word == "config") + { + GenerateSugestions(input.Substring(start + 1), _workflowList.Configuration, suggestions); + return; + } + + var o = _workflowList.Operations.FirstOrDefault(x => x.Label == word); + if (o!=null) + { + GenerateSugestions(input.Substring(start + 1), o, suggestions); + } + } + + private IEnumerable GetAllowedProperties(Type type) + { + return type.GetProperties().Where(x => x.GetCustomAttribute() == null); + } + private void GenerateSugestions(string input, object o, List suggestions) + { + var start = input.IndexOf("."); + if (start == -1) + { + var props = GetAllowedProperties(o.GetType()).Where(x => x.Name.StartsWith(input)).ToList(); + props.ForEach(x => suggestions.Add(x.Name)); + } + else + { + var word = input.Substring(0, start); + var prop = GetAllowedProperties(o.GetType()).FirstOrDefault(x => x.Name == word); + if (prop != null) + { + GenerateSugestions(input.Substring(start + 1), prop.GetValue(o), suggestions); + } + } + } + + private void TriggerChanged() + { + if(cbAutoApply.Checked) + btnOk_Click(null,null); + } + + private void btnOk_Click(object sender, EventArgs e) + { + _selectedOperation.SetParameters(_parameters); + SetOperation(_selectedOperation); + _workflowList.ExecuteTill(_selectedOperation); + _workflowList.ExecuteOne(_selectedOperation); + _visionStepsPanel.UpdateView(); + } + + private void btnCancel_Click(object sender, EventArgs e) + { + SetOperation(_selectedOperation); + } + } +} diff --git a/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.resx b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationPanel.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.Designer.cs b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.Designer.cs new file mode 100644 index 0000000..4fab9a6 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.Designer.cs @@ -0,0 +1,73 @@ +namespace Hawkeye.VisionBuilder.Features.ToolEditor +{ + partial class ToolConfigurationRow + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.label1 = new System.Windows.Forms.Label(); + this.panel1 = new System.Windows.Forms.Panel(); + this.SuspendLayout(); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(8, 0); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(38, 15); + this.label1.TabIndex = 0; + this.label1.Text = "label1"; + // + // panel1 + // + this.panel1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.panel1.Location = new System.Drawing.Point(128, 0); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(419, 27); + this.panel1.TabIndex = 1; + // + // ToolConfigurationRow + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.panel1); + this.Controls.Add(this.label1); + this.Margin = new System.Windows.Forms.Padding(8); + this.Name = "ToolConfigurationRow"; + this.Size = new System.Drawing.Size(543, 27); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private Label label1; + private Panel panel1; + } +} diff --git a/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.cs b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.cs new file mode 100644 index 0000000..20550bb --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Hawkeye.VisionBuilder.Features.ToolEditor +{ + public partial class ToolConfigurationRow : UserControl + { + private readonly Control _input; + + public ToolConfigurationRow(string name,Control input) + { + _input = input; + InitializeComponent(); + label1.Text = name; + panel1.Controls.Add(input); + + } + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + _input.Width = this.Width - panel1.Left - 15; + } + } +} diff --git a/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.resx b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/ToolEditor/ToolConfigurationRow.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs new file mode 100644 index 0000000..d2d1075 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.Designer.cs @@ -0,0 +1,191 @@ +namespace Hawkeye.VisionBuilder.Features.VisionSteps +{ + partial class VisionStepsPanel + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + dataGridView1 = new DataGridView(); + panel1 = new Panel(); + lblProcessingTime = new Label(); + label1 = new Label(); + btnTest = new Button(); + button1 = new Button(); + Indicator = new DataGridViewTextBoxColumn(); + Link = new DataGridViewTextBoxColumn(); + UserName = new DataGridViewTextBoxColumn(); + Result = new DataGridViewTextBoxColumn(); + TypeName = new DataGridViewTextBoxColumn(); + Time = new DataGridViewTextBoxColumn(); + ((System.ComponentModel.ISupportInitialize)dataGridView1).BeginInit(); + panel1.SuspendLayout(); + SuspendLayout(); + // + // dataGridView1 + // + dataGridView1.AllowDrop = true; + dataGridView1.AllowUserToAddRows = false; + dataGridView1.AllowUserToDeleteRows = false; + dataGridView1.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; + dataGridView1.Columns.AddRange(new DataGridViewColumn[] { Indicator, Link, UserName, Result, TypeName, Time }); + dataGridView1.Dock = DockStyle.Fill; + dataGridView1.Location = new Point(0, 40); + dataGridView1.MultiSelect = false; + dataGridView1.Name = "dataGridView1"; + dataGridView1.ReadOnly = true; + dataGridView1.RowHeadersVisible = false; + dataGridView1.RowTemplate.Height = 25; + dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + dataGridView1.Size = new Size(540, 562); + dataGridView1.TabIndex = 0; + dataGridView1.DragDrop += dataGridView1_DragDrop; + dataGridView1.DragOver += dataGridView1_DragOver; + dataGridView1.KeyDown += dataGridView1_KeyDown; + dataGridView1.MouseDown += dataGridView1_MouseDown; + dataGridView1.MouseMove += dataGridView1_MouseMove; + dataGridView1.MouseUp += dataGridView1_MouseUp; + // + // panel1 + // + panel1.Controls.Add(lblProcessingTime); + panel1.Controls.Add(label1); + panel1.Controls.Add(btnTest); + panel1.Controls.Add(button1); + panel1.Dock = DockStyle.Top; + panel1.Location = new Point(0, 0); + panel1.Name = "panel1"; + panel1.Size = new Size(540, 40); + panel1.TabIndex = 1; + // + // lblProcessingTime + // + lblProcessingTime.AutoSize = true; + lblProcessingTime.Location = new Point(312, 16); + lblProcessingTime.Name = "lblProcessingTime"; + lblProcessingTime.Size = new Size(32, 15); + lblProcessingTime.TabIndex = 3; + lblProcessingTime.Text = "0 ms"; + // + // label1 + // + label1.AutoSize = true; + label1.Location = new Point(216, 16); + label1.Name = "label1"; + label1.Size = new Size(94, 15); + label1.TabIndex = 2; + label1.Text = "Processing time:"; + // + // btnTest + // + btnTest.Location = new Point(112, 8); + btnTest.Name = "btnTest"; + btnTest.Size = new Size(96, 23); + btnTest.TabIndex = 1; + btnTest.Text = "Test run"; + btnTest.UseVisualStyleBackColor = true; + btnTest.Click += btnTest_Click; + // + // button1 + // + button1.Location = new Point(8, 8); + button1.Name = "button1"; + button1.Size = new Size(96, 23); + button1.TabIndex = 0; + button1.Text = "&Add tool"; + button1.UseVisualStyleBackColor = true; + button1.Click += button1_Click; + // + // Indicator + // + Indicator.HeaderText = ""; + Indicator.MinimumWidth = 20; + Indicator.Name = "Indicator"; + Indicator.ReadOnly = true; + Indicator.Width = 32; + // + // Link + // + Link.HeaderText = ""; + Link.Name = "Link"; + Link.ReadOnly = true; + Link.Width = 32; + // + // UserName + // + UserName.HeaderText = "Label"; + UserName.Name = "UserName"; + UserName.ReadOnly = true; + // + // Result + // + Result.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; + Result.HeaderText = "Result"; + Result.Name = "Result"; + Result.ReadOnly = true; + // + // TypeName + // + TypeName.HeaderText = "Type"; + TypeName.Name = "TypeName"; + TypeName.ReadOnly = true; + // + // Time + // + Time.HeaderText = "Time"; + Time.Name = "Time"; + Time.ReadOnly = true; + // + // VisionStepsPanel + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(540, 602); + Controls.Add(dataGridView1); + Controls.Add(panel1); + Name = "VisionStepsPanel"; + Text = "Vision Steps"; + ((System.ComponentModel.ISupportInitialize)dataGridView1).EndInit(); + panel1.ResumeLayout(false); + panel1.PerformLayout(); + ResumeLayout(false); + } + + #endregion + + private DataGridView dataGridView1; + private Panel panel1; + private Button button1; + private Button btnTest; + private Label lblProcessingTime; + private Label label1; + private DataGridViewTextBoxColumn Indicator; + private DataGridViewTextBoxColumn Link; + private DataGridViewTextBoxColumn UserName; + private DataGridViewTextBoxColumn Result; + private DataGridViewTextBoxColumn TypeName; + private DataGridViewTextBoxColumn Time; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs new file mode 100644 index 0000000..ceb1704 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.cs @@ -0,0 +1,355 @@ +using System.Diagnostics; +using System.Drawing.Drawing2D; +using System.Windows.Forms; +using Hawkeye.VisionBuilder.Features.ImagePreview; +using Hawkeye.VisionBuilder.Panels; +using Hawkeye.VisionBuilder.Workflow; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements; +using Hawkeye.VisionBuilder.Workflow.Links; +using Rectangle = System.Drawing.Rectangle; + +namespace Hawkeye.VisionBuilder.Features.VisionSteps +{ + public partial class VisionStepsPanel : BasePannel + { + private readonly WorkflowList _workflowList; + private readonly OperationDiscoveryService _discoveryService; + private readonly ImagePreviewPanel _imagePreviewPanel; + + private BindingSource _bindingSource = new BindingSource(); + + + public VisionStepsPanel(Workflow.WorkflowList workflowList, OperationDiscoveryService discoveryService, + ImagePreview.ImagePreviewPanel imagePreviewPanel) + { + _workflowList = workflowList; + _discoveryService = discoveryService; + _imagePreviewPanel = imagePreviewPanel; + + InitializeComponent(); + dataGridView1.AutoGenerateColumns = false; + + + dataGridView1.DataSource = _bindingSource; + dataGridView1.Columns[0].DataPropertyName = nameof(BaseOperationDecorator.Result); + dataGridView1.Columns[2].DataPropertyName = nameof(BaseOperationDecorator.Label); + dataGridView1.Columns[2].ReadOnly = false; + dataGridView1.ReadOnly = false; + + dataGridView1.Columns[3].DataPropertyName = nameof(BaseOperationDecorator.ResultString); + dataGridView1.Columns[4].DataPropertyName = nameof(BaseOperationDecorator.TypeName); + dataGridView1.Columns[5].DataPropertyName = nameof(BaseOperationDecorator.ExecutionTimeString); + + dataGridView1.DefaultCellStyle.SelectionBackColor = Color.Bisque; + dataGridView1.DefaultCellStyle.SelectionForeColor = Color.Black; + dataGridView1.CellPainting += DataGridView1_CellPainting; + + + dataGridView1.SelectionChanged += DataGridView1_SelectionChanged; + + + + + RefreshWorkflow(); + + //dataGridView1.Paint += DataGridView1_Paint; + + } + + + + public void RefreshWorkflow() + { + _bindingSource.Clear(); + foreach (BaseOperation operation in _workflowList.Operations) + { + _bindingSource.Add(new BaseOperationDecorator(operation)); + } + } + + + public event Action OperationSelected = delegate { }; + + private void DataGridView1_SelectionChanged(object? sender, EventArgs e) + { + + } + + public void UpdateView() + { + _imagePreviewPanel.ClearGraphics(); + _imagePreviewPanel.SetImage(_workflowList.Context.ActiveImage); + _workflowList.Context.GraphicsElements.ForEach(_imagePreviewPanel.AddGraphics); + _imagePreviewPanel.Refresh(); + Refresh(); + } + + + private void DataGridView1_Paint(object? sender, PaintEventArgs e) + { + var g = e.Graphics; + var c1 = dataGridView1.GetCellDisplayRectangle(1, 0, true); + var c2 = dataGridView1.GetCellDisplayRectangle(1, 1, true); + + Pen start = new Pen(Color.Blue, 2); + + Pen body = new Pen(Color.Blue, 2); + + Pen end = new Pen(Color.Blue, 2); + + end.EndCap = LineCap.Custom; + + AdjustableArrowCap bigArrow = new AdjustableArrowCap(5, 5); + end.CustomEndCap = bigArrow; + + g.DrawLine(start, c1.Right, (c1.Bottom - c1.Top) / 2 + c1.Top, (c1.Right - c1.Left) / 2 + c1.Left, + (c1.Bottom - c1.Top) / 2 + c1.Top); + + g.DrawLine(body, (c1.Right - c1.Left) / 2 + c1.Left, (c1.Bottom - c1.Top) / 2 + c1.Top, + (c2.Right - c2.Left) / 2 + c1.Left, (c2.Bottom - c2.Top) / 2 + c2.Top); + + g.DrawLine(end, (c2.Right - c2.Left) / 2 + c2.Left, (c2.Bottom - c2.Top) / 2 + c2.Top, c2.Right, + (c2.Bottom - c2.Top) / 2 + c2.Top); + + + } + + + private void DataGridView1_CellPainting(object? sender, DataGridViewCellPaintingEventArgs e) + { + + if (e.ColumnIndex == 0 && e.RowIndex > -1 && e.Value != null) + { + var cx = e.CellBounds.Width / 2 + e.CellBounds.Left; + var cy = e.CellBounds.Height / 2 + e.CellBounds.Top; + var radius = e.CellBounds.Height / 2 - 3; + var c1 = dataGridView1.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, true); + e.PaintBackground(c1, true); + + if ((bool)e.Value) + { + e.Graphics.FillEllipse(Brushes.Green, cx - radius, cy - radius, radius * 2, radius * 2); + } + else + { + e.Graphics.FillEllipse(Brushes.Red, cx - radius, cy - radius, radius * 2, radius * 2); + } + + e.Handled = true; + + } + } + + private static ToolStripMenuItem EnsureCategory(ToolStripItemCollection collection, string category) + { + var name = category.Split("/", StringSplitOptions.RemoveEmptyEntries); + ToolStripMenuItem currentCategory = null; + foreach (var s in name) + { + if (currentCategory == null) + { + if (!collection.ContainsKey(s)) + { + currentCategory = new ToolStripMenuItem() + { + Name = s, + Text = s + }; + collection.Add(currentCategory); + } + else + { + currentCategory = collection[s] as ToolStripMenuItem; + } + } + else + { + if (!currentCategory.DropDownItems.ContainsKey(s)) + { + var newCategory = new ToolStripMenuItem() + { + Name = s, + Text = s + }; + currentCategory.DropDownItems.Add(newCategory); + currentCategory = newCategory; + } + else + { + currentCategory = currentCategory.DropDownItems[s] as ToolStripMenuItem; + } + } + } + + return currentCategory!; + } + + private void button1_Click(object sender, EventArgs e) + { + var factories = _discoveryService.DiscoverOperations(); + var context = new ContextMenuStrip(); + + foreach (OperationDescription desc in factories) + { + var descCategory = desc.Category; + ToolStripMenuItem currentCategory = EnsureCategory(context.Items, descCategory); + + currentCategory.DropDownItems.Add(desc.Name).Click += (o, args) => + { + var operation = _discoveryService.CreateInstance(desc.Type); + _workflowList.Operations.Add(operation); + int num = 1; + while (_workflowList.Operations.Any(x => x.Label == operation.Label + num)) num++;//generate unique name + + + operation.Label += "" + num; + _bindingSource.Add(new BaseOperationDecorator(operation)); + + dataGridView1.ClearSelection(); + dataGridView1.Rows[^1].Selected = true; + ProcessRowSelection(); + }; + } + + context.Show(button1, button1.Location); + } + + private void btnTest_Click(object sender, EventArgs e) + { + ExecuteWorkflow(); + } + public WorkflowList WorkflowList => _workflowList; + public void ExecuteWorkflow() + { + _workflowList.Context = new Context(); + var sw = Stopwatch.StartNew(); + _workflowList.Operations.ForEach(x => x.NeedsUpdate = true); + _workflowList.Execute(); + sw.Stop(); + lblProcessingTime.Text = sw.ElapsedMilliseconds.ToString() + " ms"; + _imagePreviewPanel.SetImage(_workflowList.Context.ActiveImage); + _imagePreviewPanel.ClearGraphics(); + _workflowList.Context.GraphicsElements.ForEach(_imagePreviewPanel.AddGraphics); + Refresh(); + } + + private void dataGridView1_MouseUp(object sender, MouseEventArgs e) + { + ProcessRowSelection(); + } + + + public BaseOperation? GetSelectedOperation() + { + if (dataGridView1.SelectedRows.Count == 0) return null; + var rowIndex = dataGridView1.SelectedRows[0].Index; + return (_bindingSource[rowIndex] as BaseOperationDecorator)?.Operation; + } + + public void ProcessRowSelection() + { + if (dataGridView1.SelectedRows.Count == 0) return; + var rowIndex = dataGridView1.SelectedRows[0].Index; + var x = (_bindingSource[rowIndex] as BaseOperationDecorator); + OperationSelected.Invoke(x.Operation); + _imagePreviewPanel.ClearGraphics(); + foreach (BaseOperation operation in _workflowList.Operations) + { + if (operation.NeedsUpdate) _workflowList.ExecuteOne(operation); + if (operation == x.Operation) break; + } + + UpdateView(); + } + + + + private void dataGridView1_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Delete) + { + if (dataGridView1.SelectedRows.Count == 0) return; + var rowIndex = dataGridView1.SelectedRows[0].Index; + _bindingSource.RemoveAt(rowIndex); + _workflowList.Operations.RemoveAt(rowIndex); + if (rowIndex > 0) + { + dataGridView1.ClearSelection(); + dataGridView1.Rows[rowIndex - 1].Selected = true; + ProcessRowSelection(); + } + } + } + + private Rectangle dragBoxFromMouseDown; + private int rowIndexFromMouseDown; + private int rowIndexOfItemUnderMouseToDrop; + + private void dataGridView1_MouseMove(object sender, MouseEventArgs e) + { + if ((e.Button & MouseButtons.Left) == MouseButtons.Left) + { + // If the mouse moves outside the rectangle, start the drag. + if (dragBoxFromMouseDown != Rectangle.Empty && + !dragBoxFromMouseDown.Contains(e.X, e.Y)) + { + + // Proceed with the drag and drop, passing in the list item. + DragDropEffects dropEffect = dataGridView1.DoDragDrop( + dataGridView1.Rows[rowIndexFromMouseDown], + DragDropEffects.Move); + } + } + } + + private void dataGridView1_MouseDown(object sender, MouseEventArgs e) + { + // Get the index of the item the mouse is below. + rowIndexFromMouseDown = dataGridView1.HitTest(e.X, e.Y).RowIndex; + if (rowIndexFromMouseDown != -1) + { + // Remember the point where the mouse down occurred. + // The DragSize indicates the size that the mouse can move + // before a drag event should be started. + Size dragSize = SystemInformation.DragSize; + + // Create a rectangle using the DragSize, with the mouse position being + // at the center of the rectangle. + dragBoxFromMouseDown = new Rectangle(new Point(e.X - (dragSize.Width / 2), + e.Y - (dragSize.Height / 2)), + dragSize); + } + else + // Reset the rectangle if the mouse is not over an item in the ListBox. + dragBoxFromMouseDown = Rectangle.Empty; + } + + private void dataGridView1_DragOver(object sender, DragEventArgs e) + { + e.Effect = DragDropEffects.Move; + } + + private void dataGridView1_DragDrop(object sender, DragEventArgs e) + { + // The mouse locations are relative to the screen, so they must be + // converted to client coordinates. + Point clientPoint = dataGridView1.PointToClient(new Point(e.X, e.Y)); + + // Get the row index of the item the mouse is below. + rowIndexOfItemUnderMouseToDrop = + dataGridView1.HitTest(clientPoint.X, clientPoint.Y).RowIndex; + + var movable = _workflowList.Operations[rowIndexFromMouseDown]; + _workflowList.Operations.RemoveAt(rowIndexFromMouseDown); + if (rowIndexOfItemUnderMouseToDrop == -1) rowIndexOfItemUnderMouseToDrop = _workflowList.Operations.Count; + + _workflowList.Operations.Insert(rowIndexOfItemUnderMouseToDrop, movable); + dataGridView1.ClearSelection(); + RefreshWorkflow(); + dataGridView1.Rows[rowIndexOfItemUnderMouseToDrop].Selected = true; + ProcessRowSelection(); + + } + } +} diff --git a/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx new file mode 100644 index 0000000..bb59ef4 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/VisionSteps/VisionStepsPanel.resx @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + True + + + True + + + True + + + True + + + True + + + True + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.Designer.cs b/Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.Designer.cs new file mode 100644 index 0000000..ef4852e --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.Designer.cs @@ -0,0 +1,193 @@ +namespace Hawkeye.VisionBuilder.Features.Yolo +{ + partial class AttributeSetter + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + label1 = new Label(); + tbMinWidth = new TextBox(); + tbMaxWidth = new TextBox(); + Label2 = new Label(); + tbMinHeight = new TextBox(); + label3 = new Label(); + tbMaxHeight = new TextBox(); + label4 = new Label(); + tbMinArea = new TextBox(); + label5 = new Label(); + tbMaxArea = new TextBox(); + label6 = new Label(); + button1 = new Button(); + SuspendLayout(); + // + // label1 + // + label1.AutoSize = true; + label1.Location = new Point(16, 16); + label1.Name = "label1"; + label1.Size = new Size(60, 15); + label1.TabIndex = 0; + label1.Text = "MinWidth"; + // + // tbMinWidth + // + tbMinWidth.Location = new Point(88, 16); + tbMinWidth.Name = "tbMinWidth"; + tbMinWidth.Size = new Size(100, 23); + tbMinWidth.TabIndex = 1; + // + // tbMaxWidth + // + tbMaxWidth.Location = new Point(88, 48); + tbMaxWidth.Name = "tbMaxWidth"; + tbMaxWidth.Size = new Size(100, 23); + tbMaxWidth.TabIndex = 3; + // + // Label2 + // + Label2.AutoSize = true; + Label2.Location = new Point(16, 48); + Label2.Name = "Label2"; + Label2.Size = new Size(62, 15); + Label2.TabIndex = 2; + Label2.Text = "MaxWidth"; + // + // tbMinHeight + // + tbMinHeight.Location = new Point(88, 80); + tbMinHeight.Name = "tbMinHeight"; + tbMinHeight.Size = new Size(100, 23); + tbMinHeight.TabIndex = 5; + // + // label3 + // + label3.AutoSize = true; + label3.Location = new Point(16, 80); + label3.Name = "label3"; + label3.Size = new Size(64, 15); + label3.TabIndex = 4; + label3.Text = "MinHeight"; + // + // tbMaxHeight + // + tbMaxHeight.Location = new Point(88, 112); + tbMaxHeight.Name = "tbMaxHeight"; + tbMaxHeight.Size = new Size(100, 23); + tbMaxHeight.TabIndex = 7; + // + // label4 + // + label4.AutoSize = true; + label4.Location = new Point(16, 112); + label4.Name = "label4"; + label4.Size = new Size(66, 15); + label4.TabIndex = 6; + label4.Text = "MaxHeight"; + // + // tbMinArea + // + tbMinArea.Location = new Point(88, 144); + tbMinArea.Name = "tbMinArea"; + tbMinArea.Size = new Size(100, 23); + tbMinArea.TabIndex = 9; + // + // label5 + // + label5.AutoSize = true; + label5.Location = new Point(16, 144); + label5.Name = "label5"; + label5.Size = new Size(52, 15); + label5.TabIndex = 8; + label5.Text = "MinArea"; + // + // tbMaxArea + // + tbMaxArea.Location = new Point(88, 176); + tbMaxArea.Name = "tbMaxArea"; + tbMaxArea.Size = new Size(100, 23); + tbMaxArea.TabIndex = 11; + // + // label6 + // + label6.AutoSize = true; + label6.Location = new Point(16, 176); + label6.Name = "label6"; + label6.Size = new Size(54, 15); + label6.TabIndex = 10; + label6.Text = "MaxArea"; + // + // button1 + // + button1.Location = new Point(64, 208); + button1.Name = "button1"; + button1.Size = new Size(75, 23); + button1.TabIndex = 12; + button1.Text = "Apply"; + button1.UseVisualStyleBackColor = true; + button1.Click += button1_Click; + // + // AttributeSetter + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(216, 247); + Controls.Add(button1); + Controls.Add(tbMaxArea); + Controls.Add(label6); + Controls.Add(tbMinArea); + Controls.Add(label5); + Controls.Add(tbMaxHeight); + Controls.Add(label4); + Controls.Add(tbMinHeight); + Controls.Add(label3); + Controls.Add(tbMaxWidth); + Controls.Add(Label2); + Controls.Add(tbMinWidth); + Controls.Add(label1); + FormBorderStyle = FormBorderStyle.FixedToolWindow; + Name = "AttributeSetter"; + Text = "AttributeSetter"; + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Label label1; + private TextBox tbMinWidth; + private TextBox tbMaxWidth; + private Label Label2; + private TextBox tbMinHeight; + private Label label3; + private TextBox tbMaxHeight; + private Label label4; + private TextBox tbMinArea; + private Label label5; + private TextBox tbMaxArea; + private Label label6; + private Button button1; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.cs b/Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.cs new file mode 100644 index 0000000..f0ed867 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using Hawkeye.VisionBuilder.Workflow; + +namespace Hawkeye.VisionBuilder.Features.Yolo +{ + public partial class AttributeSetter : Form + { + private readonly WorkflowList _list; + + public AttributeSetter(WorkflowList list) + { + _list = list; + InitializeComponent(); + } + + void TrySet(TextBox textBox, Action setter) + { + if (int.TryParse(textBox.Text, out var v)) + { + setter(v); + } + } + + + private void button1_Click(object sender, EventArgs e) + { + foreach (var operation in _list.Operations) + { + if (operation is Hawkeye.VisionBuilder.Workflow.Operations.AI.YoloPickDetected yolo) + { + TrySet(tbMinWidth, v => yolo.MinWidth = v); + TrySet(tbMinHeight, v => yolo.MinHeight = v); + TrySet(tbMaxWidth, v => yolo.MaxWidth = v); + TrySet(tbMaxHeight, v => yolo.MaxHeight = v); + TrySet(tbMinArea, v => yolo.MinArea = v); + TrySet(tbMaxArea, v => yolo.MaxArea = v); + } + } + DialogResult = DialogResult.OK; + } + } +} diff --git a/Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.resx b/Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.resx new file mode 100644 index 0000000..af32865 --- /dev/null +++ b/Hawkeye.VisionBuilder/Features/Yolo/AttributeSetter.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Hawkeye.VisionBuilder.csproj b/Hawkeye.VisionBuilder/Hawkeye.VisionBuilder.csproj new file mode 100644 index 0000000..8cc3630 --- /dev/null +++ b/Hawkeye.VisionBuilder/Hawkeye.VisionBuilder.csproj @@ -0,0 +1,56 @@ + + + + net8.0-windows + enable + true + enable + Debug;Release;CPU + WinExe + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resource1.resx + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resource1.Designer.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Localization/ILocalizer.cs b/Hawkeye.VisionBuilder/Localization/ILocalizer.cs new file mode 100644 index 0000000..3383093 --- /dev/null +++ b/Hawkeye.VisionBuilder/Localization/ILocalizer.cs @@ -0,0 +1,6 @@ +namespace Hawkeye.VisionBuilder.Localization; + +public interface ILocalizer +{ + string Localize(string text); +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Localization/NoLocalization.cs b/Hawkeye.VisionBuilder/Localization/NoLocalization.cs new file mode 100644 index 0000000..cd71d95 --- /dev/null +++ b/Hawkeye.VisionBuilder/Localization/NoLocalization.cs @@ -0,0 +1,9 @@ +namespace Hawkeye.VisionBuilder.Localization; + +public class NoLocalization:ILocalizer +{ + public string Localize(string text) + { + return text; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/MainWindow.Designer.cs b/Hawkeye.VisionBuilder/MainWindow.Designer.cs new file mode 100644 index 0000000..494740a --- /dev/null +++ b/Hawkeye.VisionBuilder/MainWindow.Designer.cs @@ -0,0 +1,248 @@ +namespace Hawkeye.VisionBuilder +{ + partial class MainWindow + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + toolStrip1 = new ToolStrip(); + toolStripButton1 = new ToolStripButton(); + toolBtnConfiguration = new ToolStripButton(); + menuStrip1 = new MenuStrip(); + fileToolStripMenuItem = new ToolStripMenuItem(); + loadRecipeToolStripMenuItem = new ToolStripMenuItem(); + saveRecipeToolStripMenuItem = new ToolStripMenuItem(); + editToolStripMenuItem = new ToolStripMenuItem(); + copyPathToolStripMenuItem = new ToolStripMenuItem(); + copyOnStopToolStripMenuItem = new ToolStripMenuItem(); + saveAsToolStripMenuItem = new ToolStripMenuItem(); + toolsToolStripMenuItem = new ToolStripMenuItem(); + generateFromColorProfileToolStripMenuItem = new ToolStripMenuItem(); + colorProfilesToolStripMenuItem = new ToolStripMenuItem(); + updateAllToolStripMenuItem = new ToolStripMenuItem(); + yoloToolStripMenuItem = new ToolStripMenuItem(); + generateFromModelToolStripMenuItem = new ToolStripMenuItem(); + massSetAttributeToolStripMenuItem = new ToolStripMenuItem(); + windowsToolStripMenuItem = new ToolStripMenuItem(); + imageStatisticsToolStripMenuItem = new ToolStripMenuItem(); + toolStrip1.SuspendLayout(); + menuStrip1.SuspendLayout(); + SuspendLayout(); + // + // toolStrip1 + // + toolStrip1.Items.AddRange(new ToolStripItem[] { toolStripButton1, toolBtnConfiguration }); + toolStrip1.Location = new Point(0, 24); + toolStrip1.Name = "toolStrip1"; + toolStrip1.Size = new Size(1461, 25); + toolStrip1.TabIndex = 1; + toolStrip1.Text = "toolStrip1"; + // + // toolStripButton1 + // + toolStripButton1.Image = Resources.grid; + toolStripButton1.ImageTransparentColor = Color.Magenta; + toolStripButton1.Name = "toolStripButton1"; + toolStripButton1.Size = new Size(124, 22); + toolStripButton1.Text = "Organize windows"; + toolStripButton1.Click += OrganizeWindows; + // + // toolBtnConfiguration + // + toolBtnConfiguration.Image = Resources.diaphragm; + toolBtnConfiguration.ImageTransparentColor = Color.Magenta; + toolBtnConfiguration.Name = "toolBtnConfiguration"; + toolBtnConfiguration.Size = new Size(101, 22); + toolBtnConfiguration.Text = "Configuration"; + toolBtnConfiguration.Click += toolBtnConfiguration_Click; + // + // menuStrip1 + // + menuStrip1.Items.AddRange(new ToolStripItem[] { fileToolStripMenuItem, editToolStripMenuItem, toolsToolStripMenuItem, windowsToolStripMenuItem }); + menuStrip1.Location = new Point(0, 0); + menuStrip1.Name = "menuStrip1"; + menuStrip1.Size = new Size(1461, 24); + menuStrip1.TabIndex = 3; + menuStrip1.Text = "menuStrip1"; + // + // fileToolStripMenuItem + // + fileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { loadRecipeToolStripMenuItem, saveRecipeToolStripMenuItem }); + fileToolStripMenuItem.Name = "fileToolStripMenuItem"; + fileToolStripMenuItem.Size = new Size(37, 20); + fileToolStripMenuItem.Text = "File"; + // + // loadRecipeToolStripMenuItem + // + loadRecipeToolStripMenuItem.Name = "loadRecipeToolStripMenuItem"; + loadRecipeToolStripMenuItem.Size = new Size(135, 22); + loadRecipeToolStripMenuItem.Text = "Load recipe"; + loadRecipeToolStripMenuItem.Click += loadRecipeToolStripMenuItem_Click; + // + // saveRecipeToolStripMenuItem + // + saveRecipeToolStripMenuItem.Name = "saveRecipeToolStripMenuItem"; + saveRecipeToolStripMenuItem.Size = new Size(135, 22); + saveRecipeToolStripMenuItem.Text = "Save recipe"; + saveRecipeToolStripMenuItem.Click += saveRecipeToolStripMenuItem_Click; + // + // editToolStripMenuItem + // + editToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { copyPathToolStripMenuItem, copyOnStopToolStripMenuItem, saveAsToolStripMenuItem }); + editToolStripMenuItem.Name = "editToolStripMenuItem"; + editToolStripMenuItem.Size = new Size(39, 20); + editToolStripMenuItem.Text = "Edit"; + // + // copyPathToolStripMenuItem + // + copyPathToolStripMenuItem.Name = "copyPathToolStripMenuItem"; + copyPathToolStripMenuItem.Size = new Size(196, 22); + copyPathToolStripMenuItem.Text = "Copy path"; + copyPathToolStripMenuItem.Click += copyPathToolStripMenuItem_Click; + // + // copyOnStopToolStripMenuItem + // + copyOnStopToolStripMenuItem.CheckOnClick = true; + copyOnStopToolStripMenuItem.Name = "copyOnStopToolStripMenuItem"; + copyOnStopToolStripMenuItem.Size = new Size(196, 22); + copyOnStopToolStripMenuItem.Text = "Copy on stop"; + copyOnStopToolStripMenuItem.Click += copyOnErrorToolStripMenuItem_Click; + // + // saveAsToolStripMenuItem + // + saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem"; + saveAsToolStripMenuItem.ShortcutKeys = Keys.Control | Keys.Shift | Keys.S; + saveAsToolStripMenuItem.Size = new Size(196, 22); + saveAsToolStripMenuItem.Text = "Save as ..."; + saveAsToolStripMenuItem.Click += saveAsToolStripMenuItem_Click; + // + // toolsToolStripMenuItem + // + toolsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { generateFromColorProfileToolStripMenuItem, colorProfilesToolStripMenuItem, yoloToolStripMenuItem }); + toolsToolStripMenuItem.Name = "toolsToolStripMenuItem"; + toolsToolStripMenuItem.Size = new Size(46, 20); + toolsToolStripMenuItem.Text = "Tools"; + // + // generateFromColorProfileToolStripMenuItem + // + generateFromColorProfileToolStripMenuItem.Name = "generateFromColorProfileToolStripMenuItem"; + generateFromColorProfileToolStripMenuItem.Size = new Size(217, 22); + generateFromColorProfileToolStripMenuItem.Text = "Generate from color profile"; + generateFromColorProfileToolStripMenuItem.Click += generateFromColorProfileToolStripMenuItem_Click; + // + // colorProfilesToolStripMenuItem + // + colorProfilesToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { updateAllToolStripMenuItem }); + colorProfilesToolStripMenuItem.Name = "colorProfilesToolStripMenuItem"; + colorProfilesToolStripMenuItem.Size = new Size(217, 22); + colorProfilesToolStripMenuItem.Text = "Color profiles"; + // + // updateAllToolStripMenuItem + // + updateAllToolStripMenuItem.Name = "updateAllToolStripMenuItem"; + updateAllToolStripMenuItem.Size = new Size(127, 22); + updateAllToolStripMenuItem.Text = "Update all"; + updateAllToolStripMenuItem.Click += updateAllToolStripMenuItem_Click; + // + // yoloToolStripMenuItem + // + yoloToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { generateFromModelToolStripMenuItem, massSetAttributeToolStripMenuItem }); + yoloToolStripMenuItem.Name = "yoloToolStripMenuItem"; + yoloToolStripMenuItem.Size = new Size(217, 22); + yoloToolStripMenuItem.Text = "Yolo"; + // + // generateFromModelToolStripMenuItem + // + generateFromModelToolStripMenuItem.Name = "generateFromModelToolStripMenuItem"; + generateFromModelToolStripMenuItem.Size = new Size(187, 22); + generateFromModelToolStripMenuItem.Text = "Generate from model"; + generateFromModelToolStripMenuItem.Click += generateFromYoloToolStripMenuItem_Click; + // + // massSetAttributeToolStripMenuItem + // + massSetAttributeToolStripMenuItem.Name = "massSetAttributeToolStripMenuItem"; + massSetAttributeToolStripMenuItem.Size = new Size(187, 22); + massSetAttributeToolStripMenuItem.Text = "Mass set attribute"; + massSetAttributeToolStripMenuItem.Click += massSetAttributeToolStripMenuItem_Click; + // + // windowsToolStripMenuItem + // + windowsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { imageStatisticsToolStripMenuItem }); + windowsToolStripMenuItem.Name = "windowsToolStripMenuItem"; + windowsToolStripMenuItem.Size = new Size(68, 20); + windowsToolStripMenuItem.Text = "Windows"; + // + // imageStatisticsToolStripMenuItem + // + imageStatisticsToolStripMenuItem.Name = "imageStatisticsToolStripMenuItem"; + imageStatisticsToolStripMenuItem.Size = new Size(180, 22); + imageStatisticsToolStripMenuItem.Text = "Image statistics"; + imageStatisticsToolStripMenuItem.Click += imageStatisticsToolStripMenuItem_Click; + // + // MainWindow + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1461, 768); + Controls.Add(toolStrip1); + Controls.Add(menuStrip1); + IsMdiContainer = true; + MainMenuStrip = menuStrip1; + Name = "MainWindow"; + StartPosition = FormStartPosition.CenterScreen; + Text = "Hawkeye Vision Builder"; + toolStrip1.ResumeLayout(false); + toolStrip1.PerformLayout(); + menuStrip1.ResumeLayout(false); + menuStrip1.PerformLayout(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private ToolStrip toolStrip1; + private ToolStripButton toolStripButton1; + private MenuStrip menuStrip1; + private ToolStripMenuItem fileToolStripMenuItem; + private ToolStripMenuItem loadRecipeToolStripMenuItem; + private ToolStripMenuItem saveRecipeToolStripMenuItem; + private ToolStripButton toolBtnConfiguration; + private ToolStripMenuItem toolsToolStripMenuItem; + private ToolStripMenuItem generateFromColorProfileToolStripMenuItem; + private ToolStripMenuItem colorProfilesToolStripMenuItem; + private ToolStripMenuItem updateAllToolStripMenuItem; + private ToolStripMenuItem editToolStripMenuItem; + private ToolStripMenuItem copyPathToolStripMenuItem; + private ToolStripMenuItem copyOnStopToolStripMenuItem; + private ToolStripMenuItem yoloToolStripMenuItem; + private ToolStripMenuItem generateFromModelToolStripMenuItem; + private ToolStripMenuItem massSetAttributeToolStripMenuItem; + private ToolStripMenuItem saveAsToolStripMenuItem; + private ToolStripMenuItem windowsToolStripMenuItem; + private ToolStripMenuItem imageStatisticsToolStripMenuItem; + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/MainWindow.cs b/Hawkeye.VisionBuilder/MainWindow.cs new file mode 100644 index 0000000..2810e36 --- /dev/null +++ b/Hawkeye.VisionBuilder/MainWindow.cs @@ -0,0 +1,418 @@ +using Compunet.YoloV8; +using Hawkeye.VisionBuilder.Features.ColorProgileGenerator; +using Hawkeye.VisionBuilder.Features.ImagePreview; +using Hawkeye.VisionBuilder.Features.ImagesStrip; +using Hawkeye.VisionBuilder.Features.ToolEditor; +using Hawkeye.VisionBuilder.Features.VisionSteps; +using Hawkeye.VisionBuilder.Features.Yolo; +using Hawkeye.VisionBuilder.Panels; +using Hawkeye.VisionBuilder.Workflow; +using Hawkeye.VisionBuilder.Workflow.ColorStudioClasses; +using Hawkeye.VisionBuilder.Workflow.Configuration; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using Hawkeye.VisionBuilder.Workflow.Operations.AI; +using Hawkeye.VisionBuilder.Workflow.Operations.Filters; +using Hawkeye.VisionBuilder.Workflow.Operations.Memory; + +using Inspectron.Settings.WindowsWizard; +using Inspectron.Settings.WindowsWizard.Controls; +using Microsoft.WindowsAPICodePack.Dialogs; +using OpenCvSharp; +using OpenCvSharp.Extensions; +using System.Windows.Forms; +using Hawkeye.VisionBuilder.Features.ImageStatistics; +using Size = OpenCvSharp.Size; + +namespace Hawkeye.VisionBuilder +{ + public partial class MainWindow : Form + { + private readonly WorkflowList _workflowList; + private readonly OperationDiscoveryService _discoveryService; + private readonly ToolConfigurationPanel _toolConfigurationPanel; + private readonly ImagePreviewPanel _previewPanel; + private readonly VisionStepsPanel _visionStepsPanel; + private ImageStripPanel _imagesStripPanel; + public MainWindow(WorkflowList workflowList, OperationDiscoveryService discoveryService) + { + _workflowList = workflowList; + _discoveryService = discoveryService; + InitializeComponent(); + + + { + _previewPanel = new ImagePreviewPanel(); + _previewPanel.MdiParent = this; + _previewPanel.Show(); + } + { + _visionStepsPanel = new VisionStepsPanel(workflowList, discoveryService, _previewPanel); + _visionStepsPanel.MdiParent = this; + _visionStepsPanel.Show(); + } + { + _toolConfigurationPanel = new ToolConfigurationPanel(workflowList, _visionStepsPanel); + _toolConfigurationPanel.MdiParent = this; + _toolConfigurationPanel.Show(); + } + { + ReinitCameraPanel(workflowList); + } + _visionStepsPanel.OperationSelected += operation => + { + _toolConfigurationPanel.SetOperation(operation); + + }; + + //var codeEditor = new ScriptEditorPanel(); + //codeEditor.MdiParent = this; + //codeEditor.Show(); + + + } + + private void ReinitCameraPanel(WorkflowList workflowList) + { + if (_imagesStripPanel != null) + { + _imagesStripPanel.Close(); + _imagesStripPanel.Dispose(); + _imagesStripPanel = null; + } + + + + + _imagesStripPanel = new ImageStripPanel(); + + _workflowList.ImageSource= _imagesStripPanel; + + _imagesStripPanel.ImageSelected += data => + { + if (_imagesStripPanel is ImageStripPanel imagesStripPanel) + { + if (imagesStripPanel.IsExecutingWorkflow) + { + _visionStepsPanel.ExecuteWorkflow(); + var result = _visionStepsPanel.WorkflowList.Operations.All(x => x.Result == true); + ProcessResult(result); + (_imagesStripPanel as ImageStripPanel).InformImageProcessed(result); + return; + } + } + + _visionStepsPanel.ProcessRowSelection(); + var selectedOperation = _visionStepsPanel.GetSelectedOperation(); + if (selectedOperation != null) + { + ProcessResult(selectedOperation.Result); + (_imagesStripPanel as ImageStripPanel).InformImageProcessed(selectedOperation.Result); + } + + }; + if(Directory.Exists(_workflowList.Configuration.EmulationPath)) + _imagesStripPanel.LoadImages(_workflowList.Configuration.EmulationPath); + + _imagesStripPanel.MdiParent = this; + _imagesStripPanel.Show(); + } + + string _saveErrorDirectory = null; + void ProcessResult(bool result) + { + if (_statisticsDialog != null) + { + if (result) + { + _statisticsDialog.AddGoodImage((_imagesStripPanel as ImageStripPanel).GetImagePath()); + } + else + { + _statisticsDialog.AddBadImage((_imagesStripPanel as ImageStripPanel).GetImagePath(), _visionStepsPanel.WorkflowList.Operations.Where(x=>x.Result==false).Select(x=>x.Label).ToArray()); + } + } + + if (!copyOnStopToolStripMenuItem.Checked) return; + var shouldSaveOnError = (_imagesStripPanel as ImageStripPanel).StopOnErrors; + var shouldSaveOnNoError = (_imagesStripPanel as ImageStripPanel).StopOnNoErrors; + if ((!result && shouldSaveOnError) || (result && shouldSaveOnNoError)) + { + var resultFile = Path.Combine(_saveErrorDirectory, Path.GetFileName((_imagesStripPanel as ImageStripPanel).GetImagePath())); + if (!File.Exists(resultFile)) + { + File.Copy((_imagesStripPanel as ImageStripPanel).GetImagePath(), Path.Combine(_saveErrorDirectory, Path.GetFileName((_imagesStripPanel as ImageStripPanel).GetImagePath()))); + } + + } + + + } + + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + OrganizeWindows(null, null); + } + + private void OrganizeWindows(object sender, EventArgs e) + { + _previewPanel.Left = 0; + _previewPanel.Top = 0; + _previewPanel.Width = this.ClientSize.Width / 2; + _previewPanel.Height = (this.ClientSize.Height - 40) - 30 - _imagesStripPanel.Height; + + _imagesStripPanel.Top = _previewPanel.Height; + _imagesStripPanel.Left = 0; + _imagesStripPanel.Width = _previewPanel.Width; + + + _toolConfigurationPanel.Top = 0; + _toolConfigurationPanel.Left = this.ClientSize.Width / 2; + _toolConfigurationPanel.Width = this.ClientSize.Width / 2 - 10; + _toolConfigurationPanel.Height = (this.ClientSize.Height - 40) / 2; + + _visionStepsPanel.Top = (this.ClientSize.Height - 40) / 2; + _visionStepsPanel.Left = this.ClientSize.Width / 2; + _visionStepsPanel.Width = this.ClientSize.Width / 2 - 10; + _visionStepsPanel.Height = (this.ClientSize.Height - 40) / 2 - 30; + + } + + private void loadRecipeToolStripMenuItem_Click(object sender, EventArgs e) + { + OpenFileDialog dialog = new OpenFileDialog(); + dialog.Filter = "*.hrcp|*.hrcp"; + PrepareDialog(dialog); + + if (dialog.ShowDialog() == DialogResult.OK) + { + var path = dialog.FileName; + LoadRecipe(path); + } + + } + + private void PrepareDialog(FileDialog dialog) + { + if (_loadedPath != null) + { + dialog.InitialDirectory = Path.GetDirectoryName(Path.GetFullPath(_loadedPath)); + dialog.FileName = Path.GetFileNameWithoutExtension(_loadedPath); + } + else + { + Directory.CreateDirectory(Path.GetFullPath(@"..\Data\Recipes")); + dialog.InitialDirectory = Path.GetFullPath(@"..\Data\Recipes"); + } + } + + private string _loadedPath = null; + + + public void LoadRecipe(string path) + { + _loadedPath = path; + var bytes = File.ReadAllBytes(path); + MemoryStream stream = new MemoryStream(bytes); + BinaryReader reader = new BinaryReader(stream); + + + _workflowList.PythonMissing += _workflowList_PythonMissing; + _workflowList.Load(reader, _discoveryService); + ReinitCameraPanel(_workflowList); + OrganizeWindows(null, null); + _visionStepsPanel.RefreshWorkflow(); + } + + private void _workflowList_PythonMissing() + { + SetupConfig(); + + } + + private void saveRecipeToolStripMenuItem_Click(object sender, EventArgs e) + { + SaveFileDialog dialog = new SaveFileDialog(); + dialog.Filter = "*.hrcp|*.hrcp"; + PrepareDialog(dialog); + if (dialog.ShowDialog() == DialogResult.OK) + { + MemoryStream ms = new MemoryStream(); + BinaryWriter bw = new BinaryWriter(ms); + + _workflowList.RecipeImage = _workflowList.Context.LastCameraImage.ImageData; + _workflowList.Save(bw); + ms.Close(); + File.WriteAllBytes(dialog.FileName, ms.ToArray()); + } + } + + private void toolBtnConfiguration_Click(object sender, EventArgs e) + { + SetupConfig(); + ReinitCameraPanel(_workflowList); + OrganizeWindows(null, null); + } + + private void SetupConfig() + { + SetupPage.Begin("Camera", "Configuration") + .AddItem(_workflowList.Configuration, nameof(_workflowList.Configuration.RuntimeCameraType)) + .AddItem(_workflowList.Configuration, + nameof(_workflowList.Configuration.DevelopmentCameraType)) + .AddItem(_workflowList.Configuration, nameof(_workflowList.Configuration.EmulationPath), + editor => editor.FolderPick = true) + .Next("Recipe selection") + .AddItem(_workflowList.Configuration.RecipeSelectionConfig, nameof(RecipeSelectionConfig.SerialEnabled)) + .AddItem(_workflowList.Configuration.RecipeSelectionConfig, nameof(RecipeSelectionConfig.ComPort)) + .Next("Outputs") + .AddItem(_workflowList.Configuration, nameof(_workflowList.Configuration.Outputs)) + .AddItem(_workflowList.Configuration, nameof(_workflowList.Configuration.ResultPin)) + .AddItem(_workflowList.Configuration, nameof(_workflowList.Configuration.SerialPort)) + .AddItem(_workflowList.Configuration, nameof(_workflowList.Configuration.Delay), editor => editor.Max = 1000) + .Next("Python") + .AddItem(_workflowList.Configuration, nameof(_workflowList.Configuration.PythonPath), + editor => editor.FolderPick = false) + .Next("Bad tweak") + .AddItem(_workflowList.Configuration.BadTweakLearning, nameof(BadTweakLearning.Enabled)) + .AddItem(_workflowList.Configuration.BadTweakLearning, nameof(BadTweakLearning.ThresholdLevel), + editor => + { + editor.Min = 0; + editor.Max = 255; + }) + .Next("Statistics") + .ShowSetupDialog(); + } + + private void generateFromColorProfileToolStripMenuItem_Click(object sender, EventArgs e) + { + var tool = new ColorProfileSelection(_workflowList); + tool.ShowDialog(); + _visionStepsPanel.RefreshWorkflow(); + } + + private void updateAllToolStripMenuItem_Click(object sender, EventArgs e) + { + var dialog = new OpenFileDialog(); + dialog.Filter = "*.colorprofile|*.colorprofile"; + if (dialog.ShowDialog() == DialogResult.OK) + { + var path = dialog.FileName; + var operations = _workflowList.Operations; + foreach (var operation in operations) + { + if (operation is ColorProfileOperation colorProfileOperation) + { + colorProfileOperation.ColorProfilePath.Path = path; + } + } + } + + } + + private void copyPathToolStripMenuItem_Click(object sender, EventArgs e) + { + if (_imagesStripPanel is not ImageStripPanel panel) return; + var imagePath = panel.GetImagePath(); + if (imagePath == null) return; + Clipboard.SetText(imagePath); + } + + private void copyOnErrorToolStripMenuItem_Click(object sender, EventArgs e) + { + FolderBrowserDialog dialog = new FolderBrowserDialog(); + if (dialog.ShowDialog() == DialogResult.OK) + { + _saveErrorDirectory = dialog.SelectedPath; + return; + } + copyOnStopToolStripMenuItem.Checked = false; + } + + private void generateFromYoloToolStripMenuItem_Click(object sender, EventArgs e) + { + // pick yolo file + OpenFileDialog dialog = new OpenFileDialog(); + dialog.Filter = "*.onnx|*.onnx"; + dialog.InitialDirectory = Path.GetFullPath(@"..\Data\Models"); + if (dialog.ShowDialog() == DialogResult.OK) + { + var path = dialog.FileName; + var predictor = YoloV8Predictor.Create(path); + var classes = predictor.Metadata.Names; + foreach (var cls in classes) + { + { + var operation = new YoloPickDetected(); + operation.Label = cls.Name; + operation.TypeId = cls.Id + 1; + _workflowList.Operations.Add(operation); + } + } + _visionStepsPanel.RefreshWorkflow(); + } + } + + private void massSetAttributeToolStripMenuItem_Click(object sender, EventArgs e) + { + AttributeSetter setter = new AttributeSetter(_workflowList); + if (setter.ShowDialog() == DialogResult.OK) + { + _visionStepsPanel.RefreshWorkflow(); + } + } + + private string? _saveAsPath = null; + + private void saveAsToolStripMenuItem_Click(object sender, EventArgs e) + { + if (_imagesStripPanel is not ImageStripPanel panel) return; + + var path = GetSaveAsPath(); + if (path == null) return; + var imagePath = panel.GetImagePath(); + if (imagePath == null) return; + var bytes = File.ReadAllBytes(imagePath); + File.WriteAllBytes(Path.Combine(path, Path.GetFileName(imagePath)), bytes); + } + + string GetSaveAsPath() + { + if (_saveAsPath == null) + { + CommonOpenFileDialog dialog = new CommonOpenFileDialog(); + dialog.IsFolderPicker = true; + if (dialog.ShowDialog() == CommonFileDialogResult.Ok) + { + _saveAsPath = dialog.FileName; + } + } + return _saveAsPath; + } + + ImageStatisticsDialog _statisticsDialog = null; + private void imageStatisticsToolStripMenuItem_Click(object sender, EventArgs e) + { + if (_statisticsDialog!=null) + { + return; + } + + _statisticsDialog = new ImageStatisticsDialog(); + _statisticsDialog.Show(this); + _statisticsDialog.FormClosed += + (s, args) => + { + _statisticsDialog = null; + }; + _statisticsDialog.FileSelected += _statisticsDialog_FileSelected; + } + + private void _statisticsDialog_FileSelected(string file) + { + (_imagesStripPanel as ImageStripPanel).SelectImage(file); + } + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/MainWindow.resx b/Hawkeye.VisionBuilder/MainWindow.resx new file mode 100644 index 0000000..9e94bde --- /dev/null +++ b/Hawkeye.VisionBuilder/MainWindow.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 122, 17 + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Panels/BasePannel.Designer.cs b/Hawkeye.VisionBuilder/Panels/BasePannel.Designer.cs new file mode 100644 index 0000000..3b5308f --- /dev/null +++ b/Hawkeye.VisionBuilder/Panels/BasePannel.Designer.cs @@ -0,0 +1,49 @@ +namespace Hawkeye.VisionBuilder.Panels +{ + partial class BasePannel + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.SuspendLayout(); + // + // BasePannel + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "BasePannel"; + this.Text = "BasePannel"; + this.ResumeLayout(false); + + } + + #endregion + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Panels/BasePannel.cs b/Hawkeye.VisionBuilder/Panels/BasePannel.cs new file mode 100644 index 0000000..dbd57d4 --- /dev/null +++ b/Hawkeye.VisionBuilder/Panels/BasePannel.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Hawkeye.VisionBuilder.Panels +{ + public partial class BasePannel : Form + { + public BasePannel() + { + InitializeComponent(); + } + } +} diff --git a/Hawkeye.VisionBuilder/Panels/BasePannel.resx b/Hawkeye.VisionBuilder/Panels/BasePannel.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/Hawkeye.VisionBuilder/Panels/BasePannel.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Panels/IInitializable.cs b/Hawkeye.VisionBuilder/Panels/IInitializable.cs new file mode 100644 index 0000000..e99745b --- /dev/null +++ b/Hawkeye.VisionBuilder/Panels/IInitializable.cs @@ -0,0 +1,9 @@ +using Hawkeye.VisionBuilder.Localization; + +namespace Hawkeye.VisionBuilder; + +public interface IInitializable +{ + void Initialize(ILocalizer localizer); + +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Program.cs b/Hawkeye.VisionBuilder/Program.cs new file mode 100644 index 0000000..27f1a6e --- /dev/null +++ b/Hawkeye.VisionBuilder/Program.cs @@ -0,0 +1,43 @@ +using System.Globalization; +using Hawkeye.VisionBuilder.Workflow; +using Hawkeye.VisionBuilder.Workflow.Operations; +using Ninject; + +namespace Hawkeye.VisionBuilder +{ + internal static class Program + { + + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main(string[] args) + { + ApplicationConfiguration.Initialize(); + WinConsole.Initialize(); + ThreadPool.SetMinThreads(64, 4); + + + Thread.CurrentThread.CurrentCulture=new CultureInfo("en-US"); + Thread.CurrentThread.CurrentUICulture=new CultureInfo("en-US"); + + + + StandardKernel kernel = new StandardKernel(); + + + + WorkflowList workflow = new WorkflowList(); + OperationDiscoveryService service = new OperationDiscoveryService(workflow); + workflow.Operations.Add(new GetImageOperation(workflow)); + + kernel.Bind().ToConstant(service); + kernel.Bind().ToConstant(workflow); + + + Application.Run(kernel.Get()); + } + + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/Resources.Designer.cs b/Hawkeye.VisionBuilder/Resources.Designer.cs new file mode 100644 index 0000000..780e062 --- /dev/null +++ b/Hawkeye.VisionBuilder/Resources.Designer.cs @@ -0,0 +1,83 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Hawkeye.VisionBuilder { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Hawkeye.VisionBuilder.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap diaphragm { + get { + object obj = ResourceManager.GetObject("diaphragm", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap grid { + get { + object obj = ResourceManager.GetObject("grid", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/Hawkeye.VisionBuilder/Resources.resx b/Hawkeye.VisionBuilder/Resources.resx new file mode 100644 index 0000000..6cd8b1a --- /dev/null +++ b/Hawkeye.VisionBuilder/Resources.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + icons\diaphragm.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + icons\grid.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/UIEventHandler.cs b/Hawkeye.VisionBuilder/UIEventHandler.cs new file mode 100644 index 0000000..af6e465 --- /dev/null +++ b/Hawkeye.VisionBuilder/UIEventHandler.cs @@ -0,0 +1,6 @@ +using System; + +namespace Hawkeye.VisionBuilder; + +public delegate void UIEventHandler(TControl? sender, TArgs e) + where TControl : Control where TArgs : EventArgs; diff --git a/Hawkeye.VisionBuilder/WinConsole.cs b/Hawkeye.VisionBuilder/WinConsole.cs new file mode 100644 index 0000000..81e95b7 --- /dev/null +++ b/Hawkeye.VisionBuilder/WinConsole.cs @@ -0,0 +1,118 @@ +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Hawkeye.VisionBuilder +{ + public static class WinConsole + { + public static void Initialize(bool alwaysCreateNewConsole = true, bool minimizeWindow = false) + { + bool consoleAttached = true; + if (alwaysCreateNewConsole + || (AttachConsole(ATTACH_PARRENT) == 0 + && Marshal.GetLastWin32Error() != ERROR_ACCESS_DENIED)) + { + consoleAttached = AllocConsole() != 0; + } + + if (consoleAttached) + { + InitializeOutStream(); + InitializeInStream(); + } + + if (minimizeWindow) + { + const int SW_MINIMIZE = 6; + var handle = GetConsoleWindow(); + ShowWindow(handle, SW_MINIMIZE); + } + } + + public static void MinimizeWindow() + { + const int SW_MINIMIZE = 6; + var handle = GetConsoleWindow(); + ShowWindow(handle, SW_MINIMIZE); + } + + + [DllImport("user32.dll")] + private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + [DllImport("kernel32.dll")] + private static extern IntPtr GetConsoleWindow(); + private static void InitializeOutStream() + { + var fs = CreateFileStream("CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE, FileAccess.Write); + if (fs != null) + { + var writer = new StreamWriter(fs) { AutoFlush = true }; + Console.SetOut(writer); + Console.SetError(writer); + } + } + + private static void InitializeInStream() + { + var fs = CreateFileStream("CONIN$", GENERIC_READ, FILE_SHARE_READ, FileAccess.Read); + if (fs != null) + { + Console.SetIn(new StreamReader(fs)); + } + } + + private static FileStream CreateFileStream(string name, uint win32DesiredAccess, uint win32ShareMode, + FileAccess dotNetFileAccess) + { + var file = new SafeFileHandle(CreateFileW(name, win32DesiredAccess, win32ShareMode, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero), true); + if (!file.IsInvalid) + { + var fs = new FileStream(file, dotNetFileAccess); + return fs; + } + return null; + } + + #region Win API Functions and Constants + [DllImport("kernel32.dll", + EntryPoint = "AllocConsole", + SetLastError = true, + CharSet = CharSet.Auto, + CallingConvention = CallingConvention.StdCall)] + private static extern int AllocConsole(); + + [DllImport("kernel32.dll", + EntryPoint = "AttachConsole", + SetLastError = true, + CharSet = CharSet.Auto, + CallingConvention = CallingConvention.StdCall)] + private static extern UInt32 AttachConsole(UInt32 dwProcessId); + + [DllImport("kernel32.dll", + EntryPoint = "CreateFileW", + SetLastError = true, + CharSet = CharSet.Auto, + CallingConvention = CallingConvention.StdCall)] + private static extern IntPtr CreateFileW( + string lpFileName, + UInt32 dwDesiredAccess, + UInt32 dwShareMode, + IntPtr lpSecurityAttributes, + UInt32 dwCreationDisposition, + UInt32 dwFlagsAndAttributes, + IntPtr hTemplateFile + ); + + private const UInt32 GENERIC_WRITE = 0x40000000; + private const UInt32 GENERIC_READ = 0x80000000; + private const UInt32 FILE_SHARE_READ = 0x00000001; + private const UInt32 FILE_SHARE_WRITE = 0x00000002; + private const UInt32 OPEN_EXISTING = 0x00000003; + private const UInt32 FILE_ATTRIBUTE_NORMAL = 0x80; + private const UInt32 ERROR_ACCESS_DENIED = 5; + + private const UInt32 ATTACH_PARRENT = 0xFFFFFFFF; + + #endregion + } +} \ No newline at end of file diff --git a/Hawkeye.VisionBuilder/icons/diaphragm.png b/Hawkeye.VisionBuilder/icons/diaphragm.png new file mode 100644 index 0000000000000000000000000000000000000000..e0798ae4fd8b4017c4a6951d16aea4b5d4ca7d4a GIT binary patch literal 1332 zcmV-41q?S?1gsP(z$m! zb1vJ5($aQjI;V9#ovi)aYkhmKv-jEpo7Vi7EKY0e0A3W54Ztd(wBwrPbebkk)37{J z4;859r9#vAp!=PwyLRG=e1Z)fvOzUD2($v8`Qz=^Gv}zLNx6|2FIKI6J#UwNwM^o3EMZs(uC7aWKTE1{+lzt)kaG~V+ObUUn zi)!`OKv&J7;bvO^{!?<#*eHLDhbTA9rKX-*eQj`vwSgYIiA2S+v5~g=Py2T-2;e&{ zw~vi;%rH?T5Q!`;c|khOQ~%tdc4TPA=Q4DqA{_dm`cQC7R)AGM%hE`QUk&q-FBDyT z@~ZGS8+&fzok(PiX@;)&LJ!VWe=<;>0T6<-lPTP@C&c1l+AM==nl$zYXy^~1npRGo zCzW&;ndx8V092ilFGr(no-dXXA~trRC^^B#-dj|L$8Etr7L9DH{$i>W3czHFucXW_ z&Qk`fW^|Yhy|*byrEDb!P%Vp+XzVC}QgcF9$73|+631d-W!X_F2m-II92$I4NI7Ub z`$H><$JVYoa?h_MB?qk>s|Twd?sJUB-YMP1=-%6S52+p$z9;(o~EHcKuIE=AvF~RMG@q_w&q$1NnXmUDq#C`a)6XmqpNNW0e>}8%K zMO0(|A{g@^6<%Cq8J;682t2OzwN)i1B)-)k-6 z*bD$b$Y4h#LFPNkE`ZJFo7aO?<$v_}nE~k7D8hjN+a^*hx)_Nhr1ktd%?b52?7F&1 zjAj4;T-ho*di!WyEQFoy*PPH$%f8MQadNglqY+)+EPm`8q$wJ=UyghLUJus#$9c8u zdGY1^v8ecxFeomats#!QZZ0nTGZjGbM zD_Q5va#@{hgW-|6K1G>2=e~0GPXFz>%=YcK`G)j6b@qE)yi)9;&gH^gpdpU`j-u<} q(l84>guHGpRJu8EsYUeIiTfY%mdCjqQ35kC} zVrfw-be2+U3L#Owg4&EiqA|&gp?O!KVcs&ILP)$*p4@Zq@8#Xx4^sVisnQrYD@-b| z14L&+t_1A))9q^jNB6dCun06KWFF+PIM=Q7M1aB5=RDBmhOg0$;a1PYrCU9@`HXr# zTeT5oZo>f!EMuWhND7dtw7hS`e?%!j3h*ZZiq2`NX!eczh~7~E`!1afyREA&T?7qe z4ep%Ai@mTFp~2(AsL8tH8>y(Ka@L>dC;=QkTC?}cm_$n~a6o>cS67vO)FmXSk*C07 z=cBLSo7pFMb@d`4qnMK!a)(X|J0@F=pZ+1kZq@WQ0zkRp2BWW924l;gXai`%Qp_NK2>K7zEsb6K*f)TSxkqYg|pS%MpKnn2d0D0H2Jmq9^ zt_N~e0SqlWKoo&(f2!@;pKZGWwyFM#<}oe1lBW7UdIvXPZrb%nir@eM002ovPDHLk FV1k$Q-P-^F literal 0 HcmV?d00001 diff --git a/VisionBuilder.UI.Common/CameraSettings.cs b/VisionBuilder.UI.Common/CameraSettings.cs new file mode 100644 index 0000000..c53e067 --- /dev/null +++ b/VisionBuilder.UI.Common/CameraSettings.cs @@ -0,0 +1,31 @@ +using Inspectron.Settings; + +namespace VisionBuilder.UI.Common; + +public class CameraSettings : ISettings +{ + public string CameraName { get; } + + public CameraSettings(string cameraName) + { + CameraName = cameraName; + } + + public enum EImageSource + { + Emulation, + Hawkeye, + IDS, + Basler, + + } + + public EImageSource Camera1Source { get; set; } + public string Camera1Label { get; set; } + + public void RegisterSettings(InspectronSettings settings) + { + settings.RegisterSimple(this, () => this.Camera1Source, CameraName, "Image source"); + settings.RegisterSimple(this, () => this.Camera1Label, CameraName, "Label"); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Commands/ImageProcessedEvent.cs b/VisionBuilder.UI.Common/Commands/ImageProcessedEvent.cs new file mode 100644 index 0000000..f9a6b19 --- /dev/null +++ b/VisionBuilder.UI.Common/Commands/ImageProcessedEvent.cs @@ -0,0 +1,17 @@ +using OpenCvSharp; +using VisionBuilder.UI.Common.Commands.Interfaces; + +namespace VisionBuilder.UI.Common.Commands; + +public class ImageProcessedEvent:IEvent +{ + public DateTime SessionStart { get; set; } + public string ImageSource { get; set; } + public string RecipeName { get; set; } + public Mat ImageOriginal { get; set; } + public Mat ImageAnalysis { get; set; } + + public bool HasError { get; set; } + public List ErrorNames { get; set; } + public TimeSpan AnalysisTime { get; set; } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Commands/Interfaces/ICommand.cs b/VisionBuilder.UI.Common/Commands/Interfaces/ICommand.cs new file mode 100644 index 0000000..31aa9c9 --- /dev/null +++ b/VisionBuilder.UI.Common/Commands/Interfaces/ICommand.cs @@ -0,0 +1,6 @@ +namespace VisionBuilder.UI.Common.Commands.Interfaces; + +public interface ICommand +{ + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Commands/Interfaces/IEvent.cs b/VisionBuilder.UI.Common/Commands/Interfaces/IEvent.cs new file mode 100644 index 0000000..6757653 --- /dev/null +++ b/VisionBuilder.UI.Common/Commands/Interfaces/IEvent.cs @@ -0,0 +1,6 @@ +namespace VisionBuilder.UI.Common.Commands.Interfaces; + +public interface IEvent +{ + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Commands/Interfaces/IEventHandler.cs b/VisionBuilder.UI.Common/Commands/Interfaces/IEventHandler.cs new file mode 100644 index 0000000..448b1ba --- /dev/null +++ b/VisionBuilder.UI.Common/Commands/Interfaces/IEventHandler.cs @@ -0,0 +1,6 @@ +namespace VisionBuilder.UI.Common.Commands.Interfaces; + +public interface IEventHandler where TEvent : IEvent +{ + void Handle(TEvent @event); +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Commands/SessionEndedEvent.cs b/VisionBuilder.UI.Common/Commands/SessionEndedEvent.cs new file mode 100644 index 0000000..b3e3938 --- /dev/null +++ b/VisionBuilder.UI.Common/Commands/SessionEndedEvent.cs @@ -0,0 +1,8 @@ +using VisionBuilder.UI.Common.Commands.Interfaces; + +namespace VisionBuilder.UI.Common.Commands; + +public class SessionEndedEvent: IEvent +{ + public DateTime SessionEnded { get; set; } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Commands/SessionStartedEvent.cs b/VisionBuilder.UI.Common/Commands/SessionStartedEvent.cs new file mode 100644 index 0000000..bd38ac3 --- /dev/null +++ b/VisionBuilder.UI.Common/Commands/SessionStartedEvent.cs @@ -0,0 +1,10 @@ +using VisionBuilder.UI.Common.Commands.Interfaces; + +namespace VisionBuilder.UI.Common.Commands; + +public class SessionStartedEvent : IEvent +{ + public string RecipeName { get; set; } + public DateTime SessionStarted { get; set; } + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ExampleMainViewModel.cs b/VisionBuilder.UI.Common/ExampleMainViewModel.cs new file mode 100644 index 0000000..5ed2312 --- /dev/null +++ b/VisionBuilder.UI.Common/ExampleMainViewModel.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.Metrics; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace VisionBuilder.UI.Common; + +public partial class ExampleMainViewModel : ObservableObject +{ + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(IncrementCommand))] + private int _counter; + + [RelayCommand(CanExecute = nameof(CanIncrement))] + private void Increment() + { + Counter++; + } + + private bool CanIncrement => this.Counter < 10; +} diff --git a/VisionBuilder.UI.Common/Extensions.cs b/VisionBuilder.UI.Common/Extensions.cs new file mode 100644 index 0000000..d20e1ae --- /dev/null +++ b/VisionBuilder.UI.Common/Extensions.cs @@ -0,0 +1,19 @@ +using Inspectron.Settings; +using Ninject; + +namespace VisionBuilder.UI.Common; + +public static class Extensions +{ + public static void RegisterSettings(this IKernel self) + { + // get all types from kernel that implement ISettings + var settingsTypes = self.GetAll(typeof(ISettings)).Select(x=>(ISettings)x).ToList(); + // register each type as ISettings + foreach (var settingsInstance in settingsTypes) + { + settingsInstance.RegisterSettings(self.Get()); + } + + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ISettings.cs b/VisionBuilder.UI.Common/ISettings.cs new file mode 100644 index 0000000..8d43729 --- /dev/null +++ b/VisionBuilder.UI.Common/ISettings.cs @@ -0,0 +1,8 @@ +using Inspectron.Settings; + +namespace VisionBuilder.UI.Common; + +public interface ISettings +{ + public void RegisterSettings(InspectronSettings settings); +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/NoLearning.cs b/VisionBuilder.UI.Common/NoLearning.cs new file mode 100644 index 0000000..8f8a065 --- /dev/null +++ b/VisionBuilder.UI.Common/NoLearning.cs @@ -0,0 +1,17 @@ +using OpenCvSharp; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +namespace VisionBuilder.UI.Common; + +public class NoLearning:ILearningTool +{ + public bool IsLearningEnabled(string recipeName) + { + return false; + } + + public void Learn(string recipeName, Mat image) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/PathSettingsUtils.cs b/VisionBuilder.UI.Common/PathSettingsUtils.cs new file mode 100644 index 0000000..b846066 --- /dev/null +++ b/VisionBuilder.UI.Common/PathSettingsUtils.cs @@ -0,0 +1,47 @@ +namespace VisionBuilder.UI.Common; + +public static class PathSettingsUtils +{ + public static string ConvertVariables( + string text, + DateTime imageTime, + DateTime sessionTime, + string recipeName = "", + string defectName = "", + string cameraName = "") + { + if (string.IsNullOrEmpty(text)) + return text; + + var replacements = new Dictionary + { + ["$RECIPE_NAME"] = recipeName, + ["$DEFECT_NAME"] = defectName, + ["$CAMERA_NAME"] = cameraName, + + ["$HOUR"] = imageTime.Hour.ToString("D2"), + ["$MINUTE"] = imageTime.Minute.ToString("D2"), + ["$SECOND"] = imageTime.Second.ToString("D2"), + ["$MS"] = imageTime.Millisecond.ToString("D3"), + + ["$DAY"] = imageTime.Day.ToString("D2"), + ["$MONTH"] = imageTime.Month.ToString("D2"), + ["$YEAR"] = imageTime.Year.ToString(), + + ["$SS_HOUR"] = sessionTime.Hour.ToString("D2"), + ["$SS_MINUTE"] = sessionTime.Minute.ToString("D2"), + ["$SS_SECOND"] = sessionTime.Second.ToString("D2"), + + ["$SS_DAY"] = sessionTime.Day.ToString("D2"), + ["$SS_MONTH"] = sessionTime.ToString("MMMM"), + ["$SS_YEAR"] = sessionTime.Year.ToString() + }; + + foreach (var pair in replacements) + { + text = text.Replace(pair.Key, pair.Value); + } + + return text; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Processing/IImageSource.cs b/VisionBuilder.UI.Common/Processing/IImageSource.cs new file mode 100644 index 0000000..1a17427 --- /dev/null +++ b/VisionBuilder.UI.Common/Processing/IImageSource.cs @@ -0,0 +1,9 @@ +using OpenCvSharp; + +namespace VisionBuilder.UI.Common.RecipeProcessing; + +public interface IImageSource +{ + string GetName(); + Task GetImage(CancellationToken token); +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Processing/ILoadingService.cs b/VisionBuilder.UI.Common/Processing/ILoadingService.cs new file mode 100644 index 0000000..c87cdd4 --- /dev/null +++ b/VisionBuilder.UI.Common/Processing/ILoadingService.cs @@ -0,0 +1,7 @@ +namespace VisionBuilder.UI.Common.RecipeProcessing; + +public interface ILoadingService +{ + void StartLoading(string title); + void StopLoading(string title); +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Processing/IRecognitionControl.cs b/VisionBuilder.UI.Common/Processing/IRecognitionControl.cs new file mode 100644 index 0000000..ccdc3b5 --- /dev/null +++ b/VisionBuilder.UI.Common/Processing/IRecognitionControl.cs @@ -0,0 +1,17 @@ +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.ViewModel.Classes; + +namespace VisionBuilder.UI.Common.RecipeProcessing; + +public interface IRecognitionControl +{ + + List GetRecipesData(); + void SetRecipe(RecipeData recipe); + void Start(); + void Stop(); + event Action ImageProcessed; + event Action SessionStarted; + event Action SessionEnded; + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/UIConfiguration.cs b/VisionBuilder.UI.Common/UIConfiguration.cs new file mode 100644 index 0000000..d0bfbd6 --- /dev/null +++ b/VisionBuilder.UI.Common/UIConfiguration.cs @@ -0,0 +1,21 @@ +using Inspectron.Settings; + +namespace VisionBuilder.UI.Common; + +public class UIConfiguration: ISettings +{ + /// + /// Max errors to be displayed in the UI. + /// + public int MaxErrorCount { get; set; } = 100; + + public string AdminPassword { get; set; } = ""; + + public void RegisterSettings(InspectronSettings settings) + { + settings.RegisterSimple(this, () => this.MaxErrorCount, "Errors", "Max errors count", "UI"); + settings.RegisterSimple(this, () => this.AdminPassword, "System", "Password", "UI"); + } + + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/Classes/ErrorData.cs b/VisionBuilder.UI.Common/ViewModel/Classes/ErrorData.cs new file mode 100644 index 0000000..3246d4f --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/Classes/ErrorData.cs @@ -0,0 +1,16 @@ +using OpenCvSharp; + +namespace VisionBuilder.UI.Common.ViewModel.Classes; + +public class ErrorData +{ + public ErrorData() + { + } + + public Mat ImageOriginal { get; set; } + public Mat ImageAnalysis { get; set; } + + public string RecipeName { get; set; } + public string Title { get; set; } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/Classes/RecipeData.cs b/VisionBuilder.UI.Common/ViewModel/Classes/RecipeData.cs new file mode 100644 index 0000000..4a9d763 --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/Classes/RecipeData.cs @@ -0,0 +1,9 @@ +using OpenCvSharp; + +namespace VisionBuilder.UI.Common.ViewModel.Classes; + +public class RecipeData +{ + public Mat? Image { get; set; } + public string RecipeName { get; set; } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/ErrorPreviewVM.cs b/VisionBuilder.UI.Common/ViewModel/ErrorPreviewVM.cs new file mode 100644 index 0000000..b6a4986 --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/ErrorPreviewVM.cs @@ -0,0 +1,102 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using OpenCvSharp; +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.Commands.Interfaces; +using VisionBuilder.UI.Common.ViewModel.Classes; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace VisionBuilder.UI.Common.ViewModel; + +public partial class ErrorPreviewVM:ObservableObject +{ + + private ErrorsVM _errorsVm; + private ErrorData _errorData; + + private readonly ILearningTool _learningTool; + + + + [ObservableProperty] + private string _recipeName; + + [ObservableProperty] + private bool _learnButtonVisible; + + [ObservableProperty] + private bool _isNextImageEnabled; + + [ObservableProperty] + private bool _isPreviousImageEnabled; + + [ObservableProperty] + private string _imageName; + + [ObservableProperty] + private Mat _imageAnalysis; + + public ErrorPreviewVM(ErrorsVM errorsVm, ErrorData errorData, string recipeName, UIConfiguration uiConfiguration, ILearningTool learningTool) + { + _errorsVm = errorsVm; + _errorData = errorData; + _recipeName = recipeName; + _learningTool = learningTool; + + UpdateData(); + } + + + + + [RelayCommand(CanExecute = nameof(IsNextImageEnabled))] + public void NextImage() + { + var imageIndex = _errorsVm.Errors.IndexOf(_errorData); + if (imageIndex >= _errorsVm.Errors.Count) return; + imageIndex++; + _errorData = _errorsVm.Errors[imageIndex]; + UpdateData(); + } + + [RelayCommand(CanExecute = nameof(IsPreviousImageEnabled))] + public void PreviousImage() + { + var imageIndex = _errorsVm.Errors.IndexOf(_errorData); + if (imageIndex <= 0) return; + imageIndex--; + _errorData = _errorsVm.Errors[imageIndex]; + UpdateData(); + + + } + + private void UpdateData() + { + var imageIndex = _errorsVm.Errors.IndexOf(_errorData); + + if (imageIndex < 0) + { + IsNextImageEnabled = false; + IsPreviousImageEnabled = false; + ImageName = string.Empty; + ImageAnalysis = null; + return; + } + + IsNextImageEnabled = imageIndex < _errorsVm.Errors.Count - 1; + IsPreviousImageEnabled = imageIndex > 0; + ImageName = _errorData.Title; + ImageAnalysis = _errorData.ImageAnalysis; + LearnButtonVisible = _learningTool.IsLearningEnabled(_errorData.RecipeName); + } + + [RelayCommand(CanExecute = nameof(LearnButtonVisible))] + public void Learn() + { + _learningTool.Learn(_errorData.RecipeName, _errorData.ImageOriginal); + } + + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/ErrorsVM.cs b/VisionBuilder.UI.Common/ViewModel/ErrorsVM.cs new file mode 100644 index 0000000..eaa5133 --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/ErrorsVM.cs @@ -0,0 +1,60 @@ +using System.Collections.ObjectModel; +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.Commands.Interfaces; +using VisionBuilder.UI.Common.ViewModel.Classes; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +namespace VisionBuilder.UI.Common.ViewModel; + +public class ErrorsVM:IEventHandler, IEventHandler +{ + private readonly UIConfiguration _uiConfiguration; + private readonly IImagePreviewService _imagePreviewService; + private readonly ILearningTool _learningTool; + + public ErrorsVM(UIConfiguration uiConfiguration, IImagePreviewService imagePreviewService, ILearningTool learningTool) + { + _uiConfiguration = uiConfiguration; + _imagePreviewService = imagePreviewService; + _learningTool = learningTool; + } + + + public ObservableCollection Errors { get; set; } = new ObservableCollection(); + + public void ShowImage(ErrorData errorData) + { + if (errorData == null) return; + + var vm = new ErrorPreviewVM(this, errorData, errorData.RecipeName, _uiConfiguration, _learningTool); + _imagePreviewService.ShowImagePreview(vm); + + } + + + + public void Handle(ImageProcessedEvent @event) + { + if(!@event.HasError)return; + + ErrorData errorData = new ErrorData + { + RecipeName = @event.RecipeName, + ImageOriginal = @event.ImageOriginal, + ImageAnalysis = @event.ImageAnalysis, + Title = @event.ErrorNames +" "+ DateTime.Now.ToString("T") + }; + + if (Errors.Count >= _uiConfiguration.MaxErrorCount) + { + Errors.RemoveAt(0); + } + + Errors.Add(errorData); + } + + public void Handle(SessionStartedEvent @event) + { + Errors.Clear(); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IImagePreviewService.cs b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IImagePreviewService.cs new file mode 100644 index 0000000..8fff6e4 --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IImagePreviewService.cs @@ -0,0 +1,9 @@ +using VisionBuilder.UI.Common.ViewModel.Classes; + +namespace VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +public interface IImagePreviewService +{ + void ShowImagePreview(ErrorPreviewVM errorPreviewVm); + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ILearningTool.cs b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ILearningTool.cs new file mode 100644 index 0000000..3072018 --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ILearningTool.cs @@ -0,0 +1,9 @@ +using OpenCvSharp; + +namespace VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +public interface ILearningTool +{ + public bool IsLearningEnabled(string recipeName); + public void Learn(string recipeName, Mat image); +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IMainWindowService.cs b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IMainWindowService.cs new file mode 100644 index 0000000..a0b841a --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IMainWindowService.cs @@ -0,0 +1,6 @@ +namespace VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +public interface IMainWindowService +{ + Task RunAsync(MainWindowVM vm); +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IRecipeSelectionDialogService.cs b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IRecipeSelectionDialogService.cs new file mode 100644 index 0000000..d74d79f --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/IRecipeSelectionDialogService.cs @@ -0,0 +1,6 @@ +namespace VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +public interface IRecipeSelectionDialogService +{ + bool SelectRecipe(RecipeSelectionVM recipeSelectionVm); +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ISettingsService.cs b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ISettingsService.cs new file mode 100644 index 0000000..cbe0675 --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ISettingsService.cs @@ -0,0 +1,6 @@ +namespace VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +public interface ISettingsService +{ + void ShowSettingsDialog(); +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ISingleCameraCreationService.cs b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ISingleCameraCreationService.cs new file mode 100644 index 0000000..150d2a5 --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/Interfaces/UI/ISingleCameraCreationService.cs @@ -0,0 +1,6 @@ +namespace VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +public interface ISingleCameraCreationService +{ + void CreateCamera(SingleCameraVM cameraVm); +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/MainWindowVM.cs b/VisionBuilder.UI.Common/ViewModel/MainWindowVM.cs new file mode 100644 index 0000000..01286be --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/MainWindowVM.cs @@ -0,0 +1,30 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.Commands.Interfaces; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +namespace VisionBuilder.UI.Common.ViewModel; + +public partial class MainWindowVM:ObservableObject +{ + private readonly ISettingsService _settingsService; + + + public MainWindowVM(ISettingsService settingsService) + { + _settingsService = settingsService; + } + + public ObservableCollection SingleCameraVms { get; set; } = + new ObservableCollection(); + + + [RelayCommand] + public void Settings() + { + _settingsService.ShowSettingsDialog(); + } + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/PreviewVM.cs b/VisionBuilder.UI.Common/ViewModel/PreviewVM.cs new file mode 100644 index 0000000..9516a28 --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/PreviewVM.cs @@ -0,0 +1,18 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using OpenCvSharp; +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.Commands.Interfaces; + +namespace VisionBuilder.UI.Common.ViewModel; + +public partial class PreviewVM:ObservableObject, IEventHandler +{ + [ObservableProperty] private Mat _imagePreview; + [ObservableProperty] private bool _isError; + + public void Handle(ImageProcessedEvent @event) + { + IsError = @event.HasError; + ImagePreview = @event.ImageAnalysis.Clone(); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/RecipeSelectionVM.cs b/VisionBuilder.UI.Common/ViewModel/RecipeSelectionVM.cs new file mode 100644 index 0000000..e7f5930 --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/RecipeSelectionVM.cs @@ -0,0 +1,10 @@ +using System.Collections.ObjectModel; +using VisionBuilder.UI.Common.ViewModel.Classes; + +namespace VisionBuilder.UI.Common.ViewModel; + +public class RecipeSelectionVM +{ + public ObservableCollection Recipes { get; set; } + public RecipeData SelectedRecipe { get; set; } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs b/VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs new file mode 100644 index 0000000..f4796e9 --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs @@ -0,0 +1,112 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System.Collections.ObjectModel; +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.Commands.Interfaces; +using VisionBuilder.UI.Common.RecipeProcessing; +using VisionBuilder.UI.Common.ViewModel; +using VisionBuilder.UI.Common.ViewModel.Classes; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +namespace VisionBuilder.UI.Common +{ + public partial class SingleCameraVM : ObservableObject, IEventHandler + { + private readonly IRecipeSelectionDialogService _recipeSelectionDialogService; + private readonly IRecognitionControl _recognitionControl; + + public ErrorsVM ErrorsVm { get; private set; } + public StatisticsVM StatisticsVm { get; private set; } + public PreviewVM PreviewVm { get; private set; } + + [ObservableProperty] + private string _currentRecipeName = "Select recipe"; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanStart))] + [NotifyPropertyChangedFor(nameof(CanStop))] + [NotifyCanExecuteChangedFor(nameof(SelectRecipeCommand))] + [NotifyCanExecuteChangedFor(nameof(StartCommand))] + [NotifyCanExecuteChangedFor(nameof(StopCommand))] + private bool _isRunning; + [ObservableProperty] + private string _cameraName; + + + public bool CanStop => IsRunning; + public bool CanStart => !IsRunning && SelectedRecipe!=null; + public bool IsNotRunning => !IsRunning; + private bool _handlingEnabled = true; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanStop))] + [NotifyPropertyChangedFor(nameof(CanStart))] + [NotifyCanExecuteChangedFor(nameof(SelectRecipeCommand))] + [NotifyCanExecuteChangedFor(nameof(StartCommand))] + [NotifyCanExecuteChangedFor(nameof(StopCommand))] + private RecipeData? _selectedRecipe; + + + public SingleCameraVM(CameraSettings cameraSettings, UIConfiguration uiConfiguration, IRecipeSelectionDialogService recipeSelectionDialogService, IRecognitionControl recognitionControl, IImagePreviewService imagePreviewService, ILearningTool learningTool) + { + _recipeSelectionDialogService = recipeSelectionDialogService; + + _recognitionControl = recognitionControl; + ErrorsVm = new ErrorsVM(uiConfiguration, imagePreviewService, learningTool); + StatisticsVm = new StatisticsVM(); + PreviewVm = new PreviewVM(); + + CameraName = cameraSettings.CameraName; + + recognitionControl.ImageProcessed += Handle; + + recognitionControl.SessionStarted += StatisticsVm.Handle; + recognitionControl.SessionStarted += ErrorsVm.Handle; + } + + + + [RelayCommand(CanExecute = nameof(IsNotRunning))] + public void SelectRecipe() + { + var vm = new RecipeSelectionVM(); + vm.Recipes = new ObservableCollection(_recognitionControl.GetRecipesData()); + if (!_recipeSelectionDialogService.SelectRecipe(vm))return; + SelectedRecipe = vm.SelectedRecipe; + _recognitionControl.SetRecipe(SelectedRecipe); + CurrentRecipeName = SelectedRecipe?.RecipeName ?? "Select recipe"; + } + + [RelayCommand(CanExecute = nameof(CanStart))] + public void Start() + { + _handlingEnabled = true; + _recognitionControl.Start(); + IsRunning = true; + } + + [RelayCommand(CanExecute = nameof(CanStop))] + public void Stop() + { + _handlingEnabled = false; + _recognitionControl.Stop(); + IsRunning = false; + } + + + public void Handle(ImageProcessedEvent @event) + { + if (!_handlingEnabled) return; + try + { + PreviewVm.Handle(@event); + StatisticsVm.Handle(@event); + ErrorsVm.Handle(@event); + } + catch(Exception e) + { + + } + } + } +} diff --git a/VisionBuilder.UI.Common/ViewModel/StatisticsVM.cs b/VisionBuilder.UI.Common/ViewModel/StatisticsVM.cs new file mode 100644 index 0000000..29177f1 --- /dev/null +++ b/VisionBuilder.UI.Common/ViewModel/StatisticsVM.cs @@ -0,0 +1,58 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.Text; +using System.Threading; +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.Commands.Interfaces; + + +namespace VisionBuilder.UI.Common.ViewModel; + +public partial class StatisticsVM: ObservableObject,IEventHandler, IEventHandler +{ + + [ObservableProperty] private string _sessionStarted; + [ObservableProperty] private string _recipeName; + + [ObservableProperty] private string _processingTime=""; + + [ObservableProperty] private int _good; + [ObservableProperty] private int _bad; + [ObservableProperty] private int _total; + + [ObservableProperty] private string _statisticsDetails=""; + + private readonly Dictionary _errorTypes = new(); + + public void Handle(ImageProcessedEvent @event) + { + + RecipeName= @event.RecipeName; + + Total++; + + if (!@event.HasError) + { + Good++; + return; + } + + Bad++; + foreach (var name in @event.ErrorNames) + { + _errorTypes[name] = _errorTypes.TryGetValue(name, out var count) ? count + 1 : 1; + } + + StatisticsDetails = string.Join(Environment.NewLine, _errorTypes.Select(e => $"{e.Key}: {e.Value}")); + ProcessingTime = (int)@event.AnalysisTime.TotalMilliseconds + "ms"; + } + + public void Handle(SessionStartedEvent @event) + { + // reset everything + _good = 0; + _bad = 0; + _total = 0; + _errorTypes.Clear(); + SessionStarted = @event.SessionStarted.ToString("yyyy-MM-dd HH:mm:ss"); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/VisionBuilder.UI.Common.csproj b/VisionBuilder.UI.Common/VisionBuilder.UI.Common.csproj new file mode 100644 index 0000000..9c400a2 --- /dev/null +++ b/VisionBuilder.UI.Common/VisionBuilder.UI.Common.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs b/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs new file mode 100644 index 0000000..0c5d809 --- /dev/null +++ b/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs @@ -0,0 +1,179 @@ +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Hawkeye.VisionBuilder.Workflow; +using Hawkeye.VisionBuilder.Workflow.Datatypes; +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.RecipeProcessing; +using VisionBuilder.UI.Common.ViewModel.Classes; + +namespace VisionBuilder.UI.Recipes.HawkeyeRecipe +{ + public class HawkeyeRecognitionControl: IRecognitionControl + { + private readonly IImageSource _imageSource; + private readonly HawkeyeRecognitionSettings _recognitionConfiguration; + private readonly ILoadingService _loadingService; + + private CancellationTokenSource _cts; + private Task _loopTask; + + public HawkeyeRecognitionControl(IImageSource imageSource, HawkeyeRecognitionSettings recognitionConfiguration, ILoadingService loadingService) + { + _imageSource = imageSource; + _recognitionConfiguration = recognitionConfiguration; + _loadingService = loadingService; + } + + public string GetCameraName() + { + return _imageSource.GetName(); + } + + public List GetRecipesData() + { + _loadingService.StartLoading($"Loading recipes for {_recognitionConfiguration.CameraName}..."); + try + { + var files = WorkflowRecipeHelper.ListRecipeFiles(); + List recipes = new List(); + foreach (var file in files) + { + var workFlow = WorkflowList.LoadFromFile(file); + + var recipe = new RecipeData + { + RecipeName = Path.GetFileNameWithoutExtension(file), + Image = workFlow.RecipeImage + }; + recipes.Add(recipe); + } + + return recipes; + } + finally + { + _loadingService.StopLoading($"Loading recipes for {_recognitionConfiguration.CameraName}..."); + } + } + + RecipeData _currentRecipe; + + public void SetRecipe(RecipeData recipe) + { + // error if running + if (IsRunning) + { + throw new InvalidOperationException("Cannot change recipe while recognition is running."); + } + _currentRecipe = recipe; + } + + public bool IsRunning { get; set; } + + private DateTime _startTime; + private WorkflowList _workflow; + + public void Start() + { + + if (_currentRecipe == null) + { + throw new InvalidOperationException("No recipe set."); + } + if (IsRunning) + { + throw new InvalidOperationException("Recognition is already running."); + } + _loadingService.StartLoading($"Starting recipe {_currentRecipe.RecipeName} on {_recognitionConfiguration.CameraName}..."); + _workflow = WorkflowList.LoadFromFile("..\\Data\\Recipes\\" + _currentRecipe.RecipeName + ".hrcp"); + _startTime = DateTime.Now; + IsRunning = true; + _cts = new CancellationTokenSource(); + _loopTask = Task.Run(() => Loop(_cts.Token), _cts.Token); + + SessionStarted?.Invoke(new SessionStartedEvent + { + RecipeName = _currentRecipe.RecipeName, + SessionStarted = _startTime, + + }); + _loadingService.StopLoading($"Starting recipe {_currentRecipe.RecipeName} on {_recognitionConfiguration.CameraName}..."); + + } + + public void Stop() + { + if (!IsRunning) + return ; + _loadingService.StartLoading($"Stopping {_recognitionConfiguration.CameraName}"); + _cts?.Cancel(); + try + { + _loopTask?.Wait(); + } + catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is TaskCanceledException)) + { + // Ignore cancellation exceptions + } + finally + { + IsRunning = false; + _cts?.Dispose(); + _cts = null; + _loopTask = null; + } + + SessionEnded?.Invoke(new SessionEndedEvent + { + SessionEnded = DateTime.Now + }); + _loadingService.StopLoading($"Stopping {_recognitionConfiguration.CameraName}"); + + } + + private void Loop(CancellationToken token) + { + // Start the recognition process using the Hawkeye library + + while (!token.IsCancellationRequested) + { + + + var sw = Stopwatch.StartNew(); + _workflow.ImageSource= _imageSource; + _workflow.Execute(); + sw.Stop(); + + if (sw.ElapsedMilliseconds < _recognitionConfiguration.MinimumProcessingTime) + { + Thread.Sleep(_recognitionConfiguration.MinimumProcessingTime - (int)sw.ElapsedMilliseconds); + } + string[] errorNames = _workflow.Operations.Where(x => !x.Result).Select(x => x.Label).ToArray(); + + var annotated = _workflow.Context.ActiveImage.ImageData.Clone(); + var canvas = new OpenCVCanvas(annotated); + _workflow.Context.GraphicsElements.ForEach(x => x.Draw(canvas)); + var processingResult = new ImageProcessedEvent() + { + SessionStart = _startTime, + RecipeName = _currentRecipe.RecipeName, + AnalysisTime = sw.Elapsed, + ErrorNames = errorNames.ToList(), + HasError = errorNames.Length > 0, + ImageSource = _imageSource.GetName(), + ImageOriginal = _workflow.Context.LastCameraImage.ImageData, + ImageAnalysis = annotated + }; + + ImageProcessed(processingResult); + + + } + } + + public event Action ImageProcessed = delegate{}; + public event Action? SessionStarted = delegate { }; + public event Action? SessionEnded = delegate { }; + } +} diff --git a/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionSettings.cs b/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionSettings.cs new file mode 100644 index 0000000..de1a3cc --- /dev/null +++ b/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionSettings.cs @@ -0,0 +1,20 @@ +using Inspectron.Settings; +using VisionBuilder.UI.Common; + +namespace VisionBuilder.UI.Recipes.HawkeyeRecipe; + +public class HawkeyeRecognitionSettings: ISettings +{ + public string CameraName { get; } + + public HawkeyeRecognitionSettings(string cameraName) + { + CameraName = cameraName; + } + + public int MinimumProcessingTime { get; set; } + public void RegisterSettings(InspectronSettings settings) + { + settings.RegisterSimple(this, () => this.MinimumProcessingTime, CameraName+"/Hawkeye recognition", "Minimum processing time (ms)"); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Recipes.HawkeyeRecipe/OpenCVCanvas.cs b/VisionBuilder.UI.Recipes.HawkeyeRecipe/OpenCVCanvas.cs new file mode 100644 index 0000000..30e1fb0 --- /dev/null +++ b/VisionBuilder.UI.Recipes.HawkeyeRecipe/OpenCVCanvas.cs @@ -0,0 +1,63 @@ +using Hawkeye.VisionBuilder.Workflow; +using Hawkeye.VisionBuilder.Workflow.Datatypes.Elements; +using OpenCvSharp; + +namespace VisionBuilder.UI.Recipes.HawkeyeRecipe; + +public class OpenCVCanvas:ICanvas +{ + private readonly Mat _image; + + public OpenCVCanvas(Mat image) + { + _image = image; + } + + public void DrawRectangle(Vector2 start, Vector2 size, bool isGood) + { + _image.Rectangle( + new Point(start.X, start.Y), + new Point(start.X + size.X, start.Y + size.Y), + isGood ? Scalar.Green : Scalar.Red, + 2); + } + + public void DrawLine(Vector2 start, Vector2 end, LineType type) + { + var color = type switch + { + LineType.Good => Scalar.Green, + LineType.Bad => Scalar.Red, + _ => Scalar.White + }; + _image.Line( + new Point(start.X, start.Y), + new Point(end.X, end.Y), + color, + 2); + + } + + public void DrawPoly(Vector2[] points, bool isGood) + { + var color = isGood ? Scalar.Green : Scalar.Red; + var cvPoints = points.Select(p => new Point(p.X, p.Y)).ToArray(); + _image.Polylines([cvPoints], true, color, 2); + + } + + public void DrawCross(Vector2 location) + { + throw new NotImplementedException(); + } + + public void DrawNode(Vector2 location) + { + throw new NotImplementedException(); + } + + public void DrawArrow(Vector2 location, Vector2 direction) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Recipes.HawkeyeRecipe/VisionBuilder.UI.Recipes.HawkeyeRecipe.csproj b/VisionBuilder.UI.Recipes.HawkeyeRecipe/VisionBuilder.UI.Recipes.HawkeyeRecipe.csproj new file mode 100644 index 0000000..3d5072b --- /dev/null +++ b/VisionBuilder.UI.Recipes.HawkeyeRecipe/VisionBuilder.UI.Recipes.HawkeyeRecipe.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/VisionBuilder.UI.Recipes.HawkeyeRecipe/WorkflowRecipeHelper.cs b/VisionBuilder.UI.Recipes.HawkeyeRecipe/WorkflowRecipeHelper.cs new file mode 100644 index 0000000..5455cbe --- /dev/null +++ b/VisionBuilder.UI.Recipes.HawkeyeRecipe/WorkflowRecipeHelper.cs @@ -0,0 +1,19 @@ +using Hawkeye.VisionBuilder.Workflow; + +namespace VisionBuilder.UI.Recipes.HawkeyeRecipe; + +public static class WorkflowRecipeHelper +{ + + + private const string RECIPES_DIR = "..\\Data\\Recipes"; + public static List ListRecipeFiles() + { + Directory.CreateDirectory(RECIPES_DIR); + var recipes = Directory.GetFiles(RECIPES_DIR, "*.hrcp"); + return recipes.ToList(); + } + + + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Recipes.Scripted.Tests/BasicSyntax.cs b/VisionBuilder.UI.Recipes.Scripted.Tests/BasicSyntax.cs new file mode 100644 index 0000000..787f13a --- /dev/null +++ b/VisionBuilder.UI.Recipes.Scripted.Tests/BasicSyntax.cs @@ -0,0 +1,32 @@ +namespace VisionBuilder.UI.Recipes.Scripted.Tests +{ + public class Tests + { + + public void TestAddFunction(int a, int b, out int c) + { + c = a + b; + } + + [SetUp] + public void Setup() + { + } + + [Test] + public void Test1() + { + // setup + // todo: register function + + string script = @" + test_add_function(1,2,c) +"; + + int c = 0; + // todo: get value of c from engine + + Assert.Pass(); + } + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Recipes.Scripted.Tests/VisionBuilder.UI.Recipes.Scripted.Tests.csproj b/VisionBuilder.UI.Recipes.Scripted.Tests/VisionBuilder.UI.Recipes.Scripted.Tests.csproj new file mode 100644 index 0000000..8b5797e --- /dev/null +++ b/VisionBuilder.UI.Recipes.Scripted.Tests/VisionBuilder.UI.Recipes.Scripted.Tests.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + diff --git a/VisionBuilder.UI.Recipes.Scripted/.claude/settings.local.json b/VisionBuilder.UI.Recipes.Scripted/.claude/settings.local.json new file mode 100644 index 0000000..842f54c --- /dev/null +++ b/VisionBuilder.UI.Recipes.Scripted/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(rg:*)", + "Bash(grep:*)" + ] + }, + "enableAllProjectMcpServers": false +} \ No newline at end of file diff --git a/VisionBuilder.UI.Recipes.Scripted/ETokenType.cs b/VisionBuilder.UI.Recipes.Scripted/ETokenType.cs new file mode 100644 index 0000000..753084d --- /dev/null +++ b/VisionBuilder.UI.Recipes.Scripted/ETokenType.cs @@ -0,0 +1,2 @@ +namespace VisionBuilder.UI.Recipes.Scripted; + diff --git a/VisionBuilder.UI.Recipes.Scripted/Tokenizer.cs b/VisionBuilder.UI.Recipes.Scripted/Tokenizer.cs new file mode 100644 index 0000000..e9e820f --- /dev/null +++ b/VisionBuilder.UI.Recipes.Scripted/Tokenizer.cs @@ -0,0 +1,143 @@ +using System.Text.RegularExpressions; + +namespace VisionBuilder.UI.Recipes.Scripted; + +public enum ETokenType +{ + EOF, + UnexpectedToken, + Identifier, + String, + + + OpenParenthesis, + CloseParenthesis +} +public class Token +{ + public Token(ETokenType type, string value, int line, int column, int start, int end) + { + Type = type; + Value = value; + Line = line; + Column = column; + Start = start; + End = end; + } + + + + public ETokenType Type { get; set; } + public string Value { get; set; } + public int Line { get; set; } + public int Column { get; set; } + public int Start { get; set; } + public int End { get; set; } + + public override string ToString() + { + return string.Format("{0} ({1})", Value, Type); + } + + +} +public class Tokenizer +{ + + public int Position { get; private set; } + public string Code { get; private set; } + + public Tokenizer(string code) + { + Code = code; + } + + private Dictionary _specs = new() + { + // comment + {@"^\/\/[^\n]*",null}, + // multi-line comment + {@"^\/\*.*\*\/",null}, + // whitespace + {@"^\s+",null}, + + {"^\"(\\\"|[^\"])*'",ETokenType.String}, + + {@"^\(",ETokenType.OpenParenthesis}, + {@"^\)",ETokenType.CloseParenthesis}, + {@"^[a-zA-Z_][a-zA-Z0-9_]*",ETokenType.Identifier}, + }; + + + private int GetCurrentLine() + { + var line = 1; + for (var i = 0; i < Position; i++) + { + if (Code[i] == '\n') + line++; + } + + return line; + + } + + private int GetCurrentColumn() + { + var column = 1; + for (var i = 0; i < Position; i++) + { + if (Code[i] == '\n') + column = 1; + else + column++; + } + + return column; + } + + public bool HasNextToken() + { + return Position < Code.Length; + } + public Token NextToken() + { + if (!HasNextToken()) + return new Token(ETokenType.EOF, string.Empty, GetCurrentLine(), GetCurrentColumn(), Position, Position); + var line = GetCurrentLine(); + var column = GetCurrentColumn(); + if (column == 1) + { + + if (Regex.IsMatch(Code.Substring(Position), @"^\s*\*")) + { + var match = Regex.Match(Code.Substring(Position), @"^\s*\*[^\n]*"); + Position += match.Length; + return NextToken(); + } + } + foreach (var spec in _specs) + { + var match = Regex.Match(Code.Substring(Position), spec.Key); + if (match.Success) + { + + var start = Position; + Position += match.Length; + if (spec.Value.HasValue) + { + if (spec.Value.Value == ETokenType.String) + { + return new Token(spec.Value.Value, Regex.Unescape(match.Value.Substring(1, match.Value.Length - 2)), line, column, start, Position); + } + return new Token(spec.Value.Value, match.Value, line, column, start, Position); + } + + else + return NextToken(); + } + } + + return new Token(ETokenType.UnexpectedToken, Code.Substring(Position, 1), line, column, Position, Position + 1); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Recipes.Scripted/VisionBuilder.UI.Recipes.Scripted.csproj b/VisionBuilder.UI.Recipes.Scripted/VisionBuilder.UI.Recipes.Scripted.csproj new file mode 100644 index 0000000..8336241 --- /dev/null +++ b/VisionBuilder.UI.Recipes.Scripted/VisionBuilder.UI.Recipes.Scripted.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/VisionBuilder.UI.Ringbuffer/VisionBuilder.UI.Ringbuffer.csproj b/VisionBuilder.UI.Ringbuffer/VisionBuilder.UI.Ringbuffer.csproj new file mode 100644 index 0000000..555f89e --- /dev/null +++ b/VisionBuilder.UI.Ringbuffer/VisionBuilder.UI.Ringbuffer.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbuffer.cs b/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbuffer.cs new file mode 100644 index 0000000..b7dfd89 --- /dev/null +++ b/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbuffer.cs @@ -0,0 +1,127 @@ +using Inspectron.Fastbuffer; +using Inspectron.Fastbuffer.Filesystems; +using System.Drawing; +using System.Drawing.Imaging; +using OpenCvSharp; +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.RecipeProcessing; + +namespace Lindt.Colorballs.Duo.Utils; + +public class VisionBuilderRingbuffer +{ + private readonly VisionBuilderRingbufferSettings _settings; + private readonly IRecognitionControl _recognitionControl; + + public VisionBuilderRingbuffer(VisionBuilderRingbufferSettings settings, IRecognitionControl recognitionControl) + { + _settings = settings; + _recognitionControl = recognitionControl; + _recognitionControl.SessionStarted += _recognitionControl_SessionStarted; + _recognitionControl.ImageProcessed += _recognitionControl_ImageProcessed; + } + + private void _recognitionControl_ImageProcessed(VisionBuilder.UI.Common.Commands.ImageProcessedEvent obj) + { + if (obj.HasError) + { + WriteBadResult(obj); + return; + } + WriteGoodResult(obj); + } + + private void _recognitionControl_SessionStarted(VisionBuilder.UI.Common.Commands.SessionStartedEvent obj) + { + NewProduct(obj.RecipeName); + } + + private IsolatedRingbuffer _goodRingbufferIsolated; + private string _path; + private DateTime _startTime; + private string _product; + + + public void Dispose() + { + + } + + + public void WriteGoodResult(ImageProcessedEvent result) + { + if (result.ImageOriginal == null) return; + var filename = PathSettingsUtils.ConvertVariables(_settings.FilenameTemplate, DateTime.Now, _startTime, _product,cameraName: _settings.CameraName) + ".png"; + try + { + var image = result.ImageOriginal; + // save to memory stream + _goodRingbufferIsolated.Write(image.ToBytes(), filename); + } + catch (Exception e) + { + Console.WriteLine($"Error writing good result at {filename}: " + e.Message); + } + } + + Dictionary _badRingbuffers = new Dictionary(); + + private IsolatedRingbuffer GetBadRingbuffer(string defectName) + { + if (_badRingbuffers.ContainsKey(defectName)) + { + return _badRingbuffers[defectName]; + } + var path = PathSettingsUtils.ConvertVariables(Path.Combine(_settings.Root, _settings.BadImagesPathTemplate), _startTime, _startTime, _product, cameraName: _settings.CameraName); + var ringbuffer = new IsolatedRingbuffer(path, _settings.MaxBad, new AsyncFilesystem()); + _badRingbuffers[defectName] = ringbuffer; + return ringbuffer; + } + + private void SaveResultImage(Mat image, string defectName, DateTime time, bool isAnalysis) + { + var filename = PathSettingsUtils.ConvertVariables(_settings.FilenameTemplate,time,_startTime,_product,defectName,_settings.CameraName) + (isAnalysis ? "_analysis" : "") + ".png"; + var ringbuffer = GetBadRingbuffer(defectName); + try + { + + + ringbuffer.Write(image.ToBytes(), filename); + } + catch (Exception e) + { + Console.WriteLine($"Error writing bad result for {filename}: " + e.Message); + } + } + + public void WriteBadResult(ImageProcessedEvent result) + { + var defectName = "unknown"; + + if (result.ErrorNames.Any()) + { + defectName = result.ErrorNames.First(); + } + + var time = DateTime.Now; + SaveResultImage(result.ImageOriginal, defectName, time, false); + SaveResultImage(result.ImageAnalysis, defectName, time, true); + } + + public void NewProduct(string product) + { + _startTime = DateTime.Now; + _product = product; + _path = PathSettingsUtils.ConvertVariables(Path.Combine(_settings.Root,_settings.GoodImagesPathTemplate), _startTime, _startTime, _product,cameraName:_settings.CameraName); + _goodRingbufferIsolated?.Dispose(); + _goodRingbufferIsolated = new IsolatedRingbuffer(_path, _settings.MaxGood, new AsyncFilesystem()); + + foreach (var ringbuffer in _badRingbuffers) + { + ringbuffer.Value.Dispose(); + } + + _badRingbuffers.Clear(); + + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbufferSettings.cs b/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbufferSettings.cs new file mode 100644 index 0000000..d802275 --- /dev/null +++ b/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbufferSettings.cs @@ -0,0 +1,85 @@ +using Inspectron.Settings; +using Inspectron.Settings.Attributes; +using System.ComponentModel; +using System.Security.Cryptography.X509Certificates; +using VisionBuilder.UI.Common; + +namespace Lindt.Colorballs.Duo.Utils; + +public class VisionBuilderRingbufferSettings : ISettings +{ + + public VisionBuilderRingbufferSettings(string cameraName) + { + CameraName = cameraName; + } + + public string CameraName { get; } + + public int MaxBad { get; set; } = 10000; + public int MaxGood { get; set; } = 100; + + + [SettingDescription( + @" +Specifies directory, in which MaxBad and MaxGood are applied. +You can use the following system variables: + +$RECIPE_NAME - name of the current recipe +$DEFECT_NAME - name of the current defect(for bad images) +$CAMERA_NAME - name of the camera + +$HOUR - hour of the image +$MINUTE - minute of the image +$SECOND - second of the image +$MS - milliseconds of the image + +$DAY - day of the image +$MONTH - month of the image +$YEAR - year of the image + +$SS_HOUR - hour of the session start +$SS_MINUTE - minute of the session start +$SS_SECOND - second of the session start + +$SS_DAY - day of the session start +$SS_MONTH - month of the session start +$SS_YEAR - year of the session start + +" + )] + [SettingPreview(typeof(VisionBuilderRingbufferSettings), nameof(ImagesPathTemplatePreview))] + public string Root { get; set; } = @"..\Data\Ringbuffer\$SS_MONTH $SS_YEAR"; + + + + [SettingDescriptionAttribute("Specifies directory, relative to the Root")] + [SettingPreview(typeof(VisionBuilderRingbufferSettings), nameof(ImagesPathTemplatePreview))] + public string GoodImagesPathTemplate { get; set; } = @"\$RECIPE_NAME"; + + [SettingDescriptionAttribute("Specifies directory, relative to the Root")] + [SettingPreview(typeof(VisionBuilderRingbufferSettings),nameof(ImagesPathTemplatePreview))] + public string BadImagesPathTemplate { get; set; } = + @"\$RECIPE_NAME\$DEFECT_NAME"; + + + [SettingPreview(typeof(VisionBuilderRingbufferSettings), nameof(ImagesPathTemplatePreview))] + public string FilenameTemplate { get; set; } = "$YEAR-$MONTH-$DAY $HOUR-$MINUTE-$SECOND $MS"; + + + public void RegisterSettings(InspectronSettings settings) + { + settings.RegisterSimple(this, () => this.MaxGood, CameraName+"/Ringbuffer", nameof(MaxGood)); + settings.RegisterSimple(this, () => this.MaxBad, CameraName + "/Ringbuffer", nameof(MaxBad)); + settings.RegisterSimple(this, () => this.Root, CameraName + "/Ringbuffer", nameof(Root)); + settings.RegisterSimple(this, () => this.GoodImagesPathTemplate, CameraName + "/Ringbuffer", nameof(GoodImagesPathTemplate)); + settings.RegisterSimple(this, () => this.BadImagesPathTemplate, CameraName + "/Ringbuffer", nameof(BadImagesPathTemplate)); + settings.RegisterSimple(this, () => this.FilenameTemplate, CameraName + "/Ringbuffer", nameof(FilenameTemplate)); + } + + public static string ImagesPathTemplatePreview(object template) + { + return PathSettingsUtils.ConvertVariables((string) template, DateTime.Now, DateTime.Now, "my recipe", + "my defect", "my camera"); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Statistics/README.md b/VisionBuilder.UI.Statistics/README.md new file mode 100644 index 0000000..98c7ef0 --- /dev/null +++ b/VisionBuilder.UI.Statistics/README.md @@ -0,0 +1,121 @@ +# VisionBuilder.UI.Statistics + +A .NET 8 library for collecting and managing image processing statistics in VisionBuilder applications. This library tracks session data, defect counts, and exports statistics to CSV files for analysis. + +## Overview + +The VisionBuilder.UI.Statistics library provides comprehensive statistics collection for computer vision and image processing workflows. It automatically tracks session events, image processing results, and defect data, organizing them into structured CSV reports. + +## Features + +- **Session Tracking**: Monitors recognition sessions from start to end +- **Defect Recording**: Tracks image processing errors and defect types +- **CSV Export**: Generates detailed statistics files for analysis +- **Configurable Paths**: Flexible output directory configuration with variable substitution +- **Monthly Reports**: Aggregates data into monthly statistics files +- **Thread-Safe**: Uses concurrent collections for reliable multi-threaded operation + +## Core Components + +### VisionBuilderStatistics + +The main statistics collection class that implements `IInitializable`. + +**Key Responsibilities:** +- Monitors recognition control events +- Records session start/end times +- Tracks defects and error types +- Exports data to CSV files + +**Event Handlers:** +- `SessionStarted`: Initializes new statistics session +- `ImageProcessed`: Records defect data for failed images +- `SessionEnded`: Finalizes and exports session statistics + +### VisionBuilderStatisticsSettings + +Configuration class for statistics collection settings. + +**Properties:** +- `CameraName`: Identifier for the camera/station +- `PathTemplate`: Configurable output directory with variable substitution + +**Supported Variables:** +- `$RECIPE_NAME` - Current recipe name +- `$DEFECT_NAME` - Defect type name +- `$CAMERA_NAME` - Camera identifier +- Time variables: `$HOUR`, `$MINUTE`, `$SECOND`, `$MS` +- Date variables: `$DAY`, `$MONTH`, `$YEAR` +- Session start variables: `$SS_HOUR`, `$SS_MINUTE`, `$SS_SECOND`, `$SS_DAY`, `$SS_MONTH`, `$SS_YEAR` + +## Output Files + +### statistics_by_events.csv +Contains detailed event-by-event statistics with columns: +- Start Date, Start Time +- End Date, End Time +- Error Time +- Product (Recipe) +- Error (Defect Type) + +### statistics_month.csv +Monthly aggregated statistics for trend analysis. + +## Dependencies + +- **Serilog** (4.3.0): Logging framework +- **Inspectron.Statistics**: Core statistics functionality +- **VisionBuilder.UI.Common**: Common UI components and interfaces + +## Usage + +```csharp +// Configure settings +var settings = new VisionBuilderStatisticsSettings("Camera1") +{ + PathTemplate = @"..\Data\Statistics\$SS_MONTH $SS_YEAR\$RECIPE_NAME\" +}; + +// Initialize statistics collector +var statistics = new VisionBuilderStatistics( + settings, + recognitionControl, + loadingService +); + +// Initialize to process any pending statistics +statistics.Initialize(); +``` + +## Architecture + +The library follows an event-driven architecture: + +1. **Session Start**: Creates temporary CSV file and begins tracking +2. **Image Processing**: Records defects as they occur during processing +3. **Session End**: Finalizes temporary file and merges into main statistics +4. **Cleanup**: Removes temporary files and updates monthly aggregations + +## File Management + +- Temporary files use GUID naming: `tmp_{guid}.csv` +- Files are sorted by creation time during consolidation +- Automatic directory creation for output paths +- Safe cleanup of temporary files after processing + +## Error Handling + +- Comprehensive exception handling with Serilog logging +- Graceful handling of invalid date/time data +- Safe file operations with proper resource disposal +- Loading service integration for user feedback + +## Thread Safety + +Uses `ConcurrentDictionary` for defect counting to ensure thread-safe operations in multi-threaded image processing environments. + +## Target Framework + +- .NET 8.0 +- Nullable reference types enabled +- Implicit usings enabled \ No newline at end of file diff --git a/VisionBuilder.UI.Statistics/VisionBuilder.UI.Statistics.csproj b/VisionBuilder.UI.Statistics/VisionBuilder.UI.Statistics.csproj new file mode 100644 index 0000000..98b6e11 --- /dev/null +++ b/VisionBuilder.UI.Statistics/VisionBuilder.UI.Statistics.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/VisionBuilder.UI.Statistics/VisionBuilderStatistics.cs b/VisionBuilder.UI.Statistics/VisionBuilderStatistics.cs new file mode 100644 index 0000000..967ce90 --- /dev/null +++ b/VisionBuilder.UI.Statistics/VisionBuilderStatistics.cs @@ -0,0 +1,145 @@ +using Inspectron.Statistics; +using Lindt.Colorballs.Duo.Utils; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Reflection.PortableExecutable; +using Ninject; +using Serilog; +using VisionBuilder.UI.Common; +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.RecipeProcessing; + +namespace VisionBuilder.UI.Statistics +{ + public class VisionBuilderStatistics:IInitializable + { + private CsvStatistics _csvStatistics; + private readonly VisionBuilderStatisticsSettings _settings; + private string _filePath; + private readonly IRecognitionControl _recognitionControl; + private readonly ILoadingService _loadingService; + private DateTime _startTime; + private string _currentRecipe; + private string _fileName; + private string _directoryPath; + + private const string BY_EVENTS_FILE_NAME = "statistics_by_events.csv"; + private const string MONTHLY_FILE_NAME = "statistics_month.csv"; + private const string EMPTY_DATE = ""; + private const string EMPTY_TIME = ""; + + + public VisionBuilderStatistics(VisionBuilderStatisticsSettings settings, IRecognitionControl recognitionControl, ILoadingService loadingService) + { + _settings = settings; + _recognitionControl = recognitionControl; + _loadingService = loadingService; + + _recognitionControl.SessionStarted += _recognitionControl_SessionStarted; + _recognitionControl.ImageProcessed += _recognitionControl_ImageProcessed; + _recognitionControl.SessionEnded += _recognitionControl_SessionEnded; + } + + private void _recognitionControl_SessionEnded(SessionEndedEvent obj) + { + _loadingService.StartLoading("Writing statistics for " + _settings.CameraName); + try + { + _csvStatistics.WriteLine([_startTime.ToString("d"), _startTime.ToString("T"), DateTime.Now.ToString("d"), DateTime.Now.ToString("T"), "", "", ""]); + _csvStatistics.Dispose(); + WriteFinalStatistics(); + } + finally + { + _loadingService.StopLoading("Writing statistics for " + _settings.CameraName); + } + } + + private void _recognitionControl_ImageProcessed(ImageProcessedEvent obj) + { + if (obj.HasError) + { + RecordBadImage(obj.ErrorNames[0]); + } + } + + private void _recognitionControl_SessionStarted(SessionStartedEvent obj) + { + _startTime = DateTime.Now; + _currentRecipe = obj.RecipeName; + _fileName = "tmp_"+Guid.NewGuid().ToString()+".csv"; + _directoryPath = PathSettingsUtils.ConvertVariables(_settings.PathTemplate, DateTime.Now, DateTime.Now, + obj.RecipeName, cameraName: _settings.CameraName); + _filePath = Path.Combine(_directoryPath, _fileName); + var headers = new[] { _settings.StartDateColumnName, _settings.StartTimeColumnName, _settings.EndDateColumnName, _settings.EndTimeColumnName, _settings.ErrorTimeColumnName, _settings.ProductColumnName, _settings.ErrorColumnName }; + if (!Directory.Exists(Path.GetDirectoryName(_filePath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(_filePath)); + } + _csvStatistics = new CsvStatistics(_filePath, headers); + _csvStatistics.WriteLine([_startTime.ToString("d"), _startTime.ToString("T")]); // session started + _defects = new ConcurrentDictionary(); + } + + ConcurrentDictionary _defects = new(); + private void RecordBadImage(string defectName) + { + + _csvStatistics.WriteLine( + + [_startTime.ToString("d"), _startTime.ToString("T"), EMPTY_DATE, EMPTY_TIME, DateTime.Now.ToString("T"), _currentRecipe, defectName] + + ); + + _defects.AddOrUpdate(defectName, 1, (key, oldValue) => oldValue + 1); + } + + private void WriteMonthStatistics(string filename) + { + // read second line to get the start time + var lines = File.ReadAllLines(filename); + var lastLine = lines[^1]; + if (lines.Length < 2) return; // no data to write + var startDate = DateTime.Parse(lastLine.Split(',')[0]); + var startTime = DateTime.Parse(lastLine.Split(',')[1]); + var endDate = DateTime.Parse(lastLine.Split(',')[2]); + var endTime = DateTime.Parse(lastLine.Split(',')[3]); + + // all must be non-empty + if (startDate == DateTime.MinValue || startTime == DateTime.MinValue || endDate == DateTime.MinValue || endTime == DateTime.MinValue) + { + Log.Error("Invalid date or time in statistics file: " + filename); + throw new InvalidOperationException("Invalid date or time in statistics file: " + filename); + } + } + + private void WriteFinalStatistics() + { + try + { + var tmpFiles = Directory.GetFiles(_directoryPath, "tmp_*"); + // order by creation time + Array.Sort(tmpFiles, (x, y) => File.GetCreationTime(x).CompareTo(File.GetCreationTime(y))); + foreach (var file in tmpFiles) + { + var finalFileName = Path.Combine(_directoryPath, BY_EVENTS_FILE_NAME); + var lines = File.ReadAllLines(file); + File.AppendAllLines(finalFileName, lines.Skip(1)); // skip header + WriteMonthStatistics(file); + File.Delete(file); + } + } + catch (Exception e) + { + Log.Error("Error writing statistics for " + _settings.CameraName+ e); + } + } + + public void Initialize() + { + WriteFinalStatistics(); + } + } +} diff --git a/VisionBuilder.UI.Statistics/VisionBuilderStatisticsSettings.cs b/VisionBuilder.UI.Statistics/VisionBuilderStatisticsSettings.cs new file mode 100644 index 0000000..89a39c6 --- /dev/null +++ b/VisionBuilder.UI.Statistics/VisionBuilderStatisticsSettings.cs @@ -0,0 +1,64 @@ +using Inspectron.Settings; +using Inspectron.Settings.Attributes; +using VisionBuilder.UI.Common; + +namespace VisionBuilder.UI.Statistics; + +public class VisionBuilderStatisticsSettings: ISettings +{ + public string CameraName { get; } + + public VisionBuilderStatisticsSettings(string cameraName) + { + CameraName = cameraName; + } + + [SettingDescription( + @" +Specifies directory, in which MaxBad and MaxGood are applied. +You can use the following system variables: + +$RECIPE_NAME - name of the current recipe +$DEFECT_NAME - name of the current defect(for bad images) +$CAMERA_NAME - name of the camera + +$HOUR - hour of the image +$MINUTE - minute of the image +$SECOND - second of the image +$MS - milliseconds of the image + +$DAY - day of the image +$MONTH - month of the image +$YEAR - year of the image + +$SS_HOUR - hour of the session start +$SS_MINUTE - minute of the session start +$SS_SECOND - second of the session start + +$SS_DAY - day of the session start +$SS_MONTH - month of the session start +$SS_YEAR - year of the session start +" + )] + public string PathTemplate { get; set; }= @$"..\Data\Statistics\$SS_MONTH $SS_YEAR\$RECIPE_NAME\"; + + public string StartDateColumnName { get; set; } = "Start Date"; + public string StartTimeColumnName { get; set; } = "Start time"; + public string EndDateColumnName { get; set; } = "End Date"; + public string EndTimeColumnName { get; set; } = "End time"; + public string ErrorTimeColumnName { get; set; } = "Error time"; + public string ProductColumnName { get; set; } = "Product"; + public string ErrorColumnName { get; set; } = "Error"; + + public void RegisterSettings(InspectronSettings settings) + { + settings.RegisterSimple(this,()=>this.PathTemplate,CameraName+"/"+"Statistics",nameof(PathTemplate)); + settings.RegisterSimple(this,()=>this.StartDateColumnName,CameraName+"/"+"Statistics",nameof(StartDateColumnName)); + settings.RegisterSimple(this,()=>this.StartTimeColumnName,CameraName+"/"+"Statistics",nameof(StartTimeColumnName)); + settings.RegisterSimple(this,()=>this.EndDateColumnName,CameraName+"/"+"Statistics",nameof(EndDateColumnName)); + settings.RegisterSimple(this,()=>this.EndTimeColumnName,CameraName+"/"+"Statistics",nameof(EndTimeColumnName)); + settings.RegisterSimple(this,()=>this.ErrorTimeColumnName,CameraName+"/"+"Statistics",nameof(ErrorTimeColumnName)); + settings.RegisterSimple(this,()=>this.ProductColumnName,CameraName+"/"+"Statistics",nameof(ProductColumnName)); + settings.RegisterSimple(this,()=>this.ErrorColumnName,CameraName+"/"+"Statistics",nameof(ErrorColumnName)); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Tests/MSTestSettings.cs b/VisionBuilder.UI.Tests/MSTestSettings.cs new file mode 100644 index 0000000..aaf278c --- /dev/null +++ b/VisionBuilder.UI.Tests/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/VisionBuilder.UI.Tests/VMTests.cs b/VisionBuilder.UI.Tests/VMTests.cs new file mode 100644 index 0000000..456ceec --- /dev/null +++ b/VisionBuilder.UI.Tests/VMTests.cs @@ -0,0 +1,19 @@ +using VisionBuilder.UI.Common; + +namespace VisionBuilder.UI.Tests +{ + [TestClass] + public sealed class VMTests + { + [TestMethod] + public void TestMethod1() + { + // Arrange + var vm = new ExampleMainViewModel(); + // Act + vm.IncrementCommand.Execute(null); + // Assert + Assert.AreEqual(1, vm.Counter); + } + } +} diff --git a/VisionBuilder.UI.Tests/VisionBuilder.UI.Tests.csproj b/VisionBuilder.UI.Tests/VisionBuilder.UI.Tests.csproj new file mode 100644 index 0000000..d43e20c --- /dev/null +++ b/VisionBuilder.UI.Tests/VisionBuilder.UI.Tests.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + latest + enable + enable + + + + + + + + + + + + + + + + diff --git a/VisionBuilder.UI.Windows.Test/AppSettings.cs b/VisionBuilder.UI.Windows.Test/AppSettings.cs new file mode 100644 index 0000000..51b4ec9 --- /dev/null +++ b/VisionBuilder.UI.Windows.Test/AppSettings.cs @@ -0,0 +1,8 @@ +using Inspectron.Settings; + +namespace VisionBuilder.UI.Windows.Test; + +public class AppSettings +{ + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows.Test/Form1.Designer.cs b/VisionBuilder.UI.Windows.Test/Form1.Designer.cs new file mode 100644 index 0000000..85cd7e9 --- /dev/null +++ b/VisionBuilder.UI.Windows.Test/Form1.Designer.cs @@ -0,0 +1,135 @@ +namespace VisionBuilder.UI.Windows.Test +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + btnMinimize = new MaterialSkin.Controls.MaterialRaisedButton(); + btnSettings = new MaterialSkin.Controls.MaterialRaisedButton(); + btnExit = new MaterialSkin.Controls.MaterialRaisedButton(); + materialDivider1 = new MaterialSkin.Controls.MaterialDivider(); + singleCameraControl1 = new VisionBuilder.UI.Windows.Components.SingleCameraControl(); + SuspendLayout(); + // + // btnMinimize + // + btnMinimize.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + btnMinimize.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnMinimize.Depth = 0; + btnMinimize.DrawBorder = true; + btnMinimize.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold); + btnMinimize.Icon = null; + btnMinimize.Location = new Point(784, 984); + btnMinimize.MouseState = MaterialSkin.MouseState.HOVER; + btnMinimize.Name = "btnMinimize"; + btnMinimize.Primary = false; + btnMinimize.Size = new Size(224, 64); + btnMinimize.TabIndex = 8; + btnMinimize.Text = "Minimize"; + btnMinimize.UseVisualStyleBackColor = true; + btnMinimize.Click += btnMinimize_Click; + // + // btnSettings + // + btnSettings.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + btnSettings.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnSettings.Depth = 0; + btnSettings.DrawBorder = true; + btnSettings.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold); + btnSettings.Icon = null; + btnSettings.Location = new Point(552, 984); + btnSettings.MouseState = MaterialSkin.MouseState.HOVER; + btnSettings.Name = "btnSettings"; + btnSettings.Primary = false; + btnSettings.Size = new Size(224, 64); + btnSettings.TabIndex = 7; + btnSettings.Text = "Settings"; + btnSettings.UseVisualStyleBackColor = true; + btnSettings.Click += btnSettings_Click; + // + // btnExit + // + btnExit.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + btnExit.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnExit.Depth = 0; + btnExit.DrawBorder = true; + btnExit.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold); + btnExit.Icon = null; + btnExit.Location = new Point(1016, 984); + btnExit.MouseState = MaterialSkin.MouseState.HOVER; + btnExit.Name = "btnExit"; + btnExit.Primary = false; + btnExit.Size = new Size(224, 64); + btnExit.TabIndex = 6; + btnExit.Text = "Exit"; + btnExit.UseVisualStyleBackColor = true; + btnExit.Click += btnExit_Click; + // + // materialDivider1 + // + materialDivider1.BackColor = Color.FromArgb(55, 71, 79); + materialDivider1.Depth = 0; + materialDivider1.Location = new Point(8, 960); + materialDivider1.MouseState = MaterialSkin.MouseState.HOVER; + materialDivider1.Name = "materialDivider1"; + materialDivider1.Size = new Size(1904, 1); + materialDivider1.TabIndex = 9; + materialDivider1.Text = "materialDivider1"; + // + // singleCameraControl1 + // + singleCameraControl1.BackColor = Color.White; + singleCameraControl1.Location = new Point(8, 32); + singleCameraControl1.Name = "singleCameraControl1"; + singleCameraControl1.Size = new Size(1904, 912); + singleCameraControl1.TabIndex = 10; + // + // Form1 + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1920, 1080); + Controls.Add(singleCameraControl1); + Controls.Add(materialDivider1); + Controls.Add(btnMinimize); + Controls.Add(btnSettings); + Controls.Add(btnExit); + Name = "Form1"; + StartPosition = FormStartPosition.CenterScreen; + Text = "Form1"; + ResumeLayout(false); + } + + #endregion + + private MaterialSkin.Controls.MaterialRaisedButton btnMinimize; + private MaterialSkin.Controls.MaterialRaisedButton btnSettings; + private MaterialSkin.Controls.MaterialRaisedButton btnExit; + private MaterialSkin.Controls.MaterialDivider materialDivider1; + private Components.SingleCameraControl singleCameraControl1; + } +} diff --git a/VisionBuilder.UI.Windows.Test/Form1.cs b/VisionBuilder.UI.Windows.Test/Form1.cs new file mode 100644 index 0000000..698946f --- /dev/null +++ b/VisionBuilder.UI.Windows.Test/Form1.cs @@ -0,0 +1,33 @@ +using MaterialSkin.Controls; +using VisionBuilder.UI.Common.ViewModel; + +namespace VisionBuilder.UI.Windows.Test +{ + public partial class Form1 : MaterialForm + { + private readonly MainWindowVM _mainWindowVm; + + public Form1(MainWindowVM mainWindowVm) + { + _mainWindowVm = mainWindowVm; + MaterialSkin.MaterialSkinManager.ConfigureForInspectron(); + InitializeComponent(); + singleCameraControl1.SetViewModel(mainWindowVm.SingleCameraVms[0]); + } + + private void btnExit_Click(object sender, EventArgs e) + { + Environment.Exit(0); + } + + private void btnMinimize_Click(object sender, EventArgs e) + { + WindowState = FormWindowState.Minimized; + } + + private void btnSettings_Click(object sender, EventArgs e) + { + _mainWindowVm.SettingsCommand.Execute(null); + } + } +} diff --git a/VisionBuilder.UI.Windows.Test/Form1.resx b/VisionBuilder.UI.Windows.Test/Form1.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/VisionBuilder.UI.Windows.Test/Form1.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/VisionBuilder.UI.Windows.Test/Program.cs b/VisionBuilder.UI.Windows.Test/Program.cs new file mode 100644 index 0000000..1123f29 --- /dev/null +++ b/VisionBuilder.UI.Windows.Test/Program.cs @@ -0,0 +1,93 @@ +using System.Collections.ObjectModel; +using VisionBuilder.UI.Common; +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.ViewModel; +using VisionBuilder.UI.Common.ViewModel.Classes; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; +using VisionBuilder.UI.Windows.Settings; +using System.Threading; +using Hawkeye.VisionBuilder.UI.Sources.Emulation; +using Inspectron.Settings; +using Inspectron.Settings.Windows.Configuration; +using Lindt.Colorballs.Duo.Utils; +using Ninject; +using OpenCvSharp; +using VisionBuilder.UI.Common.RecipeProcessing; +using VisionBuilder.UI.Recipes.HawkeyeRecipe; + +namespace VisionBuilder.UI.Windows.Test +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + + + StandardKernel kernelCamera1 = new StandardKernel(); + + + // SETTINGS // + InspectronSettings settings = new InspectronSettings("..\\Config\\AppSettings.xml"); + kernelCamera1.Bind().ToConstant(settings); + kernelCamera1.Bind().ToConstant(new UIConfiguration()); + kernelCamera1.Bind().ToConstant(new CameraSettings("Camera 1")); + kernelCamera1.Bind().ToConstant(new EmulationSettings("Camera 1")); + kernelCamera1.Bind().ToConstant(new HawkeyeRecognitionSettings("Camera 1")); + kernelCamera1.Bind().ToConstant(new VisionBuilderRingbufferSettings("Camera 1")); + + kernelCamera1.RegisterSettings(); + settings.LoadSettings(); + + + // --- // + + // SOURCES // + var cameraSettings = kernelCamera1.Get(); + switch (cameraSettings.Camera1Source) + { + case CameraSettings.EImageSource.Emulation: + kernelCamera1.Bind().To().InSingletonScope(); + break; + case CameraSettings.EImageSource.Hawkeye: + break; + case CameraSettings.EImageSource.IDS: + break; + case CameraSettings.EImageSource.Basler: + break; + default: + throw new ArgumentOutOfRangeException(); + } + // --- // + + // SERVICES // + kernelCamera1.Bind().To().InSingletonScope(); + kernelCamera1.Bind().To().InSingletonScope(); + kernelCamera1.Bind().ToSelf().InSingletonScope(); + kernelCamera1.Bind().To().InSingletonScope(); + + kernelCamera1.Get(); // initialize the ringbuffer + + var mainWindowVm = new MainWindowVM(kernelCamera1.Get()) + { + SingleCameraVms = new ObservableCollection() + { + new SingleCameraVM(kernelCamera1.Get(),kernelCamera1.Get(),new RecipeSelectionDialogService(),kernelCamera1.Get(), new ImagePreviewService(), new NoLearning()) + } + }; + + settings.LoadSettings(); + Application.Run(new Form1(mainWindowVm)); + } + } + + + + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj b/VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj new file mode 100644 index 0000000..90e6dab --- /dev/null +++ b/VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj @@ -0,0 +1,32 @@ + + + + WinExe + net8.0-windows + enable + true + enable + + + + + + + + + + + + + + + + + + + + Form + + + + \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Components/ErrorPreview.Designer.cs b/VisionBuilder.UI.Windows/Components/ErrorPreview.Designer.cs new file mode 100644 index 0000000..e6ef9b0 --- /dev/null +++ b/VisionBuilder.UI.Windows/Components/ErrorPreview.Designer.cs @@ -0,0 +1,57 @@ +namespace VisionBuilder.UI.Windows.Components +{ + partial class ErrorPreview + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + flowLayoutPanel1 = new FlowLayoutPanel(); + SuspendLayout(); + // + // flowLayoutPanel1 + // + flowLayoutPanel1.AutoScroll = true; + flowLayoutPanel1.Dock = DockStyle.Fill; + flowLayoutPanel1.Location = new Point(0, 0); + flowLayoutPanel1.Name = "flowLayoutPanel1"; + flowLayoutPanel1.Size = new Size(584, 786); + flowLayoutPanel1.TabIndex = 0; + // + // ErrorPreview + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + Controls.Add(flowLayoutPanel1); + Name = "ErrorPreview"; + Size = new Size(584, 786); + ResumeLayout(false); + } + + #endregion + + private FlowLayoutPanel flowLayoutPanel1; + } +} diff --git a/VisionBuilder.UI.Windows/Components/ErrorPreview.cs b/VisionBuilder.UI.Windows/Components/ErrorPreview.cs new file mode 100644 index 0000000..af77f75 --- /dev/null +++ b/VisionBuilder.UI.Windows/Components/ErrorPreview.cs @@ -0,0 +1,135 @@ +using System.Collections.ObjectModel; +using Microsoft.VisualBasic.Logging; +using System.Drawing.Drawing2D; +using OpenCvSharp.Extensions; +using VisionBuilder.UI.Common.ViewModel; +using VisionBuilder.UI.Common.ViewModel.Classes; +using System.Collections.Specialized; + +namespace VisionBuilder.UI.Windows.Components +{ + public partial class ErrorPreview : UserControl + { + private ErrorsVM _errorPreviewVM; + + // Keep track of subscription to avoid duplicate handlers + private ObservableCollection _subscribedErrors; + + public void SetViewModel(ErrorsVM errorPreviewVm) + { + // Unsubscribe from previous Errors collection if needed + if (_subscribedErrors != null) + _subscribedErrors.CollectionChanged -= Errors_CollectionChanged; + + _errorPreviewVM = errorPreviewVm; + + if (_errorPreviewVM != null) + { + _subscribedErrors = _errorPreviewVM.Errors; + _subscribedErrors.CollectionChanged += Errors_CollectionChanged; + + // Initial sync: clear and add all + flowLayoutPanel1.Controls.Clear(); + foreach (var error in _subscribedErrors) + { + AddErrorPreviewInternal(error); + } + } + } + + public ErrorPreview() + { + InitializeComponent(); + } + + private Font _numberedFont = new Font(FontFamily.GenericSansSerif, 10, FontStyle.Regular); + private void AddErrorPreviewInternal(ErrorData errorData) + { + var pb = new PictureBox(); + var width = 293 / 2; + var height = 220 / 2; + pb.Size = new Size(width, height + 20); + + var numberedImage = new Bitmap(width, height + 20); + var text = DateTime.Now.ToString("yy-MM-dd") + " " + DateTime.Now.ToString("T"); + + using (Graphics g = Graphics.FromImage(numberedImage)) + { + g.Clear(Color.White); + + g.InterpolationMode = InterpolationMode.HighQualityBicubic; + try + { + g.DrawImage(errorData.ImageAnalysis.ToBitmap(), 0, 0, width, height); + } + catch (Exception ex) + { + + } + + var measure = g.MeasureString(text, _numberedFont); + g.TranslateTransform(width / 2 - measure.Width / 2, height); + + g.DrawString(text, _numberedFont, Brushes.Black, 0, 0); + } + pb.Image = numberedImage; + pb.SizeMode = PictureBoxSizeMode.Zoom; + pb.Cursor = Cursors.Hand; + pb.Click += (sender, args) => + { + _errorPreviewVM.ShowImage(errorData); + }; + var tag = (errorData, text); + pb.Tag = tag; + flowLayoutPanel1.Controls.Add(pb); + flowLayoutPanel1.Controls.SetChildIndex(pb, 0); + + try + { + flowLayoutPanel1.Refresh(); + } + catch + { + + } + } + + private void Errors_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (InvokeRequired) + { + BeginInvoke(new NotifyCollectionChangedEventHandler(Errors_CollectionChanged), sender, e); + return; + } + + if (e.Action == NotifyCollectionChangedAction.Add) + { + foreach (ErrorData newError in e.NewItems) + { + AddErrorPreviewInternal(newError); + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (ErrorData oldError in e.OldItems) + { + // Remove the PictureBox whose Tag contains the oldError + foreach (Control ctrl in flowLayoutPanel1.Controls) + { + if (ctrl is PictureBox pb && pb.Tag is ValueTuple tag && tag.Item1 == oldError) + { + flowLayoutPanel1.Controls.Remove(pb); + pb.Dispose(); + break; + } + } + } + } + else if (e.Action == NotifyCollectionChangedAction.Reset) + { + flowLayoutPanel1.Controls.Clear(); + } + + } + } +} diff --git a/VisionBuilder.UI.Windows/Components/ErrorPreview.resx b/VisionBuilder.UI.Windows/Components/ErrorPreview.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/VisionBuilder.UI.Windows/Components/ErrorPreview.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Components/PreviewWindow.cs b/VisionBuilder.UI.Windows/Components/PreviewWindow.cs new file mode 100644 index 0000000..61f831e --- /dev/null +++ b/VisionBuilder.UI.Windows/Components/PreviewWindow.cs @@ -0,0 +1,44 @@ +using OpenCvSharp.Extensions; +using VisionBuilder.UI.Common.ViewModel; + +namespace VisionBuilder.UI.Windows.Components; + +public class PreviewWindow:PictureBox +{ + private PreviewVM _previewVm; + + public void SetViewModel(PreviewVM previewVm) + { + _previewVm = previewVm; + _previewVm.PropertyChanged += _previewVm_PropertyChanged; + } + + private void _previewVm_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(PreviewVM.ImagePreview): + var bmp = _previewVm.ImagePreview?.ToBitmap(); + using (Graphics g = Graphics.FromImage(bmp)) + { + // draw red border around the image if it's an error + if (_previewVm.IsError) + { + using (Pen pen = new Pen(Color.Red, 5)) + { + g.DrawRectangle(pen, 0, 0, bmp.Width - 1, bmp.Height - 1); + } + } + } + Image = bmp; + break; + + } + + } + + public PreviewWindow() + { + SizeMode = PictureBoxSizeMode.Zoom; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Components/SingleCameraControl.Designer.cs b/VisionBuilder.UI.Windows/Components/SingleCameraControl.Designer.cs new file mode 100644 index 0000000..da2da4c --- /dev/null +++ b/VisionBuilder.UI.Windows/Components/SingleCameraControl.Designer.cs @@ -0,0 +1,215 @@ +namespace VisionBuilder.UI.Windows.Components +{ + partial class SingleCameraControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + lblCameraName = new Label(); + label1 = new Label(); + previewWindow1 = new PreviewWindow(); + materialDivider1 = new MaterialSkin.Controls.MaterialDivider(); + errorPreview1 = new ErrorPreview(); + stats1 = new Stats(); + btnStart = new MaterialSkin.Controls.MaterialRaisedButton(); + flowLayoutPanel1 = new FlowLayoutPanel(); + btnSelectRecipe = new MaterialSkin.Controls.MaterialRaisedButton(); + btnStop = new MaterialSkin.Controls.MaterialRaisedButton(); + materialDivider2 = new MaterialSkin.Controls.MaterialDivider(); + ((System.ComponentModel.ISupportInitialize)previewWindow1).BeginInit(); + flowLayoutPanel1.SuspendLayout(); + SuspendLayout(); + // + // lblCameraName + // + lblCameraName.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + lblCameraName.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold, GraphicsUnit.Point, 204); + lblCameraName.Location = new Point(8, 8); + lblCameraName.Name = "lblCameraName"; + lblCameraName.Size = new Size(616, 21); + lblCameraName.TabIndex = 2; + lblCameraName.Text = "CameraName"; + lblCameraName.TextAlign = ContentAlignment.TopCenter; + // + // label1 + // + label1.Anchor = AnchorStyles.Top | AnchorStyles.Right; + label1.Font = new Font("Segoe UI", 12F); + label1.Location = new Point(656, 8); + label1.Name = "label1"; + label1.Size = new Size(408, 21); + label1.TabIndex = 4; + label1.Text = "Errors"; + label1.TextAlign = ContentAlignment.TopCenter; + // + // previewWindow1 + // + previewWindow1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + previewWindow1.Location = new Point(0, 32); + previewWindow1.Name = "previewWindow1"; + previewWindow1.Size = new Size(624, 456); + previewWindow1.SizeMode = PictureBoxSizeMode.Zoom; + previewWindow1.TabIndex = 5; + previewWindow1.TabStop = false; + // + // materialDivider1 + // + materialDivider1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; + materialDivider1.BackColor = Color.FromArgb(55, 71, 79); + materialDivider1.Depth = 0; + materialDivider1.Location = new Point(632, 32); + materialDivider1.MouseState = MaterialSkin.MouseState.HOVER; + materialDivider1.Name = "materialDivider1"; + materialDivider1.Size = new Size(1, 456); + materialDivider1.TabIndex = 6; + materialDivider1.Text = "materialDivider1"; + // + // errorPreview1 + // + errorPreview1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; + errorPreview1.Location = new Point(648, 32); + errorPreview1.Name = "errorPreview1"; + errorPreview1.Size = new Size(416, 456); + errorPreview1.TabIndex = 7; + // + // stats1 + // + stats1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; + stats1.BackColor = Color.White; + stats1.Location = new Point(1080, 32); + stats1.Name = "stats1"; + stats1.Size = new Size(320, 456); + stats1.TabIndex = 8; + // + // btnStart + // + btnStart.Anchor = AnchorStyles.Top | AnchorStyles.Right; + btnStart.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnStart.Depth = 0; + btnStart.DrawBorder = false; + btnStart.Icon = null; + btnStart.Location = new Point(85, 97); + btnStart.MouseState = MaterialSkin.MouseState.HOVER; + btnStart.Name = "btnStart"; + btnStart.Primary = true; + btnStart.Size = new Size(176, 64); + btnStart.TabIndex = 9; + btnStart.Text = "Start"; + btnStart.UseVisualStyleBackColor = true; + // + // flowLayoutPanel1 + // + flowLayoutPanel1.Controls.Add(btnSelectRecipe); + flowLayoutPanel1.Controls.Add(btnStart); + flowLayoutPanel1.Controls.Add(btnStop); + flowLayoutPanel1.Dock = DockStyle.Right; + flowLayoutPanel1.FlowDirection = FlowDirection.TopDown; + flowLayoutPanel1.Location = new Point(1408, 0); + flowLayoutPanel1.Name = "flowLayoutPanel1"; + flowLayoutPanel1.Padding = new Padding(8, 24, 8, 8); + flowLayoutPanel1.RightToLeft = RightToLeft.Yes; + flowLayoutPanel1.Size = new Size(280, 498); + flowLayoutPanel1.TabIndex = 10; + // + // btnSelectRecipe + // + btnSelectRecipe.Anchor = AnchorStyles.Top | AnchorStyles.Right; + btnSelectRecipe.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnSelectRecipe.Depth = 0; + btnSelectRecipe.DrawBorder = true; + btnSelectRecipe.Icon = null; + btnSelectRecipe.Location = new Point(85, 27); + btnSelectRecipe.MouseState = MaterialSkin.MouseState.HOVER; + btnSelectRecipe.Name = "btnSelectRecipe"; + btnSelectRecipe.Primary = false; + btnSelectRecipe.Size = new Size(176, 64); + btnSelectRecipe.TabIndex = 11; + btnSelectRecipe.Text = "Select recipe"; + btnSelectRecipe.UseVisualStyleBackColor = true; + // + // btnStop + // + btnStop.Anchor = AnchorStyles.Top | AnchorStyles.Right; + btnStop.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnStop.Depth = 0; + btnStop.DrawBorder = false; + btnStop.Icon = null; + btnStop.Location = new Point(85, 167); + btnStop.MouseState = MaterialSkin.MouseState.HOVER; + btnStop.Name = "btnStop"; + btnStop.Primary = true; + btnStop.Size = new Size(176, 64); + btnStop.TabIndex = 10; + btnStop.Text = "Stop"; + btnStop.UseVisualStyleBackColor = true; + // + // materialDivider2 + // + materialDivider2.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; + materialDivider2.BackColor = Color.FromArgb(55, 71, 79); + materialDivider2.Depth = 0; + materialDivider2.Location = new Point(1064, 32); + materialDivider2.MouseState = MaterialSkin.MouseState.HOVER; + materialDivider2.Name = "materialDivider2"; + materialDivider2.Size = new Size(1, 456); + materialDivider2.TabIndex = 11; + materialDivider2.Text = "materialDivider2"; + // + // SingleCameraControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + BackColor = Color.White; + Controls.Add(materialDivider2); + Controls.Add(flowLayoutPanel1); + Controls.Add(stats1); + Controls.Add(errorPreview1); + Controls.Add(materialDivider1); + Controls.Add(previewWindow1); + Controls.Add(label1); + Controls.Add(lblCameraName); + Name = "SingleCameraControl"; + Size = new Size(1688, 498); + ((System.ComponentModel.ISupportInitialize)previewWindow1).EndInit(); + flowLayoutPanel1.ResumeLayout(false); + ResumeLayout(false); + } + + #endregion + + private Label lblCameraName; + private Label label1; + private PreviewWindow previewWindow1; + private MaterialSkin.Controls.MaterialDivider materialDivider1; + private ErrorPreview errorPreview1; + private Stats stats1; + private MaterialSkin.Controls.MaterialRaisedButton btnStart; + private FlowLayoutPanel flowLayoutPanel1; + private MaterialSkin.Controls.MaterialRaisedButton btnStop; + private MaterialSkin.Controls.MaterialRaisedButton btnSelectRecipe; + private MaterialSkin.Controls.MaterialDivider materialDivider2; + } +} diff --git a/VisionBuilder.UI.Windows/Components/SingleCameraControl.cs b/VisionBuilder.UI.Windows/Components/SingleCameraControl.cs new file mode 100644 index 0000000..977bed1 --- /dev/null +++ b/VisionBuilder.UI.Windows/Components/SingleCameraControl.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using VisionBuilder.UI.Common; + +namespace VisionBuilder.UI.Windows.Components +{ + public partial class SingleCameraControl : UserControl + { + private SingleCameraVM _singleCameraVm; + + public void SetViewModel(SingleCameraVM singleCameraVm) + { + _singleCameraVm = singleCameraVm; + this.previewWindow1.SetViewModel(singleCameraVm.PreviewVm); + this.errorPreview1.SetViewModel(singleCameraVm.ErrorsVm); + this.stats1.SetViewModel(singleCameraVm.StatisticsVm); + + SetBindings(); + } + + private void SetBindings() + { + btnStart.Command=_singleCameraVm.StartCommand; + btnStop.Command=_singleCameraVm.StopCommand; + btnSelectRecipe.Command=_singleCameraVm.SelectRecipeCommand; + + lblCameraName.DataBindings.Add("Text", _singleCameraVm, nameof(_singleCameraVm.CameraName), true, DataSourceUpdateMode.OnPropertyChanged); + btnSelectRecipe.DataBindings.Add("Text", _singleCameraVm, nameof(_singleCameraVm.CurrentRecipeName), true, DataSourceUpdateMode.OnPropertyChanged); + + } + + public SingleCameraControl() + { + + InitializeComponent(); + } + } +} diff --git a/VisionBuilder.UI.Windows/Components/SingleCameraControl.resx b/VisionBuilder.UI.Windows/Components/SingleCameraControl.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/VisionBuilder.UI.Windows/Components/SingleCameraControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Components/Stats.Designer.cs b/VisionBuilder.UI.Windows/Components/Stats.Designer.cs new file mode 100644 index 0000000..d19b358 --- /dev/null +++ b/VisionBuilder.UI.Windows/Components/Stats.Designer.cs @@ -0,0 +1,95 @@ +namespace VisionBuilder.UI.Windows.Components +{ + partial class Stats + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + metaPanel = new FlowLayoutPanel(); + indicatorPanel = new FlowLayoutPanel(); + sideFlowLayoutPanel = new FlowLayoutPanel(); + sideFlowLayoutPanel.SuspendLayout(); + SuspendLayout(); + // + // metaPanel + // + metaPanel.AutoSize = true; + metaPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink; + metaPanel.BackColor = Color.White; + metaPanel.FlowDirection = FlowDirection.TopDown; + metaPanel.Location = new Point(3, 3); + metaPanel.MinimumSize = new Size(160, 32); + metaPanel.Name = "metaPanel"; + metaPanel.Size = new Size(160, 32); + metaPanel.TabIndex = 12; + // + // indicatorPanel + // + indicatorPanel.AutoSize = true; + indicatorPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink; + indicatorPanel.BackColor = Color.White; + indicatorPanel.FlowDirection = FlowDirection.TopDown; + indicatorPanel.Location = new Point(3, 50); + indicatorPanel.Margin = new Padding(3, 12, 3, 3); + indicatorPanel.MinimumSize = new Size(160, 32); + indicatorPanel.Name = "indicatorPanel"; + indicatorPanel.Size = new Size(160, 32); + indicatorPanel.TabIndex = 13; + // + // sideFlowLayoutPanel + // + sideFlowLayoutPanel.AutoSize = true; + sideFlowLayoutPanel.BackColor = Color.White; + sideFlowLayoutPanel.Controls.Add(metaPanel); + sideFlowLayoutPanel.Controls.Add(indicatorPanel); + sideFlowLayoutPanel.Dock = DockStyle.Fill; + sideFlowLayoutPanel.FlowDirection = FlowDirection.TopDown; + sideFlowLayoutPanel.Location = new Point(0, 0); + sideFlowLayoutPanel.Name = "sideFlowLayoutPanel"; + sideFlowLayoutPanel.Size = new Size(289, 731); + sideFlowLayoutPanel.TabIndex = 20; + // + // Stats + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + BackColor = Color.White; + Controls.Add(sideFlowLayoutPanel); + Name = "Stats"; + Size = new Size(289, 731); + sideFlowLayoutPanel.ResumeLayout(false); + sideFlowLayoutPanel.PerformLayout(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private FlowLayoutPanel metaPanel; + private FlowLayoutPanel indicatorPanel; + private FlowLayoutPanel sideFlowLayoutPanel; + } +} diff --git a/VisionBuilder.UI.Windows/Components/Stats.cs b/VisionBuilder.UI.Windows/Components/Stats.cs new file mode 100644 index 0000000..642b63d --- /dev/null +++ b/VisionBuilder.UI.Windows/Components/Stats.cs @@ -0,0 +1,115 @@ +using System.Drawing.Imaging; +using VisionBuilder.UI.Common.ViewModel; + +namespace VisionBuilder.UI.Windows.Components +{ + public partial class Stats : UserControl + { + + public void SetViewModel(StatisticsVM statisticsVm) + { + _statisticsVm = statisticsVm; + _statisticsVm.PropertyChanged += _statisticsVm_PropertyChanged; + } + + private void _statisticsVm_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(StatisticsVM.SessionStarted): + UpdateMeta("Started at", _statisticsVm.SessionStarted, true); + break; + case nameof(StatisticsVM.RecipeName): + UpdateMeta("Recipe", _statisticsVm.RecipeName, true); + break; + case nameof(StatisticsVM.ProcessingTime): + UpdateMeta("Processing time", _statisticsVm.ProcessingTime, true); + break; + case nameof(StatisticsVM.Good): + UpdateMeta("Good", _statisticsVm.Good.ToString(), true); + break; + case nameof(StatisticsVM.Bad): + UpdateMeta("Bad", _statisticsVm.Bad.ToString(), true); + break; + case nameof(StatisticsVM.Total): + UpdateMeta("Total", _statisticsVm.Total.ToString(), true); + break; + case nameof(StatisticsVM.StatisticsDetails): + UpdateMeta("Details", _statisticsVm.StatisticsDetails, false); + break; + } + } + + public Stats() + { + + InitializeComponent(); + + _imageGoodIndicator = new Bitmap(32, 32, PixelFormat.Format32bppArgb); + using (Graphics g = Graphics.FromImage(_imageGoodIndicator)) + { + g.Clear(Color.Transparent); + g.FillEllipse(Brushes.Green, 0, 0, 32, 32); + } + + _imageBadIndicator = new Bitmap(32, 32, PixelFormat.Format32bppArgb); + using (Graphics g = Graphics.FromImage(_imageBadIndicator)) + { + g.Clear(Color.Transparent); + g.FillEllipse(Brushes.Red, 0, 0, 32, 32); + } + } + private Dictionary _meta = new Dictionary(); + private readonly Bitmap _imageGoodIndicator; + private readonly Bitmap _imageBadIndicator; + private StatisticsVM _statisticsVm; + + public void UpdateMeta(string name, string text, bool bold=false) + { + Action a = new Action(() => + { + if (!_meta.ContainsKey(name)) + { + metaPanel.Controls.Add(new Label() { AutoSize = true, Margin = new Padding(0, 8, 0, 0), Text = name + ":", Font = new Font(new FontFamily("Verdana"), 8, FontStyle.Bold) }); + _meta[name] = new Label() { AutoSize = true, Margin = new Padding(0) }; + _meta[name].Font = new Font(new FontFamily("Verdana"), 8, bold?FontStyle.Bold:FontStyle.Regular); + metaPanel.Controls.Add(_meta[name]); + + } + _meta[name].Text = text; + + + metaPanel.Refresh(); + }); + + if (metaPanel.InvokeRequired) + { + metaPanel.Invoke(a); + } + else + { + a(); + } + } + + + + public static string ShortenString(string input, int maxLength) + { + if (input.Length <= maxLength) + { + return input; + } + else + { + return input.Substring(0, maxLength) + "..."; + } + } + + public void Clear() + { + _meta.Clear(); + metaPanel.Controls.Clear(); + } + } +} diff --git a/VisionBuilder.UI.Windows/Components/Stats.resx b/VisionBuilder.UI.Windows/Components/Stats.resx new file mode 100644 index 0000000..af32865 --- /dev/null +++ b/VisionBuilder.UI.Windows/Components/Stats.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Dialogs/ImagePreview.Designer.cs b/VisionBuilder.UI.Windows/Dialogs/ImagePreview.Designer.cs new file mode 100644 index 0000000..a9feab0 --- /dev/null +++ b/VisionBuilder.UI.Windows/Dialogs/ImagePreview.Designer.cs @@ -0,0 +1,168 @@ + +namespace Inspectron.HawkEye.View +{ + partial class ImagePreview + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + pictureBox1 = new PictureBox(); + materialRaisedButton1 = new MaterialSkin.Controls.MaterialRaisedButton(); + btnLearnThis = new MaterialSkin.Controls.MaterialRaisedButton(); + btnNext = new MaterialSkin.Controls.MaterialRaisedButton(); + btnPrev = new MaterialSkin.Controls.MaterialRaisedButton(); + lblErrName = new Label(); + ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); + SuspendLayout(); + // + // pictureBox1 + // + pictureBox1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + pictureBox1.Location = new Point(16, 168); + pictureBox1.Name = "pictureBox1"; + pictureBox1.Size = new Size(1540, 488); + pictureBox1.SizeMode = PictureBoxSizeMode.Zoom; + pictureBox1.TabIndex = 0; + pictureBox1.TabStop = false; + // + // materialRaisedButton1 + // + materialRaisedButton1.Anchor = AnchorStyles.Top | AnchorStyles.Right; + materialRaisedButton1.AutoSizeMode = AutoSizeMode.GrowAndShrink; + materialRaisedButton1.Cursor = Cursors.Hand; + materialRaisedButton1.Depth = 0; + materialRaisedButton1.DrawBorder = false; + materialRaisedButton1.Font = new Font("Verdana", 9F); + materialRaisedButton1.Icon = null; + materialRaisedButton1.Location = new Point(1384, 56); + materialRaisedButton1.MouseState = MaterialSkin.MouseState.HOVER; + materialRaisedButton1.Name = "materialRaisedButton1"; + materialRaisedButton1.Primary = true; + materialRaisedButton1.Size = new Size(168, 64); + materialRaisedButton1.TabIndex = 2; + materialRaisedButton1.Text = "Close"; + materialRaisedButton1.UseVisualStyleBackColor = true; + materialRaisedButton1.Click += materialRaisedButton1_Click; + // + // btnLearnThis + // + btnLearnThis.Anchor = AnchorStyles.Top | AnchorStyles.Right; + btnLearnThis.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnLearnThis.Cursor = Cursors.Hand; + btnLearnThis.Depth = 0; + btnLearnThis.DrawBorder = true; + btnLearnThis.Font = new Font("Verdana", 9F); + btnLearnThis.Icon = null; + btnLearnThis.Location = new Point(1032, 56); + btnLearnThis.MouseState = MaterialSkin.MouseState.HOVER; + btnLearnThis.Name = "btnLearnThis"; + btnLearnThis.Primary = false; + btnLearnThis.Size = new Size(168, 64); + btnLearnThis.TabIndex = 3; + btnLearnThis.Text = "Learn this"; + btnLearnThis.UseVisualStyleBackColor = true; + btnLearnThis.Click += btnLearnThis_Click; + // + // btnNext + // + btnNext.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnNext.Cursor = Cursors.Hand; + btnNext.Depth = 0; + btnNext.DrawBorder = true; + btnNext.Font = new Font("Verdana", 9F); + btnNext.Icon = null; + btnNext.Location = new Point(16, 56); + btnNext.MouseState = MaterialSkin.MouseState.HOVER; + btnNext.Name = "btnNext"; + btnNext.Primary = false; + btnNext.Size = new Size(168, 64); + btnNext.TabIndex = 4; + btnNext.Text = "Backwards"; + btnNext.UseVisualStyleBackColor = true; + btnNext.Visible = false; + btnNext.Click += btnNext_Click; + // + // btnPrev + // + btnPrev.Anchor = AnchorStyles.Top | AnchorStyles.Right; + btnPrev.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnPrev.Cursor = Cursors.Hand; + btnPrev.Depth = 0; + btnPrev.DrawBorder = true; + btnPrev.Font = new Font("Verdana", 9F); + btnPrev.Icon = null; + btnPrev.Location = new Point(1208, 56); + btnPrev.MouseState = MaterialSkin.MouseState.HOVER; + btnPrev.Name = "btnPrev"; + btnPrev.Primary = false; + btnPrev.Size = new Size(168, 64); + btnPrev.TabIndex = 5; + btnPrev.Text = "Forward"; + btnPrev.UseVisualStyleBackColor = true; + btnPrev.Visible = false; + btnPrev.Click += btnPrev_Click; + // + // lblErrName + // + lblErrName.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + lblErrName.BackColor = Color.White; + lblErrName.Location = new Point(16, 32); + lblErrName.Name = "lblErrName"; + lblErrName.Size = new Size(836, 16); + lblErrName.TabIndex = 6; + lblErrName.Text = "label1"; + lblErrName.TextAlign = ContentAlignment.MiddleLeft; + lblErrName.Visible = false; + // + // ImagePreview + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1568, 680); + Controls.Add(lblErrName); + Controls.Add(btnPrev); + Controls.Add(btnNext); + Controls.Add(btnLearnThis); + Controls.Add(materialRaisedButton1); + Controls.Add(pictureBox1); + MaximizeBox = false; + MinimizeBox = false; + Name = "ImagePreview"; + WindowState = FormWindowState.Maximized; + ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit(); + ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.PictureBox pictureBox1; + private MaterialSkin.Controls.MaterialRaisedButton materialRaisedButton1; + private MaterialSkin.Controls.MaterialRaisedButton btnLearnThis; + private MaterialSkin.Controls.MaterialRaisedButton btnNext; + private MaterialSkin.Controls.MaterialRaisedButton btnPrev; + private Label lblErrName; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Dialogs/ImagePreview.cs b/VisionBuilder.UI.Windows/Dialogs/ImagePreview.cs new file mode 100644 index 0000000..1838282 --- /dev/null +++ b/VisionBuilder.UI.Windows/Dialogs/ImagePreview.cs @@ -0,0 +1,74 @@ + +using System.ComponentModel; +using MaterialSkin.Controls; +using MaterialSkin.Core.Controls; +using OpenCvSharp.Extensions; +using VisionBuilder.UI.Common; +using VisionBuilder.UI.Common.ViewModel; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +namespace Inspectron.HawkEye.View +{ + public partial class ImagePreview : MaterialForm + { + private ErrorPreviewVM _previewVm; + + public void SetViewModel(ErrorPreviewVM errorPreviewVm) + { + _previewVm = errorPreviewVm; + + _previewVm.PropertyChanged += PreviewVmOnPropertyChanged; + btnLearnThis.Visible = _previewVm.LearnButtonVisible; + btnPrev.Visible = _previewVm.IsPreviousImageEnabled; + btnNext.Visible = _previewVm.IsNextImageEnabled; + pictureBox1.Image = _previewVm.ImageAnalysis?.ToBitmap(); + } + + private void PreviewVmOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(ErrorPreviewVM.ImageAnalysis): + pictureBox1.Image = _previewVm.ImageAnalysis?.ToBitmap(); + break; + case nameof(ErrorPreviewVM.LearnButtonVisible): + btnLearnThis.Visible = _previewVm.LearnButtonVisible; + break; + case nameof(ErrorPreviewVM.IsPreviousImageEnabled): + btnPrev.Visible = _previewVm.IsPreviousImageEnabled; + break; + case nameof(ErrorPreviewVM.IsNextImageEnabled): + btnNext.Visible = _previewVm.IsNextImageEnabled; + break; + + } + } + + public ImagePreview() + { + InitializeComponent(); + + } + + private void materialRaisedButton1_Click(object sender, EventArgs e) + { + Close(); + } + + private void btnLearnThis_Click(object sender, EventArgs e) + { + _previewVm.Learn(); + Close(); + } + + private void btnPrev_Click(object sender, EventArgs e) + { + _previewVm.PreviousImage(); + } + + private void btnNext_Click(object sender, EventArgs e) + { + _previewVm.NextImage(); + } + } +} diff --git a/VisionBuilder.UI.Windows/Dialogs/ImagePreview.resx b/VisionBuilder.UI.Windows/Dialogs/ImagePreview.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/VisionBuilder.UI.Windows/Dialogs/ImagePreview.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.Designer.cs b/VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.Designer.cs new file mode 100644 index 0000000..f077d02 --- /dev/null +++ b/VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.Designer.cs @@ -0,0 +1,85 @@ +namespace VisionBuilder.UI.Windows.Dialogs +{ + partial class RecipeSelectionDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + listView1 = new ListView(); + btnCancel = new MaterialSkin.Controls.MaterialRaisedButton(); + SuspendLayout(); + // + // listView1 + // + listView1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + listView1.BorderStyle = BorderStyle.None; + listView1.Font = new Font("Segoe UI", 12F, FontStyle.Regular, GraphicsUnit.Point, 204); + listView1.Location = new Point(8, 32); + listView1.MultiSelect = false; + listView1.Name = "listView1"; + listView1.Size = new Size(944, 575); + listView1.TabIndex = 4; + listView1.UseCompatibleStateImageBehavior = false; + listView1.MouseClick += listView1_MouseClick; + // + // btnCancel + // + btnCancel.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnCancel.Depth = 0; + btnCancel.DrawBorder = true; + btnCancel.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold, GraphicsUnit.Point, 204); + btnCancel.Icon = null; + btnCancel.Location = new Point(792, 624); + btnCancel.MouseState = MaterialSkin.MouseState.HOVER; + btnCancel.Name = "btnCancel"; + btnCancel.Primary = false; + btnCancel.Size = new Size(160, 48); + btnCancel.TabIndex = 5; + btnCancel.Text = "cancel"; + btnCancel.UseVisualStyleBackColor = true; + btnCancel.Click += btnCancel_Click; + // + // RecipeSelectionDialog + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(962, 688); + Controls.Add(btnCancel); + Controls.Add(listView1); + MaximizeBox = false; + MinimizeBox = false; + Name = "RecipeSelectionDialog"; + StartPosition = FormStartPosition.CenterScreen; + Text = "RecipeSelectionDialog"; + ResumeLayout(false); + } + + #endregion + + private ListView listView1; + private MaterialSkin.Controls.MaterialRaisedButton btnCancel; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.cs b/VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.cs new file mode 100644 index 0000000..d5b4dc3 --- /dev/null +++ b/VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.cs @@ -0,0 +1,77 @@ +using MaterialSkin.Controls; +using OpenCvSharp.XImgProc; +using System.Text.RegularExpressions; +using OpenCvSharp.Extensions; +using VisionBuilder.UI.Common.ViewModel; +using VisionBuilder.UI.Common.ViewModel.Classes; + +namespace VisionBuilder.UI.Windows.Dialogs +{ + public partial class RecipeSelectionDialog : MaterialForm + { + private readonly RecipeSelectionVM _recipeSelectionVm; + + public RecipeSelectionDialog(RecipeSelectionVM recipeSelectionVm) + { + _recipeSelectionVm = recipeSelectionVm; + + InitializeComponent(); + ReloadRecipes(); + } + + public void ReloadRecipes() + { + + var recipes = _recipeSelectionVm.Recipes.ToList(); + listView1.Items.Clear(); + + recipes = recipes.OrderBy(x => x.RecipeName).ToList(); + listView1.LargeImageList = new ImageList() { ColorDepth = ColorDepth.Depth24Bit }; + listView1.LargeImageList.ImageSize = new Size(128, 128); + + var plug = new Bitmap(128, 128); + using (Graphics g = Graphics.FromImage(plug)) + { + g.Clear(Color.Gray); + } + + listView1.LargeImageList.Images.Add("plug", plug); + + + + + recipes.ForEach(x => + { + + + + if (x.Image != null) + { + var image = x.Image.Resize(new OpenCvSharp.Size(128, 128)).ToBitmap(); + listView1.LargeImageList.Images.Add(x.RecipeName, image); + listView1.Items.Add(x.RecipeName, x.RecipeName, x.RecipeName).Tag = x; + } + else + { + listView1.Items.Add(x.RecipeName, x.RecipeName, "plug").Tag=x; + } + + }); + + } + private void listView1_MouseClick(object sender, MouseEventArgs e) + { + if (listView1.SelectedIndices.Count > 0) + { + _recipeSelectionVm.SelectedRecipe = (RecipeData)listView1.SelectedItems[0].Tag!; + } + DialogResult = DialogResult.OK; + } + + + private void btnCancel_Click(object sender, EventArgs e) + { + DialogResult = DialogResult.Cancel; + } + } +} diff --git a/VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.resx b/VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.resx new file mode 100644 index 0000000..af32865 --- /dev/null +++ b/VisionBuilder.UI.Windows/Dialogs/RecipeSelectionDialog.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Services/ImagePreviewService.cs b/VisionBuilder.UI.Windows/Services/ImagePreviewService.cs new file mode 100644 index 0000000..14fe3e6 --- /dev/null +++ b/VisionBuilder.UI.Windows/Services/ImagePreviewService.cs @@ -0,0 +1,22 @@ +using Inspectron.HawkEye.View; +using VisionBuilder.UI.Common; +using VisionBuilder.UI.Common.ViewModel; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +namespace VisionBuilder.UI.Windows.Settings; + +public class ImagePreviewService: IImagePreviewService +{ + + public ImagePreviewService() + { + + } + + public void ShowImagePreview(ErrorPreviewVM errorPreviewVm) + { + var window = new ImagePreview(); + window.SetViewModel(errorPreviewVm); + window.ShowDialog(); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Services/LoadingService.cs b/VisionBuilder.UI.Windows/Services/LoadingService.cs new file mode 100644 index 0000000..bfe3a76 --- /dev/null +++ b/VisionBuilder.UI.Windows/Services/LoadingService.cs @@ -0,0 +1,17 @@ +using MaterialSkin.Controls; +using VisionBuilder.UI.Common.RecipeProcessing; + +namespace VisionBuilder.UI.Windows.Settings; + +public class LoadingService: ILoadingService +{ + public void StartLoading(string title) + { + MaterialLoader.Instance.StartLoading(title); + } + + public void StopLoading(string title) + { + MaterialLoader.Instance.FinishLoading(title); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Services/RecipeSelectionDialogService.cs b/VisionBuilder.UI.Windows/Services/RecipeSelectionDialogService.cs new file mode 100644 index 0000000..f177581 --- /dev/null +++ b/VisionBuilder.UI.Windows/Services/RecipeSelectionDialogService.cs @@ -0,0 +1,18 @@ +using VisionBuilder.UI.Common.ViewModel; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; +using VisionBuilder.UI.Windows.Dialogs; + +namespace VisionBuilder.UI.Windows.Settings; + +public class RecipeSelectionDialogService: IRecipeSelectionDialogService +{ + public bool SelectRecipe(RecipeSelectionVM recipeSelectionVm) + { + RecipeSelectionDialog dialog = new RecipeSelectionDialog(recipeSelectionVm); + if(dialog.ShowDialog() == DialogResult.OK) + { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Services/SettingsService.cs b/VisionBuilder.UI.Windows/Services/SettingsService.cs new file mode 100644 index 0000000..2e1c78b --- /dev/null +++ b/VisionBuilder.UI.Windows/Services/SettingsService.cs @@ -0,0 +1,34 @@ +using Inspectron.Settings; +using Inspectron.Settings.Windows.Configuration; +using VisionBuilder.UI.Common; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +namespace VisionBuilder.UI.Windows.Settings; + +public class SettingsService: ISettingsService +{ + private readonly UIConfiguration _uiConfiguration; + private readonly InspectronSettings _settings; + + public SettingsService(UIConfiguration uiConfiguration, InspectronSettings settings) + { + _uiConfiguration = uiConfiguration ?? throw new ArgumentNullException(nameof(uiConfiguration)); + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + + } + + public void ShowSettingsDialog() + { + Thread th = new Thread(() => + { + OptionsWindow window = new OptionsWindow(new DefaultControlFactory()); + window.LoadFromSettings(_settings); + if (window.ShowDialog()==DialogResult.OK) + { + _settings.SaveSettings(); + } + }); + th.SetApartmentState(ApartmentState.STA); + th.Start(); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/VisionBuilder.UI.Windows.csproj b/VisionBuilder.UI.Windows/VisionBuilder.UI.Windows.csproj new file mode 100644 index 0000000..fc65df1 --- /dev/null +++ b/VisionBuilder.UI.Windows/VisionBuilder.UI.Windows.csproj @@ -0,0 +1,21 @@ + + + + net8.0-windows + enable + true + enable + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VisionBuilder.UI.sln b/VisionBuilder.UI.sln new file mode 100644 index 0000000..8f2d432 --- /dev/null +++ b/VisionBuilder.UI.sln @@ -0,0 +1,162 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35931.197 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Common", "VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj", "{5AFF312A-EF5F-49ED-BF54-5AC69F7BEC03}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Tests", "VisionBuilder.UI.Tests\VisionBuilder.UI.Tests.csproj", "{98781EAC-F3B2-4DEF-AFED-B0FEB8297025}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Windows", "VisionBuilder.UI.Windows\VisionBuilder.UI.Windows.csproj", "{B62D435C-3572-4B5B-A381-992345D343B0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaterialSkin.Core", "framework\MaterialSkin.Core\MaterialSkin.Core.csproj", "{D5FD2E9D-DA4F-1343-47E0-FBC473A149BC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Windows.Test", "VisionBuilder.UI.Windows.Test\VisionBuilder.UI.Windows.Test.csproj", "{7697A266-A721-4D74-9C5C-0D4F2F6BBF68}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inspectron.Settings", "framework\Inspectron.Settings\Inspectron.Settings.csproj", "{E286CE4C-B68A-94B6-F477-0DBF42358009}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inspectron.Settings.Windows", "framework\Inspectron.Settings.Windows\Inspectron.Settings.Windows.csproj", "{E8868ABD-E4D0-1B7E-494E-06FB1F7D1AF5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Recipes.HawkeyeRecipe", "VisionBuilder.UI.Recipes.HawkeyeRecipe\VisionBuilder.UI.Recipes.HawkeyeRecipe.csproj", "{8B7FAFEE-4066-483C-9C9C-10D2D596206D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder.Workflow", "Hawkeye.VisionBuilder.Workflow\Hawkeye.VisionBuilder.Workflow.csproj", "{C3EFD7A4-2CAD-7610-FEDE-D2E7484ED417}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Recipes", "Recipes", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Recipes.Scripted", "VisionBuilder.UI.Recipes.Scripted\VisionBuilder.UI.Recipes.Scripted.csproj", "{FC4698F6-C653-427F-9A42-22D07D466A14}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder", "Hawkeye.VisionBuilder\Hawkeye.VisionBuilder.csproj", "{8F6BA34A-D070-372B-DD72-B74905460744}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "framework", "framework", "{2B8C63F7-B7FD-464C-9045-180ED8F595F1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inspectron.Camera", "framework\Inspectron.Camera\Inspectron.Camera.csproj", "{4F937F88-70B7-A9E4-834A-D60B104686E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inspectron.Settings.WindowsWizard", "framework\Inspectron.Settings.WindowsWizard\Inspectron.Settings.WindowsWizard.csproj", "{0DAFEB64-E123-0974-B77C-3518AFD28D87}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sources", "Sources", "{D05689E3-04C6-4E3B-ACA7-3F4507CED4CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder.UI.Sources.Emulation", "Hawkeye.VisionBuilder.UI.Sources.Emulation\Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj", "{7AACFEF4-794A-42A3-9A05-9B6B4ECD5B82}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder.UI.RecipeConverter", "Hawkeye.VisionBuilder.UI.RecipeConverter\Hawkeye.VisionBuilder.UI.RecipeConverter.csproj", "{97AF69DC-5061-2643-3CC1-99B51976B05B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Recipes.Scripted.Tests", "VisionBuilder.UI.Recipes.Scripted.Tests\VisionBuilder.UI.Recipes.Scripted.Tests.csproj", "{376797BE-145E-4C48-BD7D-8BF8822A6BC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Statistics", "VisionBuilder.UI.Statistics\VisionBuilder.UI.Statistics.csproj", "{DBAE2469-ADC5-49CB-B6F5-4F440AF2E32B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inspectron.Statistics", "framework\Inspectron.Statistics\Inspectron.Statistics.csproj", "{51C2BC65-5E5D-E72A-988D-215E192DE889}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Ringbuffer", "VisionBuilder.UI.Ringbuffer\VisionBuilder.UI.Ringbuffer.csproj", "{1F9BB3BD-65C3-4B96-A880-4908CF6D9754}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inspectron.Fastbuffer", "framework\Inspectron.Fastbuffer\Inspectron.Fastbuffer.csproj", "{0A23E56A-DF4C-5174-18DF-18A771957AF4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{C828783C-1CE1-4245-8731-4AE18A28F590}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5AFF312A-EF5F-49ED-BF54-5AC69F7BEC03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AFF312A-EF5F-49ED-BF54-5AC69F7BEC03}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AFF312A-EF5F-49ED-BF54-5AC69F7BEC03}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AFF312A-EF5F-49ED-BF54-5AC69F7BEC03}.Release|Any CPU.Build.0 = Release|Any CPU + {98781EAC-F3B2-4DEF-AFED-B0FEB8297025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98781EAC-F3B2-4DEF-AFED-B0FEB8297025}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98781EAC-F3B2-4DEF-AFED-B0FEB8297025}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98781EAC-F3B2-4DEF-AFED-B0FEB8297025}.Release|Any CPU.Build.0 = Release|Any CPU + {B62D435C-3572-4B5B-A381-992345D343B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B62D435C-3572-4B5B-A381-992345D343B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B62D435C-3572-4B5B-A381-992345D343B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B62D435C-3572-4B5B-A381-992345D343B0}.Release|Any CPU.Build.0 = Release|Any CPU + {D5FD2E9D-DA4F-1343-47E0-FBC473A149BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5FD2E9D-DA4F-1343-47E0-FBC473A149BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5FD2E9D-DA4F-1343-47E0-FBC473A149BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5FD2E9D-DA4F-1343-47E0-FBC473A149BC}.Release|Any CPU.Build.0 = Release|Any CPU + {7697A266-A721-4D74-9C5C-0D4F2F6BBF68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7697A266-A721-4D74-9C5C-0D4F2F6BBF68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7697A266-A721-4D74-9C5C-0D4F2F6BBF68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7697A266-A721-4D74-9C5C-0D4F2F6BBF68}.Release|Any CPU.Build.0 = Release|Any CPU + {E286CE4C-B68A-94B6-F477-0DBF42358009}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E286CE4C-B68A-94B6-F477-0DBF42358009}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E286CE4C-B68A-94B6-F477-0DBF42358009}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E286CE4C-B68A-94B6-F477-0DBF42358009}.Release|Any CPU.Build.0 = Release|Any CPU + {E8868ABD-E4D0-1B7E-494E-06FB1F7D1AF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8868ABD-E4D0-1B7E-494E-06FB1F7D1AF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8868ABD-E4D0-1B7E-494E-06FB1F7D1AF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8868ABD-E4D0-1B7E-494E-06FB1F7D1AF5}.Release|Any CPU.Build.0 = Release|Any CPU + {8B7FAFEE-4066-483C-9C9C-10D2D596206D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B7FAFEE-4066-483C-9C9C-10D2D596206D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B7FAFEE-4066-483C-9C9C-10D2D596206D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B7FAFEE-4066-483C-9C9C-10D2D596206D}.Release|Any CPU.Build.0 = Release|Any CPU + {C3EFD7A4-2CAD-7610-FEDE-D2E7484ED417}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3EFD7A4-2CAD-7610-FEDE-D2E7484ED417}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3EFD7A4-2CAD-7610-FEDE-D2E7484ED417}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3EFD7A4-2CAD-7610-FEDE-D2E7484ED417}.Release|Any CPU.Build.0 = Release|Any CPU + {FC4698F6-C653-427F-9A42-22D07D466A14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC4698F6-C653-427F-9A42-22D07D466A14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC4698F6-C653-427F-9A42-22D07D466A14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC4698F6-C653-427F-9A42-22D07D466A14}.Release|Any CPU.Build.0 = Release|Any CPU + {8F6BA34A-D070-372B-DD72-B74905460744}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F6BA34A-D070-372B-DD72-B74905460744}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F6BA34A-D070-372B-DD72-B74905460744}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F6BA34A-D070-372B-DD72-B74905460744}.Release|Any CPU.Build.0 = Release|Any CPU + {4F937F88-70B7-A9E4-834A-D60B104686E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F937F88-70B7-A9E4-834A-D60B104686E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F937F88-70B7-A9E4-834A-D60B104686E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F937F88-70B7-A9E4-834A-D60B104686E0}.Release|Any CPU.Build.0 = Release|Any CPU + {0DAFEB64-E123-0974-B77C-3518AFD28D87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DAFEB64-E123-0974-B77C-3518AFD28D87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DAFEB64-E123-0974-B77C-3518AFD28D87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DAFEB64-E123-0974-B77C-3518AFD28D87}.Release|Any CPU.Build.0 = Release|Any CPU + {7AACFEF4-794A-42A3-9A05-9B6B4ECD5B82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AACFEF4-794A-42A3-9A05-9B6B4ECD5B82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AACFEF4-794A-42A3-9A05-9B6B4ECD5B82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AACFEF4-794A-42A3-9A05-9B6B4ECD5B82}.Release|Any CPU.Build.0 = Release|Any CPU + {97AF69DC-5061-2643-3CC1-99B51976B05B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97AF69DC-5061-2643-3CC1-99B51976B05B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97AF69DC-5061-2643-3CC1-99B51976B05B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97AF69DC-5061-2643-3CC1-99B51976B05B}.Release|Any CPU.Build.0 = Release|Any CPU + {376797BE-145E-4C48-BD7D-8BF8822A6BC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {376797BE-145E-4C48-BD7D-8BF8822A6BC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {376797BE-145E-4C48-BD7D-8BF8822A6BC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {376797BE-145E-4C48-BD7D-8BF8822A6BC5}.Release|Any CPU.Build.0 = Release|Any CPU + {DBAE2469-ADC5-49CB-B6F5-4F440AF2E32B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBAE2469-ADC5-49CB-B6F5-4F440AF2E32B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBAE2469-ADC5-49CB-B6F5-4F440AF2E32B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBAE2469-ADC5-49CB-B6F5-4F440AF2E32B}.Release|Any CPU.Build.0 = Release|Any CPU + {51C2BC65-5E5D-E72A-988D-215E192DE889}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51C2BC65-5E5D-E72A-988D-215E192DE889}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51C2BC65-5E5D-E72A-988D-215E192DE889}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51C2BC65-5E5D-E72A-988D-215E192DE889}.Release|Any CPU.Build.0 = Release|Any CPU + {1F9BB3BD-65C3-4B96-A880-4908CF6D9754}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F9BB3BD-65C3-4B96-A880-4908CF6D9754}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F9BB3BD-65C3-4B96-A880-4908CF6D9754}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F9BB3BD-65C3-4B96-A880-4908CF6D9754}.Release|Any CPU.Build.0 = Release|Any CPU + {0A23E56A-DF4C-5174-18DF-18A771957AF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A23E56A-DF4C-5174-18DF-18A771957AF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A23E56A-DF4C-5174-18DF-18A771957AF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A23E56A-DF4C-5174-18DF-18A771957AF4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D5FD2E9D-DA4F-1343-47E0-FBC473A149BC} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} + {E286CE4C-B68A-94B6-F477-0DBF42358009} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} + {E8868ABD-E4D0-1B7E-494E-06FB1F7D1AF5} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} + {8B7FAFEE-4066-483C-9C9C-10D2D596206D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {FC4698F6-C653-427F-9A42-22D07D466A14} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {4F937F88-70B7-A9E4-834A-D60B104686E0} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} + {0DAFEB64-E123-0974-B77C-3518AFD28D87} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} + {7AACFEF4-794A-42A3-9A05-9B6B4ECD5B82} = {D05689E3-04C6-4E3B-ACA7-3F4507CED4CC} + {376797BE-145E-4C48-BD7D-8BF8822A6BC5} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {DBAE2469-ADC5-49CB-B6F5-4F440AF2E32B} = {C828783C-1CE1-4245-8731-4AE18A28F590} + {51C2BC65-5E5D-E72A-988D-215E192DE889} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} + {1F9BB3BD-65C3-4B96-A880-4908CF6D9754} = {C828783C-1CE1-4245-8731-4AE18A28F590} + {0A23E56A-DF4C-5174-18DF-18A771957AF4} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3CE42AE5-D79F-4E97-A246-AA8FD228B677} + EndGlobalSection +EndGlobal diff --git a/framework/Inspectron.Camera/CameraExtensions.cs b/framework/Inspectron.Camera/CameraExtensions.cs new file mode 100644 index 0000000..782833b --- /dev/null +++ b/framework/Inspectron.Camera/CameraExtensions.cs @@ -0,0 +1,14 @@ +using Inspectron.Camera.Emulation; +using Ninject; + +namespace Inspectron.Camera +{ + public static class CameraExtensions + { + public static IKernel UseEmulationCamera(this IKernel self,string[] folders) + { + self.Rebind().ToConstant(new EmulationCameraProvider(folders)).InSingletonScope(); + return self; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/CameraParameter.cs b/framework/Inspectron.Camera/CameraParameter.cs new file mode 100644 index 0000000..fbac213 --- /dev/null +++ b/framework/Inspectron.Camera/CameraParameter.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Inspectron.Camera +{ + /// + /// Standard implementation of ICameraParameter interface + /// + public class CameraParameter : ICameraParameter, INotifyPropertyChanged + { + public CameraParameter(string name, Func get, Action set, + IEnumerable values = null) + { + Name = name; + this.setter = set; + this.getter = get; + Values = new ReadOnlyCollection(values != null ? + values.ToList() : new List()); + } + + public string Name { get; private set; } + + public object Value + { + get + { + return getter(); + } + set + { + if (setter == null) + throw new NotSupportedException("This is a read-only parameter"); + + setter(value); + + RaisePropertyChanged("Value"); + } + } + + public bool IsReadOnly { get { return setter == null; } } + + public ReadOnlyCollection Values { get; private set; } + + + Action setter; + Func getter; + + private void RaisePropertyChanged(string prop) + { + if (PropertyChanged != null) + PropertyChanged(this, new PropertyChangedEventArgs(prop)); + } + + public event PropertyChangedEventHandler PropertyChanged; + } +} diff --git a/framework/Inspectron.Camera/Emulation/EmulationCamera.cs b/framework/Inspectron.Camera/Emulation/EmulationCamera.cs new file mode 100644 index 0000000..6803d7a --- /dev/null +++ b/framework/Inspectron.Camera/Emulation/EmulationCamera.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Threading; +using Inspectron.Camera.Image; + +namespace Inspectron.Camera.Emulation +{ + public class EmulationCamera : ICamera + { + private string[] _selectedFiles = new string[0]; + private int _index = 0; + private string _currentFolder; + + public void Reset() + { + _index = 0; + } + + public EmulationCamera(ICameraProvider provider) + { + Provider = provider; + } + + public bool SingleRun { get; set; }=false; + + + public void SetFolder(string imagesFolder) + { + try + { + _currentFolder = imagesFolder; + var path = Path.Combine(@"..\Data\Emulation", imagesFolder); + _selectedFiles = Directory.GetFiles(path, "*.bmp") + .Concat(Directory.GetFiles(path, "*.png")) + .Concat(Directory.GetFiles(path, "*.jpg")) + .Where(x => !Path.GetFileNameWithoutExtension(x).Contains("_analysis")) + .ToArray(); + } + catch + { + } + } + + public void SetFolderAbsolute(string imagesFolder) + { + try + { + _currentFolder = imagesFolder; + var path = imagesFolder; + _selectedFiles = Directory.GetFiles(path, "*.bmp") + .Concat(Directory.GetFiles(path, "*.png")) + .Concat(Directory.GetFiles(path, "*.jpg")) + .Where(x => !Path.GetFileNameWithoutExtension(x).Contains("_analysis")) + .ToArray(); + } + catch + { + } + } + + public string Name { get; set; } = "Emulation"; + + public void Open() + { + IsOpen = true; + } + + public bool IsOpen { get; private set; } + + public void Close() + { + IsOpen = false; + } + + public bool IsGrabbingContinuous { get; } + + public void StartGrabContinuous() + { + throw new NotImplementedException(); + } + + public void StopGrabContinuous() + { + throw new NotImplementedException(); + } + + public void LoadParameters(string parametersFile) + { + } + + public void SaveParameters(string parametersFile) + { + throw new NotImplementedException(); + } + + public void SaveParametersToDevice() + { + throw new NotImplementedException(); + } + + public int CycleDelay { get; set; } = 1; + public int PerImageDelay { get; set; } = 5000; + public event Action OnLoopOver=delegate{}; + public IImage GrabSingle() + { + if (_selectedFiles.Length == 0) return null; + var id = (_index) % _selectedFiles.Length; + if (id == 0) + { + if(_index>0) + OnLoopOver(); + if (SingleRun&&_index>0) + { + // infinite sleep + Thread.Sleep(Timeout.Infinite); + } + Console.WriteLine("starting new emulation cycle"); + Thread.Sleep(CycleDelay); + } + else + { + Thread.Sleep(PerImageDelay); + } + + var path = _selectedFiles[id]; + _index++; + var bmp = new Bitmap(path); + + Console.WriteLine($"Getting image {path}"); + + return new BitmapImage(bmp); + } + + public event ImageGrabbedHandler ImageGrabbed; + public ICameraCapabilities Capabilities { get; } = new EmulationCameraCapabilities(); + + public IEnumerable Parameters => new List() + { + new CameraParameter("Folder", () => this.CurrentFolder, x => CurrentFolder = (string) x) + }; + + public ICameraProvider Provider { get; } + + public string CurrentFolder + { + get => _currentFolder; + set => _currentFolder = value; + } + + public int Index + { + get => _index; + set => _index = value; + } + + public IEnumerable<(Bitmap bmp, string path)> GetImages() + { + foreach (string selectedFile in _selectedFiles) + { + yield return (new Bitmap(selectedFile), selectedFile); + } + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/Emulation/EmulationCameraCapabilities.cs b/framework/Inspectron.Camera/Emulation/EmulationCameraCapabilities.cs new file mode 100644 index 0000000..1223413 --- /dev/null +++ b/framework/Inspectron.Camera/Emulation/EmulationCameraCapabilities.cs @@ -0,0 +1,10 @@ +namespace Inspectron.Camera.Emulation +{ + public class EmulationCameraCapabilities:ICameraCapabilities + { + public bool CanGrabSingle { get; } = true; + public bool CanGrabContinuous { get; } = false; + public bool CanSaveParametersToFile { get; } = false; + public bool CanSaveParametersToDevice { get; } = false; + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/Emulation/EmulationCameraProvider.cs b/framework/Inspectron.Camera/Emulation/EmulationCameraProvider.cs new file mode 100644 index 0000000..3528167 --- /dev/null +++ b/framework/Inspectron.Camera/Emulation/EmulationCameraProvider.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; + +namespace Inspectron.Camera.Emulation +{ + public class EmulationCameraProvider:ICameraProvider + { + + private EmulationCamera _emulationCamera; + + public EmulationCameraProvider(params string[] emulationFolders) + { + List cameras = new List(); + foreach (string s in emulationFolders) + { + _emulationCamera = new EmulationCamera(this); + _emulationCamera.Name = "Emulation "+Path.GetFileNameWithoutExtension(s); + _emulationCamera.SetFolder(s); + cameras.Add(_emulationCamera); + } + + Cameras=new ReadOnlyCollection(cameras); + } + + public string Name { get; } = "Emulation camera provider"; + public ReadOnlyCollection Discover() + { + return Cameras; + } + + public ReadOnlyCollection Cameras { get; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/Enums/EStandardRotation.cs b/framework/Inspectron.Camera/Enums/EStandardRotation.cs new file mode 100644 index 0000000..51414c5 --- /dev/null +++ b/framework/Inspectron.Camera/Enums/EStandardRotation.cs @@ -0,0 +1,10 @@ +namespace Inspectron.Camera.Enums +{ + public enum EStandardRotation + { + Rot0, + Rot90, + Rot180, + Rot270 + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/ICamera.cs b/framework/Inspectron.Camera/ICamera.cs new file mode 100644 index 0000000..cea9595 --- /dev/null +++ b/framework/Inspectron.Camera/ICamera.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using Inspectron.Camera.Image; + +namespace Inspectron.Camera +{ + public interface ICamera + { + /// + /// Returns the name of the camera + /// + string Name { get; } + + /// + /// Opens the camera + /// + void Open(); + + /// + /// Returns true if the camera is opened + /// + bool IsOpen { get; } + + /// + /// Closes the camera + /// + void Close(); + + /// + /// Returns true if the camera is live + /// + bool IsGrabbingContinuous { get; } + + /// + /// Stats live asynchronous acquisition. Images are transmitted the ImageGrabbed Event + /// + void StartGrabContinuous(); + + /// + /// Stops the asynchronous acquisition. + /// + void StopGrabContinuous(); + + /// + /// Loads the parameters from a filename + /// + /// + void LoadParameters(string parametersFile); + + /// + /// Saves the camera parameters to the given filename + /// + /// + void SaveParameters(string parametersFile); + + /// + /// Saves the parameters to the camera memory + /// + void SaveParametersToDevice(); + + /// + /// Synchronously grab an image + /// + /// + IImage GrabSingle(); + + /// + /// Event occuring when a new frame is acquired + /// + event ImageGrabbedHandler ImageGrabbed; + + /// + /// Returns the capabilities of the camera + /// + ICameraCapabilities Capabilities { get; } + + /// + /// Returns a Ienumerable containing all of the camera + /// + IEnumerable Parameters { get; } + + /// + /// Reference to Parent Camera Provider + /// + ICameraProvider Provider { get; } + } + /// + /// Handler called when an image has been grabbed + /// + /// the grabbed image + public delegate void ImageGrabbedHandler(ICamera sender, IImage image); +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/ICameraCapabilities.cs b/framework/Inspectron.Camera/ICameraCapabilities.cs new file mode 100644 index 0000000..0bfaff4 --- /dev/null +++ b/framework/Inspectron.Camera/ICameraCapabilities.cs @@ -0,0 +1,25 @@ +namespace Inspectron.Camera +{ + /// + /// Interface specifying the capabilities of the camera + /// + public interface ICameraCapabilities + { + /// + /// Indicates that handle synchronous acquisition. (see ICamera Grab method) + /// + bool CanGrabSingle { get; } + /// + /// Indicates that handle live asynchronous acquisition. Images are transmitted the ImageGrabbed Event. (see ICamera StartLive / StopLive methods) + /// + bool CanGrabContinuous { get; } + /// + /// Indicates that the camera can save and load the current parameter set to a file + /// + bool CanSaveParametersToFile { get; } + /// + /// Indicates that the camera can save the current parameter set to memory + /// + bool CanSaveParametersToDevice { get; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/ICameraParameter.cs b/framework/Inspectron.Camera/ICameraParameter.cs new file mode 100644 index 0000000..c863bf9 --- /dev/null +++ b/framework/Inspectron.Camera/ICameraParameter.cs @@ -0,0 +1,27 @@ +using System.Collections.ObjectModel; + +namespace Inspectron.Camera +{ + /// + /// ICameraParameter interface, standard interface to list and change camera parameters + /// + public interface ICameraParameter + { + /// + /// User friendly name + /// + string Name { get; } + /// + /// Getter and setter to values, null setter if readonly + /// + object Value { get; set; } + /// + /// Indicates that the parameter is readonly, Value setter is null + /// + bool IsReadOnly { get; } + /// + /// Values that can be chosen in case of fixed parameters set. + /// + ReadOnlyCollection Values { get; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/ICameraProvider.cs b/framework/Inspectron.Camera/ICameraProvider.cs new file mode 100644 index 0000000..a649966 --- /dev/null +++ b/framework/Inspectron.Camera/ICameraProvider.cs @@ -0,0 +1,22 @@ +using System.Collections.ObjectModel; + +namespace Inspectron.Camera +{ + public interface ICameraProvider + { + + /// + /// + /// + string Name { get; } + /// + /// + /// + /// + ReadOnlyCollection Discover(); + /// + /// + /// + ReadOnlyCollection Cameras { get; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/Image/BitmapImage.cs b/framework/Inspectron.Camera/Image/BitmapImage.cs new file mode 100644 index 0000000..97c8cc8 --- /dev/null +++ b/framework/Inspectron.Camera/Image/BitmapImage.cs @@ -0,0 +1,35 @@ + + +using System.Drawing; +using System.Drawing.Imaging; + +namespace Inspectron.Camera.Image +{ + public class BitmapImage : IImage + { + private readonly Bitmap _bitmap; + + public BitmapImage(Bitmap bitmap) + { + _bitmap = bitmap; + } + + public int Width => _bitmap.Width; + + public int Height => _bitmap.Height; + + public int Channels => throw new System.NotImplementedException(); + + public Bitmap Bitmap => (Bitmap)_bitmap.Clone(); + + public void Dispose() + { + + } + + public void Save(string path) + { + _bitmap.Save(path,ImageFormat.Bmp); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/Image/ByteImage.cs b/framework/Inspectron.Camera/Image/ByteImage.cs new file mode 100644 index 0000000..2f6388f --- /dev/null +++ b/framework/Inspectron.Camera/Image/ByteImage.cs @@ -0,0 +1,131 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; + +namespace Inspectron.Camera.Image +{ + public class ByteImage : IImage + { + + Byte[] data; + int step; + + /// + /// Initializes a newinstance with the given image dimensions and pixel-data + /// + /// the image width in pixel + /// the image height in pixel + /// the number of color channels (1-4) + /// the bit-depth per channel (8 or 16) + /// pointer to the pixel data in unmanaged memory + /// the number of bytes per image row + public ByteImage(int width, int height, int channels, + Byte[] data, int step) + { + Width = width; + Height = height; + Channels = channels; + + this.data = data; + this.step = step; + + ProcessBitmapCache(); + } + + private void ProcessBitmapCache() + { + var image = new Bitmap(Width, Height, + Channels == 1 ? PixelFormat.Format8bppIndexed : PixelFormat.Format24bppRgb); + var lockedDst = image.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.WriteOnly, + image.PixelFormat); + Marshal.Copy(data, 0, lockedDst.Scan0, data.Length); + image.UnlockBits(lockedDst); + if (Channels == 1) + { + image.Palette = _monoPalette; + } + + _cachedBMP = image; + } + + static ColorPalette GetGrayScalePalette() + { + Bitmap bmp = new Bitmap(1, 1, PixelFormat.Format8bppIndexed); + + ColorPalette monoPalette = bmp.Palette; + + Color[] entries = monoPalette.Entries; + + for (int i = 0; i < 256; i++) + { + entries[i] = Color.FromArgb(i, i, i); + } + + return monoPalette; + } + + private static ColorPalette _monoPalette = GetGrayScalePalette(); + public int Width { get; private set; } + public int Height { get; private set; } + public int Channels { get; private set; } + + public IImageLock Lock { get { return new ImageLock(this); } } + private Bitmap _cachedBMP = null; + public Bitmap Bitmap + { + get + { + + + return (Bitmap)_cachedBMP.Clone(); + } + } + + public byte[] Data + { + get => data; + set => data = value; + } + + public int Step + { + get => step; + set => step = value; + } + + class ImageLock : IImageLock + { + ByteImage parent; + GCHandle pinnedArray; + internal ImageLock(ByteImage parent) { this.parent = parent; } + + public int Step { get { return parent.step; } } + + public IntPtr PixelData + { + get + { + pinnedArray = GCHandle.Alloc(parent.data, GCHandleType.Pinned); + IntPtr pointer = pinnedArray.AddrOfPinnedObject(); + return pointer; + } + } + + public void Dispose() + { + pinnedArray.Free(); + } + } + public void Save(string path) + { + (new Bitmap(new MemoryStream(data))).Save(path,ImageFormat.Bmp); + } + + public void Dispose() + { + data = null; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/Image/Extensions.cs b/framework/Inspectron.Camera/Image/Extensions.cs new file mode 100644 index 0000000..1d71144 --- /dev/null +++ b/framework/Inspectron.Camera/Image/Extensions.cs @@ -0,0 +1,18 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; + +namespace Inspectron.Camera.Image +{ + public static class Extensions + { + public static ByteImage ToByteImage(this Bitmap self,int channels) + { + MemoryStream ms = new MemoryStream(); + var bmp = self; + bmp.Save(ms, ImageFormat.Bmp); + ByteImage bi = new ByteImage(bmp.Width, bmp.Height, channels, ms.ToArray(), bmp.Width); + return bi; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/Image/IImage.cs b/framework/Inspectron.Camera/Image/IImage.cs new file mode 100644 index 0000000..539a77c --- /dev/null +++ b/framework/Inspectron.Camera/Image/IImage.cs @@ -0,0 +1,14 @@ +using System; +using System.Drawing.Imaging; + +namespace Inspectron.Camera.Image +{ + public interface IImage:IDisposable + { + int Width { get; } + int Height { get; } + int Channels { get; } + void Save(string path); + System.Drawing.Bitmap Bitmap { get; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/Image/IImageLock.cs b/framework/Inspectron.Camera/Image/IImageLock.cs new file mode 100644 index 0000000..1d364ea --- /dev/null +++ b/framework/Inspectron.Camera/Image/IImageLock.cs @@ -0,0 +1,10 @@ +using System; + +namespace Inspectron.Camera.Image +{ + public interface IImageLock:IDisposable + { + int Step { get; } + IntPtr PixelData { get; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/Inspectron.Camera.csproj b/framework/Inspectron.Camera/Inspectron.Camera.csproj new file mode 100644 index 0000000..15ca3be --- /dev/null +++ b/framework/Inspectron.Camera/Inspectron.Camera.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + + + + + + + + + + + + + diff --git a/framework/Inspectron.Camera/NoImage/NoImageCamera.cs b/framework/Inspectron.Camera/NoImage/NoImageCamera.cs new file mode 100644 index 0000000..fa3d133 --- /dev/null +++ b/framework/Inspectron.Camera/NoImage/NoImageCamera.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Threading; +using Inspectron.Camera.Image; + +namespace Inspectron.Camera.NoImage +{ + public class NoImageCamera:ICamera + { + public NoImageCamera(ICameraProvider provider) + { + Provider = provider; + } + + private int _acquisitionDelay = 0; + public string Name { get; } + public void Open() + { + + } + + public bool IsOpen { get; } + public void Close() + { + + } + + public bool IsGrabbingContinuous { get; private set; } + + public void StartGrabContinuous() + { + IsGrabbingContinuous = true; + } + + public void StopGrabContinuous() + { + IsGrabbingContinuous = false; + } + + public void LoadParameters(string parametersFile) + { + throw new System.NotImplementedException(); + } + + public void SaveParameters(string parametersFile) + { + throw new System.NotImplementedException(); + } + + public void SaveParametersToDevice() + { + throw new System.NotImplementedException(); + } + + public int AcquisitionDelay + { + get => _acquisitionDelay; + set => _acquisitionDelay = value; + } + + public IImage GrabSingle() + { + Thread.Sleep(AcquisitionDelay); + return null; + } + + public event ImageGrabbedHandler ImageGrabbed=delegate{}; + public ICameraCapabilities Capabilities =>new NoImageCameraCapabilities(); + public IEnumerable Parameters =>new List() + { + new CameraParameter("AcquisitionDelay",() => this.AcquisitionDelay,x=>AcquisitionDelay=(int)x) + }; + public ICameraProvider Provider { get; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/NoImage/NoImageCameraCapabilities.cs b/framework/Inspectron.Camera/NoImage/NoImageCameraCapabilities.cs new file mode 100644 index 0000000..8725562 --- /dev/null +++ b/framework/Inspectron.Camera/NoImage/NoImageCameraCapabilities.cs @@ -0,0 +1,10 @@ +namespace Inspectron.Camera.NoImage +{ + public class NoImageCameraCapabilities:ICameraCapabilities + { + public bool CanGrabSingle { get; }=true; + public bool CanGrabContinuous { get; } = true; + public bool CanSaveParametersToFile { get; } = false; + public bool CanSaveParametersToDevice { get; } = false; + } +} \ No newline at end of file diff --git a/framework/Inspectron.Camera/NoImage/NoImageCameraProvider.cs b/framework/Inspectron.Camera/NoImage/NoImageCameraProvider.cs new file mode 100644 index 0000000..0e5f6a3 --- /dev/null +++ b/framework/Inspectron.Camera/NoImage/NoImageCameraProvider.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Inspectron.Camera.NoImage +{ + public class NoImageCameraProvider : ICameraProvider + { + public string Name { get; } = "No image camera provider"; + public int CamerasToDiscover { get; set; } = 1; + public ReadOnlyCollection Discover() + { + return Enumerable.Range(0, CamerasToDiscover).Select(x => (ICamera)new NoImageCamera(this)).ToList().AsReadOnly(); + } + + public ReadOnlyCollection Cameras { get; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Fastbuffer/Filesystems/AsyncFilesystem.cs b/framework/Inspectron.Fastbuffer/Filesystems/AsyncFilesystem.cs new file mode 100644 index 0000000..822cf25 --- /dev/null +++ b/framework/Inspectron.Fastbuffer/Filesystems/AsyncFilesystem.cs @@ -0,0 +1,82 @@ +using Inspectron.Fastbuffer.Interfaces; +using System.Collections.Concurrent; + +namespace Inspectron.Fastbuffer.Filesystems; + +public class AsyncFilesystem : IFilesystem +{ + private readonly DirectFilesystem _directFilesystem = new(); + private readonly ConcurrentDictionary _buffer = new(); + private readonly BlockingCollection<(string path, byte[] data)> _writeQueue = new(); + private readonly Thread _backgroundThread; + private readonly object _lock = new(); + + public AsyncFilesystem() + { + _backgroundThread = new Thread(ProcessWriteQueue) { IsBackground = true }; + _backgroundThread.Name= "AsyncFilesystem"; + _backgroundThread.Start(); + } + + public void Write(string path, byte[] data) + { + + lock (_lock) + { + _buffer[path] = data; + _writeQueue.Add((path, data)); + } + } + + public byte[] Read(string path) + { + lock (_lock) + { + if (_buffer.TryGetValue(path, out var data)) + { + return data; + } + } + return _directFilesystem.Read(path); + } + + public void Delete(string path) + { + + lock (_lock) + { + // pats is in buffer, so it's not written to disk yet + if (_buffer.TryRemove(path, out _)) + { + return; + } + _directFilesystem.Delete(path); + } + + } + + private void ProcessWriteQueue() + { + foreach (var (path, data) in _writeQueue.GetConsumingEnumerable()) + { + _directFilesystem.Write(path, data); + lock (_lock) + _buffer.TryRemove(path, out _); + + } + } + + public void Dispose() + { + _writeQueue.CompleteAdding(); + _backgroundThread.Join(); + } + + public void Clear() + { + lock (_lock) + { + while (_writeQueue.TryTake(out _)) { } + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Fastbuffer/Filesystems/DirectFilesystem.cs b/framework/Inspectron.Fastbuffer/Filesystems/DirectFilesystem.cs new file mode 100644 index 0000000..23ae172 --- /dev/null +++ b/framework/Inspectron.Fastbuffer/Filesystems/DirectFilesystem.cs @@ -0,0 +1,31 @@ +using Inspectron.Fastbuffer.Interfaces; +using Serilog; + +namespace Inspectron.Fastbuffer.Filesystems; + +public class DirectFilesystem:IFilesystem +{ + public void Write(string path, byte[] data) + { + // ensure path + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllBytes(path, data); + } + + public byte[] Read(string path) + { + return File.ReadAllBytes(path); + } + + public void Delete(string path) + { + Log.Debug($"Deleting {path}"); + if (File.Exists(path)) + File.Delete(path); + } + + public void Dispose() + { + + } +} \ No newline at end of file diff --git a/framework/Inspectron.Fastbuffer/IndexFile.cs b/framework/Inspectron.Fastbuffer/IndexFile.cs new file mode 100644 index 0000000..af805fc --- /dev/null +++ b/framework/Inspectron.Fastbuffer/IndexFile.cs @@ -0,0 +1,150 @@ +using System.Text; +using Inspectron.Fastbuffer.Interfaces; +using Serilog; + +namespace Inspectron.Fastbuffer; + +public class IndexFile +{ + private readonly string _indexFilePath; + private readonly int _bufferSize; + private readonly IFilesystem _filesystem; + private readonly string? _bufferPath; + + private const int RecordSize = 512; + private const int IndexSize = 4; + private const int HeaderSize = 4; + + public IndexFile(string path, int bufferSize, IFilesystem filesystem) + { + _indexFilePath = path; + _bufferSize = bufferSize; + _filesystem = filesystem; + ValidateFile(); + _bufferPath = Path.Combine(Path.GetDirectoryName(_indexFilePath), ".."); + } + + private void ValidateFile() + { + if (!File.Exists(_indexFilePath)) + { + CreateFile(); + } + // check file size + var fileInfo = new FileInfo(_indexFilePath); + if (fileInfo.Length != _bufferSize * RecordSize + HeaderSize) + { + UpdateFile(); + } + } + + private void UpdateFile() + { + using var fileStream = File.Open(_indexFilePath, FileMode.Open); + var actualSize = fileStream.Length; + if (actualSize < _bufferSize+ HeaderSize) + { + fileStream.Seek(0, SeekOrigin.End); + var buffer = new byte[_bufferSize * RecordSize - actualSize]; + fileStream.Write(buffer, HeaderSize, buffer.Length); + } + } + + private void CreateFile() + { + using var fileStream = File.Create(_indexFilePath); + var buffer = new byte[_bufferSize* RecordSize + HeaderSize]; + fileStream.Write(buffer, 0, buffer.Length); + fileStream.Seek(0, SeekOrigin.Begin); + var bytes = BitConverter.GetBytes(-1); + fileStream.Write(bytes, 0, bytes.Length); + } + + public int GetCurrentArtifactId() + { + using var fileStream = File.Open(_indexFilePath, FileMode.Open); + using var reader = new BinaryReader(fileStream); + return reader.ReadInt32(); + } + + public void SetCurrentArtifactId(int id) + { + using var fileStream = File.Open(_indexFilePath, FileMode.Open); + using var writer = new BinaryWriter(fileStream); + writer.Write(id); + } + + public string GetArtifactPath(int id) + { + using var fileStream = File.Open(_indexFilePath, FileMode.Open); + fileStream.Seek(id * RecordSize+HeaderSize, SeekOrigin.Begin); + byte[] bytes = new byte[RecordSize]; + fileStream.Read(bytes, 0, RecordSize); + return Encoding.UTF8.GetString(bytes).TrimEnd('\0'); + } + + public int GetNextId() + { + var currentId = GetCurrentArtifactId(); + if (currentId + 1 >= _bufferSize) + { + currentId = 0; + } + else + { + currentId++; + } + return currentId; + } + + public void DeleteArtifact(int id) + { + var relPath = GetArtifactPath(id); + var filePath = Path.Combine(_bufferPath, relPath); + try + { + _filesystem.Delete(filePath); + } + catch (Exception e) + { + Log.Error("Error deleting artifact {id}: {message}",id,e.Message); + } + + } + + public void WriteNextArtifact(byte[] data, string fileName) + { + var id = GetNextId(); + Log.Debug("Writing artifact {id}",id); + + var existingPath = GetArtifactPath(id); + Log.Debug("Existing path: {existingPath}", existingPath); + + DeleteArtifact(id); + var filePath = Path.Combine(_bufferPath, fileName); + _filesystem.Write(filePath, data); + SetCurrentArtifactId(id); + using var fileStream = File.Open(_indexFilePath, FileMode.Open); + fileStream.Seek(id * RecordSize+ HeaderSize, SeekOrigin.Begin); + var bytes = Encoding.UTF8.GetBytes(fileName); + fileStream.Write(bytes, 0, bytes.Length); + var padding = new byte[RecordSize - bytes.Length]; + fileStream.Write(padding, 0, padding.Length); + } + + public byte[] ReadArtifact(int id) + { + using var fileStream = File.Open(_indexFilePath, FileMode.Open); + fileStream.Seek(id * RecordSize+ HeaderSize, SeekOrigin.Begin); + var bytes = new byte[RecordSize]; + fileStream.Read(bytes, 0, RecordSize); + var fileName = Encoding.UTF8.GetString(bytes).TrimEnd('\0'); + var filePath = Path.Combine(_bufferPath, fileName); + return _filesystem.Read(filePath); + } + + + + + +} \ No newline at end of file diff --git a/framework/Inspectron.Fastbuffer/Inspectron.Fastbuffer.csproj b/framework/Inspectron.Fastbuffer/Inspectron.Fastbuffer.csproj new file mode 100644 index 0000000..4d752c1 --- /dev/null +++ b/framework/Inspectron.Fastbuffer/Inspectron.Fastbuffer.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/framework/Inspectron.Fastbuffer/Inspectron.Fastbuffer.sln b/framework/Inspectron.Fastbuffer/Inspectron.Fastbuffer.sln new file mode 100644 index 0000000..e07caaf --- /dev/null +++ b/framework/Inspectron.Fastbuffer/Inspectron.Fastbuffer.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.34928.147 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inspectron.Fastbuffer", "Inspectron.Fastbuffer.csproj", "{697FB408-0899-423C-8D67-C4ABDBED0C09}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inspectron.Fastbuffer.Tests", "..\Inspectron.Fastbuffer.Tests\Inspectron.Fastbuffer.Tests.csproj", "{5D1F5AAE-20DC-4C4A-B361-D1D42230BDF7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {697FB408-0899-423C-8D67-C4ABDBED0C09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {697FB408-0899-423C-8D67-C4ABDBED0C09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {697FB408-0899-423C-8D67-C4ABDBED0C09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {697FB408-0899-423C-8D67-C4ABDBED0C09}.Release|Any CPU.Build.0 = Release|Any CPU + {5D1F5AAE-20DC-4C4A-B361-D1D42230BDF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D1F5AAE-20DC-4C4A-B361-D1D42230BDF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D1F5AAE-20DC-4C4A-B361-D1D42230BDF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D1F5AAE-20DC-4C4A-B361-D1D42230BDF7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F3769CF4-894E-4BEE-9366-89E7505F27BF} + EndGlobalSection +EndGlobal diff --git a/framework/Inspectron.Fastbuffer/Interfaces/IFilesystem.cs b/framework/Inspectron.Fastbuffer/Interfaces/IFilesystem.cs new file mode 100644 index 0000000..8e6a358 --- /dev/null +++ b/framework/Inspectron.Fastbuffer/Interfaces/IFilesystem.cs @@ -0,0 +1,8 @@ +namespace Inspectron.Fastbuffer.Interfaces; + +public interface IFilesystem:IDisposable +{ + public void Write(string path, byte[] data); + public byte[] Read(string path); + public void Delete(string path); +} \ No newline at end of file diff --git a/framework/Inspectron.Fastbuffer/IsolatedRingbuffer.cs b/framework/Inspectron.Fastbuffer/IsolatedRingbuffer.cs new file mode 100644 index 0000000..b06ad3c --- /dev/null +++ b/framework/Inspectron.Fastbuffer/IsolatedRingbuffer.cs @@ -0,0 +1,38 @@ +using Inspectron.Fastbuffer.Interfaces; + +namespace Inspectron.Fastbuffer; + +public class IsolatedRingbuffer: IDisposable +{ + private readonly string _path; + private readonly int _bufferSize; + private readonly IFilesystem _filesystem; + private readonly string _indexPath; + private readonly IndexFile _index; + + public const string BufferDirectory = ".rb"; + public const string IndexFile = "index.idx"; + + public IsolatedRingbuffer(string path, int bufferSize, IFilesystem filesystem) + { + _path = path; + _bufferSize = bufferSize; + _filesystem = filesystem; + + _indexPath = Path.Combine(_path, BufferDirectory,IndexFile ); + Directory.CreateDirectory(Path.Combine(_path, BufferDirectory)); + + _index = new IndexFile(_indexPath, _bufferSize, _filesystem); + + } + + public void Write(byte[] data, string fileName) + { + _index.WriteNextArtifact(data, fileName); + } + + public void Dispose() + { + _filesystem.Dispose(); + } +} \ No newline at end of file diff --git a/framework/Inspectron.Fastbuffer/RingbufferRepository.cs b/framework/Inspectron.Fastbuffer/RingbufferRepository.cs new file mode 100644 index 0000000..a0c79c8 --- /dev/null +++ b/framework/Inspectron.Fastbuffer/RingbufferRepository.cs @@ -0,0 +1,52 @@ +using Inspectron.Fastbuffer.Interfaces; + +namespace Inspectron.Fastbuffer +{ + public class RingbufferRepository + { + private readonly string _path; + + private readonly string _goodIndexPath; + private readonly string _badIndexPath; + private readonly IndexFile _goodIndex; + private readonly IndexFile _badIndex; + public const string BufferDirectory = ".rb"; + public const string GoodIndexFile = "good.idx"; + public const string BadIndexFile = "bad.idx"; + public RingbufferRepository(string path, int goodBufferSize, int badBufferSize, IFilesystem filesystem) + { + _path = path; + + _goodIndexPath = Path.Combine(_path, BufferDirectory, GoodIndexFile); + _badIndexPath = Path.Combine(_path, BufferDirectory, BadIndexFile); + Directory.CreateDirectory(Path.Combine(_path,BufferDirectory)); + _goodIndex = new IndexFile(_goodIndexPath, goodBufferSize, filesystem); + _badIndex = new IndexFile(_badIndexPath, badBufferSize, filesystem); + + var id= _goodIndex.GetCurrentArtifactId(); + Console.WriteLine(@"RB size: "+id); + for (int i = 0; i < 3; i++) + { + var artifact = _goodIndex.GetArtifactPath(i); + Console.WriteLine(@"Artifact: "+artifact); + } + + } + + public IndexFile GoodIndex => _goodIndex; + + public IndexFile BadIndex => _badIndex; + + public void WriteGood(byte[] data, string fileName) + { + _goodIndex.WriteNextArtifact(data, fileName); + } + + public void WriteBad(byte[] data, string fileName) + { + _badIndex.WriteNextArtifact(data, fileName); + } + + + } +} diff --git a/framework/Inspectron.Ringbuffer/Index/GroupIndex.cs b/framework/Inspectron.Ringbuffer/Index/GroupIndex.cs new file mode 100644 index 0000000..ee9cb53 --- /dev/null +++ b/framework/Inspectron.Ringbuffer/Index/GroupIndex.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Inspectron.Ringbuffer.Interfaces; + +namespace Inspectron.Ringbuffer +{ + public class GroupIndex:IIndex + { + private readonly int _groupPathPart; + private readonly int _goodBadPathPart; + + public GroupIndex(int groupPathPart,int goodBadPathPart) + { + _groupPathPart = groupPathPart; + _goodBadPathPart = goodBadPathPart; + } + + + private readonly object _lock = new object(); + private readonly Dictionary _groups = new Dictionary(); + + + public void Track(string filePath) + { + + var parts = Path.GetDirectoryName(filePath).Split(Path.DirectorySeparatorChar); + + string groupKey; + try + { + groupKey = Path.Combine(Enumerable.Range(0, _groupPathPart + 1).Select(x => parts[x]).ToArray()); + } + catch (Exception e) + { + Console.WriteLine($"Tracking {filePath}"); + Console.WriteLine(e); + return; + } + + + string badGoodPart; + if (_goodBadPathPart == -1) badGoodPart = RingbufferPath.GOOD_DIR; + else + badGoodPart = parts[_goodBadPathPart]; + + lock (_lock) + { + if (!_groups.ContainsKey(groupKey)) + { + _groups[groupKey] = new ProductResults(); + } + + if (badGoodPart == RingbufferPath.GOOD_DIR) + { + _groups[groupKey].Good.Insert(0,filePath); + } + else + { + _groups[groupKey].Bad.Insert(0, filePath); + } + + } + } + + public List GetFilesToDelete(int allowedAmountGood, int allowedAmountBad) + { + lock (_lock) + { + var filesToDelete = new List(); + foreach (var p in _groups) + { + var goodToDelete = p.Value.Good.Skip(allowedAmountGood).ToList(); + var badToDelete = p.Value.Bad.Skip(allowedAmountBad).ToList(); + + goodToDelete.ForEach(x => p.Value.Good.Remove(x)); + badToDelete.ForEach(x => p.Value.Bad.Remove(x)); + filesToDelete.AddRange(goodToDelete); + filesToDelete.AddRange(badToDelete); + } + + return filesToDelete; + } + } + private class ProductResults + { + public List Good { get; set; }=new List(); + public List Bad { get; set; }=new List(); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/Index/ProductIndex.cs b/framework/Inspectron.Ringbuffer/Index/ProductIndex.cs new file mode 100644 index 0000000..ccf05da --- /dev/null +++ b/framework/Inspectron.Ringbuffer/Index/ProductIndex.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Inspectron.Ringbuffer.Interfaces; + +namespace Inspectron.Ringbuffer +{ + /// + /// Ringbuffer\Product\Good\File + /// + public class ProductIndex : IIndex + { + private readonly object _lock = new object(); + private readonly Dictionary _products = new Dictionary(); + + public void Track(string filePath) + { + var product = Path.GetDirectoryName(Path.GetDirectoryName(filePath)).Split(Path.DirectorySeparatorChar) + .Last(); + var result = Path.GetDirectoryName(filePath).Split(Path.DirectorySeparatorChar).Last(); + lock (_lock) + { + if (!_products.ContainsKey(product)) + _products[product] = new ProductResults + { + Good = new List(), + Bad = new List() + }; + + if (result == RingbufferFolder.GOOD_DIR) + { + if (!_products[product].Good.Contains(filePath)) + _products[product].Good.Insert(0, filePath); + } + + else + { + if (!_products[product].Bad.Contains(filePath)) + _products[product].Bad.Insert(0, filePath); + } + } + } + + public List GetFilesToDelete(int allowedAmountGood, int allowedAmountBad) + { + lock (_lock) + { + var filesToDelete = new List(); + foreach (var p in _products) + { + var goodToDelete = p.Value.Good.Skip(allowedAmountGood).ToList(); + var badToDelete = p.Value.Bad.Skip(allowedAmountGood).ToList(); + + goodToDelete.ForEach(x => p.Value.Good.Remove(x)); + badToDelete.ForEach(x => p.Value.Bad.Remove(x)); + filesToDelete.AddRange(goodToDelete); + filesToDelete.AddRange(badToDelete); + } + + return filesToDelete; + } + } + + private class ProductResults + { + public List Good { get; set; } + public List Bad { get; set; } + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/Index/RootIndex.cs b/framework/Inspectron.Ringbuffer/Index/RootIndex.cs new file mode 100644 index 0000000..8cdb42a --- /dev/null +++ b/framework/Inspectron.Ringbuffer/Index/RootIndex.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Inspectron.Ringbuffer.Interfaces; + +namespace Inspectron.Ringbuffer +{ + /// + /// Ringbuffer\Good\Product\File + /// + public class RootIndex : IIndex + { + private readonly List _trackedGoodFiles = new List(); + private readonly List _trackedBadFiles = new List(); + private readonly object _lock = new object(); + + public void Track(string filePath) + { + lock (_lock) + { + if (IsGood(filePath)) + { + if (!_trackedGoodFiles.Contains(filePath)) + _trackedGoodFiles.Insert(0, filePath); + } + else + { + if (!_trackedBadFiles.Contains(filePath)) + _trackedBadFiles.Insert(0, filePath); + } + } + } + + + public List GetFilesToDelete(int allowedAmountGood, int allowedAmountBad) + { + lock (_lock) + { + var resGood = _trackedGoodFiles.Skip(allowedAmountGood).ToList(); + var resBad = _trackedBadFiles.Skip(allowedAmountBad).ToList(); + + resGood.ForEach(x => { _trackedGoodFiles.Remove(x); }); + resBad.ForEach(x => { _trackedBadFiles.Remove(x); }); + + return resGood.Concat(resBad).ToList(); + } + } + + private bool IsGood(string filePath) + { + var folder = Path.GetDirectoryName(Path.GetDirectoryName(filePath)).Split(Path.DirectorySeparatorChar) + .Last(); + return folder == RingbufferFolder.GOOD_DIR; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/Inspectron.Ringbuffer.csproj b/framework/Inspectron.Ringbuffer/Inspectron.Ringbuffer.csproj new file mode 100644 index 0000000..85ea45e --- /dev/null +++ b/framework/Inspectron.Ringbuffer/Inspectron.Ringbuffer.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + Debug;Release;CPU + + + + + + + + + diff --git a/framework/Inspectron.Ringbuffer/Interfaces/IFileIdentity.cs b/framework/Inspectron.Ringbuffer/Interfaces/IFileIdentity.cs new file mode 100644 index 0000000..b7497d7 --- /dev/null +++ b/framework/Inspectron.Ringbuffer/Interfaces/IFileIdentity.cs @@ -0,0 +1,9 @@ +namespace Inspectron.Ringbuffer.Interfaces +{ + public interface IFileIdentity + { + string WriteTo(string directoryPath); + } + + +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/Interfaces/IFileIdentityFactory.cs b/framework/Inspectron.Ringbuffer/Interfaces/IFileIdentityFactory.cs new file mode 100644 index 0000000..58afc35 --- /dev/null +++ b/framework/Inspectron.Ringbuffer/Interfaces/IFileIdentityFactory.cs @@ -0,0 +1,8 @@ +namespace Inspectron.Ringbuffer.Interfaces +{ + public interface IFileIdentityFactory + { + IFileIdentity CreateFrom(T image); + IFileVerifier CreateVerifier(); + } +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/Interfaces/IFileVerifier.cs b/framework/Inspectron.Ringbuffer/Interfaces/IFileVerifier.cs new file mode 100644 index 0000000..90579f8 --- /dev/null +++ b/framework/Inspectron.Ringbuffer/Interfaces/IFileVerifier.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Inspectron.Ringbuffer.Interfaces +{ + public interface IFileVerifier + { + bool IsMainFile(string filePath); + List LinkedFiles(string filePath); + } +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/Interfaces/IFolderWritter.cs b/framework/Inspectron.Ringbuffer/Interfaces/IFolderWritter.cs new file mode 100644 index 0000000..b4ff044 --- /dev/null +++ b/framework/Inspectron.Ringbuffer/Interfaces/IFolderWritter.cs @@ -0,0 +1,8 @@ +namespace Inspectron.Ringbuffer.Interfaces +{ + public interface IFolderWritter + { + void WriteGood(IFileIdentity fileIdentity, string product); + void WriteBad(IFileIdentity fileIdentity, string product); + } +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/Interfaces/IIndex.cs b/framework/Inspectron.Ringbuffer/Interfaces/IIndex.cs new file mode 100644 index 0000000..dfdcd76 --- /dev/null +++ b/framework/Inspectron.Ringbuffer/Interfaces/IIndex.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Inspectron.Ringbuffer.Interfaces +{ + public interface IIndex + { + void Track(string filePath); + List GetFilesToDelete(int allowedAmountGood,int allowedAmountBad); + + } +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/RingbufferConfiguration.cs b/framework/Inspectron.Ringbuffer/RingbufferConfiguration.cs new file mode 100644 index 0000000..f27f757 --- /dev/null +++ b/framework/Inspectron.Ringbuffer/RingbufferConfiguration.cs @@ -0,0 +1,25 @@ +namespace Inspectron.Ringbuffer +{ + public class RingbufferConfiguration + { + /// + /// How many images should be present in Good folder + /// After reaching this amount older images will be deleted + /// + public int MaxAmountOfGoodImages { get; set; } + + /// + /// How many images should be present in Bad folder + /// After reaching this amount older images will be deleted + /// + public int MaxAmountOfBadImages { get; set; } + + /// + /// If enabled folder structure will be like "Good\MyProduct\001.bmp + /// If disabled folder structure will be like "MyProduct\Good\001.bmp + /// + public bool ProductFirst { get; set; } + + + } +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/RingbufferFolder.cs b/framework/Inspectron.Ringbuffer/RingbufferFolder.cs new file mode 100644 index 0000000..d32b967 --- /dev/null +++ b/framework/Inspectron.Ringbuffer/RingbufferFolder.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using Inspectron.Ringbuffer.Interfaces; + +namespace Inspectron.Ringbuffer +{ + [Obsolete] + public class RingbufferFolder:IFolderWritter + { + + private string _ringbufferFolder; + private RingbufferConfiguration _configuration; + private IFileVerifier _fileVerifier; + + private IIndex _index; + + public RingbufferFolder(string ringbufferFolder, RingbufferConfiguration configuration, IFileVerifier fileVerifier) + { + _ringbufferFolder = ringbufferFolder; + _configuration = configuration; + _fileVerifier = fileVerifier; + EnsureDirectory(_ringbufferFolder); + InitIndex(); + + + var th1 = new Thread(RemoveLoop); + th1.Priority = ThreadPriority.Normal; + th1.Start(); + + var th2 = new Thread(SaveLoop); + th2.Priority = ThreadPriority.Normal; + th2.Start(); + } + + private void RemoveLoop() + { + while (true) + { + var filesToDelete=_index.GetFilesToDelete(_configuration.MaxAmountOfGoodImages, _configuration.MaxAmountOfBadImages); + + filesToDelete.ForEach(x=> + { + DeleteFile(x); + _fileVerifier.LinkedFiles(x).ForEach(DeleteFile); + }); + + Thread.Sleep(1000); + } + } + + ConcurrentQueue _saveQueue = new ConcurrentQueue(); + + + object _lock=new object(); + + private void EnsureDirectory(string directory) + { + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + } + private void SaveLoop() + { + while (true) + { + if (_saveQueue.TryDequeue(out var candidate)) + { + lock (_lock) + { + EnsureDirectory(candidate.TargetDirectory); + var written=candidate.FileIdentity.WriteTo(candidate.TargetDirectory); + _index.Track(written); + } + } + else + { + Thread.Sleep(1); + } + } + } + + + + private void InitIndex() + { + if (_configuration.ProductFirst) + { + _index=new ProductIndex(); + } + else + { + _index=new RootIndex(); + } + IndexFiles(); + } + + public void DeleteFile(string fileToDelete) + { + var fi = new System.IO.FileInfo(fileToDelete); + if (fi.Exists) + { + lock (_lock) + { + fi.Delete(); + } + + fi.Refresh(); + while (fi.Exists) + { + System.Threading.Thread.Sleep(10); + fi.Refresh(); + } + } + } + + private void IndexFiles() + { + var allFiles=Directory.GetFiles(_ringbufferFolder, "*.*", SearchOption.AllDirectories); + var verifiedFiles = allFiles.Where(x => _fileVerifier.IsMainFile(x)).ToList(); + var orderedFiles = verifiedFiles.OrderByDescending(x => new FileInfo(x).CreationTimeUtc).ToList(); + orderedFiles.ForEach(_index.Track); + } + + public const string GOOD_DIR = "Good"; + public const string BAD_DIR = "Bad"; + public void WriteGood(IFileIdentity fileIdentity,string product) + { + var path = _configuration.ProductFirst + ? Path.Combine(_ringbufferFolder, product, GOOD_DIR) + : Path.Combine(_ringbufferFolder, GOOD_DIR, product); + + + _saveQueue.Enqueue(new SaveCandidate(fileIdentity, path)); + } + + public void WriteBad(IFileIdentity fileIdentity, string product) + { + var path = _configuration.ProductFirst + ? Path.Combine(_ringbufferFolder, product, BAD_DIR) + : Path.Combine(_ringbufferFolder, BAD_DIR, product); + + _saveQueue.Enqueue(new SaveCandidate(fileIdentity, path)); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/RingbufferPath.cs b/framework/Inspectron.Ringbuffer/RingbufferPath.cs new file mode 100644 index 0000000..602fed7 --- /dev/null +++ b/framework/Inspectron.Ringbuffer/RingbufferPath.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using Inspectron.Ringbuffer.Interfaces; + +namespace Inspectron.Ringbuffer +{ + public class RingbufferPath : IFolderWritter,IDisposable + { + private readonly string _pathFormat; + private readonly RingbufferPathConfiguration _configuration; + private readonly IFileVerifier _fileVerifier; + + public const string PRODUCT = "$PRODUCT$"; + public const string GOODBAD = "$GOODBAD$"; + public const string DATE = "$date$"; + public const string MONTH = "$month$"; + public const string YEAR = "$year$"; + + private IIndex _index; + ConcurrentQueue _saveQueue = new ConcurrentQueue(); + object _lock = new object(); + + private string _ringbufferFolder; + private int _productPartId; + private int _goodBadPartId; + private int _subPartId; + + public RingbufferPath(RingbufferPathConfiguration configuration, IFileVerifier fileVerifier) + { + _pathFormat = configuration.PathFormat; + _configuration = configuration; + _fileVerifier = fileVerifier; + + var pathFormat = configuration.PathFormat; + + var pathParts = Path.Combine(configuration.RootDirectory,pathFormat).Split(new char[]{ Path.DirectorySeparatorChar },StringSplitOptions.RemoveEmptyEntries); + + _ringbufferFolder = configuration.RootDirectory; + + _productPartId = -1; + _goodBadPartId = -1; + + for (int i = 1; i < pathParts.Length; i++) + { + if (pathParts[i] == PRODUCT) _productPartId = i; + if (pathParts[i] == GOODBAD) _goodBadPartId = i; + + } + + //if(_productPartId==-1||_goodBadPartId==-1)throw new Exception("one of the markers is missing"); + + EnsureDirectory(_ringbufferFolder); + InitIndex(); + + + var th1 = new Thread(RemoveLoop); + th1.IsBackground = true; + th1.Priority = ThreadPriority.Normal; + th1.Start(); + + var th2 = new Thread(SaveLoop); + th2.IsBackground = true; + th2.Priority = ThreadPriority.Normal; + th2.Start(); + } + + private void InitIndex() + { + _index = new GroupIndex(_configuration.GroupPartId,_goodBadPartId); + IndexFiles(); + } + private void IndexFiles() + { + + + var allFiles = Directory.GetFiles(_ringbufferFolder, "*.*", SearchOption.AllDirectories); + var verifiedFiles = allFiles.Where(x => _fileVerifier.IsMainFile(x)).ToList(); + var orderedFiles = verifiedFiles.OrderByDescending(x => new FileInfo(x).CreationTimeUtc).ToList(); + orderedFiles.ForEach(_index.Track); + } + + private void RemoveLoop() + { + while (!_stop) + { + var filesToDelete = _index.GetFilesToDelete(_configuration.MaxAmountOfGoodImages, _configuration.MaxAmountOfBadImages); + + filesToDelete.ForEach(x => + { + DeleteFile(x); + _fileVerifier.LinkedFiles(x).ForEach(DeleteFile); + }); + + Thread.Sleep(1000); + } + } + public void DeleteFile(string fileToDelete) + { + var fi = new System.IO.FileInfo(fileToDelete); + if (fi.Exists) + { + lock (_lock) + { + fi.Delete(); + } + + fi.Refresh(); + while (fi.Exists) + { + System.Threading.Thread.Sleep(10); + fi.Refresh(); + } + } + } + + private bool _stop = false; + private void SaveLoop() + { + while (!_stop) + { + if (_saveQueue.TryDequeue(out var candidate)) + { + lock (_lock) + { + EnsureDirectory(candidate.TargetDirectory); + var written = candidate.FileIdentity.WriteTo(candidate.TargetDirectory); + _index.Track(written); + } + } + else + { + Thread.Sleep(1); + } + } + } + private void EnsureDirectory(string directory) + { + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + } + public static string GOOD_DIR = "Good"; + public static string BAD_DIR = "Bad"; + + string TransformPath(string originalPath,bool isGood,string product) + { + var time = DateTime.Now; + + var res= originalPath.Replace(PRODUCT, product) + .Replace(GOODBAD, isGood ? GOOD_DIR : BAD_DIR) + .Replace(MONTH,time.ToString("MMMM")) + .Replace(YEAR,time.Year.ToString()) + .Replace(DATE,time.ToString("dd.MM.yyyy")); + return Path.Combine(_configuration.RootDirectory, res); + } + + public void WriteGood(IFileIdentity fileIdentity, string product) + { + var path = TransformPath(_configuration.PathFormat, true, product); + + + _saveQueue.Enqueue(new SaveCandidate(fileIdentity, path)); + } + + public void WriteBad(IFileIdentity fileIdentity, string product) + { + var path = TransformPath(_configuration.PathFormat, false, product); + + _saveQueue.Enqueue(new SaveCandidate(fileIdentity, path)); + } + + public void Dispose() + { + _stop = true; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/RingbufferPathConfiguration.cs b/framework/Inspectron.Ringbuffer/RingbufferPathConfiguration.cs new file mode 100644 index 0000000..fcb803d --- /dev/null +++ b/framework/Inspectron.Ringbuffer/RingbufferPathConfiguration.cs @@ -0,0 +1,35 @@ +namespace Inspectron.Ringbuffer +{ + public class RingbufferPathConfiguration + { + /// + /// How many images should be present in Good folder + /// After reaching this amount older images will be deleted + /// + public int MaxAmountOfGoodImages { get; set; } + + /// + /// How many images should be present in Bad folder + /// After reaching this amount older images will be deleted + /// + public int MaxAmountOfBadImages { get; set; } + + /// + /// Where to store images + /// Supports $PRODUCT$ mark to put product name into its place + /// Supports $GOODBAD$ mark to put product quality into its place + /// + public string PathFormat { get; set; } + + /// + /// Specifies which part of the path should be checked for maximum files + /// + public int GroupPartId { get; set; } + + /// + /// Ringbuffer's root directory + /// + public string RootDirectory { get; set; } = "./"; + + } +} \ No newline at end of file diff --git a/framework/Inspectron.Ringbuffer/SaveCandidate.cs b/framework/Inspectron.Ringbuffer/SaveCandidate.cs new file mode 100644 index 0000000..ca88434 --- /dev/null +++ b/framework/Inspectron.Ringbuffer/SaveCandidate.cs @@ -0,0 +1,16 @@ +using Inspectron.Ringbuffer.Interfaces; + +namespace Inspectron.Ringbuffer +{ + public class SaveCandidate + { + public SaveCandidate(IFileIdentity fileIdentity, string targetDirectory) + { + FileIdentity = fileIdentity; + TargetDirectory = targetDirectory; + } + + public IFileIdentity FileIdentity { get; set; } + public string TargetDirectory { get; set; } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings.Windows/Configuration/DefaultControlFactory.cs b/framework/Inspectron.Settings.Windows/Configuration/DefaultControlFactory.cs new file mode 100644 index 0000000..b90bb2e --- /dev/null +++ b/framework/Inspectron.Settings.Windows/Configuration/DefaultControlFactory.cs @@ -0,0 +1,187 @@ +using System; +using System.Reflection; +using System.Windows.Forms; +using Inspectron.Settings.Attributes; +using Microsoft.WindowsAPICodePack.Dialogs; + +namespace Inspectron.Settings.Windows.Configuration; + +public class DefaultControlFactory : IControlFactory +{ + public virtual Control CreateControl(string name, string description, PropertyInfo propertyInfo, + object initialValue, Action valueChangedCallback, OptionsWindow optionsWindow) + { + var type = propertyInfo.PropertyType; + + // Check for SettingDescriptionAttribute + var settingDescriptionAttribute = propertyInfo.GetCustomAttribute(); + string settingDescription = settingDescriptionAttribute?.Description; + + // Check for SettingPreviewAttribute + var previewAttribute = propertyInfo.GetCustomAttribute(); + Label previewLabel = null; + Func getPreview = null; + + if (previewAttribute != null) + { + var method = previewAttribute.PreviewClass.GetMethod(previewAttribute.PreviewFunction, BindingFlags.Public | BindingFlags.Static); + getPreview = value => (string)method.Invoke(null, new[] { value }); + previewLabel = new Label + { + AutoSize = true, + Padding = new Padding(15, 0, 0, 0), + Font = new System.Drawing.Font("Segoe UI", 10, System.Drawing.FontStyle.Italic), + Text = getPreview(initialValue) + }; + } + + if (type == typeof(string) && name.EndsWith("Path", StringComparison.OrdinalIgnoreCase)) + { + return CreatePathControl(name, description, initialValue, valueChangedCallback, optionsWindow, settingDescription, previewLabel, getPreview); + } + else if (type == typeof(string)) + { + return CreateStringControl(description, initialValue, valueChangedCallback, settingDescription, previewLabel, getPreview); + } + else if (type == typeof(int)) + { + return CreateIntControl(description, initialValue, valueChangedCallback, settingDescription, previewLabel, getPreview); + } + else if (type == typeof(bool)) + { + return CreateBoolControl(description, initialValue, valueChangedCallback, settingDescription, previewLabel, getPreview); + } + else if (type.IsEnum) + { + return CreateEnumControl(description, type, initialValue, valueChangedCallback, settingDescription, previewLabel, getPreview); + } + else + { + return new Label { Text = $"Unsupported type: {type.Name}", AutoSize = true }; + } + } + + private Control CreatePathControl(string name, string description, object initialValue, Action valueChangedCallback, + OptionsWindow optionsWindow, string settingDescription, Label previewLabel, Func getPreview) + { + var textBox = new TextBox { Text = initialValue as string, Width = 400 }; + var label = new Label { Text = description, AutoSize = true, Padding = new Padding(0, 5, 0, 0) }; + var browseButton = new Button { Text = "Browse", AutoSize = true }; + + browseButton.Click += (s, e) => + { + using (var dialog = new CommonOpenFileDialog { IsFolderPicker = true }) + { + if (dialog.ShowDialog(optionsWindow.Handle) == CommonFileDialogResult.Ok) + { + textBox.Text = dialog.FileName; + valueChangedCallback(dialog.FileName); + if (previewLabel != null && getPreview != null) + previewLabel.Text = getPreview(dialog.FileName); + } + } + }; + + textBox.TextChanged += (s, e) => + { + valueChangedCallback(textBox.Text); + if (previewLabel != null && getPreview != null) + previewLabel.Text = getPreview(textBox.Text); + }; + + return CreateOuterPanel(settingDescription, previewLabel, label, textBox, browseButton); + } + + private Control CreateStringControl(string description, object initialValue, Action valueChangedCallback, + string settingDescription, Label previewLabel, Func getPreview) + { + var textBox = new TextBox { Text = initialValue as string, Width = 400 }; + var label = new Label { Text = description, AutoSize = true, Padding = new Padding(0, 5, 0, 0) }; + + textBox.TextChanged += (s, e) => + { + valueChangedCallback(textBox.Text); + if (previewLabel != null && getPreview != null) + previewLabel.Text = getPreview(textBox.Text); + }; + + return CreateOuterPanel(settingDescription, previewLabel, label, textBox); + } + + private Control CreateIntControl(string description, object initialValue, Action valueChangedCallback, + string settingDescription, Label previewLabel, Func getPreview) + { + var label = new Label { Text = description, AutoSize = true, Padding = new Padding(0, 5, 0, 0) }; + var numericUpDown = new NumericUpDown { Minimum = 0, Maximum = 99999999999, Value = Convert.ToDecimal(initialValue), Width = 200 }; + + numericUpDown.ValueChanged += (s, e) => + { + valueChangedCallback(Convert.ToInt32(numericUpDown.Value)); + if (previewLabel != null && getPreview != null) + previewLabel.Text = getPreview(Convert.ToInt32(numericUpDown.Value)); + }; + + return CreateOuterPanel(settingDescription, previewLabel, label, numericUpDown); + } + + private Control CreateBoolControl(string description, object initialValue, Action valueChangedCallback, + string settingDescription, Label previewLabel, Func getPreview) + { + var checkBox = new CheckBox { Text = description, Checked = (bool)initialValue, AutoSize = true }; + + checkBox.CheckedChanged += (s, e) => + { + valueChangedCallback(checkBox.Checked); + if (previewLabel != null && getPreview != null) + previewLabel.Text = getPreview(checkBox.Checked); + }; + + return CreateOuterPanel(settingDescription, previewLabel, checkBox); + } + + private Control CreateEnumControl(string description, Type enumType, object initialValue, Action valueChangedCallback, + string settingDescription, Label previewLabel, Func getPreview) + { + var label = new Label { Text = description, AutoSize = true, Padding = new Padding(0, 5, 0, 0) }; + var comboBox = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 200 }; + + // Populate ComboBox with enum values + foreach (var value in Enum.GetValues(enumType)) + { + comboBox.Items.Add(value); + } + + // Set initial value + comboBox.SelectedItem = initialValue; + + comboBox.SelectedIndexChanged += (s, e) => + { + valueChangedCallback(comboBox.SelectedItem); + if (previewLabel != null && getPreview != null) + previewLabel.Text = getPreview(comboBox.SelectedItem); + }; + + return CreateOuterPanel(settingDescription, previewLabel, label, comboBox); + } + + private Control CreateOuterPanel(string settingDescription, Label previewLabel, params Control[] controls) + { + var outerPanel = new FlowLayoutPanel { FlowDirection = FlowDirection.TopDown, AutoSize = true, Margin = new Padding(0, 25, 0, 0) }; + + if (!string.IsNullOrEmpty(settingDescription)) + { + var descriptionLabel = new Label { Text = settingDescription, AutoSize = true, Padding = new Padding(0, 5, 0, 0) }; + outerPanel.Controls.Add(descriptionLabel); + } + + foreach (var control in controls) + { + outerPanel.Controls.Add(control); + } + + if (previewLabel != null) + outerPanel.Controls.Add(previewLabel); + + return outerPanel; + } +} diff --git a/framework/Inspectron.Settings.Windows/Configuration/IControlFactory.cs b/framework/Inspectron.Settings.Windows/Configuration/IControlFactory.cs new file mode 100644 index 0000000..d0620ec --- /dev/null +++ b/framework/Inspectron.Settings.Windows/Configuration/IControlFactory.cs @@ -0,0 +1,11 @@ +using System; +using System.Reflection; +using System.Windows.Forms; + +namespace Inspectron.Settings.Windows.Configuration; + +public interface IControlFactory +{ + Control CreateControl(string name, string description, PropertyInfo propertyInfo, object initialValue, + Action valueChangedCallback, OptionsWindow optionsWindow); +} diff --git a/framework/Inspectron.Settings.Windows/Configuration/OptionsSetting.cs b/framework/Inspectron.Settings.Windows/Configuration/OptionsSetting.cs new file mode 100644 index 0000000..5c51783 --- /dev/null +++ b/framework/Inspectron.Settings.Windows/Configuration/OptionsSetting.cs @@ -0,0 +1,12 @@ +using System.Reflection; + +namespace Inspectron.Settings.Windows.Configuration; + +internal class OptionSetting +{ + public object Owner { get; set; } + public PropertyInfo Property { get; set; } + public string Description { get; set; } + public string TreePath { get; set; } + public object Value { get; set; } // Temporary value until OK is pressed +} diff --git a/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs new file mode 100644 index 0000000..2f142fd --- /dev/null +++ b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs @@ -0,0 +1,69 @@ +using System.Drawing; +using System.Windows.Forms; + +namespace Inspectron.Settings.Windows.Configuration; + +public partial class OptionsWindow +{ + private TreeView treeViewCategories; + private FlowLayoutPanel flowLayoutPanelSettings; + private Button buttonOK; + private Button buttonCancel; + + private void InitializeComponent() + { + treeViewCategories = new TreeView(); + flowLayoutPanelSettings = new FlowLayoutPanel(); + buttonOK = new Button(); + buttonCancel = new Button(); + SuspendLayout(); + // + // treeViewCategories + // + treeViewCategories.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left; + treeViewCategories.Location = new Point(12, 12); + treeViewCategories.Name = "treeViewCategories"; + treeViewCategories.Size = new Size(200, 582); + treeViewCategories.TabIndex = 0; + // + // flowLayoutPanelSettings + // + flowLayoutPanelSettings.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + flowLayoutPanelSettings.AutoScroll = true; + flowLayoutPanelSettings.FlowDirection = FlowDirection.TopDown; + flowLayoutPanelSettings.Location = new Point(220, 12); + flowLayoutPanelSettings.Name = "flowLayoutPanelSettings"; + flowLayoutPanelSettings.Size = new Size(852, 582); + flowLayoutPanelSettings.TabIndex = 1; + flowLayoutPanelSettings.WrapContents = false; + // + // buttonOK + // + buttonOK.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + buttonOK.Location = new Point(912, 612); + buttonOK.Name = "buttonOK"; + buttonOK.Size = new Size(75, 23); + buttonOK.TabIndex = 2; + buttonOK.Text = "OK"; + // + // buttonCancel + // + buttonCancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + buttonCancel.Location = new Point(1002, 612); + buttonCancel.Name = "buttonCancel"; + buttonCancel.Size = new Size(75, 23); + buttonCancel.TabIndex = 3; + buttonCancel.Text = "Cancel"; + // + // OptionsWindow + // + ClientSize = new Size(1089, 649); + Controls.Add(treeViewCategories); + Controls.Add(flowLayoutPanelSettings); + Controls.Add(buttonOK); + Controls.Add(buttonCancel); + Name = "OptionsWindow"; + Text = "Options"; + ResumeLayout(false); + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.cs b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.cs new file mode 100644 index 0000000..769ad0b --- /dev/null +++ b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Windows.Forms; + +namespace Inspectron.Settings.Windows.Configuration; + +public partial class OptionsWindow : Form +{ + private Dictionary> settingsByCategory; + private IControlFactory controlFactory; + + public OptionsWindow(IControlFactory controlFactory) + { + this.controlFactory = controlFactory; + settingsByCategory = new Dictionary>(); + + InitializeComponent(); + this.treeViewCategories.AfterSelect += new TreeViewEventHandler(this.treeViewCategories_AfterSelect); + this.buttonOK.Click += new EventHandler(this.buttonOK_Click); + this.buttonCancel.Click += new EventHandler(this.buttonCancel_Click); + } + + public void LoadFromSettings(InspectronSettings inspectronSettings) + { + // UserSettings is a Tree with folders (string) and UserSettingsInfo nodes. + // Traverse the tree to find all UserSettingsInfo nodes and their PropertyDescriptors. + + void Traverse(object nodeObj, string path) + { + var node = nodeObj as dynamic; // Tree + object value = node.Value; + + if (value is string folderName) + { + // Folder node, append to path and traverse children + string newPath = string.IsNullOrEmpty(path) ? folderName : $"{path}/{folderName}"; + foreach (var child in node.Children) + Traverse(child, newPath); + } + else if (value is Inspectron.Settings.InspectronSettings.UserSettingsInfo info) + { + // Leaf node: user settings group + string groupName = info.Name; + string groupPath = string.IsNullOrEmpty(path) ? groupName : $"{path}/{groupName}"; + foreach (var pd in info.Settings) + { + // Try to get owner from BoundPropertyDescriptor, else skip + object owner = null; + string description = pd.Description; + if (pd is Inspectron.Settings.BoundPropertyDescriptor bpd) + { + owner = bpd.Owner; + if (string.IsNullOrEmpty(description)) + description = bpd.DisplayName; + } + else + { + // If not a BoundPropertyDescriptor, cannot get owner, skip + continue; + } + RegisterUserSetting(owner, bpd.PropertyInfo.Name, groupPath, description); + } + } + } + + Traverse(inspectronSettings.UserSettings.Root, ""); + } + + public void RegisterUserSetting(object owner, string propertyName, string treePath, string description = null) + { + // Get the PropertyInfo from owner and propertyName + var property = owner.GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); + if (property == null) + throw new ArgumentException($"Property '{propertyName}' not found on owner type."); + + if (string.IsNullOrEmpty(description)) + description = propertyName; + + // Create an OptionSetting instance + var setting = new OptionSetting + { + Owner = owner, + Property = property, + Description = description, + TreePath = treePath, + Value = property.GetValue(owner) + }; + + // Add to settingsByCategory + if (!settingsByCategory.TryGetValue(treePath, out var settingsList)) + { + settingsList = new List(); + settingsByCategory[treePath] = settingsList; + } + settingsList.Add(setting); + + // Add nodes to treeViewCategories + AddTreeNodes(treeViewCategories, treePath); + } + + private void AddTreeNodes(TreeView treeView, string treePath) + { + var parts = treePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + TreeNodeCollection nodes = treeView.Nodes; + foreach (var part in parts) + { + var node = Enumerable.Cast(nodes).FirstOrDefault(n => n.Text == part); + if (node == null) + { + node = new TreeNode(part); + nodes.Add(node); + } + nodes = node.Nodes; + } + } + + private void treeViewCategories_AfterSelect(object sender, TreeViewEventArgs e) + { + // Display the settings for the selected category + string selectedPath = GetFullPath(e.Node); + DisplaySettings(selectedPath); + } + + private string GetFullPath(TreeNode node) + { + if (node.Parent == null) + return node.Text; + else + return GetFullPath(node.Parent) + "/" + node.Text; + } + + private void DisplaySettings(string treePath) + { + flowLayoutPanelSettings.Controls.Clear(); + + if (settingsByCategory.TryGetValue(treePath, out var settingsList)) + { + foreach (var setting in settingsList) + { + var control = controlFactory.CreateControl( + setting.Property.Name, + setting.Description, + setting.Property, + setting.Value, + (newValue) => setting.Value = newValue, + this); + + flowLayoutPanelSettings.Controls.Add(control); + } + } + } + + private void buttonOK_Click(object sender, EventArgs e) + { + // Update the owner properties with the values + foreach (var settingsList in settingsByCategory.Values) + { + foreach (var setting in settingsList) + { + setting.Property.SetValue(setting.Owner, setting.Value); + } + } + + this.DialogResult = DialogResult.OK; + this.Close(); + } + + private void buttonCancel_Click(object sender, EventArgs e) + { + this.DialogResult = DialogResult.Cancel; + this.Close(); + } +} + diff --git a/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.resx b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/Inspectron.Settings.Windows/Inspectron.Settings.Windows.csproj b/framework/Inspectron.Settings.Windows/Inspectron.Settings.Windows.csproj new file mode 100644 index 0000000..4639b59 --- /dev/null +++ b/framework/Inspectron.Settings.Windows/Inspectron.Settings.Windows.csproj @@ -0,0 +1,16 @@ + + + + net8.0-windows + true + + + + + + + + + + + diff --git a/framework/Inspectron.Settings.Windows/PropHelper.cs b/framework/Inspectron.Settings.Windows/PropHelper.cs new file mode 100644 index 0000000..4341440 --- /dev/null +++ b/framework/Inspectron.Settings.Windows/PropHelper.cs @@ -0,0 +1,18 @@ +using System.Linq; +using System.Reflection; + +namespace Inspectron.Settings.Windows +{ + public static class PropHelper + { + public static PropertyInfo GetPrp(object obj,string name) + { + return obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) + .First(x => x.Name == name); + } + public static string SplitCamelCase(string input) + { + return System.Text.RegularExpressions.Regex.Replace(input, "([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim(); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.Designer.cs b/framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.Designer.cs new file mode 100644 index 0000000..5c311ee --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.Designer.cs @@ -0,0 +1,61 @@ +namespace Inspectron.Settings.WindowsWizard.Controls +{ + partial class CheckEditor + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.checkBox1 = new System.Windows.Forms.CheckBox(); + this.SuspendLayout(); + // + // checkBox1 + // + this.checkBox1.AutoSize = true; + this.checkBox1.Location = new System.Drawing.Point(3, 3); + this.checkBox1.Name = "checkBox1"; + this.checkBox1.Size = new System.Drawing.Size(98, 21); + this.checkBox1.TabIndex = 0; + this.checkBox1.Text = "checkBox1"; + this.checkBox1.UseVisualStyleBackColor = true; + this.checkBox1.CheckedChanged += new System.EventHandler(this.checkBox1_CheckedChanged); + // + // CheckEditor + // + this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.checkBox1); + this.Name = "CheckEditor"; + this.Size = new System.Drawing.Size(141, 29); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.CheckBox checkBox1; + } +} diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.cs b/framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.cs new file mode 100644 index 0000000..9439f25 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using Inspectron.Settings.WindowsWizard.Interfaces; + +namespace Inspectron.Settings.WindowsWizard.Controls +{ + public partial class CheckEditor : UserControl, ISetupItem + { + private PropertyInfo _prp; + private object _obj; + + public CheckEditor() + { + InitializeComponent(); + } + public void SetProperty(object obj, string propertyName) + { + checkBox1.Text = PropHelper.SplitCamelCase(propertyName); + _prp = PropHelper.GetPrp(obj, propertyName); + _obj = obj; + checkBox1.Checked = (bool)_prp.GetValue(obj); + } + + private void checkBox1_CheckedChanged(object sender, EventArgs e) + { + _prp.SetValue(_obj, checkBox1.Checked); + } + } +} diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.resx b/framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/CheckEditor.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.Designer.cs b/framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.Designer.cs new file mode 100644 index 0000000..f12ce4d --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.Designer.cs @@ -0,0 +1,46 @@ +namespace Inspectron.Settings.WindowsWizard.Controls +{ + partial class EnumEditor + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.SuspendLayout(); + // + // EnumEditor + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.Name = "EnumEditor"; + this.Size = new System.Drawing.Size(112, 51); + this.ResumeLayout(false); + + } + + #endregion + } +} diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.cs b/framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.cs new file mode 100644 index 0000000..04d7aba --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.cs @@ -0,0 +1,50 @@ +using System.Reflection; +using Inspectron.Settings.WindowsWizard.Interfaces; + +namespace Inspectron.Settings.WindowsWizard.Controls +{ + public partial class EnumEditor : UserControl,ISetupItem + { + private PropertyInfo _prp; + private object _obj; + + public EnumEditor() + { + InitializeComponent(); + } + + public void SetProperty(object obj, string propertyName) + { + GroupBox gb = new GroupBox(); + gb.Text = PropHelper.SplitCamelCase(propertyName); + this.Controls.Add(gb); + gb.Dock = DockStyle.Fill; + FlowLayoutPanel flp = new FlowLayoutPanel(); + + gb.Controls.Add(flp); + flp.Dock = DockStyle.Fill; + _prp = PropHelper.GetPrp(obj, propertyName); + _obj = obj; + var names = Enum.GetNames(_prp.PropertyType); + var values = Enum.GetValues(_prp.PropertyType); + var curValue = _prp.GetValue(_obj); + int cnt = 0; + foreach (string name in names) + { + var rb = new RadioButton() {Text = name}; + var cnt1 = cnt; + rb.CheckedChanged += (sender, args) => + { + if (rb.Checked) _prp.SetValue(_obj, values.GetValue(cnt1)); + }; + flp.Controls.Add(rb); + if ((int)curValue == (int)values.GetValue(cnt1)) + { + rb.Checked = true; + } + cnt++; + } + this.Controls.Add(gb); + } + } +} diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.resx b/framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/EnumEditor.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/NumberEditor.Designer.cs b/framework/Inspectron.Settings.WindowsWizard/Controls/NumberEditor.Designer.cs new file mode 100644 index 0000000..1d6fd43 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/NumberEditor.Designer.cs @@ -0,0 +1,73 @@ +namespace Inspectron.Settings.WindowsWizard.Controls +{ + partial class NumberEditor + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.label1 = new System.Windows.Forms.Label(); + this.numericUpDown1 = new System.Windows.Forms.NumericUpDown(); + ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit(); + this.SuspendLayout(); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(2, 5); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(35, 13); + this.label1.TabIndex = 0; + this.label1.Text = "label1"; + // + // numericUpDown1 + // + this.numericUpDown1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.numericUpDown1.Location = new System.Drawing.Point(109, 3); + this.numericUpDown1.Name = "numericUpDown1"; + this.numericUpDown1.Size = new System.Drawing.Size(147, 20); + this.numericUpDown1.TabIndex = 1; + this.numericUpDown1.ValueChanged += new System.EventHandler(this.numericUpDown1_ValueChanged); + // + // NumberEditor + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.numericUpDown1); + this.Controls.Add(this.label1); + this.Name = "NumberEditor"; + this.Size = new System.Drawing.Size(259, 25); + ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label label1; + private System.Windows.Forms.NumericUpDown numericUpDown1; + } +} diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/NumberEditor.cs b/framework/Inspectron.Settings.WindowsWizard/Controls/NumberEditor.cs new file mode 100644 index 0000000..708a629 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/NumberEditor.cs @@ -0,0 +1,56 @@ +using System.Reflection; +using Inspectron.Settings.WindowsWizard.Interfaces; + +namespace Inspectron.Settings.WindowsWizard.Controls +{ + public partial class NumberEditor : UserControl, ISetupItem + { + private PropertyInfo _prp; + private object _obj; + private int _number; + + public NumberEditor() + { + InitializeComponent(); + } + + public int Number + { + get { return _number; } + set + { + if (_obj == null) return; + _number = value; + _prp.SetValue(_obj, value); + } + } + + public int Min + { + get => (int)numericUpDown1.Minimum; + set => numericUpDown1.Minimum = value; + } + + public int Max + { + get => (int) numericUpDown1.Maximum; + set => numericUpDown1.Maximum = value; + } + + + public void SetProperty(object obj, string propertyName) + { + label1.Text = PropHelper.SplitCamelCase(propertyName); + _prp = PropHelper.GetPrp(obj, propertyName); + _obj = obj; + numericUpDown1.Value =_number = ((int)_prp.GetValue(obj) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.Designer.cs b/framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.Designer.cs new file mode 100644 index 0000000..10404e6 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.Designer.cs @@ -0,0 +1,90 @@ +namespace Inspectron.Settings.WindowsWizard.Controls +{ + partial class PathEditor + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.textBox1 = new System.Windows.Forms.TextBox(); + this.button1 = new System.Windows.Forms.Button(); + this.label1 = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // textBox1 + // + this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.textBox1.Location = new System.Drawing.Point(150, 2); + this.textBox1.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.textBox1.Name = "textBox1"; + this.textBox1.ReadOnly = true; + this.textBox1.Size = new System.Drawing.Size(274, 20); + this.textBox1.TabIndex = 1; + // + // button1 + // + this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.button1.Location = new System.Drawing.Point(428, 2); + this.button1.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(29, 19); + this.button1.TabIndex = 2; + this.button1.Text = "..."; + this.button1.UseVisualStyleBackColor = true; + this.button1.Click += new System.EventHandler(this.button1_Click); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(2, 5); + this.label1.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(35, 13); + this.label1.TabIndex = 3; + this.label1.Text = "label1"; + // + // PathEditor + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.label1); + this.Controls.Add(this.button1); + this.Controls.Add(this.textBox1); + this.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.Name = "PathEditor"; + this.Size = new System.Drawing.Size(460, 25); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.TextBox textBox1; + private System.Windows.Forms.Button button1; + private System.Windows.Forms.Label label1; + } +} diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.cs b/framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.cs new file mode 100644 index 0000000..f8e5358 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.cs @@ -0,0 +1,57 @@ +using System.Reflection; +using Inspectron.Settings.WindowsWizard.Interfaces; +using Microsoft.WindowsAPICodePack.Dialogs; + +namespace Inspectron.Settings.WindowsWizard.Controls +{ + public partial class PathEditor : UserControl,ISetupItem + { + + private PropertyInfo _prp; + private object _obj; + public PathEditor() + { + InitializeComponent(); + } + + public bool FolderPick { get; set; } + + public string SelectedFolderPath + { + get => textBox1.Text; + set + { + if (_obj == null) return; + textBox1.Text = value; + _prp.SetValue(_obj,value); + } + } + + private void button1_Click(object sender, EventArgs e) + { + var dialog = new CommonOpenFileDialog(); + dialog.IsFolderPicker = FolderPick; + dialog.InitialDirectory = SelectedFolderPath; + if (dialog.ShowDialog() == CommonFileDialogResult.Ok) + { + SelectedFolderPath = dialog.FileName; + FileChanged(); + } + } + + public event Action FileChanged = delegate { }; + public void SetProperty(object obj, string propertyName) + { + label1.Text = PropHelper.SplitCamelCase(propertyName); + _prp = PropHelper.GetPrp(obj, propertyName); + _obj = obj; + SelectedFolderPath =(string)_prp.GetValue(obj); + if(SelectedFolderPath!=null) FileChanged(); + } + + public void HidePath() + { + textBox1.Visible = false; + } + } +} diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.resx b/framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/PathEditor.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.Designer.cs b/framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.Designer.cs new file mode 100644 index 0000000..f4e2450 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.Designer.cs @@ -0,0 +1,72 @@ +namespace Inspectron.Settings.WindowsWizard.Controls +{ + partial class StringEditor + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.label1 = new System.Windows.Forms.Label(); + this.textBox1 = new System.Windows.Forms.TextBox(); + this.SuspendLayout(); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(3, 6); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(35, 13); + this.label1.TabIndex = 0; + this.label1.Text = "label1"; + // + // textBox1 + // + this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.textBox1.Location = new System.Drawing.Point(150, 3); + this.textBox1.Name = "textBox1"; + this.textBox1.Size = new System.Drawing.Size(176, 20); + this.textBox1.TabIndex = 1; + this.textBox1.TextChanged += new System.EventHandler(this.textBox1_TextChanged); + // + // StringEditor + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.textBox1); + this.Controls.Add(this.label1); + this.Name = "StringEditor"; + this.Size = new System.Drawing.Size(329, 28); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label label1; + private System.Windows.Forms.TextBox textBox1; + } +} diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.cs b/framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.cs new file mode 100644 index 0000000..79edeae --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.cs @@ -0,0 +1,41 @@ +using System.Reflection; +using Inspectron.Settings.WindowsWizard.Interfaces; + +namespace Inspectron.Settings.WindowsWizard.Controls +{ + public partial class StringEditor : UserControl, ISetupItem + { + private PropertyInfo _prp; + private object _obj; + private bool _password; + + public StringEditor() + { + InitializeComponent(); + } + + public bool Password + { + get { return _password; } + set + { + if (value) textBox1.PasswordChar = '*'; + _password = value; + } + } + + public void SetProperty(object obj, string propertyName) + { + _prp = PropHelper.GetPrp(obj, propertyName); + _obj = obj; + textBox1.Text = (string) _prp.GetValue(obj); + label1.Text = PropHelper.SplitCamelCase(propertyName); + } + + private void textBox1_TextChanged(object sender, EventArgs e) + { + if (_obj == null) return; + _prp.SetValue(_obj, textBox1.Text); + } + } +} diff --git a/framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.resx b/framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Controls/StringEditor.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/Inspectron.Settings.WindowsWizard/Enums/ESetupResult.cs b/framework/Inspectron.Settings.WindowsWizard/Enums/ESetupResult.cs new file mode 100644 index 0000000..55a6c38 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Enums/ESetupResult.cs @@ -0,0 +1,9 @@ +namespace Inspectron.Settings.WindowsWizard.Enums +{ + public enum ESetupResult + { + Cancel, + Back, + Next + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings.WindowsWizard/Inspectron.Settings.WindowsWizard.csproj b/framework/Inspectron.Settings.WindowsWizard/Inspectron.Settings.WindowsWizard.csproj new file mode 100644 index 0000000..aec55bb --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Inspectron.Settings.WindowsWizard.csproj @@ -0,0 +1,30 @@ + + + + net7.0-windows + true + enable + enable + Debug;Release;CPU + + + + + + + + + True + True + Resource1.resx + + + + + + ResXFileCodeGenerator + Resource1.Designer.cs + + + + diff --git a/framework/Inspectron.Settings.WindowsWizard/Interfaces/ISetupItem.cs b/framework/Inspectron.Settings.WindowsWizard/Interfaces/ISetupItem.cs new file mode 100644 index 0000000..77f7184 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Interfaces/ISetupItem.cs @@ -0,0 +1,7 @@ +namespace Inspectron.Settings.WindowsWizard.Interfaces +{ + public interface ISetupItem + { + void SetProperty(object obj,string propertyName); + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings.WindowsWizard/PropHelper.cs b/framework/Inspectron.Settings.WindowsWizard/PropHelper.cs new file mode 100644 index 0000000..000c496 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/PropHelper.cs @@ -0,0 +1,17 @@ +using System.Reflection; + +namespace Inspectron.Settings.WindowsWizard +{ + public static class PropHelper + { + public static PropertyInfo GetPrp(object obj,string name) + { + return obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) + .First(x => x.Name == name); + } + public static string SplitCamelCase(string input) + { + return System.Text.RegularExpressions.Regex.Replace(input, "([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim(); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings.WindowsWizard/Resource1.Designer.cs b/framework/Inspectron.Settings.WindowsWizard/Resource1.Designer.cs new file mode 100644 index 0000000..3ccd7fc --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Resource1.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Inspectron.Settings.WindowsWizard { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource1 { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource1() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Inspectron.Settings.WindowsWizard.Resource1", typeof(Resource1).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap setup { + get { + object obj = ResourceManager.GetObject("setup", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/framework/Inspectron.Settings.WindowsWizard/Resource1.resx b/framework/Inspectron.Settings.WindowsWizard/Resource1.resx new file mode 100644 index 0000000..5278131 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/Resource1.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Resources\setup.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/framework/Inspectron.Settings.WindowsWizard/Resources/setup.png b/framework/Inspectron.Settings.WindowsWizard/Resources/setup.png new file mode 100644 index 0000000000000000000000000000000000000000..17b26330569d9cbe6d70e22b92ba7d4df27aee4a GIT binary patch literal 134004 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∾; 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�Rs-UVS(4*zyWtLHm0> zE_5q-*k9e0XNdCAI`LiZT{IIWsa_MdVGcf%=S-2Pyt{rS&=-TYxsJd#%m_|Y8JmJmXcM&J0|Egdc# z&e=)vs;$U+au5p4NTBWG==wUhy^U{o?9-9|HO*=P@s3SIH@g@9siTD*@vtrImww_j zFr8Iz+O|(9eOJgyDEM=$#Z6wlb5)yd2ovE7wJgucA z&$o-+SCdOF8qlZJR-wa{yQ{sfB*HH;g8D8E8x^f*$;sWk-_>Kith48ji~R>%+>zYv zS~gkmJ%LNBY$-yz>VV6FSo0(7MljG&=fJV|RuSJZ=>f5%J#dy@26TIJhYy zqbM+FJD7->6VZoIk@iE~F@$+<0=83S+Kwe_ohG93NYIGyYAzNfcvVSsS!1Wx>;N9t zYDX>C1~f@aqs4pmvUs6cB*@Cm#`5f>AVOD}k2Cn@XnC^z&4elm_O5TszInugmRNEA z1vZUJ!a{=zC}Ms!j{mh7=%&z92=1eXs7_;cO6#N^?K@~|nG4U$MiEt7uGc(T3Gj9Q zG+3oXxpaRtB8ud_%1DPZpBb*}d9-d{m%eG66$?rOYwA7J?w2;OL1xp)q0Ah@tK-(Y#$`dB5W~ zY^14cA`UhsN{H9!k*F3Fuj}OKFgwTFE^u$GGXgVcH%K&Sqcf*@ci>l_*zLMvU{hFH zVJVzlz3({8)V6#FnJ(Tgq`z$wOY%!@P;JnTz*G!r)(h&_Ig}j;<@Z$8y@DCg;HQ5R ze)=ubK5=_es}=n_Ap!mLU%!3T^rNJY0XaDW7ERy8(k2XR=K7F!Lzj;APD*>yK{Magk+P_!sz?lD5HBRi-h>Qxeyc!M&Furw{TUVkcrp^o#LJN zpl0*A+K@b*y{ALT+N4~(TVhOta6$FbgzPk_Rh}TK;u9&}I3^x+B9H0*A1^ZGqX9-r zU33g>ac-H;$(w)71_>yJz4ZtG{4a({vwu4`N(U9anUxOS+*zCK%g7xzZ}+#cy^|c8 z_<xPn=S@aeKAY3(|Hm&crcMa9$*s5(pT*FVkDZt=x=^EWrT$1k@-!%W zrFR7i=+sVO%|H0zgTJE)%XtE*soQ6jUVn_Ez?vM!^_^iGLl zuXE}CTL~yc32@qIc=C!^#^S(=o zBk#nVl2b9g)Bfr6>C<_IWZuwd70YhefB%v8C0%>^A3x + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.panel1 = new System.Windows.Forms.Panel(); + this.checkBox1 = new System.Windows.Forms.CheckBox(); + this.button1 = new System.Windows.Forms.Button(); + this.panel2 = new System.Windows.Forms.Panel(); + this.pictureBox1 = new System.Windows.Forms.PictureBox(); + this.tabControl1 = new System.Windows.Forms.TabControl(); + this.panel1.SuspendLayout(); + this.panel2.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); + this.SuspendLayout(); + // + // panel1 + // + this.panel1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.panel1.Controls.Add(this.checkBox1); + this.panel1.Controls.Add(this.button1); + this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom; + this.panel1.Location = new System.Drawing.Point(0, 433); + this.panel1.Margin = new System.Windows.Forms.Padding(2); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(814, 34); + this.panel1.TabIndex = 0; + // + // checkBox1 + // + this.checkBox1.AutoSize = true; + this.checkBox1.Location = new System.Drawing.Point(4, 5); + this.checkBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.checkBox1.Name = "checkBox1"; + this.checkBox1.Size = new System.Drawing.Size(93, 19); + this.checkBox1.TabIndex = 1; + this.checkBox1.Text = "Expert mode"; + this.checkBox1.UseVisualStyleBackColor = true; + this.checkBox1.CheckedChanged += new System.EventHandler(this.checkBox1_CheckedChanged); + // + // button1 + // + this.button1.Location = new System.Drawing.Point(737, 2); + this.button1.Margin = new System.Windows.Forms.Padding(2); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(65, 22); + this.button1.TabIndex = 0; + this.button1.Text = "Done"; + this.button1.UseVisualStyleBackColor = true; + this.button1.Click += new System.EventHandler(this.button1_Click); + // + // panel2 + // + this.panel2.Controls.Add(this.pictureBox1); + this.panel2.Dock = System.Windows.Forms.DockStyle.Left; + this.panel2.Location = new System.Drawing.Point(0, 0); + this.panel2.Margin = new System.Windows.Forms.Padding(2); + this.panel2.Name = "panel2"; + this.panel2.Size = new System.Drawing.Size(175, 433); + this.panel2.TabIndex = 1; + // + // pictureBox1 + // + this.pictureBox1.Dock = System.Windows.Forms.DockStyle.Fill; + this.pictureBox1.Image = global::Inspectron.Settings.WindowsWizard.Resource1.setup; + this.pictureBox1.Location = new System.Drawing.Point(0, 0); + this.pictureBox1.Margin = new System.Windows.Forms.Padding(2); + this.pictureBox1.Name = "pictureBox1"; + this.pictureBox1.Size = new System.Drawing.Size(175, 433); + this.pictureBox1.TabIndex = 0; + this.pictureBox1.TabStop = false; + // + // tabControl1 + // + this.tabControl1.Location = new System.Drawing.Point(183, 14); + this.tabControl1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.tabControl1.Name = "tabControl1"; + this.tabControl1.SelectedIndex = 0; + this.tabControl1.Size = new System.Drawing.Size(617, 413); + this.tabControl1.TabIndex = 3; + // + // SetupPage + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(814, 467); + this.ControlBox = false; + this.Controls.Add(this.tabControl1); + this.Controls.Add(this.panel2); + this.Controls.Add(this.panel1); + this.Margin = new System.Windows.Forms.Padding(2); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "SetupPage"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Setup"; + this.panel1.ResumeLayout(false); + this.panel1.PerformLayout(); + this.panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Panel panel1; + private System.Windows.Forms.Button button1; + private System.Windows.Forms.Panel panel2; + private System.Windows.Forms.PictureBox pictureBox1; + private System.Windows.Forms.TabControl tabControl1; + private System.Windows.Forms.CheckBox checkBox1; + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings.WindowsWizard/SetupPage.cs b/framework/Inspectron.Settings.WindowsWizard/SetupPage.cs new file mode 100644 index 0000000..1f28068 --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/SetupPage.cs @@ -0,0 +1,156 @@ +using Inspectron.Settings.WindowsWizard.Enums; +using Inspectron.Settings.WindowsWizard.Interfaces; + +namespace Inspectron.Settings.WindowsWizard +{ + public partial class SetupPage : Form + { + public SetupPage() + { + InitializeComponent(); + } + + public SetupPage PreviousPage { get; set; } + public SetupPage NextPage { get; set; } + public FlowLayoutPanel CurrentLayout { get; set; } + + public static SetupPage Begin(string tabName,string title=null,Func expertModeCheck=null) + { + + var sp = new SetupPage(); + if (title != null) + { + sp.Text = title; + } + sp.ExpertModeCheck = expertModeCheck; + if (sp.ExpertModeCheck == null) + { + sp.checkBox1.Visible = false; + } + TabPage tp = new TabPage(); + tp.Text = tabName; + FlowLayoutPanel panel = new FlowLayoutPanel(); + panel.Dock = DockStyle.Fill; + panel.FlowDirection = FlowDirection.TopDown; + tp.Controls.Add(panel); + sp.tabControl1.TabPages.Add(tp); + sp.CurrentLayout = panel; + return sp; + } + + public Func ExpertModeCheck { get; set; } + List _expertPages=new List(); + public SetupPage Next(string tabName,bool forExpert=false) + { + TabPage tp = new TabPage(); + tp.Text = tabName; + + FlowLayoutPanel panel = new FlowLayoutPanel(); + panel.Dock = DockStyle.Fill; + panel.FlowDirection = FlowDirection.TopDown; + tp.Controls.Add(panel); + + + if (forExpert) + { + _expertPages.Add(tp); + } + else + { + this.tabControl1.TabPages.Add(tp); + } + this.CurrentLayout = panel; + + return this; + } + + public static string SplitCamelCase(string input) + { + return System.Text.RegularExpressions.Regex.Replace(input, "([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim(); + } + + public ESetupResult SetupResult { get; private set; } = ESetupResult.Cancel; + + public void ShowSetup() + { + SetupPage start = this; + while (start.PreviousPage!=null) + { + start = start.PreviousPage; + } + + start.Show(); + } + public void ShowSetupDialog() + { + SetupPage start = this; + while (start.PreviousPage != null) + { + start = start.PreviousPage; + } + + start.ShowDialog(); + } + public SetupPage FinishWith(Action setupFinished) + { + this.SetupFinished = setupFinished; + return this; + } + + public Action SetupFinished { get; set; } + + public SetupPage AddItem(ISetupItem item) + { + CurrentLayout.Controls.Add(item as UserControl); + return this; + } + + public SetupPage AddItem(object obj, string prp,Action itemAction=null) where T:ISetupItem,new() + { + T pe = new T(); + itemAction?.Invoke(pe); + pe.SetProperty(obj, prp); + (pe as UserControl).Width = 500; + + this.AddItem(pe); + return this; + } + + private void button1_Click(object sender, EventArgs e) + { + + SetupFinished?.Invoke(); + this.Close(); + + + + } + + private void checkBox1_CheckedChanged(object sender, EventArgs e) + { + if (checkBox1.Checked) + { + if ((ExpertModeCheck?.Invoke() ?? true)) + { + foreach (TabPage page in _expertPages) + { + this.tabControl1.TabPages.Add(page); + } + } + else + { + checkBox1.Checked = false; + } + + } + else + { + foreach (TabPage page in _expertPages) + { + this.tabControl1.TabPages.Remove(page); + + } + } + } + } +} diff --git a/framework/Inspectron.Settings.WindowsWizard/SetupPage.resx b/framework/Inspectron.Settings.WindowsWizard/SetupPage.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/framework/Inspectron.Settings.WindowsWizard/SetupPage.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/Inspectron.Settings/AdaptablePath.cs b/framework/Inspectron.Settings/AdaptablePath.cs new file mode 100644 index 0000000..c432a10 --- /dev/null +++ b/framework/Inspectron.Settings/AdaptablePath.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; + +namespace Inspectron.Settings +{ + public class AdaptablePath : Path, IAdaptable, IDecoratable + { + /// + /// Constructor + /// Single object making up the path + public AdaptablePath(T last) + : base(last) + { + } + + /// + /// Constructor + /// Path as sequence of objects + public AdaptablePath(IEnumerable path) + : base(path) + { + } + + /// + /// Constructor + /// Path as collection of objects + public AdaptablePath(ICollection path) + : base(path) + { + } + + #region IAdaptable, IDecoratable, and Related Methods + + /// + /// Gets an adapter of the specified type or null + /// Adapter type + /// Adapter of the specified type or null + public object GetAdapter(Type type) + { + object adapter = Last.As(type); + if (adapter != null) + return adapter; + + if (type.IsAssignableFrom(GetType())) + return this; + + return null; + } + + /// + /// Gets all decorators of the specified type + /// Decorator type + /// Enumeration of non-null decorators that are of the specified type. The enumeration may be empty. + public IEnumerable GetDecorators(Type type) + { + foreach (object obj in Last.AsAll(type)) + yield return obj; + } + + // implement the following members as a convenience when extension methods aren't available + + /// + /// Converts a reference to the given type by first trying a CLR cast, and then + /// trying to get an adapter + /// Desired type, must be ref type + /// Converted reference for the given object or null + public U As() + where U : class + { + return Adapters.As(this); + } + + /// + /// Converts a reference to the given type by first trying a CLR cast, and then + /// trying to get an adapter; if none is available, throws an AdaptationException + /// Desired type, must be ref type + /// Converted reference for the given object + public U Cast() where U : class + { + return Adapters.Cast(this); + } + + /// + /// Returns whether the given reference can be converted to one of + /// the desired type + /// Adapter type, must be ref type + /// True iff the given object can be converted + public bool Is() + where U : class + { + return Adapters.Is(this); + } + + /// + /// Returns an enumeration of all decorators that can convert a reference to the given type + /// Decorator type, must be ref type + /// Enumerable returning all decorators of the given type + public IEnumerable AsAll() + where U : class + { + return Adapters.AsAll(this); + } + + #endregion + + /// + /// Concatenates object with path + /// Prefix object + /// Optional path + /// Concatenated path, with lhs as first object + public static AdaptablePath operator +(T lhs, AdaptablePath rhs) + { + if (rhs == null) + return new AdaptablePath(lhs); + T[] path = new T[1 + rhs.Count]; + path[0] = lhs; + rhs.CopyTo(path, 1); + return new AdaptablePath(path); + } + + /// + /// Concatenates path with object + /// Optional path + /// Suffix object + /// Concatenated path, with rhs as last object + public static AdaptablePath operator +(AdaptablePath lhs, T rhs) + { + if (lhs == null) + return new AdaptablePath(rhs); + T[] path = new T[lhs.Count + 1]; + lhs.CopyTo(path, 0); + path[lhs.Count] = rhs; + return new AdaptablePath(path); + } + + /// + /// Concatenates 2 paths + /// First path. Can be null. + /// Second path. Can be null. + /// Concatenated path, with rhs as prefix and lhs as suffix. Is null if both lhs and rhs are null. + public static AdaptablePath operator +(AdaptablePath lhs, AdaptablePath rhs) + { + if (lhs == null) + return rhs; + if (rhs == null) + return lhs; + T[] path = new T[lhs.Count + rhs.Count]; + lhs.CopyTo(path, 0); + rhs.CopyTo(path, lhs.Count); + return new AdaptablePath(path); + } + + /// + /// Converts from the path type to another type + /// Desired type + /// Item to convert + /// Item converted to given type or null + protected override U Convert(T item) + { + U u = item.As(); + return u; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/AdaptationException.cs b/framework/Inspectron.Settings/AdaptationException.cs new file mode 100644 index 0000000..76553a3 --- /dev/null +++ b/framework/Inspectron.Settings/AdaptationException.cs @@ -0,0 +1,25 @@ +using System; + +namespace Inspectron.Settings +{ + public class AdaptationException : Exception + { + /// + /// Constructor + /// Message explaining why this object couldn't be adapted + public AdaptationException(string message) + : base(message) + { + } + + /// + /// Constructor + /// Message explaining why this object couldn't be adapted + /// The exception that prevented adaptation. Will become the + /// InnerException property. + public AdaptationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/Adapters.cs b/framework/Inspectron.Settings/Adapters.cs new file mode 100644 index 0000000..4ae66b8 --- /dev/null +++ b/framework/Inspectron.Settings/Adapters.cs @@ -0,0 +1,336 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Inspectron.Settings +{ + public static class Adapters + { + /// + /// Converts a reference to the given type by first trying a CLR cast, and then + /// trying to get an adapter + /// Reference to convert + /// Desired type, should be ref type + /// Converted reference for the given object or null + public static object As(this object reference, Type type) + { + if (reference == null) + return null; + + if (type == null) + throw new ArgumentNullException("type"); + + // is the adapted object compatible? + if (type.IsAssignableFrom(reference.GetType())) + return reference; + + // try to get an adapter + var adaptable = reference as IAdaptable; + if (adaptable != null) + { + object adapter = adaptable.GetAdapter(type); + if (adapter != null) + return adapter; + } + + return null; + } + + /// + /// Converts a reference to the given type by first trying a CLR cast, and then + /// trying to get an adapter + /// Desired type, must be ref type + /// Reference to convert + /// Converted reference for the given object or null + public static T As(this object reference) + where T : class + { + if (reference == null) + return null; + + // try a normal cast + var converted = reference as T; + + // if that fails, try to get an adapter + if (converted == null) + { + var adaptable = reference as IAdaptable; + if (adaptable != null) + converted = adaptable.GetAdapter(typeof(T)) as T; + } + + return converted; + } + + /// + /// Converts a reference to the given type by first trying a CLR cast, and then + /// trying to get an adapter + /// Desired type, must be ref type + /// Adaptable object + /// Converted reference for the given object or null + public static T As(this IAdaptable adaptable) + where T : class + { + if (adaptable == null) + return null; + + // try a normal cast + var converted = adaptable as T; + + // if that fails, try to get an adapter + if (converted == null) + converted = adaptable.GetAdapter(typeof(T)) as T; + + return converted; + } + + /// + /// Converts a reference to the given type by first trying a CLR cast, and then + /// trying to get an adapter; if none is available, throws an AdaptationException + /// Reference to convert + /// Desired type, should be ref type + /// Converted reference for the given object + public static object Cast(this object reference, Type type) + { + object converted = As(reference, type); + if (converted == null) + throw new AdaptationException(type.Name + " adapter required"); + return converted; + } + + /// + /// Converts a reference to the given type by first trying a CLR cast, and then + /// trying to get an adapter; if none is available, throws an AdaptationException + /// Desired type, must be ref type + /// Reference to convert + /// Converted reference for the given object + public static T Cast(this object reference) + where T : class + { + T converted = As(reference); + if (converted == null) + throw new AdaptationException(typeof(T).Name + " adapter required"); + return converted; + } + + /// + /// Converts a reference to the given type by first trying a CLR cast, and then + /// trying to get an adapter; if none is available, throws an AdaptationException + /// Desired type, must be ref type + /// Adaptable object + /// Converted reference for the given object + public static T Cast(this IAdaptable adaptable) + where T : class + { + T converted = As(adaptable); + if (converted == null) + throw new AdaptationException(typeof(T).Name + " adapter required"); + return converted; + } + + /// + /// Returns a value indicating if the given reference can be converted to one of + /// the desired type + /// Reference to test + /// Desired type, should be ref type + /// True iff the given object can be converted + public static bool Is(this object reference, Type type) + { + return As(reference, type) != null; + } + + /// + /// Returns a value indicating if the given reference can be converted to one of + /// the desired type + /// Adapter type, must be ref type + /// Reference to test + /// True iff the given object can be converted + public static bool Is(this object reference) + where T : class + { + return As(reference) != null; + } + + /// + /// Returns a value indicating if the given reference can be converted to one of + /// the desired type + /// Adapter type, must be ref type + /// Adaptable object + /// True iff the given object can be converted + public static bool Is(this IAdaptable adaptable) + where T : class + { + return As(adaptable) != null; + } + + /// + /// Gets all decorators that can convert a reference to the given type + /// Reference to convert + /// Decorator type, should be ref type + /// Enumerable returning all decorators of the given type. + /// Decorators are never null. The enumeration may be empty. + public static IEnumerable AsAll(this object reference, Type type) + { + if (reference != null) + { + // if IDecoratable, use that to get decorators + var decoratable = reference as IDecoratable; + if (decoratable != null) + return decoratable.GetDecorators(type); + + // is the decorated object compatible? + if (type.IsAssignableFrom(reference.GetType())) + return new object[] { reference }; + } + + return new List(); + } + + /// + /// Gets all decorators that can convert a reference to the given type + /// Decorator type, must be ref type + /// Reference to convert + /// Enumerable returning all decorators of the given type. + /// Decorators are never null. The enumeration may be empty. + public static IEnumerable AsAll(this object reference) + where T : class + { + if (reference != null) + { + // if IDecoratable, use that to get decorators + var decoratable = reference as IDecoratable; + if (decoratable != null) + return AsAll(decoratable); + + // otherwise, cast + var t = reference as T; + if (t != null) + return new T[] { t }; + } + + return new List(); + } + + /// + /// Gets an enumeration of all decorators that can convert a reference to the given type + /// Decorator type, must be ref type + /// Decoratable object + /// Enumerable returning all decorators of the given type + public static IEnumerable AsAll(this IDecoratable decoratable) + where T : class + { + if (decoratable != null) + { + foreach (object decorator in decoratable.GetDecorators(typeof(T))) + yield return decorator as T; + } + } + + /// + /// Gets an adapter that converts an enumerable to an enumerable of another type + /// Enumerable to adapt + /// Adapter type, should be ref type + /// Enumerable returning adapted items + public static IEnumerable AsIEnumerable(this IEnumerable enumerable, Type type) + { + if (enumerable != null) + { + foreach (object item in enumerable) + { + object adapter = As(item, type); + if (adapter != null) + yield return adapter; + } + } + } + + /// + /// Returns an enumeration for an adapter that converts an enumerable to an enumerable of another type + /// Adapter type, must be ref type + /// Enumerable to adapt + /// Enumerable returning adapted items + public static IEnumerable AsIEnumerable(this IEnumerable enumerable) + where T : class + { + if (enumerable != null) + { + foreach (object item in enumerable) + { + T adapter = As(item); + if (adapter != null) + yield return adapter; + } + } + } + + /// + /// Returns a value indicating if any of the items in the enumerable are + /// adaptable to the given type + /// Enumerable to adapt + /// Adapter type, should be ref type + /// True iff any of the items in the enumerable are adaptable to + /// the given type + public static bool Any(this IEnumerable enumerable, Type type) + { + if (enumerable != null) + { + foreach (object item in enumerable) + { + object adapter = As(item, type); + if (adapter != null) + return true; + } + } + + return false; + } + + /// + /// Returns a value indicating if any of the items in the enumerable are + /// adaptable to the given type + /// Adapter type, must be ref type + /// Enumerable to adapt + /// True iff any of the items in the enumerable are adaptable to + /// the given type + public static bool Any(this IEnumerable enumerable) + where T : class + { + return Any(enumerable, typeof(T)); + } + + /// + /// Returns a value indicating if all of the items in the enumerable are + /// adaptable to the given type + /// Enumerable to adapt + /// Adapter type, should be ref type + /// True iff all of the items in the enumerable are adaptable to + /// the given type + public static bool All(this IEnumerable enumerable, Type type) + { + if (enumerable != null) + { + foreach (object item in enumerable) + { + object adapter = As(item, type); + if (adapter == null) + return false; + } + } + + return true; + } + + /// + /// Returns a value indicating if all of the items in the enumerable are + /// adaptable to the given type + /// Adapter type, must be ref type + /// Enumerable to adapt + /// True iff all of the items in the enumerable are adaptable to + /// the given type + public static bool All(this IEnumerable enumerable) + where T : class + { + return All(enumerable, typeof(T)); + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/Attributes/SettingDescriptionAttribute.cs b/framework/Inspectron.Settings/Attributes/SettingDescriptionAttribute.cs new file mode 100644 index 0000000..781e4ba --- /dev/null +++ b/framework/Inspectron.Settings/Attributes/SettingDescriptionAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace Inspectron.Settings.Attributes +{ + public class SettingDescriptionAttribute: Attribute + { + public string Description { get; } + + public SettingDescriptionAttribute(string description) + { + Description = description; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/Attributes/SettingPreviewAttribute.cs b/framework/Inspectron.Settings/Attributes/SettingPreviewAttribute.cs new file mode 100644 index 0000000..5212dac --- /dev/null +++ b/framework/Inspectron.Settings/Attributes/SettingPreviewAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace Inspectron.Settings.Attributes +{ + public class SettingPreviewAttribute: System.Attribute + { + public Type PreviewClass { get; } + public string PreviewFunction { get; } + + + public SettingPreviewAttribute(Type previewClass, string previewFunction) + { + PreviewClass = previewClass; + PreviewFunction = previewFunction; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/BoundPropertyDescriptor.cs b/framework/Inspectron.Settings/BoundPropertyDescriptor.cs new file mode 100644 index 0000000..8341759 --- /dev/null +++ b/framework/Inspectron.Settings/BoundPropertyDescriptor.cs @@ -0,0 +1,494 @@ +using System; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; + +namespace Inspectron.Settings +{ + /// + /// A specialization of System.ComponentModel.PropertyDescriptor that is bound + /// to a specific property of an object or type. If the property's setter is private, + /// this BoundPropertyDescriptor's IsReadOnly property is true. + /// Use this class to expose an object or type's property for property editing. + public class BoundPropertyDescriptor : PropertyDescriptor + { + public delegate object OnClickDelegate(object sender, EventArgs e); + public OnClickDelegate OnClick { get; set; } + /// + /// Constructor for instance properties + /// Property owner + /// Lambda expression that accesses the property; + /// e.g., () => myObject.MyProperty + /// Property display name + /// Property category + /// Property description + public BoundPropertyDescriptor( + object owner, + Expression> expression, + string displayName, + string category, + string description) + + : this(displayName, category, description) + { + PropertyInfo propertyInfo = GetPropertyInfo(expression); + Init(owner, null, null, propertyInfo, null, null); + } + + /// + /// Constructor for static properties + /// Type holding static property + /// Lambda expression that accesses the property; + /// e.g., () => MyClass.MyProperty + /// Property display name + /// Property category + /// Property description + public BoundPropertyDescriptor( + Type ownerType, + Expression> expression, + string displayName, + string category, + string description) + + : this(displayName, category, description) + { + PropertyInfo propertyInfo = GetPropertyInfo(expression); + Init(null, ownerType, null, propertyInfo, null, null); + } + + /// + /// Constructor for instance properties + /// Property owner + /// Lambda expression that accesses the property; + /// e.g., () => myObject.MyProperty + /// Property display name + /// Property category + /// Property description + /// Editor for property + /// TypeConverter for property + public BoundPropertyDescriptor( + object owner, + Expression> expression, + string displayName, + string category, + string description, + object editor, + TypeConverter converter) + + : this(displayName, category, description) + { + PropertyInfo propertyInfo = GetPropertyInfo(expression); + Init(owner, null, null, propertyInfo, editor, converter); + } + + /// + /// Constructor for static properties + /// Type holding static property + /// Lambda expression that accesses the property; + /// e.g., () => MyClass.MyProperty + /// Property display name + /// Property category + /// Property description + /// Editor for property + /// TypeConverter for property + public BoundPropertyDescriptor( + Type ownerType, + Expression> expression, + string displayName, + string category, + string description, + object editor, + TypeConverter converter) + + : this(displayName, category, description) + { + PropertyInfo propertyInfo = GetPropertyInfo(expression); + Init(null, ownerType, null, propertyInfo, editor, converter); + } + + /// + /// Constructor for instance properties + /// Property owner + /// Property name + /// Property display name + /// Property category + /// Property description + public BoundPropertyDescriptor( + object owner, + string name, + string displayName, + string category, + string description) + + : this(displayName, category, description) + { + Init(owner, null, name, null, null, null); + } + + /// + /// Constructor for instance properties + /// Property owner + /// Property name + /// Property display name + /// Property category + /// Property description + /// Editor for property + public BoundPropertyDescriptor( + object owner, + string name, + string displayName, + string category, + string description, + object editor) + + : this(displayName, category, description) + { + Init(owner, null, name, null, editor, null); + } + + /// + /// Constructor for instance properties + /// Property owner + /// Property name + /// Property display name + /// Property category + /// Property description + /// Editor for property + /// TypeConverter for property + public BoundPropertyDescriptor( + object owner, + string name, + string displayName, + string category, + string description, + object editor, + TypeConverter converter) + + : this(displayName, category, description) + { + Init(owner, null, name, null, editor, converter); + } + + /// + /// Constructor for static properties + /// Type holding static property + /// Property name + /// Property display name + /// Property category + /// Property description + public BoundPropertyDescriptor( + Type ownerType, + string name, + string displayName, + string category, + string description) + + : this(displayName, category, description) + { + Init(null, ownerType, name, null, null, null); + } + + /// + /// Constructor for static properties + /// Type holding static property + /// Property name + /// Property display name + /// Property category + /// Property description + /// Editor for property + public BoundPropertyDescriptor( + Type ownerType, + string name, + string displayName, + string category, + string description, + object editor) + + : this(displayName, category, description) + { + Init(null, ownerType, name, null, editor, null); + } + + /// + /// Constructor for static properties + /// Type holding static property + /// Property name + /// Property display name + /// Property category + /// Property description + /// Editor for property + /// TypeConverter for property + public BoundPropertyDescriptor( + Type ownerType, + string name, + string displayName, + string category, + string description, + object editor, + TypeConverter converter) + + : this(displayName, category, description) + { + Init(null, ownerType, name, null, editor, converter); + } + + private void Init( + object owner, + Type ownerType, + string name, + PropertyInfo propertyInfo, + object editor, + TypeConverter converter) + { + m_owner = owner; + + // if given the property owner, ignore the ownerType parameter + if (owner != null) + ownerType = owner.GetType(); + m_ownerType = ownerType; + + if (string.IsNullOrEmpty(name)) + { + if (propertyInfo == null) + throw new ArgumentException("either 'name' or 'propertyInfo' must be non-null"); + name = propertyInfo.Name; + } + + // if given the property info, don't use reflection to find it + _propertyInfo = propertyInfo; + if (_propertyInfo == null) + { + _propertyInfo = m_ownerType.GetProperty( + name, + BindingFlags.Public | + BindingFlags.Instance | + BindingFlags.Static); + + if (_propertyInfo == null) + throw new ArgumentException(name + ": Property doesn't exist"); + } + + // look at "set_" method to determine if property is read-only. + // (PropertyInfo.CanWrite will return true if there is a set + // accessor, even if that accessor is made inaccessible using + // asymmetric accessor accessibility.) + MethodInfo setInfo = m_ownerType.GetMethod("set_" + name, + BindingFlags.Public | + BindingFlags.Instance | + BindingFlags.Static); + + m_readOnly = (setInfo == null); + + m_editor = editor; + m_typeConverter = converter; + } + + private BoundPropertyDescriptor( + string displayName, + string category, + string description) + + : base(displayName, + new Attribute[] { new CategoryAttribute(category), + new DescriptionAttribute(description), }) + { + } + + /// + /// When overridden in a derived class, returns whether resetting an object changes its value + /// The component to test for reset capability + /// True iff resetting the component changes its value + public override bool CanResetValue(object component) + { + object defaultValue; + return GetDefaultValue(out defaultValue) && !Object.Equals(GetValue(null), defaultValue); + } + + /// + /// Gets the component this property is bound to. Is null, if bound to a static class's property. + public object Owner + { + get { return m_owner; } + } + + /// + /// Gets the type of the component this property is bound to + public override Type ComponentType + { + get { return m_ownerType; } + } + + /// + /// Gets the type of the property + public override Type PropertyType + { + get { return _propertyInfo.PropertyType; } + } + + /// + /// Gets whether this property is read-only + public override bool IsReadOnly + { + get { return m_readOnly; } + } + + /// + /// Resets the value for this property of the component to the default value + /// The component with the property value that is to be reset to the default value + public override void ResetValue(object component) + { + object defaultValue; + GetDefaultValue(out defaultValue); + SetValue(component, defaultValue); + } + + /// + /// Determines whether the value of this property needs to be persisted + /// The component with the property to be examined for persistence + /// True iff the property should be persisted + public override bool ShouldSerializeValue(object component) + { + object val = GetValue(component); + + object defaultValue; + if (!GetDefaultValue(out defaultValue) && val == null) + return false; + else + return !val.Equals(defaultValue); + } + + /// + /// Returns the Owner's value (if the Owner is not null) or the component's value of the property + /// Component to examine + /// The value of a property + public override object GetValue(object component) + { + if (m_owner != null) + component = m_owner; + return _propertyInfo.GetValue(component, null); + } + + /// + /// Sets the value of the Owner (if not null) or component + /// Component + /// The new value + public override void SetValue(object component, object value) + { + if (m_owner != null) + component = m_owner; + _propertyInfo.SetValue(component, value, null); + } + + /// + /// Gets the default value + /// Is set to the default value or null, if it couldn't be determined + /// Whether or not the default value was determined + /// Uses reflection to look for a DefaultValueAttribute + public virtual bool GetDefaultValue(out object result) + { + bool foundDefault = false; + result = null; + object[] attributes = _propertyInfo.GetCustomAttributes(typeof(DefaultValueAttribute), false); + if (attributes.Length > 0) + { + foundDefault = true; + result = (attributes[0] as DefaultValueAttribute).Value; + if (result != null && result.GetType() != _propertyInfo.PropertyType) + { + // Default value type is not the same as the property type; convert it. + // This can happen if the property's type is not CLS-compliant (e.g. UInt32). + TypeConverter converter = TypeDescriptor.GetConverter(result); + if (converter.CanConvertTo(_propertyInfo.PropertyType)) + { + result = converter.ConvertTo(result, _propertyInfo.PropertyType); + } + else + { + // Try using the converter associated with the source instead of the target + // (Not sure if this is useful for not, but can it hurt?) + converter = TypeDescriptor.GetConverter(_propertyInfo.PropertyType); + if (converter.CanConvertFrom(result.GetType())) + { + result = converter.ConvertFrom(result); + } + } + } + } + + return foundDefault; + } + + /// + /// Returns an editor of the specified type + /// Base type of editor, which is used to differentiate between multiple + /// editors that a property supports + /// An instance of the requested editor type, or null if an editor cannot be found + public override object GetEditor(Type editorBaseType) + { + if (m_editor != null && + editorBaseType.IsInstanceOfType(m_editor)) + { + return m_editor; + } + + if (editorBaseType.IsEnum) + { + + } + return base.GetEditor(editorBaseType); + } + + /// + /// Gets the type converter for this property + public override TypeConverter Converter + { + get + { + if (m_typeConverter != null) + return m_typeConverter; + + return base.Converter; + } + } + + public PropertyInfo PropertyInfo => _propertyInfo; + + // Does not return null. Will throw an exception if 'expression' is poorly formed. + private static PropertyInfo GetPropertyInfo(Expression> expression) + { + PropertyInfo propertyInfo = null; + MemberExpression memberExpression = expression.Body as MemberExpression; + if (memberExpression != null) + { + // this is the usual case for when a property has a public getter and setter + propertyInfo = memberExpression.Member as PropertyInfo; + } + else + { + // if the setter is private, the expression is a UnaryExpression type for some reason. + UnaryExpression unaryExpression = expression.Body as UnaryExpression; + if (unaryExpression != null) + { + memberExpression = unaryExpression.Operand as MemberExpression; + if (memberExpression != null) + propertyInfo = memberExpression.Member as PropertyInfo; + } + } + + if (propertyInfo == null) + throw new ArgumentException( + "lambda expression was not properly formed." + + " Should be \" => myObject.MyProperty\" or" + + " \" => MyClass.MyProperty\""); + + return propertyInfo; + } + + object m_owner; + Type m_ownerType; + PropertyInfo _propertyInfo; + bool m_readOnly; + private object m_editor; + private TypeConverter m_typeConverter; + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/Event.cs b/framework/Inspectron.Settings/Event.cs new file mode 100644 index 0000000..1d4ab89 --- /dev/null +++ b/framework/Inspectron.Settings/Event.cs @@ -0,0 +1,43 @@ +using System; +using System.ComponentModel; + +namespace Inspectron.Settings +{ + public static class Event + { + public static void Raise(this EventHandler handler, object sender, EventArgs e) + { + if (handler != null) handler(sender, e); + } + + public static void Raise(this EventHandler handler, object sender, T e) where T : EventArgs + { + if (handler != null) handler(sender, e); + } + + public static bool RaiseCancellable(this CancelEventHandler handler, object sender, CancelEventArgs e) + { + if (handler != null) + foreach (CancelEventHandler h in handler.GetInvocationList()) + { + h(sender, e); + if (e.Cancel) break; + } + + return e.Cancel; + } + + public static bool RaiseCancellable(this EventHandler handler, object sender, T e) + where T : CancelEventArgs + { + if (handler != null) + foreach (EventHandler h in handler.GetInvocationList()) + { + h(sender, e); + if (e.Cancel) break; + } + + return e.Cancel; + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/IAdaptable.cs b/framework/Inspectron.Settings/IAdaptable.cs new file mode 100644 index 0000000..93f29c6 --- /dev/null +++ b/framework/Inspectron.Settings/IAdaptable.cs @@ -0,0 +1,13 @@ +using System; + +namespace Inspectron.Settings +{ + public interface IAdaptable + { + /// + /// Gets an adapter of the specified type or null + /// Adapter type + /// Adapter of the specified type or null if no adapter available + object GetAdapter(Type type); + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/IDecoratable.cs b/framework/Inspectron.Settings/IDecoratable.cs new file mode 100644 index 0000000..08fc757 --- /dev/null +++ b/framework/Inspectron.Settings/IDecoratable.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace Inspectron.Settings +{ + public interface IDecoratable + { + /// + /// Gets all decorators of the specified type + /// Decorator type + /// Enumeration of non-null decorators that are of the specified type. The enumeration may be empty. + IEnumerable GetDecorators(Type type); + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/ITreeView.cs b/framework/Inspectron.Settings/ITreeView.cs new file mode 100644 index 0000000..7682871 --- /dev/null +++ b/framework/Inspectron.Settings/ITreeView.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Inspectron.Settings +{ + public interface ITreeView + { + /// + /// Gets the root object of the tree view + object Root + { + get; + } + + /// + /// Obtains enumeration of the children of the given parent object + /// Parent object + /// Enumeration of children of the parent object + IEnumerable GetChildren(object parent); + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/Inspectron.Settings.csproj b/framework/Inspectron.Settings/Inspectron.Settings.csproj new file mode 100644 index 0000000..1a7770f --- /dev/null +++ b/framework/Inspectron.Settings/Inspectron.Settings.csproj @@ -0,0 +1,8 @@ + + + + netstandard2.1 + Debug;Release;CPU + + + diff --git a/framework/Inspectron.Settings/InspectronSettings.cs b/framework/Inspectron.Settings/InspectronSettings.cs new file mode 100644 index 0000000..357ec14 --- /dev/null +++ b/framework/Inspectron.Settings/InspectronSettings.cs @@ -0,0 +1,845 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; +using System.Threading; +using System.Xml; + +namespace Inspectron.Settings +{ + + public class InspectronSettings + { + + public InspectronSettings(string path=null) + { + Assembly assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + + AssemblyName assemblyName = assembly.GetName(); + _applicationName = assemblyName.Name; + Version version = assemblyName.Version; + _versionString = version.Major + "." + version.Minor; + + string startupPath = Path.GetDirectoryName(new Uri(assemblyName.CodeBase).LocalPath); + _defaultSettingsPath = Path.Combine(startupPath, "DefaultSettings.xml"); + + + + if (path == null) + { + var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + _settingsPath = string.Format("{0}\\{1}\\{2}\\AppSettings.xml", appDataPath, _applicationName, _versionString); + } + else + { + _settingsPath = Path.Combine(path,"AppSettings.xml"); + } + + + + } + internal Path GetSettingsPath(string pathName) + { + string[] pathSegments = pathName.Split('/', 16); + object[] path = new object[pathSegments.Length + 1]; + + // first node is the settings tree root + Tree node = (Tree)_userSettings; + path[0] = _userSettings; + + // middle nodes are folders + for (int i = 1; i < path.Length - 1; i++) + { + node = GetOrCreateFolder(pathSegments[i - 1], node); + path[i] = node; + } + + // leaf node is user settings object + foreach (Tree leaf in node.Children) + { + UserSettingsInfo info = leaf.Value as UserSettingsInfo; + if (info != null && info.Name == pathSegments[pathSegments.Length - 1]) + { + path[path.Length - 1] = leaf; + break; + } + } + + return new Path(path); + } + /// + /// Gets or sets the current state of all properties (Memento pattern) + public object State + { + get + { + MemoryStream stream = new MemoryStream(); + Serialize(stream); + return stream; + } + set + { + MemoryStream stream = value as MemoryStream; + if (stream == null) + throw new ArgumentException("Not a valid memento"); + stream.Position = 0; + Deserialize(stream); + } + } + + private IEnumerable UserPropertyDescriptors + { + get + { + var userSettings = UserSettings as Tree; + if (userSettings == null) + throw new InvalidOperationException("userSettings"); + var all = userSettings.LevelOrder.Where(x => x.Value is UserSettingsInfo); + foreach (Tree node in all) + { + UserSettingsInfo info = node.Value as UserSettingsInfo; + if (info != null) + { + foreach (PropertyDescriptor property in info.Settings) + yield return property; + } + } + } + } + private static readonly object _unusedComponent = new object(); + public void SetDefaults() + { + foreach (SettingsInfo info in _settings.Values) + { + foreach (SettingsInfo.Setting setting in info.Settings.Values) + { + if (setting.PropertyDescriptor != null && + setting.PropertyDescriptor.CanResetValue(_unusedComponent)) + { + setting.PropertyDescriptor.ResetValue(_unusedComponent); + } + } + } + // Load default settings if exist. + if (File.Exists(_defaultSettingsPath)) + { + using (Stream stream = File.OpenRead(_defaultSettingsPath)) + Deserialize(stream); + } + } + internal List GetProperties(Tree tree) + { + var info = tree.Value as UserSettingsInfo; + if (info != null) + return info.Settings; + + return null; + } + protected class SettingsInfo + { + /// + /// Constructor with name + /// Name associated with this group of settings + public SettingsInfo(string name) + { + Name = name; + } + + /// + /// Add a setting to group of settings as a name and value, replacing the previous value if present + /// Setting name + /// Setting value + public void Add(string name, string valueString) + { + Setting setting; + if (Settings.TryGetValue(name, out setting)) + { + setting.Set(name, valueString); + } + else + { + Settings.Add(name, new Setting(name, valueString)); + } + } + + /// + /// Add a setting to group of settings as a PropertyDescriptor, replacing the previous value if present + /// PropertyDescriptor describing setting + public void Add(PropertyDescriptor descriptor) + { + Setting setting; + if (Settings.TryGetValue(descriptor.Name, out setting)) + { + setting.Set(descriptor); + } + else + { + Settings.Add(descriptor.Name, new Setting(descriptor)); + } + } + + /// + /// Name associated with this group of settings + public readonly string Name; + /// + /// Dictionary for names and values of settings in group + public readonly SortedDictionary Settings = new SortedDictionary(); + + /// + /// Class for handling individual setting information + public class Setting + { + /// + /// Constructor with PropertyDescriptor + /// PropertyDescriptor describing setting + public Setting(PropertyDescriptor descriptor) + { + PropertyDescriptor = descriptor; + } + + /// + /// Constructor with name and value of setting + /// Setting name + /// Setting value + public Setting(string name, string valueString) + { + Name = name; + ValueString = valueString; + } + + /// + /// Set a setting's value with name and value of setting + /// Setting name + /// Setting value + public void Set(string name, string valueString) + { + Name = name; + ValueString = valueString; + if (PropertyDescriptor != null) + { + SetValue(); + } + } + + /// + /// Set a setting's value with PropertyDescriptor + /// PropertyDescriptor describing setting + public void Set(PropertyDescriptor descriptor) + { + PropertyDescriptor = descriptor; + if (Name != null && ValueString != null) + { + SetValue(); + } + } + + private void SetValue() + { + if (!CanMakeChanges) + return; + + object value = GetValue(PropertyDescriptor.PropertyType, ValueString); + PropertyDescriptor.SetValue(null, value); + } + + /// + /// Setting name + public string Name; + /// + /// Setting value + public string ValueString; + /// + /// Setting PropertyDescriptor + public PropertyDescriptor PropertyDescriptor; + } + + /// + /// Set whether property descriptors are allowed to make changes + /// Used when persisted settings are being loaded from disk + /// to prevent property descriptors from being set multiple times as + /// in the case where DefaultSettings.xml and AppSettings.xml both exist + public static bool CanMakeChanges { private get; set; } + } + private static object GetValue(Type type, string valueString) + { + object value = null; + try + { + TypeConverter converter = TypeDescriptor.GetConverter(type); + if (CanConvertToAndFromString(converter)) + { + value = converter.ConvertFromInvariantString(valueString); + } + else + { + // deserialize + byte[] data = Convert.FromBase64String(valueString); + using (MemoryStream stream = new MemoryStream(data)) + { + BinaryFormatter formatter = new BinaryFormatter(); + value = formatter.Deserialize(stream); + } + } + } + catch + { + value = null; + } + return value; + } + private static bool CanConvertToAndFromString(TypeConverter converter) + { + return converter.CanConvertFrom(typeof(string)) && + converter.CanConvertTo(typeof(string)); + } + /// + /// Class for group of user settings + public class UserSettingsInfo + { + /// + /// Constructor with name and PropertyDescriptors + /// Name of group of user settings + /// PropertyDescriptors with settings + public UserSettingsInfo(string name, PropertyDescriptor[] settings) + { + Name = name; + Settings = new List(settings); + } + + /// + /// Name of group of user settings + public readonly string Name; + /// + /// PropertyDescriptors with settings + public readonly List Settings; + } + private readonly SortedDictionary _settings = new SortedDictionary(); + private ITreeView _userSettings = new TreeView(string.Empty); + + public void RegisterSettings(string uid, params PropertyDescriptor[] settings) + { + SettingsInfo settingsInfo; + if (!_settings.TryGetValue(uid, out settingsInfo)) + { + settingsInfo = new SettingsInfo(uid); + _settings.Add(uid, settingsInfo); + } + + foreach (PropertyDescriptor descriptor in settings) + settingsInfo.Add(descriptor); + } + public ITreeView UserSettings + { + get { return _userSettings; } + } + + public void RegisterSimple(object owner,Expression> expression,string pathName,string name,string category="General",string description="") + { + var descriptor = new BoundPropertyDescriptor(owner,expression, name, category, + description); + + RegisterSettings(pathName,descriptor); + + RegisterUserSettings(pathName,descriptor); + } + + public void RegisterUserSettings(string pathName, params PropertyDescriptor[] settings) + { + if (string.IsNullOrEmpty(pathName)) + throw new ArgumentException("pathName"); + + string[] path = pathName.Split('/', 16); + + // get root folder + Tree folder = UserSettings as Tree; + if (folder == null) + throw new InvalidOperationException("userSettings"); + + // for each subsequent segment of the path, get folder + for (int i = 0; i < path.Length - 1; i++) + folder = GetOrCreateFolder(path[i], folder); + + // get the node that should hold the settings, if it already exists + string name = path[path.Length - 1]; + UserSettingsInfo existing = null; + int index = 0; + foreach (Tree node in folder.Children) + { + UserSettingsInfo info = node.Value as UserSettingsInfo; + if (info != null) + { + if (info.Name == name) + { + existing = info; + break; + } + if (info.Name.CompareTo(name) < 0) + index++; + } + } + + // add the settings, either by merging with the existing node or creating a new one + if (existing != null) + { + foreach (PropertyDescriptor pd in settings) + existing.Settings.Add(pd); + } + else + { + Tree node = new Tree(new UserSettingsInfo(name, settings)); + folder.Children.Insert(index, node); + } + } + private static string GetMutexName(string pathName) + { + string safeName = pathName; + + //255 characters will break IpcChannel constructor. 250 works. + if (safeName.Length > 250) + safeName = safeName.Substring(safeName.Length - 250); + + // The Mutex constructor will crash if given '\', unless it's part of a valid path. + // NextInstanceMonitor.ActivateApplication() will crash if there are '/' characters. + safeName = safeName.Replace('/', '-'); + safeName = safeName.Replace('\\', '-'); + + return safeName; + } + protected void Serialize(Stream stream) + { + Saving.Raise(this, EventArgs.Empty); + + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.AppendChild(xmlDoc.CreateXmlDeclaration("1.0", "utf-8", "yes")); + XmlElement root = xmlDoc.CreateElement("settings"); + xmlDoc.AppendChild(root); + + // add application name and version to the root element. + root.SetAttribute("appName", _applicationName); + root.SetAttribute("appVersion", _versionString); + + foreach (SettingsInfo info in _settings.Values) + { + XmlElement block = xmlDoc.CreateElement("block"); + block.SetAttribute("id", info.Name); + + foreach (SettingsInfo.Setting setting in info.Settings.Values) + { + PropertyDescriptor descriptor = setting.PropertyDescriptor; + if (descriptor != null) + { + object value = descriptor.GetValue(null); + if (CanWriteValue(value)) + WriteValue(descriptor.Name, value, block); + } + else + { + WriteValue(setting.Name, setting.ValueString, block); + } + } + // skip empty block + if (block.ChildNodes.Count > 0) + root.AppendChild(block); + } + + XmlWriterSettings settings = new XmlWriterSettings(); + settings.CloseOutput = false; + settings.Indent = true; + + using (XmlWriter writer = XmlWriter.Create(stream, settings)) + { + xmlDoc.WriteTo(writer); + } + } + private bool CanWriteValue(object value) + { + if (value == null) + return false; + + TypeConverter converter = TypeDescriptor.GetConverter(value.GetType()); + return CanConvertToAndFromString(converter) || value.GetType().IsSerializable; + } + private void WriteValue(string name, object value, XmlElement block) + { + if (value == null) + return; + + // skip persisting if any exception occurs + string valueString = null; + Type type = value.GetType(); + TypeConverter converter = TypeDescriptor.GetConverter(type); + if (CanConvertToAndFromString(converter)) + { + valueString = converter.ConvertToInvariantString(value); + } + else if (type.IsSerializable) + { + // serialize + BinaryFormatter formatter = new BinaryFormatter(); + using (MemoryStream stream = new MemoryStream()) + { + formatter.Serialize(stream, value); + valueString = Convert.ToBase64String(stream.GetBuffer()); + } + } + + if (string.IsNullOrEmpty(valueString)) + return; + + XmlDocument xmlDoc = block.OwnerDocument; + XmlElement elmValue = xmlDoc.CreateElement("value"); + elmValue.SetAttribute("name", name); + elmValue.SetAttribute("type", type.Name); + + // if the valueString is xmlDoc then + XmlDocument temp = StringToXmlDoc(valueString); + if (temp != null) + { + // remove xml declaration if exists + XmlDeclaration decl = temp.FirstChild as XmlDeclaration; + if (decl != null) + temp.RemoveChild(decl); + elmValue.InnerXml = temp.DocumentElement.OuterXml; + } + else + { + elmValue.InnerText = valueString; + } + + block.AppendChild(elmValue); + } + private XmlDocument StringToXmlDoc(string strXml) + { + XmlDocument xmlDoc = null; + try + { + int len = (strXml.Length > 20) ? 20 : strXml.Length; + string test = RemoveAllWhiteSpace(strXml.Substring(0, len)).ToLower(); + if (test.Contains(" GetOrCreateFolder(string name, Tree tree) + { + // search for folder + Tree result = null; + int index = 0; + foreach (Tree child in tree.Children) + { + string folderName = child.Value as string; + if (folderName != null) + { + if (folderName == name) + { + result = child; + break; + } + + if (folderName.CompareTo(name) < 0) + index++; + } + else // child is UserSettingsInfo + { + index++; // folders should follow settings nodes + } + + } + + // if not found, create it + if (result == null) + { + result = new Tree(name); + tree.Children.Insert(index, result); + } + + return result; + } + + + public event EventHandler Saving; + public event EventHandler Loading; + public event EventHandler Reloaded; + + + private class TreeView : Tree, ITreeView + { + public TreeView(object root) + : base(root) + { + } + + #region ITreeView Members + + public object Root + { + get { return this; } + } + + public IEnumerable GetChildren(object parent) + { + foreach (object child in ((Tree)parent).Children) + yield return child; + } + + #endregion + + #region IItemView Members + + /// + /// Gets item's display information + /// Item being displayed + /// Item info, to fill out + public void GetInfo(object item, ItemInfo info) + { + object value = ((Tree)item).Value; + if (value is string) + { + info.Label = (string)value; + //info.ImageIndex = info.GetImageList().Images.IndexOfKey(Resources.FolderImage); + info.AllowSelect = false; + } + else + { + UserSettingsInfo settingsInfo = value as UserSettingsInfo; + info.Label = settingsInfo.Name; + info.AllowLabelEdit = false; + //info.ImageIndex = info.GetImageList().Images.IndexOfKey(Resources.PreferencesImage); + info.IsLeaf = true; + } + } + + #endregion + } + + + + private string _settingsPath; + private string _defaultSettingsPath; + private string _applicationName; + private string _versionString; + private string _propertyViewState; + + + public void LoadSettings() + { + Loading.Raise(this, EventArgs.Empty); + + try + { + bool defaultSettingsExists = File.Exists(_defaultSettingsPath); + bool appSettingsExists = File.Exists(_settingsPath); + + // only update property descriptors during the DefaultSettings.xml pass + // if DefaultSettings.xml exists and AppSettings.xml does not exist + SettingsInfo.CanMakeChanges = defaultSettingsExists && !appSettingsExists; + + // first, load default settings, if they exist: + if (defaultSettingsExists) + { + using (Stream stream = File.OpenRead(_defaultSettingsPath)) + Deserialize(stream); + } + + // restore for AppSettings.xml pass + SettingsInfo.CanMakeChanges = true; + + // now load user settings, overriding defaults: + if (appSettingsExists) + { + using (Stream stream = File.OpenRead(_settingsPath)) + Deserialize(stream); + } + } + catch (Exception ex) + { + + } + finally + { + // restore value + SettingsInfo.CanMakeChanges = true; + } + } + + protected void ApplyStoredSettings() + { + foreach (SettingsInfo info in _settings.Values) + { + foreach (SettingsInfo.Setting setting in info.Settings.Values) + { + if (setting.PropertyDescriptor is BoundPropertyDescriptor pd) + { + if (setting.ValueString == null) + { + continue; + } + object v; + if (setting.PropertyDescriptor.PropertyType.IsEnum) + { + v = Enum.Parse(setting.PropertyDescriptor.PropertyType, setting.ValueString); + } + else + { + v = Convert.ChangeType(setting.ValueString, setting.PropertyDescriptor.PropertyType); + } + pd.SetValue(pd.Owner, v); + } + } + } + } + + + protected bool Deserialize(Stream stream) + { + // create XML DOM from stream + // if failed, display message box and return + try + { + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.Load(stream); + + XmlElement root = xmlDoc.DocumentElement; + + // get all the blocks + XmlNodeList blocks = root.SelectNodes("block"); + if (blocks == null || blocks.Count == 0) + throw new Exception("The setting file is empty"); + + foreach (XmlElement block in blocks) + { + try + { + string id = block.GetAttribute("id"); + + SettingsInfo info; + if (!_settings.TryGetValue(id, out info)) + { + info = new SettingsInfo(id); + _settings.Add(id, info); + } + + // get a list of value element in each block + XmlNodeList valueNodes = block.SelectNodes("value"); + + // skip over empty block + if (valueNodes == null || valueNodes.Count == 0) + continue; + + foreach (XmlElement xmlElement in valueNodes) + { + string name = xmlElement.GetAttribute("name"); + string valueString = GetElementValueString(xmlElement); + + info.Add(name, valueString); + } + } + catch (Exception ex) + { + + } + } + ApplyStoredSettings(); + Reloaded.Raise(this, EventArgs.Empty); + } + catch (Exception ex) + { + + + return false; + } + + return true; + } + private static string GetElementValueString(XmlElement element) + { + string valueString = element.InnerText; + if (string.IsNullOrEmpty(valueString)) + valueString = element.InnerXml; + + return valueString; + } + + + + + + + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/ItemInfo.cs b/framework/Inspectron.Settings/ItemInfo.cs new file mode 100644 index 0000000..19d3f64 --- /dev/null +++ b/framework/Inspectron.Settings/ItemInfo.cs @@ -0,0 +1,148 @@ +namespace Inspectron.Settings +{ + public abstract class ItemInfo + { + /// + /// Constructor for items with no associated images to be drawn + public ItemInfo() + { + CheckBoxEnabled = true; + } + + /// + /// Gets or sets item's label + /// Default is empty string if item has no label + public string Label + { + get { return m_label; } + set { m_label = value; } + } + + + + /// + /// Gets or sets item's description + /// Default is empty string if item has no description + public string Description + { + get { return m_description; } + set { m_description = value; } + } + + /// + /// Gets or sets whether item should have a check box control + /// Default is false + public bool HasCheck + { + get { return m_hasCheck; } + set { m_hasCheck = value; } + } + + /// + /// Gets or sets whether check box is enabled + /// Default is true. + /// This property makes sense only if HasCheck is true + public bool CheckBoxEnabled + { + get; + set; + } + + /// + /// Gets or sets whether item is checked + /// Default is false + public abstract bool Checked + { + get; + set; + } + + /// + /// Gets or sets whether item is a leaf (has no sub-items) + /// Used by tree controls to inhibit drawing the node expander; default + /// is false + public bool IsLeaf + { + get { return m_isLeaf; } + set { m_isLeaf = value; } + } + + /// + /// Gets or sets a value indicating whether the label is editable + /// Used by tree controls to inhibit editing the node label; default + /// is true + public bool AllowLabelEdit + { + get { return m_allowLabelEdit; } + set { m_allowLabelEdit = value; } + } + + /// + /// Gets or sets whether the item is selectable + /// Used by tree controls to inhibit selecting the node; default + /// is true + public bool AllowSelect + { + get { return m_allowSelect; } + set { m_allowSelect = value; } + } + + /// + /// Gets or sets whether this item is expanded in the view. Set by + /// Control adapters - client code shouldn't set this value. + public bool IsExpandedInView + { + get { return m_isExpandedInView; } + set { m_isExpandedInView = value; } + } + + /// + /// Gets or sets index of item's image in image list + /// Default is -1 if item has no image. + /// DAN: This is not required by WPF so could be moved to WinFormsItemInfo + public int ImageIndex + { + get { return m_imageIndex; } + set { m_imageIndex = value; } + } + + /// + /// Gets or sets index of item's "State" image in image list + /// Default is -1 if item has no "State" image. + /// DAN: This is not required by WPF so could be moved to WinFormsItemInfo + public int StateImageIndex + { + get { return m_stateImageIndex; } + set { m_stateImageIndex = value; } + } + + /// + /// Gets or sets item's properties for lists and tree lists + /// Default is an empty array if item has no properties + public object[] Properties + { + get { return m_properties; } + set { m_properties = value; } + } + + /// + /// Gets or sets item's mouse hover over text + public string HoverText + { + get { return m_hoverText; } + set { m_hoverText = value; } + } + + private string m_label = string.Empty; + private string m_description = string.Empty; + private int m_imageIndex = -1; + private int m_stateImageIndex = -1; + private object[] m_properties = new object[0]; + private bool m_hasCheck; + private bool m_isLeaf; + private bool m_allowLabelEdit = true; + private bool m_allowSelect = true; + private bool m_isExpandedInView; + private string m_hoverText = string.Empty; + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/Path.cs b/framework/Inspectron.Settings/Path.cs new file mode 100644 index 0000000..cda0dcc --- /dev/null +++ b/framework/Inspectron.Settings/Path.cs @@ -0,0 +1,405 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Inspectron.Settings +{ + public class Path : IList, IEquatable> + { + /// + /// Constructor using single object + /// Single object making up the path + public Path(T last) + { + m_path = new T[1]; + m_path[0] = last; + } + + /// + /// Constructor using sequence of objects + /// Path, as sequence of objects + public Path(IEnumerable path) + { + m_path = path.ToArray(); + } + + /// + /// Constructor using collection of objects + /// Path, as collection of objects + public Path(ICollection path) + { + m_path = new T[path.Count]; + path.CopyTo(m_path, 0); + } + + /// + /// Gets or sets the first object in the path + public T First + { + get { return m_path[0]; } + set { m_path[0] = value; } + } + + /// + /// Gets or sets the last object in the path + public T Last + { + get { return m_path[m_path.Length - 1]; } + set { m_path[m_path.Length - 1] = value; } + } + + /// + /// Obtains a prefix with the specified length + /// Prefix length + /// Prefix with the specified length + public Path Prefix(int length) + { + CheckSubPathLength(length); + + T[] path = new T[length]; + Array.Copy(m_path, 0, path, 0, length); + + return new Path(path); + } + + /// + /// Obtains a suffix with the specified length + /// Suffix length + /// Suffix with the specified length + public Path Suffix(int length) + { + CheckSubPathLength(length); + + T[] path = new T[length]; + int offset = m_path.Length - length; + Array.Copy(m_path, offset, path, 0, length); + + return new Path(path); + } + + /// + /// Converts path to a path of another type + /// Path type to convert to + /// Path of new type + public Path Convert() + where U : class + { + U[] converted = new U[m_path.Length]; + for (int i = 0; i < m_path.Length; i++) + converted[i] = Convert(m_path[i]); + return new Path(converted); + } + + /// + /// Converts from the path type to another type + /// Desired type + /// Item to convert + /// Item, converted to given type, or null + protected virtual U Convert(T item) + where U : class + { + U u = item as U; + return u; + } + + /// + /// Tests path for equality + /// Other path + /// True iff this path is equivalent to other + public bool Equals(Path other) + { + if (object.Equals(other, null)) + return false; + + if (m_path.Length != other.m_path.Length) + return false; + + for (int i = 0; i < m_path.Length; i++) + if (!m_path[i].Equals(other.m_path[i])) + return false; + + return true; + } + + /// + /// Tests object for equality + /// Other object + /// True iff this path is equivalent to other object + public override bool Equals(object obj) + { + Path path = obj as Path; + if (path != null) + return Equals(path); + + return false; + } + + /// + /// Obtains hash code + /// Hash code + public override int GetHashCode() + { + int hash = 0; + foreach (T obj in m_path) + hash ^= obj.GetHashCode(); + + return hash; + } + + /// + /// Tests paths for equality + /// First path + /// Second path + /// True iff paths are equivalent + public static bool operator ==(Path o1, Path o2) + { + if (object.Equals(o1, null)) + return object.Equals(o2, null); + else + return o1.Equals(o2); + } + + /// + /// Tests paths for inequality + /// First path + /// Second path + /// True iff paths are not equivalent + public static bool operator !=(Path o1, Path o2) + { + if (object.Equals(o1, null)) + return !object.Equals(o2, null); + else + return !o1.Equals(o2); + } + + /// + /// Concatenates object with path + /// Prefix object + /// Optional path, may be null + /// Concatenated path, with lhs as first object + public static Path operator +(T lhs, Path rhs) + { + if (rhs == null) + return new Path(lhs); + T[] path = new T[1 + rhs.Count]; + path[0] = lhs; + Array.Copy(rhs.m_path, 0, path, 1, rhs.Count); + return new Path(path); + } + + /// + /// Concatenates path with object + /// Optional path, may be null + /// Suffix object + /// Concatenated path, with rhs as last object + public static Path operator +(Path lhs, T rhs) + { + if (lhs == null) + return new Path(rhs); + T[] path = new T[lhs.Count + 1]; + Array.Copy(lhs.m_path, 0, path, 0, lhs.Count); + path[path.Length - 1] = rhs; + return new Path(path); + } + + /// + /// Concatenates two paths + /// First path. Can be null. + /// Second path. Can be null. + /// Concatenated path, with rhs as prefix and lhs as suffix. Is null if both lhs and rhs are null. + public static Path operator +(Path lhs, Path rhs) + { + if (lhs == null) + return rhs; + if (rhs == null) + return lhs; + T[] path = new T[lhs.Count + rhs.Count]; + Array.Copy(lhs.m_path, 0, path, 0, lhs.Count); + Array.Copy(rhs.m_path, 0, path, lhs.Count, rhs.Count); + return new Path(path); + } + + /// + /// Gets the enumeration of each path's last item; i.e., the property 'Last' + /// Enumeration of Path objects, whose Last property is returned + /// Last property of each Path in 'paths', in the same order as 'paths' + public static IEnumerable GetLastItems(IEnumerable> paths) + { + foreach (Path path in paths) + yield return path.Last; + } + + #region IList Members + + /// + /// Determines the index of a specific item in the + /// The object to locate in the + /// + /// The index of item if found in the list; otherwise -1 + /// + public int IndexOf(T item) + { + for (int i = 0; i < m_path.Length; i++) + if (m_path[i].Equals(item)) + return i; + + return -1; + } + + /// + /// Inserts an item to the at the specified index + /// The zero-based index at which item should be inserted + /// The object to insert into the + /// The is read-only + /// index is not a valid index in the + public void Insert(int index, T item) + { + throw new NotSupportedException(); + } + + /// + /// Removes the item at the specified index + /// The zero-based index of the item to remove + /// The is read-only + /// index is not a valid index in the + public void RemoveAt(int index) + { + throw new NotSupportedException(); + } + + /// + /// Gets or sets the item at the specified index + /// Index at which to set value + public T this[int index] + { + get { return m_path[index]; } + set { m_path[index] = value; } + } + + #endregion + + #region ICollection Members + + /// + /// Adds an item to the + /// The object to add to the + /// The + /// is read-only + public void Add(T item) + { + throw new NotSupportedException(); + } + + /// + /// Removes all items from the + /// The + /// is read-only + public void Clear() + { + throw new NotSupportedException(); + } + + /// + /// Determines whether the contains a specific value + /// The object to locate in the + /// + /// True iff item is found in the + /// + public bool Contains(T item) + { + foreach (T obj in m_path) + if (obj.Equals(item)) + return true; + + return false; + } + + /// + /// Copies the elements of the to an , + /// starting at a particular index + /// The one-dimensional that is the destination of the elements + /// copied from . + /// The must have zero-based indexing. + /// The zero-based index in array at which copying begins + /// ArrayIndex is less than 0 + /// Array is null + /// Array is multidimensional.-or- + /// arrayIndex is equal to or greater than the length of array.-or- + /// The number of elements in the source is greater than + /// the available space from arrayIndex to the end of the destination array.-or- + /// Type T cannot be cast automatically to the type of the destination array. + public void CopyTo(T[] array, int arrayIndex) + { + m_path.CopyTo(array, arrayIndex); + } + + /// + /// Gets the number of elements contained in the + public int Count + { + get { return m_path.Length; } + } + + /// + /// Gets whether the is read-only + public bool IsReadOnly + { + get { return false; } + } + + /// + /// Removes the first occurrence of a specific object from the + /// The object to remove from the + /// + /// True iff item was successfully removed from the . + /// This method also returns false if item is not found in the original . + /// + /// The is read-only + public bool Remove(T item) + { + throw new NotSupportedException(); + } + + #endregion + + #region IEnumerable Members + + /// + /// Returns an enumerator that iterates through the collection + /// + /// A that can be used to iterate through the collection + /// + public IEnumerator GetEnumerator() + { + return ((IEnumerable)m_path).GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return m_path.GetEnumerator(); + } + + #endregion + + /// + /// Private constructor + private Path(T[] path) + { + m_path = path; + } + + private void CheckSubPathLength(int length) + { + if (length < 1) + throw new InvalidOperationException("Length must be > 0"); + if (length > m_path.Length) + throw new InvalidOperationException("Length greater than path length"); + } + + private readonly T[] m_path; + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/Tree.cs b/framework/Inspectron.Settings/Tree.cs new file mode 100644 index 0000000..18bb77a --- /dev/null +++ b/framework/Inspectron.Settings/Tree.cs @@ -0,0 +1,336 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text; + +namespace Inspectron.Settings +{ + [Serializable] + public class Tree + { + /// + /// Constructor of empty tree + public Tree() + : this(default(T)) + { + } + + /// + /// Constructor for tree with one node + /// Value associated with this tree + public Tree(T value) + { + m_value = value; + m_children = new ChildCollection(this); + } + + /// + /// Gets or sets tree's parent. Is null if this is a root node. + public Tree Parent + { + get { return m_parent; } + set + { + if (m_parent != value) + { + if (m_parent != null) + m_parent.Children.Remove(this); + + m_parent = value; + + if (m_parent != null) + m_parent.Children.Add(this); + } + } + } + + /// + /// Gets the list of children nodes. Is the same as 'this' because this Tree implements IList. + public IList> Children + { + get { return m_children; } + } + + /// + /// Gets or sets the value associated with the tree + public T Value + { + get { return m_value; } + set { m_value = value; } + } + + /// + /// Tests for equality + /// Other object + /// True iff object is a tree with the same structure and values as this tree + public override bool Equals(object obj) + { + if (obj == null) + return false; + + Tree other = obj as Tree; + if (other == null) + return false; + + if (!m_value.Equals(other.m_value)) + return false; + + if (m_children.Count != other.m_children.Count) + return false; + + for (int i = 0; i < m_children.Count; i++) + if (!(m_children[i]).Equals(other.m_children[i])) + return false; + + return true; + } + + /// + /// Tests for similarity + /// Other tree + /// True iff other has the same structure as this tree + /// Same structure means same node structure + public bool Similar(Tree other) + { + if (this == other) + return true; + + if (other == null) + return false; + + if (m_children.Count != other.m_children.Count) + return false; + + for (int i = 0; i < m_children.Count; i++) + if (!(m_children[i]).Similar(other.m_children[i])) + return false; + + return true; + } + + /// + /// Returns hash code for tree + /// Hash code + public override int GetHashCode() + { + int result = 0; + foreach (Tree tree in PreOrder) + result ^= tree.Value.GetHashCode(); + + return result; + } + + /// + /// Converts tree to string of the form "(Value(Child1),...,(ChildN))" + /// String representation of tree + public override string ToString() + { + StringBuilder builder = new StringBuilder(); + builder.Append('('); + Stringify(builder); + builder.Append(')'); + return builder.ToString(); + } + + private void Stringify(StringBuilder builder) + { + builder.Append(m_value.ToString()); + if (!IsLeaf) + { + builder.Append('('); + bool firstTime = true; + foreach (Tree t in m_children) + { + if (firstTime) + firstTime = false; + else + builder.Append(','); + t.Stringify(builder); + } + builder.Append(')'); + } + } + + /// + /// Tests if this tree is a descendant of another + /// Possible ancestor + /// True iff this tree is a descendant of the other + /// A tree is considered a descendant of itself + public bool IsDescendantOf(Tree ancestor) + { + Tree descendant = this; + while (descendant != null) + { + if (ancestor == descendant) + return true; + descendant = descendant.Parent; + } + return false; + } + + /// + /// Gets whether a tree is a leaf (no children) + public bool IsLeaf + { + get { return m_children.Count == 0; } + } + + /// + /// Gets level, or depth, in tree + public int Level + { + get + { + int result = 0; + Tree ancestor = m_parent; + while (ancestor != null) + { + result++; + ancestor = ancestor.Parent; + } + return result; + } + } + + /// + /// Gets number of descendants, including the tree itself + public int DescendantCount + { + get + { + int n = 0; + foreach (Tree tree in PreOrder) + n++; + return n; + } + } + + /// + /// Gets an enumeration of all the nodes of the tree in pre-order (depth first). + /// For example, the root node is first, followed by its first child (and its + /// children and so on) and then the second child of the root (and its children + /// and so on) etc. + public IEnumerable> PreOrder + { + get + { + Stack> nodes = new Stack>(); + nodes.Push(this); + while (nodes.Count > 0) + { + Tree node = nodes.Pop(); + yield return node; + + // push children in reverse order + for (int i = node.m_children.Count - 1; i >= 0; i--) + nodes.Push(node.m_children[i]); + } + } + } + + /// + /// Gets an enumeration of all the nodes of the tree in post-order. This means that for + /// each node, the children are visited first (starting with the first child) and then + /// the parent node is enumerated. + public IEnumerable> PostOrder + { + get + { + // push each non-leaf node twice to represent nodes whose children haven't been visited + // rather than storing a bit on each node. + Stack> nodes = new Stack>(); + nodes.Push(this); + if (!IsLeaf) + nodes.Push(this); + + while (nodes.Count > 1) + { + Tree node = nodes.Pop(); + if (node != nodes.Peek()) + { + yield return node; + } + else + { + // push children in reverse order + for (int i = node.m_children.Count - 1; i >= 0; i--) + { + Tree child = node.m_children[i]; + nodes.Push(child); + if (!child.IsLeaf) + nodes.Push(child); + } + } + } + + yield return nodes.Pop(); + } + } + + /// + /// Gets an enumeration of all the nodes in a breadth-first order. This means that the + /// root is enumerated first (level 0), followed by all of its children (level 1), + /// followed by all of their children (level 2), and so on. + public IEnumerable> LevelOrder + { + get + { + Queue> nodes = new Queue>(); + nodes.Enqueue(this); + while (nodes.Count > 0) + { + Tree node = nodes.Dequeue(); + yield return node; + + // queue children + foreach (Tree child in node.m_children) + nodes.Enqueue(child); + } + } + } + + private class ChildCollection : Collection> + { + public ChildCollection(Tree parent) + { + m_parent = parent; + } + + protected override void InsertItem(int index, Tree item) + { + item.m_parent = m_parent; + + base.InsertItem(index, item); + } + + protected override void RemoveItem(int index) + { + Items[index].m_parent = null; + + base.RemoveItem(index); + } + + protected override void SetItem(int index, Tree item) + { + Items[index].m_parent = null; + item.m_parent = m_parent; + + base.SetItem(index, item); + } + + protected override void ClearItems() + { + foreach (Tree subTree in Items) + subTree.m_parent = null; + + base.ClearItems(); + } + + private readonly Tree m_parent; + } + + private T m_value; + private Tree m_parent; + private readonly ChildCollection m_children; + } +} \ No newline at end of file diff --git a/framework/Inspectron.Statistics/CsvStatistics.cs b/framework/Inspectron.Statistics/CsvStatistics.cs new file mode 100644 index 0000000..9e0e190 --- /dev/null +++ b/framework/Inspectron.Statistics/CsvStatistics.cs @@ -0,0 +1,48 @@ +using System.Globalization; +using CsvHelper; + +namespace Inspectron.Statistics; + +public class CsvStatistics +{ + private readonly string _filePath; + private readonly StreamWriter _stream; + private readonly FileStream _fileStream; + private readonly CsvWriter _csv; + + public CsvStatistics(string filePath, string[] headers) + { + _filePath = filePath; + var newFile = !File.Exists(filePath); + _fileStream = File.Open(_filePath, FileMode.Append); + _stream = new StreamWriter(_fileStream); + _csv = new CsvWriter(_stream,CultureInfo.InvariantCulture); + if (newFile) + { + foreach (var header in headers) + { + _csv.WriteField(header); + } + _csv.NextRecord(); + _csv.Flush(); + } + } + + public void WriteLine(string[] values) + { + for (int i = 0; i < values.Length; i++) + { + _csv.WriteField(values[i]); + } + _csv.NextRecord(); + _csv.Flush(); + } + + public void Dispose() + { + _csv.Dispose(); + _stream.Dispose(); + _fileStream.Dispose(); + + } +} \ No newline at end of file diff --git a/framework/Inspectron.Statistics/IStatistics.cs b/framework/Inspectron.Statistics/IStatistics.cs new file mode 100644 index 0000000..a380842 --- /dev/null +++ b/framework/Inspectron.Statistics/IStatistics.cs @@ -0,0 +1,6 @@ +namespace Inspectron.Statistics; + +public interface IStatistics:IDisposable +{ + void WriteLine(StatisticsRecord record); +} \ No newline at end of file diff --git a/framework/Inspectron.Statistics/Inspectron.Statistics.csproj b/framework/Inspectron.Statistics/Inspectron.Statistics.csproj new file mode 100644 index 0000000..927e80c --- /dev/null +++ b/framework/Inspectron.Statistics/Inspectron.Statistics.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/framework/Inspectron.Statistics/LockedCsvStatistics.cs b/framework/Inspectron.Statistics/LockedCsvStatistics.cs new file mode 100644 index 0000000..eaa6cc9 --- /dev/null +++ b/framework/Inspectron.Statistics/LockedCsvStatistics.cs @@ -0,0 +1,92 @@ +using System.Globalization; +using CsvHelper; + +namespace Inspectron.Statistics; + +public class LockedCsvStatistics : IStatistics +{ + private readonly string _filePath; + private readonly string _lockFilePath; + private readonly string[] _headers; + + public LockedCsvStatistics(string filePath, string[] headers) + { + _filePath = filePath; + _lockFilePath = filePath + ".lock"; + _headers = headers; + + var directoryPath = Path.GetDirectoryName(filePath); + if (directoryPath != null && !Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + } + + public void WriteLine(StatisticsRecord record) + { + AcquireLock(); + + try + { + bool writeHeaders = !File.Exists(_filePath) || new FileInfo(_filePath).Length == 0; + + using var fileStream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.None); + using var streamWriter = new StreamWriter(fileStream); + using var csvWriter = new CsvWriter(streamWriter, CultureInfo.InvariantCulture); + + if (writeHeaders) + { + foreach (var header in _headers) + { + csvWriter.WriteField(header); + } + csvWriter.NextRecord(); + csvWriter.Flush(); + } + + foreach (var value in record.Values) + { + csvWriter.WriteField(value); + } + csvWriter.NextRecord(); + csvWriter.Flush(); + } + finally + { + ReleaseLock(); + } + } + + public void Dispose() + { + // No resources to dispose + } + + private void AcquireLock() + { + while (true) + { + try + { + using (File.Open(_lockFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None)) + { + // Lock acquired + break; + } + } + catch (IOException) + { + // Lock file already exists, wait and retry + Thread.Sleep(10); + } + } + } + + private void ReleaseLock() + { + if (File.Exists(_lockFilePath)) + { + File.Delete(_lockFilePath); + } + } +} diff --git a/framework/Inspectron.Statistics/StatisticsRecord.cs b/framework/Inspectron.Statistics/StatisticsRecord.cs new file mode 100644 index 0000000..8e2f1cf --- /dev/null +++ b/framework/Inspectron.Statistics/StatisticsRecord.cs @@ -0,0 +1,6 @@ +namespace Inspectron.Statistics; + +public class StatisticsRecord +{ + public string[] Values { get; set; } +} \ No newline at end of file diff --git a/framework/Inspectron.Statistics/StatisticsWritter.cs b/framework/Inspectron.Statistics/StatisticsWritter.cs new file mode 100644 index 0000000..7029e0f --- /dev/null +++ b/framework/Inspectron.Statistics/StatisticsWritter.cs @@ -0,0 +1,64 @@ +using System.Collections.Concurrent; + +namespace Inspectron.Statistics; + +public class StatisticsWritter:IStatistics +{ + private readonly Func _statisticsBuilder; + + private bool _running = true; + public StatisticsWritter(Func statisticsBuilder) + { + _statisticsBuilder = statisticsBuilder; + Thread th = new Thread(WritingLoop); + th.IsBackground = false; + th.Start(); + } + + private void WritingLoop() + { + while (_running) + { + try + { + using (var statistics = _statisticsBuilder()) + { + while (_recordQueue.TryDequeue(out var res)) + { + var error = false; + do + { + try + { + statistics.WriteLine(res); + } + catch (Exception e) + { + error = true; + Thread.Sleep(1000); + } + } while (error); + + } + } + } + catch + { + + } + + Thread.Sleep(1000); + } + } + + public void Dispose() + { + _running = false; + } + + private ConcurrentQueue _recordQueue = new ConcurrentQueue(); + public void WriteLine(StatisticsRecord record) + { + _recordQueue.Enqueue(record); + } +} \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Animations/AnimationDirection.cs b/framework/MaterialSkin.Core/Animations/AnimationDirection.cs new file mode 100644 index 0000000..6537cc9 --- /dev/null +++ b/framework/MaterialSkin.Core/Animations/AnimationDirection.cs @@ -0,0 +1,12 @@ +namespace MaterialSkin.Animations +{ + enum AnimationDirection + { + In, //In. Stops if finished. + Out, //Out. Stops if finished. + InOutIn, //Same as In, but changes to InOutOut if finished. + InOutOut, //Same as Out. + InOutRepeatingIn, // Same as In, but changes to InOutRepeatingOut if finished. + InOutRepeatingOut // Same as Out, but changes to InOutRepeatingIn if finished. + } +} diff --git a/framework/MaterialSkin.Core/Animations/AnimationManager.cs b/framework/MaterialSkin.Core/Animations/AnimationManager.cs new file mode 100644 index 0000000..8e34ea1 --- /dev/null +++ b/framework/MaterialSkin.Core/Animations/AnimationManager.cs @@ -0,0 +1,363 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Windows.Forms; + +namespace MaterialSkin.Animations +{ + class AnimationManager + { + public bool InterruptAnimation { get; set; } + public double Increment { get; set; } + public double SecondaryIncrement { get; set; } + public AnimationType AnimationType { get; set; } + public bool Singular { get; set; } + + public delegate void AnimationFinished(object sender); + public event AnimationFinished OnAnimationFinished; + + public delegate void AnimationProgress(object sender); + public event AnimationProgress OnAnimationProgress; + + private readonly List _animationProgresses; + private readonly List _animationSources; + private readonly List _animationDirections; + private readonly List _animationDatas; + + private const double MIN_VALUE = 0.00; + private const double MAX_VALUE = 1.00; + + private readonly Timer _animationTimer = new Timer { Interval = 5, Enabled = false }; + + /// + /// Constructor + /// + /// If true, only one animation is supported. The current animation will be replaced with the new one. If false, a new animation is added to the list. + public AnimationManager(bool singular = true) + { + _animationProgresses = new List(); + _animationSources = new List(); + _animationDirections = new List(); + _animationDatas = new List(); + + Increment = 0.03; + SecondaryIncrement = 0.03; + AnimationType = AnimationType.Linear; + InterruptAnimation = true; + Singular = singular; + + if (Singular) + { + _animationProgresses.Add(0); + _animationSources.Add(new Point(0, 0)); + _animationDirections.Add(AnimationDirection.In); + } + + _animationTimer.Tick += AnimationTimerOnTick; + } + + private void AnimationTimerOnTick(object sender, EventArgs eventArgs) + { + for (var i = 0; i < _animationProgresses.Count; i++) + { + UpdateProgress(i); + + if (!Singular) + { + if ((_animationDirections[i] == AnimationDirection.InOutIn && _animationProgresses[i] == MAX_VALUE)) + { + _animationDirections[i] = AnimationDirection.InOutOut; + } + else if ((_animationDirections[i] == AnimationDirection.InOutRepeatingIn && _animationProgresses[i] == MIN_VALUE)) + { + _animationDirections[i] = AnimationDirection.InOutRepeatingOut; + } + else if ((_animationDirections[i] == AnimationDirection.InOutRepeatingOut && _animationProgresses[i] == MIN_VALUE)) + { + _animationDirections[i] = AnimationDirection.InOutRepeatingIn; + } + else if ( + (_animationDirections[i] == AnimationDirection.In && _animationProgresses[i] == MAX_VALUE) || + (_animationDirections[i] == AnimationDirection.Out && _animationProgresses[i] == MIN_VALUE) || + (_animationDirections[i] == AnimationDirection.InOutOut && _animationProgresses[i] == MIN_VALUE)) + { + _animationProgresses.RemoveAt(i); + _animationSources.RemoveAt(i); + _animationDirections.RemoveAt(i); + _animationDatas.RemoveAt(i); + } + } + else + { + if ((_animationDirections[i] == AnimationDirection.InOutIn && _animationProgresses[i] == MAX_VALUE)) + { + _animationDirections[i] = AnimationDirection.InOutOut; + } + else if ((_animationDirections[i] == AnimationDirection.InOutRepeatingIn && _animationProgresses[i] == MAX_VALUE)) + { + _animationDirections[i] = AnimationDirection.InOutRepeatingOut; + } + else if ((_animationDirections[i] == AnimationDirection.InOutRepeatingOut && _animationProgresses[i] == MIN_VALUE)) + { + _animationDirections[i] = AnimationDirection.InOutRepeatingIn; + } + } + } + + OnAnimationProgress?.Invoke(this); + } + + public bool IsAnimating() + { + return _animationTimer.Enabled; + } + + public void StartNewAnimation(AnimationDirection animationDirection, object[] data = null) + { + StartNewAnimation(animationDirection, new Point(0, 0), data); + } + + public void StartNewAnimation(AnimationDirection animationDirection, Point animationSource, object[] data = null) + { + if (!IsAnimating() || InterruptAnimation) + { + if (Singular && _animationDirections.Count > 0) + { + _animationDirections[0] = animationDirection; + } + else + { + _animationDirections.Add(animationDirection); + } + + if (Singular && _animationSources.Count > 0) + { + _animationSources[0] = animationSource; + } + else + { + _animationSources.Add(animationSource); + } + + if (!(Singular && _animationProgresses.Count > 0)) + { + switch (_animationDirections[_animationDirections.Count - 1]) + { + case AnimationDirection.InOutRepeatingIn: + case AnimationDirection.InOutIn: + case AnimationDirection.In: + _animationProgresses.Add(MIN_VALUE); + break; + case AnimationDirection.InOutRepeatingOut: + case AnimationDirection.InOutOut: + case AnimationDirection.Out: + _animationProgresses.Add(MAX_VALUE); + break; + default: + throw new Exception("Invalid AnimationDirection"); + } + } + + if (Singular && _animationDatas.Count > 0) + { + _animationDatas[0] = data ?? new object[] { }; + } + else + { + _animationDatas.Add(data ?? new object[] { }); + } + + } + + _animationTimer.Start(); + } + + public void UpdateProgress(int index) + { + switch (_animationDirections[index]) + { + case AnimationDirection.InOutRepeatingIn: + case AnimationDirection.InOutIn: + case AnimationDirection.In: + IncrementProgress(index); + break; + case AnimationDirection.InOutRepeatingOut: + case AnimationDirection.InOutOut: + case AnimationDirection.Out: + DecrementProgress(index); + break; + default: + throw new Exception("No AnimationDirection has been set"); + } + } + + private void IncrementProgress(int index) + { + _animationProgresses[index] += Increment; + if (_animationProgresses[index] > MAX_VALUE) + { + _animationProgresses[index] = MAX_VALUE; + + for (int i = 0; i < GetAnimationCount(); i++) + { + if (_animationDirections[i] == AnimationDirection.InOutIn) return; + if (_animationDirections[i] == AnimationDirection.InOutRepeatingIn) return; + if (_animationDirections[i] == AnimationDirection.InOutRepeatingOut) return; + if (_animationDirections[i] == AnimationDirection.InOutOut && _animationProgresses[i] != MAX_VALUE) return; + if (_animationDirections[i] == AnimationDirection.In && _animationProgresses[i] != MAX_VALUE) return; + } + + _animationTimer.Stop(); + OnAnimationFinished?.Invoke(this); + } + } + + private void DecrementProgress(int index) + { + _animationProgresses[index] -= (_animationDirections[index] == AnimationDirection.InOutOut || _animationDirections[index] == AnimationDirection.InOutRepeatingOut) ? SecondaryIncrement : Increment; + if (_animationProgresses[index] < MIN_VALUE) + { + _animationProgresses[index] = MIN_VALUE; + + for (var i = 0; i < GetAnimationCount(); i++) + { + if (_animationDirections[i] == AnimationDirection.InOutIn) return; + if (_animationDirections[i] == AnimationDirection.InOutRepeatingIn) return; + if (_animationDirections[i] == AnimationDirection.InOutRepeatingOut) return; + if (_animationDirections[i] == AnimationDirection.InOutOut && _animationProgresses[i] != MIN_VALUE) return; + if (_animationDirections[i] == AnimationDirection.Out && _animationProgresses[i] != MIN_VALUE) return; + } + + _animationTimer.Stop(); + OnAnimationFinished?.Invoke(this); + } + } + + public double GetProgress() + { + if (!Singular) + throw new Exception("Animation is not set to Singular."); + + if (_animationProgresses.Count == 0) + throw new Exception("Invalid animation"); + + return GetProgress(0); + } + + public double GetProgress(int index) + { + if (!(index < GetAnimationCount())) + throw new IndexOutOfRangeException("Invalid animation index"); + + switch (AnimationType) + { + case AnimationType.Linear: + return AnimationLinear.CalculateProgress(_animationProgresses[index]); + case AnimationType.EaseInOut: + return AnimationEaseInOut.CalculateProgress(_animationProgresses[index]); + case AnimationType.EaseOut: + return AnimationEaseOut.CalculateProgress(_animationProgresses[index]); + case AnimationType.CustomQuadratic: + return AnimationCustomQuadratic.CalculateProgress(_animationProgresses[index]); + default: + throw new NotImplementedException("The given AnimationType is not implemented"); + } + + } + + public Point GetSource(int index) + { + if (!(index < GetAnimationCount())) + throw new IndexOutOfRangeException("Invalid animation index"); + + return _animationSources[index]; + } + + public Point GetSource() + { + if (!Singular) + throw new Exception("Animation is not set to Singular."); + + if (_animationSources.Count == 0) + throw new Exception("Invalid animation"); + + return _animationSources[0]; + } + + public AnimationDirection GetDirection() + { + if (!Singular) + throw new Exception("Animation is not set to Singular."); + + if (_animationDirections.Count == 0) + throw new Exception("Invalid animation"); + + return _animationDirections[0]; + } + + public AnimationDirection GetDirection(int index) + { + if (!(index < _animationDirections.Count)) + throw new IndexOutOfRangeException("Invalid animation index"); + + return _animationDirections[index]; + } + + public object[] GetData() + { + if (!Singular) + throw new Exception("Animation is not set to Singular."); + + if (_animationDatas.Count == 0) + throw new Exception("Invalid animation"); + + return _animationDatas[0]; + } + + public object[] GetData(int index) + { + if (!(index < _animationDatas.Count)) + throw new IndexOutOfRangeException("Invalid animation index"); + + return _animationDatas[index]; + } + + public int GetAnimationCount() + { + return _animationProgresses.Count; + } + + public void SetProgress(double progress) + { + if (!Singular) + throw new Exception("Animation is not set to Singular."); + + if (_animationProgresses.Count == 0) + throw new Exception("Invalid animation"); + + _animationProgresses[0] = progress; + } + + public void SetDirection(AnimationDirection direction) + { + if (!Singular) + throw new Exception("Animation is not set to Singular."); + + if (_animationProgresses.Count == 0) + throw new Exception("Invalid animation"); + + _animationDirections[0] = direction; + } + + public void SetData(object[] data) + { + if (!Singular) + throw new Exception("Animation is not set to Singular."); + + if (_animationDatas.Count == 0) + throw new Exception("Invalid animation"); + + _animationDatas[0] = data; + } + } +} diff --git a/framework/MaterialSkin.Core/Animations/Animations.cs b/framework/MaterialSkin.Core/Animations/Animations.cs new file mode 100644 index 0000000..4dd3d40 --- /dev/null +++ b/framework/MaterialSkin.Core/Animations/Animations.cs @@ -0,0 +1,53 @@ +using System; + +namespace MaterialSkin.Animations +{ + enum AnimationType + { + Linear, + EaseInOut, + EaseOut, + CustomQuadratic + } + + static class AnimationLinear + { + public static double CalculateProgress(double progress) + { + return progress; + } + } + + static class AnimationEaseInOut + { + public static double PI = Math.PI; + public static double PI_HALF = Math.PI / 2; + + public static double CalculateProgress(double progress) + { + return EaseInOut(progress); + } + + private static double EaseInOut(double s) + { + return s - Math.Sin(s * 2 * PI) / (2 * PI); + } + } + + public static class AnimationEaseOut + { + public static double CalculateProgress(double progress) + { + return -1 * progress * (progress - 2); + } + } + + public static class AnimationCustomQuadratic + { + public static double CalculateProgress(double progress) + { + var kickoff = 0.6; + return 1 - Math.Cos((Math.Max(progress, kickoff) - kickoff) * Math.PI / (2 - (2 * kickoff))); + } + } +} diff --git a/framework/MaterialSkin.Core/ColorScheme.cs b/framework/MaterialSkin.Core/ColorScheme.cs new file mode 100644 index 0000000..4119bb1 --- /dev/null +++ b/framework/MaterialSkin.Core/ColorScheme.cs @@ -0,0 +1,371 @@ +using System.Drawing; + +namespace MaterialSkin +{ + public class ColorScheme + { + public readonly Color PrimaryColor, DarkPrimaryColor, LightPrimaryColor, AccentColor, TextColor; + public readonly Pen PrimaryPen, DarkPrimaryPen, LightPrimaryPen, AccentPen, TextPen; + public readonly Brush PrimaryBrush, DarkPrimaryBrush, LightPrimaryBrush, AccentBrush, TextBrush; + + /// + /// Defines the Color Scheme to be used for all forms. + /// + /// The primary color, a -500 color is suggested here. + /// A darker version of the primary color, a -700 color is suggested here. + /// A lighter version of the primary color, a -100 color is suggested here. + /// The accent color, a -200 color is suggested here. + /// The text color, the one with the highest contrast is suggested. + public ColorScheme(Primary primary, Primary darkPrimary, Primary lightPrimary, Accent accent, TextShade textShade) + { + //Color + PrimaryColor = ((int)primary).ToColor(); + DarkPrimaryColor = ((int)darkPrimary).ToColor(); + LightPrimaryColor = ((int)lightPrimary).ToColor(); + AccentColor = ((int)accent).ToColor(); + TextColor = ((int)textShade).ToColor(); + + //Pen + PrimaryPen = new Pen(PrimaryColor); + DarkPrimaryPen = new Pen(DarkPrimaryColor); + LightPrimaryPen = new Pen(LightPrimaryColor); + AccentPen = new Pen(AccentColor); + TextPen = new Pen(TextColor); + + //Brush + PrimaryBrush = new SolidBrush(PrimaryColor); + DarkPrimaryBrush = new SolidBrush(DarkPrimaryColor); + LightPrimaryBrush = new SolidBrush(LightPrimaryColor); + AccentBrush = new SolidBrush(AccentColor); + TextBrush = new SolidBrush(TextColor); + } + + public ColorScheme(int primary, int darkPrimary, int lightPrimary, Accent accent, TextShade textShade) + { + //Color + PrimaryColor = ((int)primary).ToColor(); + DarkPrimaryColor = ((int)darkPrimary).ToColor(); + LightPrimaryColor = ((int)lightPrimary).ToColor(); + AccentColor = ((int)accent).ToColor(); + TextColor = ((int)textShade).ToColor(); + + //Pen + PrimaryPen = new Pen(PrimaryColor); + DarkPrimaryPen = new Pen(DarkPrimaryColor); + LightPrimaryPen = new Pen(LightPrimaryColor); + AccentPen = new Pen(AccentColor); + TextPen = new Pen(TextColor); + + //Brush + PrimaryBrush = new SolidBrush(PrimaryColor); + DarkPrimaryBrush = new SolidBrush(DarkPrimaryColor); + LightPrimaryBrush = new SolidBrush(LightPrimaryColor); + AccentBrush = new SolidBrush(AccentColor); + TextBrush = new SolidBrush(TextColor); + } + } + + public static class ColorExtension + { + /// + /// Convert an integer number to a Color. + /// + /// + public static Color ToColor(this int argb) + { + return Color.FromArgb( + (argb & 0xff0000) >> 16, + (argb & 0xff00) >> 8, + argb & 0xff); + } + + /// + /// Removes the alpha component of a color. + /// + /// + /// + public static Color RemoveAlpha(this Color color) + { + return Color.FromArgb(color.R, color.G, color.B); + } + + /// + /// Converts a 0-100 integer to a 0-255 color component. + /// + /// + /// + public static int PercentageToColorComponent(this int percentage) + { + return (int)((percentage / 100d) * 255d); + } + } + + //Color constantes + public enum TextShade + { + WHITE = 0xFFFFFF, + BLACK = 0x212121 + } + + public enum Primary + { + Red50 = 0xFFEBEE, + Red100 = 0xFFCDD2, + Red200 = 0xEF9A9A, + Red300 = 0xE57373, + Red400 = 0xEF5350, + Red500 = 0xF44336, + Red600 = 0xE53935, + Red700 = 0xD32F2F, + Red800 = 0xC62828, + Red900 = 0xB71C1C, + Pink50 = 0xFCE4EC, + Pink100 = 0xF8BBD0, + Pink200 = 0xF48FB1, + Pink300 = 0xF06292, + Pink400 = 0xEC407A, + Pink500 = 0xE91E63, + Pink600 = 0xD81B60, + Pink700 = 0xC2185B, + Pink800 = 0xAD1457, + Pink900 = 0x880E4F, + Purple50 = 0xF3E5F5, + Purple100 = 0xE1BEE7, + Purple200 = 0xCE93D8, + Purple300 = 0xBA68C8, + Purple400 = 0xAB47BC, + Purple500 = 0x9C27B0, + Purple600 = 0x8E24AA, + Purple700 = 0x7B1FA2, + Purple800 = 0x6A1B9A, + Purple900 = 0x4A148C, + DeepPurple50 = 0xEDE7F6, + DeepPurple100 = 0xD1C4E9, + DeepPurple200 = 0xB39DDB, + DeepPurple300 = 0x9575CD, + DeepPurple400 = 0x7E57C2, + DeepPurple500 = 0x673AB7, + DeepPurple600 = 0x5E35B1, + DeepPurple700 = 0x512DA8, + DeepPurple800 = 0x4527A0, + DeepPurple900 = 0x311B92, + Indigo50 = 0xE8EAF6, + Indigo100 = 0xC5CAE9, + Indigo200 = 0x9FA8DA, + Indigo300 = 0x7986CB, + Indigo400 = 0x5C6BC0, + Indigo500 = 0x3F51B5, + Indigo600 = 0x3949AB, + Indigo700 = 0x303F9F, + Indigo800 = 0x283593, + Indigo900 = 0x1A237E, + Blue50 = 0xE3F2FD, + Blue100 = 0xBBDEFB, + Blue200 = 0x90CAF9, + Blue300 = 0x64B5F6, + Blue400 = 0x42A5F5, + Blue500 = 0x2196F3, + Blue600 = 0x1E88E5, + Blue700 = 0x1976D2, + Blue800 = 0x1565C0, + Blue900 = 0x0D47A1, + LightBlue50 = 0xE1F5FE, + LightBlue100 = 0xB3E5FC, + LightBlue200 = 0x81D4FA, + LightBlue300 = 0x4FC3F7, + LightBlue400 = 0x29B6F6, + LightBlue500 = 0x03A9F4, + LightBlue600 = 0x039BE5, + LightBlue700 = 0x0288D1, + LightBlue800 = 0x0277BD, + LightBlue900 = 0x01579B, + Cyan50 = 0xE0F7FA, + Cyan100 = 0xB2EBF2, + Cyan200 = 0x80DEEA, + Cyan300 = 0x4DD0E1, + Cyan400 = 0x26C6DA, + Cyan500 = 0x00BCD4, + Cyan600 = 0x00ACC1, + Cyan700 = 0x0097A7, + Cyan800 = 0x00838F, + Cyan900 = 0x006064, + Teal50 = 0xE0F2F1, + Teal100 = 0xB2DFDB, + Teal200 = 0x80CBC4, + Teal300 = 0x4DB6AC, + Teal400 = 0x26A69A, + Teal500 = 0x009688, + Teal600 = 0x00897B, + Teal700 = 0x00796B, + Teal800 = 0x00695C, + Teal900 = 0x004D40, + Green50 = 0xE8F5E9, + Green100 = 0xC8E6C9, + Green200 = 0xA5D6A7, + Green300 = 0x81C784, + Green400 = 0x66BB6A, + Green500 = 0x4CAF50, + Green600 = 0x43A047, + Green700 = 0x388E3C, + Green800 = 0x2E7D32, + Green900 = 0x1B5E20, + LightGreen50 = 0xF1F8E9, + LightGreen100 = 0xDCEDC8, + LightGreen200 = 0xC5E1A5, + LightGreen300 = 0xAED581, + LightGreen400 = 0x9CCC65, + LightGreen500 = 0x8BC34A, + LightGreen600 = 0x7CB342, + LightGreen700 = 0x689F38, + LightGreen800 = 0x558B2F, + LightGreen900 = 0x33691E, + Lime50 = 0xF9FBE7, + Lime100 = 0xF0F4C3, + Lime200 = 0xE6EE9C, + Lime300 = 0xDCE775, + Lime400 = 0xD4E157, + Lime500 = 0xCDDC39, + Lime600 = 0xC0CA33, + Lime700 = 0xAFB42B, + Lime800 = 0x9E9D24, + Lime900 = 0x827717, + Yellow50 = 0xFFFDE7, + Yellow100 = 0xFFF9C4, + Yellow200 = 0xFFF59D, + Yellow300 = 0xFFF176, + Yellow400 = 0xFFEE58, + Yellow500 = 0xFFEB3B, + Yellow600 = 0xFDD835, + Yellow700 = 0xFBC02D, + Yellow800 = 0xF9A825, + Yellow900 = 0xF57F17, + Amber50 = 0xFFF8E1, + Amber100 = 0xFFECB3, + Amber200 = 0xFFE082, + Amber300 = 0xFFD54F, + Amber400 = 0xFFCA28, + Amber500 = 0xFFC107, + Amber600 = 0xFFB300, + Amber700 = 0xFFA000, + Amber800 = 0xFF8F00, + Amber900 = 0xFF6F00, + Orange50 = 0xFFF3E0, + Orange100 = 0xFFE0B2, + Orange200 = 0xFFCC80, + Orange300 = 0xFFB74D, + Orange400 = 0xFFA726, + Orange500 = 0xFF9800, + Orange600 = 0xFB8C00, + Orange700 = 0xF57C00, + Orange800 = 0xEF6C00, + Orange900 = 0xE65100, + DeepOrange50 = 0xFBE9E7, + DeepOrange100 = 0xFFCCBC, + DeepOrange200 = 0xFFAB91, + DeepOrange300 = 0xFF8A65, + DeepOrange400 = 0xFF7043, + DeepOrange500 = 0xFF5722, + DeepOrange600 = 0xF4511E, + DeepOrange700 = 0xE64A19, + DeepOrange800 = 0xD84315, + DeepOrange900 = 0xBF360C, + Brown50 = 0xEFEBE9, + Brown100 = 0xD7CCC8, + Brown200 = 0xBCAAA4, + Brown300 = 0xA1887F, + Brown400 = 0x8D6E63, + Brown500 = 0x795548, + Brown600 = 0x6D4C41, + Brown700 = 0x5D4037, + Brown800 = 0x4E342E, + Brown900 = 0x3E2723, + Grey50 = 0xFAFAFA, + Grey100 = 0xF5F5F5, + Grey200 = 0xEEEEEE, + Grey300 = 0xE0E0E0, + Grey400 = 0xBDBDBD, + Grey500 = 0x9E9E9E, + Grey600 = 0x757575, + Grey700 = 0x616161, + Grey800 = 0x424242, + Grey900 = 0x212121, + BlueGrey50 = 0xECEFF1, + BlueGrey100 = 0xCFD8DC, + BlueGrey200 = 0xB0BEC5, + BlueGrey300 = 0x90A4AE, + BlueGrey400 = 0x78909C, + BlueGrey500 = 0x607D8B, + BlueGrey600 = 0x546E7A, + BlueGrey700 = 0x455A64, + BlueGrey800 = 0x37474F, + BlueGrey900 = 0x263238 + } + + public enum Accent + { + Red100 = 0xFF8A80, + Red200 = 0xFF5252, + Red400 = 0xFF1744, + Red700 = 0xD50000, + Pink100 = 0xFF80AB, + Pink200 = 0xFF4081, + Pink400 = 0xF50057, + Pink700 = 0xC51162, + Purple100 = 0xEA80FC, + Purple200 = 0xE040FB, + Purple400 = 0xD500F9, + Purple700 = 0xAA00FF, + DeepPurple100 = 0xB388FF, + DeepPurple200 = 0x7C4DFF, + DeepPurple400 = 0x651FFF, + DeepPurple700 = 0x6200EA, + Indigo100 = 0x8C9EFF, + Indigo200 = 0x536DFE, + Indigo400 = 0x3D5AFE, + Indigo700 = 0x304FFE, + Blue100 = 0x82B1FF, + Blue200 = 0x448AFF, + Blue400 = 0x2979FF, + Blue700 = 0x2962FF, + LightBlue100 = 0x80D8FF, + LightBlue200 = 0x40C4FF, + LightBlue400 = 0x00B0FF, + LightBlue700 = 0x0091EA, + Cyan100 = 0x84FFFF, + Cyan200 = 0x18FFFF, + Cyan400 = 0x00E5FF, + Cyan700 = 0x00B8D4, + Teal100 = 0xA7FFEB, + Teal200 = 0x64FFDA, + Teal400 = 0x1DE9B6, + Teal700 = 0x00BFA5, + Green100 = 0xB9F6CA, + Green200 = 0x69F0AE, + Green400 = 0x00E676, + Green700 = 0x00C853, + LightGreen100 = 0xCCFF90, + LightGreen200 = 0xB2FF59, + LightGreen400 = 0x76FF03, + LightGreen700 = 0x64DD17, + Lime100 = 0xF4FF81, + Lime200 = 0xEEFF41, + Lime400 = 0xC6FF00, + Lime700 = 0xAEEA00, + Yellow100 = 0xFFFF8D, + Yellow200 = 0xFFFF00, + Yellow400 = 0xFFEA00, + Yellow700 = 0xFFD600, + Amber100 = 0xFFE57F, + Amber200 = 0xFFD740, + Amber400 = 0xFFC400, + Amber700 = 0xFFAB00, + Orange100 = 0xFFD180, + Orange200 = 0xFFAB40, + Orange400 = 0xFF9100, + Orange700 = 0xFF6D00, + DeepOrange100 = 0xFF9E80, + DeepOrange200 = 0xFF6E40, + DeepOrange400 = 0xFF3D00, + DeepOrange700 = 0xDD2C00 + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialCheckBox.cs b/framework/MaterialSkin.Core/Controls/MaterialCheckBox.cs new file mode 100644 index 0000000..b10ad0f --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialCheckBox.cs @@ -0,0 +1,250 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Text; +using System.Windows.Forms; +using MaterialSkin.Animations; + +namespace MaterialSkin.Controls +{ + public class MaterialCheckBox : CheckBox, IMaterialControl + { + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + [Browsable(false)] + public Point MouseLocation { get; set; } + + private bool _ripple; + [Category("Behavior")] + public bool Ripple + { + get { return _ripple; } + set + { + _ripple = value; + AutoSize = AutoSize; //Make AutoSize directly set the bounds. + + if (value) + { + Margin = new Padding(0); + } + + Invalidate(); + } + } + + private readonly AnimationManager _animationManager; + private readonly AnimationManager _rippleAnimationManager; + + private const int CHECKBOX_SIZE = 18; + private const int CHECKBOX_SIZE_HALF = CHECKBOX_SIZE / 2; + private const int CHECKBOX_INNER_BOX_SIZE = CHECKBOX_SIZE - 4; + + private int _boxOffset; + private Rectangle _boxRectangle; + + public MaterialCheckBox() + { + _animationManager = new AnimationManager + { + AnimationType = AnimationType.EaseInOut, + Increment = 0.05 + }; + _rippleAnimationManager = new AnimationManager(false) + { + AnimationType = AnimationType.Linear, + Increment = 0.10, + SecondaryIncrement = 0.08 + }; + _animationManager.OnAnimationProgress += sender => Invalidate(); + _rippleAnimationManager.OnAnimationProgress += sender => Invalidate(); + + CheckedChanged += (sender, args) => + { + _animationManager.StartNewAnimation(Checked ? AnimationDirection.In : AnimationDirection.Out); + }; + + Ripple = true; + MouseLocation = new Point(-1, -1); + } + + protected override void OnSizeChanged(EventArgs e) + { + base.OnSizeChanged(e); + + _boxOffset = Height / 2 - 9; + _boxRectangle = new Rectangle(_boxOffset, _boxOffset, CHECKBOX_SIZE - 1, CHECKBOX_SIZE - 1); + } + + public override Size GetPreferredSize(Size proposedSize) + { + var w = _boxOffset + CHECKBOX_SIZE + 2 + (int)CreateGraphics().MeasureString(Text, SkinManager.ROBOTO_MEDIUM_10).Width; + return Ripple ? new Size(w, 30) : new Size(w, 20); + } + + private static readonly Point[] CheckmarkLine = { new Point(3, 8), new Point(7, 12), new Point(14, 5) }; + private const int TEXT_OFFSET = 22; + protected override void OnPaint(PaintEventArgs pevent) + { + var g = pevent.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.TextRenderingHint = TextRenderingHint.AntiAlias; + + // clear the control + g.Clear(Parent.BackColor); + + var CHECKBOX_CENTER = _boxOffset + CHECKBOX_SIZE_HALF - 1; + + var animationProgress = _animationManager.GetProgress(); + + var colorAlpha = Enabled ? (int)(animationProgress * 255.0) : SkinManager.GetCheckBoxOffDisabledColor().A; + var backgroundAlpha = Enabled ? (int)(SkinManager.GetCheckboxOffColor().A * (1.0 - animationProgress)) : SkinManager.GetCheckBoxOffDisabledColor().A; + + var brush = new SolidBrush(Color.FromArgb(colorAlpha, Enabled ? SkinManager.ColorScheme.AccentColor : SkinManager.GetCheckBoxOffDisabledColor())); + var brush3 = new SolidBrush(Enabled ? SkinManager.ColorScheme.AccentColor : SkinManager.GetCheckBoxOffDisabledColor()); + var pen = new Pen(brush.Color); + + // draw ripple animation + if (Ripple && _rippleAnimationManager.IsAnimating()) + { + for (var i = 0; i < _rippleAnimationManager.GetAnimationCount(); i++) + { + var animationValue = _rippleAnimationManager.GetProgress(i); + var animationSource = new Point(CHECKBOX_CENTER, CHECKBOX_CENTER); + var rippleBrush = new SolidBrush(Color.FromArgb((int)((animationValue * 40)), ((bool)_rippleAnimationManager.GetData(i)[0]) ? Color.Black : brush.Color)); + var rippleHeight = (Height % 2 == 0) ? Height - 3 : Height - 2; + var rippleSize = (_rippleAnimationManager.GetDirection(i) == AnimationDirection.InOutIn) ? (int)(rippleHeight * (0.8d + (0.2d * animationValue))) : rippleHeight; + using (var path = DrawHelper.CreateRoundRect(animationSource.X - rippleSize / 2, animationSource.Y - rippleSize / 2, rippleSize, rippleSize, rippleSize / 2)) + { + g.FillPath(rippleBrush, path); + } + + rippleBrush.Dispose(); + } + } + + brush3.Dispose(); + + var checkMarkLineFill = new Rectangle(_boxOffset, _boxOffset, (int)((int)(FontHeight * 1.1) * animationProgress), (int)(FontHeight * 1.1)); + using (var checkmarkPath = DrawHelper.CreateRoundRect(_boxOffset, _boxOffset, (int)(FontHeight*1.1), (int)(FontHeight * 1.1), 1f)) + { + var brush2 = new SolidBrush(DrawHelper.BlendColor(Parent.BackColor, Enabled ? SkinManager.GetCheckboxOffColor() : SkinManager.GetCheckBoxOffDisabledColor(), backgroundAlpha)); + var pen2 = new Pen(brush2.Color); + g.FillPath(brush2, checkmarkPath); + g.DrawPath(pen2, checkmarkPath); + + g.FillRectangle(new SolidBrush(Parent.BackColor), _boxOffset + 2, _boxOffset + 2, (int)(FontHeight * 1.1) -4, (int)(FontHeight * 1.1) -4); + g.DrawRectangle(new Pen(Parent.BackColor), _boxOffset + 2, _boxOffset + 2, (int)(FontHeight * 1.1) -4, (int)(FontHeight * 1.1) - 4); + + brush2.Dispose(); + pen2.Dispose(); + + if (Enabled) + { + g.FillPath(brush, checkmarkPath); + g.DrawPath(pen, checkmarkPath); + } + else if (Checked) + { + g.SmoothingMode = SmoothingMode.None; + g.FillRectangle(brush, _boxOffset + 2, _boxOffset + 2, CHECKBOX_INNER_BOX_SIZE, CHECKBOX_INNER_BOX_SIZE); + g.SmoothingMode = SmoothingMode.AntiAlias; + } + + g.DrawImageUnscaledAndClipped(DrawCheckMarkBitmap(), checkMarkLineFill); + } + + // draw checkbox text + SizeF stringSize = g.MeasureString(Text, Font); + g.DrawString( + Text, + Font, + Enabled ? SkinManager.GetPrimaryTextBrush() : SkinManager.GetDisabledOrHintBrush(), + _boxOffset + TEXT_OFFSET, Height / 2 - stringSize.Height / 2); + + // dispose used paint objects + pen.Dispose(); + brush.Dispose(); + } + + private Bitmap DrawCheckMarkBitmap() + { + var checkMark = new Bitmap(CHECKBOX_SIZE, CHECKBOX_SIZE); + var g = Graphics.FromImage(checkMark); + + // clear everything, transparent + g.Clear(Color.Transparent); + + // draw the checkmark lines + using (var pen = new Pen(Parent.BackColor, 2)) + { + g.DrawLines(pen, CheckmarkLine); + } + + return checkMark; + } + + public override bool AutoSize + { + get { return base.AutoSize; } + set + { + base.AutoSize = value; + if (value) + { + Size = new Size(10, 10); + } + } + } + + private bool IsMouseInCheckArea() + { + return _boxRectangle.Contains(MouseLocation); + } + + protected override void OnCreateControl() + { + base.OnCreateControl(); + Font = SkinManager.ROBOTO_MEDIUM_10; + + if (DesignMode) return; + + MouseState = MouseState.OUT; + MouseEnter += (sender, args) => + { + MouseState = MouseState.HOVER; + }; + MouseLeave += (sender, args) => + { + MouseLocation = new Point(-1, -1); + MouseState = MouseState.OUT; + }; + MouseDown += (sender, args) => + { + MouseState = MouseState.DOWN; + + if (Ripple && args.Button == MouseButtons.Left && IsMouseInCheckArea()) + { + _rippleAnimationManager.SecondaryIncrement = 0; + _rippleAnimationManager.StartNewAnimation(AnimationDirection.InOutIn, new object[] { Checked }); + } + }; + MouseUp += (sender, args) => + { + MouseState = MouseState.HOVER; + _rippleAnimationManager.SecondaryIncrement = 0.08; + }; + MouseMove += (sender, args) => + { + MouseLocation = args.Location; + Cursor = IsMouseInCheckArea() ? Cursors.Hand : Cursors.Default; + }; + } + + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialComboBox.cs b/framework/MaterialSkin.Core/Controls/MaterialComboBox.cs new file mode 100644 index 0000000..6fd1853 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialComboBox.cs @@ -0,0 +1,44 @@ +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + + +namespace MaterialSkin.Controls +{ + public partial class MaterialComboBox : ComboBox + { + + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + public MaterialComboBox() + { + + DrawMode = DrawMode.OwnerDrawFixed; + + DrawItem += AdvancedComboBox_DrawItem; + + } + + void AdvancedComboBox_DrawItem(object sender, DrawItemEventArgs e) + { + if (e.Index < 0) + return; + + ComboBox combo = sender as ComboBox; + if ((e.State & DrawItemState.Selected) == DrawItemState.Selected) + e.Graphics.FillRectangle(new SolidBrush(SkinManager.ColorScheme.AccentColor), + e.Bounds); + else + e.Graphics.FillRectangle(new SolidBrush(combo.BackColor), + e.Bounds); + + e.Graphics.DrawString(combo.Items[e.Index].ToString(), e.Font, + SkinManager.GetRaisedButtonTextBrush((e.State & DrawItemState.Selected) == DrawItemState.Selected), + new Point(e.Bounds.X, e.Bounds.Y)); + + e.DrawFocusRectangle(); + } + + + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialContextMenuStrip.cs b/framework/MaterialSkin.Core/Controls/MaterialContextMenuStrip.cs new file mode 100644 index 0000000..fc05b13 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialContextMenuStrip.cs @@ -0,0 +1,195 @@ +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Text; +using System.Windows.Forms; +using MaterialSkin.Animations; + +namespace MaterialSkin.Controls +{ + public class MaterialContextMenuStrip : ContextMenuStrip, IMaterialControl + { + //Properties for managing the material design properties + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + + + internal AnimationManager AnimationManager; + internal Point AnimationSource; + + public delegate void ItemClickStart(object sender, ToolStripItemClickedEventArgs e); + public event ItemClickStart OnItemClickStart; + + public MaterialContextMenuStrip() + { + Renderer = new MaterialToolStripRender(); + + AnimationManager = new AnimationManager(false) + { + Increment = 0.07, + AnimationType = AnimationType.Linear + }; + AnimationManager.OnAnimationProgress += sender => Invalidate(); + AnimationManager.OnAnimationFinished += sender => OnItemClicked(_delayesArgs); + + BackColor = SkinManager.GetApplicationBackgroundColor(); + } + + protected override void OnMouseUp(MouseEventArgs mea) + { + base.OnMouseUp(mea); + + AnimationSource = mea.Location; + } + + private ToolStripItemClickedEventArgs _delayesArgs; + protected override void OnItemClicked(ToolStripItemClickedEventArgs e) + { + if (e.ClickedItem != null && !(e.ClickedItem is ToolStripSeparator)) + { + if (e == _delayesArgs) + { + //The event has been fired manualy because the args are the ones we saved for delay + base.OnItemClicked(e); + } + else + { + //Interrupt the default on click, saving the args for the delay which is needed to display the animaton + _delayesArgs = e; + + //Fire custom event to trigger actions directly but keep cms open + OnItemClickStart?.Invoke(this, e); + + //Start animation + AnimationManager.StartNewAnimation(AnimationDirection.In); + } + } + } + } + + public class MaterialToolStripMenuItem : ToolStripMenuItem + { + public MaterialToolStripMenuItem() + { + AutoSize = false; + Size = new Size(120, 30); + } + + protected override ToolStripDropDown CreateDefaultDropDown() + { + var baseDropDown = base.CreateDefaultDropDown(); + if (DesignMode) return baseDropDown; + + var defaultDropDown = new MaterialContextMenuStrip(); + defaultDropDown.Items.AddRange(baseDropDown.Items); + + return defaultDropDown; + } + } + + internal class MaterialToolStripRender : ToolStripProfessionalRenderer, IMaterialControl + { + //Properties for managing the material design properties + public int Depth { get; set; } + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + public MouseState MouseState { get; set; } + + + protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) + { + var g = e.Graphics; + g.TextRenderingHint = TextRenderingHint.AntiAlias; + + var itemRect = GetItemRect(e.Item); + var textRect = new Rectangle(24, itemRect.Y, itemRect.Width - (24 + 16), itemRect.Height); + g.DrawString( + e.Text, + SkinManager.ROBOTO_MEDIUM_10, + e.Item.Enabled ? SkinManager.GetPrimaryTextBrush() : SkinManager.GetDisabledOrHintBrush(), + textRect, + new StringFormat { LineAlignment = StringAlignment.Center }); + } + + protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e) + { + var g = e.Graphics; + g.Clear(SkinManager.GetApplicationBackgroundColor()); + + //Draw background + var itemRect = GetItemRect(e.Item); + g.FillRectangle(e.Item.Selected && e.Item.Enabled ? SkinManager.GetCmsSelectedItemBrush() : new SolidBrush(SkinManager.GetApplicationBackgroundColor()), itemRect); + + //Ripple animation + var toolStrip = e.ToolStrip as MaterialContextMenuStrip; + if (toolStrip != null) + { + var animationManager = toolStrip.AnimationManager; + var animationSource = toolStrip.AnimationSource; + if (toolStrip.AnimationManager.IsAnimating() && e.Item.Bounds.Contains(animationSource)) + { + for (int i = 0; i < animationManager.GetAnimationCount(); i++) + { + var animationValue = animationManager.GetProgress(i); + var rippleBrush = new SolidBrush(Color.FromArgb((int)(51 - (animationValue * 50)), Color.Black)); + var rippleSize = (int)(animationValue * itemRect.Width * 2.5); + g.FillEllipse(rippleBrush, new Rectangle(animationSource.X - rippleSize / 2, itemRect.Y - itemRect.Height, rippleSize, itemRect.Height * 3)); + } + } + } + } + + protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) + { + //base.OnRenderImageMargin(e); + } + + protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) + { + var g = e.Graphics; + + g.FillRectangle(new SolidBrush(SkinManager.GetApplicationBackgroundColor()), e.Item.Bounds); + g.DrawLine( + new Pen(SkinManager.GetDividersColor()), + new Point(e.Item.Bounds.Left, e.Item.Bounds.Height / 2), + new Point(e.Item.Bounds.Right, e.Item.Bounds.Height / 2)); + } + + protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) + { + var g = e.Graphics; + + g.DrawRectangle( + new Pen(SkinManager.GetDividersColor()), + new Rectangle(e.AffectedBounds.X, e.AffectedBounds.Y, e.AffectedBounds.Width - 1, e.AffectedBounds.Height - 1)); + } + + protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) + { + var g = e.Graphics; + const int ARROW_SIZE = 4; + + var arrowMiddle = new Point(e.ArrowRectangle.X + e.ArrowRectangle.Width / 2, e.ArrowRectangle.Y + e.ArrowRectangle.Height / 2); + var arrowBrush = e.Item.Enabled ? SkinManager.GetPrimaryTextBrush() : SkinManager.GetDisabledOrHintBrush(); + using (var arrowPath = new GraphicsPath()) + { + arrowPath.AddLines( + new[] { + new Point(arrowMiddle.X - ARROW_SIZE, arrowMiddle.Y - ARROW_SIZE), + new Point(arrowMiddle.X, arrowMiddle.Y), + new Point(arrowMiddle.X - ARROW_SIZE, arrowMiddle.Y + ARROW_SIZE) }); + arrowPath.CloseFigure(); + + g.FillPath(arrowBrush, arrowPath); + } + } + + private Rectangle GetItemRect(ToolStripItem item) + { + return new Rectangle(0, item.ContentRectangle.Y, item.ContentRectangle.Width + 4, item.ContentRectangle.Height); + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialDivider.cs b/framework/MaterialSkin.Core/Controls/MaterialDivider.cs new file mode 100644 index 0000000..b666544 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialDivider.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using System.Windows.Forms; + +namespace MaterialSkin.Controls +{ + public sealed class MaterialDivider : Control, IMaterialControl + { + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + + public MaterialDivider() + { + SetStyle(ControlStyles.SupportsTransparentBackColor, true); + + } + + protected override void OnPaintBackground(PaintEventArgs pevent) + { + BackColor = SkinManager.ColorScheme.PrimaryColor; + base.OnPaintBackground(pevent); + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialFlatButton.cs b/framework/MaterialSkin.Core/Controls/MaterialFlatButton.cs new file mode 100644 index 0000000..02fdbde --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialFlatButton.cs @@ -0,0 +1,200 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Text; +using System.Windows.Forms; +using MaterialSkin.Animations; + +namespace MaterialSkin.Controls +{ + public class MaterialFlatButton : Button, IMaterialControl + { + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + public bool Primary { get; set; } + + private readonly AnimationManager _animationManager; + private readonly AnimationManager _hoverAnimationManager; + + private SizeF _textSize; + + private Image _icon; + public Image Icon + { + get { return _icon; } + set + { + _icon = value; + if (AutoSize) + Size = GetPreferredSize(); + Invalidate(); + } + } + + public MaterialFlatButton() + { + Primary = false; + + _animationManager = new AnimationManager(false) + { + Increment = 0.03, + AnimationType = AnimationType.EaseOut + }; + _hoverAnimationManager = new AnimationManager + { + Increment = 0.07, + AnimationType = AnimationType.Linear + }; + + _hoverAnimationManager.OnAnimationProgress += sender => Invalidate(); + _animationManager.OnAnimationProgress += sender => Invalidate(); + + AutoSizeMode = AutoSizeMode.GrowAndShrink; + + Margin = new Padding(4, 6, 4, 6); + Padding = new Padding(0); + } + + public override string Text + { + get { return base.Text; } + set + { + base.Text = value; + _textSize = CreateGraphics().MeasureString(value.ToUpper(), SkinManager.ROBOTO_MEDIUM_10); + if (AutoSize) + Size = GetPreferredSize(); + Invalidate(); + } + } + + protected override void OnPaint(PaintEventArgs pevent) + { + var g = pevent.Graphics; + g.TextRenderingHint = TextRenderingHint.AntiAlias; + + g.Clear(Parent.BackColor); + + //Hover + Color c = SkinManager.GetFlatButtonHoverBackgroundColor(); + using (Brush b = new SolidBrush(Color.FromArgb((int)(_hoverAnimationManager.GetProgress() * c.A), c.RemoveAlpha()))) + g.FillRectangle(b, ClientRectangle); + + //Ripple + if (_animationManager.IsAnimating()) + { + g.SmoothingMode = SmoothingMode.AntiAlias; + for (var i = 0; i < _animationManager.GetAnimationCount(); i++) + { + var animationValue = _animationManager.GetProgress(i); + var animationSource = _animationManager.GetSource(i); + + using (Brush rippleBrush = new SolidBrush(Color.FromArgb((int)(101 - (animationValue * 100)), Color.Black))) + { + var rippleSize = (int)(animationValue * Width * 2); + g.FillEllipse(rippleBrush, new Rectangle(animationSource.X - rippleSize / 2, animationSource.Y - rippleSize / 2, rippleSize, rippleSize)); + } + } + g.SmoothingMode = SmoothingMode.None; + } + + //Icon + var iconRect = new Rectangle(8, 6, 24, 24); + + if (string.IsNullOrEmpty(Text)) + // Center Icon + iconRect.X += 2; + + if (Icon != null) + g.DrawImage(Icon, iconRect); + + //Text + var textRect = ClientRectangle; + + if (Icon != null) + { + // + // Resize and move Text container + // + + // First 8: left padding + // 24: icon width + // Second 4: space between Icon and Text + // Third 8: right padding + textRect.Width -= 8 + 24 + 4 + 8; + + // First 8: left padding + // 24: icon width + // Second 4: space between Icon and Text + textRect.X += 8 + 24 + 4; + } + + g.DrawString( + Text.ToUpper(), + SkinManager.ROBOTO_MEDIUM_10, + Enabled ? (Primary ? SkinManager.ColorScheme.PrimaryBrush : SkinManager.GetPrimaryTextBrush()) : SkinManager.GetFlatButtonDisabledTextBrush(), + textRect, + new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center } + ); + } + + private Size GetPreferredSize() + { + return GetPreferredSize(new Size(0, 0)); + } + + public override Size GetPreferredSize(Size proposedSize) + { + // Provides extra space for proper padding for content + var extra = 16; + + if (Icon != null) + // 24 is for icon size + // 4 is for the space between icon & text + extra += 24 + 4; + + return new Size((int)Math.Ceiling(_textSize.Width) + extra, 36); + } + + protected override void OnCreateControl() + { + base.OnCreateControl(); + if (DesignMode) return; + + MouseState = MouseState.OUT; + MouseEnter += (sender, args) => + { + MouseState = MouseState.HOVER; + _hoverAnimationManager.StartNewAnimation(AnimationDirection.In); + Invalidate(); + }; + MouseLeave += (sender, args) => + { + MouseState = MouseState.OUT; + _hoverAnimationManager.StartNewAnimation(AnimationDirection.Out); + Invalidate(); + }; + MouseDown += (sender, args) => + { + if (args.Button == MouseButtons.Left) + { + MouseState = MouseState.DOWN; + + _animationManager.StartNewAnimation(AnimationDirection.In, args.Location); + Invalidate(); + } + }; + MouseUp += (sender, args) => + { + MouseState = MouseState.HOVER; + + Invalidate(); + }; + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialForm.cs b/framework/MaterialSkin.Core/Controls/MaterialForm.cs new file mode 100644 index 0000000..7026667 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialForm.cs @@ -0,0 +1,619 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Text; +using System.Linq; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace MaterialSkin.Controls +{ + public class MaterialForm : Form, IMaterialControl + { + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + public new FormBorderStyle FormBorderStyle { get { return base.FormBorderStyle; } set { base.FormBorderStyle = value; } } + public bool Sizable { get; set; } + + [DllImport("user32.dll")] + public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam); + + [DllImport("user32.dll")] + public static extern bool ReleaseCapture(); + + [DllImport("user32.dll")] + public static extern int TrackPopupMenuEx(IntPtr hmenu, uint fuFlags, int x, int y, IntPtr hwnd, IntPtr lptpm); + + [DllImport("user32.dll")] + public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert); + + [DllImport("user32.dll")] + public static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags); + + [DllImport("User32.dll", CharSet = CharSet.Auto)] + public static extern bool GetMonitorInfo(HandleRef hmonitor, [In, Out] MONITORINFOEX info); + + public const int WM_NCLBUTTONDOWN = 0xA1; + public const int HT_CAPTION = 0x2; + public const int WM_MOUSEMOVE = 0x0200; + public const int WM_LBUTTONDOWN = 0x0201; + public const int WM_LBUTTONUP = 0x0202; + public const int WM_LBUTTONDBLCLK = 0x0203; + public const int WM_RBUTTONDOWN = 0x0204; + private const int HTBOTTOMLEFT = 16; + private const int HTBOTTOMRIGHT = 17; + private const int HTLEFT = 10; + private const int HTRIGHT = 11; + private const int HTBOTTOM = 15; + private const int HTTOP = 12; + private const int HTTOPLEFT = 13; + private const int HTTOPRIGHT = 14; + private const int BORDER_WIDTH = 7; + private ResizeDirection _resizeDir; + private ButtonState _buttonState = ButtonState.None; + + private const int WMSZ_TOP = 3; + private const int WMSZ_TOPLEFT = 4; + private const int WMSZ_TOPRIGHT = 5; + private const int WMSZ_LEFT = 1; + private const int WMSZ_RIGHT = 2; + private const int WMSZ_BOTTOM = 6; + private const int WMSZ_BOTTOMLEFT = 7; + private const int WMSZ_BOTTOMRIGHT = 8; + + private readonly Dictionary _resizingLocationsToCmd = new Dictionary + { + {HTTOP, WMSZ_TOP}, + {HTTOPLEFT, WMSZ_TOPLEFT}, + {HTTOPRIGHT, WMSZ_TOPRIGHT}, + {HTLEFT, WMSZ_LEFT}, + {HTRIGHT, WMSZ_RIGHT}, + {HTBOTTOM, WMSZ_BOTTOM}, + {HTBOTTOMLEFT, WMSZ_BOTTOMLEFT}, + {HTBOTTOMRIGHT, WMSZ_BOTTOMRIGHT} + }; + + private const int STATUS_BAR_BUTTON_WIDTH = STATUS_BAR_HEIGHT; + private const int STATUS_BAR_HEIGHT = 24; + private const int ACTION_BAR_HEIGHT = 40; + + private const uint TPM_LEFTALIGN = 0x0000; + private const uint TPM_RETURNCMD = 0x0100; + + private const int WM_SYSCOMMAND = 0x0112; + private const int WS_MINIMIZEBOX = 0x20000; + private const int WS_SYSMENU = 0x00080000; + + private const int MONITOR_DEFAULTTONEAREST = 2; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 4)] + public class MONITORINFOEX + { + public int cbSize = Marshal.SizeOf(typeof(MONITORINFOEX)); + public RECT rcMonitor = new RECT(); + public RECT rcWork = new RECT(); + public int dwFlags = 0; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + public char[] szDevice = new char[32]; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + + public int Width() + { + return right - left; + } + + public int Height() + { + return bottom - top; + } + } + + private enum ResizeDirection + { + BottomLeft, + Left, + Right, + BottomRight, + Bottom, + None + } + + private enum ButtonState + { + XOver, + MaxOver, + MinOver, + XDown, + MaxDown, + MinDown, + None + } + + private readonly Cursor[] _resizeCursors = { Cursors.SizeNESW, Cursors.SizeWE, Cursors.SizeNWSE, Cursors.SizeWE, Cursors.SizeNS }; + + private Rectangle _minButtonBounds; + private Rectangle _maxButtonBounds; + private Rectangle _xButtonBounds; + private Rectangle _actionBarBounds; + private Rectangle _statusBarBounds; + + private bool _maximized; + private Size _previousSize; + private Point _previousLocation; + private bool _headerMouseDown; + private MouseMessageFilter _mouseMessageFilter; + + + public MaterialForm() + { + FormBorderStyle = FormBorderStyle.None; + AutoScaleMode = AutoScaleMode.None; + DoubleBuffered = true; + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); + + // This enables the form to trigger the MouseMove event even when mouse is over another control + //_mouseMessageFilter = new MouseMessageFilter(); + //Application.AddMessageFilter(_mouseMessageFilter); + //MouseMessageFilter.MouseMove += OnGlobalMouseMove; + } + + public void RemoveMessageFilter() + { + Application.RemoveMessageFilter(_mouseMessageFilter); + MouseMessageFilter.MouseMove -= OnGlobalMouseMove; + } + + protected override void WndProc(ref Message m) + { + base.WndProc(ref m); + if (DesignMode || IsDisposed) return; + + if (m.Msg == WM_LBUTTONDBLCLK) + { + MaximizeWindow(!_maximized); + } + else if (m.Msg == WM_MOUSEMOVE && _maximized && + (_statusBarBounds.Contains(PointToClient(Cursor.Position)) || _actionBarBounds.Contains(PointToClient(Cursor.Position))) && + !(_minButtonBounds.Contains(PointToClient(Cursor.Position)) || _maxButtonBounds.Contains(PointToClient(Cursor.Position)) || _xButtonBounds.Contains(PointToClient(Cursor.Position)))) + { + if (_headerMouseDown) + { + _maximized = false; + _headerMouseDown = false; + + var mousePoint = PointToClient(Cursor.Position); + if (mousePoint.X < Width / 2) + Location = mousePoint.X < _previousSize.Width / 2 ? + new Point(Cursor.Position.X - mousePoint.X, Cursor.Position.Y - mousePoint.Y) : + new Point(Cursor.Position.X - _previousSize.Width / 2, Cursor.Position.Y - mousePoint.Y); + else + Location = Width - mousePoint.X < _previousSize.Width / 2 ? + new Point(Cursor.Position.X - _previousSize.Width + Width - mousePoint.X, Cursor.Position.Y - mousePoint.Y) : + new Point(Cursor.Position.X - _previousSize.Width / 2, Cursor.Position.Y - mousePoint.Y); + + Size = _previousSize; + ReleaseCapture(); + SendMessage(Handle, WM_NCLBUTTONDOWN, HT_CAPTION, 0); + } + } + else if (m.Msg == WM_LBUTTONDOWN && + (_statusBarBounds.Contains(PointToClient(Cursor.Position)) || _actionBarBounds.Contains(PointToClient(Cursor.Position))) && + !(_minButtonBounds.Contains(PointToClient(Cursor.Position)) || _maxButtonBounds.Contains(PointToClient(Cursor.Position)) || _xButtonBounds.Contains(PointToClient(Cursor.Position)))) + { + if (!_maximized) + { + ReleaseCapture(); + SendMessage(Handle, WM_NCLBUTTONDOWN, HT_CAPTION, 0); + } + else + { + _headerMouseDown = true; + } + } + else if (m.Msg == WM_RBUTTONDOWN) + { + Point cursorPos = PointToClient(Cursor.Position); + + if (_statusBarBounds.Contains(cursorPos) && !_minButtonBounds.Contains(cursorPos) && + !_maxButtonBounds.Contains(cursorPos) && !_xButtonBounds.Contains(cursorPos)) + { + // Show default system menu when right clicking titlebar + var id = TrackPopupMenuEx(GetSystemMenu(Handle, false), TPM_LEFTALIGN | TPM_RETURNCMD, Cursor.Position.X, Cursor.Position.Y, Handle, IntPtr.Zero); + + // Pass the command as a WM_SYSCOMMAND message + SendMessage(Handle, WM_SYSCOMMAND, id, 0); + } + } + else if (m.Msg == WM_NCLBUTTONDOWN) + { + // This re-enables resizing by letting the application know when the + // user is trying to resize a side. This is disabled by default when using WS_SYSMENU. + if (!Sizable) return; + + byte bFlag = 0; + + // Get which side to resize from + if (_resizingLocationsToCmd.ContainsKey((int)m.WParam)) + bFlag = (byte)_resizingLocationsToCmd[(int)m.WParam]; + + if (bFlag != 0) + SendMessage(Handle, WM_SYSCOMMAND, 0xF000 | bFlag, (int)m.LParam); + } + else if (m.Msg == WM_LBUTTONUP) + { + _headerMouseDown = false; + } + } + + public bool DropShadow { get; set; } = true; + protected override CreateParams CreateParams + { + get + { + const int CS_DROPSHADOW = 0x20000; + var par = base.CreateParams; + // WS_SYSMENU: Trigger the creation of the system menu + // WS_MINIMIZEBOX: Allow minimizing from taskbar + par.Style = par.Style | WS_MINIMIZEBOX | WS_SYSMENU; // Turn on the WS_MINIMIZEBOX style flag + if(DropShadow) + par.ClassStyle |= CS_DROPSHADOW; + return par; + } + } + + protected override void OnMouseDown(MouseEventArgs e) + { + if (DesignMode) return; + UpdateButtons(e); + + if (e.Button == MouseButtons.Left && !_maximized) + ResizeForm(_resizeDir); + base.OnMouseDown(e); + } + + protected override void OnMouseLeave(EventArgs e) + { + base.OnMouseLeave(e); + if (DesignMode) return; + _buttonState = ButtonState.None; + Invalidate(); + } + + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + + if (DesignMode) return; + + if (Sizable) + { + //True if the mouse is hovering over a child control + var isChildUnderMouse = GetChildAtPoint(e.Location) != null; + + if (e.Location.X < BORDER_WIDTH && e.Location.Y > Height - BORDER_WIDTH && !isChildUnderMouse && !_maximized) + { + _resizeDir = ResizeDirection.BottomLeft; + Cursor = Cursors.SizeNESW; + } + else if (e.Location.X < BORDER_WIDTH && !isChildUnderMouse && !_maximized) + { + _resizeDir = ResizeDirection.Left; + Cursor = Cursors.SizeWE; + } + else if (e.Location.X > Width - BORDER_WIDTH && e.Location.Y > Height - BORDER_WIDTH && !isChildUnderMouse && !_maximized) + { + _resizeDir = ResizeDirection.BottomRight; + Cursor = Cursors.SizeNWSE; + } + else if (e.Location.X > Width - BORDER_WIDTH && !isChildUnderMouse && !_maximized) + { + _resizeDir = ResizeDirection.Right; + Cursor = Cursors.SizeWE; + } + else if (e.Location.Y > Height - BORDER_WIDTH && !isChildUnderMouse && !_maximized) + { + _resizeDir = ResizeDirection.Bottom; + Cursor = Cursors.SizeNS; + } + else + { + _resizeDir = ResizeDirection.None; + + //Only reset the cursor when needed, this prevents it from flickering when a child control changes the cursor to its own needs + if (_resizeCursors.Contains(Cursor)) + { + Cursor = Cursors.Default; + } + } + } + + UpdateButtons(e); + } + + + + protected void OnGlobalMouseMove(object sender, MouseEventArgs e) + { + + if (IsDisposed) return; + // Convert to client position and pass to Form.MouseMove + var clientCursorPos = PointToClient(e.Location); + var newE = new MouseEventArgs(MouseButtons.None, 0, clientCursorPos.X, clientCursorPos.Y, 0); + OnMouseMove(newE); + + } + + private void UpdateButtons(MouseEventArgs e, bool up = false) + { + if (DesignMode) return; + var oldState = _buttonState; + bool showMin = MinimizeBox && ControlBox; + bool showMax = MaximizeBox && ControlBox; + + if (e.Button == MouseButtons.Left && !up) + { + if (showMin && !showMax && _maxButtonBounds.Contains(e.Location)) + _buttonState = ButtonState.MinDown; + else if (showMin && showMax && _minButtonBounds.Contains(e.Location)) + _buttonState = ButtonState.MinDown; + else if (showMax && _maxButtonBounds.Contains(e.Location)) + _buttonState = ButtonState.MaxDown; + else if (ControlBox && _xButtonBounds.Contains(e.Location)) + _buttonState = ButtonState.XDown; + else + _buttonState = ButtonState.None; + } + else + { + if (showMin && !showMax && _maxButtonBounds.Contains(e.Location)) + { + _buttonState = ButtonState.MinOver; + + if (oldState == ButtonState.MinDown && up) + WindowState = FormWindowState.Minimized; + } + else if (showMin && showMax && _minButtonBounds.Contains(e.Location)) + { + _buttonState = ButtonState.MinOver; + + if (oldState == ButtonState.MinDown && up) + WindowState = FormWindowState.Minimized; + } + else if (MaximizeBox && ControlBox && _maxButtonBounds.Contains(e.Location)) + { + _buttonState = ButtonState.MaxOver; + + if (oldState == ButtonState.MaxDown && up) + MaximizeWindow(!_maximized); + + } + else if (ControlBox && _xButtonBounds.Contains(e.Location)) + { + _buttonState = ButtonState.XOver; + + if (oldState == ButtonState.XDown && up) + Close(); + } + else _buttonState = ButtonState.None; + } + + if (oldState != _buttonState) Invalidate(); + } + + public void MaximizeWindow(bool maximize) + { + if (!MaximizeBox || !ControlBox) return; + + _maximized = maximize; + + if (maximize) + { + var monitorHandle = MonitorFromWindow(Handle, MONITOR_DEFAULTTONEAREST); + var monitorInfo = new MONITORINFOEX(); + GetMonitorInfo(new HandleRef(null, monitorHandle), monitorInfo); + _previousSize = Size; + _previousLocation = Location; + + //var area=Screen.FromHandle(this.Handle).WorkingArea; + + Size = new Size(monitorInfo.rcWork.Width(), monitorInfo.rcWork.Height()); + Location = new Point(monitorInfo.rcWork.left, monitorInfo.rcWork.top); + //Size = new Size(area.Width, area.Height); + //Location = new Point(area.Left, area.Top); + } + else + { + Size = _previousSize; + Location = _previousLocation; + } + + } + + protected override void OnMouseUp(MouseEventArgs e) + { + if (DesignMode) return; + UpdateButtons(e, true); + + base.OnMouseUp(e); + ReleaseCapture(); + } + + private void ResizeForm(ResizeDirection direction) + { + if (DesignMode) return; + var dir = -1; + switch (direction) + { + case ResizeDirection.BottomLeft: + dir = HTBOTTOMLEFT; + break; + case ResizeDirection.Left: + dir = HTLEFT; + break; + case ResizeDirection.Right: + dir = HTRIGHT; + break; + case ResizeDirection.BottomRight: + dir = HTBOTTOMRIGHT; + break; + case ResizeDirection.Bottom: + dir = HTBOTTOM; + break; + } + + ReleaseCapture(); + if (dir != -1) + { + SendMessage(Handle, WM_NCLBUTTONDOWN, dir, 0); + } + } + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + + _minButtonBounds = new Rectangle((Width - SkinManager.FORM_PADDING / 2) - 3 * STATUS_BAR_BUTTON_WIDTH, 0, STATUS_BAR_BUTTON_WIDTH, STATUS_BAR_HEIGHT); + _maxButtonBounds = new Rectangle((Width - SkinManager.FORM_PADDING / 2) - 2 * STATUS_BAR_BUTTON_WIDTH, 0, STATUS_BAR_BUTTON_WIDTH, STATUS_BAR_HEIGHT); + _xButtonBounds = new Rectangle((Width - SkinManager.FORM_PADDING / 2) - STATUS_BAR_BUTTON_WIDTH, 0, STATUS_BAR_BUTTON_WIDTH, STATUS_BAR_HEIGHT); + _statusBarBounds = new Rectangle(0, 0, Width, STATUS_BAR_HEIGHT); + _actionBarBounds = new Rectangle(0, STATUS_BAR_HEIGHT, Width, ACTION_BAR_HEIGHT); + } + + public bool ShowSubHeader { get; set; } + protected override void OnPaint(PaintEventArgs e) + { + var g = e.Graphics; + g.TextRenderingHint = TextRenderingHint.AntiAlias; + + g.Clear(SkinManager.GetApplicationBackgroundColor()); + g.FillRectangle(SkinManager.ColorScheme.DarkPrimaryBrush, _statusBarBounds); + if(ShowSubHeader) g.FillRectangle(SkinManager.ColorScheme.PrimaryBrush, _actionBarBounds); + + //Draw border + using (var borderPen = new Pen(SkinManager.GetDividersColor(), 1)) + { + g.DrawLine(borderPen, new Point(0, _statusBarBounds.Bottom), new Point(0, Height - 2)); + g.DrawLine(borderPen, new Point(Width - 1, _statusBarBounds.Bottom), new Point(Width - 1, Height - 2)); + g.DrawLine(borderPen, new Point(0, Height - 1), new Point(Width - 1, Height - 1)); + } + + // Determine whether or not we even should be drawing the buttons. + bool showMin = MinimizeBox && ControlBox; + bool showMax = MaximizeBox && ControlBox; + var hoverBrush = SkinManager.GetFlatButtonHoverBackgroundBrush(); + var downBrush = SkinManager.GetFlatButtonPressedBackgroundBrush(); + + // When MaximizeButton == false, the minimize button will be painted in its place + if (_buttonState == ButtonState.MinOver && showMin) + g.FillRectangle(hoverBrush, showMax ? _minButtonBounds : _maxButtonBounds); + + if (_buttonState == ButtonState.MinDown && showMin) + g.FillRectangle(downBrush, showMax ? _minButtonBounds : _maxButtonBounds); + + if (_buttonState == ButtonState.MaxOver && showMax) + g.FillRectangle(hoverBrush, _maxButtonBounds); + + if (_buttonState == ButtonState.MaxDown && showMax) + g.FillRectangle(downBrush, _maxButtonBounds); + + if (_buttonState == ButtonState.XOver && ControlBox) + g.FillRectangle(hoverBrush, _xButtonBounds); + + if (_buttonState == ButtonState.XDown && ControlBox) + g.FillRectangle(downBrush, _xButtonBounds); + + using (var formButtonsPen = new Pen(SkinManager.ACTION_BAR_TEXT_SECONDARY, 2)) + { + // Minimize button. + if (showMin) + { + int x = showMax ? _minButtonBounds.X : _maxButtonBounds.X; + int y = showMax ? _minButtonBounds.Y : _maxButtonBounds.Y; + + g.DrawLine( + formButtonsPen, + x + (int)(_minButtonBounds.Width * 0.33), + y + (int)(_minButtonBounds.Height * 0.66), + x + (int)(_minButtonBounds.Width * 0.66), + y + (int)(_minButtonBounds.Height * 0.66) + ); + } + + // Maximize button + if (showMax) + { + g.DrawRectangle( + formButtonsPen, + _maxButtonBounds.X + (int)(_maxButtonBounds.Width * 0.33), + _maxButtonBounds.Y + (int)(_maxButtonBounds.Height * 0.36), + (int)(_maxButtonBounds.Width * 0.39), + (int)(_maxButtonBounds.Height * 0.31) + ); + } + + // Close button + if (ControlBox) + { + g.DrawLine( + formButtonsPen, + _xButtonBounds.X + (int)(_xButtonBounds.Width * 0.33), + _xButtonBounds.Y + (int)(_xButtonBounds.Height * 0.33), + _xButtonBounds.X + (int)(_xButtonBounds.Width * 0.66), + _xButtonBounds.Y + (int)(_xButtonBounds.Height * 0.66) + ); + + g.DrawLine( + formButtonsPen, + _xButtonBounds.X + (int)(_xButtonBounds.Width * 0.66), + _xButtonBounds.Y + (int)(_xButtonBounds.Height * 0.33), + _xButtonBounds.X + (int)(_xButtonBounds.Width * 0.33), + _xButtonBounds.Y + (int)(_xButtonBounds.Height * 0.66)); + } + } + + //Form title + g.DrawString(Text, SkinManager.ROBOTO_MEDIUM_12, SkinManager.ColorScheme.TextBrush, new Rectangle(SkinManager.FORM_PADDING, STATUS_BAR_HEIGHT, Width-20, ACTION_BAR_HEIGHT), new StringFormat { LineAlignment = StringAlignment.Center,Alignment = StringAlignment.Near }); + //Primary title + g.DrawString(PrimaryText, new Font("Roboto",20,FontStyle.Bold),SkinManager.ColorScheme.TextBrush, new Rectangle(SkinManager.FORM_PADDING, STATUS_BAR_HEIGHT, Width, ACTION_BAR_HEIGHT), new StringFormat { LineAlignment = StringAlignment.Center, Alignment = StringAlignment.Center }); + g.DrawString(HeaderText, new Font("Roboto", 12, FontStyle.Bold), SkinManager.ColorScheme.TextBrush, new Rectangle(SkinManager.FORM_PADDING, 0, Width, STATUS_BAR_HEIGHT), new StringFormat { LineAlignment = StringAlignment.Center, Alignment = StringAlignment.Near }); + } + public string PrimaryText { get; set; } + public string HeaderText { get; set; } + public Font TitleFont { get; set; } + } + + public class MouseMessageFilter : IMessageFilter + { + private const int WM_MOUSEMOVE = 0x0200; + + public static event MouseEventHandler MouseMove; + + public bool PreFilterMessage(ref Message m) + { + + if (m.Msg == WM_MOUSEMOVE) + { + if (MouseMove != null) + { + int x = Control.MousePosition.X, y = Control.MousePosition.Y; + + MouseMove(null, new MouseEventArgs(MouseButtons.None, 0, x, y, 0)); + } + } + return false; + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialForm.resx b/framework/MaterialSkin.Core/Controls/MaterialForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Controls/MaterialIcon.cs b/framework/MaterialSkin.Core/Controls/MaterialIcon.cs new file mode 100644 index 0000000..c340e80 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialIcon.cs @@ -0,0 +1,171 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Threading.Tasks; +using System.Windows.Forms; +using MaterialSkin.Animations; + + +namespace MaterialSkin.Controls +{ + public class MaterialIcon:Control + { + private Image _image; + private Bitmap _bmp; + private AnimationManager _animationManager; + private Point _lastLocation; + private Bitmap _backBuffer = null; + private bool _primary; + + public MaterialIcon() + { + Cursor = Cursors.Hand; + Clickable = true; + _animationManager = new AnimationManager(false) + { + Increment = 0.02, + AnimationType = AnimationType.EaseOut + }; + + _animationManager.OnAnimationProgress += sender => Invalidate(); + _animationManager.OnAnimationFinished += AnimationManagerOnOnAnimationFinished; + } + + private void AnimationManagerOnOnAnimationFinished(object sender) + { + + } + + + + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + + public Image Image + { + get { return _image; } + set + { + _image = value; + RebuildIcon(); + } + } + + public bool Primary + { + get { return _primary; } + set + { + _primary = value; + if (_image == null) return; + RebuildIcon(); + } + } + + protected override void OnEnabledChanged(EventArgs e) + { + base.OnEnabledChanged(e); + RebuildIcon(); + } + + protected override void OnResize(EventArgs e) + { + _backBuffer=new Bitmap(Width,Height); + if (_image == null) return; + RebuildIcon(); + base.OnResize(e); + } + + private void RebuildIcon() + { + + Task.Run(() => + { + lock (_lockObject) + { + var tmp = new Bitmap(_image); + ReplaceColor(tmp, Color.Black, + Enabled + ? (_primary + ? SkinManager.GetApplicationBackgroundColor() + : SkinManager.ColorScheme.PrimaryColor) + : Color.FromArgb(0xAA, 0xAA, 0xAA)); + _bmp = tmp; + Invalidate(); + } + }); + + } + + protected override void OnPaintBackground(PaintEventArgs pevent) + { + //base.OnPaintBackground(pevent); + } + protected override void OnMouseDown(MouseEventArgs mevent) + { + base.OnMouseDown(mevent); + if (!Enabled) return; + _lastLocation = mevent.Location; + _animationManager.StartNewAnimation(AnimationDirection.In, _lastLocation); + } + + public bool Clickable { get; set; } + protected override void OnClick(EventArgs e) + { + if (!Clickable) return; + + base.OnClick(e); + } + protected override void OnPaint(PaintEventArgs pe) + { + if (Image == null) return; + if(_bmp == null)return; + if(_backBuffer==null)return; + using (Graphics g=Graphics.FromImage(_backBuffer)) + { + g.InterpolationMode = InterpolationMode.Bicubic; + g.SmoothingMode = SmoothingMode.HighQuality; + g.Clear(_primary? SkinManager.ColorScheme.PrimaryColor : SkinManager.GetApplicationBackgroundColor()); + if (_animationManager.IsAnimating()) + { + for (int i = 0; i < _animationManager.GetAnimationCount(); i++) + { + var animationValue = _animationManager.GetProgress(i); + var animationSource = _animationManager.GetSource(i); + var rippleBrush = new SolidBrush(Color.FromArgb((int)(51 - (animationValue * 50)), _primary ? SkinManager.GetApplicationBackgroundColor() : SkinManager.ColorScheme.PrimaryColor)); + var rippleSize = (int)(animationValue * Width * 2); + g.FillEllipse(rippleBrush, new Rectangle(animationSource.X - rippleSize / 2, animationSource.Y - rippleSize / 2, rippleSize, rippleSize)); + } + } + g.DrawImage(_bmp,1,1,Width-2,Height-2); + } + + + pe.Graphics.DrawImage(_backBuffer, 0,0); + + + } + object _lockObject = new object(); + private void ReplaceColor(Bitmap bmp, Color oldColor, Color newColor) + { + + { + var lockedBitmap = bmp; + + for (int y = 0; y < lockedBitmap.Height; y++) + { + for (int x = 0; x < lockedBitmap.Width; x++) + { + if (lockedBitmap.GetPixel(x, y).A > 0) + { + lockedBitmap.SetPixel(x, y, newColor); + } + } + } + } + } + + } +} \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Controls/MaterialIndicator.Designer.cs b/framework/MaterialSkin.Core/Controls/MaterialIndicator.Designer.cs new file mode 100644 index 0000000..3a206b4 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialIndicator.Designer.cs @@ -0,0 +1,44 @@ +namespace MaterialSkin.Controls +{ + partial class MaterialIndicator + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.SuspendLayout(); + // + // MaterialIndicator + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Name = "MaterialIndicator"; + this.ResumeLayout(false); + + } + + #endregion + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialIndicator.cs b/framework/MaterialSkin.Core/Controls/MaterialIndicator.cs new file mode 100644 index 0000000..21e03c8 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialIndicator.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace MaterialSkin.Controls +{ + public partial class MaterialIndicator : UserControl + { + private bool _checked; + + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + + public Color CheckedColor { get; set; } + public Color UncheckedColor { get; set; } + public Color BorderColor { get; set; } + public MaterialIndicator() + { + InitializeComponent(); + + } + + + public bool Checked + { + get { return _checked; } + set + { + _checked = value; + Invalidate(); + } + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + base.OnPaintBackground(e); + } + + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.FillEllipse(new SolidBrush(Checked?CheckedColor:UncheckedColor),1,1,Width-2,Height-2); + e.Graphics.DrawEllipse(new Pen(BorderColor),1,1,Width-2,Height-2 ); + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialIndicator.resx b/framework/MaterialSkin.Core/Controls/MaterialIndicator.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialIndicator.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Controls/MaterialInputBox.Designer.cs b/framework/MaterialSkin.Core/Controls/MaterialInputBox.Designer.cs new file mode 100644 index 0000000..f79e450 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialInputBox.Designer.cs @@ -0,0 +1,115 @@ + +namespace MaterialSkin.Core.Controls +{ + partial class MaterialInputBox + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.materialRaisedButton1 = new MaterialSkin.Controls.MaterialRaisedButton(); + this.materialRaisedButton2 = new MaterialSkin.Controls.MaterialRaisedButton(); + this.textBox1 = new System.Windows.Forms.TextBox(); + this.label1 = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // materialRaisedButton1 + // + this.materialRaisedButton1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.materialRaisedButton1.Cursor = System.Windows.Forms.Cursors.Hand; + this.materialRaisedButton1.Depth = 0; + this.materialRaisedButton1.DrawBorder = false; + this.materialRaisedButton1.Icon = null; + this.materialRaisedButton1.Location = new System.Drawing.Point(440, 88); + this.materialRaisedButton1.MouseState = MaterialSkin.MouseState.HOVER; + this.materialRaisedButton1.Name = "materialRaisedButton1"; + this.materialRaisedButton1.Primary = true; + this.materialRaisedButton1.Size = new System.Drawing.Size(128, 48); + this.materialRaisedButton1.TabIndex = 0; + this.materialRaisedButton1.Text = "OK"; + this.materialRaisedButton1.UseVisualStyleBackColor = true; + this.materialRaisedButton1.Click += new System.EventHandler(this.materialRaisedButton1_Click); + // + // materialRaisedButton2 + // + this.materialRaisedButton2.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.materialRaisedButton2.Cursor = System.Windows.Forms.Cursors.Hand; + this.materialRaisedButton2.Depth = 0; + this.materialRaisedButton2.DrawBorder = true; + this.materialRaisedButton2.Icon = null; + this.materialRaisedButton2.Location = new System.Drawing.Point(304, 88); + this.materialRaisedButton2.MouseState = MaterialSkin.MouseState.HOVER; + this.materialRaisedButton2.Name = "materialRaisedButton2"; + this.materialRaisedButton2.Primary = false; + this.materialRaisedButton2.Size = new System.Drawing.Size(128, 48); + this.materialRaisedButton2.TabIndex = 1; + this.materialRaisedButton2.Text = "Cancel"; + this.materialRaisedButton2.UseVisualStyleBackColor = true; + this.materialRaisedButton2.Click += new System.EventHandler(this.materialRaisedButton2_Click); + // + // textBox1 + // + this.textBox1.Location = new System.Drawing.Point(16, 56); + this.textBox1.Name = "textBox1"; + this.textBox1.Size = new System.Drawing.Size(552, 23); + this.textBox1.TabIndex = 0; + this.textBox1.KeyDown += new System.Windows.Forms.KeyEventHandler(this.textBox1_KeyDown); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.BackColor = System.Drawing.Color.White; + this.label1.Location = new System.Drawing.Point(16, 32); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(38, 15); + this.label1.TabIndex = 3; + this.label1.Text = "label1"; + // + // MaterialInputBox + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(579, 149); + this.Controls.Add(this.label1); + this.Controls.Add(this.textBox1); + this.Controls.Add(this.materialRaisedButton2); + this.Controls.Add(this.materialRaisedButton1); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "MaterialInputBox"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private MaterialSkin.Controls.MaterialRaisedButton materialRaisedButton1; + private MaterialSkin.Controls.MaterialRaisedButton materialRaisedButton2; + private System.Windows.Forms.TextBox textBox1; + private System.Windows.Forms.Label label1; + } +} \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Controls/MaterialInputBox.cs b/framework/MaterialSkin.Core/Controls/MaterialInputBox.cs new file mode 100644 index 0000000..ca73f74 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialInputBox.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Text; +using System.Windows.Forms; +using MaterialSkin.Controls; + +namespace MaterialSkin.Core.Controls +{ + public partial class MaterialInputBox : MaterialForm + { + private MaterialInputBox() + { + InitializeComponent(); + } + + + public static DialogResult Prompt(string title, string defaultValue, out string result, bool masked = false) + { + MaterialInputBox box = new MaterialInputBox {label1 = {Text = title}, textBox1 = {Text = defaultValue}}; + if (masked) box.textBox1.PasswordChar = '*'; + var res= box.ShowDialog(); + result = box.textBox1.Text; + return res; + } + + private void textBox1_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Return) + { + DialogResult = DialogResult.OK; + } + } + + private void materialRaisedButton2_Click(object sender, EventArgs e) + { + DialogResult = DialogResult.Cancel; + } + + private void materialRaisedButton1_Click(object sender, EventArgs e) + { + DialogResult = DialogResult.OK; + } + } + +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialInputBox.resx b/framework/MaterialSkin.Core/Controls/MaterialInputBox.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialInputBox.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Controls/MaterialLabel.cs b/framework/MaterialSkin.Core/Controls/MaterialLabel.cs new file mode 100644 index 0000000..bf2efdc --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialLabel.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using System.Windows.Forms; + +namespace MaterialSkin.Controls +{ + public class MaterialLabel : Label, IMaterialControl + { + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + protected override void OnCreateControl() + { + base.OnCreateControl(); + + ForeColor = SkinManager.GetPrimaryTextColor(); + Font = SkinManager.ROBOTO_REGULAR_11; + + BackColorChanged += (sender, args) => ForeColor = SkinManager.GetPrimaryTextColor(); + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialListView.cs b/framework/MaterialSkin.Core/Controls/MaterialListView.cs new file mode 100644 index 0000000..c9e74f4 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialListView.cs @@ -0,0 +1,163 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace MaterialSkin.Controls +{ + public class MaterialListView : ListView, IMaterialControl + { + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + [Browsable(false)] + public Point MouseLocation { get; set; } + [Browsable(false)] + private ListViewItem HoveredItem { get; set; } + + public MaterialListView() + { + GridLines = false; + FullRowSelect = true; + HeaderStyle = ColumnHeaderStyle.Nonclickable; + View = View.Details; + OwnerDraw = true; + ResizeRedraw = true; + BorderStyle = BorderStyle.None; + SetStyle(ControlStyles.DoubleBuffer | ControlStyles.OptimizedDoubleBuffer, true); + + //Fix for hovers, by default it doesn't redraw + //TODO: should only redraw when the hovered line changed, this to reduce unnecessary redraws + MouseLocation = new Point(-1, -1); + MouseState = MouseState.OUT; + MouseEnter += delegate + { + MouseState = MouseState.HOVER; + }; + MouseLeave += delegate + { + MouseState = MouseState.OUT; + MouseLocation = new Point(-1, -1); + HoveredItem = null; + Invalidate(); + }; + MouseDown += delegate { MouseState = MouseState.DOWN; }; + MouseUp += delegate { MouseState = MouseState.HOVER; }; + MouseMove += delegate (object sender, MouseEventArgs args) + { + MouseLocation = args.Location; + var currentHoveredItem = this.GetItemAt(MouseLocation.X, MouseLocation.Y); + if (HoveredItem != currentHoveredItem) + { + HoveredItem = currentHoveredItem; + Invalidate(); + } + }; + } + + protected override void OnDrawColumnHeader(DrawListViewColumnHeaderEventArgs e) + { + e.Graphics.FillRectangle(new SolidBrush(SkinManager.GetApplicationBackgroundColor()), new Rectangle(e.Bounds.X, e.Bounds.Y, Width, e.Bounds.Height)); + e.Graphics.DrawString(e.Header.Text, + SkinManager.ROBOTO_MEDIUM_10, + SkinManager.GetSecondaryTextBrush(), + new Rectangle(e.Bounds.X + ITEM_PADDING, e.Bounds.Y + ITEM_PADDING, e.Bounds.Width - ITEM_PADDING * 2, e.Bounds.Height - ITEM_PADDING * 2), + getStringFormat()); + } + + private const int ITEM_PADDING = 12; + protected override void OnDrawItem(DrawListViewItemEventArgs e) + { + //We draw the current line of items (= item with subitems) on a temp bitmap, then draw the bitmap at once. This is to reduce flickering. + var b = new Bitmap(e.Item.Bounds.Width, e.Item.Bounds.Height); + var g = Graphics.FromImage(b); + + //always draw default background + g.FillRectangle(new SolidBrush(SkinManager.GetApplicationBackgroundColor()), new Rectangle(new Point(e.Bounds.X, 0), e.Bounds.Size)); + + if (e.State.HasFlag(ListViewItemStates.Selected)) + { + //selected background + g.FillRectangle(SkinManager.GetFlatButtonPressedBackgroundBrush(), new Rectangle(new Point(e.Bounds.X, 0), e.Bounds.Size)); + } + else if (e.Bounds.Contains(MouseLocation) && MouseState == MouseState.HOVER) + { + //hover background + g.FillRectangle(SkinManager.GetFlatButtonHoverBackgroundBrush(), new Rectangle(new Point(e.Bounds.X, 0), e.Bounds.Size)); + } + + + //Draw separator + g.DrawLine(new Pen(SkinManager.GetDividersColor()), e.Bounds.Left, 0, e.Bounds.Right, 0); + + foreach (ListViewItem.ListViewSubItem subItem in e.Item.SubItems) + { + //Draw text + g.DrawString(subItem.Text, SkinManager.ROBOTO_MEDIUM_10, SkinManager.GetPrimaryTextBrush(), + new Rectangle(subItem.Bounds.X + ITEM_PADDING, ITEM_PADDING, subItem.Bounds.Width - 2 * ITEM_PADDING, subItem.Bounds.Height - 2 * ITEM_PADDING), + getStringFormat()); + } + + e.Graphics.DrawImage((Image)b.Clone(), new Point(0, e.Item.Bounds.Location.Y)); + g.Dispose(); + b.Dispose(); + } + + private StringFormat getStringFormat() + { + return new StringFormat + { + FormatFlags = StringFormatFlags.LineLimit, + Trimming = StringTrimming.EllipsisCharacter, + Alignment = StringAlignment.Near, + LineAlignment = StringAlignment.Center + }; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class LogFont + { + public int lfHeight = 0; + public int lfWidth = 0; + public int lfEscapement = 0; + public int lfOrientation = 0; + public int lfWeight = 0; + public byte lfItalic = 0; + public byte lfUnderline = 0; + public byte lfStrikeOut = 0; + public byte lfCharSet = 0; + public byte lfOutPrecision = 0; + public byte lfClipPrecision = 0; + public byte lfQuality = 0; + public byte lfPitchAndFamily = 0; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string lfFaceName = string.Empty; + } + + protected override void OnCreateControl() + { + base.OnCreateControl(); + + // This hack tries to apply the Roboto (24) font to all ListViewItems in this ListView + // It only succeeds if the font is installed on the system. + // Otherwise, a default sans serif font is used. + var roboto24 = new Font(SkinManager.ROBOTO_MEDIUM_12.FontFamily, 24); + var roboto24Logfont = new LogFont(); + roboto24.ToLogFont(roboto24Logfont); + + try + { + // Font.FromLogFont is the method used when drawing ListViewItems. I 'test' it in this safer context to avoid unhandled exceptions later. + Font = Font.FromLogFont(roboto24Logfont); + } + catch (ArgumentException) + { + Font = new Font(FontFamily.GenericSansSerif, 24); + } + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialLoader.Designer.cs b/framework/MaterialSkin.Core/Controls/MaterialLoader.Designer.cs new file mode 100644 index 0000000..16f5b89 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialLoader.Designer.cs @@ -0,0 +1,47 @@ +namespace MaterialSkin.Controls +{ + partial class MaterialLoader + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + SuspendLayout(); + // + // MaterialLoader + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(422, 189); + ControlBox = false; + Name = "MaterialLoader"; + StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + ResumeLayout(false); + + } + + #endregion + } +} \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Controls/MaterialLoader.cs b/framework/MaterialSkin.Core/Controls/MaterialLoader.cs new file mode 100644 index 0000000..0cf0934 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialLoader.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace MaterialSkin.Controls +{ + public partial class MaterialLoader : MaterialForm + { + private static MaterialLoader _instance; + private static Thread _loaderThread; + private static ManualResetEvent _instanceReadyEvent = new ManualResetEvent(false); + private readonly Dictionary _loadingProcesses; + + private MaterialLoader() + { + InitializeComponent(); + RemoveMessageFilter(); + _loadingProcesses = new Dictionary(); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.StartPosition = FormStartPosition.CenterScreen; + } + + public static MaterialLoader Instance + { + get + { + if (_instance == null || _instance.IsDisposed) + { + _instanceReadyEvent.Reset(); + _loaderThread = new Thread(() => + { + _instance = new MaterialLoader(); + _instance.Shown += (_, _) => + { + _instanceReadyEvent.Set(); + }; + Application.Run(_instance); + }); + _loaderThread.SetApartmentState(ApartmentState.STA); + _loaderThread.Start(); + _instanceReadyEvent.WaitOne(); + } + return _instance; + } + } + + public void StartLoading(string title) + { + if (_instance == null || !_instance.Visible) + { + Instance.Invoke(new Action(() => + { + Instance.Show(); + Instance.Visible= true; + })); + } + + Instance.Invoke(new Action(() => + { + if (_loadingProcesses.ContainsKey(title)) + { + return; // Prevent duplicate loading processes with the same title + } + + var progressBar = new MaterialProgressBar + { + Width = Instance.ClientSize.Width - 20, + Height = 20, + Top = 50 + (_loadingProcesses.Count * 30), + Left = 10, + Style = ProgressBarStyle.Marquee, + MarqueeAnimationSpeed = 30, + Value = 50, + BackColor = Color.White + }; + + var label = new Label + { + Text = title, + AutoSize = true, + Top = progressBar.Top - 20, + Left = progressBar.Left, + BackColor = Color.White + }; + + _loadingProcesses[title] = (progressBar, label); + Instance.Controls.Add(label); + Instance.Controls.Add(progressBar); + Instance.Height = 50 + (_loadingProcesses.Count * 30); + Instance.Refresh(); + })); + } + + public void FinishLoading(string title) + { + if (_instance == null) + { + return; + } + + Instance.Invoke(new Action(() => + { + if (_loadingProcesses.TryGetValue(title, out var components)) + { + Instance.Controls.Remove(components.ProgressBar); + Instance.Controls.Remove(components.Label); + _loadingProcesses.Remove(title); + RearrangeProgressBarsAndLabels(); + } + + if (_loadingProcesses.Count == 0) + { + Instance.Visible= false; + } + })); + } + + private void RearrangeProgressBarsAndLabels() + { + int index = 0; + foreach (var components in _loadingProcesses.Values) + { + components.ProgressBar.Top = 50 + (index * 30); + components.Label.Top = components.ProgressBar.Top - 20; + index++; + } + + this.Height = 50 + (_loadingProcesses.Count * 30); + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialLoader.resx b/framework/MaterialSkin.Core/Controls/MaterialLoader.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialLoader.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Controls/MaterialMessageBox.Designer.cs b/framework/MaterialSkin.Core/Controls/MaterialMessageBox.Designer.cs new file mode 100644 index 0000000..72333bc --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialMessageBox.Designer.cs @@ -0,0 +1,90 @@ +namespace MaterialSkin.Controls +{ + partial class MaterialMessageBox + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.pnlButtons = new System.Windows.Forms.FlowLayoutPanel(); + this.lblTitle = new System.Windows.Forms.Label(); + this.lblText = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // pnlButtons + // + this.pnlButtons.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.pnlButtons.AutoSize = true; + this.pnlButtons.Location = new System.Drawing.Point(280, 204); + this.pnlButtons.Name = "pnlButtons"; + this.pnlButtons.Padding = new System.Windows.Forms.Padding(0, 20, 0, 0); + this.pnlButtons.Size = new System.Drawing.Size(140, 100); + this.pnlButtons.TabIndex = 0; + // + // lblTitle + // + this.lblTitle.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.lblTitle.Location = new System.Drawing.Point(0, 20); + this.lblTitle.Name = "lblTitle"; + this.lblTitle.Size = new System.Drawing.Size(1340, 40); + this.lblTitle.TabIndex = 1; + this.lblTitle.Text = "label1"; + this.lblTitle.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // lblText + // + this.lblText.Location = new System.Drawing.Point(280, 80); + this.lblText.Name = "lblText"; + this.lblText.Size = new System.Drawing.Size(680, 100); + this.lblText.TabIndex = 2; + this.lblText.Text = "label1"; + this.lblText.TextAlign = System.Drawing.ContentAlignment.TopCenter; + // + // MaterialMessageBox + // + this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 26F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.ClientSize = new System.Drawing.Size(1338, 312); + this.ControlBox = false; + this.Controls.Add(this.lblText); + this.Controls.Add(this.lblTitle); + this.Controls.Add(this.pnlButtons); + this.Font = new System.Drawing.Font("Roboto", 15.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204))); + this.Margin = new System.Windows.Forms.Padding(6); + this.Name = "MaterialMessageBox"; + this.StartPosition = System.Windows.Forms.FormStartPosition.Manual; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + private System.Windows.Forms.FlowLayoutPanel pnlButtons; + private System.Windows.Forms.Label lblTitle; + private System.Windows.Forms.Label lblText; + } +} \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Controls/MaterialMessageBox.cs b/framework/MaterialSkin.Core/Controls/MaterialMessageBox.cs new file mode 100644 index 0000000..1857b3f --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialMessageBox.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace MaterialSkin.Controls +{ + public partial class MaterialMessageBox : MaterialForm + { + [Browsable(false)] + private MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + private MaterialMessageBox() + { + InitializeComponent(); + BackColor = SkinManager.GetApplicationBackgroundColor(); + Left = 0; + Width = Screen.PrimaryScreen.WorkingArea.Width; + Top = (Screen.PrimaryScreen.WorkingArea.Height - Height) / 2; + lblTitle.BackColor = SkinManager.ColorScheme.PrimaryColor; + lblTitle.ForeColor= SkinManager.ColorScheme.TextColor; + lblText.Left = (Width - lblText.Width) / 2; + + } + + private void AlignControls() + { + pnlButtons.Left = (Width - pnlButtons.Width) / 2; + } + + public static DialogResult Show(string text) + { + return Show(null, text, string.Empty, MessageBoxButtons.OK); + } + public static DialogResult Show(IWin32Window owner,string text, string caption, MessageBoxButtons buttons) + { + var mb = new MaterialMessageBox(); + mb.lblTitle.Text = caption; + mb.lblText.Text = text; + List results; + switch (buttons) + { + case MessageBoxButtons.OK: + results=new List(){DialogResult.OK}; + break; + case MessageBoxButtons.OKCancel: + results = new List() { DialogResult.OK,DialogResult.Cancel }; + break; + case MessageBoxButtons.AbortRetryIgnore: + results = new List() { DialogResult.Abort,DialogResult.Retry,DialogResult.Ignore }; + break; + case MessageBoxButtons.YesNoCancel: + results = new List() { DialogResult.Yes,DialogResult.No,DialogResult.Cancel }; + break; + case MessageBoxButtons.YesNo: + results = new List() { DialogResult.Yes,DialogResult.No }; + break; + case MessageBoxButtons.RetryCancel: + results = new List() { DialogResult.Retry,DialogResult.Cancel }; + break; + default: + throw new ArgumentOutOfRangeException(nameof(buttons), buttons, null); + } + + var btns=Enum.GetNames(typeof(DialogResult)); + foreach (var btn in results) + { + MaterialRaisedButton opt = new MaterialRaisedButton(); + opt.Size = new Size(180, 60); + opt.Margin = new Padding(10, 20, 10, 0); + opt.Text = Enum.GetName(typeof(DialogResult),btn); + opt.Click += (sender, args) => + mb.DialogResult = btn; + mb.pnlButtons.Controls.Add(opt); + mb.AlignControls(); + } + + return mb.ShowDialog(owner); + } + + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialMessageBox.resx b/framework/MaterialSkin.Core/Controls/MaterialMessageBox.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialMessageBox.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Controls/MaterialPanel.cs b/framework/MaterialSkin.Core/Controls/MaterialPanel.cs new file mode 100644 index 0000000..eb958a4 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialPanel.cs @@ -0,0 +1,16 @@ +using System.ComponentModel; +using System.Windows.Forms; + +namespace MaterialSkin.Controls +{ + public class MaterialPanel:Panel + { + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + protected override void OnPaint(PaintEventArgs e) + { + + e.Graphics.Clear(SkinManager.GetApplicationBackgroundColor()); + } + } +} \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Controls/MaterialProgressBar.cs b/framework/MaterialSkin.Core/Controls/MaterialProgressBar.cs new file mode 100644 index 0000000..cda231e --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialProgressBar.cs @@ -0,0 +1,134 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace MaterialSkin.Controls +{ + /// + /// Material design-like progress bar + /// + public class MaterialProgressBar : ProgressBar, IMaterialControl + { + private int _animation; + + /// + /// Initializes a new instance of the class. + /// + public MaterialProgressBar() + { + SetStyle(ControlStyles.UserPaint, true); + SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + Timer timer = new Timer(); + timer.Interval = 33; + timer.Tick += Timer_Tick; + timer.Enabled = true; + } + + private void Timer_Tick(object sender, EventArgs e) + { + Invalidate(); + } + + /// + /// Gets or sets the depth. + /// + /// + /// The depth. + /// + [Browsable(false)] + public int Depth { get; set; } + + /// + /// Gets the skin manager. + /// + /// + /// The skin manager. + /// + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + + /// + /// Gets or sets the state of the mouse. + /// + /// + /// The state of the mouse. + /// + [Browsable(false)] + public MouseState MouseState { get; set; } + + /// + /// Performs the work of setting the specified bounds of this control. + /// + /// The new property value of the control. + /// The new property value of the control. + /// The new property value of the control. + /// The new property value of the control. + /// A bitwise combination of the values. + protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) + { + base.SetBoundsCore(x, y, width, 5, specified); + } + + protected override void CreateHandle() + { + base.CreateHandle(); + if (Style == ProgressBarStyle.Marquee) + StartAnimation(); + } + + private void StartAnimation() + { + _animation = 0; + } + + /// + /// Raises the event. + /// + /// A that contains the event data. + protected override void OnPaint(PaintEventArgs e) + { + if (Style == ProgressBarStyle.Marquee) + { + try + { + + + + var animationProgress = (int)(e.ClipRectangle.Width * ((double)_animation / Maximum)); + var doneProgress = (int)(e.ClipRectangle.Width * ((double)Value / Maximum)); + if (doneProgress + animationProgress > e.ClipRectangle.Width) + doneProgress = e.ClipRectangle.Width - animationProgress; + + _animation+=Step; + if (_animation >= 100) _animation = -Value; + e.Graphics.FillRectangle(SkinManager.GetDisabledOrHintBrush(), 0, 0, e.ClipRectangle.Width, e.ClipRectangle.Height); + e.Graphics.FillRectangle(SkinManager.ColorScheme.PrimaryBrush, animationProgress, 0, doneProgress, e.ClipRectangle.Height); + //Task.Delay(33).ContinueWith((x) => + //{ + // try + // { + // System.Action a = () => { }; + // if (InvokeRequired) + // Invoke(a); + // else + // a(); + // }catch{} + + //}); + } + catch (Exception exception) + { + + } + } + else + { + var doneProgress = (int)(e.ClipRectangle.Width * ((double)Value / Maximum)); + e.Graphics.FillRectangle(SkinManager.ColorScheme.PrimaryBrush, 0, 0, doneProgress, e.ClipRectangle.Height); + e.Graphics.FillRectangle(SkinManager.GetDisabledOrHintBrush(), doneProgress, 0, e.ClipRectangle.Width, e.ClipRectangle.Height); + } + + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialRadioButton.cs b/framework/MaterialSkin.Core/Controls/MaterialRadioButton.cs new file mode 100644 index 0000000..aef9d71 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialRadioButton.cs @@ -0,0 +1,211 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Text; +using System.Windows.Forms; +using MaterialSkin.Animations; + +namespace MaterialSkin.Controls +{ + public class MaterialRadioButton : RadioButton, IMaterialControl + { + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + [Browsable(false)] + public Point MouseLocation { get; set; } + + private bool ripple; + [Category("Behavior")] + public bool Ripple + { + get { return ripple; } + set + { + ripple = value; + AutoSize = AutoSize; //Make AutoSize directly set the bounds. + + if (value) + { + Margin = new Padding(0); + } + + Invalidate(); + } + } + + // animation managers + private readonly AnimationManager _animationManager; + private readonly AnimationManager _rippleAnimationManager; + + // size related variables which should be recalculated onsizechanged + private Rectangle _radioButtonBounds; + private int _boxOffset; + + // size constants + private const int RADIOBUTTON_SIZE = 19; + private const int RADIOBUTTON_SIZE_HALF = RADIOBUTTON_SIZE / 2; + private const int RADIOBUTTON_OUTER_CIRCLE_WIDTH = 2; + private const int RADIOBUTTON_INNER_CIRCLE_SIZE = RADIOBUTTON_SIZE - (2 * RADIOBUTTON_OUTER_CIRCLE_WIDTH); + + public MaterialRadioButton() + { + SetStyle(ControlStyles.DoubleBuffer | ControlStyles.OptimizedDoubleBuffer, true); + + _animationManager = new AnimationManager + { + AnimationType = AnimationType.EaseInOut, + Increment = 0.06 + }; + _rippleAnimationManager = new AnimationManager(false) + { + AnimationType = AnimationType.Linear, + Increment = 0.10, + SecondaryIncrement = 0.08 + }; + _animationManager.OnAnimationProgress += sender => Invalidate(); + _rippleAnimationManager.OnAnimationProgress += sender => Invalidate(); + + CheckedChanged += (sender, args) => _animationManager.StartNewAnimation(Checked ? AnimationDirection.In : AnimationDirection.Out); + + SizeChanged += OnSizeChanged; + + Ripple = true; + MouseLocation = new Point(-1, -1); + } + private void OnSizeChanged(object sender, EventArgs eventArgs) + { + _boxOffset = Height / 2 - (int)Math.Ceiling(RADIOBUTTON_SIZE / 2d); + _radioButtonBounds = new Rectangle(_boxOffset, _boxOffset, RADIOBUTTON_SIZE, RADIOBUTTON_SIZE); + } + + public override Size GetPreferredSize(Size proposedSize) + { + var width = _boxOffset + 20 + (int)CreateGraphics().MeasureString(Text, SkinManager.ROBOTO_MEDIUM_10).Width; + return Ripple ? new Size(width, 30) : new Size(width, 20); + } + + protected override void OnPaint(PaintEventArgs pevent) + { + var g = pevent.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.TextRenderingHint = TextRenderingHint.AntiAlias; + + // clear the control + g.Clear(Parent.BackColor); + + var RADIOBUTTON_CENTER = _boxOffset + RADIOBUTTON_SIZE_HALF; + + var animationProgress = _animationManager.GetProgress(); + + int colorAlpha = Enabled ? (int)(animationProgress * 255.0) : SkinManager.GetCheckBoxOffDisabledColor().A; + int backgroundAlpha = Enabled ? (int)(SkinManager.GetCheckboxOffColor().A * (1.0 - animationProgress)) : SkinManager.GetCheckBoxOffDisabledColor().A; + float animationSize = (float)(animationProgress * 8f); + float animationSizeHalf = animationSize / 2; + animationSize = (float)(animationProgress * 9f); + + var brush = new SolidBrush(Color.FromArgb(colorAlpha, Enabled ? SkinManager.ColorScheme.AccentColor : SkinManager.GetCheckBoxOffDisabledColor())); + var pen = new Pen(brush.Color); + + // draw ripple animation + if (Ripple && _rippleAnimationManager.IsAnimating()) + { + for (var i = 0; i < _rippleAnimationManager.GetAnimationCount(); i++) + { + var animationValue = _rippleAnimationManager.GetProgress(i); + var animationSource = new Point(RADIOBUTTON_CENTER, RADIOBUTTON_CENTER); + var rippleBrush = new SolidBrush(Color.FromArgb((int)((animationValue * 40)), ((bool)_rippleAnimationManager.GetData(i)[0]) ? Color.Black : brush.Color)); + var rippleHeight = (Height % 2 == 0) ? Height - 3 : Height - 2; + var rippleSize = (_rippleAnimationManager.GetDirection(i) == AnimationDirection.InOutIn) ? (int)(rippleHeight * (0.8d + (0.2d * animationValue))) : rippleHeight; + using (var path = DrawHelper.CreateRoundRect(animationSource.X - rippleSize / 2, animationSource.Y - rippleSize / 2, rippleSize, rippleSize, rippleSize / 2)) + { + g.FillPath(rippleBrush, path); + } + + rippleBrush.Dispose(); + } + } + + // draw radiobutton circle + Color uncheckedColor = DrawHelper.BlendColor(Parent.BackColor, Enabled ? SkinManager.GetCheckboxOffColor() : SkinManager.GetCheckBoxOffDisabledColor(), backgroundAlpha); + + using (var path = DrawHelper.CreateRoundRect(_boxOffset, _boxOffset, RADIOBUTTON_SIZE, RADIOBUTTON_SIZE, 9f)) + { + g.FillPath(new SolidBrush(uncheckedColor), path); + + if (Enabled) + { + g.FillPath(brush, path); + } + } + + g.FillEllipse( + new SolidBrush(Parent.BackColor), + RADIOBUTTON_OUTER_CIRCLE_WIDTH + _boxOffset, + RADIOBUTTON_OUTER_CIRCLE_WIDTH + _boxOffset, + RADIOBUTTON_INNER_CIRCLE_SIZE, + RADIOBUTTON_INNER_CIRCLE_SIZE); + + if (Checked) + { + using (var path = DrawHelper.CreateRoundRect(RADIOBUTTON_CENTER - animationSizeHalf, RADIOBUTTON_CENTER - animationSizeHalf, animationSize, animationSize, 4f)) + { + g.FillPath(brush, path); + } + } + SizeF stringSize = g.MeasureString(Text, SkinManager.ROBOTO_MEDIUM_10); + g.DrawString(Text, SkinManager.ROBOTO_MEDIUM_10, Enabled ? SkinManager.GetPrimaryTextBrush() : SkinManager.GetDisabledOrHintBrush(), _boxOffset + 22, Height / 2 - stringSize.Height / 2); + + brush.Dispose(); + pen.Dispose(); + } + + private bool IsMouseInCheckArea() + { + return _radioButtonBounds.Contains(MouseLocation); + } + + protected override void OnCreateControl() + { + base.OnCreateControl(); + Font = SkinManager.ROBOTO_MEDIUM_10; + + if (DesignMode) return; + + MouseState = MouseState.OUT; + MouseEnter += (sender, args) => + { + MouseState = MouseState.HOVER; + }; + MouseLeave += (sender, args) => + { + MouseLocation = new Point(-1, -1); + MouseState = MouseState.OUT; + }; + MouseDown += (sender, args) => + { + MouseState = MouseState.DOWN; + + if (Ripple && args.Button == MouseButtons.Left && IsMouseInCheckArea()) + { + _rippleAnimationManager.SecondaryIncrement = 0; + _rippleAnimationManager.StartNewAnimation(AnimationDirection.InOutIn, new object[] { Checked }); + } + }; + MouseUp += (sender, args) => + { + MouseState = MouseState.HOVER; + _rippleAnimationManager.SecondaryIncrement = 0.08; + }; + MouseMove += (sender, args) => + { + MouseLocation = args.Location; + Cursor = IsMouseInCheckArea() ? Cursors.Hand : Cursors.Default; + }; + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialRaisedButton.cs b/framework/MaterialSkin.Core/Controls/MaterialRaisedButton.cs new file mode 100644 index 0000000..f229b64 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialRaisedButton.cs @@ -0,0 +1,216 @@ +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Text; +using System.Windows.Forms; +using MaterialSkin.Animations; +using System; + +namespace MaterialSkin.Controls +{ + public class MaterialRaisedButton : Button, IMaterialControl + { + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + public bool Primary { get; set; } + + + public bool DrawBorder + { + get { return _drawBorder; } + set + { + _drawBorder = value; + } + } + + private readonly AnimationManager _animationManager; + + private SizeF _textSize; + + private Image _icon; + private bool _drawBorder; + private Point _lastLocation; + private bool _wasClick; + + public Image Icon + { + get { return _icon; } + set + { + _icon = value; + if (AutoSize) + Size = GetPreferredSize(); + Invalidate(); + } + } + + public MaterialRaisedButton() + { + Primary = true; + Cursor = Cursors.Hand; + _animationManager = new AnimationManager(false) + { + Increment = 0.02, + AnimationType = AnimationType.EaseOut + }; + _animationManager.OnAnimationProgress += sender => Invalidate(); + _animationManager.OnAnimationFinished +=AnimationManagerOnOnAnimationFinished; + AutoSizeMode = AutoSizeMode.GrowAndShrink; + + } + + private void AnimationManagerOnOnAnimationFinished(object o) + { + if (_wasClick) + { + _wasClick = false; + + + } + } + + public override string Text + { + get { return base.Text; } + set + { + base.Text = value; + _textSize = CreateGraphics().MeasureString(value.ToUpper(), SkinManager.ROBOTO_MEDIUM_10); + if (AutoSize) + Size = GetPreferredSize(); + Invalidate(); + } + } + + protected override void OnMouseUp(MouseEventArgs mevent) + { + base.OnMouseUp(mevent); + + + } + + protected override void OnMouseDown(MouseEventArgs mevent) + { + base.OnMouseDown(mevent); + _lastLocation = mevent.Location; + + } + + protected override void OnClick(EventArgs e) + { + _animationManager.StartNewAnimation(AnimationDirection.In,_lastLocation); + base.OnClick(e); + } + + + protected override void OnPaint(PaintEventArgs pevent) + { + try + { + var g = pevent.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + //g.TextRenderingHint = TextRenderingHint.AntiAlias; + + g.Clear(Parent.BackColor); + + using (var backgroundPath = DrawHelper.CreateRoundRect(ClientRectangle.X, + ClientRectangle.Y, + ClientRectangle.Width - 1, + ClientRectangle.Height - 1, + 1f)) + { + + g.FillPath( + Enabled + ? (Primary + ? SkinManager.ColorScheme.PrimaryBrush + : SkinManager.GetRaisedButtonBackgroundBrush()) + : Brushes.DarkGray, backgroundPath); + if (!Primary && this.DrawBorder) + { + g.DrawPath(SkinManager.ColorScheme.PrimaryPen, backgroundPath); + } + } + + if (_animationManager.IsAnimating()) + { + for (int i = 0; i < _animationManager.GetAnimationCount(); i++) + { + var animationValue = _animationManager.GetProgress(i); + var animationSource = _animationManager.GetSource(i); + var rippleBrush = new SolidBrush(Color.FromArgb((int) (51 - (animationValue * 50)), + Primary ? Color.White : SkinManager.ColorScheme.PrimaryColor)); + var rippleSize = (int) (animationValue * Width * 2); + g.FillEllipse(rippleBrush, + new Rectangle(animationSource.X - rippleSize / 2, animationSource.Y - rippleSize / 2, + rippleSize, rippleSize)); + } + } + + //Icon + var iconRect = new Rectangle(8, 6, 24, 24); + + if (string.IsNullOrEmpty(Text)) + // Center Icon + iconRect.X += 2; + + if (Icon != null) + g.DrawImage(Icon, iconRect); + + //Text + var textRect = ClientRectangle; + + if (Icon != null) + { + // + // Resize and move Text container + // + + // First 8: left padding + // 24: icon width + // Second 4: space between Icon and Text + // Third 8: right padding + textRect.Width -= 8 + 24 + 4 + 8; + + // First 8: left padding + // 24: icon width + // Second 4: space between Icon and Text + textRect.X += 8 + 24 + 4; + } + + g.DrawString( + Text.ToUpper(), + Font, + Primary ? SkinManager.GetRaisedButtonTextBrush(Primary) : SkinManager.ColorScheme.PrimaryBrush, + textRect, + new StringFormat {Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center}); + } + catch + { + } + } + + private Size GetPreferredSize() + { + return GetPreferredSize(new Size(0, 0)); + } + + public override Size GetPreferredSize(Size proposedSize) + { + // Provides extra space for proper padding for content + var extra = 16; + + if (Icon != null) + // 24 is for icon size + // 4 is for the space between icon & text + extra += 24 + 4; + + return new Size((int)Math.Ceiling(_textSize.Width) + extra, 36); + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialSingleLineTextField.cs b/framework/MaterialSkin.Core/Controls/MaterialSingleLineTextField.cs new file mode 100644 index 0000000..1120c9e --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialSingleLineTextField.cs @@ -0,0 +1,1169 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using MaterialSkin.Animations; + +namespace MaterialSkin.Controls +{ + public class MaterialSingleLineTextField : Control, IMaterialControl + { + //Properties for managing the material design properties + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + + private readonly BaseTextBox _baseTextBox; + private readonly AnimationManager _animationManager; + + public override string Text { get { return _baseTextBox.Text; } set { _baseTextBox.Text = value; } } + public new object Tag { get { return _baseTextBox.Tag; } set { _baseTextBox.Tag = value; } } + public new int MaxLength { get { return _baseTextBox.MaxLength; } set { _baseTextBox.MaxLength = value; } } + + public string SelectedText { get { return _baseTextBox.SelectedText; } set { _baseTextBox.SelectedText = value; } } + public string Hint { get { return _baseTextBox.Hint; } set { _baseTextBox.Hint = value; } } + + public int SelectionStart { get { return _baseTextBox.SelectionStart; } set { _baseTextBox.SelectionStart = value; } } + public int SelectionLength { get { return _baseTextBox.SelectionLength; } set { _baseTextBox.SelectionLength = value; } } + public int TextLength => _baseTextBox.TextLength; + + public bool UseSystemPasswordChar { get { return _baseTextBox.UseSystemPasswordChar; } set { _baseTextBox.UseSystemPasswordChar = value; } } + public char PasswordChar { get { return _baseTextBox.PasswordChar; } set { _baseTextBox.PasswordChar = value; } } + + public void SelectAll() { _baseTextBox.SelectAll(); } + public void Clear() { _baseTextBox.Clear(); } + public void Focus() { _baseTextBox.Focus(); } + + + # region Forwarding events to baseTextBox + public event EventHandler AcceptsTabChanged + { + add + { + _baseTextBox.AcceptsTabChanged += value; + } + remove + { + _baseTextBox.AcceptsTabChanged -= value; + } + } + + public new event EventHandler AutoSizeChanged + { + add + { + _baseTextBox.AutoSizeChanged += value; + } + remove + { + _baseTextBox.AutoSizeChanged -= value; + } + } + + public new event EventHandler BackgroundImageChanged + { + add + { + _baseTextBox.BackgroundImageChanged += value; + } + remove + { + _baseTextBox.BackgroundImageChanged -= value; + } + } + + public new event EventHandler BackgroundImageLayoutChanged + { + add + { + _baseTextBox.BackgroundImageLayoutChanged += value; + } + remove + { + _baseTextBox.BackgroundImageLayoutChanged -= value; + } + } + + public new event EventHandler BindingContextChanged + { + add + { + _baseTextBox.BindingContextChanged += value; + } + remove + { + _baseTextBox.BindingContextChanged -= value; + } + } + + public event EventHandler BorderStyleChanged + { + add + { + _baseTextBox.BorderStyleChanged += value; + } + remove + { + _baseTextBox.BorderStyleChanged -= value; + } + } + + public new event EventHandler CausesValidationChanged + { + add + { + _baseTextBox.CausesValidationChanged += value; + } + remove + { + _baseTextBox.CausesValidationChanged -= value; + } + } + + public new event UICuesEventHandler ChangeUICues + { + add + { + _baseTextBox.ChangeUICues += value; + } + remove + { + _baseTextBox.ChangeUICues -= value; + } + } + + public new event EventHandler Click + { + add + { + _baseTextBox.Click += value; + } + remove + { + _baseTextBox.Click -= value; + } + } + + public new event EventHandler ClientSizeChanged + { + add + { + _baseTextBox.ClientSizeChanged += value; + } + remove + { + _baseTextBox.ClientSizeChanged -= value; + } + } + + + + public new event EventHandler ContextMenuStripChanged + { + add + { + _baseTextBox.ContextMenuStripChanged += value; + } + remove + { + _baseTextBox.ContextMenuStripChanged -= value; + } + } + + public new event ControlEventHandler ControlAdded + { + add + { + _baseTextBox.ControlAdded += value; + } + remove + { + _baseTextBox.ControlAdded -= value; + } + } + + public new event ControlEventHandler ControlRemoved + { + add + { + _baseTextBox.ControlRemoved += value; + } + remove + { + _baseTextBox.ControlRemoved -= value; + } + } + + public new event EventHandler CursorChanged + { + add + { + _baseTextBox.CursorChanged += value; + } + remove + { + _baseTextBox.CursorChanged -= value; + } + } + + public new event EventHandler Disposed + { + add + { + _baseTextBox.Disposed += value; + } + remove + { + _baseTextBox.Disposed -= value; + } + } + + public new event EventHandler DockChanged + { + add + { + _baseTextBox.DockChanged += value; + } + remove + { + _baseTextBox.DockChanged -= value; + } + } + + public new event EventHandler DoubleClick + { + add + { + _baseTextBox.DoubleClick += value; + } + remove + { + _baseTextBox.DoubleClick -= value; + } + } + + public new event DragEventHandler DragDrop + { + add + { + _baseTextBox.DragDrop += value; + } + remove + { + _baseTextBox.DragDrop -= value; + } + } + + public new event DragEventHandler DragEnter + { + add + { + _baseTextBox.DragEnter += value; + } + remove + { + _baseTextBox.DragEnter -= value; + } + } + + public new event EventHandler DragLeave + { + add + { + _baseTextBox.DragLeave += value; + } + remove + { + _baseTextBox.DragLeave -= value; + } + } + + public new event DragEventHandler DragOver + { + add + { + _baseTextBox.DragOver += value; + } + remove + { + _baseTextBox.DragOver -= value; + } + } + + public new event EventHandler EnabledChanged + { + add + { + _baseTextBox.EnabledChanged += value; + } + remove + { + _baseTextBox.EnabledChanged -= value; + } + } + + public new event EventHandler Enter + { + add + { + _baseTextBox.Enter += value; + } + remove + { + _baseTextBox.Enter -= value; + } + } + + public new event EventHandler FontChanged + { + add + { + _baseTextBox.FontChanged += value; + } + remove + { + _baseTextBox.FontChanged -= value; + } + } + + public new event EventHandler ForeColorChanged + { + add + { + _baseTextBox.ForeColorChanged += value; + } + remove + { + _baseTextBox.ForeColorChanged -= value; + } + } + + public new event GiveFeedbackEventHandler GiveFeedback + { + add + { + _baseTextBox.GiveFeedback += value; + } + remove + { + _baseTextBox.GiveFeedback -= value; + } + } + + public new event EventHandler GotFocus + { + add + { + _baseTextBox.GotFocus += value; + } + remove + { + _baseTextBox.GotFocus -= value; + } + } + + public new event EventHandler HandleCreated + { + add + { + _baseTextBox.HandleCreated += value; + } + remove + { + _baseTextBox.HandleCreated -= value; + } + } + + public new event EventHandler HandleDestroyed + { + add + { + _baseTextBox.HandleDestroyed += value; + } + remove + { + _baseTextBox.HandleDestroyed -= value; + } + } + + public new event HelpEventHandler HelpRequested + { + add + { + _baseTextBox.HelpRequested += value; + } + remove + { + _baseTextBox.HelpRequested -= value; + } + } + + public event EventHandler HideSelectionChanged + { + add + { + _baseTextBox.HideSelectionChanged += value; + } + remove + { + _baseTextBox.HideSelectionChanged -= value; + } + } + + public new event EventHandler ImeModeChanged + { + add + { + _baseTextBox.ImeModeChanged += value; + } + remove + { + _baseTextBox.ImeModeChanged -= value; + } + } + + public new event InvalidateEventHandler Invalidated + { + add + { + _baseTextBox.Invalidated += value; + } + remove + { + _baseTextBox.Invalidated -= value; + } + } + + public new event KeyEventHandler KeyDown + { + add + { + _baseTextBox.KeyDown += value; + } + remove + { + _baseTextBox.KeyDown -= value; + } + } + + public new event KeyPressEventHandler KeyPress + { + add + { + _baseTextBox.KeyPress += value; + } + remove + { + _baseTextBox.KeyPress -= value; + } + } + + public new event KeyEventHandler KeyUp + { + add + { + _baseTextBox.KeyUp += value; + } + remove + { + _baseTextBox.KeyUp -= value; + } + } + + public new event LayoutEventHandler Layout + { + add + { + _baseTextBox.Layout += value; + } + remove + { + _baseTextBox.Layout -= value; + } + } + + public new event EventHandler Leave + { + add + { + _baseTextBox.Leave += value; + } + remove + { + _baseTextBox.Leave -= value; + } + } + + public new event EventHandler LocationChanged + { + add + { + _baseTextBox.LocationChanged += value; + } + remove + { + _baseTextBox.LocationChanged -= value; + } + } + + public new event EventHandler LostFocus + { + add + { + _baseTextBox.LostFocus += value; + } + remove + { + _baseTextBox.LostFocus -= value; + } + } + + public new event EventHandler MarginChanged + { + add + { + _baseTextBox.MarginChanged += value; + } + remove + { + _baseTextBox.MarginChanged -= value; + } + } + + public event EventHandler ModifiedChanged + { + add + { + _baseTextBox.ModifiedChanged += value; + } + remove + { + _baseTextBox.ModifiedChanged -= value; + } + } + + public new event EventHandler MouseCaptureChanged + { + add + { + _baseTextBox.MouseCaptureChanged += value; + } + remove + { + _baseTextBox.MouseCaptureChanged -= value; + } + } + + public new event MouseEventHandler MouseClick + { + add + { + _baseTextBox.MouseClick += value; + } + remove + { + _baseTextBox.MouseClick -= value; + } + } + + public new event MouseEventHandler MouseDoubleClick + { + add + { + _baseTextBox.MouseDoubleClick += value; + } + remove + { + _baseTextBox.MouseDoubleClick -= value; + } + } + + public new event MouseEventHandler MouseDown + { + add + { + _baseTextBox.MouseDown += value; + } + remove + { + _baseTextBox.MouseDown -= value; + } + } + + public new event EventHandler MouseEnter + { + add + { + _baseTextBox.MouseEnter += value; + } + remove + { + _baseTextBox.MouseEnter -= value; + } + } + + public new event EventHandler MouseHover + { + add + { + _baseTextBox.MouseHover += value; + } + remove + { + _baseTextBox.MouseHover -= value; + } + } + + public new event EventHandler MouseLeave + { + add + { + _baseTextBox.MouseLeave += value; + } + remove + { + _baseTextBox.MouseLeave -= value; + } + } + + public new event MouseEventHandler MouseMove + { + add + { + _baseTextBox.MouseMove += value; + } + remove + { + _baseTextBox.MouseMove -= value; + } + } + + public new event MouseEventHandler MouseUp + { + add + { + _baseTextBox.MouseUp += value; + } + remove + { + _baseTextBox.MouseUp -= value; + } + } + + public new event MouseEventHandler MouseWheel + { + add + { + _baseTextBox.MouseWheel += value; + } + remove + { + _baseTextBox.MouseWheel -= value; + } + } + + public new event EventHandler Move + { + add + { + _baseTextBox.Move += value; + } + remove + { + _baseTextBox.Move -= value; + } + } + + public event EventHandler MultilineChanged + { + add + { + _baseTextBox.MultilineChanged += value; + } + remove + { + _baseTextBox.MultilineChanged -= value; + } + } + + public new event EventHandler PaddingChanged + { + add + { + _baseTextBox.PaddingChanged += value; + } + remove + { + _baseTextBox.PaddingChanged -= value; + } + } + + public new event PaintEventHandler Paint + { + add + { + _baseTextBox.Paint += value; + } + remove + { + _baseTextBox.Paint -= value; + } + } + + public new event EventHandler ParentChanged + { + add + { + _baseTextBox.ParentChanged += value; + } + remove + { + _baseTextBox.ParentChanged -= value; + } + } + + public new event PreviewKeyDownEventHandler PreviewKeyDown + { + add + { + _baseTextBox.PreviewKeyDown += value; + } + remove + { + _baseTextBox.PreviewKeyDown -= value; + } + } + + public new event QueryAccessibilityHelpEventHandler QueryAccessibilityHelp + { + add + { + _baseTextBox.QueryAccessibilityHelp += value; + } + remove + { + _baseTextBox.QueryAccessibilityHelp -= value; + } + } + + public new event QueryContinueDragEventHandler QueryContinueDrag + { + add + { + _baseTextBox.QueryContinueDrag += value; + } + remove + { + _baseTextBox.QueryContinueDrag -= value; + } + } + + public event EventHandler ReadOnlyChanged + { + add + { + _baseTextBox.ReadOnlyChanged += value; + } + remove + { + _baseTextBox.ReadOnlyChanged -= value; + } + } + + public new event EventHandler RegionChanged + { + add + { + _baseTextBox.RegionChanged += value; + } + remove + { + _baseTextBox.RegionChanged -= value; + } + } + + public new event EventHandler Resize + { + add + { + _baseTextBox.Resize += value; + } + remove + { + _baseTextBox.Resize -= value; + } + } + + public new event EventHandler RightToLeftChanged + { + add + { + _baseTextBox.RightToLeftChanged += value; + } + remove + { + _baseTextBox.RightToLeftChanged -= value; + } + } + + public new event EventHandler SizeChanged + { + add + { + _baseTextBox.SizeChanged += value; + } + remove + { + _baseTextBox.SizeChanged -= value; + } + } + + public new event EventHandler StyleChanged + { + add + { + _baseTextBox.StyleChanged += value; + } + remove + { + _baseTextBox.StyleChanged -= value; + } + } + + public new event EventHandler SystemColorsChanged + { + add + { + _baseTextBox.SystemColorsChanged += value; + } + remove + { + _baseTextBox.SystemColorsChanged -= value; + } + } + + public new event EventHandler TabIndexChanged + { + add + { + _baseTextBox.TabIndexChanged += value; + } + remove + { + _baseTextBox.TabIndexChanged -= value; + } + } + + public new event EventHandler TabStopChanged + { + add + { + _baseTextBox.TabStopChanged += value; + } + remove + { + _baseTextBox.TabStopChanged -= value; + } + } + + public event EventHandler TextAlignChanged + { + add + { + _baseTextBox.TextAlignChanged += value; + } + remove + { + _baseTextBox.TextAlignChanged -= value; + } + } + + public new event EventHandler TextChanged + { + add + { + _baseTextBox.TextChanged += value; + } + remove + { + _baseTextBox.TextChanged -= value; + } + } + + public new event EventHandler Validated + { + add + { + _baseTextBox.Validated += value; + } + remove + { + _baseTextBox.Validated -= value; + } + } + + public new event CancelEventHandler Validating + { + add + { + _baseTextBox.Validating += value; + } + remove + { + _baseTextBox.Validating -= value; + } + } + + public new event EventHandler VisibleChanged + { + add + { + _baseTextBox.VisibleChanged += value; + } + remove + { + _baseTextBox.VisibleChanged -= value; + } + } + #endregion + + + public MaterialSingleLineTextField() + { + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.DoubleBuffer, true); + + _animationManager = new AnimationManager + { + Increment = 0.06, + AnimationType = AnimationType.EaseInOut, + InterruptAnimation = false + }; + _animationManager.OnAnimationProgress += sender => Invalidate(); + + _baseTextBox = new BaseTextBox + { + BorderStyle = BorderStyle.None, + Font = SkinManager.ROBOTO_REGULAR_11, + ForeColor = SkinManager.GetPrimaryTextColor(), + Location = new Point(0, 0), + Width = Width, + Height = Height - 5 + }; + + if (!Controls.Contains(_baseTextBox) && !DesignMode) + { + Controls.Add(_baseTextBox); + } + + _baseTextBox.GotFocus += (sender, args) => _animationManager.StartNewAnimation(AnimationDirection.In); + _baseTextBox.LostFocus += (sender, args) => _animationManager.StartNewAnimation(AnimationDirection.Out); + BackColorChanged += (sender, args) => + { + _baseTextBox.BackColor = BackColor; + _baseTextBox.ForeColor = SkinManager.GetPrimaryTextColor(); + }; + + //Fix for tabstop + _baseTextBox.TabStop = true; + this.TabStop = false; + } + + protected override void OnPaint(PaintEventArgs pevent) + { + var g = pevent.Graphics; + g.Clear(Parent.BackColor); + + var lineY = _baseTextBox.Bottom + 3; + + if (!_animationManager.IsAnimating()) + { + //No animation + g.FillRectangle(_baseTextBox.Focused ? SkinManager.ColorScheme.PrimaryBrush : SkinManager.GetDividersBrush(), _baseTextBox.Location.X, lineY, _baseTextBox.Width, _baseTextBox.Focused ? 2 : 1); + } + else + { + //Animate + int animationWidth = (int)(_baseTextBox.Width * _animationManager.GetProgress()); + int halfAnimationWidth = animationWidth / 2; + int animationStart = _baseTextBox.Location.X + _baseTextBox.Width / 2; + + //Unfocused background + g.FillRectangle(SkinManager.GetDividersBrush(), _baseTextBox.Location.X, lineY, _baseTextBox.Width, 1); + + //Animated focus transition + g.FillRectangle(SkinManager.ColorScheme.PrimaryBrush, animationStart - halfAnimationWidth, lineY, animationWidth, 2); + } + } + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + + _baseTextBox.Location = new Point(0, 0); + _baseTextBox.Width = Width; + + Height = _baseTextBox.Height + 5; + } + + protected override void OnCreateControl() + { + base.OnCreateControl(); + + _baseTextBox.BackColor = Parent.BackColor; + _baseTextBox.ForeColor = SkinManager.GetPrimaryTextColor(); + } + + private class BaseTextBox : TextBox + { + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, string lParam); + + private const int EM_SETCUEBANNER = 0x1501; + private const char EmptyChar = (char)0; + private const char VisualStylePasswordChar = '\u25CF'; + private const char NonVisualStylePasswordChar = '\u002A'; + + private string hint = string.Empty; + public string Hint + { + get { return hint; } + set + { + hint = value; + SendMessage(Handle, EM_SETCUEBANNER, (int)IntPtr.Zero, Hint); + } + } + + private char _passwordChar = EmptyChar; + public new char PasswordChar + { + get { return _passwordChar; } + set + { + _passwordChar = value; + SetBasePasswordChar(); + } + } + + public new void SelectAll() + { + BeginInvoke((MethodInvoker)delegate () + { + base.Focus(); + base.SelectAll(); + }); + } + + public new void Focus() + { + BeginInvoke((MethodInvoker)delegate () + { + base.Focus(); + }); + } + + private char _useSystemPasswordChar = EmptyChar; + public new bool UseSystemPasswordChar + { + get { return _useSystemPasswordChar != EmptyChar; } + set + { + if (value) + { + _useSystemPasswordChar = Application.RenderWithVisualStyles ? VisualStylePasswordChar : NonVisualStylePasswordChar; + } + else + { + _useSystemPasswordChar = EmptyChar; + } + + SetBasePasswordChar(); + } + } + + private void SetBasePasswordChar() + { + base.PasswordChar = UseSystemPasswordChar ? _useSystemPasswordChar : _passwordChar; + } + + public BaseTextBox() + { + MaterialContextMenuStrip cms = new TextBoxContextMenuStrip(); + cms.Opening += ContextMenuStripOnOpening; + cms.OnItemClickStart += ContextMenuStripOnItemClickStart; + + ContextMenuStrip = cms; + } + + private void ContextMenuStripOnItemClickStart(object sender, ToolStripItemClickedEventArgs toolStripItemClickedEventArgs) + { + switch (toolStripItemClickedEventArgs.ClickedItem.Text) + { + case "Undo": + Undo(); + break; + case "Cut": + Cut(); + break; + case "Copy": + Copy(); + break; + case "Paste": + Paste(); + break; + case "Delete": + SelectedText = string.Empty; + break; + case "Select All": + SelectAll(); + break; + } + } + + private void ContextMenuStripOnOpening(object sender, CancelEventArgs cancelEventArgs) + { + var strip = sender as TextBoxContextMenuStrip; + if (strip != null) + { + strip.Undo.Enabled = CanUndo; + strip.Cut.Enabled = !string.IsNullOrEmpty(SelectedText); + strip.Copy.Enabled = !string.IsNullOrEmpty(SelectedText); + strip.Paste.Enabled = Clipboard.ContainsText(); + strip.Delete.Enabled = !string.IsNullOrEmpty(SelectedText); + strip.SelectAll.Enabled = !string.IsNullOrEmpty(Text); + } + } + } + + private class TextBoxContextMenuStrip : MaterialContextMenuStrip + { + public readonly ToolStripItem Undo = new MaterialToolStripMenuItem { Text = "Undo" }; + public readonly ToolStripItem Seperator1 = new ToolStripSeparator(); + public readonly ToolStripItem Cut = new MaterialToolStripMenuItem { Text = "Cut" }; + public readonly ToolStripItem Copy = new MaterialToolStripMenuItem { Text = "Copy" }; + public readonly ToolStripItem Paste = new MaterialToolStripMenuItem { Text = "Paste" }; + public readonly ToolStripItem Delete = new MaterialToolStripMenuItem { Text = "Delete" }; + public readonly ToolStripItem Seperator2 = new ToolStripSeparator(); + public readonly ToolStripItem SelectAll = new MaterialToolStripMenuItem { Text = "Select All" }; + + public TextBoxContextMenuStrip() + { + Items.AddRange(new[] + { + Undo, + Seperator1, + Cut, + Copy, + Paste, + Delete, + Seperator2, + SelectAll + }); + } + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialTabControl.cs b/framework/MaterialSkin.Core/Controls/MaterialTabControl.cs new file mode 100644 index 0000000..85d5bc9 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialTabControl.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel; +using System.Windows.Forms; + +namespace MaterialSkin.Controls +{ + public class MaterialTabControl : TabControl, IMaterialControl + { + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + + protected override void WndProc(ref Message m) + { + if (m.Msg == 0x1328 && !DesignMode) m.Result = (IntPtr)1; + else base.WndProc(ref m); + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialTabSelector.cs b/framework/MaterialSkin.Core/Controls/MaterialTabSelector.cs new file mode 100644 index 0000000..a6c4a0c --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialTabSelector.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Text; +using System.Windows.Forms; +using MaterialSkin.Animations; + +namespace MaterialSkin.Controls +{ + public class MaterialTabSelector : Control, IMaterialControl + { + [Browsable(false)] + public int Depth { get; set; } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + [Browsable(false)] + public MouseState MouseState { get; set; } + + private MaterialTabControl _baseTabControl; + public MaterialTabControl BaseTabControl + { + get { return _baseTabControl; } + set + { + _baseTabControl = value; + if (_baseTabControl == null) return; + _previousSelectedTabIndex = _baseTabControl.SelectedIndex; + _baseTabControl.Deselected += (sender, args) => + { + _previousSelectedTabIndex = _baseTabControl.SelectedIndex; + }; + _baseTabControl.SelectedIndexChanged += (sender, args) => + { + _animationManager.SetProgress(0); + _animationManager.StartNewAnimation(AnimationDirection.In); + }; + _baseTabControl.ControlAdded += delegate + { + Invalidate(); + }; + _baseTabControl.ControlRemoved += delegate + { + Invalidate(); + }; + } + } + + private int _previousSelectedTabIndex; + private Point _animationSource; + private readonly AnimationManager _animationManager; + + private List _tabRects; + private const int TAB_HEADER_PADDING = 24; + private const int TAB_INDICATOR_HEIGHT = 2; + + public MaterialTabSelector() + { + SetStyle(ControlStyles.DoubleBuffer | ControlStyles.OptimizedDoubleBuffer, true); + Height = 48; + + _animationManager = new AnimationManager + { + AnimationType = AnimationType.EaseOut, + Increment = 0.04 + }; + _animationManager.OnAnimationProgress += sender => Invalidate(); + } + + protected override void OnPaint(PaintEventArgs e) + { + var g = e.Graphics; + g.TextRenderingHint = TextRenderingHint.AntiAlias; + + g.Clear(SkinManager.ColorScheme.PrimaryColor); + + if (_baseTabControl == null) return; + + if (!_animationManager.IsAnimating() || _tabRects == null || _tabRects.Count != _baseTabControl.TabCount) + UpdateTabRects(); + + var animationProgress = _animationManager.GetProgress(); + + //Click feedback + if (_animationManager.IsAnimating()) + { + var rippleBrush = new SolidBrush(Color.FromArgb((int)(51 - (animationProgress * 50)), Color.White)); + var rippleSize = (int)(animationProgress * _tabRects[_baseTabControl.SelectedIndex].Width * 1.75); + + g.SetClip(_tabRects[_baseTabControl.SelectedIndex]); + g.FillEllipse(rippleBrush, new Rectangle(_animationSource.X - rippleSize / 2, _animationSource.Y - rippleSize / 2, rippleSize, rippleSize)); + g.ResetClip(); + rippleBrush.Dispose(); + } + + //Draw tab headers + foreach (TabPage tabPage in _baseTabControl.TabPages) + { + var currentTabIndex = _baseTabControl.TabPages.IndexOf(tabPage); + Brush textBrush = new SolidBrush(Color.FromArgb(CalculateTextAlpha(currentTabIndex, animationProgress), SkinManager.ColorScheme.TextColor)); + + g.DrawString(tabPage.Text.ToUpper(), SkinManager.ROBOTO_MEDIUM_10, textBrush, _tabRects[currentTabIndex], new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }); + textBrush.Dispose(); + } + + //Animate tab indicator + var previousSelectedTabIndexIfHasOne = _previousSelectedTabIndex == -1 ? _baseTabControl.SelectedIndex : _previousSelectedTabIndex; + var previousActiveTabRect = _tabRects[previousSelectedTabIndexIfHasOne]; + var activeTabPageRect = _tabRects[_baseTabControl.SelectedIndex]; + + var y = activeTabPageRect.Bottom - 2; + var x = previousActiveTabRect.X + (int)((activeTabPageRect.X - previousActiveTabRect.X) * animationProgress); + var width = previousActiveTabRect.Width + (int)((activeTabPageRect.Width - previousActiveTabRect.Width) * animationProgress); + + g.FillRectangle(SkinManager.ColorScheme.AccentBrush, x, y, width, TAB_INDICATOR_HEIGHT); + } + + private int CalculateTextAlpha(int tabIndex, double animationProgress) + { + int primaryA = SkinManager.ACTION_BAR_TEXT.A; + int secondaryA = SkinManager.ACTION_BAR_TEXT_SECONDARY.A; + + if (tabIndex == _baseTabControl.SelectedIndex && !_animationManager.IsAnimating()) + { + return primaryA; + } + if (tabIndex != _previousSelectedTabIndex && tabIndex != _baseTabControl.SelectedIndex) + { + return secondaryA; + } + if (tabIndex == _previousSelectedTabIndex) + { + return primaryA - (int)((primaryA - secondaryA) * animationProgress); + } + return secondaryA + (int)((primaryA - secondaryA) * animationProgress); + } + + protected override void OnMouseUp(MouseEventArgs e) + { + base.OnMouseUp(e); + + if (_tabRects == null) UpdateTabRects(); + for (var i = 0; i < _tabRects.Count; i++) + { + if (_tabRects[i].Contains(e.Location)) + { + _baseTabControl.SelectedIndex = i; + } + } + + _animationSource = e.Location; + } + + private void UpdateTabRects() + { + _tabRects = new List(); + + //If there isn't a base tab control, the rects shouldn't be calculated + //If there aren't tab pages in the base tab control, the list should just be empty which has been set already; exit the void + if (_baseTabControl == null || _baseTabControl.TabCount == 0) return; + + //Calculate the bounds of each tab header specified in the base tab control + using (var b = new Bitmap(1, 1)) + { + using (var g = Graphics.FromImage(b)) + { + _tabRects.Add(new Rectangle(SkinManager.FORM_PADDING, 0, TAB_HEADER_PADDING * 2 + (int)g.MeasureString(_baseTabControl.TabPages[0].Text, SkinManager.ROBOTO_MEDIUM_10).Width, Height)); + for (int i = 1; i < _baseTabControl.TabPages.Count; i++) + { + _tabRects.Add(new Rectangle(_tabRects[i - 1].Right, 0, TAB_HEADER_PADDING * 2 + (int)g.MeasureString(_baseTabControl.TabPages[i].Text, SkinManager.ROBOTO_MEDIUM_10).Width, Height)); + } + } + } + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialUpDown.Designer.cs b/framework/MaterialSkin.Core/Controls/MaterialUpDown.Designer.cs new file mode 100644 index 0000000..4b77bd9 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialUpDown.Designer.cs @@ -0,0 +1,103 @@ +using MaterialSkin.Core.Resources; +namespace MaterialSkin.Controls +{ + partial class MaterialUpDown + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.textBox1 = new System.Windows.Forms.TextBox(); + this.materialIcon2 = new MaterialSkin.Controls.MaterialIcon(); + this.materialIcon1 = new MaterialSkin.Controls.MaterialIcon(); + this.SuspendLayout(); + // + // textBox1 + // + this.textBox1.BorderStyle = System.Windows.Forms.BorderStyle.None; + this.textBox1.Font = new System.Drawing.Font("Roboto", 15.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204))); + this.textBox1.Location = new System.Drawing.Point(80, 20); + this.textBox1.Name = "textBox1"; + this.textBox1.Size = new System.Drawing.Size(100, 28); + this.textBox1.TabIndex = 13; + this.textBox1.Text = "0000"; + this.textBox1.TextAlign = System.Windows.Forms.HorizontalAlignment.Center; + this.textBox1.Click += new System.EventHandler(this.textBox1_Click); + this.textBox1.TextChanged += new System.EventHandler(this.textBox1_TextChanged); + this.textBox1.KeyDown += new System.Windows.Forms.KeyEventHandler(this.textBox1_KeyDown); + this.textBox1.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.textBox1_KeyPress); + this.textBox1.Leave += new System.EventHandler(this.textBox1_Leave); + // + // materialIcon2 + // + this.materialIcon2.Clickable = true; + this.materialIcon2.Cursor = System.Windows.Forms.Cursors.Hand; + this.materialIcon2.Image = Resource1.rounded_add_button; + this.materialIcon2.Location = new System.Drawing.Point(200, 0); + this.materialIcon2.Name = "materialIcon2"; + this.materialIcon2.Primary = false; + this.materialIcon2.Size = new System.Drawing.Size(64, 64); + this.materialIcon2.TabIndex = 12; + this.materialIcon2.Text = "materialIcon2"; + this.materialIcon2.Click += new System.EventHandler(this.materialIcon2_Click); + this.materialIcon2.MouseDown += new System.Windows.Forms.MouseEventHandler(this.materialIcon2_MouseDown); + this.materialIcon2.MouseLeave += new System.EventHandler(this.materialIcon2_MouseLeave); + this.materialIcon2.MouseUp += new System.Windows.Forms.MouseEventHandler(this.materialIcon2_MouseUp); + // + // materialIcon1 + // + this.materialIcon1.Clickable = true; + this.materialIcon1.Cursor = System.Windows.Forms.Cursors.Hand; + this.materialIcon1.Image = Resource1.round_delete_button; + this.materialIcon1.Location = new System.Drawing.Point(0, 0); + this.materialIcon1.Name = "materialIcon1"; + this.materialIcon1.Primary = false; + this.materialIcon1.Size = new System.Drawing.Size(64, 64); + this.materialIcon1.TabIndex = 11; + this.materialIcon1.Text = "materialIcon1"; + this.materialIcon1.Click += new System.EventHandler(this.materialIcon1_Click); + this.materialIcon1.MouseDown += new System.Windows.Forms.MouseEventHandler(this.materialIcon1_MouseDown); + this.materialIcon1.MouseLeave += new System.EventHandler(this.materialIcon1_MouseLeave); + this.materialIcon1.MouseUp += new System.Windows.Forms.MouseEventHandler(this.materialIcon1_MouseUp); + // + // MaterialUpDown + // + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; + this.Controls.Add(this.textBox1); + this.Controls.Add(this.materialIcon2); + this.Controls.Add(this.materialIcon1); + this.Name = "MaterialUpDown"; + this.Size = new System.Drawing.Size(266, 66); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + private MaterialIcon materialIcon2; + private MaterialIcon materialIcon1; + private System.Windows.Forms.TextBox textBox1; + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialUpDown.cs b/framework/MaterialSkin.Core/Controls/MaterialUpDown.cs new file mode 100644 index 0000000..6ef5fcf --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialUpDown.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Data; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Security.Principal; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + + +namespace MaterialSkin.Controls +{ + public partial class MaterialUpDown : UserControl,INotifyPropertyChanged + { + private double _value; + private bool _mouseDown; + private int _direction; + + public MaterialUpDown() + { + InitializeComponent(); + textBox1.BackColor = SkinManager.GetApplicationBackgroundColor(); + + AllowDecimal = false; + AllowKeyboard=true; + } + [Browsable(false)] + public MaterialSkinManager SkinManager => MaterialSkinManager.Instance; + + public bool AllowDecimal { get; set; } + + public bool AllowKeyboard + { + get { return _allowKeyboard; } + set + { + _allowKeyboard = value; + textBox1.ReadOnly = !_allowKeyboard || _readOnly; + } + } + + public bool HideControls + { + get { return _hideControls; } + set + { + _hideControls = value; + materialIcon1.Visible = !value; + materialIcon2.Visible = !value; + } + } + + public double Value + { + get { return _value; } + set + { + _value = value; + if (_value>Max) + { + _value = Max; + } + + if (_value < Min) + { + _value = Min; + } + + if (!AllowDecimal) + { + _value = Math.Floor(_value); + } + + Action a = ()=> textBox1.Text = _value.ToString(); + if (textBox1.InvokeRequired) + textBox1.Invoke(a); + else + a(); + } + } + + public bool ReadOnly + { + get { return _readOnly; } + set + { + _readOnly = value; + materialIcon1.Enabled = !value; + materialIcon2.Enabled = !value; + textBox1.ReadOnly = !_allowKeyboard || _readOnly; + } + } + + public event PropertyChangedEventHandler PropertyChanged= delegate { }; + + public double Max { get; set; } + public double Min { get; set; } + public double Step { get; set; } + protected override void OnPaintBackground(PaintEventArgs e) + { + e.Graphics.Clear(SkinManager.GetApplicationBackgroundColor()); + } + + private void materialIcon2_Click(object sender, EventArgs e) + { + + } + + private void materialIcon1_Click(object sender, EventArgs e) + { + + } + + private async void HoldCountDown(CancellationToken cancellationSourceToken) + { + await Task.Delay(500); + + var startTime = DateTime.Now; + while (_mouseDown&&!cancellationSourceToken.IsCancellationRequested) + { + Value += Step*_direction; + await Task.Delay(100/(int)Math.Max(1,Math.Min(5,(DateTime.Now-startTime).TotalSeconds))); + } + } + + private CancellationTokenSource _cancellationSource; + private bool _readOnly; + private bool _allowKeyboard; + + private void materialIcon2_MouseDown(object sender, MouseEventArgs e) + { + + _cancellationSource =new CancellationTokenSource(); + Value += Step; + + _mouseDown = true; + _direction = 1; + Task.Run(()=> HoldCountDown(_cancellationSource.Token)); + } + + private void materialIcon2_MouseUp(object sender, MouseEventArgs e) + { + if (_cancellationSource != null) + { + _cancellationSource.Cancel(); + _cancellationSource = null; + } + _mouseDown = false; + PropertyChanged(this,new PropertyChangedEventArgs(nameof(Value))); + } + + private void materialIcon1_MouseDown(object sender, MouseEventArgs e) + { + _cancellationSource = new CancellationTokenSource(); + Value -= Step; + + _mouseDown = true; + _direction = -1; + Task.Run(() => HoldCountDown(_cancellationSource.Token)); + } + + private void materialIcon1_MouseUp(object sender, MouseEventArgs e) + { + if (_cancellationSource != null) + { + _cancellationSource.Cancel(); + _cancellationSource = null; + } + _mouseDown = false; + PropertyChanged(this, new PropertyChangedEventArgs(nameof(Value))); + } + + private void materialIcon2_MouseLeave(object sender, EventArgs e) + { + materialIcon2_MouseUp(null,null); + } + + private void materialIcon1_MouseLeave(object sender, EventArgs e) + { + materialIcon1_MouseUp(null, null); + } + + private void textBox1_KeyPress(object sender, KeyPressEventArgs e) + { + + + if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar) && + (e.KeyChar != '.')) + { + e.Handled = true; + return; + } + + // only allow one decimal point + if ((e.KeyChar == '.') && ((sender as TextBox).Text.IndexOf('.') > -1)) + { + e.Handled = true; + return; + } + + _validate = true; + } + + + + private CancellationTokenSource _cancellationTokenSource = null; + private bool _validate; + private bool _hideControls; + + private void textBox1_Click(object sender, EventArgs e) + { + + if (textBox1.ReadOnly) return; + textBox1.SelectAll(); + + } + + private void textBox1_TextChanged(object sender, EventArgs e) + { + + } + + private void textBox1_Leave(object sender, EventArgs e) + { + if (double.TryParse((sender as TextBox).Text, NumberStyles.AllowDecimalPoint, CultureInfo.GetCultureInfo("en-us"), out var value)) + { + if (_cancellationTokenSource != null) _cancellationTokenSource.Cancel(); + _cancellationTokenSource = new CancellationTokenSource(); + Task.Run(async () => + { + var newValue = value; + var token = _cancellationTokenSource.Token; + if (token.IsCancellationRequested) throw new OperationCanceledException(); + Value = newValue; + PropertyChanged(this, new PropertyChangedEventArgs(nameof(Value))); + }); + + } + } + + private void textBox1_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Enter) + { + + textBox1_Leave(textBox1,null); + e.Handled = true; + } + } + } +} diff --git a/framework/MaterialSkin.Core/Controls/MaterialUpDown.resx b/framework/MaterialSkin.Core/Controls/MaterialUpDown.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/framework/MaterialSkin.Core/Controls/MaterialUpDown.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/framework/MaterialSkin.Core/DrawHelper.cs b/framework/MaterialSkin.Core/DrawHelper.cs new file mode 100644 index 0000000..c8c9962 --- /dev/null +++ b/framework/MaterialSkin.Core/DrawHelper.cs @@ -0,0 +1,43 @@ +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace MaterialSkin +{ + static class DrawHelper + { + public static GraphicsPath CreateRoundRect(float x, float y, float width, float height, float radius) + { + var gp = new GraphicsPath(); + gp.AddLine(x + radius, y, x + width - (radius * 2), y); + gp.AddArc(x + width - (radius * 2), y, radius * 2, radius * 2, 270, 90); + gp.AddLine(x + width, y + radius, x + width, y + height - (radius * 2)); + gp.AddArc(x + width - (radius * 2), y + height - (radius * 2), radius * 2, radius * 2, 0, 90); + gp.AddLine(x + width - (radius * 2), y + height, x + radius, y + height); + gp.AddArc(x, y + height - (radius * 2), radius * 2, radius * 2, 90, 90); + gp.AddLine(x, y + height - (radius * 2), x, y + radius); + gp.AddArc(x, y, radius * 2, radius * 2, 180, 90); + gp.CloseFigure(); + return gp; + } + + public static GraphicsPath CreateRoundRect(Rectangle rect, float radius) + { + return CreateRoundRect(rect.X, rect.Y, rect.Width, rect.Height, radius); + } + + public static Color BlendColor(Color backgroundColor, Color frontColor, double blend) + { + var ratio = blend / 255d; + var invRatio = 1d - ratio; + var r = (int)((backgroundColor.R * invRatio) + (frontColor.R * ratio)); + var g = (int)((backgroundColor.G * invRatio) + (frontColor.G * ratio)); + var b = (int)((backgroundColor.B * invRatio) + (frontColor.B * ratio)); + return Color.FromArgb(r, g, b); + } + + public static Color BlendColor(Color backgroundColor, Color frontColor) + { + return BlendColor(backgroundColor, frontColor, frontColor.A); + } + } +} diff --git a/framework/MaterialSkin.Core/Gesture/GestureEventArgs.cs b/framework/MaterialSkin.Core/Gesture/GestureEventArgs.cs new file mode 100644 index 0000000..908b15f --- /dev/null +++ b/framework/MaterialSkin.Core/Gesture/GestureEventArgs.cs @@ -0,0 +1,279 @@ +#region The MIT License (MIT) +// +// Copyright (c) 2011 Robert Prouse http://www.alteridem.net +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +#endregion + +#region Using Directives + +using System; +using System.Drawing; + +#endregion + +namespace MaterialSkin.Gesture +{ + #region GestureEventArgs Class + + /// + /// Base class for all the gesture based events + /// + public abstract class GestureEventArgs : EventArgs + { + internal GestureEventArgs( GestureInfo info ) + { + Location = new Point( info.location.x, info.location.y ); + Info = info; + Handled = true; + } + + #region Helper Methods + + protected static int LoDWord( IntPtr lParam ) + { + return LoDWord( lParam.ToInt64() ); + } + + protected static int HiDWord( IntPtr lParam ) + { + return HiDWord( lParam.ToInt64() ); + } + + protected static int LoDWord( long l ) + { + return (int)(l & 0xFFFFFFFF); + } + + protected static int HiDWord( long l ) + { + return (int)((l >> 32) & 0xFFFFFFFF); + } + + protected static short LoWord( int i ) + { + return (short)(i & 0xFFFF); + } + + protected static short HiWord( int i ) + { + return (short)((i >> 16) & 0xFFFF); + } + + #endregion + + #region Properties + + protected GestureInfo Info { get; set; } + + /// + /// Gets the location of the gesture in Screen (not client) coordinates. + /// + public Point Location { get; private set; } + + /// + /// Gets a value indicating whether this is beginning. + /// + public bool Begin + { + get { return Info.Begin; } + } + + /// + /// Gets a value indicating whether this is ending + /// + public bool End + { + get { return Info.End; } + } + + /// + /// Gets or sets a value indicating whether the window message was handled. Set this to false if you don't handle the message. + /// + public bool Handled { get; set; } + + #endregion + } + + #endregion + + #region PanEventArgs Class + + /// + /// Event for the Pan gesture + /// + public class PanEventArgs : GestureEventArgs + { + internal PanEventArgs( GestureInfo info, Point lastPanPoint ) + : base( info ) + { + int hiword = HiDWord( info.arguments ); + InertiaVector = new Point( LoWord( hiword ), HiWord( hiword ) ); + PanOffset = new Point( Location.X - lastPanPoint.X, Location.Y - lastPanPoint.Y ); + } + + /// + /// Gets a value indicating whether this has triggered inertia. + /// + public bool Inertia + { + get { return Info.Inertia; } + } + + public Point InertiaVector { get; private set; } + + /// + /// Gets the pan offset since the last pan message. + /// + public Point PanOffset { get; private set; } + } + + #endregion + + #region ZoomEventArgs Class + + /// + /// Event for the Zoom gesture + /// + public class ZoomEventArgs : GestureEventArgs + { + internal ZoomEventArgs( GestureInfo info, long lastZoomDistance ) + : base( info ) + { + Distance = info.arguments; + PercentChange = (double)Distance / lastZoomDistance; + } + + /// + /// Gets the distance between the two points as they are being zoomed. + /// + public long Distance { get; private set; } + + /// + /// Gets the percent changed since the last zoom message + /// + public double PercentChange { get; private set; } + } + + #endregion + + #region PressAndTapEventArgs Class + + /// + /// Event for the Press and Tap gesture + /// + public class PressAndTapEventArgs : GestureEventArgs + { + internal PressAndTapEventArgs( GestureInfo info ) + : base( info ) + { + int pointsStruct = LoDWord( info.arguments ); + Distance = new Point( LoWord( pointsStruct ), HiWord( pointsStruct ) ); + } + + /// + /// Gets the distance between the two points. + /// + public Point Distance { get; private set; } + } + + #endregion + + #region RotateEventArgs Class + + /// + /// Event for the Rotate gesture + /// + public class RotateEventArgs : GestureEventArgs + { + internal RotateEventArgs( GestureInfo info, double lastRotation ) + : base( info ) + { + int loword = LoDWord( info.arguments ); + TotalAngle = RotateAngleFromArgument( loword ); + Angle = TotalAngle - lastRotation; + string msg = string.Format("Total:{0} Angle:{1} Last:{2}", TotalAngle, Angle, lastRotation ); + System.Diagnostics.Debug.WriteLine( msg ); + } + + /// + /// Gesture argument helper that converts an argument to a rotation angle. + /// + /// The argument to convert. Should be an unsigned 16-bit value. + /// + private static double RotateAngleFromArgument( int arg ) + { + return ((arg / 65535.0) * 4.0 * Math.PI) - 2.0 * Math.PI; + } + + /// + /// Gets the angle of rotation in Radians since the beginning of the gesture + /// + public double TotalAngle { get; private set; } + + /// + /// Gets the angle of rotation in Degrees since the beginning of the gesture + /// + public double TotalDegrees + { + get { return RadiandsToDegrees( TotalAngle ); } + } + + /// + /// Gets the angle of rotation in Radians since the last rotation message + /// + public double Angle { get; private set; } + + /// + /// Gets the angle of rotation in Degrees since the last rotation message + /// + public double Degrees + { + get { return RadiandsToDegrees( Angle ); } + } + + private double RadiandsToDegrees( double radians ) + { + return radians * 180.0 / Math.PI; + } + } + + #endregion + + #region TwoFingerTapEventArgs Class + + /// + /// Base class for the Two Finger Tap gesture + /// + public class TwoFingerTapEventArgs : GestureEventArgs + { + internal TwoFingerTapEventArgs( GestureInfo info ) + : base( info ) + { + Distance = info.arguments; + } + + /// + /// Gets the distance between the two points. + /// + public long Distance { get; private set; } + } + + #endregion +} \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Gesture/GestureListener.cs b/framework/MaterialSkin.Core/Gesture/GestureListener.cs new file mode 100644 index 0000000..8c793c7 --- /dev/null +++ b/framework/MaterialSkin.Core/Gesture/GestureListener.cs @@ -0,0 +1,446 @@ +#region The MIT License (MIT) +// +// Copyright (c) 2011 Robert Prouse http://www.alteridem.net +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +#endregion + +#region Using Directives + +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Security.Permissions; +using System.Threading.Tasks; +using System.Windows.Forms; + +#endregion + +namespace MaterialSkin.Gesture +{ + [PermissionSet( SecurityAction.Demand, Name = "FullTrust" )] + public sealed class GestureListener : NativeWindow + { + #region Private Members + + // Saved state + private Point _lastPanPoint; + private double _lastRotation; + private long _lastZoom; + + private readonly Control _parent; + private readonly GestureConfig[] m_configs; + private int _touchInputSize; + + #endregion + + #region Public Events + + public event EventHandler Pan; + public event EventHandler PressAndTap; + public event EventHandler Rotate; + public event EventHandler TwoFingerTap; + public event EventHandler Zoom; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the class to receive all gestures. + /// + /// The parent. + public GestureListener( Control parent ) + : this( parent, new[] { new GestureConfig( 0, GestureConfigurationFlag.GC_ALLGESTURES, 0 ) } ) + { + } + + /// + /// Initializes a new instance of the class to receive specific gestures. + /// + /// The parent. + /// The gesture configurations. + public GestureListener( Control parent, GestureConfig[] configs ) + { + if ( parent.IsHandleCreated ) + { + Initialize( parent ); + } + else + { + parent.HandleCreated += OnHandleCreated; + } + parent.HandleDestroyed += OnHandleDestroyed; + + _parent = parent; + m_configs = configs; + _touchInputSize = Marshal.SizeOf(new TOUCHINPUT()); + } + + #endregion + + #region Private Methods + [DllImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool RegisterTouchWindow(System.IntPtr hWnd, ulong ulFlags); + private void Initialize( Control parent ) + { + AssignHandle( parent.Handle ); + NativeMethods.SetGestureConfig( parent.Handle, m_configs ); + + } + + public void LoadMultitouch() + { + Task.Run(async () => + { + await Task.Delay(1000); + Action a = () => + { + if (!RegisterTouchWindow(_parent.Handle, 0)) + { + + } + }; + _parent.Invoke(a); + + }); + + } + private void OnHandleCreated( object sender, EventArgs e ) + { + // Window is now created, assign handle to NativeWindow. + var control = sender as Control; + if ( control != null ) + { + Initialize( control ); + } + } + + private void OnHandleDestroyed( object sender, EventArgs e ) + { + // Window was destroyed, release hook. + ReleaseHandle(); + } + + #endregion + + #region WndProc + + /// + /// Invokes the default window procedure associated with this window. + /// + /// A that is associated with the current Windows message. + [PermissionSet( SecurityAction.Demand, Name = "FullTrust" )] + protected override void WndProc( ref Message m ) + { + bool handled = false; + + // Listen for operating system messages + switch ( m.Msg ) + { + case WindowMessage.WM_GESTURE: + GestureInfo info; + if ( NativeMethods.GetGestureInfo( m.LParam, out info ) ) + { + switch ( (GestureId)info.id ) + { + case GestureId.Pan: + handled = OnPan( info ); + break; + case GestureId.PressAndTap: + handled = OnPressAndTap( info ); + break; + case GestureId.Rotate: + handled = OnRotate( info ); + break; + case GestureId.TwoFingerTap: + handled = OnTwoFingerTap( info ); + break; + case GestureId.Zoom: + handled = OnZoom( info ); + break; + } + if ( handled ) + { + NativeMethods.CloseGestureInfoHandle( m.LParam ); + } + } + break; + case WindowMessage.WM_TOUCH: + MessageBox.Show("touch"); + this.DecodeTouch(ref m); + break; + //case WindowMessage.WM_POINTERUPDATE: + // MessageBox.Show("ptr"); + // break; + } + if ( !handled ) + { + base.WndProc( ref m ); + } + } + private static int LoWord(int number) + { + return (number & 0xffff); + } + [StructLayout(LayoutKind.Sequential)] + private struct TOUCHINPUT + { + public int x; + public int y; + public System.IntPtr hSource; + public int dwID; + public int dwFlags; + public int dwMask; + public int dwTime; + public System.IntPtr dwExtraInfo; + public int cxContact; + public int cyContact; + } + + [StructLayout(LayoutKind.Sequential)] + private struct POINTS + { + public short x; + public short y; + } + public event EventHandler Touchdown; // touch down event handler + public event EventHandler Touchup; // touch up event handler + public event EventHandler TouchMove; // touch move event handler + + // EventArgs passed to Touch handlers + public class WMTouchEventArgs : System.EventArgs + { + // Private data members + private int x; // touch x client coordinate in pixels + private int y; // touch y client coordinate in pixels + private int id; // contact ID + private int mask; // mask which fields in the structure are valid + private int flags; // flags + private int time; // touch event time + private int contactX; // x size of the contact area in pixels + private int contactY; // y size of the contact area in pixels + + // Access to data members + public int LocationX + { + get { return x; } + set { x = value; } + } + public int LocationY + { + get { return y; } + set { y = value; } + } + public int Id + { + get { return id; } + set { id = value; } + } + public int Flags + { + get { return flags; } + set { flags = value; } + } + public int Mask + { + get { return mask; } + set { mask = value; } + } + public int Time + { + get { return time; } + set { time = value; } + } + public int ContactX + { + get { return contactX; } + set { contactX = value; } + } + public int ContactY + { + get { return contactY; } + set { contactY = value; } + } + public bool IsPrimaryContact + { + get { return (flags & WindowMessage.TOUCHEVENTF_PRIMARY) != 0; } + } + + // Constructor + public WMTouchEventArgs() + { + } + } + [DllImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetTouchInputInfo(System.IntPtr hTouchInput, int cInputs, [In, Out] TOUCHINPUT[] pInputs, int cbSize); + [DllImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern void CloseTouchInputHandle(System.IntPtr lParam); + public bool DecodeTouch(ref Message m) + { + // More than one touchinput may be associated with a touch message, + // so an array is needed to get all event information. + int inputCount = LoWord(m.WParam.ToInt32()); // Number of touch inputs, actual per-contact messages + + TOUCHINPUT[] inputs; // Array of TOUCHINPUT structures + inputs = new TOUCHINPUT[inputCount]; // Allocate the storage for the parameters of the per-contact messages + + // Unpack message parameters into the array of TOUCHINPUT structures, each + // representing a message for one single contact. + if (!GetTouchInputInfo(m.LParam, inputCount, inputs, _touchInputSize)) + { + // Get touch info failed. + return false; + } + + // For each contact, dispatch the message to the appropriate message + // handler. + bool handled = false; // Boolean, is message handled + for (int i = 0; i < inputCount; i++) + { + TOUCHINPUT ti = inputs[i]; + + // Assign a handler to this message. + EventHandler handler = null; // Touch event handler + if ((ti.dwFlags & WindowMessage.TOUCHEVENTF_DOWN) != 0) + { + handler = Touchdown; + } + else if ((ti.dwFlags & WindowMessage.TOUCHEVENTF_UP) != 0) + { + handler = Touchup; + } + else if ((ti.dwFlags & WindowMessage.TOUCHEVENTF_MOVE) != 0) + { + handler = TouchMove; + } + + // Convert message parameters into touch event arguments and handle the event. + if (handler != null) + { + // Convert the raw touchinput message into a touchevent. + WMTouchEventArgs te = new WMTouchEventArgs(); // Touch event arguments + + // TOUCHINFO point coordinates and contact size is in 1/100 of a pixel; convert it to pixels. + // Also convert screen to client coordinates. + te.ContactY = ti.cyContact / 100; + te.ContactX = ti.cxContact / 100; + te.Id = ti.dwID; + { + Point pt = _parent.PointToClient(new Point(ti.x / 100, ti.y / 100)); + te.LocationX = pt.X; + te.LocationY = pt.Y; + } + te.Time = ti.dwTime; + te.Mask = ti.dwMask; + te.Flags = ti.dwFlags; + + // Invoke the event handler. + handler(this, te); + + // Mark this event as handled. + handled = true; + } + } + + CloseTouchInputHandle(m.LParam); + + return handled; + } + + private bool OnPan( GestureInfo info ) + { + if ( Pan != null ) + { + if ( info.Begin ) + { + _lastPanPoint = new Point( info.location.x, info.location.y ); + } + var args = new PanEventArgs( info, _lastPanPoint ); + _lastPanPoint = new Point( info.location.x, info.location.y ); + Pan( this, args ); + return args.Handled; + } + return false; + } + + private bool OnPressAndTap( GestureInfo info ) + { + if ( PressAndTap != null ) + { + var args = new PressAndTapEventArgs( info ); + PressAndTap( this, args ); + return args.Handled; + } + return false; + } + + private bool OnRotate( GestureInfo info ) + { + if ( Rotate != null ) + { + if ( info.Begin ) + { + _lastRotation = 0; + } + var args = new RotateEventArgs( info, _lastRotation ); + if ( !info.Begin ) + { + // First rotation is the angle the fingers are at, so don't use it + _lastRotation = args.TotalAngle; + } + Rotate( this, args ); + return args.Handled; + } + return false; + } + + private bool OnTwoFingerTap( GestureInfo info ) + { + if ( TwoFingerTap != null ) + { + var args = new TwoFingerTapEventArgs( info ); + TwoFingerTap( this, args ); + return args.Handled; + } + return false; + } + + private bool OnZoom( GestureInfo info ) + { + if ( Zoom != null ) + { + if ( info.Begin ) + { + _lastZoom = info.arguments; + } + var args = new ZoomEventArgs( info, _lastZoom ); + _lastZoom = args.Distance; + Zoom( this, args ); + return args.Handled; + } + return false; + } + + #endregion + } +} diff --git a/framework/MaterialSkin.Core/Gesture/NativeMethods.cs b/framework/MaterialSkin.Core/Gesture/NativeMethods.cs new file mode 100644 index 0000000..828fedd --- /dev/null +++ b/framework/MaterialSkin.Core/Gesture/NativeMethods.cs @@ -0,0 +1,341 @@ +#region The MIT License (MIT) +// +// Copyright (c) 2011 Robert Prouse http://www.alteridem.net +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +#endregion + +#region Using Directives + +using System; +using System.Runtime.InteropServices; + +#endregion + +namespace MaterialSkin.Gesture +{ + internal static class NativeMethods + { + #region Private Delegates + + [UnmanagedFunctionPointer( CallingConvention.Cdecl )] + private delegate bool GetGestureInfoPtr( IntPtr gestureInfoHandle, ref GestureInfo pGestureInfo ); + + [UnmanagedFunctionPointer( CallingConvention.Cdecl )] + private delegate bool CloseGestureInfoHandlePtr( IntPtr gestureInfoHandle ); + + [UnmanagedFunctionPointer( CallingConvention.Cdecl )] + private delegate bool SetGestureConfigPtr( IntPtr hwnd, uint reserved, uint ids, GestureConfig[] configs, uint size ); + + private static readonly GetGestureInfoPtr _pGetGestureInfoPtr; + private static readonly CloseGestureInfoHandlePtr _pCloseGestureInfoHandle; + private static readonly SetGestureConfigPtr _pSetGestureConfig; + + #endregion + + #region Static Construction + + static NativeMethods() + { + var user32 = new UnmanagedLibrary( "user32" ); + _pGetGestureInfoPtr = user32.GetUnmanagedFunction( "GetGestureInfo" ); + _pCloseGestureInfoHandle = user32.GetUnmanagedFunction( "CloseGestureInfoHandle" ); + _pSetGestureConfig = user32.GetUnmanagedFunction( "SetGestureConfig" ); + } + + #endregion + + #region Public Interface + + /// + /// Registers the HWND to receive all gestures. + /// + /// + /// http://msdn.microsoft.com/en-us/library/windows/desktop/dd353231%28v=vs.85%29.aspx + /// + /// The HWND. + /// + public static bool SetGestureConfig( IntPtr hwnd ) + { + if ( _pSetGestureConfig == null ) + return false; + + var configs = new[] { new GestureConfig( 0, GestureConfigurationFlag.GC_ALLGESTURES, 0 ) }; + return SetGestureConfig(hwnd, configs); + } + + /// + /// Registers the HWND to receive specific gestures. + /// + /// + /// http://msdn.microsoft.com/en-us/library/windows/desktop/dd353231%28v=vs.85%29.aspx + /// + /// The HWND. + /// The gesture configurations + /// + public static bool SetGestureConfig( IntPtr hwnd, GestureConfig[] configs ) + { + return _pSetGestureConfig( hwnd, 0, 1, configs, (uint)Marshal.SizeOf( typeof( GestureConfig ) ) ); + } + + /// + /// Gets the gesture info. + /// + /// The gesture info handle. + /// The gesture info. + /// + public static bool GetGestureInfo( IntPtr gestureInfoHandle, out GestureInfo gestureInfo ) + { + gestureInfo = new GestureInfo(); + + if ( _pGetGestureInfoPtr == null ) + return false; + + gestureInfo.size = Marshal.SizeOf( gestureInfo ); + return _pGetGestureInfoPtr( gestureInfoHandle, ref gestureInfo ); + } + + /// + /// Closes the gesture info handle. + /// + /// The gesture info handle. + /// + public static bool CloseGestureInfoHandle( IntPtr gestureInfoHandle ) + { + if ( _pCloseGestureInfoHandle == null ) + return false; + + return _pCloseGestureInfoHandle( gestureInfoHandle ); + } + + #endregion + } + + /// + /// Gesture configuration flags + /// + internal static class GestureConfigurationFlag + { + public const int GC_ALLGESTURES = 0x00000001; + public const int GC_ZOOM = 0x00000001; + public const int GC_PAN = 0x00000001; + public const int GC_PAN_WITH_SINGLE_FINGER_VERTICALLY = 0x00000002; + public const int GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY = 0x00000004; + public const int GC_PAN_WITH_GUTTER = 0x00000008; + public const int GC_PAN_WITH_INERTIA = 0x00000010; + public const int GC_ROTATE = 0x00000001; + public const int GC_TWOFINGERTAP = 0x00000001; + public const int GC_PRESSANDTAP = 0x00000001; + } + + /// + /// Gesture flags - GestureInfo.flags + /// + [Flags] + public enum GestureFlags + { + /// + /// GF_BEGIN + /// + Begin = 0x1, + /// + /// GF_INERTIA + /// + Inertia = 0x2, + /// + /// GF_END + /// + End = 0x4 + } + + /// + /// Gesture IDs - GestureInfo.id + /// + public enum GestureId + { + /// + /// GID_BEGIN + /// + Begin = 1, + /// + /// GID_END + /// + End = 2, + /// + /// GID_ZOOM + /// + Zoom = 3, + /// + /// GID_PAN + /// + Pan = 4, + /// + /// GID_ROTATE + /// + Rotate = 5, + /// + /// GID_TWOFINGERTAP + /// + TwoFingerTap = 6, + /// + /// GID_PRESSANDTAP + /// + PressAndTap = 7 + } + + /// + /// Window Messages + /// + internal static class WindowMessage + { + public const int WM_TOUCH = 0x0240; + public const int WM_POINTERUPDATE =0x0245; + public const int WM_GESTURE = 0x0119; + public const int WM_GESTURENOTIFY = 0x011A; + public const int TOUCHEVENTF_MOVE = 0x0001; + public const int TOUCHEVENTF_DOWN = 0x0002; + public const int TOUCHEVENTF_UP = 0x0004; + public const int TOUCHEVENTF_INRANGE = 0x0008; + public const int TOUCHEVENTF_PRIMARY = 0x0010; + public const int TOUCHEVENTF_NOCOALESCE = 0x0020; + public const int TOUCHEVENTF_PEN = 0x0040; + + // Touch input mask values (TOUCHINPUT.dwMask) [winuser.h] + public const int TOUCHINPUTMASKF_TIMEFROMSYSTEM = 0x0001; // the dwTime field contains a system generated value + public const int TOUCHINPUTMASKF_EXTRAINFO = 0x0002; // the dwExtraInfo field is valid + public const int TOUCHINPUTMASKF_CONTACTAREA = 0x0004; // the cxContact and cyContact fields are valid + + + } + + [StructLayout( LayoutKind.Sequential )] + public struct Points + { + public short x; + public short y; + } + + /// + /// Gesture configuration structure + /// + /// + /// Used in SetGestureConfig and GetGestureConfig + /// http://msdn.microsoft.com/en-us/library/windows/desktop/dd353231%28v=vs.85%29.aspx + /// + [StructLayout( LayoutKind.Sequential )] + public struct GestureConfig + { + public GestureConfig( uint id, uint want, uint block ) + { + Id = id; + Want = want; + Block = block; + } + + /// + /// The identifier for the type of configuration that will have messages enabled or disabled. + /// + public uint Id; + + /// + /// The messages to enable. + /// + public uint Want; + + /// + /// The messages to disable. + /// + public uint Block; + } + + /// + /// Stores information about a gesture. + /// + /// + /// - Pass the HGESTUREINFO received in the WM_GESTURE message lParam into the + /// GetGestureInfo function to retrieve this information. + /// - If cbExtraArgs is non-zero, pass the HGESTUREINFO received in the WM_GESTURE + /// message lParam into the GetGestureExtraArgs function to retrieve extended + /// argument information. + /// + [StructLayout( LayoutKind.Sequential )] + public struct GestureInfo + { + /// + /// The size of the structure, in bytes. The caller must set this. + /// + public int size; + /// + /// The state of the gesture. Contains GestureFlags. + /// + public int flags; + /// + /// The identifier of the gesture command. Contains GestureId. + /// + public int id; + /// + /// A handle to the window that is targeted by this gesture. + /// + public IntPtr hwnd; + /// + /// A Points structure containing the coordinates associated with the gesture. These coordinates are always relative to the origin of the screen. + /// + public Points location; + /// + /// An internally used identifier for the structure. + /// + public int instanceId; + /// + /// An internally used identifier for the sequence. + /// + public int sequenceId; + /// + /// A 64-bit unsigned integer that contains the arguments for gestures that fit into 8 bytes. + /// + public Int64 arguments; + /// + /// The size, in bytes, of extra arguments that accompany this gesture. + /// + public int extraArguments; + + /// + /// Gets a value indicating whether the Begin is set. + /// + public bool Begin + { + get { return ((GestureFlags)flags & GestureFlags.Begin) == GestureFlags.Begin; } + } + + /// + /// Gets a value indicating whether the End is set. + /// + public bool End + { + get { return ((GestureFlags)flags & GestureFlags.End) == GestureFlags.End; } + } + + /// + /// Gets a value indicating whether the Inertia is set + /// + public bool Inertia + { + get { return ((GestureFlags)flags & GestureFlags.Inertia) == GestureFlags.Inertia; } + } + } +} diff --git a/framework/MaterialSkin.Core/Gesture/UnmanagedLibrary.cs b/framework/MaterialSkin.Core/Gesture/UnmanagedLibrary.cs new file mode 100644 index 0000000..e344efc --- /dev/null +++ b/framework/MaterialSkin.Core/Gesture/UnmanagedLibrary.cs @@ -0,0 +1,142 @@ +#region Copyright (c) Microsoft Corporation. +// +// The source code of UnmanagedLibrary.cs is quoted from Mike Stall's article: +// +// Type-safe Managed wrappers for kernel32!GetProcAddress +// http://blogs.msdn.com/jmstall/archive/2007/01/06/Typesafe-GetProcAddress.aspx +// +// This source is subject to the Microsoft Public License. +// See http://www.opensource.org/licenses/MS-PL. +// All other rights reserved. +// +// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +// +#endregion + +#region Using Directives + +using System; +using System.IO; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Security.Permissions; +using Microsoft.Win32.SafeHandles; + +#endregion + +namespace MaterialSkin.Gesture +{ + /// + /// Utility class to wrap an unmanaged DLL and be responsible for freeing it. + /// + /// This is a managed wrapper over the native LoadLibrary, GetProcAddress, and + /// FreeLibrary calls. + /// + public sealed class UnmanagedLibrary : IDisposable + { + #region Safe Handles and Native imports + + // See http://msdn.microsoft.com/msdnmag/issues/05/10/Reliability/ for more about safe handles. + [SecurityPermission( SecurityAction.LinkDemand, UnmanagedCode = true )] + sealed class SafeLibraryHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private SafeLibraryHandle() : base( true ) { } + + protected override bool ReleaseHandle() + { + return NativeMethods.FreeLibrary( handle ); + } + } + + static class NativeMethods + { + const string KERNEL32 = "kernel32"; + [DllImport( KERNEL32, CharSet = CharSet.Auto, BestFitMapping = false, SetLastError = true )] + public static extern SafeLibraryHandle LoadLibrary( string fileName ); + + [ReliabilityContract( Consistency.WillNotCorruptState, Cer.Success )] + [DllImport( KERNEL32, SetLastError = true )] + [return: MarshalAs( UnmanagedType.Bool )] + public static extern bool FreeLibrary( IntPtr hModule ); + + [DllImport( KERNEL32 )] + public static extern IntPtr GetProcAddress( SafeLibraryHandle hModule, String procname ); + } + + #endregion // Safe Handles and Native imports + + /// + /// Constructor to load a dll and be responible for freeing it. + /// + /// full path name of dll to load + /// if fileName can't be found + /// Throws exceptions on failure. Most common failure would be file-not-found, or + /// that the file is not a loadable image. + public UnmanagedLibrary( string fileName ) + { + m_hLibrary = NativeMethods.LoadLibrary( fileName ); + if ( m_hLibrary.IsInvalid ) + { + int hr = Marshal.GetHRForLastWin32Error(); + Marshal.ThrowExceptionForHR( hr ); + } + } + + /// + /// Dynamically lookup a function in the dll via kernel32!GetProcAddress. + /// + /// raw name of the function in the export table. + /// null if function is not found. Else a delegate to the unmanaged function. + /// + /// GetProcAddress results are valid as long as the dll is not yet unloaded. This + /// is very very dangerous to use since you need to ensure that the dll is not unloaded + /// until after you're done with any objects implemented by the dll. For example, if you + /// get a delegate that then gets an IUnknown implemented by this dll, + /// you can not dispose this library until that IUnknown is collected. Else, you may free + /// the library and then the CLR may call release on that IUnknown and it will crash. + public TDelegate GetUnmanagedFunction( string functionName ) where TDelegate : class + { + IntPtr p = NativeMethods.GetProcAddress( m_hLibrary, functionName ); + + // Failure is a common case, especially for adaptive code. + if ( p == IntPtr.Zero ) + { + return null; + } + Delegate function = Marshal.GetDelegateForFunctionPointer( p, typeof( TDelegate ) ); + + // Ideally, we'd just make the constraint on TDelegate be + // System.Delegate, but compiler error CS0702 (constrained can't be System.Delegate) + // prevents that. So we make the constraint system.object and do the cast from object-->TDelegate. + object o = function; + + return (TDelegate)o; + } + + #region IDisposable Members + /// + /// Call FreeLibrary on the unmanaged dll. All function pointers + /// handed out from this class become invalid after this. + /// + /// This is very dangerous because it suddenly invalidate + /// everything retrieved from this dll. This includes any functions + /// handed out via GetProcAddress, and potentially any objects returned + /// from those functions (which may have an implemention in the + /// dll). + /// + public void Dispose() + { + if ( !m_hLibrary.IsClosed ) + { + m_hLibrary.Close(); + } + } + + // Unmanaged resource. CLR will ensure SafeHandles get freed, without requiring a finalizer on this class. + SafeLibraryHandle m_hLibrary; + + #endregion + } +} \ No newline at end of file diff --git a/framework/MaterialSkin.Core/IMaterialControl.cs b/framework/MaterialSkin.Core/IMaterialControl.cs new file mode 100644 index 0000000..f5d8c79 --- /dev/null +++ b/framework/MaterialSkin.Core/IMaterialControl.cs @@ -0,0 +1,17 @@ +namespace MaterialSkin +{ + public interface IMaterialControl + { + int Depth { get; set; } + MaterialSkinManager SkinManager { get; } + MouseState MouseState { get; set; } + + } + + public enum MouseState + { + HOVER, + DOWN, + OUT + } +} diff --git a/framework/MaterialSkin.Core/MaterialSkin.Core.csproj b/framework/MaterialSkin.Core/MaterialSkin.Core.csproj new file mode 100644 index 0000000..a220d4b --- /dev/null +++ b/framework/MaterialSkin.Core/MaterialSkin.Core.csproj @@ -0,0 +1,117 @@ + + + + net8.0-windows + true + + + + + + + + + + + + + + + Component + + + Component + + + Component + + + Component + + + Component + + + Form + + + Component + + + UserControl + + + MaterialIndicator.cs + + + Component + + + Component + + + Form + + + MaterialLoader.cs + + + Form + + + MaterialMessageBox.cs + + + Component + + + Component + + + Component + + + Component + + + Component + + + Component + + + Component + + + UserControl + + + MaterialUpDown.cs + + + True + True + Resource1.resx + + + + + + MaterialIndicator.cs + + + MaterialLoader.cs + + + MaterialMessageBox.cs + + + MaterialUpDown.cs + + + ResXFileCodeGenerator + Resource1.Designer.cs + + + + diff --git a/framework/MaterialSkin.Core/MaterialSkinManager.cs b/framework/MaterialSkin.Core/MaterialSkinManager.cs new file mode 100644 index 0000000..16bb553 --- /dev/null +++ b/framework/MaterialSkin.Core/MaterialSkinManager.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Text; +using System.Linq; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using MaterialSkin.Controls; +using MaterialSkin.Core.Resources; + + +namespace MaterialSkin +{ + public class MaterialSkinManager + { + public static void ConfigureForInspectron() + { + Instance.ColorScheme = new ColorScheme(Primary.Red800, Primary.Red800, Primary.Red400, Accent.Red200, TextShade.BLACK); + + } + + //Singleton instance + private static MaterialSkinManager _instance; + + //Forms to control + private readonly List _formsToManage = new List(); + + //Theme + private Themes _theme; + public Themes Theme + { + get { return _theme; } + set + { + _theme = value; + UpdateBackgrounds(); + } + } + + private ColorScheme _colorScheme; + public ColorScheme ColorScheme + { + get { return _colorScheme; } + set + { + _colorScheme = value; + UpdateBackgrounds(); + } + } + + public enum Themes : byte + { + LIGHT, + DARK + } + + //Constant color values + private static readonly Color PRIMARY_TEXT_BLACK = Color.FromArgb(222, 0, 0, 0); + private static readonly Brush PRIMARY_TEXT_BLACK_BRUSH = new SolidBrush(PRIMARY_TEXT_BLACK); + public static Color SECONDARY_TEXT_BLACK = Color.FromArgb(138, 0, 0, 0); + public static Brush SECONDARY_TEXT_BLACK_BRUSH = new SolidBrush(SECONDARY_TEXT_BLACK); + private static readonly Color DISABLED_OR_HINT_TEXT_BLACK = Color.FromArgb(66, 0, 0, 0); + private static readonly Brush DISABLED_OR_HINT_TEXT_BLACK_BRUSH = new SolidBrush(DISABLED_OR_HINT_TEXT_BLACK); + private static readonly Color DIVIDERS_BLACK = Color.FromArgb(31, 0, 0, 0); + private static readonly Brush DIVIDERS_BLACK_BRUSH = new SolidBrush(DIVIDERS_BLACK); + + private static readonly Color PRIMARY_TEXT_WHITE = Color.FromArgb(255, 255, 255, 255); + private static readonly Brush PRIMARY_TEXT_WHITE_BRUSH = new SolidBrush(PRIMARY_TEXT_WHITE); + public static Color SECONDARY_TEXT_WHITE = Color.FromArgb(179, 255, 255, 255); + public static Brush SECONDARY_TEXT_WHITE_BRUSH = new SolidBrush(SECONDARY_TEXT_WHITE); + private static readonly Color DISABLED_OR_HINT_TEXT_WHITE = Color.FromArgb(77, 255, 255, 255); + private static readonly Brush DISABLED_OR_HINT_TEXT_WHITE_BRUSH = new SolidBrush(DISABLED_OR_HINT_TEXT_WHITE); + private static readonly Color DIVIDERS_WHITE = Color.FromArgb(31, 255, 255, 255); + private static readonly Brush DIVIDERS_WHITE_BRUSH = new SolidBrush(DIVIDERS_WHITE); + + // Checkbox colors + private static readonly Color CHECKBOX_OFF_LIGHT = Color.FromArgb(138, 0, 0, 0); + private static readonly Brush CHECKBOX_OFF_LIGHT_BRUSH = new SolidBrush(CHECKBOX_OFF_LIGHT); + private static readonly Color CHECKBOX_OFF_DISABLED_LIGHT = Color.FromArgb(66, 0, 0, 0); + private static readonly Brush CHECKBOX_OFF_DISABLED_LIGHT_BRUSH = new SolidBrush(CHECKBOX_OFF_DISABLED_LIGHT); + + private static readonly Color CHECKBOX_OFF_DARK = Color.FromArgb(179, 255, 255, 255); + private static readonly Brush CHECKBOX_OFF_DARK_BRUSH = new SolidBrush(CHECKBOX_OFF_DARK); + private static readonly Color CHECKBOX_OFF_DISABLED_DARK = Color.FromArgb(77, 255, 255, 255); + private static readonly Brush CHECKBOX_OFF_DISABLED_DARK_BRUSH = new SolidBrush(CHECKBOX_OFF_DISABLED_DARK); + + //Raised button + private static readonly Color RAISED_BUTTON_BACKGROUND = Color.FromArgb(255, 255, 255, 255); + private static readonly Brush RAISED_BUTTON_BACKGROUND_BRUSH = new SolidBrush(RAISED_BUTTON_BACKGROUND); + private static readonly Color RAISED_BUTTON_TEXT_LIGHT = PRIMARY_TEXT_WHITE; + private static readonly Brush RAISED_BUTTON_TEXT_LIGHT_BRUSH = new SolidBrush(RAISED_BUTTON_TEXT_LIGHT); + private static readonly Color RAISED_BUTTON_TEXT_DARK = PRIMARY_TEXT_BLACK; + private static readonly Brush RAISED_BUTTON_TEXT_DARK_BRUSH = new SolidBrush(RAISED_BUTTON_TEXT_DARK); + + //Flat button + private static readonly Color FLAT_BUTTON_BACKGROUND_HOVER_LIGHT = Color.FromArgb(20.PercentageToColorComponent(), 0x999999.ToColor()); + private static readonly Brush FLAT_BUTTON_BACKGROUND_HOVER_LIGHT_BRUSH = new SolidBrush(FLAT_BUTTON_BACKGROUND_HOVER_LIGHT); + private static readonly Color FLAT_BUTTON_BACKGROUND_PRESSED_LIGHT = Color.FromArgb(40.PercentageToColorComponent(), 0x999999.ToColor()); + private static readonly Brush FLAT_BUTTON_BACKGROUND_PRESSED_LIGHT_BRUSH = new SolidBrush(FLAT_BUTTON_BACKGROUND_PRESSED_LIGHT); + private static readonly Color FLAT_BUTTON_DISABLEDTEXT_LIGHT = Color.FromArgb(26.PercentageToColorComponent(), 0x000000.ToColor()); + private static readonly Brush FLAT_BUTTON_DISABLEDTEXT_LIGHT_BRUSH = new SolidBrush(FLAT_BUTTON_DISABLEDTEXT_LIGHT); + + private static readonly Color FLAT_BUTTON_BACKGROUND_HOVER_DARK = Color.FromArgb(15.PercentageToColorComponent(), 0xCCCCCC.ToColor()); + private static readonly Brush FLAT_BUTTON_BACKGROUND_HOVER_DARK_BRUSH = new SolidBrush(FLAT_BUTTON_BACKGROUND_HOVER_DARK); + private static readonly Color FLAT_BUTTON_BACKGROUND_PRESSED_DARK = Color.FromArgb(25.PercentageToColorComponent(), 0xCCCCCC.ToColor()); + private static readonly Brush FLAT_BUTTON_BACKGROUND_PRESSED_DARK_BRUSH = new SolidBrush(FLAT_BUTTON_BACKGROUND_PRESSED_DARK); + private static readonly Color FLAT_BUTTON_DISABLEDTEXT_DARK = Color.FromArgb(30.PercentageToColorComponent(), 0xFFFFFF.ToColor()); + private static readonly Brush FLAT_BUTTON_DISABLEDTEXT_DARK_BRUSH = new SolidBrush(FLAT_BUTTON_DISABLEDTEXT_DARK); + + //ContextMenuStrip + private static readonly Color CMS_BACKGROUND_LIGHT_HOVER = Color.FromArgb(255, 238, 238, 238); + private static readonly Brush CMS_BACKGROUND_HOVER_LIGHT_BRUSH = new SolidBrush(CMS_BACKGROUND_LIGHT_HOVER); + + private static readonly Color CMS_BACKGROUND_DARK_HOVER = Color.FromArgb(38, 204, 204, 204); + private static readonly Brush CMS_BACKGROUND_HOVER_DARK_BRUSH = new SolidBrush(CMS_BACKGROUND_DARK_HOVER); + + //Application background + private static readonly Color BACKGROUND_LIGHT = Color.FromArgb(255, 255, 255, 255); + private static Brush BACKGROUND_LIGHT_BRUSH = new SolidBrush(BACKGROUND_LIGHT); + + private static readonly Color BACKGROUND_DARK = Color.FromArgb(255, 51, 51, 51); + private static Brush BACKGROUND_DARK_BRUSH = new SolidBrush(BACKGROUND_DARK); + + //Application action bar + public readonly Color ACTION_BAR_TEXT = Color.FromArgb(255, 255, 255, 255); + public readonly Brush ACTION_BAR_TEXT_BRUSH = new SolidBrush(Color.FromArgb(255, 255, 255, 255)); + public readonly Color ACTION_BAR_TEXT_SECONDARY = Color.FromArgb(153, 255, 255, 255); + public readonly Brush ACTION_BAR_TEXT_SECONDARY_BRUSH = new SolidBrush(Color.FromArgb(153, 255, 255, 255)); + + public Color GetPrimaryTextColor() + { + return Theme == Themes.LIGHT ? PRIMARY_TEXT_BLACK : PRIMARY_TEXT_WHITE; + } + + public Brush GetPrimaryTextBrush() + { + return Theme == Themes.LIGHT ? PRIMARY_TEXT_BLACK_BRUSH : PRIMARY_TEXT_WHITE_BRUSH; + } + + public Color GetSecondaryTextColor() + { + return Theme == Themes.LIGHT ? SECONDARY_TEXT_BLACK : SECONDARY_TEXT_WHITE; + } + + public Brush GetSecondaryTextBrush() + { + return Theme == Themes.LIGHT ? SECONDARY_TEXT_BLACK_BRUSH : SECONDARY_TEXT_WHITE_BRUSH; + } + + public Color GetDisabledOrHintColor() + { + return Theme == Themes.LIGHT ? DISABLED_OR_HINT_TEXT_BLACK : DISABLED_OR_HINT_TEXT_WHITE; + } + + public Brush GetDisabledOrHintBrush() + { + return Theme == Themes.LIGHT ? DISABLED_OR_HINT_TEXT_BLACK_BRUSH : DISABLED_OR_HINT_TEXT_WHITE_BRUSH; + } + + public Color GetDividersColor() + { + return Theme == Themes.LIGHT ? DIVIDERS_BLACK : DIVIDERS_WHITE; + } + + public Brush GetDividersBrush() + { + return Theme == Themes.LIGHT ? DIVIDERS_BLACK_BRUSH : DIVIDERS_WHITE_BRUSH; + } + + public Color GetCheckboxOffColor() + { + return Theme == Themes.LIGHT ? CHECKBOX_OFF_LIGHT : CHECKBOX_OFF_DARK; + } + + public Brush GetCheckboxOffBrush() + { + return Theme == Themes.LIGHT ? CHECKBOX_OFF_LIGHT_BRUSH : CHECKBOX_OFF_DARK_BRUSH; + } + + public Color GetCheckBoxOffDisabledColor() + { + return Theme == Themes.LIGHT ? CHECKBOX_OFF_DISABLED_LIGHT : CHECKBOX_OFF_DISABLED_DARK; + } + + public Brush GetCheckBoxOffDisabledBrush() + { + return Theme == Themes.LIGHT ? CHECKBOX_OFF_DISABLED_LIGHT_BRUSH : CHECKBOX_OFF_DISABLED_DARK_BRUSH; + } + + public Brush GetRaisedButtonBackgroundBrush() + { + return RAISED_BUTTON_BACKGROUND_BRUSH; + } + + public Brush GetRaisedButtonTextBrush(bool primary) + { + return primary ? RAISED_BUTTON_TEXT_LIGHT_BRUSH : RAISED_BUTTON_TEXT_DARK_BRUSH; + } + + public Color GetFlatButtonHoverBackgroundColor() + { + return Theme == Themes.LIGHT ? FLAT_BUTTON_BACKGROUND_HOVER_LIGHT : FLAT_BUTTON_BACKGROUND_HOVER_DARK; + } + + public Brush GetFlatButtonHoverBackgroundBrush() + { + return Theme == Themes.LIGHT ? FLAT_BUTTON_BACKGROUND_HOVER_LIGHT_BRUSH : FLAT_BUTTON_BACKGROUND_HOVER_DARK_BRUSH; + } + + public Color GetFlatButtonPressedBackgroundColor() + { + return Theme == Themes.LIGHT ? FLAT_BUTTON_BACKGROUND_PRESSED_LIGHT : FLAT_BUTTON_BACKGROUND_PRESSED_DARK; + } + + public Brush GetFlatButtonPressedBackgroundBrush() + { + return Theme == Themes.LIGHT ? FLAT_BUTTON_BACKGROUND_PRESSED_LIGHT_BRUSH : FLAT_BUTTON_BACKGROUND_PRESSED_DARK_BRUSH; + } + + public Brush GetFlatButtonDisabledTextBrush() + { + return Theme == Themes.LIGHT ? FLAT_BUTTON_DISABLEDTEXT_LIGHT_BRUSH : FLAT_BUTTON_DISABLEDTEXT_DARK_BRUSH; + } + + public Brush GetCmsSelectedItemBrush() + { + return Theme == Themes.LIGHT ? CMS_BACKGROUND_HOVER_LIGHT_BRUSH : CMS_BACKGROUND_HOVER_DARK_BRUSH; + } + + public Color GetApplicationBackgroundColor() + { + return Theme == Themes.LIGHT ? BACKGROUND_LIGHT : BACKGROUND_DARK; + } + + //Roboto font + public Font ROBOTO_MEDIUM_12; + public Font ROBOTO_REGULAR_11; + public Font ROBOTO_MEDIUM_11; + public Font ROBOTO_MEDIUM_10; + + //Other constants + public int FORM_PADDING = 14; + + [DllImport("gdi32.dll")] + private static extern IntPtr AddFontMemResourceEx(IntPtr pbFont, uint cbFont, IntPtr pvd, [In] ref uint pcFonts); + + private MaterialSkinManager() + { + ROBOTO_MEDIUM_12 = new Font(LoadFont(Resource1.Roboto_Medium), 12f); + ROBOTO_MEDIUM_10 = new Font(LoadFont(Resource1.Roboto_Medium), 10f); + ROBOTO_REGULAR_11 = new Font(LoadFont(Resource1.Roboto_Regular), 11f); + ROBOTO_MEDIUM_11 = new Font(LoadFont(Resource1.Roboto_Medium), 11f); + Theme = Themes.LIGHT; + ColorScheme = new ColorScheme(Primary.BlueGrey800, Primary.BlueGrey900, Primary.BlueGrey500, Accent.LightBlue200, TextShade.WHITE); + } + + public static MaterialSkinManager Instance => _instance ?? (_instance = new MaterialSkinManager()); + + public void AddFormToManage(MaterialForm materialForm) + { + _formsToManage.Add(materialForm); + UpdateBackgrounds(); + } + + public void RemoveFormToManage(MaterialForm materialForm) + { + _formsToManage.Remove(materialForm); + } + + private readonly PrivateFontCollection privateFontCollection = new PrivateFontCollection(); + + private FontFamily LoadFont(byte[] fontResource) + { + int dataLength = fontResource.Length; + IntPtr fontPtr = Marshal.AllocCoTaskMem(dataLength); + Marshal.Copy(fontResource, 0, fontPtr, dataLength); + + uint cFonts = 0; + AddFontMemResourceEx(fontPtr, (uint)fontResource.Length, IntPtr.Zero, ref cFonts); + privateFontCollection.AddMemoryFont(fontPtr, dataLength); + + return privateFontCollection.Families.Last(); + } + + private void UpdateBackgrounds() + { + var newBackColor = GetApplicationBackgroundColor(); + foreach (var materialForm in _formsToManage) + { + materialForm.BackColor = newBackColor; + UpdateControl(materialForm, newBackColor); + } + } + + private void UpdateToolStrip(ToolStrip toolStrip, Color newBackColor) + { + if (toolStrip == null) return; + + toolStrip.BackColor = newBackColor; + foreach (ToolStripItem control in toolStrip.Items) + { + control.BackColor = newBackColor; + if (control is MaterialToolStripMenuItem && (control as MaterialToolStripMenuItem).HasDropDown) + { + + //recursive call + UpdateToolStrip((control as MaterialToolStripMenuItem).DropDown, newBackColor); + } + } + } + + private void UpdateControl(Control controlToUpdate, Color newBackColor) + { + if (controlToUpdate == null) return; + + if (controlToUpdate.ContextMenuStrip != null) + { + UpdateToolStrip(controlToUpdate.ContextMenuStrip, newBackColor); + } + var tabControl = controlToUpdate as MaterialTabControl; + if (tabControl != null) + { + foreach (TabPage tabPage in tabControl.TabPages) + { + tabPage.BackColor = newBackColor; + } + } + + if (controlToUpdate is MaterialDivider) + { + controlToUpdate.BackColor = GetDividersColor(); + } + + if (controlToUpdate is MaterialListView) + { + controlToUpdate.BackColor = newBackColor; + + } + + //recursive call + foreach (Control control in controlToUpdate.Controls) + { + UpdateControl(control, newBackColor); + } + + controlToUpdate.Invalidate(); + } + } +} diff --git a/framework/MaterialSkin.Core/Resources/Resource1.Designer.cs b/framework/MaterialSkin.Core/Resources/Resource1.Designer.cs new file mode 100644 index 0000000..8f2cb79 --- /dev/null +++ b/framework/MaterialSkin.Core/Resources/Resource1.Designer.cs @@ -0,0 +1,113 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MaterialSkin.Core.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource1 { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource1() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MaterialSkin.Core.Resources.Resource1", typeof(Resource1).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap drop_down_arrow { + get { + object obj = ResourceManager.GetObject("drop_down_arrow", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] Roboto_Medium { + get { + object obj = ResourceManager.GetObject("Roboto_Medium", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] Roboto_Regular { + get { + object obj = ResourceManager.GetObject("Roboto_Regular", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap round_delete_button { + get { + object obj = ResourceManager.GetObject("round_delete_button", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap rounded_add_button { + get { + object obj = ResourceManager.GetObject("rounded_add_button", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/framework/MaterialSkin.Core/Resources/Resource1.resx b/framework/MaterialSkin.Core/Resources/Resource1.resx new file mode 100644 index 0000000..2d637be --- /dev/null +++ b/framework/MaterialSkin.Core/Resources/Resource1.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + drop-down-arrow.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + roboto-medium.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + roboto-regular.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + rounded-add-button.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + round-delete-button.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Resources/Roboto-Medium.ttf b/framework/MaterialSkin.Core/Resources/Roboto-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a3c1a1f1702ebf8f5771ae96197539ddc507f04a GIT binary patch literal 127488 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

9AxHZ~IbT3h=$85=u+hltSGXC~N5kVn{GK7RUy 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 }9naMW3e6&}Zp3`W)R(pQk(M3v{RU3Vo5jL|>-6 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 H=d=F@iuu`M literal 0 HcmV?d00001 diff --git a/framework/MaterialSkin.Core/Resources/Roboto-Regular.ttf b/framework/MaterialSkin.Core/Resources/Roboto-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0e58508a64bf413a58c24bf60a0e8a90d663f304 GIT binary patch literal 126072 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 G5dQ-=cGtK7 literal 0 HcmV?d00001 diff --git a/framework/MaterialSkin.Core/Resources/drop-down-arrow.png b/framework/MaterialSkin.Core/Resources/drop-down-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..ebdaa052d498335111901bd86973a492492121b9 GIT binary patch literal 478 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 literal 0 HcmV?d00001