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.
- package/README.md +53 -1
- package/lib/abstract-3d.d.ts +27 -1
- package/lib/abstract-3d.d.ts.map +1 -1
- package/lib/abstract-3d.js +26 -2
- package/lib/abstract-3d.js.map +1 -1
- package/lib/renderers/dxf/dxf-encoding/dxf-build.d.ts.map +1 -1
- package/lib/renderers/dxf/dxf-encoding/dxf-build.js +1 -2
- package/lib/renderers/dxf/dxf-encoding/dxf-build.js.map +1 -1
- package/lib/renderers/ifc/ifc.test.d.ts +2 -0
- package/lib/renderers/ifc/ifc.test.d.ts.map +1 -0
- package/lib/renderers/ifc/ifc.test.js +380 -0
- package/lib/renderers/ifc/ifc.test.js.map +1 -0
- package/lib/renderers/index.d.ts +1 -0
- package/lib/renderers/index.d.ts.map +1 -1
- package/lib/renderers/index.js +1 -0
- package/lib/renderers/index.js.map +1 -1
- package/lib/renderers/react/index.d.ts +2 -5
- package/lib/renderers/react/index.d.ts.map +1 -1
- package/lib/renderers/react/index.js +1 -2
- package/lib/renderers/react/index.js.map +1 -1
- package/lib/renderers/react/react.d.ts +16 -42
- package/lib/renderers/react/react.d.ts.map +1 -1
- package/lib/renderers/react/react.js +56 -32
- package/lib/renderers/react/react.js.map +1 -1
- package/lib/renderers/three/appearance/appearance.d.ts +46 -0
- package/lib/renderers/three/appearance/appearance.d.ts.map +1 -0
- package/lib/renderers/three/appearance/appearance.js +102 -0
- package/lib/renderers/three/appearance/appearance.js.map +1 -0
- package/lib/renderers/three/appearance/appearance.test.d.ts +2 -0
- package/lib/renderers/three/appearance/appearance.test.d.ts.map +1 -0
- package/lib/renderers/three/appearance/appearance.test.js +97 -0
- package/lib/renderers/three/appearance/appearance.test.js.map +1 -0
- package/lib/renderers/three/appearance.d.ts +41 -0
- package/lib/renderers/three/appearance.d.ts.map +1 -0
- package/lib/renderers/three/appearance.js +100 -0
- package/lib/renderers/three/appearance.js.map +1 -0
- package/lib/renderers/three/build/build.d.ts +53 -0
- package/lib/renderers/three/build/build.d.ts.map +1 -0
- package/lib/renderers/three/build/build.js +181 -0
- package/lib/renderers/three/build/build.js.map +1 -0
- package/lib/renderers/three/build/build.test.d.ts +2 -0
- package/lib/renderers/three/build/build.test.d.ts.map +1 -0
- package/lib/renderers/three/build/build.test.js +120 -0
- package/lib/renderers/three/build/build.test.js.map +1 -0
- package/lib/renderers/three/build/materials.d.ts +20 -0
- package/lib/renderers/three/build/materials.d.ts.map +1 -0
- package/lib/renderers/three/build/materials.js +168 -0
- package/lib/renderers/three/build/materials.js.map +1 -0
- package/lib/renderers/three/build/parts.d.ts +65 -0
- package/lib/renderers/three/build/parts.d.ts.map +1 -0
- package/lib/renderers/three/build/parts.js +203 -0
- package/lib/renderers/three/build/parts.js.map +1 -0
- package/lib/renderers/three/build.d.ts +50 -0
- package/lib/renderers/three/build.d.ts.map +1 -0
- package/lib/renderers/three/build.js +210 -0
- package/lib/renderers/three/build.js.map +1 -0
- package/lib/renderers/three/camera/camera.d.ts +40 -0
- package/lib/renderers/three/camera/camera.d.ts.map +1 -0
- package/lib/renderers/three/camera/camera.js +51 -0
- package/lib/renderers/three/camera/camera.js.map +1 -0
- package/lib/renderers/three/camera/camera.test.d.ts +2 -0
- package/lib/renderers/three/camera/camera.test.d.ts.map +1 -0
- package/lib/renderers/three/camera/camera.test.js +46 -0
- package/lib/renderers/three/camera/camera.test.js.map +1 -0
- package/lib/renderers/three/camera/controls.d.ts +24 -0
- package/lib/renderers/three/camera/controls.d.ts.map +1 -0
- package/lib/renderers/three/camera/controls.js +39 -0
- package/lib/renderers/three/camera/controls.js.map +1 -0
- package/lib/renderers/three/camera/gizmo.d.ts +44 -0
- package/lib/renderers/three/camera/gizmo.d.ts.map +1 -0
- package/lib/renderers/three/camera/gizmo.js +43 -0
- package/lib/renderers/three/camera/gizmo.js.map +1 -0
- package/lib/renderers/three/camera.d.ts +63 -0
- package/lib/renderers/three/camera.d.ts.map +1 -0
- package/lib/renderers/three/camera.js +85 -0
- package/lib/renderers/three/camera.js.map +1 -0
- package/lib/renderers/three/compile/compile.d.ts +59 -0
- package/lib/renderers/three/compile/compile.d.ts.map +1 -0
- package/lib/renderers/three/compile/compile.js +88 -0
- package/lib/renderers/three/compile/compile.js.map +1 -0
- package/lib/renderers/three/compile/compile.test.d.ts +2 -0
- package/lib/renderers/three/compile/compile.test.d.ts.map +1 -0
- package/lib/renderers/three/compile/compile.test.js +93 -0
- package/lib/renderers/three/compile/compile.test.js.map +1 -0
- package/lib/renderers/three/compile.d.ts +58 -0
- package/lib/renderers/three/compile.d.ts.map +1 -0
- package/lib/renderers/three/compile.js +89 -0
- package/lib/renderers/three/compile.js.map +1 -0
- package/lib/renderers/three/controls.d.ts +27 -0
- package/lib/renderers/three/controls.d.ts.map +1 -0
- package/lib/renderers/three/controls.js +115 -0
- package/lib/renderers/three/controls.js.map +1 -0
- package/lib/renderers/three/environment/studio.d.ts +9 -0
- package/lib/renderers/three/environment/studio.d.ts.map +1 -0
- package/lib/renderers/three/environment/studio.js +15 -0
- package/lib/renderers/three/environment/studio.js.map +1 -0
- package/lib/renderers/three/geometry/geometry.d.ts +21 -0
- package/lib/renderers/three/geometry/geometry.d.ts.map +1 -0
- package/lib/renderers/three/geometry/geometry.js +141 -0
- package/lib/renderers/three/geometry/geometry.js.map +1 -0
- package/lib/renderers/three/geometry/geometry.test.d.ts +2 -0
- package/lib/renderers/three/geometry/geometry.test.d.ts.map +1 -0
- package/lib/renderers/three/geometry/geometry.test.js +80 -0
- package/lib/renderers/three/geometry/geometry.test.js.map +1 -0
- package/lib/renderers/three/geometry.d.ts +20 -0
- package/lib/renderers/three/geometry.d.ts.map +1 -0
- package/lib/renderers/three/geometry.js +164 -0
- package/lib/renderers/three/geometry.js.map +1 -0
- package/lib/renderers/three/gizmo.d.ts +49 -0
- package/lib/renderers/three/gizmo.d.ts.map +1 -0
- package/lib/renderers/three/gizmo.js +172 -0
- package/lib/renderers/three/gizmo.js.map +1 -0
- package/lib/renderers/three/index.d.ts +16 -0
- package/lib/renderers/three/index.d.ts.map +1 -0
- package/lib/renderers/three/index.js +6 -0
- package/lib/renderers/three/index.js.map +1 -0
- package/lib/renderers/three/input.d.ts +25 -0
- package/lib/renderers/three/input.d.ts.map +1 -0
- package/lib/renderers/three/input.js +71 -0
- package/lib/renderers/three/input.js.map +1 -0
- package/lib/renderers/three/lines.d.ts +16 -0
- package/lib/renderers/three/lines.d.ts.map +1 -0
- package/lib/renderers/three/lines.js +63 -0
- package/lib/renderers/three/lines.js.map +1 -0
- package/lib/renderers/three/materials.d.ts +15 -0
- package/lib/renderers/three/materials.d.ts.map +1 -0
- package/lib/renderers/three/materials.js +70 -0
- package/lib/renderers/three/materials.js.map +1 -0
- package/lib/renderers/three/options.d.ts +68 -0
- package/lib/renderers/three/options.d.ts.map +1 -0
- package/lib/renderers/three/options.js +64 -0
- package/lib/renderers/three/options.js.map +1 -0
- package/lib/renderers/three/overlays.d.ts +25 -0
- package/lib/renderers/three/overlays.d.ts.map +1 -0
- package/lib/renderers/three/overlays.js +57 -0
- package/lib/renderers/three/overlays.js.map +1 -0
- package/lib/renderers/three/quality/quality.d.ts +18 -0
- package/lib/renderers/three/quality/quality.d.ts.map +1 -0
- package/lib/renderers/three/quality/quality.js +28 -0
- package/lib/renderers/three/quality/quality.js.map +1 -0
- package/lib/renderers/three/quality/quality.test.d.ts +2 -0
- package/lib/renderers/three/quality/quality.test.d.ts.map +1 -0
- package/lib/renderers/three/quality/quality.test.js +23 -0
- package/lib/renderers/three/quality/quality.test.js.map +1 -0
- package/lib/renderers/three/quality.d.ts +24 -0
- package/lib/renderers/three/quality.d.ts.map +1 -0
- package/lib/renderers/three/quality.js +67 -0
- package/lib/renderers/three/quality.js.map +1 -0
- package/lib/renderers/three/shadow/key-light.d.ts +6 -0
- package/lib/renderers/three/shadow/key-light.d.ts.map +1 -0
- package/lib/renderers/three/shadow/key-light.js +39 -0
- package/lib/renderers/three/shadow/key-light.js.map +1 -0
- package/lib/renderers/three/shadow/shadow.d.ts +21 -0
- package/lib/renderers/three/shadow/shadow.d.ts.map +1 -0
- package/lib/renderers/three/shadow/shadow.js +120 -0
- package/lib/renderers/three/shadow/shadow.js.map +1 -0
- package/lib/renderers/three/text/text.d.ts +55 -0
- package/lib/renderers/three/text/text.d.ts.map +1 -0
- package/lib/renderers/three/text/text.js +90 -0
- package/lib/renderers/three/text/text.js.map +1 -0
- package/lib/renderers/three/text/text.test.d.ts +2 -0
- package/lib/renderers/three/text/text.test.d.ts.map +1 -0
- package/lib/renderers/three/text/text.test.js +70 -0
- package/lib/renderers/three/text/text.test.js.map +1 -0
- package/lib/renderers/three/text.d.ts +53 -0
- package/lib/renderers/three/text.d.ts.map +1 -0
- package/lib/renderers/three/text.js +151 -0
- package/lib/renderers/three/text.js.map +1 -0
- package/lib/renderers/three/viewer/options.d.ts +82 -0
- package/lib/renderers/three/viewer/options.d.ts.map +1 -0
- package/lib/renderers/three/viewer/options.js +34 -0
- package/lib/renderers/three/viewer/options.js.map +1 -0
- package/lib/renderers/three/viewer/options.test.d.ts +2 -0
- package/lib/renderers/three/viewer/options.test.d.ts.map +1 -0
- package/lib/renderers/three/viewer/options.test.js +38 -0
- package/lib/renderers/three/viewer/options.test.js.map +1 -0
- package/lib/renderers/three/viewer/overlays.d.ts +31 -0
- package/lib/renderers/three/viewer/overlays.d.ts.map +1 -0
- package/lib/renderers/three/viewer/overlays.js +43 -0
- package/lib/renderers/three/viewer/overlays.js.map +1 -0
- package/lib/renderers/three/viewer/viewer.d.ts +102 -0
- package/lib/renderers/three/viewer/viewer.d.ts.map +1 -0
- package/lib/renderers/three/viewer/viewer.js +488 -0
- package/lib/renderers/three/viewer/viewer.js.map +1 -0
- package/lib/renderers/three/viewer.d.ts +38 -0
- package/lib/renderers/three/viewer.d.ts.map +1 -0
- package/lib/renderers/three/viewer.js +435 -0
- package/lib/renderers/three/viewer.js.map +1 -0
- package/package.json +34 -16
- package/src/abstract-3d.ts +60 -2
- package/src/renderers/dxf/dxf-encoding/dxf-build.ts +1 -2
- package/src/renderers/ifc/ifc.test.ts +7 -5
- package/src/renderers/index.ts +1 -0
- package/src/renderers/react/index.ts +2 -5
- package/src/renderers/react/react.tsx +107 -181
- package/src/renderers/three/appearance/appearance.test.ts +153 -0
- package/src/renderers/three/appearance/appearance.ts +150 -0
- package/src/renderers/three/build/build.test.ts +149 -0
- package/src/renderers/three/build/build.ts +278 -0
- package/src/renderers/three/build/materials.ts +222 -0
- package/src/renderers/three/build/parts.ts +277 -0
- package/src/renderers/three/camera/camera.test.ts +53 -0
- package/src/renderers/three/camera/camera.ts +82 -0
- package/src/renderers/three/camera/controls.ts +65 -0
- package/src/renderers/three/camera/gizmo.ts +94 -0
- package/src/renderers/three/compile/compile.test.ts +126 -0
- package/src/renderers/three/compile/compile.ts +151 -0
- package/src/renderers/three/environment/studio.ts +18 -0
- package/src/renderers/three/geometry/geometry.test.ts +121 -0
- package/src/renderers/three/geometry/geometry.ts +189 -0
- package/src/renderers/three/index.ts +15 -0
- package/src/renderers/three/quality/quality.test.ts +37 -0
- package/src/renderers/three/quality/quality.ts +36 -0
- package/src/renderers/three/shadow/key-light.ts +43 -0
- package/src/renderers/three/shadow/shadow.ts +170 -0
- package/src/renderers/three/text/text.test.ts +79 -0
- package/src/renderers/three/text/text.ts +128 -0
- package/src/renderers/three/viewer/options.ts +112 -0
- package/src/renderers/three/viewer/overlays.ts +71 -0
- package/src/renderers/three/viewer/viewer.ts +596 -0
- package/lib/renderers/react/react-hotspot.d.ts +0 -27
- package/lib/renderers/react/react-hotspot.d.ts.map +0 -1
- package/lib/renderers/react/react-hotspot.js +0 -37
- package/lib/renderers/react/react-hotspot.js.map +0 -1
- package/src/custom_typings/react-three-fiber.d.ts +0 -9
- package/src/custom_typings/react-three-postprocessing.d.ts +0 -1
- package/src/renderers/react/react-camera.tsx +0 -391
- package/src/renderers/react/react-dimension.tsx +0 -122
- package/src/renderers/react/react-group.test.ts +0 -56
- package/src/renderers/react/react-group.tsx +0 -202
- package/src/renderers/react/react-image-material.tsx +0 -139
- package/src/renderers/react/react-material.tsx +0 -91
- package/src/renderers/react/react-mesh.tsx +0 -512
- package/src/renderers/react/react-relation.tsx +0 -86
- package/src/renderers/react/react-scene.tsx +0 -111
- 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
|
|
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
|
-
|
|
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)
|
|
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((
|
|
25
|
-
ids(
|
|
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
|
package/src/renderers/index.ts
CHANGED
|
@@ -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
|
|
3
|
-
export
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
readonly
|
|
22
|
-
readonly
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
readonly
|
|
31
|
-
readonly
|
|
32
|
-
readonly
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
}
|