three-cad-viewer 4.1.2 → 4.2.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.
Files changed (58) hide show
  1. package/Readme.md +12 -5
  2. package/dist/camera/camera.d.ts +14 -2
  3. package/dist/core/studio-manager.d.ts +91 -0
  4. package/dist/core/types.d.ts +260 -9
  5. package/dist/core/viewer-state.d.ts +28 -2
  6. package/dist/core/viewer.d.ts +200 -6
  7. package/dist/index.d.ts +7 -2
  8. package/dist/rendering/environment.d.ts +239 -0
  9. package/dist/rendering/light-detection.d.ts +44 -0
  10. package/dist/rendering/material-factory.d.ts +77 -2
  11. package/dist/rendering/material-presets.d.ts +32 -0
  12. package/dist/rendering/room-environment.d.ts +13 -0
  13. package/dist/rendering/studio-composer.d.ts +130 -0
  14. package/dist/rendering/studio-floor.d.ts +53 -0
  15. package/dist/rendering/texture-cache.d.ts +142 -0
  16. package/dist/rendering/triplanar.d.ts +37 -0
  17. package/dist/scene/animation.d.ts +1 -1
  18. package/dist/scene/clipping.d.ts +31 -0
  19. package/dist/scene/nestedgroup.d.ts +64 -27
  20. package/dist/scene/objectgroup.d.ts +47 -0
  21. package/dist/three-cad-viewer.css +339 -29
  22. package/dist/three-cad-viewer.esm.js +27567 -11874
  23. package/dist/three-cad-viewer.esm.js.map +1 -1
  24. package/dist/three-cad-viewer.esm.min.js +10 -4
  25. package/dist/three-cad-viewer.js +27486 -11787
  26. package/dist/three-cad-viewer.min.js +10 -4
  27. package/dist/ui/display.d.ts +147 -0
  28. package/dist/utils/decode-instances.d.ts +60 -0
  29. package/dist/utils/utils.d.ts +10 -0
  30. package/package.json +4 -2
  31. package/src/_version.ts +1 -1
  32. package/src/camera/camera.ts +27 -10
  33. package/src/core/studio-manager.ts +682 -0
  34. package/src/core/types.ts +328 -9
  35. package/src/core/viewer-state.ts +84 -4
  36. package/src/core/viewer.ts +453 -22
  37. package/src/index.ts +25 -1
  38. package/src/rendering/environment.ts +840 -0
  39. package/src/rendering/light-detection.ts +327 -0
  40. package/src/rendering/material-factory.ts +456 -2
  41. package/src/rendering/material-presets.ts +303 -0
  42. package/src/rendering/raycast.ts +2 -2
  43. package/src/rendering/room-environment.ts +192 -0
  44. package/src/rendering/studio-composer.ts +577 -0
  45. package/src/rendering/studio-floor.ts +108 -0
  46. package/src/rendering/texture-cache.ts +1020 -0
  47. package/src/rendering/triplanar.ts +329 -0
  48. package/src/scene/animation.ts +3 -2
  49. package/src/scene/clipping.ts +59 -0
  50. package/src/scene/nestedgroup.ts +399 -0
  51. package/src/scene/objectgroup.ts +186 -11
  52. package/src/scene/orientation.ts +12 -0
  53. package/src/scene/render-shape.ts +55 -21
  54. package/src/types/n8ao.d.ts +28 -0
  55. package/src/ui/display.ts +1032 -27
  56. package/src/ui/index.html +181 -44
  57. package/src/utils/decode-instances.ts +233 -0
  58. package/src/utils/utils.ts +33 -20
@@ -2,12 +2,13 @@ import * as THREE from "three";
2
2
  import { BoundingBox } from "./bbox.js";
3
3
  import { ObjectGroup, isObjectGroup } from "./objectgroup.js";
4
4
  import { MaterialFactory } from "../rendering/material-factory.js";
5
- import type { ZebraColorScheme, ZebraMappingMode, Shapes, ColorValue } from "../core/types";
5
+ import type { ZebraColorScheme, ZebraMappingMode, StudioTextureMapping, Shapes, ColorValue, MaterialAppearance, MaterialXMaterial, TextureEntry } from "../core/types";
6
6
  interface ShapeData {
7
7
  vertices: Float32Array | number[][];
8
8
  normals: Float32Array | number[][];
9
9
  triangles: Uint32Array | number[][];
10
10
  edges?: Float32Array | number[][];
11
+ uvs?: Float32Array | number[];
11
12
  }
12
13
  interface EdgeData {
13
14
  edges: Float32Array | number[][];
@@ -44,6 +45,7 @@ interface ShapeEntry {
44
45
  width: number;
45
46
  height: number;
46
47
  };
48
+ material?: string;
47
49
  }
48
50
  type GroupsMap = Record<string, ObjectGroup | CompoundGroup>;
49
51
  /**
@@ -56,32 +58,6 @@ declare class CompoundGroup extends THREE.Group {
56
58
  /** Type guard property following Three.js convention */
57
59
  readonly isCompoundGroup = true;
58
60
  }
59
- /**
60
- * Manages hierarchical 3D geometry rendering from tessellated CAD data.
61
- *
62
- * NestedGroup is the central scene graph manager that:
63
- * - Parses Shapes data into Three.js geometry
64
- * - Creates ObjectGroup instances for individual shapes, edges, and vertices
65
- * - Maintains a flat `groups` map for path-based access
66
- * - Handles materials, transparency, and clipping planes
67
- *
68
- * ## Architecture
69
- * ```
70
- * NestedGroup (manager)
71
- * └── rootGroup (THREE.Group)
72
- * └── CompoundGroup (per assembly)
73
- * └── ObjectGroup (per shape/edge/vertex)
74
- * └── THREE.Mesh / LineSegments2
75
- * ```
76
- *
77
- * ## Key Methods
78
- * - `render()`: Build geometry from Shapes data
79
- * - `setTransparent()`: Toggle transparency mode
80
- * - `setClipPlanes()`: Apply clipping planes
81
- * - `groups[path]`: Access ObjectGroup by path
82
- *
83
- * @internal - This is an internal class used by Viewer
84
- */
85
61
  declare class NestedGroup {
86
62
  shapes: Shapes;
87
63
  width: number;
@@ -103,6 +79,16 @@ declare class NestedGroup {
103
79
  groups: GroupsMap;
104
80
  clipPlanes: THREE.Plane[] | null;
105
81
  materialFactory: MaterialFactory;
82
+ texturesTable: Record<string, TextureEntry> | null;
83
+ materialsTable: Record<string, string | MaterialXMaterial | MaterialAppearance> | null;
84
+ resolvedMaterials: Map<string, MaterialAppearance>;
85
+ /** Cache for threejs-materials entries resolved from the materials table */
86
+ resolvedMaterialX: Map<string, MaterialXMaterial>;
87
+ private _textureCache;
88
+ private _studioMaterialCache;
89
+ /** Sharing keys of materials that have textures (for UV generation on cache hits) */
90
+ private _texturedMaterialKeys;
91
+ private _isStudioMode;
106
92
  /**
107
93
  * Create a NestedGroup for rendering CAD geometry.
108
94
  * @param shapes - The tessellated shape data to render.
@@ -121,6 +107,26 @@ declare class NestedGroup {
121
107
  * Dispose of all resources and clean up memory.
122
108
  */
123
109
  dispose(): void;
110
+ /**
111
+ * Resolve a material tag to its definition.
112
+ *
113
+ * Returns either a MaterialAppearance (for builtin presets) or a
114
+ * MaterialXMaterial (for threejs-materials entries). The caller must check the
115
+ * return type to determine which factory method to use.
116
+ *
117
+ * Resolution order:
118
+ * 1. Check caches (resolvedMaterials / resolvedMaterialX)
119
+ * 2. Look up in root-level `materials` table:
120
+ * - string starting with "builtin:" → MATERIAL_PRESETS lookup
121
+ * - object with `properties` key → threejs-materials entry
122
+ * 3. Direct lookup in MATERIAL_PRESETS by tag name
123
+ * 4. No match → warning, return null
124
+ *
125
+ * @param tag - The material tag from a leaf node
126
+ * @param objectPath - The object path (for warning messages)
127
+ * @returns Resolved material definition or null if not found
128
+ */
129
+ resolveMaterialTag(tag: string, objectPath: string): MaterialAppearance | MaterialXMaterial | null;
124
130
  /**
125
131
  * Check if array is nested (number[][]).
126
132
  */
@@ -272,6 +278,37 @@ declare class NestedGroup {
272
278
  * Set the mapping mode for zebra stripes.
273
279
  */
274
280
  setZebraMappingMode(flag: ZebraMappingMode): void;
281
+ /**
282
+ * Enter Studio mode: build and apply studio materials to all ObjectGroups.
283
+ *
284
+ * Material resolution per ObjectGroup:
285
+ * 1. Resolve the material tag via `resolveMaterialTag()`
286
+ * - MaterialXMaterial → `createStudioMaterialFromMaterialX`
287
+ * - MaterialAppearance → `createStudioMaterial` (builtin presets)
288
+ * - null (no tag) → fallback plastic-glossy preset tinted with CAD color
289
+ * 2. Cache by sharing key for reuse across objects with the same tag+color
290
+ * 3. Clone BackSide variant for renderback objects
291
+ * 4. Auto-generate box-projected UVs when textured but geometry has no UVs
292
+ */
293
+ enterStudioMode(textureMapping?: StudioTextureMapping): Promise<string[]>;
294
+ /**
295
+ * Leave Studio mode: restore CAD materials on all ObjectGroups.
296
+ * Does NOT clear the material cache (allows fast re-entry).
297
+ */
298
+ leaveStudioMode(): void;
299
+ /**
300
+ * Clear cached Studio materials so they are rebuilt on next enterStudioMode.
301
+ */
302
+ clearStudioMaterialCache(): void;
303
+ /**
304
+ * Set edge visibility across all ObjectGroups while in Studio mode.
305
+ * @param visible - Whether edges should be visible
306
+ */
307
+ setStudioShowEdges(visible: boolean): void;
308
+ /**
309
+ * Dispose all Studio mode resources (material cache + texture cache).
310
+ */
311
+ private _disposeStudioResources;
275
312
  }
276
313
  /**
277
314
  * Type guard to check if an object is a CompoundGroup instance.
@@ -69,9 +69,17 @@ declare class ObjectGroup extends THREE.Group {
69
69
  vertexFocusSize: number;
70
70
  edgeFocusWidth: number;
71
71
  shapeGeometry?: THREE.BufferGeometry | null;
72
+ /** Material tag from shapes data, used for Studio mode material lookup */
73
+ materialTag: string;
72
74
  minZ?: number;
73
75
  height?: number;
74
76
  private _zebra;
77
+ private _cadFrontMaterial;
78
+ private _cadBackMaterial;
79
+ private _cadOriginalColor;
80
+ private _cadOriginalBackColor;
81
+ private _isStudioMode;
82
+ private _cadEdgesVisible;
75
83
  /**
76
84
  * Create an ObjectGroup for managing a CAD object's visual representation.
77
85
  * @param opacity - Default opacity value (0.0 to 1.0).
@@ -86,6 +94,10 @@ declare class ObjectGroup extends THREE.Group {
86
94
  * Get the zebra tool, creating it on first access.
87
95
  */
88
96
  get zebra(): ZebraTool;
97
+ /**
98
+ * Whether this ObjectGroup is currently in Studio mode.
99
+ */
100
+ get isStudioMode(): boolean;
89
101
  /**
90
102
  * Dispose of all resources and clean up memory.
91
103
  * Releases geometry, materials, children, and zebra tool.
@@ -252,6 +264,41 @@ declare class ObjectGroup extends THREE.Group {
252
264
  * Set the mapping mode for zebra stripes.
253
265
  */
254
266
  setZebraMappingMode(value: ZebraMappingMode): void;
267
+ /**
268
+ * Enter Studio mode: swap CAD materials for pre-built Studio materials.
269
+ *
270
+ * The caller (NestedGroup) is responsible for resolving material tags and
271
+ * building MeshPhysicalMaterial instances via MaterialFactory. ObjectGroup
272
+ * just receives the finished materials and performs the swap.
273
+ *
274
+ * On first call, saves the current CAD material references so they can be
275
+ * restored by `leaveStudioMode()`. Copies the `material.visible` flag from
276
+ * CAD to Studio material to preserve tree-view hide/show state. Updates
277
+ * `originalColor` / `originalBackColor` so highlight/unhighlight works
278
+ * correctly in Studio mode.
279
+ *
280
+ * @param studioFront - Studio material for front face, or null if this object has no front mesh
281
+ * @param studioBack - Studio material for back face, or null if back face should not be swapped
282
+ */
283
+ enterStudioMode(studioFront: THREE.MeshPhysicalMaterial | null, studioBack: THREE.MeshPhysicalMaterial | null): void;
284
+ /**
285
+ * Leave Studio mode: restore CAD materials.
286
+ *
287
+ * Copies `material.visible` from Studio back to CAD material to preserve
288
+ * any visibility changes made while in Studio mode (e.g., tree-view toggle).
289
+ * Restores `originalColor` / `originalBackColor` to CAD material colors.
290
+ * Restores edge visibility to the state saved when entering Studio mode.
291
+ */
292
+ leaveStudioMode(): void;
293
+ /**
294
+ * Toggle edge visibility while in Studio mode.
295
+ *
296
+ * Only affects edges (not vertices). Should only be called while in
297
+ * Studio mode; the saved CAD edge visibility is not affected.
298
+ *
299
+ * @param visible - Whether edges should be visible
300
+ */
301
+ setStudioShowEdges(visible: boolean): void;
255
302
  }
256
303
  /**
257
304
  * Type guard to check if an object is an ObjectGroup instance.
@@ -4,6 +4,7 @@
4
4
  --tcv-font-color: #333;
5
5
  --tcv-bg-color: #fff;
6
6
  --tcv-bg-overlay-color: rgba(255, 255, 255, 0.7);
7
+ --tcv-bg-overlay-rest-color: rgba(255, 255, 255, 0.14);
7
8
  --tcv-bg-highlight-color: #ddd;
8
9
  --tcv-bg-pressed-color: #eee;
9
10
  --tcv-bg-pressed-border-color: #bbb;
@@ -63,6 +64,7 @@
63
64
  --tcv-font-color: #ddd;
64
65
  --tcv-bg-color: #444;
65
66
  --tcv-bg-overlay-color: rgba(68, 68, 68, 0.7);
67
+ --tcv-bg-overlay-rest-color: rgba(68, 68, 68, 0.14);
66
68
  --tcv-bg-highlight-color: rgb(104, 104, 104);
67
69
  --tcv-bg-pressed-color: rgb(94, 94, 94);
68
70
  --tcv-bg-pressed-border-color: #999;
@@ -258,6 +260,7 @@ input[type="radio"] {
258
260
 
259
261
 
260
262
  .tcv_cad_viewer {
263
+ --tcv-ui-row-gap: 8px;
261
264
  margin: 0;
262
265
  display: flex;
263
266
  flex-direction: column;
@@ -292,7 +295,7 @@ input[type="radio"] {
292
295
 
293
296
  .tcv_cad_tree_toggles {
294
297
  text-align: right;
295
- margin-right: 0px;
298
+ padding: 0 4px;
296
299
  margin-bottom: 3px;
297
300
  }
298
301
 
@@ -310,6 +313,7 @@ input[type="radio"] {
310
313
  .tcv_cad_tree_glass {
311
314
  border: none !important;
312
315
  height: fit-content;
316
+ background-color: var(--tcv-bg-overlay-rest-color);
313
317
  }
314
318
 
315
319
  .tcv_cad_tree_glass:hover {
@@ -321,8 +325,6 @@ input[type="radio"] {
321
325
  height: 146px;
322
326
  font-family: sans-serif;
323
327
  font-size: 12px;
324
- border-style: solid;
325
- border-width: 1px;
326
328
  overflow: hidden;
327
329
  user-select:text;
328
330
  }
@@ -353,12 +355,38 @@ input[type="radio"] {
353
355
  font-weight: bold;
354
356
  margin-left: 4px;
355
357
  padding-bottom: 4px;
358
+ }
359
+
360
+ .tcv_toggle_tools {
361
+ vertical-align: middle;
362
+ display: inline-block;
363
+ width: 0.8rem;
364
+ cursor: pointer;
365
+ }
366
+
367
+ .tcv_toggle_tools_wrapper {
368
+ display: none;
369
+ margin-left: 4px;
370
+ cursor: pointer;
371
+ }
372
+
373
+ .tcv_tools_label {
374
+ font-size: 12px;
375
+ font-family: sans-serif;
376
+ font-weight: bold;
377
+ margin-left: 4px;
378
+ padding-bottom: 4px;
356
379
  vertical-align: middle;
357
380
  user-select: none;
358
381
  }
359
382
 
360
383
  .tcv_cad_info_glass {
361
384
  display: none;
385
+ border: none !important;
386
+ background: var(--tcv-bg-overlay-rest-color);
387
+ }
388
+
389
+ .tcv_cad_info_glass:hover {
362
390
  background: var(--tcv-bg-overlay-color);
363
391
  }
364
392
 
@@ -474,7 +502,7 @@ th {
474
502
 
475
503
  .tcv_cad_animation {
476
504
  height: 36px;
477
- background-color: var(--tcv-bg-overlay-color);
505
+ background-color: var(--tcv-bg-overlay-rest-color);
478
506
  border: none;
479
507
  margin: 0;
480
508
  padding: 0px;
@@ -484,9 +512,13 @@ th {
484
512
  z-index: 100;
485
513
  }
486
514
 
515
+ .tcv_cad_animation:hover {
516
+ background-color: var(--tcv-bg-overlay-color);
517
+ }
518
+
487
519
  .tcv_cad_zscale {
488
520
  height: 36px;
489
- background-color: var(--tcv-bg-overlay-color);
521
+ background-color: var(--tcv-bg-overlay-rest-color);
490
522
  border: none;
491
523
  margin: 0;
492
524
  padding: 0px;
@@ -497,6 +529,10 @@ th {
497
529
  align-content: center;
498
530
  }
499
531
 
532
+ .tcv_cad_zscale:hover {
533
+ background-color: var(--tcv-bg-overlay-color);
534
+ }
535
+
500
536
  .tcv_cad_tools {
501
537
  height: 38px;
502
538
  background-color: var(--tcv-bg-overlay-color);
@@ -552,7 +588,7 @@ th {
552
588
  }
553
589
 
554
590
  .tcv_box_content {
555
- overflow: scroll;
591
+ overflow: auto;
556
592
  height: 100%;
557
593
  /* margin: 0px 0px 0px 4px; */
558
594
  padding-top: 2px;
@@ -658,7 +694,10 @@ input[type="button"] {
658
694
  /* margin-bottom: 6px; */
659
695
  background-color: transparent;
660
696
  color: var(--tcv-font-color);
661
- font-size: 14px;
697
+ font-size: 12px;
698
+ overflow: hidden;
699
+ text-overflow: ellipsis;
700
+ white-space: nowrap;
662
701
  }
663
702
 
664
703
  .tcv_tab:hover {
@@ -776,47 +815,60 @@ input[type="button"] {
776
815
  }
777
816
 
778
817
  .tcv_slider_group {
779
- margin-bottom: 10px;
818
+ margin-bottom: var(--tcv-ui-row-gap);
819
+ }
820
+
821
+ .tcv_slider_group > div {
822
+ display: flex;
823
+ align-items: center;
824
+ gap: 4px;
825
+ }
826
+
827
+ .tcv_slider_group > div:first-child {
828
+ align-items: flex-end;
829
+ }
830
+
831
+ .tcv_slider_group .tcv_label {
832
+ margin-bottom: 0;
780
833
  }
781
834
 
782
835
  .tcv_material_ambientlight {
783
836
  margin-top: 4px;
784
837
  }
785
838
 
786
- .tcv_cad_clip_container {
787
- margin-top: 14px;
788
- overflow: hidden;
839
+ .tcv_cad_material_container .tcv_studio_slider_group {
840
+ width: 60%;
789
841
  }
790
842
 
791
- .tcv_cad_material_container {
843
+ .tcv_cad_clip_container,
844
+ .tcv_cad_material_container,
845
+ .tcv_cad_zebra_container,
846
+ .tcv_cad_studio_container {
792
847
  overflow: hidden;
848
+ padding: 0 4px;
793
849
  }
794
850
 
795
- .tcv_cad_zebra_container {
796
- overflow: hidden;
851
+ .tcv_cad_clip_container .tcv_label,
852
+ .tcv_cad_material_container .tcv_label,
853
+ .tcv_cad_zebra_container .tcv_label,
854
+ .tcv_cad_studio_container .tcv_label {
855
+ margin-left: 0;
856
+ margin-right: 0;
797
857
  }
798
858
 
799
- .tcv_zebra_stripe_count {
800
- margin-top: 20px;
801
- }
802
859
 
803
860
  .tcv_zebra_radio {
804
- margin-top: 8px;
805
- margin-bottom: 4px;
861
+ margin-bottom: var(--tcv-ui-row-gap);
806
862
  margin-left: -3px;
807
863
  }
808
864
 
809
- .tcv_zebra_mapping {
810
- margin-top: 8px;
811
- margin-bottom: 4px;
812
- }
813
-
814
865
  .tcv_clip_checks {
815
- margin-top: 20px;
866
+ margin-top: var(--tcv-ui-row-gap);
816
867
  }
817
868
 
818
869
  .tcv_clip_input {
819
- width: 25%;
870
+ width: 36px;
871
+ flex-shrink: 0;
820
872
  border: 1px solid #d3d3d3;
821
873
  background-color: var(--tcv-bg-overlay-color);
822
874
  color: var(--tcv-font-color);
@@ -825,10 +877,11 @@ input[type="button"] {
825
877
  .tcv_clip_slider {
826
878
  -webkit-appearance: none;
827
879
  appearance: none;
828
- width: 60%;
880
+ flex: 1;
881
+ min-width: 0;
829
882
  height: 5px;
830
883
  border-radius: 2px;
831
- background: #d3d3d3;
884
+ background: #a0a0a0;
832
885
  outline: none;
833
886
  opacity: 0.7;
834
887
  -webkit-transition: 0.2s;
@@ -860,7 +913,6 @@ input[type="button"] {
860
913
  }
861
914
 
862
915
  .tcv_lbl_norm_plane1 {
863
- margin-top: 20px;
864
916
  color: var(--tcv-x-color);
865
917
  }
866
918
 
@@ -955,6 +1007,126 @@ input[type="button"] {
955
1007
  font-size: 12px;
956
1008
  }
957
1009
 
1010
+ .tcv_studio_select {
1011
+ font-family: sans-serif;
1012
+ font-size: 13px;
1013
+ padding: 2px 4px;
1014
+ border: 1px solid #d3d3d3;
1015
+ border-radius: 3px;
1016
+ background-color: var(--tcv-bg-overlay-color);
1017
+ color: var(--tcv-font-color);
1018
+ width: 55%;
1019
+ flex-shrink: 0;
1020
+ margin-left: auto;
1021
+ outline: none;
1022
+ }
1023
+
1024
+ .tcv_studio_texture_mapping {
1025
+ width: auto;
1026
+ }
1027
+
1028
+ .tcv_studio_select:hover {
1029
+ opacity: 1;
1030
+ }
1031
+
1032
+ .tcv_studio_select option {
1033
+ background-color: var(--tcv-bg-color);
1034
+ color: var(--tcv-font-color);
1035
+ }
1036
+
1037
+ .tcv_studio_row {
1038
+ display: flex;
1039
+ align-items: center;
1040
+ justify-content: space-between;
1041
+ margin-bottom: var(--tcv-ui-row-gap);
1042
+ }
1043
+
1044
+ .tcv_studio_row > .tcv_label {
1045
+ margin-left: 0;
1046
+ padding-bottom: 0;
1047
+ flex-shrink: 0;
1048
+ }
1049
+
1050
+ .tcv_studio_slider_group {
1051
+ display: flex;
1052
+ align-items: center;
1053
+ width: 55%;
1054
+ flex-shrink: 0;
1055
+ margin-left: auto;
1056
+ gap: 4px;
1057
+ }
1058
+
1059
+ .tcv_studio_slider_group > .tcv_clip_slider {
1060
+ flex: 1;
1061
+ width: auto;
1062
+ min-width: 0;
1063
+ }
1064
+
1065
+ .tcv_studio_slider_group > .tcv_clip_input {
1066
+ width: 36px;
1067
+ flex-shrink: 0;
1068
+ }
1069
+
1070
+ .tcv_studio_checks {
1071
+ display: flex;
1072
+ gap: 16px;
1073
+ margin-top: var(--tcv-ui-row-gap);
1074
+ margin-bottom: var(--tcv-ui-row-gap);
1075
+ }
1076
+
1077
+ .tcv_studio_4k_row {
1078
+ /* Align checkbox with the slider/dropdown column (45% label + 55% control) */
1079
+ margin-left: 45%;
1080
+ gap: 4px;
1081
+ }
1082
+
1083
+ .tcv_studio_group_spacer {
1084
+ height: calc(var(--tcv-ui-row-gap) * 0.6);
1085
+ }
1086
+
1087
+ .tcv_studio_info {
1088
+ font-size: 12px;
1089
+ margin-top: 8px;
1090
+ color: var(--tcv-font-color);
1091
+ opacity: 0.7;
1092
+ font-style: italic;
1093
+ }
1094
+
1095
+ .tcv_warning_banner {
1096
+ position: absolute;
1097
+ top: 4px;
1098
+ left: 50%;
1099
+ transform: translateX(-50%);
1100
+ z-index: 100;
1101
+ background: #e8a735;
1102
+ color: #000;
1103
+ padding: 4px 12px;
1104
+ border-radius: 4px;
1105
+ font-size: 12px;
1106
+ line-height: 1.4;
1107
+ max-width: 80%;
1108
+ text-align: center;
1109
+ pointer-events: auto;
1110
+ box-shadow: 0 1px 4px rgba(0,0,0,0.3);
1111
+ }
1112
+
1113
+ .tcv_studio_spinner {
1114
+ display: none;
1115
+ width: 8px;
1116
+ height: 8px;
1117
+ border: 2px solid var(--tcv-font-color);
1118
+ border-top-color: transparent;
1119
+ border-radius: 50%;
1120
+ animation: tcv_spin 0.7s linear infinite;
1121
+ opacity: 0.6;
1122
+ float: left;
1123
+ margin-top: 5px;
1124
+ }
1125
+
1126
+ @keyframes tcv_spin {
1127
+ to { transform: rotate(360deg); }
1128
+ }
1129
+
958
1130
  .tcv_filter_menu {
959
1131
  position: relative;
960
1132
  }
@@ -1253,4 +1425,142 @@ input[type="button"] {
1253
1425
 
1254
1426
  .tcv_angle_val {
1255
1427
  text-align: right;
1428
+ }
1429
+
1430
+ /* MATERIAL EDITOR PANEL */
1431
+
1432
+ .tcv_mat_editor {
1433
+ position: absolute;
1434
+ top: 8px;
1435
+ right: 8px;
1436
+ width: 250px;
1437
+ max-height: calc(100% - 16px);
1438
+ display: flex;
1439
+ flex-direction: column;
1440
+ background-color: var(--tcv-bg-overlay-rest-color);
1441
+ border: 1px solid lightgray;
1442
+ border-radius: 6px;
1443
+ font-family: sans-serif;
1444
+ font-size: 13px;
1445
+ color: var(--tcv-font-color);
1446
+ user-select: none;
1447
+ z-index: 10;
1448
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
1449
+ }
1450
+
1451
+ .tcv_mat_editor:hover {
1452
+ background-color: var(--tcv-bg-overlay-color);
1453
+ }
1454
+
1455
+ .tcv_mat_editor_titlebar {
1456
+ display: flex;
1457
+ align-items: center;
1458
+ justify-content: space-between;
1459
+ padding: 4px 6px;
1460
+ border-bottom: 1px solid lightgray;
1461
+ background-color: var(--tcv-bg-overlay-color);
1462
+ border-radius: 5px 5px 0 0;
1463
+ flex-shrink: 0;
1464
+ cursor: move;
1465
+ }
1466
+
1467
+ .tcv_mat_editor_titlebar_btns {
1468
+ display: flex;
1469
+ gap: 2px;
1470
+ flex-shrink: 0;
1471
+ }
1472
+
1473
+ .tcv_mat_editor_title {
1474
+ font-weight: bold;
1475
+ font-size: 12px;
1476
+ }
1477
+
1478
+ .tcv_mat_editor_close {
1479
+ cursor: pointer;
1480
+ }
1481
+
1482
+ .tcv_mat_editor_path {
1483
+ padding: 3px 6px;
1484
+ font-size: 11px;
1485
+ color: var(--tcv-font-color);
1486
+ opacity: 0.7;
1487
+ border-bottom: 1px solid lightgray;
1488
+ white-space: nowrap;
1489
+ overflow: hidden;
1490
+ text-overflow: ellipsis;
1491
+ flex-shrink: 0;
1492
+ user-select: text;
1493
+ }
1494
+
1495
+ .tcv_mat_editor_content {
1496
+ padding: 6px;
1497
+ overflow-y: auto;
1498
+ flex: 1;
1499
+ min-height: 0;
1500
+ }
1501
+
1502
+ .tcv_mat_editor_section {
1503
+ font-size: 11px;
1504
+ font-weight: bold;
1505
+ text-transform: uppercase;
1506
+ letter-spacing: 0.5px;
1507
+ color: var(--tcv-font-color);
1508
+ opacity: 0.6;
1509
+ margin-top: 8px;
1510
+ margin-bottom: 4px;
1511
+ padding-bottom: 2px;
1512
+ border-bottom: 1px solid var(--tcv-bg-overlay-color);
1513
+ }
1514
+
1515
+ .tcv_mat_editor_section:first-child {
1516
+ margin-top: 0;
1517
+ }
1518
+
1519
+ .tcv_mat_editor_row {
1520
+ display: flex;
1521
+ align-items: center;
1522
+ justify-content: space-between;
1523
+ margin-bottom: 5px;
1524
+ }
1525
+
1526
+ .tcv_mat_editor_label {
1527
+ font-size: 12px;
1528
+ color: var(--tcv-font-color);
1529
+ flex-shrink: 0;
1530
+ max-width: 45%;
1531
+ overflow: hidden;
1532
+ text-overflow: ellipsis;
1533
+ white-space: nowrap;
1534
+ }
1535
+
1536
+ /* Reuse tcv_studio_slider_group layout for slider + value */
1537
+ .tcv_mat_editor_slider_group {
1538
+ display: flex;
1539
+ align-items: center;
1540
+ width: 52%;
1541
+ flex-shrink: 0;
1542
+ margin-left: auto;
1543
+ gap: 4px;
1544
+ }
1545
+
1546
+ /* Sliders reuse tcv_clip_slider class (set in JS) */
1547
+ .tcv_mat_editor_slider_group > .tcv_clip_slider {
1548
+ flex: 1;
1549
+ width: auto;
1550
+ min-width: 0;
1551
+ }
1552
+
1553
+ /* Value inputs reuse tcv_clip_input class (set in JS) */
1554
+ .tcv_mat_editor_slider_group > .tcv_clip_input {
1555
+ width: 36px;
1556
+ flex-shrink: 0;
1557
+ }
1558
+
1559
+ .tcv_mat_editor_changed {
1560
+ color: #cc3333 !important;
1561
+ }
1562
+
1563
+ .tcv_mat_editor_toggle.tcv_active {
1564
+ background-color: var(--tcv-bg-overlay-color);
1565
+ border-radius: 6px;
1256
1566
  }