abstract-3d 3.1.2 → 4.0.0-beta.10

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 (236) hide show
  1. package/README.md +53 -1
  2. package/lib/abstract-3d.d.ts +27 -1
  3. package/lib/abstract-3d.d.ts.map +1 -1
  4. package/lib/abstract-3d.js +26 -2
  5. package/lib/abstract-3d.js.map +1 -1
  6. package/lib/renderers/dxf/dxf-encoding/dxf-build.d.ts.map +1 -1
  7. package/lib/renderers/dxf/dxf-encoding/dxf-build.js +1 -2
  8. package/lib/renderers/dxf/dxf-encoding/dxf-build.js.map +1 -1
  9. package/lib/renderers/ifc/ifc.test.d.ts +2 -0
  10. package/lib/renderers/ifc/ifc.test.d.ts.map +1 -0
  11. package/lib/renderers/ifc/ifc.test.js +380 -0
  12. package/lib/renderers/ifc/ifc.test.js.map +1 -0
  13. package/lib/renderers/index.d.ts +1 -0
  14. package/lib/renderers/index.d.ts.map +1 -1
  15. package/lib/renderers/index.js +1 -0
  16. package/lib/renderers/index.js.map +1 -1
  17. package/lib/renderers/react/index.d.ts +2 -5
  18. package/lib/renderers/react/index.d.ts.map +1 -1
  19. package/lib/renderers/react/index.js +1 -2
  20. package/lib/renderers/react/index.js.map +1 -1
  21. package/lib/renderers/react/react.d.ts +16 -42
  22. package/lib/renderers/react/react.d.ts.map +1 -1
  23. package/lib/renderers/react/react.js +56 -32
  24. package/lib/renderers/react/react.js.map +1 -1
  25. package/lib/renderers/three/appearance/appearance.d.ts +46 -0
  26. package/lib/renderers/three/appearance/appearance.d.ts.map +1 -0
  27. package/lib/renderers/three/appearance/appearance.js +102 -0
  28. package/lib/renderers/three/appearance/appearance.js.map +1 -0
  29. package/lib/renderers/three/appearance/appearance.test.d.ts +2 -0
  30. package/lib/renderers/three/appearance/appearance.test.d.ts.map +1 -0
  31. package/lib/renderers/three/appearance/appearance.test.js +97 -0
  32. package/lib/renderers/three/appearance/appearance.test.js.map +1 -0
  33. package/lib/renderers/three/appearance.d.ts +41 -0
  34. package/lib/renderers/three/appearance.d.ts.map +1 -0
  35. package/lib/renderers/three/appearance.js +100 -0
  36. package/lib/renderers/three/appearance.js.map +1 -0
  37. package/lib/renderers/three/build/build.d.ts +53 -0
  38. package/lib/renderers/three/build/build.d.ts.map +1 -0
  39. package/lib/renderers/three/build/build.js +181 -0
  40. package/lib/renderers/three/build/build.js.map +1 -0
  41. package/lib/renderers/three/build/build.test.d.ts +2 -0
  42. package/lib/renderers/three/build/build.test.d.ts.map +1 -0
  43. package/lib/renderers/three/build/build.test.js +120 -0
  44. package/lib/renderers/three/build/build.test.js.map +1 -0
  45. package/lib/renderers/three/build/materials.d.ts +20 -0
  46. package/lib/renderers/three/build/materials.d.ts.map +1 -0
  47. package/lib/renderers/three/build/materials.js +168 -0
  48. package/lib/renderers/three/build/materials.js.map +1 -0
  49. package/lib/renderers/three/build/parts.d.ts +65 -0
  50. package/lib/renderers/three/build/parts.d.ts.map +1 -0
  51. package/lib/renderers/three/build/parts.js +203 -0
  52. package/lib/renderers/three/build/parts.js.map +1 -0
  53. package/lib/renderers/three/build.d.ts +50 -0
  54. package/lib/renderers/three/build.d.ts.map +1 -0
  55. package/lib/renderers/three/build.js +210 -0
  56. package/lib/renderers/three/build.js.map +1 -0
  57. package/lib/renderers/three/camera/camera.d.ts +40 -0
  58. package/lib/renderers/three/camera/camera.d.ts.map +1 -0
  59. package/lib/renderers/three/camera/camera.js +51 -0
  60. package/lib/renderers/three/camera/camera.js.map +1 -0
  61. package/lib/renderers/three/camera/camera.test.d.ts +2 -0
  62. package/lib/renderers/three/camera/camera.test.d.ts.map +1 -0
  63. package/lib/renderers/three/camera/camera.test.js +46 -0
  64. package/lib/renderers/three/camera/camera.test.js.map +1 -0
  65. package/lib/renderers/three/camera/controls.d.ts +24 -0
  66. package/lib/renderers/three/camera/controls.d.ts.map +1 -0
  67. package/lib/renderers/three/camera/controls.js +39 -0
  68. package/lib/renderers/three/camera/controls.js.map +1 -0
  69. package/lib/renderers/three/camera/gizmo.d.ts +44 -0
  70. package/lib/renderers/three/camera/gizmo.d.ts.map +1 -0
  71. package/lib/renderers/three/camera/gizmo.js +43 -0
  72. package/lib/renderers/three/camera/gizmo.js.map +1 -0
  73. package/lib/renderers/three/camera.d.ts +63 -0
  74. package/lib/renderers/three/camera.d.ts.map +1 -0
  75. package/lib/renderers/three/camera.js +85 -0
  76. package/lib/renderers/three/camera.js.map +1 -0
  77. package/lib/renderers/three/compile/compile.d.ts +59 -0
  78. package/lib/renderers/three/compile/compile.d.ts.map +1 -0
  79. package/lib/renderers/three/compile/compile.js +88 -0
  80. package/lib/renderers/three/compile/compile.js.map +1 -0
  81. package/lib/renderers/three/compile/compile.test.d.ts +2 -0
  82. package/lib/renderers/three/compile/compile.test.d.ts.map +1 -0
  83. package/lib/renderers/three/compile/compile.test.js +93 -0
  84. package/lib/renderers/three/compile/compile.test.js.map +1 -0
  85. package/lib/renderers/three/compile.d.ts +58 -0
  86. package/lib/renderers/three/compile.d.ts.map +1 -0
  87. package/lib/renderers/three/compile.js +89 -0
  88. package/lib/renderers/three/compile.js.map +1 -0
  89. package/lib/renderers/three/controls.d.ts +27 -0
  90. package/lib/renderers/three/controls.d.ts.map +1 -0
  91. package/lib/renderers/three/controls.js +115 -0
  92. package/lib/renderers/three/controls.js.map +1 -0
  93. package/lib/renderers/three/environment/studio.d.ts +9 -0
  94. package/lib/renderers/three/environment/studio.d.ts.map +1 -0
  95. package/lib/renderers/three/environment/studio.js +15 -0
  96. package/lib/renderers/three/environment/studio.js.map +1 -0
  97. package/lib/renderers/three/geometry/geometry.d.ts +21 -0
  98. package/lib/renderers/three/geometry/geometry.d.ts.map +1 -0
  99. package/lib/renderers/three/geometry/geometry.js +141 -0
  100. package/lib/renderers/three/geometry/geometry.js.map +1 -0
  101. package/lib/renderers/three/geometry/geometry.test.d.ts +2 -0
  102. package/lib/renderers/three/geometry/geometry.test.d.ts.map +1 -0
  103. package/lib/renderers/three/geometry/geometry.test.js +80 -0
  104. package/lib/renderers/three/geometry/geometry.test.js.map +1 -0
  105. package/lib/renderers/three/geometry.d.ts +20 -0
  106. package/lib/renderers/three/geometry.d.ts.map +1 -0
  107. package/lib/renderers/three/geometry.js +164 -0
  108. package/lib/renderers/three/geometry.js.map +1 -0
  109. package/lib/renderers/three/gizmo.d.ts +49 -0
  110. package/lib/renderers/three/gizmo.d.ts.map +1 -0
  111. package/lib/renderers/three/gizmo.js +172 -0
  112. package/lib/renderers/three/gizmo.js.map +1 -0
  113. package/lib/renderers/three/index.d.ts +16 -0
  114. package/lib/renderers/three/index.d.ts.map +1 -0
  115. package/lib/renderers/three/index.js +6 -0
  116. package/lib/renderers/three/index.js.map +1 -0
  117. package/lib/renderers/three/input.d.ts +25 -0
  118. package/lib/renderers/three/input.d.ts.map +1 -0
  119. package/lib/renderers/three/input.js +71 -0
  120. package/lib/renderers/three/input.js.map +1 -0
  121. package/lib/renderers/three/lines.d.ts +16 -0
  122. package/lib/renderers/three/lines.d.ts.map +1 -0
  123. package/lib/renderers/three/lines.js +63 -0
  124. package/lib/renderers/three/lines.js.map +1 -0
  125. package/lib/renderers/three/materials.d.ts +15 -0
  126. package/lib/renderers/three/materials.d.ts.map +1 -0
  127. package/lib/renderers/three/materials.js +70 -0
  128. package/lib/renderers/three/materials.js.map +1 -0
  129. package/lib/renderers/three/options.d.ts +68 -0
  130. package/lib/renderers/three/options.d.ts.map +1 -0
  131. package/lib/renderers/three/options.js +64 -0
  132. package/lib/renderers/three/options.js.map +1 -0
  133. package/lib/renderers/three/overlays.d.ts +25 -0
  134. package/lib/renderers/three/overlays.d.ts.map +1 -0
  135. package/lib/renderers/three/overlays.js +57 -0
  136. package/lib/renderers/three/overlays.js.map +1 -0
  137. package/lib/renderers/three/quality/quality.d.ts +18 -0
  138. package/lib/renderers/three/quality/quality.d.ts.map +1 -0
  139. package/lib/renderers/three/quality/quality.js +28 -0
  140. package/lib/renderers/three/quality/quality.js.map +1 -0
  141. package/lib/renderers/three/quality/quality.test.d.ts +2 -0
  142. package/lib/renderers/three/quality/quality.test.d.ts.map +1 -0
  143. package/lib/renderers/three/quality/quality.test.js +23 -0
  144. package/lib/renderers/three/quality/quality.test.js.map +1 -0
  145. package/lib/renderers/three/quality.d.ts +24 -0
  146. package/lib/renderers/three/quality.d.ts.map +1 -0
  147. package/lib/renderers/three/quality.js +67 -0
  148. package/lib/renderers/three/quality.js.map +1 -0
  149. package/lib/renderers/three/shadow/key-light.d.ts +6 -0
  150. package/lib/renderers/three/shadow/key-light.d.ts.map +1 -0
  151. package/lib/renderers/three/shadow/key-light.js +39 -0
  152. package/lib/renderers/three/shadow/key-light.js.map +1 -0
  153. package/lib/renderers/three/shadow/shadow.d.ts +21 -0
  154. package/lib/renderers/three/shadow/shadow.d.ts.map +1 -0
  155. package/lib/renderers/three/shadow/shadow.js +120 -0
  156. package/lib/renderers/three/shadow/shadow.js.map +1 -0
  157. package/lib/renderers/three/text/text.d.ts +55 -0
  158. package/lib/renderers/three/text/text.d.ts.map +1 -0
  159. package/lib/renderers/three/text/text.js +90 -0
  160. package/lib/renderers/three/text/text.js.map +1 -0
  161. package/lib/renderers/three/text/text.test.d.ts +2 -0
  162. package/lib/renderers/three/text/text.test.d.ts.map +1 -0
  163. package/lib/renderers/three/text/text.test.js +70 -0
  164. package/lib/renderers/three/text/text.test.js.map +1 -0
  165. package/lib/renderers/three/text.d.ts +53 -0
  166. package/lib/renderers/three/text.d.ts.map +1 -0
  167. package/lib/renderers/three/text.js +151 -0
  168. package/lib/renderers/three/text.js.map +1 -0
  169. package/lib/renderers/three/viewer/options.d.ts +82 -0
  170. package/lib/renderers/three/viewer/options.d.ts.map +1 -0
  171. package/lib/renderers/three/viewer/options.js +34 -0
  172. package/lib/renderers/three/viewer/options.js.map +1 -0
  173. package/lib/renderers/three/viewer/options.test.d.ts +2 -0
  174. package/lib/renderers/three/viewer/options.test.d.ts.map +1 -0
  175. package/lib/renderers/three/viewer/options.test.js +38 -0
  176. package/lib/renderers/three/viewer/options.test.js.map +1 -0
  177. package/lib/renderers/three/viewer/overlays.d.ts +31 -0
  178. package/lib/renderers/three/viewer/overlays.d.ts.map +1 -0
  179. package/lib/renderers/three/viewer/overlays.js +43 -0
  180. package/lib/renderers/three/viewer/overlays.js.map +1 -0
  181. package/lib/renderers/three/viewer/viewer.d.ts +102 -0
  182. package/lib/renderers/three/viewer/viewer.d.ts.map +1 -0
  183. package/lib/renderers/three/viewer/viewer.js +488 -0
  184. package/lib/renderers/three/viewer/viewer.js.map +1 -0
  185. package/lib/renderers/three/viewer.d.ts +38 -0
  186. package/lib/renderers/three/viewer.d.ts.map +1 -0
  187. package/lib/renderers/three/viewer.js +435 -0
  188. package/lib/renderers/three/viewer.js.map +1 -0
  189. package/package.json +34 -16
  190. package/src/abstract-3d.ts +60 -2
  191. package/src/renderers/dxf/dxf-encoding/dxf-build.ts +1 -2
  192. package/src/renderers/ifc/ifc.test.ts +7 -5
  193. package/src/renderers/index.ts +1 -0
  194. package/src/renderers/react/index.ts +2 -5
  195. package/src/renderers/react/react.tsx +107 -181
  196. package/src/renderers/three/appearance/appearance.test.ts +153 -0
  197. package/src/renderers/three/appearance/appearance.ts +150 -0
  198. package/src/renderers/three/build/build.test.ts +149 -0
  199. package/src/renderers/three/build/build.ts +278 -0
  200. package/src/renderers/three/build/materials.ts +222 -0
  201. package/src/renderers/three/build/parts.ts +277 -0
  202. package/src/renderers/three/camera/camera.test.ts +53 -0
  203. package/src/renderers/three/camera/camera.ts +82 -0
  204. package/src/renderers/three/camera/controls.ts +65 -0
  205. package/src/renderers/three/camera/gizmo.ts +94 -0
  206. package/src/renderers/three/compile/compile.test.ts +126 -0
  207. package/src/renderers/three/compile/compile.ts +151 -0
  208. package/src/renderers/three/environment/studio.ts +18 -0
  209. package/src/renderers/three/geometry/geometry.test.ts +121 -0
  210. package/src/renderers/three/geometry/geometry.ts +189 -0
  211. package/src/renderers/three/index.ts +15 -0
  212. package/src/renderers/three/quality/quality.test.ts +37 -0
  213. package/src/renderers/three/quality/quality.ts +36 -0
  214. package/src/renderers/three/shadow/key-light.ts +43 -0
  215. package/src/renderers/three/shadow/shadow.ts +170 -0
  216. package/src/renderers/three/text/text.test.ts +79 -0
  217. package/src/renderers/three/text/text.ts +128 -0
  218. package/src/renderers/three/viewer/options.ts +112 -0
  219. package/src/renderers/three/viewer/overlays.ts +71 -0
  220. package/src/renderers/three/viewer/viewer.ts +596 -0
  221. package/lib/renderers/react/react-hotspot.d.ts +0 -27
  222. package/lib/renderers/react/react-hotspot.d.ts.map +0 -1
  223. package/lib/renderers/react/react-hotspot.js +0 -37
  224. package/lib/renderers/react/react-hotspot.js.map +0 -1
  225. package/src/custom_typings/react-three-fiber.d.ts +0 -9
  226. package/src/custom_typings/react-three-postprocessing.d.ts +0 -1
  227. package/src/renderers/react/react-camera.tsx +0 -391
  228. package/src/renderers/react/react-dimension.tsx +0 -122
  229. package/src/renderers/react/react-group.test.ts +0 -56
  230. package/src/renderers/react/react-group.tsx +0 -202
  231. package/src/renderers/react/react-image-material.tsx +0 -139
  232. package/src/renderers/react/react-material.tsx +0 -91
  233. package/src/renderers/react/react-mesh.tsx +0 -512
  234. package/src/renderers/react/react-relation.tsx +0 -86
  235. package/src/renderers/react/react-scene.tsx +0 -111
  236. package/src/renderers/react/react-types.ts +0 -9
@@ -8,21 +8,23 @@ import { render } from "./ifc.js";
8
8
  const red = { normal: "rgb(255, 0, 0)", opacity: 0.5 };
9
9
  const scene = (groups: Scene["groups"], relations?: Scene["relations"]): Scene => ({ size_deprecated: vec3Zero, groups, relations });
10
10
  const count = (ifc: string, entity: string): number => ifc.split(`=${entity}(`).length - 1;
11
- const ids = (s: string): ReadonlyArray<string> => Array.from(s.matchAll(/#(\d+)/g), (match) => match[1]);
11
+ const ids = (s: string): ReadonlyArray<string> => Array.from(s.matchAll(/#(\d+)/g), (match) => match.at(1) ?? "");
12
12
  /** Random GlobalIds replaced, to compare files */
13
13
  const normalize = (ifc: string): string => ifc.replaceAll(/'[0-9A-Za-z_$]{22}'/g, "'guid'");
14
14
 
15
- const entities = (ifc: string): ReadonlyMap<string, string> => new Map(Array.from(ifc.matchAll(/^#(\d+)=(.*);$/gm), (match) => [match[1], match[2]]));
15
+ /** By id; looked up by ids destructured from a list, so by possibly undefined ones */
16
+ const entities = (ifc: string): ReadonlyMap<string | undefined, string> =>
17
+ new Map(Array.from(ifc.matchAll(/^#(\d+)=(.*);$/gm), (match) => [match.at(1), match.at(2) ?? ""]));
16
18
 
17
19
  /** The #id of the element or port named `name` */
18
- const named = (ifc: string, name: string): string => new RegExp(`^#(\\d+)=IFC\\w+\\('[^']{22}',#\\d+,'${name}',`, "m").exec(ifc)![1];
20
+ const named = (ifc: string, name: string): string => new RegExp(`^#(\\d+)=IFC\\w+\\('[^']{22}',#\\d+,'${name}',`, "m").exec(ifc)?.at(1) ?? "";
19
21
 
20
22
  /** The layer of every face set and polyline of the element named `name`, in order */
21
23
  function elementLayers(ifc: string, name: string): ReadonlyArray<string | undefined> {
22
24
  const byId = entities(ifc);
23
25
  const layers = new Map(
24
- Array.from(ifc.matchAll(/[=]IFCPRESENTATIONLAYERASSIGNMENT\('([^']*)',\$,\(([^)]*)\),\$\);/g)).flatMap(([, layer, items]) =>
25
- ids(items).map((item) => [item, layer] as const)
26
+ Array.from(ifc.matchAll(/[=]IFCPRESENTATIONLAYERASSIGNMENT\('([^']*)',\$,\(([^)]*)\),\$\);/g)).flatMap((match) =>
27
+ ids(match.at(2) ?? "").map((item) => [item, match.at(1)] as const)
26
28
  )
27
29
  );
28
30
  // The element's owner history, placement and shape, the shape's representations and their context and items
@@ -2,5 +2,6 @@ export * as Dxf from "./dxf/index.js";
2
2
  export * as Svg from "./svg/index.js";
3
3
  export * as Stl from "./stl/index.js";
4
4
  export * as React3Js from "./react/index.js";
5
+ export * as Three from "./three/index.js";
5
6
  export * as Step from "./step/index.js";
6
7
  export * as Ifc from "./ifc/index.js";
@@ -1,6 +1,3 @@
1
1
  export * from "./react.js";
2
- export * from "./react-types.js";
3
- export type { SceneMouseEvent } from "./react-scene.js";
4
- export type { CameraType, Camera, ControlsHelper } from "./react-camera.js";
5
- export type { MaterialState } from "./react-material.js";
6
- export { ERROR_IMG_KEY } from "./react-material.js";
2
+ export type { CameraOptions as Camera, BufferZones, ControlsHelper, ControlsOptions, MaterialState, SceneEvent, Quality } from "../three/index.js";
3
+ export { ERROR_IMG_KEY } from "../three/index.js";
@@ -1,187 +1,113 @@
1
- import { Html, type OrbitControlsProps } from "@react-three/drei";
2
- import { Canvas, type CanvasProps, type ThreeEvent } from "@react-three/fiber";
3
- import { EffectComposer, N8AO } from "@react-three/postprocessing";
4
- import React, { memo, useMemo } from "react";
5
- import type { Scene, View, Group, Relation } from "../../abstract-3d.js";
6
- import type { ControlsHelper, Camera, BufferZones } from "./react-camera.js";
7
- import { ReactCamera } from "./react-camera.js";
8
- import type { MaterialState } from "./react-material.js";
9
- import { ReactScene } from "./react-scene.js";
10
- import type { ReactPopover } from "./react-types.js";
1
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import type { Group, Scene } from "../../abstract-3d.js";
4
+ import type { ControlsHelper, MountedOverlay, Popover, Viewer as ThreeViewer, ViewerOptions } from "../three/index.js";
5
+ import { createViewer, disposeViewer, updateViewer } from "../three/index.js";
11
6
 
12
- type ReactProps = {
13
- readonly scene: Scene | undefined; // undefined to allow for simple early loadign the js when lazy loading the React3Js renderer
14
- readonly selectedIds: Record<string, boolean> | undefined;
15
- /**
16
- * The ids of the relations whose shape is drawn to be clicked, e.g. where a component can be inserted. While defined, even when empty, only the
17
- * groups whose material state is Accept can be clicked
18
- */
19
- readonly activeRelations?: ReadonlyArray<string> | undefined;
20
- readonly activeComponents?: Record<string, MaterialState> | undefined;
21
- readonly hoveredIdsExternal?: ReadonlyArray<string> | undefined;
22
- readonly showDimensions?: boolean;
23
- readonly popovers?: ReadonlyArray<ReactPopover>;
24
- readonly useAnimations?: boolean;
25
- readonly camera?: Camera;
26
- readonly view?: View;
27
- readonly useAlphaTest?: boolean;
7
+ /**
8
+ * The three.js viewer as a React component: it creates and disposes the viewer, passes each group of props when they change, and portals popovers,
9
+ * tooltips and the fallback. The viewer's overlays carry ids, and their content is looked up in this render's props, so new content doesn't loop back
10
+ * through the viewer
11
+ */
12
+
13
+ export type ReactPopover = Popover<React.ReactNode>;
14
+
15
+ export type ViewerProps = Omit<ViewerOptions<React.ReactNode>, "hoveredIds" | "onOverlays" | "onLoad" | "gizmo"> & {
16
+ readonly scene: Scene | undefined;
17
+ readonly hoveredIdsExternal?: ReadonlyArray<string>;
18
+ /** A viewcube or axes gizmo */
28
19
  readonly controlsHelper?: ControlsHelper;
29
- readonly canvasProps?: Omit<CanvasProps & React.RefAttributes<HTMLCanvasElement>, "children">;
30
- readonly orbitContolsProps?: OrbitControlsProps & React.RefAttributes<unknown>;
31
- readonly materialStateImages?: Record<string, string>;
32
- readonly sceneFallback?: React.JSX.Element;
33
- /**
34
- * The scene's lights, rendered in the canvas instead of the default ambient and directional lights, e.g. an environment map for reflections and a
35
- * key light. A constant element, as a new one each render re-renders the whole scene
36
- */
37
- readonly lights?: React.JSX.Element;
38
- readonly useOldMode?: boolean;
39
- readonly bufferZones?: BufferZones;
40
- readonly fitPadding?: number;
41
- readonly onClickGroup?: (
42
- id: string | undefined,
43
- rootData: Record<string, string> | undefined,
44
- data: Record<string, string> | undefined,
45
- e: ThreeEvent<MouseEvent>
46
- ) => void;
47
- readonly onHoverGroup?: (
48
- id: string | undefined,
49
- rootData: Record<string, string> | undefined,
50
- data: Record<string, string> | undefined,
51
- e: ThreeEvent<MouseEvent>
52
- ) => void;
53
- readonly onContextMenuGroup?: (
54
- id: string,
55
- rootData: Record<string, string> | undefined,
56
- data: Record<string, string> | undefined,
57
- left: number,
58
- top: number,
59
- e: ThreeEvent<MouseEvent>
60
- ) => void;
61
- readonly getTooltips?: (id: string) => ReadonlyArray<ReactPopover>;
62
- readonly onClickRelation?: (relation: Relation, e: ThreeEvent<MouseEvent>) => void;
63
- readonly createGroupKey?: (g: Group, idx: number, rootData: Record<string, string> | undefined, id: string) => string;
64
- readonly createGroupId?: (g: Group) => string;
20
+ /** Shown while loading, and while there's no scene */
21
+ readonly sceneFallback?: React.ReactNode;
22
+ readonly className?: string;
23
+ readonly style?: React.CSSProperties;
65
24
  };
66
25
 
67
- export const render = memo(
68
- ({
69
- scene,
70
- selectedIds,
71
- useOldMode,
72
- activeRelations,
73
- activeComponents,
74
- hoveredIdsExternal,
75
- popovers: reactPopovers,
76
- showDimensions = true,
77
- useAnimations = false,
78
- camera = { type: "Perspective" },
79
- view = "front",
80
- controlsHelper,
81
- canvasProps,
82
- orbitContolsProps,
83
- materialStateImages,
84
- sceneFallback,
85
- lights,
86
- useAlphaTest = true,
87
- onClickGroup,
88
- onHoverGroup,
89
- onContextMenuGroup,
90
- getTooltips,
91
- onClickRelation,
92
- createGroupKey,
93
- createGroupId,
94
- bufferZones,
95
- fitPadding,
96
- }: ReactProps): React.JSX.Element => {
97
- const intensity = useOldMode ? 2 : 0.4;
98
- const suspenseFallback = useMemo(() => <Html center>{sceneFallback ?? <></>}</Html>, [sceneFallback]);
99
- return scene ? (
100
- <Canvas dpr={[1, window.devicePixelRatio]} frameloop="demand" {...canvasProps}>
101
- <React.Suspense fallback={suspenseFallback}>
102
- <ReactCamera
103
- bufferZones={bufferZones}
104
- fitPadding={fitPadding}
105
- scene={scene}
106
- useAnimations={useAnimations}
107
- camera={camera}
108
- view={view}
109
- controlsHelper={controlsHelper}
110
- orbitContolsProps={orbitContolsProps}
111
- />
112
- {lights ?? (
113
- <>
114
- <ambientLight intensity={3.5} />
115
- <directionalLight
116
- position={[
117
- -(scene.center_deprecated?.x ?? 0) - 1.5 * scene.size_deprecated.x,
118
- -(scene.center_deprecated?.y ?? 0) + 1.5 * scene.size_deprecated.y,
119
- -(scene.center_deprecated?.z ?? 0),
120
- ]}
121
- intensity={intensity}
122
- />
123
- <directionalLight
124
- position={[
125
- -(scene.center_deprecated?.x ?? 0),
126
- -(scene.center_deprecated?.y ?? 0) + 1 * scene.size_deprecated.y,
127
- -(scene.center_deprecated?.z ?? 0) + 1.5 * scene.size_deprecated.z,
128
- ]}
129
- intensity={intensity}
130
- />
131
- <directionalLight
132
- position={[
133
- -(scene.center_deprecated?.x ?? 0) + 2 * scene.size_deprecated.x,
134
- -(scene.center_deprecated?.y ?? 0),
135
- -(scene.center_deprecated?.z ?? 0) + 1.5 * scene.size_deprecated.z,
136
- ]}
137
- intensity={intensity}
138
- />
139
- <directionalLight
140
- position={[
141
- -(scene.center_deprecated?.x ?? 0) - 1.5 * scene.size_deprecated.x,
142
- -(scene.center_deprecated?.y ?? 0),
143
- -(scene.center_deprecated?.z ?? 0),
144
- ]}
145
- intensity={0.4}
146
- />
147
- <directionalLight
148
- position={[
149
- -(scene.center_deprecated?.x ?? 0) + 1.5 * scene.size_deprecated.x,
150
- -(scene.center_deprecated?.y ?? 0),
151
- -(scene.center_deprecated?.z ?? 0),
152
- ]}
153
- intensity={0.4}
154
- />
155
- </>
156
- )}
26
+ export function Viewer(props: ViewerProps): React.JSX.Element {
27
+ const { scene, createGroupId, useAlphaTest, selectedIds, hoveredIdsExternal, onClickGroup, view, bufferZones, fitPadding, controls } = props;
28
+ const { lights, environmentIntensity, studio, groundShadow, shadows, quality, showDimensions, popovers, getTooltips } = props;
29
+ const { onHoverGroup, onContextMenuGroup, onClickRelation, onPointerMissed, sceneFallback, className, style } = props;
30
+ // Equal by value, so inline objects don't refit the camera, recreate the gizmo or rebuild the scene
31
+ const camera = useStableValue(props.camera);
32
+ const gizmo = useStableValue(props.controlsHelper);
33
+ const activeComponents = useStableValue(props.activeComponents);
34
+ const activeRelations = useStableValue(props.activeRelations);
35
+ const materialStateImages = useStableValue(props.materialStateImages);
36
+ const container = useRef<HTMLDivElement>(null);
37
+ const viewer = useRef<ThreeViewer<string> | undefined>(undefined);
38
+ const [overlays, setOverlays] = useState<ReadonlyArray<MountedOverlay<string>>>([]);
39
+ const [loaded, setLoaded] = useState(false);
40
+
41
+ useLayoutEffect(() => {
42
+ viewer.current = createViewer(container.current!, { onOverlays: setOverlays, onLoad: () => setLoaded(true) });
43
+ return () => disposeViewer(viewer.current!);
44
+ }, []);
45
+
46
+ // The latest createGroupId through a stable function, so an inline one doesn't rebuild the scene each render, as 3.x didn't; a new scene does
47
+ const latestGroupId = useRef(createGroupId);
48
+ useLayoutEffect(() => {
49
+ latestGroupId.current = createGroupId;
50
+ });
51
+ const [stableGroupId] = useState(
52
+ () =>
53
+ (g: Group): string =>
54
+ latestGroupId.current?.(g) ?? ""
55
+ );
56
+ useEffect(() => updateViewer(viewer.current!, { scene, createGroupId: stableGroupId }), [scene, stableGroupId]);
57
+ useEffect(
58
+ () => updateViewer(viewer.current!, { activeComponents, activeRelations, useAlphaTest, materialStateImages }),
59
+ [activeComponents, activeRelations, useAlphaTest, materialStateImages]
60
+ );
61
+ useEffect(
62
+ () => updateViewer(viewer.current!, { selectedIds, hoveredIds: hoveredIdsExternal, onClickGroup }),
63
+ [selectedIds, hoveredIdsExternal, onClickGroup]
64
+ );
65
+ useEffect(() => updateViewer(viewer.current!, { view, bufferZones, fitPadding, camera }), [view, bufferZones, fitPadding, camera]);
66
+ useEffect(() => updateViewer(viewer.current!, { controls }), [controls]);
67
+ useEffect(() => updateViewer(viewer.current!, { gizmo }), [gizmo]);
68
+ useEffect(() => updateViewer(viewer.current!, { lights, environmentIntensity, studio }), [lights, environmentIntensity, studio]);
69
+ useEffect(() => updateViewer(viewer.current!, { quality }), [quality]);
70
+ useEffect(() => updateViewer(viewer.current!, { showDimensions }), [showDimensions]);
71
+ useEffect(() => updateViewer(viewer.current!, { groundShadow, shadows }), [groundShadow, shadows]);
72
+ useEffect(
73
+ () =>
74
+ updateViewer(viewer.current!, {
75
+ popovers: popovers?.map((p) => ({ ...p, content: p.id })),
76
+ getTooltips: getTooltips && ((id) => getTooltips(id).map((t) => ({ ...t, content: id }))),
77
+ }),
78
+ [popovers, getTooltips]
79
+ );
80
+ useEffect(
81
+ () => updateViewer(viewer.current!, { onHoverGroup, onContextMenuGroup, onClickRelation, onPointerMissed }),
82
+ [onHoverGroup, onContextMenuGroup, onClickRelation, onPointerMissed]
83
+ );
84
+
85
+ return (
86
+ <div ref={container} className={className} style={{ position: "relative", width: "100%", height: "100%", overflow: "hidden", ...style }}>
87
+ {overlays.map((o) =>
88
+ createPortal(
89
+ o.key.startsWith("popover:")
90
+ ? popovers?.find((p) => p.id === o.content)?.content
91
+ : getTooltips?.(o.content).find((t) => `tooltip:${t.id}` === o.key)?.content,
92
+ o.element,
93
+ o.key
94
+ )
95
+ )}
96
+ {(!scene || !loaded) && sceneFallback !== undefined && (
97
+ <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none" }}>
98
+ {sceneFallback}
99
+ </div>
100
+ )}
101
+ </div>
102
+ );
103
+ }
157
104
 
158
- <ReactScene
159
- scene={scene}
160
- selectedIds={selectedIds}
161
- activeRelations={activeRelations}
162
- activeComponents={activeComponents}
163
- showDimensions={showDimensions}
164
- hoveredIdsExternal={hoveredIdsExternal}
165
- reactPopovers={reactPopovers}
166
- materialStateImages={materialStateImages}
167
- onClickGroup={onClickGroup}
168
- onHoverGroup={onHoverGroup}
169
- onContextMenuGroup={onContextMenuGroup}
170
- getTooltips={getTooltips}
171
- onClickRelation={onClickRelation}
172
- createGroupKey={createGroupKey}
173
- createGroupId={createGroupId}
174
- useAlphaTest={useAlphaTest}
175
- />
176
- {!useOldMode && (
177
- <EffectComposer multisampling={8}>
178
- <N8AO aoRadius={40} denoiseSamples={4} distanceFalloff={0.2} intensity={2} screenSpaceRadius halfRes />
179
- </EffectComposer>
180
- )}
181
- </React.Suspense>
182
- </Canvas>
183
- ) : (
184
- (sceneFallback ?? <></>)
185
- );
105
+ /** The previous value while the new one is equal by value */
106
+ function useStableValue<T>(value: T): T {
107
+ const [stable, setStable] = useState(value);
108
+ const changed = JSON.stringify(value) !== JSON.stringify(stable);
109
+ if (changed) {
110
+ setStable(value);
186
111
  }
187
- );
112
+ return changed ? value : stable;
113
+ }
@@ -0,0 +1,153 @@
1
+ import { Color, Vector4 } from "three";
2
+ import { assert, describe, expect, it } from "vitest";
3
+ import type { Material, Mesh } from "../../../abstract-3d.js";
4
+ import { box, line, text, vec3, vec3Zero } from "../../../abstract-3d.js";
5
+ import { shade } from "../../../utils.js";
6
+ import type { Item, Style, Target } from "../compile/compile.js";
7
+ import type { MaterialClass, PaintState } from "./appearance.js";
8
+ import { getHighlight, getItemColor, getMaterialClass, getTargetStates, HOVERED, SELECTED } from "./appearance.js";
9
+
10
+ const red = "rgb(255, 0, 0)";
11
+ const select = "rgb(14,82,184)";
12
+ const item = (mesh: Mesh, style: Style = "group", role: Item["role"] = "solid"): Item => ({
13
+ mesh,
14
+ role,
15
+ style,
16
+ matrix: undefined!,
17
+ target: 0,
18
+ data: undefined,
19
+ animation: undefined,
20
+ });
21
+ const solid = (material: Material, style: Style = "group"): Item => item(box(vec3(1, 1, 1), material), style);
22
+ const group: Target = { kind: "group", id: "fan", root: 0, rootData: undefined };
23
+ const relation: Target = { kind: "relation", relation: { id: "r", pos: vec3Zero } };
24
+ const idle: PaintState = { relationsActive: false, dimensionsClickable: true };
25
+ const round = (v: number): number => Math.round(v * 1e4) / 1e4;
26
+ const rgba = (color: string, alpha = 1): ReadonlyArray<number> => [...new Color(color).toArray(), alpha].map(round);
27
+ const classify = (i: Item, target: Target, state: Partial<PaintState> = {}): MaterialClass => {
28
+ const [targetState] = getTargetStates([target], { ...idle, ...state });
29
+ assert(targetState);
30
+ return getMaterialClass(i, targetState);
31
+ };
32
+ const paint = (i: Item, target: Target, partial: Partial<PaintState> = {}): ReadonlyArray<number> => {
33
+ const state = { ...idle, ...partial };
34
+ const [targetState] = getTargetStates([target], state);
35
+ assert(targetState);
36
+ return getItemColor(i, targetState, getHighlight(target, 0, state), new Vector4())
37
+ .toArray()
38
+ .map(round);
39
+ };
40
+
41
+ describe("getMaterialClass", () => {
42
+ it("lights solids, with the material's metalness, roughness and side", () => {
43
+ expect(classify(solid({ normal: red, metalness: 0.3, roughness: 0.7 }), group)).toEqual({
44
+ lit: true,
45
+ metalness: 0.3,
46
+ roughness: 0.7,
47
+ side: "double",
48
+ transparent: false,
49
+ castShadow: true,
50
+ });
51
+ expect(classify(solid({ normal: red, backfaceCulling: true }), group)).toMatchObject({ side: "front", metalness: 0, roughness: 1 });
52
+ });
53
+
54
+ it("casts shadows from opaque solids unless their material turns it off, e.g. for symbols", () => {
55
+ expect(classify(solid({ normal: red, castShadow: false }), group).castShadow).toBe(false);
56
+ expect(classify(solid({ normal: red, opacity: 0.5 }), group).castShadow).toBe(false);
57
+ expect(classify(solid({ normal: red }), group, { relationsActive: true }).castShadow).toBe(false);
58
+ expect(classify(solid({ normal: red }, "relation"), relation, { relationsActive: true }).castShadow).toBe(false);
59
+ });
60
+
61
+ it("gives solids of a type its finish, in one class whatever their metalness and roughness, unless a material state replaces it", () => {
62
+ const painted = classify(solid({ normal: red, type: "painted", metalness: 0.45, roughness: 0.4 }), group);
63
+ expect(painted).toMatchObject({ lit: true, type: "painted" });
64
+ expect(classify(solid({ normal: "rgb(0,0,255)", type: "painted", metalness: 0.9 }), group)).toEqual(painted);
65
+ expect(classify(solid({ normal: red, type: "painted" }), group, { activeComponents: { fan: "Accept" } }).type).toBeUndefined();
66
+ });
67
+
68
+ it("ignores a type that isn't a material type, e.g. a number from a database", () => {
69
+ const fromDatabase = { normal: red, metalness: 0.3, roughness: 0.7, type: 2 } as unknown as Material;
70
+ expect(classify(solid(fromDatabase), group)).toMatchObject({ metalness: 0.3, roughness: 0.7 });
71
+ expect(classify(solid(fromDatabase), group).type).toBeUndefined();
72
+ });
73
+
74
+ it("makes glass see-through unless its material has an opacity", () => {
75
+ expect(classify(solid({ normal: red, type: "glass" }), group).transparent).toBe(true);
76
+ expect(paint(solid({ normal: red, type: "glass" }), group)).toEqual(rgba(red, 0.25));
77
+ expect(classify(solid({ normal: red, type: "glass", opacity: 1 }), group).transparent).toBe(false);
78
+ });
79
+
80
+ it("makes translucent materials and disabled groups transparent", () => {
81
+ expect(classify(solid({ normal: red, opacity: 0.5 }), group).transparent).toBe(true);
82
+ expect(classify(solid({ normal: red }), group, { relationsActive: true }).transparent).toBe(true);
83
+ expect(classify(solid({ normal: red }), group, { relationsActive: true, activeComponents: { fan: "Accept" } }).transparent).toBe(false);
84
+ });
85
+
86
+ it("uses the material state's metalness and roughness", () => {
87
+ expect(classify(solid({ normal: red }), group, { activeComponents: { fan: "Error" } })).toMatchObject({ metalness: 0.5, roughness: 0.5 });
88
+ });
89
+
90
+ it("draws relations unlit on both sides, and their outlines on the back", () => {
91
+ expect(classify(solid({ normal: red }, "relation"), relation)).toMatchObject({ lit: false, side: "double", transparent: false });
92
+ expect(classify(solid({ normal: red }, "outline"), relation)).toMatchObject({ lit: false, side: "back" });
93
+ });
94
+ });
95
+
96
+ describe("getHighlight", () => {
97
+ it("combines hover by the pointer or by id with selection", () => {
98
+ expect(getHighlight(group, 0, idle)).toBe(0);
99
+ expect(getHighlight(group, 0, { ...idle, hoveredTarget: 1 })).toBe(0);
100
+ expect(getHighlight(group, 0, { ...idle, hoveredTarget: 0, selectedIds: { fan: true } })).toBe(HOVERED + SELECTED);
101
+ expect(getHighlight(group, 0, { ...idle, hoveredIds: new Set(["fan"]) })).toBe(HOVERED);
102
+ });
103
+ });
104
+
105
+ describe("getItemColor", () => {
106
+ it("is the material's color and opacity", () => {
107
+ expect(paint(solid({ normal: red, opacity: 0.5 }), group)).toEqual(rgba(red, 0.5));
108
+ });
109
+
110
+ it("darkens hovered groups, by the pointer or by id, and colors selected ones", () => {
111
+ expect(paint(solid({ normal: red }), group, { hoveredTarget: 0 })).toEqual(rgba(shade(-0.4, red)!));
112
+ expect(paint(solid({ normal: red }), group, { hoveredIds: new Set(["fan"]) })).toEqual(rgba(shade(-0.4, red)!));
113
+ expect(paint(solid({ normal: red }), group, { selectedIds: { fan: true } })).toEqual(rgba(select));
114
+ expect(paint(solid({ normal: red }), group, { selectedIds: { fan: true }, hoveredTarget: 0 })).toEqual(rgba(shade(-0.4, select)!));
115
+ });
116
+
117
+ it("ignores hover by id for groups without an id", () => {
118
+ expect(paint(solid({ normal: red }), { ...group, id: "" }, { hoveredIds: new Set([""]) })).toEqual(rgba(red));
119
+ });
120
+
121
+ it("colors groups by their material state and fades disabled ones", () => {
122
+ expect(paint(solid({ normal: red }), group, { activeComponents: { fan: "Accept" } })).toEqual(rgba("rgb(0,148,91)"));
123
+ expect(paint(solid({ normal: red, opacity: 0.8 }), group, { relationsActive: true })).toEqual(rgba(red, 0.1));
124
+ });
125
+
126
+ it("selects text in a dark blue", () => {
127
+ expect(paint(item(text(vec3Zero, "A", 1, { normal: red }), "group", "text"), group, { selectedIds: { fan: true } })).toEqual(
128
+ rgba("rgb(0, 26, 65)")
129
+ );
130
+ });
131
+
132
+ it("outlines relations in black, also while hovered", () => {
133
+ expect(paint(solid({ normal: "rgb(0,148,91)" }, "outline"), relation)).toEqual(rgba("rgb(0, 0, 0)"));
134
+ expect(paint(solid({ normal: "rgb(0,148,91)" }, "outline"), relation, { hoveredTarget: 0 })).toEqual(rgba("rgb(0, 0, 0)"));
135
+ });
136
+
137
+ it("keeps lines in their material's color", () => {
138
+ expect(paint(item(line(vec3Zero, vec3(1, 0, 0), 1, { normal: red }), "group", "line"), group, { selectedIds: { fan: true } })).toEqual(rgba(red));
139
+ });
140
+
141
+ it("highlights clickable dimensions when hovered or selected", () => {
142
+ const dimension: Target = { kind: "dimension", id: "fan" };
143
+ const d = solid({ normal: red }, "dimension");
144
+ expect(paint(d, dimension, { hoveredTarget: 0 })).toEqual(rgba(select));
145
+ expect(paint(d, dimension, { selectedIds: { fan: true } })).toEqual(rgba(select));
146
+ expect(paint(d, dimension, { selectedIds: { fan: true }, dimensionsClickable: false })).toEqual(rgba(red));
147
+ expect(paint(d, dimension, { selectedIds: { fan: true }, relationsActive: true })).toEqual(rgba(red));
148
+ });
149
+
150
+ it("darkens hovered relations", () => {
151
+ expect(paint(solid({ normal: red }, "relation"), relation, { hoveredTarget: 0 })).toEqual(rgba(shade(-0.4, red)!));
152
+ });
153
+ });
@@ -0,0 +1,150 @@
1
+ import { Color, type Vector4 } from "three";
2
+ import type { Material, MaterialType } from "../../../abstract-3d.js";
3
+ import { materialTypes } from "../../../abstract-3d.js";
4
+ import { shade } from "../../../utils.js";
5
+ import type { Item, Target } from "../compile/compile.js";
6
+
7
+ /** Material classes and per-instance colors of items, by the viewer's state, as the r3f renderer's materials */
8
+
9
+ export type MaterialState = "Accept" | "Error" | "Warning";
10
+ export const ERROR_IMG_KEY = "error";
11
+
12
+ /** The state that decides material classes, so changing it rebuilds the scene */
13
+ export type StructureState = {
14
+ readonly activeComponents?: Record<string, MaterialState>;
15
+ /** Relations are active, e.g. to pick one to insert a component on, so only the groups whose material state is Accept can be clicked */
16
+ readonly relationsActive: boolean;
17
+ };
18
+
19
+ /** A target's material state, and whether it's disabled: faded and not clickable while relations are active */
20
+ export type TargetState = { readonly materialState?: MaterialState; readonly disabled: boolean };
21
+
22
+ /** The state that only changes colors */
23
+ export type PaintState = StructureState & {
24
+ readonly selectedIds?: Record<string, boolean>;
25
+ readonly hoveredIds?: ReadonlySet<string>;
26
+ /** The target under the pointer */
27
+ readonly hoveredTarget?: number;
28
+ readonly dimensionsClickable: boolean;
29
+ };
30
+
31
+ /** Items of one material class share a material, and are drawn in one batch */
32
+ export type MaterialClass = {
33
+ readonly lit: boolean;
34
+ /** A physical finish of its own, instead of metalness and roughness */
35
+ readonly type?: MaterialType;
36
+ readonly metalness: number;
37
+ readonly roughness: number;
38
+ readonly side: "front" | "back" | "double";
39
+ readonly transparent: boolean;
40
+ /** Opaque solids whose material doesn't turn it off cast shadows */
41
+ readonly castShadow: boolean;
42
+ };
43
+
44
+ /** Bit flags of how a target is highlighted, so only targets whose highlight changed need a repaint */
45
+ export const HOVERED = 1;
46
+ export const SELECTED = 2;
47
+
48
+ const disabledOpacity = 0.125;
49
+ const selectColor = "rgb(14,82,184)";
50
+ const textSelectColor = "rgb(0, 26, 65)";
51
+ /** Of the inverted hull around a relation's boxes */
52
+ const outlineColor = "rgb(0, 0, 0)";
53
+ const stateMaterials: Record<MaterialState, Material> = {
54
+ Accept: { normal: "rgb(0,148,91)", metalness: 0.5, roughness: 0.5 },
55
+ Error: { normal: "#b82f3a", metalness: 0.5, roughness: 0.5 },
56
+ Warning: { normal: "rgb(240, 197, 48)", metalness: 0.5, roughness: 0.5 },
57
+ };
58
+ const unlit = { lit: false, metalness: 0, roughness: 1, transparent: false, castShadow: false } as const;
59
+ /** Of glass without an opacity of its own */
60
+ const glassOpacity = 0.25;
61
+
62
+ export function getTargetStates(targets: ReadonlyArray<Target>, structure: StructureState): ReadonlyArray<TargetState> {
63
+ return targets.map((target) => {
64
+ const materialState = target.kind === "group" ? structure.activeComponents?.[target.id] : undefined;
65
+ return { materialState, disabled: target.kind === "group" && structure.relationsActive && materialState !== "Accept" };
66
+ });
67
+ }
68
+
69
+ export function getMaterialClass(item: Item, { materialState, disabled }: TargetState): MaterialClass {
70
+ const { material } = item.mesh;
71
+ switch (item.style) {
72
+ case "relation":
73
+ return { ...unlit, side: "double" };
74
+ case "outline":
75
+ return { ...unlit, side: "back" };
76
+ case "dimension":
77
+ return { ...unlit, side: "front", transparent: (material.opacity ?? 1) < 1 };
78
+ default: {
79
+ // A material state's color and finish replace the material's
80
+ const mat = materialState ? stateMaterials[materialState] : material;
81
+ // Only a known type, as data may have a type of its own, e.g. a number
82
+ const type = item.role === "solid" && materialTypes.includes(mat.type as MaterialType) ? mat.type : undefined;
83
+ return {
84
+ lit: item.role === "solid",
85
+ ...(type ? { type, metalness: 0, roughness: 0 } : { metalness: mat.metalness ?? 0, roughness: mat.roughness ?? 1 }),
86
+ side: item.role === "text" || material.backfaceCulling ? "front" : "double",
87
+ transparent: getOpacity(material) < 1 || disabled,
88
+ castShadow: item.role === "solid" && getOpacity(material) === 1 && !disabled && material.castShadow !== false,
89
+ };
90
+ }
91
+ }
92
+ }
93
+
94
+ export function getHighlight(target: Target, targetIndex: number, state: PaintState): number {
95
+ const hoveredByPointer = state.hoveredTarget === targetIndex;
96
+ switch (target.kind) {
97
+ case "relation":
98
+ return hoveredByPointer ? HOVERED : 0;
99
+ case "dimension":
100
+ return state.dimensionsClickable && !state.relationsActive && target.id !== undefined && (hoveredByPointer || !!state.selectedIds?.[target.id])
101
+ ? SELECTED
102
+ : 0;
103
+ default:
104
+ return (
105
+ (hoveredByPointer || (target.id !== "" && !!state.hoveredIds?.has(target.id)) ? HOVERED : 0) + (state.selectedIds?.[target.id] ? SELECTED : 0)
106
+ );
107
+ }
108
+ }
109
+
110
+ /** The item's linear color and opacity by its target's highlight, written to `out`; hovered colors are darkened */
111
+ export function getItemColor(item: Item, { materialState, disabled }: TargetState, highlight: number, out: Vector4): Vector4 {
112
+ const { material } = item.mesh;
113
+ const hovered = highlight % SELECTED === HOVERED;
114
+ const selected = highlight >= SELECTED;
115
+ const opacity = getOpacity(material);
116
+ switch (item.style) {
117
+ case "relation":
118
+ return setColor(out, material.normal, hovered, 1);
119
+ case "outline":
120
+ return setColor(out, outlineColor, false, 1);
121
+ case "dimension":
122
+ return setColor(out, selected ? selectColor : material.normal, false, opacity);
123
+ default: {
124
+ const style = selected
125
+ ? item.role === "text"
126
+ ? textSelectColor
127
+ : selectColor
128
+ : materialState && item.role !== "image"
129
+ ? stateMaterials[materialState].normal
130
+ : material.normal;
131
+ return item.role === "line"
132
+ ? setColor(out, material.normal, false, 1)
133
+ : setColor(out, style, hovered, item.role === "solid" && disabled ? opacity * disabledOpacity : opacity);
134
+ }
135
+ }
136
+ }
137
+
138
+ /** Glass is mostly see-through unless its material says otherwise */
139
+ function getOpacity(material: Material): number {
140
+ return material.opacity ?? (material.type === "glass" ? glassOpacity : 1);
141
+ }
142
+
143
+ const colors = new Map<string, Color>();
144
+
145
+ function setColor(out: Vector4, style: string, hovered: boolean, opacity: number): Vector4 {
146
+ const key = hovered ? `hovered ${style}` : style;
147
+ const color = colors.get(key) ?? new Color(hovered ? (shade(-0.4, style) ?? style) : style);
148
+ colors.set(key, color);
149
+ return out.set(color.r, color.g, color.b, opacity);
150
+ }