@realitycollective/webxr-uiextensions 0.1.0-preview.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/adapter.d.ts +105 -0
- package/dist/adapter.js +2 -0
- package/dist/adapter.js.map +1 -0
- package/dist/chrome/markup.d.ts +40 -0
- package/dist/chrome/markup.js +56 -0
- package/dist/chrome/markup.js.map +1 -0
- package/dist/controls/element.d.ts +22 -0
- package/dist/controls/element.js +40 -0
- package/dist/controls/element.js.map +1 -0
- package/dist/controls/expandable-label.d.ts +32 -0
- package/dist/controls/expandable-label.js +63 -0
- package/dist/controls/expandable-label.js.map +1 -0
- package/dist/controls/log-view.d.ts +38 -0
- package/dist/controls/log-view.js +83 -0
- package/dist/controls/log-view.js.map +1 -0
- package/dist/controls/stepper.d.ts +32 -0
- package/dist/controls/stepper.js +78 -0
- package/dist/controls/stepper.js.map +1 -0
- package/dist/controls/toggle.d.ts +33 -0
- package/dist/controls/toggle.js +63 -0
- package/dist/controls/toggle.js.map +1 -0
- package/dist/controls/upgrade.d.ts +31 -0
- package/dist/controls/upgrade.js +87 -0
- package/dist/controls/upgrade.js.map +1 -0
- package/dist/core/dock-state.d.ts +58 -0
- package/dist/core/dock-state.js +64 -0
- package/dist/core/dock-state.js.map +1 -0
- package/dist/core/drag-math.d.ts +34 -0
- package/dist/core/drag-math.js +44 -0
- package/dist/core/drag-math.js.map +1 -0
- package/dist/core/events.d.ts +17 -0
- package/dist/core/events.js +40 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/expandable-model.d.ts +35 -0
- package/dist/core/expandable-model.js +57 -0
- package/dist/core/expandable-model.js.map +1 -0
- package/dist/core/hold-to-drag.d.ts +24 -0
- package/dist/core/hold-to-drag.js +34 -0
- package/dist/core/hold-to-drag.js.map +1 -0
- package/dist/core/log-model.d.ts +42 -0
- package/dist/core/log-model.js +69 -0
- package/dist/core/log-model.js.map +1 -0
- package/dist/core/region-layout.d.ts +41 -0
- package/dist/core/region-layout.js +79 -0
- package/dist/core/region-layout.js.map +1 -0
- package/dist/core/region-registry.d.ts +35 -0
- package/dist/core/region-registry.js +97 -0
- package/dist/core/region-registry.js.map +1 -0
- package/dist/core/stepper-model.d.ts +24 -0
- package/dist/core/stepper-model.js +46 -0
- package/dist/core/stepper-model.js.map +1 -0
- package/dist/core/toggle-model.d.ts +11 -0
- package/dist/core/toggle-model.js +25 -0
- package/dist/core/toggle-model.js.map +1 -0
- package/dist/core/window-manager.d.ts +81 -0
- package/dist/core/window-manager.js +172 -0
- package/dist/core/window-manager.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/scene.d.ts +79 -0
- package/dist/scene.js +43 -0
- package/dist/scene.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-view.js","sourceRoot":"","sources":["../../src/controls/log-view.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Log view - `data-uix=\"log-view\"`.\n *\n * A console-style scrolling list rendered into a fixed pool of row elements\n * (UIKitML's text rule: each row needs a literal placeholder child). Rows\n * are the descendants with role `line`, in document order; their count is\n * the viewport height.\n *\n * ```html\n * <div data-uix=\"log-view\" data-uix-id=\"combat\" data-uix-capacity=\"200\">\n * <div class=\"rows\">\n * <span data-uix-role=\"line\">.</span>\n * <span data-uix-role=\"line\">.</span>\n * <span data-uix-role=\"line\">.</span>\n * </div>\n * <div class=\"log-buttons\">\n * <button data-uix-role=\"up\">▲</button>\n * <button data-uix-role=\"down\">▼</button>\n * <button data-uix-role=\"clear\">clear</button>\n * </div>\n * <span data-uix-role=\"status\">.</span>\n * </div>\n * ```\n */\nimport { LogModel } from '../core/log-model.js';\nimport { UixElement, attrNumber, findRole, findRoles } from './element.js';\n\nexport class LogViewHandle {\n constructor(\n readonly model: LogModel,\n private rows: UixElement[],\n private statusElement: UixElement | undefined,\n ) {\n this.render();\n }\n\n push(message: string, level = 'info'): void {\n this.model.push(message, level);\n this.render();\n }\n\n clear(): void {\n this.model.clear();\n this.render();\n }\n\n scrollUp(rows = 1): void {\n this.model.scrollUp(rows);\n this.render();\n }\n\n scrollDown(rows = 1): void {\n this.model.scrollDown(rows);\n this.render();\n }\n\n private render(): void {\n const visible = this.model.visible();\n this.rows.forEach((row, index) => {\n const entry = visible[index];\n row.setProperties({\n // UIKitML renders \"\" as empty text - exactly what vacant rows need.\n text: entry ? entry.message : '',\n });\n });\n if (this.statusElement) {\n const suffix = this.model.isFollowing ? '' : ' (scrolled)';\n this.statusElement.setProperties({\n text: `${this.model.count} entries${suffix}`,\n });\n }\n }\n}\n\nexport function upgradeLogView(root: UixElement): LogViewHandle {\n const rows = findRoles(root, 'line');\n const capacity = attrNumber(root, 'uixCapacity');\n const model = new LogModel({\n visibleRows: Math.max(1, rows.length),\n ...(capacity !== undefined && { capacity }),\n });\n const handle = new LogViewHandle(model, rows, findRole(root, 'status'));\n findRole(root, 'up')?.addEventListener('click', () => handle.scrollUp());\n findRole(root, 'down')?.addEventListener('click', () => handle.scrollDown());\n findRole(root, 'clear')?.addEventListener('click', () => handle.clear());\n return handle;\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stepper control - `data-uix="stepper"`.
|
|
3
|
+
*
|
|
4
|
+
* Markup contract (roles are `data-uix-role` attributes on descendants):
|
|
5
|
+
* ```html
|
|
6
|
+
* <div data-uix="stepper" data-uix-id="count" data-uix-min="0" data-uix-max="10"
|
|
7
|
+
* data-uix-step="1" data-uix-value="5" class="my-stepper">
|
|
8
|
+
* <button data-uix-role="decrement">-</button>
|
|
9
|
+
* <span data-uix-role="value">.</span>
|
|
10
|
+
* <button data-uix-role="increment">+</button>
|
|
11
|
+
* </div>
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { Emitter } from '../core/events.js';
|
|
15
|
+
import { StepperModel } from '../core/stepper-model.js';
|
|
16
|
+
import { UixElement } from './element.js';
|
|
17
|
+
export declare class StepperHandle {
|
|
18
|
+
readonly model: StepperModel;
|
|
19
|
+
private valueElement;
|
|
20
|
+
private decrementElement;
|
|
21
|
+
private incrementElement;
|
|
22
|
+
readonly events: Emitter<{
|
|
23
|
+
change: number;
|
|
24
|
+
}>;
|
|
25
|
+
constructor(model: StepperModel, valueElement: UixElement | undefined, decrementElement: UixElement | undefined, incrementElement: UixElement | undefined);
|
|
26
|
+
get value(): number;
|
|
27
|
+
set(value: number): void;
|
|
28
|
+
increment(): void;
|
|
29
|
+
decrement(): void;
|
|
30
|
+
private render;
|
|
31
|
+
}
|
|
32
|
+
export declare function upgradeStepper(root: UixElement): StepperHandle;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stepper control - `data-uix="stepper"`.
|
|
3
|
+
*
|
|
4
|
+
* Markup contract (roles are `data-uix-role` attributes on descendants):
|
|
5
|
+
* ```html
|
|
6
|
+
* <div data-uix="stepper" data-uix-id="count" data-uix-min="0" data-uix-max="10"
|
|
7
|
+
* data-uix-step="1" data-uix-value="5" class="my-stepper">
|
|
8
|
+
* <button data-uix-role="decrement">-</button>
|
|
9
|
+
* <span data-uix-role="value">.</span>
|
|
10
|
+
* <button data-uix-role="increment">+</button>
|
|
11
|
+
* </div>
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { Emitter } from '../core/events.js';
|
|
15
|
+
import { StepperModel } from '../core/stepper-model.js';
|
|
16
|
+
import { attrNumber, findRole } from './element.js';
|
|
17
|
+
const DISABLED_OPACITY = 0.35;
|
|
18
|
+
export class StepperHandle {
|
|
19
|
+
model;
|
|
20
|
+
valueElement;
|
|
21
|
+
decrementElement;
|
|
22
|
+
incrementElement;
|
|
23
|
+
events = new Emitter();
|
|
24
|
+
constructor(model, valueElement, decrementElement, incrementElement) {
|
|
25
|
+
this.model = model;
|
|
26
|
+
this.valueElement = valueElement;
|
|
27
|
+
this.decrementElement = decrementElement;
|
|
28
|
+
this.incrementElement = incrementElement;
|
|
29
|
+
this.render();
|
|
30
|
+
}
|
|
31
|
+
get value() {
|
|
32
|
+
return this.model.value;
|
|
33
|
+
}
|
|
34
|
+
set(value) {
|
|
35
|
+
if (this.model.set(value)) {
|
|
36
|
+
this.render();
|
|
37
|
+
this.events.emit('change', this.model.value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
increment() {
|
|
41
|
+
this.set(this.model.value + this.model.step);
|
|
42
|
+
}
|
|
43
|
+
decrement() {
|
|
44
|
+
this.set(this.model.value - this.model.step);
|
|
45
|
+
}
|
|
46
|
+
render() {
|
|
47
|
+
this.valueElement?.setProperties({ text: String(this.model.value) });
|
|
48
|
+
this.decrementElement?.setProperties({
|
|
49
|
+
opacity: this.model.canDecrement ? 1 : DISABLED_OPACITY,
|
|
50
|
+
});
|
|
51
|
+
this.incrementElement?.setProperties({
|
|
52
|
+
opacity: this.model.canIncrement ? 1 : DISABLED_OPACITY,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function upgradeStepper(root) {
|
|
57
|
+
const options = {};
|
|
58
|
+
const min = attrNumber(root, 'uixMin');
|
|
59
|
+
const max = attrNumber(root, 'uixMax');
|
|
60
|
+
const step = attrNumber(root, 'uixStep');
|
|
61
|
+
const value = attrNumber(root, 'uixValue');
|
|
62
|
+
if (min !== undefined)
|
|
63
|
+
options.min = min;
|
|
64
|
+
if (max !== undefined)
|
|
65
|
+
options.max = max;
|
|
66
|
+
if (step !== undefined)
|
|
67
|
+
options.step = step;
|
|
68
|
+
if (value !== undefined)
|
|
69
|
+
options.value = value;
|
|
70
|
+
const model = new StepperModel(options);
|
|
71
|
+
const decrement = findRole(root, 'decrement');
|
|
72
|
+
const increment = findRole(root, 'increment');
|
|
73
|
+
const handle = new StepperHandle(model, findRole(root, 'value'), decrement, increment);
|
|
74
|
+
decrement?.addEventListener('click', () => handle.decrement());
|
|
75
|
+
increment?.addEventListener('click', () => handle.increment());
|
|
76
|
+
return handle;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=stepper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stepper.js","sourceRoot":"","sources":["../../src/controls/stepper.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Stepper control - `data-uix=\"stepper\"`.\n *\n * Markup contract (roles are `data-uix-role` attributes on descendants):\n * ```html\n * <div data-uix=\"stepper\" data-uix-id=\"count\" data-uix-min=\"0\" data-uix-max=\"10\"\n * data-uix-step=\"1\" data-uix-value=\"5\" class=\"my-stepper\">\n * <button data-uix-role=\"decrement\">-</button>\n * <span data-uix-role=\"value\">.</span>\n * <button data-uix-role=\"increment\">+</button>\n * </div>\n * ```\n */\nimport { Emitter } from '../core/events.js';\nimport { StepperModel, type StepperOptions } from '../core/stepper-model.js';\nimport { UixElement, attrNumber, findRole } from './element.js';\n\nconst DISABLED_OPACITY = 0.35;\n\nexport class StepperHandle {\n readonly events = new Emitter<{ change: number }>();\n\n constructor(\n readonly model: StepperModel,\n private valueElement: UixElement | undefined,\n private decrementElement: UixElement | undefined,\n private incrementElement: UixElement | undefined,\n ) {\n this.render();\n }\n\n get value(): number {\n return this.model.value;\n }\n\n set(value: number): void {\n if (this.model.set(value)) {\n this.render();\n this.events.emit('change', this.model.value);\n }\n }\n\n increment(): void {\n this.set(this.model.value + this.model.step);\n }\n\n decrement(): void {\n this.set(this.model.value - this.model.step);\n }\n\n private render(): void {\n this.valueElement?.setProperties({ text: String(this.model.value) });\n this.decrementElement?.setProperties({\n opacity: this.model.canDecrement ? 1 : DISABLED_OPACITY,\n });\n this.incrementElement?.setProperties({\n opacity: this.model.canIncrement ? 1 : DISABLED_OPACITY,\n });\n }\n}\n\nexport function upgradeStepper(root: UixElement): StepperHandle {\n const options: StepperOptions = {};\n const min = attrNumber(root, 'uixMin');\n const max = attrNumber(root, 'uixMax');\n const step = attrNumber(root, 'uixStep');\n const value = attrNumber(root, 'uixValue');\n if (min !== undefined) options.min = min;\n if (max !== undefined) options.max = max;\n if (step !== undefined) options.step = step;\n if (value !== undefined) options.value = value;\n const model = new StepperModel(options);\n const decrement = findRole(root, 'decrement');\n const increment = findRole(root, 'increment');\n const handle = new StepperHandle(model, findRole(root, 'value'), decrement, increment);\n decrement?.addEventListener('click', () => handle.decrement());\n increment?.addEventListener('click', () => handle.increment());\n return handle;\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toggle control - `data-uix="toggle"`.
|
|
3
|
+
*
|
|
4
|
+
* The whole control is the click target. Optional attributes:
|
|
5
|
+
* `data-uix-on` (initial state), `data-uix-on-text` / `data-uix-off-text`
|
|
6
|
+
* (label written into the `label` role), `data-uix-on-color` /
|
|
7
|
+
* `data-uix-off-color` (background color applied to the control root).
|
|
8
|
+
*
|
|
9
|
+
* ```html
|
|
10
|
+
* <div data-uix="toggle" data-uix-id="shield" data-uix-on="true"
|
|
11
|
+
* data-uix-on-text="SHIELD ON" data-uix-off-text="SHIELD OFF"
|
|
12
|
+
* data-uix-on-color="#2e7d32" data-uix-off-color="#5d4037" class="my-toggle">
|
|
13
|
+
* <span data-uix-role="label">.</span>
|
|
14
|
+
* </div>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import { Emitter } from '../core/events.js';
|
|
18
|
+
import { ToggleModel } from '../core/toggle-model.js';
|
|
19
|
+
import { UixElement } from './element.js';
|
|
20
|
+
export declare class ToggleHandle {
|
|
21
|
+
readonly model: ToggleModel;
|
|
22
|
+
private root;
|
|
23
|
+
private labelElement;
|
|
24
|
+
readonly events: Emitter<{
|
|
25
|
+
change: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
constructor(model: ToggleModel, root: UixElement, labelElement: UixElement | undefined);
|
|
28
|
+
get value(): boolean;
|
|
29
|
+
set(value: boolean): void;
|
|
30
|
+
toggle(): void;
|
|
31
|
+
private render;
|
|
32
|
+
}
|
|
33
|
+
export declare function upgradeToggle(root: UixElement): ToggleHandle;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toggle control - `data-uix="toggle"`.
|
|
3
|
+
*
|
|
4
|
+
* The whole control is the click target. Optional attributes:
|
|
5
|
+
* `data-uix-on` (initial state), `data-uix-on-text` / `data-uix-off-text`
|
|
6
|
+
* (label written into the `label` role), `data-uix-on-color` /
|
|
7
|
+
* `data-uix-off-color` (background color applied to the control root).
|
|
8
|
+
*
|
|
9
|
+
* ```html
|
|
10
|
+
* <div data-uix="toggle" data-uix-id="shield" data-uix-on="true"
|
|
11
|
+
* data-uix-on-text="SHIELD ON" data-uix-off-text="SHIELD OFF"
|
|
12
|
+
* data-uix-on-color="#2e7d32" data-uix-off-color="#5d4037" class="my-toggle">
|
|
13
|
+
* <span data-uix-role="label">.</span>
|
|
14
|
+
* </div>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import { Emitter } from '../core/events.js';
|
|
18
|
+
import { ToggleModel } from '../core/toggle-model.js';
|
|
19
|
+
import { attrBoolean, attrString, findRole } from './element.js';
|
|
20
|
+
export class ToggleHandle {
|
|
21
|
+
model;
|
|
22
|
+
root;
|
|
23
|
+
labelElement;
|
|
24
|
+
events = new Emitter();
|
|
25
|
+
constructor(model, root, labelElement) {
|
|
26
|
+
this.model = model;
|
|
27
|
+
this.root = root;
|
|
28
|
+
this.labelElement = labelElement;
|
|
29
|
+
this.render();
|
|
30
|
+
}
|
|
31
|
+
get value() {
|
|
32
|
+
return this.model.value;
|
|
33
|
+
}
|
|
34
|
+
set(value) {
|
|
35
|
+
if (this.model.set(value)) {
|
|
36
|
+
this.render();
|
|
37
|
+
this.events.emit('change', this.model.value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
toggle() {
|
|
41
|
+
this.set(!this.model.value);
|
|
42
|
+
}
|
|
43
|
+
render() {
|
|
44
|
+
const on = this.model.value;
|
|
45
|
+
const text = on
|
|
46
|
+
? (attrString(this.root, 'uixOnText') ?? 'ON')
|
|
47
|
+
: (attrString(this.root, 'uixOffText') ?? 'OFF');
|
|
48
|
+
this.labelElement?.setProperties({ text });
|
|
49
|
+
const color = on
|
|
50
|
+
? attrString(this.root, 'uixOnColor')
|
|
51
|
+
: attrString(this.root, 'uixOffColor');
|
|
52
|
+
if (color) {
|
|
53
|
+
this.root.setProperties({ backgroundColor: color });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function upgradeToggle(root) {
|
|
58
|
+
const model = new ToggleModel(attrBoolean(root, 'uixOn') ?? false);
|
|
59
|
+
const handle = new ToggleHandle(model, root, findRole(root, 'label'));
|
|
60
|
+
root.addEventListener('click', () => handle.toggle());
|
|
61
|
+
return handle;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=toggle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toggle.js","sourceRoot":"","sources":["../../src/controls/toggle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAc,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7E,MAAM,OAAO,YAAY;IAIZ;IACD;IACA;IALD,MAAM,GAAG,IAAI,OAAO,EAAuB,CAAC;IAErD,YACW,KAAkB,EACnB,IAAgB,EAChB,YAAoC;QAFnC,UAAK,GAAL,KAAK,CAAa;QACnB,SAAI,GAAJ,IAAI,CAAY;QAChB,iBAAY,GAAZ,YAAY,CAAwB;QAE5C,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;IAC1B,CAAC;IAED,GAAG,CAAC,KAAc;QAChB,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAEO,MAAM;QACZ,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAC5B,MAAM,IAAI,GAAG,EAAE;YACb,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,IAAI,CAAC;YAC9C,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,KAAK,CAAC,CAAC;QACnD,IAAI,CAAC,YAAY,EAAE,aAAa,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,EAAE;YACd,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC;YACrC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QACzC,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,aAAa,CAAC,IAAgB;IAC5C,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC;IACnE,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACtE,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACtD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * Toggle control - `data-uix=\"toggle\"`.\n *\n * The whole control is the click target. Optional attributes:\n * `data-uix-on` (initial state), `data-uix-on-text` / `data-uix-off-text`\n * (label written into the `label` role), `data-uix-on-color` /\n * `data-uix-off-color` (background color applied to the control root).\n *\n * ```html\n * <div data-uix=\"toggle\" data-uix-id=\"shield\" data-uix-on=\"true\"\n * data-uix-on-text=\"SHIELD ON\" data-uix-off-text=\"SHIELD OFF\"\n * data-uix-on-color=\"#2e7d32\" data-uix-off-color=\"#5d4037\" class=\"my-toggle\">\n * <span data-uix-role=\"label\">.</span>\n * </div>\n * ```\n */\nimport { Emitter } from '../core/events.js';\nimport { ToggleModel } from '../core/toggle-model.js';\nimport { UixElement, attrBoolean, attrString, findRole } from './element.js';\n\nexport class ToggleHandle {\n readonly events = new Emitter<{ change: boolean }>();\n\n constructor(\n readonly model: ToggleModel,\n private root: UixElement,\n private labelElement: UixElement | undefined,\n ) {\n this.render();\n }\n\n get value(): boolean {\n return this.model.value;\n }\n\n set(value: boolean): void {\n if (this.model.set(value)) {\n this.render();\n this.events.emit('change', this.model.value);\n }\n }\n\n toggle(): void {\n this.set(!this.model.value);\n }\n\n private render(): void {\n const on = this.model.value;\n const text = on\n ? (attrString(this.root, 'uixOnText') ?? 'ON')\n : (attrString(this.root, 'uixOffText') ?? 'OFF');\n this.labelElement?.setProperties({ text });\n const color = on\n ? attrString(this.root, 'uixOnColor')\n : attrString(this.root, 'uixOffColor');\n if (color) {\n this.root.setProperties({ backgroundColor: color });\n }\n }\n}\n\nexport function upgradeToggle(root: UixElement): ToggleHandle {\n const model = new ToggleModel(attrBoolean(root, 'uixOn') ?? false);\n const handle = new ToggleHandle(model, root, findRole(root, 'label'));\n root.addEventListener('click', () => handle.toggle());\n return handle;\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel upgrade - scans a loaded panel for `data-uix` markers and wires the
|
|
3
|
+
* matching control behaviour. App code retrieves the resulting handles via
|
|
4
|
+
* `panelControlsFor(document)` and the control's `data-uix-id`.
|
|
5
|
+
*/
|
|
6
|
+
import { UixElement } from './element.js';
|
|
7
|
+
import { StepperHandle } from './stepper.js';
|
|
8
|
+
import { ToggleHandle } from './toggle.js';
|
|
9
|
+
import { ExpandableLabelHandle } from './expandable-label.js';
|
|
10
|
+
import { LogViewHandle } from './log-view.js';
|
|
11
|
+
export type ControlHandle = StepperHandle | ToggleHandle | ExpandableLabelHandle | LogViewHandle;
|
|
12
|
+
export declare class PanelControls {
|
|
13
|
+
private handles;
|
|
14
|
+
private anonymous;
|
|
15
|
+
add(id: string | undefined, handle: ControlHandle): void;
|
|
16
|
+
/** Typed accessors - throw on wrong id so mistakes surface immediately. */
|
|
17
|
+
stepper(id: string): StepperHandle;
|
|
18
|
+
toggle(id: string): ToggleHandle;
|
|
19
|
+
expandableLabel(id: string): ExpandableLabelHandle;
|
|
20
|
+
logView(id: string): LogViewHandle;
|
|
21
|
+
get(id: string): ControlHandle | undefined;
|
|
22
|
+
ids(): string[];
|
|
23
|
+
private expect;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Upgrade every `data-uix` control under `root` and remember the result
|
|
27
|
+
* against `documentKey` (the panel's `UIKitDocument`). Idempotent per key.
|
|
28
|
+
*/
|
|
29
|
+
export declare function upgradePanel(documentKey: object, root: UixElement): PanelControls;
|
|
30
|
+
/** Controls of an upgraded panel; undefined before `UIControlsSystem` ran. */
|
|
31
|
+
export declare function panelControlsFor(documentKey: object): PanelControls | undefined;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel upgrade - scans a loaded panel for `data-uix` markers and wires the
|
|
3
|
+
* matching control behaviour. App code retrieves the resulting handles via
|
|
4
|
+
* `panelControlsFor(document)` and the control's `data-uix-id`.
|
|
5
|
+
*/
|
|
6
|
+
import { attrString, walk } from './element.js';
|
|
7
|
+
import { StepperHandle, upgradeStepper } from './stepper.js';
|
|
8
|
+
import { ToggleHandle, upgradeToggle } from './toggle.js';
|
|
9
|
+
import { ExpandableLabelHandle, upgradeExpandableLabel } from './expandable-label.js';
|
|
10
|
+
import { LogViewHandle, upgradeLogView } from './log-view.js';
|
|
11
|
+
export class PanelControls {
|
|
12
|
+
handles = new Map();
|
|
13
|
+
anonymous = 0;
|
|
14
|
+
add(id, handle) {
|
|
15
|
+
const key = id ?? `uix-control-${this.anonymous++}`;
|
|
16
|
+
if (this.handles.has(key)) {
|
|
17
|
+
throw new Error(`[uix] duplicate control id "${key}" in panel`);
|
|
18
|
+
}
|
|
19
|
+
this.handles.set(key, handle);
|
|
20
|
+
}
|
|
21
|
+
/** Typed accessors - throw on wrong id so mistakes surface immediately. */
|
|
22
|
+
stepper(id) {
|
|
23
|
+
return this.expect(id, StepperHandle);
|
|
24
|
+
}
|
|
25
|
+
toggle(id) {
|
|
26
|
+
return this.expect(id, ToggleHandle);
|
|
27
|
+
}
|
|
28
|
+
expandableLabel(id) {
|
|
29
|
+
return this.expect(id, ExpandableLabelHandle);
|
|
30
|
+
}
|
|
31
|
+
logView(id) {
|
|
32
|
+
return this.expect(id, LogViewHandle);
|
|
33
|
+
}
|
|
34
|
+
get(id) {
|
|
35
|
+
return this.handles.get(id);
|
|
36
|
+
}
|
|
37
|
+
ids() {
|
|
38
|
+
return [...this.handles.keys()];
|
|
39
|
+
}
|
|
40
|
+
expect(id, kind) {
|
|
41
|
+
const handle = this.handles.get(id);
|
|
42
|
+
if (!handle) {
|
|
43
|
+
throw new Error(`[uix] no control "${id}" in panel (have: ${this.ids().join(', ') || 'none'})`);
|
|
44
|
+
}
|
|
45
|
+
if (!(handle instanceof kind)) {
|
|
46
|
+
throw new Error(`[uix] control "${id}" is not a ${kind.name}`);
|
|
47
|
+
}
|
|
48
|
+
return handle;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const UPGRADERS = {
|
|
52
|
+
stepper: upgradeStepper,
|
|
53
|
+
toggle: upgradeToggle,
|
|
54
|
+
'expandable-label': upgradeExpandableLabel,
|
|
55
|
+
'log-view': upgradeLogView,
|
|
56
|
+
};
|
|
57
|
+
const registry = new WeakMap();
|
|
58
|
+
/**
|
|
59
|
+
* Upgrade every `data-uix` control under `root` and remember the result
|
|
60
|
+
* against `documentKey` (the panel's `UIKitDocument`). Idempotent per key.
|
|
61
|
+
*/
|
|
62
|
+
export function upgradePanel(documentKey, root) {
|
|
63
|
+
const existing = registry.get(documentKey);
|
|
64
|
+
if (existing) {
|
|
65
|
+
return existing;
|
|
66
|
+
}
|
|
67
|
+
const controls = new PanelControls();
|
|
68
|
+
walk(root, (element) => {
|
|
69
|
+
const marker = attrString(element, 'uix');
|
|
70
|
+
if (marker === undefined) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const upgrader = UPGRADERS[marker];
|
|
74
|
+
if (!upgrader) {
|
|
75
|
+
console.warn(`[uix] unknown control type data-uix="${marker}" - skipped`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
controls.add(attrString(element, 'uixId'), upgrader(element));
|
|
79
|
+
});
|
|
80
|
+
registry.set(documentKey, controls);
|
|
81
|
+
return controls;
|
|
82
|
+
}
|
|
83
|
+
/** Controls of an upgraded panel; undefined before `UIControlsSystem` ran. */
|
|
84
|
+
export function panelControlsFor(documentKey) {
|
|
85
|
+
return registry.get(documentKey);
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=upgrade.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../src/controls/upgrade.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAc,UAAU,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAQ9D,MAAM,OAAO,aAAa;IAChB,OAAO,GAAG,IAAI,GAAG,EAAyB,CAAC;IAC3C,SAAS,GAAG,CAAC,CAAC;IAEtB,GAAG,CAAC,EAAsB,EAAE,MAAqB;QAC/C,MAAM,GAAG,GAAG,EAAE,IAAI,eAAe,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;QACpD,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,+BAA+B,GAAG,YAAY,CAAC,CAAC;QAClE,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAChC,CAAC;IAED,2EAA2E;IAC3E,OAAO,CAAC,EAAU;QAChB,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,aAAa,CAAC,CAAC;IACxC,CAAC;IAED,MAAM,CAAC,EAAU;QACf,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;IACvC,CAAC;IAED,eAAe,CAAC,EAAU;QACxB,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,qBAAqB,CAAC,CAAC;IAChD,CAAC;IAED,OAAO,CAAC,EAAU;QAChB,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,aAAa,CAAC,CAAC;IACxC,CAAC;IAED,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED,GAAG;QACD,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAClC,CAAC;IAEO,MAAM,CACZ,EAAU,EACV,IAA0C;QAE1C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,qBAAqB,EAAE,qBAAqB,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC;QAClG,CAAC;QACD,IAAI,CAAC,CAAC,MAAM,YAAY,IAAI,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,kBAAkB,EAAE,cAAc,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACjE,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAED,MAAM,SAAS,GAAwD;IACrE,OAAO,EAAE,cAAc;IACvB,MAAM,EAAE,aAAa;IACrB,kBAAkB,EAAE,sBAAsB;IAC1C,UAAU,EAAE,cAAc;CAC3B,CAAC;AAEF,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAyB,CAAC;AAEtD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,WAAmB,EAAE,IAAgB;IAChE,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC3C,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,aAAa,EAAE,CAAC;IACrC,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE;QACrB,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC1C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QACD,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;QACnC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CAAC,wCAAwC,MAAM,aAAa,CAAC,CAAC;YAC1E,OAAO;QACT,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IACH,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACpC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,gBAAgB,CAAC,WAAmB;IAClD,OAAO,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AACnC,CAAC","sourcesContent":["/**\n * Panel upgrade - scans a loaded panel for `data-uix` markers and wires the\n * matching control behaviour. App code retrieves the resulting handles via\n * `panelControlsFor(document)` and the control's `data-uix-id`.\n */\nimport { UixElement, attrString, walk } from './element.js';\nimport { StepperHandle, upgradeStepper } from './stepper.js';\nimport { ToggleHandle, upgradeToggle } from './toggle.js';\nimport { ExpandableLabelHandle, upgradeExpandableLabel } from './expandable-label.js';\nimport { LogViewHandle, upgradeLogView } from './log-view.js';\n\nexport type ControlHandle =\n | StepperHandle\n | ToggleHandle\n | ExpandableLabelHandle\n | LogViewHandle;\n\nexport class PanelControls {\n private handles = new Map<string, ControlHandle>();\n private anonymous = 0;\n\n add(id: string | undefined, handle: ControlHandle): void {\n const key = id ?? `uix-control-${this.anonymous++}`;\n if (this.handles.has(key)) {\n throw new Error(`[uix] duplicate control id \"${key}\" in panel`);\n }\n this.handles.set(key, handle);\n }\n\n /** Typed accessors - throw on wrong id so mistakes surface immediately. */\n stepper(id: string): StepperHandle {\n return this.expect(id, StepperHandle);\n }\n\n toggle(id: string): ToggleHandle {\n return this.expect(id, ToggleHandle);\n }\n\n expandableLabel(id: string): ExpandableLabelHandle {\n return this.expect(id, ExpandableLabelHandle);\n }\n\n logView(id: string): LogViewHandle {\n return this.expect(id, LogViewHandle);\n }\n\n get(id: string): ControlHandle | undefined {\n return this.handles.get(id);\n }\n\n ids(): string[] {\n return [...this.handles.keys()];\n }\n\n private expect<T extends ControlHandle>(\n id: string,\n kind: abstract new (...args: never[]) => T,\n ): T {\n const handle = this.handles.get(id);\n if (!handle) {\n throw new Error(`[uix] no control \"${id}\" in panel (have: ${this.ids().join(', ') || 'none'})`);\n }\n if (!(handle instanceof kind)) {\n throw new Error(`[uix] control \"${id}\" is not a ${kind.name}`);\n }\n return handle;\n }\n}\n\nconst UPGRADERS: Record<string, (root: UixElement) => ControlHandle> = {\n stepper: upgradeStepper,\n toggle: upgradeToggle,\n 'expandable-label': upgradeExpandableLabel,\n 'log-view': upgradeLogView,\n};\n\nconst registry = new WeakMap<object, PanelControls>();\n\n/**\n * Upgrade every `data-uix` control under `root` and remember the result\n * against `documentKey` (the panel's `UIKitDocument`). Idempotent per key.\n */\nexport function upgradePanel(documentKey: object, root: UixElement): PanelControls {\n const existing = registry.get(documentKey);\n if (existing) {\n return existing;\n }\n const controls = new PanelControls();\n walk(root, (element) => {\n const marker = attrString(element, 'uix');\n if (marker === undefined) {\n return;\n }\n const upgrader = UPGRADERS[marker];\n if (!upgrader) {\n console.warn(`[uix] unknown control type data-uix=\"${marker}\" - skipped`);\n return;\n }\n controls.add(attrString(element, 'uixId'), upgrader(element));\n });\n registry.set(documentKey, controls);\n return controls;\n}\n\n/** Controls of an upgraded panel; undefined before `UIControlsSystem` ran. */\nexport function panelControlsFor(documentKey: object): PanelControls | undefined {\n return registry.get(documentKey);\n}\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dock state machine - pure logic, no engine imports.
|
|
3
|
+
*
|
|
4
|
+
* A window is always in exactly one dock mode:
|
|
5
|
+
*
|
|
6
|
+
* - `world-locked` - placed in space; the window keeps its world transform.
|
|
7
|
+
* - `body-follow` - lazily follows the player (IWSDK `Follower` on the head),
|
|
8
|
+
* with a deadzone so it doesn't jitter.
|
|
9
|
+
* - `head-locked` - rigidly attached to the view (IWSDK `ScreenSpace` outside
|
|
10
|
+
* XR / a zero-tolerance follow inside XR). Use sparingly.
|
|
11
|
+
*
|
|
12
|
+
* The state machine itself only decides WHICH engine ingredients a mode needs;
|
|
13
|
+
* `DockSystem` applies them (adding/removing `Follower` / `ScreenSpace`).
|
|
14
|
+
* Keeping the decision pure makes every transition unit-testable without a
|
|
15
|
+
* headset or a renderer.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DockMode: {
|
|
18
|
+
readonly WorldLocked: "world-locked";
|
|
19
|
+
readonly BodyFollow: "body-follow";
|
|
20
|
+
readonly HeadLocked: "head-locked";
|
|
21
|
+
};
|
|
22
|
+
export type DockModeValue = (typeof DockMode)[keyof typeof DockMode];
|
|
23
|
+
/** Engine-agnostic description of what a dock mode requires. */
|
|
24
|
+
export interface DockRecipe {
|
|
25
|
+
/** Requires an IWSDK `Follower` targeting the player's head. */
|
|
26
|
+
follower: boolean;
|
|
27
|
+
/** Requires an IWSDK `ScreenSpace` component (non-XR HUD lock). */
|
|
28
|
+
screenSpace: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* When true the window should re-sync its follow position immediately on
|
|
31
|
+
* entering the mode (jump to the ideal spot instead of drifting there).
|
|
32
|
+
*/
|
|
33
|
+
snapOnEnter: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function isDockMode(value: unknown): value is DockModeValue;
|
|
36
|
+
export declare function recipeFor(mode: DockModeValue): DockRecipe;
|
|
37
|
+
/** A transition plan: what to add and what to remove, in engine terms. */
|
|
38
|
+
export interface DockTransition {
|
|
39
|
+
from: DockModeValue;
|
|
40
|
+
to: DockModeValue;
|
|
41
|
+
addFollower: boolean;
|
|
42
|
+
removeFollower: boolean;
|
|
43
|
+
addScreenSpace: boolean;
|
|
44
|
+
removeScreenSpace: boolean;
|
|
45
|
+
snap: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compute the minimal set of engine changes to move between dock modes.
|
|
49
|
+
* Returns `undefined` for a no-op (same mode).
|
|
50
|
+
*/
|
|
51
|
+
export declare function planTransition(from: DockModeValue, to: DockModeValue): DockTransition | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The "pin" affordance on a window's title bar toggles between following the
|
|
54
|
+
* player and being placed in space. Head-locked windows unpin to world-locked
|
|
55
|
+
* too - pinning something rigidly to the user's face is never the toggle
|
|
56
|
+
* target you want.
|
|
57
|
+
*/
|
|
58
|
+
export declare function togglePinned(mode: DockModeValue): DockModeValue;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dock state machine - pure logic, no engine imports.
|
|
3
|
+
*
|
|
4
|
+
* A window is always in exactly one dock mode:
|
|
5
|
+
*
|
|
6
|
+
* - `world-locked` - placed in space; the window keeps its world transform.
|
|
7
|
+
* - `body-follow` - lazily follows the player (IWSDK `Follower` on the head),
|
|
8
|
+
* with a deadzone so it doesn't jitter.
|
|
9
|
+
* - `head-locked` - rigidly attached to the view (IWSDK `ScreenSpace` outside
|
|
10
|
+
* XR / a zero-tolerance follow inside XR). Use sparingly.
|
|
11
|
+
*
|
|
12
|
+
* The state machine itself only decides WHICH engine ingredients a mode needs;
|
|
13
|
+
* `DockSystem` applies them (adding/removing `Follower` / `ScreenSpace`).
|
|
14
|
+
* Keeping the decision pure makes every transition unit-testable without a
|
|
15
|
+
* headset or a renderer.
|
|
16
|
+
*/
|
|
17
|
+
export const DockMode = {
|
|
18
|
+
WorldLocked: 'world-locked',
|
|
19
|
+
BodyFollow: 'body-follow',
|
|
20
|
+
HeadLocked: 'head-locked',
|
|
21
|
+
};
|
|
22
|
+
const RECIPES = {
|
|
23
|
+
[DockMode.WorldLocked]: { follower: false, screenSpace: false, snapOnEnter: false },
|
|
24
|
+
[DockMode.BodyFollow]: { follower: true, screenSpace: false, snapOnEnter: true },
|
|
25
|
+
[DockMode.HeadLocked]: { follower: true, screenSpace: true, snapOnEnter: true },
|
|
26
|
+
};
|
|
27
|
+
export function isDockMode(value) {
|
|
28
|
+
return (value === DockMode.WorldLocked ||
|
|
29
|
+
value === DockMode.BodyFollow ||
|
|
30
|
+
value === DockMode.HeadLocked);
|
|
31
|
+
}
|
|
32
|
+
export function recipeFor(mode) {
|
|
33
|
+
return RECIPES[mode];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Compute the minimal set of engine changes to move between dock modes.
|
|
37
|
+
* Returns `undefined` for a no-op (same mode).
|
|
38
|
+
*/
|
|
39
|
+
export function planTransition(from, to) {
|
|
40
|
+
if (from === to) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const a = recipeFor(from);
|
|
44
|
+
const b = recipeFor(to);
|
|
45
|
+
return {
|
|
46
|
+
from,
|
|
47
|
+
to,
|
|
48
|
+
addFollower: !a.follower && b.follower,
|
|
49
|
+
removeFollower: a.follower && !b.follower,
|
|
50
|
+
addScreenSpace: !a.screenSpace && b.screenSpace,
|
|
51
|
+
removeScreenSpace: a.screenSpace && !b.screenSpace,
|
|
52
|
+
snap: b.snapOnEnter,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The "pin" affordance on a window's title bar toggles between following the
|
|
57
|
+
* player and being placed in space. Head-locked windows unpin to world-locked
|
|
58
|
+
* too - pinning something rigidly to the user's face is never the toggle
|
|
59
|
+
* target you want.
|
|
60
|
+
*/
|
|
61
|
+
export function togglePinned(mode) {
|
|
62
|
+
return mode === DockMode.WorldLocked ? DockMode.BodyFollow : DockMode.WorldLocked;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=dock-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dock-state.js","sourceRoot":"","sources":["../../src/core/dock-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,WAAW,EAAE,cAAc;IAC3B,UAAU,EAAE,aAAa;IACzB,UAAU,EAAE,aAAa;CACjB,CAAC;AAiBX,MAAM,OAAO,GAAsC;IACjD,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE;IACnF,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE;IAChF,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE;CAChF,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,CACL,KAAK,KAAK,QAAQ,CAAC,WAAW;QAC9B,KAAK,KAAK,QAAQ,CAAC,UAAU;QAC7B,KAAK,KAAK,QAAQ,CAAC,UAAU,CAC9B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAmB;IAC3C,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;AACvB,CAAC;AAaD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAmB,EACnB,EAAiB;IAEjB,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;QAChB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC1B,MAAM,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;IACxB,OAAO;QACL,IAAI;QACJ,EAAE;QACF,WAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ;QACtC,cAAc,EAAE,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,QAAQ;QACzC,cAAc,EAAE,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,WAAW;QAC/C,iBAAiB,EAAE,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC,WAAW;QAClD,IAAI,EAAE,CAAC,CAAC,WAAW;KACpB,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,IAAmB;IAC9C,OAAO,IAAI,KAAK,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;AACpF,CAAC","sourcesContent":["/**\n * Dock state machine - pure logic, no engine imports.\n *\n * A window is always in exactly one dock mode:\n *\n * - `world-locked` - placed in space; the window keeps its world transform.\n * - `body-follow` - lazily follows the player (IWSDK `Follower` on the head),\n * with a deadzone so it doesn't jitter.\n * - `head-locked` - rigidly attached to the view (IWSDK `ScreenSpace` outside\n * XR / a zero-tolerance follow inside XR). Use sparingly.\n *\n * The state machine itself only decides WHICH engine ingredients a mode needs;\n * `DockSystem` applies them (adding/removing `Follower` / `ScreenSpace`).\n * Keeping the decision pure makes every transition unit-testable without a\n * headset or a renderer.\n */\n\nexport const DockMode = {\n WorldLocked: 'world-locked',\n BodyFollow: 'body-follow',\n HeadLocked: 'head-locked',\n} as const;\n\nexport type DockModeValue = (typeof DockMode)[keyof typeof DockMode];\n\n/** Engine-agnostic description of what a dock mode requires. */\nexport interface DockRecipe {\n /** Requires an IWSDK `Follower` targeting the player's head. */\n follower: boolean;\n /** Requires an IWSDK `ScreenSpace` component (non-XR HUD lock). */\n screenSpace: boolean;\n /**\n * When true the window should re-sync its follow position immediately on\n * entering the mode (jump to the ideal spot instead of drifting there).\n */\n snapOnEnter: boolean;\n}\n\nconst RECIPES: Record<DockModeValue, DockRecipe> = {\n [DockMode.WorldLocked]: { follower: false, screenSpace: false, snapOnEnter: false },\n [DockMode.BodyFollow]: { follower: true, screenSpace: false, snapOnEnter: true },\n [DockMode.HeadLocked]: { follower: true, screenSpace: true, snapOnEnter: true },\n};\n\nexport function isDockMode(value: unknown): value is DockModeValue {\n return (\n value === DockMode.WorldLocked ||\n value === DockMode.BodyFollow ||\n value === DockMode.HeadLocked\n );\n}\n\nexport function recipeFor(mode: DockModeValue): DockRecipe {\n return RECIPES[mode];\n}\n\n/** A transition plan: what to add and what to remove, in engine terms. */\nexport interface DockTransition {\n from: DockModeValue;\n to: DockModeValue;\n addFollower: boolean;\n removeFollower: boolean;\n addScreenSpace: boolean;\n removeScreenSpace: boolean;\n snap: boolean;\n}\n\n/**\n * Compute the minimal set of engine changes to move between dock modes.\n * Returns `undefined` for a no-op (same mode).\n */\nexport function planTransition(\n from: DockModeValue,\n to: DockModeValue,\n): DockTransition | undefined {\n if (from === to) {\n return undefined;\n }\n const a = recipeFor(from);\n const b = recipeFor(to);\n return {\n from,\n to,\n addFollower: !a.follower && b.follower,\n removeFollower: a.follower && !b.follower,\n addScreenSpace: !a.screenSpace && b.screenSpace,\n removeScreenSpace: a.screenSpace && !b.screenSpace,\n snap: b.snapOnEnter,\n };\n}\n\n/**\n * The \"pin\" affordance on a window's title bar toggles between following the\n * player and being placed in space. Head-locked windows unpin to world-locked\n * too - pinning something rigidly to the user's face is never the toggle\n * target you want.\n */\nexport function togglePinned(mode: DockModeValue): DockModeValue {\n return mode === DockMode.WorldLocked ? DockMode.BodyFollow : DockMode.WorldLocked;\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Laser-drag math - pure vector logic for title-bar window dragging.
|
|
3
|
+
*
|
|
4
|
+
* The interaction model matches the Quest system shell: grab the title bar
|
|
5
|
+
* with the ray, and the window rides the ray at the distance it was grabbed
|
|
6
|
+
* at, yawing to keep facing the user. All math is on plain `[x, y, z]`
|
|
7
|
+
* tuples so it is trivially unit-testable; the ECS `DragSystem` shuttles
|
|
8
|
+
* three.js vectors in and out.
|
|
9
|
+
*/
|
|
10
|
+
import type { Vec3 } from './region-layout.js';
|
|
11
|
+
export interface DragSession {
|
|
12
|
+
/** Ray-hit distance at grab time; the window keeps riding at this range. */
|
|
13
|
+
distance: number;
|
|
14
|
+
/** Window origin minus grab point, world space, captured at grab time. */
|
|
15
|
+
holdOffset: Vec3;
|
|
16
|
+
}
|
|
17
|
+
export declare function normalize(v: Vec3): Vec3;
|
|
18
|
+
/**
|
|
19
|
+
* Begin a drag: capture how far along the ray the grab landed and where the
|
|
20
|
+
* window origin sits relative to the grab point.
|
|
21
|
+
*/
|
|
22
|
+
export declare function beginDrag(rayOrigin: Vec3, grabPoint: Vec3, windowPosition: Vec3): DragSession;
|
|
23
|
+
/**
|
|
24
|
+
* Window position for the current ray. `rayDirection` must be normalized
|
|
25
|
+
* (pointer rays from the input layer already are).
|
|
26
|
+
*/
|
|
27
|
+
export declare function dragPosition(session: DragSession, rayOrigin: Vec3, rayDirection: Vec3): Vec3;
|
|
28
|
+
/**
|
|
29
|
+
* Yaw (radians, three.js Y-up convention) that turns the window's -Z face
|
|
30
|
+
* toward `viewer` - the "PivotY" billboard used while dragging so a window
|
|
31
|
+
* never gets parked edge-on. Returns `current` when the viewer is directly
|
|
32
|
+
* above/below (degenerate on the Y axis).
|
|
33
|
+
*/
|
|
34
|
+
export declare function faceViewerYaw(windowPosition: Vec3, viewer: Vec3, current: number): number;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
2
|
+
const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
3
|
+
const scale = (v, s) => [v[0] * s, v[1] * s, v[2] * s];
|
|
4
|
+
const length = (v) => Math.hypot(v[0], v[1], v[2]);
|
|
5
|
+
export function normalize(v) {
|
|
6
|
+
const len = length(v);
|
|
7
|
+
if (len === 0) {
|
|
8
|
+
throw new Error('[uix] cannot normalize a zero-length vector');
|
|
9
|
+
}
|
|
10
|
+
return scale(v, 1 / len);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Begin a drag: capture how far along the ray the grab landed and where the
|
|
14
|
+
* window origin sits relative to the grab point.
|
|
15
|
+
*/
|
|
16
|
+
export function beginDrag(rayOrigin, grabPoint, windowPosition) {
|
|
17
|
+
return {
|
|
18
|
+
distance: length(sub(grabPoint, rayOrigin)),
|
|
19
|
+
holdOffset: sub(windowPosition, grabPoint),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Window position for the current ray. `rayDirection` must be normalized
|
|
24
|
+
* (pointer rays from the input layer already are).
|
|
25
|
+
*/
|
|
26
|
+
export function dragPosition(session, rayOrigin, rayDirection) {
|
|
27
|
+
const point = add(rayOrigin, scale(rayDirection, session.distance));
|
|
28
|
+
return add(point, session.holdOffset);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Yaw (radians, three.js Y-up convention) that turns the window's -Z face
|
|
32
|
+
* toward `viewer` - the "PivotY" billboard used while dragging so a window
|
|
33
|
+
* never gets parked edge-on. Returns `current` when the viewer is directly
|
|
34
|
+
* above/below (degenerate on the Y axis).
|
|
35
|
+
*/
|
|
36
|
+
export function faceViewerYaw(windowPosition, viewer, current) {
|
|
37
|
+
const dx = viewer[0] - windowPosition[0];
|
|
38
|
+
const dz = viewer[2] - windowPosition[2];
|
|
39
|
+
if (dx === 0 && dz === 0) {
|
|
40
|
+
return current;
|
|
41
|
+
}
|
|
42
|
+
return Math.atan2(dx, dz);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=drag-math.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drag-math.js","sourceRoot":"","sources":["../../src/core/drag-math.ts"],"names":[],"mappings":"AAkBA,MAAM,GAAG,GAAG,CAAC,CAAO,EAAE,CAAO,EAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChF,MAAM,GAAG,GAAG,CAAC,CAAO,EAAE,CAAO,EAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChF,MAAM,KAAK,GAAG,CAAC,CAAO,EAAE,CAAS,EAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3E,MAAM,MAAM,GAAG,CAAC,CAAO,EAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEjE,MAAM,UAAU,SAAS,CAAC,CAAO;IAC/B,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACtB,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,SAAe,EAAE,SAAe,EAAE,cAAoB;IAC9E,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC3C,UAAU,EAAE,GAAG,CAAC,cAAc,EAAE,SAAS,CAAC;KAC3C,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,OAAoB,EAAE,SAAe,EAAE,YAAkB;IACpF,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,OAAO,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,cAAoB,EAAE,MAAY,EAAE,OAAe;IAC/E,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;IACzC,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;IACzC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;AAC5B,CAAC","sourcesContent":["/**\n * Laser-drag math - pure vector logic for title-bar window dragging.\n *\n * The interaction model matches the Quest system shell: grab the title bar\n * with the ray, and the window rides the ray at the distance it was grabbed\n * at, yawing to keep facing the user. All math is on plain `[x, y, z]`\n * tuples so it is trivially unit-testable; the ECS `DragSystem` shuttles\n * three.js vectors in and out.\n */\nimport type { Vec3 } from './region-layout.js';\n\nexport interface DragSession {\n /** Ray-hit distance at grab time; the window keeps riding at this range. */\n distance: number;\n /** Window origin minus grab point, world space, captured at grab time. */\n holdOffset: Vec3;\n}\n\nconst sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];\nconst add = (a: Vec3, b: Vec3): Vec3 => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];\nconst scale = (v: Vec3, s: number): Vec3 => [v[0] * s, v[1] * s, v[2] * s];\nconst length = (v: Vec3): number => Math.hypot(v[0], v[1], v[2]);\n\nexport function normalize(v: Vec3): Vec3 {\n const len = length(v);\n if (len === 0) {\n throw new Error('[uix] cannot normalize a zero-length vector');\n }\n return scale(v, 1 / len);\n}\n\n/**\n * Begin a drag: capture how far along the ray the grab landed and where the\n * window origin sits relative to the grab point.\n */\nexport function beginDrag(rayOrigin: Vec3, grabPoint: Vec3, windowPosition: Vec3): DragSession {\n return {\n distance: length(sub(grabPoint, rayOrigin)),\n holdOffset: sub(windowPosition, grabPoint),\n };\n}\n\n/**\n * Window position for the current ray. `rayDirection` must be normalized\n * (pointer rays from the input layer already are).\n */\nexport function dragPosition(session: DragSession, rayOrigin: Vec3, rayDirection: Vec3): Vec3 {\n const point = add(rayOrigin, scale(rayDirection, session.distance));\n return add(point, session.holdOffset);\n}\n\n/**\n * Yaw (radians, three.js Y-up convention) that turns the window's -Z face\n * toward `viewer` - the \"PivotY\" billboard used while dragging so a window\n * never gets parked edge-on. Returns `current` when the viewer is directly\n * above/below (degenerate on the Y axis).\n */\nexport function faceViewerYaw(windowPosition: Vec3, viewer: Vec3, current: number): number {\n const dx = viewer[0] - windowPosition[0];\n const dz = viewer[2] - windowPosition[2];\n if (dx === 0 && dz === 0) {\n return current;\n }\n return Math.atan2(dx, dz);\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal typed event emitter.
|
|
3
|
+
*
|
|
4
|
+
* The library keeps its observable surface deliberately tiny - consumers that
|
|
5
|
+
* want richer eventing can bridge these into their own bus (for example the
|
|
6
|
+
* Reality Collective service framework's IEventService).
|
|
7
|
+
*/
|
|
8
|
+
export type Listener<T> = (payload: T) => void;
|
|
9
|
+
export declare class Emitter<TEvents extends Record<string, unknown>> {
|
|
10
|
+
private listeners;
|
|
11
|
+
on<K extends keyof TEvents>(event: K, listener: Listener<TEvents[K]>): () => void;
|
|
12
|
+
once<K extends keyof TEvents>(event: K, listener: Listener<TEvents[K]>): () => void;
|
|
13
|
+
off<K extends keyof TEvents>(event: K, listener: Listener<TEvents[K]>): void;
|
|
14
|
+
emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void;
|
|
15
|
+
listenerCount<K extends keyof TEvents>(event: K): number;
|
|
16
|
+
clear(): void;
|
|
17
|
+
}
|