feat: v0.5.0 feature round - measurement, view, tree, section, export upgrades
Measurement / interaction: - Circle-center snap in Snap(): click near a circular edge to snap to its center (hole-to-hole pitch measuring); nearest of vertex vs circle wins - Esc cancels pending multi-step measurement, then exits mode, then gizmo; hotkeys P/D/E/F/C/A/M toggle measure modes (ignored while typing) - Volume/centroid via mesh signed volume (open-shell detection rejects unreliable results); tree context menu adds it as a measurement View / UI: - Standard views (Iso/Front/Top/Right, Z-up) + orthographic toggle; gizmo overlay camera now switches type to match main camera - GridSplitters for tree/measurement panels; node tooltip shows AABB LxWxH - Tree name filter (matches + ancestors + subtree, auto-expand); context menu: isolate node / invert visibility / show all - Settings persistence (window bounds, unit, 10-file MRU) in %LOCALAPPDATA%/STPViewer/settings.json; recent-files dropdown Files / section: - Interference accepts >=2 visible files, checks all pairs - Export STEP: visible Solids/Shells at current aligned positions via CADability ExportStep; SmokeTest --export-test round-trip verifies - Custom section plane by picking 3 points (axis combo 3-pt); plane position generalized to AABB-corner projection; numeric position input - Bump version to 0.5.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+133
-79
@@ -7,7 +7,8 @@
|
|||||||
Title="STPViewer — STP/STEP 3D 檢視器"
|
Title="STPViewer — STP/STEP 3D 檢視器"
|
||||||
Height="820" Width="1320"
|
Height="820" Width="1320"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
AllowDrop="True" Drop="Window_Drop" DragOver="Window_DragOver">
|
AllowDrop="True" Drop="Window_Drop" DragOver="Window_DragOver"
|
||||||
|
PreviewKeyDown="Window_PreviewKeyDown">
|
||||||
|
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<conv:EnumToBoolConverter x:Key="ModeConv" />
|
<conv:EnumToBoolConverter x:Key="ModeConv" />
|
||||||
@@ -25,23 +26,37 @@
|
|||||||
<ToolBar>
|
<ToolBar>
|
||||||
<Button Command="{Binding ImportCommand}" Padding="8,4" FontSize="13"
|
<Button Command="{Binding ImportCommand}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="匯入 STP/STEP/STL/DXF(可複選,也可直接拖放到視窗)">📂 匯入</Button>
|
ToolTip="匯入 STP/STEP/STL/DXF(可複選,也可直接拖放到視窗)">📂 匯入</Button>
|
||||||
|
<Button Click="Recent_Click" Padding="4,4" FontSize="13"
|
||||||
|
ToolTip="最近開啟的檔案">▾</Button>
|
||||||
<Button Command="{Binding ZoomAllCommand}" Padding="8,4" FontSize="13"
|
<Button Command="{Binding ZoomAllCommand}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="縮放至全部模型">🔍 全覽</Button>
|
ToolTip="縮放至全部模型">🔍 全覽</Button>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
<TextBlock Text="視圖:" VerticalAlignment="Center" Margin="4,0,2,0" />
|
||||||
|
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Iso" Padding="6,4" FontSize="13"
|
||||||
|
ToolTip="等角視圖">等角</Button>
|
||||||
|
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Front" Padding="6,4" FontSize="13"
|
||||||
|
ToolTip="前視圖(-Y 方向看,Z 向上)">前</Button>
|
||||||
|
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Top" Padding="6,4" FontSize="13"
|
||||||
|
ToolTip="上視圖(+Z 往下看)">上</Button>
|
||||||
|
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Right" Padding="6,4" FontSize="13"
|
||||||
|
ToolTip="右視圖(+X 方向看,Z 向上)">右</Button>
|
||||||
|
<ToggleButton IsChecked="{Binding OrthographicView}" Padding="6,4" FontSize="13" Margin="2,0"
|
||||||
|
ToolTip="正交投影(平行投影,量尺寸不受透視變形;再按切回透視)">正交</ToggleButton>
|
||||||
|
<Separator />
|
||||||
<TextBlock Text="量測:" VerticalAlignment="Center" Margin="4,0,2,0" />
|
<TextBlock Text="量測:" VerticalAlignment="Center" Margin="4,0,2,0" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="量測點座標(吸附頂點)"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="量測點座標(吸附頂點/圓心)— 快速鍵 P"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}">📍 點</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}">📍 點</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩點距離 + ΔXYZ"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩點距離 + ΔXYZ(點到圓邊會吸圓心 → 可量孔對孔 pitch)— 快速鍵 D"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}">📏 距離</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}">📏 距離</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="邊長(直線/曲線/圓弧)"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="邊長(直線/曲線/圓弧)— 快速鍵 E"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}">📐 邊</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}">📐 邊</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面積 + 曲面類型/法向量"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面積 + 曲面類型/法向量 — 快速鍵 F"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}">⬛ 面</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}">⬛ 面</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="圓心/半徑/直徑/周長"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="圓心/半徑/直徑/周長 — 快速鍵 C"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}">⭕ 圓</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}">⭕ 圓</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩面/兩邊夾角(點兩個面或靠近直線邊)"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩面/兩邊夾角(點兩個面或靠近直線邊)— 快速鍵 A"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}">∠ 角度</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}">∠ 角度</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面到面最短距離(網格近似)"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面到面最短距離(網格近似)— 快速鍵 M"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=FaceDistance}">⇔ 面距</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=FaceDistance}">⇔ 面距</ToggleButton>
|
||||||
<Separator />
|
<Separator />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}"
|
<ToggleButton Style="{StaticResource ModeButton}"
|
||||||
@@ -65,10 +80,10 @@
|
|||||||
<ToggleButton IsChecked="{Binding GizmoEnabled}" Padding="8,4" FontSize="13"
|
<ToggleButton IsChecked="{Binding GizmoEnabled}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="三軸操作器:對樹面板選取的檔案顯示 XYZ 箭頭(沿軸移動)與旋轉環(繞軸轉任意角度),每次放開即定位">⊹ 操作器</ToggleButton>
|
ToolTip="三軸操作器:對樹面板選取的檔案顯示 XYZ 箭頭(沿軸移動)與旋轉環(繞軸轉任意角度),每次放開即定位">⊹ 操作器</ToggleButton>
|
||||||
<Button Command="{Binding CheckInterferenceCommand}" Padding="8,4" FontSize="13"
|
<Button Command="{Binding CheckInterferenceCommand}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="干涉檢查:檢查 2 個可見檔案是否相交(match 判定);無相交時回報最小間隙">🧩 干涉</Button>
|
ToolTip="干涉檢查:2 個以上可見檔案兩兩檢查是否相交(match 判定);無相交時回報最小間隙">🧩 干涉</Button>
|
||||||
<Separator />
|
<Separator />
|
||||||
<Button Command="{Binding ClearMeasurementsCommand}" Padding="8,4" FontSize="13"
|
<Button Command="{Binding ClearMeasurementsCommand}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="清除全部量測與標記">🧹 清除</Button>
|
ToolTip="清除全部量測與標記(Esc = 取消進行中量測/退出模式)">🧹 清除</Button>
|
||||||
<Separator />
|
<Separator />
|
||||||
<ToggleButton IsChecked="{Binding UseInch}" Padding="8,4" FontSize="13"
|
<ToggleButton IsChecked="{Binding UseInch}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="切換顯示單位 mm ⇄ inch(既有量測即時換算)">mm ⇄ in</ToggleButton>
|
ToolTip="切換顯示單位 mm ⇄ inch(既有量測即時換算)">mm ⇄ in</ToggleButton>
|
||||||
@@ -77,22 +92,27 @@
|
|||||||
ToolTip="量測結果匯出 CSV">💾 CSV</Button>
|
ToolTip="量測結果匯出 CSV">💾 CSV</Button>
|
||||||
<Button Command="{Binding SaveScreenshotCommand}" Padding="8,4" FontSize="13"
|
<Button Command="{Binding SaveScreenshotCommand}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="3D 視圖截圖存 PNG(2x 解析度)">📷 截圖</Button>
|
ToolTip="3D 視圖截圖存 PNG(2x 解析度)">📷 截圖</Button>
|
||||||
|
<Button Command="{Binding ExportStepFileCommand}" Padding="8,4" FontSize="13"
|
||||||
|
ToolTip="把可見檔案(目前對齊位置)匯出成新 STEP 檔 — 對齊插合結果可交接 CAD;不動原始檔">📤 STEP</Button>
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
<ToolBar>
|
<ToolBar>
|
||||||
<ToggleButton IsChecked="{Binding SectionEnabled}" Padding="8,4" FontSize="13"
|
<ToggleButton IsChecked="{Binding SectionEnabled}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="開啟/關閉剖面檢視">✂ 剖面</ToggleButton>
|
ToolTip="開啟/關閉剖面檢視">✂ 剖面</ToggleButton>
|
||||||
<ComboBox SelectedIndex="{Binding SectionAxisIndex}" Width="52" Margin="4,0"
|
<ComboBox SelectedIndex="{Binding SectionAxisIndex}" Width="60" Margin="4,0"
|
||||||
VerticalAlignment="Center" ToolTip="剖切軸向"
|
VerticalAlignment="Center" ToolTip="剖切軸向(3點 = 在模型上點 3 點定義任意平面)"
|
||||||
IsEnabled="{Binding SectionEnabled}">
|
IsEnabled="{Binding SectionEnabled}">
|
||||||
<ComboBoxItem>X</ComboBoxItem>
|
<ComboBoxItem>X</ComboBoxItem>
|
||||||
<ComboBoxItem>Y</ComboBoxItem>
|
<ComboBoxItem>Y</ComboBoxItem>
|
||||||
<ComboBoxItem>Z</ComboBoxItem>
|
<ComboBoxItem>Z</ComboBoxItem>
|
||||||
|
<ComboBoxItem>3點</ComboBoxItem>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
<Slider Value="{Binding SectionPosition}" Minimum="0" Maximum="100" Width="180"
|
<Slider Value="{Binding SectionPosition}" Minimum="0" Maximum="100" Width="180"
|
||||||
VerticalAlignment="Center" Margin="4,0" ToolTip="剖面位置(沿軸 %)"
|
VerticalAlignment="Center" Margin="4,0" ToolTip="剖面位置(沿法向 %)"
|
||||||
IsEnabled="{Binding SectionEnabled}" />
|
IsEnabled="{Binding SectionEnabled}" />
|
||||||
<TextBlock Text="{Binding SectionPosition, StringFormat={}{0:F0}%}" Width="36"
|
<TextBox Text="{Binding SectionPosition, StringFormat={}{0:F0}}" Width="34"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" ToolTip="剖面位置 %(0–100,可直接輸入,離開欄位生效)"
|
||||||
|
IsEnabled="{Binding SectionEnabled}" />
|
||||||
|
<TextBlock Text="%" VerticalAlignment="Center" Margin="1,0,0,0" />
|
||||||
<CheckBox IsChecked="{Binding SectionFlip}" Content="反向" VerticalAlignment="Center"
|
<CheckBox IsChecked="{Binding SectionFlip}" Content="反向" VerticalAlignment="Center"
|
||||||
Margin="4,0" ToolTip="切換保留側" IsEnabled="{Binding SectionEnabled}" />
|
Margin="4,0" ToolTip="切換保留側" IsEnabled="{Binding SectionEnabled}" />
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
@@ -111,75 +131,105 @@
|
|||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="280" />
|
<ColumnDefinition Width="280" MinWidth="160" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="300" />
|
<ColumnDefinition Width="*" MinWidth="300" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="300" MinWidth="160" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- ─── 裝配樹面板 ─── -->
|
<!-- ─── 裝配樹面板 ─── -->
|
||||||
<GroupBox Grid.Column="0" Header="模型 / 裝配樹" Margin="4">
|
<GroupBox Grid.Column="0" Header="模型 / 裝配樹" Margin="4">
|
||||||
<TreeView ItemsSource="{Binding Roots}"
|
<DockPanel>
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
<TextBox DockPanel.Dock="Top" Margin="0,2,0,4"
|
||||||
VirtualizingPanel.IsVirtualizing="True"
|
Text="{Binding TreeFilter, UpdateSourceTrigger=PropertyChanged}"
|
||||||
SelectedItemChanged="Tree_SelectedItemChanged">
|
ToolTip="輸入名稱過濾裝配樹(只影響樹面板顯示,不影響 3D 可見性;清空 = 全顯)" />
|
||||||
<TreeView.ItemContainerStyle>
|
<TreeView ItemsSource="{Binding Roots}"
|
||||||
<Style TargetType="TreeViewItem">
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
VirtualizingPanel.IsVirtualizing="True"
|
||||||
</Style>
|
SelectedItemChanged="Tree_SelectedItemChanged">
|
||||||
</TreeView.ItemContainerStyle>
|
<TreeView.ItemContainerStyle>
|
||||||
<TreeView.ItemTemplate>
|
<Style TargetType="TreeViewItem">
|
||||||
<HierarchicalDataTemplate DataType="{x:Type vm:ModelNodeViewModel}"
|
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
||||||
ItemsSource="{Binding Children}">
|
<Setter Property="Visibility"
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,1"
|
Value="{Binding IsFilterVisible, Converter={StaticResource BoolToVis}}" />
|
||||||
ToolTip="{Binding ToolTipText}">
|
</Style>
|
||||||
<CheckBox IsChecked="{Binding IsVisible}" VerticalAlignment="Center"
|
</TreeView.ItemContainerStyle>
|
||||||
ToolTip="顯示 / 隱藏(含子節點)" />
|
<TreeView.ItemTemplate>
|
||||||
<Button Width="14" Height="14" Margin="3,0"
|
<HierarchicalDataTemplate DataType="{x:Type vm:ModelNodeViewModel}"
|
||||||
VerticalAlignment="Center" ToolTip="點擊換色(含子節點)"
|
ItemsSource="{Binding Children}">
|
||||||
Command="{Binding CycleColorCommand}">
|
<StackPanel Orientation="Horizontal" Margin="0,1"
|
||||||
<Button.Template>
|
Background="Transparent"
|
||||||
<ControlTemplate TargetType="Button">
|
ToolTip="{Binding ToolTipText}"
|
||||||
<Border BorderBrush="Gray" BorderThickness="1" CornerRadius="2">
|
Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||||
<Border.Background>
|
<StackPanel.ContextMenu>
|
||||||
<SolidColorBrush Color="{Binding Color}" />
|
<ContextMenu>
|
||||||
</Border.Background>
|
<MenuItem Header="👁 只顯示此節點"
|
||||||
</Border>
|
Command="{Binding PlacementTarget.Tag.IsolateNodeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
</ControlTemplate>
|
CommandParameter="{Binding}" />
|
||||||
</Button.Template>
|
<MenuItem Header="🔄 反轉顯示"
|
||||||
</Button>
|
Command="{Binding PlacementTarget.Tag.InvertVisibilityCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||||
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"
|
<MenuItem Header="☀ 全部顯示"
|
||||||
MaxWidth="130" TextTrimming="CharacterEllipsis">
|
Command="{Binding PlacementTarget.Tag.ShowAllCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||||
<TextBlock.Style>
|
<Separator />
|
||||||
<Style TargetType="TextBlock">
|
<MenuItem Header="⚖ 量體積 / 質心"
|
||||||
<Style.Triggers>
|
Command="{Binding PlacementTarget.Tag.MeasureVolumeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
<DataTrigger Binding="{Binding IsRoot}" Value="True">
|
CommandParameter="{Binding}" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
</ContextMenu>
|
||||||
</DataTrigger>
|
</StackPanel.ContextMenu>
|
||||||
</Style.Triggers>
|
<CheckBox IsChecked="{Binding IsVisible}" VerticalAlignment="Center"
|
||||||
</Style>
|
ToolTip="顯示 / 隱藏(含子節點)" />
|
||||||
</TextBlock.Style>
|
<Button Width="14" Height="14" Margin="3,0"
|
||||||
</TextBlock>
|
VerticalAlignment="Center" ToolTip="點擊換色(含子節點)"
|
||||||
<CheckBox IsChecked="{Binding ShowEdges}" Content="邊" FontSize="10"
|
Command="{Binding CycleColorCommand}">
|
||||||
VerticalAlignment="Center" Margin="4,0,0,0"
|
<Button.Template>
|
||||||
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
<ControlTemplate TargetType="Button">
|
||||||
ToolTip="顯示輪廓邊線(邊線量大時轉動視角會變慢)" />
|
<Border BorderBrush="Gray" BorderThickness="1" CornerRadius="2">
|
||||||
<Button Content="🔍" Width="20" Margin="3,0,0,0" Padding="0"
|
<Border.Background>
|
||||||
VerticalAlignment="Center" ToolTip="縮放至此節點"
|
<SolidColorBrush Color="{Binding Color}" />
|
||||||
Command="{Binding DataContext.ZoomNodeCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
</Border.Background>
|
||||||
CommandParameter="{Binding}" />
|
</Border>
|
||||||
<Button Content="✕" Width="20" Padding="0" Margin="2,0,0,0"
|
</ControlTemplate>
|
||||||
VerticalAlignment="Center" ToolTip="移除此檔案"
|
</Button.Template>
|
||||||
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
</Button>
|
||||||
Command="{Binding DataContext.RemoveRootCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"
|
||||||
CommandParameter="{Binding}" />
|
MaxWidth="130" TextTrimming="CharacterEllipsis">
|
||||||
</StackPanel>
|
<TextBlock.Style>
|
||||||
</HierarchicalDataTemplate>
|
<Style TargetType="TextBlock">
|
||||||
</TreeView.ItemTemplate>
|
<Style.Triggers>
|
||||||
</TreeView>
|
<DataTrigger Binding="{Binding IsRoot}" Value="True">
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
<CheckBox IsChecked="{Binding ShowEdges}" Content="邊" FontSize="10"
|
||||||
|
VerticalAlignment="Center" Margin="4,0,0,0"
|
||||||
|
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="顯示輪廓邊線(邊線量大時轉動視角會變慢)" />
|
||||||
|
<Button Content="🔍" Width="20" Margin="3,0,0,0" Padding="0"
|
||||||
|
VerticalAlignment="Center" ToolTip="縮放至此節點"
|
||||||
|
Command="{Binding DataContext.ZoomNodeCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
CommandParameter="{Binding}" />
|
||||||
|
<Button Content="✕" Width="20" Padding="0" Margin="2,0,0,0"
|
||||||
|
VerticalAlignment="Center" ToolTip="移除此檔案"
|
||||||
|
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
||||||
|
Command="{Binding DataContext.RemoveRootCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
CommandParameter="{Binding}" />
|
||||||
|
</StackPanel>
|
||||||
|
</HierarchicalDataTemplate>
|
||||||
|
</TreeView.ItemTemplate>
|
||||||
|
</TreeView>
|
||||||
|
</DockPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
|
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch" Background="#FFD8D8D8"
|
||||||
|
ToolTip="拖曳調整面板寬度" />
|
||||||
|
|
||||||
<!-- ─── 3D 視圖 ─── -->
|
<!-- ─── 3D 視圖 ─── -->
|
||||||
<h:HelixViewport3D x:Name="viewport" Grid.Column="1"
|
<h:HelixViewport3D x:Name="viewport" Grid.Column="2"
|
||||||
ShowViewCube="True" ShowCoordinateSystem="True"
|
ShowViewCube="True" ShowCoordinateSystem="True"
|
||||||
ShowFrameRate="False" ZoomExtentsWhenLoaded="True"
|
ShowFrameRate="False" ZoomExtentsWhenLoaded="True"
|
||||||
PanGesture="MiddleClick" PanGesture2="Shift+LeftClick"
|
PanGesture="MiddleClick" PanGesture2="Shift+LeftClick"
|
||||||
@@ -208,11 +258,15 @@
|
|||||||
|
|
||||||
<!-- 操作器疊圖層:透明 Viewport3D 疊在主視窗上、永遠最上層。
|
<!-- 操作器疊圖層:透明 Viewport3D 疊在主視窗上、永遠最上層。
|
||||||
只在操作器開啟時 IsHitTestVisible(否則空白處滑鼠才會穿透回主視窗) -->
|
只在操作器開啟時 IsHitTestVisible(否則空白處滑鼠才會穿透回主視窗) -->
|
||||||
<Viewport3D x:Name="gizmoOverlay" Grid.Column="1"
|
<Viewport3D x:Name="gizmoOverlay" Grid.Column="2"
|
||||||
IsHitTestVisible="{Binding GizmoEnabled}" />
|
IsHitTestVisible="{Binding GizmoEnabled}" />
|
||||||
|
|
||||||
|
<GridSplitter Grid.Column="3" Width="5" HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch" Background="#FFD8D8D8"
|
||||||
|
ToolTip="拖曳調整面板寬度" />
|
||||||
|
|
||||||
<!-- ─── 量測結果面板 ─── -->
|
<!-- ─── 量測結果面板 ─── -->
|
||||||
<GroupBox Grid.Column="2" Header="量測結果" Margin="4">
|
<GroupBox Grid.Column="4" Header="量測結果" Margin="4">
|
||||||
<ListBox ItemsSource="{Binding Measurements}" HorizontalContentAlignment="Stretch"
|
<ListBox ItemsSource="{Binding Measurements}" HorizontalContentAlignment="Stretch"
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using STPViewer.Services;
|
using STPViewer.Services;
|
||||||
using STPViewer.ViewModels;
|
using STPViewer.ViewModels;
|
||||||
@@ -10,6 +13,7 @@ namespace STPViewer;
|
|||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
private readonly MainViewModel _vm = new();
|
private readonly MainViewModel _vm = new();
|
||||||
|
private readonly AppSettings _settings;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
@@ -21,12 +25,54 @@ public partial class MainWindow : Window
|
|||||||
_vm.Attach(viewport);
|
_vm.Attach(viewport);
|
||||||
_vm.AttachOverlay(gizmoOverlay);
|
_vm.AttachOverlay(gizmoOverlay);
|
||||||
Loaded += MainWindow_Loaded;
|
Loaded += MainWindow_Loaded;
|
||||||
|
Closing += MainWindow_Closing;
|
||||||
|
|
||||||
|
// 使用者設定:視窗位置大小(View 職責)+ 單位/MRU(VM 職責)
|
||||||
|
_settings = SettingsService.Load();
|
||||||
|
ApplyWindowSettings();
|
||||||
|
_vm.LoadSettings(_settings);
|
||||||
|
|
||||||
// Gizmo 操作器放在疊圖層;manipulator 會把 MouseUp 標 handled,需 handledEventsToo 才收得到
|
// Gizmo 操作器放在疊圖層;manipulator 會把 MouseUp 標 handled,需 handledEventsToo 才收得到
|
||||||
gizmoOverlay.AddHandler(MouseLeftButtonUpEvent,
|
gizmoOverlay.AddHandler(MouseLeftButtonUpEvent,
|
||||||
new MouseButtonEventHandler((_, _) => _vm.OnGizmoMouseUp()), handledEventsToo: true);
|
new MouseButtonEventHandler((_, _) => _vm.OnGizmoMouseUp()), handledEventsToo: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyWindowSettings()
|
||||||
|
{
|
||||||
|
if (_settings.WindowWidth >= 400 && _settings.WindowHeight >= 300)
|
||||||
|
{
|
||||||
|
Width = _settings.WindowWidth;
|
||||||
|
Height = _settings.WindowHeight;
|
||||||
|
}
|
||||||
|
// 位置要落在目前虛擬桌面內才還原(避免拔掉外接螢幕後視窗開在螢幕外)
|
||||||
|
if (!double.IsNaN(_settings.WindowLeft) && !double.IsNaN(_settings.WindowTop) &&
|
||||||
|
_settings.WindowLeft >= SystemParameters.VirtualScreenLeft - 50 &&
|
||||||
|
_settings.WindowTop >= SystemParameters.VirtualScreenTop - 50 &&
|
||||||
|
_settings.WindowLeft <= SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth - 200 &&
|
||||||
|
_settings.WindowTop <= SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight - 200)
|
||||||
|
{
|
||||||
|
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||||
|
Left = _settings.WindowLeft;
|
||||||
|
Top = _settings.WindowTop;
|
||||||
|
}
|
||||||
|
if (_settings.WindowMaximized)
|
||||||
|
WindowState = WindowState.Maximized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MainWindow_Closing(object? sender, CancelEventArgs e)
|
||||||
|
{
|
||||||
|
Rect r = WindowState == WindowState.Normal
|
||||||
|
? new Rect(Left, Top, Width, Height)
|
||||||
|
: RestoreBounds;
|
||||||
|
_settings.WindowLeft = r.Left;
|
||||||
|
_settings.WindowTop = r.Top;
|
||||||
|
_settings.WindowWidth = r.Width;
|
||||||
|
_settings.WindowHeight = r.Height;
|
||||||
|
_settings.WindowMaximized = WindowState == WindowState.Maximized;
|
||||||
|
_vm.SaveSettingsInto(_settings);
|
||||||
|
SettingsService.Save(_settings);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>支援命令列帶檔開啟:STPViewer.exe a.stp b.stl …</summary>
|
/// <summary>支援命令列帶檔開啟:STPViewer.exe a.stp b.stl …</summary>
|
||||||
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -36,6 +82,47 @@ public partial class MainWindow : Window
|
|||||||
await _vm.ImportFilesAsync(files);
|
await _vm.ImportFilesAsync(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Esc / 量測模式快速鍵(焦點在輸入框時不攔截,樹搜尋、剖面數值可正常打字)</summary>
|
||||||
|
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (Keyboard.FocusedElement is TextBoxBase or ComboBox) return;
|
||||||
|
if (e.Key == Key.Escape)
|
||||||
|
{
|
||||||
|
_vm.OnEscape();
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Keyboard.Modifiers != ModifierKeys.None) return;
|
||||||
|
if (_vm.OnHotKey(e.Key))
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>最近檔案下拉(▾):即時從 VM 的 MRU 清單組選單</summary>
|
||||||
|
private void Recent_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var menu = new ContextMenu
|
||||||
|
{
|
||||||
|
PlacementTarget = (UIElement)sender,
|
||||||
|
Placement = PlacementMode.Bottom,
|
||||||
|
};
|
||||||
|
if (_vm.RecentFiles.Count == 0)
|
||||||
|
{
|
||||||
|
menu.Items.Add(new MenuItem { Header = "(沒有最近開啟的檔案)", IsEnabled = false });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (string f in _vm.RecentFiles)
|
||||||
|
menu.Items.Add(new MenuItem
|
||||||
|
{
|
||||||
|
Header = System.IO.Path.GetFileName(f),
|
||||||
|
ToolTip = f,
|
||||||
|
Command = _vm.ImportRecentCommand,
|
||||||
|
CommandParameter = f,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
menu.IsOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
private void Tree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) =>
|
private void Tree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) =>
|
||||||
_vm.SelectedNode = e.NewValue as ModelNodeViewModel;
|
_vm.SelectedNode = e.NewValue as ModelNodeViewModel;
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ public enum MeasureMode
|
|||||||
Align3, // 三點對齊:旋轉+平移(來源 3 點 → 目標 3 點)
|
Align3, // 三點對齊:旋轉+平移(來源 3 點 → 目標 3 點)
|
||||||
Drag, // 拖曳模式:左鍵按住零件沿螢幕平面拖動(放開才烘進 B-rep)
|
Drag, // 拖曳模式:左鍵按住零件沿螢幕平面拖動(放開才烘進 B-rep)
|
||||||
Interference, // 干涉檢查結果(非點擊模式,僅作為結果類型)
|
Interference, // 干涉檢查結果(非點擊模式,僅作為結果類型)
|
||||||
|
Volume, // 體積/質心(樹右鍵選單觸發,非點擊模式,僅作為結果類型)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ public static class Units
|
|||||||
public static string A(double mm2, UnitSystem u) =>
|
public static string A(double mm2, UnitSystem u) =>
|
||||||
u == UnitSystem.Millimeter ? $"{mm2:N3} mm²" : $"{mm2 / (MmPerInch * MmPerInch):N4} in²";
|
u == UnitSystem.Millimeter ? $"{mm2:N3} mm²" : $"{mm2 / (MmPerInch * MmPerInch):N4} in²";
|
||||||
|
|
||||||
|
/// <summary>體積</summary>
|
||||||
|
public static string V(double mm3, UnitSystem u) =>
|
||||||
|
u == UnitSystem.Millimeter ? $"{mm3:N2} mm³" : $"{mm3 / (MmPerInch * MmPerInch * MmPerInch):N4} in³";
|
||||||
|
|
||||||
/// <summary>座標分量(無單位字尾)</summary>
|
/// <summary>座標分量(無單位字尾)</summary>
|
||||||
public static string C(double mm, UnitSystem u) =>
|
public static string C(double mm, UnitSystem u) =>
|
||||||
u == UnitSystem.Millimeter ? $"{mm:F3}" : $"{mm / MmPerInch:F4}";
|
u == UnitSystem.Millimeter ? $"{mm:F3}" : $"{mm / MmPerInch:F4}";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<Version>0.4.0</Version> <!-- 單一版號來源:標題與 publish-winapp 打包資料夾都讀這個 -->
|
<Version>0.5.0</Version> <!-- 單一版號來源:標題與 publish-winapp 打包資料夾都讀這個 -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ public class MeasurementService
|
|||||||
|
|
||||||
// ─── 拾取輔助 ────────────────────────────────────────────────
|
// ─── 拾取輔助 ────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>命中點吸附到最近的 B-rep 頂點(容差內),否則回傳原始表面點</summary>
|
/// <summary>
|
||||||
|
/// 命中點吸附:點到圓形邊附近(容差內)→ 吸附**圓心**(量兩孔 pitch 的關鍵,v0.5.0);
|
||||||
|
/// 否則吸附最近的 B-rep 頂點(容差內);都沒有 → 回傳原始表面點。
|
||||||
|
/// 圓心與頂點都在容差內時,比「命中點到圓形邊」與「命中點到頂點」誰近(圓弧端點常與圓重疊,
|
||||||
|
/// 點在圓邊上時圓心優先勝出才符合直覺)。
|
||||||
|
/// </summary>
|
||||||
public Point3D Snap(FaceInfo fi, Point3D hit, double tolerance)
|
public Point3D Snap(FaceInfo fi, Point3D hit, double tolerance)
|
||||||
{
|
{
|
||||||
if (fi.BrepFace is null) return hit;
|
if (fi.BrepFace is null) return hit;
|
||||||
@@ -47,6 +52,22 @@ public class MeasurementService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { /* 頂點存取失敗則不吸附 */ }
|
catch { /* 頂點存取失敗則不吸附 */ }
|
||||||
|
|
||||||
|
// 圓形邊:以「命中點到邊曲線」的距離參與比較,勝出時吸附到圓心
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (Edge e in fi.BrepFace.AllEdges)
|
||||||
|
{
|
||||||
|
if (e.Curve3D is not Ellipse { IsCircle: true } el) continue;
|
||||||
|
double pos;
|
||||||
|
try { pos = el.PositionOf(ToGeo(hit)); } catch { continue; }
|
||||||
|
if (double.IsNaN(pos)) continue;
|
||||||
|
Point3D onCurve = ToP3(el.PointAt(Math.Clamp(pos, 0, 1)));
|
||||||
|
double d = (onCurve - hit).Length;
|
||||||
|
if (d < bestD) { bestD = d; best = ToP3(el.Center); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* 邊存取失敗則維持頂點吸附結果 */ }
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,6 +427,47 @@ public class MeasurementService
|
|||||||
return (pa, pb);
|
return (pa, pb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 體積 / 質心(封閉網格 signed volume)────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 網格體積與質心:各三角形與原點構成的四面體有向體積加總(divergence theorem)。
|
||||||
|
/// 只對「封閉」網格有意義(開放殼會得到不可靠的值 → IsClosedReliable=false 供呼叫端提示)。
|
||||||
|
/// 多個 mesh(多實體)直接加總。
|
||||||
|
/// </summary>
|
||||||
|
public static (double VolumeMm3, Point3D Centroid, bool Reliable) MeshVolume(
|
||||||
|
System.Collections.Generic.IEnumerable<MeshGeometry3D> meshes)
|
||||||
|
{
|
||||||
|
double vol6 = 0; // Σ 6×有向體積
|
||||||
|
double absVol6 = 0; // Σ |6×有向體積|(封閉性 sanity check 用)
|
||||||
|
double cx = 0, cy = 0, cz = 0; // Σ 四面體質心 × 6×有向體積
|
||||||
|
foreach (MeshGeometry3D mesh in meshes)
|
||||||
|
{
|
||||||
|
Point3DCollection pos = mesh.Positions;
|
||||||
|
Int32Collection idx = mesh.TriangleIndices;
|
||||||
|
for (int i = 0; i + 2 < idx.Count; i += 3)
|
||||||
|
{
|
||||||
|
Point3D a = pos[idx[i]], b = pos[idx[i + 1]], c = pos[idx[i + 2]];
|
||||||
|
// 6V = a · (b × c)
|
||||||
|
double v = a.X * (b.Y * c.Z - b.Z * c.Y)
|
||||||
|
+ a.Y * (b.Z * c.X - b.X * c.Z)
|
||||||
|
+ a.Z * (b.X * c.Y - b.Y * c.X);
|
||||||
|
vol6 += v;
|
||||||
|
absVol6 += Math.Abs(v);
|
||||||
|
// 四面體(a,b,c,原點)質心 = (a+b+c)/4
|
||||||
|
cx += (a.X + b.X + c.X) / 4 * v;
|
||||||
|
cy += (a.Y + b.Y + c.Y) / 4 * v;
|
||||||
|
cz += (a.Z + b.Z + c.Z) / 4 * v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double volume = Math.Abs(vol6) / 6;
|
||||||
|
// 有向體積相對正負抵銷比例過高 → 可能是開放殼/法向不一致,數值不可靠
|
||||||
|
bool reliable = absVol6 > 0 && Math.Abs(vol6) / absVol6 > 0.01 && volume > 1e-9;
|
||||||
|
Point3D centroid = Math.Abs(vol6) > 1e-12
|
||||||
|
? new Point3D(cx / vol6, cy / vol6, cz / vol6)
|
||||||
|
: default;
|
||||||
|
return (volume, centroid, reliable);
|
||||||
|
}
|
||||||
|
|
||||||
// 點到三角形最近點:共用 InterferenceService.ClosestPointOnTriangle(v0.4.0 去重複)
|
// 點到三角形最近點:共用 InterferenceService.ClosestPointOnTriangle(v0.4.0 去重複)
|
||||||
|
|
||||||
// ─── overlay helpers ─────────────────────────────────────────
|
// ─── overlay helpers ─────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace STPViewer.Services;
|
||||||
|
|
||||||
|
/// <summary>使用者設定(%LOCALAPPDATA%\STPViewer\settings.json)</summary>
|
||||||
|
public class AppSettings
|
||||||
|
{
|
||||||
|
public double WindowWidth { get; set; } = double.NaN;
|
||||||
|
public double WindowHeight { get; set; } = double.NaN;
|
||||||
|
public double WindowLeft { get; set; } = double.NaN;
|
||||||
|
public double WindowTop { get; set; } = double.NaN;
|
||||||
|
public bool WindowMaximized { get; set; }
|
||||||
|
public bool UseInch { get; set; }
|
||||||
|
public List<string> RecentFiles { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>設定讀寫:壞檔/缺檔一律回預設值,儲存失敗靜默(設定不值得讓程式閃退)</summary>
|
||||||
|
public static class SettingsService
|
||||||
|
{
|
||||||
|
public const int MaxRecentFiles = 10;
|
||||||
|
|
||||||
|
private static readonly string SettingsPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"STPViewer", "settings.json");
|
||||||
|
|
||||||
|
public static AppSettings Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(SettingsPath))
|
||||||
|
return JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(SettingsPath)) ?? new AppSettings();
|
||||||
|
}
|
||||||
|
catch { /* 壞檔回預設 */ }
|
||||||
|
return new AppSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Save(AppSettings settings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(SettingsPath)!);
|
||||||
|
File.WriteAllText(SettingsPath,
|
||||||
|
JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
}
|
||||||
|
catch { /* 儲存失敗靜默 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
@@ -39,6 +40,48 @@ public partial class MainViewModel
|
|||||||
StatusText = $"已匯出 {Measurements.Count} 筆量測 → {Path.GetFileName(dlg.FileName)}";
|
StatusText = $"已匯出 {Measurements.Count} 筆量測 → {Path.GetFileName(dlg.FileName)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 把可見檔案(目前位置,含對齊/旋轉/拖曳結果)匯出成「新的」STEP 檔 —
|
||||||
|
/// 對齊插合後的配合結果可交接給 CAD;不動任何原始檔(唯讀原則不變)。
|
||||||
|
/// 只含 B-rep Solid/Shell(STL 網格、DXF 線架構無法進 STEP B-rep)。
|
||||||
|
/// </summary>
|
||||||
|
[RelayCommand(CanExecute = nameof(IsIdle))]
|
||||||
|
private void ExportStepFile()
|
||||||
|
{
|
||||||
|
var geos = Roots.Where(r => r.IsVisible)
|
||||||
|
.SelectMany(r => r.Leaves()).Where(l => l.IsVisible)
|
||||||
|
.SelectMany(l => l.SourceGeos)
|
||||||
|
.Where(g => g is CADability.GeoObject.Solid or CADability.GeoObject.Shell)
|
||||||
|
.ToList();
|
||||||
|
if (geos.Count == 0)
|
||||||
|
{
|
||||||
|
StatusText = "沒有可匯出的 B-rep 幾何(STEP 匯出僅含 Solid/Shell;請勾選要匯出的 STEP 檔案)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dlg = new SaveFileDialog
|
||||||
|
{
|
||||||
|
Title = "匯出目前場景為 STEP(零件在目前對齊位置)",
|
||||||
|
Filter = "STEP 檔案 (*.stp)|*.stp|STEP 檔案 (*.step)|*.step",
|
||||||
|
FileName = $"assembly_{DateTime.Now:yyyyMMdd_HHmmss}.stp",
|
||||||
|
};
|
||||||
|
if (dlg.ShowDialog() != true) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// B-rep 已含目前位置(TransformRoot 對 Solid/Shell 整體 Modify)→ 直接寫出
|
||||||
|
CADability.Project project = CADability.Project.CreateSimpleProject();
|
||||||
|
CADability.Model model = project.GetActiveModel();
|
||||||
|
foreach (CADability.GeoObject.IGeoObject g in geos) model.Add(g);
|
||||||
|
new CADability.ExportStep().WriteToFile(dlg.FileName, project);
|
||||||
|
StatusText = $"已匯出 STEP({geos.Count} 個實體,目前位置)→ {Path.GetFileName(dlg.FileName)}";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusText = $"STEP 匯出失敗:{ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void SaveScreenshot()
|
private void SaveScreenshot()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public partial class MainViewModel
|
|||||||
// 操作器疊圖層:另一個透明 Viewport3D 疊在主視窗上、相機同步、只放操作器 →
|
// 操作器疊圖層:另一個透明 Viewport3D 疊在主視窗上、相機同步、只放操作器 →
|
||||||
// 操作器不在主場景,永不被實體遮擋(always-on-top);空白處滑鼠穿透回主視窗
|
// 操作器不在主場景,永不被實體遮擋(always-on-top);空白處滑鼠穿透回主視窗
|
||||||
private System.Windows.Controls.Viewport3D? _overlayViewport;
|
private System.Windows.Controls.Viewport3D? _overlayViewport;
|
||||||
private PerspectiveCamera? _overlayCamera;
|
private ProjectionCamera? _overlayCamera; // 型別跟隨主相機(透視/正交切換,v0.5.0)
|
||||||
|
|
||||||
/// <summary>注入操作器疊圖層(透明 Viewport3D,疊在主視窗上、永遠最上層)</summary>
|
/// <summary>注入操作器疊圖層(透明 Viewport3D,疊在主視窗上、永遠最上層)</summary>
|
||||||
public void AttachOverlay(System.Windows.Controls.Viewport3D overlay)
|
public void AttachOverlay(System.Windows.Controls.Viewport3D overlay)
|
||||||
@@ -37,16 +37,41 @@ public partial class MainViewModel
|
|||||||
SyncOverlayCamera();
|
SyncOverlayCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>疊圖層相機跟隨主相機(每次主相機變更時呼叫,讓操作器疊在正確螢幕位置)</summary>
|
/// <summary>
|
||||||
|
/// 疊圖層相機跟隨主相機(每次主相機變更時呼叫,讓操作器疊在正確螢幕位置)。
|
||||||
|
/// 主相機可能在透視/正交間切換(Orthographic 屬性)→ 疊圖層相機「型別」也要跟著換,投影才一致。
|
||||||
|
/// </summary>
|
||||||
private void SyncOverlayCamera()
|
private void SyncOverlayCamera()
|
||||||
{
|
{
|
||||||
if (_overlayCamera is null || _viewport?.Camera is not ProjectionCamera src) return;
|
if (_overlayViewport is null || _viewport?.Camera is not ProjectionCamera src) return;
|
||||||
|
|
||||||
|
if (src is PerspectiveCamera p)
|
||||||
|
{
|
||||||
|
if (_overlayCamera is not PerspectiveCamera op)
|
||||||
|
{
|
||||||
|
op = new PerspectiveCamera();
|
||||||
|
_overlayCamera = op;
|
||||||
|
_overlayViewport.Camera = op;
|
||||||
|
}
|
||||||
|
op.FieldOfView = p.FieldOfView;
|
||||||
|
}
|
||||||
|
else if (src is OrthographicCamera o)
|
||||||
|
{
|
||||||
|
if (_overlayCamera is not OrthographicCamera oo)
|
||||||
|
{
|
||||||
|
oo = new OrthographicCamera();
|
||||||
|
_overlayCamera = oo;
|
||||||
|
_overlayViewport.Camera = oo;
|
||||||
|
}
|
||||||
|
oo.Width = o.Width;
|
||||||
|
}
|
||||||
|
if (_overlayCamera is null) return;
|
||||||
|
|
||||||
_overlayCamera.Position = src.Position;
|
_overlayCamera.Position = src.Position;
|
||||||
_overlayCamera.LookDirection = src.LookDirection;
|
_overlayCamera.LookDirection = src.LookDirection;
|
||||||
_overlayCamera.UpDirection = src.UpDirection;
|
_overlayCamera.UpDirection = src.UpDirection;
|
||||||
_overlayCamera.NearPlaneDistance = src.NearPlaneDistance;
|
_overlayCamera.NearPlaneDistance = src.NearPlaneDistance;
|
||||||
_overlayCamera.FarPlaneDistance = src.FarPlaneDistance;
|
_overlayCamera.FarPlaneDistance = src.FarPlaneDistance;
|
||||||
if (src is PerspectiveCamera p) _overlayCamera.FieldOfView = p.FieldOfView;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnGizmoEnabledChanged(bool value) => UpdateGizmo();
|
partial void OnGizmoEnabledChanged(bool value) => UpdateGizmo();
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ public partial class MainViewModel
|
|||||||
{
|
{
|
||||||
if (_viewport is null) return;
|
if (_viewport is null) return;
|
||||||
var visibleRoots = Roots.Where(r => r.IsVisible).ToList();
|
var visibleRoots = Roots.Where(r => r.IsVisible).ToList();
|
||||||
if (visibleRoots.Count != 2)
|
if (visibleRoots.Count < 2)
|
||||||
{
|
{
|
||||||
StatusText = $"干涉檢查需要剛好 2 個可見檔案(目前 {visibleRoots.Count} 個)— 用樹面板勾選";
|
StatusText = $"干涉檢查需要至少 2 個可見檔案(目前 {visibleRoots.Count} 個)— 用樹面板勾選";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,16 +33,27 @@ public partial class MainViewModel
|
|||||||
.Select(m => _faceMap[(GeometryModel3D)m].Mesh)
|
.Select(m => _faceMap[(GeometryModel3D)m].Mesh)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var a = MeshesOf(visibleRoots[0]);
|
// 網格快照(開跑後零件被改走也不會半途換料;v0.4.0 起運算期間指令已鎖,這裡是雙保險)
|
||||||
var b = MeshesOf(visibleRoots[1]);
|
var meshes = visibleRoots.ToDictionary(r => r, MeshesOf);
|
||||||
string nameA = visibleRoots[0].Name, nameB = visibleRoots[1].Name;
|
|
||||||
|
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
StatusText = $"干涉檢查中:{nameA} ⟷ {nameB} …";
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
InterferenceResult result = await Task.Run(() => InterferenceService.Check(a, b));
|
int pairs = 0, intersecting = 0;
|
||||||
AddInterferenceResult(result, nameA, nameB);
|
for (int i = 0; i < visibleRoots.Count; i++)
|
||||||
|
for (int j = i + 1; j < visibleRoots.Count; j++)
|
||||||
|
{
|
||||||
|
pairs++;
|
||||||
|
string nameA = visibleRoots[i].Name, nameB = visibleRoots[j].Name;
|
||||||
|
StatusText = $"干涉檢查中(第 {pairs} 組):{nameA} ⟷ {nameB} …";
|
||||||
|
var a = meshes[visibleRoots[i]];
|
||||||
|
var b = meshes[visibleRoots[j]];
|
||||||
|
InterferenceResult result = await Task.Run(() => InterferenceService.Check(a, b));
|
||||||
|
AddInterferenceResult(result, nameA, nameB);
|
||||||
|
if (result.Intersects) intersecting++;
|
||||||
|
}
|
||||||
|
if (pairs > 1)
|
||||||
|
StatusText = $"干涉檢查完成:{pairs} 組配對、{intersecting} 組相交";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,11 +38,116 @@ public partial class MainViewModel
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool sectionFlip;
|
private bool sectionFlip;
|
||||||
|
|
||||||
partial void OnSectionEnabledChanged(bool value) => ScheduleSection();
|
// ── 3點自訂剖切平面(SectionAxisIndex == 3,v0.5.0)──
|
||||||
partial void OnSectionAxisIndexChanged(int value) => ScheduleSection();
|
private const int CustomAxisIndex = 3;
|
||||||
partial void OnSectionPositionChanged(double value) => ScheduleSection();
|
private Vector3D? _customNormal; // null = 尚未定義(等使用者點 3 點)
|
||||||
|
private readonly List<Point3D> _sectionPicks = new();
|
||||||
|
private readonly List<Visual3D> _sectionPickOverlays = new();
|
||||||
|
|
||||||
|
partial void OnSectionEnabledChanged(bool value)
|
||||||
|
{
|
||||||
|
if (!value) CancelSectionPickIfActive(quiet: true);
|
||||||
|
ScheduleSection();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSectionAxisIndexChanged(int value)
|
||||||
|
{
|
||||||
|
if (value == CustomAxisIndex)
|
||||||
|
{
|
||||||
|
if (_customNormal is null)
|
||||||
|
StatusText = "3點剖面:請在模型上點 3 個點定義剖切平面(Esc 取消)";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 離開自訂平面 → 丟棄舊平面(再選「3點」即重新定義)
|
||||||
|
_customNormal = null;
|
||||||
|
ClearSectionPicks();
|
||||||
|
}
|
||||||
|
ScheduleSection();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSectionPositionChanged(double value)
|
||||||
|
{
|
||||||
|
if (value < 0) { SectionPosition = 0; return; } // 位置數值輸入框防呆
|
||||||
|
if (value > 100) { SectionPosition = 100; return; }
|
||||||
|
ScheduleSection();
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnSectionFlipChanged(bool value) => ScheduleSection();
|
partial void OnSectionFlipChanged(bool value) => ScheduleSection();
|
||||||
|
|
||||||
|
/// <summary>3點剖面定義模式中的點擊(OnViewportClick 轉入)。回傳 true = 已消費此點擊。</summary>
|
||||||
|
public bool HandleSectionPlanePick(System.Windows.Point position)
|
||||||
|
{
|
||||||
|
if (!SectionEnabled || SectionAxisIndex != CustomAxisIndex || _customNormal is not null) return false;
|
||||||
|
if (_viewport is null) return true;
|
||||||
|
|
||||||
|
Point3D? hit = null;
|
||||||
|
foreach (var h in _viewport.Viewport.FindHits(position))
|
||||||
|
{
|
||||||
|
if (h.Model is null) continue;
|
||||||
|
if (_faceMap.ContainsKey(h.Model) || _mergedFaceRanges.ContainsKey(h.Model))
|
||||||
|
{
|
||||||
|
hit = h.Position;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hit is null)
|
||||||
|
{
|
||||||
|
StatusText = $"3點剖面:未命中模型表面(已選 {_sectionPicks.Count}/3 點,Esc 取消)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_sectionPicks.Count == 2 &&
|
||||||
|
Vector3D.CrossProduct(_sectionPicks[1] - _sectionPicks[0], hit.Value - _sectionPicks[0]).LengthSquared < 1e-12)
|
||||||
|
{
|
||||||
|
StatusText = "3點剖面:三點共線,請改選不共線的第 3 點";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sectionPicks.Add(hit.Value);
|
||||||
|
double markerR = Math.Clamp(SceneDiagonal() * 0.004, 0.01, 5.0);
|
||||||
|
var marker = new SphereVisual3D { Center = hit.Value, Radius = markerR, Fill = System.Windows.Media.Brushes.DodgerBlue };
|
||||||
|
_sectionPickOverlays.Add(marker);
|
||||||
|
_viewport.Children.Add(marker);
|
||||||
|
|
||||||
|
if (_sectionPicks.Count < 3)
|
||||||
|
{
|
||||||
|
StatusText = $"3點剖面:已選 {_sectionPicks.Count}/3 點";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3D n = Vector3D.CrossProduct(_sectionPicks[1] - _sectionPicks[0], _sectionPicks[2] - _sectionPicks[0]);
|
||||||
|
ClearSectionPicks();
|
||||||
|
if (n.LengthSquared < 1e-12)
|
||||||
|
{
|
||||||
|
StatusText = "3點剖面:三點退化(共線),請重新點選";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
n.Normalize();
|
||||||
|
_customNormal = n;
|
||||||
|
StatusText = $"3點剖面:平面已定義(法向 {n.X:F2}, {n.Y:F2}, {n.Z:F2})— 用位置滑桿掃剖面";
|
||||||
|
ApplySection();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>取消 3 點平面定義(Esc / 關剖面)。回傳 true = 有取消動作。</summary>
|
||||||
|
private bool CancelSectionPickIfActive(bool quiet = false)
|
||||||
|
{
|
||||||
|
bool picking = SectionEnabled && SectionAxisIndex == CustomAxisIndex && _customNormal is null;
|
||||||
|
if (_sectionPicks.Count == 0 && !picking) return false;
|
||||||
|
ClearSectionPicks();
|
||||||
|
if (SectionAxisIndex == CustomAxisIndex && _customNormal is null)
|
||||||
|
SectionAxisIndex = 0; // 回 X 軸
|
||||||
|
if (!quiet) StatusText = "已取消 3 點剖面定義";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearSectionPicks()
|
||||||
|
{
|
||||||
|
foreach (Visual3D v in _sectionPickOverlays) _viewport?.Children.Remove(v);
|
||||||
|
_sectionPickOverlays.Clear();
|
||||||
|
_sectionPicks.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
private void ScheduleSection()
|
private void ScheduleSection()
|
||||||
{
|
{
|
||||||
_sectionTimer.Stop();
|
_sectionTimer.Stop();
|
||||||
@@ -82,28 +187,40 @@ public partial class MainViewModel
|
|||||||
Rect3D bounds = UnionBounds();
|
Rect3D bounds = UnionBounds();
|
||||||
if (bounds.IsEmpty) return;
|
if (bounds.IsEmpty) return;
|
||||||
|
|
||||||
Vector3D axis = SectionAxisIndex switch
|
Vector3D axis;
|
||||||
|
if (SectionAxisIndex == CustomAxisIndex)
|
||||||
{
|
{
|
||||||
0 => new Vector3D(1, 0, 0),
|
if (_customNormal is null) return; // 平面尚未定義(等使用者點 3 點)
|
||||||
1 => new Vector3D(0, 1, 0),
|
axis = _customNormal.Value;
|
||||||
_ => new Vector3D(0, 0, 1),
|
}
|
||||||
};
|
else
|
||||||
|
{
|
||||||
|
axis = SectionAxisIndex switch
|
||||||
|
{
|
||||||
|
0 => new Vector3D(1, 0, 0),
|
||||||
|
1 => new Vector3D(0, 1, 0),
|
||||||
|
_ => new Vector3D(0, 0, 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
double t = SectionPosition / 100.0;
|
double t = SectionPosition / 100.0;
|
||||||
Point3D min = bounds.Location;
|
Point3D min = bounds.Location;
|
||||||
var size = new Vector3D(bounds.SizeX, bounds.SizeY, bounds.SizeZ);
|
var size = new Vector3D(bounds.SizeX, bounds.SizeY, bounds.SizeZ);
|
||||||
double planeCoord = SectionAxisIndex switch
|
|
||||||
{
|
|
||||||
0 => min.X + size.X * t,
|
|
||||||
1 => min.Y + size.Y * t,
|
|
||||||
_ => min.Z + size.Z * t,
|
|
||||||
};
|
|
||||||
Point3D center = min + size / 2;
|
Point3D center = min + size / 2;
|
||||||
Point3D planePoint = SectionAxisIndex switch
|
// 平面位置:場景 AABB 8 個角投影到法向的 [dmin, dmax],滑桿 t 內插
|
||||||
|
//(軸向法向 = 與舊版逐軸計算等價;任意法向也成立)
|
||||||
|
double dmin = double.MaxValue, dmax = double.MinValue;
|
||||||
|
for (int ci = 0; ci < 8; ci++)
|
||||||
{
|
{
|
||||||
0 => new Point3D(planeCoord, center.Y, center.Z),
|
var corner = new Point3D(
|
||||||
1 => new Point3D(center.X, planeCoord, center.Z),
|
(ci & 1) == 0 ? min.X : min.X + size.X,
|
||||||
_ => new Point3D(center.X, center.Y, planeCoord),
|
(ci & 2) == 0 ? min.Y : min.Y + size.Y,
|
||||||
};
|
(ci & 4) == 0 ? min.Z : min.Z + size.Z);
|
||||||
|
double d = Vector3D.DotProduct((Vector3D)corner, axis);
|
||||||
|
if (d < dmin) dmin = d;
|
||||||
|
if (d > dmax) dmax = d;
|
||||||
|
}
|
||||||
|
double dStar = dmin + (dmax - dmin) * t;
|
||||||
|
Point3D planePoint = center + axis * (dStar - Vector3D.DotProduct((Vector3D)center, axis));
|
||||||
Vector3D normal = SectionFlip ? -axis : axis;
|
Vector3D normal = SectionFlip ? -axis : axis;
|
||||||
|
|
||||||
// UI 執行緒快照(原始 mesh 皆已 Freeze,可跨執行緒),背景平行裁切,回 UI 一次換上。
|
// UI 執行緒快照(原始 mesh 皆已 Freeze,可跨執行緒),背景平行裁切,回 UI 一次換上。
|
||||||
@@ -137,7 +254,8 @@ public partial class MainViewModel
|
|||||||
|
|
||||||
ApplyRenderMode(); // 剖面開啟 → 改用逐面(裁切後幾何)
|
ApplyRenderMode(); // 剖面開啟 → 改用逐面(裁切後幾何)
|
||||||
UpdateSectionPlaneVisual(planePoint, axis, size);
|
UpdateSectionPlaneVisual(planePoint, axis, size);
|
||||||
StatusText = $"剖面:{"XYZ"[SectionAxisIndex]} 軸 {SectionPosition:F0}%" + (SectionFlip ? "(反向)" : "");
|
string axisName = SectionAxisIndex == CustomAxisIndex ? "自訂平面" : $"{"XYZ"[SectionAxisIndex]} 軸";
|
||||||
|
StatusText = $"剖面:{axisName} {SectionPosition:F0}%" + (SectionFlip ? "(反向)" : "");
|
||||||
} while (_sectionReapply);
|
} while (_sectionReapply);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -155,8 +273,10 @@ public partial class MainViewModel
|
|||||||
{
|
{
|
||||||
0 => (new Vector3D(0, 1, 0), size.Y, size.Z),
|
0 => (new Vector3D(0, 1, 0), size.Y, size.Z),
|
||||||
1 => (new Vector3D(1, 0, 0), size.X, size.Z),
|
1 => (new Vector3D(1, 0, 0), size.X, size.Z),
|
||||||
_ => (new Vector3D(1, 0, 0), size.X, size.Y),
|
2 => (new Vector3D(1, 0, 0), size.X, size.Y),
|
||||||
|
_ => (AnyPerpendicular(axis), size.Length * 0.8, size.Length * 0.8), // 自訂平面
|
||||||
};
|
};
|
||||||
|
lenDir.Normalize();
|
||||||
_sectionPlaneVisual = new RectangleVisual3D
|
_sectionPlaneVisual = new RectangleVisual3D
|
||||||
{
|
{
|
||||||
Origin = planePoint,
|
Origin = planePoint,
|
||||||
@@ -168,4 +288,14 @@ public partial class MainViewModel
|
|||||||
};
|
};
|
||||||
_viewport!.Children.Add(_sectionPlaneVisual);
|
_viewport!.Children.Add(_sectionPlaneVisual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>取任一與 n 垂直的單位向量(自訂平面的指示矩形方向用)</summary>
|
||||||
|
private static Vector3D AnyPerpendicular(Vector3D n)
|
||||||
|
{
|
||||||
|
Vector3D v = Math.Abs(n.Z) < 0.9
|
||||||
|
? Vector3D.CrossProduct(n, new Vector3D(0, 0, 1))
|
||||||
|
: Vector3D.CrossProduct(n, new Vector3D(1, 0, 0));
|
||||||
|
v.Normalize();
|
||||||
|
return v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,16 +61,55 @@ public partial class MainViewModel : ObservableObject
|
|||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyCanExecuteChangedFor(nameof(ImportCommand))]
|
[NotifyCanExecuteChangedFor(nameof(ImportCommand))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(ImportRecentCommand))]
|
||||||
[NotifyCanExecuteChangedFor(nameof(RotateRootCommand))]
|
[NotifyCanExecuteChangedFor(nameof(RotateRootCommand))]
|
||||||
[NotifyCanExecuteChangedFor(nameof(CheckInterferenceCommand))]
|
[NotifyCanExecuteChangedFor(nameof(CheckInterferenceCommand))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(ExportStepFileCommand))]
|
||||||
private bool isBusy;
|
private bool isBusy;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool useInch;
|
private bool useInch;
|
||||||
|
|
||||||
|
/// <summary>樹面板名稱過濾字串(只影響樹顯示,不影響 3D 可見性)</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private string? treeFilter;
|
||||||
|
|
||||||
|
partial void OnTreeFilterChanged(string? value)
|
||||||
|
{
|
||||||
|
foreach (ModelNodeViewModel root in Roots) root.ApplyFilter(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>最近開啟檔案(MRU,最多 10 筆;隨 settings.json 保存)</summary>
|
||||||
|
public ObservableCollection<string> RecentFiles { get; } = new();
|
||||||
|
|
||||||
public ObservableCollection<ModelNodeViewModel> Roots { get; } = new();
|
public ObservableCollection<ModelNodeViewModel> Roots { get; } = new();
|
||||||
public ObservableCollection<MeasurementResult> Measurements { get; } = new();
|
public ObservableCollection<MeasurementResult> Measurements { get; } = new();
|
||||||
|
|
||||||
|
// ─── 使用者設定(視窗由 MainWindow 處理;VM 管單位 + MRU)─────
|
||||||
|
|
||||||
|
public void LoadSettings(AppSettings s)
|
||||||
|
{
|
||||||
|
UseInch = s.UseInch;
|
||||||
|
RecentFiles.Clear();
|
||||||
|
foreach (string f in s.RecentFiles.Where(File.Exists).Take(SettingsService.MaxRecentFiles))
|
||||||
|
RecentFiles.Add(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveSettingsInto(AppSettings s)
|
||||||
|
{
|
||||||
|
s.UseInch = UseInch;
|
||||||
|
s.RecentFiles = RecentFiles.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TouchRecent(string path)
|
||||||
|
{
|
||||||
|
string full = Path.GetFullPath(path);
|
||||||
|
RecentFiles.Remove(full);
|
||||||
|
RecentFiles.Insert(0, full);
|
||||||
|
while (RecentFiles.Count > SettingsService.MaxRecentFiles)
|
||||||
|
RecentFiles.RemoveAt(RecentFiles.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
public MainViewModel()
|
public MainViewModel()
|
||||||
{
|
{
|
||||||
_sectionTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
|
_sectionTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
|
||||||
@@ -140,6 +179,10 @@ public partial class MainViewModel : ObservableObject
|
|||||||
await ImportFilesAsync(dlg.FileNames);
|
await ImportFilesAsync(dlg.FileNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>MRU 下拉點選(檔案可能已被移走 → ImportFilesAsync 會報匯入失敗)</summary>
|
||||||
|
[RelayCommand(CanExecute = nameof(IsIdle))]
|
||||||
|
private Task ImportRecentAsync(string path) => ImportFilesAsync(new[] { path });
|
||||||
|
|
||||||
public async Task ImportFilesAsync(IEnumerable<string> paths)
|
public async Task ImportFilesAsync(IEnumerable<string> paths)
|
||||||
{
|
{
|
||||||
if (_viewport is null) return;
|
if (_viewport is null) return;
|
||||||
@@ -162,6 +205,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
ImportedFileData data = await Task.Run(() => _importService.Import(path));
|
ImportedFileData data = await Task.Run(() => _importService.Import(path));
|
||||||
BuildRoot(data);
|
BuildRoot(data);
|
||||||
|
TouchRecent(path);
|
||||||
ok++;
|
ok++;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -200,6 +244,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
root.ShowEdges = false;
|
root.ShowEdges = false;
|
||||||
|
|
||||||
Roots.Add(root);
|
Roots.Add(root);
|
||||||
|
if (!string.IsNullOrWhiteSpace(TreeFilter)) root.ApplyFilter(TreeFilter); // 新檔套用目前過濾
|
||||||
foreach (ModelNodeViewModel leaf in root.Leaves())
|
foreach (ModelNodeViewModel leaf in root.Leaves())
|
||||||
{
|
{
|
||||||
leaf.VisualStateChanged += _ => SyncLeafVisuals(root);
|
leaf.VisualStateChanged += _ => SyncLeafVisuals(root);
|
||||||
@@ -410,6 +455,44 @@ public partial class MainViewModel : ObservableObject
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ZoomAll() => _viewport?.ZoomExtents(400);
|
private void ZoomAll() => _viewport?.ZoomExtents(400);
|
||||||
|
|
||||||
|
/// <summary>標準視圖(機構慣例 Z 向上):Iso / Front / Back / Top / Bottom / Left / Right</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void SetStandardView(string name)
|
||||||
|
{
|
||||||
|
if (_viewport?.Camera is not ProjectionCamera cam) return;
|
||||||
|
(Vector3D dir, Vector3D up) = name switch
|
||||||
|
{
|
||||||
|
"Front" => (new Vector3D(0, 1, 0), new Vector3D(0, 0, 1)), // 從 -Y 往 +Y 看
|
||||||
|
"Back" => (new Vector3D(0, -1, 0), new Vector3D(0, 0, 1)),
|
||||||
|
"Top" => (new Vector3D(0, 0, -1), new Vector3D(0, 1, 0)),
|
||||||
|
"Bottom" => (new Vector3D(0, 0, 1), new Vector3D(0, -1, 0)),
|
||||||
|
"Right" => (new Vector3D(-1, 0, 0), new Vector3D(0, 0, 1)),
|
||||||
|
"Left" => (new Vector3D(1, 0, 0), new Vector3D(0, 0, 1)),
|
||||||
|
_ => (new Vector3D(-1, -1, -1), new Vector3D(0, 0, 1)), // Iso
|
||||||
|
};
|
||||||
|
dir.Normalize();
|
||||||
|
Rect3D b = UnionBounds();
|
||||||
|
Point3D center = b.IsEmpty ? new Point3D()
|
||||||
|
: new Point3D(b.X + b.SizeX / 2, b.Y + b.SizeY / 2, b.Z + b.SizeZ / 2);
|
||||||
|
double dist = Math.Max(SceneDiagonal() * 1.8, 10);
|
||||||
|
cam.Position = center - dir * dist;
|
||||||
|
cam.LookDirection = dir * dist; // 目標點 = Position + LookDirection = 場景中心
|
||||||
|
cam.UpDirection = up;
|
||||||
|
_viewport.ZoomExtents(400);
|
||||||
|
StatusText = $"視圖:{name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>正交投影切換(平行投影,量尺寸不受透視變形;Helix 保持視角換相機型別)</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool orthographicView;
|
||||||
|
|
||||||
|
partial void OnOrthographicViewChanged(bool value)
|
||||||
|
{
|
||||||
|
if (_viewport is null) return;
|
||||||
|
_viewport.Orthographic = value;
|
||||||
|
StatusText = value ? "正交投影(平行投影,適合尺寸確認)" : "透視投影";
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void RemoveRoot(ModelNodeViewModel root)
|
private void RemoveRoot(ModelNodeViewModel root)
|
||||||
{
|
{
|
||||||
@@ -440,6 +523,92 @@ public partial class MainViewModel : ObservableObject
|
|||||||
StatusText = $"已移除:{root.Name}";
|
StatusText = $"已移除:{root.Name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 顯示控制 / 體積(樹右鍵選單)────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>只顯示此節點(其餘全部隱藏;「全部顯示」復原)</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void IsolateNode(ModelNodeViewModel node)
|
||||||
|
{
|
||||||
|
foreach (ModelNodeViewModel r in Roots) SetVisibleRecursive(r, false);
|
||||||
|
SetVisibleRecursive(node, true);
|
||||||
|
StatusText = $"只顯示「{node.Name}」(樹右鍵 → 全部顯示 復原)";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void InvertVisibility()
|
||||||
|
{
|
||||||
|
foreach (ModelNodeViewModel l in Roots.SelectMany(r => r.Leaves()))
|
||||||
|
l.IsVisible = !l.IsVisible;
|
||||||
|
StatusText = "已反轉顯示";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ShowAll()
|
||||||
|
{
|
||||||
|
foreach (ModelNodeViewModel r in Roots) SetVisibleRecursive(r, true);
|
||||||
|
StatusText = "已全部顯示";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>逐節點顯式遞迴設定可見性。不能只設 root(setter 同值不觸發 cascade,
|
||||||
|
/// 之前的隔離操作可能留下「父關子開」的混合狀態)</summary>
|
||||||
|
private static void SetVisibleRecursive(ModelNodeViewModel node, bool visible)
|
||||||
|
{
|
||||||
|
node.IsVisible = visible;
|
||||||
|
foreach (ModelNodeViewModel c in node.Children) SetVisibleRecursive(c, visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>體積/質心(網格 signed volume,封閉實體才可靠)→ 加進量測結果 + 質心標記</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void MeasureVolume(ModelNodeViewModel node)
|
||||||
|
{
|
||||||
|
var meshes = node.Leaves()
|
||||||
|
.Where(l => l.FacesContent is not null)
|
||||||
|
.SelectMany(l => l.FacesContent!.Children)
|
||||||
|
.OfType<GeometryModel3D>()
|
||||||
|
.Where(g => _faceMap.ContainsKey(g))
|
||||||
|
.Select(g => _faceMap[g].Mesh)
|
||||||
|
.ToList();
|
||||||
|
if (meshes.Count == 0)
|
||||||
|
{
|
||||||
|
StatusText = $"「{node.Name}」沒有可計算的面網格(線架構無體積)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(double vol, Point3D centroid, bool reliable) = MeasurementService.MeshVolume(meshes);
|
||||||
|
if (!reliable)
|
||||||
|
{
|
||||||
|
StatusText = $"「{node.Name}」非封閉網格(開放殼),無法可靠計算體積";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double diag = SceneDiagonal();
|
||||||
|
double markerR = Math.Clamp(diag * 0.004, 0.01, 5.0);
|
||||||
|
string label = NextLabel(MeasureMode.Volume, "V");
|
||||||
|
string name = node.Name;
|
||||||
|
Rect3D b = node.Bounds;
|
||||||
|
var m = new MeasurementResult
|
||||||
|
{
|
||||||
|
Kind = MeasureMode.Volume,
|
||||||
|
TitleFor = u => $"{label} {name} V ≈ {Units.V(vol, u)}",
|
||||||
|
DetailFor = u => $"體積 ≈ {Units.V(vol, u)}(網格近似)\n質心 {Units.P(centroid, u)}\n" +
|
||||||
|
$"外形(AABB){Units.C(b.SizeX, u)} × {Units.C(b.SizeY, u)} × {Units.C(b.SizeZ, u)} " +
|
||||||
|
(u == UnitSystem.Millimeter ? "mm" : "in"),
|
||||||
|
};
|
||||||
|
m.Overlays.Add(new SphereVisual3D { Center = centroid, Radius = markerR, Fill = Brushes.OrangeRed });
|
||||||
|
var volLabel = new BillboardTextVisual3D
|
||||||
|
{
|
||||||
|
Position = centroid + new Vector3D(0, 0, markerR * 2),
|
||||||
|
Text = $"{label} {Units.V(vol, UnitSystem.Millimeter)}",
|
||||||
|
Foreground = Brushes.Black,
|
||||||
|
Background = new SolidColorBrush(Color.FromArgb(200, 255, 255, 210)),
|
||||||
|
Padding = new Thickness(4, 2, 4, 2),
|
||||||
|
FontSize = 14,
|
||||||
|
};
|
||||||
|
m.Overlays.Add(volLabel);
|
||||||
|
m.DynamicLabels.Add((volLabel, u => $"{label} {Units.V(vol, u)}"));
|
||||||
|
AddMeasurement(m);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 量測 ────────────────────────────────────────────────────
|
// ─── 量測 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
partial void OnCurrentModeChanged(MeasureMode value)
|
partial void OnCurrentModeChanged(MeasureMode value)
|
||||||
@@ -470,10 +639,52 @@ public partial class MainViewModel : ObservableObject
|
|||||||
StatusText = value ? "顯示單位:inch" : "顯示單位:mm";
|
StatusText = value ? "顯示單位:inch" : "顯示單位:mm";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Esc(MainWindow 轉發):先取消進行中的多段量測(保留模式),再按退回瀏覽模式;再退操作器</summary>
|
||||||
|
public void OnEscape()
|
||||||
|
{
|
||||||
|
if (CancelSectionPickIfActive()) return; // 3點剖面定義中 → 先取消定義
|
||||||
|
bool hadPending = _pendingPoint is not null || _pendingDirection is not null ||
|
||||||
|
_pendingFace is not null || _pendingAlign is not null || _align3.Count > 0;
|
||||||
|
if (hadPending)
|
||||||
|
{
|
||||||
|
CancelPending();
|
||||||
|
StatusText = "已取消目前量測起點(再按 Esc 退出量測模式)";
|
||||||
|
}
|
||||||
|
else if (CurrentMode != MeasureMode.None)
|
||||||
|
{
|
||||||
|
CurrentMode = MeasureMode.None;
|
||||||
|
}
|
||||||
|
else if (GizmoEnabled)
|
||||||
|
{
|
||||||
|
GizmoEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>量測模式快速鍵(MainWindow 轉發;無修飾鍵)。同鍵再按 = 退回瀏覽。回傳 false 表示非快速鍵。</summary>
|
||||||
|
public bool OnHotKey(System.Windows.Input.Key key)
|
||||||
|
{
|
||||||
|
MeasureMode? m = key switch
|
||||||
|
{
|
||||||
|
System.Windows.Input.Key.P => MeasureMode.Point,
|
||||||
|
System.Windows.Input.Key.D => MeasureMode.Distance,
|
||||||
|
System.Windows.Input.Key.E => MeasureMode.Edge,
|
||||||
|
System.Windows.Input.Key.F => MeasureMode.Face,
|
||||||
|
System.Windows.Input.Key.C => MeasureMode.Circle,
|
||||||
|
System.Windows.Input.Key.A => MeasureMode.Angle,
|
||||||
|
System.Windows.Input.Key.M => MeasureMode.FaceDistance,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
if (m is null) return false;
|
||||||
|
CurrentMode = CurrentMode == m ? MeasureMode.None : m.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>MainWindow 滑鼠左鍵點擊轉發進來</summary>
|
/// <summary>MainWindow 滑鼠左鍵點擊轉發進來</summary>
|
||||||
public void OnViewportClick(Point position)
|
public void OnViewportClick(Point position)
|
||||||
{
|
{
|
||||||
if (_viewport is null || IsBusy || CurrentMode is MeasureMode.None or MeasureMode.Drag) return;
|
if (_viewport is null || IsBusy) return;
|
||||||
|
if (HandleSectionPlanePick(position)) return; // 3點剖面定義模式優先吃點擊(v0.5.0)
|
||||||
|
if (CurrentMode is MeasureMode.None or MeasureMode.Drag) return;
|
||||||
|
|
||||||
var hits = _viewport.Viewport.FindHits(position);
|
var hits = _viewport.Viewport.FindHits(position);
|
||||||
FaceInfo? fi = null;
|
FaceInfo? fi = null;
|
||||||
|
|||||||
@@ -57,8 +57,24 @@ public partial class ModelNodeViewModel : ObservableObject
|
|||||||
public int FaceCount { get; init; }
|
public int FaceCount { get; init; }
|
||||||
public int TriangleCount { get; init; }
|
public int TriangleCount { get; init; }
|
||||||
|
|
||||||
/// <summary>世界座標邊界(零件平移後同步位移)</summary>
|
/// <summary>世界座標邊界(零件平移/旋轉後由 RecomputeBounds 更新;連動 Stats/Tooltip 的外形尺寸)</summary>
|
||||||
public Rect3D Bounds { get; set; }
|
public Rect3D Bounds
|
||||||
|
{
|
||||||
|
get => _bounds;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_bounds == value) return;
|
||||||
|
_bounds = value;
|
||||||
|
OnPropertyChanged(nameof(Stats));
|
||||||
|
OnPropertyChanged(nameof(ToolTipText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private Rect3D _bounds = Rect3D.Empty;
|
||||||
|
|
||||||
|
/// <summary>外形尺寸 L×W×H(AABB,mm)</summary>
|
||||||
|
public string SizeText => Bounds.IsEmpty
|
||||||
|
? ""
|
||||||
|
: $"{Bounds.SizeX:F1} × {Bounds.SizeY:F1} × {Bounds.SizeZ:F1} mm";
|
||||||
|
|
||||||
/// <summary>來源 B-rep 物件(零件平移時整體 Modify,避免共用邊被重複位移)</summary>
|
/// <summary>來源 B-rep 物件(零件平移時整體 Modify,避免共用邊被重複位移)</summary>
|
||||||
public List<CADability.GeoObject.IGeoObject> SourceGeos { get; } = new();
|
public List<CADability.GeoObject.IGeoObject> SourceGeos { get; } = new();
|
||||||
@@ -67,10 +83,32 @@ public partial class ModelNodeViewModel : ObservableObject
|
|||||||
public bool HasBrep { get; init; } = true;
|
public bool HasBrep { get; init; } = true;
|
||||||
|
|
||||||
public string Stats =>
|
public string Stats =>
|
||||||
$"{SolidCount} 實體 · {FaceCount} 面 · {TriangleCount:N0} △" + (HasBrep ? "" : " · 無B-rep");
|
$"{SolidCount} 實體 · {FaceCount} 面 · {TriangleCount:N0} △" + (HasBrep ? "" : " · 無B-rep") +
|
||||||
|
(Bounds.IsEmpty ? "" : $"\n外形 {SizeText}");
|
||||||
|
|
||||||
public string ToolTipText => (FilePath is not null ? FilePath + "\n" : "") + Stats;
|
public string ToolTipText => (FilePath is not null ? FilePath + "\n" : "") + Stats;
|
||||||
|
|
||||||
|
// ── 樹搜尋/過濾(只影響樹面板顯示,不影響 3D 可見性)──
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isFilterVisible = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 套用名稱過濾:符合者 + 其祖先 + 其子樹 顯示。空字串 = 全顯。
|
||||||
|
/// 過濾中有符合的子孫時自動展開。回傳此節點是否顯示。
|
||||||
|
/// </summary>
|
||||||
|
public bool ApplyFilter(string? filter, bool ancestorMatched = false)
|
||||||
|
{
|
||||||
|
bool empty = string.IsNullOrWhiteSpace(filter);
|
||||||
|
bool selfMatch = !empty && Name.Contains(filter!, StringComparison.OrdinalIgnoreCase);
|
||||||
|
bool anyChild = false;
|
||||||
|
foreach (ModelNodeViewModel c in Children)
|
||||||
|
anyChild |= c.ApplyFilter(filter, ancestorMatched || selfMatch);
|
||||||
|
IsFilterVisible = empty || selfMatch || ancestorMatched || anyChild;
|
||||||
|
if (!empty && anyChild) IsExpanded = true;
|
||||||
|
return IsFilterVisible;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 視覺物件 ──
|
// ── 視覺物件 ──
|
||||||
public ModelVisual3D? BodyVisual { get; set; } // leaf:面網格容器(內容在兩種模式間切換)
|
public ModelVisual3D? BodyVisual { get; set; } // leaf:面網格容器(內容在兩種模式間切換)
|
||||||
public Model3DGroup? FacesContent { get; set; } // leaf:逐面 GeometryModel3D(量測拾取用)
|
public Model3DGroup? FacesContent { get; set; } // leaf:逐面 GeometryModel3D(量測拾取用)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using CADability;
|
||||||
|
using CADability.GeoObject;
|
||||||
|
using STPViewer.Services;
|
||||||
|
using IOPath = System.IO.Path; // CADability.GeoObject.Path(曲線)撞名
|
||||||
|
|
||||||
|
namespace SmokeTest;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// STEP 匯出往返驗證(v0.5.0 匯出 STEP 功能的無 UI 驗證):
|
||||||
|
/// 匯入來源 STEP → 走與 MainViewModel.ExportStepFile 相同路徑寫出新檔 → 重新匯入比對實體數。
|
||||||
|
/// </summary>
|
||||||
|
public static class ExportTest
|
||||||
|
{
|
||||||
|
public static int Run(string sourcePath, string outPath)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"=== ExportTest: {IOPath.GetFileName(sourcePath)} → {IOPath.GetFileName(outPath)} ===");
|
||||||
|
ImportedFileData data = new StepImportService().Import(sourcePath);
|
||||||
|
var geos = Collect(data.Root).Where(g => g is Solid or Shell).ToList();
|
||||||
|
Console.WriteLine($" 來源 Solid/Shell 物件 = {geos.Count}(Solids = {data.Root.SolidCount})");
|
||||||
|
|
||||||
|
Project project = Project.CreateSimpleProject();
|
||||||
|
Model model = project.GetActiveModel();
|
||||||
|
foreach (IGeoObject g in geos) model.Add(g);
|
||||||
|
new ExportStep().WriteToFile(outPath, project);
|
||||||
|
var fi = new FileInfo(outPath);
|
||||||
|
Console.WriteLine($" 已寫出({fi.Length:N0} bytes)");
|
||||||
|
if (fi.Length < 100)
|
||||||
|
{
|
||||||
|
Console.WriteLine("ExportTest: FAIL(輸出檔近乎空白)");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportedFileData back = new StepImportService().Import(outPath);
|
||||||
|
Console.WriteLine($" 回讀 Solids = {back.Root.SolidCount}, Faces = {back.Root.FaceCount}");
|
||||||
|
bool ok = back.Root.SolidCount == data.Root.SolidCount && back.Root.FaceCount > 0;
|
||||||
|
Console.WriteLine(ok
|
||||||
|
? "ExportTest: PASS(實體數一致)"
|
||||||
|
: $"ExportTest: FAIL(來源 Solids {data.Root.SolidCount} ↔ 回讀 {back.Root.SolidCount})");
|
||||||
|
return ok ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<IGeoObject> Collect(ImportedNode n)
|
||||||
|
{
|
||||||
|
foreach (IGeoObject g in n.SourceGeos) yield return g;
|
||||||
|
foreach (ImportedNode c in n.Children)
|
||||||
|
foreach (IGeoObject g in Collect(c))
|
||||||
|
yield return g;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,9 @@ if (args[0] == "--align-test")
|
|||||||
if (args[0] == "--bbox")
|
if (args[0] == "--bbox")
|
||||||
return SmokeTest.BBoxDump.Run(args[1]);
|
return SmokeTest.BBoxDump.Run(args[1]);
|
||||||
|
|
||||||
|
if (args[0] == "--export-test")
|
||||||
|
return SmokeTest.ExportTest.Run(args[1], args[2]) == 0 ? 0 : 2;
|
||||||
|
|
||||||
var service = new StepImportService { Progress = s => Console.WriteLine($" [階段] {s}") };
|
var service = new StepImportService { Progress = s => Console.WriteLine($" [階段] {s}") };
|
||||||
int failed = 0;
|
int failed = 0;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user