Merge remote-tracking branch 'origin/main' into feat/editor-ux-rendering-placement
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user