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)
|
||||
{
|
||||
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);
|
||||
if (importResult.missingFiles.length === 0) {
|
||||
this.panelSet.SetPanelIcon (this.filesPanel, 'files');
|
||||
@ -161,6 +168,10 @@ export class Navigator
|
||||
}
|
||||
this.materialsPanel.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 ();
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
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 { MeshItem, NavigatorItemRecurse, NodeItem } from './navigatoritems.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';
|
||||
|
||||
const MeshesPanelMode =
|
||||
@ -30,6 +30,7 @@ class NavigatorMaterialsPopupButton extends NavigatorPopupButton
|
||||
|
||||
let materialsText = FLoc ('Materials ({0})', this.materialInfoArray.length);
|
||||
this.buttonText.innerHTML = materialsText;
|
||||
SetElementAccessibleName (this.button, materialsText);
|
||||
}
|
||||
|
||||
OnButtonClick ()
|
||||
@ -110,6 +111,10 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
||||
|
||||
Clear ()
|
||||
{
|
||||
console.log ('[step-demo] MeshesPanel.Clear', {
|
||||
nodeItems : this.nodeIdToItem.size,
|
||||
meshItems : this.meshInstanceIdToItem.size
|
||||
});
|
||||
this.ClearMeshTree ();
|
||||
ClearDomElement (this.titleButtonsDiv);
|
||||
ClearDomElement (this.buttonsDiv);
|
||||
@ -180,9 +185,8 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
||||
{
|
||||
function CreateButton (parentDiv, button, className, onClick)
|
||||
{
|
||||
button.div = AddDiv (parentDiv, 'ov_navigator_button');
|
||||
button.div.setAttribute ('alt', button.name);
|
||||
button.div.setAttribute ('title', button.name);
|
||||
button.div = AddButtonElement (parentDiv, 'ov_navigator_button');
|
||||
SetElementAccessibleName (button.div, button.name);
|
||||
if (className) {
|
||||
button.div.classList.add (className);
|
||||
}
|
||||
@ -383,9 +387,10 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
||||
function AddModelNodeToTree (panel, model, node, parentItem, mode)
|
||||
{
|
||||
if (panel.stepDeletionState !== null && panel.stepDeletionState.IsNodeDeletedByPath (panel.stepDeletionState.GetNodePath (node.GetId ()))) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let hasVisibleContent = false;
|
||||
let meshNodes = [];
|
||||
for (let childNode of node.GetChildNodes ()) {
|
||||
if (mode === MeshesPanelMode.TreeView) {
|
||||
@ -393,15 +398,24 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
||||
meshNodes.push (childNode);
|
||||
} else {
|
||||
let nodeItem = CreateNodeItem (panel, childNode);
|
||||
parentItem.AddChild (nodeItem);
|
||||
AddModelNodeToTree (panel, model, childNode, nodeItem, mode);
|
||||
let childHasVisibleContent = AddModelNodeToTree (panel, model, childNode, nodeItem, mode);
|
||||
if (childHasVisibleContent) {
|
||||
parentItem.AddChild (nodeItem);
|
||||
hasVisibleContent = true;
|
||||
} else {
|
||||
panel.nodeIdToItem.delete (childNode.GetId ());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AddModelNodeToTree (panel, model, childNode, parentItem, mode);
|
||||
if (AddModelNodeToTree (panel, model, childNode, parentItem, mode)) {
|
||||
hasVisibleContent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 ()) {
|
||||
let mesh = model.GetMesh (meshIndex);
|
||||
@ -412,12 +426,19 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
||||
}
|
||||
}
|
||||
AddMeshToNodeTree (panel, node, mesh, meshIndex, parentItem, mode);
|
||||
hasVisibleContent = true;
|
||||
}
|
||||
|
||||
return hasVisibleContent;
|
||||
}
|
||||
|
||||
let rootNode = model.GetRootNode ();
|
||||
this.rootItem = CreateDummyRootItem (this, rootNode);
|
||||
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)
|
||||
@ -461,6 +482,12 @@ export class NavigatorMeshesPanel extends NavigatorPanel
|
||||
IsMeshVisible (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 ();
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { IsDefined } from '../engine/core/core.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)
|
||||
{
|
||||
@ -12,11 +12,10 @@ export function ScrollToView (element)
|
||||
|
||||
export class TreeViewButton
|
||||
{
|
||||
constructor (imagePath)
|
||||
constructor (imagePath, accessibleName)
|
||||
{
|
||||
this.imagePath = imagePath;
|
||||
this.mainElement = CreateSvgIconElement (this.imagePath, 'ov_tree_item_button');
|
||||
this.mainElement.setAttribute ('src', this.imagePath);
|
||||
this.mainElement = CreateSvgIconButtonElement (this.imagePath, 'ov_tree_item_button', accessibleName);
|
||||
}
|
||||
|
||||
SetImage (imagePath)
|
||||
@ -59,6 +58,7 @@ export class TreeViewItem
|
||||
{
|
||||
this.mainElement.classList.add ('clickable');
|
||||
this.mainElement.style.cursor = 'pointer';
|
||||
MakeElementButtonLike (this.mainElement, this.name, onClick);
|
||||
this.mainElement.addEventListener ('click', onClick);
|
||||
}
|
||||
|
||||
@ -129,7 +129,7 @@ export class TreeViewGroupItem extends TreeViewItem
|
||||
this.openButtonIcon = 'arrow_down';
|
||||
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);
|
||||
}
|
||||
|
||||
@ -185,7 +185,9 @@ export class TreeViewGroupItem extends TreeViewItem
|
||||
{
|
||||
if (this.childrenDiv === null) {
|
||||
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.ShowChildren (this.isChildrenVisible);
|
||||
this.openCloseButton.addEventListener ('click', (ev) => {
|
||||
@ -196,6 +198,14 @@ export class TreeViewGroupItem extends TreeViewItem
|
||||
}
|
||||
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
|
||||
|
||||
@ -4,7 +4,7 @@ import { ImportErrorCode, ImportSettings } from '../engine/import/importer.js';
|
||||
import { NavigationMode, ProjectionMode } from '../engine/viewer/camera.js';
|
||||
import { RGBColor } from '../engine/model/color.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 { HandleEvent } from './eventhandler.js';
|
||||
import { HashHandler } from './hashhandler.js';
|
||||
@ -16,7 +16,7 @@ import { ThreeModelLoaderUI } from './threemodelloaderui.js';
|
||||
import { Toolbar } from './toolbar.js';
|
||||
import { DownloadModel, ShowExportDialog } from './exportdialog.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 { ShowSharingDialog } from './sharingdialog.js';
|
||||
import { GetDefaultMaterials, ReplaceDefaultMaterialsColor } from '../engine/model/modelutils.js';
|
||||
@ -201,6 +201,7 @@ export class Website
|
||||
this.uiState = WebsiteUIState.Undefined;
|
||||
this.layouter = new WebsiteLayouter (this.parameters, this.navigator, this.sidebar, this.viewer, this.measureTool);
|
||||
this.model = null;
|
||||
this.currentImportResult = null;
|
||||
this.stepDeletionState = null;
|
||||
this.deleteSelectedButton = null;
|
||||
this.stepSaveButton = null;
|
||||
@ -228,7 +229,17 @@ export class Website
|
||||
this.InitDragAndDrop ();
|
||||
this.InitSidebar ();
|
||||
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.SetMouseMoveHandler (this.OnModelMouseMoved.bind (this));
|
||||
@ -289,6 +300,7 @@ export class Website
|
||||
CloseAllDialogs ();
|
||||
|
||||
this.model = null;
|
||||
this.currentImportResult = null;
|
||||
this.stepDeletionState = null;
|
||||
this.viewer.Clear ();
|
||||
|
||||
@ -304,7 +316,8 @@ export class Website
|
||||
OnModelLoaded (importResult, threeObject)
|
||||
{
|
||||
this.model = importResult.model;
|
||||
this.parameters.fileNameDiv.innerHTML = importResult.mainFile;
|
||||
this.currentImportResult = importResult;
|
||||
this.parameters.fileNameDiv.textContent = importResult.mainFile;
|
||||
let importedExtension = GetFileExtension (importResult.mainFile);
|
||||
if (importedExtension === 'stp' || importedExtension === 'step') {
|
||||
this.stepDeletionState = new StepDeletionState (importResult.model);
|
||||
@ -312,6 +325,7 @@ export class Website
|
||||
this.stepDeletionState = null;
|
||||
}
|
||||
this.UpdateStepButtonsVisibility (this.stepDeletionState !== null);
|
||||
this.UpdateEnvironmentMap ();
|
||||
this.viewer.SetMainObject (threeObject);
|
||||
this.viewer.SetUpVector (Direction.Y, false);
|
||||
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 ()
|
||||
{
|
||||
if (this.stepDeletionState === null) {
|
||||
@ -520,20 +543,27 @@ export class Website
|
||||
return;
|
||||
}
|
||||
|
||||
let deletedPath = null;
|
||||
if (this.navigator.selection.type === SelectionType.Node) {
|
||||
deletedPath = this.stepDeletionState.GetNodePath (this.navigator.selection.nodeId);
|
||||
this.stepDeletionState.DeleteNodeById (this.navigator.selection.nodeId);
|
||||
} else if (this.navigator.selection.type === SelectionType.Mesh) {
|
||||
deletedPath = this.stepDeletionState.GetMeshNodePath (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.FillTree ({
|
||||
model : this.model,
|
||||
missingFiles : [],
|
||||
...this.currentImportResult,
|
||||
stepDeletionState : this.stepDeletionState
|
||||
});
|
||||
this.UpdateMeshesVisibility ();
|
||||
this.viewer.Render ();
|
||||
this.ShowTransientStatus (Loc ('Selected subtree deleted.'));
|
||||
}
|
||||
|
||||
UpdateStepButtonsVisibility (isVisible)
|
||||
@ -548,14 +578,51 @@ export class Website
|
||||
|
||||
async SaveEditedStepFile ()
|
||||
{
|
||||
console.log ('[step-demo] SaveEditedStepFile start');
|
||||
let importer = this.modelLoaderUI.GetImporter ();
|
||||
let fileList = importer.GetFileList ();
|
||||
let sourceFile = fileList.FindFileByPath (this.parameters.fileNameDiv.innerHTML);
|
||||
if (sourceFile === null || sourceFile.content === null || this.stepDeletionState === null) {
|
||||
let mainFileName = this.currentImportResult !== null ? this.currentImportResult.mainFile : this.parameters.fileNameDiv.textContent;
|
||||
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;
|
||||
}
|
||||
|
||||
if (!IsStepFileName (sourceFile.name)) {
|
||||
ShowMessageDialog (
|
||||
Loc ('Save STEP Failed'),
|
||||
Loc ('The current model is not backed by a STEP source file.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -565,9 +632,15 @@ export class Website
|
||||
sourceFile.content,
|
||||
this.stepDeletionState.GetDeletedNodePaths ()
|
||||
);
|
||||
console.log ('[step-demo] SaveEditedStepFile response', {
|
||||
size : fileBlob.size,
|
||||
type : fileBlob.type
|
||||
});
|
||||
let downloadUrl = URL.createObjectURL (fileBlob);
|
||||
DownloadUrlAsFile (downloadUrl, BuildStepOutputFileName (sourceFile.name));
|
||||
URL.revokeObjectURL (downloadUrl);
|
||||
window.setTimeout (() => {
|
||||
URL.revokeObjectURL (downloadUrl);
|
||||
}, 1000);
|
||||
} catch (err) {
|
||||
ShowMessageDialog (
|
||||
Loc ('Save STEP Failed'),
|
||||
@ -709,12 +782,13 @@ export class Website
|
||||
InitViewer ()
|
||||
{
|
||||
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.SetEdgeSettings (this.settings.edgeSettings);
|
||||
this.viewer.SetBackgroundColor (this.settings.backgroundColor);
|
||||
this.viewer.SetNavigationMode (this.cameraSettings.navigationMode);
|
||||
this.viewer.SetProjectionMode (this.cameraSettings.projectionMode);
|
||||
this.UpdateEnvironmentMap ();
|
||||
}
|
||||
|
||||
InitToolbar ()
|
||||
@ -1069,8 +1143,8 @@ export class Website
|
||||
|
||||
UpdatePanelsVisibility ()
|
||||
{
|
||||
let showNavigator = CookieGetBoolVal ('ov_show_navigator', true);
|
||||
let showSidebar = CookieGetBoolVal ('ov_show_sidebar', true);
|
||||
let showNavigator = IsSmallWidth () ? false : CookieGetBoolVal ('ov_show_navigator', true);
|
||||
let showSidebar = IsSmallWidth () ? false : CookieGetBoolVal ('ov_show_sidebar', true);
|
||||
this.navigator.ShowPanels (showNavigator);
|
||||
this.sidebar.ShowPanels (showSidebar);
|
||||
}
|
||||
@ -1081,6 +1155,7 @@ export class Website
|
||||
buttonLink.setAttribute ('href', link);
|
||||
buttonLink.setAttribute ('target', '_blank');
|
||||
buttonLink.setAttribute ('rel', 'noopener noreferrer');
|
||||
SetElementAccessibleName (buttonLink, title);
|
||||
InstallTooltip (buttonLink, title);
|
||||
AddSvgIconElement (buttonLink, icon, 'header_button');
|
||||
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 popupDiv = AddDiv (document.body, 'ov_bottom_floating_panel');
|
||||
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', () => {
|
||||
CookieSetBoolVal ('ov_cookie_consent', true);
|
||||
popupDiv.remove ();
|
||||
|
||||
@ -2,37 +2,95 @@ import json
|
||||
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=""):
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
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():
|
||||
input_path = sys.argv[1]
|
||||
deleted_paths_path = sys.argv[2]
|
||||
output_path = sys.argv[3]
|
||||
input_path, deleted_paths_path, output_path = get_script_arguments(sys.argv)
|
||||
|
||||
with open(deleted_paths_path, "r", encoding="utf-8") as deleted_file:
|
||||
deleted_paths = set(json.load(deleted_file))
|
||||
|
||||
import FreeCAD
|
||||
import ImportGui
|
||||
import Import
|
||||
|
||||
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)))
|
||||
path_to_object.sort(key=lambda item: item[0].count("/"), reverse=True)
|
||||
for object_path, document_object in path_to_object:
|
||||
if object_path in deleted_paths:
|
||||
document.removeObject(document_object.Name)
|
||||
objects_to_delete = []
|
||||
root_objects = list(document.RootObjects)
|
||||
for deleted_path in deleted_paths:
|
||||
objects_to_delete.extend(collect_subtree_objects(root_objects, deleted_path))
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ in ("__main__", "freecad_trim_step"):
|
||||
main()
|
||||
|
||||
@ -30,6 +30,7 @@ class FreeCADWorker:
|
||||
[
|
||||
self.freecad_cmd,
|
||||
os.fspath(pathlib.Path(__file__).with_name("freecad_trim_step.py")),
|
||||
"--pass",
|
||||
os.fspath(input_path),
|
||||
os.fspath(delete_manifest_path),
|
||||
os.fspath(output_path),
|
||||
@ -39,6 +40,11 @@ class FreeCADWorker:
|
||||
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
|
||||
|
||||
def _build_output_name(self, file_name):
|
||||
|
||||
@ -18,6 +18,17 @@ def create_app(worker=None):
|
||||
app = Flask(__name__)
|
||||
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")
|
||||
def save_step():
|
||||
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