Skip to content

pythontk v0.9.12

Latest

Choose a tag to compare

@m3trik m3trik released this 10 Aug 03:01
  • 2026-08-10 — User-facing messages go vendor-neutral (public-repo audit). Metadata's unreadable-file guidance and FileUtils.write_atomic's docstring stop naming specific sync vendors — the diagnosis ("a cloud-sync client holds / hasn't hydrated the file") is vendor-independent; is_cloud_placeholder's docstring keeps its vendor examples, since enumerating the sync engines it detects is documentation, not messaging.

  • 2026-08-09 — One mode vocabulary for every bridge, and round_trip stopped being welded to one artifact shape. SEND_TO/SAVE_AS/ROUND_TRIP are an on-disk contract, not just a Python one — each template's BRIDGE_MODES tuple names them — yet five bridges (mayatk/blendertk/extapps Marmoset, mayatk/blendertk Substance) spelled their own copy, and spelled it roundtrip against the canon's round_trip. A second definition of a file format's keywords is a second dialect: the two were not interchangeable, and nothing said so, so a mode that looked shared was not. All five now import from script_template; ScriptTemplate.normalize_modes is the one place legacy spellings fold to the canon, applied by declared_modes and by the Substance engine's own ast-based parser (which reads templates directly and would otherwise have dropped a stale roundtrip out of its filter and quietly reduced a headless round trip to a one-way send — an unrecognized mode does not raise). Existing templates, including user-authored ones, keep working.

    Two adjacent fixes fell out of it. ScriptTemplate.declared_values is now the raw <FIELD> = (...) reader and declared_modes is that plus normalize_modes — because templates declare more than modes through the same convention (mayatk's bake template also carries BRIDGE_OUTPUT_EXT / BRIDGE_OUTPUT / BRIDGE_TIMEOUT), and folding mode spellings in the shared reader would rewrite any other field whose value collided with a legacy one. And ScriptLaunchBridge.list_template_modes filtered against spec.modes alone, so any bridge with a run_spec relabelled its blocking templates as send_to — the discovery helpers fall back to allowed[0] for anything outside the list, and the panel then routes a blocking recipe through send(), leaving __OUT_FILE__ empty and the app to fail minutes in. It now derives from the deliverer registry via the new modes property; insertion order keeps send_to as the lenient fallback.

    ROUND_TRIP now means only "the result lands back in the host", leaving the artifact shape to whichever deliverer is registered for it: ScriptRoundTripDeliverer for the target editing the payload in place (RizomUV, judged by REWRITTEN), ScriptRunDeliverer for the target writing a new intermediate (mayatk's Blender lightmap bake returns a manifest, judged by CREATED). round_trip() therefore gates on the deliverer registry rather than on round_trip_spec — a bridge whose round trip produces an artifact declares modes=(SAVE_AS, ROUND_TRIP) on its existing run_spec, since the machinery is identical and only the destination differs — and takes an optional out, normalized through resolve_save_path exactly as save_as does. Without this the bake had to advertise itself as save_as and send the artist looking for a deliverable that is deliberately never kept.

  • 2026-08-08 — PlateEmitter and Color.kelvin_to_linear_rgb — the two primitives a fixture light needs, owned once. Turning a thin luminaire plate into a rectangular area light is the same arithmetic in every host (thin axis = emission normal, other two = rectangle, stand clear of the housing), so geo_utils.plate_emitter.PlateEmitter.from_bounds owns it and blendertk's lights_from_geometry now calls it instead of carrying its own copy — which is how the two would have drifted the moment mayatk grew a twin. It carries both hard-won guards with it: the coplanar rule (a reference point derived from a ceiling grid lies IN that grid's plane, so the sign of the displacement to it is modelling noise — believed only past the plate's own thickness, floored at 1% of its long edge for the zero-thickness plane people model lenses as) and the clearance rule (offset applies on top of half the plate's thickness, or the light sits inside a housing that blocks it). up_axis is the one host difference: Blender resolves an ambiguous plate down about Z, Maya about Y.

    ImgUtils.kelvin_to_linear_rgb converts a colour temperature to LINEAR RGB — the value a renderer wants, against the sRGB-encoded one a colour picker shows, which fed to a light reads markedly too pale. Tanner Helland's fit to the Planckian locus, linearised, normalised to peak 1 so temperature sets hue and intensity stays a separate dial. It sits with the other colour-space conversions (srgb_to_linear, convert_rgb_to_hsv) rather than on the Color value type, because it CONVERTS into a colour rather than describing one; written pure-stdlib so it still answers when this module's optional numpy/Pillow imports have degraded to None.

  • 2026-08-08 — The room was black in its own WebXR preview: three correct behaviours compounding one lost texture. Fixed with a metallic_roughness sidecar section, plus web texture optimization (125.5 MB -> 6.4 MB). Bisected with a headless-browser screenshot harness against the live preview server, eliminating in turn the KHR_texture_transform path, the material clones, the embed dedupe and the lightmap image itself — each structurally correct — until the one number left was the packed ORM: FBX2glTF writes a solid-white metallic/roughness texture when it cannot resolve the source maps (measured: the room's real metallic map means 0.12). glTF reads metallic from blue, so the whole room rendered metallic=1; a pure metal has no diffuse response; a lightmap contributes only to diffuse; and a lightmapped viewer turns its scene lights off. Three correct behaviours, one lost map, zero error messages. set_glb_metallic_roughness is the third sidecar applier: it packs the ORIGINAL maps through MapFactory.pack_orm_texture (the registry's one ORM packer — R fills white so the image stays neutral if later wired as occlusion), embeds through the session's dedupe cache, and repairs by material name like its two siblings.

    The application order was the second half of the bug. The preview deliverer converted (which wires lightmaps, CLONING each material as it stands) and applied the sidecar afterwards — so every repair landed on the base material no primitive references anymore. Nested-dict sharing let base colour leak through to the clones by luck; top-level keys (emissiveFactor, and the new metallicRoughnessTexture would have joined it) did not. The deliverer now converts with lightmaps=False and runs one open_glb session ordered sidecar-then-lightmaps, so clones copy fully repaired materials.

    optimize_glb_textures is the new web-delivery pass (opt-in; the preview deliverer runs it, plain conversions stay byte-stable): every embedded image is decoded, resized to a longest edge (2048 default; images the lightmap_web manifest names are exempt — the bake sized those deliberately), and re-encoded WebP (alpha-capable, universally decoded by WebXR-class browsers), with textures gaining the standard EXT_texture_webp binding and their plain source kept as the spec's fallback. The BIN chunk is repacked — image payloads replaced, former data-URI images relocated into it (dropping base64's 33%), every other bufferView copied verbatim with offsets recomputed; a repack forces the writer down the full-rewrite path (GlbEdit.replace_rest), because the in-place fast path would write the new JSON over the OLD BIN. Measured on the production room end to end, with a headless screenshot proving the result renders: 125.5 MB -> 6.37 MB, image payload 106.6 -> 3.7 MB, KHR transforms and all 48 per-instance bindings intact. KTX2/basis (GPU-resident compression, the real headset-memory win) is deliberately not this pass — it needs an external encoder and KHR_texture_basisu; backlogged.

  • 2026-08-08 — A GLB embed reuses bytes the file already carries: 125.5 MB -> 94.7 MB on a production room. _embed_image's dedupe cache is keyed by the source PATH, so it could only collapse embeds made during one session. It could not see that the FBX->GLB conversion had already written the same texture into the BIN chunk — and since the scene sidecar exists precisely to re-apply the channels FBX translation drops (base colour, emissive), the normal case was two copies of the same bytes, the second inflated ~33% by base64 in the JSON chunk. Measured on the OFFICE_ENV room: base colour, table diffuse and emissive were each present twice — 23.1 MB of duplicate payload costing 30.8 MB on disk, a quarter of the deliverable, and it grew the JSON chunk to 35 MB against 2.6 MB of actual geometry. GlbEdit.image_by_content now indexes the existing images by sha256 of their payload (bufferView-backed and data: alike, built lazily so a session that embeds nothing never reads past the JSON) and texture_for_image points a texture at the survivor. Content-addressed, not name-addressed: the two copies arrive by different routes and agree on nothing else — the bufferView image is named by the FBX exporter, the sidecar's by its source path. Verified end to end on the real preview push: 14 images -> 11, JSON 35.06 MB -> 0.1 MB, identical KHR_texture_transform wiring and 48 per-instance rects.

  • 2026-08-08 — Per-instance lightmap rects in GLB, via KHR_texture_transform — plus ImgUtils.flip_rect_v. A lightmapped scene with instanced geometry needs each instance to sample its own patch of a shared atlas while the mesh data stays shared. glTF already has the standard way to say that, so apply_glb_lightmaps uses it rather than inventing an extras contract only our own viewer would understand: a node whose manifest record carries a non-identity scaleOffset gets a cloned material whose occlusionTexture carries a KHR_texture_transform, and extensionsUsed declares it — so three.js, model-viewer, Babylon and the production WebXR app all render it correctly with no viewer changes at all. When two such nodes share a glTF mesh (FBX2glTF preserves Maya instancing as node-level mesh reuse — probe-measured), the mesh JSON entry is cloned while its accessors and bufferViews are not, so per-instance materials cost no geometry. One atlas still embeds once no matter how many instances bind it, and an identity-rect manifest produces exactly what it did before: no clones, no extension. The encode is now lazy at the bind point rather than eager — an entry whose primitives all fail the TEXCOORD_1/already-claimed guards used to leave an orphan embedded image behind.

    ImgUtils.flip_rect_v is the shared V-flip: glTF's V axis runs opposite Blender's, so the same rect is [sx, sy, ox, 1-sy-oy] there — scales unchanged, offset mirrored. One helper (involutory, unit-tested as such) rather than the constant inlined at each site; the Unity path stays bottom-left native and never calls it.

  • 2026-08-08 — StrUtils.truncate(..., mode="path") — shorten a path by dropping whole components, never half a folder name. The existing middle mode cuts by character count, so a path comes back with debris at the seam (O:/Cloud/Projects/jets/..ures/body_DIFF.png) — the elision reads as part of a name and the eye has to work out where the real boundary was. The new mode cuts only at separators, and grows what it keeps in the order that carries meaning: the head opens on the drive/root and its first directory (a bare O:/… locates nothing), the tail then takes the filename and as many of its parents as fit, and leftover budget goes back to the head — O:/Cloud/Projects/…/textures/c130j_body_DIFF.png at 48. Separator style is preserved (a backslash path stays backslashed) and a leading separator run survives, so a UNC share keeps its //server/. Degenerate shapes fall back to middle rather than inventing a boundary: fewer than three components, a filename that alone overruns the budget, or a string with no separator at all — and the marker is never emitted between components that were adjacent (an ellipsis that elides nothing). 11 tests including a sweep asserting the result never exceeds length at any budget from 8 to 80, and one pinning the docstring example (a wrong example is a wrong contract). Consumed by uitk's TableWidget.set_column_truncation and the mayatk/blendertk Texture Path Editor's "Truncate Texture Paths" toggle.

  • 2026-08-08 — GLB deliverables self-feed committed lightmaps: MeshConvert.apply_glb_lightmaps + ImgUtils.encode_hdr_for_web. The scene-state principle made real for GLB: a host DCC's bake commits to the scene (markers + a lightmap_metadata manifest on the data_export carrier that rides every FBX — the same manifest unitytk already binds Unity-native lightmaps from), and the deliverable feeds its own repair. Two probe-verified facts make it zero-coupling: FBX2glTF v0.13.1 exports the second FBX UV set as TEXCOORD_1 (RAW_VERTEX_ATTRIBUTE_UV1 in the tagged source), and the --user-properties flag fbx_to_glb already passes transcribes the carrier's string props into node extras with no size limit. So the manifest is already inside every GLB this converter produces, and apply_glb_lightmaps — run unconditionally in fbx_to_glb's existing post-conversion edit session (lightmaps=True escape hatch; warn-only like the alpha repair) — reads it back out, resolves the EXR basenames via the manifest's dir locate hint, encodes once per unique atlas, and binds occlusionTexture on texCoord 1 plus the root-extras lightmap_web manifest the WebXR viewer already parses. Every consumer of fbx_to_glb — the Scene Exporters, the WebXR previews, tentacle's Maya quick export — got lightmapped GLBs with zero edits. Absent manifest = clean no-op; every miss is loud (no TEXCOORD_1 in the primitives, an unresolvable map, two objects claiming one material with different maps, a newer manifest schema — refused rather than misread, since wrong binding reads as a bad bake). Web intensity is the encode scalar only: authored intensity is already in the EXR texels.

    encode_hdr_for_web is the cv2 twin of blendertk's bpy-I/O encode_for_web (Blender ships no cv2): raw float read — deliberately NOT load_image, which quantizes to 8-bit — nonzero-percentile divide before the clipping linear_to_srgb, PNG bytes + the recovery scalar. Both test suites pin the same golden constant (0.5 → scalar 0.5) so the implementations cannot drift. _embed_image_bytes splits the tail off _embed_image so in-memory encoders share the session dedupe cache (an atlas shared by six objects embeds once).