structuring store
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { CameraSchema } from "./camera";
|
||||
|
||||
const customId = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 16);
|
||||
|
||||
/**
|
||||
* Material preset name reference
|
||||
* @example 'white', 'brick', 'wood', 'glass', 'preview-valid'
|
||||
*/
|
||||
export const Material = z.string().optional();
|
||||
export const generateId = <T extends string>(prefix: T): `${T}_${string}` =>
|
||||
`${prefix}_${customId()}` as `${T}_${string}`;
|
||||
export const objectId = <T extends string>(prefix: T) => {
|
||||
const schema = z.templateLiteral([`${prefix}_`, z.string()]);
|
||||
|
||||
return schema.default(() => generateId(prefix) as z.infer<typeof schema>);
|
||||
};
|
||||
export const nodeType = <T extends string>(type: T) =>
|
||||
z.literal(type).default(type);
|
||||
|
||||
export const BaseNode = z.object({
|
||||
object: z.literal("node").default("node"),
|
||||
id: z.string(), // objectId('node'), @Aymericr: Thing is if we specify objectId here, when using BaseNode.extend, TS complains that the id is not assignable to the more specific type in the extended node
|
||||
type: nodeType("node"),
|
||||
name: z.string().optional(),
|
||||
parentId: z.string().nullable().default(null),
|
||||
visible: z.boolean().optional().default(true),
|
||||
camera: CameraSchema.optional(),
|
||||
metadata: z.json().optional().default({}),
|
||||
});
|
||||
|
||||
export type BaseNode = z.infer<typeof BaseNode>;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const Vector3Schema = z.tuple([z.number(), z.number(), z.number()]);
|
||||
|
||||
export const CameraSchema = z.object({
|
||||
position: Vector3Schema,
|
||||
target: Vector3Schema,
|
||||
mode: z.enum(["perspective", "orthographic"]).default("perspective"),
|
||||
fov: z.number().optional(), // For perspective
|
||||
zoom: z.number().optional(), // For orthographic
|
||||
});
|
||||
|
||||
export type Camera = z.infer<typeof CameraSchema>;
|
||||
@@ -0,0 +1,21 @@
|
||||
import dedent from "dedent";
|
||||
import { z } from "zod";
|
||||
import { BaseNode, nodeType, objectId } from "../base";
|
||||
import { LevelNode } from "./level";
|
||||
|
||||
export const BuildingNode = BaseNode.extend({
|
||||
id: objectId("building"),
|
||||
type: nodeType("building"),
|
||||
children: z.array(LevelNode).default([LevelNode.parse({})]),
|
||||
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||
rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||
}).describe(
|
||||
dedent`
|
||||
Building node - used to represent a building
|
||||
- position: position in site coordinate system
|
||||
- rotation: rotation in site coordinate system
|
||||
- children: array of level nodes (each level is a tree of floor and wall nodes)
|
||||
`
|
||||
);
|
||||
|
||||
export type BuildingNode = z.infer<typeof BuildingNode>;
|
||||
@@ -0,0 +1,48 @@
|
||||
import dedent from "dedent";
|
||||
import { z } from "zod";
|
||||
import { BaseNode, nodeType, objectId } from "../base";
|
||||
|
||||
export const ItemNode = BaseNode.extend({
|
||||
id: objectId("item"),
|
||||
type: nodeType("item"),
|
||||
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||
rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||
side: z.enum(["front", "back"]).optional(),
|
||||
|
||||
asset: z
|
||||
.object({
|
||||
category: z.string(),
|
||||
src: z.string(),
|
||||
dimensions: z.tuple([z.number(), z.number(), z.number()]), // [w, h, d]
|
||||
attachTo: z.enum(["wall", "wall-side", "ceiling"]).optional(),
|
||||
// These are "Corrective" transforms to normalize the GLB
|
||||
offset: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||
rotation: z
|
||||
.tuple([z.number(), z.number(), z.number()])
|
||||
.default([0, 0, 0]),
|
||||
scale: z
|
||||
.union([z.number(), z.tuple([z.number(), z.number(), z.number()])])
|
||||
.default(1),
|
||||
})
|
||||
.default({
|
||||
category: "",
|
||||
src: "",
|
||||
dimensions: [1, 1, 1],
|
||||
offset: [0, 0, 0],
|
||||
rotation: [0, 0, 0],
|
||||
scale: 1,
|
||||
}),
|
||||
}).describe(dedent`Item node - used to represent a item in the building
|
||||
- position: position in level coordinate system (or parent coordinate system if attached)
|
||||
- rotation: rotation in level coordinate system (or parent coordinate system if attached)
|
||||
- asset: asset data
|
||||
- category: category of the item
|
||||
- dimensions: size in level coordinate system
|
||||
- src: url of the model
|
||||
- attachTo: where to attach the item (wall, wall-side, ceiling)
|
||||
- offset: corrective position offset for the model
|
||||
- rotation: corrective rotation for the model
|
||||
- scale: corrective scale for the model
|
||||
`);
|
||||
|
||||
export type ItemNode = z.infer<typeof ItemNode>;
|
||||
@@ -0,0 +1,20 @@
|
||||
import dedent from "dedent";
|
||||
import { z } from "zod";
|
||||
import { BaseNode, nodeType, objectId } from "../base";
|
||||
import { WallNode } from "./wall";
|
||||
|
||||
export const LevelNode = BaseNode.extend({
|
||||
id: objectId("level"),
|
||||
type: nodeType("level"),
|
||||
children: z.array(z.discriminatedUnion("type", [WallNode])).default([]),
|
||||
// Specific props
|
||||
level: z.number().default(0),
|
||||
}).describe(
|
||||
dedent`
|
||||
Level node - used to represent a level in the building
|
||||
- children: array of floor, wall, ceiling, roof, item nodes
|
||||
- level: level number
|
||||
`
|
||||
);
|
||||
|
||||
export type LevelNode = z.infer<typeof LevelNode>;
|
||||
@@ -0,0 +1,47 @@
|
||||
// lib/scenegraph/schema/nodes/site.ts
|
||||
|
||||
import dedent from "dedent";
|
||||
import { z } from "zod";
|
||||
import { BaseNode, nodeType, objectId } from "../base";
|
||||
import { BuildingNode } from "./building";
|
||||
import { ItemNode } from "./item";
|
||||
|
||||
// 2D Polygon
|
||||
const PropertyLineData = z.object({
|
||||
type: z.literal("polygon"),
|
||||
points: z.array(z.tuple([z.number(), z.number()])),
|
||||
});
|
||||
|
||||
// 3D Polygon/Mesh
|
||||
// const TerrainData = z.object({
|
||||
// type: z.literal('terrain'),
|
||||
// points: z.array(z.tuple([z.number(), z.number(), z.number()])),
|
||||
// })
|
||||
|
||||
export const SiteNode = BaseNode.extend({
|
||||
id: objectId("site"),
|
||||
type: nodeType("site"),
|
||||
// Specific props
|
||||
polygon: PropertyLineData.optional().default({
|
||||
type: "polygon",
|
||||
// Default 30x30 square matching GRID_SIZE
|
||||
points: [
|
||||
[0, 0],
|
||||
[30, 0],
|
||||
[30, 30],
|
||||
[0, 30],
|
||||
],
|
||||
}),
|
||||
// terrain: TerrainData,
|
||||
children: z
|
||||
.array(z.discriminatedUnion("type", [BuildingNode, ItemNode]))
|
||||
.default([BuildingNode.parse({})]),
|
||||
}).describe(
|
||||
dedent`
|
||||
Site node - used to represent a site
|
||||
- polygon: polygon data
|
||||
- children: array of building and item nodes
|
||||
`
|
||||
);
|
||||
|
||||
export type SiteNode = z.infer<typeof SiteNode>;
|
||||
@@ -0,0 +1,32 @@
|
||||
import dedent from "dedent";
|
||||
import { z } from "zod";
|
||||
import { BaseNode, nodeType, objectId } from "../base";
|
||||
// import { DoorNode } from "./door";
|
||||
// import { ItemNode } from "./item";
|
||||
// import { WindowNode } from "./window";
|
||||
|
||||
export const WallNode = BaseNode.extend({
|
||||
id: objectId("wall"),
|
||||
type: nodeType("wall"),
|
||||
// get children() {
|
||||
// return z
|
||||
// .array(z.discriminatedUnion("type", [DoorNode, WindowNode, ItemNode]))
|
||||
// .default([]);
|
||||
// },
|
||||
// Specific props
|
||||
thickness: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
// e.g., start/end points for path
|
||||
start: z.tuple([z.number(), z.number()]),
|
||||
end: z.tuple([z.number(), z.number()]),
|
||||
}).describe(
|
||||
dedent`
|
||||
Wall node - used to represent a wall in the building
|
||||
- thickness: thickness in meters
|
||||
- height: height in meters
|
||||
- start: start point of the wall in level coordinate system
|
||||
- end: end point of the wall in level coordinate system
|
||||
- size: size of the wall in grid units
|
||||
`
|
||||
);
|
||||
export type WallNode = z.infer<typeof WallNode>;
|
||||
@@ -0,0 +1,16 @@
|
||||
import z from "zod";
|
||||
import { BuildingNode } from "./nodes/building";
|
||||
import { LevelNode } from "./nodes/level";
|
||||
import { SiteNode } from "./nodes/site";
|
||||
import { WallNode } from "./nodes/wall";
|
||||
|
||||
export const AnyNode = z.discriminatedUnion("type", [
|
||||
SiteNode,
|
||||
BuildingNode,
|
||||
LevelNode,
|
||||
WallNode,
|
||||
]);
|
||||
|
||||
export type AnyNode = z.infer<typeof AnyNode>;
|
||||
export type AnyNodeType = AnyNode["type"];
|
||||
export type AnyNodeId = AnyNode["id"];
|
||||
@@ -0,0 +1,159 @@
|
||||
import { create } from "zustand";
|
||||
import { ItemNode } from "../schema/nodes/item";
|
||||
import { LevelNode } from "../schema/nodes/level";
|
||||
import { WallNode } from "../schema/nodes/wall";
|
||||
import { AnyNode, AnyNodeId } from "../schema/types";
|
||||
|
||||
type SceneState = {
|
||||
// 1. The Data: A flat dictionary of all nodes
|
||||
nodes: Record<AnyNodeId, AnyNode>;
|
||||
|
||||
// 2. The Root: Which nodes are at the top level?
|
||||
rootNodeIds: AnyNodeId[];
|
||||
|
||||
// 3. The "Dirty" Set: For the Wall/Physics systems
|
||||
dirtyNodes: Set<AnyNodeId>;
|
||||
|
||||
// Actions
|
||||
loadScene: () => void;
|
||||
markDirty: (id: AnyNodeId) => void;
|
||||
clearDirty: (id: AnyNodeId) => void;
|
||||
|
||||
levelMode: "stacked" | "exploded" | "solo" | "manual";
|
||||
setLevelMode: (mode: "stacked" | "exploded" | "solo" | "manual") => void;
|
||||
};
|
||||
|
||||
const useScene = create<SceneState>()((set, get) => ({
|
||||
// 1. Flat dictionary of all nodes
|
||||
nodes: {},
|
||||
|
||||
// 2. Root node IDs
|
||||
rootNodeIds: [],
|
||||
|
||||
// 3. Dirty set
|
||||
dirtyNodes: new Set<AnyNodeId>(),
|
||||
|
||||
loadScene: () => {
|
||||
const level0 = LevelNode.parse({
|
||||
level: 0,
|
||||
children: [],
|
||||
});
|
||||
const level1 = LevelNode.parse({
|
||||
level: 1,
|
||||
children: [],
|
||||
});
|
||||
const level2 = LevelNode.parse({
|
||||
level: 2,
|
||||
children: [],
|
||||
});
|
||||
|
||||
const wall0 = WallNode.parse({
|
||||
start: [0, 0],
|
||||
end: [5, 0],
|
||||
children: [],
|
||||
});
|
||||
|
||||
const wall1 = WallNode.parse({
|
||||
start: [0, 0],
|
||||
end: [0, 5],
|
||||
children: [],
|
||||
});
|
||||
|
||||
const wall2 = WallNode.parse({
|
||||
start: [5, 5],
|
||||
end: [0, 5],
|
||||
children: [],
|
||||
});
|
||||
|
||||
const wall3 = WallNode.parse({
|
||||
start: [5, 5],
|
||||
end: [5, 0],
|
||||
children: [],
|
||||
});
|
||||
|
||||
level0.children.push(wall0.id, wall1.id, wall2.id, wall3.id);
|
||||
|
||||
// Define all nodes flat
|
||||
const nodes: Record<AnyNodeId, AnyNode> = {
|
||||
// Level 0
|
||||
|
||||
wall_0_0: WallNode.parse({
|
||||
id: "wall_0_0",
|
||||
start: [0, 0],
|
||||
end: [5, 0],
|
||||
children: ["item_1_1", "item_1_2", "item_1_3"],
|
||||
}),
|
||||
wall_0_1: WallNode.parse({
|
||||
id: "wall_0_1",
|
||||
start: [0, 0],
|
||||
end: [0, 5],
|
||||
}),
|
||||
wall_0_2: WallNode.parse({
|
||||
id: "wall_0_2",
|
||||
start: [5, 5],
|
||||
end: [0, 5],
|
||||
}),
|
||||
wall_0_3: WallNode.parse({
|
||||
id: "wall_0_3",
|
||||
start: [5, 5],
|
||||
end: [5, 0],
|
||||
}),
|
||||
|
||||
// Level 1
|
||||
level_1: LevelNode.parse({
|
||||
id: "level_1",
|
||||
level: 1,
|
||||
children: ["item_1_0"],
|
||||
}),
|
||||
item_1_0: ItemNode.parse({
|
||||
id: "item_1_0",
|
||||
position: [-1, 0, 0],
|
||||
}),
|
||||
item_1_1: ItemNode.parse({
|
||||
id: "item_1_1",
|
||||
parentId: "wall_0_0",
|
||||
position: [2.5, 0.8, 0],
|
||||
}),
|
||||
item_1_2: ItemNode.parse({
|
||||
id: "item_1_2",
|
||||
parentId: "wall_0_0",
|
||||
position: [1, 0.8, 0],
|
||||
}),
|
||||
item_1_3: ItemNode.parse({
|
||||
id: "item_1_3",
|
||||
parentId: "wall_0_0",
|
||||
position: [4, 0.8, 0],
|
||||
}),
|
||||
|
||||
// Level 2
|
||||
level_2: LevelNode.parse({
|
||||
id: "level_2",
|
||||
level: 2,
|
||||
children: [],
|
||||
}),
|
||||
};
|
||||
|
||||
// Root nodes are the levels
|
||||
const rootNodeIds = ["level_0", "level_1", "level_2"];
|
||||
|
||||
get().dirtyNodes.add("wall_0_0");
|
||||
get().dirtyNodes.add("wall_0_1");
|
||||
get().dirtyNodes.add("wall_0_2");
|
||||
get().dirtyNodes.add("wall_0_3");
|
||||
set({ nodes, rootNodeIds });
|
||||
},
|
||||
|
||||
markDirty: (id: string) => {
|
||||
get().dirtyNodes.add(id);
|
||||
},
|
||||
|
||||
clearDirty: (id: string) => {
|
||||
get().dirtyNodes.delete(id);
|
||||
},
|
||||
|
||||
levelMode: "exploded",
|
||||
setLevelMode: (mode) => set({ levelMode: mode }),
|
||||
}));
|
||||
|
||||
useScene.getState().loadScene();
|
||||
export default useScene;
|
||||
Reference in New Issue
Block a user