Merge remote-tracking branch 'origin/main' into feat/editor-ux-rendering-placement

This commit is contained in:
Aymeric Rabot
2026-06-10 14:21:35 -04:00
51 changed files with 1338 additions and 515 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@pascal-app/core",
"version": "0.8.0",
"version": "0.9.1",
"description": "Core library for Pascal 3D building editor",
"type": "module",
"main": "./dist/index.js",
@@ -83,7 +83,7 @@
"@types/bun": "^1.3.0",
"@types/react": "^19.2.2",
"@types/three": "^0.184.0",
"typescript": "6.0.2"
"typescript": "6.0.3"
},
"keywords": [
"3d",
+10 -1
View File
@@ -220,6 +220,14 @@ type AIChatEvents = {
}
}
export interface RoomPresetCreateEvent {
zoneId: ZoneNode['id']
}
type RoomPresetEvents = {
'room-preset:create': RoomPresetCreateEvent
}
type EditorEvents = GridEvents &
NodeEvents<'wall', WallEvent> &
NodeEvents<'fence', FenceEvent> &
@@ -260,6 +268,7 @@ type EditorEvents = GridEvents &
WindowAnimationEvents &
ThumbnailEvents &
SnapshotEvents &
AIChatEvents
AIChatEvents &
RoomPresetEvents
export const emitter = mitt<EditorEvents>()
+2
View File
@@ -20,6 +20,7 @@ export type {
RidgeVentEvent,
RoofEvent,
RoofSegmentEvent,
RoomPresetCreateEvent,
ScanEvent,
ShelfEvent,
SiteEvent,
@@ -60,6 +61,7 @@ export {
isOperationDoorType,
SECTIONAL_GARAGE_RENDER_OPEN_SCALE,
} from './lib/door-operation'
export { getDefaultLevelName, getLevelDisplayName } from './lib/level-name'
export {
type Point2D as PolygonPoint2D,
pointInPolygon as pointInPolygon2D,
+11
View File
@@ -0,0 +1,11 @@
import type { LevelNode } from '../schema'
export function getDefaultLevelName(level: number): string {
if (level === 0) return 'Ground Floor'
if (level > 0) return `Floor ${level}`
return `Basement ${-level}`
}
export function getLevelDisplayName(level: Pick<LevelNode, 'name' | 'level'>): string {
return level.name || getDefaultLevelName(level.level)
}
@@ -0,0 +1,70 @@
import { describe, expect, test } from 'bun:test'
import type { CollectionId } from '../schema/collections'
import type { AnyNode, AnyNodeId } from '../schema/types'
import { forkSceneGraph, type SceneGraph } from './clone-scene-graph'
function makeNode(id: string, type: string, extra: Record<string, unknown> = {}): AnyNode {
return {
object: 'node',
id,
type,
parentId: null,
visible: true,
metadata: {},
...extra,
} as unknown as AnyNode
}
function makeSceneGraph(): SceneGraph {
const site = makeNode('site_1', 'site', { children: ['level_1'] })
const level = makeNode('level_1', 'level', {
parentId: 'site_1',
children: ['wall_1', 'scan_1', 'guide_1'],
})
const wall = makeNode('wall_1', 'wall', { parentId: 'level_1' })
const scan = makeNode('scan_1', 'scan', { parentId: 'level_1', url: 'scan.glb' })
const guide = makeNode('guide_1', 'guide', { parentId: 'level_1', url: 'guide.png' })
return {
nodes: {
['site_1' as AnyNodeId]: site,
['level_1' as AnyNodeId]: level,
['wall_1' as AnyNodeId]: wall,
['scan_1' as AnyNodeId]: scan,
['guide_1' as AnyNodeId]: guide,
},
rootNodeIds: ['site_1' as AnyNodeId],
collections: {
['collection_1' as CollectionId]: {
id: 'collection_1' as CollectionId,
name: 'References',
nodeIds: ['scan_1', 'guide_1'] as AnyNodeId[],
},
},
}
}
describe('forkSceneGraph', () => {
test('strips scan and guide nodes by default', () => {
const forked = forkSceneGraph(makeSceneGraph())
const nodes = Object.values(forked.nodes)
expect(nodes.some((node) => node.type === 'scan')).toBe(false)
expect(nodes.some((node) => node.type === 'guide')).toBe(false)
expect(nodes.some((node) => node.type === 'wall')).toBe(true)
expect(forked.collections).toEqual({})
})
test('preserves scan and guide nodes when requested', () => {
const forked = forkSceneGraph(makeSceneGraph(), { preserveScans: true })
const nodes = Object.values(forked.nodes)
expect(nodes.some((node) => node.type === 'scan')).toBe(true)
expect(nodes.some((node) => node.type === 'guide')).toBe(true)
expect(nodes.map((node) => node.id)).not.toContain('scan_1')
expect(nodes.map((node) => node.id)).not.toContain('guide_1')
expect(
Object.values(forked.collections ?? {}).flatMap((collection) => collection.nodeIds),
).toHaveLength(2)
})
})
+14 -4
View File
@@ -226,12 +226,22 @@ export function cloneLevelSubtree(
return { clonedNodes, newLevelId, idMap }
}
export type ForkSceneGraphOptions = {
preserveScans?: boolean
}
/**
* Forks a scene graph for use as a new project: clones with new IDs and strips
* scan and guide nodes (and their references) since those contain user-uploaded
* imagery that shouldn't carry over to a forked project.
* Forks a scene graph for use as a new project: clones with new IDs and, by
* default, strips scan and guide nodes since they contain user-uploaded imagery.
*/
export function forkSceneGraph(sceneGraph: SceneGraph): SceneGraph {
export function forkSceneGraph(
sceneGraph: SceneGraph,
options: ForkSceneGraphOptions = {},
): SceneGraph {
if (options.preserveScans) {
return cloneSceneGraph(sceneGraph)
}
const { nodes, rootNodeIds, collections } = sceneGraph
// First, identify scan and guide node IDs to exclude (user-uploaded imagery)