summaryrefslogtreecommitdiff
path: root/apps/web/src/preview/components/overlay-layer.tsx
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/preview/components/overlay-layer.tsx
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/preview/components/overlay-layer.tsx')
-rw-r--r--apps/web/src/preview/components/overlay-layer.tsx197
1 files changed, 197 insertions, 0 deletions
diff --git a/apps/web/src/preview/components/overlay-layer.tsx b/apps/web/src/preview/components/overlay-layer.tsx
new file mode 100644
index 00000000..eb730f6e
--- /dev/null
+++ b/apps/web/src/preview/components/overlay-layer.tsx
@@ -0,0 +1,197 @@
+"use client";
+
+import type {
+ PreviewOverlayHudAnchor,
+ PreviewOverlayInstance,
+ PreviewOverlayPlane,
+} from "@/preview/overlays";
+import { usePreviewViewport } from "./preview-viewport";
+
+const HUD_ANCHOR_CLASS_NAMES: Record<PreviewOverlayHudAnchor, string> = {
+ "top-left": "absolute top-2 left-2 flex flex-col items-start gap-1.5",
+ "top-right": "absolute top-2 right-2 flex flex-col items-end gap-1.5",
+ "bottom-left":
+ "absolute bottom-2 left-2 flex flex-col-reverse items-start gap-1.5",
+ "bottom-right":
+ "absolute right-2 bottom-2 flex flex-col-reverse items-end gap-1.5",
+};
+
+function getPositionedMountStyle({
+ instance,
+ baseStyle,
+ sceneLeft,
+ sceneTop,
+ sceneWidth,
+ sceneHeight,
+}: {
+ instance: PreviewOverlayInstance;
+ baseStyle: {
+ pointerEvents: "none" | "auto";
+ zIndex: number | undefined;
+ };
+ sceneLeft: number;
+ sceneTop: number;
+ sceneWidth: number;
+ sceneHeight: number;
+}) {
+ if (instance.mount.kind === "hud") {
+ return baseStyle;
+ }
+
+ const isSceneMount = instance.mount.kind === "scene";
+ const mount = instance.mount;
+ const hasExplicitBounds =
+ mount.x != null ||
+ mount.y != null ||
+ mount.width != null ||
+ mount.height != null;
+
+ return {
+ ...baseStyle,
+ left: (isSceneMount ? sceneLeft : 0) + (mount.x ?? 0),
+ top: (isSceneMount ? sceneTop : 0) + (mount.y ?? 0),
+ ...(hasExplicitBounds
+ ? {
+ ...(mount.width != null ? { width: mount.width } : {}),
+ ...(mount.height != null ? { height: mount.height } : {}),
+ }
+ : {
+ width: isSceneMount ? sceneWidth : "100%",
+ height: isSceneMount ? sceneHeight : "100%",
+ }),
+ } as const;
+}
+
+export function PreviewOverlayLayer({
+ instances,
+ plane,
+}: {
+ instances: PreviewOverlayInstance[];
+ plane: PreviewOverlayPlane;
+}) {
+ const viewport = usePreviewViewport();
+ const visibleInstances = instances
+ .filter((instance) => (instance.plane ?? "over-interaction") === plane)
+ .sort((left, right) => (left.zIndex ?? 0) - (right.zIndex ?? 0));
+
+ if (visibleInstances.length === 0) {
+ return null;
+ }
+
+ const hudInstancesByAnchor = visibleInstances.reduce<
+ Record<PreviewOverlayHudAnchor, PreviewOverlayInstance[]>
+ >(
+ (groups, instance) => {
+ if (instance.mount.kind === "hud") {
+ groups[instance.mount.anchor].push(instance);
+ }
+ return groups;
+ },
+ {
+ "top-left": [],
+ "top-right": [],
+ "bottom-left": [],
+ "bottom-right": [],
+ },
+ );
+
+ return (
+ <>
+ {visibleInstances.map((instance) => {
+ if (instance.mount.kind === "hud") {
+ return null;
+ }
+
+ const pointerEvents = instance.pointerEvents ?? "none";
+ const baseStyle = {
+ pointerEvents,
+ zIndex: instance.zIndex,
+ } as const;
+ const content = instance.render({
+ sceneHeight: viewport.sceneHeight,
+ sceneWidth: viewport.sceneWidth,
+ });
+
+ switch (instance.mount.kind) {
+ case "scene":
+ return (
+ <div
+ key={instance.id}
+ className="absolute"
+ style={getPositionedMountStyle({
+ instance,
+ baseStyle,
+ sceneLeft: viewport.sceneLeft,
+ sceneTop: viewport.sceneTop,
+ sceneWidth: viewport.sceneWidth,
+ sceneHeight: viewport.sceneHeight,
+ })}
+ >
+ {content}
+ </div>
+ );
+
+ case "viewport":
+ return (
+ <div
+ key={instance.id}
+ className="absolute"
+ style={getPositionedMountStyle({
+ instance,
+ baseStyle,
+ sceneLeft: viewport.sceneLeft,
+ sceneTop: viewport.sceneTop,
+ sceneWidth: viewport.sceneWidth,
+ sceneHeight: viewport.sceneHeight,
+ })}
+ >
+ {content}
+ </div>
+ );
+
+ default:
+ return null;
+ }
+ })}
+ {(
+ Object.entries(hudInstancesByAnchor) as Array<
+ [PreviewOverlayHudAnchor, PreviewOverlayInstance[]]
+ >
+ ).map(([anchor, anchorInstances]) => {
+ if (anchorInstances.length === 0) {
+ return null;
+ }
+
+ const sortedAnchorInstances = [...anchorInstances].sort(
+ (left, right) =>
+ (left.mount.kind === "hud" ? (left.mount.order ?? 0) : 0) -
+ (right.mount.kind === "hud" ? (right.mount.order ?? 0) : 0),
+ );
+
+ return (
+ <div key={anchor} className={HUD_ANCHOR_CLASS_NAMES[anchor]}>
+ {sortedAnchorInstances.map((instance) => {
+ const pointerEvents = instance.pointerEvents ?? "none";
+ const content = instance.render({
+ sceneHeight: viewport.sceneHeight,
+ sceneWidth: viewport.sceneWidth,
+ });
+
+ return (
+ <div
+ key={instance.id}
+ style={{
+ pointerEvents,
+ zIndex: instance.zIndex,
+ }}
+ >
+ {content}
+ </div>
+ );
+ })}
+ </div>
+ );
+ })}
+ </>
+ );
+}