From 4311854f570c2ba0858ebfd6c782216e8a228fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Resul=20=C3=96v=C3=BCr?= <92828257+ovurrsl@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:54:29 +0000 Subject: [PATCH 1/2] feat(nodes): steel i-beam column cross-section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Columns gain an `i-beam` cross-section: two flanges spanning the width at the front and back faces, joined by a web along the depth. Plate thicknesses come from one core helper (`columnIBeamSection`) shared by the renderer, the top-surface region, the plan outline and IFC export. - Capitals, bands and carvings treat the section as a box, so caps are plates; a bare i-beam's top support region is its I outline. - The plan draws a bare i-beam as its true section, in the nodes floorplan and the editor's reference-floor footprint; with plates it draws the plate rectangle. - `add_column` accepts `crossSection: 'i-beam'`. - IFC export writes an IfcIShapeProfileDef extrusion and import reads one back as an i-beam column. - The panel offers I-Beam beside Round / Square / Rectangular (2×2 grid) and shows Depth for it; shaft profile controls are hidden for it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FX8vGAP5g43AHR1jixUeXr --- .../src/agent-operations/add-column.test.ts | 28 ++++++++ packages/core/src/agent-tools/columns.ts | 10 +-- packages/core/src/index.ts | 1 + packages/core/src/lib/column-geometry.test.ts | 32 +++++++++ packages/core/src/lib/column-geometry.ts | 36 ++++++++++ packages/core/src/schema/nodes/column.ts | 3 +- .../src/components/editor/floorplan-panel.tsx | 18 ++++- .../ifc-converter/src/export/ifc-model.ts | 22 ++++++ .../ifc-converter/src/export/scene-to-ifc.ts | 9 ++- packages/ifc-converter/src/index.ts | 15 ++-- .../tests/export-roundtrip.test.ts | 43 +++++++++++- .../ifc-converter/tests/export-step-check.ts | 1 + packages/nodes/src/column/floorplan.ts | 19 +++++- packages/nodes/src/column/i-beam.test.tsx | 68 +++++++++++++++++++ packages/nodes/src/column/panel.tsx | 30 ++++++-- packages/nodes/src/column/renderer.tsx | 43 +++++++++++- packages/nodes/src/column/shape.ts | 12 +++- packages/nodes/src/column/surface.ts | 5 ++ 18 files changed, 373 insertions(+), 22 deletions(-) create mode 100644 packages/core/src/agent-operations/add-column.test.ts create mode 100644 packages/core/src/lib/column-geometry.test.ts create mode 100644 packages/core/src/lib/column-geometry.ts create mode 100644 packages/nodes/src/column/i-beam.test.tsx diff --git a/packages/core/src/agent-operations/add-column.test.ts b/packages/core/src/agent-operations/add-column.test.ts new file mode 100644 index 0000000000..7919926d3a --- /dev/null +++ b/packages/core/src/agent-operations/add-column.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from 'bun:test' +import { z } from 'zod' +import { addColumnTool } from '../agent-tools/columns' +import { type ColumnNode, LevelNode } from '../schema' +import { addColumn } from './add-column' + +const level = LevelNode.parse({ id: 'level_columns' }) +const nodes = { [level.id]: level } +const context = { activeLevelId: level.id } +const input = z.object(addColumnTool.input) + +function created(outcome: ReturnType) { + return outcome.changes?.create?.[0]?.node as ColumnNode +} + +test('add_column creates an i-beam column', () => { + const column = created( + addColumn( + nodes, + input.parse({ x: 1, z: 2, crossSection: 'i-beam', width: 0.3, depth: 0.4 }), + context, + ), + ) + expect(column.crossSection).toBe('i-beam') + expect(column.width).toBe(0.3) + expect(column.depth).toBe(0.4) + expect(column.position).toEqual([1, 0, 2]) +}) diff --git a/packages/core/src/agent-tools/columns.ts b/packages/core/src/agent-tools/columns.ts index 038293c65b..d58d2bfc2e 100644 --- a/packages/core/src/agent-tools/columns.ts +++ b/packages/core/src/agent-tools/columns.ts @@ -22,11 +22,13 @@ export const addColumnTool = { }).optional(), width: measurement('length', 'm', { positive: true, - description: 'Width when crossSection is "square" / "rectangular" (default: 0.44).', + description: + 'Width when crossSection is "square" / "rectangular" / "i-beam" (default: 0.44).', }).optional(), depth: measurement('length', 'm', { positive: true, - description: 'Depth when crossSection is "square" / "rectangular" (default: 0.44).', + description: + 'Depth when crossSection is "square" / "rectangular" / "i-beam" (default: 0.44).', }).optional(), rotation: measurement('angle', 'deg', { description: 'Y-axis rotation (default: 0).', @@ -38,10 +40,10 @@ export const addColumnTool = { "Visual style: 'plain' (default smooth pillar), 'faceted' (sharp prism faces), 'fluted' (vertical channels — classical), 'lathe-turned' (turned bands — porch / interior), 'dravidian-carved' (heavy carving — temple), 'cluster' (bundled colonnette — gothic).", ), crossSection: z - .enum(['round', 'square', 'rectangular', 'octagonal', 'sixteen-sided']) + .enum(['round', 'square', 'rectangular', 'octagonal', 'sixteen-sided', 'i-beam']) .optional() .describe( - "Plan shape: 'round' (default), 'square' (equal width/depth), 'rectangular' (independent width/depth), 'octagonal', 'sixteen-sided'.", + "Plan shape: 'round' (default), 'square' (equal width/depth), 'rectangular' (independent width/depth), 'octagonal', 'sixteen-sided', 'i-beam' (steel H/I section: flanges span width, web runs along depth).", ), level: z .string() diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dd584944b6..ce53980671 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -129,6 +129,7 @@ export { computeCeilingSurfaceCells, parseCeilingRegionRole, } from './lib/ceiling-surface' +export { columnIBeamOutline, columnIBeamSection } from './lib/column-geometry' export { isCutterName, resolveCutterHost } from './lib/cutter-host' export { clampDoorOperationState, diff --git a/packages/core/src/lib/column-geometry.test.ts b/packages/core/src/lib/column-geometry.test.ts new file mode 100644 index 0000000000..bf7750efe3 --- /dev/null +++ b/packages/core/src/lib/column-geometry.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from 'bun:test' +import { columnIBeamOutline, columnIBeamSection } from './column-geometry' + +test('i-beam plates scale with the section and keep a minimum thickness', () => { + expect(columnIBeamSection(0.3, 0.4)).toEqual({ + flangeThickness: expect.closeTo(0.04, 9), + webThickness: expect.closeTo(0.03, 9), + }) + expect(columnIBeamSection(0.05, 0.05)).toEqual({ flangeThickness: 0.01, webThickness: 0.01 }) +}) + +test('i-beam outline spans the section with a web narrower than the flanges', () => { + const outline = columnIBeamOutline(0.3, 0.4) + expect(outline).toHaveLength(12) + const xs = outline.map(([x]) => x) + const zs = outline.map(([, z]) => z) + expect(Math.min(...xs)).toBeCloseTo(-0.15) + expect(Math.max(...xs)).toBeCloseTo(0.15) + expect(Math.min(...zs)).toBeCloseTo(-0.2) + expect(Math.max(...zs)).toBeCloseTo(0.2) + // The web edge sits half a web thickness off the axis. + expect(outline[3]).toEqual([expect.closeTo(0.015, 9), expect.closeTo(-0.16, 9)]) + // Shoelace area: two flanges plus the web. + const area = + Math.abs( + outline.reduce((sum, [x, z], i) => { + const [nx, nz] = outline[(i + 1) % outline.length]! + return sum + x * nz - nx * z + }, 0), + ) / 2 + expect(area).toBeCloseTo(2 * 0.3 * 0.04 + 0.03 * 0.32, 9) +}) diff --git a/packages/core/src/lib/column-geometry.ts b/packages/core/src/lib/column-geometry.ts new file mode 100644 index 0000000000..fd67dda1cc --- /dev/null +++ b/packages/core/src/lib/column-geometry.ts @@ -0,0 +1,36 @@ +/** + * Plate thicknesses of an i-beam column. The flanges span the width (local X) + * at the front and back faces; the web joins them along the depth (local Z). + */ +export function columnIBeamSection( + width: number, + depth: number, +): { flangeThickness: number; webThickness: number } { + return { + flangeThickness: Math.max(0.01, depth * 0.1), + webThickness: Math.max(0.01, width * 0.1), + } +} + +/** Plan outline of an i-beam section centred on the column axis, in local XZ. */ +export function columnIBeamOutline(width: number, depth: number): [number, number][] { + const { flangeThickness, webThickness } = columnIBeamSection(width, depth) + const x = width / 2 + const z = depth / 2 + const webX = webThickness / 2 + const innerZ = Math.max(0, z - flangeThickness) + return [ + [-x, -z], + [x, -z], + [x, -innerZ], + [webX, -innerZ], + [webX, innerZ], + [x, innerZ], + [x, z], + [-x, z], + [-x, innerZ], + [-webX, innerZ], + [-webX, -innerZ], + [-x, -innerZ], + ] +} diff --git a/packages/core/src/schema/nodes/column.ts b/packages/core/src/schema/nodes/column.ts index fb1144a22b..9c691b57bd 100644 --- a/packages/core/src/schema/nodes/column.ts +++ b/packages/core/src/schema/nodes/column.ts @@ -19,6 +19,7 @@ export const ColumnCrossSection = z.enum([ 'rectangular', 'octagonal', 'sixteen-sided', + 'i-beam', ]) export const ColumnShaftProfile = z.enum(['straight', 'tapered', 'bulged', 'baluster', 'hourglass']) @@ -172,7 +173,7 @@ export const ColumnNode = BaseNode.extend({ }).describe(dedent` Column node - used to represent structural or decorative pillars/columns. - style: visual approach such as plain, lathe-turned, carved, or cluster - - crossSection: plan shape used by the procedural renderer + - crossSection: plan shape used by the procedural renderer; 'i-beam' is a steel H/I section whose flanges span the width at the front and back faces - height/radius/width/depth: primary dimensions in meters - edgeSoftness: bevel radius for square/plinth/block edges - shaftProfile/shaftDetail: profile and surface treatment of the shaft diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx index eaeabcb7be..8740dec59d 100644 --- a/packages/editor/src/components/editor/floorplan-panel.tsx +++ b/packages/editor/src/components/editor/floorplan-panel.tsx @@ -9,6 +9,7 @@ import { CeilingNode as CeilingNodeSchema, type ColumnNode, calculateLevelMiters, + columnIBeamOutline, createSizedStairFlight, DEFAULT_ANGLE_STEP, type DoorNode, @@ -2023,6 +2024,17 @@ function getColumnPlanFootprint(column: ColumnNode): Point2D[] { return getRotatedRectanglePolygon(center, width, depth, column.rotation) } + if ( + column.crossSection === 'i-beam' && + column.baseStyle === 'none' && + column.capitalStyle === 'none' + ) { + return columnIBeamOutline(column.width, column.depth).map(([localX, localY]) => { + const [offsetX, offsetY] = rotatePlanVector(localX, localY, column.rotation) + return { x: center.x + offsetX, y: center.y + offsetY } + }) + } + const shaftWidth = column.crossSection === 'round' || column.crossSection === 'octagonal' || @@ -2046,7 +2058,11 @@ function getColumnPlanFootprint(column: ColumnNode): Point2D[] { column.depth * column.capitalDepthScale, ) - if (column.crossSection === 'square' || column.crossSection === 'rectangular') { + if ( + column.crossSection === 'square' || + column.crossSection === 'rectangular' || + column.crossSection === 'i-beam' + ) { return getRotatedRectanglePolygon(center, width, depth, column.rotation) } diff --git a/packages/ifc-converter/src/export/ifc-model.ts b/packages/ifc-converter/src/export/ifc-model.ts index 821ff6483a..9f67eb9423 100644 --- a/packages/ifc-converter/src/export/ifc-model.ts +++ b/packages/ifc-converter/src/export/ifc-model.ts @@ -151,6 +151,28 @@ export class IfcModel { return this.step.add('IFCRECTANGLEPROFILEDEF', enumValue('AREA'), null, position, xDim, yDim) } + iShapeProfile( + overallWidth: number, + overallDepth: number, + webThickness: number, + flangeThickness: number, + ): StepRef { + const position = this.step.add('IFCAXIS2PLACEMENT2D', this.point2([0, 0]), null) + return this.step.add( + 'IFCISHAPEPROFILEDEF', + enumValue('AREA'), + null, + position, + overallWidth, + overallDepth, + webThickness, + flangeThickness, + null, + null, + null, + ) + } + circleProfile(radius: number): StepRef { const position = this.step.add('IFCAXIS2PLACEMENT2D', this.point2([0, 0]), null) return this.step.add('IFCCIRCLEPROFILEDEF', enumValue('AREA'), null, position, radius) diff --git a/packages/ifc-converter/src/export/scene-to-ifc.ts b/packages/ifc-converter/src/export/scene-to-ifc.ts index 069d169d27..8527975a8e 100644 --- a/packages/ifc-converter/src/export/scene-to-ifc.ts +++ b/packages/ifc-converter/src/export/scene-to-ifc.ts @@ -6,6 +6,7 @@ import { type Collection, type ColumnNode, calculateLevelMiters, + columnIBeamSection, DEFAULT_WALL_THICKNESS, type DoorNode, difference, @@ -1279,8 +1280,10 @@ export function buildIfcExport(input: IfcExportInput): IfcExportResult { } } const [x, y] = planToIfc(column.position[0], column.position[2]) - const profile = - column.crossSection === 'rectangular' || column.crossSection === 'square' + const iBeam = column.crossSection === 'i-beam' && columnIBeamSection(column.width, column.depth) + const profile = iBeam + ? model.iShapeProfile(column.width, column.depth, iBeam.webThickness, iBeam.flangeThickness) + : column.crossSection === 'rectangular' || column.crossSection === 'square' ? model.rectangleProfile( column.width, column.crossSection === 'square' ? column.width : column.depth, @@ -1644,7 +1647,7 @@ function isPlainColumn(column: ColumnNode): boolean { column.baseStyle === 'none' && column.capitalStyle === 'none' && (column.supportStyle ?? 'vertical') === 'vertical' && - ['round', 'square', 'rectangular'].includes(column.crossSection) + ['round', 'square', 'rectangular', 'i-beam'].includes(column.crossSection) ) } diff --git a/packages/ifc-converter/src/index.ts b/packages/ifc-converter/src/index.ts index cea14ce9b8..db4996c6a8 100644 --- a/packages/ifc-converter/src/index.ts +++ b/packages/ifc-converter/src/index.ts @@ -368,8 +368,8 @@ type ExtrusionData = { yDim: number | null profilePoints: number[][] | null // Detected swept-area profile shape (model units, pre-unitFactor). - // 'round' carries `radius`; 'rectangular' carries xDim/yDim. - profileShape: 'round' | 'rectangular' | null + // 'round' carries `radius`; 'rectangular' and 'i-beam' carry xDim/yDim. + profileShape: 'round' | 'rectangular' | 'i-beam' | null radius: number | null // IfcArbitraryProfileDefWithVoids inner rings, same frame as profilePoints. innerCurves: number[][][] @@ -462,6 +462,13 @@ function extractFromExtrusionItem( if (result.profileShape === null) result.profileShape = 'rectangular' } + // IfcIShapeProfileDef: OverallWidth spans the flanges, OverallDepth the web. + if (profile.OverallWidth?.value && profile.WebThickness?.value) { + result.xDim = profile.OverallWidth.value + result.yDim = profile.OverallDepth?.value ?? null + result.profileShape = 'i-beam' + } + // Extract profile points — OuterCurve for ArbitraryClosedProfileDef const curveRef = profile.OuterCurve?.value if (curveRef) { @@ -2341,7 +2348,7 @@ export async function convertIfcToPascal( let width: number | undefined let depth: number | undefined let height: number | undefined - let profileShape: 'round' | 'rectangular' | null = null + let profileShape: 'round' | 'rectangular' | 'i-beam' | null = null let profileRadius: number | undefined try { @@ -2392,7 +2399,7 @@ export async function convertIfcToPascal( width, depth, height, - crossSection: isRect ? 'rectangular' : 'round', + crossSection: profileShape === 'i-beam' ? 'i-beam' : isRect ? 'rectangular' : 'round', radius: profileRadius ?? Math.max(width ?? 0.44, depth ?? 0.44) / 2, style: 'plain', shaftProfile: 'straight', diff --git a/packages/ifc-converter/tests/export-roundtrip.test.ts b/packages/ifc-converter/tests/export-roundtrip.test.ts index 8c68d047ff..451ccaf16b 100644 --- a/packages/ifc-converter/tests/export-roundtrip.test.ts +++ b/packages/ifc-converter/tests/export-roundtrip.test.ts @@ -5,10 +5,11 @@ import { fileURLToPath } from 'node:url' import { type AnyNode, type Collection, - type ColumnNode, + ColumnNode, type DoorNode, getLevelElevations, ItemNode, + LevelNode, type SlabNode, type WallNode, type WindowNode, @@ -402,6 +403,46 @@ describe('IFC export — columns', () => { }) }) +describe('IFC export — i-beam columns', () => { + const level = LevelNode.parse({ id: 'level_steel', name: 'Steel', height: 3 }) + const beam = ColumnNode.parse({ + id: 'column_ibeam', + name: 'Steel column', + parentId: level.id, + position: [1, 0, 2], + rotation: Math.PI / 2, + crossSection: 'i-beam', + width: 0.3, + depth: 0.4, + height: 2.8, + style: 'plain', + baseStyle: 'none', + capitalStyle: 'none', + shaftProfile: 'straight', + }) + const ifc = exportSceneToIfc({ + nodes: { [level.id]: { ...level, children: [beam.id] } as AnyNode, [beam.id]: beam }, + timestamp: EPOCH, + }) + + test('exports an IfcIShapeProfileDef extrusion', () => { + const entities = expectWellFormedStep(ifc) + const profile = [...entities.values()].find((entity) => entity.type === 'IFCISHAPEPROFILEDEF') + expect(profile).toBeDefined() + expect(ifc).toMatch(/IFCISHAPEPROFILEDEF\(\.AREA\.,\$,#\d+,0\.3,0\.4,0\.03,0\.04,\$,\$,\$\)/) + }) + + test('re-imports the section as an i-beam', async () => { + const [column] = nodesOf(await reimport(ifc), 'column') + expect(column?.crossSection).toBe('i-beam') + expect(column?.width).toBeCloseTo(0.3, MM) + expect(column?.depth).toBeCloseTo(0.4, MM) + expect(column?.height).toBeCloseTo(2.8, MM) + expect(column?.position[0]).toBeCloseTo(1, MM) + expect(column?.position[2]).toBeCloseTo(2, MM) + }) +}) + describe('IFC export — sample house round trip', () => { let first: PascalSceneGraph let second: PascalSceneGraph diff --git a/packages/ifc-converter/tests/export-step-check.ts b/packages/ifc-converter/tests/export-step-check.ts index 4650b814da..840903521c 100644 --- a/packages/ifc-converter/tests/export-step-check.ts +++ b/packages/ifc-converter/tests/export-step-check.ts @@ -38,6 +38,7 @@ const ATTRIBUTE_COUNTS: Record = { IFCARBITRARYPROFILEDEFWITHVOIDS: 4, IFCRECTANGLEPROFILEDEF: 5, IFCCIRCLEPROFILEDEF: 4, + IFCISHAPEPROFILEDEF: 10, IFCEXTRUDEDAREASOLID: 4, IFCPRODUCTDEFINITIONSHAPE: 3, IFCWALL: 9, diff --git a/packages/nodes/src/column/floorplan.ts b/packages/nodes/src/column/floorplan.ts index 548ebc97b2..16c2c9cc09 100644 --- a/packages/nodes/src/column/floorplan.ts +++ b/packages/nodes/src/column/floorplan.ts @@ -1,6 +1,7 @@ import { type AnyNode, type ColumnNode, + columnIBeamOutline, type FloorplanGeometry, type FloorplanPoint, type GeometryContext, @@ -411,6 +412,18 @@ function getColumnPlanFootprint(column: ColumnNode): PlanPoint[] { return getRotatedRectanglePolygon(center, width, depth, -column.rotation) } + // A bare i-beam (no base or capital plates) reads as its true section. + if ( + column.crossSection === 'i-beam' && + column.baseStyle === 'none' && + column.capitalStyle === 'none' + ) { + return columnIBeamOutline(column.width, column.depth).map(([x, z]) => { + const [offsetX, offsetY] = rotatePlanVector(x, z, -column.rotation) + return { x: center.x + offsetX, y: center.y + offsetY } + }) + } + // Standalone column: shaft profile expanded for base + capital. const isRound = column.crossSection === 'round' || @@ -429,7 +442,11 @@ function getColumnPlanFootprint(column: ColumnNode): PlanPoint[] { column.depth * column.capitalDepthScale, ) - if (column.crossSection === 'square' || column.crossSection === 'rectangular') { + if ( + column.crossSection === 'square' || + column.crossSection === 'rectangular' || + column.crossSection === 'i-beam' + ) { return getRotatedRectanglePolygon(center, width, depth, -column.rotation) } diff --git a/packages/nodes/src/column/i-beam.test.tsx b/packages/nodes/src/column/i-beam.test.tsx new file mode 100644 index 0000000000..be1d89aa81 --- /dev/null +++ b/packages/nodes/src/column/i-beam.test.tsx @@ -0,0 +1,68 @@ +import { describe, expect, test } from 'bun:test' +import { ColumnNode } from '@pascal-app/core' +import { create } from '@react-three/test-renderer' +import { Box3, type Mesh } from 'three' +import { getColumnFloorplanFootprint } from './floorplan' +import { ColumnPreview } from './renderer' +import { columnCapitalBlocks, columnShaftLayout } from './shape' +import { columnTopSurfaces } from './surface' + +const bare = { + crossSection: 'i-beam', + width: 0.3, + depth: 0.4, + height: 3, + baseStyle: 'none', + capitalStyle: 'none', +} as const + +describe('i-beam column', () => { + test('renders two flanges and a web spanning the section', async () => { + const renderer = await create() + const meshes: Mesh[] = [] + renderer.scene.instance.updateMatrixWorld(true) + renderer.scene.instance.traverse((object) => { + if ((object as Mesh).isMesh) meshes.push(object as Mesh) + }) + expect(meshes).toHaveLength(3) + const bounds = new Box3() + for (const mesh of meshes) bounds.expandByObject(mesh) + expect(bounds.min.x).toBeCloseTo(-0.15) + expect(bounds.max.x).toBeCloseTo(0.15) + expect(bounds.min.z).toBeCloseTo(-0.2) + expect(bounds.max.z).toBeCloseTo(0.2) + expect(bounds.max.y).toBeCloseTo(3) + const web = meshes.find((mesh) => { + const box = new Box3().setFromObject(mesh) + return box.max.x - box.min.x < 0.1 + }) + expect(web).toBeDefined() + await renderer.unmount() + }) + + test('a bare i-beam draws its true section in plan', () => { + const column = ColumnNode.parse({ ...bare, position: [2, 0, 1] }) + const footprint = getColumnFloorplanFootprint(column) + expect(footprint).toHaveLength(12) + expect(Math.min(...footprint.map(([x]) => x))).toBeCloseTo(1.85) + expect(Math.max(...footprint.map(([, z]) => z))).toBeCloseTo(1.2) + }) + + test('an i-beam with plates draws the plate rectangle in plan', () => { + const column = ColumnNode.parse({ ...bare, baseStyle: 'simple-square' }) + expect(getColumnFloorplanFootprint(column)).toHaveLength(4) + }) + + test('caps with box plates and supports objects on the section', () => { + const capped = ColumnNode.parse({ ...bare, capitalStyle: 'simple-slab' }) + const layout = columnShaftLayout(capped) + expect( + columnCapitalBlocks(capped, layout.shaftY + layout.shaftHeight, layout.capitalHeight), + ).toEqual([expect.objectContaining({ kind: 'box' })]) + + const [top] = columnTopSurfaces(ColumnNode.parse(bare)) + expect(top?.position).toEqual([0, 3, 0]) + expect(top?.region).toMatchObject({ kind: 'polygon' }) + expect(top?.region.kind === 'polygon' && top.region.points).toHaveLength(12) + }) +}) diff --git a/packages/nodes/src/column/panel.tsx b/packages/nodes/src/column/panel.tsx index 3c93a902bc..f8be782122 100644 --- a/packages/nodes/src/column/panel.tsx +++ b/packages/nodes/src/column/panel.tsx @@ -443,7 +443,7 @@ export default function ColumnPanel() { ) : ( <> -
+
{( [ { @@ -513,6 +513,26 @@ export default function ColumnPanel() { ), }, + { + value: 'i-beam', + label: 'I-Beam', + icon: ( + + ), + }, ] as { value: ColumnNode['crossSection'] label: string @@ -690,7 +710,9 @@ export default function ColumnPanel() { handleUpdate({ width: value, radius: value / 2, - ...(node.crossSection === 'rectangular' ? {} : { depth: value }), + ...(node.crossSection === 'rectangular' || node.crossSection === 'i-beam' + ? {} + : { depth: value }), }) } precision={2} @@ -698,7 +720,7 @@ export default function ColumnPanel() { unit="m" value={node.width} /> - {node.crossSection === 'rectangular' && ( + {(node.crossSection === 'rectangular' || node.crossSection === 'i-beam') && ( - {!isBraceSupport && ( + {!isBraceSupport && node.crossSection !== 'i-beam' && (