@base2datadesign/viewer-react 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ export type AccessAssetsItem = {
2
+ key: string;
3
+ label: string;
4
+ onClick: () => void | Promise<void>;
5
+ disabled?: boolean;
6
+ };
7
+ export type AccessAssetsMenuProps = {
8
+ label?: string;
9
+ items: AccessAssetsItem[];
10
+ disabled?: boolean;
11
+ className?: string;
12
+ };
13
+ export default function AccessAssetsMenu({ label, items, disabled, className, }: AccessAssetsMenuProps): import("react/jsx-runtime").JSX.Element | null;
14
+ //# sourceMappingURL=AccessAssetsMenu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccessAssetsMenu.d.ts","sourceRoot":"","sources":["../src/AccessAssetsMenu.tsx"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,KAAuB,EACvB,KAAK,EACL,QAAgB,EAChB,SAAS,GACV,EAAE,qBAAqB,kDAsFvB"}
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ export default function AccessAssetsMenu({ label = "access assets", items, disabled = false, className, }) {
4
+ const containerRef = useRef(null);
5
+ const [open, setOpen] = useState(false);
6
+ const resolvedItems = useMemo(() => items.filter((item) => item && item.label && typeof item.onClick === "function"), [items]);
7
+ useEffect(() => {
8
+ if (!open)
9
+ return;
10
+ const handler = (event) => {
11
+ if (!containerRef.current)
12
+ return;
13
+ if (!containerRef.current.contains(event.target)) {
14
+ setOpen(false);
15
+ }
16
+ };
17
+ window.addEventListener("mousedown", handler);
18
+ return () => window.removeEventListener("mousedown", handler);
19
+ }, [open]);
20
+ if (!resolvedItems.length)
21
+ return null;
22
+ return (_jsxs("div", { ref: containerRef, className: className, style: { width: "100%", maxWidth: "220px" }, children: [_jsxs("button", { type: "button", onClick: () => setOpen((prev) => !prev), disabled: disabled, style: {
23
+ width: "100%",
24
+ borderRadius: "8px",
25
+ border: "1px solid #e2e8f0",
26
+ background: "#ffffff",
27
+ padding: "10px 12px",
28
+ fontSize: "12px",
29
+ textTransform: "lowercase",
30
+ color: "#475569",
31
+ cursor: disabled ? "not-allowed" : "pointer",
32
+ opacity: disabled ? 0.6 : 1,
33
+ display: "flex",
34
+ alignItems: "center",
35
+ justifyContent: "space-between",
36
+ gap: "8px",
37
+ }, children: [_jsx("span", { children: label }), _jsx("span", { style: { fontSize: "12px", transform: open ? "rotate(180deg)" : "rotate(0deg)", transition: "transform 0.2s ease" }, children: "\u25BE" })] }), open ? (_jsx("div", { style: { marginTop: "10px", display: "flex", flexDirection: "column", gap: "8px" }, children: resolvedItems.map((item) => (_jsx("button", { type: "button", onClick: async () => {
38
+ try {
39
+ await item.onClick();
40
+ }
41
+ finally {
42
+ setOpen(false);
43
+ }
44
+ }, disabled: disabled || item.disabled, style: {
45
+ width: "100%",
46
+ borderRadius: "8px",
47
+ border: "1px dashed #e2e8f0",
48
+ padding: "10px 12px",
49
+ fontSize: "12px",
50
+ textTransform: "lowercase",
51
+ color: "#64748b",
52
+ background: "transparent",
53
+ cursor: disabled || item.disabled ? "not-allowed" : "pointer",
54
+ opacity: disabled || item.disabled ? 0.5 : 1,
55
+ }, children: item.label }, item.key))) })) : null] }));
56
+ }
@@ -0,0 +1,17 @@
1
+ import { type UploadResult } from "@base2datadesign/viewer-kit";
2
+ export type AssetUploadCardProps = {
3
+ disabled?: boolean;
4
+ apiRoute?: string;
5
+ rhinoApiRoute?: string;
6
+ maxBytes?: number;
7
+ timeoutMs?: number;
8
+ preview?: Record<string, unknown>;
9
+ accept?: string;
10
+ onSolved: (result: UploadResult) => void | Promise<void>;
11
+ onClear?: () => void;
12
+ className?: string;
13
+ title?: string;
14
+ description?: string;
15
+ };
16
+ export default function AssetUploadCard({ disabled, apiRoute, rhinoApiRoute, maxBytes, timeoutMs, preview, accept, onSolved, onClear, className, title, description, }: AssetUploadCardProps): import("react/jsx-runtime").JSX.Element;
17
+ //# sourceMappingURL=AssetUploadCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AssetUploadCard.d.ts","sourceRoot":"","sources":["../src/AssetUploadCard.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,YAAY,EAClB,MAAM,6BAA6B,CAAC;AAErC,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAUF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EACtC,QAAQ,EACR,QAA0B,EAC1B,aAAgC,EAChC,QAAyC,EACzC,SAA2C,EAC3C,OAAO,EACP,MAAuB,EACvB,QAAQ,EACR,OAAO,EACP,SAAS,EACT,KAAsB,EACtB,WAAsF,GACvF,EAAE,oBAAoB,2CAqKtB"}
@@ -0,0 +1,126 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { DEFAULT_UPLOAD_LIMITS, detectUploadKind, loadMeshFile, solveGrasshopperFile, solveRhino3dmFile, } from "@base2datadesign/viewer-kit";
4
+ const DEFAULT_ACCEPT = ".gh,.ghx,.3dm,.obj,.fbx,.gltf,.glb,.stl,.ply";
5
+ const formatBytes = (bytes) => {
6
+ if (!Number.isFinite(bytes))
7
+ return "";
8
+ const mb = bytes / (1024 * 1024);
9
+ return `${mb.toFixed(0)}MB`;
10
+ };
11
+ export default function AssetUploadCard({ disabled, apiRoute = "/api/gh-solve", rhinoApiRoute = "/api/rhino-3dm", maxBytes = DEFAULT_UPLOAD_LIMITS.maxBytes, timeoutMs = DEFAULT_UPLOAD_LIMITS.timeoutMs, preview, accept = DEFAULT_ACCEPT, onSolved, onClear, className, title = "Asset Upload", description = "Upload .gh/.ghx/.3dm or mesh assets (OBJ/FBX/GLTF/STL/PLY) to preview.", }) {
12
+ const [busy, setBusy] = useState(false);
13
+ const [lastFileName, setLastFileName] = useState(null);
14
+ const [error, setError] = useState(null);
15
+ const [dragActive, setDragActive] = useState(false);
16
+ const [statusLine, setStatusLine] = useState("ready");
17
+ const [statusTone, setStatusTone] = useState("muted");
18
+ const statusText = useMemo(() => (busy ? "processing..." : statusLine), [busy, statusLine]);
19
+ const maxLabel = formatBytes(maxBytes);
20
+ const updateStatus = (fileName, detail, tone = "muted") => {
21
+ setStatusLine(`${fileName} · ${detail}`);
22
+ setStatusTone(tone);
23
+ };
24
+ const uploadAndSolve = async (file) => {
25
+ if (disabled || busy)
26
+ return;
27
+ const kind = detectUploadKind(file);
28
+ if (kind === "unknown") {
29
+ setError("unsupported file type");
30
+ return;
31
+ }
32
+ if (typeof maxBytes === "number" && maxBytes > 0 && file.size > maxBytes) {
33
+ setError(`file exceeds ${formatBytes(maxBytes)} limit`);
34
+ return;
35
+ }
36
+ setBusy(true);
37
+ setError(null);
38
+ setLastFileName(file.name);
39
+ updateStatus(file.name, "starting");
40
+ try {
41
+ let result;
42
+ if (kind === "gh" || kind === "ghx") {
43
+ const solved = await solveGrasshopperFile(file, {
44
+ apiRoute,
45
+ preview,
46
+ onStatus: (detail) => updateStatus(file.name, detail),
47
+ });
48
+ result = { ...solved, file };
49
+ }
50
+ else if (kind === "3dm") {
51
+ const solved = await solveRhino3dmFile(file, {
52
+ apiRoute: rhinoApiRoute,
53
+ onStatus: (detail) => updateStatus(file.name, detail),
54
+ });
55
+ result = { ...solved, file };
56
+ }
57
+ else {
58
+ const solved = await loadMeshFile(file, {
59
+ maxBytes,
60
+ timeoutMs,
61
+ onStatus: (detail) => updateStatus(file.name, detail),
62
+ });
63
+ result = { ...solved, file };
64
+ }
65
+ await onSolved(result);
66
+ updateStatus(file.name, "completed", "success");
67
+ }
68
+ catch (e) {
69
+ const message = e?.message ?? "failed to process upload";
70
+ setError(message);
71
+ updateStatus(file.name, `error: ${message}`, "error");
72
+ }
73
+ finally {
74
+ setBusy(false);
75
+ }
76
+ };
77
+ const inputId = "asset-upload-input";
78
+ return (_jsxs("section", { className: className, style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: "12px" }, children: [_jsxs("div", { children: [_jsx("h3", { style: { fontSize: "12px", fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase" }, children: title }), _jsx("p", { style: { marginTop: "6px", fontSize: "12px", lineHeight: 1.5 }, children: description }), _jsxs("p", { style: { marginTop: "4px", fontSize: "11px", opacity: 0.7 }, children: ["limits: ", maxLabel, ", timeout ", timeoutMs, "ms"] })] }), onClear ? (_jsx("button", { type: "button", onClick: onClear, disabled: disabled || busy, style: { height: "32px" }, children: "clear" })) : null] }), _jsxs("div", { role: "button", tabIndex: 0, onDragEnter: (event) => {
79
+ event.preventDefault();
80
+ event.stopPropagation();
81
+ if (disabled || busy)
82
+ return;
83
+ setDragActive(true);
84
+ }, onDragOver: (event) => {
85
+ event.preventDefault();
86
+ event.stopPropagation();
87
+ if (disabled || busy)
88
+ return;
89
+ setDragActive(true);
90
+ }, onDragLeave: (event) => {
91
+ event.preventDefault();
92
+ event.stopPropagation();
93
+ setDragActive(false);
94
+ }, onDrop: (event) => {
95
+ event.preventDefault();
96
+ event.stopPropagation();
97
+ setDragActive(false);
98
+ const dropped = event.dataTransfer.files?.[0];
99
+ if (!dropped)
100
+ return;
101
+ void uploadAndSolve(dropped);
102
+ }, onClick: () => {
103
+ if (disabled || busy)
104
+ return;
105
+ const input = document.getElementById(inputId);
106
+ input?.click();
107
+ }, style: {
108
+ borderRadius: "12px",
109
+ border: "1px dashed #94a3b8",
110
+ padding: "14px",
111
+ background: dragActive ? "rgba(16, 185, 129, 0.08)" : "rgba(255,255,255,0.85)",
112
+ cursor: disabled || busy ? "not-allowed" : "pointer",
113
+ opacity: disabled || busy ? 0.6 : 1,
114
+ }, children: [_jsx("div", { style: { fontSize: "12px", textTransform: "lowercase" }, children: lastFileName ? `loaded ${lastFileName}` : "drop asset file" }), _jsx("div", { style: { marginTop: "6px", fontSize: "11px", opacity: 0.7 }, children: statusText }), error ? (_jsx("div", { style: { marginTop: "6px", fontSize: "11px", color: "#b91c1c" }, children: error })) : null] }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: "10px", flexWrap: "wrap" }, children: [_jsx("button", { type: "button", onClick: () => {
115
+ if (disabled || busy)
116
+ return;
117
+ const input = document.getElementById(inputId);
118
+ input?.click();
119
+ }, children: "select file" }), _jsx("span", { style: { fontSize: "11px", opacity: 0.7 }, children: accept.replace(/\./g, "").toUpperCase() })] }), _jsx("input", { id: inputId, type: "file", accept: accept, style: { display: "none" }, onChange: (event) => {
120
+ const file = event.target.files?.[0];
121
+ if (!file)
122
+ return;
123
+ void uploadAndSolve(file);
124
+ event.currentTarget.value = "";
125
+ } })] }));
126
+ }
@@ -0,0 +1,23 @@
1
+ export type LightingOption = {
2
+ id: string;
3
+ label: string;
4
+ summary?: string;
5
+ };
6
+ export type SkyOption = {
7
+ id: string;
8
+ label: string;
9
+ description?: string;
10
+ };
11
+ export type ViewControlsProps = {
12
+ activeLightingPreset: string;
13
+ onLightingChange: (preset: string) => void;
14
+ onCameraViewChange: (view: string) => void;
15
+ skyOptions: SkyOption[];
16
+ activeSkyId: string;
17
+ onSkyChange: (skyId: string) => void;
18
+ lightingOptions: LightingOption[];
19
+ veilsEnabled?: boolean;
20
+ onToggleVeils?: () => void;
21
+ };
22
+ export default function ViewControls({ activeLightingPreset, onLightingChange, onCameraViewChange, skyOptions, activeSkyId, onSkyChange, lightingOptions, veilsEnabled, onToggleVeils, }: ViewControlsProps): import("react/jsx-runtime").JSX.Element;
23
+ //# sourceMappingURL=ViewControls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ViewControls.d.ts","sourceRoot":"","sources":["../src/ViewControls.tsx"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC;AA2CF,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACnC,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,EACV,WAAW,EACX,WAAW,EACX,eAAe,EACf,YAAmB,EACnB,aAAa,GACd,EAAE,iBAAiB,2CA6QnB"}
@@ -0,0 +1,182 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ const cardStyle = {
4
+ position: "relative",
5
+ background: "rgba(255,255,255,0.92)",
6
+ border: "1px solid #e2e8f0",
7
+ borderRadius: "10px",
8
+ boxShadow: "0 12px 24px rgba(15,23,42,0.12)",
9
+ overflow: "visible",
10
+ };
11
+ const labelStyle = {
12
+ fontSize: "12px",
13
+ textTransform: "lowercase",
14
+ color: "#475569",
15
+ };
16
+ const veilStyle = {
17
+ pointerEvents: "none",
18
+ position: "absolute",
19
+ inset: 0,
20
+ display: "flex",
21
+ alignItems: "center",
22
+ justifyContent: "center",
23
+ fontSize: "11px",
24
+ textTransform: "uppercase",
25
+ letterSpacing: "0.1em",
26
+ color: "rgba(100,116,139,0.7)",
27
+ };
28
+ const dropdownStyle = {
29
+ position: "absolute",
30
+ bottom: "calc(100% + 8px)",
31
+ left: 0,
32
+ background: "rgba(255,255,255,0.98)",
33
+ border: "1px solid #e2e8f0",
34
+ borderRadius: "10px",
35
+ padding: "8px",
36
+ boxShadow: "0 16px 24px rgba(15,23,42,0.14)",
37
+ minWidth: "220px",
38
+ zIndex: 60,
39
+ };
40
+ export default function ViewControls({ activeLightingPreset, onLightingChange, onCameraViewChange, skyOptions, activeSkyId, onSkyChange, lightingOptions, veilsEnabled = true, onToggleVeils, }) {
41
+ const lightingMenuRef = useRef(null);
42
+ const skyMenuRef = useRef(null);
43
+ const [lightingExpanded, setLightingExpanded] = useState(false);
44
+ const [skyExpanded, setSkyExpanded] = useState(false);
45
+ const [lightingHover, setLightingHover] = useState(false);
46
+ const [skyHover, setSkyHover] = useState(false);
47
+ const [viewHover, setViewHover] = useState(false);
48
+ const activeLighting = useMemo(() => lightingOptions.find((option) => option.id === activeLightingPreset) ?? lightingOptions[0], [lightingOptions, activeLightingPreset]);
49
+ const activeSky = useMemo(() => skyOptions.find((option) => option.id === activeSkyId) ?? skyOptions[0], [skyOptions, activeSkyId]);
50
+ useEffect(() => {
51
+ if (!lightingExpanded && !skyExpanded)
52
+ return;
53
+ const onMouseDown = (event) => {
54
+ const target = event.target;
55
+ if (!target)
56
+ return;
57
+ if (lightingMenuRef.current?.contains(target))
58
+ return;
59
+ if (skyMenuRef.current?.contains(target))
60
+ return;
61
+ setLightingExpanded(false);
62
+ setSkyExpanded(false);
63
+ };
64
+ const onKeyDown = (event) => {
65
+ if (event.key !== "Escape")
66
+ return;
67
+ setLightingExpanded(false);
68
+ setSkyExpanded(false);
69
+ };
70
+ window.addEventListener("mousedown", onMouseDown);
71
+ window.addEventListener("keydown", onKeyDown);
72
+ return () => {
73
+ window.removeEventListener("mousedown", onMouseDown);
74
+ window.removeEventListener("keydown", onKeyDown);
75
+ };
76
+ }, [lightingExpanded, skyExpanded]);
77
+ return (_jsxs("div", { style: { display: "flex", gap: "10px", alignItems: "flex-end" }, children: [_jsx("div", { ref: lightingMenuRef, children: _jsxs("div", { style: cardStyle, onMouseEnter: () => setLightingHover(true), onMouseLeave: () => setLightingHover(false), children: [lightingExpanded ? (_jsx("div", { style: dropdownStyle, children: lightingOptions.map((option) => (_jsxs("button", { type: "button", onClick: () => {
78
+ onLightingChange(option.id);
79
+ setLightingExpanded(false);
80
+ }, style: {
81
+ width: "100%",
82
+ border: "none",
83
+ borderRadius: "8px",
84
+ padding: "8px 10px",
85
+ textAlign: "left",
86
+ background: option.id === activeLightingPreset ? "rgba(49,67,144,0.12)" : "transparent",
87
+ color: option.id === activeLightingPreset ? "#314390" : "#475569",
88
+ cursor: "pointer",
89
+ display: "flex",
90
+ flexDirection: "column",
91
+ gap: "2px",
92
+ fontSize: "12px",
93
+ textTransform: "lowercase",
94
+ }, children: [_jsx("span", { children: option.label }), option.summary ? (_jsx("span", { style: { fontSize: "10px", opacity: 0.7 }, children: option.summary })) : null] }, option.id))) })) : null, _jsxs("button", { type: "button", onClick: () => {
95
+ setLightingExpanded((prev) => !prev);
96
+ setSkyExpanded(false);
97
+ }, style: {
98
+ display: "flex",
99
+ alignItems: "center",
100
+ justifyContent: "space-between",
101
+ gap: "10px",
102
+ padding: "8px 12px",
103
+ border: "none",
104
+ background: "transparent",
105
+ width: "100%",
106
+ cursor: "pointer",
107
+ }, children: [_jsx("span", { style: {
108
+ ...labelStyle,
109
+ opacity: !veilsEnabled || lightingExpanded || lightingHover ? 1 : 0,
110
+ transition: "opacity 0.2s ease",
111
+ }, children: activeLighting?.label ?? activeLightingPreset }), _jsx("span", { style: {
112
+ fontSize: "11px",
113
+ color: "#64748b",
114
+ opacity: !veilsEnabled || lightingExpanded || lightingHover ? 1 : 0,
115
+ transition: "opacity 0.2s ease",
116
+ }, children: lightingExpanded ? "▼" : "▲" })] }), veilsEnabled && !lightingExpanded && !lightingHover ? _jsx("div", { style: veilStyle, children: "LIGHTING" }) : null] }) }), _jsxs("div", { style: { ...cardStyle, display: "flex" }, onMouseEnter: () => setViewHover(true), onMouseLeave: () => setViewHover(false), children: [_jsx("button", { type: "button", onClick: () => onCameraViewChange("interior"), style: {
117
+ padding: "8px 12px",
118
+ border: "none",
119
+ borderRight: "1px solid #e2e8f0",
120
+ background: "transparent",
121
+ cursor: "pointer",
122
+ minWidth: "84px",
123
+ ...labelStyle,
124
+ color: !veilsEnabled || viewHover ? "#475569" : "transparent",
125
+ }, children: "interior" }), _jsx("button", { type: "button", onClick: () => onCameraViewChange("iso"), style: {
126
+ padding: "8px 12px",
127
+ border: "none",
128
+ background: "transparent",
129
+ cursor: "pointer",
130
+ minWidth: "84px",
131
+ ...labelStyle,
132
+ color: !veilsEnabled || viewHover ? "#475569" : "transparent",
133
+ }, children: "perspective" }), veilsEnabled && !viewHover ? _jsx("div", { style: veilStyle, children: "VIEW" }) : null] }), skyOptions.length ? (_jsx("div", { ref: skyMenuRef, children: _jsxs("div", { style: cardStyle, onMouseEnter: () => setSkyHover(true), onMouseLeave: () => setSkyHover(false), children: [skyExpanded ? (_jsx("div", { style: dropdownStyle, children: skyOptions.map((option) => (_jsxs("button", { type: "button", onClick: () => {
134
+ onSkyChange(option.id);
135
+ setSkyExpanded(false);
136
+ }, style: {
137
+ width: "100%",
138
+ border: "none",
139
+ borderRadius: "8px",
140
+ padding: "8px 10px",
141
+ textAlign: "left",
142
+ background: option.id === activeSkyId ? "rgba(49,67,144,0.12)" : "transparent",
143
+ color: option.id === activeSkyId ? "#314390" : "#475569",
144
+ cursor: "pointer",
145
+ display: "flex",
146
+ flexDirection: "column",
147
+ gap: "2px",
148
+ fontSize: "12px",
149
+ textTransform: "lowercase",
150
+ }, children: [_jsx("span", { children: option.label }), option.description ? (_jsx("span", { style: { fontSize: "10px", opacity: 0.7 }, children: option.description })) : null] }, option.id))) })) : null, _jsxs("button", { type: "button", onClick: () => {
151
+ setSkyExpanded((prev) => !prev);
152
+ setLightingExpanded(false);
153
+ }, style: {
154
+ display: "flex",
155
+ alignItems: "center",
156
+ justifyContent: "space-between",
157
+ gap: "10px",
158
+ padding: "8px 12px",
159
+ border: "none",
160
+ background: "transparent",
161
+ width: "100%",
162
+ cursor: "pointer",
163
+ }, children: [_jsx("span", { style: {
164
+ ...labelStyle,
165
+ opacity: !veilsEnabled || skyExpanded || skyHover ? 1 : 0,
166
+ transition: "opacity 0.2s ease",
167
+ }, children: activeSky?.label ?? "preset dome" }), _jsx("span", { style: {
168
+ fontSize: "11px",
169
+ color: "#64748b",
170
+ opacity: !veilsEnabled || skyExpanded || skyHover ? 1 : 0,
171
+ transition: "opacity 0.2s ease",
172
+ }, children: skyExpanded ? "▼" : "▲" })] }), veilsEnabled && !skyExpanded && !skyHover ? _jsx("div", { style: veilStyle, children: "BACKGROUND" }) : null] }) })) : null, onToggleVeils ? (_jsx("button", { type: "button", onClick: onToggleVeils, "aria-label": "Toggle veils", style: {
173
+ alignSelf: "stretch",
174
+ border: "none",
175
+ background: "transparent",
176
+ fontSize: "11px",
177
+ textTransform: "uppercase",
178
+ letterSpacing: "0.14em",
179
+ color: "#64748b",
180
+ cursor: "pointer",
181
+ }, children: "VEILS" })) : null] }));
182
+ }
@@ -5,8 +5,9 @@ export type ViewerCanvasProps = {
5
5
  assetResolver?: ViewerCreateOptions["assetResolver"];
6
6
  camera?: ViewerCreateOptions["camera"];
7
7
  renderer?: ViewerCreateOptions["renderer"];
8
+ usePostprocessing?: ViewerCreateOptions["usePostprocessing"];
8
9
  className?: string;
9
10
  onReady?: (handle: ViewerHandle | null) => void;
10
11
  };
11
- export default function ViewerCanvas({ presetId, presets, assetResolver, camera, renderer, className, onReady, }: ViewerCanvasProps): import("react/jsx-runtime").JSX.Element;
12
+ export default function ViewerCanvas({ presetId, presets, assetResolver, camera, renderer, usePostprocessing, className, onReady, }: ViewerCanvasProps): import("react/jsx-runtime").JSX.Element;
12
13
  //# sourceMappingURL=ViewerCanvas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ViewerCanvas.d.ts","sourceRoot":"","sources":["../src/ViewerCanvas.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEpG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,aAAa,CAAC,EAAE,mBAAmB,CAAC,eAAe,CAAC,CAAC;IACrD,MAAM,CAAC,EAAE,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,KAAK,IAAI,CAAC;CACjD,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACnC,QAAQ,EACR,OAAO,EACP,aAAa,EACb,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACR,EAAE,iBAAiB,2CAwDnB"}
1
+ {"version":3,"file":"ViewerCanvas.d.ts","sourceRoot":"","sources":["../src/ViewerCanvas.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEpG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,aAAa,CAAC,EAAE,mBAAmB,CAAC,eAAe,CAAC,CAAC;IACrD,MAAM,CAAC,EAAE,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAC3C,iBAAiB,CAAC,EAAE,mBAAmB,CAAC,mBAAmB,CAAC,CAAC;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,KAAK,IAAI,CAAC;CACjD,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACnC,QAAQ,EACR,OAAO,EACP,aAAa,EACb,MAAM,EACN,QAAQ,EACR,iBAAiB,EACjB,SAAS,EACT,OAAO,GACR,EAAE,iBAAiB,2CA8InB"}
@@ -1,48 +1,136 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useRef } from "react";
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { createViewer } from "@base2datadesign/viewer-kit";
4
- export default function ViewerCanvas({ presetId, presets, assetResolver, camera, renderer, className, onReady, }) {
5
- const containerRef = useRef(null);
4
+ export default function ViewerCanvas({ presetId, presets, assetResolver, camera, renderer, usePostprocessing, className, onReady, }) {
5
+ const [container, setContainer] = useState(null);
6
6
  const handleRef = useRef(null);
7
7
  const presetIdRef = useRef(presetId);
8
8
  const initialPresetRef = useRef(presetId);
9
+ const instanceRef = useRef(0);
9
10
  const createOptions = useMemo(() => {
10
11
  return {
11
- container: containerRef.current,
12
12
  presetId: initialPresetRef.current,
13
13
  presets,
14
14
  assetResolver,
15
15
  camera,
16
16
  renderer,
17
- hooks: {
18
- onReady: (handle) => {
19
- handleRef.current = handle;
20
- onReady?.(handle);
21
- },
22
- onError: (error) => {
23
- console.error("[viewer-react] Failed to initialise viewer", error);
24
- onReady?.(null);
25
- },
26
- },
17
+ usePostprocessing,
27
18
  };
28
- }, [assetResolver, camera, onReady, presets, renderer]);
29
- useEffect(() => {
30
- const container = containerRef.current;
19
+ }, [assetResolver, camera, presets, renderer, usePostprocessing]);
20
+ const handleContainerRef = useCallback((node) => {
21
+ setContainer(node);
22
+ }, []);
23
+ useLayoutEffect(() => {
31
24
  if (!container)
32
25
  return;
33
- const handle = createViewer({ ...createOptions, container });
34
- handleRef.current = handle;
35
- handle.start();
36
- const handleResize = () => handle.resize();
37
- window.addEventListener("resize", handleResize);
38
- return () => {
39
- window.removeEventListener("resize", handleResize);
40
- handle.stop();
41
- handle.dispose();
26
+ instanceRef.current += 1;
27
+ const instanceId = instanceRef.current;
28
+ let disposed = false;
29
+ let initialized = false;
30
+ let resizeObserver = null;
31
+ let sizeObserver = null;
32
+ let rafId = null;
33
+ let handleResize = null;
34
+ let handle = null;
35
+ const cleanup = () => {
36
+ disposed = true;
37
+ if (rafId !== null) {
38
+ cancelAnimationFrame(rafId);
39
+ rafId = null;
40
+ }
41
+ sizeObserver?.disconnect();
42
+ sizeObserver = null;
43
+ resizeObserver?.disconnect();
44
+ resizeObserver = null;
45
+ if (handleResize) {
46
+ window.removeEventListener("resize", handleResize);
47
+ handleResize = null;
48
+ }
49
+ if (handle) {
50
+ handle.stop();
51
+ handle.dispose();
52
+ }
42
53
  handleRef.current = null;
43
- onReady?.(null);
54
+ if (instanceRef.current === instanceId) {
55
+ onReady?.(null);
56
+ }
57
+ };
58
+ const initViewer = () => {
59
+ if (disposed || initialized)
60
+ return;
61
+ if (container.clientWidth === 0 || container.clientHeight === 0)
62
+ return;
63
+ initialized = true;
64
+ container.innerHTML = "";
65
+ handle = createViewer({
66
+ ...createOptions,
67
+ container,
68
+ hooks: {
69
+ onReady: (viewer) => {
70
+ if (disposed || instanceRef.current !== instanceId)
71
+ return;
72
+ viewer.__viewerId = instanceId;
73
+ handleRef.current = viewer;
74
+ console.log("[viewer-react] ready", { viewerId: instanceId });
75
+ onReady?.(viewer);
76
+ },
77
+ onError: (error) => {
78
+ if (disposed || instanceRef.current !== instanceId)
79
+ return;
80
+ console.error("[viewer-react] Failed to initialise viewer", error);
81
+ onReady?.(null);
82
+ },
83
+ },
84
+ });
85
+ handleRef.current = handle;
86
+ handle.__viewerId = instanceId;
87
+ console.log("[viewer-react] create", { viewerId: instanceId });
88
+ handle.start();
89
+ const onResize = () => handle?.resize();
90
+ handleResize = onResize;
91
+ window.addEventListener("resize", onResize);
92
+ onResize();
93
+ if (typeof ResizeObserver !== "undefined") {
94
+ resizeObserver = new ResizeObserver(() => handle?.resize());
95
+ resizeObserver.observe(container);
96
+ }
97
+ else {
98
+ requestAnimationFrame(() => handle?.resize());
99
+ }
100
+ };
101
+ const waitForSize = () => {
102
+ if (disposed || initialized)
103
+ return;
104
+ initViewer();
105
+ if (initialized)
106
+ return;
107
+ if (typeof ResizeObserver !== "undefined") {
108
+ sizeObserver = new ResizeObserver(() => {
109
+ if (initialized || disposed)
110
+ return;
111
+ initViewer();
112
+ if (initialized) {
113
+ sizeObserver?.disconnect();
114
+ sizeObserver = null;
115
+ }
116
+ });
117
+ sizeObserver.observe(container);
118
+ }
119
+ else {
120
+ const tick = () => {
121
+ if (disposed || initialized)
122
+ return;
123
+ initViewer();
124
+ if (!initialized) {
125
+ rafId = requestAnimationFrame(tick);
126
+ }
127
+ };
128
+ rafId = requestAnimationFrame(tick);
129
+ }
44
130
  };
45
- }, [createOptions, onReady]);
131
+ waitForSize();
132
+ return cleanup;
133
+ }, [container, createOptions, onReady]);
46
134
  useEffect(() => {
47
135
  if (!handleRef.current)
48
136
  return;
@@ -51,5 +139,5 @@ export default function ViewerCanvas({ presetId, presets, assetResolver, camera,
51
139
  handleRef.current.setPreset(presetId);
52
140
  }
53
141
  }, [presetId]);
54
- return _jsx("div", { ref: containerRef, className: className });
142
+ return _jsx("div", { ref: handleContainerRef, className: className });
55
143
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export { default as ViewerCanvas } from "./ViewerCanvas";
2
+ export { default as AssetUploadCard } from "./AssetUploadCard";
3
+ export { default as AccessAssetsMenu } from "./AccessAssetsMenu";
4
+ export { default as ViewControls } from "./ViewControls";
2
5
  export type { ViewerCanvasProps } from "./ViewerCanvas";
3
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -1 +1,4 @@
1
1
  export { default as ViewerCanvas } from "./ViewerCanvas";
2
+ export { default as AssetUploadCard } from "./AssetUploadCard";
3
+ export { default as AccessAssetsMenu } from "./AccessAssetsMenu";
4
+ export { default as ViewControls } from "./ViewControls";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base2datadesign/viewer-react",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "main": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@base2datadesign/viewer-kit": "^0.1.3"
21
+ "@base2datadesign/viewer-kit": "^0.2.0"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "react": ">=18"