Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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')"
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ FutolStructure is a browser-based structural engineering workbench for reinforce
<a href="SECURITY.md">Security</a>
</p>

[![Build](https://img.shields.io/badge/build-v3.16.116-2563eb)](https://futolstructure.vercel.app)
[![Build](https://img.shields.io/badge/build-v3.16.117-2563eb)](https://futolstructure.vercel.app)
[![Validation](https://github.com/michaelfutol/futolstructure/actions/workflows/validate.yml/badge.svg)](https://github.com/michaelfutol/futolstructure/actions/workflows/validate.yml)
![Platform](https://img.shields.io/badge/platform-browser-0f766e)
![Maturity](https://img.shields.io/badge/maturity-technical%20preview-f59e0b)
Expand Down Expand Up @@ -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. |
Expand Down Expand Up @@ -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

Expand Down
31 changes: 31 additions & 0 deletions docs/FS-117_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions docs/RELEASE_HYGIENE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -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.
Expand Down
23 changes: 10 additions & 13 deletions v3/dxf-export.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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` },
Expand Down Expand Up @@ -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';
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down
86 changes: 78 additions & 8 deletions v3/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1821,7 +1821,7 @@
</div>
<img class="logo-icon" src="assets/futolstructure-icon.png" alt="FutolStructure">
<span style="font-weight: 700; font-size: 0.88rem; color: var(--text-primary); letter-spacing: -0.01em;">FutolStructure</span>
<span title="Build FS-116" style="font-size: 0.58rem; color: var(--text-muted); font-weight: 500; padding: 1px 5px; background: var(--surface); border: 1px solid var(--border); border-radius: 3px;">v3.16.116</span>
<button id="buildVersionBadge" type="button" title="View FS-117 release provenance" onclick="showReleaseInfo()" style="font-size:0.58rem; color:var(--text-muted); font-weight:500; padding:1px 5px; background:var(--surface); border:1px solid var(--border); border-radius:3px; cursor:pointer;">v3.16.117</button>
</div>
<!-- Action Toolbar -->
<div class="header-actions">
Expand Down Expand Up @@ -3915,6 +3915,67 @@ <h3 style="color:#8b5cf6; margin:0 0 16px 0; display:flex; align-items:center; g

const FSTR_FILE_TYPE = 'FutolStructure.StructuralModel';
const FSTR_SCHEMA_VERSION = '0.1.0';
const FSTR_APP_VERSION = '3.16.117';
const FSTR_BUILD_ID = 'FS-117';
const FSTR_RELEASE_NAME = 'DXF R12 Hotfix';
const FSTR_RELEASE_MANIFEST_FALLBACK = Object.freeze({
appVersion: FSTR_APP_VERSION,
buildId: FSTR_BUILD_ID,
releaseName: FSTR_RELEASE_NAME,
fstrSchemaVersion: FSTR_SCHEMA_VERSION,
gitCommit: '58edb71ce88b0060a8a2d9901664e91ac61dfcc2',
buildDate: '2026-07-14',
validatedTargets: {
DXF: 'AutoCAD R12 ASCII parser-audited and AutoCAD 2025 core-opened'
}
});
let fsReleaseManifest = { ...FSTR_RELEASE_MANIFEST_FALLBACK };

function getReleaseManifest() {
return JSON.parse(JSON.stringify(fsReleaseManifest || FSTR_RELEASE_MANIFEST_FALLBACK));
}

function getProjectProvenance() {
return {
releaseManifest: getReleaseManifest(),
projectId: '',
sourceRevisionId: '',
fstrSchemaVersion: FSTR_SCHEMA_VERSION
};
}

async function loadReleaseManifest() {
try {
const response = await fetch('release-manifest.json', { cache: 'no-store' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const manifest = await response.json();
if (manifest?.appVersion && manifest?.buildId && manifest?.fstrSchemaVersion) {
fsReleaseManifest = { ...FSTR_RELEASE_MANIFEST_FALLBACK, ...manifest };
}
} catch (err) {
console.warn('Release manifest fallback active:', err.message);
}
const badge = document.getElementById('buildVersionBadge');
if (badge) {
badge.textContent = `v${fsReleaseManifest.appVersion}`;
badge.title = `${fsReleaseManifest.buildId} | ${fsReleaseManifest.releaseName || FSTR_RELEASE_NAME}`;
}
return getReleaseManifest();
}

function showReleaseInfo() {
const manifest = getReleaseManifest();
alert([
`FutolStructure v${manifest.appVersion}`,
`${manifest.buildId} - ${manifest.releaseName || FSTR_RELEASE_NAME}`,
`Schema: ${manifest.fstrSchemaVersion}`,
`Release commit: ${manifest.gitCommit || 'local/uncommitted candidate'}`,
`Build date: ${manifest.buildDate || 'unknown'}`
].join('\n'));
}

window.getReleaseManifest = getReleaseManifest;
window.getProjectProvenance = getProjectProvenance;
const FSTR_HIDDEN_GEOMETRY_POLICY = 'preserve-intentional-hidden-geometry';
const FSTR_AUTOSAVE_STATE_REVISION = 'FS-107-floor-aware-autosave';

Expand Down Expand Up @@ -6523,22 +6584,30 @@ <h3 style="color:#8b5cf6; margin:0 0 16px 0; display:flex; align-items:center; g
}

function generateDXFLinetypeTable() {
let dxf = '0\nTABLE\n2\nLTYPE\n70\n2\n';
let dxf = '0\nTABLE\n2\nLTYPE\n70\n5\n';
dxf += '0\nLTYPE\n2\nBYBLOCK\n70\n0\n3\n\n72\n65\n73\n0\n40\n0.0\n';
dxf += '0\nLTYPE\n2\nBYLAYER\n70\n0\n3\n\n72\n65\n73\n0\n40\n0.0\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\nCENTER2\n70\n0\n3\nCenter ____ _ ____ _ ____\n72\n65\n73\n4\n40\n1.125\n49\n0.75\n49\n-0.125\n49\n0.125\n49\n-0.125\n';
dxf += '0\nLTYPE\n2\nHIDDEN2\n70\n0\n3\nHidden __ __ __ __\n72\n65\n73\n2\n40\n0.5\n49\n0.25\n49\n-0.25\n';
dxf += '0\nENDTAB\n';
return dxf;
}

function generateDXFLayerTable() {
let dxf = `0\nTABLE\n2\nLAYER\n70\n${DXF_STRUCTURAL_LAYERS.length}\n`;
let dxf = `0\nTABLE\n2\nLAYER\n70\n${DXF_STRUCTURAL_LAYERS.length + 1}\n`;
dxf += '0\nLAYER\n2\n0\n70\n0\n62\n7\n6\nCONTINUOUS\n';
DXF_STRUCTURAL_LAYERS.forEach(layer => {
dxf += `0\nLAYER\n2\n${layer.name}\n70\n0\n62\n${layer.colorIndex}\n6\n${layer.linetype}\n370\n${getDxfLineweightCode(layer.lineweightMm)}\n`;
dxf += `0\nLAYER\n2\n${layer.name}\n70\n0\n62\n${layer.colorIndex}\n6\n${layer.linetype}\n`;
});
dxf += '0\nENDTAB\n';
return dxf;
}

function generateDXFTextStyleTable() {
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.shx\n4\n\n0\nENDTAB\n';
}

// Update member dimensions from the table and sync with left panel
function updateMemberDimensions() {
// Read values from dimension table
Expand Down Expand Up @@ -19566,6 +19635,7 @@ <h3 style="color:#8b5cf6; margin:0 0 16px 0; display:flex; align-items:center; g
const sb = document.getElementById('statusText');
if (sb) { sb.textContent = 'Init error — check console'; sb.style.color = 'var(--danger)'; }
}
void loadReleaseManifest();
};

// ========== v3.0: DXF EXPORT (AutoCAD) ==========
Expand All @@ -19578,14 +19648,14 @@ <h3 style="color:#8b5cf6; margin:0 0 16px 0; display:flex; align-items:center; g

// ========== HEADER SECTION ==========
dxf += '0\nSECTION\n2\nHEADER\n';
dxf += '9\n$ACADVER\n1\nAC1015\n'; // AutoCAD 2000 format
dxf += '9\n$INSUNITS\n70\n6\n'; // Units: meters
dxf += '9\n$ACADVER\n1\nAC1009\n'; // AutoCAD R12 ASCII
dxf += '0\nENDSEC\n';

// ========== TABLES SECTION (Linetypes + Layers) ==========
dxf += '0\nSECTION\n2\nTABLES\n';
dxf += generateDXFLinetypeTable();
dxf += generateDXFLayerTable();
dxf += generateDXFTextStyleTable();
dxf += '0\nENDSEC\n';

// ========== ENTITIES SECTION ==========
Expand Down Expand Up @@ -19695,7 +19765,7 @@ <h3 style="color:#8b5cf6; margin:0 0 16px 0; display:flex; align-items:center; g
});

dxf += '0\nENDSEC\n0\nEOF\n';
return dxf;
return dxf.replace(/\r?\n/g, '\r\n');
}

function exportToDXF() {
Expand Down
18 changes: 18 additions & 0 deletions v3/release-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"appVersion": "3.16.117",
"buildId": "FS-117",
"releaseName": "DXF R12 Hotfix",
"fstrSchemaVersion": "0.1.0",
"gitCommit": "58edb71ce88b0060a8a2d9901664e91ac61dfcc2",
"buildDate": "2026-07-14",
"validatedFixtures": [
"Olango_2026-07-14_3Floors_GF-2F-RF"
],
"validatedTargets": {
"DXF": "AutoCAD R12 ASCII parser-audited and AutoCAD 2025 core-opened",
"STAAD": "STAAD.Pro 2024 gravity baseline validated",
"ETABS": "ETABS 22.6 OAPI gravity/modal baseline validated",
"IFC": "IFC2x3 IfcOpenShell schema and geometry validated",
"SAFE": "ETABS Story as SAFE V12 handoff only"
}
}
38 changes: 38 additions & 0 deletions v3/tools/check-dxf-autocad.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
param(
[Parameter(Mandatory = $true)]
[string]$DxfPath,

[string]$AutoCadCorePath = 'C:\Program Files\Autodesk\AutoCAD 2025\accoreconsole.exe'
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

$resolvedDxf = (Resolve-Path -LiteralPath $DxfPath).Path
$resolvedCore = (Resolve-Path -LiteralPath $AutoCadCorePath).Path
$scriptPath = Join-Path ([IO.Path]::GetTempPath()) ("futolstructure-autocad-open-{0}.scr" -f [Guid]::NewGuid().ToString('N'))

try {
[IO.File]::WriteAllText($scriptPath, "_.QUIT`r`n", [Text.Encoding]::ASCII)
$output = & $resolvedCore /i $resolvedDxf /s $scriptPath /l en-US 2>&1
$exitCode = $LASTEXITCODE
$text = (($output -join [Environment]::NewLine) -replace "`0", '')
$rejectionPattern = 'Invalid or incomplete DXF|Unknown group|drawing discarded|ErrorStatus='
$rejected = $text -match $rejectionPattern

if ($exitCode -ne 0 -or $rejected) {
$tail = ($text -split "`r?`n" | Select-Object -Last 30) -join [Environment]::NewLine
throw "AutoCAD Core Engine rejected the DXF (exit $exitCode).`n$tail"
}

[pscustomobject]@{
ok = $true
reader = 'AutoCAD 2025 Core Engine'
path = $resolvedDxf
exitCode = $exitCode
regeneratedModel = $text -match 'Regenerating model'
recoveryMessage = $text -match $rejectionPattern
} | ConvertTo-Json
} finally {
Remove-Item -LiteralPath $scriptPath -Force -ErrorAction SilentlyContinue
}
Loading
Loading