abstract-3d 3.1.1 → 4.0.0-beta.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 (229) hide show
  1. package/README.md +21 -1
  2. package/lib/renderers/dxf/dxf-encoding/dxf-build.d.ts.map +1 -1
  3. package/lib/renderers/dxf/dxf-encoding/dxf-build.js +1 -2
  4. package/lib/renderers/dxf/dxf-encoding/dxf-build.js.map +1 -1
  5. package/lib/renderers/ifc/ifc.test.d.ts +2 -0
  6. package/lib/renderers/ifc/ifc.test.d.ts.map +1 -0
  7. package/lib/renderers/ifc/ifc.test.js +380 -0
  8. package/lib/renderers/ifc/ifc.test.js.map +1 -0
  9. package/lib/renderers/index.d.ts +1 -0
  10. package/lib/renderers/index.d.ts.map +1 -1
  11. package/lib/renderers/index.js +1 -0
  12. package/lib/renderers/index.js.map +1 -1
  13. package/lib/renderers/react/index.d.ts +2 -5
  14. package/lib/renderers/react/index.d.ts.map +1 -1
  15. package/lib/renderers/react/index.js +1 -2
  16. package/lib/renderers/react/index.js.map +1 -1
  17. package/lib/renderers/react/react.d.ts +16 -42
  18. package/lib/renderers/react/react.d.ts.map +1 -1
  19. package/lib/renderers/react/react.js +49 -32
  20. package/lib/renderers/react/react.js.map +1 -1
  21. package/lib/renderers/three/appearance/appearance.d.ts +41 -0
  22. package/lib/renderers/three/appearance/appearance.d.ts.map +1 -0
  23. package/lib/renderers/three/appearance/appearance.js +89 -0
  24. package/lib/renderers/three/appearance/appearance.js.map +1 -0
  25. package/lib/renderers/three/appearance/appearance.test.d.ts +2 -0
  26. package/lib/renderers/three/appearance/appearance.test.d.ts.map +1 -0
  27. package/lib/renderers/three/appearance/appearance.test.js +97 -0
  28. package/lib/renderers/three/appearance/appearance.test.js.map +1 -0
  29. package/lib/renderers/three/appearance.d.ts +41 -0
  30. package/lib/renderers/three/appearance.d.ts.map +1 -0
  31. package/lib/renderers/three/appearance.js +100 -0
  32. package/lib/renderers/three/appearance.js.map +1 -0
  33. package/lib/renderers/three/build/build.d.ts +51 -0
  34. package/lib/renderers/three/build/build.d.ts.map +1 -0
  35. package/lib/renderers/three/build/build.js +174 -0
  36. package/lib/renderers/three/build/build.js.map +1 -0
  37. package/lib/renderers/three/build/build.test.d.ts +2 -0
  38. package/lib/renderers/three/build/build.test.d.ts.map +1 -0
  39. package/lib/renderers/three/build/build.test.js +120 -0
  40. package/lib/renderers/three/build/build.test.js.map +1 -0
  41. package/lib/renderers/three/build/materials.d.ts +12 -0
  42. package/lib/renderers/three/build/materials.d.ts.map +1 -0
  43. package/lib/renderers/three/build/materials.js +55 -0
  44. package/lib/renderers/three/build/materials.js.map +1 -0
  45. package/lib/renderers/three/build/parts.d.ts +65 -0
  46. package/lib/renderers/three/build/parts.d.ts.map +1 -0
  47. package/lib/renderers/three/build/parts.js +203 -0
  48. package/lib/renderers/three/build/parts.js.map +1 -0
  49. package/lib/renderers/three/build.d.ts +50 -0
  50. package/lib/renderers/three/build.d.ts.map +1 -0
  51. package/lib/renderers/three/build.js +210 -0
  52. package/lib/renderers/three/build.js.map +1 -0
  53. package/lib/renderers/three/camera/camera.d.ts +40 -0
  54. package/lib/renderers/three/camera/camera.d.ts.map +1 -0
  55. package/lib/renderers/three/camera/camera.js +51 -0
  56. package/lib/renderers/three/camera/camera.js.map +1 -0
  57. package/lib/renderers/three/camera/camera.test.d.ts +2 -0
  58. package/lib/renderers/three/camera/camera.test.d.ts.map +1 -0
  59. package/lib/renderers/three/camera/camera.test.js +46 -0
  60. package/lib/renderers/three/camera/camera.test.js.map +1 -0
  61. package/lib/renderers/three/camera/controls.d.ts +24 -0
  62. package/lib/renderers/three/camera/controls.d.ts.map +1 -0
  63. package/lib/renderers/three/camera/controls.js +39 -0
  64. package/lib/renderers/three/camera/controls.js.map +1 -0
  65. package/lib/renderers/three/camera/gizmo.d.ts +44 -0
  66. package/lib/renderers/three/camera/gizmo.d.ts.map +1 -0
  67. package/lib/renderers/three/camera/gizmo.js +42 -0
  68. package/lib/renderers/three/camera/gizmo.js.map +1 -0
  69. package/lib/renderers/three/camera.d.ts +63 -0
  70. package/lib/renderers/three/camera.d.ts.map +1 -0
  71. package/lib/renderers/three/camera.js +85 -0
  72. package/lib/renderers/three/camera.js.map +1 -0
  73. package/lib/renderers/three/compile/compile.d.ts +59 -0
  74. package/lib/renderers/three/compile/compile.d.ts.map +1 -0
  75. package/lib/renderers/three/compile/compile.js +88 -0
  76. package/lib/renderers/three/compile/compile.js.map +1 -0
  77. package/lib/renderers/three/compile/compile.test.d.ts +2 -0
  78. package/lib/renderers/three/compile/compile.test.d.ts.map +1 -0
  79. package/lib/renderers/three/compile/compile.test.js +93 -0
  80. package/lib/renderers/three/compile/compile.test.js.map +1 -0
  81. package/lib/renderers/three/compile.d.ts +58 -0
  82. package/lib/renderers/three/compile.d.ts.map +1 -0
  83. package/lib/renderers/three/compile.js +89 -0
  84. package/lib/renderers/three/compile.js.map +1 -0
  85. package/lib/renderers/three/controls.d.ts +27 -0
  86. package/lib/renderers/three/controls.d.ts.map +1 -0
  87. package/lib/renderers/three/controls.js +115 -0
  88. package/lib/renderers/three/controls.js.map +1 -0
  89. package/lib/renderers/three/geometry/geometry.d.ts +21 -0
  90. package/lib/renderers/three/geometry/geometry.d.ts.map +1 -0
  91. package/lib/renderers/three/geometry/geometry.js +132 -0
  92. package/lib/renderers/three/geometry/geometry.js.map +1 -0
  93. package/lib/renderers/three/geometry/geometry.test.d.ts +2 -0
  94. package/lib/renderers/three/geometry/geometry.test.d.ts.map +1 -0
  95. package/lib/renderers/three/geometry/geometry.test.js +80 -0
  96. package/lib/renderers/three/geometry/geometry.test.js.map +1 -0
  97. package/lib/renderers/three/geometry.d.ts +20 -0
  98. package/lib/renderers/three/geometry.d.ts.map +1 -0
  99. package/lib/renderers/three/geometry.js +164 -0
  100. package/lib/renderers/three/geometry.js.map +1 -0
  101. package/lib/renderers/three/gizmo.d.ts +49 -0
  102. package/lib/renderers/three/gizmo.d.ts.map +1 -0
  103. package/lib/renderers/three/gizmo.js +172 -0
  104. package/lib/renderers/three/gizmo.js.map +1 -0
  105. package/lib/renderers/three/index.d.ts +16 -0
  106. package/lib/renderers/three/index.d.ts.map +1 -0
  107. package/lib/renderers/three/index.js +6 -0
  108. package/lib/renderers/three/index.js.map +1 -0
  109. package/lib/renderers/three/input.d.ts +25 -0
  110. package/lib/renderers/three/input.d.ts.map +1 -0
  111. package/lib/renderers/three/input.js +71 -0
  112. package/lib/renderers/three/input.js.map +1 -0
  113. package/lib/renderers/three/lines.d.ts +16 -0
  114. package/lib/renderers/three/lines.d.ts.map +1 -0
  115. package/lib/renderers/three/lines.js +63 -0
  116. package/lib/renderers/three/lines.js.map +1 -0
  117. package/lib/renderers/three/materials.d.ts +15 -0
  118. package/lib/renderers/three/materials.d.ts.map +1 -0
  119. package/lib/renderers/three/materials.js +70 -0
  120. package/lib/renderers/three/materials.js.map +1 -0
  121. package/lib/renderers/three/options.d.ts +68 -0
  122. package/lib/renderers/three/options.d.ts.map +1 -0
  123. package/lib/renderers/three/options.js +64 -0
  124. package/lib/renderers/three/options.js.map +1 -0
  125. package/lib/renderers/three/overlays.d.ts +25 -0
  126. package/lib/renderers/three/overlays.d.ts.map +1 -0
  127. package/lib/renderers/three/overlays.js +57 -0
  128. package/lib/renderers/three/overlays.js.map +1 -0
  129. package/lib/renderers/three/quality/quality.d.ts +37 -0
  130. package/lib/renderers/three/quality/quality.d.ts.map +1 -0
  131. package/lib/renderers/three/quality/quality.js +64 -0
  132. package/lib/renderers/three/quality/quality.js.map +1 -0
  133. package/lib/renderers/three/quality/quality.test.d.ts +2 -0
  134. package/lib/renderers/three/quality/quality.test.d.ts.map +1 -0
  135. package/lib/renderers/three/quality/quality.test.js +23 -0
  136. package/lib/renderers/three/quality/quality.test.js.map +1 -0
  137. package/lib/renderers/three/quality.d.ts +24 -0
  138. package/lib/renderers/three/quality.d.ts.map +1 -0
  139. package/lib/renderers/three/quality.js +67 -0
  140. package/lib/renderers/three/quality.js.map +1 -0
  141. package/lib/renderers/three/text/text.d.ts +55 -0
  142. package/lib/renderers/three/text/text.d.ts.map +1 -0
  143. package/lib/renderers/three/text/text.js +90 -0
  144. package/lib/renderers/three/text/text.js.map +1 -0
  145. package/lib/renderers/three/text/text.test.d.ts +2 -0
  146. package/lib/renderers/three/text/text.test.d.ts.map +1 -0
  147. package/lib/renderers/three/text/text.test.js +70 -0
  148. package/lib/renderers/three/text/text.test.js.map +1 -0
  149. package/lib/renderers/three/text.d.ts +53 -0
  150. package/lib/renderers/three/text.d.ts.map +1 -0
  151. package/lib/renderers/three/text.js +151 -0
  152. package/lib/renderers/three/text.js.map +1 -0
  153. package/lib/renderers/three/viewer/options.d.ts +71 -0
  154. package/lib/renderers/three/viewer/options.d.ts.map +1 -0
  155. package/lib/renderers/three/viewer/options.js +31 -0
  156. package/lib/renderers/three/viewer/options.js.map +1 -0
  157. package/lib/renderers/three/viewer/options.test.d.ts +2 -0
  158. package/lib/renderers/three/viewer/options.test.d.ts.map +1 -0
  159. package/lib/renderers/three/viewer/options.test.js +38 -0
  160. package/lib/renderers/three/viewer/options.test.js.map +1 -0
  161. package/lib/renderers/three/viewer/overlays.d.ts +31 -0
  162. package/lib/renderers/three/viewer/overlays.d.ts.map +1 -0
  163. package/lib/renderers/three/viewer/overlays.js +43 -0
  164. package/lib/renderers/three/viewer/overlays.js.map +1 -0
  165. package/lib/renderers/three/viewer/viewer.d.ts +90 -0
  166. package/lib/renderers/three/viewer/viewer.d.ts.map +1 -0
  167. package/lib/renderers/three/viewer/viewer.js +432 -0
  168. package/lib/renderers/three/viewer/viewer.js.map +1 -0
  169. package/lib/renderers/three/viewer.d.ts +38 -0
  170. package/lib/renderers/three/viewer.d.ts.map +1 -0
  171. package/lib/renderers/three/viewer.js +435 -0
  172. package/lib/renderers/three/viewer.js.map +1 -0
  173. package/package.json +26 -9
  174. package/src/abstract-3d.ts +1 -1
  175. package/src/custom_typings/n8ao.d.ts +11 -0
  176. package/src/renderers/dxf/dxf-encoding/dxf-build.ts +1 -2
  177. package/src/renderers/index.ts +1 -0
  178. package/src/renderers/react/index.ts +2 -5
  179. package/src/renderers/react/react.tsx +96 -181
  180. package/src/renderers/three/appearance/appearance.test.ts +116 -0
  181. package/src/renderers/three/appearance/appearance.ts +132 -0
  182. package/src/renderers/three/build/build.test.ts +141 -0
  183. package/src/renderers/three/build/build.ts +270 -0
  184. package/src/renderers/three/build/materials.ts +85 -0
  185. package/src/renderers/three/build/parts.ts +277 -0
  186. package/src/renderers/three/camera/camera.test.ts +53 -0
  187. package/src/renderers/three/camera/camera.ts +82 -0
  188. package/src/renderers/three/camera/controls.ts +65 -0
  189. package/src/renderers/three/camera/gizmo.ts +93 -0
  190. package/src/renderers/three/compile/compile.test.ts +113 -0
  191. package/src/renderers/three/compile/compile.ts +151 -0
  192. package/src/renderers/three/geometry/geometry.test.ts +95 -0
  193. package/src/renderers/three/geometry/geometry.ts +179 -0
  194. package/src/renderers/three/index.ts +15 -0
  195. package/src/renderers/three/quality/quality.test.ts +30 -0
  196. package/src/renderers/three/quality/quality.ts +95 -0
  197. package/src/renderers/three/text/text.test.ts +79 -0
  198. package/src/renderers/three/text/text.ts +128 -0
  199. package/src/renderers/three/viewer/options.ts +98 -0
  200. package/src/renderers/three/viewer/overlays.ts +71 -0
  201. package/src/renderers/three/viewer/viewer.ts +540 -0
  202. package/lib/renderers/dxf/color.d.ts +0 -7
  203. package/lib/renderers/dxf/color.d.ts.map +0 -1
  204. package/lib/renderers/dxf/color.js +0 -311
  205. package/lib/renderers/dxf/color.js.map +0 -1
  206. package/lib/renderers/dxf/dxf-encoding.d.ts +0 -17
  207. package/lib/renderers/dxf/dxf-encoding.d.ts.map +0 -1
  208. package/lib/renderers/dxf/dxf-encoding.js +0 -18467
  209. package/lib/renderers/dxf/dxf-encoding.js.map +0 -1
  210. package/lib/renderers/react/react-hotspot.d.ts +0 -27
  211. package/lib/renderers/react/react-hotspot.d.ts.map +0 -1
  212. package/lib/renderers/react/react-hotspot.js +0 -37
  213. package/lib/renderers/react/react-hotspot.js.map +0 -1
  214. package/lib/renderers/shared.d.ts +0 -30
  215. package/lib/renderers/shared.d.ts.map +0 -1
  216. package/lib/renderers/shared.js +0 -164
  217. package/lib/renderers/shared.js.map +0 -1
  218. package/src/custom_typings/react-three-fiber.d.ts +0 -9
  219. package/src/custom_typings/react-three-postprocessing.d.ts +0 -1
  220. package/src/renderers/react/react-camera.tsx +0 -391
  221. package/src/renderers/react/react-dimension.tsx +0 -122
  222. package/src/renderers/react/react-group.test.ts +0 -56
  223. package/src/renderers/react/react-group.tsx +0 -202
  224. package/src/renderers/react/react-image-material.tsx +0 -139
  225. package/src/renderers/react/react-material.tsx +0 -91
  226. package/src/renderers/react/react-mesh.tsx +0 -512
  227. package/src/renderers/react/react-relation.tsx +0 -86
  228. package/src/renderers/react/react-scene.tsx +0 -111
  229. package/src/renderers/react/react-types.ts +0 -9
@@ -1,187 +1,102 @@
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 { 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, environment, environmentIntensity, quality, showDimensions, popovers, getTooltips } = props;
29
+ const { onHoverGroup, onContextMenuGroup, onClickRelation, 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
+ useEffect(() => updateViewer(viewer.current!, { scene, createGroupId }), [scene, createGroupId]);
47
+ useEffect(
48
+ () => updateViewer(viewer.current!, { activeComponents, activeRelations, useAlphaTest, materialStateImages }),
49
+ [activeComponents, activeRelations, useAlphaTest, materialStateImages]
50
+ );
51
+ useEffect(
52
+ () => updateViewer(viewer.current!, { selectedIds, hoveredIds: hoveredIdsExternal, onClickGroup }),
53
+ [selectedIds, hoveredIdsExternal, onClickGroup]
54
+ );
55
+ useEffect(() => updateViewer(viewer.current!, { view, bufferZones, fitPadding, camera }), [view, bufferZones, fitPadding, camera]);
56
+ useEffect(() => updateViewer(viewer.current!, { controls }), [controls]);
57
+ useEffect(() => updateViewer(viewer.current!, { gizmo }), [gizmo]);
58
+ useEffect(() => updateViewer(viewer.current!, { lights, environment, environmentIntensity }), [lights, environment, environmentIntensity]);
59
+ useEffect(() => updateViewer(viewer.current!, { quality }), [quality]);
60
+ useEffect(() => updateViewer(viewer.current!, { showDimensions }), [showDimensions]);
61
+ useEffect(
62
+ () =>
63
+ updateViewer(viewer.current!, {
64
+ popovers: popovers?.map((p) => ({ ...p, content: p.id })),
65
+ getTooltips: getTooltips && ((id) => getTooltips(id).map((t) => ({ ...t, content: id }))),
66
+ }),
67
+ [popovers, getTooltips]
68
+ );
69
+ useEffect(
70
+ () => updateViewer(viewer.current!, { onHoverGroup, onContextMenuGroup, onClickRelation }),
71
+ [onHoverGroup, onContextMenuGroup, onClickRelation]
72
+ );
73
+
74
+ return (
75
+ <div ref={container} className={className} style={{ position: "relative", width: "100%", height: "100%", overflow: "hidden", ...style }}>
76
+ {overlays.map((o) =>
77
+ createPortal(
78
+ o.key.startsWith("popover:")
79
+ ? popovers?.find((p) => p.id === o.content)?.content
80
+ : getTooltips?.(o.content).find((t) => `tooltip:${t.id}` === o.key)?.content,
81
+ o.element,
82
+ o.key
83
+ )
84
+ )}
85
+ {(!scene || !loaded) && sceneFallback !== undefined && (
86
+ <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none" }}>
87
+ {sceneFallback}
88
+ </div>
89
+ )}
90
+ </div>
91
+ );
92
+ }
157
93
 
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
- );
94
+ /** The previous value while the new one is equal by value */
95
+ function useStableValue<T>(value: T): T {
96
+ const [stable, setStable] = useState(value);
97
+ const changed = JSON.stringify(value) !== JSON.stringify(stable);
98
+ if (changed) {
99
+ setStable(value);
186
100
  }
187
- );
101
+ return changed ? value : stable;
102
+ }
@@ -0,0 +1,116 @@
1
+ import { Color, Vector4 } from "three";
2
+ import { 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
+ getMaterialClass(i, getTargetStates([target], { ...idle, ...state })[0]);
29
+ const paint = (i: Item, target: Target, partial: Partial<PaintState> = {}): ReadonlyArray<number> => {
30
+ const state = { ...idle, ...partial };
31
+ return getItemColor(i, getTargetStates([target], state)[0], getHighlight(target, 0, state), new Vector4())
32
+ .toArray()
33
+ .map(round);
34
+ };
35
+
36
+ describe("getMaterialClass", () => {
37
+ it("lights solids, with the material's metalness, roughness and side", () => {
38
+ expect(classify(solid({ normal: red, metalness: 0.3, roughness: 0.7 }), group)).toEqual({
39
+ lit: true,
40
+ metalness: 0.3,
41
+ roughness: 0.7,
42
+ side: "double",
43
+ transparent: false,
44
+ });
45
+ expect(classify(solid({ normal: red, backfaceCulling: true }), group)).toMatchObject({ side: "front", metalness: 0, roughness: 1 });
46
+ });
47
+
48
+ it("makes translucent materials and disabled groups transparent", () => {
49
+ expect(classify(solid({ normal: red, opacity: 0.5 }), group).transparent).toBe(true);
50
+ expect(classify(solid({ normal: red }), group, { relationsActive: true }).transparent).toBe(true);
51
+ expect(classify(solid({ normal: red }), group, { relationsActive: true, activeComponents: { fan: "Accept" } }).transparent).toBe(false);
52
+ });
53
+
54
+ it("uses the material state's metalness and roughness", () => {
55
+ expect(classify(solid({ normal: red }), group, { activeComponents: { fan: "Error" } })).toMatchObject({ metalness: 0.5, roughness: 0.5 });
56
+ });
57
+
58
+ it("draws relations unlit on both sides, and their outlines on the back", () => {
59
+ expect(classify(solid({ normal: red }, "relation"), relation)).toMatchObject({ lit: false, side: "double", transparent: false });
60
+ expect(classify(solid({ normal: red }, "outline"), relation)).toMatchObject({ lit: false, side: "back" });
61
+ });
62
+ });
63
+
64
+ describe("getHighlight", () => {
65
+ it("combines hover by the pointer or by id with selection", () => {
66
+ expect(getHighlight(group, 0, idle)).toBe(0);
67
+ expect(getHighlight(group, 0, { ...idle, hoveredTarget: 1 })).toBe(0);
68
+ expect(getHighlight(group, 0, { ...idle, hoveredTarget: 0, selectedIds: { fan: true } })).toBe(HOVERED + SELECTED);
69
+ expect(getHighlight(group, 0, { ...idle, hoveredIds: new Set(["fan"]) })).toBe(HOVERED);
70
+ });
71
+ });
72
+
73
+ describe("getItemColor", () => {
74
+ it("is the material's color and opacity", () => {
75
+ expect(paint(solid({ normal: red, opacity: 0.5 }), group)).toEqual(rgba(red, 0.5));
76
+ });
77
+
78
+ it("darkens hovered groups, by the pointer or by id, and colors selected ones", () => {
79
+ expect(paint(solid({ normal: red }), group, { hoveredTarget: 0 })).toEqual(rgba(shade(-0.4, red)!));
80
+ expect(paint(solid({ normal: red }), group, { hoveredIds: new Set(["fan"]) })).toEqual(rgba(shade(-0.4, red)!));
81
+ expect(paint(solid({ normal: red }), group, { selectedIds: { fan: true } })).toEqual(rgba(select));
82
+ expect(paint(solid({ normal: red }), group, { selectedIds: { fan: true }, hoveredTarget: 0 })).toEqual(rgba(shade(-0.4, select)!));
83
+ });
84
+
85
+ it("ignores hover by id for groups without an id", () => {
86
+ expect(paint(solid({ normal: red }), { ...group, id: "" }, { hoveredIds: new Set([""]) })).toEqual(rgba(red));
87
+ });
88
+
89
+ it("colors groups by their material state and fades disabled ones", () => {
90
+ expect(paint(solid({ normal: red }), group, { activeComponents: { fan: "Accept" } })).toEqual(rgba("rgb(0,148,91)"));
91
+ expect(paint(solid({ normal: red, opacity: 0.8 }), group, { relationsActive: true })).toEqual(rgba(red, 0.1));
92
+ });
93
+
94
+ it("selects text in a dark blue", () => {
95
+ expect(paint(item(text(vec3Zero, "A", 1, { normal: red }), "group", "text"), group, { selectedIds: { fan: true } })).toEqual(
96
+ rgba("rgb(0, 26, 65)")
97
+ );
98
+ });
99
+
100
+ it("keeps lines in their material's color", () => {
101
+ expect(paint(item(line(vec3Zero, vec3(1, 0, 0), 1, { normal: red }), "group", "line"), group, { selectedIds: { fan: true } })).toEqual(rgba(red));
102
+ });
103
+
104
+ it("highlights clickable dimensions when hovered or selected", () => {
105
+ const dimension: Target = { kind: "dimension", id: "fan" };
106
+ const d = solid({ normal: red }, "dimension");
107
+ expect(paint(d, dimension, { hoveredTarget: 0 })).toEqual(rgba(select));
108
+ expect(paint(d, dimension, { selectedIds: { fan: true } })).toEqual(rgba(select));
109
+ expect(paint(d, dimension, { selectedIds: { fan: true }, dimensionsClickable: false })).toEqual(rgba(red));
110
+ expect(paint(d, dimension, { selectedIds: { fan: true }, relationsActive: true })).toEqual(rgba(red));
111
+ });
112
+
113
+ it("darkens hovered relations", () => {
114
+ expect(paint(solid({ normal: red }, "relation"), relation, { hoveredTarget: 0 })).toEqual(rgba(shade(-0.4, red)!));
115
+ });
116
+ });
@@ -0,0 +1,132 @@
1
+ import { Color, type Vector4 } from "three";
2
+ import type { Material } from "../../../abstract-3d.js";
3
+ import { shade } from "../../../utils.js";
4
+ import type { Item, Target } from "../compile/compile.js";
5
+
6
+ /** Material classes and per-instance colors of items, by the viewer's state, as the r3f renderer's materials */
7
+
8
+ export type MaterialState = "Accept" | "Error" | "Warning";
9
+ export const ERROR_IMG_KEY = "error";
10
+
11
+ /** The state that decides material classes, so changing it rebuilds the scene */
12
+ export type StructureState = {
13
+ readonly activeComponents?: Record<string, MaterialState>;
14
+ /** 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 */
15
+ readonly relationsActive: boolean;
16
+ };
17
+
18
+ /** A target's material state, and whether it's disabled: faded and not clickable while relations are active */
19
+ export type TargetState = { readonly materialState?: MaterialState; readonly disabled: boolean };
20
+
21
+ /** The state that only changes colors */
22
+ export type PaintState = StructureState & {
23
+ readonly selectedIds?: Record<string, boolean>;
24
+ readonly hoveredIds?: ReadonlySet<string>;
25
+ /** The target under the pointer */
26
+ readonly hoveredTarget?: number;
27
+ readonly dimensionsClickable: boolean;
28
+ };
29
+
30
+ /** Items of one material class share a material, and are drawn in one batch */
31
+ export type MaterialClass = {
32
+ readonly lit: boolean;
33
+ readonly metalness: number;
34
+ readonly roughness: number;
35
+ readonly side: "front" | "back" | "double";
36
+ readonly transparent: boolean;
37
+ };
38
+
39
+ /** Bit flags of how a target is highlighted, so only targets whose highlight changed need a repaint */
40
+ export const HOVERED = 1;
41
+ export const SELECTED = 2;
42
+
43
+ const disabledOpacity = 0.125;
44
+ const selectColor = "rgb(14,82,184)";
45
+ const textSelectColor = "rgb(0, 26, 65)";
46
+ const stateMaterials: Record<MaterialState, Material> = {
47
+ Accept: { normal: "rgb(0,148,91)", metalness: 0.5, roughness: 0.5 },
48
+ Error: { normal: "#b82f3a", metalness: 0.5, roughness: 0.5 },
49
+ Warning: { normal: "rgb(240, 197, 48)", metalness: 0.5, roughness: 0.5 },
50
+ };
51
+ const unlit = { lit: false, metalness: 0, roughness: 1, transparent: false } as const;
52
+
53
+ export function getTargetStates(targets: ReadonlyArray<Target>, structure: StructureState): ReadonlyArray<TargetState> {
54
+ return targets.map((target) => {
55
+ const materialState = target.kind === "group" ? structure.activeComponents?.[target.id] : undefined;
56
+ return { materialState, disabled: target.kind === "group" && structure.relationsActive && materialState !== "Accept" };
57
+ });
58
+ }
59
+
60
+ export function getMaterialClass(item: Item, { materialState, disabled }: TargetState): MaterialClass {
61
+ const { material } = item.mesh;
62
+ switch (item.style) {
63
+ case "relation":
64
+ return { ...unlit, side: "double" };
65
+ case "outline":
66
+ return { ...unlit, side: "back" };
67
+ case "dimension":
68
+ return { ...unlit, side: "front", transparent: (material.opacity ?? 1) < 1 };
69
+ default: {
70
+ const mat = materialState ? stateMaterials[materialState] : material;
71
+ return {
72
+ lit: item.role === "solid",
73
+ metalness: mat.metalness ?? 0,
74
+ roughness: mat.roughness ?? 1,
75
+ side: item.role === "text" || material.backfaceCulling ? "front" : "double",
76
+ transparent: (material.opacity ?? 1) < 1 || disabled,
77
+ };
78
+ }
79
+ }
80
+ }
81
+
82
+ export function getHighlight(target: Target, targetIndex: number, state: PaintState): number {
83
+ const hoveredByPointer = state.hoveredTarget === targetIndex;
84
+ switch (target.kind) {
85
+ case "relation":
86
+ return hoveredByPointer ? HOVERED : 0;
87
+ case "dimension":
88
+ return state.dimensionsClickable && !state.relationsActive && target.id !== undefined && (hoveredByPointer || !!state.selectedIds?.[target.id])
89
+ ? SELECTED
90
+ : 0;
91
+ default:
92
+ return (
93
+ (hoveredByPointer || (target.id !== "" && !!state.hoveredIds?.has(target.id)) ? HOVERED : 0) + (state.selectedIds?.[target.id] ? SELECTED : 0)
94
+ );
95
+ }
96
+ }
97
+
98
+ /** The item's linear color and opacity by its target's highlight, written to `out`; hovered colors are darkened */
99
+ export function getItemColor(item: Item, { materialState, disabled }: TargetState, highlight: number, out: Vector4): Vector4 {
100
+ const { material } = item.mesh;
101
+ const hovered = highlight % SELECTED === HOVERED;
102
+ const selected = highlight >= SELECTED;
103
+ const opacity = material.opacity ?? 1;
104
+ switch (item.style) {
105
+ case "relation":
106
+ case "outline":
107
+ return setColor(out, material.normal, hovered, 1);
108
+ case "dimension":
109
+ return setColor(out, selected ? selectColor : material.normal, false, opacity);
110
+ default: {
111
+ const style = selected
112
+ ? item.role === "text"
113
+ ? textSelectColor
114
+ : selectColor
115
+ : materialState && item.role !== "image"
116
+ ? stateMaterials[materialState].normal
117
+ : material.normal;
118
+ return item.role === "line"
119
+ ? setColor(out, material.normal, false, 1)
120
+ : setColor(out, style, hovered, item.role === "solid" && disabled ? opacity * disabledOpacity : opacity);
121
+ }
122
+ }
123
+ }
124
+
125
+ const colors = new Map<string, Color>();
126
+
127
+ function setColor(out: Vector4, style: string, hovered: boolean, opacity: number): Vector4 {
128
+ const key = hovered ? `hovered ${style}` : style;
129
+ const color = colors.get(key) ?? new Color(hovered ? (shade(-0.4, style) ?? style) : style);
130
+ colors.set(key, color);
131
+ return out.set(color.r, color.g, color.b, opacity);
132
+ }