- 2026-11-10 --
CadImportconverts real CAD exports, and records what identifies each part across the source's revisions: its part number, the properties its CAD system wrote and a fingerprint of its shape (file_utils/cad_import/_cad_import.pyconvert(part_id=), newPART_KEYS,REFINE_PATIENCE,REFINE_MAX_GROWTH;_occ.py;cad_record.pynewCadPart.part_number/properties/fingerprint,CadMesh.tessellated_faces,CadDeviation.source_gap;file_utils/README.md,docs/README.md;.github/workflows/tests.ymlinstalls thecadextra). Run against NIST's 17 public AP242 test models (exported from several CAD systems; public domain, kept out of the repo), the import crashed on one, raised on another and failed four of the rest. Fixed: a part the reader leaves unnamed killed the process (OCP 8'sFindAttributecrashes on a label without the attribute;IsAttributeasks first). A face the file states as triangles alone (AP242 tessellated geometry) raised inverifyand its part measured 0 x 0 x 0; it is written as the file states it, counted (tessellated_faces) and named in a warning, and sized from its triangles. A real export states an edge within its tolerance of its face, not on it (up to 0.104 mm in NIST's models), and a mesh node on the edge sits on the edge's curve: the deviation is now measured on the triangle through each node's surface point at its own UV, its sag the tessellation's own error, and the largest node offset is recorded assource_gapand named in a warning past the budget; the written mesh lies within their sum of the surfaces (review: subtracting the offsets from the written mesh's distance instead hid a 0.065 sag on a sphere whose nodes sat outward). OpenCascade's mesher is not monotonic in its chord (on one model a tighter chord measured 0.045 where the looser one had 0.036, and the next tighter 0.0098), so a round that fails to gain is tried tighter from its own chord and the best mesh kept;REFINE_PATIENCErounds in a row that remove less thanREFINE_GAINof the peak's excess over the budget stop it, and so does a mesh pastREFINE_MAX_GROWTH(16) times the first one's triangles; the warning says which stopped it. The mesher now checks its deflection inside every face, not only freeform ones (an analytic sphere strayed 1.6 times the chord, now 1.3).heatmapswithoutverifysays none is written. A part's area and volume are OpenCascade's integration over its surfaces: on NIST's models within 4e-5 of a 0.002 mm mesh, and 0.9% low on a deliberately wavy freeform panel (a review), which the record now says in place of "exact". The fingerprint is an exact-match key: the same geometry read again keeps it, a re-export through another system can hash apart, matched within a tolerance onsize,areaandvolume. A part with no surface (a PMI file's points or curves) is recorded without a mesh and told once at info level, not warned of per profile. New:part_number(the STEP product's id),properties(the user-defined attributes OpenCascade reads, keyed as the CAD system wrote them: NX writes the file name as the id and keepsPart NumberandRevisionas properties), andfingerprint(a hash of volume, centroid and inertia in the part's frame, scaled by its size: a renamed copy or a metres file keeps it, a mirrored copy does not). A piece OpenCascade splits from a product (a solid beside its PMI points) states the product's; a product of its own whose id is blank states none.part_idpicks a part's id: by default the part number, else the name; any ofPART_KEYS, a property's name, a callable over the part's facts, or a chain of them. An id keeps a part number's dashes and dots (HX-100.2) and letters of any alphabet; parts of different products sharing one are numbered and named in a warning; a key no part holds is named in one. NIST: 17 of 17 convert with every GLB valid (0 errors, 0 warnings), 16 with every mesh within budget; the 17th is a mesher limit, recorded as over budget and logged.- Test(s):
test_cad_import.py+24 (62): newTestRealExports(an unnamed part, red an access violation; triangles-only faces, red a raise and a 0 size; an edge stated 0.05 off its face within budget withsource_gaprecorded, red 0.05 over; a source gap never hides a curved triangle's sag, red 0.32 for 0.38; a loose edge never stops the refinement a sphere beside it needs, red stopped over budget; a part with no surface recorded without a warning, red a warning per profile), newTestIdentity(part number and properties recorded; ids take the part number first; a property, a callable, the fingerprint or a chain; a key no part holds warned; parts of two products sharing an id numbered and warned; pieces of one product share its number unwarned, red warned; the fingerprint the same renamed and in metres, different mirrored; a piece takes its product's number, red under a mutation; a blank-id product states none, red its assembly's; IGES states none),TestVerify+test_one_worse_round_never_stops_the_refinement(red: stopped over budget), a worse last round leaves the best mesh in the GLB, rounds spent gaining told apart from a stall, a gain resets the patience, the triangle ceiling stops it (each red under its mutation: no restore, never stop, never reset, no ceiling), heatmaps without verify warned (red silent), the contract +part_idrefused before any work and ids keeping dashes, dots and accents (redCaf_Bracket);test_cad_record.py5. 67 passed.
- Test(s):
- 2026-11-10 -- Removed:
MapOptimizer.optimize_map'scheck_existing, its deprecation window closed (due in 0.15.0;core_utils/engines/textures/map_optimizer.py). It reused an output on its mtime alone, so a copy written under other settings or from another file of the same name shipped stale;MapOptimizer.stage_mapsreuses an encode on the source's identity and the settings. Passing it is now aTypeError. No caller in the ecosystem passes it.- Test(s):
test_map_optimizer.pytest_optimize_map_check_existing_is_removed(red first: it warned and reused); the module 145 passed;check_api_surface.py pythontk --checkretirements on time.
- Test(s):
- 2026-11-09 -- New:
CadImportconverts STEP and IGES into one mesh per unique part per tessellation profile, an assembled GLB per profile, and aCadRecordof the product's truth; withverifyevery mesh is measured against the exact CAD surfaces and held to its profile's budget (file_utils/cad_import/new:_cad_import.pyCadImport,_occ.pyevery OpenCascade call,cad_record.pyCadRecord; rootDEFAULT_INCLUDE,test/surface_snapshot.json; thepythontk[cad]extra, Python 3.11+;.github/workflows/tests.ymlinstalls it andlibgl1;file_utils/README.md,docs/README.md).ptk.CadImport.convert("product.step", profiles=["hero", "realtime"], verify=True)writes, beside the file:parts/<part>_<profile>.objin millimetres with exact surface normals and one material per colour, each repeated part tessellated once;product_<profile>.glb, the whole tree with names, colours and placements in metres and +Y up (OpenCascade's own glTF writer: 0 errors and 0 warnings from the Khronos validator); andproduct.cad.json, the assembly tree as placements, each part's size, area and volume integrated over its exact surfaces, colours in linear RGBA, the file's SHA-256 and each mesh's chord. A file stated in any unit reads in millimetres, whatever unit the OpenCascade session reads in. Named profiles (hero,standard,realtime) set the mesher's chord and angle; a profile is also a mapping (name,chord,angle, optionalbudget, default the chord; any other key is refused). The mesher bounds its nodes and edges, not a triangle's inside: an analytic sphere's realtime mesh strays 1.6 times its chord. Soverifymeasures each triangle's centroid and edge midpoints against their face's exact surface (where a flat triangle strays furthest: a 45-point grid per triangle found the same peaks) and tessellates a part over its budget again with a tighter chord: at mostREFINE_ROUNDS, never belowREFINE_FLOORof the chord at once, stopping once a round gains less thanREFINE_GAINand keeping the best mesh, and naming a part still over budget in a warning. No one OpenCascade projection holds on a freeform B-spline (the global one read a seventh of a wavy panel's own nodes as up to 1.6 mm off it, the seeded one falls back to it, the local distance search diverges elsewhere); every candidate lands on the surface and so only overstates, so the least of them and the distance to the sample's nearest node is kept, which matched the true peak on that panel and the closed form on a sphere. A face the mesher leaves empty is recorded (unmeshed_faces,unmeshed_area) and named, and failswithinonce its area exceeds a budget-wide square.heatmapswrites each mesh, face for face as its OBJ, coloured by its deviation (viridis to the budget, magenta past it). Part ids never collide ignoring case (they name files), profile names must be file-safe, a file that fails to read leaves nothing behind and the error says to export STEP AP242, and native files (Creo, CATIA, SolidWorks, NX) are refused with the same fix. A part's meshes sit in the frame OpenCascade's glTF writer places it in. The record names its source by file name only.- Test(s):
test_cad_import.pynew (38: the contract before OpenCascade loads, a CI guard that fails where the backend is missing, a misspelt profile key, then assemblies OpenCascade itself writes, so every expected number is exact: 3 unique parts and 7 placements, an unnamed placement named for its part, a pin placed through a sub-assembly at its composed position, exact area and volume, a face's own colour kept, outward triangles with radial normals, the GLB throughconftest.gltf_validator, a metres file and a metres session both read as millimetres, an IGES part, a failed read; and verification: every mesh of a sphere and a torus within budget at two profiles, red first with no refinement (the sphere 1.2 times over and named), refinement that stops helping keeping the best mesh, the GLB holding the refined tessellation, the peak exactly the distance at its point, a flat part's peak located, a freeform B-spline panel within budget with no tighter tessellation (red first: the false peaks drove the refinement past a million triangles; red again with the local search removed), a located part's mesh in the GLB's frame, an empty sliver face recorded and named, the heatmap face for face as its OBJ (red first with alternating colours));test_cad_record.pynew (5). 43 passed. A public AP214 assembly (5 parts, 18 placements, 3 levels) converts in 2 s with every mesh verified and both GLBs valid.
- Test(s):
- 2026-11-09 -- Fix:
MeshOps.comparemeasures deviation both ways and finds the true peak; it reported a coarsened sphere a hundred times too close and never saw a part the input had lost (file_utils/mesh_ops.pyMeshOps.compare, newheatmap_path/heatmap_range, new keyshausdorff_peak_at,input_to_reference_peak,reference_to_input_peak,heatmap,heatmap_range; defaultsample_num50000; private_union_diagonal,_surface_samples,_distances). It sampled only the input's vertices, in one direction, and dropped any sample farther than half the bounding diagonal: a 528-face radius-10 sphere measured 0.0013 against its true chord sag of 0.1689, and a reference holding a second sphere 100 units away measured the same. Each surface is now sampled (its vertices, an even stride of them pastsample_num, andsample_numarea-weighted face points from a fixed seed, so the same two meshes always measure the same and a gate's verdict never flips on a rerun; the faces are weighed in blocks into one number per face, which the draw reuses as its distribution, so a multi-GB scan adds no copy of its face table), every sample is measured against the other surface with nothing dropped for distance, and the result says which direction the peak came from and where it lies.samplesnow counts both directions; asample_numbelow 1 and aheatmap_rangeat or below 0 are refused.heatmap_pathwrites the denser mesh coloured by its distance to the other (viridis, 0 toheatmap_range). The photogrammetry mesh gate'shausdorff_peak_pctnow reads the real deviation, so a configuredmax_hausdorff_peak_pctrule may fail where it passed before.- Test(s): red first, then green:
test_mesh_ops.pynewTestCompareDeviation(9: the closed-form sag within 2% in either order, red 0.0013; a part missing from the input fails and is located, red 0.0013; a part the reference lacks fails; identical meshes measure zero; three runs measure the same, where Montecarlo sampling gave four peaks in four calls; a sample count below 1 refused, redZeroDivisionError; a heatmap range at or below 0 refused, red accepted; the heatmap colours the denser mesh by its distance; keys gate-prefix safe). 37 passed, 1 skipped (the missing-engine path); extapps' photogrammetry workflows 143 passed.
- Test(s): red first, then green: