diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4964453..d55d067 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -25,11 +25,24 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install DXF acceptance dependency + run: python -m pip install -r v3/tools/requirements-dxf.txt - name: Validate application source and engine run: node v3/tools/check-fs.js --no-browser + - name: Validate browser workflow and DXF round trip + env: + FS_HEADLESS: '1' + run: node v3/tools/check-fs.js + - name: Validate deployment configuration run: | node -e "JSON.parse(require('fs').readFileSync('vercel.json', 'utf8')); console.log('vercel.json: valid')" diff --git a/README.md b/README.md index 89c93d3..9f36568 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ FutolStructure is a browser-based structural engineering workbench for reinforce Security
-[](https://futolstructure.vercel.app) +[](https://futolstructure.vercel.app) [](https://github.com/michaelfutol/futolstructure/actions/workflows/validate.yml)   @@ -64,7 +64,7 @@ FutolStructure uses a shared active-model payload so geometry counts and member | Target | Current status | | --- | --- | -| DXF | Every floor exports layout and tributary plans, plus foundation/base reactions, synchronized schedules, load summary, and preliminary BOQ tables on governed structural layers. | +| DXF | AutoCAD R12 ASCII (`AC1009`) exports every floor layout and tributary plan, foundation/base reactions, synchronized schedules, load summary, and preliminary BOQ tables on governed structural layers. Output is parser-audited and round-trip checked with `ezdxf`; the recovered Olango package also opens in AutoCAD 2025 Core Engine without recovery. | | IFC2x3 | Active columns, beams, slabs, and storey organization are exported for BIM review. | | STAAD.Pro | The gravity baseline, frame/plate geometry, beam insertion offsets, and statics balance were validated in STAAD.Pro 2024. | | ETABS 22 | The OAPI builder creates a dated working copy, assigns the governed mass baseline, runs modal analysis, and exports audit artifacts. | @@ -95,10 +95,17 @@ node v3/tools/check-fs.js --no-browser Run the full browser smoke check with Chrome or Edge installed: ```bash +python -m pip install -r v3/tools/requirements-dxf.txt node v3/tools/check-fs.js ``` -The browser smoke covers initialization, plan geometry, slab ownership, cantilever behavior, persistence and recovery guards, member locking, measurement tools, stair persistence, 3D rendering, coordinated DXF package completeness, and export payload parity. +The browser smoke covers initialization, plan geometry, slab ownership, cantilever behavior, persistence and recovery guards, member locking, measurement tools, stair persistence, 3D rendering, coordinated DXF completeness, strict DXF open/audit/save/reopen validation, and export payload parity. + +Where AutoCAD 2025 is installed, run the native read gate against a generated package without modifying it: + +```powershell +powershell -File v3/tools/check-dxf-autocad.ps1 -DxfPath "path/to/package.dxf" +``` ## Repository Layout diff --git a/docs/FS-117_RELEASE_NOTES.md b/docs/FS-117_RELEASE_NOTES.md new file mode 100644 index 0000000..3f24330 --- /dev/null +++ b/docs/FS-117_RELEASE_NOTES.md @@ -0,0 +1,31 @@ +# FS-117 DXF R12 Hotfix + +## Scope + +FS-117 repairs the coordinated DXF package envelope without changing structural model geometry, analysis behavior, schedules, or BOQ generation. + +## DXF Contract + +- AutoCAD R12 ASCII (`AC1009`). +- Windows-safe CRLF line endings and a valid EOF terminator. +- Mandatory layer `0`. +- Standard `BYBLOCK`, `BYLAYER`, and `CONTINUOUS` linetypes. +- Corrected `CENTER2` pattern length and `HIDDEN2` support. +- `txt.shx` standard text style. +- No R2000-only layer lineweight group `370`. +- No unsupported R12 linetype alignment group `74`. + +## Acceptance + +Release acceptance requires: + +1. Source and full browser regression. +2. Current three-floor Olango project regression. +3. Strict `ezdxf` open, audit, save, and reopen with zero errors or fixes. +4. Exact layer and entity retention after round trip. +5. AutoCAD 2025 Core Engine open/regenerate without recovery. +6. Visual inspection of the complete coordinated drawing package. + +## Exclusions + +Protected Project Revisions, historical helper scripts, broad UI changes, solver changes, and unrelated modeling changes are not part of FS-117. diff --git a/docs/RELEASE_HYGIENE.md b/docs/RELEASE_HYGIENE.md index 7cdddf4..ae00444 100644 --- a/docs/RELEASE_HYGIENE.md +++ b/docs/RELEASE_HYGIENE.md @@ -20,7 +20,11 @@ Use this checklist before pushing a public FutolStructure update. - `v3/assets/futolstructure-icon.png` - `v3/assets/screenshots/` - `v3/engine/` +- `v3/dxf-export.js` - `v3/tools/check-fs.js` +- `v3/tools/check-dxf-autocad.ps1` +- `v3/tools/validate-dxf.py` +- `v3/tools/requirements-dxf.txt` - `docs/` - deployment config such as `vercel.json` @@ -35,9 +39,16 @@ git diff --check For browser-sensitive changes, also run: ```bash +python -m pip install -r v3/tools/requirements-dxf.txt node v3/tools/check-fs.js ``` +For a DXF release on a workstation with AutoCAD 2025: + +```powershell +powershell -File v3/tools/check-dxf-autocad.ps1 -DxfPath "path/to/package.dxf" +``` + ## If The Working Tree Is Dirty This repo often has historical or recovery edits. Do not stage everything blindly. diff --git a/v3/dxf-export.js b/v3/dxf-export.js index f34250e..502449d 100644 --- a/v3/dxf-export.js +++ b/v3/dxf-export.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - const DXF_PACKAGE_BUILD = 'FS-116'; + const DXF_PACKAGE_BUILD = 'FS-117'; const DXF_TEXT_LAYER = 'S-TEXT'; const GRID_LETTERS = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; @@ -122,16 +122,11 @@ } function generatePackageLinetypeTable() { - let dxf = '0\nTABLE\n2\nLTYPE\n70\n3\n'; - dxf += '0\nLTYPE\n2\nCONTINUOUS\n70\n0\n3\nSolid line\n72\n65\n73\n0\n40\n0.0\n'; - dxf += '0\nLTYPE\n2\nCENTER2\n70\n0\n3\nCenter ____ _ ____ _ ____\n72\n65\n73\n4\n40\n1.25\n49\n0.75\n74\n0\n49\n-0.125\n74\n0\n49\n0.125\n74\n0\n49\n-0.125\n74\n0\n'; - dxf += '0\nLTYPE\n2\nHIDDEN2\n70\n0\n3\nHidden __ __ __ __\n72\n65\n73\n2\n40\n0.5\n49\n0.25\n74\n0\n49\n-0.25\n74\n0\n'; - dxf += '0\nENDTAB\n'; - return dxf; + return generateDXFLinetypeTable(); } function generatePackageTextStyleTable() { - return '0\nTABLE\n2\nSTYLE\n70\n1\n0\nSTYLE\n2\nSTANDARD\n70\n0\n40\n0\n41\n1\n50\n0\n71\n0\n42\n0.2\n3\ntxt\n4\n\n0\nENDTAB\n'; + return generateDXFTextStyleTable(); } function getGridCoordinates() { @@ -730,6 +725,8 @@ const modelRows = [ { item: 'Build', value: DXF_PACKAGE_BUILD }, + { item: 'FSTR schema', value: window.getProjectProvenance?.().fstrSchemaVersion || 'unknown' }, + { item: 'Source revision', value: window.getProjectProvenance?.().sourceRevisionId || 'unsaved working state' }, { item: 'Floors', value: state.floors?.length || 0 }, { item: 'Grid', value: `${state.xSpans?.length || 0}x${state.ySpans?.length || 0}` }, { item: 'Concrete', value: `fc'=${finite(state.fc, 21)} MPa` }, @@ -830,9 +827,7 @@ ? writer.bounds : { minX: 0, minY: 0, maxX: 100, maxY: 100 }; let dxf = '0\nSECTION\n2\nHEADER\n'; - dxf += '9\n$ACADVER\n1\nAC1015\n'; - dxf += '9\n$INSUNITS\n70\n6\n'; - dxf += '9\n$MEASUREMENT\n70\n1\n'; + dxf += '9\n$ACADVER\n1\nAC1009\n'; dxf += `9\n$EXTMIN\n10\n${fixed(bounds.minX)}\n20\n${fixed(bounds.minY)}\n30\n0\n`; dxf += `9\n$EXTMAX\n10\n${fixed(bounds.maxX)}\n20\n${fixed(bounds.maxY)}\n30\n0\n`; dxf += '0\nENDSEC\n'; @@ -844,7 +839,7 @@ dxf += '0\nSECTION\n2\nENTITIES\n'; dxf += writer.content(); dxf += '0\nENDSEC\n0\nEOF\n'; - return dxf; + return dxf.replace(/\r?\n/g, '\r\n'); } function generateCoordinatedDXFContent() { @@ -893,7 +888,9 @@ audit.entityCounts = { ...writer.entityCounts }; audit.layerUsage = { ...writer.layerUsage }; audit.bytes = new TextEncoder().encode(dxf).length; - audit.validTerminator = dxf.endsWith('0\nEOF\n'); + audit.dxfVersion = 'AC1009'; + audit.lineEnding = 'CRLF'; + audit.validTerminator = dxf.endsWith('0\r\nEOF\r\n'); audit.requiredLayerEntities = [DXF_LAYER.GRID, DXF_LAYER.TEXT, DXF_LAYER.COLUMN, DXF_LAYER.BEAM, DXF_LAYER.SLAB] .every(layer => (writer.layerUsage[layer] || 0) > 0); window.lastDXFExportAudit = audit; diff --git a/v3/index.html b/v3/index.html index 9475571..8e16f5a 100644 --- a/v3/index.html +++ b/v3/index.html @@ -1821,7 +1821,7 @@