Refactor asset types and improve UI primitives

Replaces usage of the Asset type with AssetInput across editor components and hooks for consistency with core types. Adds default dimensions fallback for asset placement logic. Refactors UI primitives (button, sidebar, opacity control) to support forwarding refs and asChild prop, and updates imports for primitives. Updates Radix UI dependencies in package.json and bun.lock.
This commit is contained in:
Aymeric Rabot
2026-01-30 22:01:25 -05:00
parent 5b0682ea54
commit 6c99851006
19 changed files with 272 additions and 142 deletions
+18 -13
View File
@@ -1,6 +1,8 @@
import type { AnyNode, AnyNodeId } from '../../schema'
import type { SceneState } from '../use-scene'
type AnyContainerNode = AnyNode & { children: string[] }
export const createNodesAction = (
set: (fn: (state: SceneState) => Partial<SceneState>) => void,
get: () => SceneState,
@@ -54,7 +56,7 @@ export const updateNodesAction = (
get: () => SceneState,
updates: { id: AnyNodeId; data: Partial<AnyNode> }[],
) => {
const parentsToUpdate = new Set<string>()
const parentsToUpdate = new Set<AnyNodeId>()
set((state) => {
const nextNodes = { ...state.nodes }
@@ -66,28 +68,30 @@ export const updateNodesAction = (
// Handle Reparenting Logic
if (data.parentId !== undefined && data.parentId !== currentNode.parentId) {
// 1. Remove from old parent
if (currentNode.parentId && nextNodes[currentNode.parentId]) {
const oldParent = nextNodes[currentNode.parentId] as AnyContainerNode
const oldParentId = currentNode.parentId as AnyNodeId | null
if (oldParentId && nextNodes[oldParentId]) {
const oldParent = nextNodes[oldParentId] as AnyContainerNode
nextNodes[oldParent.id] = {
...oldParent,
children: oldParent.children.filter((childId) => childId !== id),
}
} as AnyNode
parentsToUpdate.add(oldParent.id)
}
// 2. Add to new parent
if (data.parentId && nextNodes[data.parentId]) {
const newParent = nextNodes[data.parentId] as AnyContainerNode
const newParentId = data.parentId as AnyNodeId | null
if (newParentId && nextNodes[newParentId]) {
const newParent = nextNodes[newParentId] as AnyContainerNode
nextNodes[newParent.id] = {
...newParent,
children: Array.from(new Set([...newParent.children, id])),
}
} as AnyNode
parentsToUpdate.add(newParent.id)
}
}
// Apply the update
nextNodes[id] = { ...nextNodes[id], ...data }
nextNodes[id] = { ...nextNodes[id], ...data } as AnyNode
}
return { nodes: nextNodes }
@@ -103,7 +107,7 @@ export const deleteNodesAction = (
get: () => SceneState,
ids: AnyNodeId[],
) => {
const parentsToMarkDirty = new Set<string>()
const parentsToMarkDirty = new Set<AnyNodeId>()
set((state) => {
const nextNodes = { ...state.nodes }
@@ -114,13 +118,14 @@ export const deleteNodesAction = (
if (!node) continue
// 1. Remove reference from Parent
if (node.parentId && nextNodes[node.parentId]) {
const parent = nextNodes[node.parentId] as AnyContainerNode
const parentId = node.parentId as AnyNodeId | null
if (parentId && nextNodes[parentId]) {
const parent = nextNodes[parentId] as AnyContainerNode
if (parent.children) {
nextNodes[parent.id] = {
...parent,
children: parent.children.filter((cid) => cid !== id),
}
} as AnyNode
parentsToMarkDirty.add(parent.id)
}
}
@@ -134,7 +139,7 @@ export const deleteNodesAction = (
// Inside the deleteNodes loop
if ('children' in node && node.children.length > 0) {
// Recursively delete all children first
get().deleteNodes(node.children)
get().deleteNodes(node.children as AnyNodeId[])
}
}
+7 -2
View File
@@ -1,7 +1,8 @@
'use client'
import type { TemporalState } from 'zundo'
import { temporal } from 'zundo'
import { create } from 'zustand'
import { create, type StoreApi, type UseBoundStore } from 'zustand'
import { persist } from 'zustand/middleware'
import { BuildingNode } from '../schema'
import { LevelNode } from '../schema/nodes/level'
@@ -39,7 +40,11 @@ export type SceneState = {
// type PartializedStoreState = Pick<SceneState, 'rootNodeIds' | 'nodes'>;
const useScene = create<SceneState>()(
type UseSceneStore = UseBoundStore<StoreApi<SceneState>> & {
temporal: StoreApi<TemporalState<Pick<SceneState, 'nodes' | 'rootNodeIds'>>>
}
const useScene: UseSceneStore = create<SceneState>()(
persist(
temporal(
(set, get) => ({
+4 -4
View File
@@ -5,7 +5,7 @@ import type {
BaseNode,
BuildingNode,
LevelNode,
Zone,
ZoneNode,
} from "@pascal-app/core";
import type { Object3D } from "three";
@@ -14,7 +14,7 @@ import { create } from "zustand";
type SelectionPath = {
buildingId: BuildingNode["id"] | null;
levelId: LevelNode["id"] | null;
zoneId: Zone["id"] | null;
zoneId: ZoneNode["id"] | null;
selectedIds: BaseNode["id"][]; // For items/assets (multi-select)
};
@@ -25,8 +25,8 @@ type Outliner = {
type ViewerState = {
selection: SelectionPath;
hoveredId: AnyNode["id"] | Zone["id"] | null;
setHoveredId: (id: AnyNode["id"] | Zone["id"] | null) => void;
hoveredId: AnyNode["id"] | ZoneNode["id"] | null;
setHoveredId: (id: AnyNode["id"] | ZoneNode["id"] | null) => void;
cameraMode: "perspective" | "orthographic";
setCameraMode: (mode: "perspective" | "orthographic") => void;