Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/ifc-converter/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated Next env change

Low Severity

This commit changes apps/ifc-converter/next-env.d.ts (an auto-generated Next.js types stub marked “should not be edited”) with no connection to the Fireplace feature. It looks like an accidental local regen included in the PR.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a8d3b37. Configure here.


// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
3 changes: 3 additions & 0 deletions packages/core/src/events/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
ElevatorNode,
EyebrowVentNode,
FenceNode,
FireplaceNode,
GuideNode,
GutterNode,
HvacEquipmentNode,
Expand Down Expand Up @@ -86,6 +87,7 @@ export interface NodeEvent<T extends AnyNode = AnyNode> {

export type WallEvent = NodeEvent<WallNode>
export type FenceEvent = NodeEvent<FenceNode>
export type FireplaceEvent = NodeEvent<FireplaceNode>
export type ItemEvent = NodeEvent<ItemNode>
export type SiteEvent = NodeEvent<SiteNode>
export type BuildingEvent = NodeEvent<BuildingNode>
Expand Down Expand Up @@ -254,6 +256,7 @@ type RoomPresetEvents = {
type EditorEvents = GridEvents &
NodeEvents<'wall', WallEvent> &
NodeEvents<'fence', FenceEvent> &
NodeEvents<'fireplace', FireplaceEvent> &
NodeEvents<'item', ItemEvent> &
NodeEvents<'site', SiteEvent> &
NodeEvents<'building', BuildingEvent> &
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 @@ -11,6 +11,7 @@ export type {
ElevatorEvent,
EventSuffix,
FenceEvent,
FireplaceEvent,
GridEvent,
GuideEvent,
GutterEvent,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ export {
} from './nodes/elevator'
export { EyebrowVentNode } from './nodes/eyebrow-vent'
export { FenceBaseStyle, FenceNode, FenceStyle } from './nodes/fence'
export {
FireplaceMaterialRole,
FireplaceNode,
FireplaceStyle,
FireStyle,
} from './nodes/fireplace'
export { GuideNode, GuideScaleReference } from './nodes/guide'
export { GutterNode, GutterOutlet } from './nodes/gutter'
export { HvacEquipmentNode } from './nodes/hvac-equipment'
Expand Down
69 changes: 69 additions & 0 deletions packages/core/src/schema/nodes/fireplace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
import { MaterialSchema } from '../material'

export const FireplaceMaterialRole = z.enum(['mantel', 'surround', 'hearth', 'firebox'])
export type FireplaceMaterialRole = z.infer<typeof FireplaceMaterialRole>

export const FireplaceStyle = z.enum(['wall', 'freestanding', 'corner', 'double-sided'])
export type FireplaceStyle = z.infer<typeof FireplaceStyle>

export const FireStyle = z.enum(['none', 'small', 'medium', 'large', 'roaring'])
export type FireStyle = z.infer<typeof FireStyle>

export const FireplaceNode = BaseNode.extend({
id: objectId('fireplace'),
type: nodeType('fireplace'),

material: MaterialSchema.optional(),
materialPreset: z.string().optional(),
mantelMaterial: MaterialSchema.optional(),
mantelMaterialPreset: z.string().optional(),
hearthMaterial: MaterialSchema.optional(),
hearthMaterialPreset: z.string().optional(),
fireboxMaterial: MaterialSchema.optional(),
fireboxMaterialPreset: z.string().optional(),

position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
rotation: z.number().default(0),

style: FireplaceStyle.default('wall'),

width: z.number().min(0.6).max(4).default(1.5),
height: z.number().min(0.8).max(4).default(2.2),
depth: z.number().min(0.3).max(1.5).default(0.6),

fireboxWidth: z.number().min(0.3).max(3).default(0.9),
fireboxHeight: z.number().min(0.3).max(3).default(0.7),
fireboxDepth: z.number().min(0.2).max(1.2).default(0.4),
fireboxSillHeight: z.number().min(0).max(1.5).default(0.3),

mantelHeight: z.number().min(0.05).max(0.5).default(0.12),
mantelOverhang: z.number().min(0).max(0.3).default(0.08),
mantelThickness: z.number().min(0.03).max(0.2).default(0.06),
mantelWidth: z.number().min(0).max(1).default(0.1),

hearthDepth: z.number().min(0).max(0.8).default(0.35),
hearthHeight: z.number().min(0.02).max(0.2).default(0.05),
hearthWidth: z.number().min(0).max(1.5).default(0.15),

surroundWidth: z.number().min(0.05).max(0.5).default(0.15),
lintelHeight: z.number().min(0.05).max(0.4).default(0.12),

cornerAngle: z.number().min(30).max(90).default(45),

fire: FireStyle.default('medium'),
fireColor: z.enum(['orange', 'amber', 'blue', 'white']).default('orange'),
}).describe(
dedent`
Fireplace — an architectural fireplace with mantel, hearth, surround,
firebox, and optional animated fire. Hosted on a level; placed against
a wall (wall), free in space (freestanding), in a corner (corner), or
penetrating a wall (double-sided). The fire is a procedural particle
system rendered in the viewer — toggle with the \`fire\` field.
`,
)

export type FireplaceNode = z.infer<typeof FireplaceNode>
export type FireplaceNodeId = FireplaceNode['id']
2 changes: 2 additions & 0 deletions packages/core/src/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { DuctTerminalNode } from './nodes/duct-terminal'
import { ElevatorNode } from './nodes/elevator'
import { EyebrowVentNode } from './nodes/eyebrow-vent'
import { FenceNode } from './nodes/fence'
import { FireplaceNode } from './nodes/fireplace'
import { GuideNode } from './nodes/guide'
import { GutterNode } from './nodes/gutter'
import { HvacEquipmentNode } from './nodes/hvac-equipment'
Expand Down Expand Up @@ -83,6 +84,7 @@ export const AnyNode = z.discriminatedUnion('type', [
PipeSegmentNode,
PipeFittingNode,
PipeTrapNode,
FireplaceNode,
])

export type AnyNode = z.infer<typeof AnyNode>
Expand Down
162 changes: 162 additions & 0 deletions packages/nodes/src/fireplace/definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import type {
FireplaceNode as FireplaceNodeType,
HandleDescriptor,
NodeDefinition,
} from '@pascal-app/core'
import { buildFireplaceGeometry } from './geometry'
import { fireplaceParametrics } from './parametrics'
import { FireplaceNode } from './schema'

const SIDE_HANDLE_OFFSET = 0.18
const HEIGHT_HANDLE_OFFSET = 0.22
const ROTATE_CORNER_OFFSET = 0.32
const ROTATE_RING_OFFSET = 0.04
const MOVE_FRONT_OFFSET = 0.35

const MIN_FP_WIDTH = 0.6
const MIN_FP_DEPTH = 0.3
const MIN_FP_HEIGHT = 0.8

function fireplaceWidthHandle(): HandleDescriptor<FireplaceNodeType> {
return {
kind: 'linear-resize',
axis: 'x',
anchor: 'center',
min: MIN_FP_WIDTH,
currentValue: (n) => n.width,
apply: (_n, newValue) => ({ width: newValue }),
placement: {
position: (n) => [n.width / 2 + SIDE_HANDLE_OFFSET, n.height / 2, 0],
},
}
}

function fireplaceDepthHandle(): HandleDescriptor<FireplaceNodeType> {
return {
kind: 'linear-resize',
axis: 'z',
anchor: 'center',
min: MIN_FP_DEPTH,
currentValue: (n) => n.depth,
apply: (_n, newValue) => ({ depth: newValue }),
placement: {
position: (n) => [0, n.height / 2, n.depth / 2 + SIDE_HANDLE_OFFSET],
},
}
}

function fireplaceHeightHandle(): HandleDescriptor<FireplaceNodeType> {
return {
kind: 'linear-resize',
axis: 'y',
anchor: 'min',
min: MIN_FP_HEIGHT,
currentValue: (n) => n.height,
apply: (_n, newValue) => ({ height: newValue }),
placement: {
position: (n) => [0, n.height + HEIGHT_HANDLE_OFFSET, 0],
},
}
}

function fireplaceRotateHandle(): HandleDescriptor<FireplaceNodeType> {
return {
kind: 'arc-resize',
axis: 'angular',
shape: 'rotate',
apply: (initial, delta) => ({
rotation: (initial.rotation ?? 0) - delta,
}),
placement: {
position: (n) => [n.width / 2, n.height / 2, n.depth / 2 + ROTATE_CORNER_OFFSET],
rotationY: () => -Math.PI / 4,
},
decoration: {
kind: 'ring',
radius: (n) => Math.hypot(n.width / 2, n.depth / 2) + ROTATE_RING_OFFSET,
y: (n) => n.height / 2,
},
}
}

function fireplaceMoveHandle(): HandleDescriptor<FireplaceNodeType> {
return {
kind: 'translate',
placement: {
position: (n) => [0, 0.02, n.depth / 2 + MOVE_FRONT_OFFSET],
},
apply: (_n, pos) => ({ position: [pos[0], pos[1], pos[2]] }),
snapExtents: (n) => [n.width, n.depth],
}
}

function fireplaceHandles(_node: FireplaceNodeType): HandleDescriptor<FireplaceNodeType>[] {
return [
fireplaceWidthHandle(),
fireplaceDepthHandle(),
fireplaceHeightHandle(),
fireplaceRotateHandle(),
fireplaceMoveHandle(),
]
}

export const fireplaceDefinition: NodeDefinition<typeof FireplaceNode> = {
kind: 'fireplace',
snapProfile: 'item',
facingIndicator: true,
schemaVersion: 1,
schema: FireplaceNode,
category: 'furnish',
surfaceRole: 'furnishing',

defaults: () => ({
object: 'node',
parentId: null,
visible: true,
metadata: {},
position: [0, 0, 0],
rotation: 0,
style: 'wall',
width: 1.5,
height: 2.2,
depth: 0.6,
fireboxWidth: 0.9,
fireboxHeight: 0.7,
fireboxDepth: 0.4,
fireboxSillHeight: 0.3,
mantelHeight: 0.12,
mantelOverhang: 0.08,
mantelThickness: 0.06,
mantelWidth: 0.1,
hearthDepth: 0.35,
hearthHeight: 0.05,
hearthWidth: 0.15,
surroundWidth: 0.15,
lintelHeight: 0.12,
cornerAngle: 45,
fire: 'medium',
fireColor: 'orange',
}),

capabilities: {
movable: { axes: ['x', 'z'], gridSnap: true },
rotatable: {
axes: ['y'],
snapAngles: [0, Math.PI / 4, Math.PI / 2, (3 * Math.PI) / 4, Math.PI],
},
selectable: { hitVolume: 'bbox' },
duplicable: true,
deletable: true,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing slab elevation support

Medium Severity

Unlike shelf, column, spawn, and item, the fireplace definition never declares capabilities.floorPlaced. FloorElevationSystem therefore never lifts it onto overlapping slabs, so a fireplace on a raised slab stays at level Y and sinks through the slab. The placement tool still calls getFloorStackPreviewPosition, which cannot elevate correctly without a footprint.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a8d3b37. Configure here.


handles: fireplaceHandles,
parametrics: fireplaceParametrics,
geometry: buildFireplaceGeometry,
renderer: { kind: 'parametric', module: () => import('./renderer') },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate fireplace geometry mounted

High Severity

The fireplace geometry is rendered twice. It's defined in NodeDefinition.geometry for GeometrySystem to handle, but the custom FireplaceRenderer also explicitly renders the same geometry. This results in doubled visuals and can cause transformation issues, like dropped corner yaw, especially since markDirty is called on every render.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a8d3b37. Configure here.

tool: () => import('./tool'),
presentation: {
label: 'Fireplace',
icon: { kind: 'iconify', name: 'lucide:flame' },
paletteSection: 'furnish',
},
}
Loading