5180 fichiers 38.5 Mo 0 téléchargements 603 vues mis à jour il y a 12 heures ui
nitro-v36-with-renderer-v36 / src / components / room / widgets / avatar-info / infostand / ImagePositionEditorView.tsx
ImagePositionEditorView.tsx 8.6 Ko Télécharger le fichier
import { GetRoomEngine, RoomObjectCategory, RoomObjectVariable } from '@octane/renderer';
import { FC, PointerEvent as ReactPointerEvent, useCallback, useEffect, useRef, useState } from 'react';
import { LocalizeText } from '../../../../../api';
import { Button, OctaneCardContentView, OctaneCardHeaderView, OctaneCardView } from '../../../../../common';
import { Slider } from '../../../../../common/Slider';
import { NumberInput } from '../../../../../common/NumberInput';
import '../../../../../css/room/RoomWidgetsSweepAir.css';
import { NativeTextTree } from '../../../../../common/native-text/NativeTextTree';

const PAD_W = 230;
const PAD_H = 150;
// How many offset units one pixel of drag represents.
const UNITS_PER_PX = 1.5;

interface Props {
    roomId: number;
    objectId: number;
    isWallItem: boolean;
    initialX: number;
    initialY: number;
    initialZ: number;
    initialScale: number;
    initialAlpha: number;
    onClose: () => void;
    onSave: (x: number, y: number, z: number, scale: number, alpha: number) => void;
}

export const ImagePositionEditorView: FC<Props> = (props) => {
    const { roomId, objectId, isWallItem, initialX, initialY, initialZ, initialScale, initialAlpha, onClose, onSave } = props;
    const [x, setX] = useState(initialX);
    const [y, setY] = useState(initialY);
    const [z, setZ] = useState(initialZ);
    const [scale, setScale] = useState(initialScale || 100);
    const [alpha, setAlpha] = useState(initialAlpha ?? 100);
    const padRef = useRef<HTMLDivElement>(null);
    const draggingRef = useRef(false);

    const category = isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR;

    // Local-only live preview: set the branding model values directly. The model
    // bumps its update counter so the visualization re-renders next frame.
    // Nothing is sent to the server until Save.
    const applyLive = useCallback(
        (nx: number, ny: number, nz: number, nScale: number, nAlpha: number) => {
            const roomObject = GetRoomEngine().getRoomObject(roomId, objectId, category);
            if (!roomObject?.model) return;

            roomObject.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_X, nx);
            roomObject.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_Y, ny);
            roomObject.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_Z, nz);
            roomObject.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_SCALE, nScale);
            roomObject.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_ALPHA, nAlpha);
        },
        [roomId, objectId, category]
    );

    useEffect(() => {
        applyLive(x, y, z, scale, alpha);
    }, [x, y, z, scale, alpha, applyLive]);

    const setFromPointer = useCallback((clientX: number, clientY: number) => {
        const rect = padRef.current?.getBoundingClientRect();
        if (!rect) return;

        const cx = rect.left + rect.width / 2;
        const cy = rect.top + rect.height / 2;

        setX(Math.round((clientX - cx) * UNITS_PER_PX));
        setY(Math.round((clientY - cy) * UNITS_PER_PX));
    }, []);

    const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
        draggingRef.current = true;
        padRef.current?.setPointerCapture(event.pointerId);
        setFromPointer(event.clientX, event.clientY);
    };

    const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
        if (draggingRef.current) setFromPointer(event.clientX, event.clientY);
    };

    const onPointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
        draggingRef.current = false;
        padRef.current?.releasePointerCapture?.(event.pointerId);
    };

    const cancel = () => {
        applyLive(initialX, initialY, initialZ, initialScale || 100, initialAlpha ?? 100);
        onClose();
    };

    const save = () => {
        onSave(x, y, z, scale, alpha);
        onClose();
    };

    const dotLeft = PAD_W / 2 + x / UNITS_PER_PX;
    const dotTop = PAD_H / 2 + y / UNITS_PER_PX;
    const clampedLeft = Math.max(0, Math.min(PAD_W, dotLeft));
    const clampedTop = Math.max(0, Math.min(PAD_H, dotTop));

    return (
        <NativeTextTree>
            <OctaneCardView className="no-resize octane-image-position-editor" uniqueKey="image-position-editor" theme="primary-slim">
                <OctaneCardHeaderView headerText={LocalizeText('image.position.editor.title')} onCloseClick={cancel} />
                <OctaneCardContentView>
                    <div className="flex flex-col gap-2">
                        <span className="text-[11px] text-black/60">{LocalizeText('image.position.editor.hint')}</span>
                        <div
                            ref={padRef}
                            onPointerDown={onPointerDown}
                            onPointerMove={onPointerMove}
                            onPointerUp={onPointerUp}
                            className="relative cursor-crosshair self-center rounded border border-black/30 bg-[#1b2733]"
                            style={{ width: PAD_W, height: PAD_H }}
                        >
                            {/* center crosshair */}
                            <div className="pointer-events-none absolute left-1/2 top-0 h-full w-px -translate-x-1/2 bg-white/10" />
                            <div className="pointer-events-none absolute left-0 top-1/2 h-px w-full -translate-y-1/2 bg-white/10" />
                            {/* draggable dot */}
                            <div
                                className="pointer-events-none absolute h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-sky-400 shadow-[0_0_6px_rgba(56,189,248,0.8)] ring-2 ring-white/70"
                                style={{ left: clampedLeft, top: clampedTop }}
                            />
                        </div>

                        <div className="flex items-center gap-2">
                            <span className="w-12 shrink-0 text-[11px] text-black/70">{LocalizeText('image.position.editor.scale')}</span>
                            <Slider disabledButton className="octane-slider--compact grow" min={10} max={500} step={1} value={scale} onChange={(value) => setScale(value || 100)} />
                            <span className="w-12 text-right text-[11px] tabular-nums text-black/70">{(scale / 100).toFixed(2)}x</span>
                        </div>

                        <div className="flex items-center gap-2">
                            <span className="w-12 shrink-0 text-[11px] text-black/70">{LocalizeText('image.position.editor.opacity')}</span>
                            <Slider disabledButton className="octane-slider--compact grow" min={0} max={100} step={1} value={alpha} onChange={(value) => setAlpha(Number.isFinite(value) ? value : 100)} />
                            <span className="w-12 text-right text-[11px] tabular-nums text-black/70">{alpha}%</span>
                        </div>

                        <div className="grid grid-cols-3 gap-2">
                            <label className="flex flex-col gap-0.5 text-[11px] text-black/70">
                                {LocalizeText('image.position.editor.offsetx')}
                                <NumberInput value={x} onChange={(e) => setX(e.target.valueAsNumber || 0)} className="form-control form-control-sm" />
                            </label>
                            <label className="flex flex-col gap-0.5 text-[11px] text-black/70">
                                {LocalizeText('image.position.editor.offsety')}
                                <NumberInput value={y} onChange={(e) => setY(e.target.valueAsNumber || 0)} className="form-control form-control-sm" />
                            </label>
                            <label className="flex flex-col gap-0.5 text-[11px] text-black/70">
                                {LocalizeText('image.position.editor.offsetz')}
                                <NumberInput value={z} onChange={(e) => setZ(e.target.valueAsNumber || 0)} className="form-control form-control-sm" />
                            </label>
                        </div>

                        <div className="flex justify-end gap-2">
                            <Button variant="secondary" onClick={cancel}>
                                {LocalizeText('image.position.editor.cancel')}
                            </Button>
                            <Button variant="success" onClick={save}>
                                {LocalizeText('save')}
                            </Button>
                        </div>
                    </div>
                </OctaneCardContentView>
            </OctaneCardView>
        </NativeTextTree>
    );
};
15Sujets
1Dépôts
20Membres
4En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser