Note, typically HAnimSegment USE nodes are placed in containerField="segments" which should be deleted (per 4.1) or sorted after the HAnimHumanoid.skeleton field.
If you would like to refer to this comment somewhere else in this project, copy and paste the following link:
I have improved the sort of child nodes within HAnimHumanoid, which mostly solves the XML-output issue 1.
For issue 2, while looking at how to properly handle generating the containerField value, realized
a. No containerField information is applied anywhere in x3d.py so this is a missing capability (rather than an incorrect configuration).
b. Python objects have no way to find out whether they have a parent, and indeed may have multiple parents. Thus there is insufficient information available to a given node, which is a design deficiency in the current structure of the x3d.py library.
Fixing this design gap will require adjustments to each node's autogenerated field signatures and setter methods. Am looking to add the following properties to each node's class: parentNode linking to the parent node, and parentField that identifies the name of the parent node's field that is being set by the parent adding the node. These additions will enable proper identification of XML containerField values when special parent-child field relationships are needed. Having access to this information will also provide further expressive power to Python programmers using the x3d.py library.
Last edit: Don Brutzman 2026-06-22
If you would like to refer to this comment somewhere else in this project, copy and paste the following link:
Thank you for addressing this, I have been traveling so I'm just getting back to my x3d experiments. I will update to 4.1 for future projects and I'm happy to resolve the ticket on Github when you deem fit. It seems issue 1 is resolved.
Claude walked through the x3d.py source (4.0.65.3) and pointed to an example. Take PhysicalMaterial's XML() serializer as an example — around line 70878 it does:
if self.baseTexture: # output this SFNode result += self.baseTexture.XML(indentLevel=indentLevel+1, syntax=syntax) if self.emissiveTexture: # output this SFNode result += self.emissiveTexture.XML(indentLevel=indentLevel+1, syntax=syntax)
The parent is iterating its named fields — it knows this texture is baseTexture. But XML() takes no containerField parameter, so the child (ImageTexture) has no idea which slot it's sitting in. The child just writes <imagetexture ...=""> and hopes for the best. A conformant reader falls back to the default containerField (texture), which PhysicalMaterial doesn't define, and the texture silently vanishes.</imagetexture>
The parentNode / parentField approach you're proposing is the right fix — it's more robust than just patching the serializer, since it also gives Python programmers proper parent-awareness for programmatic use. Worth noting that Vincent Marchetti built a working containerField extension back in March (https://github.com/vincentmarchetti/x3dpy_containerfield) using an XSLT stylesheet that patches the code generator. His approach and yours are complementary — his solves the serialization side, yours solves the object-model side.
Here's a minimal PBR reproduction to complement John's HAnim examples. It's a different part of the spec hitting the exact same bug — every non-default containerField is affected:
""" PBR texture silently vanishes — containerField='baseTexture' never emitted. x3d 4.0.65.3, Python 3.12. Confirmed in X_ITE and Castle Model Viewer 5.3: texture renders only after manually injecting containerField='baseTexture'. """fromx3dimportx3dasXbrick_texture=X.ImageTexture(url=["brick.png"])brick_material=X.PhysicalMaterial(baseColor=(1,1,1),roughness=0.8,metallic=0.0,)brick_material.baseTexture=brick_texture# correct in memorybrick_box=X.Shape(appearance=X.Appearance(material=brick_material),geometry=X.Box(size=(2,2,2)))ground=X.Shape(appearance=X.Appearance(material=X.PhysicalMaterial(baseColor=(0.3,0.6,0.2),roughness=1.0)),geometry=X.Box(size=(20,0.1,20)))scene=X.X3D(version="4.1",head=X.head(),Scene=X.Scene(children=[X.Viewpoint(position=(5,4,5),orientation=(-0.2,1,0.1,0.8)),X.DirectionalLight(direction=(-0.5,-1,-0.3)),X.Transform(translation=(0,1,0),children=[brick_box]),ground,]))print(scene.XML())# ACTUAL output:# <PhysicalMaterial># <ImageTexture url='"brick.png"'/> <-- no containerField# </PhysicalMaterial>## EXPECTED:# <PhysicalMaterial># <ImageTexture containerField='baseTexture' url='"brick.png"'/># </PhysicalMaterial>## The box renders flat white. No error, no warning, validates clean.
You're left staring at a white box wondering what you did wrong. The answer is nothing — the serializer just didn't write down which slot it put the texture in. The same pattern hits all five PBR texture slots and every HAnim skeleton/joints/segments placement.
Issue 1 and issue 2 now appear to be fixed. See attached output, note corrected order of child nodes under HAnimHumanoid as well as inclusion of non-default containerField values on USE nodes.
Release 4.0.65.5 of updated x3d.py is now available at
Apologies for prematurely closing this ticket before all corrective actions were complete.
Please expand the excerpt to provide a full test program (e.g. PhysicallyBasedMaterialTest.py or somesuch) so that this issue can be thoroughly tested. I will place the program into version control as part of X3DPSAIL python/examples so that it can serve as a regression test for long-term quality assurance (QA).
Last edit: Don Brutzman 2026-08-02
If you would like to refer to this comment somewhere else in this project, copy and paste the following link:
Thank you for reopening it — the close was reasonable given what the ticket said at the time.
First, a correction from my side. I posted a reproducer earlier today built against x3d.py 4.0.65.4, which is what my environment had pinned. You had already announced the fix in 4.0.65.5 on 30 June in this ticket, and you were right — I should have re-tested against the release you named before posting. Verified now across both:
check
4.0.65.3
4.0.65.5
HAnimJoint in skeleton gets containerField='skeleton'
FAIL
PASS
HAnimSegment in segments gets containerField='segments'
FAIL
PASS
DEF precedes USE in document order
FAIL
PASS
ImageTexture in baseTexture gets containerField='baseTexture'
FAIL
PASS
HAnimHumanoid.version='2.0' serialized
FAIL
FAIL
The fix is also correctly slot-aware rather than blanket — a texture in the defaulttexture slot still emits no containerField, which is right. The test below asserts that as an explicit control, so a later change cannot regress into emitting it everywhere.
Named as you suggested, and written for the use you described — a long-term QA regression test in python/examples.
Only dependency is x3d.py (plus sys). No xmllint, no local schema copy, no network. Python 3.6+.
Exit status is deliberate. It exits 0 on 4.0.65.5 and 1 on 4.0.65.3. The one still-failing item is reported as [OPEN] and does not fail the run, because a QA test that is permanently red from a known-unfixed issue stops being a signal — the build is already broken, so a real regression changes nothing visible. --strict fails on open items too, if you would rather it did.
Items 1–3 are the lock on your fix. If any starts failing, it regressed.
The single remaining item on 4.0.65.5 is HAnimHumanoid.version dropped from XML — set on the object and retained there (obj.version == '2.0'), absent from the serialization. It is not in the ticket text, but your own "expected output" excerpt above carries version='2.0', so I believe it belongs. Happy to open it as its own ticket if you would rather close this one.
#!/usr/bin/env python3"""PhysicallyBasedMaterialTest.pyRegression test LOCKING IN the containerField fix released in x3d.py 4.0.65.5(SourceForge x3d ticket #117 / Python-SAI issues #2, #3). python3 PhysicallyBasedMaterialTest.py # 0 = no regression python3 PhysicallyBasedMaterialTest.py --strict # also fail on open itemsEXIT STATUS IS DELIBERATE. On 4.0.65.5 this exits 0 even though item 4 below isstill broken, because item 4 is KNOWN-OPEN upstream rather than a regression. AQA test that is permanently red because of an unfixed issue stops being a signal-- the build is already broken, so a real regression changes nothing visible.Items 1-3 are the lock: if any of them starts failing, the fix has regressed andthe exit status goes to 1. 4.0.65.3 -> exit 1 (items 1-3 fail: the pre-fix behaviour) 4.0.65.5 -> exit 0 (items 1-3 pass, item 4 reported as OPEN)STATUS: all containerField checks below PASS on 4.0.65.5 and FAIL on 4.0.65.3.This test exists to keep it that way, which is what Don Brutzman asked for --a program that can live in X3DPSAIL python/examples as long-term QA.WHAT IT COVERS 1. PhysicalMaterial + baseTexture -- the case from the June 23 comment. An ImageTexture in the `baseTexture` slot must serialize with containerField='baseTexture'. Without it the texture reparses into the default `texture` slot, which PhysicalMaterial does not define, and the surface renders untextured with no error anywhere. 2. The default slot must NOT carry containerField. Emitting it there would be redundant, and canonical X3D omits an attribute equal to its default. This is the control: it proves the fix is slot-aware rather than blanket. 3. HAnimHumanoid skeleton/segments -- containerField on non-default node fields, and DEF ordered before USE. 4. HAnimHumanoid.version -- STILL FAILING as of 4.0.65.5. Set on the object, absent from the XML. Kept in this file as an open item rather than split into its own, so one run reports the whole picture.Alexander Hoffman, 2026-08-03. Verified against x3d.py 4.0.65.3 (fails 1-3) and4.0.65.5 (passes 1-3, fails 4)."""importsysfromx3dimportx3dasXVERSION=2FAILURES=[]# a REGRESSION -- the build should go redOPEN=[]# a known-open defect -- reported, does not fail the rundefcheck(label,ok,expected,actual,open_issue=False):"""open_issue=True marks a defect that is KNOWN-OPEN upstream. It is reported but does not fail the run. A QA regression test that is permanently red because of an unfixed issue stops being a signal -- the build is already broken, so a real regression changes nothing visible. Run with --strict to fail on open items too. """tag='OPEN'if(notokandopen_issue)else('PASS'ifokelse'FAIL')print(f" [{tag}] {label}")ifnotok:print(f" expected: {expected}")print(f" actual : {actual}")(OPENifopen_issueelseFAILURES).append(label)defmain():print(f"PhysicallyBasedMaterialTest v{VERSION} -- x3d.py "f"{getattr(X,'__version__','(version attribute absent)')}\n")# --- 1. non-default slot: baseTexture ---------------------------------app=X.Appearance(material=X.PhysicalMaterial(DEF="brick",baseTexture=X.ImageTexture(DEF="btex",url=["brick.png"])))xml=app.XML()print(" --- PhysicalMaterial with baseTexture ---")forlineinxml.splitlines():print(" "+line)check("ImageTexture in `baseTexture` carries containerField='baseTexture'","containerField='baseTexture'"inxml,"<ImageTexture ... containerField='baseTexture'/>","no containerField -- reparses into `texture`, which ""PhysicalMaterial does not define; the surface renders untextured")# --- 2. control: the DEFAULT slot must stay bare -----------------------bare=X.Appearance(texture=X.ImageTexture(DEF="t2",url=["b.png"])).XML()check("control: default `texture` slot emits NO containerField","containerField"notinbare,"<ImageTexture DEF='t2' url='\"b.png\"'/>",bare.strip())# --- 3. HAnim non-default node fields ---------------------------------h=X.HAnimHumanoid(DEF="hanim_Test",name="Test",version="2.0",skeleton=[X.HAnimJoint(DEF="hanim_humanoid_root",name="humanoid_root",children=[X.HAnimSegment(DEF="hanim_sacrum",name="sacrum")])],segments=[X.HAnimSegment(USE="hanim_sacrum")])hx=h.XML()print("\n --- HAnimHumanoid ---")forlineinhx.splitlines():print(" "+line)check("HAnimJoint in `skeleton` carries containerField='skeleton'","containerField='skeleton'"inhx,"containerField='skeleton'","absent -- the joint reparses into `children` and the skeleton ""stops being a skeleton")check("HAnimSegment in `segments` carries containerField='segments'","containerField='segments'"inhx,"containerField='segments'","absent")i_def,i_use=hx.find("DEF='hanim_sacrum'"),hx.find("USE='hanim_sacrum'")check("DEF precedes USE in document order",-1<i_def<i_use,"the DEF node serializes first",f"DEF at {i_def}, USE at {i_use} -- USE is a forward reference")# --- 4. still open on 4.0.65.5 ----------------------------------------check("HAnimHumanoid.version='2.0' is serialized","version="inhx,"<HAnimHumanoid ... version='2.0'>",f"absent from XML although the object still holds "f"{getattr(h,'version',None)!r}",open_issue=True)strict="--strict"insys.argvprint()ifOPEN:print(f"KNOWN-OPEN ({len(OPEN)}), not counted as regressions: "+"; ".join(OPEN))ifFAILURES:print(f"REGRESSION ({len(FAILURES)}): "+"; ".join(FAILURES))return1ifOPENandstrict:print("--strict: failing on known-open items.")return1print("No regressions."+(" Known-open items above."ifOPENelse""))return0if__name__=="__main__":sys.exit(main())
Also worth flagging separately, since it is broader than this ticket and still reproduces on 4.0.65.5: x3d.py binds xmlns:xsd to https://www.w3.org/2001/XMLSchema-instance, where the canonical namespace name is http:// (no s). Namespace names are compared as literal strings rather than resolved, so xsd:noNamespaceSchemaLocation stops being recognised and every document x3d.py writes fails XSD validation. Changing that one character makes the same bytes validate. Details and an isolating test are in Python-SAI issue #3.
If you would like to refer to this comment somewhere else in this project, copy and paste the following link:
Thanks, Don. Very much appreciated. I've been avoiding the stairs to my office. I'll work on getting the laptop upstairs.
Note, typically HAnimSegment USE nodes are placed in containerField="segments" which should be deleted (per 4.1) or sorted after the HAnimHumanoid.skeleton field.
I have improved the sort of child nodes within HAnimHumanoid, which mostly solves the XML-output issue 1.
For issue 2, while looking at how to properly handle generating the containerField value, realized
a. No containerField information is applied anywhere in x3d.py so this is a missing capability (rather than an incorrect configuration).
b. Python objects have no way to find out whether they have a parent, and indeed may have multiple parents. Thus there is insufficient information available to a given node, which is a design deficiency in the current structure of the x3d.py library.
Fixing this design gap will require adjustments to each node's autogenerated field signatures and setter methods. Am looking to add the following properties to each node's class: parentNode linking to the parent node, and parentField that identifies the name of the parent node's field that is being set by the parent adding the node. These additions will enable proper identification of XML containerField values when special parent-child field relationships are needed. Having access to this information will also provide further expressive power to Python programmers using the x3d.py library.
Last edit: Don Brutzman 2026-06-22
Hi John and Don,
Thank you for addressing this, I have been traveling so I'm just getting back to my x3d experiments. I will update to 4.1 for future projects and I'm happy to resolve the ticket on Github when you deem fit. It seems issue 1 is resolved.
Claude walked through the x3d.py source (4.0.65.3) and pointed to an example. Take PhysicalMaterial's XML() serializer as an example — around line 70878 it does:
The parent is iterating its named fields — it knows this texture is baseTexture. But XML() takes no containerField parameter, so the child (ImageTexture) has no idea which slot it's sitting in. The child just writes <imagetexture ...=""> and hopes for the best. A conformant reader falls back to the default containerField (texture), which PhysicalMaterial doesn't define, and the texture silently vanishes.</imagetexture>
The parentNode / parentField approach you're proposing is the right fix — it's more robust than just patching the serializer, since it also gives Python programmers proper parent-awareness for programmatic use. Worth noting that Vincent Marchetti built a working containerField extension back in March (https://github.com/vincentmarchetti/x3dpy_containerfield) using an XSLT stylesheet that patches the code generator. His approach and yours are complementary — his solves the serialization side, yours solves the object-model side.
Here's a minimal PBR reproduction to complement John's HAnim examples. It's a different part of the spec hitting the exact same bug — every non-default containerField is affected:
You're left staring at a white box wondering what you did wrong. The answer is nothing — the serializer just didn't write down which slot it put the texture in. The same pattern hits all five PBR texture slots and every HAnim skeleton/joints/segments placement.
Happy to help test once the parentField plumbing is in place.
I hope you find this helpful!
— Alexander
Issue 1 and issue 2 now appear to be fixed. See attached output, note corrected order of child nodes under HAnimHumanoid as well as inclusion of non-default containerField values on USE nodes.
Release 4.0.65.5 of updated x3d.py is now available at
Last edit: Don Brutzman 2026-06-30
Apologies for prematurely closing this ticket before all corrective actions were complete.
Please expand the excerpt to provide a full test program (e.g. PhysicallyBasedMaterialTest.py or somesuch) so that this issue can be thoroughly tested. I will place the program into version control as part of X3DPSAIL python/examples so that it can serve as a regression test for long-term quality assurance (QA).
Last edit: Don Brutzman 2026-08-02
Thank you for reopening it — the close was reasonable given what the ticket said at the time.
First, a correction from my side. I posted a reproducer earlier today built against x3d.py 4.0.65.4, which is what my environment had pinned. You had already announced the fix in 4.0.65.5 on 30 June in this ticket, and you were right — I should have re-tested against the release you named before posting. Verified now across both:
HAnimJointinskeletongetscontainerField='skeleton'HAnimSegmentinsegmentsgetscontainerField='segments'DEFprecedesUSEin document orderImageTextureinbaseTexturegetscontainerField='baseTexture'HAnimHumanoid.version='2.0'serializedThe fix is also correctly slot-aware rather than blanket — a texture in the default
textureslot still emits nocontainerField, which is right. The test below asserts that as an explicit control, so a later change cannot regress into emitting it everywhere.Output on 4.0.65.5:
PhysicallyBasedMaterialTest.pyNamed as you suggested, and written for the use you described — a long-term QA regression test in
python/examples.x3d.py(plussys). Noxmllint, no local schema copy, no network. Python 3.6+.[OPEN]and does not fail the run, because a QA test that is permanently red from a known-unfixed issue stops being a signal — the build is already broken, so a real regression changes nothing visible.--strictfails on open items too, if you would rather it did.The single remaining item on 4.0.65.5 is
HAnimHumanoid.versiondropped from XML — set on the object and retained there (obj.version == '2.0'), absent from the serialization. It is not in the ticket text, but your own "expected output" excerpt above carriesversion='2.0', so I believe it belongs. Happy to open it as its own ticket if you would rather close this one.Also worth flagging separately, since it is broader than this ticket and still reproduces on 4.0.65.5:
x3d.pybindsxmlns:xsdtohttps://www.w3.org/2001/XMLSchema-instance, where the canonical namespace name ishttp://(no s). Namespace names are compared as literal strings rather than resolved, soxsd:noNamespaceSchemaLocationstops being recognised and every document x3d.py writes fails XSD validation. Changing that one character makes the same bytes validate. Details and an isolating test are in Python-SAI issue #3.Thanks for working on this guys. I probably have more test cases surrounding parent ComposedCubeMapTexture and 6 ImageTexture children. Here's one: https://github.com/coderextreme/X3DJSONLD/blob/main/src/main/python/net/x3djsonld/data/ballx_ite.py
Obviously, one will need to add XML output to verify.