@preference-sl/pref-viewer 2.14.0-beta.0 → 2.14.0-beta.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/package.json +1 -1
- package/src/babylonjs-animation-controller.js +119 -3
- package/src/babylonjs-animation-opening.js +59 -0
- package/src/babylonjs-controller.js +113 -0
- package/src/localization/translations.js +16 -0
- package/src/pref-viewer-3d.js +92 -2
- package/src/pref-viewer-menu-3d.js +125 -6
- package/src/pref-viewer.js +128 -1
- package/src/styles.js +97 -1
package/package.json
CHANGED
|
@@ -45,15 +45,22 @@ export default class BabylonJSAnimationController {
|
|
|
45
45
|
#highlightColor = Color3.FromHexString(PrefViewerStyleVariables.colorPrimary);
|
|
46
46
|
#highlightLayer = null;
|
|
47
47
|
#overlayLayer = null;
|
|
48
|
-
#useHighlightLayer = true;
|
|
49
|
-
#lastHighlightedMeshId = null;
|
|
50
|
-
#lastHighlightedNodeIds = [];
|
|
48
|
+
#useHighlightLayer = true;
|
|
49
|
+
#lastHighlightedMeshId = null;
|
|
50
|
+
#lastHighlightedNodeIds = [];
|
|
51
|
+
|
|
52
|
+
// Static property to store global highlight enabled state
|
|
53
|
+
static #globalHighlightEnabled = true;
|
|
54
|
+
static #instanceId = 0;
|
|
55
|
+
#thisInstanceId = 0;
|
|
51
56
|
|
|
52
57
|
/**
|
|
53
58
|
* Creates a new BabylonJSAnimationController for a Babylon.js scene.
|
|
54
59
|
* @param {AssetContainer|Scene} assetContainer - The Babylon.js asset container or scene instance.
|
|
55
60
|
*/
|
|
56
61
|
constructor(assetContainer) {
|
|
62
|
+
BabylonJSAnimationController.#instanceId++;
|
|
63
|
+
this.#thisInstanceId = BabylonJSAnimationController.#instanceId;
|
|
57
64
|
this.#scene = assetContainer.scene ? assetContainer.scene : assetContainer;
|
|
58
65
|
this.#assetContainer = assetContainer;
|
|
59
66
|
this.#canvas = this.#scene._engine._renderingCanvas;
|
|
@@ -459,6 +466,17 @@ export default class BabylonJSAnimationController {
|
|
|
459
466
|
* Returns whether highlight visuals changed in this call and whether any mesh remains highlighted.
|
|
460
467
|
*/
|
|
461
468
|
highlightMeshes(pickingInfo) {
|
|
469
|
+
// If highlight is disabled globally, just remove any existing highlight and return
|
|
470
|
+
if (!BabylonJSAnimationController.#globalHighlightEnabled) {
|
|
471
|
+
if (this.#lastHighlightedMeshId !== null) {
|
|
472
|
+
this.#removeHighlight();
|
|
473
|
+
this.#lastHighlightedMeshId = null;
|
|
474
|
+
this.#lastHighlightedNodeIds = [];
|
|
475
|
+
return { changed: true, highlighted: false };
|
|
476
|
+
}
|
|
477
|
+
return { changed: false, highlighted: false };
|
|
478
|
+
}
|
|
479
|
+
|
|
462
480
|
let changed = false;
|
|
463
481
|
let highlighted = false;
|
|
464
482
|
|
|
@@ -504,6 +522,40 @@ export default class BabylonJSAnimationController {
|
|
|
504
522
|
return { changed: changed, highlighted: highlighted };
|
|
505
523
|
}
|
|
506
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Configures the highlight settings for animated nodes on hover.
|
|
527
|
+
* @public
|
|
528
|
+
* @param {{enabled?: boolean, color?: string}} config - Configuration object.
|
|
529
|
+
* @param {boolean} [config.enabled] - Whether to enable highlight on hover. If false, removes highlight.
|
|
530
|
+
* @param {string} [config.color] - Hex color string for the highlight (e.g., "#FF0000"). Only used if enabled is not false.
|
|
531
|
+
* @returns {void}
|
|
532
|
+
*/
|
|
533
|
+
setHighlightConfig(config = {}) {
|
|
534
|
+
// Handle enabled state
|
|
535
|
+
if (config.enabled !== undefined) {
|
|
536
|
+
BabylonJSAnimationController.#globalHighlightEnabled = config.enabled;
|
|
537
|
+
if (config.enabled === false) {
|
|
538
|
+
this.#removeHighlight();
|
|
539
|
+
this.#lastHighlightedMeshId = null;
|
|
540
|
+
this.#lastHighlightedNodeIds = [];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Update color regardless of enabled state so it's ready when re-enabled
|
|
545
|
+
if (config.color) {
|
|
546
|
+
try {
|
|
547
|
+
this.#highlightColor = Color3.FromHexString(config.color);
|
|
548
|
+
} catch (e) {
|
|
549
|
+
console.warn("Invalid highlight color:", config.color);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
// Re-apply highlight with new color if meshes are currently highlighted
|
|
553
|
+
if (BabylonJSAnimationController.#globalHighlightEnabled && this.#lastHighlightedNodeIds.length > 0) {
|
|
554
|
+
this.#addHighlight(this.#lastHighlightedNodeIds);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
507
559
|
/**
|
|
508
560
|
* Hides and disposes the animation control menu if it exists.
|
|
509
561
|
* @public
|
|
@@ -514,6 +566,70 @@ export default class BabylonJSAnimationController {
|
|
|
514
566
|
this.#canvas?.parentElement?.querySelectorAll("div.pref-viewer-3d.animation-menu").forEach((menu) => menu.remove());
|
|
515
567
|
}
|
|
516
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Returns a snapshot of all available animations and their current state.
|
|
571
|
+
* @public
|
|
572
|
+
* @returns {Array<{name: string, state: number, progress: number, loop: boolean, nodes: string[]}>}
|
|
573
|
+
*/
|
|
574
|
+
getAnimations() {
|
|
575
|
+
return this.#openingAnimations.map((anim) => ({
|
|
576
|
+
name: anim.name,
|
|
577
|
+
state: anim.state,
|
|
578
|
+
progress: anim.progress,
|
|
579
|
+
loop: anim.loop,
|
|
580
|
+
nodes: anim.nodes ?? [],
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Controls a named animation by applying the specified action.
|
|
586
|
+
* @public
|
|
587
|
+
* @param {string} name - The name of the animation to control.
|
|
588
|
+
* @param {"open"|"close"|"pause"|"goToOpened"|"goToClosed"} action - The action to apply.
|
|
589
|
+
* @returns {boolean} True if the animation was found and the action was applied; otherwise false.
|
|
590
|
+
*/
|
|
591
|
+
playAnimation(name, action) {
|
|
592
|
+
const anim = this.#openingAnimations.find((a) => a.name === name);
|
|
593
|
+
if (!anim) return false;
|
|
594
|
+
switch (action) {
|
|
595
|
+
case "open": anim.playOpen(); break;
|
|
596
|
+
case "close": anim.playClose(); break;
|
|
597
|
+
case "pause": anim.pause(); break;
|
|
598
|
+
case "goToOpened": anim.goToOpened(); break;
|
|
599
|
+
case "goToClosed": anim.goToClosed(); break;
|
|
600
|
+
default: return false;
|
|
601
|
+
}
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Seeks a named animation to the given normalized progress position (0–1).
|
|
607
|
+
* @public
|
|
608
|
+
* @param {string} name - The name of the animation to seek.
|
|
609
|
+
* @param {number} progress - Progress value between 0 and 1.
|
|
610
|
+
* @returns {boolean} True if the animation was found and seeked; otherwise false.
|
|
611
|
+
*/
|
|
612
|
+
setAnimationProgress(name, progress) {
|
|
613
|
+
const anim = this.#openingAnimations.find((a) => a.name === name);
|
|
614
|
+
if (!anim) return false;
|
|
615
|
+
anim.setProgress(progress);
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Enables or disables loop mode for a named animation.
|
|
621
|
+
* @public
|
|
622
|
+
* @param {string} name - The name of the animation to configure.
|
|
623
|
+
* @param {boolean} loop - True to enable looping; false to disable.
|
|
624
|
+
* @returns {boolean} True if the animation was found and loop mode was set; otherwise false.
|
|
625
|
+
*/
|
|
626
|
+
setAnimationLoop(name, loop) {
|
|
627
|
+
const anim = this.#openingAnimations.find((a) => a.name === name);
|
|
628
|
+
if (!anim) return false;
|
|
629
|
+
anim.setLoop(loop);
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
|
|
517
633
|
/**
|
|
518
634
|
* Displays the animation control menu for the animated node under the pointer.
|
|
519
635
|
* Hides the current menu when picking info is invalid or when no animated node is found.
|
|
@@ -589,4 +589,63 @@ export default class OpeningAnimation {
|
|
|
589
589
|
get state() {
|
|
590
590
|
return this.#state;
|
|
591
591
|
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Returns the normalized progress (0–1) of the current animation.
|
|
595
|
+
* @public
|
|
596
|
+
* @returns {number}
|
|
597
|
+
*/
|
|
598
|
+
get progress() {
|
|
599
|
+
return this.#getProgress();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Returns whether loop mode is currently enabled for this animation.
|
|
604
|
+
* @public
|
|
605
|
+
* @returns {boolean}
|
|
606
|
+
*/
|
|
607
|
+
get loop() {
|
|
608
|
+
return this.#loop;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Returns the node IDs affected by this animation.
|
|
613
|
+
* @public
|
|
614
|
+
* @returns {string[]}
|
|
615
|
+
*/
|
|
616
|
+
get nodes() {
|
|
617
|
+
return [...this.#nodes];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Seeks the animation to the given normalized progress position (0–1).
|
|
622
|
+
* Mirrors the same threshold/snap logic used by the animation control menu.
|
|
623
|
+
* @public
|
|
624
|
+
* @param {number} progress - Progress value between 0 and 1.
|
|
625
|
+
* @returns {void}
|
|
626
|
+
*/
|
|
627
|
+
setProgress(progress) {
|
|
628
|
+
progress = this.#checkProgress(progress);
|
|
629
|
+
if (progress === 0) {
|
|
630
|
+
this.goToClosed();
|
|
631
|
+
} else if (progress === 1) {
|
|
632
|
+
this.goToOpened();
|
|
633
|
+
} else {
|
|
634
|
+
const frame = this.#getFrameFromProgress(progress);
|
|
635
|
+
this.#goToFrameAndPause(frame);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Enables or disables loop mode for this animation.
|
|
641
|
+
* @public
|
|
642
|
+
* @param {boolean} loop - True to enable looping; false to disable.
|
|
643
|
+
* @returns {void}
|
|
644
|
+
*/
|
|
645
|
+
setLoop(loop) {
|
|
646
|
+
this.#loop = Boolean(loop);
|
|
647
|
+
if (this.#menu) {
|
|
648
|
+
this.#menu.animationLoop = this.#loop;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
592
651
|
}
|
|
@@ -85,6 +85,9 @@ export default class BabylonJSController {
|
|
|
85
85
|
ambientOcclusionEnabled: true,
|
|
86
86
|
iblEnabled: true,
|
|
87
87
|
shadowsEnabled: false,
|
|
88
|
+
// Highlight settings
|
|
89
|
+
highlightEnabled: true,
|
|
90
|
+
highlightColor: "#ff6700",
|
|
88
91
|
};
|
|
89
92
|
|
|
90
93
|
// Canvas HTML element
|
|
@@ -496,10 +499,16 @@ export default class BabylonJSController {
|
|
|
496
499
|
|
|
497
500
|
let changed = false;
|
|
498
501
|
Object.keys(settings).forEach((key) => {
|
|
502
|
+
// Handle boolean settings
|
|
499
503
|
if (typeof settings[key] === "boolean" && this.#settings[key] !== settings[key]) {
|
|
500
504
|
this.#settings[key] = settings[key];
|
|
501
505
|
changed = true;
|
|
502
506
|
}
|
|
507
|
+
// Handle string settings (like illuminationColor)
|
|
508
|
+
if (typeof settings[key] === "string" && this.#settings[key] !== settings[key]) {
|
|
509
|
+
this.#settings[key] = settings[key];
|
|
510
|
+
changed = true;
|
|
511
|
+
}
|
|
503
512
|
});
|
|
504
513
|
|
|
505
514
|
if (changed) {
|
|
@@ -526,9 +535,14 @@ export default class BabylonJSController {
|
|
|
526
535
|
}
|
|
527
536
|
const parsed = JSON.parse(serialized);
|
|
528
537
|
Object.keys(BabylonJSController.DEFAULT_RENDER_SETTINGS).forEach((key) => {
|
|
538
|
+
// Handle boolean settings
|
|
529
539
|
if (typeof parsed?.[key] === "boolean") {
|
|
530
540
|
this.#settings[key] = parsed[key];
|
|
531
541
|
}
|
|
542
|
+
// Handle string settings (like illuminationColor)
|
|
543
|
+
if (typeof parsed?.[key] === "string") {
|
|
544
|
+
this.#settings[key] = parsed[key];
|
|
545
|
+
}
|
|
532
546
|
});
|
|
533
547
|
} catch (error) {
|
|
534
548
|
console.warn("PrefViewer: unable to load render settings", error);
|
|
@@ -2458,6 +2472,11 @@ export default class BabylonJSController {
|
|
|
2458
2472
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
2459
2473
|
this.#setMaxSimultaneousLights();
|
|
2460
2474
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
2475
|
+
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
2476
|
+
this.#setHighlightConfig({
|
|
2477
|
+
enabled: this.#settings.highlightEnabled,
|
|
2478
|
+
color: this.#settings.highlightColor,
|
|
2479
|
+
});
|
|
2461
2480
|
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
2462
2481
|
this.#attachAnimationChangedListener();
|
|
2463
2482
|
}
|
|
@@ -3019,4 +3038,98 @@ export default class BabylonJSController {
|
|
|
3019
3038
|
async reloadWithCurrentSettings() {
|
|
3020
3039
|
return await this.#loadContainers();
|
|
3021
3040
|
}
|
|
3041
|
+
|
|
3042
|
+
/**
|
|
3043
|
+
* Sets highlight configuration for animation hover effects.
|
|
3044
|
+
* @public
|
|
3045
|
+
* @param {{showAnimationMenu?:boolean, highlightColor?:string, highlightEnabled?:boolean}} config - Highlight configuration.
|
|
3046
|
+
* @returns {void}
|
|
3047
|
+
*/
|
|
3048
|
+
setIlluminationConfig(config = {}) {
|
|
3049
|
+
|
|
3050
|
+
if (config.showAnimationMenu !== undefined) {
|
|
3051
|
+
// Handle show-animation-menu via the 3D component's parent
|
|
3052
|
+
// This is handled in pref-viewer-3d.js attributeChangedCallback
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
// Handle highlight color for animation hover
|
|
3056
|
+
if (config.highlightColor !== undefined || config.highlightEnabled !== undefined) {
|
|
3057
|
+
// Normalize highlightEnabled: accept boolean, string "true"/"false", or null
|
|
3058
|
+
let enabled = config.highlightEnabled;
|
|
3059
|
+
if (typeof enabled === "string") {
|
|
3060
|
+
enabled = enabled.toLowerCase() !== "false";
|
|
3061
|
+
} else if (enabled === null) {
|
|
3062
|
+
enabled = undefined;
|
|
3063
|
+
}
|
|
3064
|
+
// Keep #settings in sync so getRenderSettings() always reflects the real state
|
|
3065
|
+
if (enabled !== undefined) {
|
|
3066
|
+
this.#settings.highlightEnabled = enabled;
|
|
3067
|
+
}
|
|
3068
|
+
if (config.highlightColor !== undefined && config.highlightColor !== null) {
|
|
3069
|
+
this.#settings.highlightColor = config.highlightColor;
|
|
3070
|
+
}
|
|
3071
|
+
this.#setHighlightConfig({
|
|
3072
|
+
color: config.highlightColor ?? undefined,
|
|
3073
|
+
enabled,
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
// Persist settings
|
|
3078
|
+
this.#storeRenderSettings();
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
/**
|
|
3082
|
+
* Returns a snapshot of all available animations and their current state.
|
|
3083
|
+
* @public
|
|
3084
|
+
* @returns {Array<{name: string, state: number, progress: number, loop: boolean, nodes: string[]}>}
|
|
3085
|
+
*/
|
|
3086
|
+
getAnimations() {
|
|
3087
|
+
return this.#babylonJSAnimationController?.getAnimations() ?? [];
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
/**
|
|
3091
|
+
* Controls a named animation by applying the specified action.
|
|
3092
|
+
* @public
|
|
3093
|
+
* @param {string} name - The name of the animation to control.
|
|
3094
|
+
* @param {"open"|"close"|"pause"|"goToOpened"|"goToClosed"} action - The action to apply.
|
|
3095
|
+
* @returns {boolean} True if the animation was found and the action was applied; otherwise false.
|
|
3096
|
+
*/
|
|
3097
|
+
playAnimation(name, action) {
|
|
3098
|
+
return this.#babylonJSAnimationController?.playAnimation(name, action) ?? false;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
/**
|
|
3102
|
+
* Seeks a named animation to the given normalized progress position (0–1).
|
|
3103
|
+
* @public
|
|
3104
|
+
* @param {string} name - The name of the animation to seek.
|
|
3105
|
+
* @param {number} progress - Progress value between 0 and 1.
|
|
3106
|
+
* @returns {boolean} True if the animation was found and seeked; otherwise false.
|
|
3107
|
+
*/
|
|
3108
|
+
setAnimationProgress(name, progress) {
|
|
3109
|
+
return this.#babylonJSAnimationController?.setAnimationProgress(name, progress) ?? false;
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
/**
|
|
3113
|
+
* Enables or disables loop mode for a named animation.
|
|
3114
|
+
* @public
|
|
3115
|
+
* @param {string} name - The name of the animation to configure.
|
|
3116
|
+
* @param {boolean} loop - True to enable looping; false to disable.
|
|
3117
|
+
* @returns {boolean} True if the animation was found and loop mode was set; otherwise false.
|
|
3118
|
+
*/
|
|
3119
|
+
setAnimationLoop(name, loop) {
|
|
3120
|
+
return this.#babylonJSAnimationController?.setAnimationLoop(name, loop) ?? false;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
/**
|
|
3124
|
+
* Configures the highlight settings for animated nodes on hover.
|
|
3125
|
+
* @private
|
|
3126
|
+
* @param {{enabled?: boolean, color?: string}} config - Configuration object.
|
|
3127
|
+
* @returns {void}
|
|
3128
|
+
*/
|
|
3129
|
+
#setHighlightConfig(config = {}) {
|
|
3130
|
+
if (this.#babylonJSAnimationController && typeof this.#babylonJSAnimationController.setHighlightConfig === "function") {
|
|
3131
|
+
this.#babylonJSAnimationController.setHighlightConfig(config);
|
|
3132
|
+
this.#requestRender({ frames: 2 });
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3022
3135
|
}
|
|
@@ -29,6 +29,14 @@ export const translations = {
|
|
|
29
29
|
label: "Shadows",
|
|
30
30
|
helper: "Enables shadows.",
|
|
31
31
|
},
|
|
32
|
+
highlightEnabled: {
|
|
33
|
+
label: "Highlight",
|
|
34
|
+
helper: "Highlight animated elements on hover.",
|
|
35
|
+
},
|
|
36
|
+
highlightColor: {
|
|
37
|
+
label: "Highlight Color",
|
|
38
|
+
helper: "Color for hover highlight on animations.",
|
|
39
|
+
},
|
|
32
40
|
},
|
|
33
41
|
},
|
|
34
42
|
downloadDialog: {
|
|
@@ -79,6 +87,14 @@ export const translations = {
|
|
|
79
87
|
label: "Sombras",
|
|
80
88
|
helper: "Activa sombras.",
|
|
81
89
|
},
|
|
90
|
+
highlightEnabled: {
|
|
91
|
+
label: "Resaltado",
|
|
92
|
+
helper: "Resaltar elementos animados al pasar el ratón.",
|
|
93
|
+
},
|
|
94
|
+
highlightColor: {
|
|
95
|
+
label: "Color de resaltado",
|
|
96
|
+
helper: "Color del resaltado al pasar sobre animaciones.",
|
|
97
|
+
},
|
|
82
98
|
},
|
|
83
99
|
},
|
|
84
100
|
downloadDialog: {
|
package/src/pref-viewer-3d.js
CHANGED
|
@@ -97,7 +97,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
97
97
|
* @returns {string[]} Array of attribute names that trigger attributeChangedCallback.
|
|
98
98
|
*/
|
|
99
99
|
static get observedAttributes() {
|
|
100
|
-
return ["show-model", "show-scene", "visible"];
|
|
100
|
+
return ["show-model", "show-scene", "visible", "show-animation-menu", "highlight-color", "highlight-enabled"];
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
@@ -137,6 +137,18 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
137
137
|
this.hide();
|
|
138
138
|
}
|
|
139
139
|
break;
|
|
140
|
+
case "show-animation-menu":
|
|
141
|
+
case "highlight-color":
|
|
142
|
+
case "highlight-enabled":
|
|
143
|
+
// Pass these to BabylonJSController if initialized
|
|
144
|
+
if (this.#babylonJSController && typeof this.#babylonJSController.setIlluminationConfig === "function") {
|
|
145
|
+
this.#babylonJSController.setIlluminationConfig({
|
|
146
|
+
showAnimationMenu: this.getAttribute("show-animation-menu"),
|
|
147
|
+
highlightColor: this.getAttribute("highlight-color"),
|
|
148
|
+
highlightEnabled: this.getAttribute("highlight-enabled"),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
140
152
|
}
|
|
141
153
|
}
|
|
142
154
|
|
|
@@ -229,6 +241,16 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
229
241
|
#initializeBabylonJS() {
|
|
230
242
|
this.#babylonJSController = new BabylonJSController(this.#canvas, this.#data.containers, this.#data.options);
|
|
231
243
|
this.#babylonJSController.enable();
|
|
244
|
+
|
|
245
|
+
// Apply highlight attributes that were set before the controller was ready
|
|
246
|
+
const highlightColor = this.getAttribute("highlight-color");
|
|
247
|
+
const highlightEnabled = this.getAttribute("highlight-enabled");
|
|
248
|
+
if (highlightColor !== null || highlightEnabled !== null) {
|
|
249
|
+
this.#babylonJSController.setIlluminationConfig({
|
|
250
|
+
highlightColor: highlightColor ?? undefined,
|
|
251
|
+
highlightEnabled: highlightEnabled ?? undefined,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
232
254
|
}
|
|
233
255
|
|
|
234
256
|
/**
|
|
@@ -594,6 +616,11 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
594
616
|
|
|
595
617
|
const loadDetail = await this.#babylonJSController.load(forceReload);
|
|
596
618
|
|
|
619
|
+
// Apply initial render settings if provided in options
|
|
620
|
+
if (config.options?.render) {
|
|
621
|
+
await this.applyRenderSettings(config.options.render);
|
|
622
|
+
}
|
|
623
|
+
|
|
597
624
|
return { ...loadDetail, load: this.#onLoaded() };
|
|
598
625
|
}
|
|
599
626
|
|
|
@@ -622,6 +649,12 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
622
649
|
if (needUpdateIBL) {
|
|
623
650
|
someSetted = someSetted || (await this.#babylonJSController.setIBLOptions());
|
|
624
651
|
}
|
|
652
|
+
|
|
653
|
+
// Apply render settings if provided
|
|
654
|
+
if (options.render) {
|
|
655
|
+
await this.applyRenderSettings(options.render);
|
|
656
|
+
}
|
|
657
|
+
|
|
625
658
|
const detail = this.#onSetOptions();
|
|
626
659
|
return { success: someSetted, detail: detail };
|
|
627
660
|
}
|
|
@@ -794,6 +827,48 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
794
827
|
return this.#babylonJSController.getRenderSettings();
|
|
795
828
|
}
|
|
796
829
|
|
|
830
|
+
/**
|
|
831
|
+
* Returns a snapshot of all available animations and their current state.
|
|
832
|
+
* @public
|
|
833
|
+
* @returns {Array<{name: string, state: number, progress: number, loop: boolean, nodes: string[]}>}
|
|
834
|
+
*/
|
|
835
|
+
getAnimations() {
|
|
836
|
+
return this.#babylonJSController?.getAnimations() ?? [];
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Controls a named animation by applying the specified action.
|
|
841
|
+
* @public
|
|
842
|
+
* @param {string} name - The name of the animation to control.
|
|
843
|
+
* @param {"open"|"close"|"pause"|"goToOpened"|"goToClosed"} action - The action to apply.
|
|
844
|
+
* @returns {boolean} True if the animation was found and the action was applied; otherwise false.
|
|
845
|
+
*/
|
|
846
|
+
playAnimation(name, action) {
|
|
847
|
+
return this.#babylonJSController?.playAnimation(name, action) ?? false;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Seeks a named animation to the given normalized progress position (0–1).
|
|
852
|
+
* @public
|
|
853
|
+
* @param {string} name - The name of the animation to seek.
|
|
854
|
+
* @param {number} progress - Progress value between 0 and 1.
|
|
855
|
+
* @returns {boolean} True if the animation was found and seeked; otherwise false.
|
|
856
|
+
*/
|
|
857
|
+
setAnimationProgress(name, progress) {
|
|
858
|
+
return this.#babylonJSController?.setAnimationProgress(name, progress) ?? false;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Enables or disables loop mode for a named animation.
|
|
863
|
+
* @public
|
|
864
|
+
* @param {string} name - The name of the animation to configure.
|
|
865
|
+
* @param {boolean} loop - True to enable looping; false to disable.
|
|
866
|
+
* @returns {boolean} True if the animation was found and loop mode was set; otherwise false.
|
|
867
|
+
*/
|
|
868
|
+
setAnimationLoop(name, loop) {
|
|
869
|
+
return this.#babylonJSController?.setAnimationLoop(name, loop) ?? false;
|
|
870
|
+
}
|
|
871
|
+
|
|
797
872
|
/**
|
|
798
873
|
* Reports whether an IBL environment map is currently available.
|
|
799
874
|
* @public
|
|
@@ -821,7 +896,22 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
821
896
|
if (!this.#babylonJSController) {
|
|
822
897
|
return { changed: false, success: false };
|
|
823
898
|
}
|
|
824
|
-
|
|
899
|
+
|
|
900
|
+
// Handle highlight settings separately for immediate effect without reload
|
|
901
|
+
if (settings.highlightEnabled !== undefined || settings.highlightColor !== undefined) {
|
|
902
|
+
if (typeof this.#babylonJSController.setIlluminationConfig === "function") {
|
|
903
|
+
// Convert string "false" to boolean false
|
|
904
|
+
const highlightEnabled = settings.highlightEnabled === "false" ? false : settings.highlightEnabled;
|
|
905
|
+
this.#babylonJSController.setIlluminationConfig({
|
|
906
|
+
highlightEnabled: highlightEnabled,
|
|
907
|
+
highlightColor: settings.highlightColor,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Exclude highlight settings from the reload — they are handled above without requiring a scene reload
|
|
913
|
+
const { highlightEnabled: _he, highlightColor: _hc, ...reloadSettings } = settings;
|
|
914
|
+
const scheduled = this.#babylonJSController.scheduleRenderSettingsReload(reloadSettings);
|
|
825
915
|
if (!scheduled.changed) {
|
|
826
916
|
return { changed: false, success: true };
|
|
827
917
|
}
|
|
@@ -31,6 +31,9 @@ import { DEFAULT_LOCALE, resolveLocale, translate } from "./localization/i18n.js
|
|
|
31
31
|
export default class PrefViewerMenu3D extends HTMLElement {
|
|
32
32
|
#elements = {
|
|
33
33
|
applyButton: null,
|
|
34
|
+
colorPicker: null,
|
|
35
|
+
colorSlider: null,
|
|
36
|
+
colorPickerPreview: null,
|
|
34
37
|
panel: null,
|
|
35
38
|
pendingLabel: null,
|
|
36
39
|
status: null,
|
|
@@ -44,6 +47,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
44
47
|
|
|
45
48
|
#handles = {
|
|
46
49
|
onApplyClick: null,
|
|
50
|
+
onColorSliderInput: null,
|
|
47
51
|
onHostLeave: null,
|
|
48
52
|
onPanelEnter: null,
|
|
49
53
|
onSwitchChange: null,
|
|
@@ -51,7 +55,10 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
51
55
|
};
|
|
52
56
|
|
|
53
57
|
#DEFAULT_RENDER_SETTINGS = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
|
|
54
|
-
|
|
58
|
+
// Only include boolean settings as switches (exclude illuminationColor which is handled by color picker)
|
|
59
|
+
#MENU_SWITCHES = Object.keys(this.#DEFAULT_RENDER_SETTINGS).filter(key =>
|
|
60
|
+
typeof this.#DEFAULT_RENDER_SETTINGS[key] === "boolean"
|
|
61
|
+
);
|
|
55
62
|
#appliedSettings = { ...this.#DEFAULT_RENDER_SETTINGS };
|
|
56
63
|
#draftSettings = { ...this.#DEFAULT_RENDER_SETTINGS };
|
|
57
64
|
#switchAvailability = {};
|
|
@@ -72,7 +79,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
72
79
|
* @returns {string[]} Array of attribute names to observe.
|
|
73
80
|
*/
|
|
74
81
|
static get observedAttributes() {
|
|
75
|
-
return ["culture"];
|
|
82
|
+
return ["culture", "show-illumination", "illumination-color"];
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
/**
|
|
@@ -86,6 +93,20 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
86
93
|
attributeChangedCallback(name, _old, value) {
|
|
87
94
|
if (name === "culture") {
|
|
88
95
|
this.#setCultureInternal(value || DEFAULT_LOCALE);
|
|
96
|
+
} else if (name === "show-illumination" || name === "illumination-color") {
|
|
97
|
+
// Update draft settings when attributes change
|
|
98
|
+
if (name === "show-illumination") {
|
|
99
|
+
this.#draftSettings.illuminationEnabled = value === "true" || value === true;
|
|
100
|
+
// Update the corresponding switch if it exists
|
|
101
|
+
const switchEl = this.#elements.switches?.illuminationEnabled;
|
|
102
|
+
if (switchEl) {
|
|
103
|
+
switchEl.checked = this.#draftSettings.illuminationEnabled;
|
|
104
|
+
}
|
|
105
|
+
} else if (name === "illumination-color") {
|
|
106
|
+
this.#draftSettings.illuminationColor = value;
|
|
107
|
+
// Update color picker preview if exists
|
|
108
|
+
this.#updateColorPickerPreview(value);
|
|
109
|
+
}
|
|
89
110
|
}
|
|
90
111
|
}
|
|
91
112
|
|
|
@@ -139,10 +160,16 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
139
160
|
<div class="menu-switches">
|
|
140
161
|
${this.#MENU_SWITCHES.map((key) => this.#renderSwitchRow(key, this.#texts)).join("")}
|
|
141
162
|
</div>
|
|
163
|
+
<div class="menu-color-picker" data-setting="highlightColor">
|
|
164
|
+
<div class="color-slider-row">
|
|
165
|
+
<span class="color-picker-label">${this.#texts.switches?.highlightColor?.label || "Highlight Color"}</span>
|
|
166
|
+
<span class="color-preview" style="background-color: ${this.#draftSettings.highlightColor || '#ff6700'}"></span>
|
|
167
|
+
</div>
|
|
168
|
+
<input type="range" class="color-slider" min="0" max="359" value="${PrefViewerMenu3D.#hexToHue(this.#draftSettings.highlightColor || '#ff6700')}">
|
|
169
|
+
</div>
|
|
142
170
|
<div class="menu-footer">
|
|
143
171
|
<span class="menu-pending" aria-live="polite">${this.#texts.pendingLabel}</span>
|
|
144
172
|
<span class="menu-status" aria-live="assertive"></span>
|
|
145
|
-
<button class="menu-apply" type="button" disabled>${this.#texts.actions.apply}</button>
|
|
146
173
|
</div>
|
|
147
174
|
</div>
|
|
148
175
|
`;
|
|
@@ -271,6 +298,12 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
271
298
|
this.#elements.applyButton = this.querySelector(".menu-apply");
|
|
272
299
|
this.#elements.pendingLabel = this.querySelector(".menu-pending");
|
|
273
300
|
this.#elements.status = this.querySelector(".menu-status");
|
|
301
|
+
|
|
302
|
+
// Cache color picker elements
|
|
303
|
+
this.#elements.colorPicker = this.querySelector(".menu-color-picker");
|
|
304
|
+
this.#elements.colorSlider = this.querySelector(".color-slider");
|
|
305
|
+
this.#elements.colorPickerPreview = this.querySelector(".color-preview");
|
|
306
|
+
|
|
274
307
|
this.#MENU_SWITCHES.forEach((key) => {
|
|
275
308
|
this.#elements.switches[key] = this.querySelector(`input[data-setting="${key}"]`);
|
|
276
309
|
const wrapper = this.querySelector(`.menu-switch[data-setting="${key}"]`);
|
|
@@ -301,13 +334,15 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
301
334
|
};
|
|
302
335
|
this.#handles.onSwitchChange = (event) => this.#handleSwitchChange(event);
|
|
303
336
|
this.#handles.onApplyClick = () => this.#emitApply();
|
|
337
|
+
this.#handles.onColorSliderInput = (event) => this.#handleColorSliderInput(event);
|
|
304
338
|
|
|
305
339
|
this.#elements.toggleButton.addEventListener("mouseenter", this.#handles.onToggleEnter);
|
|
306
340
|
this.#elements.toggleButton.addEventListener("focus", this.#handles.onToggleEnter);
|
|
307
341
|
this.#elements.panel.addEventListener("mouseenter", this.#handles.onPanelEnter);
|
|
308
342
|
this.addEventListener("mouseleave", this.#handles.onHostLeave);
|
|
309
343
|
Object.values(this.#elements.switches).forEach((input) => input?.addEventListener("change", this.#handles.onSwitchChange));
|
|
310
|
-
this.#elements.applyButton
|
|
344
|
+
this.#elements.applyButton?.addEventListener("click", this.#handles.onApplyClick);
|
|
345
|
+
this.#elements.colorSlider?.addEventListener("input", this.#handles.onColorSliderInput);
|
|
311
346
|
}
|
|
312
347
|
|
|
313
348
|
/**
|
|
@@ -325,6 +360,8 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
325
360
|
this.removeEventListener("mouseleave", this.#handles.onHostLeave);
|
|
326
361
|
Object.values(this.#elements.switches).forEach((input) => input?.removeEventListener("change", this.#handles.onSwitchChange));
|
|
327
362
|
this.#elements.applyButton?.removeEventListener("click", this.#handles.onApplyClick);
|
|
363
|
+
|
|
364
|
+
this.#elements.colorSlider?.removeEventListener("input", this.#handles.onColorSliderInput);
|
|
328
365
|
}
|
|
329
366
|
|
|
330
367
|
/**
|
|
@@ -364,6 +401,77 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
364
401
|
}
|
|
365
402
|
this.#draftSettings[key] = event.currentTarget.checked;
|
|
366
403
|
this.#updatePendingState();
|
|
404
|
+
|
|
405
|
+
// For highlight toggle, apply immediately without waiting for user to click Apply
|
|
406
|
+
if (key === "highlightEnabled") {
|
|
407
|
+
this.#emitApply(true);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Handles clicks on the color picker buttons.
|
|
413
|
+
* @private
|
|
414
|
+
* @param {Event} event - Click event.
|
|
415
|
+
* @returns {void}
|
|
416
|
+
*/
|
|
417
|
+
#handleColorSliderInput(event) {
|
|
418
|
+
const hue = parseInt(event.target.value, 10);
|
|
419
|
+
const color = PrefViewerMenu3D.#hueToHex(hue);
|
|
420
|
+
this.#draftSettings.highlightColor = color;
|
|
421
|
+
this.#updateColorPickerPreview(color);
|
|
422
|
+
this.#updatePendingState();
|
|
423
|
+
this.#emitApply(true);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
static #hueToHex(hue) {
|
|
427
|
+
const h = ((hue % 360) + 360) % 360;
|
|
428
|
+
const a = Math.min(0.5, 1 - 0.5);
|
|
429
|
+
const f = (n) => {
|
|
430
|
+
const k = (n + h / 30) % 12;
|
|
431
|
+
return 0.5 - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
432
|
+
};
|
|
433
|
+
return '#' + [f(0), f(8), f(4)].map((v) => Math.round(v * 255).toString(16).padStart(2, '0')).join('');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
static #hexToHue(hex) {
|
|
437
|
+
if (!hex || hex.length < 7) return 24;
|
|
438
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
439
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
440
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
441
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
442
|
+
if (max === min) return 0;
|
|
443
|
+
const d = max - min;
|
|
444
|
+
const h = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
445
|
+
return Math.round(h * 60);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Updates the color picker UI (input value, preview, selected button).
|
|
450
|
+
* @private
|
|
451
|
+
* @param {string} color - Selected color.
|
|
452
|
+
* @returns {void}
|
|
453
|
+
*/
|
|
454
|
+
#updateColorPickerUI(color) {
|
|
455
|
+
this.#updateColorPickerPreview(color);
|
|
456
|
+
if (this.#elements.colorSlider) {
|
|
457
|
+
const hue = PrefViewerMenu3D.#hexToHue(color);
|
|
458
|
+
// Only update slider position if not actively focused (user dragging)
|
|
459
|
+
if (document.activeElement !== this.#elements.colorSlider) {
|
|
460
|
+
this.#elements.colorSlider.value = hue;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Updates the color preview element.
|
|
467
|
+
* @private
|
|
468
|
+
* @param {string} color - Color to preview.
|
|
469
|
+
* @returns {void}
|
|
470
|
+
*/
|
|
471
|
+
#updateColorPickerPreview(color) {
|
|
472
|
+
if (this.#elements.colorPickerPreview) {
|
|
473
|
+
this.#elements.colorPickerPreview.style.backgroundColor = color;
|
|
474
|
+
}
|
|
367
475
|
}
|
|
368
476
|
|
|
369
477
|
/**
|
|
@@ -371,8 +479,9 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
371
479
|
* @private
|
|
372
480
|
* @returns {void}
|
|
373
481
|
*/
|
|
374
|
-
#emitApply() {
|
|
375
|
-
|
|
482
|
+
#emitApply(force = false) {
|
|
483
|
+
// For illumination/color changes, we apply immediately regardless of pending state
|
|
484
|
+
if (!force && this.#isApplying) {
|
|
376
485
|
return;
|
|
377
486
|
}
|
|
378
487
|
|
|
@@ -554,6 +663,16 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
554
663
|
this.#draftSettings = { ...this.#appliedSettings };
|
|
555
664
|
this.#updateSwitches();
|
|
556
665
|
this.#updatePendingState();
|
|
666
|
+
|
|
667
|
+
// Sync color slider with current highlightColor
|
|
668
|
+
if (this.#draftSettings.highlightColor) {
|
|
669
|
+
this.#updateColorPickerUI(this.#draftSettings.highlightColor);
|
|
670
|
+
}
|
|
671
|
+
// Update illumination color picker if present
|
|
672
|
+
if (this.#draftSettings.illuminationColor) {
|
|
673
|
+
this.#updateColorPickerUI(this.#draftSettings.illuminationColor);
|
|
674
|
+
}
|
|
675
|
+
|
|
557
676
|
this.setApplying(false);
|
|
558
677
|
this.#setStatusMessage("");
|
|
559
678
|
}
|
package/src/pref-viewer.js
CHANGED
|
@@ -106,7 +106,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
106
106
|
* @returns {string[]} Array of attribute names to observe.
|
|
107
107
|
*/
|
|
108
108
|
static get observedAttributes() {
|
|
109
|
-
return ["config", "culture", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene"];
|
|
109
|
+
return ["config", "culture", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene", "show-menu", "accent-color", "show-animation-menu", "highlight-color", "highlight-enabled"];
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
@@ -174,6 +174,17 @@ export default class PrefViewer extends HTMLElement {
|
|
|
174
174
|
const showScene = value.toLowerCase() === "true";
|
|
175
175
|
showScene ? this.showScene() : this.hideScene();
|
|
176
176
|
break;
|
|
177
|
+
case "show-menu":
|
|
178
|
+
// Recreate menu to apply show/hide setting
|
|
179
|
+
this.#createMenu3D();
|
|
180
|
+
break;
|
|
181
|
+
case "accent-color":
|
|
182
|
+
case "show-animation-menu":
|
|
183
|
+
case "highlight-color":
|
|
184
|
+
case "highlight-enabled":
|
|
185
|
+
// Pass these attributes to the 3D component and menu
|
|
186
|
+
this.#updateComponentAttributes(name, value);
|
|
187
|
+
break;
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
190
|
|
|
@@ -256,6 +267,15 @@ export default class PrefViewer extends HTMLElement {
|
|
|
256
267
|
#createComponent3D() {
|
|
257
268
|
this.#component3D = document.createElement("pref-viewer-3d");
|
|
258
269
|
this.#component3D.setAttribute("visible", "false");
|
|
270
|
+
|
|
271
|
+
// Pass new configuration attributes to 3D component
|
|
272
|
+
const newAttrs = ["show-animation-menu", "highlight-color", "highlight-enabled"];
|
|
273
|
+
for (const attr of newAttrs) {
|
|
274
|
+
if (this.hasAttribute(attr)) {
|
|
275
|
+
this.#component3D.setAttribute(attr, this.getAttribute(attr));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
259
279
|
this.#handlers.on3DSceneLoaded = () => {
|
|
260
280
|
this.#menu3DSyncSettings();
|
|
261
281
|
this.#menu3D?.setApplying(false);
|
|
@@ -271,6 +291,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
271
291
|
/**
|
|
272
292
|
* Creates (or recreates) the PrefViewerMenu3D element, wires hover/apply handlers, and syncs it with the 3D component.
|
|
273
293
|
* When a menu already exists it is removed so the DOM stays clean.
|
|
294
|
+
* Respects the "show-menu" attribute to determine if menu should be visible.
|
|
274
295
|
* @private
|
|
275
296
|
* @returns {void}
|
|
276
297
|
*/
|
|
@@ -282,6 +303,16 @@ export default class PrefViewer extends HTMLElement {
|
|
|
282
303
|
this.#menu3D.removeEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
|
|
283
304
|
this.#menu3D.remove();
|
|
284
305
|
}
|
|
306
|
+
|
|
307
|
+
// Check if menu should be shown (default: true if not specified)
|
|
308
|
+
const showMenuAttr = this.getAttribute("show-menu");
|
|
309
|
+
const showMenu = showMenuAttr === null || showMenuAttr === "true";
|
|
310
|
+
|
|
311
|
+
if (!showMenu) {
|
|
312
|
+
this.#menu3D = null;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
285
316
|
this.#menu3D = document.createElement("pref-viewer-menu-3d");
|
|
286
317
|
if (typeof this.#menu3D.setCulture === "function") {
|
|
287
318
|
this.#menu3D.setCulture(this.#culture);
|
|
@@ -289,6 +320,14 @@ export default class PrefViewer extends HTMLElement {
|
|
|
289
320
|
this.#menu3D.setAttribute("culture", this.#culture);
|
|
290
321
|
}
|
|
291
322
|
|
|
323
|
+
// Pass illumination-related attributes to menu
|
|
324
|
+
const illumAttrs = ["show-illumination", "illumination-color"];
|
|
325
|
+
for (const attr of illumAttrs) {
|
|
326
|
+
if (this.hasAttribute(attr)) {
|
|
327
|
+
this.#menu3D.setAttribute(attr, this.getAttribute(attr));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
292
331
|
if (!this.#handlers.onMenuApply) {
|
|
293
332
|
this.#handlers.onMenuApply = this.#onMenuApply.bind(this);
|
|
294
333
|
}
|
|
@@ -305,6 +344,33 @@ export default class PrefViewer extends HTMLElement {
|
|
|
305
344
|
this.#menu3DUpdateAvailability();
|
|
306
345
|
}
|
|
307
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Updates attributes on child components (3D viewer and menu) based on parent attributes.
|
|
349
|
+
* @private
|
|
350
|
+
* @param {string} name - Attribute name.
|
|
351
|
+
* @param {*} value - Attribute value.
|
|
352
|
+
* @returns {void}
|
|
353
|
+
*/
|
|
354
|
+
#updateComponentAttributes(name, value) {
|
|
355
|
+
// Pass to 3D component
|
|
356
|
+
if (this.#component3D) {
|
|
357
|
+
if (value === null) {
|
|
358
|
+
this.#component3D.removeAttribute(name);
|
|
359
|
+
} else {
|
|
360
|
+
this.#component3D.setAttribute(name, value);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Pass to menu
|
|
365
|
+
if (this.#menu3D) {
|
|
366
|
+
if (value === null) {
|
|
367
|
+
this.#menu3D.removeAttribute(name);
|
|
368
|
+
} else {
|
|
369
|
+
this.#menu3D.setAttribute(name, value);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
308
374
|
/**
|
|
309
375
|
* Reads the last persisted locale from localStorage so the viewer restores user preference.
|
|
310
376
|
* @private
|
|
@@ -1243,6 +1309,48 @@ export default class PrefViewer extends HTMLElement {
|
|
|
1243
1309
|
this.#component2D.zoomOut();
|
|
1244
1310
|
}
|
|
1245
1311
|
|
|
1312
|
+
/**
|
|
1313
|
+
* Returns a snapshot of all available animations and their current state.
|
|
1314
|
+
* @public
|
|
1315
|
+
* @returns {Array<{name: string, state: number, progress: number, loop: boolean, nodes: string[]}>}
|
|
1316
|
+
*/
|
|
1317
|
+
getAnimations() {
|
|
1318
|
+
return this.#component3D?.getAnimations() ?? [];
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Controls a named animation by applying the specified action.
|
|
1323
|
+
* @public
|
|
1324
|
+
* @param {string} name - The name of the animation to control.
|
|
1325
|
+
* @param {"open"|"close"|"pause"|"goToOpened"|"goToClosed"} action - The action to apply.
|
|
1326
|
+
* @returns {boolean} True if the animation was found and the action was applied; otherwise false.
|
|
1327
|
+
*/
|
|
1328
|
+
playAnimation(name, action) {
|
|
1329
|
+
return this.#component3D?.playAnimation(name, action) ?? false;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Seeks a named animation to the given normalized progress position (0–1).
|
|
1334
|
+
* @public
|
|
1335
|
+
* @param {string} name - The name of the animation to seek.
|
|
1336
|
+
* @param {number} progress - Progress value between 0 and 1.
|
|
1337
|
+
* @returns {boolean} True if the animation was found and seeked; otherwise false.
|
|
1338
|
+
*/
|
|
1339
|
+
setAnimationProgress(name, progress) {
|
|
1340
|
+
return this.#component3D?.setAnimationProgress(name, progress) ?? false;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Enables or disables loop mode for a named animation.
|
|
1345
|
+
* @public
|
|
1346
|
+
* @param {string} name - The name of the animation to configure.
|
|
1347
|
+
* @param {boolean} loop - True to enable looping; false to disable.
|
|
1348
|
+
* @returns {boolean} True if the animation was found and loop mode was set; otherwise false.
|
|
1349
|
+
*/
|
|
1350
|
+
setAnimationLoop(name, loop) {
|
|
1351
|
+
return this.#component3D?.setAnimationLoop(name, loop) ?? false;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1246
1354
|
/**
|
|
1247
1355
|
* ---------------------------
|
|
1248
1356
|
* Public properties (setters)
|
|
@@ -1321,6 +1429,25 @@ export default class PrefViewer extends HTMLElement {
|
|
|
1321
1429
|
this.loadScene(value);
|
|
1322
1430
|
}
|
|
1323
1431
|
|
|
1432
|
+
/**
|
|
1433
|
+
* Enables or disables the hover highlight effect at runtime.
|
|
1434
|
+
* Accepts boolean or string "true"/"false".
|
|
1435
|
+
* @public
|
|
1436
|
+
* @param {boolean|string} value
|
|
1437
|
+
*/
|
|
1438
|
+
set highlightEnabled(value) {
|
|
1439
|
+
this.setAttribute("highlight-enabled", String(value));
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Sets the hover highlight color at runtime.
|
|
1444
|
+
* @public
|
|
1445
|
+
* @param {string} value - Hex color string, e.g. "#ff6700".
|
|
1446
|
+
*/
|
|
1447
|
+
set highlightColor(value) {
|
|
1448
|
+
this.setAttribute("highlight-color", value);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1324
1451
|
/**
|
|
1325
1452
|
* ---------------------------
|
|
1326
1453
|
* Public properties (getters)
|
package/src/styles.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
export const PrefViewerStyleVariables = {
|
|
2
|
-
colorPrimary: "#ff6700", //
|
|
2
|
+
colorPrimary: "#ff6700", // Default accent color - can be overridden via attribute
|
|
3
3
|
fontFamily: "'Roboto', ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji",
|
|
4
4
|
};
|
|
5
5
|
|
|
6
|
+
// Default palette colors for illumination color picker
|
|
7
|
+
export const DEFAULT_ILLUMINATION_PALETTE = [
|
|
8
|
+
"#ffffff", // White
|
|
9
|
+
"#ff6700", // Orange (default)
|
|
10
|
+
"#ff0000", // Red
|
|
11
|
+
"#00ff00", // Green
|
|
12
|
+
"#0000ff", // Blue
|
|
13
|
+
"#ffff00", // Yellow
|
|
14
|
+
"#ff00ff", // Magenta
|
|
15
|
+
"#00ffff", // Cyan
|
|
16
|
+
"#ffb6c1", // Light Pink
|
|
17
|
+
"#98fb98", // Light Green
|
|
18
|
+
"#87ceeb", // Light Blue
|
|
19
|
+
"#dda0dd", // Plum
|
|
20
|
+
];
|
|
21
|
+
|
|
6
22
|
export const PrefViewerStyles = `
|
|
7
23
|
:host .pref-viewer-wrapper {
|
|
8
24
|
display: grid !important;
|
|
@@ -67,6 +83,10 @@ export const PrefViewer3DStyles = `
|
|
|
67
83
|
display: none;
|
|
68
84
|
}
|
|
69
85
|
|
|
86
|
+
pref-viewer-3d[show-animation-menu="false"] > div.animation-menu {
|
|
87
|
+
display: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
70
90
|
pref-viewer-3d {
|
|
71
91
|
grid-column: 1;
|
|
72
92
|
grid-row: 1;
|
|
@@ -181,6 +201,8 @@ export const PrefViewerMenu3DStyles = `
|
|
|
181
201
|
|
|
182
202
|
pref-viewer-menu-3d>.menu-wrapper>.menu-panel {
|
|
183
203
|
width: var(--panel-width);
|
|
204
|
+
max-height: 70vh;
|
|
205
|
+
overflow-y: auto;
|
|
184
206
|
border-radius: var(--panel-radius);
|
|
185
207
|
background: var(--panel-bg-color);
|
|
186
208
|
border: 1px solid var(--panel-border-color);
|
|
@@ -386,6 +408,80 @@ export const PrefViewerMenu3DStyles = `
|
|
|
386
408
|
pref-viewer-menu-3d[data-applying]>.menu-wrapper>.menu-panel>.menu-footer>.menu-apply {
|
|
387
409
|
opacity: 0.75;
|
|
388
410
|
}
|
|
411
|
+
|
|
412
|
+
/* Color Slider Styles */
|
|
413
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-color-picker {
|
|
414
|
+
display: flex;
|
|
415
|
+
flex-direction: column;
|
|
416
|
+
gap: 8px;
|
|
417
|
+
padding: var(--panel-spacing) 0;
|
|
418
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-color-picker:last-child {
|
|
422
|
+
border-bottom: none;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-color-picker>.color-slider-row {
|
|
426
|
+
display: flex;
|
|
427
|
+
align-items: center;
|
|
428
|
+
justify-content: space-between;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-color-picker>.color-slider-row>.color-picker-label {
|
|
432
|
+
font-size: var(--font-size-medium);
|
|
433
|
+
font-weight: var(--font-weight-medium);
|
|
434
|
+
color: var(--text-color);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-color-picker>.color-slider-row>.color-preview {
|
|
438
|
+
width: 20px;
|
|
439
|
+
height: 20px;
|
|
440
|
+
border-radius: 50%;
|
|
441
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
442
|
+
flex-shrink: 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-color-picker>.color-slider {
|
|
446
|
+
-webkit-appearance: none;
|
|
447
|
+
appearance: none;
|
|
448
|
+
width: 100%;
|
|
449
|
+
height: 10px;
|
|
450
|
+
border-radius: 5px;
|
|
451
|
+
cursor: pointer;
|
|
452
|
+
background: linear-gradient(to right,
|
|
453
|
+
hsl(0,100%,50%), hsl(30,100%,50%), hsl(60,100%,50%), hsl(90,100%,50%),
|
|
454
|
+
hsl(120,100%,50%), hsl(150,100%,50%), hsl(180,100%,50%), hsl(210,100%,50%),
|
|
455
|
+
hsl(240,100%,50%), hsl(270,100%,50%), hsl(300,100%,50%), hsl(330,100%,50%),
|
|
456
|
+
hsl(360,100%,50%));
|
|
457
|
+
outline: none;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-color-picker>.color-slider::-webkit-slider-thumb {
|
|
461
|
+
-webkit-appearance: none;
|
|
462
|
+
width: 16px;
|
|
463
|
+
height: 16px;
|
|
464
|
+
border-radius: 50%;
|
|
465
|
+
background: white;
|
|
466
|
+
border: 2px solid rgba(0, 0, 0, 0.25);
|
|
467
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
|
|
468
|
+
cursor: pointer;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-color-picker>.color-slider::-moz-range-thumb {
|
|
472
|
+
width: 16px;
|
|
473
|
+
height: 16px;
|
|
474
|
+
border-radius: 50%;
|
|
475
|
+
background: white;
|
|
476
|
+
border: 2px solid rgba(0, 0, 0, 0.25);
|
|
477
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
|
|
478
|
+
cursor: pointer;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-color-picker[data-disabled] {
|
|
482
|
+
opacity: 0.5;
|
|
483
|
+
pointer-events: none;
|
|
484
|
+
}
|
|
389
485
|
`;
|
|
390
486
|
|
|
391
487
|
export const PrefViewer3DAnimationMenuStyles = `
|