mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-26 02:59:37 +00:00
pagescene: drop view-only mutation gates + rename selector
Visitors can now fully interact with someone else's scene — move objects, add shapes, edit colors, etc. The previous strict view-only mode is gone. Save remains the only auth/ownership-gated action; its rewiring lands in a follow-up. Renames computeIsViewOnly to computeIsVisitingOthersScene to reflect the remaining role (it only drives the PreviewBox today; Save-copy label and Reset button consume it next). Drops getIsViewOnly()/useIsViewOnly() early-returns from engine actions, drag-drop, gizmo attach, keybinds, and the Outliner row controls. Strips !isViewOnly wrappers from chrome in Stage3DBody so the prompt box, controls, panel, and model selector show for everyone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,11 @@ export type {
|
||||
} from "./lib/adapter";
|
||||
|
||||
// Store
|
||||
export { usePageSceneStore, useIsViewOnly, getIsViewOnly } from "./lib/PageSceneStore";
|
||||
export {
|
||||
usePageSceneStore,
|
||||
useIsVisitingOthersScene,
|
||||
getIsVisitingOthersScene,
|
||||
} from "./lib/PageSceneStore";
|
||||
export type {
|
||||
Camera,
|
||||
FocalLengthDragging,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { MediaItem } from "../models";
|
||||
import { usePageSceneStore, getIsViewOnly } from "../PageSceneStore";
|
||||
import { usePageSceneStore } from "../PageSceneStore";
|
||||
import { AssetType } from "../enums";
|
||||
import type Editor from "../engine/editor";
|
||||
import { pickDropPosition } from "../engine/pickDropPosition";
|
||||
@@ -37,7 +37,6 @@ class DndAsset {
|
||||
item: MediaItem,
|
||||
editor: Editor | null,
|
||||
) {
|
||||
if (getIsViewOnly()) return;
|
||||
if (event.button === 0) {
|
||||
this.editor = editor;
|
||||
const store = usePageSceneStore.getState();
|
||||
|
||||
@@ -495,15 +495,21 @@ export const usePageSceneStore = create<PageSceneState>((set, get) => ({
|
||||
setCamViewCanvasEl: (el) => set({ camViewCanvasEl: el }),
|
||||
}));
|
||||
|
||||
// View-only mode: visitor is looking at someone else's scene (owner is
|
||||
// True when the visitor is looking at someone else's scene (owner is
|
||||
// known and isn't them). A blank scene has no owner so this returns
|
||||
// false; the scene owner viewing their own scene returns false too.
|
||||
const computeIsViewOnly = (s: PageSceneState): boolean =>
|
||||
//
|
||||
// This no longer gates mutations — visitors can fully interact with
|
||||
// the scene. It only drives ownership-aware UI: the primary Save
|
||||
// button switches to "Save copy" (forks the scene to the visitor's
|
||||
// account on confirm) and the PreviewBox shows the author's
|
||||
// generation result.
|
||||
const computeIsVisitingOthersScene = (s: PageSceneState): boolean =>
|
||||
s.sceneMeta.ownerToken !== undefined &&
|
||||
s.sceneMeta.ownerToken !== s.currentUserToken;
|
||||
|
||||
export const useIsViewOnly = (): boolean =>
|
||||
usePageSceneStore(computeIsViewOnly);
|
||||
export const useIsVisitingOthersScene = (): boolean =>
|
||||
usePageSceneStore(computeIsVisitingOthersScene);
|
||||
|
||||
export const getIsViewOnly = (): boolean =>
|
||||
computeIsViewOnly(usePageSceneStore.getState());
|
||||
export const getIsVisitingOthersScene = (): boolean =>
|
||||
computeIsVisitingOthersScene(usePageSceneStore.getState());
|
||||
|
||||
@@ -64,7 +64,7 @@ import { useViewportSize } from "./hooks/useViewportSize";
|
||||
import { GridVisibleChangedEvent } from "./engine/events/EngineEvent";
|
||||
import { pickDropPosition } from "./engine/pickDropPosition";
|
||||
import { AssetType, CameraAspectRatio } from "./enums";
|
||||
import { usePageSceneStore, useIsViewOnly } from "./PageSceneStore";
|
||||
import { usePageSceneStore, useIsVisitingOthersScene } from "./PageSceneStore";
|
||||
import type { MediaItem } from "./models/assets";
|
||||
|
||||
const PAGE_ID: ModelPage = ModelPage.Stage3D;
|
||||
@@ -107,7 +107,7 @@ export const Stage3DBody = ({
|
||||
);
|
||||
const gridVisible = usePageSceneStore((s) => s.gridVisible);
|
||||
const previewImageUrl = usePageSceneStore((s) => s.sceneMeta.previewImageUrl);
|
||||
const isViewOnly = useIsViewOnly();
|
||||
const isVisitingOthersScene = useIsVisitingOthersScene();
|
||||
const addCamera = usePageSceneStore((s) => s.addCamera);
|
||||
const updateCamera = usePageSceneStore((s) => s.updateCamera);
|
||||
const deleteCamera = usePageSceneStore((s) => s.deleteCamera);
|
||||
@@ -407,7 +407,7 @@ export const Stage3DBody = ({
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
{!isViewOnly && <OnboardingHelper />}
|
||||
<OnboardingHelper />
|
||||
|
||||
<div className="relative flex h-full w-full">
|
||||
<div id="engine-n-panels-wrapper" className="flex h-full w-full">
|
||||
@@ -417,22 +417,20 @@ export const Stage3DBody = ({
|
||||
</SceneContainer>
|
||||
|
||||
<FocalLengthDisplay />
|
||||
{!isViewOnly && <PoseModeSelector />}
|
||||
<PoseModeSelector />
|
||||
|
||||
{!isViewOnly && (
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ControlsTopButtons />
|
||||
<Controls3D showImageTo3DButton={showImageTo3DButton} />
|
||||
<div className="flex items-start justify-end gap-2 pr-2 pt-2">
|
||||
<AnonHintChip />
|
||||
</div>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ControlsTopButtons />
|
||||
<Controls3D showImageTo3DButton={showImageTo3DButton} />
|
||||
<div className="flex items-start justify-end gap-2 pr-2 pt-2">
|
||||
<AnonHintChip />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0"
|
||||
@@ -446,12 +444,12 @@ export const Stage3DBody = ({
|
||||
<PreviewEngineCamera />
|
||||
</div>
|
||||
|
||||
{!isViewOnly && <ControlPanelSceneObject />}
|
||||
<ControlPanelSceneObject />
|
||||
</div>
|
||||
|
||||
{isViewOnly && <PreviewBox imageUrl={previewImageUrl} />}
|
||||
{isVisitingOthersScene && <PreviewBox imageUrl={previewImageUrl} />}
|
||||
|
||||
{!isViewOnly && <PromptBox3D
|
||||
<PromptBox3D
|
||||
cameras={cameras}
|
||||
cameraAspectRatio={camAspect}
|
||||
disableHotkeyInput={disableHotkeyInput}
|
||||
@@ -495,7 +493,7 @@ export const Stage3DBody = ({
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>}
|
||||
/>
|
||||
|
||||
<LoadingDots
|
||||
className="absolute left-0 top-0 z-50"
|
||||
@@ -504,7 +502,7 @@ export const Stage3DBody = ({
|
||||
message={editorLoader.message}
|
||||
/>
|
||||
|
||||
{!isViewOnly && modelSelectorPlacement === "bottom-left" && (
|
||||
{modelSelectorPlacement === "bottom-left" && (
|
||||
<div className="absolute bottom-6 left-6 z-20 flex items-center gap-3">
|
||||
<ClassyModelSelector
|
||||
items={STAGE_3D_PAGE_MODEL_LIST}
|
||||
@@ -519,7 +517,7 @@ export const Stage3DBody = ({
|
||||
)}
|
||||
{(showCostCalculator || showHelpMenu) && (
|
||||
<div className="absolute bottom-6 right-6 z-20 flex items-center gap-2">
|
||||
{showCostCalculator && !isViewOnly && (
|
||||
{showCostCalculator && (
|
||||
<CostCalculatorButton modelPage={PAGE_ID} />
|
||||
)}
|
||||
{showHelpMenu && <HelpMenuButton />}
|
||||
|
||||
@@ -3,14 +3,12 @@ import type Editor from "../engine/editor";
|
||||
import { MediaItem } from "../models";
|
||||
import { CreateAction } from "../engine/editor/actions/CreateAction";
|
||||
import { ObjectAddedEvent } from "../engine/events/EngineEvent";
|
||||
import { getIsViewOnly } from "../PageSceneStore";
|
||||
|
||||
export async function addCharacter(
|
||||
editor: Editor,
|
||||
item: MediaItem,
|
||||
position?: THREE.Vector3,
|
||||
): Promise<string | undefined> {
|
||||
if (getIsViewOnly()) return undefined;
|
||||
const obj = await editor.sceneManager?.create(
|
||||
item.media_id,
|
||||
item.name ?? "character",
|
||||
|
||||
@@ -3,14 +3,12 @@ import type Editor from "../engine/editor";
|
||||
import { MediaItem } from "../models";
|
||||
import { CreateAction } from "../engine/editor/actions/CreateAction";
|
||||
import { ObjectAddedEvent } from "../engine/events/EngineEvent";
|
||||
import { getIsViewOnly } from "../PageSceneStore";
|
||||
|
||||
export async function addObject(
|
||||
editor: Editor,
|
||||
item: MediaItem,
|
||||
position?: THREE.Vector3,
|
||||
): Promise<string | undefined> {
|
||||
if (getIsViewOnly()) return undefined;
|
||||
const obj = await editor.sceneManager?.create(
|
||||
item.media_id,
|
||||
item.name ?? "object",
|
||||
|
||||
@@ -3,14 +3,12 @@ import type Editor from "../engine/editor";
|
||||
import { MediaItem } from "../models";
|
||||
import { CreateAction } from "../engine/editor/actions/CreateAction";
|
||||
import { ObjectAddedEvent } from "../engine/events/EngineEvent";
|
||||
import { getIsViewOnly } from "../PageSceneStore";
|
||||
|
||||
export async function addShape(
|
||||
editor: Editor,
|
||||
item: MediaItem,
|
||||
position?: THREE.Vector3,
|
||||
): Promise<string | undefined> {
|
||||
if (getIsViewOnly()) return undefined;
|
||||
const obj = await editor.sceneManager?.create(
|
||||
"Parim",
|
||||
item.media_id,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type Editor from "../engine/editor";
|
||||
import { ColorAction } from "../engine/editor/actions/ColorAction";
|
||||
import { getIsViewOnly } from "../PageSceneStore";
|
||||
|
||||
// One-shot apply: use when the change is atomic (e.g. a button click
|
||||
// or programmatic setter). Records a single ColorAction.
|
||||
@@ -15,7 +14,6 @@ export function setObjectColor(
|
||||
uuid: string,
|
||||
color: string,
|
||||
): void {
|
||||
if (getIsViewOnly()) return;
|
||||
const obj = editor.activeScene.scene.getObjectByProperty("uuid", uuid);
|
||||
if (!obj) return;
|
||||
const before = (obj.userData.color as string) ?? "#ffffff";
|
||||
@@ -38,9 +36,6 @@ export function beginColorSession(
|
||||
editor: Editor,
|
||||
uuid: string,
|
||||
): ColorSession {
|
||||
if (getIsViewOnly()) {
|
||||
return { apply() {}, commit() {} };
|
||||
}
|
||||
const startObj = editor.activeScene.scene.getObjectByProperty(
|
||||
"uuid",
|
||||
uuid,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type Editor from "../engine/editor";
|
||||
import { LockAction } from "../engine/editor/actions/LockAction";
|
||||
import { OutlinerItemLockToggledEvent } from "../engine/events/EngineEvent";
|
||||
import { getIsViewOnly } from "../PageSceneStore";
|
||||
|
||||
// Toggle the locked state of an object: flips userData.locked, runs
|
||||
// the gizmo attach/detach side effect, syncs the outliner row icon in
|
||||
@@ -10,7 +9,6 @@ import { getIsViewOnly } from "../PageSceneStore";
|
||||
// View callers (Outliner) only need to provide editor + uuid; the
|
||||
// action handles the engine + store + history orchestration.
|
||||
export function toggleObjectLock(editor: Editor, uuid: string): void {
|
||||
if (getIsViewOnly()) return;
|
||||
const before = editor.selection.isObjectLocked(uuid);
|
||||
editor.bus.emit(new OutlinerItemLockToggledEvent(uuid));
|
||||
editor.selection.lockUnlockObject(uuid);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type Editor from "../engine/editor";
|
||||
import { VisibilityAction } from "../engine/editor/actions/VisibilityAction";
|
||||
import { OutlinerItemVisibilityToggledEvent } from "../engine/events/EngineEvent";
|
||||
import { getIsViewOnly } from "../PageSceneStore";
|
||||
|
||||
// Toggle the visibility of an object: flips obj.visible +
|
||||
// userData.visible, syncs the outliner row icon in the Zustand store,
|
||||
@@ -11,7 +10,6 @@ import { getIsViewOnly } from "../PageSceneStore";
|
||||
// helper reads the before-state from the live engine object so the
|
||||
// view doesn't have to plumb it.
|
||||
export function toggleObjectVisibility(editor: Editor, uuid: string): void {
|
||||
if (getIsViewOnly()) return;
|
||||
const obj = editor.activeScene.scene.getObjectByProperty("uuid", uuid);
|
||||
if (!obj) return;
|
||||
const before = obj.visible;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type Editor from "../engine/editor";
|
||||
import { TransformAction } from "../engine/editor/actions/TransformAction";
|
||||
import { getIsViewOnly } from "../PageSceneStore";
|
||||
|
||||
// A transform "session" — opened on the first edit of a multi-input
|
||||
// editing flow (e.g. Object Panel position/rotation/scale text fields),
|
||||
@@ -25,9 +24,6 @@ export function beginTransformSession(
|
||||
editor: Editor,
|
||||
uuid: string,
|
||||
): TransformSession {
|
||||
if (getIsViewOnly()) {
|
||||
return { commit() {} };
|
||||
}
|
||||
const action = new TransformAction(editor, uuid);
|
||||
return {
|
||||
commit() {
|
||||
|
||||
@@ -2,19 +2,17 @@ import { useContext } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRightToBracket } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { EngineContext } from "../../contexts/EngineContext/EngineContext";
|
||||
import { usePageSceneStore, useIsViewOnly } from "../../PageSceneStore";
|
||||
import { usePageSceneStore } from "../../PageSceneStore";
|
||||
|
||||
// Quiet "you're signed out" affordance shown in the editor playground for
|
||||
// anonymous visitors. Hidden in view-only mode (the visitor is consuming
|
||||
// someone else's scene; no upsell pressure needed). The actual per-action
|
||||
// signup CTAs (Save, Generate, Upload) still fire when the user clicks
|
||||
// those buttons — this chip just sets expectations up front.
|
||||
// Quiet "you're signed out" affordance shown in the editor for anonymous
|
||||
// visitors — playground or someone else's scene alike. The actual
|
||||
// per-action signup CTAs (Save, Generate, Upload) still fire when the
|
||||
// user clicks those buttons; this chip just sets expectations up front.
|
||||
export const AnonHintChip = () => {
|
||||
const editor = useContext(EngineContext);
|
||||
const currentUserToken = usePageSceneStore((s) => s.currentUserToken);
|
||||
const isViewOnly = useIsViewOnly();
|
||||
|
||||
if (currentUserToken || isViewOnly) return null;
|
||||
if (currentUserToken) return null;
|
||||
|
||||
const handleClick = () => {
|
||||
editor?.adapter.promptSignup?.("hint");
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Transition } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { EngineContext } from "../../contexts/EngineContext";
|
||||
import { OutlinerItem, usePageSceneStore, useIsViewOnly } from "../../PageSceneStore";
|
||||
import { OutlinerItem, usePageSceneStore } from "../../PageSceneStore";
|
||||
import { useViewportSize } from "../../hooks/useViewportSize";
|
||||
import { CameraAspectRatio } from "../../enums";
|
||||
import { toggleObjectLock } from "../../actions/toggleObjectLock";
|
||||
@@ -26,7 +26,6 @@ const OutlinerRow = ({ item }: { item: OutlinerItem }) => {
|
||||
const isSelected = usePageSceneStore(
|
||||
(s) => s.outlinerSelectedItem?.id === item.id,
|
||||
);
|
||||
const isViewOnly = useIsViewOnly();
|
||||
|
||||
const editorEngine = useContext(EngineContext);
|
||||
|
||||
@@ -80,39 +79,37 @@ const OutlinerRow = ({ item }: { item: OutlinerItem }) => {
|
||||
</div>
|
||||
{item.name}
|
||||
</span>
|
||||
{!isViewOnly && (
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleToggleLock}
|
||||
style={{
|
||||
opacity: hovered || item.locked ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div className="w-3">
|
||||
<FontAwesomeIcon
|
||||
icon={item.locked ? faLock : faLockOpen}
|
||||
className="opacity-80 transition-opacity duration-100 hover:opacity-100"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleToggleVisibility}
|
||||
style={{
|
||||
opacity: hovered || !item.visible ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div className="w-4">
|
||||
<FontAwesomeIcon
|
||||
icon={item.visible ? faEye : faEyeSlash}
|
||||
className={twMerge(
|
||||
"opacity-80 transition-opacity duration-100 hover:opacity-100",
|
||||
item.locked && "text-white/90",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleToggleLock}
|
||||
style={{
|
||||
opacity: hovered || item.locked ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div className="w-3">
|
||||
<FontAwesomeIcon
|
||||
icon={item.locked ? faLock : faLockOpen}
|
||||
className="opacity-80 transition-opacity duration-100 hover:opacity-100"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleToggleVisibility}
|
||||
style={{
|
||||
opacity: hovered || !item.visible ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div className="w-4">
|
||||
<FontAwesomeIcon
|
||||
icon={item.visible ? faEye : faEyeSlash}
|
||||
className={twMerge(
|
||||
"opacity-80 transition-opacity duration-100 hover:opacity-100",
|
||||
item.locked && "text-white/90",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,13 +2,14 @@ import { useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
// Small fixed-position card shown next to the camera in view-only mode.
|
||||
// Renders the author's generation result so a visitor can see what the
|
||||
// scene was made to produce. Today this is scaffolding only — the URL
|
||||
// data path (likely a new field returned by adapter.loadScene's response
|
||||
// payload, propagated into sceneMeta.previewImageUrl) is wired up in a
|
||||
// follow-up PR. Renders null when no URL is supplied, so it's safe to
|
||||
// mount unconditionally inside the isViewOnly branch.
|
||||
// Small fixed-position card shown next to the camera when a visitor is
|
||||
// looking at someone else's scene. Renders the author's generation
|
||||
// result so a visitor can see what the scene was made to produce. Today
|
||||
// this is scaffolding only — the URL data path (likely a new field
|
||||
// returned by adapter.loadScene's response payload, propagated into
|
||||
// sceneMeta.previewImageUrl) is wired up in a follow-up PR. Renders
|
||||
// null when no URL is supplied, so it's safe to mount unconditionally
|
||||
// inside the isVisitingOthersScene branch.
|
||||
export interface PreviewBoxProps {
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { TransformAction } from "./editor/actions/TransformAction";
|
||||
|
||||
import Stats from "three/examples/jsm/libs/stats.module.js";
|
||||
import { SparkRenderer } from "@sparkjsdev/spark";
|
||||
import { usePageSceneStore, getIsViewOnly } from "../PageSceneStore";
|
||||
import { usePageSceneStore } from "../PageSceneStore";
|
||||
import { ensureAmmoLoaded } from "./ammoLoader";
|
||||
import { EngineEventBus } from "./events/EngineEventBus";
|
||||
import { EngineStoreBridge } from "./EngineStoreBridge";
|
||||
@@ -787,7 +787,6 @@ class Editor {
|
||||
}
|
||||
|
||||
deleteObject(uuid: string) {
|
||||
if (getIsViewOnly()) return;
|
||||
const obj = this.activeScene.scene.getObjectByProperty("uuid", uuid);
|
||||
if (obj) this.history.record(new DeleteAction(this, obj));
|
||||
this.mouse_controls?.clearFKVisuals();
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
import type * as THREE from "three";
|
||||
import { TransformControls } from "../TransformControls.js";
|
||||
import { getIsViewOnly } from "../../PageSceneStore";
|
||||
|
||||
export type TransformMode = "translate" | "rotate" | "scale";
|
||||
export type TransformSpace = "world" | "local";
|
||||
@@ -91,7 +90,6 @@ export class GizmoController {
|
||||
}
|
||||
|
||||
attach(object: THREE.Object3D) {
|
||||
if (getIsViewOnly()) return;
|
||||
this.control?.attach(object);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
TransformSpaceChangedEvent,
|
||||
} from "./events/EngineEvent";
|
||||
import type { PoseMode, TransformSpace } from "../PageSceneStore";
|
||||
import { getIsViewOnly } from "../PageSceneStore";
|
||||
|
||||
const EDITABLE_INPUT_TYPES = new Set([
|
||||
"text",
|
||||
@@ -345,7 +344,6 @@ export class MouseControls {
|
||||
this.focus();
|
||||
return;
|
||||
} else if (event.key === "Backspace" || event.key === "Delete") {
|
||||
if (getIsViewOnly()) return;
|
||||
if (this.selected) {
|
||||
this.selected.forEach((selected) => {
|
||||
this.deleteObject(selected.uuid);
|
||||
@@ -386,18 +384,15 @@ export class MouseControls {
|
||||
this.deps.bus.emit(new SelectedModeChangedEvent("scale"));
|
||||
return;
|
||||
} else if (event.key === "k") {
|
||||
if (getIsViewOnly()) return;
|
||||
this.toggleFKMode();
|
||||
return;
|
||||
} else if (event.key === "b") {
|
||||
if (getIsViewOnly()) return;
|
||||
// Open asset modal
|
||||
this.deps.bus.emit(new AssetModalVisibilityChangedEvent(true, true));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !this.isProcessing) {
|
||||
if (getIsViewOnly()) return;
|
||||
const keyLower = event.key.toLowerCase();
|
||||
if (keyLower === "z" && !event.shiftKey) {
|
||||
// undo
|
||||
|
||||
Reference in New Issue
Block a user