@react-arch/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/index.d.ts +253 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +632 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 React Arch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @react-arch/core
|
|
2
|
+
|
|
3
|
+
The canonical semantic building model and the immutable commands that operate on
|
|
4
|
+
it. **The single source of truth** for building data.
|
|
5
|
+
|
|
6
|
+
Depends only on `@react-arch/shared` and `@react-arch/geometry` — never on React,
|
|
7
|
+
Three.js, PixiJS, or browser APIs (ADR 0001).
|
|
8
|
+
|
|
9
|
+
## Model
|
|
10
|
+
|
|
11
|
+
`BuildingDocument → Building → Floor → { walls, rooms, openings, objects }`,
|
|
12
|
+
plus `materials`, `assets`, `units`, `version`, and `metadata`. Walls are stored
|
|
13
|
+
by **centerline + thickness**; the visible polygon is derived by
|
|
14
|
+
`@react-arch/geometry`. Openings attach to walls semantically (by `offset` along
|
|
15
|
+
the centerline), so they follow when a wall moves.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
Pure functions returning `{ document, changes, warnings }` — they never mutate
|
|
20
|
+
the input:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createWall, createOpening, deleteWall } from "@react-arch/core";
|
|
24
|
+
|
|
25
|
+
const a = createWall(doc, { floorId: "ground", start: [0, 0], end: [5, 0] });
|
|
26
|
+
const b = createOpening(a.document, { wallId, type: "door", offset: 2, width: 0.9, height: 2.1 });
|
|
27
|
+
// deleteWall cascades: attached openings are removed too.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Floor commands: `createFloor`, `duplicateFloor`, `reorderFloor`, `updateFloor`,
|
|
31
|
+
`deleteFloor`. Room/opening commands mirror the wall ones.
|
|
32
|
+
|
|
33
|
+
## History
|
|
34
|
+
|
|
35
|
+
`History` provides snapshot-based undo/redo (`push`, `undo`, `redo`,
|
|
36
|
+
`canUndo`/`canRedo`). Designed to migrate to patches/op-logs later without
|
|
37
|
+
changing the surface.
|
|
38
|
+
|
|
39
|
+
## Serialization
|
|
40
|
+
|
|
41
|
+
`serialize` / `deserialize` (JSON is the lossless canonical format) with a
|
|
42
|
+
forward-only `migrate` registry and `isCompatibleVersion`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Units, Vec2, Vec3 } from "@react-arch/shared";
|
|
2
|
+
|
|
3
|
+
//#region src/model.d.ts
|
|
4
|
+
/** The current on-disk model version. Bump when the schema changes. */
|
|
5
|
+
declare const MODEL_VERSION = "0.1.0";
|
|
6
|
+
type OpeningType = "door" | "window" | "opening";
|
|
7
|
+
type SwingDirection = "in" | "out";
|
|
8
|
+
type HingeSide = "left" | "right";
|
|
9
|
+
interface Wall {
|
|
10
|
+
id: string;
|
|
11
|
+
floorId: string;
|
|
12
|
+
start: Vec2;
|
|
13
|
+
end: Vec2;
|
|
14
|
+
thickness: number;
|
|
15
|
+
height: number;
|
|
16
|
+
materialId?: string;
|
|
17
|
+
layerId?: string;
|
|
18
|
+
locked?: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface Opening {
|
|
21
|
+
id: string;
|
|
22
|
+
floorId: string;
|
|
23
|
+
wallId: string;
|
|
24
|
+
type: OpeningType;
|
|
25
|
+
/** Distance along the wall centerline to the opening center. */
|
|
26
|
+
offset: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
sillHeight: number;
|
|
30
|
+
swing?: SwingDirection;
|
|
31
|
+
hinge?: HingeSide;
|
|
32
|
+
materialId?: string;
|
|
33
|
+
}
|
|
34
|
+
interface Room {
|
|
35
|
+
id: string;
|
|
36
|
+
floorId: string;
|
|
37
|
+
name: string;
|
|
38
|
+
/** Explicit polygon (metres). Rooms may also be derived from wall loops. */
|
|
39
|
+
polygon: Vec2[];
|
|
40
|
+
boundaryWallIds?: string[];
|
|
41
|
+
floorMaterialId?: string;
|
|
42
|
+
ceilingMaterialId?: string;
|
|
43
|
+
usageType?: string;
|
|
44
|
+
height?: number;
|
|
45
|
+
}
|
|
46
|
+
interface BuildingObject {
|
|
47
|
+
id: string;
|
|
48
|
+
floorId: string;
|
|
49
|
+
type: string;
|
|
50
|
+
position: Vec3;
|
|
51
|
+
rotation: Vec3;
|
|
52
|
+
scale: Vec3;
|
|
53
|
+
assetId?: string;
|
|
54
|
+
metadata?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
interface Floor {
|
|
57
|
+
id: string;
|
|
58
|
+
buildingId: string;
|
|
59
|
+
name: string;
|
|
60
|
+
elevation: number;
|
|
61
|
+
height: number;
|
|
62
|
+
visible: boolean;
|
|
63
|
+
locked: boolean;
|
|
64
|
+
walls: Wall[];
|
|
65
|
+
rooms: Room[];
|
|
66
|
+
openings: Opening[];
|
|
67
|
+
objects: BuildingObject[];
|
|
68
|
+
}
|
|
69
|
+
interface Building {
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
floors: Floor[];
|
|
73
|
+
}
|
|
74
|
+
type MaterialCategory = "wall" | "floor" | "ceiling" | "glass" | "wood" | "metal" | "ceramic" | "custom";
|
|
75
|
+
interface Material {
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
category: MaterialCategory;
|
|
79
|
+
baseColor: string;
|
|
80
|
+
roughness?: number;
|
|
81
|
+
metalness?: number;
|
|
82
|
+
opacity?: number;
|
|
83
|
+
textureUrl?: string;
|
|
84
|
+
}
|
|
85
|
+
interface Asset {
|
|
86
|
+
id: string;
|
|
87
|
+
name: string;
|
|
88
|
+
url?: string;
|
|
89
|
+
kind: string;
|
|
90
|
+
}
|
|
91
|
+
interface Site {
|
|
92
|
+
id: string;
|
|
93
|
+
name: string;
|
|
94
|
+
polygon?: Vec2[];
|
|
95
|
+
}
|
|
96
|
+
interface BuildingDocument {
|
|
97
|
+
id: string;
|
|
98
|
+
version: string;
|
|
99
|
+
name: string;
|
|
100
|
+
units: Units;
|
|
101
|
+
site?: Site;
|
|
102
|
+
buildings: Building[];
|
|
103
|
+
materials: Material[];
|
|
104
|
+
assets: Asset[];
|
|
105
|
+
metadata: Record<string, unknown>;
|
|
106
|
+
}
|
|
107
|
+
/** A single semantic change emitted by a command. */
|
|
108
|
+
interface ModelChange {
|
|
109
|
+
kind: "create" | "update" | "delete";
|
|
110
|
+
entity: "wall" | "room" | "opening" | "floor" | "object" | "document";
|
|
111
|
+
id: string;
|
|
112
|
+
}
|
|
113
|
+
interface ModelWarning {
|
|
114
|
+
code: string;
|
|
115
|
+
message: string;
|
|
116
|
+
entityId?: string;
|
|
117
|
+
}
|
|
118
|
+
interface CommandResult {
|
|
119
|
+
document: BuildingDocument;
|
|
120
|
+
changes: ModelChange[];
|
|
121
|
+
warnings: ModelWarning[];
|
|
122
|
+
}
|
|
123
|
+
declare function findFloor(doc: BuildingDocument, floorId: string): {
|
|
124
|
+
building: Building;
|
|
125
|
+
floor: Floor;
|
|
126
|
+
} | undefined;
|
|
127
|
+
declare function allFloors(doc: BuildingDocument): Floor[];
|
|
128
|
+
declare function findWall(doc: BuildingDocument, wallId: string): Wall | undefined;
|
|
129
|
+
declare function findRoom(doc: BuildingDocument, roomId: string): Room | undefined;
|
|
130
|
+
declare function findOpening(doc: BuildingDocument, openingId: string): Opening | undefined;
|
|
131
|
+
type EntityKind = "wall" | "room" | "opening" | "floor" | "object";
|
|
132
|
+
interface EntityRef {
|
|
133
|
+
kind: EntityKind;
|
|
134
|
+
id: string;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/commands.d.ts
|
|
138
|
+
interface CreateWallInput {
|
|
139
|
+
floorId: string;
|
|
140
|
+
start: Vec2;
|
|
141
|
+
end: Vec2;
|
|
142
|
+
thickness?: number;
|
|
143
|
+
height?: number;
|
|
144
|
+
materialId?: string;
|
|
145
|
+
id?: string;
|
|
146
|
+
}
|
|
147
|
+
declare function createWall(doc: BuildingDocument, input: CreateWallInput): CommandResult;
|
|
148
|
+
declare function updateWall(doc: BuildingDocument, wallId: string, patch: Partial<Omit<Wall, "id" | "floorId">>): CommandResult;
|
|
149
|
+
/** Move a wall's endpoints, keeping attached openings within bounds. */
|
|
150
|
+
declare function moveWall(doc: BuildingDocument, wallId: string, start: Vec2, end: Vec2): CommandResult;
|
|
151
|
+
declare function deleteWall(doc: BuildingDocument, wallId: string): CommandResult;
|
|
152
|
+
interface CreateRoomInput {
|
|
153
|
+
floorId: string;
|
|
154
|
+
name: string;
|
|
155
|
+
polygon: Vec2[];
|
|
156
|
+
id?: string;
|
|
157
|
+
floorMaterialId?: string;
|
|
158
|
+
ceilingMaterialId?: string;
|
|
159
|
+
usageType?: string;
|
|
160
|
+
}
|
|
161
|
+
/** Convenience: build a rectangular room from origin + size. */
|
|
162
|
+
declare function rectRoomPolygon(x: number, y: number, width: number, depth: number): Vec2[];
|
|
163
|
+
declare function createRoom(doc: BuildingDocument, input: CreateRoomInput): CommandResult;
|
|
164
|
+
declare function updateRoom(doc: BuildingDocument, roomId: string, patch: Partial<Omit<Room, "id" | "floorId">>): CommandResult;
|
|
165
|
+
declare function deleteRoom(doc: BuildingDocument, roomId: string): CommandResult;
|
|
166
|
+
interface CreateOpeningInput {
|
|
167
|
+
wallId: string;
|
|
168
|
+
type: OpeningType;
|
|
169
|
+
offset: number;
|
|
170
|
+
width: number;
|
|
171
|
+
height: number;
|
|
172
|
+
sillHeight?: number;
|
|
173
|
+
id?: string;
|
|
174
|
+
}
|
|
175
|
+
declare function createOpening(doc: BuildingDocument, input: CreateOpeningInput): CommandResult;
|
|
176
|
+
declare function updateOpening(doc: BuildingDocument, openingId: string, patch: Partial<Omit<Opening, "id" | "floorId" | "wallId">>): CommandResult;
|
|
177
|
+
declare function moveOpening(doc: BuildingDocument, openingId: string, offset: number): CommandResult;
|
|
178
|
+
declare function deleteOpening(doc: BuildingDocument, openingId: string): CommandResult;
|
|
179
|
+
interface CreateFloorInput {
|
|
180
|
+
buildingId: string;
|
|
181
|
+
name: string;
|
|
182
|
+
elevation: number;
|
|
183
|
+
height?: number;
|
|
184
|
+
id?: string;
|
|
185
|
+
}
|
|
186
|
+
declare function createFloor(doc: BuildingDocument, input: CreateFloorInput): CommandResult;
|
|
187
|
+
declare function duplicateFloor(doc: BuildingDocument, floorId: string): CommandResult;
|
|
188
|
+
declare function reorderFloor(doc: BuildingDocument, floorId: string, newElevation: number): CommandResult;
|
|
189
|
+
declare function updateFloor(doc: BuildingDocument, floorId: string, patch: Partial<Pick<Floor, "name" | "elevation" | "height" | "visible" | "locked">>): CommandResult;
|
|
190
|
+
declare function deleteFloor(doc: BuildingDocument, floorId: string): CommandResult;
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/history.d.ts
|
|
193
|
+
interface HistoryEntry {
|
|
194
|
+
id: string;
|
|
195
|
+
label: string;
|
|
196
|
+
before: BuildingDocument;
|
|
197
|
+
after: BuildingDocument;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Command-based undo/redo. Keeps full document snapshots per entry which is
|
|
201
|
+
* simple and correct for the MVP; this can later move to patches / op logs
|
|
202
|
+
* without changing the public surface.
|
|
203
|
+
*/
|
|
204
|
+
declare class History {
|
|
205
|
+
private limit;
|
|
206
|
+
private past;
|
|
207
|
+
private future;
|
|
208
|
+
constructor(limit?: number);
|
|
209
|
+
push(label: string, before: BuildingDocument, after: BuildingDocument): void;
|
|
210
|
+
canUndo(): boolean;
|
|
211
|
+
canRedo(): boolean;
|
|
212
|
+
undo(): BuildingDocument | null;
|
|
213
|
+
redo(): BuildingDocument | null;
|
|
214
|
+
get undoLabel(): string | null;
|
|
215
|
+
get redoLabel(): string | null;
|
|
216
|
+
clear(): void;
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/serialize.d.ts
|
|
220
|
+
declare function migrate(input: Record<string, unknown>): Record<string, unknown>;
|
|
221
|
+
declare function serialize(doc: BuildingDocument): string;
|
|
222
|
+
declare function deserialize(json: string): BuildingDocument;
|
|
223
|
+
declare function isCompatibleVersion(version: string): boolean;
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/defaults.d.ts
|
|
226
|
+
declare const DEFAULT_MATERIALS: Material[];
|
|
227
|
+
interface EmptyDocumentOptions {
|
|
228
|
+
name?: string;
|
|
229
|
+
units?: "metric" | "imperial";
|
|
230
|
+
buildingName?: string;
|
|
231
|
+
}
|
|
232
|
+
declare function createEmptyDocument(options?: EmptyDocumentOptions): BuildingDocument;
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/furniture.d.ts
|
|
235
|
+
/**
|
|
236
|
+
* Placeholder furniture geometry. Each entry is the base footprint (metres) at
|
|
237
|
+
* scale 1: `w` along plan X, `d` along plan Y, `h` vertical. A building object's
|
|
238
|
+
* `scale` multiplies these. Intentionally simple boxes for the MVP — no
|
|
239
|
+
* photorealistic assets.
|
|
240
|
+
*/
|
|
241
|
+
interface FurnitureDims {
|
|
242
|
+
width: number;
|
|
243
|
+
depth: number;
|
|
244
|
+
height: number;
|
|
245
|
+
}
|
|
246
|
+
declare const FURNITURE_TYPES: string[];
|
|
247
|
+
/** Resolve the footprint of a furniture type, applying its scale. */
|
|
248
|
+
declare function furnitureDims(type: string, scale?: Vec3): FurnitureDims;
|
|
249
|
+
/** A subtle per-category colour for the placeholder geometry. */
|
|
250
|
+
declare function furnitureColor(type: string): string;
|
|
251
|
+
//#endregion
|
|
252
|
+
export { Asset, Building, BuildingDocument, BuildingObject, CommandResult, CreateFloorInput, CreateOpeningInput, CreateRoomInput, CreateWallInput, DEFAULT_MATERIALS, EmptyDocumentOptions, EntityKind, EntityRef, FURNITURE_TYPES, Floor, FurnitureDims, HingeSide, History, HistoryEntry, MODEL_VERSION, Material, MaterialCategory, ModelChange, ModelWarning, Opening, OpeningType, Room, Site, SwingDirection, Wall, allFloors, createEmptyDocument, createFloor, createOpening, createRoom, createWall, deleteFloor, deleteOpening, deleteRoom, deleteWall, deserialize, duplicateFloor, findFloor, findOpening, findRoom, findWall, furnitureColor, furnitureDims, isCompatibleVersion, migrate, moveOpening, moveWall, rectRoomPolygon, reorderFloor, serialize, updateFloor, updateOpening, updateRoom, updateWall };
|
|
253
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/model.ts","../src/commands.ts","../src/history.ts","../src/serialize.ts","../src/defaults.ts","../src/furniture.ts"],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import { createId, deepClone } from "@react-arch/shared";
|
|
2
|
+
import { openingFits, wallLength } from "@react-arch/geometry";
|
|
3
|
+
//#region src/model.ts
|
|
4
|
+
/** The current on-disk model version. Bump when the schema changes. */
|
|
5
|
+
const MODEL_VERSION = "0.1.0";
|
|
6
|
+
function findFloor(doc, floorId) {
|
|
7
|
+
for (const building of doc.buildings) {
|
|
8
|
+
const floor = building.floors.find((f) => f.id === floorId);
|
|
9
|
+
if (floor) return {
|
|
10
|
+
building,
|
|
11
|
+
floor
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function allFloors(doc) {
|
|
16
|
+
return doc.buildings.flatMap((b) => b.floors);
|
|
17
|
+
}
|
|
18
|
+
function findWall(doc, wallId) {
|
|
19
|
+
for (const floor of allFloors(doc)) {
|
|
20
|
+
const w = floor.walls.find((x) => x.id === wallId);
|
|
21
|
+
if (w) return w;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function findRoom(doc, roomId) {
|
|
25
|
+
for (const floor of allFloors(doc)) {
|
|
26
|
+
const r = floor.rooms.find((x) => x.id === roomId);
|
|
27
|
+
if (r) return r;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function findOpening(doc, openingId) {
|
|
31
|
+
for (const floor of allFloors(doc)) {
|
|
32
|
+
const o = floor.openings.find((x) => x.id === openingId);
|
|
33
|
+
if (o) return o;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/commands.ts
|
|
38
|
+
/**
|
|
39
|
+
* Commands are pure: they take a document and return a brand-new document plus
|
|
40
|
+
* a list of changes and warnings. They never mutate the input. The studio /
|
|
41
|
+
* editor layers feed these into the history stack for undo/redo.
|
|
42
|
+
*/
|
|
43
|
+
function mutate(doc, fn) {
|
|
44
|
+
const draft = deepClone(doc);
|
|
45
|
+
const warnings = [];
|
|
46
|
+
return {
|
|
47
|
+
document: draft,
|
|
48
|
+
changes: fn(draft, warnings),
|
|
49
|
+
warnings
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function requireFloor(doc, floorId) {
|
|
53
|
+
const found = findFloor(doc, floorId);
|
|
54
|
+
if (!found) throw new Error(`Floor not found: ${floorId}`);
|
|
55
|
+
return found.floor;
|
|
56
|
+
}
|
|
57
|
+
function createWall(doc, input) {
|
|
58
|
+
return mutate(doc, (draft) => {
|
|
59
|
+
const floor = requireFloor(draft, input.floorId);
|
|
60
|
+
const wall = {
|
|
61
|
+
id: input.id ?? createId("wall"),
|
|
62
|
+
floorId: input.floorId,
|
|
63
|
+
start: input.start,
|
|
64
|
+
end: input.end,
|
|
65
|
+
thickness: input.thickness ?? .2,
|
|
66
|
+
height: input.height ?? floor.height,
|
|
67
|
+
materialId: input.materialId
|
|
68
|
+
};
|
|
69
|
+
floor.walls.push(wall);
|
|
70
|
+
return [{
|
|
71
|
+
kind: "create",
|
|
72
|
+
entity: "wall",
|
|
73
|
+
id: wall.id
|
|
74
|
+
}];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function updateWall(doc, wallId, patch) {
|
|
78
|
+
return mutate(doc, (draft, warnings) => {
|
|
79
|
+
for (const floor of allFloorsOf(draft)) {
|
|
80
|
+
const wall = floor.walls.find((w) => w.id === wallId);
|
|
81
|
+
if (!wall) continue;
|
|
82
|
+
Object.assign(wall, patch);
|
|
83
|
+
reattachOpenings(floor, wall, warnings);
|
|
84
|
+
return [{
|
|
85
|
+
kind: "update",
|
|
86
|
+
entity: "wall",
|
|
87
|
+
id: wallId
|
|
88
|
+
}];
|
|
89
|
+
}
|
|
90
|
+
warnings.push({
|
|
91
|
+
code: "wall-missing",
|
|
92
|
+
message: `Wall ${wallId} not found`
|
|
93
|
+
});
|
|
94
|
+
return [];
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Move a wall's endpoints, keeping attached openings within bounds. */
|
|
98
|
+
function moveWall(doc, wallId, start, end) {
|
|
99
|
+
return updateWall(doc, wallId, {
|
|
100
|
+
start,
|
|
101
|
+
end
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function deleteWall(doc, wallId) {
|
|
105
|
+
return mutate(doc, (draft) => {
|
|
106
|
+
const changes = [];
|
|
107
|
+
for (const floor of allFloorsOf(draft)) {
|
|
108
|
+
const idx = floor.walls.findIndex((w) => w.id === wallId);
|
|
109
|
+
if (idx === -1) continue;
|
|
110
|
+
floor.walls.splice(idx, 1);
|
|
111
|
+
changes.push({
|
|
112
|
+
kind: "delete",
|
|
113
|
+
entity: "wall",
|
|
114
|
+
id: wallId
|
|
115
|
+
});
|
|
116
|
+
floor.openings = floor.openings.filter((o) => {
|
|
117
|
+
if (o.wallId === wallId) {
|
|
118
|
+
changes.push({
|
|
119
|
+
kind: "delete",
|
|
120
|
+
entity: "opening",
|
|
121
|
+
id: o.id
|
|
122
|
+
});
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
});
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
return changes;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** Convenience: build a rectangular room from origin + size. */
|
|
133
|
+
function rectRoomPolygon(x, y, width, depth) {
|
|
134
|
+
return [
|
|
135
|
+
[x, y],
|
|
136
|
+
[x + width, y],
|
|
137
|
+
[x + width, y + depth],
|
|
138
|
+
[x, y + depth]
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
function createRoom(doc, input) {
|
|
142
|
+
return mutate(doc, (draft) => {
|
|
143
|
+
const floor = requireFloor(draft, input.floorId);
|
|
144
|
+
const room = {
|
|
145
|
+
id: input.id ?? createId("room"),
|
|
146
|
+
floorId: input.floorId,
|
|
147
|
+
name: input.name,
|
|
148
|
+
polygon: input.polygon,
|
|
149
|
+
floorMaterialId: input.floorMaterialId,
|
|
150
|
+
ceilingMaterialId: input.ceilingMaterialId,
|
|
151
|
+
usageType: input.usageType
|
|
152
|
+
};
|
|
153
|
+
floor.rooms.push(room);
|
|
154
|
+
return [{
|
|
155
|
+
kind: "create",
|
|
156
|
+
entity: "room",
|
|
157
|
+
id: room.id
|
|
158
|
+
}];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function updateRoom(doc, roomId, patch) {
|
|
162
|
+
return mutate(doc, (draft, warnings) => {
|
|
163
|
+
for (const floor of allFloorsOf(draft)) {
|
|
164
|
+
const room = floor.rooms.find((r) => r.id === roomId);
|
|
165
|
+
if (!room) continue;
|
|
166
|
+
Object.assign(room, patch);
|
|
167
|
+
return [{
|
|
168
|
+
kind: "update",
|
|
169
|
+
entity: "room",
|
|
170
|
+
id: roomId
|
|
171
|
+
}];
|
|
172
|
+
}
|
|
173
|
+
warnings.push({
|
|
174
|
+
code: "room-missing",
|
|
175
|
+
message: `Room ${roomId} not found`
|
|
176
|
+
});
|
|
177
|
+
return [];
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function deleteRoom(doc, roomId) {
|
|
181
|
+
return mutate(doc, (draft) => {
|
|
182
|
+
for (const floor of allFloorsOf(draft)) {
|
|
183
|
+
const idx = floor.rooms.findIndex((r) => r.id === roomId);
|
|
184
|
+
if (idx === -1) continue;
|
|
185
|
+
floor.rooms.splice(idx, 1);
|
|
186
|
+
return [{
|
|
187
|
+
kind: "delete",
|
|
188
|
+
entity: "room",
|
|
189
|
+
id: roomId
|
|
190
|
+
}];
|
|
191
|
+
}
|
|
192
|
+
return [];
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function createOpening(doc, input) {
|
|
196
|
+
return mutate(doc, (draft, warnings) => {
|
|
197
|
+
for (const floor of allFloorsOf(draft)) {
|
|
198
|
+
const wall = floor.walls.find((w) => w.id === input.wallId);
|
|
199
|
+
if (!wall) continue;
|
|
200
|
+
const opening = {
|
|
201
|
+
id: input.id ?? createId("opening"),
|
|
202
|
+
floorId: floor.id,
|
|
203
|
+
wallId: input.wallId,
|
|
204
|
+
type: input.type,
|
|
205
|
+
offset: input.offset,
|
|
206
|
+
width: input.width,
|
|
207
|
+
height: input.height,
|
|
208
|
+
sillHeight: input.sillHeight ?? (input.type === "window" ? .9 : 0)
|
|
209
|
+
};
|
|
210
|
+
if (!openingFits(wall, opening)) warnings.push({
|
|
211
|
+
code: "opening-overflow",
|
|
212
|
+
message: `Opening ${opening.id} does not fit within wall ${wall.id}`,
|
|
213
|
+
entityId: opening.id
|
|
214
|
+
});
|
|
215
|
+
floor.openings.push(opening);
|
|
216
|
+
return [{
|
|
217
|
+
kind: "create",
|
|
218
|
+
entity: "opening",
|
|
219
|
+
id: opening.id
|
|
220
|
+
}];
|
|
221
|
+
}
|
|
222
|
+
warnings.push({
|
|
223
|
+
code: "wall-missing",
|
|
224
|
+
message: `Wall ${input.wallId} not found for opening`
|
|
225
|
+
});
|
|
226
|
+
return [];
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function updateOpening(doc, openingId, patch) {
|
|
230
|
+
return mutate(doc, (draft, warnings) => {
|
|
231
|
+
for (const floor of allFloorsOf(draft)) {
|
|
232
|
+
const opening = floor.openings.find((o) => o.id === openingId);
|
|
233
|
+
if (!opening) continue;
|
|
234
|
+
Object.assign(opening, patch);
|
|
235
|
+
const wall = floor.walls.find((w) => w.id === opening.wallId);
|
|
236
|
+
if (wall && !openingFits(wall, opening)) warnings.push({
|
|
237
|
+
code: "opening-overflow",
|
|
238
|
+
message: `Opening ${opening.id} no longer fits its wall`,
|
|
239
|
+
entityId: opening.id
|
|
240
|
+
});
|
|
241
|
+
return [{
|
|
242
|
+
kind: "update",
|
|
243
|
+
entity: "opening",
|
|
244
|
+
id: openingId
|
|
245
|
+
}];
|
|
246
|
+
}
|
|
247
|
+
return [];
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function moveOpening(doc, openingId, offset) {
|
|
251
|
+
return updateOpening(doc, openingId, { offset });
|
|
252
|
+
}
|
|
253
|
+
function deleteOpening(doc, openingId) {
|
|
254
|
+
return mutate(doc, (draft) => {
|
|
255
|
+
for (const floor of allFloorsOf(draft)) {
|
|
256
|
+
const idx = floor.openings.findIndex((o) => o.id === openingId);
|
|
257
|
+
if (idx === -1) continue;
|
|
258
|
+
floor.openings.splice(idx, 1);
|
|
259
|
+
return [{
|
|
260
|
+
kind: "delete",
|
|
261
|
+
entity: "opening",
|
|
262
|
+
id: openingId
|
|
263
|
+
}];
|
|
264
|
+
}
|
|
265
|
+
return [];
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function createFloor(doc, input) {
|
|
269
|
+
return mutate(doc, (draft, warnings) => {
|
|
270
|
+
const building = draft.buildings.find((b) => b.id === input.buildingId);
|
|
271
|
+
if (!building) {
|
|
272
|
+
warnings.push({
|
|
273
|
+
code: "building-missing",
|
|
274
|
+
message: "Building not found"
|
|
275
|
+
});
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
const floor = {
|
|
279
|
+
id: input.id ?? createId("floor"),
|
|
280
|
+
buildingId: building.id,
|
|
281
|
+
name: input.name,
|
|
282
|
+
elevation: input.elevation,
|
|
283
|
+
height: input.height ?? 2.8,
|
|
284
|
+
visible: true,
|
|
285
|
+
locked: false,
|
|
286
|
+
walls: [],
|
|
287
|
+
rooms: [],
|
|
288
|
+
openings: [],
|
|
289
|
+
objects: []
|
|
290
|
+
};
|
|
291
|
+
building.floors.push(floor);
|
|
292
|
+
building.floors.sort((a, b) => a.elevation - b.elevation);
|
|
293
|
+
return [{
|
|
294
|
+
kind: "create",
|
|
295
|
+
entity: "floor",
|
|
296
|
+
id: floor.id
|
|
297
|
+
}];
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function duplicateFloor(doc, floorId) {
|
|
301
|
+
return mutate(doc, (draft) => {
|
|
302
|
+
for (const building of draft.buildings) {
|
|
303
|
+
const floor = building.floors.find((f) => f.id === floorId);
|
|
304
|
+
if (!floor) continue;
|
|
305
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
306
|
+
const copy = deepClone(floor);
|
|
307
|
+
copy.id = createId("floor");
|
|
308
|
+
copy.name = `${floor.name} (copy)`;
|
|
309
|
+
copy.elevation = floor.elevation + floor.height;
|
|
310
|
+
copy.walls.forEach((w) => {
|
|
311
|
+
const nid = createId("wall");
|
|
312
|
+
idMap.set(w.id, nid);
|
|
313
|
+
w.id = nid;
|
|
314
|
+
w.floorId = copy.id;
|
|
315
|
+
});
|
|
316
|
+
copy.openings.forEach((o) => {
|
|
317
|
+
o.id = createId("opening");
|
|
318
|
+
o.floorId = copy.id;
|
|
319
|
+
o.wallId = idMap.get(o.wallId) ?? o.wallId;
|
|
320
|
+
});
|
|
321
|
+
copy.rooms.forEach((r) => {
|
|
322
|
+
r.id = createId("room");
|
|
323
|
+
r.floorId = copy.id;
|
|
324
|
+
});
|
|
325
|
+
copy.objects.forEach((ob) => {
|
|
326
|
+
ob.id = createId("object");
|
|
327
|
+
ob.floorId = copy.id;
|
|
328
|
+
});
|
|
329
|
+
building.floors.push(copy);
|
|
330
|
+
building.floors.sort((a, b) => a.elevation - b.elevation);
|
|
331
|
+
return [{
|
|
332
|
+
kind: "create",
|
|
333
|
+
entity: "floor",
|
|
334
|
+
id: copy.id
|
|
335
|
+
}];
|
|
336
|
+
}
|
|
337
|
+
return [];
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function reorderFloor(doc, floorId, newElevation) {
|
|
341
|
+
return mutate(doc, (draft) => {
|
|
342
|
+
const floor = requireFloor(draft, floorId);
|
|
343
|
+
floor.elevation = newElevation;
|
|
344
|
+
for (const building of draft.buildings) building.floors.sort((a, b) => a.elevation - b.elevation);
|
|
345
|
+
return [{
|
|
346
|
+
kind: "update",
|
|
347
|
+
entity: "floor",
|
|
348
|
+
id: floorId
|
|
349
|
+
}];
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
function updateFloor(doc, floorId, patch) {
|
|
353
|
+
return mutate(doc, (draft) => {
|
|
354
|
+
const floor = requireFloor(draft, floorId);
|
|
355
|
+
Object.assign(floor, patch);
|
|
356
|
+
return [{
|
|
357
|
+
kind: "update",
|
|
358
|
+
entity: "floor",
|
|
359
|
+
id: floorId
|
|
360
|
+
}];
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function deleteFloor(doc, floorId) {
|
|
364
|
+
return mutate(doc, (draft) => {
|
|
365
|
+
for (const building of draft.buildings) {
|
|
366
|
+
const idx = building.floors.findIndex((f) => f.id === floorId);
|
|
367
|
+
if (idx === -1) continue;
|
|
368
|
+
building.floors.splice(idx, 1);
|
|
369
|
+
return [{
|
|
370
|
+
kind: "delete",
|
|
371
|
+
entity: "floor",
|
|
372
|
+
id: floorId
|
|
373
|
+
}];
|
|
374
|
+
}
|
|
375
|
+
return [];
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
function allFloorsOf(doc) {
|
|
379
|
+
return doc.buildings.flatMap((b) => b.floors);
|
|
380
|
+
}
|
|
381
|
+
/** Clamp openings so they stay attached after a wall changes length. */
|
|
382
|
+
function reattachOpenings(floor, wall, warnings) {
|
|
383
|
+
const len = wallLength(wall);
|
|
384
|
+
for (const opening of floor.openings) {
|
|
385
|
+
if (opening.wallId !== wall.id) continue;
|
|
386
|
+
const half = opening.width / 2;
|
|
387
|
+
const clamped = Math.min(Math.max(opening.offset, half), Math.max(half, len - half));
|
|
388
|
+
if (clamped !== opening.offset) {
|
|
389
|
+
opening.offset = clamped;
|
|
390
|
+
warnings.push({
|
|
391
|
+
code: "opening-reclamped",
|
|
392
|
+
message: `Opening ${opening.id} re-positioned to stay on wall`,
|
|
393
|
+
entityId: opening.id
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/history.ts
|
|
400
|
+
/**
|
|
401
|
+
* Command-based undo/redo. Keeps full document snapshots per entry which is
|
|
402
|
+
* simple and correct for the MVP; this can later move to patches / op logs
|
|
403
|
+
* without changing the public surface.
|
|
404
|
+
*/
|
|
405
|
+
var History = class {
|
|
406
|
+
limit;
|
|
407
|
+
past = [];
|
|
408
|
+
future = [];
|
|
409
|
+
constructor(limit = 200) {
|
|
410
|
+
this.limit = limit;
|
|
411
|
+
}
|
|
412
|
+
push(label, before, after) {
|
|
413
|
+
this.past.push({
|
|
414
|
+
id: createId("hist"),
|
|
415
|
+
label,
|
|
416
|
+
before,
|
|
417
|
+
after
|
|
418
|
+
});
|
|
419
|
+
if (this.past.length > this.limit) this.past.shift();
|
|
420
|
+
this.future = [];
|
|
421
|
+
}
|
|
422
|
+
canUndo() {
|
|
423
|
+
return this.past.length > 0;
|
|
424
|
+
}
|
|
425
|
+
canRedo() {
|
|
426
|
+
return this.future.length > 0;
|
|
427
|
+
}
|
|
428
|
+
undo() {
|
|
429
|
+
const entry = this.past.pop();
|
|
430
|
+
if (!entry) return null;
|
|
431
|
+
this.future.push(entry);
|
|
432
|
+
return entry.before;
|
|
433
|
+
}
|
|
434
|
+
redo() {
|
|
435
|
+
const entry = this.future.pop();
|
|
436
|
+
if (!entry) return null;
|
|
437
|
+
this.past.push(entry);
|
|
438
|
+
return entry.after;
|
|
439
|
+
}
|
|
440
|
+
get undoLabel() {
|
|
441
|
+
return this.past.at(-1)?.label ?? null;
|
|
442
|
+
}
|
|
443
|
+
get redoLabel() {
|
|
444
|
+
return this.future.at(-1)?.label ?? null;
|
|
445
|
+
}
|
|
446
|
+
clear() {
|
|
447
|
+
this.past = [];
|
|
448
|
+
this.future = [];
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/serialize.ts
|
|
453
|
+
const migrations = {};
|
|
454
|
+
function migrate(input) {
|
|
455
|
+
let doc = { ...input };
|
|
456
|
+
let guard = 0;
|
|
457
|
+
while (doc.version !== "0.1.0" && guard < 50) {
|
|
458
|
+
const m = migrations[doc.version];
|
|
459
|
+
if (!m) break;
|
|
460
|
+
doc = m(doc);
|
|
461
|
+
guard += 1;
|
|
462
|
+
}
|
|
463
|
+
return doc;
|
|
464
|
+
}
|
|
465
|
+
function serialize(doc) {
|
|
466
|
+
return JSON.stringify(doc, null, 2);
|
|
467
|
+
}
|
|
468
|
+
function deserialize(json) {
|
|
469
|
+
return migrate(JSON.parse(json));
|
|
470
|
+
}
|
|
471
|
+
function isCompatibleVersion(version) {
|
|
472
|
+
const [a, b] = version.split(".");
|
|
473
|
+
const [ca, cb] = MODEL_VERSION.split(".");
|
|
474
|
+
return a === ca && b === cb;
|
|
475
|
+
}
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/defaults.ts
|
|
478
|
+
const DEFAULT_MATERIALS = [
|
|
479
|
+
{
|
|
480
|
+
id: "mat-plaster",
|
|
481
|
+
name: "White Plaster",
|
|
482
|
+
category: "wall",
|
|
483
|
+
baseColor: "#e8e6e1",
|
|
484
|
+
roughness: .9
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
id: "mat-concrete",
|
|
488
|
+
name: "Concrete",
|
|
489
|
+
category: "wall",
|
|
490
|
+
baseColor: "#9b9b97",
|
|
491
|
+
roughness: .8
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
id: "mat-oak",
|
|
495
|
+
name: "Oak Floor",
|
|
496
|
+
category: "floor",
|
|
497
|
+
baseColor: "#b88a52",
|
|
498
|
+
roughness: .6
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
id: "mat-dark-wood",
|
|
502
|
+
name: "Dark Wood",
|
|
503
|
+
category: "wood",
|
|
504
|
+
baseColor: "#4a3525",
|
|
505
|
+
roughness: .5
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: "mat-glass",
|
|
509
|
+
name: "Glass",
|
|
510
|
+
category: "glass",
|
|
511
|
+
baseColor: "#bcd6e6",
|
|
512
|
+
opacity: .35,
|
|
513
|
+
roughness: .05,
|
|
514
|
+
metalness: .1
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
id: "mat-metal",
|
|
518
|
+
name: "Metal",
|
|
519
|
+
category: "metal",
|
|
520
|
+
baseColor: "#8a8d91",
|
|
521
|
+
metalness: .9,
|
|
522
|
+
roughness: .3
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
id: "mat-tile",
|
|
526
|
+
name: "Ceramic Tile",
|
|
527
|
+
category: "ceramic",
|
|
528
|
+
baseColor: "#d8dcdd",
|
|
529
|
+
roughness: .4
|
|
530
|
+
}
|
|
531
|
+
];
|
|
532
|
+
function createEmptyDocument(options = {}) {
|
|
533
|
+
const buildingId = createId("bld");
|
|
534
|
+
return {
|
|
535
|
+
id: createId("doc"),
|
|
536
|
+
version: MODEL_VERSION,
|
|
537
|
+
name: options.name ?? "Untitled",
|
|
538
|
+
units: options.units ?? "metric",
|
|
539
|
+
buildings: [{
|
|
540
|
+
id: buildingId,
|
|
541
|
+
name: options.buildingName ?? options.name ?? "Building",
|
|
542
|
+
floors: []
|
|
543
|
+
}],
|
|
544
|
+
materials: [...DEFAULT_MATERIALS],
|
|
545
|
+
assets: [],
|
|
546
|
+
metadata: {}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
//#endregion
|
|
550
|
+
//#region src/furniture.ts
|
|
551
|
+
const CATALOG = {
|
|
552
|
+
bed: {
|
|
553
|
+
width: 1,
|
|
554
|
+
depth: 1,
|
|
555
|
+
height: .5
|
|
556
|
+
},
|
|
557
|
+
sofa: {
|
|
558
|
+
width: 2,
|
|
559
|
+
depth: .9,
|
|
560
|
+
height: .75
|
|
561
|
+
},
|
|
562
|
+
table: {
|
|
563
|
+
width: 1.2,
|
|
564
|
+
depth: .8,
|
|
565
|
+
height: .74
|
|
566
|
+
},
|
|
567
|
+
desk: {
|
|
568
|
+
width: 1.2,
|
|
569
|
+
depth: .6,
|
|
570
|
+
height: .74
|
|
571
|
+
},
|
|
572
|
+
chair: {
|
|
573
|
+
width: .5,
|
|
574
|
+
depth: .5,
|
|
575
|
+
height: .9
|
|
576
|
+
},
|
|
577
|
+
sink: {
|
|
578
|
+
width: .5,
|
|
579
|
+
depth: .42,
|
|
580
|
+
height: .85
|
|
581
|
+
},
|
|
582
|
+
toilet: {
|
|
583
|
+
width: .4,
|
|
584
|
+
depth: .6,
|
|
585
|
+
height: .78
|
|
586
|
+
},
|
|
587
|
+
shower: {
|
|
588
|
+
width: .9,
|
|
589
|
+
depth: .9,
|
|
590
|
+
height: .12
|
|
591
|
+
},
|
|
592
|
+
"kitchen counter": {
|
|
593
|
+
width: 1,
|
|
594
|
+
depth: .6,
|
|
595
|
+
height: .9
|
|
596
|
+
},
|
|
597
|
+
wardrobe: {
|
|
598
|
+
width: 1.2,
|
|
599
|
+
depth: .6,
|
|
600
|
+
height: 2
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
const DEFAULT_DIMS = {
|
|
604
|
+
width: .6,
|
|
605
|
+
depth: .6,
|
|
606
|
+
height: .6
|
|
607
|
+
};
|
|
608
|
+
const FURNITURE_TYPES = Object.keys(CATALOG);
|
|
609
|
+
/** Resolve the footprint of a furniture type, applying its scale. */
|
|
610
|
+
function furnitureDims(type, scale = [
|
|
611
|
+
1,
|
|
612
|
+
1,
|
|
613
|
+
1
|
|
614
|
+
]) {
|
|
615
|
+
const base = CATALOG[type] ?? DEFAULT_DIMS;
|
|
616
|
+
return {
|
|
617
|
+
width: base.width * scale[0],
|
|
618
|
+
depth: base.depth * scale[1],
|
|
619
|
+
height: base.height * scale[2]
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
/** A subtle per-category colour for the placeholder geometry. */
|
|
623
|
+
function furnitureColor(type) {
|
|
624
|
+
if (type === "shower" || type === "sink" || type === "toilet") return "#9fb6c2";
|
|
625
|
+
if (type.includes("counter") || type === "table" || type === "desk") return "#9c7b54";
|
|
626
|
+
if (type === "bed" || type === "sofa") return "#7c8aa0";
|
|
627
|
+
return "#8a8f99";
|
|
628
|
+
}
|
|
629
|
+
//#endregion
|
|
630
|
+
export { DEFAULT_MATERIALS, FURNITURE_TYPES, History, MODEL_VERSION, allFloors, createEmptyDocument, createFloor, createOpening, createRoom, createWall, deleteFloor, deleteOpening, deleteRoom, deleteWall, deserialize, duplicateFloor, findFloor, findOpening, findRoom, findWall, furnitureColor, furnitureDims, isCompatibleVersion, migrate, moveOpening, moveWall, rectRoomPolygon, reorderFloor, serialize, updateFloor, updateOpening, updateRoom, updateWall };
|
|
631
|
+
|
|
632
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["wl"],"sources":["../src/model.ts","../src/commands.ts","../src/history.ts","../src/serialize.ts","../src/defaults.ts","../src/furniture.ts"],"sourcesContent":["import type { Units, Vec2, Vec3 } from \"@react-arch/shared\";\n\n/** The current on-disk model version. Bump when the schema changes. */\nexport const MODEL_VERSION = \"0.1.0\";\n\nexport type OpeningType = \"door\" | \"window\" | \"opening\";\nexport type SwingDirection = \"in\" | \"out\";\nexport type HingeSide = \"left\" | \"right\";\n\nexport interface Wall {\n id: string;\n floorId: string;\n start: Vec2;\n end: Vec2;\n thickness: number;\n height: number;\n materialId?: string;\n layerId?: string;\n locked?: boolean;\n}\n\nexport interface Opening {\n id: string;\n floorId: string;\n wallId: string;\n type: OpeningType;\n /** Distance along the wall centerline to the opening center. */\n offset: number;\n width: number;\n height: number;\n sillHeight: number;\n swing?: SwingDirection;\n hinge?: HingeSide;\n materialId?: string;\n}\n\nexport interface Room {\n id: string;\n floorId: string;\n name: string;\n /** Explicit polygon (metres). Rooms may also be derived from wall loops. */\n polygon: Vec2[];\n boundaryWallIds?: string[];\n floorMaterialId?: string;\n ceilingMaterialId?: string;\n usageType?: string;\n height?: number;\n}\n\nexport interface BuildingObject {\n id: string;\n floorId: string;\n type: string;\n position: Vec3;\n rotation: Vec3;\n scale: Vec3;\n assetId?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface Floor {\n id: string;\n buildingId: string;\n name: string;\n elevation: number;\n height: number;\n visible: boolean;\n locked: boolean;\n walls: Wall[];\n rooms: Room[];\n openings: Opening[];\n objects: BuildingObject[];\n}\n\nexport interface Building {\n id: string;\n name: string;\n floors: Floor[];\n}\n\nexport type MaterialCategory =\n | \"wall\"\n | \"floor\"\n | \"ceiling\"\n | \"glass\"\n | \"wood\"\n | \"metal\"\n | \"ceramic\"\n | \"custom\";\n\nexport interface Material {\n id: string;\n name: string;\n category: MaterialCategory;\n baseColor: string;\n roughness?: number;\n metalness?: number;\n opacity?: number;\n textureUrl?: string;\n}\n\nexport interface Asset {\n id: string;\n name: string;\n url?: string;\n kind: string;\n}\n\nexport interface Site {\n id: string;\n name: string;\n polygon?: Vec2[];\n}\n\nexport interface BuildingDocument {\n id: string;\n version: string;\n name: string;\n units: Units;\n site?: Site;\n buildings: Building[];\n materials: Material[];\n assets: Asset[];\n metadata: Record<string, unknown>;\n}\n\n/** A single semantic change emitted by a command. */\nexport interface ModelChange {\n kind: \"create\" | \"update\" | \"delete\";\n entity: \"wall\" | \"room\" | \"opening\" | \"floor\" | \"object\" | \"document\";\n id: string;\n}\n\nexport interface ModelWarning {\n code: string;\n message: string;\n entityId?: string;\n}\n\nexport interface CommandResult {\n document: BuildingDocument;\n changes: ModelChange[];\n warnings: ModelWarning[];\n}\n\n// ---------------------------------------------------------------------------\n// Read helpers\n// ---------------------------------------------------------------------------\n\nexport function findFloor(\n doc: BuildingDocument,\n floorId: string,\n): { building: Building; floor: Floor } | undefined {\n for (const building of doc.buildings) {\n const floor = building.floors.find((f) => f.id === floorId);\n if (floor) return { building, floor };\n }\n return undefined;\n}\n\nexport function allFloors(doc: BuildingDocument): Floor[] {\n return doc.buildings.flatMap((b) => b.floors);\n}\n\nexport function findWall(doc: BuildingDocument, wallId: string): Wall | undefined {\n for (const floor of allFloors(doc)) {\n const w = floor.walls.find((x) => x.id === wallId);\n if (w) return w;\n }\n return undefined;\n}\n\nexport function findRoom(doc: BuildingDocument, roomId: string): Room | undefined {\n for (const floor of allFloors(doc)) {\n const r = floor.rooms.find((x) => x.id === roomId);\n if (r) return r;\n }\n return undefined;\n}\n\nexport function findOpening(\n doc: BuildingDocument,\n openingId: string,\n): Opening | undefined {\n for (const floor of allFloors(doc)) {\n const o = floor.openings.find((x) => x.id === openingId);\n if (o) return o;\n }\n return undefined;\n}\n\nexport type EntityKind = \"wall\" | \"room\" | \"opening\" | \"floor\" | \"object\";\n\nexport interface EntityRef {\n kind: EntityKind;\n id: string;\n}\n","import { createId, deepClone, type Vec2 } from \"@react-arch/shared\";\nimport { openingFits, wallLength, type WallLike } from \"@react-arch/geometry\";\nimport {\n type BuildingDocument,\n type CommandResult,\n type Floor,\n type ModelChange,\n type ModelWarning,\n type Opening,\n type OpeningType,\n type Room,\n type Wall,\n findFloor,\n} from \"./model.js\";\n\n/**\n * Commands are pure: they take a document and return a brand-new document plus\n * a list of changes and warnings. They never mutate the input. The studio /\n * editor layers feed these into the history stack for undo/redo.\n */\n\nfunction mutate(\n doc: BuildingDocument,\n fn: (draft: BuildingDocument, warnings: ModelWarning[]) => ModelChange[],\n): CommandResult {\n const draft = deepClone(doc);\n const warnings: ModelWarning[] = [];\n const changes = fn(draft, warnings);\n return { document: draft, changes, warnings };\n}\n\nfunction requireFloor(doc: BuildingDocument, floorId: string): Floor {\n const found = findFloor(doc, floorId);\n if (!found) throw new Error(`Floor not found: ${floorId}`);\n return found.floor;\n}\n\n// --- Walls -----------------------------------------------------------------\n\nexport interface CreateWallInput {\n floorId: string;\n start: Vec2;\n end: Vec2;\n thickness?: number;\n height?: number;\n materialId?: string;\n id?: string;\n}\n\nexport function createWall(\n doc: BuildingDocument,\n input: CreateWallInput,\n): CommandResult {\n return mutate(doc, (draft) => {\n const floor = requireFloor(draft, input.floorId);\n const wall: Wall = {\n id: input.id ?? createId(\"wall\"),\n floorId: input.floorId,\n start: input.start,\n end: input.end,\n thickness: input.thickness ?? 0.2,\n height: input.height ?? floor.height,\n materialId: input.materialId,\n };\n floor.walls.push(wall);\n return [{ kind: \"create\", entity: \"wall\", id: wall.id }];\n });\n}\n\nexport function updateWall(\n doc: BuildingDocument,\n wallId: string,\n patch: Partial<Omit<Wall, \"id\" | \"floorId\">>,\n): CommandResult {\n return mutate(doc, (draft, warnings) => {\n for (const floor of allFloorsOf(draft)) {\n const wall = floor.walls.find((w) => w.id === wallId);\n if (!wall) continue;\n Object.assign(wall, patch);\n reattachOpenings(floor, wall, warnings);\n return [{ kind: \"update\", entity: \"wall\", id: wallId }];\n }\n warnings.push({ code: \"wall-missing\", message: `Wall ${wallId} not found` });\n return [];\n });\n}\n\n/** Move a wall's endpoints, keeping attached openings within bounds. */\nexport function moveWall(\n doc: BuildingDocument,\n wallId: string,\n start: Vec2,\n end: Vec2,\n): CommandResult {\n return updateWall(doc, wallId, { start, end });\n}\n\nexport function deleteWall(doc: BuildingDocument, wallId: string): CommandResult {\n return mutate(doc, (draft) => {\n const changes: ModelChange[] = [];\n for (const floor of allFloorsOf(draft)) {\n const idx = floor.walls.findIndex((w) => w.id === wallId);\n if (idx === -1) continue;\n floor.walls.splice(idx, 1);\n changes.push({ kind: \"delete\", entity: \"wall\", id: wallId });\n // Cascade: drop openings attached to the wall.\n floor.openings = floor.openings.filter((o) => {\n if (o.wallId === wallId) {\n changes.push({ kind: \"delete\", entity: \"opening\", id: o.id });\n return false;\n }\n return true;\n });\n break;\n }\n return changes;\n });\n}\n\n// --- Rooms -----------------------------------------------------------------\n\nexport interface CreateRoomInput {\n floorId: string;\n name: string;\n polygon: Vec2[];\n id?: string;\n floorMaterialId?: string;\n ceilingMaterialId?: string;\n usageType?: string;\n}\n\n/** Convenience: build a rectangular room from origin + size. */\nexport function rectRoomPolygon(\n x: number,\n y: number,\n width: number,\n depth: number,\n): Vec2[] {\n return [\n [x, y],\n [x + width, y],\n [x + width, y + depth],\n [x, y + depth],\n ];\n}\n\nexport function createRoom(\n doc: BuildingDocument,\n input: CreateRoomInput,\n): CommandResult {\n return mutate(doc, (draft) => {\n const floor = requireFloor(draft, input.floorId);\n const room: Room = {\n id: input.id ?? createId(\"room\"),\n floorId: input.floorId,\n name: input.name,\n polygon: input.polygon,\n floorMaterialId: input.floorMaterialId,\n ceilingMaterialId: input.ceilingMaterialId,\n usageType: input.usageType,\n };\n floor.rooms.push(room);\n return [{ kind: \"create\", entity: \"room\", id: room.id }];\n });\n}\n\nexport function updateRoom(\n doc: BuildingDocument,\n roomId: string,\n patch: Partial<Omit<Room, \"id\" | \"floorId\">>,\n): CommandResult {\n return mutate(doc, (draft, warnings) => {\n for (const floor of allFloorsOf(draft)) {\n const room = floor.rooms.find((r) => r.id === roomId);\n if (!room) continue;\n Object.assign(room, patch);\n return [{ kind: \"update\", entity: \"room\", id: roomId }];\n }\n warnings.push({ code: \"room-missing\", message: `Room ${roomId} not found` });\n return [];\n });\n}\n\nexport function deleteRoom(doc: BuildingDocument, roomId: string): CommandResult {\n return mutate(doc, (draft) => {\n for (const floor of allFloorsOf(draft)) {\n const idx = floor.rooms.findIndex((r) => r.id === roomId);\n if (idx === -1) continue;\n floor.rooms.splice(idx, 1);\n return [{ kind: \"delete\", entity: \"room\", id: roomId }];\n }\n return [];\n });\n}\n\n// --- Openings --------------------------------------------------------------\n\nexport interface CreateOpeningInput {\n wallId: string;\n type: OpeningType;\n offset: number;\n width: number;\n height: number;\n sillHeight?: number;\n id?: string;\n}\n\nexport function createOpening(\n doc: BuildingDocument,\n input: CreateOpeningInput,\n): CommandResult {\n return mutate(doc, (draft, warnings) => {\n for (const floor of allFloorsOf(draft)) {\n const wall = floor.walls.find((w) => w.id === input.wallId);\n if (!wall) continue;\n const opening: Opening = {\n id: input.id ?? createId(\"opening\"),\n floorId: floor.id,\n wallId: input.wallId,\n type: input.type,\n offset: input.offset,\n width: input.width,\n height: input.height,\n sillHeight: input.sillHeight ?? (input.type === \"window\" ? 0.9 : 0),\n };\n if (!openingFits(wall, opening)) {\n warnings.push({\n code: \"opening-overflow\",\n message: `Opening ${opening.id} does not fit within wall ${wall.id}`,\n entityId: opening.id,\n });\n }\n floor.openings.push(opening);\n return [{ kind: \"create\", entity: \"opening\", id: opening.id }];\n }\n warnings.push({\n code: \"wall-missing\",\n message: `Wall ${input.wallId} not found for opening`,\n });\n return [];\n });\n}\n\nexport function updateOpening(\n doc: BuildingDocument,\n openingId: string,\n patch: Partial<Omit<Opening, \"id\" | \"floorId\" | \"wallId\">>,\n): CommandResult {\n return mutate(doc, (draft, warnings) => {\n for (const floor of allFloorsOf(draft)) {\n const opening = floor.openings.find((o) => o.id === openingId);\n if (!opening) continue;\n Object.assign(opening, patch);\n const wall = floor.walls.find((w) => w.id === opening.wallId);\n if (wall && !openingFits(wall, opening)) {\n warnings.push({\n code: \"opening-overflow\",\n message: `Opening ${opening.id} no longer fits its wall`,\n entityId: opening.id,\n });\n }\n return [{ kind: \"update\", entity: \"opening\", id: openingId }];\n }\n return [];\n });\n}\n\nexport function moveOpening(\n doc: BuildingDocument,\n openingId: string,\n offset: number,\n): CommandResult {\n return updateOpening(doc, openingId, { offset });\n}\n\nexport function deleteOpening(\n doc: BuildingDocument,\n openingId: string,\n): CommandResult {\n return mutate(doc, (draft) => {\n for (const floor of allFloorsOf(draft)) {\n const idx = floor.openings.findIndex((o) => o.id === openingId);\n if (idx === -1) continue;\n floor.openings.splice(idx, 1);\n return [{ kind: \"delete\", entity: \"opening\", id: openingId }];\n }\n return [];\n });\n}\n\n// --- Floors ----------------------------------------------------------------\n\nexport interface CreateFloorInput {\n buildingId: string;\n name: string;\n elevation: number;\n height?: number;\n id?: string;\n}\n\nexport function createFloor(\n doc: BuildingDocument,\n input: CreateFloorInput,\n): CommandResult {\n return mutate(doc, (draft, warnings) => {\n const building = draft.buildings.find((b) => b.id === input.buildingId);\n if (!building) {\n warnings.push({ code: \"building-missing\", message: \"Building not found\" });\n return [];\n }\n const floor: Floor = {\n id: input.id ?? createId(\"floor\"),\n buildingId: building.id,\n name: input.name,\n elevation: input.elevation,\n height: input.height ?? 2.8,\n visible: true,\n locked: false,\n walls: [],\n rooms: [],\n openings: [],\n objects: [],\n };\n building.floors.push(floor);\n building.floors.sort((a, b) => a.elevation - b.elevation);\n return [{ kind: \"create\", entity: \"floor\", id: floor.id }];\n });\n}\n\nexport function duplicateFloor(\n doc: BuildingDocument,\n floorId: string,\n): CommandResult {\n return mutate(doc, (draft) => {\n for (const building of draft.buildings) {\n const floor = building.floors.find((f) => f.id === floorId);\n if (!floor) continue;\n const idMap = new Map<string, string>();\n const copy: Floor = deepClone(floor);\n copy.id = createId(\"floor\");\n copy.name = `${floor.name} (copy)`;\n copy.elevation = floor.elevation + floor.height;\n // Re-id child entities and keep opening→wall references consistent.\n copy.walls.forEach((w) => {\n const nid = createId(\"wall\");\n idMap.set(w.id, nid);\n w.id = nid;\n w.floorId = copy.id;\n });\n copy.openings.forEach((o) => {\n o.id = createId(\"opening\");\n o.floorId = copy.id;\n o.wallId = idMap.get(o.wallId) ?? o.wallId;\n });\n copy.rooms.forEach((r) => {\n r.id = createId(\"room\");\n r.floorId = copy.id;\n });\n copy.objects.forEach((ob) => {\n ob.id = createId(\"object\");\n ob.floorId = copy.id;\n });\n building.floors.push(copy);\n building.floors.sort((a, b) => a.elevation - b.elevation);\n return [{ kind: \"create\", entity: \"floor\", id: copy.id }];\n }\n return [];\n });\n}\n\nexport function reorderFloor(\n doc: BuildingDocument,\n floorId: string,\n newElevation: number,\n): CommandResult {\n return mutate(doc, (draft) => {\n const floor = requireFloor(draft, floorId);\n floor.elevation = newElevation;\n for (const building of draft.buildings) {\n building.floors.sort((a, b) => a.elevation - b.elevation);\n }\n return [{ kind: \"update\", entity: \"floor\", id: floorId }];\n });\n}\n\nexport function updateFloor(\n doc: BuildingDocument,\n floorId: string,\n patch: Partial<Pick<Floor, \"name\" | \"elevation\" | \"height\" | \"visible\" | \"locked\">>,\n): CommandResult {\n return mutate(doc, (draft) => {\n const floor = requireFloor(draft, floorId);\n Object.assign(floor, patch);\n return [{ kind: \"update\", entity: \"floor\", id: floorId }];\n });\n}\n\nexport function deleteFloor(doc: BuildingDocument, floorId: string): CommandResult {\n return mutate(doc, (draft) => {\n for (const building of draft.buildings) {\n const idx = building.floors.findIndex((f) => f.id === floorId);\n if (idx === -1) continue;\n building.floors.splice(idx, 1);\n return [{ kind: \"delete\", entity: \"floor\", id: floorId }];\n }\n return [];\n });\n}\n\n// --- internals -------------------------------------------------------------\n\nfunction allFloorsOf(doc: BuildingDocument): Floor[] {\n return doc.buildings.flatMap((b) => b.floors);\n}\n\n/** Clamp openings so they stay attached after a wall changes length. */\nfunction reattachOpenings(floor: Floor, wall: Wall, warnings: ModelWarning[]): void {\n const wl: WallLike = wall;\n const len = wallLength(wl);\n for (const opening of floor.openings) {\n if (opening.wallId !== wall.id) continue;\n const half = opening.width / 2;\n const clamped = Math.min(Math.max(opening.offset, half), Math.max(half, len - half));\n if (clamped !== opening.offset) {\n opening.offset = clamped;\n warnings.push({\n code: \"opening-reclamped\",\n message: `Opening ${opening.id} re-positioned to stay on wall`,\n entityId: opening.id,\n });\n }\n }\n}\n","import { createId } from \"@react-arch/shared\";\nimport type { BuildingDocument } from \"./model.js\";\n\nexport interface HistoryEntry {\n id: string;\n label: string;\n before: BuildingDocument;\n after: BuildingDocument;\n}\n\n/**\n * Command-based undo/redo. Keeps full document snapshots per entry which is\n * simple and correct for the MVP; this can later move to patches / op logs\n * without changing the public surface.\n */\nexport class History {\n private past: HistoryEntry[] = [];\n private future: HistoryEntry[] = [];\n\n constructor(private limit = 200) {}\n\n push(label: string, before: BuildingDocument, after: BuildingDocument): void {\n this.past.push({ id: createId(\"hist\"), label, before, after });\n if (this.past.length > this.limit) this.past.shift();\n this.future = [];\n }\n\n canUndo(): boolean {\n return this.past.length > 0;\n }\n\n canRedo(): boolean {\n return this.future.length > 0;\n }\n\n undo(): BuildingDocument | null {\n const entry = this.past.pop();\n if (!entry) return null;\n this.future.push(entry);\n return entry.before;\n }\n\n redo(): BuildingDocument | null {\n const entry = this.future.pop();\n if (!entry) return null;\n this.past.push(entry);\n return entry.after;\n }\n\n get undoLabel(): string | null {\n return this.past.at(-1)?.label ?? null;\n }\n\n get redoLabel(): string | null {\n return this.future.at(-1)?.label ?? null;\n }\n\n clear(): void {\n this.past = [];\n this.future = [];\n }\n}\n","import { MODEL_VERSION, type BuildingDocument } from \"./model.js\";\n\n/**\n * Migration registry. Each migration upgrades a document one version forward.\n * Add new entries as the schema evolves; `migrate` chains them in order.\n */\ntype Migration = (doc: Record<string, unknown>) => Record<string, unknown>;\n\nconst migrations: Record<string, Migration> = {\n // Example placeholder for the first real migration:\n // \"0.0.1\": (doc) => ({ ...doc, version: \"0.1.0\", assets: doc.assets ?? [] }),\n};\n\nexport function migrate(input: Record<string, unknown>): Record<string, unknown> {\n let doc = { ...input };\n let guard = 0;\n while (doc.version !== MODEL_VERSION && guard < 50) {\n const m = migrations[doc.version as string];\n if (!m) break; // No path forward; validation will report the mismatch.\n doc = m(doc);\n guard += 1;\n }\n return doc;\n}\n\nexport function serialize(doc: BuildingDocument): string {\n return JSON.stringify(doc, null, 2);\n}\n\nexport function deserialize(json: string): BuildingDocument {\n const raw = JSON.parse(json) as Record<string, unknown>;\n return migrate(raw) as unknown as BuildingDocument;\n}\n\nexport function isCompatibleVersion(version: string): boolean {\n // Same major.minor is considered compatible for the MVP.\n const [a, b] = version.split(\".\");\n const [ca, cb] = MODEL_VERSION.split(\".\");\n return a === ca && b === cb;\n}\n","import { createId } from \"@react-arch/shared\";\nimport type { BuildingDocument, Material } from \"./model.js\";\nimport { MODEL_VERSION } from \"./model.js\";\n\nexport const DEFAULT_MATERIALS: Material[] = [\n { id: \"mat-plaster\", name: \"White Plaster\", category: \"wall\", baseColor: \"#e8e6e1\", roughness: 0.9 },\n { id: \"mat-concrete\", name: \"Concrete\", category: \"wall\", baseColor: \"#9b9b97\", roughness: 0.8 },\n { id: \"mat-oak\", name: \"Oak Floor\", category: \"floor\", baseColor: \"#b88a52\", roughness: 0.6 },\n { id: \"mat-dark-wood\", name: \"Dark Wood\", category: \"wood\", baseColor: \"#4a3525\", roughness: 0.5 },\n { id: \"mat-glass\", name: \"Glass\", category: \"glass\", baseColor: \"#bcd6e6\", opacity: 0.35, roughness: 0.05, metalness: 0.1 },\n { id: \"mat-metal\", name: \"Metal\", category: \"metal\", baseColor: \"#8a8d91\", metalness: 0.9, roughness: 0.3 },\n { id: \"mat-tile\", name: \"Ceramic Tile\", category: \"ceramic\", baseColor: \"#d8dcdd\", roughness: 0.4 },\n];\n\nexport interface EmptyDocumentOptions {\n name?: string;\n units?: \"metric\" | \"imperial\";\n buildingName?: string;\n}\n\nexport function createEmptyDocument(\n options: EmptyDocumentOptions = {},\n): BuildingDocument {\n const buildingId = createId(\"bld\");\n return {\n id: createId(\"doc\"),\n version: MODEL_VERSION,\n name: options.name ?? \"Untitled\",\n units: options.units ?? \"metric\",\n buildings: [\n {\n id: buildingId,\n name: options.buildingName ?? options.name ?? \"Building\",\n floors: [],\n },\n ],\n materials: [...DEFAULT_MATERIALS],\n assets: [],\n metadata: {},\n };\n}\n","import type { Vec3 } from \"@react-arch/shared\";\n\n/**\n * Placeholder furniture geometry. Each entry is the base footprint (metres) at\n * scale 1: `w` along plan X, `d` along plan Y, `h` vertical. A building object's\n * `scale` multiplies these. Intentionally simple boxes for the MVP — no\n * photorealistic assets.\n */\nexport interface FurnitureDims {\n width: number;\n depth: number;\n height: number;\n}\n\nconst CATALOG: Record<string, FurnitureDims> = {\n bed: { width: 1.0, depth: 1.0, height: 0.5 },\n sofa: { width: 2.0, depth: 0.9, height: 0.75 },\n table: { width: 1.2, depth: 0.8, height: 0.74 },\n desk: { width: 1.2, depth: 0.6, height: 0.74 },\n chair: { width: 0.5, depth: 0.5, height: 0.9 },\n sink: { width: 0.5, depth: 0.42, height: 0.85 },\n toilet: { width: 0.4, depth: 0.6, height: 0.78 },\n shower: { width: 0.9, depth: 0.9, height: 0.12 },\n \"kitchen counter\": { width: 1.0, depth: 0.6, height: 0.9 },\n wardrobe: { width: 1.2, depth: 0.6, height: 2.0 },\n};\n\nconst DEFAULT_DIMS: FurnitureDims = { width: 0.6, depth: 0.6, height: 0.6 };\n\nexport const FURNITURE_TYPES = Object.keys(CATALOG);\n\n/** Resolve the footprint of a furniture type, applying its scale. */\nexport function furnitureDims(type: string, scale: Vec3 = [1, 1, 1]): FurnitureDims {\n const base = CATALOG[type] ?? DEFAULT_DIMS;\n return {\n width: base.width * scale[0],\n depth: base.depth * scale[1],\n height: base.height * scale[2],\n };\n}\n\n/** A subtle per-category colour for the placeholder geometry. */\nexport function furnitureColor(type: string): string {\n if (type === \"shower\" || type === \"sink\" || type === \"toilet\") return \"#9fb6c2\";\n if (type.includes(\"counter\") || type === \"table\" || type === \"desk\") return \"#9c7b54\";\n if (type === \"bed\" || type === \"sofa\") return \"#7c8aa0\";\n return \"#8a8f99\";\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@react-arch/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@react-arch/shared": "0.1.0",
|
|
16
|
+
"@react-arch/geometry": "0.1.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5.7.2",
|
|
20
|
+
"vitest": "^2.1.8"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/react-arch/react-arch.git",
|
|
31
|
+
"directory": "packages/core"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run --passWithNoTests",
|
|
37
|
+
"lint": "oxlint src",
|
|
38
|
+
"build": "tsdown"
|
|
39
|
+
}
|
|
40
|
+
}
|