Repository navigation
Releases
0.3.137 - what 0.3.136 got wrong, and the shape nobody could check
Two reports on 0.3.136. The first found that the multi-view check this project
had just added could say "view 1 and view 2 agree on Z (0.000..0.000) ... is one
part" about two views 70 mm apart, and that the world boxes it printed had
min > max. The second found a swept solid that had folded back on itself
reporting ok:true, and two measuring tools disagreeing about whether a part had
a solid at all. The first report caught this project's own faults twice over: a
wrong comparison, and a wrong format() call.
N2 - THE WORLD BOX WAS PRINTED IN THE WRONG ORDER. The box is
[minx,miny,minz,maxx,maxy,maxz] and the reply passed it to format(*box), so
min-x landed against min-y: a 15 x 24 mm sketch printed as
X 0.000..-0.000 Y 0.000..15.367 Z 23.876..0.000. The alignment check read the
same list correctly, so the numbers were right and the sentence was wrong -
the worse of the two, because the sentence is what gets quoted back.
N1 - "CO-PLANAR" IS NOT "ALIGNED". The check compared all three axes, so two
sketches on xy "agreed on Z" at 0.000..0.000 because both were on that plane. It
now compares only the axes a frame's u/v point along, ignores a zero-width range
(an edge is not a direction), and requires EVERY shared axis to overlap. Two
views 70 mm apart now read: they share a sheet direction but do not line up,
X 0.000..60.000 vs 70.000..130.000 mm - NOT aligned.
N3 - A SHARED ANCHOR CANNOT CLOSE A GAP THE SHEET PUT THERE. A real drawing lays
its views out side by side; one anchor moves the whole sheet and leaves the 70 mm
between them intact. Each view now takes placement={'origin_2d': [x, y]} or
align={'view': 1, 'axis': 'x', 'anchor': 'center'}. Measured on B30: align moves
the second view -70.0000 mm and two pads then share X 0.000..60.000.
N4 / N5 - WHAT A FAILED CALL LEAVES BEHIND. SKETCH_NAME_TAKEN said "Nothing was
kept" while the empty sketch stayed as TopSection_new; it is now DELETED again
through the selection (1 -> 1 in list_features) and modified is reported
truthfully. The deviation probes left an empty MCP_Probes set because
HybridBodies.Remove does not exist on this build (".Remove");
selection.Add(set) + Delete() does remove it (1 -> 0).
N6 - stats is an alias and now says so (deprecated_fields).
C3 / C4 - WHICH BODY A MEASUREMENT MEANS. catia_get_bounding_box only ever
looked at Part.MainBody, so a solid in a second body answered "That part has no
solid shape yet" while catia_get_inertia read 2577 mm3 from it. The box tool now
takes body=, falls back to the one body that holds a solid (and says so in
scope), and refuses with the list when two do. catia_activate_document now says
when the body this session had in work belonged to another document and it fell
back to an empty main body.
C6 - A BOOLEAN HAS A TYPE. A boolean the caller named came out type "unknown"
while the pad above it said "pad": the NLS prefix table cannot read a chosen
name, and a feature from body.Shapes is a bare Item. The call that builds it now
records what it built, per document.
C7 - A PLAN THAT ONLY ACTIVATED A DOCUMENT BUILDS NOTHING THERE. A plan whose
first step was catia_activate_document listed 36 pre-existing elements as "36
element(s) this plan had already built" and offered rollback:true, which would
have deleted somebody else's model. Nothing in a switched-to document is
attributed to the plan any more; a document the plan CREATED still counts whole.
C1 - THE SHAPE SIGNAL A SWEEP ACTUALLY OWNS. The report's collapsing sweep came
out 1.4 % from the ruled sum, so the volume agreement it printed was true and
useless. The 2 % branch no longer claims the loft IS the chain of truncated
cones - it says every station was taken, and that a surface can still fold back.
And the stations are now checked for GROWTH RATE (mm of radius per mm of spine):
that case reaches 2.53 mm/mm into the tip and 0.71 mm/mm through the bulge, and
from 0.5 the reply carries a WARNING naming the segment and pointing at
catia_shape_audit's narrowest_bins. A NEGATIVE gap against the ruled sum is
still called a verdict: oblique sections can only ADD material.
C2 - THE LOFT'S CLOSING POINT. AddSectionToLoft(section, orientation,
closing_point) has had that third argument since 0.3.77, always None.
catia_solid_loft(closing_points=[...]) now passes it, and it is honoured:
measured on B30 2026-11-15 on two eccentric ellipses, 4523.6938 mm3 automatic
against 4523.8263 mm3 with the point at their +Y extreme. This is the control
that stops one side of a moving-section surface twisting against the other.
NOT available, measured and recorded in the ceilings:
HybridShapeLoft.SetSectionCoupling exists and every call answers
".SetSectionCoupling" - direct and dynamic dispatch, values 1 to 4 - so
SectionCoupling only reads back (always 1). Also measured and recorded:
Part.InWorkObject is not the active body; it reads back the feature being edited
('凸台.1') with a bare Item whose Shapes answers ".Shapes".
NOT IN THIS RELEASE, AND WHY: per-FACE colour (C5) needs a colour route that does
not go through the tree; clean="solid_only" still leaves datum planes visible
(C8) because hiding GSD sets also hides the solids built on them; the imported
outline still has no driving dimensions (N7), which needs a fitting and
constraint design of its own rather than a parameter.
VERIFIED ON B30: scripts/b30_v0438_acceptance.py 26/26 - the not-aligned verdict
and the right-way-round boxes, align moving a view -70.0000 mm, a refused name
deleting its sketch, no MCP_Probes left, a second-body solid measured, the empty
work body named, a boolean typed, a plan attributing nothing across documents,
the steep-growth warning, and a closing point that changes the solid.
2212 tests. 153 tools. 446 files, 2358633 bytes.
0.3.136 - the two gaps that were real
The 2026-11-15 reports ended their correction with a list of what is left. Two
of those items were "already exists but nothing says so", and 0.3.135 closed
them. Two were genuinely missing, and they are what this release does: C2, a
multi-view sheet has no datum, and R2, the polyline-to-spline conversion's cost
cannot be read back. Both are answered with measurements that can come out wrong,
and the wrong case is in the release notes as well as the right one.
C2 - SEVERAL VIEWS, ONE SHEET FRAME
import_drawing(views=[...]) several regions in ONE call, one sketch each.
Every entry takes the page/region/layer/
min_size/max_aspect/closed_only filters plus
its own plane, offset and name.
ONE ANCHOR FOR THE WHOLE SHEET every view is shifted so a single anchor
(default: the lower-left corner of the union)
sits at the sketch origin. This is the fix: a
per-view origin='bbox' - the only thing that
existed - put each view's own corner at (0,0)
and destroyed exactly the alignment the sheet
had.
THE SHARED AXIS IS u the reply compares the views' world boxes and
names the axes they agree on: "view 1 and view
2 agree on X (0.000..60.000 mm)". A FRONT view
is {'type':'frame','origin':[0,0,0],
'u':[1,0,0],'v':[0,0,1]}, a TOP view is the
same with 'v':[0,1,0]; both put the sheet's x
on world X.
MEASURED (B30 2026-11-15) a 60 x 40 view on the front frame and a 60 x 25
view on the top frame padded to an X range of
0.000..60.000 IN BOTH SOLIDS, and the front
view alone is already 0.000..60.000 - so the
agreement is not two boxes covering for each
other.
THE NEGATIVE CASE IS REPORTED 'xy' and 'zx' have u vectors of +X and +Z, so
the same sheet answers NO axis agrees instead
of a green tick. Top-level page/region/layers/
min_size/max_aspect/closed_only/plane/offset/
name beside views are REFUSED as ambiguous, and
so is origin='bbox'. Every entry is checked
before any file is read.
R2 - THE CONVERSION NOW SAYS WHAT IT COST
deviation_mm import_drawing(profile_mode='spline') and
catia_gsd_spline(element=..., closed=true)
probe the outline's SIDE MIDPOINTS - where an
interpolating curve bulges - plus three corners
it must pass through. The corners are the
calibration: 0.0000 mm means the probe measures
what it claims; when they do not, the reply
says the reading is NOT calibrated instead of
quoting a maximum.
MEASURED (B30 2026-11-15) a 60 x 40 rectangle: corners 0.0000 mm, side
midpoints 18.3594 mm for the sketch spline and
13.8286 mm for the 3D spline.
THE TWO ARE DIFFERENT CURVES 100 x 40 rectangle, same side midpoint:
33.1387 mm (2D sketch spline) vs 25.6325 mm
(3D gsd spline); at the centre 41.1690 vs
45.6325. Neither side quotes the other's
number, and catia_gsd_spline's description
carries both so the difference is not a
surprise later.
deviation=false turns the measurement off on either tool. It
costs one point element per probe, deleted
again after.
WHAT WAS MEASURED TO GET HERE, AND WHAT STAYED IMPOSSIBLE
A point-on-curve cannot be placed on a Line2D inside a sketch, nor on a CLOSED
spline - both answer "方法 UpdateObject 失败" - so the probes are COORDINATE
points and the target is the curve itself.
A SKETCH exposes NOTHING to SPAWorkbench: Length, Area, Volume, GetPoint,
GetCOG, GetPlane and GetBoundingBox all fail on a sketch reference. So a view's
world box is computed from its frame, and the alignment is PROVEN by padding
both views and reading the solids. There is no way to ask a sketch where it is.
Both facts are in catia_diagnose's ceilings, beside the 2D-vs-3D interpolant
difference.
A cell-count correction, found by this release's own acceptance run: a four-LINE
rectangle reports FIVE sketch elements, because the sketch's own absolute axis
(绝对轴) is one of them. The 0.3.135 reply credited the extra element to the
spline's control points, which was wrong for a segments import; both replies
now name both reasons and the mode they apply to.
VERIFIED ON B30: scripts/b30_v0437_acceptance.py 17/17 - single-view regression,
deviation quoted and calibrated, its own number from gsd_spline and different
from the 2D one, deviation=false silent, two sketches from one call, the shared
axis named, both solids 0..60 in X, the NO-axis-agrees case, and an explicit
anchor.
2184 tests. 153 tools. 440 files, 2317389 bytes.
0.3.135 - one curve, and saying so
Two reports on 0.3.134, the second correcting the first. The first said "MCP has
no way to turn an imported 110-line outline into one curve, so a PDF can only
become a plate". The second found that catia_gsd_spline(element=, closed=true) had always done it, and lofted to 4838.70 mm3. Both were
right about the defect: THE CAPABILITY EXISTED AND NOTHING SAID SO - not the
LOFT_REJECTED hint, not the import reply, not read_drawing, whose
closed:true / free_ends:0 promised a pad and said nothing about a loft.
R1 / R5 - THE QUESTION IS NOW ASKED AT EVERY STEP THAT ANSWERS IT
read_drawing each contour carries curves and loft_ready: 110 lines is
curves=110, loft_ready=false; a circle is 1/true. The page-wide
curve statistics moved to page_level_stats, because "775 arcs
fitted" beside "the profile is 110 lines" read like a bug twice.
import_drawing(profile_mode="segments"|"spline")
"spline" draws each multi-element contour as ONE closed spline
at import time. Measured on B30 2026-11-15 on the report's own
sheet: one spline through 111 control points, two sections loft
to 4838.39 mm3 - catia_solid_loft took the sketch directly. A
circle or ellipse is left EXACT (it is already one curve) and
the reply says how many of each. The reply also names the
sharpest turn and how many corners are sharper than 20 degrees,
because a spline is interpolating: a 20x12 rectangle became
24.04 x 21.46 mm when lofted (measured 2026-11-03).
import reply "1 curve(s): pad/pocket OK and a loft section needs one" or
"N curve(s) ... this outline cannot be one as it is", plus the
two ways to make it one.
LOFT_REJECTED quotes the paste-able call, with the offending sketch's real
name in it: catia_gsd_spline(element='', closed=true).
C2 - import_drawing.plane ACCEPTS THE FRAME OBJECT create_sketch TAKES. The
description had promised "the same values catia_create_sketch takes" since
0.3.128 while the schema answered "received object; expected string". A frame is
how two orthographic views are placed on a shared datum. Measured:
{"type":"frame","origin":[0,0,33],"u":[0,1,0],"v":[0,0,1]} creates the sketch on
that datum.
R3 - A NAME A PLAN CAN RELY ON. catia_create_sketch(name=...) pins the sketch
name, and a name already in use is REFUSED (SKETCH_NAME_TAKEN, with the names in
use) instead of being silently suffixed to 草图.1#2 - which is invisible while
writing a run_plan, whose args are static strings. A loft section that does not
exist now lists what the part actually holds.
C4 - catia_set_parameter on a name that is not there answers PARAMETER_NOT_FOUND
with the closest matches, like the formula-driven branch has since 0.3.128.
C6 - catia_get_inertia SAYS WHICH BODY IT MEASURED. Measured: the same part gives
2000 mm3 with SideView in work and 24000 mm3 with 零件几何体 in work, and the
reply said only volume_mm3. It now carries scope (active_body|main_body) and body,
and run_plan passes both through with a note when the number is one body.
C3 / C7 - MEASURED, NOT FAKED. Dimension VALUES cannot be read off that drawing:
text_runs = 0, because every callout was converted to curves before export - the
numbers are geometry, not text. Recorded as UNAVAILABLE_ON_B30, a new status
word: the data is missing, so no wrapper could help. And clean hides the
sketches inside bodies but not geometrical sets, so a visible GSD element
overlays the solid - the report's "black curve" sits on a part whose own reply
listed GSD_Set with one SHOWN element.
A MEASUREMENT THAT CORRECTED THE REPORT: the spline import leaves its control
points in the sketch (111 point elements beside the one spline). That is CATIA's
2D spline, not a leak - a 4-point closed spline leaves 5 - and
GeometricElements has no Remove (".Remove", measured). Both counts are
now stated, instead of letting 113 elements look like the import ran three times.
VERIFIED ON B30: scripts/b30_v0436_audit.py 12/12 (curves/loft_ready, one curve
imported, loft accepted at 4838.39 mm3, frame plane, pinned and refused sketch
names, the section list in the error, PARAMETER_NOT_FOUND, scope+body, and the
paste-able hint inside a real LOFT_REJECTED).
2153 tests. 153 tools. 435 files, 2284319 bytes.
0.3.134 - a test that could only pass on a slow machine
CI on 0.3.133 went red on 3.12 and GREEN on 3.10 at the same commit:
FAILED test_route_matrix.py::test_two_calls_of_one_scope_do_quote_a_delta
AssertionError: assert '%' in '... readings are equal to the clock, not to
each other. warm_up=true pays this cost ...'
The test runs three real warm calls and demanded a percentage in the reply. The
3.12 runner finished the second one faster than the clock could resolve, so the
previous reading was 0 ms - and 0.3.132 stopped quoting a ratio against zero on
purpose, because it is a division by zero dressed as a measurement. The branch is
right; the assertion was written when every machine was slow enough to measure it.
It now accepts either honest outcome - a delta, or the sentence naming the 0 ms
reading - and forbids the one reply that would be wrong: claiming there was no
earlier warm call. The 0 ms branch itself is still covered deterministically by
test_reported_issues_2026_11_13_v0433.py::
test_a_zero_ms_warm_reading_is_still_a_previous_call, which drives
describe(elapsed_ms=0.0) directly and does not depend on speed.
NOTHING IN THE SHIPPED TOOL BEHAVIOUR CHANGED. The difference between 0.3.133 and
this release is one test, the version strings and the changelog. 0.3.133's zip is
correct for a user of the tools and red for a user running the suite on a fast
machine, which is reason enough to publish rather than sit on it.
Also in this line, unchanged from 0.3.133 and worth repeating because it is the
answer to "does this server do sheet metal?" - the parameters, yes; the geometry,
no:
catia_sheetmetal_parameters reads AND sets thickness / bend radius / K factor
/ corner relief through part.Parameters, where
Value is writable (measured: 线宽 2.0 -> 1.5 moved
GetThickness() to 1.5); create=true adds the set.
The K factor needs DIN 一般公式\Activity off, and
the reply names the rule it switched off.
catia_new_sheetmetal catia_new_part + the set.
NO wall / flange / bend / unfold / flat pattern: SheetMetalFactory has 23
members and NO AddNew* at all.
2136 tests. 153 tools. 433 files, 2265708 bytes.
0.3.133 - sheet metal: the parameters were reachable all along
The 2026-11-03 report asked for eight sheet-metal tools after modelling a real
bracket without any of them. What this project recorded in reply was a ceiling
saying the parameters "can be read and nothing can be built from them". That was
half wrong, and the half that was wrong is the useful half.
WHAT WAS RE-MEASURED (scripts/b30_v0434_probe_sheetmetal.py, B30 2026-11-15)
-
Part.SheetMetalFactory - 23 members, 22 of them the COM base, one real
method: CreateSheetMetalParameters(). Its result - 23 members, one method,
GetThickness(). NO AddNew* ANYWHERE, so no wall, flange, bend, unfold or
flat pattern can be wrapped. A workbench cannot change that: an interface
does not grow methods. -
part.SheetMetalParameters answers "方法 SheetMetalParameters 失败" until that
call has run - the object is CREATED, not found. -
THE VALUES ARE ALSO IN part.Parameters, as ordinary parameters, and
Parameter.Value there IS writable:钣金参数.1\线宽 2.0 -> 1.5 GetThickness() reads 1.5 钣金参数.1\弯曲半径 4.0 -> 1.905 钣金参数.1\K 因子 0.400515 refuses Value while DIN 一般公式\Activity is True; takes 0.5 once it is off 钣金参数.1\拐角止裂槽半径长度 1.0 -> 0.8 钣金参数.1\拐角止裂槽轴长度 2.0 -> 1.5 钣金参数.1\Minimum Bend Radius 0.0 -> 1.0
SO THE REPORT'S ITEMS 1 AND 2 ARE DELIVERABLES, NOT LIMITS
catia_sheetmetal_parameters reads thickness / bend radius / K factor /
corner relief AND sets any of them.
create=true adds the set to a part that has
none (once per document - a second call fails,
measured). Every value is quoted read back, the
thickness is cross-checked against
GetThickness(), and when a value was locked by a
rule the reply names the rule it switched off.
catia_new_sheetmetal catia_new_part plus the set - the report's
new_sheetmetal. It hands back a document that
carries the design intent; the geometry still
has to be extrude + cut, and that substitute's
price stays on the record: 76 faces against the
source's 57, +0.49 % volume, measured 2026-11-03.
The leaves are localised ('线宽' on this build, 'Minimum Bend Radius' English on
the same document), so the lookup is a NAME match over known spellings, and a
property it cannot find is reported missing with its siblings listed - never
silently skipped, because the next thing that happens to a wrong parameter is
somebody cutting metal to it.
A DEFECT THIS RELEASE FOUND IN ITSELF: the first version computed the
GetThickness() cross-check BEFORE the write loop, so a call that set the
thickness to 1.5 reported "get_thickness_mm": 2.0 immediately beside
"after": 1.5. The acceptance case caught it on its fifth cell; both sides are
now read at the same moment, and a test holds it.
VERIFIED ON B30: scripts/b30_v0435_audit.py 10/10 (refusal with the way out,
create + CATIA's defaults, a second create is a no-op, thickness and bend radius
written and read back, the cross-check at the NEW value, the K factor with the
DIN rule named, relief written, catia_new_sheetmetal at 1.2 mm, a negative
thickness refused as BAD_ARGUMENT, and SheetMetalFactory still exposing no
AddNew*).
2136 tests. 153 tools. 433 files, 2264933 bytes.
0.3.132 - 0 ms is a reading, not an absence
CI on 0.3.131 went red on PYTHON 3.10 ONLY. The failing test was not one of the
new ones, and the code it failed on was not touched by that release:
FAILED test_a_repeat_warm_call_quotes_this_sessions_difference
AssertionError: assert 'for the previous warm call on this document' in
"... There is no earlier warm call on this document in this session yet ..."
The bookkeeping in regime.py decided "was there a previous warm call?" with
if previous: - and previous is a measured MILLISECOND COUNT. A warm call that
finishes faster than the clock can resolve stores 0.0, which is falsy. So the
reply said "no earlier warm call on this document" about a call that had just
happened, and quoted neither a delta nor a percentage.
The 3.12 runner's second call happened to take longer than the timer's
resolution, which is the whole reason this survived: the branch is right whenever
the machine is slow enough to measure it.
None now means "no previous call", and 0 ms gets its own sentence - it names the
previous call with the same scope (because it happened) and quotes no percentage,
because a ratio against zero is a division by zero dressed as a measurement.
Two tests hold both branches, one of them driving describe() with elapsed_ms=0.0
so neither can depend on the machine again.
Nothing else in this release is different from 0.3.131 - whose zip on the
releases page carries this defect.
2120 tests. 151 tools. 429 files, 2239744 bytes.
0.3.131 - the edges can be read, and the picture can be closer
The 2026-11-13 report (13th round) was already answered by 0.3.123 - its two P0s,
its P1-1 and P2-1/P2-2 are in that entry. Two items were left; both are answered
now with a measurement instead of a note.
- P1-5 - A TOPOLOGICAL EDGE CAN BE SAMPLED. The note that said otherwise had
measured the wrong object.
The ask was list_edges(coords=true, samples=N), because one point per edge cannot
rebuild a curve: the reporter had to recompute every contour from side files.
What stood in the way was a note in this project saying a topological edge cannot
carry a point-on-curve parameter. The note was wrong - it had measured the wrapper:
- Edge.GetCurve(), GetVertices() and GetFaces() do NOT exist on the object the
selection hands back (".GetCurve", measured on B30 2026-11-15). That
is where the note came from. It is a real dead end, but not the only route. - AddNewPointOnCurveFromPercent(edge, ratio, 0) + UpdateObject WORKS: 12/12
edges of a padded plate, 18/18 once four corners were filleted (so ARCS
count), and five points at 0/25/50/75/100 % of a 40 mm edge landed at
(0,40,10) (0,30,10) (0,20,10) (0,10,10) (0,0,10) - exactly 10.000 mm apart. - The real limit is a CLOSED rim: the two rims of a padded circle refuse 0/2
("方法 UpdateObject 失败"), because a point-on-curve parameter needs a curve
with two ends - the same matrix the engrave code measured.
So samples=N places the points in a scratch set, reads them all in ONE batch, and
deletes the set. An edge that refuses carries samples_unavailable with CATIA's own
words, because an invented point would be a profile the part never had. Capped at
50 per edge and 400 per call, both refused with the numbers in the message.
- P2-3 - THERE IS NO CROP HERE, AND THERE IS NOW A ZOOM.
crop= was asked for so a report figure would not need an external image tool. It
is not implementable here without lying about it: CATIA exposes no capture region,
and cutting the written JPEG needs an image decoder, which this server does not
ship (its dependencies are mcp, pywin32 and jsonschema).
What exists instead is catia_screenshot(zoom=), which multiplies the camera's own
zoom - measured writable and exact on B30: the object covered 24668 / 35989 /
41588 pixels at x0.6 / x1 / x1.6 of the same window. The zoom is restored after
the capture, so the next screenshot is of the window the caller was looking at.
A BUG THIS FOUND IN ITSELF: the first version captured byte-identical images at
x1 and x1.6. viewer.Update() was being called while the camera was still where it
was, and CaptureToFile does not always force a redraw - the repaint now happens
after the zoom is set. Without the acceptance case's byte comparison, this would
have shipped as "the zoom parameter exists and does nothing".
VERIFIED ON B30: scripts/b30_v0433_audit.py 7/7 (five points 10.000 mm apart,
18/18 with arcs, closed rim marked not invented, no scratch set or volume left
behind, the 400-point cap, two captures that differ, camera restored), plus the
probe that settled the sampling rule, scripts/b30_v0431_probe_edge_samples.py.
2118 tests. 151 tools. 428 files, 2238005 bytes.
0.3.130 - the drawing path, end to end
The 2026-11-14 (v0.3.128) report, eleven findings. Its headline was the one
that cost the model: the import said "closed", catia_pad then refused the same
sketch with six free endpoints, and the build stopped there.
- CLOSURE WAS MEASURED AT THE ONE SEAM THAT COULD NOT FAIL
"closed" meant "does the first piece's start meet the last piece's end" - true by
construction after stitching, while a joint in the middle of the same contour was
3.97, 5.36 and 5.68 um apart. One measurement taken where it cannot fail, and the
failure landed three steps later looking like CATIA's opinion.
Every joint is checked now, the stitcher snaps the endpoints it joins, and the
reply carries free_ends per contour as well as in the counts - the same number
catia_pad acts on. On B30 the report's own region pads to 1395.06 mm3, exactly
the contour's area x 4.8 mm, and catia_pad accepts it.
- A PLAN'S ROLLBACK DID NOT SEE A DOCUMENT IT HAD CREATED
"What is new" was a set of NAMES. catia_new_part's first sketch is also called
草图.1, so in a part that already held one the diff answered "nothing was
created" while 草图.1 stood in the tree - the first report's complaint about a
9-step plan, back through a different door.
The snapshot carries the document identity now, and a plan that switched
document reports everything as created. Measured: created_features =
['凸台.1', '草图.1'], rollback leaves the tree empty.
- PAGES WERE NUMBERED BY OBJECT ID
source said "page 3" and "page 12" on a two-page file, there was no way to select
one, and both pages landed in one coordinate system: the imported sheet measured
372 x 271 mm against an A4 page of 297 x 210.
There is a page argument now (1..N in write order), a pages table with each
page's size, and page_bounds_mm beside bounds_mm. On the report's own file: each
page 297.011 x 210.016 mm, page 1 = 1417 contours, page 2 = 878.
- CLIPPING PATHS WERE COUNTED, NOT APPLIED
86 of 86 rectangular clips on page 1 are applied now, 989 painted paths that fall
outside the visible area are dropped, and the reply says how many. Building this,
the acceptance case found a bug in the new code itself: Liang-Barsky's p and q
were swapped, so a segment STARTING exactly on the clip boundary was read as
"parallel and outside" and thrown away - a square with an edge on the page box
lost both boundary edges. Fixed, with the boundary case as a test.
- THE ASCII FALLBACK MAPPED EVERY CHINESE NAME ONTO ONE FILE
Keeping only what was already ASCII turned 苹果AirPods参数化版 and 苹果AirPods一二代
into the same AirPods.CATPart, so the second part could not be created at all.
There is an explicit ascii_name now, and a collision is resolved only when the
path is held by an OPEN document whose part number proves it is a different part.
The suffix comes from the original name, not a counter, so a retry lands on the
same file. On B30 both parts were created - AirPods.CATPart and
AirPods_d74a53.CATPart - and a retry of the same call still answers "already
open" instead of inventing a third file.
- "ARCS STAY ARCS" WAS TRUE OF DXF AND SVG ONLY
A PDF stores Beziers, and a 15 x 24 mm profile arrived as 110 chords while the
tool description promised it would not be faceted.
Cubics that ARE a circle - the four every drawing program uses - are now fitted
back to a real arc; the rest are sampled to curve_tolerance, and the reply says
how many of each. Page 1: 632 arcs fitted, 69 genuine free-form curves sampled.
Proved by a volume: a 40 x 20 plate with a D10 hole made of four Beziers pads to
7214.61 mm3 against the analytic 7214.60 - a 16-sided hole would have been
7234.63.
- min_size HAD TO BE FOUND BY BISECTION
4196 contours, and the report's only way through was "试到 min_size=10 才降到 14".
catia_read_drawing returns a size histogram now, so the cliff is read instead of
guessed: page 1 has 191 contours under 10 mm across and 195 between 10 and 20.
- region AND bbox_mm WERE TWO FRAMES
region compares against the DRAWING's coordinates while bbox_mm reports the
shifted result, so "read the bbox, set region from it" - the workflow the tool
itself recommends - selected the wrong part of the sheet.
Both frames are in the reply now, and drawing_bounds_mm is the one region uses.
- A DRAWING FRAME'S EDGE LINE IS CLOSED, 346 mm LONG AND 0.17 mm THICK
closed_only and min_size both pass it. max_aspect drops it: on page 1, 8 closed
contours became 7, 37 thin ones removed, nothing wider than 16.8:1 kept. Each
contour reports its own aspect too.
- THE TWO SEMI-AXES WERE BOTH CALLED 半径
A formula bound to the wrong one moves the short axis where the long one was
meant, and nothing in the name says which is which.
The constraints are renamed at creation to MajorRadius.N / MinorRadius.N -
measured: V5 does accept a written Constraint.Name, and one ellipse came back as
MajorRadius.1 and MinorRadius.2. The rename is read back and only the read-back
name is quoted, and catia_sketch_get_geometry carries drives on every constraint,
so the answer survives a build that refuses the rename.
- catia_list_features READ A DIFFERENT DOCUMENT FROM EVERY OTHER TOOL
The remembered body was checked by name, and a body from a document that is
still open keeps its name - so "it is still alive" was true for a body belonging
to another part. It now resolves the body from the active document's Bodies
collection, checks COM identity before name, and puts the document it actually
read into the reply.
VERIFIED ON B30: scripts/b30_v0432_audit.py 13/13 (page sizes against a PDF
reader's own numbers, clip counts, arcs, the volume above, both Chinese names,
the renamed constraints), probe_closure 6/6, probe_plan 6/6.
2105 tests. 151 tools. 425 files, 2223479 bytes.
0.3.129 - two promises about not connecting
Found from CI, not from a report: of 141 workflow runs, every single one
had failed. The last two failures were a validate-stage assertion and an
internal-error assertion reading connect/COM_ERROR - because CI has no CATIA.
The tests were the symptom. The cause:
- THE SERVER OPENED A SESSION FOR EVERY TOOL
_execute_tool connected before dispatching everything except connect and
disconnect. So catia_server_info - added in 0.3.127 and documented as "never
connects, it answers connected=false" - started CATIA to read a version, and
catia_read_drawing (0.3.128, "parses a file, nothing else") could not read a
file without a session either.
Both tools now declare NO_SESSION and the server honours it. The declaration is
opt-in, so a tool that needs a session still gets one - asserted in both
directions, because "no session needed" is exactly the kind of flag that spreads
until nothing connects.
- catia_drawing_add_frame CONNECTED BEFORE CHECKING ITS ARGUMENTS
Its comment has said "everything that can be refused BEFORE the sheet is touched"
since 0.3.68 - but the four lines above that comment connected and grabbed the
active drawing. So margin=-5 and title_block.width=0 needed a live session to be
told they were typos, and on CI came back as a connect-stage COM error, which
sends the caller to check their session instead of the number they passed.
The connect now sits where the sheet is touched, below the argument checks.
- TWO TESTS DEPENDED ON THIS MACHINE
Both assert the SHAPE of a failure envelope, not anything about CATIA, but both
built a real server - so they passed only where CATIA happened to be running.
One now refuses on an argument alone (a zero-width title block instead of an
oversized one: judging oversize needs the sheet size, so it cannot be an
argument-only refusal); the other stubs the connect. Three new tests hold the
line: a session-free tool must not open a session, a tool that needs one must
still get it, and add_frame must refuse before it connects.
VERIFIED THE WAY CI SEES IT: those files run with ensure_connected replaced by a
COM error, which is the CI condition - 57 pass. Then the real run: workflow
43591584778, test (3.10) success and test (3.12) success, the first green run in
the repository's history.
2076 tests. 151 tools. 421 files, 2191592 bytes.
0.3.128 - a drawing goes in
The 2026-11-14 report's P0-1, the last P0 item it opened: 147 tools and not
one of them could read a 2D drawing. The workaround it forced - pip install a PDF
renderer OUTSIDE the MCP, render to PNG, crop at high zoom, read the numbers with
a vision model, type them into 21 plan steps - is not a feature. It cannot be
repeated, and nothing links the model back to the drawing.
The fix is not OCR. It is reading the geometry the drawing already contains.
- TWO TOOLS
- catia_read_drawing - what is in this file: format, the file's own units and
the resulting mm per unit, contour and element counts, the layer table, how
small the smallest contour is (text and arrowheads are tiny), the bounds in
mm, and how many text runs were drawn as TEXT and are therefore unreadable.
IT NEVER CONNECTS TO CATIA, so it is safe to run while something else holds
the session, and it is the call to make BEFORE deciding on filters. - catia_import_drawing - the same parse, drawn into a new sketch at 1:1, one
call, closed at the end, with what CATIA then sees (element count, outline
length) in the reply beside the bounds in mm.
DXF (ASCII), SVG and VECTOR PDF, parsed with the standard library only: no
ezdxf, no svgpathtools, no PDF library - none of them is installed and none
was added.
- PIECES KEEP THEIR OWN KIND
Lines stay lines, arcs stay arcs - DXF BULGES included, which are arc segments
inside a polyline - and circles stay circles. A pad built on the import is not
faceted, which is the difference between a round hole and a 24-sided one.
Measured on B30 2026-11-14, one fresh document per case:
SVG 40 x 20 plate + D6 hole, pad 10 7717.26 mm3 vs 7717.26 analytic
DXF square as four separate LINEs 3600.00 vs 3600.00
DXF stadium, bulge=1 caps, pad 5 5570.80 vs 5570.80 (chords: 4000)
DXF 1 x 0.5 INCH ($INSUNITS=1), pad 2 645.16 vs 645.16
DXF 1 x 0.5 mm at 1:100 (scale=100) 10000.00 vs 10000.00
- STITCHING, BECAUSE A CAD EXPORT HAS ONE ENTITY PER EDGE
The workspace's A3 sample parses to 672 contours of which 83 are closed - and all
83 of those are 3.4-4.6 mm arrowheads. So closed_only used to remove the entire
model and a pad on the raw import was impossible. Edges that share an endpoint are
now joined into loops.
WHERE MORE THAN ONE UNUSED EDGE MEETS A JOINT, THE CHAIN STOPS and the count is
reported. A guess there is a profile the drawing never had, it pads to a
plausible-looking solid, and nothing in the reply would say so.
- IT REFUSES RATHER THAN TRUNCATES
Over 2000 sketch elements the import stops, with the element count, the contour
count and the smallest contour - enough to choose min_size / layers / region from
data instead of guessing. On the 6489-element vectorised sample the refusal IS
the right answer, and the audit checks the document still holds no sketch
afterwards.
CEILINGS, all measured and now in catia_diagnose: DWG is refused (binary
container; CATIA needs a translator too - re-save as DXF); raster images cannot
be read at all, a scan has no geometry; text drawn as text is not converted,
because a glyph's outline lives in the font program - the reply counts the runs
it skipped, so "the dimensions are missing" is stated rather than discovered.
Two ordering rules from earlier rounds, applied here before they could bite:
argument validation runs BEFORE the file is touched, and parse_* return the
file's own units with mm_per_unit beside them while read_drawing applies the
factor - so a test can check either half on its own.
2073 tests. 151 tools. 12/12 B30 checks. 421 files, 2188800 bytes.