Skip to content
3 changes: 3 additions & 0 deletions packages/core/src/events/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
GuideNode,
GutterNode,
HvacEquipmentNode,
ImportedMeshNode,
ItemNode,
LevelNode,
LinesetNode,
Expand Down Expand Up @@ -92,6 +93,7 @@ export interface NodeEvent<T extends AnyNode = AnyNode> {
export type WallEvent = NodeEvent<WallNode>
export type FenceEvent = NodeEvent<FenceNode>
export type ItemEvent = NodeEvent<ItemNode>
export type ImportedMeshEvent = NodeEvent<ImportedMeshNode>
export type SiteEvent = NodeEvent<SiteNode>
export type BuildingEvent = NodeEvent<BuildingNode>
export type CabinetEvent = NodeEvent<CabinetNode>
Expand Down Expand Up @@ -294,6 +296,7 @@ type EditorEvents = GridEvents &
NodeEvents<'cabinet', CabinetEvent> &
NodeEvents<'cabinet-module', CabinetModuleEvent> &
NodeEvents<'item', ItemEvent> &
NodeEvents<'imported-mesh', ImportedMeshEvent> &
NodeEvents<'site', SiteEvent> &
NodeEvents<'building', BuildingEvent> &
NodeEvents<'elevator', ElevatorEvent> &
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type {
GridEvent,
GuideEvent,
GutterEvent,
ImportedMeshEvent,
ItemEvent,
LevelEvent,
MeasurementEvent,
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export { FenceBaseStyle, FenceNode, FenceStyle } from './nodes/fence'
export { GuideNode, GuideScaleReference } from './nodes/guide'
export { GutterNode, GutterOutlet } from './nodes/gutter'
export { HvacEquipmentNode } from './nodes/hvac-equipment'
export {
ImportedMeshNode,
ImportedMeshPrimitive,
type ImportedMeshPrimitive as ImportedMeshPrimitiveValue,
} from './nodes/imported-mesh'
export type {
AnimationEffect,
Asset,
Expand Down
36 changes: 36 additions & 0 deletions packages/core/src/schema/nodes/imported-mesh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'

const finiteNumber = z.number().finite()
const vector3Array = z
.array(finiteNumber)
.refine((values) => values.length % 3 === 0, 'Expected XYZ triples')

export const ImportedMeshPrimitive = z.object({
positions: vector3Array,
normals: vector3Array.optional(),
indices: z.array(z.number().int().nonnegative()).default([]),
color: z.string().default('#94a3b8'),
opacity: z.number().min(0).max(1).default(1),
})

export type ImportedMeshPrimitive = z.infer<typeof ImportedMeshPrimitive>

/** Triangle geometry retained when an import has no native Pascal shape. */
export const ImportedMeshNode = BaseNode.extend({
id: objectId('imesh'),
type: nodeType('imported-mesh'),
position: z.tuple([finiteNumber, finiteNumber, finiteNumber]).default([0, 0, 0]),
rotation: z.tuple([finiteNumber, finiteNumber, finiteNumber]).default([0, 0, 0]),
primitives: z.array(ImportedMeshPrimitive).default([]),
}).describe(
dedent`
Imported mesh node - triangle geometry preserved from an external model
- position / rotation: transform in the parent coordinate system
- primitives: indexed triangle buffers in meters with source display colors
- metadata: source-format identity and properties
`,
)

export type ImportedMeshNode = z.infer<typeof ImportedMeshNode>
2 changes: 2 additions & 0 deletions packages/core/src/schema/nodes/level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { DuctTerminalNode } from './duct-terminal'
import type { FenceNode } from './fence'
import type { GuideNode } from './guide'
import type { HvacEquipmentNode } from './hvac-equipment'
import type { ImportedMeshNode } from './imported-mesh'
import type { ItemNode } from './item'
import type { LinesetNode } from './lineset'
import type { LiquidLineNode } from './liquid-line'
Expand All @@ -34,6 +35,7 @@ type CoreLevelChildId =
| ConstructionDimensionNode['id']
| StructuralGridNode['id']
| ItemNode['id']
| ImportedMeshNode['id']
| ZoneNode['id']
| SlabNode['id']
| CeilingNode['id']
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { FenceNode } from './nodes/fence'
import { GuideNode } from './nodes/guide'
import { GutterNode } from './nodes/gutter'
import { HvacEquipmentNode } from './nodes/hvac-equipment'
import { ImportedMeshNode } from './nodes/imported-mesh'
import { ItemNode } from './nodes/item'
import { LevelNode } from './nodes/level'
import { LinesetNode } from './nodes/lineset'
Expand Down Expand Up @@ -58,6 +59,7 @@ export const AnyNode = z.discriminatedUnion('type', [
CabinetNode,
CabinetModuleNode,
ItemNode,
ImportedMeshNode,
ZoneNode,
SlabNode,
CeilingNode,
Expand Down
18 changes: 18 additions & 0 deletions packages/ifc-converter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,21 @@ IFC bytes, returns `{ nodes, rootNodeIds, stats }` shaped against
`@pascal-app/core` schemas.

No DOM, no React. The UI lives in `apps/ifc-converter`.

Native Pascal nodes are produced when the converter can recover the required
parameters for sites, buildings, levels, walls, doors, windows, slabs, columns,
and IFC spaces (as room zones). IFC stair flights are retained as exact imported
meshes; roofs retain Pascal hierarchy and source metadata but are not yet a
complete parametric conversion. Beams,
railings, coverings, furnishings, proxies, curtain walls, plates, members,
footings, and elements whose native parameters cannot be recovered are retained
as selectable `imported-mesh` nodes using their IFC triangle geometry and color.
Imported meshes are import-only and do not appear as empty objects in the editor
palette.

Door families are derived from `IfcDoor.OperationType`. Glazing is applied from
the standardized `Pset_DoorCommon.GlazingAreaFraction` property rather than
from element names or project-specific conventions.

Browser callers use the default WebIFC WASM path (`/`). Node callers can pass
`{ wasmPath: '/absolute/path/to/web-ifc/' }` in `ConversionOptions`.
65 changes: 65 additions & 0 deletions packages/ifc-converter/src/door-semantics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { DoorNode } from '@pascal-app/core'

type DoorStyle = Partial<DoorNode>

const glazedSegments: DoorNode['segments'] = [
{
type: 'glass',
heightRatio: 1,
columnRatios: [1],
dividerThickness: 0.025,
panelDepth: 0.008,
panelInset: 0.035,
},
]

/** Map standardized IfcDoor operation values to Pascal door families. */
export function doorStyleFromIfcOperation(operationType: unknown): DoorStyle {
const operation = String(operationType ?? '').toUpperCase()
const leafCount = operation.startsWith('DOUBLE_DOOR') ? 2 : 1

if (operation.includes('SLIDING')) {
return {
doorType: 'sliding',
leafCount,
slideDirection: operation.includes('RIGHT') ? 'right' : 'left',
trackStyle: 'visible',
threshold: false,
}
}

if (operation.includes('FOLDING')) {
return {
doorType: 'folding',
leafCount,
}
}
Comment thread
yorhodes marked this conversation as resolved.

if (operation === 'ROLLINGUP') {
return {
doorCategory: 'garage',
doorType: 'garage-rollup',
trackStyle: 'overhead',
}
}

if (operation.startsWith('DOUBLE_DOOR')) {
return { doorType: 'double', leafCount: 2 }
}

if (operation.includes('SWING_RIGHT')) return { hingesSide: 'right' }
if (operation.includes('SWING_LEFT')) return { hingesSide: 'left' }
return {}
}

/** Apply the standardized Pset_DoorCommon glazing fraction after psets load. */
export function doorGlazingStyle(door: DoorNode, glazingAreaFraction: unknown): DoorStyle {
const fraction = Number(glazingAreaFraction)
if (!Number.isFinite(fraction) || fraction <= 0) return {}

return {
doorType: door.doorType === 'double' ? 'french' : door.doorType,
segments: glazedSegments,
contentPadding: [0.04, 0.05],
}
}
Loading