Skip to content

Fix SPATIAL_FILTER transforms: lost on DXF read, transposed between the two formats - #1230

Open
redbluevn wants to merge 1 commit into
DomCR:masterfrom
redbluevn:moredwg/pr78-spatial-filter-transforms
Open

redbluevn wants to merge 1 commit into
DomCR:masterfrom
redbluevn:moredwg/pr78-spatial-filter-transforms

Conversation

@redbluevn

Copy link
Copy Markdown

A SPATIAL_FILTER (an XCLIP boundary) carries two matrices that place the boundary. Four faults between the four ends that read and write them.

1. The DXF reader threw one away

if (tmp.InsertTransformRead)
{
    filter.InsertTransform = new Matrix4(array);
    tmp.InsertTransformRead = true;      // already true here; no effect
}
else
{
    filter.InverseInsertTransform = new Matrix4(array);
    // never sets the flag
}

The flag starts false and is only assigned inside the branch that already requires it to be true. So both twelve-double blocks take the else: InverseInsertTransform is written twice, the second overwriting the first, and InsertTransform is never assigned at all.

2. The two formats disagreed about the layout

Both DWG and DXF record these as three rows of four with the translation in the fourth column. That is the transpose of how Matrix4 holds one: CreateTranslation writes M30/M31/M32, and operator *(Matrix4, XYZ) reads the translation from there.

The DXF path already landed in the library's convention — Matrix4(double[]) maps elements[0..3] to M00, M10, M20, M30, so the file's rows become the matrix's columns. The DWG reader and writer kept the file's layout instead.

The consequence is quiet and total: the same matrix read from a DWG dropped its own translation when applied to a point, while read from a DXF it did not. For a clip boundary, translation is most of what these matrices carry.

The fix

read4x3Matrix and write4x3Matrix transpose, so the DWG path lands where the DXF path already was; and the DXF writer emits each file row from a matrix column, which is exactly what Matrix4(double[]) undoes on the way back in. All four ends now agree, and SpatialFilter.InverseInsertTransform can be used with Matrix4's own operators without the caller knowing any of this.

Why nothing caught it

SingleCaseGenerator.InsertWithSpatialFilter builds its filter with identity matrices — and an identity survives being transposed, dropped, or applied twice. The fixture now carries a scale together with a translation.

That alone is still not enough: the single-object comparison does not reach these properties (I checked — with the fixture strengthened and the writer fix reverted, it still passed). So SpatialFilterRoundTripTests asserts them directly, including the one case a per-format round trip structurally cannot see: that DWG and DXF agree with each other. A format can be perfectly self-consistent and still disagree with the other.

All three new tests fail against the current code.

Evidence

samples/sample_AC1032.dwg holds a clip whose inverse transform is a 25.4 scale with a translation of (-1087.9487, 942.61675, 0). Through this library's own DXF it came back as the identity — so a clipped insert reported the whole block instead of the clipped part.

dotnet test on this branch: 2316 passed / 17 failed, against master at 592d70a on this machine at 2313 / 17 — the same pre-existing failures, plus the three tests added here.

Related

Found while checking a fidelity gate that flagged a lost entity after #1226 made something finally consume these matrices. #1226 transposes at the point of use and explicitly leaves this normalisation as a separate question; this PR is that answer, and #1226 should drop its .Transpose() if both land.

Same family: #1223, #1224, #1225, #1226, #1227, #1228, #1229, DomCR/CSUtilities#23.

…any direction

A SPATIAL_FILTER carries two matrices that place the clip boundary. Four faults
between the four ends that read and write them.

The DXF reader threw one away: the flag telling the inverse from the forward
transform was set inside the branch that already required it to be true, so it
never became true. Both twelve-double blocks went into InverseInsertTransform,
the second overwriting the first, and InsertTransform was never assigned at all.

The formats disagreed about the layout. Both record these as three rows of four
with the translation in the fourth column - the transpose of how Matrix4 holds
one, since CreateTranslation writes M30/M31/M32 and operator* reads the
translation from there. The DXF path already landed in the library's convention
(Matrix4(double[]) maps elements 0..3 to M00, M10, M20, M30); the DWG reader and
writer kept the file's. The same matrix read from a DWG therefore dropped its own
translation when applied to a point, and read from a DXF did not.

The DWG reader and writer now transpose, and the DXF writer emits each file row
from a matrix COLUMN, which is what Matrix4(double[]) undoes on the way in.

Why nothing caught it: the single-object fixture built its filter with IDENTITY
matrices, and an identity survives being transposed, dropped or doubled. It now
carries a scale together with a translation. That alone is still not enough - the
comparison does not reach these properties - so SpatialFilterRoundTripTests
asserts them directly, including the case a per-format round trip structurally
cannot see: that DWG and DXF agree with each other.

Measured: sample_AC1032.dwg holds a clip whose inverse transform is a 25.4 scale
with a translation. Through this library's own DXF it came back as the identity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant