fix: stabilize step tree deletion save flow
This commit is contained in:
parent
a87affac10
commit
03497f97a7
@ -153,6 +153,13 @@ export class Navigator
|
|||||||
|
|
||||||
FillTree (importResult)
|
FillTree (importResult)
|
||||||
{
|
{
|
||||||
|
console.log ('[step-demo] FillTree start', {
|
||||||
|
usedFiles : importResult.usedFiles ? importResult.usedFiles.length : null,
|
||||||
|
missingFiles : importResult.missingFiles ? importResult.missingFiles.length : null
|
||||||
|
});
|
||||||
|
this.filesPanel.Clear ();
|
||||||
|
this.materialsPanel.Clear ();
|
||||||
|
this.meshesPanel.Clear ();
|
||||||
this.filesPanel.Fill (importResult);
|
this.filesPanel.Fill (importResult);
|
||||||
if (importResult.missingFiles.length === 0) {
|
if (importResult.missingFiles.length === 0) {
|
||||||
this.panelSet.SetPanelIcon (this.filesPanel, 'files');
|
this.panelSet.SetPanelIcon (this.filesPanel, 'files');
|
||||||
@ -161,6 +168,10 @@ export class Navigator
|
|||||||
}
|
}
|
||||||
this.materialsPanel.Fill (importResult);
|
this.materialsPanel.Fill (importResult);
|
||||||
this.meshesPanel.Fill (importResult);
|
this.meshesPanel.Fill (importResult);
|
||||||
|
console.log ('[step-demo] FillTree done', {
|
||||||
|
meshItems : this.meshesPanel.MeshItemCount (),
|
||||||
|
rootChildren : this.meshesPanel.rootItem !== null ? this.meshesPanel.rootItem.children.length : null
|
||||||
|
});
|
||||||
this.OnSelectionChanged ();
|
this.OnSelectionChanged ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { MeshInstanceId } from '../engine/model/meshinstance.js';
|
import { MeshInstanceId } from '../engine/model/meshinstance.js';
|
||||||
import { AddDiv, CreateDiv, ShowDomElement, ClearDomElement, InsertDomElementBefore, SetDomElementHeight, GetDomElementOuterHeight, IsDomElementVisible } from '../engine/viewer/domutils.js';
|
import { AddDiv, CreateDiv, ShowDomElement, ClearDomElement, InsertDomElementBefore, SetDomElementHeight, GetDomElementOuterHeight, IsDomElementVisible, AddButtonElement } from '../engine/viewer/domutils.js';
|
||||||
import { CalculatePopupPositionToElementBottomRight, ShowListPopup } from './dialogs.js';
|
import { CalculatePopupPositionToElementBottomRight, ShowListPopup } from './dialogs.js';
|
||||||
import { MeshItem, NavigatorItemRecurse, NodeItem } from './navigatoritems.js';
|
import { MeshItem, NavigatorItemRecurse, NodeItem } from './navigatoritems.js';
|
||||||
import { NavigatorPanel, NavigatorPopupButton } from './navigatorpanel.js';
|
import { NavigatorPanel, NavigatorPopupButton } from './navigatorpanel.js';
|
||||||
import { AddSvgIconElement, GetMaterialName, GetMeshName, GetNodeName, SetSvgIconImageElement } from './utils.js';
|
import { AddSvgIconElement, GetMaterialName, GetMeshName, GetNodeName, SetElementAccessibleName, SetSvgIconImageElement } from './utils.js';
|
||||||
import { Loc, FLoc } from '../engine/core/localization.js';
|
import { Loc, FLoc } from '../engine/core/localization.js';
|
||||||
|
|
||||||
const MeshesPanelMode =
|
const MeshesPanelMode =
|
||||||
@ -30,6 +30,7 @@ class NavigatorMaterialsPopupButton extends NavigatorPopupButton
|
|||||||
|
|
||||||
let materialsText = FLoc ('Materials ({0})', this.materialInfoArray.length);
|
let materialsText = FLoc ('Materials ({0})', this.materialInfoArray.length);
|
||||||
this.buttonText.innerHTML = materialsText;
|
this.buttonText.innerHTML = materialsText;
|
||||||
|
SetElementAccessibleName (this.button, materialsText);
|
||||||
}
|
}
|
||||||
|
|
||||||
OnButtonClick ()
|
OnButtonClick ()
|
||||||
@ -110,6 +111,10 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
|||||||
|
|
||||||
Clear ()
|
Clear ()
|
||||||
{
|
{
|
||||||
|
console.log ('[step-demo] MeshesPanel.Clear', {
|
||||||
|
nodeItems : this.nodeIdToItem.size,
|
||||||
|
meshItems : this.meshInstanceIdToItem.size
|
||||||
|
});
|
||||||
this.ClearMeshTree ();
|
this.ClearMeshTree ();
|
||||||
ClearDomElement (this.titleButtonsDiv);
|
ClearDomElement (this.titleButtonsDiv);
|
||||||
ClearDomElement (this.buttonsDiv);
|
ClearDomElement (this.buttonsDiv);
|
||||||
@ -180,9 +185,8 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
|||||||
{
|
{
|
||||||
function CreateButton (parentDiv, button, className, onClick)
|
function CreateButton (parentDiv, button, className, onClick)
|
||||||
{
|
{
|
||||||
button.div = AddDiv (parentDiv, 'ov_navigator_button');
|
button.div = AddButtonElement (parentDiv, 'ov_navigator_button');
|
||||||
button.div.setAttribute ('alt', button.name);
|
SetElementAccessibleName (button.div, button.name);
|
||||||
button.div.setAttribute ('title', button.name);
|
|
||||||
if (className) {
|
if (className) {
|
||||||
button.div.classList.add (className);
|
button.div.classList.add (className);
|
||||||
}
|
}
|
||||||
@ -383,9 +387,10 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
|||||||
function AddModelNodeToTree (panel, model, node, parentItem, mode)
|
function AddModelNodeToTree (panel, model, node, parentItem, mode)
|
||||||
{
|
{
|
||||||
if (panel.stepDeletionState !== null && panel.stepDeletionState.IsNodeDeletedByPath (panel.stepDeletionState.GetNodePath (node.GetId ()))) {
|
if (panel.stepDeletionState !== null && panel.stepDeletionState.IsNodeDeletedByPath (panel.stepDeletionState.GetNodePath (node.GetId ()))) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hasVisibleContent = false;
|
||||||
let meshNodes = [];
|
let meshNodes = [];
|
||||||
for (let childNode of node.GetChildNodes ()) {
|
for (let childNode of node.GetChildNodes ()) {
|
||||||
if (mode === MeshesPanelMode.TreeView) {
|
if (mode === MeshesPanelMode.TreeView) {
|
||||||
@ -393,15 +398,24 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
|||||||
meshNodes.push (childNode);
|
meshNodes.push (childNode);
|
||||||
} else {
|
} else {
|
||||||
let nodeItem = CreateNodeItem (panel, childNode);
|
let nodeItem = CreateNodeItem (panel, childNode);
|
||||||
parentItem.AddChild (nodeItem);
|
let childHasVisibleContent = AddModelNodeToTree (panel, model, childNode, nodeItem, mode);
|
||||||
AddModelNodeToTree (panel, model, childNode, nodeItem, mode);
|
if (childHasVisibleContent) {
|
||||||
|
parentItem.AddChild (nodeItem);
|
||||||
|
hasVisibleContent = true;
|
||||||
|
} else {
|
||||||
|
panel.nodeIdToItem.delete (childNode.GetId ());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
AddModelNodeToTree (panel, model, childNode, parentItem, mode);
|
if (AddModelNodeToTree (panel, model, childNode, parentItem, mode)) {
|
||||||
|
hasVisibleContent = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let meshNode of meshNodes) {
|
for (let meshNode of meshNodes) {
|
||||||
AddModelNodeToTree (panel, model, meshNode, parentItem, mode);
|
if (AddModelNodeToTree (panel, model, meshNode, parentItem, mode)) {
|
||||||
|
hasVisibleContent = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (let meshIndex of node.GetMeshIndices ()) {
|
for (let meshIndex of node.GetMeshIndices ()) {
|
||||||
let mesh = model.GetMesh (meshIndex);
|
let mesh = model.GetMesh (meshIndex);
|
||||||
@ -412,12 +426,19 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
AddMeshToNodeTree (panel, node, mesh, meshIndex, parentItem, mode);
|
AddMeshToNodeTree (panel, node, mesh, meshIndex, parentItem, mode);
|
||||||
|
hasVisibleContent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return hasVisibleContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rootNode = model.GetRootNode ();
|
let rootNode = model.GetRootNode ();
|
||||||
this.rootItem = CreateDummyRootItem (this, rootNode);
|
this.rootItem = CreateDummyRootItem (this, rootNode);
|
||||||
AddModelNodeToTree (this, model, rootNode, this.rootItem, this.mode);
|
AddModelNodeToTree (this, model, rootNode, this.rootItem, this.mode);
|
||||||
|
console.log ('[step-demo] FillMeshTree', {
|
||||||
|
rootChildren : this.rootItem.children.length,
|
||||||
|
labels : this.rootItem.children.map ((child) => child.name)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateMaterialList (materialInfoArray)
|
UpdateMaterialList (materialInfoArray)
|
||||||
@ -461,6 +482,12 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
|||||||
IsMeshVisible (meshInstanceId)
|
IsMeshVisible (meshInstanceId)
|
||||||
{
|
{
|
||||||
let meshItem = this.GetMeshItem (meshInstanceId);
|
let meshItem = this.GetMeshItem (meshInstanceId);
|
||||||
|
if (meshItem === undefined) {
|
||||||
|
console.log ('[step-demo] IsMeshVisible missing mesh item', {
|
||||||
|
meshKey : meshInstanceId.GetKey ()
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return meshItem.IsVisible ();
|
return meshItem.IsVisible ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { IsDefined } from '../engine/core/core.js';
|
import { IsDefined } from '../engine/core/core.js';
|
||||||
import { AddDiv, CreateDiv, ShowDomElement, ClearDomElement, InsertDomElementBefore, InsertDomElementAfter } from '../engine/viewer/domutils.js';
|
import { AddDiv, CreateDiv, ShowDomElement, ClearDomElement, InsertDomElementBefore, InsertDomElementAfter } from '../engine/viewer/domutils.js';
|
||||||
import { CreateSvgIconElement, SetSvgIconImageElement } from './utils.js';
|
import { CreateSvgIconElement, SetSvgIconImageElement, CreateSvgIconButtonElement, MakeElementButtonLike } from './utils.js';
|
||||||
|
|
||||||
export function ScrollToView (element)
|
export function ScrollToView (element)
|
||||||
{
|
{
|
||||||
@ -12,11 +12,10 @@ export function ScrollToView (element)
|
|||||||
|
|
||||||
export class TreeViewButton
|
export class TreeViewButton
|
||||||
{
|
{
|
||||||
constructor (imagePath)
|
constructor (imagePath, accessibleName)
|
||||||
{
|
{
|
||||||
this.imagePath = imagePath;
|
this.imagePath = imagePath;
|
||||||
this.mainElement = CreateSvgIconElement (this.imagePath, 'ov_tree_item_button');
|
this.mainElement = CreateSvgIconButtonElement (this.imagePath, 'ov_tree_item_button', accessibleName);
|
||||||
this.mainElement.setAttribute ('src', this.imagePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SetImage (imagePath)
|
SetImage (imagePath)
|
||||||
@ -59,6 +58,7 @@ export class TreeViewItem
|
|||||||
{
|
{
|
||||||
this.mainElement.classList.add ('clickable');
|
this.mainElement.classList.add ('clickable');
|
||||||
this.mainElement.style.cursor = 'pointer';
|
this.mainElement.style.cursor = 'pointer';
|
||||||
|
MakeElementButtonLike (this.mainElement, this.name, onClick);
|
||||||
this.mainElement.addEventListener ('click', onClick);
|
this.mainElement.addEventListener ('click', onClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ export class TreeViewGroupItem extends TreeViewItem
|
|||||||
this.openButtonIcon = 'arrow_down';
|
this.openButtonIcon = 'arrow_down';
|
||||||
this.closeButtonIcon = 'arrow_right';
|
this.closeButtonIcon = 'arrow_right';
|
||||||
|
|
||||||
this.openCloseButton = CreateSvgIconElement (this.openButtonIcon, 'ov_tree_item_icon');
|
this.openCloseButton = CreateSvgIconButtonElement (this.openButtonIcon, 'ov_tree_item_icon ov_tree_item_toggle', 'Toggle children');
|
||||||
InsertDomElementBefore (this.openCloseButton, this.nameElement);
|
InsertDomElementBefore (this.openCloseButton, this.nameElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,7 +185,9 @@ export class TreeViewGroupItem extends TreeViewItem
|
|||||||
{
|
{
|
||||||
if (this.childrenDiv === null) {
|
if (this.childrenDiv === null) {
|
||||||
this.childrenDiv = CreateDiv ('ov_tree_view_children');
|
this.childrenDiv = CreateDiv ('ov_tree_view_children');
|
||||||
InsertDomElementAfter (this.childrenDiv, this.mainElement);
|
if (this.mainElement.parentNode !== null) {
|
||||||
|
InsertDomElementAfter (this.childrenDiv, this.mainElement);
|
||||||
|
}
|
||||||
this.Show (this.isVisible);
|
this.Show (this.isVisible);
|
||||||
this.ShowChildren (this.isChildrenVisible);
|
this.ShowChildren (this.isChildrenVisible);
|
||||||
this.openCloseButton.addEventListener ('click', (ev) => {
|
this.openCloseButton.addEventListener ('click', (ev) => {
|
||||||
@ -196,6 +198,14 @@ export class TreeViewGroupItem extends TreeViewItem
|
|||||||
}
|
}
|
||||||
return this.childrenDiv;
|
return this.childrenDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AddDomElements (parentDiv)
|
||||||
|
{
|
||||||
|
super.AddDomElements (parentDiv);
|
||||||
|
if (this.childrenDiv !== null && this.childrenDiv.parentNode === null) {
|
||||||
|
InsertDomElementAfter (this.childrenDiv, this.mainElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TreeViewGroupButtonItem extends TreeViewGroupItem
|
export class TreeViewGroupButtonItem extends TreeViewGroupItem
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { ImportErrorCode, ImportSettings } from '../engine/import/importer.js';
|
|||||||
import { NavigationMode, ProjectionMode } from '../engine/viewer/camera.js';
|
import { NavigationMode, ProjectionMode } from '../engine/viewer/camera.js';
|
||||||
import { RGBColor } from '../engine/model/color.js';
|
import { RGBColor } from '../engine/model/color.js';
|
||||||
import { Viewer } from '../engine/viewer/viewer.js';
|
import { Viewer } from '../engine/viewer/viewer.js';
|
||||||
import { AddDiv, AddDomElement, ShowDomElement, SetDomElementOuterHeight, CreateDomElement, GetDomElementOuterWidth } from '../engine/viewer/domutils.js';
|
import { AddDiv, AddDomElement, ShowDomElement, SetDomElementOuterHeight, CreateDomElement, GetDomElementOuterWidth, AddButtonElement } from '../engine/viewer/domutils.js';
|
||||||
import { CalculatePopupPositionToScreen, ShowListPopup, ShowMessageDialog } from './dialogs.js';
|
import { CalculatePopupPositionToScreen, ShowListPopup, ShowMessageDialog } from './dialogs.js';
|
||||||
import { HandleEvent } from './eventhandler.js';
|
import { HandleEvent } from './eventhandler.js';
|
||||||
import { HashHandler } from './hashhandler.js';
|
import { HashHandler } from './hashhandler.js';
|
||||||
@ -16,7 +16,7 @@ import { ThreeModelLoaderUI } from './threemodelloaderui.js';
|
|||||||
import { Toolbar } from './toolbar.js';
|
import { Toolbar } from './toolbar.js';
|
||||||
import { DownloadModel, ShowExportDialog } from './exportdialog.js';
|
import { DownloadModel, ShowExportDialog } from './exportdialog.js';
|
||||||
import { ShowSnapshotDialog } from './snapshotdialog.js';
|
import { ShowSnapshotDialog } from './snapshotdialog.js';
|
||||||
import { AddSvgIconElement, DownloadUrlAsFile, GetFilesFromDataTransfer, InstallTooltip, IsSmallWidth } from './utils.js';
|
import { AddSvgIconElement, DownloadUrlAsFile, GetFilesFromDataTransfer, InstallTooltip, IsSmallWidth, SetElementAccessibleName } from './utils.js';
|
||||||
import { ShowOpenUrlDialog } from './openurldialog.js';
|
import { ShowOpenUrlDialog } from './openurldialog.js';
|
||||||
import { ShowSharingDialog } from './sharingdialog.js';
|
import { ShowSharingDialog } from './sharingdialog.js';
|
||||||
import { GetDefaultMaterials, ReplaceDefaultMaterialsColor } from '../engine/model/modelutils.js';
|
import { GetDefaultMaterials, ReplaceDefaultMaterialsColor } from '../engine/model/modelutils.js';
|
||||||
@ -201,6 +201,7 @@ export class Website
|
|||||||
this.uiState = WebsiteUIState.Undefined;
|
this.uiState = WebsiteUIState.Undefined;
|
||||||
this.layouter = new WebsiteLayouter (this.parameters, this.navigator, this.sidebar, this.viewer, this.measureTool);
|
this.layouter = new WebsiteLayouter (this.parameters, this.navigator, this.sidebar, this.viewer, this.measureTool);
|
||||||
this.model = null;
|
this.model = null;
|
||||||
|
this.currentImportResult = null;
|
||||||
this.stepDeletionState = null;
|
this.stepDeletionState = null;
|
||||||
this.deleteSelectedButton = null;
|
this.deleteSelectedButton = null;
|
||||||
this.stepSaveButton = null;
|
this.stepSaveButton = null;
|
||||||
@ -228,7 +229,17 @@ export class Website
|
|||||||
this.InitDragAndDrop ();
|
this.InitDragAndDrop ();
|
||||||
this.InitSidebar ();
|
this.InitSidebar ();
|
||||||
this.InitNavigator ();
|
this.InitNavigator ();
|
||||||
this.InitCookieConsent ();
|
if (window.requestIdleCallback) {
|
||||||
|
window.requestIdleCallback (() => {
|
||||||
|
this.InitCookieConsent ();
|
||||||
|
}, {
|
||||||
|
timeout : 2000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.setTimeout (() => {
|
||||||
|
this.InitCookieConsent ();
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
|
||||||
this.viewer.SetMouseClickHandler (this.OnModelClicked.bind (this));
|
this.viewer.SetMouseClickHandler (this.OnModelClicked.bind (this));
|
||||||
this.viewer.SetMouseMoveHandler (this.OnModelMouseMoved.bind (this));
|
this.viewer.SetMouseMoveHandler (this.OnModelMouseMoved.bind (this));
|
||||||
@ -289,6 +300,7 @@ export class Website
|
|||||||
CloseAllDialogs ();
|
CloseAllDialogs ();
|
||||||
|
|
||||||
this.model = null;
|
this.model = null;
|
||||||
|
this.currentImportResult = null;
|
||||||
this.stepDeletionState = null;
|
this.stepDeletionState = null;
|
||||||
this.viewer.Clear ();
|
this.viewer.Clear ();
|
||||||
|
|
||||||
@ -304,7 +316,8 @@ export class Website
|
|||||||
OnModelLoaded (importResult, threeObject)
|
OnModelLoaded (importResult, threeObject)
|
||||||
{
|
{
|
||||||
this.model = importResult.model;
|
this.model = importResult.model;
|
||||||
this.parameters.fileNameDiv.innerHTML = importResult.mainFile;
|
this.currentImportResult = importResult;
|
||||||
|
this.parameters.fileNameDiv.textContent = importResult.mainFile;
|
||||||
let importedExtension = GetFileExtension (importResult.mainFile);
|
let importedExtension = GetFileExtension (importResult.mainFile);
|
||||||
if (importedExtension === 'stp' || importedExtension === 'step') {
|
if (importedExtension === 'stp' || importedExtension === 'step') {
|
||||||
this.stepDeletionState = new StepDeletionState (importResult.model);
|
this.stepDeletionState = new StepDeletionState (importResult.model);
|
||||||
@ -312,6 +325,7 @@ export class Website
|
|||||||
this.stepDeletionState = null;
|
this.stepDeletionState = null;
|
||||||
}
|
}
|
||||||
this.UpdateStepButtonsVisibility (this.stepDeletionState !== null);
|
this.UpdateStepButtonsVisibility (this.stepDeletionState !== null);
|
||||||
|
this.UpdateEnvironmentMap ();
|
||||||
this.viewer.SetMainObject (threeObject);
|
this.viewer.SetMainObject (threeObject);
|
||||||
this.viewer.SetUpVector (Direction.Y, false);
|
this.viewer.SetUpVector (Direction.Y, false);
|
||||||
this.navigator.FillTree ({
|
this.navigator.FillTree ({
|
||||||
@ -480,6 +494,15 @@ export class Website
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShowTransientStatus (message)
|
||||||
|
{
|
||||||
|
let statusPopup = AddDiv (document.body, 'ov_bottom_floating_panel');
|
||||||
|
AddDiv (statusPopup, 'ov_floating_panel_text', message);
|
||||||
|
window.setTimeout (() => {
|
||||||
|
statusPopup.remove ();
|
||||||
|
}, 1800);
|
||||||
|
}
|
||||||
|
|
||||||
DeleteSelectedStepNode ()
|
DeleteSelectedStepNode ()
|
||||||
{
|
{
|
||||||
if (this.stepDeletionState === null) {
|
if (this.stepDeletionState === null) {
|
||||||
@ -520,20 +543,27 @@ export class Website
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deletedPath = null;
|
||||||
if (this.navigator.selection.type === SelectionType.Node) {
|
if (this.navigator.selection.type === SelectionType.Node) {
|
||||||
|
deletedPath = this.stepDeletionState.GetNodePath (this.navigator.selection.nodeId);
|
||||||
this.stepDeletionState.DeleteNodeById (this.navigator.selection.nodeId);
|
this.stepDeletionState.DeleteNodeById (this.navigator.selection.nodeId);
|
||||||
} else if (this.navigator.selection.type === SelectionType.Mesh) {
|
} else if (this.navigator.selection.type === SelectionType.Mesh) {
|
||||||
|
deletedPath = this.stepDeletionState.GetMeshNodePath (this.navigator.selection.meshInstanceId);
|
||||||
this.stepDeletionState.DeleteMeshNode (this.navigator.selection.meshInstanceId);
|
this.stepDeletionState.DeleteMeshNode (this.navigator.selection.meshInstanceId);
|
||||||
}
|
}
|
||||||
|
console.log ('[step-demo] DeleteSelectedStepNode', {
|
||||||
|
deletedPath : deletedPath,
|
||||||
|
deletedPaths : this.stepDeletionState.GetDeletedNodePaths ()
|
||||||
|
});
|
||||||
|
|
||||||
this.navigator.SetSelection (null);
|
this.navigator.SetSelection (null);
|
||||||
this.navigator.FillTree ({
|
this.navigator.FillTree ({
|
||||||
model : this.model,
|
...this.currentImportResult,
|
||||||
missingFiles : [],
|
|
||||||
stepDeletionState : this.stepDeletionState
|
stepDeletionState : this.stepDeletionState
|
||||||
});
|
});
|
||||||
this.UpdateMeshesVisibility ();
|
this.UpdateMeshesVisibility ();
|
||||||
this.viewer.Render ();
|
this.viewer.Render ();
|
||||||
|
this.ShowTransientStatus (Loc ('Selected subtree deleted.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateStepButtonsVisibility (isVisible)
|
UpdateStepButtonsVisibility (isVisible)
|
||||||
@ -548,14 +578,51 @@ export class Website
|
|||||||
|
|
||||||
async SaveEditedStepFile ()
|
async SaveEditedStepFile ()
|
||||||
{
|
{
|
||||||
|
console.log ('[step-demo] SaveEditedStepFile start');
|
||||||
let importer = this.modelLoaderUI.GetImporter ();
|
let importer = this.modelLoaderUI.GetImporter ();
|
||||||
let fileList = importer.GetFileList ();
|
let fileList = importer.GetFileList ();
|
||||||
let sourceFile = fileList.FindFileByPath (this.parameters.fileNameDiv.innerHTML);
|
let mainFileName = this.currentImportResult !== null ? this.currentImportResult.mainFile : this.parameters.fileNameDiv.textContent;
|
||||||
if (sourceFile === null || sourceFile.content === null || this.stepDeletionState === null) {
|
let sourceFile = fileList.FindFileByPath (mainFileName);
|
||||||
|
if (sourceFile === null && fileList.GetFiles ().length === 1) {
|
||||||
|
sourceFile = fileList.GetFiles ()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log ('[step-demo] SaveEditedStepFile source', {
|
||||||
|
mainFileName : mainFileName,
|
||||||
|
foundSource : sourceFile !== null,
|
||||||
|
hasContent : sourceFile !== null ? sourceFile.content !== null : null,
|
||||||
|
deletedPaths : this.stepDeletionState !== null ? this.stepDeletionState.GetDeletedNodePaths () : null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.stepDeletionState === null) {
|
||||||
|
ShowMessageDialog (
|
||||||
|
Loc ('Save STEP Failed'),
|
||||||
|
Loc ('This action is only available for STEP files.')
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceFile === null) {
|
||||||
|
ShowMessageDialog (
|
||||||
|
Loc ('Save STEP Failed'),
|
||||||
|
Loc ('The original STEP file could not be found in the current import session.')
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceFile.content === null) {
|
||||||
|
ShowMessageDialog (
|
||||||
|
Loc ('Save STEP Failed'),
|
||||||
|
Loc ('The original STEP file content is not available.')
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsStepFileName (sourceFile.name)) {
|
if (!IsStepFileName (sourceFile.name)) {
|
||||||
|
ShowMessageDialog (
|
||||||
|
Loc ('Save STEP Failed'),
|
||||||
|
Loc ('The current model is not backed by a STEP source file.')
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -565,9 +632,15 @@ export class Website
|
|||||||
sourceFile.content,
|
sourceFile.content,
|
||||||
this.stepDeletionState.GetDeletedNodePaths ()
|
this.stepDeletionState.GetDeletedNodePaths ()
|
||||||
);
|
);
|
||||||
|
console.log ('[step-demo] SaveEditedStepFile response', {
|
||||||
|
size : fileBlob.size,
|
||||||
|
type : fileBlob.type
|
||||||
|
});
|
||||||
let downloadUrl = URL.createObjectURL (fileBlob);
|
let downloadUrl = URL.createObjectURL (fileBlob);
|
||||||
DownloadUrlAsFile (downloadUrl, BuildStepOutputFileName (sourceFile.name));
|
DownloadUrlAsFile (downloadUrl, BuildStepOutputFileName (sourceFile.name));
|
||||||
URL.revokeObjectURL (downloadUrl);
|
window.setTimeout (() => {
|
||||||
|
URL.revokeObjectURL (downloadUrl);
|
||||||
|
}, 1000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ShowMessageDialog (
|
ShowMessageDialog (
|
||||||
Loc ('Save STEP Failed'),
|
Loc ('Save STEP Failed'),
|
||||||
@ -709,12 +782,13 @@ export class Website
|
|||||||
InitViewer ()
|
InitViewer ()
|
||||||
{
|
{
|
||||||
let canvas = AddDomElement (this.parameters.viewerDiv, 'canvas');
|
let canvas = AddDomElement (this.parameters.viewerDiv, 'canvas');
|
||||||
|
canvas.setAttribute ('tabindex', '0');
|
||||||
|
canvas.setAttribute ('aria-label', Loc ('3D model viewer'));
|
||||||
this.viewer.Init (canvas);
|
this.viewer.Init (canvas);
|
||||||
this.viewer.SetEdgeSettings (this.settings.edgeSettings);
|
this.viewer.SetEdgeSettings (this.settings.edgeSettings);
|
||||||
this.viewer.SetBackgroundColor (this.settings.backgroundColor);
|
this.viewer.SetBackgroundColor (this.settings.backgroundColor);
|
||||||
this.viewer.SetNavigationMode (this.cameraSettings.navigationMode);
|
this.viewer.SetNavigationMode (this.cameraSettings.navigationMode);
|
||||||
this.viewer.SetProjectionMode (this.cameraSettings.projectionMode);
|
this.viewer.SetProjectionMode (this.cameraSettings.projectionMode);
|
||||||
this.UpdateEnvironmentMap ();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InitToolbar ()
|
InitToolbar ()
|
||||||
@ -1069,8 +1143,8 @@ export class Website
|
|||||||
|
|
||||||
UpdatePanelsVisibility ()
|
UpdatePanelsVisibility ()
|
||||||
{
|
{
|
||||||
let showNavigator = CookieGetBoolVal ('ov_show_navigator', true);
|
let showNavigator = IsSmallWidth () ? false : CookieGetBoolVal ('ov_show_navigator', true);
|
||||||
let showSidebar = CookieGetBoolVal ('ov_show_sidebar', true);
|
let showSidebar = IsSmallWidth () ? false : CookieGetBoolVal ('ov_show_sidebar', true);
|
||||||
this.navigator.ShowPanels (showNavigator);
|
this.navigator.ShowPanels (showNavigator);
|
||||||
this.sidebar.ShowPanels (showSidebar);
|
this.sidebar.ShowPanels (showSidebar);
|
||||||
}
|
}
|
||||||
@ -1081,6 +1155,7 @@ export class Website
|
|||||||
buttonLink.setAttribute ('href', link);
|
buttonLink.setAttribute ('href', link);
|
||||||
buttonLink.setAttribute ('target', '_blank');
|
buttonLink.setAttribute ('target', '_blank');
|
||||||
buttonLink.setAttribute ('rel', 'noopener noreferrer');
|
buttonLink.setAttribute ('rel', 'noopener noreferrer');
|
||||||
|
SetElementAccessibleName (buttonLink, title);
|
||||||
InstallTooltip (buttonLink, title);
|
InstallTooltip (buttonLink, title);
|
||||||
AddSvgIconElement (buttonLink, icon, 'header_button');
|
AddSvgIconElement (buttonLink, icon, 'header_button');
|
||||||
this.parameters.headerButtonsDiv.appendChild (buttonLink);
|
this.parameters.headerButtonsDiv.appendChild (buttonLink);
|
||||||
@ -1097,7 +1172,8 @@ export class Website
|
|||||||
let text = Loc ('This website uses cookies to offer you better user experience. See the details at the <a target="_blank" href="info/cookies.html">Cookies Policy</a> page.');
|
let text = Loc ('This website uses cookies to offer you better user experience. See the details at the <a target="_blank" href="info/cookies.html">Cookies Policy</a> page.');
|
||||||
let popupDiv = AddDiv (document.body, 'ov_bottom_floating_panel');
|
let popupDiv = AddDiv (document.body, 'ov_bottom_floating_panel');
|
||||||
AddDiv (popupDiv, 'ov_floating_panel_text', text);
|
AddDiv (popupDiv, 'ov_floating_panel_text', text);
|
||||||
let acceptButton = AddDiv (popupDiv, 'ov_button ov_floating_panel_button', Loc ('Accept'));
|
let acceptButton = AddButtonElement (popupDiv, 'ov_button ov_floating_panel_button', Loc ('Accept'));
|
||||||
|
SetElementAccessibleName (acceptButton, Loc ('Accept cookies'));
|
||||||
acceptButton.addEventListener ('click', () => {
|
acceptButton.addEventListener ('click', () => {
|
||||||
CookieSetBoolVal ('ov_cookie_consent', true);
|
CookieSetBoolVal ('ov_cookie_consent', true);
|
||||||
popupDiv.remove ();
|
popupDiv.remove ();
|
||||||
|
|||||||
@ -2,37 +2,95 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def get_script_arguments(argv):
|
||||||
|
if "--pass" in argv:
|
||||||
|
pass_index = argv.index("--pass")
|
||||||
|
script_args = argv[pass_index + 1 :]
|
||||||
|
else:
|
||||||
|
script_args = argv[2:]
|
||||||
|
|
||||||
|
if len(script_args) < 3:
|
||||||
|
raise ValueError("Expected input path, deleted paths manifest, and output path.")
|
||||||
|
|
||||||
|
return script_args[0], script_args[1], script_args[2]
|
||||||
|
|
||||||
|
|
||||||
|
def should_include_in_tree(document_object):
|
||||||
|
type_id = getattr(document_object, "TypeId", "")
|
||||||
|
return type_id not in {"App::Origin", "App::Line", "App::Plane", "Part::Feature"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_tree_children(objects):
|
||||||
|
filtered_objects = [document_object for document_object in objects if should_include_in_tree(document_object)]
|
||||||
|
|
||||||
|
def sort_key(document_object):
|
||||||
|
child_count = len(get_tree_children(list(getattr(document_object, "OutList", []))))
|
||||||
|
is_leaf_part = child_count == 0
|
||||||
|
return (1 if is_leaf_part else 0)
|
||||||
|
|
||||||
|
filtered_objects.sort(key=sort_key)
|
||||||
|
return filtered_objects
|
||||||
|
|
||||||
|
|
||||||
def enumerate_objects(objects, parent_path=""):
|
def enumerate_objects(objects, parent_path=""):
|
||||||
for index, document_object in enumerate(objects):
|
filtered_objects = get_tree_children(objects)
|
||||||
|
for index, document_object in enumerate(filtered_objects):
|
||||||
object_path = str(index) if parent_path == "" else parent_path + "/" + str(index)
|
object_path = str(index) if parent_path == "" else parent_path + "/" + str(index)
|
||||||
yield object_path, document_object
|
yield object_path, document_object
|
||||||
child_objects = list(document_object.OutList)
|
child_objects = list(getattr(document_object, "OutList", []))
|
||||||
yield from enumerate_objects(child_objects, object_path)
|
yield from enumerate_objects(child_objects, object_path)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_subtree_objects(objects, target_path, parent_path=""):
|
||||||
|
collected_objects = []
|
||||||
|
filtered_objects = get_tree_children(objects)
|
||||||
|
for index, document_object in enumerate(filtered_objects):
|
||||||
|
object_path = str(index) if parent_path == "" else parent_path + "/" + str(index)
|
||||||
|
child_objects = list(getattr(document_object, "OutList", []))
|
||||||
|
if target_path is None:
|
||||||
|
collected_objects.append(document_object)
|
||||||
|
collected_objects.extend(collect_subtree_objects(child_objects, None, object_path))
|
||||||
|
elif object_path == target_path:
|
||||||
|
collected_objects.append(document_object)
|
||||||
|
collected_objects.extend(collect_subtree_objects(child_objects, None, object_path))
|
||||||
|
elif target_path.startswith(object_path + "/"):
|
||||||
|
collected_objects.extend(collect_subtree_objects(child_objects, target_path, object_path))
|
||||||
|
return collected_objects
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
input_path = sys.argv[1]
|
input_path, deleted_paths_path, output_path = get_script_arguments(sys.argv)
|
||||||
deleted_paths_path = sys.argv[2]
|
|
||||||
output_path = sys.argv[3]
|
|
||||||
|
|
||||||
with open(deleted_paths_path, "r", encoding="utf-8") as deleted_file:
|
with open(deleted_paths_path, "r", encoding="utf-8") as deleted_file:
|
||||||
deleted_paths = set(json.load(deleted_file))
|
deleted_paths = set(json.load(deleted_file))
|
||||||
|
|
||||||
import FreeCAD
|
import FreeCAD
|
||||||
import ImportGui
|
import Import
|
||||||
|
|
||||||
document = FreeCAD.newDocument("TrimmedStep")
|
document = FreeCAD.newDocument("TrimmedStep")
|
||||||
ImportGui.insert(input_path, document.Name)
|
Import.insert(input_path, document.Name)
|
||||||
|
|
||||||
path_to_object = list(enumerate_objects(list(document.RootObjects)))
|
objects_to_delete = []
|
||||||
path_to_object.sort(key=lambda item: item[0].count("/"), reverse=True)
|
root_objects = list(document.RootObjects)
|
||||||
for object_path, document_object in path_to_object:
|
for deleted_path in deleted_paths:
|
||||||
if object_path in deleted_paths:
|
objects_to_delete.extend(collect_subtree_objects(root_objects, deleted_path))
|
||||||
document.removeObject(document_object.Name)
|
|
||||||
|
|
||||||
ImportGui.export(document.Objects, output_path)
|
unique_objects = []
|
||||||
|
seen_names = set()
|
||||||
|
for document_object in objects_to_delete:
|
||||||
|
object_name = getattr(document_object, "Name", None)
|
||||||
|
if object_name is None or object_name in seen_names:
|
||||||
|
continue
|
||||||
|
seen_names.add(object_name)
|
||||||
|
unique_objects.append(document_object)
|
||||||
|
|
||||||
|
unique_objects.sort(key=lambda document_object: len(list(getattr(document_object, "OutListRecursive", []))), reverse=True)
|
||||||
|
for document_object in unique_objects:
|
||||||
|
document.removeObject(document_object.Name)
|
||||||
|
|
||||||
|
Import.export(list(document.RootObjects), output_path)
|
||||||
FreeCAD.closeDocument(document.Name)
|
FreeCAD.closeDocument(document.Name)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ in ("__main__", "freecad_trim_step"):
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -30,6 +30,7 @@ class FreeCADWorker:
|
|||||||
[
|
[
|
||||||
self.freecad_cmd,
|
self.freecad_cmd,
|
||||||
os.fspath(pathlib.Path(__file__).with_name("freecad_trim_step.py")),
|
os.fspath(pathlib.Path(__file__).with_name("freecad_trim_step.py")),
|
||||||
|
"--pass",
|
||||||
os.fspath(input_path),
|
os.fspath(input_path),
|
||||||
os.fspath(delete_manifest_path),
|
os.fspath(delete_manifest_path),
|
||||||
os.fspath(output_path),
|
os.fspath(output_path),
|
||||||
@ -39,6 +40,11 @@ class FreeCADWorker:
|
|||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not output_path.exists():
|
||||||
|
raise RuntimeError(
|
||||||
|
"FreeCAD did not produce the expected STEP output file."
|
||||||
|
)
|
||||||
|
|
||||||
return output_path.read_bytes(), output_name
|
return output_path.read_bytes(), output_name
|
||||||
|
|
||||||
def _build_output_name(self, file_name):
|
def _build_output_name(self, file_name):
|
||||||
|
|||||||
@ -18,6 +18,17 @@ def create_app(worker=None):
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
step_worker = worker or FreeCADWorker()
|
step_worker = worker or FreeCADWorker()
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def add_cors_headers(response):
|
||||||
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
response.headers["Access-Control-Allow-Methods"] = "POST, OPTIONS"
|
||||||
|
response.headers["Access-Control-Allow-Headers"] = "Content-Type"
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.route("/save-step", methods=["OPTIONS"])
|
||||||
|
def save_step_options():
|
||||||
|
return ("", 204)
|
||||||
|
|
||||||
@app.post("/save-step")
|
@app.post("/save-step")
|
||||||
def save_step():
|
def save_step():
|
||||||
uploaded_file = request.files.get("file")
|
uploaded_file = request.files.get("file")
|
||||||
|
|||||||
78
tools/step_service/tests/test_freecad_trim_step.py
Normal file
78
tools/step_service/tests/test_freecad_trim_step.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from tools.step_service.freecad_trim_step import collect_subtree_objects, get_script_arguments, get_tree_children, should_include_in_tree
|
||||||
|
|
||||||
|
|
||||||
|
class FreeCADTrimStepTests(unittest.TestCase):
|
||||||
|
def test_extracts_arguments_after_pass_marker(self):
|
||||||
|
input_path, deleted_path, output_path = get_script_arguments(
|
||||||
|
[
|
||||||
|
"FreeCADCmd.exe",
|
||||||
|
"freecad_trim_step.py",
|
||||||
|
"--pass",
|
||||||
|
"input.step",
|
||||||
|
"deleted_paths.json",
|
||||||
|
"output.step",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(input_path, "input.step")
|
||||||
|
self.assertEqual(deleted_path, "deleted_paths.json")
|
||||||
|
self.assertEqual(output_path, "output.step")
|
||||||
|
|
||||||
|
def test_excludes_freecad_reference_objects_from_tree_paths(self):
|
||||||
|
class FakeObject:
|
||||||
|
def __init__(self, type_id, children=None):
|
||||||
|
self.TypeId = type_id
|
||||||
|
self.OutList = children or []
|
||||||
|
|
||||||
|
self.assertEqual(should_include_in_tree(FakeObject("App::Part")), True)
|
||||||
|
self.assertEqual(should_include_in_tree(FakeObject("Part::Feature")), False)
|
||||||
|
self.assertEqual(should_include_in_tree(FakeObject("App::Origin")), False)
|
||||||
|
self.assertEqual(should_include_in_tree(FakeObject("App::Line")), False)
|
||||||
|
self.assertEqual(should_include_in_tree(FakeObject("App::Plane")), False)
|
||||||
|
|
||||||
|
def test_orders_assemblies_before_leaf_parts(self):
|
||||||
|
class FakeObject:
|
||||||
|
def __init__(self, label, type_id, children=None):
|
||||||
|
self.Label = label
|
||||||
|
self.TypeId = type_id
|
||||||
|
self.OutList = children or []
|
||||||
|
|
||||||
|
feature = FakeObject("SOLID", "Part::Feature")
|
||||||
|
plate = FakeObject("PLATE", "App::Part", [feature])
|
||||||
|
bracket_child = FakeObject("L-BRACKET", "App::Part", [feature])
|
||||||
|
bracket_assembly = FakeObject("L_BRACKET_ASSEMBLY_ASM", "App::Part", [bracket_child])
|
||||||
|
rod_child = FakeObject("ROD", "App::Part", [feature])
|
||||||
|
rod_assembly = FakeObject("ROD_ASM", "App::Part", [rod_child])
|
||||||
|
|
||||||
|
ordered = get_tree_children([plate, bracket_assembly, rod_assembly])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[obj.Label for obj in ordered],
|
||||||
|
["L_BRACKET_ASSEMBLY_ASM", "ROD_ASM", "PLATE"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_collects_full_subtree_for_recursive_delete(self):
|
||||||
|
class FakeObject:
|
||||||
|
def __init__(self, label, type_id, children=None):
|
||||||
|
self.Label = label
|
||||||
|
self.TypeId = type_id
|
||||||
|
self.OutList = children or []
|
||||||
|
|
||||||
|
bolt = FakeObject("BOLT", "App::Part")
|
||||||
|
nut = FakeObject("NUT", "App::Part")
|
||||||
|
sub_assembly = FakeObject("NUT_BOLT_ASSEMBLY_ASM", "App::Part", [bolt, nut])
|
||||||
|
bracket = FakeObject("L_BRACKET_ASSEMBLY_ASM", "App::Part", [sub_assembly])
|
||||||
|
plate = FakeObject("PLATE", "App::Part")
|
||||||
|
|
||||||
|
collected = collect_subtree_objects([bracket, plate], "0")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[obj.Label for obj in collected],
|
||||||
|
["L_BRACKET_ASSEMBLY_ASM", "NUT_BOLT_ASSEMBLY_ASM", "BOLT", "NUT"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
31
tools/step_service/tests/test_freecad_worker.py
Normal file
31
tools/step_service/tests/test_freecad_worker.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from tools.step_service.freecad_worker import FreeCADWorker
|
||||||
|
|
||||||
|
|
||||||
|
class FreeCADWorkerTests(unittest.TestCase):
|
||||||
|
@mock.patch("tools.step_service.freecad_worker.subprocess.run")
|
||||||
|
def test_passes_script_arguments_through_freecad(self, run_mock):
|
||||||
|
run_mock.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
|
||||||
|
|
||||||
|
worker = FreeCADWorker(freecad_cmd="FreeCADCmd")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
with mock.patch("tools.step_service.freecad_worker.tempfile.TemporaryDirectory") as temp_dir_mock:
|
||||||
|
temp_dir_mock.return_value.__enter__.return_value = temp_dir
|
||||||
|
temp_dir_mock.return_value.__exit__.return_value = False
|
||||||
|
with mock.patch("pathlib.Path.read_bytes", return_value=b"STEPDATA"):
|
||||||
|
with mock.patch("pathlib.Path.exists", return_value=True):
|
||||||
|
output_bytes, output_name = worker.trim_step_file("demo.step", b"INPUT", ["0/1"])
|
||||||
|
|
||||||
|
self.assertEqual(output_bytes, b"STEPDATA")
|
||||||
|
self.assertEqual(output_name, "demo-edited.step")
|
||||||
|
command = run_mock.call_args.args[0]
|
||||||
|
self.assertIn("--pass", command)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue
Block a user