@hatiolab/figure-model 0.4.2 → 0.4.3
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/dist/authoring-actions.d.ts +12 -3
- package/dist/authoring-actions.d.ts.map +1 -1
- package/dist/authoring-actions.js +45 -20
- package/dist/authoring-actions.js.map +1 -1
- package/dist/capabilities.js +37 -20
- package/dist/capabilities.js.map +1 -1
- package/dist/kernel-version.d.ts +1 -1
- package/dist/kernel-version.js +1 -1
- package/dist/proposal.js +1 -1
- package/dist/proposal.js.map +1 -1
- package/dist/release-contract.d.ts +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +52 -3
- package/dist/release-contract.js.map +1 -1
- package/dist/slot-layout.d.ts +83 -38
- package/dist/slot-layout.d.ts.map +1 -1
- package/dist/slot-layout.js +198 -51
- package/dist/slot-layout.js.map +1 -1
- package/dist/slot-places.d.ts +20 -10
- package/dist/slot-places.d.ts.map +1 -1
- package/dist/slot-places.js +15 -10
- package/dist/slot-places.js.map +1 -1
- package/package.json +1 -1
package/dist/slot-layout.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Vec3 } from './words.ts';
|
|
2
|
+
export type LayoutIssueCode = 'TIERS_REQUIRED' | 'MOUNT_INVALID' | 'ANCHOR_REQUIRED'
|
|
3
|
+
/** The tier names an anchor the holder does not give (params.anchorId). Checked only when the anchors are known. */
|
|
4
|
+
| 'ANCHOR_UNKNOWN' | 'TIER_NUMBER_DUPLICATE' | 'FINITE_NUMBER_REQUIRED' | 'POSITIVE_NUMBER_REQUIRED' | 'POSITIVE_INTEGER_REQUIRED' | 'ARRAY_REQUIRED' | 'OBJECT_REQUIRED' | 'AXIS_INVALID' | 'PATTERN_INVALID' | 'SLOTS_REQUIRED' | 'SLOT_NUMBER_DUPLICATE'
|
|
2
5
|
/** A value JSON would turn into null (params.value: NaN, Infinity, -Infinity). */
|
|
3
6
|
| 'NOT_PERSISTABLE';
|
|
4
7
|
/** A problem as a code, the path of the value, and parameters -- never a sentence. The screen words it. */
|
|
@@ -9,18 +12,26 @@ export interface LayoutIssue {
|
|
|
9
12
|
params?: Record<string, string | number>;
|
|
10
13
|
}
|
|
11
14
|
export declare function layoutIssue(code: LayoutIssueCode, path: string, params?: Record<string, string | number>): LayoutIssue;
|
|
12
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* How a tier carries what it holds. ON and IN seat items along the tier's local +Y -- IN differs only in that the room
|
|
17
|
+
* is enclosed (a judgement about room, not a turned direction); HANG hangs them along -Y.
|
|
18
|
+
*/
|
|
13
19
|
export type SlotMount = 'ON' | 'IN' | 'HANG';
|
|
20
|
+
/** The anchor at the holder's own origin, for a holder that gives no other. */
|
|
21
|
+
export declare const HOLDER_ANCHOR = "holder";
|
|
14
22
|
export interface SlotLayout {
|
|
15
|
-
mount: SlotMount;
|
|
16
|
-
/** In tierNo order, bottom to top. */
|
|
17
23
|
tiers: SlotLayoutTier[];
|
|
18
24
|
}
|
|
19
25
|
export interface SlotLayoutTier {
|
|
20
|
-
/**
|
|
26
|
+
/** The holder's own number for the tier: a positive integer, unique, kept when tiers are removed or reordered. */
|
|
21
27
|
tierNo: number;
|
|
22
|
-
/** The
|
|
23
|
-
|
|
28
|
+
/** The anchor the tier stands on. */
|
|
29
|
+
anchorId: string;
|
|
30
|
+
mount: SlotMount;
|
|
31
|
+
/** The tier's centre in its anchor's frame. */
|
|
32
|
+
position: Vec3;
|
|
33
|
+
/** Degrees about the anchor's Y. */
|
|
34
|
+
rotation: number;
|
|
24
35
|
layout: SlotPattern;
|
|
25
36
|
}
|
|
26
37
|
/** How the slots of one tier are arranged. */
|
|
@@ -53,16 +64,16 @@ export type SlotPattern = {
|
|
|
53
64
|
pattern: 'CUSTOM';
|
|
54
65
|
slots: PatternSlot[];
|
|
55
66
|
};
|
|
56
|
-
/** The order GRID slots are numbered in. ROW_MAJOR by default. */
|
|
67
|
+
/** The order GRID slots are numbered in. ROW_MAJOR by default. Changing it changes what an address means. */
|
|
57
68
|
export type GridOrder = 'ROW_MAJOR' | 'COL_MAJOR' | 'SERPENTINE_ROW' | 'SERPENTINE_COL';
|
|
58
|
-
/** The corner GRID slot 1 is at. { x: 'MIN', z: 'MIN' } by default. */
|
|
69
|
+
/** The corner GRID slot 1 is at. { x: 'MIN', z: 'MIN' } by default. Changing it changes what an address means. */
|
|
59
70
|
export interface GridOrigin {
|
|
60
71
|
x: 'MIN' | 'MAX';
|
|
61
72
|
z: 'MIN' | 'MAX';
|
|
62
73
|
}
|
|
63
74
|
/** One slot's room and rules. Every slot of a SINGLE, ROW or GRID tier has the same. */
|
|
64
75
|
export interface SlotSpec {
|
|
65
|
-
/** The room a held thing may take: width along X, depth along Z, height along Y. */
|
|
76
|
+
/** The room a held thing may take, in the tier's axes: width along X, depth along Z, height along Y. */
|
|
66
77
|
allowed: {
|
|
67
78
|
width: number;
|
|
68
79
|
depth: number;
|
|
@@ -80,7 +91,7 @@ export interface SlotSpec {
|
|
|
80
91
|
}
|
|
81
92
|
/** A slot of a CUSTOM tier. */
|
|
82
93
|
export interface PatternSlot extends SlotSpec {
|
|
83
|
-
/**
|
|
94
|
+
/** The tier's own number for the slot: a positive integer, unique in the tier, kept when other slots are removed. */
|
|
84
95
|
slotNo: number;
|
|
85
96
|
/** For display (FRONT, REAR, L, R …). */
|
|
86
97
|
name?: string;
|
|
@@ -94,12 +105,18 @@ export interface SlotAddress {
|
|
|
94
105
|
tier: number;
|
|
95
106
|
slot: number;
|
|
96
107
|
}
|
|
97
|
-
/** A slot found by its address
|
|
108
|
+
/** A slot found by its address, with its tier's placement on the anchor. */
|
|
98
109
|
export interface ResolvedSlot {
|
|
99
110
|
tier: number;
|
|
100
111
|
slotNo: number;
|
|
101
112
|
name?: string;
|
|
102
|
-
|
|
113
|
+
anchorId: string;
|
|
114
|
+
mount: SlotMount;
|
|
115
|
+
/** The tier's centre in the anchor's frame. */
|
|
116
|
+
tierPosition: Vec3;
|
|
117
|
+
/** The tier's turn about the anchor's Y, degrees. */
|
|
118
|
+
tierRotation: number;
|
|
119
|
+
/** The slot's centre from the tier's centre, in the tier's axes. */
|
|
103
120
|
offset: {
|
|
104
121
|
x: number;
|
|
105
122
|
z: number;
|
|
@@ -111,16 +128,23 @@ export interface ResolvedSlot {
|
|
|
111
128
|
};
|
|
112
129
|
accepts?: string[];
|
|
113
130
|
stackLimit: number;
|
|
131
|
+
/** The slot's turn about Y in the tier, degrees. */
|
|
114
132
|
rotation: number;
|
|
115
133
|
}
|
|
134
|
+
export interface ValidateSlotLayoutOptions {
|
|
135
|
+
/** The anchors the holder gives. Left out: a tier's anchor is not checked against them. */
|
|
136
|
+
anchors?: Iterable<string>;
|
|
137
|
+
}
|
|
116
138
|
/**
|
|
117
139
|
* Everything wrong with a layout; an empty list is a sound one. stackLimit may be Infinity (no limit); a layout that is
|
|
118
140
|
* to be stored as JSON is also checked with `persistenceIssues`.
|
|
119
141
|
*/
|
|
120
|
-
export declare function validateSlotLayout(layout: SlotLayout): LayoutIssue[];
|
|
142
|
+
export declare function validateSlotLayout(layout: SlotLayout, options?: ValidateSlotLayoutOptions): LayoutIssue[];
|
|
121
143
|
/** Values JSON turns into null (NaN, Infinity): stored, they would come back as a broken layout. */
|
|
122
144
|
export declare function persistenceIssues(value: unknown, path?: string): LayoutIssue[];
|
|
123
145
|
export declare function slotCount(pattern: SlotPattern): number;
|
|
146
|
+
/** A tier's slot numbers, in order. ROW and GRID count from 1; CUSTOM has the numbers it was given. */
|
|
147
|
+
export declare function slotNumbers(pattern: SlotPattern): number[];
|
|
124
148
|
/** A GRID slot's number from its row r and column c (from 1), counted from the origin corner. */
|
|
125
149
|
export declare function gridSlotNo(rows: number, cols: number, r: number, c: number, order?: GridOrder): number;
|
|
126
150
|
/** The row and column of a GRID slot number -- the inverse of gridSlotNo. */
|
|
@@ -143,21 +167,24 @@ export declare function resolveSlot(layout: SlotLayout, address: SlotAddress, op
|
|
|
143
167
|
export declare function slotIdOf(address: SlotAddress): string;
|
|
144
168
|
/** The address a slot id names, or null when it is not one. */
|
|
145
169
|
export declare function addressOfSlotId(slotId: unknown): SlotAddress | null;
|
|
146
|
-
/** Every address of a layout, tier by tier. For a big GRID, resolve only the addresses you need. */
|
|
170
|
+
/** Every address of a layout, tier by tier as written. For a big GRID, resolve only the addresses you need. */
|
|
147
171
|
export declare function slotAddresses(layout: SlotLayout): Generator<SlotAddress>;
|
|
172
|
+
/** A place in an anchor's frame: a position and a turn about the anchor's Y in degrees. */
|
|
173
|
+
export interface AnchorFrame {
|
|
174
|
+
position: Vec3;
|
|
175
|
+
rotation: number;
|
|
176
|
+
}
|
|
177
|
+
/** A slot's frame in its anchor's: the tier's placement, then the slot's offset and turn in the tier. */
|
|
178
|
+
export declare function slotFrame(slot: ResolvedSlot): AnchorFrame;
|
|
148
179
|
/** An item's size: width along X, depth along Z, height along Y -- in the layout's unit. */
|
|
149
180
|
export interface ItemDims {
|
|
150
181
|
width: number;
|
|
151
182
|
depth: number;
|
|
152
183
|
height: number;
|
|
153
184
|
}
|
|
154
|
-
/** Where an item sits
|
|
185
|
+
/** Where an item sits: its centre, its turn about Y in degrees, and one uniform scale. */
|
|
155
186
|
export interface SeatTransform {
|
|
156
|
-
position:
|
|
157
|
-
x: number;
|
|
158
|
-
y: number;
|
|
159
|
-
z: number;
|
|
160
|
-
};
|
|
187
|
+
position: Vec3;
|
|
161
188
|
rotation: number;
|
|
162
189
|
scale: number;
|
|
163
190
|
}
|
|
@@ -168,26 +195,44 @@ export declare function rotatedFootprint(dims: ItemDims, rotation: number): {
|
|
|
168
195
|
depth: number;
|
|
169
196
|
};
|
|
170
197
|
/**
|
|
171
|
-
* Where an item sits in
|
|
172
|
-
* of `belowHeight` -- the shown heights (height × scale) of what is stacked under it
|
|
173
|
-
*
|
|
174
|
-
*
|
|
198
|
+
* Where an item sits in its slot's own frame: shrunk to fit the slot's width and depth (by its turned extent, never
|
|
199
|
+
* enlarged), on top of `belowHeight` -- the shown heights (height × scale) of what is stacked under it -- along +Y for ON
|
|
200
|
+
* and IN, -Y for HANG. x and z are 0 and it is not turned: the slot's frame has the offset and the turn. `margin` is room
|
|
201
|
+
* left only for showing (0 ≤ margin < 1); whether it fits is judged elsewhere, at full size. Null when a size or value is
|
|
202
|
+
* unusable -- never NaN or Infinity.
|
|
203
|
+
* slotFrame(slot) then seatInSlot(...) = seatTransform(...)
|
|
175
204
|
*/
|
|
176
|
-
export declare function
|
|
177
|
-
/** Where
|
|
178
|
-
export declare function
|
|
179
|
-
/**
|
|
180
|
-
export declare function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
205
|
+
export declare function seatInSlot(dims: ItemDims, slot: ResolvedSlot, belowHeight?: number, margin?: number): SeatTransform | null;
|
|
206
|
+
/** Where an item sits in its anchor's frame: the slot's frame, then the seat in it. */
|
|
207
|
+
export declare function seatTransform(dims: ItemDims, slot: ResolvedSlot, belowHeight?: number, margin?: number): SeatTransform | null;
|
|
208
|
+
/** Where each of a slot's stacked items sits in the anchor's frame, bottom first. An item of unknown size is null and adds no height. */
|
|
209
|
+
export declare function stackSeatTransforms(stack: (ItemDims | null | undefined)[], slot: ResolvedSlot, margin?: number): (SeatTransform | null)[];
|
|
210
|
+
export type SlotSpaceCode =
|
|
211
|
+
/** Two slots' rooms share space (params: tier, slot, otherTier, otherSlot). Touching is not overlapping. */
|
|
212
|
+
'SLOT_OVERLAP'
|
|
213
|
+
/** A slot's footprint reaches past the face its anchor gives (params: tier, slot). */
|
|
214
|
+
| 'SLOT_OUTSIDE_ANCHOR'
|
|
215
|
+
/** The anchor gives no face size, so whether its slots stay on it is unknown (params: anchorId). */
|
|
216
|
+
| 'ANCHOR_SPACE_UNKNOWN';
|
|
217
|
+
/** A warning about a layout's room: a setting to look at, never a judgement that items can physically be put there. */
|
|
218
|
+
export interface SlotSpaceWarning {
|
|
219
|
+
code: SlotSpaceCode;
|
|
220
|
+
path: string;
|
|
221
|
+
params: Record<string, string | number>;
|
|
222
|
+
}
|
|
223
|
+
export interface SlotSpaceOptions {
|
|
224
|
+
/** The size of the face each anchor gives, centred on the anchor, in its X and Z. An anchor left out is UNKNOWN. */
|
|
225
|
+
anchorFaces?: Record<string, {
|
|
226
|
+
width: number;
|
|
227
|
+
depth: number;
|
|
228
|
+
}>;
|
|
229
|
+
}
|
|
185
230
|
/**
|
|
186
|
-
* Where
|
|
187
|
-
*
|
|
188
|
-
*
|
|
231
|
+
* Where a sound layout's slots crowd each other or leave their anchor's face, as warnings. Only slots on the same anchor
|
|
232
|
+
* are compared: anchors move apart. Rooms are each slot's allowed box, placed by its tier and turned; sharing a face or
|
|
233
|
+
* an edge is not overlapping.
|
|
189
234
|
*/
|
|
190
|
-
export declare function
|
|
235
|
+
export declare function slotSpaceWarnings(layout: SlotLayout, options?: SlotSpaceOptions): SlotSpaceWarning[];
|
|
191
236
|
export type FitVerdict = 'OK' | 'NG' | 'UNKNOWN';
|
|
192
237
|
export type FitReason = 'SLOT_NOT_FOUND' | 'TYPE_NOT_ACCEPTED' | 'WIDTH_EXCEEDED' | 'DEPTH_EXCEEDED' | 'HEIGHT_EXCEEDED' | 'STACK_LIMIT_EXCEEDED' | 'SPEC_UNKNOWN';
|
|
193
238
|
export interface FitCandidate {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slot-layout.d.ts","sourceRoot":"","sources":["../src/slot-layout.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"slot-layout.d.ts","sourceRoot":"","sources":["../src/slot-layout.ts"],"names":[],"mappings":"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"}
|
package/dist/slot-layout.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Slot layouts (ADR-0118): a holder's tiers, the slots
|
|
3
|
-
* slot, and whether it fits. Pure computation; nothing here knows a scene, a graph or a component.
|
|
2
|
+
* Slot layouts (ADR-0118): a holder's anchors, the tiers placed on them, the slots of each tier, how slots are numbered,
|
|
3
|
+
* where an item sits in a slot, and whether it fits. Pure computation; nothing here knows a scene, a graph or a component.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* (tier, slot), both counted from 1, and slot numbers restart on every tier. ROW and GRID slots are not stored: they are
|
|
7
|
-
* worked out from the rule, so thousands of slots are one rule. A slot's id is `t{tier}_s{slot}` -- a logical id, not
|
|
8
|
-
* the name of a node in any 3D asset.
|
|
5
|
+
* holder → anchor → tier → slot → item
|
|
9
6
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* An anchor is a frame the holder gives: a position and a direction, and whatever moves it (a fork, a lift). The holder
|
|
8
|
+
* decides how many anchors there are; nothing here makes one. A tier is a group of slots placed on one anchor -- a shelf
|
|
9
|
+
* level, the load area of one fork. Several tiers may share an anchor, at the same height or not. A slot is one place an
|
|
10
|
+
* item is put; its address is (tier, slot). Tier numbers are the holder's own and slot numbers the tier's own: positive,
|
|
11
|
+
* unique, kept as they are when tiers or slots are removed or shown in another order -- never renumbered by height or
|
|
12
|
+
* position. ROW and GRID slots are not stored: they are worked out from the rule, so thousands of slots are one rule. A
|
|
13
|
+
* slot's id is `t{tier}_s{slot}` -- a logical id, not the name of a node in any 3D asset.
|
|
14
|
+
*
|
|
15
|
+
* Where an item is, each part said once:
|
|
16
|
+
* item in the world = holder × anchor × tier × slot × seat
|
|
17
|
+
* The anchor's frame is the holder's to give. Here a tier is placed in its anchor's frame (position, turn about the
|
|
18
|
+
* anchor's Y), a slot in its tier's (offset in X and Z, turn about Y), and an item in its slot (stack height, scale).
|
|
19
|
+
* Lengths are in whatever unit the layout is written in; nothing here converts them. Rotations are in degrees about Y.
|
|
13
20
|
*/
|
|
14
21
|
export function layoutIssue(code, path, params) {
|
|
15
22
|
return params ? { code, path, params } : { code, path };
|
|
16
23
|
}
|
|
24
|
+
/** The anchor at the holder's own origin, for a holder that gives no other. */
|
|
25
|
+
export const HOLDER_ANCHOR = 'holder';
|
|
17
26
|
/* ── checking a layout ───────────────────────────────────────────────────── */
|
|
18
27
|
const isFiniteNumber = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
19
28
|
const isPositive = (v) => isFiniteNumber(v) && v > 0;
|
|
@@ -37,24 +46,38 @@ function checkSpec(spec, path, issues) {
|
|
|
37
46
|
* Everything wrong with a layout; an empty list is a sound one. stackLimit may be Infinity (no limit); a layout that is
|
|
38
47
|
* to be stored as JSON is also checked with `persistenceIssues`.
|
|
39
48
|
*/
|
|
40
|
-
export function validateSlotLayout(layout) {
|
|
49
|
+
export function validateSlotLayout(layout, options = {}) {
|
|
41
50
|
const issues = [];
|
|
42
51
|
if (!layout || !Array.isArray(layout.tiers) || layout.tiers.length === 0)
|
|
43
52
|
return [layoutIssue('TIERS_REQUIRED', 'tiers')];
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
const anchors = options.anchors ? new Set(options.anchors) : null;
|
|
54
|
+
const tierNos = new Set();
|
|
46
55
|
layout.tiers.forEach((tier, i) => {
|
|
47
56
|
const path = `tiers[${i}]`;
|
|
48
57
|
if (!tier || typeof tier !== 'object') {
|
|
49
58
|
issues.push(layoutIssue('OBJECT_REQUIRED', path));
|
|
50
59
|
return;
|
|
51
60
|
}
|
|
52
|
-
if (tier.tierNo
|
|
53
|
-
issues.push(layoutIssue('
|
|
54
|
-
if (
|
|
55
|
-
issues.push(layoutIssue('
|
|
56
|
-
else
|
|
57
|
-
|
|
61
|
+
if (!isPositiveInt(tier.tierNo))
|
|
62
|
+
issues.push(layoutIssue('POSITIVE_INTEGER_REQUIRED', `${path}.tierNo`));
|
|
63
|
+
else if (tierNos.has(tier.tierNo))
|
|
64
|
+
issues.push(layoutIssue('TIER_NUMBER_DUPLICATE', `${path}.tierNo`, { tierNo: tier.tierNo }));
|
|
65
|
+
else
|
|
66
|
+
tierNos.add(tier.tierNo);
|
|
67
|
+
if (typeof tier.anchorId !== 'string' || !tier.anchorId.trim())
|
|
68
|
+
issues.push(layoutIssue('ANCHOR_REQUIRED', `${path}.anchorId`));
|
|
69
|
+
else if (anchors && !anchors.has(tier.anchorId))
|
|
70
|
+
issues.push(layoutIssue('ANCHOR_UNKNOWN', `${path}.anchorId`, { anchorId: tier.anchorId }));
|
|
71
|
+
if (!['ON', 'IN', 'HANG'].includes(tier.mount))
|
|
72
|
+
issues.push(layoutIssue('MOUNT_INVALID', `${path}.mount`));
|
|
73
|
+
if (!tier.position || typeof tier.position !== 'object')
|
|
74
|
+
issues.push(layoutIssue('OBJECT_REQUIRED', `${path}.position`));
|
|
75
|
+
else
|
|
76
|
+
for (const k of ['x', 'y', 'z'])
|
|
77
|
+
if (!isFiniteNumber(tier.position[k]))
|
|
78
|
+
issues.push(layoutIssue('FINITE_NUMBER_REQUIRED', `${path}.position.${k}`));
|
|
79
|
+
if (!isFiniteNumber(tier.rotation))
|
|
80
|
+
issues.push(layoutIssue('FINITE_NUMBER_REQUIRED', `${path}.rotation`));
|
|
58
81
|
const l = tier.layout;
|
|
59
82
|
switch (l?.pattern) {
|
|
60
83
|
case 'SINGLE':
|
|
@@ -81,14 +104,24 @@ export function validateSlotLayout(layout) {
|
|
|
81
104
|
checkSpec(l.slot, `${path}.layout.slot`, issues);
|
|
82
105
|
break;
|
|
83
106
|
case 'CUSTOM': {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
slots.
|
|
107
|
+
if (!Array.isArray(l.slots)) {
|
|
108
|
+
issues.push(layoutIssue('ARRAY_REQUIRED', `${path}.layout.slots`));
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (!l.slots.length)
|
|
112
|
+
issues.push(layoutIssue('SLOTS_REQUIRED', `${path}.layout.slots`));
|
|
113
|
+
const slotNos = new Set();
|
|
114
|
+
l.slots.forEach((s, k) => {
|
|
115
|
+
const at = `${path}.layout.slots[${k}]`;
|
|
116
|
+
if (!isPositiveInt(s?.slotNo))
|
|
117
|
+
issues.push(layoutIssue('POSITIVE_INTEGER_REQUIRED', `${at}.slotNo`));
|
|
118
|
+
else if (slotNos.has(s.slotNo))
|
|
119
|
+
issues.push(layoutIssue('SLOT_NUMBER_DUPLICATE', `${at}.slotNo`, { slotNo: s.slotNo }));
|
|
120
|
+
else
|
|
121
|
+
slotNos.add(s.slotNo);
|
|
89
122
|
if (!isFiniteNumber(s?.offset?.x) || !isFiniteNumber(s?.offset?.z))
|
|
90
|
-
issues.push(layoutIssue('FINITE_NUMBER_REQUIRED', `${
|
|
91
|
-
checkSpec(s,
|
|
123
|
+
issues.push(layoutIssue('FINITE_NUMBER_REQUIRED', `${at}.offset`));
|
|
124
|
+
checkSpec(s, at, issues);
|
|
92
125
|
});
|
|
93
126
|
break;
|
|
94
127
|
}
|
|
@@ -126,6 +159,12 @@ export function slotCount(pattern) {
|
|
|
126
159
|
return pattern.slots.length;
|
|
127
160
|
}
|
|
128
161
|
}
|
|
162
|
+
/** A tier's slot numbers, in order. ROW and GRID count from 1; CUSTOM has the numbers it was given. */
|
|
163
|
+
export function slotNumbers(pattern) {
|
|
164
|
+
if (pattern.pattern === 'CUSTOM')
|
|
165
|
+
return pattern.slots.map(s => s.slotNo).sort((a, b) => a - b);
|
|
166
|
+
return Array.from({ length: slotCount(pattern) }, (_, i) => i + 1);
|
|
167
|
+
}
|
|
129
168
|
/** A GRID slot's number from its row r and column c (from 1), counted from the origin corner. */
|
|
130
169
|
export function gridSlotNo(rows, cols, r, c, order = 'ROW_MAJOR') {
|
|
131
170
|
switch (order) {
|
|
@@ -169,7 +208,7 @@ const flip = (v, max) => (max && v !== 0 ? -v : v);
|
|
|
169
208
|
*/
|
|
170
209
|
export function resolveSlot(layout, address, options = {}) {
|
|
171
210
|
const tier = layout?.tiers?.find(t => t?.tierNo === address?.tier);
|
|
172
|
-
if (!tier || !isPositiveInt(address.slot)
|
|
211
|
+
if (!tier || !isPositiveInt(address.slot))
|
|
173
212
|
return null;
|
|
174
213
|
const l = tier.layout;
|
|
175
214
|
let spec;
|
|
@@ -177,16 +216,22 @@ export function resolveSlot(layout, address, options = {}) {
|
|
|
177
216
|
let name;
|
|
178
217
|
switch (l.pattern) {
|
|
179
218
|
case 'SINGLE':
|
|
219
|
+
if (address.slot !== 1)
|
|
220
|
+
return null;
|
|
180
221
|
spec = l.slot;
|
|
181
222
|
offset = { x: 0, z: 0 };
|
|
182
223
|
break;
|
|
183
224
|
case 'ROW': {
|
|
225
|
+
if (address.slot > l.count)
|
|
226
|
+
return null;
|
|
184
227
|
const v = flip((address.slot - 1 - (l.count - 1) / 2) * l.pitch, l.origin === 'MAX');
|
|
185
228
|
spec = l.slot;
|
|
186
229
|
offset = l.axis === 'X' ? { x: v, z: 0 } : { x: 0, z: v };
|
|
187
230
|
break;
|
|
188
231
|
}
|
|
189
232
|
case 'GRID': {
|
|
233
|
+
if (address.slot > l.rows * l.cols)
|
|
234
|
+
return null;
|
|
190
235
|
const { r, c } = gridCellOf(l.rows, l.cols, address.slot, l.order);
|
|
191
236
|
const origin = l.origin ?? { x: 'MIN', z: 'MIN' };
|
|
192
237
|
spec = l.slot;
|
|
@@ -210,7 +255,10 @@ export function resolveSlot(layout, address, options = {}) {
|
|
|
210
255
|
tier: tier.tierNo,
|
|
211
256
|
slotNo: address.slot,
|
|
212
257
|
name,
|
|
213
|
-
|
|
258
|
+
anchorId: tier.anchorId,
|
|
259
|
+
mount: tier.mount,
|
|
260
|
+
tierPosition: { ...tier.position },
|
|
261
|
+
tierRotation: tier.rotation,
|
|
214
262
|
offset,
|
|
215
263
|
allowed: { ...spec.allowed },
|
|
216
264
|
accepts: spec.accepts ? [...spec.accepts] : undefined,
|
|
@@ -228,12 +276,31 @@ export function addressOfSlotId(slotId) {
|
|
|
228
276
|
const m = typeof slotId === 'string' ? /^t(\d+)_s(\d+)$/.exec(slotId) : null;
|
|
229
277
|
return m ? { tier: Number(m[1]), slot: Number(m[2]) } : null;
|
|
230
278
|
}
|
|
231
|
-
/** Every address of a layout, tier by tier. For a big GRID, resolve only the addresses you need. */
|
|
279
|
+
/** Every address of a layout, tier by tier as written. For a big GRID, resolve only the addresses you need. */
|
|
232
280
|
export function* slotAddresses(layout) {
|
|
233
281
|
for (const tier of layout?.tiers ?? [])
|
|
234
|
-
for (
|
|
282
|
+
for (const slot of slotNumbers(tier.layout))
|
|
235
283
|
yield { tier: tier.tierNo, slot };
|
|
236
284
|
}
|
|
285
|
+
/** A point of the tier's X-Z plane turned by the tier's rotation about Y (right-handed: +X turns toward -Z). */
|
|
286
|
+
function turnY(x, z, degrees) {
|
|
287
|
+
if (!degrees)
|
|
288
|
+
return { x, z };
|
|
289
|
+
const a = (degrees * Math.PI) / 180;
|
|
290
|
+
const c = Math.cos(a);
|
|
291
|
+
const s = Math.sin(a);
|
|
292
|
+
// clears the 6e-17 left at multiples of 90°
|
|
293
|
+
const clean = (v) => Math.round(v * 1e9) / 1e9;
|
|
294
|
+
return { x: clean(x * c + z * s), z: clean(-x * s + z * c) };
|
|
295
|
+
}
|
|
296
|
+
/** A slot's frame in its anchor's: the tier's placement, then the slot's offset and turn in the tier. */
|
|
297
|
+
export function slotFrame(slot) {
|
|
298
|
+
const o = turnY(slot.offset.x, slot.offset.z, slot.tierRotation);
|
|
299
|
+
return {
|
|
300
|
+
position: { x: slot.tierPosition.x + o.x, y: slot.tierPosition.y, z: slot.tierPosition.z + o.z },
|
|
301
|
+
rotation: slot.tierRotation + slot.rotation
|
|
302
|
+
};
|
|
303
|
+
}
|
|
237
304
|
export const DEFAULT_SEAT_MARGIN = 0.05;
|
|
238
305
|
const knownDims = (d) => !!d && isPositive(d.width) && isPositive(d.depth) && isPositive(d.height);
|
|
239
306
|
/** The X and Z extent of an item turned by `rotation` degrees. */
|
|
@@ -246,12 +313,14 @@ export function rotatedFootprint(dims, rotation) {
|
|
|
246
313
|
return { width: clean(dims.width * c + dims.depth * s), depth: clean(dims.width * s + dims.depth * c) };
|
|
247
314
|
}
|
|
248
315
|
/**
|
|
249
|
-
* Where an item sits in
|
|
250
|
-
* of `belowHeight` -- the shown heights (height × scale) of what is stacked under it
|
|
251
|
-
*
|
|
252
|
-
*
|
|
316
|
+
* Where an item sits in its slot's own frame: shrunk to fit the slot's width and depth (by its turned extent, never
|
|
317
|
+
* enlarged), on top of `belowHeight` -- the shown heights (height × scale) of what is stacked under it -- along +Y for ON
|
|
318
|
+
* and IN, -Y for HANG. x and z are 0 and it is not turned: the slot's frame has the offset and the turn. `margin` is room
|
|
319
|
+
* left only for showing (0 ≤ margin < 1); whether it fits is judged elsewhere, at full size. Null when a size or value is
|
|
320
|
+
* unusable -- never NaN or Infinity.
|
|
321
|
+
* slotFrame(slot) then seatInSlot(...) = seatTransform(...)
|
|
253
322
|
*/
|
|
254
|
-
export function
|
|
323
|
+
export function seatInSlot(dims, slot, belowHeight = 0, margin = DEFAULT_SEAT_MARGIN) {
|
|
255
324
|
if (!knownDims(dims))
|
|
256
325
|
return null;
|
|
257
326
|
if (!isFiniteNumber(belowHeight) || !isFiniteNumber(margin) || margin < 0 || margin >= 1)
|
|
@@ -259,34 +328,112 @@ export function seatTransform(dims, slot, mount, belowHeight = 0, margin = DEFAU
|
|
|
259
328
|
const fp = rotatedFootprint(dims, slot.rotation);
|
|
260
329
|
const scale = Math.min(1, Math.min(slot.allowed.width / fp.width, slot.allowed.depth / fp.depth) * (1 - margin));
|
|
261
330
|
const h = dims.height * scale;
|
|
262
|
-
const dir = mount === 'HANG' ? -1 : 1;
|
|
263
|
-
|
|
264
|
-
|
|
331
|
+
const dir = slot.mount === 'HANG' ? -1 : 1;
|
|
332
|
+
return { position: { x: 0, y: dir * (belowHeight + h / 2), z: 0 }, rotation: 0, scale };
|
|
333
|
+
}
|
|
334
|
+
/** Where an item sits in its anchor's frame: the slot's frame, then the seat in it. */
|
|
335
|
+
export function seatTransform(dims, slot, belowHeight = 0, margin = DEFAULT_SEAT_MARGIN) {
|
|
336
|
+
const seat = seatInSlot(dims, slot, belowHeight, margin);
|
|
337
|
+
if (!seat)
|
|
338
|
+
return null;
|
|
339
|
+
const f = slotFrame(slot);
|
|
340
|
+
return { position: { x: f.position.x, y: f.position.y + seat.position.y, z: f.position.z }, rotation: f.rotation, scale: seat.scale };
|
|
265
341
|
}
|
|
266
|
-
/** Where each of a slot's stacked items sits, bottom first. An item of unknown size is null and adds no height. */
|
|
267
|
-
export function stackSeatTransforms(stack, slot,
|
|
342
|
+
/** Where each of a slot's stacked items sits in the anchor's frame, bottom first. An item of unknown size is null and adds no height. */
|
|
343
|
+
export function stackSeatTransforms(stack, slot, margin = DEFAULT_SEAT_MARGIN) {
|
|
268
344
|
let below = 0;
|
|
269
345
|
return stack.map(dims => {
|
|
270
|
-
const t = dims ? seatTransform(dims, slot,
|
|
346
|
+
const t = dims ? seatTransform(dims, slot, below, margin) : null;
|
|
271
347
|
if (t)
|
|
272
348
|
below += dims.height * t.scale;
|
|
273
349
|
return t;
|
|
274
350
|
});
|
|
275
351
|
}
|
|
276
|
-
/**
|
|
277
|
-
|
|
278
|
-
|
|
352
|
+
/** Below this, a shared length is a touch, not an overlap. */
|
|
353
|
+
const TOUCH = 1e-6;
|
|
354
|
+
function roomOf(slot, tierIndex) {
|
|
355
|
+
const f = slotFrame(slot);
|
|
356
|
+
const hw = slot.allowed.width / 2;
|
|
357
|
+
const hd = slot.allowed.depth / 2;
|
|
358
|
+
const corners = [
|
|
359
|
+
[-hw, -hd],
|
|
360
|
+
[hw, -hd],
|
|
361
|
+
[hw, hd],
|
|
362
|
+
[-hw, hd]
|
|
363
|
+
].map(([x, z]) => {
|
|
364
|
+
const p = turnY(x, z, f.rotation);
|
|
365
|
+
return { x: f.position.x + p.x, z: f.position.z + p.z };
|
|
366
|
+
});
|
|
367
|
+
const ax = turnY(1, 0, f.rotation);
|
|
368
|
+
const az = turnY(0, 1, f.rotation);
|
|
369
|
+
const dir = slot.mount === 'HANG' ? -1 : 1;
|
|
370
|
+
const ys = [f.position.y, f.position.y + dir * slot.allowed.height];
|
|
371
|
+
const xs = corners.map(c => c.x);
|
|
372
|
+
return {
|
|
373
|
+
tierIndex,
|
|
374
|
+
tier: slot.tier,
|
|
375
|
+
slot: slot.slotNo,
|
|
376
|
+
corners,
|
|
377
|
+
axes: [ax, az],
|
|
378
|
+
y0: Math.min(...ys),
|
|
379
|
+
y1: Math.max(...ys),
|
|
380
|
+
minX: Math.min(...xs),
|
|
381
|
+
maxX: Math.max(...xs)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/** Whether two turned rectangles share area: no axis of either separates them by more than a touch. */
|
|
385
|
+
function footprintsOverlap(a, b) {
|
|
386
|
+
for (const axis of [...a.axes, ...b.axes]) {
|
|
387
|
+
const pa = a.corners.map(c => c.x * axis.x + c.z * axis.z);
|
|
388
|
+
const pb = b.corners.map(c => c.x * axis.x + c.z * axis.z);
|
|
389
|
+
const shared = Math.min(Math.max(...pa), Math.max(...pb)) - Math.max(Math.min(...pa), Math.min(...pb));
|
|
390
|
+
if (shared <= TOUCH)
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
return true;
|
|
279
394
|
}
|
|
280
395
|
/**
|
|
281
|
-
* Where
|
|
282
|
-
*
|
|
283
|
-
*
|
|
396
|
+
* Where a sound layout's slots crowd each other or leave their anchor's face, as warnings. Only slots on the same anchor
|
|
397
|
+
* are compared: anchors move apart. Rooms are each slot's allowed box, placed by its tier and turned; sharing a face or
|
|
398
|
+
* an edge is not overlapping.
|
|
284
399
|
*/
|
|
285
|
-
export function
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
400
|
+
export function slotSpaceWarnings(layout, options = {}) {
|
|
401
|
+
const warnings = [];
|
|
402
|
+
const byAnchor = new Map();
|
|
403
|
+
layout.tiers.forEach((tier, i) => {
|
|
404
|
+
const rooms = byAnchor.get(tier.anchorId) ?? [];
|
|
405
|
+
byAnchor.set(tier.anchorId, rooms);
|
|
406
|
+
for (const n of slotNumbers(tier.layout))
|
|
407
|
+
rooms.push(roomOf(resolveSlot(layout, { tier: tier.tierNo, slot: n }), i));
|
|
408
|
+
});
|
|
409
|
+
for (const [anchorId, rooms] of byAnchor) {
|
|
410
|
+
const face = options.anchorFaces?.[anchorId];
|
|
411
|
+
if (!face)
|
|
412
|
+
warnings.push({ code: 'ANCHOR_SPACE_UNKNOWN', path: 'tiers', params: { anchorId } });
|
|
413
|
+
else
|
|
414
|
+
for (const r of rooms)
|
|
415
|
+
if (r.corners.some(c => Math.abs(c.x) - face.width / 2 > TOUCH || Math.abs(c.z) - face.depth / 2 > TOUCH))
|
|
416
|
+
warnings.push({ code: 'SLOT_OUTSIDE_ANCHOR', path: `tiers[${r.tierIndex}]`, params: { tier: r.tier, slot: r.slot } });
|
|
417
|
+
// sweep along X so a big grid is not compared pair by pair
|
|
418
|
+
const sorted = [...rooms].sort((a, b) => a.minX - b.minX);
|
|
419
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
420
|
+
const a = sorted[i];
|
|
421
|
+
for (let j = i + 1; j < sorted.length && sorted[j].minX < a.maxX - TOUCH; j++) {
|
|
422
|
+
const b = sorted[j];
|
|
423
|
+
if (Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0) <= TOUCH)
|
|
424
|
+
continue;
|
|
425
|
+
if (!footprintsOverlap(a, b))
|
|
426
|
+
continue;
|
|
427
|
+
const [first, second] = a.tierIndex < b.tierIndex || (a.tierIndex === b.tierIndex && a.slot < b.slot) ? [a, b] : [b, a];
|
|
428
|
+
warnings.push({
|
|
429
|
+
code: 'SLOT_OVERLAP',
|
|
430
|
+
path: `tiers[${second.tierIndex}]`,
|
|
431
|
+
params: { tier: second.tier, slot: second.slot, otherTier: first.tier, otherSlot: first.slot }
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return warnings;
|
|
290
437
|
}
|
|
291
438
|
/**
|
|
292
439
|
* Whether one more item fits in a slot that already holds `occupants`, at full size. The count limit and the height
|