@treasuryspatial/viewer-react 0.1.11

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,15 @@
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
+ fullWidth?: boolean;
13
+ };
14
+ export default function AccessAssetsMenu({ label, items, disabled, className, fullWidth, }: AccessAssetsMenuProps): import("react/jsx-runtime").JSX.Element | null;
15
+ //# 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;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,KAAuB,EACvB,KAAK,EACL,QAAgB,EAChB,SAAS,EACT,SAAiB,GAClB,EAAE,qBAAqB,kDA0FvB"}
@@ -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, fullWidth = false, }) {
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: fullWidth ? "none" : "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,19 @@
1
+ import { type UploadResult } from "@treasuryspatial/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
+ allowEmptyGeometry?: boolean;
10
+ accept?: string;
11
+ onSolved: (result: UploadResult) => void | Promise<void>;
12
+ onClear?: () => void;
13
+ className?: string;
14
+ title?: string;
15
+ description?: string;
16
+ minimal?: boolean;
17
+ };
18
+ export default function AssetUploadCard({ disabled, apiRoute, rhinoApiRoute, maxBytes, timeoutMs, preview, allowEmptyGeometry, accept, onSolved, onClear, className, title, description, minimal, }: AssetUploadCardProps): import("react/jsx-runtime").JSX.Element;
19
+ //# 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,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,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;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAUF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EACtC,QAAQ,EACR,QAA0B,EAC1B,aAAgC,EAChC,QAAyC,EACzC,SAA2C,EAC3C,OAAO,EACP,kBAAkB,EAClB,MAAuB,EACvB,QAAQ,EACR,OAAO,EACP,SAAS,EACT,KAAsB,EACtB,WAAsF,EACtF,OAAe,GAChB,EAAE,oBAAoB,2CA6NtB"}
@@ -0,0 +1,157 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { DEFAULT_UPLOAD_LIMITS, detectUploadKind, loadMeshFile, solveGrasshopperFile, solveRhino3dmFile, } from "@treasuryspatial/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, allowEmptyGeometry, accept = DEFAULT_ACCEPT, onSolved, onClear, className, title = "Asset Upload", description = "Upload .gh/.ghx/.3dm or mesh assets (OBJ/FBX/GLTF/STL/PLY) to preview.", minimal = false, }) {
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 [hovered, setHovered] = useState(false);
19
+ const statusText = useMemo(() => (busy ? "processing..." : statusLine), [busy, statusLine]);
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
+ allowEmptyGeometry,
47
+ onStatus: (detail) => updateStatus(file.name, detail),
48
+ });
49
+ result = { ...solved, file };
50
+ }
51
+ else if (kind === "3dm") {
52
+ const solved = await solveRhino3dmFile(file, {
53
+ apiRoute: rhinoApiRoute,
54
+ onStatus: (detail) => updateStatus(file.name, detail),
55
+ });
56
+ result = { ...solved, file };
57
+ }
58
+ else {
59
+ const solved = await loadMeshFile(file, {
60
+ maxBytes,
61
+ timeoutMs,
62
+ onStatus: (detail) => updateStatus(file.name, detail),
63
+ });
64
+ result = { ...solved, file };
65
+ }
66
+ await onSolved(result);
67
+ updateStatus(file.name, "completed", "success");
68
+ }
69
+ catch (e) {
70
+ const message = e?.message ?? "failed to process upload";
71
+ setError(message);
72
+ updateStatus(file.name, `error: ${message}`, "error");
73
+ }
74
+ finally {
75
+ setBusy(false);
76
+ }
77
+ };
78
+ const inputId = "asset-upload-input";
79
+ const typesLabel = accept.replace(/\./g, "").toUpperCase().replace(/,/g, ", ");
80
+ const showStatusLine = minimal && (busy || statusLine !== "ready" || error);
81
+ return (_jsxs("section", { className: className, style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [!minimal ? (_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 })] }), onClear ? (_jsx("button", { type: "button", onClick: onClear, disabled: disabled || busy, style: { height: "32px" }, children: "clear" })) : null] })) : null, _jsxs("div", { role: "button", tabIndex: 0, onDragEnter: (event) => {
82
+ event.preventDefault();
83
+ event.stopPropagation();
84
+ if (disabled || busy)
85
+ return;
86
+ setDragActive(true);
87
+ }, onDragOver: (event) => {
88
+ event.preventDefault();
89
+ event.stopPropagation();
90
+ if (disabled || busy)
91
+ return;
92
+ setDragActive(true);
93
+ }, onDragLeave: (event) => {
94
+ event.preventDefault();
95
+ event.stopPropagation();
96
+ setDragActive(false);
97
+ }, onDrop: (event) => {
98
+ event.preventDefault();
99
+ event.stopPropagation();
100
+ setDragActive(false);
101
+ const dropped = event.dataTransfer.files?.[0];
102
+ if (!dropped)
103
+ return;
104
+ void uploadAndSolve(dropped);
105
+ }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), onClick: () => {
106
+ if (disabled || busy)
107
+ return;
108
+ const input = document.getElementById(inputId);
109
+ input?.click();
110
+ }, style: {
111
+ borderRadius: "18px",
112
+ border: "1px dashed rgba(49, 143, 78, 0.45)",
113
+ padding: "18px 18px",
114
+ minHeight: "92px",
115
+ background: dragActive ? "rgba(49, 143, 78, 0.08)" : "rgba(255,255,255,0.9)",
116
+ cursor: disabled || busy ? "not-allowed" : "pointer",
117
+ opacity: disabled || busy ? 0.6 : 1,
118
+ boxShadow: "0 10px 24px rgba(15, 23, 42, 0.06)",
119
+ }, children: [minimal ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: [_jsx("div", { style: {
120
+ fontSize: "11px",
121
+ fontWeight: 500,
122
+ letterSpacing: "0.04em",
123
+ color: hovered ? "rgb(49, 143, 78)" : "#7b8794",
124
+ opacity: hovered ? 0.95 : 0.65,
125
+ }, children: typesLabel }), showStatusLine ? (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "11px", opacity: 0.7 }, children: [_jsxs("span", { style: { display: "inline-flex", gap: "4px" }, children: [_jsx("span", { className: "upload-dot" }), _jsx("span", { className: "upload-dot" }), _jsx("span", { className: "upload-dot" })] }), _jsx("span", { style: { textTransform: "lowercase" }, children: error ? `error: ${error}` : statusText })] })) : null] })) : (_jsxs(_Fragment, { children: [_jsx("div", { style: { fontSize: "13px", fontWeight: 600, color: "#0f172a" }, children: lastFileName ? `Loaded ${lastFileName}` : "Drop a file to upload" }), _jsx("div", { style: { marginTop: "6px", fontSize: "12px", color: "#64748b" }, children: ".gh \u00B7 .ghx \u00B7 .3dm \u00B7 OBJ \u00B7 FBX \u00B7 GLTF \u00B7 STL \u00B7 PLY" }), _jsx("div", { style: { marginTop: "8px", fontSize: "11px", opacity: 0.7, textTransform: "lowercase" }, children: statusText })] })), error ? (_jsx("div", { style: { marginTop: "6px", fontSize: "11px", color: "#b91c1c" }, children: error })) : null] }), _jsx("style", { children: `
126
+ .upload-dot {
127
+ width: 5px;
128
+ height: 5px;
129
+ border-radius: 999px;
130
+ background: rgb(49, 143, 78);
131
+ opacity: 0.35;
132
+ animation: upload-dot-bounce 1s ease-in-out infinite;
133
+ }
134
+ .upload-dot:nth-child(2) {
135
+ animation-delay: 0.15s;
136
+ }
137
+ .upload-dot:nth-child(3) {
138
+ animation-delay: 0.3s;
139
+ }
140
+ @keyframes upload-dot-bounce {
141
+ 0%, 100% {
142
+ transform: translateY(0);
143
+ opacity: 0.35;
144
+ }
145
+ 50% {
146
+ transform: translateY(-3px);
147
+ opacity: 0.85;
148
+ }
149
+ }
150
+ ` }), _jsx("input", { id: inputId, type: "file", accept: accept, style: { display: "none" }, onChange: (event) => {
151
+ const file = event.target.files?.[0];
152
+ if (!file)
153
+ return;
154
+ void uploadAndSolve(file);
155
+ event.currentTarget.value = "";
156
+ } })] }));
157
+ }
@@ -0,0 +1,33 @@
1
+ export type LightingOption = {
2
+ id: string;
3
+ label: string;
4
+ summary?: string;
5
+ };
6
+ export type MaterialOption = {
7
+ id: string;
8
+ label: string;
9
+ summary?: string;
10
+ swatches?: string[];
11
+ };
12
+ export type SkyOption = {
13
+ id: string;
14
+ label: string;
15
+ description?: string;
16
+ };
17
+ export type ViewControlsProps = {
18
+ activeLightingPreset: string;
19
+ onLightingChange: (preset: string) => void;
20
+ materialOptions?: MaterialOption[];
21
+ activeMaterialPreset?: string;
22
+ onMaterialChange?: (preset: string) => void;
23
+ onCameraViewChange: (view: string) => void;
24
+ skyOptions: SkyOption[];
25
+ activeSkyId: string;
26
+ onSkyChange: (skyId: string) => void;
27
+ lightingOptions: LightingOption[];
28
+ veilsEnabled?: boolean;
29
+ onToggleVeils?: () => void;
30
+ onResetRender?: () => void;
31
+ };
32
+ export default function ViewControls({ activeLightingPreset, onLightingChange, materialOptions, activeMaterialPreset, onMaterialChange, onCameraViewChange, skyOptions, activeSkyId, onSkyChange, lightingOptions, veilsEnabled, onToggleVeils, onResetRender, }: ViewControlsProps): import("react/jsx-runtime").JSX.Element;
33
+ //# 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,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,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,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;IACnC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,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;IAC3B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC;AAyDF,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACnC,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,EACV,WAAW,EACX,WAAW,EACX,eAAe,EACf,YAAmB,EACnB,aAAa,EACb,aAAa,GACd,EAAE,iBAAiB,2CAwYnB"}
@@ -0,0 +1,250 @@
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
+ const swatchRowStyle = {
41
+ display: "flex",
42
+ gap: "6px",
43
+ marginTop: "6px",
44
+ };
45
+ const swatchStyle = (color) => ({
46
+ width: "12px",
47
+ height: "12px",
48
+ borderRadius: "999px",
49
+ background: color,
50
+ border: "1px solid rgba(15,23,42,0.1)",
51
+ });
52
+ export default function ViewControls({ activeLightingPreset, onLightingChange, materialOptions, activeMaterialPreset, onMaterialChange, onCameraViewChange, skyOptions, activeSkyId, onSkyChange, lightingOptions, veilsEnabled = true, onToggleVeils, onResetRender, }) {
53
+ const lightingMenuRef = useRef(null);
54
+ const materialMenuRef = useRef(null);
55
+ const skyMenuRef = useRef(null);
56
+ const [lightingExpanded, setLightingExpanded] = useState(false);
57
+ const [materialExpanded, setMaterialExpanded] = useState(false);
58
+ const [skyExpanded, setSkyExpanded] = useState(false);
59
+ const [lightingHover, setLightingHover] = useState(false);
60
+ const [materialHover, setMaterialHover] = useState(false);
61
+ const [skyHover, setSkyHover] = useState(false);
62
+ const [viewHover, setViewHover] = useState(false);
63
+ const activeLighting = useMemo(() => lightingOptions.find((option) => option.id === activeLightingPreset) ?? lightingOptions[0], [lightingOptions, activeLightingPreset]);
64
+ const activeSky = useMemo(() => skyOptions.find((option) => option.id === activeSkyId) ?? skyOptions[0], [skyOptions, activeSkyId]);
65
+ const activeMaterial = useMemo(() => materialOptions?.find((option) => option.id === activeMaterialPreset) ??
66
+ materialOptions?.[0], [materialOptions, activeMaterialPreset]);
67
+ useEffect(() => {
68
+ if (!lightingExpanded && !skyExpanded && !materialExpanded)
69
+ return;
70
+ const onMouseDown = (event) => {
71
+ const target = event.target;
72
+ if (!target)
73
+ return;
74
+ if (lightingMenuRef.current?.contains(target))
75
+ return;
76
+ if (materialMenuRef.current?.contains(target))
77
+ return;
78
+ if (skyMenuRef.current?.contains(target))
79
+ return;
80
+ setLightingExpanded(false);
81
+ setMaterialExpanded(false);
82
+ setSkyExpanded(false);
83
+ };
84
+ const onKeyDown = (event) => {
85
+ if (event.key !== "Escape")
86
+ return;
87
+ setLightingExpanded(false);
88
+ setMaterialExpanded(false);
89
+ setSkyExpanded(false);
90
+ };
91
+ window.addEventListener("mousedown", onMouseDown);
92
+ window.addEventListener("keydown", onKeyDown);
93
+ return () => {
94
+ window.removeEventListener("mousedown", onMouseDown);
95
+ window.removeEventListener("keydown", onKeyDown);
96
+ };
97
+ }, [lightingExpanded, materialExpanded, skyExpanded]);
98
+ 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: () => {
99
+ onLightingChange(option.id);
100
+ setLightingExpanded(false);
101
+ }, style: {
102
+ width: "100%",
103
+ border: "none",
104
+ borderRadius: "8px",
105
+ padding: "8px 10px",
106
+ textAlign: "left",
107
+ background: option.id === activeLightingPreset ? "rgba(49,67,144,0.12)" : "transparent",
108
+ color: option.id === activeLightingPreset ? "#314390" : "#475569",
109
+ cursor: "pointer",
110
+ display: "flex",
111
+ flexDirection: "column",
112
+ gap: "2px",
113
+ fontSize: "12px",
114
+ textTransform: "lowercase",
115
+ }, 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: () => {
116
+ setLightingExpanded((prev) => !prev);
117
+ setSkyExpanded(false);
118
+ }, style: {
119
+ display: "flex",
120
+ alignItems: "center",
121
+ justifyContent: "space-between",
122
+ gap: "10px",
123
+ padding: "8px 12px",
124
+ border: "none",
125
+ background: "transparent",
126
+ width: "100%",
127
+ cursor: "pointer",
128
+ }, children: [_jsx("span", { style: {
129
+ ...labelStyle,
130
+ opacity: !veilsEnabled || lightingExpanded || lightingHover ? 1 : 0,
131
+ transition: "opacity 0.2s ease",
132
+ }, children: activeLighting?.label ?? activeLightingPreset }), _jsx("span", { style: {
133
+ fontSize: "11px",
134
+ color: "#64748b",
135
+ opacity: !veilsEnabled || lightingExpanded || lightingHover ? 1 : 0,
136
+ transition: "opacity 0.2s ease",
137
+ }, children: lightingExpanded ? "▼" : "▲" })] }), veilsEnabled && !lightingExpanded && !lightingHover ? _jsx("div", { style: veilStyle, children: "LIGHTING" }) : null] }) }), materialOptions && materialOptions.length && onMaterialChange ? (_jsx("div", { ref: materialMenuRef, children: _jsxs("div", { style: cardStyle, onMouseEnter: () => setMaterialHover(true), onMouseLeave: () => setMaterialHover(false), children: [materialExpanded ? (_jsx("div", { style: dropdownStyle, children: materialOptions.map((option) => (_jsxs("button", { type: "button", onClick: () => {
138
+ onMaterialChange(option.id);
139
+ setMaterialExpanded(false);
140
+ }, style: {
141
+ width: "100%",
142
+ border: "none",
143
+ borderRadius: "8px",
144
+ padding: "8px 10px",
145
+ textAlign: "left",
146
+ background: option.id === activeMaterialPreset ? "rgba(49,67,144,0.12)" : "transparent",
147
+ color: option.id === activeMaterialPreset ? "#314390" : "#475569",
148
+ cursor: "pointer",
149
+ display: "flex",
150
+ flexDirection: "column",
151
+ gap: "2px",
152
+ fontSize: "12px",
153
+ textTransform: "lowercase",
154
+ }, children: [_jsx("span", { children: option.label }), option.summary ? (_jsx("span", { style: { fontSize: "10px", opacity: 0.7 }, children: option.summary })) : null, option.swatches && option.swatches.length ? (_jsx("div", { style: swatchRowStyle, children: option.swatches.map((color, index) => (_jsx("span", { style: swatchStyle(color) }, `${option.id}-${index}`))) })) : null] }, option.id))) })) : null, _jsxs("button", { type: "button", onClick: () => {
155
+ setMaterialExpanded((prev) => !prev);
156
+ setLightingExpanded(false);
157
+ setSkyExpanded(false);
158
+ }, style: {
159
+ display: "flex",
160
+ alignItems: "center",
161
+ justifyContent: "space-between",
162
+ gap: "10px",
163
+ padding: "8px 12px",
164
+ border: "none",
165
+ background: "transparent",
166
+ width: "100%",
167
+ cursor: "pointer",
168
+ }, children: [_jsx("span", { style: {
169
+ ...labelStyle,
170
+ opacity: !veilsEnabled || materialExpanded || materialHover ? 1 : 0,
171
+ transition: "opacity 0.2s ease",
172
+ }, children: activeMaterial?.label ?? activeMaterialPreset ?? "materials" }), _jsx("span", { style: {
173
+ fontSize: "11px",
174
+ color: "#64748b",
175
+ opacity: !veilsEnabled || materialExpanded || materialHover ? 1 : 0,
176
+ transition: "opacity 0.2s ease",
177
+ }, children: materialExpanded ? "▼" : "▲" })] }), veilsEnabled && !materialExpanded && !materialHover ? _jsx("div", { style: veilStyle, children: "MATERIALS" }) : null] }) })) : null, _jsxs("div", { style: { ...cardStyle, display: "flex" }, onMouseEnter: () => setViewHover(true), onMouseLeave: () => setViewHover(false), children: [_jsx("button", { type: "button", onClick: () => onCameraViewChange("interior"), style: {
178
+ padding: "8px 12px",
179
+ border: "none",
180
+ borderRight: "1px solid #e2e8f0",
181
+ background: "transparent",
182
+ cursor: "pointer",
183
+ minWidth: "84px",
184
+ ...labelStyle,
185
+ color: !veilsEnabled || viewHover ? "#475569" : "transparent",
186
+ }, children: "interior" }), _jsx("button", { type: "button", onClick: () => onCameraViewChange("iso"), style: {
187
+ padding: "8px 12px",
188
+ border: "none",
189
+ background: "transparent",
190
+ cursor: "pointer",
191
+ minWidth: "84px",
192
+ ...labelStyle,
193
+ color: !veilsEnabled || viewHover ? "#475569" : "transparent",
194
+ }, 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: () => {
195
+ onSkyChange(option.id);
196
+ setSkyExpanded(false);
197
+ }, style: {
198
+ width: "100%",
199
+ border: "none",
200
+ borderRadius: "8px",
201
+ padding: "8px 10px",
202
+ textAlign: "left",
203
+ background: option.id === activeSkyId ? "rgba(49,67,144,0.12)" : "transparent",
204
+ color: option.id === activeSkyId ? "#314390" : "#475569",
205
+ cursor: "pointer",
206
+ display: "flex",
207
+ flexDirection: "column",
208
+ gap: "2px",
209
+ fontSize: "12px",
210
+ textTransform: "lowercase",
211
+ }, 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: () => {
212
+ setSkyExpanded((prev) => !prev);
213
+ setLightingExpanded(false);
214
+ }, style: {
215
+ display: "flex",
216
+ alignItems: "center",
217
+ justifyContent: "space-between",
218
+ gap: "10px",
219
+ padding: "8px 12px",
220
+ border: "none",
221
+ background: "transparent",
222
+ width: "100%",
223
+ cursor: "pointer",
224
+ }, children: [_jsx("span", { style: {
225
+ ...labelStyle,
226
+ opacity: !veilsEnabled || skyExpanded || skyHover ? 1 : 0,
227
+ transition: "opacity 0.2s ease",
228
+ }, children: activeSky?.label ?? "preset dome" }), _jsx("span", { style: {
229
+ fontSize: "11px",
230
+ color: "#64748b",
231
+ opacity: !veilsEnabled || skyExpanded || skyHover ? 1 : 0,
232
+ transition: "opacity 0.2s ease",
233
+ }, children: skyExpanded ? "▼" : "▲" })] }), veilsEnabled && !skyExpanded && !skyHover ? _jsx("div", { style: veilStyle, children: "BACKGROUND" }) : null] }) })) : null, onToggleVeils ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px", alignSelf: "stretch" }, children: [onResetRender ? (_jsx("button", { type: "button", onClick: onResetRender, "aria-label": "Reset render settings", style: {
234
+ border: "none",
235
+ background: "transparent",
236
+ fontSize: "11px",
237
+ textTransform: "uppercase",
238
+ letterSpacing: "0.12em",
239
+ color: "#64748b",
240
+ cursor: "pointer",
241
+ }, children: "RESET" })) : null, _jsx("button", { type: "button", onClick: onToggleVeils, "aria-label": "Toggle veils", style: {
242
+ border: "none",
243
+ background: "transparent",
244
+ fontSize: "11px",
245
+ textTransform: "uppercase",
246
+ letterSpacing: "0.14em",
247
+ color: "#64748b",
248
+ cursor: "pointer",
249
+ }, children: "VEILS" })] })) : null] }));
250
+ }
@@ -0,0 +1,13 @@
1
+ import type { PresetCatalog, ViewerCreateOptions, ViewerHandle } from "@treasuryspatial/viewer-kit";
2
+ export type ViewerCanvasProps = {
3
+ presetId?: string;
4
+ presets?: PresetCatalog;
5
+ assetResolver?: ViewerCreateOptions["assetResolver"];
6
+ camera?: ViewerCreateOptions["camera"];
7
+ renderer?: ViewerCreateOptions["renderer"];
8
+ usePostprocessing?: ViewerCreateOptions["usePostprocessing"];
9
+ className?: string;
10
+ onReady?: (handle: ViewerHandle | null) => void;
11
+ };
12
+ export default function ViewerCanvas({ presetId, presets, assetResolver, camera, renderer, usePostprocessing, className, onReady, }: ViewerCanvasProps): import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=ViewerCanvas.d.ts.map
@@ -0,0 +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,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,2CAqInB"}
@@ -0,0 +1,135 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { createViewer } from "@treasuryspatial/viewer-kit";
4
+ export default function ViewerCanvas({ presetId, presets, assetResolver, camera, renderer, usePostprocessing, className, onReady, }) {
5
+ const [container, setContainer] = useState(null);
6
+ const handleRef = useRef(null);
7
+ const presetIdRef = useRef(presetId);
8
+ const initialPresetRef = useRef(presetId);
9
+ const instanceRef = useRef(0);
10
+ const createOptions = useMemo(() => {
11
+ return {
12
+ presetId: initialPresetRef.current,
13
+ presets,
14
+ assetResolver,
15
+ camera,
16
+ renderer,
17
+ usePostprocessing,
18
+ };
19
+ }, [assetResolver, camera, presets, renderer, usePostprocessing]);
20
+ const handleContainerRef = useCallback((node) => {
21
+ setContainer(node);
22
+ }, []);
23
+ useLayoutEffect(() => {
24
+ if (!container)
25
+ return;
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 handle = null;
34
+ const cleanup = () => {
35
+ disposed = true;
36
+ if (rafId !== null) {
37
+ cancelAnimationFrame(rafId);
38
+ rafId = null;
39
+ }
40
+ sizeObserver?.disconnect();
41
+ sizeObserver = null;
42
+ resizeObserver?.disconnect();
43
+ resizeObserver = null;
44
+ if (handle) {
45
+ handle.stop();
46
+ handle.dispose();
47
+ }
48
+ handleRef.current = null;
49
+ if (instanceRef.current === instanceId) {
50
+ onReady?.(null);
51
+ }
52
+ };
53
+ const initViewer = () => {
54
+ if (disposed || initialized)
55
+ return;
56
+ if (container.clientWidth === 0 || container.clientHeight === 0)
57
+ return;
58
+ initialized = true;
59
+ container.innerHTML = "";
60
+ handle = createViewer({
61
+ ...createOptions,
62
+ container,
63
+ hooks: {
64
+ onReady: (viewer) => {
65
+ if (disposed || instanceRef.current !== instanceId)
66
+ return;
67
+ viewer.__viewerId = instanceId;
68
+ handleRef.current = viewer;
69
+ console.log("[viewer-react] ready", { viewerId: instanceId });
70
+ onReady?.(viewer);
71
+ },
72
+ onError: (error) => {
73
+ if (disposed || instanceRef.current !== instanceId)
74
+ return;
75
+ console.error("[viewer-react] Failed to initialise viewer", error);
76
+ onReady?.(null);
77
+ },
78
+ },
79
+ });
80
+ handleRef.current = handle;
81
+ handle.__viewerId = instanceId;
82
+ console.log("[viewer-react] create", { viewerId: instanceId });
83
+ handle.start();
84
+ handle.resize();
85
+ if (typeof ResizeObserver !== "undefined") {
86
+ resizeObserver = new ResizeObserver(() => handle?.resize());
87
+ resizeObserver.observe(container);
88
+ }
89
+ else {
90
+ requestAnimationFrame(() => handle?.resize());
91
+ }
92
+ };
93
+ const waitForSize = () => {
94
+ if (disposed || initialized)
95
+ return;
96
+ initViewer();
97
+ if (initialized)
98
+ return;
99
+ if (typeof ResizeObserver !== "undefined") {
100
+ sizeObserver = new ResizeObserver(() => {
101
+ if (initialized || disposed)
102
+ return;
103
+ initViewer();
104
+ if (initialized) {
105
+ sizeObserver?.disconnect();
106
+ sizeObserver = null;
107
+ }
108
+ });
109
+ sizeObserver.observe(container);
110
+ }
111
+ else {
112
+ const tick = () => {
113
+ if (disposed || initialized)
114
+ return;
115
+ initViewer();
116
+ if (!initialized) {
117
+ rafId = requestAnimationFrame(tick);
118
+ }
119
+ };
120
+ rafId = requestAnimationFrame(tick);
121
+ }
122
+ };
123
+ waitForSize();
124
+ return cleanup;
125
+ }, [container, createOptions, onReady]);
126
+ useEffect(() => {
127
+ if (!handleRef.current)
128
+ return;
129
+ if (presetId && presetIdRef.current !== presetId) {
130
+ presetIdRef.current = presetId;
131
+ handleRef.current.setPreset(presetId);
132
+ }
133
+ }, [presetId]);
134
+ return _jsx("div", { ref: handleContainerRef, className: className });
135
+ }
@@ -0,0 +1,6 @@
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";
5
+ export type { ViewerCanvasProps } from "./ViewerCanvas";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,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 ADDED
@@ -0,0 +1,4 @@
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 ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@treasuryspatial/viewer-react",
3
+ "version": "0.1.11",
4
+ "type": "module",
5
+ "license": "UNLICENSED",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "dependencies": {
21
+ "@treasuryspatial/viewer-kit": "^0.2.31"
22
+ },
23
+ "peerDependencies": {
24
+ "react": ">=18"
25
+ },
26
+ "devDependencies": {
27
+ "react": "19.1.0",
28
+ "@types/react": "^19",
29
+ "@types/react-dom": "^19"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -b",
33
+ "typecheck": "tsc -b --pretty false --noEmit"
34
+ }
35
+ }