Works for a basic tool that just copies its input shape to its output shape.
This commit is contained in:
parent
90b5f9c269
commit
b9b228b8cd
|
@ -7,7 +7,6 @@ def getSingletonFromXML(xml, path):
|
||||||
return elem
|
return elem
|
||||||
|
|
||||||
ns = {
|
ns = {
|
||||||
'my':"http://github.com/jsmaniac/XternalApps/myTool",
|
|
||||||
'XternalApps':"http://github.com/jsmaniac/XternalApps/v1",
|
'XternalApps':"http://github.com/jsmaniac/XternalApps/v1",
|
||||||
'xforms':"http://www.w3.org/2002/xforms",
|
'xforms':"http://www.w3.org/2002/xforms",
|
||||||
'xsd':"http://www.w3.org/2001/XMLSchema",
|
'xsd':"http://www.w3.org/2001/XMLSchema",
|
||||||
|
|
|
@ -75,6 +75,7 @@ apps = Apps(
|
||||||
extra_xprop_filter = lambda processId, windowId, i: x11prop(windowId, 'WM_STATE', 'WM_STATE') is not None,
|
extra_xprop_filter = lambda processId, windowId, i: x11prop(windowId, 'WM_STATE', 'WM_STATE') is not None,
|
||||||
tools = [
|
tools = [
|
||||||
"myTool.xforms"
|
"myTool.xforms"
|
||||||
|
# TODO: generate them from """"inkscape --extension-directory"""
|
||||||
]),
|
]),
|
||||||
App('GIMP',
|
App('GIMP',
|
||||||
start_command_and_args = ['env', '-i', 'DISPLAY=:0', '/home/suzanne/perso/dotfiles/nix/result/bin/gimp', '--new-instance'],
|
start_command_and_args = ['env', '-i', 'DISPLAY=:0', '/home/suzanne/perso/dotfiles/nix/result/bin/gimp', '--new-instance'],
|
||||||
|
|
|
@ -8,12 +8,13 @@ import re
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import pprint
|
import pprint
|
||||||
|
import Part
|
||||||
|
|
||||||
parser = etree.XMLParser(resolve_entities=False)
|
parser = etree.XMLParser(resolve_entities=False)
|
||||||
|
|
||||||
FreeCADType = namedtuple('FreeCADType', ['type', 'defaultForType', 'maybeEnumValues'])
|
FreeCADType = namedtuple('FreeCADType', ['type', 'defaultForType', 'maybeEnumValues', 'maybeMIMEType'])
|
||||||
|
|
||||||
XFormsInput = namedtuple('XFormsInput', ['modelElementPath', 'label', 'simpleName', 'maybeEnum', 'groupName']) #'type', 'input', 'InputValueToModelValue', 'ModelValueToInputValue'
|
XFormsInput = namedtuple('XFormsInput', ['modelElementPath', 'label', 'simpleName', 'maybeEnum', 'groupName', 'relevance']) #'type', 'input', 'InputValueToModelValue', 'ModelValueToInputValue'
|
||||||
XFormsEnum = namedtuple('XFormsEnum', ['labels', 'values'])
|
XFormsEnum = namedtuple('XFormsEnum', ['labels', 'values'])
|
||||||
InterpretedXML = namedtuple('InterpretedXML', ['xml', 'types', 'inputs']) # Parsed XML, dictionary(modelElementPath) -> type, dictionary(formElementPath) -> XFormsInput
|
InterpretedXML = namedtuple('InterpretedXML', ['xml', 'types', 'inputs']) # Parsed XML, dictionary(modelElementPath) -> type, dictionary(formElementPath) -> XFormsInput
|
||||||
|
|
||||||
|
@ -24,16 +25,17 @@ def CreateCommand(appName, toolName):
|
||||||
App.ActiveDocument.commitTransaction()
|
App.ActiveDocument.commitTransaction()
|
||||||
|
|
||||||
def create(appName, toolName):
|
def create(appName, toolName):
|
||||||
|
sel = FreeCADGui.Selection.getSelection()
|
||||||
name = appName + toolName
|
name = appName + toolName
|
||||||
obj = App.ActiveDocument.addObject("App::DocumentObjectGroupPython", name)
|
obj = App.ActiveDocument.addObject("App::DocumentObjectGroupPython", name)
|
||||||
XternalAppsParametricTool(obj, appName, toolName)
|
XternalAppsParametricTool(obj, appName, toolName, sel)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
# TODO: read-only/immutable
|
# TODO: read-only/immutable
|
||||||
typeToFreeCADTypeDict = {
|
typeToFreeCADTypeDict = {
|
||||||
# TODO:do an XML namespace lookup instead of comparing a constant.
|
# TODO:do an XML namespace lookup instead of comparing a constant.
|
||||||
'xsd:decimal': FreeCADType(type='App::PropertyFloat', defaultForType=0.0, maybeEnumValues=None),
|
'xsd:decimal': FreeCADType(type='App::PropertyFloat', defaultForType=0.0, maybeEnumValues=None, maybeMIMEType=None),
|
||||||
'xsd:string': FreeCADType(type='App::PropertyString', defaultForType='', maybeEnumValues=None),
|
'xsd:string': FreeCADType(type='App::PropertyString', defaultForType='', maybeEnumValues=None, maybeMIMEType=None),
|
||||||
}
|
}
|
||||||
|
|
||||||
def getShortPath(root, elem, root_needs_dot = True):
|
def getShortPath(root, elem, root_needs_dot = True):
|
||||||
|
@ -80,33 +82,16 @@ def schemaTypeToFreeCADType(schemaType):
|
||||||
raise ValueError('only restrictions of xsd:string (a.k.a. enums) are supported')
|
raise ValueError('only restrictions of xsd:string (a.k.a. enums) are supported')
|
||||||
enumCases = restriction.findall('./xsd:enumeration', ns)
|
enumCases = restriction.findall('./xsd:enumeration', ns)
|
||||||
enumValues = [enumCase.attrib['value'] for enumCase in enumCases]
|
enumValues = [enumCase.attrib['value'] for enumCase in enumCases]
|
||||||
return FreeCADType(type = 'App::PropertyEnumeration', maybeEnumValues = enumValues, defaultForType = (enumValues[0] if len(enumValues) > 0 else None))
|
return FreeCADType(type = 'App::PropertyEnumeration', defaultForType = (enumValues[0] if len(enumValues) > 0 else None), maybeEnumValues = enumValues, maybeMIMEType=None)
|
||||||
elif schemaType.tag == "{http://www.w3.org/2001/XMLSchema}complexType":
|
elif schemaType.tag == "{http://www.w3.org/2001/XMLSchema}complexType":
|
||||||
return ValueError("Complex XML chema types are not supported")
|
return ValueError("Complex XML chema types are not supported")
|
||||||
|
|
||||||
def MIMETypeToFreeCADType(MIMEType):
|
def MIMETypeToFreeCADType(MIMEType):
|
||||||
if MIMEType == 'image/svg+xml':
|
if MIMEType == 'image/svg+xml':
|
||||||
return FreeCADType(type='App::PropertyLink', defaultForType=None, maybeEnumValues=None)
|
return FreeCADType(type='App::PropertyLink', defaultForType=None, maybeEnumValues=None, maybeMIMEType = MIMEType)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unsupported MIME type')
|
raise ValueError('Unsupported MIME type')
|
||||||
|
|
||||||
def encode_bytes(bytestring):
|
|
||||||
try:
|
|
||||||
return ("utf-8", bytestring.decode('utf-8', errors='strict'))
|
|
||||||
except ValueError:
|
|
||||||
from base64 import b64encode
|
|
||||||
return ("base64", b64encode(bytestring))
|
|
||||||
|
|
||||||
def decode_bytes(encoding_and_string):
|
|
||||||
encoding, string = encoding_and_string
|
|
||||||
if encoding == "utf-8":
|
|
||||||
return string.encode('utf-8')
|
|
||||||
elif encoding == "base64":
|
|
||||||
from base64 import b64decode
|
|
||||||
return b64decode(string)
|
|
||||||
else:
|
|
||||||
raise ValueError("invalid encoding: expected utf-8 or base64")
|
|
||||||
|
|
||||||
def toSimpleName(name):
|
def toSimpleName(name):
|
||||||
return re.sub(r'( |[^-a-zA-Z0-9])+', ' ', name).title().replace(' ', '')
|
return re.sub(r'( |[^-a-zA-Z0-9])+', ' ', name).title().replace(' ', '')
|
||||||
|
|
||||||
|
@ -126,8 +111,27 @@ def toUniqueSimpleName(name, mutableNextUnique):
|
||||||
mutableNextUnique[name] = mutableNextUnique[name] + 1
|
mutableNextUnique[name] = mutableNextUnique[name] + 1
|
||||||
return base + counter
|
return base + counter
|
||||||
|
|
||||||
|
def lookup(dict, part, value):
|
||||||
|
kvs = [(k, v) for k, v in dict.items() if part(v) == value]
|
||||||
|
if len(kvs) == 1:
|
||||||
|
return kvs[0]
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
def exportSVG(obj, svgfile):
|
||||||
|
import importSVG
|
||||||
|
p=App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
|
||||||
|
old_svg_export_style = p.GetInt('svg_export_style')
|
||||||
|
try:
|
||||||
|
p.SetInt('svg_export_style', 1)
|
||||||
|
importSVG.export([obj], svgfile)
|
||||||
|
finally:
|
||||||
|
p.SetInt('svg_export_style', old_svg_export_style)
|
||||||
|
|
||||||
|
# TODO: modify the SVG to set a fake Inkscape version, to avoid the pop-up dialog.
|
||||||
|
|
||||||
|
|
||||||
class XternalAppsParametricTool():
|
class XternalAppsParametricTool():
|
||||||
def __init__(self, obj, appName, toolName):
|
def __init__(self, obj, appName, toolName, sel=[]):
|
||||||
self.Type = "XternalAppsParametricTool"
|
self.Type = "XternalAppsParametricTool"
|
||||||
self.AppName = appName
|
self.AppName = appName
|
||||||
self.ToolName = toolName
|
self.ToolName = toolName
|
||||||
|
@ -141,7 +145,28 @@ class XternalAppsParametricTool():
|
||||||
self.ModelInstance = self.defaults
|
self.ModelInstance = self.defaults
|
||||||
self.createProperties(obj, self.types, self.ModelInstance, self.form)
|
self.createProperties(obj, self.types, self.ModelInstance, self.form)
|
||||||
|
|
||||||
self.MonitorChanges = True;
|
self.oldExpressionEngine = obj.ExpressionEngine
|
||||||
|
|
||||||
|
self.MonitorChanges = True
|
||||||
|
|
||||||
|
#pprint.pprint(self.types)
|
||||||
|
#pprint.pprint(self.form)
|
||||||
|
|
||||||
|
# Special treatment for the "primary" form field
|
||||||
|
primary = [input for input in self.form.values() if input.relevance == 'primary']
|
||||||
|
if len(primary) == 1:
|
||||||
|
primary = primary[0]
|
||||||
|
type = self.types[primary.modelElementPath].type
|
||||||
|
|
||||||
|
# Display the contents of the primary form element as children in the tree view
|
||||||
|
if type in ['App::PropertyLink', 'App::PropertyLinkList']:
|
||||||
|
self.form['FreeCADGroup'] = XFormsInput(modelElementPath=primary.modelElementPath, label='Group', simpleName='Group', maybeEnum=primary.maybeEnum, groupName='Base', relevance='primary')
|
||||||
|
|
||||||
|
if type == 'App::PropertyLink' and len(sel) >= 1:
|
||||||
|
setattr(obj, primary.simpleName, sel[0])
|
||||||
|
elif type == 'App::PropertyLinkList':
|
||||||
|
setattr(obj, primary.simpleName, sel)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
|
@ -160,53 +185,61 @@ class XternalAppsParametricTool():
|
||||||
self.defaults = self.xmlDefaultsToPython(self.Tool.XForms, self.types)
|
self.defaults = self.xmlDefaultsToPython(self.Tool.XForms, self.types)
|
||||||
self.form = self.xmlFormToPython(self.Tool.XForms, self.types)
|
self.form = self.xmlFormToPython(self.Tool.XForms, self.types)
|
||||||
|
|
||||||
def getDefaultModelInstance(self, obj):
|
|
||||||
xml = etree.parse(self.Tool.XForms, parser=parser)
|
|
||||||
model = xml.find('./xforms:model', ns)
|
|
||||||
instanceDocument = etree.ElementTree(model.find('./xforms:instance/*', ns))
|
|
||||||
return deepcopy(instanceDocument)
|
|
||||||
|
|
||||||
def typecheckModelInstance(self, types, instance):
|
|
||||||
"""TODO"""
|
|
||||||
|
|
||||||
def updateModelInstance(self, instance, ref, value):
|
|
||||||
"""TODO"""
|
|
||||||
|
|
||||||
def onChanged(self, obj, prop):
|
def onChanged(self, obj, prop):
|
||||||
|
import sys
|
||||||
|
#print ('onChanged' + str(prop), file=sys.stderr)
|
||||||
|
|
||||||
|
restoreMonitorChanges = self.MonitorChanges
|
||||||
if self.MonitorChanges:
|
if self.MonitorChanges:
|
||||||
try:
|
try:
|
||||||
|
#print('MonitorChanges = False')
|
||||||
self.MonitorChanges = False
|
self.MonitorChanges = False
|
||||||
inputs = [input for input in self.form.values() if input.simpleName == prop]
|
# clear expressions attached to the same part of the model
|
||||||
if len(inputs) == 1:
|
if (prop == 'ExpressionEngine'):
|
||||||
input = inputs[0]
|
# compare with oldExpressionEngine
|
||||||
|
added = []
|
||||||
|
removed = []
|
||||||
|
for (k, _) in obj.ExpressionEngine:
|
||||||
|
if k not in self.oldExpressionEngine:
|
||||||
|
added = added + [k]
|
||||||
|
#for k in self.oldExpressionEngine:
|
||||||
|
# if k not in self.oldExpressionEngine:
|
||||||
|
# removed = removed + [k]
|
||||||
|
# Clear expressions for properties linked to the one that was added.
|
||||||
|
for a in added:
|
||||||
|
_, input = lookup(self.form, lambda input: input.simpleName, a)
|
||||||
|
if input:
|
||||||
|
for other in self.form.values():
|
||||||
|
if other.modelElementPath == input.modelElementPath and other.simpleName != input.simpleName:
|
||||||
|
# clear other.simpleName
|
||||||
|
obj.setExpression(other.simpleName, None)
|
||||||
|
self.oldExpressionEngine = set([k for k, v in obj.ExpressionEngine])
|
||||||
|
|
||||||
|
_, input = lookup(self.form, lambda input: input.simpleName, prop)
|
||||||
|
if input:
|
||||||
newModelValue = getattr(obj, prop)
|
newModelValue = getattr(obj, prop)
|
||||||
if input.maybeEnum:
|
if input.maybeEnum:
|
||||||
newModelValue = input.maybeEnum[newModelValue]
|
newModelValue = input.maybeEnum[newModelValue]
|
||||||
|
# The Group property always contains a list, but we may use it to represent a link to a single element.
|
||||||
|
if prop == 'Group' and self.types[input.modelElementPath].type == 'App::PropertyLink':
|
||||||
|
if len(newModelValue) > 0:
|
||||||
|
newModelValue = newModelValue[0]
|
||||||
|
else:
|
||||||
|
newModelValue = None
|
||||||
self.ModelInstance[input.modelElementPath] = newModelValue
|
self.ModelInstance[input.modelElementPath] = newModelValue
|
||||||
for other in self.form.values():
|
for other in self.form.values():
|
||||||
if other.modelElementPath == input.modelElementPath and other.simpleName != input.simpleName:
|
if other.modelElementPath == input.modelElementPath and other.simpleName != input.simpleName:
|
||||||
newFormValue = newModelValue
|
newFormValue = newModelValue
|
||||||
|
#print('newModelValue', newModelValue)
|
||||||
if other.maybeEnum:
|
if other.maybeEnum:
|
||||||
newFormValue = [f for f, m in other.maybeEnum.items() if m == newModelValue][0]
|
newFormValue = [f for f, m in other.maybeEnum.items() if m == newModelValue][0]
|
||||||
|
#print(prop, newFormValue, other.simpleName, dict(obj.ExpressionEngine).get(prop), dict(obj.ExpressionEngine).get(other.simpleName))
|
||||||
|
#obj.setExpression(other.simpleName, dict(obj.ExpressionEngine).get(prop))
|
||||||
|
#print(obj, other.simpleName, newFormValue)
|
||||||
setattr(obj, other.simpleName, newFormValue)
|
setattr(obj, other.simpleName, newFormValue)
|
||||||
obj.setExpression(other.simpleName, None)
|
|
||||||
finally:
|
finally:
|
||||||
self.MonitorChanges = True
|
#print('MonitorChanges = ' + str(restoreMonitorChanges))
|
||||||
return
|
self.MonitorChanges = restoreMonitorChanges
|
||||||
|
|
||||||
if hasattr(self, 'SimpleInputNameToInput'):
|
|
||||||
inputPath = self.SimpleInputNameToInput.get(prop, None)
|
|
||||||
if inputPath is not None:
|
|
||||||
modelElementPath = self.Inputs[inputPath].modelElementPath
|
|
||||||
modelElement = self.ModelInstance.find(modelElementPath)
|
|
||||||
newText = self.Inputs[inputPath].InputValueToModelValue(getattr(obj, prop))
|
|
||||||
print((prop, getattr(obj, prop), modelElement.text, newText))
|
|
||||||
if modelElement.text != newText:
|
|
||||||
modelElement.text = newText
|
|
||||||
for inputPathToUpdate in self.ModelOnChanged[modelElementPath]:
|
|
||||||
if inputPathToUpdate != inputPath:
|
|
||||||
# TODO: this is terrible and will lead to infinite update loops
|
|
||||||
setattr(obj, self.InputToSimpleInputName[inputPathToUpdate], self.Inputs[inputPathToUpdate].ModelValueToInputValue(newText))
|
|
||||||
|
|
||||||
def interpretFormElement(self, xmlXFormsElement, xml, instanceDocument, types):
|
def interpretFormElement(self, xmlXFormsElement, xml, instanceDocument, types):
|
||||||
# TODO: is it safe to pass input unprotected here?
|
# TODO: is it safe to pass input unprotected here?
|
||||||
|
@ -220,8 +253,15 @@ class XternalAppsParametricTool():
|
||||||
if type is None:
|
if type is None:
|
||||||
raise Exception('Could not find type for ' + modelElementPath)
|
raise Exception('Could not find type for ' + modelElementPath)
|
||||||
path = xml.getelementpath(xmlXFormsElement)
|
path = xml.getelementpath(xmlXFormsElement)
|
||||||
path = getShortPath(xml, xmlXFormsElement) # YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
|
path = getShortPath(xml, xmlXFormsElement) # TODO !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
return (path, xmlXFormsElement, modelElementPath, type)
|
|
||||||
|
relevance = xmlXFormsElement.xpath('./@XternalApps:relevance', namespaces=ns)
|
||||||
|
if len(relevance):
|
||||||
|
relevance = relevance[0]
|
||||||
|
else:
|
||||||
|
relevance = None
|
||||||
|
|
||||||
|
return (path, xmlXFormsElement, modelElementPath, type, relevance)
|
||||||
|
|
||||||
def xmlFormToPython(self, form_xml, types):
|
def xmlFormToPython(self, form_xml, types):
|
||||||
"""Parse the …-form.xml document, and return
|
"""Parse the …-form.xml document, and return
|
||||||
|
@ -236,16 +276,31 @@ class XternalAppsParametricTool():
|
||||||
# register all inputs to inputs[pathToElement]
|
# register all inputs to inputs[pathToElement]
|
||||||
for group in xml.findall('./xforms:group', ns):
|
for group in xml.findall('./xforms:group', ns):
|
||||||
for input in group.findall('./xforms:input', ns):
|
for input in group.findall('./xforms:input', ns):
|
||||||
path, xmlXFormsElement, modelElementPath, _type = self.interpretFormElement(input, xml, instanceDocument, types)
|
path, xmlXFormsElement, modelElementPath, _type, relevance = self.interpretFormElement(input, xml, instanceDocument, types)
|
||||||
|
|
||||||
label = input.attrib['label']
|
label = input.attrib['label']
|
||||||
simpleName = toUniqueSimpleName(toSimpleName(label), nextUniqueSimpleName)
|
simpleName = toUniqueSimpleName(toSimpleName(label), nextUniqueSimpleName)
|
||||||
groupName = "/".join(input.xpath('ancestor-or-self::xforms:group/xforms:label/text()', namespaces=ns)) or None
|
groupName = "/".join(input.xpath('ancestor-or-self::xforms:group/xforms:label/text()', namespaces=ns)) or None
|
||||||
|
|
||||||
# input=xmlXFormsElement,
|
inputs[path] = XFormsInput(modelElementPath=modelElementPath, label=label, simpleName=simpleName, maybeEnum=None, groupName=groupName, relevance=relevance) # type=type,
|
||||||
inputs[path] = XFormsInput(modelElementPath=modelElementPath, label=label, simpleName=simpleName, maybeEnum=None, groupName=groupName) # type=type,
|
|
||||||
|
for upload in group.findall("./xforms:upload", ns):
|
||||||
|
path, xmlXFormsElement, modelElementPath, _type, relevance = self.interpretFormElement(upload, xml, instanceDocument, types)
|
||||||
|
|
||||||
|
label = upload.xpath('./xforms:label/text()', namespaces=ns)
|
||||||
|
if len(label) != 1:
|
||||||
|
raise ValueError("An xforms:upload element should contain exactly one xforms:label element")
|
||||||
|
label = label[0]
|
||||||
|
if upload.xpath('./xforms:filename/@ref', namespaces=ns) != ['@filename']:
|
||||||
|
raise ValueError("The ref attribute of an xforms:filename should always be the string '@filename' (this is a limitation of the XternalApps format).")
|
||||||
|
|
||||||
|
simpleName = toUniqueSimpleName(toSimpleName(label), nextUniqueSimpleName)
|
||||||
|
groupName = "/".join(upload.xpath('ancestor-or-self::xforms:group/xforms:label/text()', namespaces=ns)) or None
|
||||||
|
|
||||||
|
inputs[path] = XFormsInput(modelElementPath=modelElementPath, label=label, simpleName=simpleName, maybeEnum=None, groupName=groupName, relevance=relevance) # type=type,
|
||||||
|
|
||||||
for select1 in group.findall('./xforms:select1', ns):
|
for select1 in group.findall('./xforms:select1', ns):
|
||||||
path, xmlXFormsElement, modelElementPath, _type = self.interpretFormElement(select1, xml, instanceDocument, types)
|
path, xmlXFormsElement, modelElementPath, _type, relevance = self.interpretFormElement(select1, xml, instanceDocument, types)
|
||||||
|
|
||||||
label = select1.attrib['label']
|
label = select1.attrib['label']
|
||||||
simpleName = toUniqueSimpleName(toSimpleName(label), nextUniqueSimpleName)
|
simpleName = toUniqueSimpleName(toSimpleName(label), nextUniqueSimpleName)
|
||||||
|
@ -256,7 +311,7 @@ class XternalAppsParametricTool():
|
||||||
for item in select1.findall('./xforms:item', ns):
|
for item in select1.findall('./xforms:item', ns):
|
||||||
enum[item.attrib['label']] = item.attrib['value']
|
enum[item.attrib['label']] = item.attrib['value']
|
||||||
# input=xmlXFormsElement,
|
# input=xmlXFormsElement,
|
||||||
inputs[path] = XFormsInput(modelElementPath=modelElementPath, label=label, simpleName=simpleName, maybeEnum=enum, groupName=groupName) # type=type,
|
inputs[path] = XFormsInput(modelElementPath=modelElementPath, label=label, simpleName=simpleName, maybeEnum=enum, groupName=groupName, relevance=relevance) # type=type,
|
||||||
|
|
||||||
return inputs
|
return inputs
|
||||||
|
|
||||||
|
@ -297,7 +352,7 @@ class XternalAppsParametricTool():
|
||||||
return defaults
|
return defaults
|
||||||
|
|
||||||
def createProperties(self, obj, types, defaults, form):
|
def createProperties(self, obj, types, defaults, form):
|
||||||
for key, (modelElementPath, label, simpleName, maybeEnum, groupName) in form.items():
|
for key, (modelElementPath, label, simpleName, maybeEnum, groupName, relevance) in form.items():
|
||||||
obj.addProperty(types[modelElementPath].type,
|
obj.addProperty(types[modelElementPath].type,
|
||||||
simpleName,
|
simpleName,
|
||||||
groupName,
|
groupName,
|
||||||
|
@ -305,7 +360,6 @@ class XternalAppsParametricTool():
|
||||||
default = defaults[modelElementPath]
|
default = defaults[modelElementPath]
|
||||||
if maybeEnum is not None:
|
if maybeEnum is not None:
|
||||||
setattr(obj, simpleName, list(maybeEnum.keys()))
|
setattr(obj, simpleName, list(maybeEnum.keys()))
|
||||||
print(default, maybeEnum)
|
|
||||||
# TODO: use a bidirectional dict
|
# TODO: use a bidirectional dict
|
||||||
default = [k for k, v in maybeEnum.items() if v == default][0]
|
default = [k for k, v in maybeEnum.items() if v == default][0]
|
||||||
setattr(obj, simpleName, default)
|
setattr(obj, simpleName, default)
|
||||||
|
@ -314,5 +368,149 @@ class XternalAppsParametricTool():
|
||||||
def Tool(self):
|
def Tool(self):
|
||||||
return XternalAppsList.apps[self.AppName].Tools[self.ToolName]
|
return XternalAppsList.apps[self.AppName].Tools[self.ToolName]
|
||||||
|
|
||||||
|
def xmlCommandToPython(self, document):
|
||||||
|
"""Parse the .xml document, and return
|
||||||
|
a pair of dictionaries accepts[model_path] = style and returns[model_path] = style."""
|
||||||
|
|
||||||
|
xml = etree.parse(self.Tool.XForms, parser=parser)
|
||||||
|
|
||||||
|
model_root = xml.find('./xforms:model', ns)
|
||||||
|
instanceDocument = etree.ElementTree(model_root.find('./xforms:instance/*', ns))
|
||||||
|
|
||||||
|
command = xml.find('./XternalApps:command', ns)
|
||||||
|
method = command.attrib['method']
|
||||||
|
commandName = command.attrib['name']
|
||||||
|
|
||||||
|
accepts = command.find('./XternalApps:accepts', ns)
|
||||||
|
returns = command.find('./XternalApps:returns', ns)
|
||||||
|
|
||||||
|
maybeDefault = accepts.findall('./XternalApps:default', ns)
|
||||||
|
if len(maybeDefault) == 1:
|
||||||
|
default_style = maybeDefault[0].attrib['style']
|
||||||
|
elif len(maybeDefault) > 1:
|
||||||
|
raise ValueError('The accepts tag should contain at most one default tag')
|
||||||
|
else:
|
||||||
|
default_style = None
|
||||||
|
|
||||||
|
styles = {}
|
||||||
|
optionNames = {}
|
||||||
|
# Put all the model fields in accepts[path]
|
||||||
|
for modelElement in instanceDocument.findall('//*', ns):
|
||||||
|
path = instanceDocument.getelementpath(modelElement)
|
||||||
|
styles[path] = default_style
|
||||||
|
optionNames[path] = etree.QName(modelElement).localname
|
||||||
|
|
||||||
|
style_is_default = {k: True for k in styles.keys()}
|
||||||
|
for exception in accepts.findall('./XternalApps:exception', ns):
|
||||||
|
ref = exception.attrib['ref']
|
||||||
|
style = exception.attrib['style']
|
||||||
|
for modelElement in instanceDocument.findall(ref, exception.nsmap):
|
||||||
|
path = instanceDocument.getelementpath(modelElement)
|
||||||
|
if style_is_default[path]:
|
||||||
|
style_is_default[path] = False
|
||||||
|
styles[path] = style
|
||||||
|
else:
|
||||||
|
ValueError('overlapping exceptions in command/accepts/exception')
|
||||||
|
|
||||||
|
pipe_in = None
|
||||||
|
positionalArguments = {}
|
||||||
|
namedArguments = []
|
||||||
|
tempfiles = []
|
||||||
|
tempdirs = []
|
||||||
|
for modelElementPath, value in self.ModelInstance.items():
|
||||||
|
style = styles[modelElementPath]
|
||||||
|
type = self.types[modelElementPath]
|
||||||
|
|
||||||
|
# Convert to the type expected by the tool
|
||||||
|
if type.type == 'App::PropertyLink' and type.maybeMIMEType == 'image/svg+xml':
|
||||||
|
import tempfile, os
|
||||||
|
d = tempfile.mkdtemp()
|
||||||
|
svgfile = os.path.join(d, "sketch.svg")
|
||||||
|
tempfiles += [svgfile]
|
||||||
|
tempdirs += [d]
|
||||||
|
exportSVG(value, svgfile)
|
||||||
|
value = svgfile
|
||||||
|
else:
|
||||||
|
# TODO ################# convert the value from FreeCAD to what the program supports ###################
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
if style == 'double-dash':
|
||||||
|
namedArguments += ['--' + optionNames[modelElementPath], value]
|
||||||
|
elif style == 'pipe':
|
||||||
|
if pipe_in != None:
|
||||||
|
raise ValueError('more then one option uses a "pipe" style')
|
||||||
|
pipe_in = value
|
||||||
|
elif style == 'positional':
|
||||||
|
pos = unknown_todo() ######################################### TODO ############################
|
||||||
|
positionalArguments[pos] = value
|
||||||
|
elif style == 'exitcode':
|
||||||
|
raise ValueError('exitcode is supported only for the output of the command, not for its input')
|
||||||
|
else:
|
||||||
|
raise ValueError('unsupported argument-passing style')
|
||||||
|
|
||||||
|
positionalArguments = [v for i, vs in sorted(positionalArguments.items()) for v in vs]
|
||||||
|
if pipe_in is not None:
|
||||||
|
pipe_in_handle = open(pipe_in)
|
||||||
|
|
||||||
|
# TODO: use the XML for this
|
||||||
|
d = tempfile.mkdtemp()
|
||||||
|
result_filename = os.path.join(d, "result.svg")
|
||||||
|
pipe_out = result_filename
|
||||||
|
tempfiles += [pipe_out]
|
||||||
|
tempdirs += [d]
|
||||||
|
pipe_out_handle = open(pipe_out, 'w')
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
print([commandName] + positionalArguments + namedArguments + ['stdin=' + pipe_in, 'stdout=' + pipe_out])
|
||||||
|
proc = subprocess.Popen([commandName] + positionalArguments + namedArguments, stdin=pipe_in_handle, stdout=pipe_out_handle)
|
||||||
|
proc.communicate()
|
||||||
|
exitcode = proc.returncode
|
||||||
|
|
||||||
|
pipe_in_handle.close()
|
||||||
|
with open(result_filename, 'rb') as result_file:
|
||||||
|
result = result_file.read()
|
||||||
|
pipe_out_handle.close()
|
||||||
|
|
||||||
|
# Circumvent bug which leaves App.ActiveDocument to an incorrect value after the newDocument + closeDocument
|
||||||
|
oldActiveDocumentName = App.ActiveDocument.Name
|
||||||
|
tempDocument = App.newDocument('load_svg', hidden=True)
|
||||||
|
import importSVG
|
||||||
|
importSVG.insert(pipe_out,'load_svg')
|
||||||
|
solids = []
|
||||||
|
for o in tempDocument.Objects:
|
||||||
|
shape = o.Shape
|
||||||
|
wire = Part.Wire(shape.Edges)
|
||||||
|
#face = Part.Face(wire)
|
||||||
|
solids += [wire] #face
|
||||||
|
p = Part.makeCompound(solids)
|
||||||
|
for obj in tempDocument.Objects:
|
||||||
|
print("remove:" + obj.Name)
|
||||||
|
tempDocument.removeObject(obj.Name)
|
||||||
|
Part.show(p)
|
||||||
|
for o in tempDocument.Objects:
|
||||||
|
o2 = document.copyObject(o, False, False)
|
||||||
|
App.closeDocument('load_svg')
|
||||||
|
App.setActiveDocument(oldActiveDocumentName)
|
||||||
|
|
||||||
|
for tempfile in tempfiles:
|
||||||
|
os.remove(tempfile)
|
||||||
|
for tempdir in tempdirs:
|
||||||
|
os.rmdir(tempdir)
|
||||||
|
|
||||||
|
print(exitcode, result)
|
||||||
|
|
||||||
def execute(self, obj):
|
def execute(self, obj):
|
||||||
"""This is called when the object is recomputed"""
|
print("""This is called when the object is recomputed""")
|
||||||
|
self.xmlCommandToPython(obj.Document)
|
||||||
|
|
||||||
|
#<XternalApps:accepts>
|
||||||
|
# <XternalApps:default ref=".//*" style="double-dash">
|
||||||
|
# <XternalApps:exception ref="my:svgfile" style="pipe" />
|
||||||
|
#</XternalApps:accepts>
|
||||||
|
|
||||||
|
#<XternalApps:returns>
|
||||||
|
# <XternalApps:exception ref="my:output-svgfile" style="pipe" />
|
||||||
|
# <XternalApps:exception ref="my:output-exitcode" style="exitcode" />
|
||||||
|
#</XternalApps:returns>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,5 +2,7 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
for arg in sys.argv:
|
#for arg in sys.argv:
|
||||||
print(str(arg)
|
# print('<!--' + str(arg) + '-->')
|
||||||
|
|
||||||
|
sys.stdout.write(sys.stdin.read())
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<xforms:model>
|
<xforms:model>
|
||||||
<xforms:instance>
|
<xforms:instance>
|
||||||
<my:tool>
|
<my:tool>
|
||||||
<my:svgfile/>
|
<my:svgfile filename="" />
|
||||||
<my:option1>default value</my:option1>
|
<my:option1>default value</my:option1>
|
||||||
<my:option2/>
|
<my:option2/>
|
||||||
<my:option3>bar</my:option3>
|
<my:option3>bar</my:option3>
|
||||||
|
@ -26,10 +26,6 @@
|
||||||
<xforms:bind ref="my:option2" type="xsd:decimal" required="true()"/>
|
<xforms:bind ref="my:option2" type="xsd:decimal" required="true()"/>
|
||||||
<xforms:bind ref="my:option3" type="my:enum-option3" required="true()"/>
|
<xforms:bind ref="my:option3" type="my:enum-option3" required="true()"/>
|
||||||
<!--<xforms:submission action="myTool.py" method="exec-double-dash" />-->
|
<!--<xforms:submission action="myTool.py" method="exec-double-dash" />-->
|
||||||
<XternalApps:command medhod="exec" style="double-dash">
|
|
||||||
<XternalApps:exception ref="my:svgfile" style="pipe" />
|
|
||||||
<XternalApps:returns style="pipe" type="image/svg+xml" />
|
|
||||||
</XternalApps:command>
|
|
||||||
<xsd:schema targetNamespace="http://github.com/jsmaniac/XternalApps/myTool" xmlns:my="http://github.com/jsmaniac/XternalApps/myTool">
|
<xsd:schema targetNamespace="http://github.com/jsmaniac/XternalApps/myTool" xmlns:my="http://github.com/jsmaniac/XternalApps/myTool">
|
||||||
<xsd:simpleType name="enum-option3">
|
<xsd:simpleType name="enum-option3">
|
||||||
<xsd:restriction base="xsd:string">
|
<xsd:restriction base="xsd:string">
|
||||||
|
@ -39,12 +35,24 @@
|
||||||
</xsd:simpleType>
|
</xsd:simpleType>
|
||||||
</xsd:schema>
|
</xsd:schema>
|
||||||
</xforms:model>
|
</xforms:model>
|
||||||
|
|
||||||
|
<XternalApps:command method="exec" name="/home/suzanne/perso/projects/paper-craft/py/Mod/XternalApps/myTool.py">
|
||||||
|
<XternalApps:accepts>
|
||||||
|
<XternalApps:default style="double-dash" />
|
||||||
|
<XternalApps:exception ref="my:svgfile" style="pipe" />
|
||||||
|
</XternalApps:accepts>
|
||||||
|
<XternalApps:returns>
|
||||||
|
<XternalApps:exception ref="my:output-svgfile" style="pipe" />
|
||||||
|
<XternalApps:exception ref="my:output-exitcode" style="exitcode" />
|
||||||
|
</XternalApps:returns>
|
||||||
|
</XternalApps:command>
|
||||||
|
|
||||||
<!-- Description of the user interface follows: -->
|
<!-- Description of the user interface follows: -->
|
||||||
<xforms:group>
|
<xforms:group>
|
||||||
<xforms:label>Page 1</xforms:label>
|
<xforms:label>Page 1</xforms:label>
|
||||||
<xforms:input ref="my:option1" label="Option One ∀"/>
|
<xforms:input ref="my:option1" label="Option One ∀"/>
|
||||||
<xforms:input ref="my:option2" label="Option Two π"/>
|
<xforms:input ref="my:option2" label="Option Two π"/>
|
||||||
<xforms:upload ref="my:svgfile" accept="image/svg+xml">
|
<xforms:upload ref="my:svgfile" accept="image/svg+xml" XternalApps:relevance="primary">
|
||||||
<xforms:label>Input image</xforms:label>
|
<xforms:label>Input image</xforms:label>
|
||||||
<xforms:filename ref="@filename" />
|
<xforms:filename ref="@filename" />
|
||||||
</xforms:upload>
|
</xforms:upload>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user